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 .= ' ' . 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