'Méthode de requête invalide.', )); } // Verify nonce if (!isset($_POST['adpf_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['adpf_nonce'])), 'adpf_form_submit')) { wp_send_json_error(array( 'message' => 'Security error. Please refresh the page and try again.', )); } // Rate limiting - prevent brute force attacks if (!$this->check_rate_limit()) { wp_send_json_error(array( 'message' => 'Too many attempts. Please wait a moment before trying again.', )); } // Get form configuration $form_id = isset($_POST['adpf_form_id']) ? sanitize_text_field(wp_unslash($_POST['adpf_form_id'])) : ''; // Validate form_id length to prevent DoS if (strlen($form_id) > 100) { wp_send_json_error(array( 'message' => 'Identifiant de formulaire invalide.', )); } $form_config = $this->get_form_config($form_id); if (empty($form_config)) { wp_send_json_error(array( 'message' => 'Configuration du formulaire introuvable.', )); } // Check honeypot $settings = ADPF_Settings::get_settings(); $honeypot_field = isset($settings['honeypot_field_name']) ? $settings['honeypot_field_name'] : 'website_url'; if (isset($_POST[$honeypot_field]) && !empty($_POST[$honeypot_field])) { // Honeypot field was filled - silently reject wp_send_json_success(array( 'message' => 'Thank you! Your message has been sent successfully. We will get back to you as soon as possible.', )); } // Validate and sanitize form data $validation_result = $this->validate_form_data($form_config); if (is_wp_error($validation_result)) { wp_send_json_error(array( 'message' => $validation_result->get_error_message(), 'field' => $validation_result->get_error_code(), )); } $form_data = $validation_result; // Save entry to database $entry_id = ADPF_Database::insert_entry($form_id, $form_data); if (!$entry_id) { wp_send_json_error(array( 'message' => 'An error occurred while saving your submission. Please try again.', )); } // Send email notifications $admin_email_sent = $this->send_admin_notification($form_id, $form_data, $form_config); // Get success message from settings $settings = ADPF_Settings::get_settings(); $success_message = isset($settings['success_message']) && !empty($settings['success_message']) ? $settings['success_message'] : 'Thank you! Your message has been sent successfully. We will get back to you as soon as possible.'; // Return success response wp_send_json_success(array( 'message' => $success_message, )); } /** * Validate form data * * @param array $form_config Form configuration * @return array|WP_Error Validated and sanitized data or WP_Error */ private function validate_form_data($form_config) { $validated_data = array(); $fields = isset($form_config['fields']) ? $form_config['fields'] : array(); $settings = ADPF_Settings::get_settings(); $required_message = !empty($settings['required_field_message']) ? $settings['required_field_message'] : __('Please fill all the required fields.', 'ad-pro-forms'); foreach ($fields as $field) { $field_name = isset($field['name']) ? $field['name'] : ''; $field_type = isset($field['type']) ? $field['type'] : 'text'; $field_required = isset($field['required']) ? $field['required'] : false; $field_label = isset($field['label']) ? $field['label'] : $field_name; // Skip submit buttons, honeypot, and html_text (display-only) if (in_array($field_type, array('submit', 'honeypot', 'html_text'))) { continue; } // Get field value // For checkboxes, handle as array (multiple checkboxes) if ($field_type === 'checkbox') { // Checkboxes come as array: field_name[] // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified at start of function $checkbox_values = isset($_POST[$field_name]) && is_array($_POST[$field_name]) ? array_map('sanitize_text_field', wp_unslash($_POST[$field_name])) : array(); $field_options = isset($field['options']) ? $field['options'] : array(); // Check field-level required: at least one box must be checked if ($field_required && empty($checkbox_values)) { return new WP_Error($field_name, $required_message); } // Validate each required checkbox option foreach ($field_options as $option) { // Support both old format (string) and new format (object) $option_value = is_array($option) && isset($option['value']) ? $option['value'] : $option; $option_required = is_array($option) && isset($option['required']) ? $option['required'] : false; if ($option_required && !in_array($option_value, $checkbox_values, true)) { /* translators: %s: The checkbox option value */ return new WP_Error($field_name, sprintf(__('The option "%s" is required.', 'ad-pro-forms'), $option_value)); } } // Store selected values as array $field_value = $checkbox_values; } else { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified at start of function $field_value = isset($_POST[$field_name]) ? sanitize_text_field(wp_unslash($_POST[$field_name])) : ''; // Check required fields if ($field_required && empty($field_value)) { return new WP_Error($field_name, $required_message); } // Validate field length to prevent DoS attacks $max_length = apply_filters('adpf_field_max_length', 10000); // Default 10KB per field if (strlen($field_value) > $max_length) { return new WP_Error($field_name, 'This field is too long. Please reduce the length.'); } } // Validate and sanitize based on field type switch ($field_type) { case 'email': if (!empty($field_value)) { $field_value = sanitize_email($field_value); if (!is_email($field_value)) { return new WP_Error($field_name, 'Please enter a valid email address.'); } // Additional email validation - check for header injection attempts if (preg_match('/[\r\n]/', $field_value)) { return new WP_Error($field_name, 'Invalid email address.'); } } break; case 'tel': $field_value = sanitize_text_field($field_value); // Validate phone number format (basic) if (!empty($field_value) && !preg_match('/^[\d\s\-\+\(\)]+$/', $field_value)) { return new WP_Error($field_name, 'Numéro de téléphone invalide.'); } break; case 'number': if (!empty($field_value)) { if (!is_numeric($field_value)) { return new WP_Error($field_name, 'Please enter a valid number.'); } // Check for reasonable number range $num_value = floatval($field_value); if ($num_value > PHP_INT_MAX || $num_value < -PHP_INT_MAX) { return new WP_Error($field_name, 'Nombre invalide.'); } $field_value = $num_value; } break; case 'radio': case 'select': // Validate against allowed options if provided $field_options = isset($field['options']) ? $field['options'] : array(); if (!empty($field_options) && !empty($field_value)) { // Sanitize options array $field_options = array_map('sanitize_text_field', $field_options); if (!in_array($field_value, $field_options, true)) { return new WP_Error($field_name, 'Valeur invalide sélectionnée.'); } } $field_value = sanitize_text_field($field_value); break; case 'textarea': $field_value = sanitize_textarea_field($field_value); // Remove any potential script tags $field_value = wp_strip_all_tags($field_value); break; case 'checkbox': // Checkbox values are already sanitized as array above // Validate against allowed options $field_options = isset($field['options']) ? $field['options'] : array(); $allowed_values = array(); foreach ($field_options as $option) { $option_value = is_array($option) && isset($option['value']) ? $option['value'] : $option; $allowed_values[] = sanitize_text_field($option_value); } // Ensure all selected values are in allowed options foreach ($field_value as $selected_value) { if (!in_array($selected_value, $allowed_values, true)) { return new WP_Error($field_name, 'Valeur invalide sélectionnée.'); } } // Keep as array - will be JSON encoded when saved break; case 'text': default: $field_value = sanitize_text_field($field_value); // Additional XSS protection $field_value = wp_strip_all_tags($field_value); break; } $validated_data[$field_name] = array( 'label' => $field_label, 'value' => $field_value, 'type' => $field_type, ); } return $validated_data; } /** * Send admin notification email * * @param string $form_id Form ID * @param array $form_data Form data * @param array $form_config Form configuration * @return bool True if email was sent successfully, false otherwise */ private function send_admin_notification($form_id, $form_data, $form_config = array()) { // Custom recipient and email templates are Pro features // Always send to site admin email in free version $admin_email = get_option('admin_email'); $site_name = get_bloginfo('name'); // Sanitize form_id to prevent header injection $form_id = sanitize_text_field($form_id); // Get form title from database $form = ADPF_Database::get_form($form_id); $form_title = ($form && !empty($form->title)) ? sanitize_text_field($form->title) : ''; // Build subject using hardcoded default template $subject_form_label = !empty($form_title) ? $form_title : $form_id; $subject = str_replace( array('{form_title}', '{site_name}'), array($subject_form_label, $site_name), 'New Form Submission | {form_title}' ); $subject = sanitize_text_field($subject); // Build message safely (plain text, no HTML escaping) $message = "New form submission received\n\n"; if (!empty($form_title)) { $message .= "Form: " . $form_title . " (" . $form_id . ")\n"; } else { $message .= "Form: " . $form_id . "\n"; } $message .= "Date: " . current_time('mysql') . "\n"; $message .= "IP: " . $this->get_client_ip() . "\n\n"; $message .= "Details:\n"; $message .= "--------------------\n\n"; foreach ($form_data as $field_name => $field_data) { $label = isset($field_data['label']) ? $field_data['label'] : $field_name; $value = isset($field_data['value']) ? $field_data['value'] : ''; $field_type = isset($field_data['type']) ? $field_data['type'] : 'text'; // Don't escape values for plain text email if ($field_type === 'textarea') { // Preserve line breaks for textarea fields $value = wp_unslash($value); } // Format the field in the email if (is_array($value)) { $value = !empty($value) ? implode(', ', $value) : '—'; } if ($field_type === 'textarea' && !empty($value)) { $message .= $label . ":\n" . $value . "\n\n"; } else { $message .= $label . ": " . $value . "\n\n"; } } $message .= "\n--------------------\n"; $message .= "\nYou can view all entries in the WordPress admin panel."; // Get the site admin email for the From header $from_email = get_option('admin_email'); $from_name = $site_name; // Simple, compatible headers $headers = array( 'Content-Type: text/plain; charset=UTF-8' ); // Allow developers to modify email content $admin_email = apply_filters('adpf_admin_notification_email', $admin_email, $form_id, $form_data); $subject = apply_filters('adpf_admin_notification_subject', $subject, $form_id, $form_data); $message = apply_filters('adpf_admin_notification_message', $message, $form_id, $form_data); $headers = apply_filters('adpf_admin_notification_headers', $headers, $form_id, $form_data); // Send email $mail_sent = wp_mail($admin_email, $subject, $message, $headers); return $mail_sent; } /** * Get form configuration * * @param string $form_id Form ID * @return array|false Form configuration or false */ private function get_form_config($form_id) { // Sanitize form_id $form_id = sanitize_text_field($form_id); // First, try to get from database $form = ADPF_Database::get_form($form_id); if ($form && isset($form->config)) { // Validate config structure if (is_array($form->config) && isset($form->config['fields']) && is_array($form->config['fields'])) { return $form->config; } } // Fallback to filter (for programmatically registered forms) $config = apply_filters('adpf_form_config_' . $form_id, false); // Validate config structure if ($config && is_array($config) && isset($config['fields']) && is_array($config['fields'])) { return $config; } return false; } /** * Check rate limit to prevent brute force attacks * * @return bool True if within rate limit, false otherwise */ private function check_rate_limit() { $ip = $this->get_client_ip(); $transient_key = 'adpf_rate_limit_' . md5($ip); // Get current submission count $submissions = get_transient($transient_key); if ($submissions === false) { // First submission, set transient for 15 minutes set_transient($transient_key, 1, 15 * MINUTE_IN_SECONDS); return true; } // Check if limit exceeded (10 submissions per 15 minutes) $max_submissions = apply_filters('adpf_rate_limit_max', 10); $time_window = apply_filters('adpf_rate_limit_window', 15 * MINUTE_IN_SECONDS); if ($submissions >= $max_submissions) { return false; } // Increment counter set_transient($transient_key, $submissions + 1, $time_window); return true; } /** * Get client IP address with improved security * * @return string IP address */ private function get_client_ip() { $ip_keys = array( 'HTTP_CF_CONNECTING_IP', // Cloudflare 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR', ); foreach ($ip_keys as $key) { if (array_key_exists($key, $_SERVER) === true) { // Get first IP from comma-separated list $ip_list = explode(',', sanitize_text_field(wp_unslash($_SERVER[$key]))); $ip = trim($ip_list[0]); // Validate IP address if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { return sanitize_text_field($ip); } } } // Fallback to REMOTE_ADDR $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : ''; return sanitize_text_field($ip); } }