From 226b50642af0895948fbaa623a9b7180399a63b6 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Wed, 13 May 2026 19:15:48 +0000
Subject: [PATCH] =Queue fixes

---
 inc/rest/routes/FormRoutes.php |  295 ++++++++++++++++++++++++++++++++++++++--------------------
 1 files changed, 193 insertions(+), 102 deletions(-)

diff --git a/inc/rest/routes/FormRoutes.php b/inc/rest/routes/FormRoutes.php
index f1efa39..1cf8b23 100644
--- a/inc/rest/routes/FormRoutes.php
+++ b/inc/rest/routes/FormRoutes.php
@@ -1,12 +1,14 @@
 <?php
 namespace JVBase\rest\routes;
 
-use JVBase\rest\RestRouteManager;
-use JVBase\managers\CacheManager;
-use JVBase\meta\MetaManager;
-use JVBase\managers\CloudflareTurnstile;
+use JVBase\meta\Sanitizer;
+use JVBase\meta\Validator;
+use JVBase\rest\PermissionHandler;
+use JVBase\rest\Rest;
+use JVBase\managers\Cache;
 use JVBase\blocks\FormBlock;
-use JVBase\utility\Features;
+use JVBase\rest\Route;
+use JVBase\base\Site;
 use WP_REST_Request;
 use WP_REST_Response;
 use WP_Error;
@@ -21,18 +23,15 @@
  *
  * Handles REST API endpoints for form submissions
  */
