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 |  657 +++++++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 477 insertions(+), 180 deletions(-)

diff --git a/inc/rest/routes/FormRoutes.php b/inc/rest/routes/FormRoutes.php
index 198e702..1cf8b23 100644
--- a/inc/rest/routes/FormRoutes.php
+++ b/inc/rest/routes/FormRoutes.php
@@ -1,11 +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\rest\Route;
+use JVBase\base\Site;
 use WP_REST_Request;
 use WP_REST_Response;
 use WP_Error;
@@ -20,61 +23,50 @@
  *
  * 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);
-
-		// Initialize Cloudflare Turnstile if available
-		$this->turnstile = class_exists('JVBase\managers\CloudflareTurnstile') && jvbSiteUsesCloudflare()
-			? new CloudflareTurnstile()
-			: null;
 
 		// Add query vars
 		add_filter('query_vars', [$this, 'addQueryVars']);
 	}
 
+
+
 	/**
 	 * Register REST routes
 	 */
 	public function registerRoutes(): void
 	{
-		// Form submission endpoint
-		register_rest_route($this->namespace, '/forms', [
-			[
-				'methods' => 'POST',
-				'callback' => [$this, 'submitForm'],
-				'permission_callback' => '__return_true', // Public endpoint
-				[
-					'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();
 	}
 
 	/**
@@ -95,64 +87,94 @@
 			$form_type = $request->get_param('form_type');
 			$form_id = $request->get_param('form_id');
 			$form_data = $request->get_params();
+			$files = $request->get_file_params();
 
-			error_log('Form submission: '.print_r($request->get_params(), true));
 			// Process the submission
-			$result = $this->handleFormSubmission($form_type, $form_id, $form_data);
+			$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 (!Site::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) {
+			return true;
+		}
+
+		if (empty($token)) {
+			return false;
+		}
+
+		return JVB()->connect('cloudflare')->verifyTurnstile($token);
+	}
+
 	/**
 	 * Handle the actual form submission logic
 	 */
-	protected function handleFormSubmission(string $form_type, string $form_id, array $form_data): array|WP_Error
+	protected function handleFormSubmission(string $form_type, string $form_id, array $form_data, array $files): array|WP_Error
 	{
 		// Get form configuration
 		$form_config = FormBlock::getForm($form_type);
+		if (!$form_config) {
+			return new WP_Error('invalid_form', 'Form configuration not found.');
+		}
 
-		error_log('Config: '.print_r($form_config, true));
+		// Verify Turnstile
+		$turnstile_token = $form_data['cf-turnstile-response'] ?? '';
 
+		if (!$this->verifyTurnstile($turnstile_token)) {
+			return new WP_Error('turnstile_failed', 'Security verification failed. Please try again.');
+		}
 
-		// Verify Turnstile if enabled
-		//TODO: Reenable
-//		if (jvbSiteUsesCloudflare() && $this->turnstile) {
-//			error_log('Verifying turnstile...');
-//			$turnstile_token = $form_data['cf-turnstile-response'] ?? '';
-//			if (!$this->turnstile->verifyTurnstile($turnstile_token)) {
-//				return new WP_Error('turnstile_failed', 'Security verification failed. Please try again.');
-//			}
-//		}
+		// Validate file uploads if present
+		if (!empty($files)) {
+			try {
+				$files = $this->validateFileUploads($files, $form_config);
+			} catch (\Exception $e) {
+				return new WP_Error('file_validation_failed', 'File validation error: ' . $e->getMessage());
+			}
+		}
 
 		// Validate and sanitize form data
-		$processed_data = $this->validateAndSanitizeData($form_config, $form_data);
-		error_log('Processed data: '.print_r($processed_data, true));
+		try {
+			$processed_data = $this->validateAndSanitizeData($form_config, $form_data);
+		} 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;
 		}
 
-		// Send email notification
-		$email_sent = $this->sendEmailNotification($form_type, $form_config, $processed_data);
+		// Send email notification with attachments
+		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());
+		}
 
 		if (!$email_sent) {
 			return new WP_Error('email_failed', 'Failed to send your message. Please try again later.');
@@ -175,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 = [];
 
@@ -198,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
@@ -216,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)
@@ -225,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)) {
@@ -239,134 +263,409 @@
 	/**
 	 * Send email notification
 	 */
-	protected function sendEmailNotification(string $form_type, array $form_config, array $form_data): bool
+	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);
 		$subject = apply_filters('jvb_form_email_subject', $form_config['email_subject'], $form_type, $form_data);
 
 		// Get submitter details
 		$submitter_email = $form_data['email'] ?? null;
