forms = apply_filters('jvbForms', []); if (empty($this->forms)) { return; } $this->formContact = apply_filters('jvbFormContact', ''); // Setup Turnstile $this->turnstile_site_key = JVB_CLOUDFLARE_SITE_KEY; $this->turnstile_secret_key = JVB_CLOUDFLARE_SECRET_KEY; $this->meta = new MetaManager(null, 'form'); $this->cache = CacheManager::for('forms', WEEK_IN_SECONDS); } /** * Add query vars for form submission * @param array $vars * * @return array */ public function addQueryVars(array $vars):array { $vars[] = 'jvb_submitted'; $vars[] = 'jvb_form_error'; return $vars; } /** * @param string $type * * @return false|string */ public function renderForm(string $type):string|false { if (!array_key_exists($type, $this->forms)) { return false; } $submitted = get_query_var('jvb_submitted', false); $error = get_query_var('jvb_form_error', false); ob_start(); // Handle success state - return only success message if ($submitted) { $submission_id = sanitize_text_field($submitted); $submission_data = $this->cache->get('submission_' . $submission_id); echo '
'; echo '

'.$this->forms[$type]['success_title']??'We got it'.'!

'; if (!empty($this->forms[$type]['success_message'])) { foreach ($this->forms[$type]['success_message'] as $message) { echo '

'.$message.'

'; } } if ($submission_data) { echo '
'; echo '

Your submission:

'; echo ''; echo '
'; } echo ($this->formContact !== '') ? '

'.$this->formContact.'

' : ''; echo '
'; return ob_get_clean(); } // Handle error state - show error message above form if ($error) { echo '
'; echo '

Whoops!

'; echo '

Something went wrong there. Sorry about that.

'; echo ($this->formContact !== '') ? '

'.$this->formContact.'