-class FormRoutes extends RestRouteManager
+class FormRoutes extends Rest
 {
-	protected CacheManager $cache;
 	protected FormBlock $form_block;
-	protected CloudflareTurnstile|null $turnstile;
 
 	public function __construct()
 	{
+		$this->cacheName = 'forms';
+		$this->cacheTtl = HOUR_IN_SECONDS;
 		parent::__construct();
-		$this->action = 'form-';
-		$this->cache = CacheManager::for('forms', HOUR_IN_SECONDS);
-
 
 		// Add query vars
 		add_filter('query_vars', [$this, 'addQueryVars']);
@@ -45,35 +44,29 @@
 	 */
 	public function registerRoutes(): void
 	{
-		// Form submission endpoint
-		register_rest_route($this->namespace, '/forms', [
-			[
-				'methods' => 'POST',
-				'callback' => [$this, 'submitForm'],
-				'permission_callback' => [$this, 'checkRateLimit'], // Public endpoint, rate limited
-			],
-			[
-				'methods' => 'GET',
-				'callback' => [$this, 'getForms'],
-				'permission_callback' => [$this, 'checkPermission']
-			]
-		]);
+		// ['actionNonce'=>'dash-']
+		Route::for('forms')
+			->post([$this, 'submitForm'])
+			->args([
+				'form_type' => 'string|required',
+				'form_id' => 'string|required',
+				'timestamp' => 'string',
+				'cf-turnstile-response' => 'string',
+			])
+			->auth('public')
+			->rateLimit(5) // 5 submissions per minute
+			->get([$this, 'getForms'])
+			->auth(PermissionHandler::combine(['logged_in', ['actionNonce'=>'dash-']]))
+			->rateLimit(30)
+			->register();
 
 		// Get specific form configuration
-		register_rest_route($this->namespace, '/forms/(?P<form_type>[a-zA-Z0-9_-]+)', [
-			[
-				'methods' => 'GET',
-				'callback' => [$this, 'getForm'],
-				'permission_callback' => [$this, 'checkPermission'],
-				'args' => [
-					'form_type' => [
-						'required' => true,
-						'type' => 'string',
-						'sanitize_callback' => 'sanitize_text_field'
-					]
-				]
-			]
-		]);
+		Route::for(Route::pattern('forms/{form_type}'))
+			->get([$this, 'getForm'])
+			->arg('form_type', 'string|required')
+			->auth('logged_in')
+			->rateLimit(30)
+			->register();
 	}
 
 	/**
@@ -100,31 +93,35 @@
 			$result = $this->handleFormSubmission($form_type, $form_id, $form_data, $files);
 
 			if (is_wp_error($result)) {
-				return new WP_REST_Response([
-					'success'	=> false,
-					'message'	=> $result->get_error_message()
-				]);
+				return $this->error(
+					$result->get_error_message(),
+					$result->get_error_code(),
+					400
+				);
 			}
 			if (array_key_exists('success', $result)){
-				return new WP_REST_Response($result);
+				return $this->validationError($result);
 			}
 
-			return new WP_REST_Response([
-				'success' => true,
-				'data'	=> $result
-			], 200);
+			return $this->success($result);
 
 		} catch (Exception $e) {
-			return new WP_REST_Response([
-				'success' => false,
-				'message' => 'An error occurred while processing your submission.'
-			], 500);
+			$this->logError('Form submission error', [
+				'message' => $e->getMessage(),
+				'trace' => $e->getTraceAsString()
+			]);
+
+			return $this->error(
+				'An error occurred while processing your submission.',
+				'submission_error',
+				500
+			);
 		}
 	}
 
 	protected function verifyTurnstile(string $token): bool
 	{
-		if (!Features::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) {
+		if (!Site::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) {
 			return true;
 		}
 
@@ -142,7 +139,6 @@
 	{
 		// Get form configuration
 		$form_config = FormBlock::getForm($form_type);
-
 		if (!$form_config) {
 			return new WP_Error('invalid_form', 'Form configuration not found.');
 		}
@@ -169,7 +165,6 @@
 		} catch (\Exception $e) {
 			return new WP_Error('validation_failed', 'Data validation error: ' . $e->getMessage());
 		}
-
 		if (array_key_exists('success', $processed_data) && $processed_data['success'] === false) {
 			return $processed_data;
 		}
@@ -178,7 +173,6 @@
 		try {
 			$email_sent = $this->sendEmailNotification($form_type, $form_config, $processed_data, $files);
 		} catch (\Exception $e) {
-			;
 			return new WP_Error('email_failed', 'Email error: ' . $e->getMessage());
 		}
 
@@ -203,7 +197,9 @@
 	 */
 	protected function validateAndSanitizeData(array $form_config, array $form_data): array|WP_REST_Response
 	{
-		$meta = new MetaManager(null, 'form');
+		$validator = new Validator();
+		$sanitizer = new Sanitizer();
+
 		$processed_data = [];
 		$errors = [];
 
@@ -226,7 +222,7 @@
 			$value = $normalized_form_data[$field_name] ?? '';
 
 			if (in_array($field_config['type'], ['checkbox', 'set'])) {
-				$value = jvbCommaList((is_array($value)) ? $value : explode(',',$value));
+				$value = is_array($value) ? implode(',', $value) : $value;
 			}
 
 			// Check required fields
@@ -244,7 +240,7 @@
 			$field_config['name'] = $field_name;
 
 			// Validate field
-			if (!$meta->validator->validate($value, $field_config)) {
+			if (!$validator->validate($value, $field_config)) {
 				$label = $field_config['label'] ?? ucfirst(str_replace('_', ' ', $field_name));
 				$errors['errors'][$field_name] = [
 					'message' => sprintf('Field "%s" contains invalid data.', $label)
@@ -253,7 +249,7 @@
 			}
 
 			// Sanitize field
-			$processed_data[$field_name] = $meta->sanitizer->sanitize($value, $field_config);
+			$processed_data[$field_name] = $sanitizer->sanitize($value, $field_config);
 		}
 
 		if (!empty($errors)) {
@@ -267,9 +263,6 @@
 	/**
 	 * Send email notification
 	 */
-	/**
-	 * Send email notification
-	 */
 	protected function sendEmailNotification(string $form_type, array $form_config, array $form_data, array $files = []): bool
 	{
 		$admin_email = apply_filters('jvb_form_email_to', $form_config['email_to'], $form_type, $form_data);
@@ -286,6 +279,20 @@
 			$submitter_name = $form_data['name'];
 		}
 
+		if (array_key_exists('preheader', $form_config)) {
+			$preheader = $form_config['preheader'];
+		} else {
+			$submitter_name = $submitter_name?:'website visitor';
+			$preheader = sprintf(
+				'New %s submission from %s',
+				$form_config['title'],
+				$submitter_name
+			);
+		}
+
+		$subject .= ' ' . $submitter_name;
+
+
 		// Email headers
 		$headers = [];
 
@@ -319,23 +326,36 @@
 			);
 		}
 
-		// Build form data array for table
+		// Build form data array for table - using similar logic to JS
+		$skip_fields = array_merge(
+			['sendAll', 'action', 'form_id', 'form_type', 'timestamp', '_wpnonce', '_wp_http_referer', 'cf-turnstile-response'],
+			$form_config['ignore'] ?? []
+		);
+
 		$form = [];
 		foreach ($form_config['fields'] as $field_name => $config) {
-			// Skip file upload fields
-			if (in_array($config['type'], ['upload'])) {
+			// Skip ignored fields
+			if (in_array($field_name, $skip_fields)) {
+				continue;
+			}
+
+			// Skip upload fields (handled separately)
+			if ($config['type'] === 'upload') {
 				continue;
 			}
 
 			$value = $form_data[$field_name] ?? '';
 
 			// Skip empty fields
-			if (empty($value)) {
+			if ($this->isEmptyValue($value)) {
 				continue;
 			}
 
+			// Get label - check for summaryTitle first, then label (similar to JS checking legend then label)
+			$label = $config['summaryTitle'] ?? $config['label'] ?? ucfirst(str_replace('_', ' ', $field_name));
+
 			$form[] = [
-				'label' => $config['summaryTitle'] ?? $config['label'],
+				'label' => $label,
 				'value' => $this->formatFieldValueForEmail($field_name, $value, $config)
 			];
 		}
@@ -354,16 +374,31 @@
 		}
 
 		// Send the email
-		$email_sent = JVB()->email()->sendEmail(
+		return JVB()->email()->sendEmail(
 			$to,
 			$subject,
 			$body,
 			'',
+			$preheader,
 			$headers,
 			$attachments
 		);
+	}
 
-		return $email_sent;
+	/**
+	 * Check if value is empty (similar to JS isEmptyValue)
+	 */
+	protected function isEmptyValue($value): bool
+	{
+		if ($value === null || $value === '' || $value === false) {
+			return true;
+		}
+
+		if (is_array($value) && empty($value)) {
+			return true;
+		}
+
+		return false;
 	}
 
 	/**
@@ -371,7 +406,7 @@
 	 */
 	protected function formatFieldValueForEmail(string $field_name, mixed $value, array $field_config): string
 	{
-		if (empty($value)) {
+		if ($this->isEmptyValue($value)) {
 			return '';
 		}
 
@@ -379,9 +414,17 @@
 		$type = $field_config['subType'] ?? $type;
 
 		switch ($type) {
+			case 'repeater':
+				return $this->formatRepeaterForEmail($value, $field_config);
+
+			case 'tag-list':
+				return $this->formatTagListForEmail($value);
+
+			case 'location':
+				return $this->formatLocationForEmail($value);
+
 			case 'phone':
 			case 'tel':
-				// Format phone number as xxx-xxx-xxxx
 				$cleaned = preg_replace('/\D/', '', $value);
 				if (strlen($cleaned) === 10) {
 					return substr($cleaned, 0, 3) . '-' . substr($cleaned, 3, 3) . '-' . substr($cleaned, 6);
@@ -390,51 +433,101 @@
 
 			case 'checkbox':
 			case 'set':
-				// Convert array of values to comma-separated labels
-				if (!is_array($value)) {
-					$value = explode(',', $value);
-				}
-				$labels = [];
-				foreach ($value as $val) {
+				$values = explode(',', $value);
+				$labels = array_map(function($val) use ($field_config) {
 					$val = trim($val);
-					if (isset($field_config['options'][$val])) {
-						$labels[] = $field_config['options'][$val];
-					} else {
-						$labels[] = $val; // Fallback to value if no label found
-					}
-				}
+					return $field_config['options'][$val] ?? ucfirst(str_replace('_', ' ', $val));
+				}, $values);
 				return implode(', ', $labels);
 
 			case 'radio':
 			case 'select':
-				// Return label instead of value
 				if (isset($field_config['options'][$value])) {
 					return $field_config['options'][$value];
 				}
-				return $value;
-
-			case 'textarea':
-			case 'wysiwyg':
-				// Keep line breaks for email display
-				return $value; // nl2br is already applied in the email body
+				// Fallback: capitalize the value
+				return ucfirst(str_replace('_', ' ', $value));
 
 			case 'true_false':
 				$true_text = $field_config['true_text'] ?? 'Yes';
 				$false_text = $field_config['false_text'] ?? 'No';
 				return ($value === '1' || $value === 1 || $value === true) ? $true_text : $false_text;
 
-			case 'email':
-				return $value; // Will be clickable in email client
-
-			case 'url':
-				return $value; // Will be clickable in email client
-
 			default:
 				return is_array($value) ? implode(', ', $value) : $value;
 		}
 	}
 
 	/**
+	 * Format repeater field for email
+	 */
+	protected function formatRepeaterForEmail(array $rows, array $field_config): string
+	{
+		$output = '';
+
+		foreach ($rows as $index => $row) {
+			$output .= '<strong>Entry ' . ($index + 1) . ':</strong><br>';
+
+			foreach ($row as $sub_field => $sub_value) {
+				if ($this->isEmptyValue($sub_value)) {
+					continue;
+				}
+
+				// Get sub-field label if available
+				$sub_config = $field_config['sub_fields'][$sub_field] ?? [];
+				$label = $sub_config['label'] ?? ucfirst(str_replace('_', ' ', $sub_field));
+
+				$output .= '&nbsp;&nbsp;' . esc_html($label) . ': ' . esc_html($sub_value) . '<br>';
+			}
+
+			$output .= '<br>';
+		}
+
+		return $output;
+	}
+
+	/**
+	 * Format tag-list field for email
+	 */
+	protected function formatTagListForEmail(array $tags): string
+	{
+		$items = [];
+
+		foreach ($tags as $tag) {
+			// Get first non-empty value as display
+			$display = '';
+			foreach ($tag as $field => $value) {
+				if (!$this->isEmptyValue($value)) {
+					$display = $value;
+					break;
+				}
+			}
+
+			if ($display) {
+				$items[] = $display;
+			}
+		}
+
+		return implode(', ', $items);
+	}
+
+	/**
+	 * Format location field for email
+	 */
+	protected function formatLocationForEmail(array $location): string
+	{
+		$parts = [];
+
+		if (!empty($location['street'])) $parts[] = $location['street'];
+		if (!empty($location['city'])) $parts[] = $location['city'];
+		if (!empty($location['province'])) $parts[] = $location['province'];
+		if (!empty($location['postal_code'])) $parts[] = $location['postal_code'];
+		if (!empty($location['country'])) $parts[] = $location['country'];
+
+		return !empty($parts) ? implode(', ', $parts) : ($location['address'] ?? '');
+	}
+
+	/**
 	 * Process uploaded files for email attachments
 	 * Files are in PHP's tmp directory and will be automatically cleaned up after request
 	 */
@@ -620,7 +713,7 @@
 			];
 		}
 
-		return new WP_REST_Response($public_forms, 200);
+		return $this->success($public_forms);
 	}
 
 	/**
@@ -628,18 +721,16 @@
 	 */
 	public function getForm(WP_REST_Request $request): WP_REST_Response
 	{
-		$form_type = $request->get_param('form_type');
+		$form_type = sanitize_text_field($request->get_param('form_type'));
 		$form_config = FormBlock::getForm($form_type);
 
 		if (!$form_config) {
-			return new WP_REST_Response([
-				'error' => 'Form not found'
-			], 404);
+			return $this->notFound('Form not found');
 		}
 
 		// Remove sensitive data
 		unset($form_config['email_to']);
 
-		return new WP_REST_Response($form_config, 200);
+		return $this->success($form_config);
 	}
 }

--
Gitblit v1.10.0