From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh
---
inc/rest/routes/FormRoutes.php | 666 ++++++++++++++++++++++++++++++++++++++++---------------
1 files changed, 485 insertions(+), 181 deletions(-)
diff --git a/inc/rest/routes/FormRoutes.php b/inc/rest/routes/FormRoutes.php
index 0e5cfd2..f2bd9de 100644
--- a/inc/rest/routes/FormRoutes.php
+++ b/inc/rest/routes/FormRoutes.php
@@ -1,11 +1,17 @@
<?php
namespace JVBase\rest\routes;
-use JVBase\rest\RestRouteManager;
-use JVBase\managers\CacheManager;
-use JVBase\meta\MetaManager;
-use JVBase\managers\CloudflareTurnstile;
+use JVBase\Instance;
+use JVBase\integrations\services\Cloudflare;
+use JVBase\managers\Emails;
+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 +26,51 @@
*
* Handles REST API endpoints for form submissions
*/
-class FormRoutes extends RestRouteManager
+class FormRoutes extends Rest
{
- protected CacheManager $cache;
+ use Instance;
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 = new CacheManager('form_submissions', 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,71 +91,104 @@
$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') ) {
+ return true;
+ }
+ $cloudflare = Cloudflare::getInstance();
+ if (!$cloudflare->isSetUp()) {
+ return true;
+ }
+ if (empty($token)) {
+ return false;
+ }
+
+ return $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.');
}
// Store submission data temporarily for success display
- $this->cache->set('submission_' . $form_id, $processed_data, HOUR_IN_SECONDS);
+ $this->cache->set('submission_' . $form_id, $processed_data);
// Log successful submission
$this->recordSubmission($_SERVER['REMOTE_ADDR'], $processed_data['email'] ?? '');
@@ -175,7 +204,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 +229,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 +247,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 +256,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 +270,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 = Emails::getInstance()->h2($form_config['title']);
+ $body .= '<p style="font-size:13px;color:' . Emails::getInstance()->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 .= Emails::getInstance()->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 .= Emails::getInstance()->spacer(20);
+ $body .= Emails::getInstance()->table($form);
+
+ // Show attachment info
+ $attachments = $this->processFileAttachments($files);
+ if (!empty($attachments)) {
+ $body .= Emails::getInstance()->spacer(10);
+ $body .= Emails::getInstance()->alert(
+ sprintf('%d file(s) attached to this email', count($attachments)),
+ 'info'
+ );
+ }
+
+ // Send the email
+ return Emails::getInstance()->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 .= ' ' . 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 +720,7 @@
];
}
- return new WP_REST_Response($public_forms, 200);
+ return $this->success($public_forms);
}
/**
@@ -422,18 +728,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