';
$nav = '
';
$fields = $this->forms[$type]['fields'];
$i = 0;
foreach ($this->forms[$type]['sections'] as $slug => $section) {
$class = ($i == 0) ? ' active' : '';
?>
= esc_html($section['title']); ?>
= wp_kses_post($section['description']); ?>
$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;
}
}