From 0113d2e9c9ff34a6ffb10707cc76d34b67a0c367 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 19 Jan 2026 16:29:41 +0000
Subject: [PATCH] =Refactored window.getTemplate into a full templating class window.jvbTemplates. Refactored CRUD.js, UploadManager.js, FormController.js, PopulateForm.js with that in mind

---
 inc/rest/routes/FormRoutes.php |  462 +++++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 334 insertions(+), 128 deletions(-)

diff --git a/inc/rest/routes/FormRoutes.php b/inc/rest/routes/FormRoutes.php
index 198e702..f1efa39 100644
--- a/inc/rest/routes/FormRoutes.php
+++ b/inc/rest/routes/FormRoutes.php
@@ -6,6 +6,7 @@
 use JVBase\meta\MetaManager;
 use JVBase\managers\CloudflareTurnstile;
 use JVBase\blocks\FormBlock;
+use JVBase\utility\Features;
 use WP_REST_Request;
 use WP_REST_Response;
 use WP_Error;
@@ -32,15 +33,13 @@
 		$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
 	 */
@@ -51,12 +50,12 @@
 			[
 				'methods' => 'POST',
 				'callback' => [$this, 'submitForm'],
-				'permission_callback' => '__return_true', // Public endpoint
-				[
-					'methods' => 'GET',
-					'callback' => [$this, 'getForms'],
-					'permission_callback' => [$this, 'checkPermission']
-				]
+				'permission_callback' => [$this, 'checkRateLimit'], // Public endpoint, rate limited
+			],
+			[
+				'methods' => 'GET',
+				'callback' => [$this, 'getForms'],
+				'permission_callback' => [$this, 'checkPermission']
 			]
 		]);
 
@@ -95,10 +94,10 @@
 			$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([
@@ -123,36 +122,65 @@
 		}
 	}
 
+	protected function verifyTurnstile(string $token): bool
+	{
+		if (!Features::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);
 
-		error_log('Config: '.print_r($form_config, true));
+		if (!$form_config) {
+			return new WP_Error('invalid_form', 'Form configuration not found.');
+		}
 
+		// Verify Turnstile
+		$turnstile_token = $form_data['cf-turnstile-response'] ?? '';
 
-		// 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.');
-//			}
-//		}
+		if (!$this->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.');
@@ -239,136 +267,314 @@
 	/**
 	 * Send email notification
 	 */
-	protected function sendEmailNotification(string $form_type, array $form_config, array $form_data): bool
+	/**
+	 * 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);
 		$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>';
-		}
-
-		$body .= '</div>';
-
-		// 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') . '.'
+			);
 		}
 
+		// Build form data array for table
+		$form = [];
+		foreach ($form_config['fields'] as $field_name => $config) {
+			// Skip file upload fields
+			if (in_array($config['type'], ['upload'])) {
+				continue;
+			}
+
+			$value = $form_data[$field_name] ?? '';
+
+			// Skip empty fields
+			if (empty($value)) {
+				continue;
+			}
+
+			$form[] = [
+				'label' => $config['summaryTitle'] ?? $config['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
+		$email_sent = JVB()->email()->sendEmail(
+			$to,
+			$subject,
+			$body,
+			'',
+			$headers,
+			$attachments
+		);
+
 		return $email_sent;
 	}
 
+	/**
+	 * Format field value for email display
+	 */
+	protected function formatFieldValueForEmail(string $field_name, mixed $value, array $field_config): string
+	{
+		if (empty($value)) {
+			return '';
+		}
+
+		$type = $field_config['type'] ?? 'text';
+		$type = $field_config['subType'] ?? $type;
+
+		switch ($type) {
+			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);
+				}
+				return $value;
+
+			case 'checkbox':
+			case 'set':
+				// Convert array of values to comma-separated labels
+				if (!is_array($value)) {
+					$value = explode(',', $value);
+				}
+				$labels = [];
+				foreach ($value as $val) {
+					$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 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
+
+			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;
+		}
+	}
+
+	/**
+	 * 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;
+	}
+
 
 	/**
 	 * Record submission for rate limiting

--
Gitblit v1.10.0