' : ''; echo '
'; } $id = uniqid($type); $this->renderFormStart($type, $id); $this->renderFields($type); $this->renderTurnstile(); $this->renderFormEnd($type, $id); return ob_get_clean(); } /** * @param string $type * @param string $id * * @return void */ protected function renderFormStart(string $type, string $id):void { ?>
forms)) { return; } if (empty($this->forms[$type]['fields'])) { return; } if (array_key_exists('sections', $this->forms[$type])) { $this->renderSections($type); return; } else { foreach ($this->forms[$type]['fields'] as $field_name => $field_config) { $this->meta->render('form', $field_name, $field_config, false, false); } } } protected function renderSections(string $type):void { echo '
'; $nav = ''; $fields = $this->forms[$type]['fields']; $i = 0; foreach ($this->forms[$type]['sections'] as $slug => $section) { $class = ($i == 0) ? ' active' : ''; ?>

$config) : ?> meta->render('form', $field, $config, false, false); ?>
forms[$type]['submit']??'Submit'; // Add hidden fields ?>
connect('cloudflare'); if ($cloudflare->isSetUp()) { $cloudflare->renderTurnstile(); } } /** * Process form submission * @return void * @throws Exception */ public function processForm():void { // Verify nonce if (!isset($_POST['_wpnonce'])) { wp_redirect(home_url()); exit; } $form_id = sanitize_text_field($_POST['form_id']); if (!wp_verify_nonce($_POST['_wpnonce'], 'jvb_form_' . $form_id)) { wp_redirect(home_url()); exit; } $type = sanitize_text_field($_POST['form_type']); if (!array_key_exists($type, $this->forms)) { wp_redirect(home_url()); } error_log('Form Post Data: '.print_r($_POST, true)); // Verify Turnstile if (!$this->verifyTurnstile()) { $referer = wp_get_referer() ?: home_url($path); wp_redirect(add_query_arg('jvb_form_error', urlencode('Please complete the security check.'), $referer)); exit; } // Check rate limits $ip_address = $_SERVER['REMOTE_ADDR']; $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : ''; $rate_check = $this->checkRateLimit($ip_address, $email); if ($rate_check !== true) { $error_message = $rate_check === 'hourly_limit' ? 'Too many submissions in the last hour' : 'Too many submissions in the last 24 hours'; wp_redirect(add_query_arg('jvb_form_error', urlencode($error_message), wp_get_referer())); exit; } // Process form data $form_data = []; foreach ($this->forms[$type]??[] as $field_name => $field_config) { // Skip fields that weren't submitted (like hidden conditional fields) if (!isset($_POST[$field_name])) { continue; } $value = $_POST[$field_name]; if (!$this->meta->validator->validate($value, $field_config)) { error_log('Validation unsuccessful'); throw new Exception("Validation failed for {$field_name}"); } $form_data[$field_name] = $this->meta->sanitizer->sanitize($value, $field_config); } // Send email $email_sent = $this->sendEmail($type, $form_data); if (!$email_sent) { $referer = wp_get_referer() ?: home_url(); wp_redirect(add_query_arg('jvb_form_error', urlencode('Failed to send your message. Please try again later.'), $referer)); exit; } $this->cache->set('submission_' . $form_id, $form_data, HOUR_IN_SECONDS); // Redirect back to form with success parameter $redirect = wp_get_referer() ?: home_url($path); wp_redirect(add_query_arg('jvb_submitted', $form_id, $redirect)); exit; } /** * Send email with form data */ /** * @param string $type * @param array $form_data * * @return bool */ protected function sendEmail(string $type, array $form_data):bool { // Set up email data $to = get_bloginfo('admin_email'); $subject = $this->forms[$type]['subject']??'New Form Entry'; // Build email body $body = '

Hey team!

'; $body .= '

Date: ' . date_i18n(get_option('date_format') . ' ' . get_option('time_format')) . '

'; $body .= '
'; foreach ($form_data as $field_name => $value) { // Skip internal fields if (in_array($field_name, ['action', 'form_id', 'form_type', 'timestamp', '_wpnonce'])) { continue; } // Get field label from config $label = $this->forms[$type][$field_name]['label'] ?? $field_name; // Format value for display if (is_array($value)) { $value = implode(', ', $value); } $body .= '

' . esc_html($label) . ': ' . nl2br(esc_html($value)) . '

'; } // Add reply-to if email field exists if (isset($form_data['email'])) { $name = isset($form_data['name']) ? $form_data['name'] : ''; $headers[] = 'Reply-To: ' . $name . ' <' . $form_data['email'] . '>'; } // Send email return jvbMail($to, $subject, $body, $headers); } /** * Verify Cloudflare Turnstile token * @return bool */ protected function verifyTurnstile():bool { if (empty($_POST['cf-turnstile-response'])) { return false; } $cloudflare = JVB()->connect('cloudflare'); if (!$cloudflare->isSetUp()){ return true; } $token = $_POST['cf-turnstile-response']; $ip = $_SERVER['REMOTE_ADDR']; return $cloudflare->verifyTurnstile($token, $ip); } /** * Check rate limits for form submissions * @param string $ip_address * @param string $email * * @return string|true */ protected function checkRateLimit(string $ip_address, string $email):string|bool { // Check submissions in last hour $hour_limit = 3; $day_limit = 10; $submissions = get_transient('jvb_form_submissions_' . md5($ip_address)); if (!$submissions) { $submissions = [ 'hour' => [], 'day' => [], 'email' => [] ]; } // Clean old submissions $now = time(); $submissions['hour'] = array_filter($submissions['hour'], function ($time) use ($now) { return ($now - $time) < 3600; // Last hour }); $submissions['day'] = array_filter($submissions['day'], function ($time) use ($now) { return ($now - $time) < 86400; // Last 24 hours }); $submissions['email'] = array_filter($submissions['email'], function ($data) use ($now) { return ($now - $data['time']) < 86400; }); // Check limits if (count($submissions['hour']) >= $hour_limit) { return 'hourly_limit'; } if (count($submissions['day']) >= $day_limit) { return 'daily_limit'; } // Add new submission $submissions['hour'][] = $now; $submissions['day'][] = $now; if (!empty($email)) { $submissions['email'][] = [ 'email' => $email, 'time' => $now ]; } // Store updated submissions set_transient('jvb_form_submissions_' . md5($ip_address), $submissions, DAY_IN_SECONDS); return true; } }