-		$submitter_name = $form_data['name'] ?? '';
+		$submitter_name = '';
 
-		// Generate unique message ID for threading
-		$form_id = $form_data['form_id'] ?? uniqid();
-		$timestamp = current_time('timestamp');
-		$message_id = sprintf('<%s-%s-%s@%s>',
-			$form_type,
-			$form_id,
-			$timestamp,
-			parse_url(home_url(), PHP_URL_HOST)
-		);
-
-		// Build unified email body
-		$body = '<div style="font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif; line-height: 1.6; color: #333;">';
-		$body .= '<h2 style="color: #2563eb; border-bottom: 2px solid #e5e7eb; padding-bottom: 0.5rem;">New Form Submission: ' . esc_html($form_config['title']) . '</h2>';
-		$body .= '<p style="color: #6b7280;"><strong>Submitted:</strong> ' . current_time('F j, Y \a\t g:i A') . '</p>';
-
-		// Add note about email thread if submitter email exists
-		if ($submitter_email) {
-			$body .= '<div style="background: #f0f9ff; border-left: 4px solid #3b82f6; padding: 1rem; margin: 1rem 0; border-radius: 0 8px 8px 0;">';
-			$body .= '<p style="margin: 0; color: #1e40af;"><strong>💬 Email Thread Created</strong></p>';
-			$body .= '<p style="margin: 0.5rem 0 0 0; color: #1e40af; font-size: 0.9em;">This email includes both the admin and the person who submitted the form. Any replies will be shared between all parties, creating a conversation thread.</p>';
-			$body .= '</div>';
+		if (array_key_exists('first_name', $form_data)) {
+			$submitter_name = $form_data['first_name'];
+			$submitter_name .= array_key_exists('last_name', $form_data) ? ' '.$form_data['last_name'] : '';
+		} elseif (array_key_exists('name', $form_data)) {
+			$submitter_name = $form_data['name'];
 		}
 
-		$body .= '<div style="margin: 2rem 0;">';
-
-		// Add submission details in a nice format
-		foreach ($form_config['fields'] as $field_name => $config) {
-			$label = $config['summaryTitle'] ?? $config['label'];
-			//Submitted Value
-			$value = $form_data[$field_name];
-			if (str_contains($label, '%s')) {
-				switch ($config['type']) {
-					case 'true_false':
-						$replace = ($value === '1') ? $config['isTrue'] : $config['isFalse'];
-						break;
-				}
-				$label = sprintf(
-					$label,
-					$replace
-				);
-			}
-
-			$body .= '<div style="margin-bottom: 1rem; padding: 0.75rem; background: #f9fafb; border-radius: 6px;">';
-			$body .= '<strong style="color: #374151; display: block; margin-bottom: 0.25rem;">' . esc_html($label) . ':</strong>';
-			$body .= '<span style="color: #6b7280;">' . nl2br(esc_html($value)) . '</span>';
-			$body .= '</div>';
+		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
+			);
 		}
 
-		$body .= '</div>';
+		$subject .= ' ' . $submitter_name;
 
-		// Add footer
-		$body .= '<div style="border-top: 1px solid #e5e7eb; padding-top: 1rem; margin-top: 2rem;">';
-		$body .= '<p style="color: #9ca3af; font-size: 0.8em; margin: 0;"><em>';
-		$body .= 'This message was sent through the ' . esc_html($form_config['title']) . ' on ' . get_bloginfo('name');
+
+		// Email headers
+		$headers = [];
 
 		if ($submitter_email) {
-			$body .= '<br>To continue this conversation, simply reply to this email.';
-		}
-
-		$body .= '</em></p>';
-		$body .= '</div>';
-		$body .= '</div>';
-
-		// Prepare email headers for unified thread with enhanced threading
-		$headers = [
-			'Content-Type: text/html; charset=UTF-8',
-		];
-
-		// Set up recipients and reply-to based on whether submitter email exists
-		if ($submitter_email) {
-			// Primary recipient: admin
 			$to = $admin_email;
-
-			// Add submitter as CC so they both see the email
-			$cc_name = $submitter_name ? $submitter_name : 'Form Submitter';
+			$cc_name = $submitter_name ?: 'Submitter';
 			$headers[] = 'CC: ' . $cc_name . ' <' . $submitter_email . '>';
 
-			// Set reply-to to include both emails for unified thread
 			$site_name = get_bloginfo('name');
 			$admin_name = get_bloginfo('name') . ' Team';
-
 			$headers[] = 'Reply-To: ' . $admin_name . ' <' . $admin_email . '>, ' . $cc_name . ' <' . $submitter_email . '>';
-
-			// Set from address to be more professional
-			$headers[] = 'From: ' . $site_name . ' Forms <' . $admin_email . '>';
-
+			$headers[] = 'From: ' . $site_name . ' <' . $admin_email . '>';
 		} else {
-			// No submitter email, just send to admin
 			$to = $admin_email;
 			$site_name = get_bloginfo('name');
-			$headers[] = 'From: ' . $site_name . ' Forms <' . $admin_email . '>';
+			$headers[] = 'From: ' . $site_name . ' <' . $admin_email . '>';
 			$headers[] = 'Reply-To: ' . $admin_email;
 		}
 
