| | |
| | | <?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; |
| | |
| | | * |
| | | * 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(); |
| | | } |
| | | |
| | | /** |
| | |
| | | $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.'); |
| | |
| | | */ |
| | | 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 = []; |
| | | |
| | |
| | | $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 |
| | |
| | | $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) |
| | |
| | | } |
| | | |
| | | // Sanitize field |
| | | $processed_data[$field_name] = $meta->sanitizer->sanitize($value, $field_config); |
| | | $processed_data[$field_name] = $sanitizer->sanitize($value, $field_config); |
| | | } |
| | | |
| | | if (!empty($errors)) { |
| | |
| | | /** |
| | | * 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 = JVB()->email()->sendEmail($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; |
| | | } |
| | | |
| | | |
| | |
| | | ]; |
| | | } |
| | | |
| | | return new WP_REST_Response($public_forms, 200); |
| | | return $this->success($public_forms); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | 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); |
| | | } |
| | | } |