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