-		// Allow filtering of email data
-		$email_data = apply_filters('jvb_form_unified_email_data', [
-			'to' => $to,
-			'subject' => $subject,
-			'body' => $body,
-			'headers' => $headers,
-			'submitter_included' => !empty($submitter_email),
-			'submitter_email' => $submitter_email,
-			'message_id' => $message_id
-		], $form_type, $form_data);
+		// Build email body
+		$body = JVB()->email()->h2($form_config['title']);
+		$body .= '<p style="font-size:13px;color:' . JVB()->email()->colours['dark-200'] . ';">
+        Submitted: ' . current_time('F j, Y \a\t g:i A') . '
+    </p>';
 
-		// Send the unified email
-		$email_sent = jvbMail($email_data['to'], $email_data['subject'], $email_data['body'], implode(';',$email_data['headers']));
-
-		// Log the email sending for debugging
-		if ($email_sent) {
-			error_log("Form email sent successfully. Recipients: {$to}" .
-				($submitter_email ? " (CC: {$submitter_email})" : "") .
-				" | Message-ID: {$message_id}");
-		} else {
-			error_log("Failed to send form email to: {$to}" .
-				($submitter_email ? " (CC: {$submitter_email})" : ""));
+		// Add thread notice if CC'd
+		if ($submitter_email) {
+			$body .= JVB()->email()->notice(
+				'<strong>Email Thread Created</strong><br>Reply to this email to continue the conversation with ' .
+				esc_html($submitter_name ?: 'the submitter') . '.'
+			);
 		}
 
-		return $email_sent;
+		// 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 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 ($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' => $label,
+				'value' => $this->formatFieldValueForEmail($field_name, $value, $config)
+			];
+		}
+
+		$body .= JVB()->email()->spacer(20);
+		$body .= JVB()->email()->table($form);
+
+		// Show attachment info
+		$attachments = $this->processFileAttachments($files);
+		if (!empty($attachments)) {
+			$body .= JVB()->email()->spacer(10);
+			$body .= JVB()->email()->alert(
+				sprintf('%d file(s) attached to this email', count($attachments)),
+				'info'
+			);
+		}
+
+		// Send the email
+		return JVB()->email()->sendEmail(
+			$to,
+			$subject,
+			$body,
+			'',
+			$preheader,
+			$headers,
+			$attachments
+		);
+	}
+
+	/**
+	 * 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;
+	}
+
+	/**
+	 * Format field value for email display
+	 */
+	protected function formatFieldValueForEmail(string $field_name, mixed $value, array $field_config): string
+	{
+		if ($this->isEmptyValue($value)) {
+			return '';
+		}
+
+		$type = $field_config['type'] ?? 'text';
+		$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':
+				$cleaned = preg_replace('/\D/', '', $value);
+				if (strlen($cleaned) === 10) {
+					return substr($cleaned, 0, 3) . '-' . substr($cleaned, 3, 3) . '-' . substr($cleaned, 6);
+				}
+				return $value;
+
+			case 'checkbox':
+			case 'set':
+				$values = explode(',', $value);
+				$labels = array_map(function($val) use ($field_config) {
+					$val = trim($val);
+					return $field_config['options'][$val] ?? ucfirst(str_replace('_', ' ', $val));
+				}, $values);
+				return implode(', ', $labels);
+
+			case 'radio':
+			case 'select':
+				if (isset($field_config['options'][$value])) {
+					return $field_config['options'][$value];
+				}
+				// 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;
+
+			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
+	 */
+	protected function processFileAttachments(array $files): array
+	{
+		$attachments = [];
+
+		if (empty($files)) {
+			return $attachments;
+		}
+
+		foreach ($files as $field_name => $field_files) {
+			// Skip metadata fields
+			if (str_ends_with($field_name, '_meta')) {
+				continue;
+			}
+
+			// Handle single file
+			if (isset($field_files['tmp_name']) && !is_array($field_files['tmp_name'])) {
+				if (!empty($field_files['tmp_name']) && is_uploaded_file($field_files['tmp_name'])) {
+					$attachments[] = $field_files['tmp_name'];
+				}
+				continue;
+			}
+
+			// Handle multiple files (field[])
+			if (is_array($field_files) && isset($field_files['tmp_name'])) {
+				foreach ($field_files['tmp_name'] as $index => $tmp_name) {
+					if (!empty($tmp_name) && is_uploaded_file($tmp_name)) {
+						$attachments[] = $tmp_name;
+					}
+				}
+			}
+		}
+
+		return $attachments;
+	}
+
+	protected function validateFileUploads(array $files, array $form_config): array
+	{
+		$validated_files = [];
+		$max_file_size = 10 * 1024 * 1024; // 10MB default
+		$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf'];
+
+		foreach ($files as $field_name => $field_files) {
+			// Skip metadata fields
+			if (str_ends_with($field_name, '_meta')) {
+				continue;
+			}
+
+			// Get field config for this upload field
+			$field_config = $form_config['fields'][$field_name] ?? null;
+
+			// Override defaults if field config specifies them
+			if ($field_config) {
+				$max_file_size = $field_config['max_file_size'] ?? $max_file_size;
+				$allowed_types = $field_config['allowed_types'] ?? $allowed_types;
+			}
+
+			// Handle multiple files uploaded to same field (field[])
+			if (isset($field_files['tmp_name']) && is_array($field_files['tmp_name'])) {
+				$valid_indices = [];
+
+				foreach ($field_files['tmp_name'] as $index => $tmp_name) {
+					$file = [
+						'tmp_name' => $tmp_name,
+						'size' => $field_files['size'][$index] ?? 0,
+						'type' => $field_files['type'][$index] ?? '',
+						'name' => $field_files['name'][$index] ?? '',
+						'error' => $field_files['error'][$index] ?? UPLOAD_ERR_OK
+					];
+
+					// Validate this file
+					if ($this->isValidFile($file, $max_file_size, $allowed_types)) {
+						$valid_indices[] = $index;
+					}
+				}
+
+				// Keep only valid files in the original structure
+				if (!empty($valid_indices)) {
+					$validated_files[$field_name] = [
+						'tmp_name' => [],
+						'size' => [],
+						'type' => [],
+						'name' => [],
+						'error' => []
+					];
+
+					foreach ($valid_indices as $index) {
+						$validated_files[$field_name]['tmp_name'][] = $field_files['tmp_name'][$index];
+						$validated_files[$field_name]['size'][] = $field_files['size'][$index];
+						$validated_files[$field_name]['type'][] = $field_files['type'][$index];
+						$validated_files[$field_name]['name'][] = $field_files['name'][$index];
+						$validated_files[$field_name]['error'][] = $field_files['error'][$index];
+					}
+				}
+			}
+			// Handle single file
+			elseif (isset($field_files['tmp_name']) && !is_array($field_files['tmp_name'])) {
+				if ($this->isValidFile($field_files, $max_file_size, $allowed_types)) {
+					$validated_files[$field_name] = $field_files;
+				}
+			}
+		}
+
+		return $validated_files;
+	}
+
+	/**
+	 * Validate a single file
+	 */
+	protected function isValidFile(array $file, int $max_file_size, array $allowed_types): bool
+	{
+		// Check for upload errors
+		if ($file['error'] !== UPLOAD_ERR_OK) {
+			error_log("File upload error: " . $file['error'] . " for " . ($file['name'] ?? 'unknown'));
+			return false;
+		}
+
+		// Check file size
+		if ($file['size'] > $max_file_size) {
+			error_log("File too large: " . $file['size'] . " bytes (max: $max_file_size) for " . ($file['name'] ?? 'unknown'));
+			return false;
+		}
+
+		// Check file type
+		if (!in_array($file['type'], $allowed_types)) {
+			error_log("Invalid file type: " . $file['type'] . " for " . ($file['name'] ?? 'unknown'));
+			return false;
+		}
+
+		// Security check - verify it's actually uploaded
+		if (!is_uploaded_file($file['tmp_name'])) {
+			error_log("Security check failed: file not uploaded via POST for " . ($file['name'] ?? 'unknown'));
+			return false;
+		}
+
+		return true;
 	}
 
 
@@ -414,7 +713,7 @@
 			];
 		}
 
-		return new WP_REST_Response($public_forms, 200);
+		return $this->success($public_forms);
 	}
 
 	/**
@@ -422,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