icon_colors['base']) ) { return $_wp_admin_css_colors[$scheme]->icon_colors['base']; } if ( ! empty($_wp_admin_css_colors['modern']->icon_colors['base']) ) { return $_wp_admin_css_colors['modern']->icon_colors['base']; } return '#a7aaad'; // WordPress default menu icon base colour. } /** * Monochrome sidebar menu icon as a base64 data URI. * * The standalone brand icon (assets/img/accua-contacts-forms.svg) stays coloured * and is used unchanged in page headers and other contexts; only the sidebar menu * icon is neutral, per the WordPress.org plugin guidelines. */ function accua_forms_admin_menu_icon(){ $color = accua_forms_admin_menu_icon_color(); $svg = ''; return 'data:image/svg+xml;base64,' . base64_encode($svg); } /** * Repaint the sidebar menu icon with the active colour scheme's base colour. * * Runs on admin_init (priority 20, after register_admin_color_schemes at 1) when * the colour schemes are available. The icon set at menu-registration time uses * the fallback colour; here we overwrite it in the $menu global with the exact * scheme colour so the first paint matches svg-painter.js and there is no flash. */ add_action('admin_init', 'accua_forms_paint_menu_icon', 20); function accua_forms_paint_menu_icon(){ global $menu; if ( ! is_array($menu) ) { return; } foreach ( $menu as $i => $item ) { if ( isset($item[2]) && 'accua_forms' === $item[2] ) { $menu[$i][6] = accua_forms_admin_menu_icon(); break; } } } add_action('admin_menu', 'accua_forms_menu', -95); function accua_forms_menu(){ $dashboard_admin_page=add_menu_page('Contact Forms by Cimatti', 'Contact Forms', 'manage_options', 'accua_forms', 'accua_forms_dashboard_page', accua_forms_admin_menu_icon(), '90.90300'); add_action('load-'.$dashboard_admin_page, 'accua_forms_dashboard_page_head'); add_submenu_page('accua_forms', 'Contact Forms by Cimatti', 'Dashboard', 'manage_options', "accua_forms", 'accua_forms_dashboard_page'); $form_edit_page = add_submenu_page('accua_forms', 'Forms', 'Forms', 'manage_options', "accua_forms_list", 'accua_forms_list_page'); add_action('admin_head-'.$form_edit_page, 'accua_forms_edit_page_head'); add_action( 'admin_print_styles-'.$form_edit_page, 'accua_forms_edit_page_head_styles'); add_action( 'admin_print_scripts-'.$form_edit_page, 'accua_forms_edit_page_head_scripts'); $form_add_page = add_submenu_page('accua_forms', __('Add new form', 'contact-forms'), __('Add new', 'contact-forms'), 'manage_options', "accua_forms_add", 'accua_forms_add_page'); add_action('admin_head-'.$form_add_page, 'accua_forms_edit_page_head'); add_action( 'admin_print_styles-'.$form_add_page, 'accua_forms_edit_page_head_styles'); add_action( 'admin_print_scripts-'.$form_add_page, 'accua_forms_edit_page_head_scripts'); $form_submissions_page = add_submenu_page('accua_forms', __('Forms submissions', 'contact-forms') , __('Submissions', 'contact-forms'), 'manage_options', "accua_forms_submissions_list", '__accua_forms_submissions_list_page'); add_action('load-'.$form_submissions_page, 'accua_forms_submissions_list_page_load'); add_action('admin_head-'.$form_submissions_page, 'accua_forms_submissions_list_page_head'); add_action( 'admin_print_styles-'.$form_submissions_page, 'accua_forms_edit_page_head_styles'); $form_fields_page = add_submenu_page('accua_forms', __( 'Form fields', 'contact-forms'), __('Fields', 'contact-forms'), 'manage_options', "accua_forms_fields", 'accua_forms_fields_page'); add_action( 'admin_print_styles-'.$form_fields_page, 'accua_forms_edit_page_head_styles'); add_action( 'admin_print_scripts-'.$form_fields_page, 'accua_forms_fields_page_enqueue_scripts'); $settings_page = add_submenu_page('accua_forms', __( 'Default Forms settings', 'contact-forms'), __('Settings', 'contact-forms'), 'manage_options', "accua_forms_settings", 'accua_forms_settings_page'); add_action( 'admin_print_styles-'.$settings_page, 'accua_forms_edit_page_head_styles'); add_action( 'admin_print_scripts-'.$settings_page, 'accua_forms_settings_page_head_scripts'); wp_enqueue_script('jquery-form'); wp_enqueue_script('jquery-color'); wp_enqueue_script('jquery-ui-core'); wp_enqueue_script('jquery-ui-sortable'); wp_enqueue_script('jquery-ui-draggable'); wp_enqueue_script('jquery-ui-droppable'); wp_enqueue_script('jquery-ui-resizable'); } function accua_forms_edit_page_head_styles() { wp_enqueue_style( 'accua-forms-admin', plugins_url('assets/css/admin.css', ACCUA_FORMS_FILE), array(), ACCUA_FORMS_CSS_VERSION); // } add_action( 'admin_enqueue_scripts', 'accua_forms_enqueue_deactivation_modal' ); function accua_forms_enqueue_deactivation_modal( $hook ) { if ( $hook !== 'plugins.php' ) { return; } wp_enqueue_script( 'accua-forms-deactivation-modal', plugins_url( 'assets/js/admin/deactivation-modal.js', ACCUA_FORMS_FILE ), array( 'jquery' ), ACCUA_FORMS_JS_VERSION, true ); wp_localize_script( 'accua-forms-deactivation-modal', 'accuaFormsDeactivation', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'accua_forms_deactivation_cleanup' ), 'pluginBasename' => plugin_basename( ACCUA_FORMS_FILE ), 'i18n' => array( 'title' => __( 'What would you like to do with your Contact Forms data?', 'contact-forms' ), 'description' => __( 'You are about to deactivate Contact Forms. Choose what to do with your existing data:', 'contact-forms' ), 'deleteAll' => __( 'Delete all data', 'contact-forms' ), 'deleteAllDesc' => __( 'Permanently remove all forms, submissions, settings, and uploaded files. This cannot be undone.', 'contact-forms' ), 'anonymizeAll' => __( 'Anonymize all submissions', 'contact-forms' ), 'anonymizeAllDesc'=> __( 'Replace personal data with placeholders and set IPs to 0.0.0.0. Forms and settings will be kept. This cannot be undone.', 'contact-forms' ), 'skip' => __( 'Just deactivate', 'contact-forms' ), 'skipDesc' => __( 'Keep all data. You can reactivate the plugin later.', 'contact-forms' ), 'confirmDelete' => __( 'Are you sure? This will permanently delete ALL forms, submissions, settings, and uploaded files. This cannot be undone.', 'contact-forms' ), 'confirmAnonymize'=> __( 'Are you sure? This will anonymize ALL submissions, replacing personal data with placeholders. This cannot be undone.', 'contact-forms' ), 'processing' => __( 'Processing…', 'contact-forms' ), 'cancel' => __( 'Cancel', 'contact-forms' ), ), ) ); } function accua_forms_form_fields_order() { if (!current_user_can('manage_options')){ wp_die( -1, 403 ); } check_ajax_referer('edit_form', '_nonce_edit_form'); $post = stripslashes_deep($_POST); if (empty($post['sidebars'])) { die('-1'); } // Save to draft instead of directly to database foreach ($post['sidebars'] as $sidebar_id => $order) { if (strpos($sidebar_id, 'cimatti-accua-fields-form-area-') !== 0){ die('-1'); } $fid = substr($sidebar_id, 31); // Get draft data for this form $draft_data = _accua_forms_get_draft_data($fid); if (empty($draft_data['fields'])) { die('-1'); } $old_fields = $draft_data['fields']; unset($draft_data['fields']); $new_fields = array(); $order = explode(',', $order); foreach ($order as $i) { $i = preg_replace('/^(new-)?widget-\\d+_/', '', $i); if (isset($old_fields[$i])) { $new_fields[$i] = $old_fields[$i]; unset($old_fields[$i]); } } if($old_fields){ $new_fields += $old_fields; } $draft_data['fields'] = $new_fields; // Save to draft (not to live database) _accua_forms_save_draft($fid, $draft_data); } die('1'); } add_action( 'wp_ajax_accua-save-form-field', 'accua_forms_save_form_field'); /* azione dove vengono salvati i campi dei un form - saves to draft */ function accua_forms_save_form_field() { if (!current_user_can('manage_options')){ wp_die( -1, 403 ); } check_ajax_referer('edit_form', '_nonce_edit_form'); $post = stripslashes_deep($_POST); $fid = isset($post['form-id']) ? $post['form-id'] : ''; if (accua_forms_validate_form_id($fid) !== '') { die('-1'); } // Get draft data instead of live data $draft_data = _accua_forms_get_draft_data($fid); if (empty($draft_data['fields'])) { $draft_data['fields'] = array(); } $avail_fields = get_option('accua_forms_avail_fields', array()); @ $wid = (string) $post['widget-id']; if (isset($avail_fields[$wid])) { $check_ref = $wid; } else { if (preg_match('/^(__html|__fieldset-begin|__fieldset-end)-\d+$/', $wid, $matches)) { $check_ref = $matches[1]; } else { die('-1'); } } if (empty($post['delete_widget'])) { @ $ref = $post['id_base']; if ($ref !== $check_ref) { die('-1'); } $required = !empty($post["form-field-{$wid}-required"]); $widget_number = empty($post['multi_number']) ? (empty($post['widget_number']) ? '' : (int)$post['widget_number']) : (int)$post['multi_number']; if (isset($draft_data['fields'][$wid])) { $old_istance_data = $draft_data['fields'][$wid]; } else { $old_istance_data = array(); } $draft_data['fields'][$wid] = array ( 'version' => 2, 'istance_id' => $wid, 'widget_number' => $widget_number, 'ref' => $ref, 'required' => $required, ); if (!empty($post["form-field-{$wid}-override-label"])) { @ $label = (string) $post["form-field-{$wid}-label"]; if (!current_user_can('unfiltered_html')) { $label = wp_kses($label, 'post'); } $draft_data['fields'][$wid]['label'] = $label; } $is_file = false; $is_date = false; if (isset($avail_fields[$wid]['type'])) { if ($avail_fields[$wid]['type'] == 'file') { $is_file = true; } elseif ($avail_fields[$wid]['type'] == 'date') { $is_date = true; } } if (!empty($post["form-field-{$wid}-override-default-value"])) { @ $default_value = (string) $post["form-field-{$wid}-default-value"]; if ($is_date) { $default_value = accua_forms_filter_date($default_value); } elseif (!current_user_can('unfiltered_html')) { //This is filtered in any case because field type can change $default_value = wp_kses($default_value, 'post'); } $draft_data['fields'][$wid]['default_value'] = $default_value; } if (!empty($post["form-field-{$wid}-override-allowed-values"])) { @ $allowed_values = (string) $post["form-field-{$wid}-allowed-values"]; if ($is_file){ $draft_data['fields'][$wid]['allowed_extensions'] = accua_forms_filter_extensions($allowed_values); } else { $draft_data['fields'][$wid]['allowed_values'] = $allowed_values; } } if (!empty($post["form-field-{$wid}-override-datemin-values"])) { @ $mindate_values = (string) $post["form-field-{$wid}-min-of-date"]; $draft_data['fields'][$wid]['min_date'] = accua_forms_filter_date($mindate_values); } if (!empty($post["form-field-{$wid}-override-datemax-values"])) { @ $maxdate_values = (string) $post["form-field-{$wid}-max-of-date"]; $draft_data['fields'][$wid]['max_date'] = accua_forms_filter_date($maxdate_values); } // Save post_type for post-select and post-multicheckbox fields if (isset($post["form-field-{$wid}-post-type"])) { @ $post_type_value = (string) $post["form-field-{$wid}-post-type"]; // Validate post type $valid_post_types = get_post_types(array('public' => true)); if (isset($valid_post_types[$post_type_value])) { $draft_data['fields'][$wid]['post_type'] = $post_type_value; } } // Save spam action for reCAPTCHA v2/v3 fields (silent classification). // Only written when the "override" box is ticked - otherwise the instance // follows the site-wide default from the settings page. if (!empty($post["form-field-{$wid}-override-spam-action"]) && isset($post["form-field-{$wid}-spam-action"])) { $spam_action_value = sanitize_text_field($post["form-field-{$wid}-spam-action"]); if (isset(accua_forms_captcha_spam_action_options()[$spam_action_value])) { $draft_data['fields'][$wid]['spam_action'] = $spam_action_value; } } // Save the minimum reCAPTCHA v3 score for captcha_v3 fields if (!empty($post["form-field-{$wid}-override-score-threshold"]) && isset($post["form-field-{$wid}-score-threshold"]) && is_numeric($post["form-field-{$wid}-score-threshold"])) { $draft_data['fields'][$wid]['score_threshold'] = accua_forms_recaptcha3_clamp_score($post["form-field-{$wid}-score-threshold"]); } // Save the hide-title flag for captcha fields. The override box decides // whether the value is stored at all: a missing key means the instance // follows the site-wide default resolved by // accua_forms_captcha_hide_title(). if (!empty($post["form-field-{$wid}-override-hide-title"]) && isset($avail_fields[$wid]['type']) && in_array($avail_fields[$wid]['type'], array('captcha', 'captcha_v3', 'cap'), true)) { $draft_data['fields'][$wid]['hide_title'] = empty($post["form-field-{$wid}-hide-title"]) ? 0 : 1; } // Save country_code for telephone fields (for libphonenumber validation) if (isset($post["form-field-{$wid}-country-code"])) { $country_code = strtoupper(sanitize_text_field($post["form-field-{$wid}-country-code"])); // Validate against the list of countries $valid_countries = accua_forms_get_countries(); if (isset($valid_countries[$country_code])) { $draft_data['fields'][$wid]['country_code'] = $country_code; } } /** * Filter field instance data before saving to draft. * * @param array $field_instance The field instance data being saved. * @param string $widget_id The field widget ID. * @param array $post_data The raw POST data (already stripslashed). * @param array $field_def The field definition from avail_fields. */ $draft_data['fields'][$wid] = apply_filters( 'accua_forms_save_field_data', $draft_data['fields'][$wid], $wid, $post, isset($avail_fields[$wid]) ? $avail_fields[$wid] : array() ); // Save custom CSS class for the field wrapper if (isset($post["form-field-{$wid}-css-class"])) { $css_class_raw = sanitize_text_field($post["form-field-{$wid}-css-class"]); if ($css_class_raw !== '') { // Sanitize each class individually $classes = array_filter(array_map('sanitize_html_class', explode(' ', $css_class_raw))); $draft_data['fields'][$wid]['css_class'] = implode(' ', $classes); } else { $draft_data['fields'][$wid]['css_class'] = ''; } } // Save custom CSS ID for the field wrapper if (isset($post["form-field-{$wid}-css-id"])) { $css_id_raw = sanitize_text_field($post["form-field-{$wid}-css-id"]); $draft_data['fields'][$wid]['css_id'] = sanitize_html_class($css_id_raw); } // Save fieldset style (fieldset-begin only) if (isset($post["form-field-{$wid}-fieldset-style"])) { $allowed_fieldset_styles = array( 'border-off-title-off', 'border-on-title-off', 'border-on-title-inline', 'border-on-title-outside', 'border-on-title-inside', 'border-off-title-on', ); $fs = sanitize_text_field($post["form-field-{$wid}-fieldset-style"]); if (in_array($fs, $allowed_fieldset_styles, true)) { $draft_data['fields'][$wid]['fieldset_style'] = $fs; } } // Save custom required message override if (!empty($post["form-field-{$wid}-override-required-msg"])) { $draft_data['fields'][$wid]['custom_required_message'] = sanitize_text_field($post["form-field-{$wid}-custom-required-msg"]); } // Save custom format message override (email/phone) if (!empty($post["form-field-{$wid}-override-format-msg"])) { $draft_data['fields'][$wid]['custom_format_message'] = sanitize_text_field($post["form-field-{$wid}-custom-format-msg"]); } } else { unset($draft_data['fields'][$wid]); } // Save to draft (not to live database) _accua_forms_save_draft($fid, $draft_data); die('1'); } function accua_forms_filter_text($text) { if ( is_object( $text ) || is_array( $text ) ) { return ''; } $text = (string) $text; $text = wp_check_invalid_utf8( $text ); $text = preg_replace( '/[ \t\n\r\0\x0B]+/', ' ', $text ); $text = trim( $text ); return $text; } function accua_forms_filter_email($email) { $parts = explode('<', $email); if (isset($parts[1])) { $email = accua_forms_filter_text(trim($parts[1], "<> \t\n\r\0\x0B")); $display_name = accua_forms_filter_text(trim($parts[0])); return "$display_name <$email>"; } else { return accua_forms_filter_text($email); } } function accua_forms_filter_extensions($extensions) { $cleaned_extensions = array(); $mimes = get_allowed_mime_types(); $extensions = explode("\n", $extensions); foreach ($extensions as $extension) { $extension = strtolower( trim( ltrim( trim( $extension ), '.' ) ) ); if ($extension !== '') { foreach ( $mimes as $ext_preg => $mime_match ) { $ext_preg = '!^' . $ext_preg . '$!i'; if ( preg_match( $ext_preg, $extension ) ) { $cleaned_extensions[] = $extension; break; } } } } return implode("\n", $cleaned_extensions); } function accua_forms_filter_settings($form_settings) { foreach($form_settings as $k => $v) { switch ($k) { case 'success_message_no_message': case 'error_message_no_message': case 'admin_emails_message_no_message': case 'confirmation_emails_message_no_message': case 'use_ajax': //boolean $form_settings[$k] = (bool) $v; break; case 'layout': // Only set if valid layout value, otherwise remove to use default if ($v === 'toplabel' || $v === 'inlinelabel' || $v === 'sidebyside') { $form_settings[$k] = $v; } else { unset($form_settings[$k]); // Reset to default } break; case 'emails_from': // single email $form_settings[$k] = accua_forms_filter_email($v); break; case 'admin_emails_to': case 'emails_bcc': //emails list $form_settings[$k] = accua_forms_filter_email($v); break; case 'success_message': case 'error_message': case 'admin_emails_message': case 'confirmation_emails_message': //HTML if (!current_user_can('unfiltered_html')) { $form_settings[$k] = wp_kses($v, 'post'); } break; //case 'title': //case 'emails_from_name': //case 'admin_emails_subject': //case 'confirmation_emails_subject': //case 'style_*': default: // text field $form_settings[$k] = accua_forms_filter_text($v); } } return $form_settings; } /** * AJAX handler to restore default message values. * * Restores the default content for a specific message section: * - success_message: On-screen success message * - error_message: On-screen error message * - admin_emails: Admin notification email (subject + message only) * - confirmation_emails: Confirmation email (subject + message only) * * @since 2.0.0-beta.6 */ add_action('wp_ajax_accua_forms_restore_default_message', 'accua_forms_restore_default_message'); function accua_forms_restore_default_message() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied.', 'contact-forms')), 403); } check_ajax_referer('accua_forms_restore_default', 'nonce'); $message_type = isset($_POST['message_type']) ? sanitize_key($_POST['message_type']) : ''; // Get default values $defaults = accua_forms_get_default_form_data(); // Define which fields to restore for each message type $restore_map = array( 'success_message' => array('success_message'), 'error_message' => array('error_message'), 'admin_emails' => array('admin_emails_subject', 'admin_emails_message'), 'confirmation_emails' => array('confirmation_emails_subject', 'confirmation_emails_message'), ); if (!isset($restore_map[$message_type])) { wp_send_json_error(array('message' => __('Invalid message type.', 'contact-forms')), 400); } // Get current form data $form_data = get_option('accua_forms_default_form_data', array()); if (!is_array($form_data)) { $form_data = array(); } // Restore the specified fields $restored_values = array(); foreach ($restore_map[$message_type] as $field) { $form_data[$field] = $defaults[$field]; $restored_values[$field] = $defaults[$field]; } // Save updated form data update_option('accua_forms_default_form_data', $form_data); wp_send_json_success(array( 'message' => __('Default values restored successfully.', 'contact-forms'), 'values' => $restored_values, )); } add_action( 'wp_ajax_accua-save-form-settings', 'accua_forms_save_form_settings'); function accua_forms_save_form_settings() { if (!current_user_can('manage_options')){ wp_die( -1, 403 ); } check_ajax_referer('edit_form', '_nonce_edit_form'); $post = stripslashes_deep($_POST); $fid = isset($post['form-id']) ? $post['form-id'] : ''; if (accua_forms_validate_form_id($fid) !== '') { die('-1'); } // Get draft data instead of live data $draft_data = _accua_forms_get_draft_data($fid); $settings = array( 'title', 'success_message', 'success_message_no_message', 'error_message', 'error_message_no_message', 'emails_from_name', 'emails_from', 'admin_emails_to', 'emails_bcc', 'admin_emails_subject', 'admin_emails_message', 'admin_emails_message_no_message', 'confirmation_emails_subject', 'confirmation_emails_message', 'confirmation_emails_message_no_message', 'gads_conversion_tracking_code', //'use_ajax', 'layout', 'style_margin', 'style_border_color', 'style_border_width', 'style_border_radius', 'style_background_color', 'style_padding', 'style_color', 'style_font_size', 'style_field_spacing', 'style_field_border_color', 'style_field_border_width', 'style_field_border_radius', 'style_field_background_color', 'style_field_padding', 'style_field_color', 'style_submit_border_color', 'style_submit_border_width', 'style_submit_border_radius', 'style_submit_background_color', 'style_submit_padding', 'style_submit_color', 'style_submit_font_size', 'submission_retention_value', 'submission_retention_unit', 'submission_retention_mode', ); // print_r($post); $new_form_settings = array(); foreach($settings as $i) { if (isset($post[$i])) { $new_form_settings[$i] = $post[$i]; } if (isset($draft_data[$i])) { unset($draft_data[$i]); } } $draft_data += accua_forms_filter_settings($new_form_settings); $draft_data['use_ajax'] = !empty($post['use_ajax']); $draft_data['submission_retention_override'] = !empty($post['submission_retention_override']); // Save to draft (not to live database) _accua_forms_save_draft($fid, $draft_data); // Return JSON response for AJAX handler wp_send_json_success($draft_data); } /** * AJAX handler to publish draft to live database. * Called when user clicks the Save button. */ add_action( 'wp_ajax_accua-publish-form-draft', 'accua_forms_publish_form_draft'); function accua_forms_publish_form_draft() { if (!current_user_can('manage_options')){ wp_die( -1, 403 ); } check_ajax_referer('edit_form', '_nonce_edit_form'); $post = stripslashes_deep($_POST); $fid = isset($post['form-id']) ? $post['form-id'] : ''; if (accua_forms_validate_form_id($fid) !== '') { wp_send_json_error(array('message' => __('Invalid form ID.', 'contact-forms')), 400); } // Publish the draft $result = _accua_forms_publish_draft($fid); if ($result) { wp_send_json_success(array('message' => __('Form saved successfully.', 'contact-forms'))); } else { // Draft might not exist (nothing to publish) - this is OK for a new form // Check if form exists in database $forms_data = get_option('accua_forms_saved_forms', array()); if (isset($forms_data[$fid])) { wp_send_json_success(array('message' => __('No changes to save.', 'contact-forms'))); } else { wp_send_json_error(array('message' => __('Failed to save form.', 'contact-forms')), 500); } } } /** * AJAX handler to discard draft and reload from published data. * Called when user clicks "Discard changes". */ add_action( 'wp_ajax_accua-discard-form-draft', 'accua_forms_discard_form_draft'); function accua_forms_discard_form_draft() { if (!current_user_can('manage_options')){ wp_die( -1, 403 ); } check_ajax_referer('edit_form', '_nonce_edit_form'); $post = stripslashes_deep($_POST); $fid = isset($post['form-id']) ? $post['form-id'] : ''; if (accua_forms_validate_form_id($fid) !== '') { wp_send_json_error(array('message' => __('Invalid form ID.', 'contact-forms')), 400); } // Delete the draft _accua_forms_delete_draft($fid); wp_send_json_success(array('message' => __('Changes discarded.', 'contact-forms'))); } /** * Filter an admin-configured post_status value for post fields down to the * statuses those fields may expose in a public dropdown: publish and private. * * Draft/pending/future content is never exposed, regardless of configuration. * * @since 2.2.27 * @param string|array $post_status Comma-separated string or array of statuses. * @return array Allowed statuses (may be empty). */ function accua_forms_filter_field_post_status($post_status) { if (!is_array($post_status)) { $post_status = explode(',', (string) $post_status); } $post_status = array_map('trim', $post_status); return array_values(array_intersect($post_status, array('publish', 'private'))); } /** * Check whether an extra_args string received from the AJAX endpoint matches a * post-select / post-multicheckbox configuration actually stored by an admin, * for the post type the request resolved to. * * The extra_args string is echoed into the form markup and sent back by the * browser, so it is client-controlled. Privileged parameters (post_status=private) * are only honored when the exact string exists in a saved field configuration - * otherwise any visitor could craft a request that enumerates private post titles. * The post type is part of the match: a query string saved for one post type must * not unlock private posts of a different type (the field's post type lives in a * separate setting, so the string alone does not identify what it exposes). * * @since 2.2.27 * @param string $extra_args Sanitized extra_args string from the request. * @param string $post_type Post type the request resolved to (after the * post_type override inside extra_args, if any). * @return bool True when a stored field configuration matches both. */ function accua_forms_extra_args_is_saved_config($extra_args, $post_type) { $extra_args = trim($extra_args); if ($extra_args === '') { return false; } // Candidate configurations: array of (allowed_values, configured post type). $candidates = array(); $avail_fields = get_option('accua_forms_avail_fields', array()); foreach ($avail_fields as $field) { if (!empty($field['type']) && ($field['type'] === 'post-select' || $field['type'] === 'post-multicheckbox') && isset($field['allowed_values'])) { $candidates[] = array($field['allowed_values'], isset($field['post_type']) ? $field['post_type'] : 'page'); } } $forms = get_option('accua_forms_saved_forms', array()); foreach ($forms as $form) { if (empty($form['fields']) || !is_array($form['fields'])) { continue; } foreach ($form['fields'] as $inst) { if (!is_array($inst) || empty($inst['ref']) || !isset($avail_fields[$inst['ref']]['type'])) { continue; } $type = $avail_fields[$inst['ref']]['type']; if (($type === 'post-select' || $type === 'post-multicheckbox') && isset($inst['allowed_values'])) { $inst_post_type = isset($inst['post_type']) ? $inst['post_type'] : (isset($avail_fields[$inst['ref']]['post_type']) ? $avail_fields[$inst['ref']]['post_type'] : 'page'); $candidates[] = array($inst['allowed_values'], $inst_post_type); } } } foreach ($candidates as $candidate) { list($candidate_args, $candidate_post_type) = $candidate; if (trim(sanitize_text_field($candidate_args)) !== $extra_args) { continue; } // Resolve the candidate's effective post type the same way the request // does: a post_type override inside the string wins over the field setting. $candidate_extra = array(); wp_parse_str($extra_args, $candidate_extra); if (!empty($candidate_extra['post_type'])) { $candidate_post_type = sanitize_text_field($candidate_extra['post_type']); } if ($candidate_post_type === $post_type) { return true; } } return false; } /** * AJAX handler to get posts for post-select fields with pagination. * Available to both logged-in and anonymous users (for frontend forms). * * @since 2.0.0-beta.29 */ add_action('wp_ajax_accua_forms_get_posts', 'accua_forms_ajax_get_posts'); add_action('wp_ajax_nopriv_accua_forms_get_posts', 'accua_forms_ajax_get_posts'); function accua_forms_ajax_get_posts() { // Verify nonce // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verification if (!isset($_REQUEST['_nonce']) || !wp_verify_nonce($_REQUEST['_nonce'], 'accua_forms_get_posts')) { wp_send_json_error(array('message' => __('Security check failed.', 'contact-forms')), 403); } // Sanitize inputs $post_type = isset($_REQUEST['post_type']) ? sanitize_text_field(wp_unslash($_REQUEST['post_type'])) : 'page'; $search = isset($_REQUEST['search']) ? sanitize_text_field(wp_unslash($_REQUEST['search'])) : ''; $page = isset($_REQUEST['page']) ? absint($_REQUEST['page']) : 1; $per_page = isset($_REQUEST['per_page']) ? min(absint($_REQUEST['per_page']), 100) : 50; $extra_args = isset($_REQUEST['extra_args']) ? sanitize_text_field(wp_unslash($_REQUEST['extra_args'])) : ''; $selected = isset($_REQUEST['selected']) ? sanitize_text_field(wp_unslash($_REQUEST['selected'])) : ''; // Validate post type $valid_post_types = get_post_types(array('public' => true)); if (!isset($valid_post_types[$post_type])) { $post_type = 'page'; } // Calculate offset $offset = ($page - 1) * $per_page; // Statuses the response may contain (extended below when the field // configuration explicitly requests private posts). $allowed_statuses = array('publish'); // Build query arguments $args = array( 'post_type' => $post_type, 'number' => $per_page + 1, // Get one extra to check if there are more 'offset' => $offset, 's' => $search, ); // Parse extra arguments (backward compatibility with allowed_values textarea) if (!empty($extra_args)) { // Parse the query string format $extra = array(); wp_parse_str($extra_args, $extra); // Allow post_type override from extra_args (backward compatibility) if (isset($extra['post_type'])) { $override_post_type = sanitize_text_field($extra['post_type']); // Validate the overridden post type if (isset($valid_post_types[$override_post_type])) { $post_type = $override_post_type; $args['post_type'] = $post_type; } } // Merge only safe parameters $safe_params = array('meta_key', 'meta_value', 'authors', 'parent', 'child_of', 'exclude', 'include', 'sort_column', 'sort_order'); foreach ($safe_params as $param) { if (isset($extra[$param])) { $args[$param] = $extra[$param]; } } // post_status is a privileged parameter: only publish/private are ever // honored, and 'private' only when the extra_args string matches a field // configuration stored by an admin (or the user can read private posts, // e.g. the form editor preview). Otherwise a visitor could craft a request // that enumerates private post titles. if (!empty($extra['post_status'])) { $requested_statuses = accua_forms_filter_field_post_status($extra['post_status']); if (in_array('private', $requested_statuses, true) && !current_user_can('read_private_posts') && !accua_forms_extra_args_is_saved_config($extra_args, $post_type)) { $requested_statuses = array('publish'); } if (!empty($requested_statuses)) { $args['post_status'] = $requested_statuses; $allowed_statuses = $requested_statuses; } } } // Get posts using WPML-compatible function $posts = accua_get_pages($args); // Check if there are more results $has_more = count($posts) > $per_page; if ($has_more) { array_pop($posts); // Remove the extra item } // Format results for the dropdown $results = array(); foreach ($posts as $post) { $results[] = array( 'id' => $post->ID, 'text' => $post->post_title, ); } // If this is the first page and we have a selected value, ensure it's in the list if ($page === 1 && !empty($selected) && is_numeric($selected)) { $selected_id = absint($selected); $found = false; foreach ($results as $result) { if ($result['id'] === $selected_id) { $found = true; break; } } // If selected post not in results, fetch it separately and prepend. // Only statuses the field is allowed to expose (publish, plus private when // explicitly configured): this endpoint is available to anonymous visitors, // so it must not disclose titles of other drafts/private/pending posts. if (!$found) { $selected_post = get_post($selected_id); if ($selected_post && $selected_post->post_type === $post_type && in_array($selected_post->post_status, $allowed_statuses, true)) { array_unshift($results, array( 'id' => $selected_post->ID, 'text' => $selected_post->post_title, )); } } } wp_send_json_success(array( 'results' => $results, 'more' => $has_more, 'page' => $page, )); } /** * Get available public post types for the post-select field editor. * * @since 2.0.0-beta.29 * @return array Array of post type slug => label pairs. */ function accua_forms_get_public_post_types() { $post_types = get_post_types(array('public' => true), 'objects'); $options = array(); foreach ($post_types as $post_type) { // Skip attachments if ($post_type->name === 'attachment') { continue; } $options[$post_type->name] = $post_type->labels->singular_name; } return $options; } function accua_forms_field_settings_form_counter() { static $i = 0; $i++; return $i; } /* impostazioni dei campi */ function accua_forms_field_text_settings_form($fid, $field_data=array(), $istance_data=array()){ static $html_multi_number = 0; $i = accua_forms_field_settings_form_counter(); $hidden = ''; if (!is_array($istance_data)) { if ($istance_data === 'hidden') { $hidden = 'style="display:none"'; } $istance_data = array(); $empty_istance = true; } else { $empty_istance = empty($istance_data); } if (!is_array($field_data)){ $field_data = array(); } $field_data += array( 'version' => '1', 'id' => '__html', 'name' => __( 'Custom HTML content', 'contact-forms'), 'type' => 'html', 'description' => __('Use this special field to inject raw HTML in the form. You can use this multiple times.', 'contact-forms'), 'default_value' => '', 'allowed_values' => '', 'allowed_extensions' => '', ); $override_label = isset($istance_data['label']) ? 'checked="checked"' : ''; $override_default_value = isset($istance_data['default_value']) ? 'checked="checked"' : ''; $override_allowed_values = isset($istance_data['allowed_values']) ? 'checked="checked"' : ''; $override_allowed_extensions = ''; $override_custom_required_msg = isset($istance_data['custom_required_message']) ? 'checked="checked"' : ''; $override_custom_format_msg = isset($istance_data['custom_format_message']) ? 'checked="checked"' : ''; if ($field_data['type'] == 'file') { if (isset($istance_data['version']) && $istance_data['version'] >= 2) { if (isset($istance_data['allowed_extensions'])) { $override_allowed_extensions = 'checked="checked"'; } } else { if (isset($istance_data['allowed_values'])) { $istance_data['allowed_extensions'] = $istance_data['allowed_values']; $override_allowed_extensions = 'checked="checked"'; } } if ($field_data['version'] < 2) { $field_data['allowed_extensions'] = $field_data['allowed_values']; } if ($field_data['allowed_extensions'] === '') { $file_data = get_option('accua_forms_default_file_field_data',array()); if (isset($file_data['valid_extensions'])){ $field_data['allowed_extensions'] = $file_data['valid_extensions']; } } } if ($field_data['type'] == 'date') { $override_mindate_values = isset($istance_data['min_date']) ? 'checked="checked"' : ''; $override_maxdate_values = isset($istance_data['max_date']) ? 'checked="checked"' : ''; $istance_data += array( 'min_date' => $field_data['min_date'], 'max_date' => $field_data['max_date'], 'default_value' => $field_data['default_date_value'], ); } $istance_data += array( //'version' => 1, 'istance_id' => $field_data['id'], 'widget_number' => '', 'ref' => $field_data['id'], 'label' => $field_data['name'], 'default_value' => $field_data['default_value'], 'allowed_values' => $field_data['allowed_values'], 'allowed_extensions' => $field_data['allowed_extensions'], 'required' => false, 'post_type' => 'page', // Default post type for post-select fields 'css_class' => '', 'css_id' => '', 'custom_required_message' => '', 'custom_format_message' => '', 'fieldset_style' => 'border-off-title-off', ); foreach ($istance_data as $key => $value) { $istance_data[$key] = esc_attr($istance_data[$key]); } foreach ($field_data as $key => $value) { $field_data[$key] = esc_attr($field_data[$key]); } $multi_number = ''; $add_new = ''; if (in_array($istance_data['ref'], array('__html','__fieldset-begin','__fieldset-end'))) { $forceoverride_field = true; if ($empty_istance) { $add_new = 'multi'; $istance_data['istance_id'] .= '-__i__'; $istance_data['widget_number'] = 1; $multi_number = 1 + $html_multi_number; } else { if ($html_multi_number < $istance_data['widget_number']) { $html_multi_number = $istance_data['widget_number']; } } } else { $forceoverride_field = false; $add_new = $empty_istance ? 'single' : ''; } $fid = esc_attr($fid); $testi_eot = array ( 'label' => __( 'Label', 'contact-forms'), 'override' => __( 'override', 'contact-forms'), 'default_value' => __( 'Default value', 'contact-forms'), 'default_values' => __( 'Default value(s)', 'contact-forms'), 'desc_def' => __( 'For multiple default values, use | as separator.', 'contact-forms'), 'allowed_values' => __( 'Allowed values', 'contact-forms'), 'desc_all' => __( 'The possible values this field can contain. Enter one value per line, in the format key|label. The key is the value that will be stored in the database. The label is optional, and the key will be used as the label if no label is specified.', 'contact-forms'), 'allowed_extensions' => __( 'Allowed extensions', 'contact-forms'), 'desc_all_ext' => __( 'Accepted file extensions. One per line, without dots.', 'contact-forms'), 'required' => __( 'Required', 'contact-forms'), 'custom_HTML_content' => __( 'Custom HTML content', 'contact-forms'), 'refresh_preview' => __( 'Refresh Preview', 'contact-forms'), 'add' => __( 'Add field', 'contact-forms'), 'remove' => __( 'Remove', 'contact-forms'), 'close' => __( 'Close', 'contact-forms'), 'save' => __( 'Save', 'contact-forms'), 'min-of-date' => __( 'Min date', 'contact-forms'), 'max-of-date' => __( 'Max date', 'contact-forms') ); if ($forceoverride_field) { $override_begin = ''; $override_type = 'hidden'; $override_end = ''; } else { $override_begin = "({$testi_eot['override']}: "; $override_type = 'checkbox'; $override_end = ')'; } // phpcs:disable PluginCheck.CodeAnalysis.Heredoc.NotAllowed, WordPress.Security.EscapeOutput.HeredocOutputNotEscaped -- Heredoc used for HTML templates with pre-escaped variables $content = << {$override_begin}{$override_end}

EOT; $default_value = << {$override_begin}{$override_end}

EOT; $default_values = << {$override_begin}{$override_end}

EOT; $allowed_values = << {$override_begin}{$override_end}

{$testi_eot['desc_all']}

EOT; $allowed_ext = << {$override_begin}{$override_end}

{$testi_eot['desc_all_ext']}

EOT; $required_checked = empty($istance_data['required']) ? '' : 'checked="checked"'; $required = <<

EOT; // Custom required message override (checkbox + text input, same pattern as custom label) $custom_required_msg_label = __( 'Custom required message', 'contact-forms'); // translators: %s is the field name/label $custom_required_msg_desc = __( 'Overrides the default "required" error message. Use %s for the field name.', 'contact-forms'); $custom_required_msg = << {$override_begin}{$override_end}

{$custom_required_msg_desc}

EOT; // Custom format message override for email and telephone fields $custom_format_msg = ''; if ($field_data['type'] === 'email' || $field_data['type'] === 'autoreply_email') { $custom_format_msg_label = __( 'Custom invalid email message', 'contact-forms'); // translators: %s is the field name/label $custom_format_msg_desc = __( 'Overrides the default email format error message. Use %s for the field name.', 'contact-forms'); $custom_format_msg = << {$override_begin}{$override_end}

{$custom_format_msg_desc}

EOT; } elseif ($field_data['type'] === 'telephone') { $custom_format_msg_label = __( 'Custom invalid phone message', 'contact-forms'); // translators: %s is the field name/label $custom_format_msg_desc = __( 'Overrides the default phone format error message. Use %s for the field name.', 'contact-forms'); $custom_format_msg = << {$override_begin}{$override_end}

{$custom_format_msg_desc}

EOT; } if ($field_data['type'] == 'date'){ $default_date_value = << {$override_begin}{$override_end}

EOT; $min_date = << {$override_begin}{$override_end}

EOT; $max_date = << {$override_begin}{$override_end}

EOT; } // Post type selector for post-select and post-multicheckbox fields $post_type_selector = ''; if ($field_data['type'] === 'post-select' || $field_data['type'] === 'post-multicheckbox') { $override_post_type = isset($istance_data['post_type']) && $istance_data['post_type'] !== 'page' ? 'checked="checked"' : ''; $post_types = accua_forms_get_public_post_types(); $post_type_options = ''; $current_post_type = esc_attr($istance_data['post_type']); foreach ($post_types as $pt_slug => $pt_label) { $selected = ($pt_slug === $current_post_type) ? ' selected="selected"' : ''; $post_type_options .= ''; } $post_type_label = __('Post type', 'contact-forms'); $post_type_desc = __('Select which post type to show in the dropdown.', 'contact-forms'); $query_params_label = __('Additional query parameters', 'contact-forms'); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- This is example help text, not actual code. $query_params_desc = __('Optional: Filter posts using query parameters (e.g., authors=admin or meta_key=featured&meta_value=1). Add post_status=publish,private to also include private posts (their titles become visible to all visitors of this form). Leave empty for all published posts of the selected type.', 'contact-forms'); $post_type_selector = << {$override_begin}{$override_end}

{$post_type_desc}

{$override_begin}{$override_end}

{$query_params_desc}

EOT; } // Country selector for telephone fields (for libphonenumber validation) $country_selector = ''; if ($field_data['type'] === 'telephone') { $countries = accua_forms_get_countries(); $current_country = isset($istance_data['country_code']) ? esc_attr($istance_data['country_code']) : 'IT'; $override_country = isset($istance_data['country_code']) && $istance_data['country_code'] !== 'IT' ? 'checked="checked"' : ''; $country_options = ''; foreach ($countries as $code => $country_name) { $selected = ($code === $current_country) ? ' selected="selected"' : ''; $country_options .= ''; } $country_label = __('Default country', 'contact-forms'); // translators: Help text for phone field country selector in form editor $country_desc = __('For numbers without international prefix, validation assumes this country.', 'contact-forms'); $country_selector = << {$override_begin}{$override_end}

{$country_desc}

EOT; } // Spam action selector for reCAPTCHA v2 and v3 fields (silent classification). // Like every other field setting it is an override of the site-wide default // configured in the plugin settings page. $spam_action_selector = ''; if ($field_data['type'] === 'captcha' || $field_data['type'] === 'captcha_v3') { $spam_actions = accua_forms_captcha_spam_action_options(); $current_spam_action = accua_forms_captcha_spam_action($istance_data, $field_data['type']); $override_spam_action = isset($istance_data['spam_action']) ? 'checked="checked"' : ''; $spam_action_options = ''; foreach ($spam_actions as $sa_key => $sa_label) { $sa_selected = ($sa_key === $current_spam_action) ? ' selected="selected"' : ''; $spam_action_options .= ''; } $spam_action_label = __('When the spam check fails', 'contact-forms'); if ($field_data['type'] === 'captcha_v3') { // translators: Help text for the reCAPTCHA v3 spam action selector in the form editor $spam_action_desc = __('reCAPTCHA v3 classifies visitors silently with a score. Choose what happens to a submission that fails the check: accept it silently (normal success message, no notification emails) and mark it with the Spam lead status, move it to Trash, or delete it immediately without storing anything, or reject it with a visible error.', 'contact-forms'); } else { // translators: Help text for the reCAPTCHA v2 spam action selector in the form editor $spam_action_desc = __('Choose what happens to a submission whose reCAPTCHA verification fails: accept it silently (normal success message, no notification emails) and mark it with the Spam lead status, move it to Trash, or delete it immediately without storing anything, or reject it with a visible error so the visitor can retry the challenge.', 'contact-forms'); } $spam_action_selector = << {$override_begin}{$override_end}

{$spam_action_desc}

EOT; } // Minimum score override for reCAPTCHA v3 fields $score_threshold_field = ''; if ($field_data['type'] === 'captcha_v3') { $current_score_threshold = esc_attr(number_format(accua_forms_recaptcha3_score_threshold($istance_data), 2, '.', '')); $override_score_threshold = isset($istance_data['score_threshold']) ? 'checked="checked"' : ''; $score_threshold_label = __('Minimum score', 'contact-forms'); // translators: Help text for the reCAPTCHA v3 minimum score field in the form editor $score_threshold_desc = __('reCAPTCHA v3 scores every visitor from 0.0 (almost certainly a bot) to 1.0 (almost certainly a person). Submissions scoring below this value fail the spam check. Google suggests 0.5; raise it to be stricter, lower it if legitimate visitors are being caught.', 'contact-forms'); $score_threshold_field = << {$override_begin}{$override_end}

{$score_threshold_desc}

EOT; } // Hide-title override for captcha fields $hide_title_field = ''; if (in_array($field_data['type'], array('captcha', 'captcha_v3', 'cap'), true)) { $hide_title_checked = accua_forms_captcha_hide_title($istance_data) ? 'checked="checked"' : ''; $override_hide_title = isset($istance_data['hide_title']) ? 'checked="checked"' : ''; $hide_title_label = __('Hide field title', 'contact-forms'); // translators: Help text for the hide-title checkbox on captcha fields in the form editor $hide_title_desc = __('Do not display the field title in the form. The title stays in the markup for screen readers and error messages.', 'contact-forms'); $hide_title_field = << {$override_begin}{$override_end}

{$hide_title_desc}

EOT; } // CSS Class and CSS ID fields (universal, apply to all field types) $css_class_label = __( 'CSS Class', 'contact-forms'); $css_id_label = __( 'CSS ID', 'contact-forms'); // translators: Help text for CSS Class field in form editor $css_class_desc = __( 'Custom CSS class(es) for the field wrapper. Separate multiple classes with spaces.', 'contact-forms'); // translators: Help text for CSS ID field in form editor $css_id_desc = __( 'Custom CSS ID for the field wrapper. Must be unique on the page.', 'contact-forms'); $css_class_field = <<

{$css_class_desc}

EOT; $css_id_field = <<

{$css_id_desc}

EOT; switch ($field_data['type']) { case 'textarea': $content .= << {$override_begin}{$override_end}

$required $custom_required_msg EOT; break; case 'hidden': $content = $default_value; break; case 'checkbox': $content .= $default_value . $required . $custom_required_msg; break; case 'select': case 'radio': $content .= $default_value . $allowed_values . $required . $custom_required_msg; break; case 'post-select': $content .= $default_value . $post_type_selector . $required . $custom_required_msg; break; case 'multiselect': case 'multicheckbox': $content .= $default_values . $allowed_values . $required . $custom_required_msg; break; case 'post-multicheckbox': $content .= $default_values . $post_type_selector . $required . $custom_required_msg; break; case 'file': $content .= $allowed_ext . $required . $custom_required_msg; break; case 'submit': //just the label break; case 'fieldset-begin': $fs_label_text = __('Border and Title', 'contact-forms'); $gt_label_text = __('Group Title', 'contact-forms'); // translators: Help text under the Group Title field for fieldset groups in the form editor $gt_desc_text = __('Section heading. Shown in the form when a title option is selected.', 'contact-forms'); $fs_opts_map = array( 'border-off-title-off' => __('Border OFF | Title OFF', 'contact-forms'), 'border-on-title-off' => __('Border ON | Title OFF', 'contact-forms'), 'border-on-title-inline' => __('Border ON | Title ON (inline)', 'contact-forms'), 'border-on-title-outside' => __('Border ON | Title ON (outside)', 'contact-forms'), 'border-on-title-inside' => __('Border ON | Title ON (inside)', 'contact-forms'), 'border-off-title-on' => __('Border OFF | Title ON', 'contact-forms'), ); $fs_options_html = ''; foreach ($fs_opts_map as $opt_val => $opt_label) { $opt_selected = ($istance_data['fieldset_style'] === $opt_val) ? ' selected="selected"' : ''; $fs_options_html .= ''; } $content = <<

{$gt_desc_text}


EOT; $content .= $css_class_field . $css_id_field; break; case 'fieldset-end': //Nothing! $content = ''; break; case 'html': $content = << {$override_begin}{$override_end}

{$testi_eot['refresh_preview']}

EOT; break; case 'date': $content .= $default_date_value . $min_date . $max_date . $required . $custom_required_msg; break; case 'telephone': $content .= $default_value . $country_selector . $required . $custom_required_msg . $custom_format_msg; break; case 'email': case 'autoreply_email': $content .= $default_value . $required . $custom_required_msg . $custom_format_msg; break; case 'captcha': case 'captcha_v3': case 'cap': case 'turnstile': // Captcha fields consume neither default_value nor the custom required // message (their validators are set in the element constructors), so // those rows are not offered. The extension-settings action still fires // for parity with the default case. ob_start(); do_action( 'accua_forms_field_settings', $field_data['type'], $field_data, $istance_data ); $extra_settings = ob_get_clean(); $content .= $extra_settings . $hide_title_field . $score_threshold_field . $spam_action_selector . $required; break; case 'textfield': case 'colorpicker': case 'datepicker': case 'dateselect': default: /** * Action to render additional field settings in the form editor. * * @param string $field_type The field type identifier. * @param array $field_data The field definition. * @param array $istance_data The field instance data. * @param string $content The current settings HTML (passed by reference via output buffering). */ ob_start(); do_action( 'accua_forms_field_settings', $field_data['type'], $field_data, $istance_data ); $extra_settings = ob_get_clean(); // The captcha-only rows (hide title, score, spam action) render in the // dedicated captcha case above and are always empty here. $content .= $default_value . $extra_settings . $required . $custom_required_msg; break; } // Append CSS Class and CSS ID fields to all types except fieldset-end (which has no settings) if ($field_data['type'] !== 'fieldset-end' && $field_data['type'] !== 'fieldset-begin') { $content .= $css_class_field . $css_id_field; } $adminurl = admin_url(); return <<

{$field_data['name']}

EOT; // phpcs:enable PluginCheck.CodeAnalysis.Heredoc.NotAllowed, WordPress.Security.EscapeOutput.HeredocOutputNotEscaped } // _accua_forms_get_abs_dest_path() now lives in includes/data-deletion.php, which // uninstall.php requires on its own - the deletion needs the same path this file // uploads to, and uninstall runs with the plugin unloaded. // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function, underscore prefix indicates private function _accua_forms_get_form_data($fid = false, $return_empty = true, $restore_trash = false){ $empty_form_data = array( 'fields' => array(), 'title' => '', 'success_message' => '', 'error_message' => '', 'emails_from_name' => '', 'emails_from' => '', 'admin_emails_to' => '', 'emails_bcc' => '', 'admin_emails_subject' => '', 'admin_emails_message' => '', 'confirmation_emails_subject' => '', 'confirmation_emails_message' => '', 'use_ajax' => true, 'gads_conversion_tracking_code' => '', 'layout' => 'sidebyside', 'style_margin' => '', 'style_border_color' => '', 'style_border_width' => '', 'style_border_radius' => '', 'style_background_color' => '', 'style_padding' => '', 'style_color' => '', 'style_font_size' => '', 'style_field_spacing' => '', 'style_field_border_color' => '', 'style_field_border_width' => '', 'style_field_border_radius' => '', 'style_field_background_color' => '', 'style_field_padding' => '', 'style_field_color' => '', 'style_submit_border_color' => '', 'style_submit_border_width' => '', 'style_submit_border_radius' => '', 'style_submit_background_color' => '', 'style_submit_padding' => '', 'style_submit_color' => '', 'style_submit_font_size' => '', 'submission_retention_override' => false, 'submission_retention_value' => 0, 'submission_retention_unit' => 'months', 'submission_retention_mode' => 'anonymize', ); if ($fid === false) { return $empty_form_data; } $default_form_data = get_option('accua_forms_default_form_data',array()); if ($fid === null) { return $default_form_data + $empty_form_data; } $forms_data = get_option('accua_forms_saved_forms', array()); if ($restore_trash) { $trash_data = get_option('accua_forms_trash_forms', array()); if (isset($trash_data[$fid])) { $forms_data[$fid] = $trash_data[$fid]; update_option('accua_forms_saved_forms', $forms_data); unset($trash_data[$fid]); update_option('accua_forms_trash_forms', $trash_data); } } if (isset($forms_data[$fid])) { $form_data = array( '_overrided' => $forms_data[$fid] ) + $forms_data[$fid] + $default_form_data + $empty_form_data; return $form_data; } elseif ($return_empty) { return array( '_overrided' => array() ) + $default_form_data + $empty_form_data; } else { return null; } } /** * Draft System Functions * * The draft system allows users to make changes to forms in the admin editor * without immediately affecting the live/published form. Changes are stored in * a transient until the user clicks Save, which publishes the draft. * * Pattern follows WordPress auto-draft system. */ /** * Get the transient key for a form's draft data. * * @param string|int $fid Form ID. * @return string Transient key. */ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function function _accua_forms_get_draft_key( $fid ) { return 'accua_forms_draft_' . $fid; } /** * Initialize or get existing draft for a form. * Called when the form editor is loaded. * * If a draft exists, returns it. * If no draft exists, creates one from published data. * * @param string|int $fid Form ID. * @return array Draft data array. */ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function function _accua_forms_init_draft( $fid ) { $draft_key = _accua_forms_get_draft_key( $fid ); // Check for existing draft $draft_data = get_transient( $draft_key ); if ( $draft_data !== false ) { // Draft exists - return it return $draft_data; } // No draft - create from published data $forms_data = get_option( 'accua_forms_saved_forms', array() ); if ( isset( $forms_data[ $fid ] ) ) { $draft_data = $forms_data[ $fid ]; } else { // New form - initialize empty structure $draft_data = array( 'fields' => array() ); } // Store as draft with 24 hour expiry set_transient( $draft_key, $draft_data, DAY_IN_SECONDS ); return $draft_data; } /** * Get draft data for a form (creating if necessary). * Used by AJAX handlers to read current draft state. * * @param string|int $fid Form ID. * @return array Draft data array. */ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function function _accua_forms_get_draft_data( $fid ) { $draft_key = _accua_forms_get_draft_key( $fid ); $draft_data = get_transient( $draft_key ); if ( $draft_data === false ) { // Initialize draft from published data $draft_data = _accua_forms_init_draft( $fid ); } return $draft_data; } /** * Save data to draft transient. * Called by AJAX handlers when fields are edited. * * @param string|int $fid Form ID. * @param array $draft_data Complete draft data to save. * @return bool True on success. */ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function function _accua_forms_save_draft( $fid, $draft_data ) { $draft_key = _accua_forms_get_draft_key( $fid ); return set_transient( $draft_key, $draft_data, DAY_IN_SECONDS ); } /** * Publish draft to live data. * Called when user clicks Save button. * * @param string|int $fid Form ID. * @return bool True on success. */ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function function _accua_forms_publish_draft( $fid ) { $draft_key = _accua_forms_get_draft_key( $fid ); $draft_data = get_transient( $draft_key ); if ( $draft_data === false ) { // No draft to publish - shouldn't happen normally return false; } // Get current published data $forms_data = get_option( 'accua_forms_saved_forms', array() ); // Update with draft $forms_data[ $fid ] = $draft_data; // Save to database $result = update_option( 'accua_forms_saved_forms', $forms_data ); if ( $result ) { // Clear draft after successful publish delete_transient( $draft_key ); } return $result; } /** * Delete draft for a form. * Called when discarding changes or after successful publish. * * @param string|int $fid Form ID. * @return bool True on success. */ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function function _accua_forms_delete_draft( $fid ) { $draft_key = _accua_forms_get_draft_key( $fid ); return delete_transient( $draft_key ); } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function, underscore prefix indicates private function _accua_forms_style_parameters($params) { $ret = ''; foreach ($params as $key => $value) { $value = trim($value); if ($value !== '') { if (is_numeric($value)) { $ret .= "{$key}:{$value}px;"; } else { $ret .= "{$key}:{$value};"; } if ($key == 'border-width') { $ret .= "border-style:solid;"; } } } return $ret; } /* * generazione del form (e della preview) * */ add_action('accua_form_alter', 'accua_forms_form_generate', -999, 2); function accua_forms_form_generate($baseid, $form) { if (substr($baseid, 0, 14) == '__accua-form__') { $fid = substr($baseid,14); // Check if we're in admin preview mode - if so, read from draft $use_draft = apply_filters('accua_forms_use_draft_for_preview', false); if ($use_draft) { // Get draft data and merge with defaults $draft_data = _accua_forms_get_draft_data($fid); $default_form_data = get_option('accua_forms_default_form_data', array()); $empty_form_data = _accua_forms_get_form_data(false); // Get empty structure $form_data = array( '_overrided' => $draft_data ) + $draft_data + $default_form_data + $empty_form_data; } else { // Frontend: read from published data $form_data = _accua_forms_get_form_data($fid, false); } /* echo '"; */ if ($form_data) { // Check for preview order override (allows live preview of field reorder before save) $preview_order_override = apply_filters('accua_forms_preview_order_override', null); if ($preview_order_override && !empty($form_data['fields'])) { // Find the sidebar key for this form (format: cimatti-accua-fields-form-area-{fid}) $sidebar_key = 'cimatti-accua-fields-form-area-' . $fid; if (isset($preview_order_override[$sidebar_key])) { $order_string = $preview_order_override[$sidebar_key]; $order_array = explode(',', $order_string); // Reorder fields according to preview order $old_fields = $form_data['fields']; $new_fields = array();; foreach ($order_array as $widget_id) { // Extract instance ID from widget ID (format: widget-{type}_{instance_id}) $instance_id = preg_replace('/^(new-)?widget-\\d+_/', '', $widget_id); if (isset($old_fields[$instance_id])) { $new_fields[$instance_id] = $old_fields[$instance_id]; unset($old_fields[$instance_id]); } } // Append any remaining fields not in order if ($old_fields) { $new_fields += $old_fields; } $form_data['fields'] = $new_fields; } } $form_style = _accua_forms_style_parameters(array( 'margin' => $form_data['style_margin'], 'border-color' => $form_data['style_border_color'], 'border-width' => $form_data['style_border_width'], 'border-radius' => $form_data['style_border_radius'], 'background-color' => $form_data['style_background_color'], 'padding' => $form_data['style_padding'], 'color' => $form_data['style_color'], 'font-size' => $form_data['style_font_size'], )); $field_style = _accua_forms_style_parameters(array( 'margin-bottom' => $form_data['style_field_spacing'], 'border-color' => $form_data['style_field_border_color'], 'border-width' => $form_data['style_field_border_width'], 'border-radius' => $form_data['style_field_border_radius'], 'background-color' => $form_data['style_field_background_color'], 'padding' => $form_data['style_field_padding'], 'color' => (trim($form_data['style_field_color']) === '')?$form_data['style_color']:$form_data['style_field_color'], 'font-size' => $form_data['style_font_size'], )); $field_properties = array(); if ($field_style !== '') { $field_properties['style'] = $field_style; } // These will be set per-field in the loop below, initialized empty here $field_properties['wrapperCssClass'] = ''; $field_properties['wrapperCssId'] = ''; $submit_style = _accua_forms_style_parameters(array( 'border-color' => $form_data['style_submit_border_color'], 'border-width' => $form_data['style_submit_border_width'], 'border-radius' => $form_data['style_submit_border_radius'], 'background-color' => $form_data['style_submit_background_color'], 'padding' => $form_data['style_submit_padding'], 'color' => $form_data['style_submit_color'], 'font-size' => $form_data['style_submit_font_size'], )); $submit_properties = array(); if ($submit_style !== '') { $submit_properties['style'] = $submit_style; } if ($form_style !== '') { $form->configure(array('style' => $form_style)); } if (!empty($form_data['use_ajax'])){ $form->configure(array( "accua_ajax" => 1, )); } $add_submit = true; $fieldset_open = false; $avail_fields = get_option('accua_forms_avail_fields', array()); foreach ($form_data['fields'] as $istance_data) { if (empty($avail_fields[$istance_data['ref']])) { $field_data = array(); if (!empty($istance_data['ref'])) { if ($istance_data['ref'] == '__fieldset-begin') { $field_data = array( 'id' => '__fieldset-begin', 'name' => __('Fieldset begin', 'contact-forms'), 'type' => 'fieldset-begin', 'description' => '', ); } elseif ($istance_data['ref'] == '__fieldset-end') { $field_data = array( 'id' => '__fieldset-end', 'name' => __('Fieldset end', 'contact-forms'), 'type' => 'fieldset-end', 'description' => '', ); } } } else { $field_data = $avail_fields[$istance_data['ref']]; } $field_data += array( 'version' => 1, 'id' => '__html', 'name' => __( 'Custom HTML content', 'contact-forms'), 'type' => 'html', 'description' => __('Use this special field to inject raw HTML in the form. You can use this multiple times.', 'contact-forms'), 'default_value' => '', 'allowed_values' => '', 'allowed_extensions' => '', 'custom_required_message' => '', 'custom_format_message' => '', ); $istance_data += array( 'version' => 1, ); if ($field_data['type'] == 'file') { if ($field_data['version'] < 2) { $field_data['allowed_extensions'] = $field_data['allowed_values']; } if ($istance_data['version'] < 2 && isset($istance_data['allowed_values'])) { $istance_data['allowed_extensions'] = $istance_data['allowed_values']; } } /* prendo i dati relativi ai campi di tipo date lo faccio prima, così default value in questo modo viene sovrascritto se un campo è di un tipo diverso NB: se i campi sono già sovrascritti, si trovano già in istance_data */ if ($field_data['type'] == 'date'){ $istance_data += array( 'default_value' => $field_data['default_date_value'], 'min_date' => $field_data['min_date'], 'max_date' => $field_data['max_date'], ); } $istance_data += array( 'istance_id' => $field_data['id'], 'widget_number' => '', 'ref' => $field_data['id'], 'label' => $field_data['name'], 'default_value' => $field_data['default_value'], /* viene impostato il valore di defualt se non è un campo data */ 'allowed_values' => $field_data['allowed_values'], 'allowed_extensions' => $field_data['allowed_extensions'], 'post_type' => 'page', // Default for post-select fields 'css_class' => '', 'css_id' => '', 'custom_required_message' => '', 'custom_format_message' => '', ); $element = NULL; $element_conf = NULL; $allowed_val = trim($istance_data['allowed_values']); // For post-select fields, we use lazy loading via AJAX, so don't pre-load posts here if ($field_data['type'] == 'post-multicheckbox') { // Post-multicheckbox still needs pre-loaded options for checkbox rendering $post_type = isset($istance_data['post_type']) ? $istance_data['post_type'] : 'page'; $query_args = array(); if (!empty($allowed_val)) { wp_parse_str($allowed_val, $query_args); } // Let a post_type in the query parameters override the field's // post-type setting, matching post-select. The value comes from the // saved field configuration (not a client request) and is validated // against public post types. if (!empty($query_args['post_type'])) { $mc_public_types = get_post_types(array('public' => true)); $mc_override = sanitize_text_field($query_args['post_type']); if (isset($mc_public_types[$mc_override])) { $post_type = $mc_override; } } $query_args['post_type'] = $post_type; // Only publish/private may be exposed, even if the admin configured other statuses. if (!empty($query_args['post_status'])) { $mc_statuses = accua_forms_filter_field_post_status($query_args['post_status']); if (!empty($mc_statuses)) { $query_args['post_status'] = $mc_statuses; } else { unset($query_args['post_status']); } } $posts = accua_get_pages($query_args); $allowed_values = array(); foreach ($posts as $p) { $allowed_values[$p->ID] = $p->post_title; } } elseif ($field_data['type'] == 'post-select') { // Post-select uses lazy loading - just set empty options, JS will fetch $allowed_values = array(); } else { if ($field_data['type'] == 'file') { $filedata = get_option('accua_forms_default_file_field_data',array()); $filedata += array( 'valid_extensions' => '', 'max_filesize' => '', 'dest_path' => '', ); $allowed_val = accua_forms_filter_extensions($istance_data['allowed_extensions']); if ($allowed_val == '') { $allowed_val = accua_forms_filter_extensions($filedata['valid_extensions']); } } $allowed_val = explode("\n",$allowed_val); $allowed_values = array(); foreach ($allowed_val as $val) { $val = explode('|',$val,2); $val[0] = trim($val[0]); /* if (empty($val[0])) { continue; } */ if ((!isset($val[1])) || (trim($val[1])==='')) { if ($val[0] === '') { continue; } else { $val[1] = $val[0]; } } $allowed_values[$val[0]] = $val[1]; } } // Set per-field wrapper CSS class and ID $field_properties['wrapperCssClass'] = isset($istance_data['css_class']) ? $istance_data['css_class'] : ''; $field_properties['wrapperCssId'] = isset($istance_data['css_id']) ? $istance_data['css_id'] : ''; // Captcha fields hide their title by default: the wrapper class makes // the label screen-reader only (frontend.css), keeping it available to // assistive tech and to the error summary JS label lookup. if (in_array($field_data['type'], array('captcha', 'captcha_v3', 'cap'), true) && accua_forms_captcha_hide_title($istance_data)) { $field_properties['wrapperCssClass'] = trim($field_properties['wrapperCssClass'] . ' accua-captcha-title-hidden'); } // Resolve per-field custom validation messages (per-form instance → field definition → default) $resolved_required_msg = ''; if (!empty($istance_data['custom_required_message'])) { $resolved_required_msg = $istance_data['custom_required_message']; } elseif (!empty($field_data['custom_required_message'])) { $resolved_required_msg = $field_data['custom_required_message']; } $resolved_format_msg = ''; if (!empty($istance_data['custom_format_message'])) { $resolved_format_msg = $istance_data['custom_format_message']; } elseif (!empty($field_data['custom_format_message'])) { $resolved_format_msg = $field_data['custom_format_message']; } // Add data attributes for client-side custom messages (reset each iteration) unset($field_properties['data-custom-required-msg']); unset($field_properties['data-custom-format-msg']); if ($resolved_required_msg !== '') { $field_properties['data-custom-required-msg'] = $resolved_required_msg; } if ($resolved_format_msg !== '') { $field_properties['data-custom-format-msg'] = $resolved_format_msg; } switch ($field_data['type']) { case 'textarea': $element = new Element_Textarea($istance_data['label'], $istance_data['istance_id'], $field_properties+array('cols' => '50', 'value'=>$istance_data['default_value'])); break; case 'hidden': // Hidden inputs render without the .pfbc-element wrapper, so the // editor's CSS Class / CSS ID land on the input itself. $hidden_props = array(); if ( !empty($field_properties['wrapperCssId']) ) { $hidden_props['id'] = $field_properties['wrapperCssId']; } if ( !empty($field_properties['wrapperCssClass']) ) { $hidden_props['class'] = $field_properties['wrapperCssClass']; } $element = new Element_Hidden($istance_data['istance_id'], $istance_data['default_value'], !empty($hidden_props) ? $hidden_props : null); break; case 'checkbox': $lab = $istance_data['label']; if (!empty($istance_data['required'])) { $lab .= ' *'; } if ($allowed_values) { reset($allowed_values); $val = (string) key($allowed_values); $defval = trim($istance_data['default_value']); } elseif ($istance_data['default_value'] == '1') { $defval = $val = '1'; } else { $val = empty($istance_data['default_value'])?'1':$istance_data['default_value']; $defval = ''; } $element = new AccuaForm_Element_Checkbox('', $istance_data['istance_id'], array($val => $lab), $field_properties+array('value' => $defval)); break; case 'select': if (!isset($allowed_values[''])) { $allowed_values = array('' => '') + $allowed_values; } $defval = trim($istance_data['default_value']); $element = new AccuaForm_Element_Select($istance_data['label'], $istance_data['istance_id'], $allowed_values, $field_properties+array('value'=>$defval)); break; case 'post-select': $post_type = isset($istance_data['post_type']) ? $istance_data['post_type'] : 'page'; $extra_args = trim($istance_data['allowed_values']); $defval = trim($istance_data['default_value']); $element = new AccuaForm_Element_PostSelect($istance_data['label'], $istance_data['istance_id'], $post_type, $extra_args, $field_properties+array('value'=>$defval)); break; case 'radio': $defval = trim($istance_data['default_value']); $element = new AccuaForm_Element_Radio($istance_data['label'], $istance_data['istance_id'], $allowed_values, $field_properties+array('value'=>$defval)); break; case 'multiselect': $defval = explode('|', $istance_data['default_value']); foreach ($defval as $k => $v) { $defval[$k] = trim($v); } $element = new AccuaForm_Element_Select($istance_data['label'], $istance_data['istance_id'], $allowed_values, $field_properties+array('multiple' => true, 'value'=>$defval)); break; case 'multicheckbox': case 'post-multicheckbox': $defval = explode('|', $istance_data['default_value']); foreach ($defval as $k => $v) { $defval[$k] = trim($v); } $element = new AccuaForm_Element_Checkbox($istance_data['label'], $istance_data['istance_id'], $allowed_values, $field_properties+array('value'=>$defval)); break; case 'file': $fdata = array(); if ($allowed_values) { $fdata['validExtensions'] = array_keys($allowed_values); } if (!empty($filedata['max_size'])){ $fdata['maxSize'] = $filedata['max_size']; } $fdata['destPath'] = _accua_forms_get_abs_dest_path($filedata['dest_path']); $element = new AccuaForm_Element_File($istance_data['label'], $istance_data['istance_id'], $field_properties+$fdata); break; case 'html': $element = new Element_HTML($istance_data['default_value'], $field_properties); break; case 'email': case 'autoreply_email': $email_props = $field_properties+array('value'=>$istance_data['default_value']); if ($resolved_format_msg !== '') { $email_props['custom_format_message'] = $resolved_format_msg; } $element = new AccuaForm_Element_Email($istance_data['label'], $istance_data['istance_id'], $email_props); break; case 'colorpicker': $element = new AccuaForm_Element_ColorPicker($istance_data['label'], $istance_data['istance_id'], $field_properties+array('value'=>$istance_data['default_value'])); break; case 'fieldset-begin': if ($fieldset_open) { $form->addElement(new AccuaForm_Element_FieldsetEnd()); } else { $fieldset_open = true; } $fs_props = $field_properties; $fs_props['fieldset_style'] = isset($istance_data['fieldset_style']) ? $istance_data['fieldset_style'] : 'border-off-title-off'; $element = new AccuaForm_Element_FieldsetBegin($istance_data['label'], $istance_data['istance_id'], $fs_props); break; case 'fieldset-end': if ($fieldset_open) { $element = new AccuaForm_Element_FieldsetEnd(); $fieldset_open = false; } break; case 'submit': $add_submit = false; $submit_extra = array('name' => $istance_data['istance_id'], 'value' => $istance_data['default_value']); // Buttons render inside the shared .pfbc-buttons group without a // per-element wrapper, so the editor's CSS Class / CSS ID are // applied to the button element itself. if (!empty($field_properties['wrapperCssClass'])) { $submit_extra['class'] = $field_properties['wrapperCssClass']; } if (!empty($field_properties['wrapperCssId'])) { $submit_extra['id'] = $field_properties['wrapperCssId']; } $element = new Element_Button($istance_data['label'], 'submit', $submit_properties+$submit_extra); break; case 'captcha': $empty_captcha_data = array( 'recaptcha_force_v1' => '', 'recaptcha_public_key' => '', 'recaptcha_private_key' => '', ); $captcha_data = get_option('accua_forms_default_captcha_field_data',array()) + $empty_captcha_data; $captcha_properties = array( "description" => "", // Same accuaform_{fid} key format as v3: it identifies this // form in the request-scoped spam-flag registry shared by the // captcha validators (AccuaForm_Validation_CaptchaSpam). 'captchaAction' => 'accuaform_' . preg_replace('/[^A-Za-z0-9_]/', '_', $fid), 'spamAction' => accua_forms_captcha_spam_action($istance_data, 'captcha'), ); $captcha_use_v1 = true; if (($captcha_data['recaptcha_public_key'] !== '') && ($captcha_data['recaptcha_private_key'] !== '')) { $captcha_properties['privateKey'] = $captcha_data['recaptcha_private_key']; $captcha_properties['publicKey'] = $captcha_data['recaptcha_public_key']; $captcha_use_v1 = $captcha_data['recaptcha_force_v1']; } if ($captcha_use_v1) { $element = new Element_HTML("\n\n\n\n"); } else { $element = new AccuaForm_Element_Captcha2 ($istance_data['label'], '', $field_properties+$captcha_properties); } break; case 'captcha_v3': $captcha3_empty_data = array( 'recaptcha_v3_public_key' => '', 'recaptcha_v3_private_key' => '', ); $captcha3_data = get_option('accua_forms_default_captcha_field_data',array()) + $captcha3_empty_data; if (($captcha3_data['recaptcha_v3_public_key'] !== '') && ($captcha3_data['recaptcha_v3_private_key'] !== '')) { $captcha3_properties = array( 'description' => '', 'privateKey' => $captcha3_data['recaptcha_v3_private_key'], 'publicKey' => $captcha3_data['recaptcha_v3_public_key'], // Distinct action per form, restricted to the characters Google allows 'captchaAction' => 'accuaform_' . preg_replace('/[^A-Za-z0-9_]/', '_', $fid), 'spamAction' => accua_forms_captcha_spam_action($istance_data, 'captcha_v3'), 'scoreThreshold' => accua_forms_recaptcha3_score_threshold($istance_data), ); $element = new AccuaForm_Element_Captcha3($istance_data['label'], '', $field_properties+$captcha3_properties); } else { $element = new Element_HTML("\n\n\n\n"); } break; case 'turnstile': $element = new AccuaForm_Element_Turnstile($istance_data['label'], $istance_data['istance_id'], $field_properties+array("description" => "")); break; case 'cap': $cap_empty_data = array( 'cap_instance_url' => '', 'cap_site_key' => '', 'cap_secret_key' => '', ); $cap_data = get_option('accua_forms_default_captcha_field_data',array()) + $cap_empty_data; if (($cap_data['cap_instance_url'] !== '') && ($cap_data['cap_site_key'] !== '') && ($cap_data['cap_secret_key'] !== '')) { $cap_properties = array( 'description' => '', 'instanceUrl' => $cap_data['cap_instance_url'], 'siteKey' => $cap_data['cap_site_key'], 'secretKey' => $cap_data['cap_secret_key'], ); $element = new AccuaForm_Element_Cap($istance_data['label'], '', $field_properties+$cap_properties); } else { $element = new Element_HTML("\n\n\n\n"); } break; case 'password': $element = new Element_Password($istance_data['label'], $istance_data['istance_id'], $field_properties+array('value'=>$istance_data['default_value'])); break; case 'password-and-confirm': $id_2 = "___{$istance_data['istance_id']}___confirmpass"; $element = new Element_Password($istance_data['label'], $istance_data['istance_id'], $field_properties+array('value'=>$istance_data['default_value'])); $element_conf = new Element_Password(__("Confirm password", 'contact-forms'), $id_2, $field_properties+array('value'=>$istance_data['default_value'])); $element_conf_validator = new AccuaForm_Validation_Password(); $element_conf_validator->configure(array('otherPasswordFieldName'=>$istance_data['istance_id'])); $element_conf->setValidation($element_conf_validator); break; case 'date': $element = new AccuaForm_Element_Date($istance_data['label'], $istance_data['istance_id'], $field_properties+array('value'=>$istance_data['default_value'], 'minDate'=>$istance_data['min_date'], 'maxDate'=>$istance_data['max_date'])); break; case 'telephone': $phone_country = isset($istance_data['country_code']) ? $istance_data['country_code'] : 'IT'; $phone_props = $field_properties+array('value'=>$istance_data['default_value'], 'country_code'=>$phone_country); if ($resolved_format_msg !== '') { $phone_props['custom_format_message'] = $resolved_format_msg; } $element = new AccuaForm_Element_Telephone($istance_data['label'], $istance_data['istance_id'], $phone_props); break; //case 'textfield': default: /** * Filter to create a custom Element for an external field type. * * @param Element|null $element Null by default; return an Element to override. * @param string $field_type The field type identifier. * @param array $field_data The field definition from avail_fields. * @param array $istance_data The field instance data (label, required, etc.). * @param array $field_properties Common properties (description, shortDesc, etc.). */ $element = apply_filters( 'accua_forms_render_field_element', null, $field_data['type'], $field_data, $istance_data, $field_properties ); if ( ! $element ) { $element = new Element_Textbox($istance_data['label'], $istance_data['istance_id'], $field_properties+array('value'=>$istance_data['default_value'])); } break; } if ($element) { if (!empty($istance_data['required'])) { if (!in_array($field_data['type'], array('captcha_v3', 'cap'), true)) { // captcha_v3 and cap must not get this class: the client-side required check // would inspect their hidden token input, which is empty until solved $element->setClass('accuaforms-field-required'); } if ($field_data['type'] === 'captcha' && empty($captcha_use_v1)) { //nothing } elseif ($field_data['type'] === 'captcha_v3') { //nothing - v3 has its own validation set in the Element constructor } elseif ($field_data['type'] === 'turnstile') { //nothing - turnstile has its own validation set in the Element constructor } elseif ($field_data['type'] === 'cap') { //nothing - cap has its own validation set in the Element constructor } elseif ($field_data['type'] === 'password-and-confirm') { if ($resolved_required_msg !== '') { $req_msg = str_replace(array('%s', '%element%'), $istance_data['label'], $resolved_required_msg); } else { /* translators: Password field required error */ $req_msg = __( 'Password is required', 'contact-forms' ); } $element->setValidation(new Validation_Required($req_msg)); } else { if ($resolved_required_msg !== '') { $req_msg = str_replace(array('%s', '%element%'), $istance_data['label'], $resolved_required_msg); } else { /* translators: %element% is the field label, replaced with str_replace() */ // phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment -- Translators comment is above $req_msg = str_replace('%element%', $istance_data['label'], __( '%element% is required', 'contact-forms' )); } $element->setValidation(new Validation_Required($req_msg)); } } if ($elementName = $element->getName()) { $element->setClass('accuaform-fieldname-'.$elementName); } if ($field_data['type']) { $element->setClass('accuaform-fieldtype-'.$field_data['type']); } $form->addElement($element); if($element_conf!=NULL) { $form->addElement($element_conf); $element_conf=NULL; } } } if ($fieldset_open) { $form->addElement(new AccuaForm_Element_FieldsetEnd()); $fieldset_open = false; } /** * Filter to suppress the automatically added submit button. * * Used by the Fields page live preview, which renders a single field * with no use for a submit button. An explicit submit field in the * form is unaffected. * * @param bool $suppress False by default. */ if ($add_submit && !apply_filters('accua_forms_preview_suppress_auto_submit', false)) { $form->addElement(new Element_Button(__('Submit', 'contact-forms'), 'submit', $submit_properties)); } } } } function accua_forms_aggregate_submitted_data(&$replace_map, $params = array()) { if (empty($params['txt']) && empty($params['html']) && empty($params['json']) && empty($params['email'])) { $params += array( 'txt' => true, 'html' => true, 'json' => true, 'email' => true, ); } if (!empty($params['txt'])) { $replace_map['__submitted_txt'] = implode("\n",$replace_map['__submitted_txt_raw']); } if (!empty($params['html'])) { $replace_map['__submitted_html'] = implode("\n\n",$replace_map['__submitted_html_raw']); } if (!empty($params['json'])) { $replace_map['__submitted_json'] = _accua_forms_json_encode($replace_map['__submitted_json_raw']); } if (!empty($params['email'])) { $replace_map['__autoreply_email'] = implode('; ', $replace_map['__autoreply_email_raw']); } } add_filter('accua_form_validate', 'accua_forms_validation_handler', 10, 4); function accua_forms_validation_handler($valid, $submittedID, $submittedData, $form){ if (substr($submittedID, 0, 14) == '__accua-form__') { $fid = substr($submittedID,14); $form_data = _accua_forms_get_form_data($fid, false); if ($form_data) { return apply_filters('accua_forms_validation', $valid, $fid, $submittedData, $form); } } return $valid; } /** * Sanitize a submissions-list column key. * * The case-preserving counterpart of sanitize_key(): column keys embed the * field slug verbatim (`_field_{slug}`), and slugs are case-sensitive, so * lowercasing would silently merge two fields whose slugs differ only in case. * Allows the same character set the Fields page accepts for a slug. * * @since 2.3.0 * @param string $key Raw column key. * @return string Sanitized column key. */ function accua_forms_sanitize_column_key($key) { return preg_replace('/[^A-Za-z0-9_\-]/', '', (string) $key); } /** * Allowed captcha spam actions (reCAPTCHA v2 and v3) and their form-editor * labels. * * 'spam' accepts a submission that fails the captcha check silently (the * visitor sees the normal success message, no emails are sent) and marks it * with the Spam lead status; 'trash' and 'delete' are the other two silent * modes. 'reject' blocks the submission with a visible error - the only * behavior before 2.2.38, and the reCAPTCHA v2 default again since 2.3.0. * * @return array action key => translated label. */ function accua_forms_captcha_spam_action_options() { return array( 'spam' => __('Accept silently and mark as Spam', 'contact-forms'), 'trash' => __('Accept silently and move to Trash', 'contact-forms'), 'delete' => __('Accept silently and delete immediately', 'contact-forms'), 'reject' => __('Reject with an error message', 'contact-forms'), ); } /** * The site-wide default spam action for a captcha type, from the settings page. * * reCAPTCHA v2 and v3 keep separate defaults because their checks fail for * different reasons. A v3 failure is a low score computed invisibly, so * absorbing it silently is the sensible default. A v2 failure most often means * the visitor did not solve the checkbox they were shown - or took longer than * the two minutes a token stays valid, or retried with a token Google had * already consumed - and Google's guidance for that case is to surface the * error and let them solve it again (the AJAX handler resets the widget, so a * retry always carries a fresh token). Accepting those silently would file a * real visitor's message as spam and send no notification, so v2 defaults to * 'reject'. * * @since 2.3.0 The $type parameter, and the separate v2 default. * @param string $type Field type: 'captcha' (reCAPTCHA v2) or 'captcha_v3'. * @return string 'spam', 'trash', 'delete' or 'reject'. */ function accua_forms_captcha_default_spam_action($type = 'captcha_v3') { $captcha_data = get_option('accua_forms_default_captcha_field_data', array()); $is_v2 = ('captcha' === $type); $option_key = $is_v2 ? 'captcha_spam_action_v2' : 'captcha_spam_action'; $fallback = $is_v2 ? 'reject' : 'spam'; $action = isset($captcha_data[$option_key]) ? $captcha_data[$option_key] : ''; return isset(accua_forms_captcha_spam_action_options()[$action]) ? $action : $fallback; } /** * Resolve the spam action configured on a captcha (v2) or captcha_v3 field * instance: instance override, then the site-wide default for its type. * * @param array $istance_data The field instance data from the saved form. * @param string $type Field type: 'captcha' (v2) or 'captcha_v3'. * Defaults to the v3 resolution for backward * compatibility with pre-2.3.0 callers. * @return string 'spam', 'trash', 'delete' or 'reject'. */ function accua_forms_captcha_spam_action($istance_data, $type = 'captcha_v3') { $action = isset($istance_data['spam_action']) ? $istance_data['spam_action'] : ''; return isset(accua_forms_captcha_spam_action_options()[$action]) ? $action : accua_forms_captcha_default_spam_action($type); } /** * Find the spam action of the first captcha (v2) or captcha_v3 field of a * form, if any. * * Note: when a submission was actually flagged, the handler follows the action * recorded by the validator that failed (AccuaForm_Validation_CaptchaSpam), not * this helper - a form can carry both a v2 and a v3 field with different * actions. This remains for callers that need a form-level answer up front. * * @param array $form_data The saved form data (with 'fields'). * @return string The configured spam action, or the site-wide default if the * form has no captcha field or no explicit setting. */ function accua_forms_captcha_form_spam_action($form_data) { if (!empty($form_data['fields']) && is_array($form_data['fields'])) { $avail_fields = get_option('accua_forms_avail_fields', array()); foreach ($form_data['fields'] as $istance_data) { if (isset($istance_data['ref'], $avail_fields[$istance_data['ref']]['type']) && in_array($avail_fields[$istance_data['ref']]['type'], array('captcha', 'captcha_v3'), true)) { return accua_forms_captcha_spam_action($istance_data, $avail_fields[$istance_data['ref']]['type']); } } } return accua_forms_captcha_default_spam_action(); } /** * The site-wide default for the captcha "Hide field title" option. * * @return bool True when captcha titles are hidden by default (ships true). */ function accua_forms_captcha_default_hide_title() { $captcha_data = get_option('accua_forms_default_captcha_field_data', array()); if (!isset($captcha_data['captcha_hide_title'])) { return true; } return !empty($captcha_data['captcha_hide_title']); } /** * Whether the title of a captcha field instance (captcha, captcha_v3 or cap) * is hidden on the rendered form. Instance override, then the site-wide * default (hidden unless the administrator changed it). The label stays in * the markup visually hidden (screen-reader only), so error summary links and * assistive tech keep working. * * @param array $istance_data The field instance data from the saved form. * @return bool */ function accua_forms_captcha_hide_title($istance_data) { if (!isset($istance_data['hide_title'])) { return accua_forms_captcha_default_hide_title(); } return !empty($istance_data['hide_title']); } /** * Normalize a reCAPTCHA v3 score threshold to the 0.0 - 1.0 range Google * uses, rounded to two decimals. * * @param mixed $value The raw value (string from a form field, float, ...). * @param float $fallback Returned when $value is not numeric. * @return float */ function accua_forms_recaptcha3_clamp_score($value, $fallback = 0.5) { if (!is_numeric($value)) { return (float) $fallback; } $score = round((float) $value, 2); if ($score < 0) { return 0.0; } if ($score > 1) { return 1.0; } return $score; } /** * The site-wide default minimum reCAPTCHA v3 score, from the settings page. * * @return float 0.0 - 1.0 (ships 0.5, the value Google suggests). */ function accua_forms_recaptcha3_default_score_threshold() { $captcha_data = get_option('accua_forms_default_captcha_field_data', array()); $stored = isset($captcha_data['recaptcha_v3_score_threshold']) ? $captcha_data['recaptcha_v3_score_threshold'] : null; return accua_forms_recaptcha3_clamp_score($stored, 0.5); } /** * Resolve the minimum reCAPTCHA v3 score a submission must reach on a given * field instance: instance override, then the site-wide default. * * The accua_forms_recaptcha3_score_threshold filter still runs last, inside * the validator, and receives this value as its default. * * @param array $istance_data The field instance data from the saved form. * @return float 0.0 - 1.0. */ function accua_forms_recaptcha3_score_threshold($istance_data) { $default = accua_forms_recaptcha3_default_score_threshold(); if (isset($istance_data['score_threshold']) && is_numeric($istance_data['score_threshold'])) { return accua_forms_recaptcha3_clamp_score($istance_data['score_threshold'], $default); } return $default; } /** * Placement options for the floating reCAPTCHA v3 badge. * * Google allows hiding the badge only if the reCAPTCHA branding is shown * elsewhere in the user flow - with 'hidden' the field prints the required * notice text under the form itself. * * @return array badge key => translated label. */ function accua_forms_recaptcha3_badge_options() { return array( 'bottomright' => __('Bottom right', 'contact-forms'), 'bottomleft' => __('Bottom left', 'contact-forms'), 'hidden' => __('Hidden (the required reCAPTCHA notice is shown in the form instead)', 'contact-forms'), ); } /** * The configured reCAPTCHA v3 badge placement. * * Site-wide, not per field: the badge is a single floating element shared by * every reCAPTCHA on the page. * * @return string 'bottomright' (default), 'bottomleft' or 'hidden'. */ function accua_forms_recaptcha3_badge() { $captcha_data = get_option('accua_forms_default_captcha_field_data', array()); $badge = isset($captcha_data['recaptcha_v3_badge']) ? $captcha_data['recaptcha_v3_badge'] : ''; return isset(accua_forms_recaptcha3_badge_options()[$badge]) ? $badge : 'bottomright'; } /** @deprecated 2.2.40 Use accua_forms_captcha_spam_action_options(). */ function accua_forms_recaptcha3_spam_action_options() { return accua_forms_captcha_spam_action_options(); } /** @deprecated 2.2.40 Use accua_forms_captcha_spam_action(). */ function accua_forms_recaptcha3_spam_action($istance_data) { return accua_forms_captcha_spam_action($istance_data); } /** @deprecated 2.2.40 Use accua_forms_captcha_form_spam_action(). */ function accua_forms_recaptcha3_form_spam_action($form_data) { return accua_forms_captcha_form_spam_action($form_data); } function accua_forms_anonymize_ip($ip) { $ip = (string) $ip; $anonymize_ip_data = get_option('accua_forms_anonymize_ip_data',array()); if (empty($anonymize_ip_data['anonymize_ip_bytes'])) { return $ip; } switch ($anonymize_ip_data['anonymize_ip_bytes']) { case 1: $ip = preg_replace('/\.[^.]+$/', '.xxx', $ip); break; case 2: $ip = preg_replace('/\.[^.]+\.[^.]+$/', '.xxx.xxx', $ip); break; case 3: $ip = preg_replace('/\.[^.]+\.[^.]+\.[^.]+$/', '.xxx.xxx.xxx', $ip); break; case 4: $ip = ''; } return $ip; } add_action('accua_form_submit', 'accua_forms_form_submission_handler', -10, 3); function accua_forms_form_submission_handler($submittedID, $submittedData, $form) { if (empty($GLOBALS['wp_rewrite'])) { $GLOBALS['wp_rewrite'] = new WP_Rewrite(); } if (!class_exists('AccuaConditionalReplacer')){ require_once('AccuaConditionalReplacer.php'); } if (substr($submittedID, 0, 14) == '__accua-form__') { $fid = substr($submittedID,14); $form_data = _accua_forms_get_form_data($fid, false); if ($form_data) { global $wpdb; $time = time(); $captcha_flag_key = 'accuaform_' . preg_replace('/[^A-Za-z0-9_]/', '_', $fid); $stats_data = array( 'user_agent' => $form->stats['user_agent'], 'platform' => $form->stats['platform'], 'tentatives' => $form->stats['tentatives'], 'submit_method' => $form->stats['submit_method'], ); // reCAPTCHA v3 returned a score for this submission: keep it so the // administrator can see what the form actually scores and tune the // threshold accordingly. Recorded whether the check passed or not. if (class_exists('AccuaForm_Validation_Captcha3', false)) { $recaptcha3_score = AccuaForm_Validation_Captcha3::getScore($captcha_flag_key); if ($recaptcha3_score !== null) { $stats_data['recaptcha3_score'] = $recaptcha3_score; } } // Auto mark as spam: an email field carrying an address from the // site-wide blocklist (settings page, Other settings tab). Recorded in // the stats so the single submission page can say why it was classified; // only the reason, never the address itself - afs_stats survives every // read of the submission and is cleared on anonymization, so nothing // personal belongs in it. $has_blocklisted_email = accua_forms_submission_has_blocklisted_email($submittedData, $form_data); if ($has_blocklisted_email) { $stats_data['spam_reason'] = 'email_blocklist'; } $afs_stats = _accua_forms_json_encode($stats_data); $anonymized_ip = accua_forms_anonymize_ip($form->stats['ip']); // Silent captcha classification: when the captcha (v2) or captcha_v3 // field of this form is configured with a silent spam action, a failed // check passed validation but flagged the submission (see // Validation/CaptchaSpam.php). The action comes from the validator that // actually failed - since 2.3.0 v2 and v3 have different defaults, so a // form carrying both must not follow whichever field happens to come // first. A 'reject' action never flags, so a flagged key is always one // of the three silent actions. $captcha_spam_action = class_exists('AccuaForm_Validation_CaptchaSpam', false) ? AccuaForm_Validation_CaptchaSpam::getSpamAction($captcha_flag_key) : ''; $captcha_flagged = in_array($captcha_spam_action, array('spam', 'trash', 'delete'), true); $spam_action = $captcha_spam_action; $is_spam = $captcha_flagged; // The blocklist marks the submission as Spam whatever the captcha // decided: a captcha judges how the form was filled in, not who filled // it in. It never overrides a captcha classification that is already // stricter (trash, delete) - only a submission the captcha let through // is reclassified here. if ($has_blocklisted_email && !$is_spam) { $spam_action = 'spam'; $is_spam = true; } $insert_data = array ( 'afs_form_id' => (string) $fid, 'afs_post_id' => (string) $form->stats['pid'], 'afs_ip' => $anonymized_ip, 'afs_uri' => (string) $form->stats['uri'], 'afs_referrer' => (string) $form->stats['referrer'], 'afs_lang' => (string) $form->stats['lang'], 'afs_created' => (string) gmdate('Y-m-d H:i:s', $form->stats['created']), 'afs_submitted' => (string) gmdate('Y-m-d H:i:s', $time), 'afs_stats' => (string) $afs_stats, ); if ($is_spam) { if ($spam_action === 'trash') { $insert_data['afs_status'] = -1; } elseif ($spam_action === 'spam') { $insert_data['afs_lead_status'] = -1; } } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Form submission insert requires direct query $insert_ret = $wpdb->insert( $wpdb->prefix . 'accua_forms_submissions', $insert_data ); if ($insert_ret) { $submission_id = $form->stats['submission_id'] = $wpdb->insert_id; } else { $submission_id = $form->stats['submission_id'] = 0; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Legitimate error logging for failed DB insert error_log("[Contact Forms] unable to save submitted form data"); } $review_submission_url = admin_url('admin.php').'?page=accua_forms_submissions_list&sid='.$submission_id; $replace_map = array( '__fid' => $fid, '__subid' => $submission_id, '__pid' => $form->stats['pid'], '__ip' => $form->stats['ip'], '__anonymized_ip' => $anonymized_ip, '__uri' => $form->stats['uri'], '__url' => $form->stats['url'], '__referrer' => $form->stats['referrer'], '__lang' => $form->stats['lang'], '__locale' => $form->stats['locale'], '__created' => $form->stats['created'], '__created_day' => wp_date('l j F Y', $form->stats['created']), '__created_day_month_year' => wp_date('j F Y', $form->stats['created']), '__created_hour' => wp_date('G:i', $form->stats['created']), '__submitted' => $time, '__submitted_day' => wp_date('l j F Y', $time), '__submitted_day_month_year' => wp_date('j F Y', $time), '__submitted_hour' => wp_date('G:i', $time), '__confirmation_emails_message' => $form_data['confirmation_emails_message'], '__user_agent' => $form->stats['user_agent'], '__platform' => $form->stats['platform'], '__tentatives' => $form->stats['tentatives'], '__submit_method' => $form->stats['submit_method'], '__review_submission_url' => $review_submission_url, ); $avail_fields = get_option('accua_forms_avail_fields', array()); $replace_map['__autoreply_email_raw'] = array(); $replace_map['__submitted_txt_raw'] = array(); $replace_map['__submitted_html_raw'] = array(); $replace_map['__submitted_json_raw'] = array(); $_field_data = array(); $_istance_data = array(); $field_data = array(); foreach ($submittedData as $istance_id => $value) { if (empty($form_data['fields'][$istance_id])) { continue; } $istance_data = $form_data['fields'][$istance_id]; if (empty($avail_fields[$istance_data['ref']])) { $fieldset_data = array(); if (!empty($istance_data['ref'])) { if ($istance_data['ref'] == '__fieldset-begin') { $field_data = array( 'id' => '__fieldset-begin', 'name' => __('Fieldset begin', 'contact-forms'), 'type' => 'fieldset-begin', 'description' => '', ); } elseif ($istance_data['ref'] == '__fieldset-end') { $field_data = array( 'id' => '__fieldset-end', 'name' => __('Fieldset end', 'contact-forms'), 'type' => 'fieldset-end', 'description' => '', ); } } } else { $field_data = $avail_fields[$istance_data['ref']]; } $field_data += array( 'id' => '__html', 'name' => __( 'Custom HTML content', 'contact-forms'), 'type' => 'html', 'description' => __( 'Use this special field to inject raw HTML in the form. You can use this multiple times.', 'contact-forms'), 'default_value' => '', 'allowed_values' => '', ); /* echo "\n"; */ $istance_data += array( 'istance_id' => $field_data['id'], 'widget_number' => '', 'ref' => $field_data['id'], 'label' => $field_data['name'], 'default_value' => $field_data['default_value'], 'allowed_values' => $field_data['allowed_values'], ); $type = $field_data['type']; $file_download_url = ''; switch ($field_data['type']) { case 'checkbox': case 'submit': if (empty($value)) { $replace_map[$istance_data['istance_id']] = $value = ''; } else { if (empty($istance_data['default_value'])) { $replace_map[$istance_data['istance_id']] = 'Checked'; $value = '1'; } else { $replace_map[$istance_data['istance_id']] = $value = $istance_data['default_value']; } } break; case 'multiselect': case 'multicheckbox': case 'select': case 'radio': $el = $form->getElementByName($istance_id); $opts = $el->getOptions(); if (!is_array($value)) { $value = array($value); } $label = array(); foreach ($value as $val) { if (isset($opts[$val])) { $label[] = $opts[$val]; } } $replace_map['__label_'.$istance_data['istance_id']] = implode(", ", $label); $replace_map[$istance_data['istance_id']] = implode(', ',$value); $value = implode('|',$value); break; case 'post-multicheckbox': if (is_array($value) && $value) { $el = $form->getElementByName($istance_id); $opts = $el->getOptions(); $value2 = array(); $titles = array(); $ids = array(); $urls = array(); foreach ($value as $val) { if (isset($opts[$val])) { $titles[] = $opts[$val]; $ids[] = $val; $urls[] = get_permalink($val); $value2[] = $val . ': ' . trim(preg_replace('/[\s\n\r]+/', ' ', $opts[$val])); } } $replace_map['__label_'.$istance_data['istance_id']] = $replace_map['__post_title_'.$istance_data['istance_id']] = implode("\n", $titles); $replace_map['__post_id_'.$istance_data['istance_id']] = implode("\n", $ids); $replace_map['__post_url_'.$istance_data['istance_id']] = implode("\n", $urls); $replace_map[$istance_data['istance_id']] = $value = implode("\n", $value2); } else { $replace_map['__label_'.$istance_data['istance_id']] = $replace_map['__post_title_'.$istance_data['istance_id']] = ''; $replace_map['__post_id_'.$istance_data['istance_id']] = ''; $replace_map['__post_url_'.$istance_data['istance_id']] = ''; $replace_map[$istance_data['istance_id']] = $value = ''; } break; case 'post-select': if ($value !== '') { $post = get_post(absint($value)); // The submitted ID must belong to the post type the field is // configured for and have a status the field may expose (publish, // plus private when explicitly configured); otherwise any post ID // would be accepted. $expected_post_type = isset($istance_data['post_type']) ? $istance_data['post_type'] : 'page'; $ps_allowed_statuses = array('publish'); $ps_element = $form->getElementByName($istance_id); if ($ps_element instanceof AccuaForm_Element_PostSelect) { $expected_post_type = $ps_element->getEffectivePostType(); $ps_allowed_statuses = $ps_element->getAllowedPostStatuses(); } if ($post && in_array($post->post_status, $ps_allowed_statuses, true) && $post->post_type === $expected_post_type) { $title = $post->post_title; $replace_map['__label_'.$istance_data['istance_id']] = $replace_map['__post_title_'.$istance_data['istance_id']] = $title; $replace_map['__post_id_'.$istance_data['istance_id']] = $value; $replace_map['__post_url_'.$istance_data['istance_id']] = get_permalink($value); $value = $value . ': ' . trim(preg_replace('/[\s\n\r]+/', ' ', $title)); } else { $replace_map['__label_'.$istance_data['istance_id']] = $replace_map['__post_title_'.$istance_data['istance_id']] = ''; $replace_map['__post_id_'.$istance_data['istance_id']] = ''; $replace_map['__post_url_'.$istance_data['istance_id']] = ''; $value = ''; } } $replace_map[$istance_data['istance_id']] = $value; break; case 'autoreply_email': if ($value !== '') { $replace_map['__autoreply_email_raw'][] = $value; } $replace_map[$istance_data['istance_id']] = $value; break; case 'file': //TODO: move temp file to "{$submission_id}_{$field_data['id']}_{$file['name']}"; value is $file['name'] $buildid = $submittedData['_AccuaForm_buildID']; $file = $form->getFile($istance_id); if ($value !== null && $value !== '' && $file) { if ($form->renameFile($istance_id, "{$submission_id}_{$field_data['id']}_{$file['name']}")) { $urlfield = rawurlencode($istance_data['istance_id']); $urlfile = rawurlencode($value); $token = accua_forms_generate_download_token($submission_id); $file_download_url = admin_url('admin-ajax.php') . "?action=accua_forms_download_submitted_file&subid={$submission_id}&field={$urlfield}&file={$urlfile}&nonce=" . wp_create_nonce('accua_forms_download_nonce')."&token={$token}&_wpnonce=" . wp_create_nonce('download_file_' . $submission_id . '_' . $urlfield); } } $replace_map[$istance_data['istance_id']] = $value; $replace_map['__download_'.$istance_data['istance_id']] = $file_download_url; break; case 'password': case 'password-and-confirm': $value = trim($value); $replace_map[$istance_data['istance_id']] = $value; $type = 'hashed-password'; if ($value !== '') { $value = wp_hash_password($value); } $replace_map['__hashed_'.$istance_data['istance_id']] = $value; break; default: $replace_map[$istance_data['istance_id']] = $value; } switch ($field_data['type']) { case 'fieldset-begin': $fieldset_label = !empty($istance_data['label']) ? esc_html($istance_data['label']) : esc_html($istance_data['istance_id']); $replace_map['__submitted_txt_raw'][$istance_data['istance_id']] = "\n--- {$istance_data['label']} ---"; $replace_map['__submitted_json_raw'][$istance_data['istance_id']] = $value; $replace_map['__submitted_html_raw'][$istance_data['istance_id']] = "{$fieldset_label}"; break; case 'fieldset-end': break; case 'file': $replace_map['__submitted_txt_raw'][$istance_data['istance_id']] = "{$istance_data['istance_id']}\t$value\t$file_download_url"; $replace_map['__submitted_json_raw'][$istance_data['istance_id']] = "$value\t$file_download_url"; $replace_map['__submitted_html_raw'][$istance_data['istance_id']] = "{$istance_data['istance_id']}".esc_html($value).""; break; case 'email': case 'autoreply_email': $replace_map['__submitted_txt_raw'][$istance_data['istance_id']] = "{$istance_data['istance_id']}\t$value"; $replace_map['__submitted_json_raw'][$istance_data['istance_id']] = $value; $replace_map['__submitted_html_raw'][$istance_data['istance_id']] = "{$istance_data['istance_id']}".esc_html($value).""; break; case 'submit': break; case 'colorpicker': $replace_map['__submitted_txt_raw'][$istance_data['istance_id']] = "{$istance_data['istance_id']}\t$value"; $replace_map['__submitted_json_raw'][$istance_data['istance_id']] = $value; if ($value === '') { $value_html = ''; } else { $value_esc = esc_attr($value); $value_html = " $value_esc"; } $replace_map['__submitted_html_raw'][$istance_data['istance_id']] = "{$istance_data['istance_id']}$value_html"; break; case 'password': case 'password-and-confirm': break; default: $replace_map['__submitted_txt_raw'][$istance_data['istance_id']] = "{$istance_data['istance_id']}\t$value"; $replace_map['__submitted_json_raw'][$istance_data['istance_id']] = $value; $replace_map['__submitted_html_raw'][$istance_data['istance_id']] = "{$istance_data['istance_id']}".esc_html($value); } if ($submission_id) { // Ensure value is never NULL to prevent database errors $safe_value = $value === null ? '' : $value; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Form field values insert requires direct query $wpdb->insert( $wpdb->prefix . 'accua_forms_submissions_values', array ( 'afsv_sub_id' => $submission_id, 'afsv_field_id' => $istance_data['istance_id'], 'afsv_type' => $type, 'afsv_value' => $safe_value, ), array('%d','%s','%s','%s') ); } $_field_data[$istance_data['istance_id']] = $field_data; $_istance_data[$istance_data['istance_id']] = $istance_data; } $replace_map['__autoreply'] = (bool) ($replace_map['__autoreply_email_raw'] && $form_data['confirmation_emails_subject'] && $form_data['confirmation_emails_message']); accua_forms_aggregate_submitted_data($replace_map); //Older undocumented filter, maintained for backward compatibility. In fact filter accua_forms_form_submission_handler is called before accua_forms_submission, but for the rest they are the same $replace_map = apply_filters('accua_forms_form_submission_handler', $replace_map, $fid, $submittedData, $form, $_field_data, $_istance_data); //Newer filter, with an easier name $replace_map = apply_filters('accua_forms_submission', $replace_map, $fid, $submittedData, $form, $_field_data, $_istance_data); $submitted_html = "\n\n
" . $replace_map['__submitted_html'] . "
"; $submitted_html = str_replace("class='valori_submitted'", "class='valori_submitted' style='vertical-align:top;padding:4px 0;overflow-wrap:break-word;word-break:break-word;'", $submitted_html); $confirmation_emails_message = $replace_map['__confirmation_emails_message']; unset($replace_map['__submitted_html'], $replace_map['__confirmation_emails_message'], $replace_map['__submitted_txt_raw'], $replace_map['__submitted_html_raw'], $replace_map['__submitted_json_raw'], $replace_map['__autoreply_email_raw']); $replace_map_html = array(); foreach($replace_map as $key => $value) { $replace_map_html["!$key"] = wp_kses($value, 'post'); $replace_map_html[$key] = esc_attr($value); } $replace_map['__submitted_html'] = $replace_map_html['__submitted_html'] = $replace_map_html['!__submitted_html'] = $submitted_html; $replacer_html = new AccuaConditionalReplacer($replace_map_html); $confirmation_emails_message = $replace_map['__confirmation_emails_message'] = $replacer_html->doReplace($confirmation_emails_message); $replacer_html->appendPattern(array('__confirmation_emails_message' => $confirmation_emails_message, '!__confirmation_emails_message' => $confirmation_emails_message)); $replacer = new AccuaConditionalReplacer($replace_map); $form_data_replaced = array(); $settings = array( 'emails_from_name', 'emails_from', 'admin_emails_to', 'emails_bcc', 'admin_emails_subject', 'confirmation_emails_subject', ); foreach($settings as $i) { $form_data_replaced[$i] = $replacer->doReplace($form_data[$i]); } $settings_html = array( 'success_message', 'error_message', 'admin_emails_message', ); foreach($settings_html as $i) { $form_data_replaced[$i] = $replacer_html->doReplace($form_data[$i]); } // Track mail sending success for showing appropriate message $mail_success = true; $mail1 = true; $mail2 = true; $header = array("Content-Type: text/html; charset=".get_option('blog_charset')); $emails_from = trim($form_data_replaced['emails_from']); if (strpos($emails_from, '@') !== false) { if ((strpos($emails_from, '<') === false) && is_email($emails_from)) { $emails_from_name = trim($form_data_replaced['emails_from_name']); if ($emails_from_name !== '') { $emails_from = "=?" . get_bloginfo('charset') . "?B?" . base64_encode($emails_from_name) . "?= <$emails_from>"; } } $header[] = 'From: '.$emails_from; } if ($form_data_replaced['emails_bcc']) { $header[] = 'Bcc: '.$form_data_replaced['emails_bcc']; } if (!$is_spam && $form_data_replaced['admin_emails_to'] && $form_data_replaced['admin_emails_subject']) { /* $admin_tos = explode(',', strtr($form_data_replaced['admin_emails_to'], "\n\t\r;", ',,,,')); foreach ($admin_tos as $admin_to) { $mail1 = wp_mail(trim($admin_to), $form_data_replaced['admin_emails_subject'], $form_data_replaced['admin_emails_message'], $header); } */ $mail1 = wp_mail($form_data_replaced['admin_emails_to'], $form_data_replaced['admin_emails_subject'],''.wpautop($form_data_replaced['admin_emails_message']).'', $header); if (!$mail1) { $mail_success = false; } } if (!$is_spam && $replace_map['__autoreply'] && $replace_map['__autoreply_email'] && $form_data_replaced['confirmation_emails_subject'] && $confirmation_emails_message) { $mail2 = wp_mail($replace_map['__autoreply_email'], $form_data_replaced['confirmation_emails_subject'], ''.wpautop($confirmation_emails_message).'', $header); if (!$mail2) { $mail_success = false; } } // Determine which message to show based on mail success and user settings if ($mail_success) { // Show success message unless "Don't show any messages" is selected if (empty($form_data['success_message_no_message'])) { $message_content = trim($form_data_replaced['success_message']); if ($message_content !== '') { AccuaForm::appendSubmittedMessages(wpautop($message_content)); } } } else { // Mail failed - show error message unless "Don't show any messages" is selected if (empty($form_data['error_message_no_message'])) { $error_content = trim($form_data_replaced['error_message']); if ($error_content !== '') { AccuaForm::appendSubmittedMessages(wpautop($error_content)); } } } if ($is_spam) { if ($spam_action === 'delete' && $submission_id) { // Remove the stored rows and any uploaded files; the visitor still // saw the normal success message. accua_forms_erase_submission($submission_id, 'delete'); $submission_id = $form->stats['submission_id'] = 0; } // The two captcha hooks keep firing only for captcha classifications: // they are documented as such, and a listener acting on them (feeding // a captcha's own reporting API, tuning a score threshold) would be // misled by a submission no captcha ever looked at. The blocklist has // the type-agnostic accua_forms_spam_submission below instead. if ($captcha_flagged) { /** * Fires when a submission was silently classified as spam by a * captcha field (reCAPTCHA v2 or v3). * * @param int $submission_id The stored submission id (0 when the spam action is 'delete'). * @param string $spam_action The configured action: 'spam', 'trash' or 'delete'. * @param string $fid The form id. */ do_action('accua_forms_captcha_spam_submission', $submission_id, $captcha_spam_action, $fid); /** * Deprecated alias of accua_forms_captcha_spam_submission, kept for * backward compatibility. Since 2.2.40 it also fires for reCAPTCHA v2 * classifications, not only v3. * * @param int $submission_id The stored submission id (0 when the spam action is 'delete'). * @param string $spam_action The configured action: 'spam', 'trash' or 'delete'. * @param string $fid The form id. */ do_action('accua_forms_recaptcha3_spam_submission', $submission_id, $captcha_spam_action, $fid); } /** * Fires when a submission was silently classified as spam by any of * the plugin's spam checks. * * @since 2.3.1 * @param int $submission_id The stored submission id (0 when the spam action is 'delete'). * @param string $spam_action The action applied: 'spam', 'trash' or 'delete'. * @param string $fid The form id. * @param string $spam_reason What classified it: 'captcha' or 'email_blocklist'. */ do_action( 'accua_forms_spam_submission', $submission_id, $spam_action, $fid, $captcha_flagged ? 'captcha' : 'email_blocklist' ); } /* echo ""; */ } } } function accua_forms_get_submission_data($subid, $options = array()){ //TODO: Completa e usa per sostituire i campi global $wpdb; $subid = (int) $subid; $options += array( 'extra' => true, 'file_format' => 'name', ); $ret = array(); if ($options['extra']) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Submission data lookup requires direct query $query1 = $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}accua_forms_submissions` WHERE afs_id = %d", $subid ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- $query1 is prepared above, submission lookup requires direct query $data = $wpdb->get_row($query1); if (!empty($data)) { $created = $data->afs_created; $created[10] = 'T'; $created.='.00+00:00'; $created = strtotime($created); $submitted = $data->afs_submitted; $submitted[10] = 'T'; $submitted.='.00+00:00'; $submitted = strtotime($submitted); if ($data->afs_stats) { $stats = json_decode($data->afs_stats, true); if (!$stats) { $stats = array(); } } else { $stats = array(); } $stats += array( 'user_agent' => '', 'platform' => '', 'tentatives' => '', 'submit_method' => '', // Only present on submissions verified by a reCAPTCHA v3 field. 'recaptcha3_score' => '', ); $ret += array( '__fid' => $data->afs_form_id, '__subid' => $subid, '__pid' => $data->afs_post_id, '__ip' => $data->afs_ip, '__anonymized_ip' => accua_forms_anonymize_ip($data->afs_ip), '__uri' => $data->afs_uri, '__referrer' => $data->afs_referrer, '__lang' => $data->afs_lang, '__created' => $created, '__created_day' => wp_date('l j F Y', $created), '__created_hour' => wp_date('G:i', $created), '__submitted' => $submitted, '__submitted_day' => wp_date('l j F Y', $submitted), '__submitted_hour' => wp_date('G:i', $submitted), '__user_agent' => $stats['user_agent'], '__platform' => $stats['platform'], '__tentatives' => $stats['tentatives'], '__submit_method' => $stats['submit_method'], '__recaptcha3_score' => $stats['recaptcha3_score'], ); } } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Submission values lookup requires direct query $query2 = $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}accua_forms_submissions_values` WHERE afsv_sub_id = %d", $subid ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- $query2 is prepared above, submission values lookup requires direct query $data2 = $wpdb->get_results($query2, OBJECT); foreach ($data2 as $row) { switch ($row->afsv_type) { case 'file' : if ($options['file_format'] == 'url' || $options['file_format'] == 'link') { $fieldid = rawurlencode($row->afsv_field_id); $filename = rawurlencode($row->afsv_value); $url = admin_url('admin-ajax.php') . "?action=accua_forms_download_submitted_file&subid={$row->afsv_sub_id}&field={$fieldid}&file={$filename}&nonce=" . wp_create_nonce('accua_forms_download_nonce') . "&_wpnonce=" . wp_create_nonce('download_file_' . $row->afsv_sub_id . '_' . $fieldid); if(isset($options['token'])){ $url .= '&token='.$options['token']; } if ($options['file_format'] == 'link'){ $url = esc_url($url); $filename = esc_html($row->afsv_value); $fielddata = "{$filename}"; } else { $fielddata = $url; } } else { // $options['file_format'] == 'name' $fielddata = $row->afsv_value; } break; default: $fielddata = $row->afsv_value; } $ret[$row->afsv_field_id] = $fielddata; } return $ret; } add_shortcode( 'accua-form', 'accua_forms_shortcode_handler' ); function accua_forms_shortcode_handler($atts, $content = '', $code = '') { if (empty($atts['fid'])) { return ''; } $fid = $atts['fid']; $form_data = _accua_forms_get_form_data($fid, false); if (! $form_data) { // In preview mode, allow unsaved (new) forms to render using draft + defaults if (! apply_filters('accua_forms_use_draft_for_preview', false)) { return ''; } $default_form_data = get_option('accua_forms_default_form_data', array()); $empty_form_data = _accua_forms_get_form_data(false); $form_data = array('_overrided' => array()) + $default_form_data + $empty_form_data; } // Note: Preview field order override is handled in accua_forms_form_generate() // which applies the filter there for live preview $fid = '__accua-form__'.$fid; $out = ''; if (AccuaForm::getSubmittedID() == $fid) { /* return "
Form submitted.\n\nData: " . print_r(AccuaForm::getSubmittedData(), true) . '
'; */ // Get per-form messages (supports multiple forms on same page) $messages = AccuaForm::getSubmittedMessages($fid); if ($messages && trim($messages) !== '') { $out .= '
'.$messages.'
'; } // Non-AJAX fallback: set URL hash and scroll to result messages on page load. // The anchor elements are only created by the AJAX JS block (which is not output for // non-AJAX forms), so we scroll to the messages div by class instead. $anchor_suffix = preg_replace('/[^a-zA-Z0-9]+/', '_', str_replace('__accua-form__', '', $fid)); $hash_type = AccuaForm::isValid() ? 'formSubmitSuccess' : 'formSubmitInvalid'; $anchor_full = esc_js($hash_type . '-' . $anchor_suffix); $out .= ''; if (AccuaForm::isValid()) { return $out; } $form = AccuaForm::getSubmittedForm(); } else { $analytics_data = get_option('accua_forms_default_analytics_data',array()); // Check for preview layout override (allows live preview of layout changes before save) $layout = $form_data['layout']; $preview_layout_override = apply_filters('accua_forms_preview_layout_override', ''); if ($preview_layout_override) { $layout = $preview_layout_override; } // If layout is empty (meaning "use default"), resolve to the global default layout if (empty($layout)) { $default_form_data = get_option('accua_forms_default_form_data', array()); $layout = !empty($default_form_data['layout']) ? $default_form_data['layout'] : 'sidebyside'; } $params = array( 'layout' => $layout, 'title' => $form_data['title'], 'track_submit' => !empty($analytics_data['analytics_track_submit']), 'track_fields' => !empty($analytics_data['analytics_track_fields']), 'gads_conversion_tracking_code' => $form_data['gads_conversion_tracking_code'], ); $form = AccuaForm::create($fid, $params); } $out .= $form->render(true); $doing_ajax = function_exists('wp_doing_ajax') ? wp_doing_ajax() : (defined( 'DOING_AJAX' ) && DOING_AJAX); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check for Yoast SEO compatibility, strips HTML for preview if ($doing_ajax && isset($_REQUEST['action']) && ($_REQUEST['action'] === 'wpseo_filter_shortcodes')) { $strip_regexp = '/(]*>(.*?)<\/iframe>|]*>(.*?)<\/script>|]*)type="hidden"[^>]*>)/is'; $out = preg_replace($strip_regexp, '', $out); } return $out; } function accua_forms_include($fid, $atts=array(), $content = '', $code = '') { $atts['fid'] = $fid; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Shortcode handler manages its own escaping echo accua_forms_shortcode_handler($atts, $content, $code); } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function, double underscore prefix indicates private function __accua_forms_submissions_list_page(){ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing, actual actions have nonce checks if(isset($_GET['sid'])) { accua_forms_single_submission(); } else { accua_forms_submissions_list_page(); } } function accua_forms_submissions_list_page_load(){ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing for screen option registration if(isset($_GET['sid'])) { // Handle GET-based trash/restore actions here (before any output is sent) require_once __DIR__ . '/admin/single-submission.php'; $sid = (int) $_GET['sid']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified below before processing if ( $sid && isset( $_GET['action'] ) ) { if ( $_GET['action'] === 'trash' ) { check_admin_referer( 'del_sub_form_' . $sid ); accua_forms_trash_submission( $sid ); wp_safe_redirect( admin_url( 'admin.php?page=accua_forms_submissions_list&trashed=1' ) ); exit; } if ( $_GET['action'] === 'restore' ) { check_admin_referer( 'restore_sub_form_' . $sid ); accua_forms_restore_submission( $sid ); wp_safe_redirect( admin_url( 'admin.php?page=accua_forms_submissions_list&restored=1' ) ); exit; } } return; } add_screen_option('per_page', array( 'default' => 100, 'option' => 'accua_forms_submissions_per_page', )); // Set default hidden columns for first-time users: hide the field columns // not flagged "Show in essential columns" on the Fields page (plus the // technical main columns). add_filter( 'default_hidden_columns', function( $hidden ) { $non_essential = [ 'form_id', 'pid', 'created', 'lead_status' ]; $avail_fields = get_option( 'accua_forms_avail_fields', [] ); foreach ( (array) $avail_fields as $slug => $field_data ) { if ( empty( $field_data['essential_column'] ) ) { $non_essential[] = '_field_' . $slug; } } return array_unique( array_merge( $hidden, $non_essential ) ); } ); $screen = get_current_screen(); $screen->add_help_tab( array( 'id' => 'accua_forms_lead_statuses', 'title' => __( 'Lead Statuses', 'contact-forms' ), 'content' => '

' . accua_forms_get_lead_statuses_help() . '

', ) ); // Last, so that the export reads the hidden columns through the // default_hidden_columns filter registered above. accua_forms_submissions_maybe_bulk_export(); } add_filter('set_screen_option_accua_forms_submissions_per_page', function($status, $option, $value) { return (int) $value; }, 10, 3); /** * Stream the ticked submissions as Excel, for the two export bulk actions. * * The other bulk actions are handled by the list table's process_bulk_action(), * which the page calls from admin_head, where the response has already started; * a download has to send its own headers, so this one runs on the load hook * instead. The file itself is written by the same code the two buttons above * the table use, over the same view: the rows are narrowed to the ticked ids, * everything else (filters, search, sort order) comes from the request the * form submitted, so the file is ordered like the screen it was asked for on. * * The current action is read the way WP_List_Table::current_action() reads it * rather than by asking a list table, so nothing is built until an export is * actually being asked for. * * @return void Dies with the file when rows were ticked. Returns otherwise, * leaving process_bulk_action() to report the empty selection. */ function accua_forms_submissions_maybe_bulk_export() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified below, once the action is known to be ours if ( ! empty( $_GET['filter_action'] ) ) { return; } $action = ''; foreach ( array( 'action', 'action2' ) as $key ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified below if ( ! isset( $_GET[ $key ] ) || ! is_string( $_GET[ $key ] ) ) { continue; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified below if ( '-1' !== $_GET[ $key ] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified below $action = sanitize_key( wp_unslash( $_GET[ $key ] ) ); break; } } if ( 'export_visible' !== $action && 'export_all' !== $action ) { return; } if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'contact-forms' ), 403 ); } check_admin_referer( 'bulk-submissions' ); $ids = array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Values are cast to int if ( ! empty( $_GET['submission'] ) && is_array( $_GET['submission'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Value cast to int foreach ( $_GET['submission'] as $i ) { $i = (int) $i; if ( $i > 0 ) { $ids[ $i ] = $i; } } } if ( ! $ids ) { // Nothing ticked: let the page render and say so. return; } require_once __DIR__ . '/admin/submissions-list-page.php'; $listTable = new Accua_Forms_Submissions_List_Table(); $listTable->export_xls = true; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified above $get = stripslashes_deep( $_GET ); $get['accua_export_ids'] = array_values( $ids ); $listTable->prepare_items( true, $get ); $show_col = $listTable->export_column_keys( 'export_visible' === $action ); header( 'Content-disposition: attachment; filename=downloads-report.xls' ); header( 'Content-type: application/vnd.ms-excel' ); accua_forms_submission_page_save_excel_general( $listTable, $show_col ); die( '' ); } function accua_forms_submissions_list_page_head(){ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing, actual actions have nonce checks if(isset($_GET['sid'])) { require_once __DIR__ . '/admin/single-submission.php'; accua_forms_single_submission(true); } else { require_once __DIR__ . '/admin/submissions-list-page.php'; accua_forms_submissions_list_page(true); } } /* Generiamo token di sicurezza per poter accedere anche da anonimo - email */ function accua_forms_generate_download_token($subid) { global $wpdb; $token = wp_generate_password(32, false); // Token casuale di 32 caratteri // Controlla se esiste già un token per questo sub_id // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Token lookup requires direct query $existing_token = $wpdb->get_var($wpdb->prepare( "SELECT afsv_value FROM `{$wpdb->prefix}accua_forms_submissions_values` WHERE afsv_sub_id = %d AND afsv_field_id = '_accua_download_token'", $subid )); if ($existing_token) { return $existing_token; } else{ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Token insert requires direct query $wpdb->insert( $wpdb->prefix . 'accua_forms_submissions_values', [ 'afsv_sub_id' => $subid, 'afsv_field_id' => '_accua_download_token', 'afsv_type' => 'token', 'afsv_value' => $token ], ['%d', '%s', '%s', '%s'] ); return $token; } } function accua_forms_check_download_token($subid, $get_token) { global $wpdb; $subid = (int) $subid; // Cast to integer for security // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Token verification requires direct query $saved_token = $wpdb->get_var($wpdb->prepare( "SELECT afsv_value FROM `{$wpdb->prefix}accua_forms_submissions_values` WHERE afsv_sub_id = %d AND afsv_field_id = '_accua_download_token'", $subid )); // Debug logging for token verification (comment out in production) // error_log("Token check: Submission ID: $subid, Provided token: $get_token, Saved token: $saved_token"); return isset($get_token) && $get_token === $saved_token; } /** * Gestisce il download di un file inviato tramite un modulo. * * Questa funzione viene eseguita tramite una richiesta AJAX e permette agli utenti di scaricare * un file precedentemente caricato con un modulo. Controlla i parametri della richiesta per verificare * la presenza di un file associato a un determinato ID di invio e campo del modulo. * * - Se il parametro "html" è presente, genera una pagina HTML con un link di reindirizzamento automatico. * - Recupera le informazioni del file dal database per verificarne l'esistenza. * - Se il file esiste e può essere letto, restituisce il contenuto con gli appropriati header HTTP. * - Se il file non viene trovato, restituisce un errore 404. * * Sicurezza: * - Nonce * - Utilizza `stripslashes_deep` per sanificare i dati in ingresso. * - Protegge il database utilizzando `wpdb->prepare` per prevenire SQL Injection. * - Determina il tipo MIME del file per un download sicuro. * - Aggiunto token di verifica per utenti */ add_action('wp_ajax_accua_forms_download_submitted_file', 'accua_forms_download_submitted_file'); add_action('wp_ajax_nopriv_accua_forms_download_submitted_file', 'accua_forms_download_submitted_file'); function accua_forms_download_submitted_file(){ $get = stripslashes_deep($_GET); $token_valid = false; $nonce_valid = false; $subid = ''; if(isset($get['subid'])){ $subid = rawurlencode($get['subid']); } // First verify WordPress nonce for CSRF protection (for logged-in users) if (isset($get['_wpnonce']) && wp_verify_nonce($get['_wpnonce'], 'download_file_' . $subid . '_' . $get['field'])) { $nonce_valid = true; } // For backward compatibility with older URL format that use 'nonce' instead of '_wpnonce' if (!$nonce_valid && isset($get['nonce']) && check_ajax_referer('accua_forms_download_nonce', 'nonce', false)) { $nonce_valid = true; } // Check for token-based authentication (for email links and unauthenticated users) if (isset($get['token']) && $subid != '') { if (accua_forms_check_download_token($subid, $get['token']) == 1) { $token_valid = true; } } // If both authentication methods fail, deny access if (!$nonce_valid && !$token_valid) { wp_die(esc_html__('Security check failed.', 'contact-forms'), 403); } // Additional permission check for admin users if(!$token_valid && !$nonce_valid && $subid != ''){ // If neither token nor nonce is valid, check for logged-in admin permissions if (!is_user_logged_in() || !current_user_can('manage_options')) { wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'contact-forms')); } } if (isset($get['subid'],$get['field'],$get['file'])) { if (!empty($get['html'])) { /* export xls*/ header("Content-type: text/html"); $subid = rawurlencode($get['subid']); $fieldid = rawurlencode($get['field']); $filename = rawurlencode($get['file']); $url = admin_url('admin-ajax.php') . "?action=accua_forms_download_submitted_file&subid={$subid}&field={$fieldid}&file={$filename}&nonce=" . wp_create_nonce('accua_forms_download_nonce') . "&_wpnonce=" . wp_create_nonce('download_file_' . $subid . '_' . $fieldid); if(isset($get['token'])){ $url .= '&token='.$get['token']; } $url = esc_url($url); $filename = esc_html($get['file']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $filename and $url are pre-escaped above die("{$filename}{$filename}"); } global $wpdb; $subid = (int) $get['subid']; $field = $get['field']; $file = $get['file']; $query = $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}accua_forms_submissions_values` WHERE afsv_sub_id = %d AND afsv_field_id = %s AND afsv_value = %s", $subid, $field, $file ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- $query is prepared above, file download verification requires direct query $subval = $wpdb->get_results($query, OBJECT); if ($subval) { $file_data = get_option('accua_forms_default_file_field_data',array()) + array('dest_path' => ''); $dest_path = _accua_forms_get_abs_dest_path($file_data['dest_path']); $filename = "{$dest_path}/{$subid}_{$field}_{$file}"; if (is_file($filename) && is_readable($filename)){ if (function_exists('finfo_open')){ @ $finfo = finfo_open(FILEINFO_MIME_TYPE); if ($finfo) { @ $filetype = finfo_file($finfo, $filename); @ finfo_close($finfo); } } if (empty($filetype) && function_exists('mime_content_type')){ @ $filetype = mime_content_type($filename); } if (empty($filetype)) { $filetype = "application/octet-stream"; } // Clean any output buffers to prevent stale content from being sent before the file while (ob_get_level()) { ob_end_clean(); } // Remove all pre-set headers (admin-ajax.php sets Content-Type: text/html early) header_remove(); nocache_headers(); header("Content-Type: $filetype"); header("Content-Length: ".filesize($filename)); if (empty($_GET['view'])) { header("Content-Disposition: attachment; filename=\"$file\""); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- WP_Filesystem not suitable for binary file streaming readfile($filename); exit; } } } header("HTTP/1.0 404 Not Found"); //header("Status: 404 Not Found"); die('File not found'); } function accua_forms_buildConditionalReplacer($map = array()) { if (!class_exists('AccuaConditionalReplacer')){ require_once('AccuaConditionalReplacer.php'); } return new AccuaConditionalReplacer($map); } /** * Prepare the emoji styles for a standalone document built outside the normal * page lifecycle, such as the preview iframes. * * Those render by calling wp_print_styles() directly, which fires the * 'wp_print_styles' action. WordPress 6.4 and later still keep the deprecated * print_emoji_styles() on that action for backward compatibility, and retire it * inside wp_enqueue_emoji_styles(), which runs on wp_enqueue_scripts or * admin_enqueue_scripts. Neither of those runs in an AJAX request, so the * deprecated printer was still attached and every preview refresh logged a * deprecation notice. Doing here what a normal page load would have done keeps * the emoji styles the preview had before and drops the notice. * * A no-op on WordPress 5.9 to 6.3, our declared minimum, where * print_emoji_styles() is current and there is nothing to replace. */ function accua_forms_prepare_emoji_styles_for_preview() { if ( ! function_exists( 'wp_enqueue_emoji_styles' ) ) { return; } remove_action( 'wp_print_styles', 'print_emoji_styles' ); wp_enqueue_emoji_styles(); } add_action('wp_ajax_accua_forms_preview', 'accua_forms_preview'); function accua_forms_preview() { if (!current_user_can('manage_options')){ die (''); } // Check nonce for CSRF protection $nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'accua_forms_preview' ) ) { wp_die( esc_html__( 'Security check failed.', 'contact-forms' ), 403 ); } // Enqueue form styles before printing them accua_form_enqueue_scripts_and_styles(); // Accept temporary layout override for live preview (before save) // This allows real-time preview when user changes layout dropdown $preview_layout = ''; if ( ! empty( $_REQUEST['preview_layout'] ) ) { $layout_input = sanitize_text_field( wp_unslash( $_REQUEST['preview_layout'] ) ); $allowed_layouts = array( 'toplabel', 'sidebyside', 'inlinelabel' ); if ( in_array( $layout_input, $allowed_layouts, true ) ) { $preview_layout = $layout_input; } elseif ( 'default' === $layout_input ) { // 'default' means use the global default layout $default_form_data = get_option( 'accua_forms_default_form_data', array() ); $preview_layout = ! empty( $default_form_data['layout'] ) ? $default_form_data['layout'] : 'sidebyside'; } } // Accept temporary field order for live preview (before save) // This allows preview to show reordered fields without saving to database $preview_order = null; if ( ! empty( $_REQUEST['preview_order'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON decoded and validated below $order_json = wp_unslash( $_REQUEST['preview_order'] ); $preview_order = json_decode( $order_json, true ); if ( json_last_error() !== JSON_ERROR_NONE ) { $preview_order = null; } } // Store the preview layout override in a filter so shortcode handler can use it if ($preview_layout) { add_filter('accua_forms_preview_layout_override', function() use ($preview_layout) { return $preview_layout; }); } // Store the preview order override in a filter so shortcode handler can use it if ($preview_order) { add_filter('accua_forms_preview_order_override', function() use ($preview_order) { return $preview_order; }); } // Signal that we're in admin preview mode - form generator should read from draft add_filter('accua_forms_use_draft_for_preview', '__return_true'); echo ' '; accua_forms_prepare_emoji_styles_for_preview(); wp_print_styles(); wp_print_head_scripts(); echo ''; $preview_fid = isset($_REQUEST['fid']) ? sanitize_text_field(wp_unslash($_REQUEST['fid'])) : ''; // Check if the form has any fields - show placeholder if empty $draft_data = _accua_forms_get_draft_data($preview_fid); if (empty($draft_data['fields'])) { echo '

' . esc_html__('Add fields to the form to see the preview.', 'contact-forms') . '

'; } else { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Shortcode handler manages its own escaping echo accua_forms_shortcode_handler(array('fid' => $preview_fid)); } wp_print_footer_scripts(); echo ''; die(''); } //salvataggio file excel add_action('wp_ajax_accua_forms_submission_page_save_excel', 'accua_forms_submission_page_save_excel'); //add_action('wp_ajax_nopriv_accua_forms_submission_page_save_excel', 'accua_forms_submission_page_save_excel'); function accua_forms_submission_page_save_excel() { if (!current_user_can('manage_options')){ header("HTTP/1.0 401 Access Denied"); //header("Status: 401 Access Denied"); die('You are not authorized to access this page.'); } // Check nonce for CSRF protection if (!isset($_REQUEST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['_wpnonce'])), 'accua_forms_export_excel')) { wp_die(esc_html__('Security check failed.', 'contact-forms'), 403); } require_once __DIR__ . '/admin/submissions-list-page.php'; $listTable = new Accua_Forms_Submissions_List_Table(); $listTable->export_xls = true; $listTable->prepare_items(true); // Sanitize column selection input. Not sanitize_key(): it lowercases, and // column keys carry the field slug verbatim (_field_{slug}), which is // case-sensitive - two fields may differ only in case, so lowercasing here // dropped one of them from the export. $show_col_input = isset($_GET['accua_show_field']) ? sanitize_text_field(wp_unslash($_GET['accua_show_field'])) : ''; $show_col = array_map('accua_forms_sanitize_column_key', explode(',', $show_col_input)); $show_col = array_diff($show_col, array('singlesub')); header('Content-disposition: attachment; filename=downloads-report.xls'); header('Content-type: application/vnd.ms-excel'); accua_forms_submission_page_save_excel_general($listTable,$show_col); die(''); } function accua_forms_submission_page_save_excel_general(Accua_Forms_Submissions_List_Table $listTable,array $show_col,array $options=array()) { global $wpdb; $content_type = 'text/html; charset=' . get_option('blog_charset'); //creo il file excel ?> get_columns(); foreach($cols as $col_key=>$col_value) { if(in_array($col_key, $show_col)) { ?> items as $id_submission=>$single_submission) { // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Required to prevent timeout during large exports @ set_time_limit(10); echo ""; foreach($cols as $col_key=>$col_value) { if(in_array($col_key, $show_col)) { echo '"; } } echo ""; } ?>
'; if ($col_key == 'lead_status') { if (isset($lead_statuses[$single_submission['lead_status']])) { echo esc_html($lead_statuses[$single_submission['lead_status']]); } } elseif(isset($single_submission[$col_key])) { if ( method_exists( $listTable, 'column_' . $col_key ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- List table column methods handle their own escaping echo call_user_func( array( &$listTable, 'column_' . $col_key ), $single_submission ); } else { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- List table column_default handles escaping echo $listTable->column_default( $single_submission, $col_key ); } } echo "
$value) { $field_name = $value['name'] ?? $value['label'] ?? $key; $tokens .= $field_name . ": {" . $key . "}\n"; switch ($value['type']) { case 'file': $tokens .= $field_name . " (download link): {__download_" . $key . "}\n"; break; case 'multiselect': case 'multicheckbox': $tokens .= $field_name . " (labels): {__label_" . $key . "}\n"; break; case 'select': case 'radio': $tokens .= $field_name . " (label): {__label_" . $key . "}\n"; break; case 'post-multicheckbox': $tokens .= $field_name . " (posts titles): {__label_" . $key . "}\n"; $tokens .= $field_name . " (posts ids): {__post_id_" . $key . "}\n"; $tokens .= $field_name . " (posts urls): {__post_url_" . $key . "}\n"; break; case 'post-select': $tokens .= $field_name . " (post title): {__label_" . $key . "}\n"; $tokens .= $field_name . " (post id): {__post_id_" . $key . "}\n"; $tokens .= $field_name . " (post url): {__post_url_" . $key . "}\n"; break; } } // phpcs:disable PluginCheck.CodeAnalysis.Heredoc.NotAllowed, WordPress.Security.EscapeOutput.HeredocOutputNotEscaped -- Heredoc for tokens help HTML echo <<

Tokens

In HTML text, use {!token_name} to insert unfiltered token value

Fields

These tokens are available only if the field is added to the form
$tokens

Generic tokens

{__fid}
{__subid}
{__pid}
{__ip}
{__anonymized_ip}
{__uri}
{__url}
{__referrer}
{__lang}
{__locale}
{__created}
{__created_day}
{__created_day_month_year}
{__created_hour}
{__submitted}
{__submitted_day}
{__submitted_day_month_year}
{__submitted_hour}
{__user_agent}
{__platform}
{__tentatives}
{__submit_method}
{__recaptcha3_score}
{__submitted_txt}
{__submitted_html}
{__submitted_json}
{__autoreply}
{__autoreply_email}
{__confirmation_emails_message}
{__review_submission_url}
EOT; // phpcs:enable PluginCheck.CodeAnalysis.Heredoc.NotAllowed, WordPress.Security.EscapeOutput.HeredocOutputNotEscaped do_action('accua_forms_print_tokens'); } /** * Get posts/pages for post-select fields using get_posts() for WPML compatibility. * * Uses WordPress get_posts() instead of direct SQL to ensure WPML and other * language plugins can filter results to current language automatically. * * Performance considerations: * - Results are cached using transients (5 minute TTL) to reduce database queries * - meta_key/meta_value queries are necessary for filtering by custom fields * - post__not_in is used only when exclude is explicitly requested by admin * - Default limit of 500 posts prevents runaway queries * * Hierarchy handling: * - child_of returns all descendants of the given post; exclude_tree removes a * post and all its descendants. Both are resolved to explicit ID lists via * accua_forms_get_post_descendant_ids() before querying, so they remain * correct with pagination and search. * - hierarchical only affects ordering (parents before children) and only when * the result set is complete and title-sorted; it never drops posts whose * parent is unavailable (e.g. published children of draft parents). * * @since 2.0.0-beta.29 * @param string|array $args Query arguments (backward compatible with old function). * @return array Array of post objects. */ function accua_get_pages($args = '') { // phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- These are function parameter defaults, not actual query execution. $defaults = array( 'child_of' => 0, 'sort_order' => 'ASC', 'sort_column' => 'post_title', 'hierarchical' => 1, 'exclude' => array(), 'include' => array(), 'meta_key' => '', 'meta_value' => '', 'meta_value_lt' => '', 'meta_value_gt' => '', 'meta_value_le' => '', 'meta_value_ge' => '', 'meta_value_like' => '', 'meta_value_format' => 'string', 'authors' => '', 'parent' => -1, 'exclude_tree' => '', 'number' => 500, // Default limit for performance 'offset' => 0, 'post_type' => 'page', 'post_status' => 'publish', 'suppress_filters' => false, // IMPORTANT: Allow WPML to filter by language 's' => '', // Search term (new parameter for AJAX search) ); // phpcs:enable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value $r = wp_parse_args($args, $defaults); // Generate cache key based on arguments and current language $cache_key_data = $r; // Add current language to cache key for WPML/Polylang compatibility if (function_exists('pll_current_language')) { $cache_key_data['_lang'] = pll_current_language(); } elseif (defined('ICL_LANGUAGE_CODE')) { $cache_key_data['_lang'] = ICL_LANGUAGE_CODE; } $cache_key = 'accua_pages_' . md5(wp_json_encode($cache_key_data)); // Check transient cache first (skip for search queries and random ordering) $use_cache = empty($r['s']) && $r['sort_column'] !== 'rand'; if ($use_cache) { $cached = get_transient($cache_key); if ($cached !== false) { return $cached; } } // Validate post type $post_type = $r['post_type']; if (!is_array($post_type)) { $post_type = array_map('trim', explode(',', $post_type)); } $valid_post_types = get_post_types(); $post_type = array_filter($post_type, function($pt) use ($valid_post_types) { return in_array($pt, $valid_post_types, true); }); if (empty($post_type)) { return array(); } // Validate post status $post_status = $r['post_status']; if (!is_array($post_status)) { $post_status = array_map('trim', explode(',', $post_status)); } $valid_statuses = get_post_stati(); $post_status = array_filter($post_status, function($ps) use ($valid_statuses) { return in_array($ps, $valid_statuses, true); }); if (empty($post_status)) { $post_status = array('publish'); } // Map sort_column to orderby $orderby_map = array( 'post_title' => 'title', 'title' => 'title', 'post_date' => 'date', 'date' => 'date', 'post_modified' => 'modified', 'modified' => 'modified', 'menu_order' => 'menu_order', 'post_name' => 'name', 'name' => 'name', 'post_parent' => 'parent', 'parent' => 'parent', 'ID' => 'ID', 'rand' => 'rand', 'comment_count' => 'comment_count', 'post_author' => 'author', 'author' => 'author', ); $sort_column = $r['sort_column']; $orderby = isset($orderby_map[$sort_column]) ? $orderby_map[$sort_column] : 'title'; // Build get_posts arguments $query_args = array( 'post_type' => $post_type, 'post_status' => $post_status, 'orderby' => $orderby, 'order' => strtoupper($r['sort_order']) === 'DESC' ? 'DESC' : 'ASC', 'posts_per_page' => !empty($r['number']) ? (int) $r['number'] : 500, 'offset' => (int) $r['offset'], 'suppress_filters' => (bool) $r['suppress_filters'], ); // Search term if (!empty($r['s'])) { $query_args['s'] = sanitize_text_field($r['s']); } // Include specific posts (overrides other filters) if (!empty($r['include'])) { $include = wp_parse_id_list($r['include']); if (!empty($include)) { $query_args['post__in'] = $include; $query_args['orderby'] = 'post__in'; // Preserve include order } } else { // Exclude posts - only used when admin explicitly configures exclusions. // exclude_tree also removes all descendants of the given post, resolved // against the full tree so it works with pagination and search. $exclude_ids = array(); if (!empty($r['exclude'])) { $exclude_ids = wp_parse_id_list($r['exclude']); } if (!empty($r['exclude_tree'])) { $exclude_tree = (int) $r['exclude_tree']; $exclude_ids = array_merge($exclude_ids, array($exclude_tree), accua_forms_get_post_descendant_ids($exclude_tree, $post_type)); } // Child of: restrict to all descendants of the given post, like core get_pages(). // Resolved to an explicit ID list so it stays correct with pagination and search. if (!empty($r['child_of'])) { $descendant_ids = accua_forms_get_post_descendant_ids((int) $r['child_of'], $post_type); // post__in cannot be combined with post__not_in, so exclusions are applied to the list itself. $descendant_ids = array_values(array_diff($descendant_ids, $exclude_ids)); $query_args['post__in'] = !empty($descendant_ids) ? $descendant_ids : array(0); } elseif (!empty($exclude_ids)) { // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Exclusion is an optional admin-configured feature, not default behavior. $query_args['post__not_in'] = $exclude_ids; } // Parent filter (direct children only) if ((int) $r['parent'] >= 0) { $query_args['post_parent'] = (int) $r['parent']; } // Authors filter if (!empty($r['authors'])) { $author_ids = array(); $post_authors = preg_split('/[\s,]+/', $r['authors']); foreach ($post_authors as $post_author) { $post_author = trim($post_author); if (empty($post_author)) { continue; } if (is_numeric($post_author)) { $author_ids[] = (int) $post_author; } else { $user = get_user_by('login', $post_author); if ($user && !empty($user->ID)) { $author_ids[] = $user->ID; } } } if (!empty($author_ids)) { $query_args['author__in'] = $author_ids; } } // Build meta_query for advanced meta comparisons $meta_query = array(); // Standard meta_key/meta_value - used for filtering posts by custom field. // This is an optional admin-configured feature for advanced post filtering. if (!empty($r['meta_key'])) { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Required for custom field filtering feature. $query_args['meta_key'] = stripslashes($r['meta_key']); if (!empty($r['meta_value'])) { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Required for custom field filtering feature. $query_args['meta_value'] = stripslashes($r['meta_value']); } } // Advanced meta comparisons (lt, gt, le, ge, like) if (!empty($r['meta_key']) && ( !empty($r['meta_value_lt']) || !empty($r['meta_value_gt']) || !empty($r['meta_value_le']) || !empty($r['meta_value_ge']) || !empty($r['meta_value_like']) )) { $meta_key = stripslashes($r['meta_key']); $meta_type = 'CHAR'; switch ($r['meta_value_format']) { case 'int': $meta_type = 'NUMERIC'; break; case 'float': $meta_type = 'DECIMAL'; break; case 'timestamp': $meta_type = 'DATETIME'; break; } if (!empty($r['meta_value_lt'])) { $value = stripslashes($r['meta_value_lt']); if ($r['meta_value_format'] === 'timestamp') { $value = gmdate('Y-m-d H:i:s', strtotime($value)); } $meta_query[] = array( 'key' => $meta_key, 'value' => $value, 'compare' => '<', 'type' => $meta_type, ); } if (!empty($r['meta_value_gt'])) { $value = stripslashes($r['meta_value_gt']); if ($r['meta_value_format'] === 'timestamp') { $value = gmdate('Y-m-d H:i:s', strtotime($value)); } $meta_query[] = array( 'key' => $meta_key, 'value' => $value, 'compare' => '>', 'type' => $meta_type, ); } if (!empty($r['meta_value_le'])) { $value = stripslashes($r['meta_value_le']); if ($r['meta_value_format'] === 'timestamp') { $value = gmdate('Y-m-d H:i:s', strtotime($value)); } $meta_query[] = array( 'key' => $meta_key, 'value' => $value, 'compare' => '<=', 'type' => $meta_type, ); } if (!empty($r['meta_value_ge'])) { $value = stripslashes($r['meta_value_ge']); if ($r['meta_value_format'] === 'timestamp') { $value = gmdate('Y-m-d H:i:s', strtotime($value)); } $meta_query[] = array( 'key' => $meta_key, 'value' => $value, 'compare' => '>=', 'type' => $meta_type, ); } if (!empty($r['meta_value_like'])) { $meta_query[] = array( 'key' => $meta_key, 'value' => stripslashes($r['meta_value_like']), 'compare' => 'LIKE', ); } if (!empty($meta_query)) { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required for advanced meta comparison operators (lt, gt, like, etc.). $query_args['meta_query'] = $meta_query; // Remove simple meta_value if we're using meta_query unset($query_args['meta_value']); } } } // Get posts using WordPress function (WPML automatically filters by current language) $pages = get_posts($query_args); if (empty($pages)) { // Cache empty results too (5 minutes) if ($use_cache) { set_transient($cache_key, array(), 5 * MINUTE_IN_SECONDS); } /** * Filters the list of pages retrieved from accua_get_pages. * * @since 2.0.0-beta.29 * * @param array $pages List of page objects. * @param array $r Arguments passed to accua_get_pages. */ return apply_filters('accua_forms_get_pages', array(), $r); } // Hierarchical (tree) ordering: list parents before their children, like core // get_pages(). Only applied when the result set is complete (no search, no // offset, not truncated by the limit) and title-sorted - reordering a paginated // or filtered slice would drop children whose parent is not in the same slice. if ($r['hierarchical'] && empty($r['s']) && (int) $r['offset'] === 0 && count($pages) < (int) $query_args['posts_per_page'] && $orderby === 'title' && function_exists('get_page_children')) { $tree_ordered = get_page_children((int) $r['child_of'], $pages); if (count($tree_ordered) < count($pages)) { // Posts whose ancestors are not part of the result set (e.g. published // children of a draft parent) go at the end instead of being dropped. $tree_ids = array(); foreach ($tree_ordered as $page) { $tree_ids[$page->ID] = true; } foreach ($pages as $page) { if (!isset($tree_ids[$page->ID])) { $tree_ordered[] = $page; } } } $pages = $tree_ordered; } // Cache results for 5 minutes to improve performance if ($use_cache) { set_transient($cache_key, $pages, 5 * MINUTE_IN_SECONDS); } /** This filter is documented above */ return apply_filters('accua_forms_get_pages', $pages, $r); } /** * Get the IDs of all descendants of a post by traversing the post_parent tree. * * Used to resolve the child_of and exclude_tree arguments of accua_get_pages() * to an explicit ID list, so the main query stays correct with pagination and * search. Traverses posts of any status so that e.g. a published grandchild of * a draft child is still found (the main query applies its own status filter). * * @since 2.2.27 * @param int $parent_id Root post ID (not included in the result). * @param string|array $post_type Post type(s) to traverse. * @return int[] Descendant post IDs. */ function accua_forms_get_post_descendant_ids($parent_id, $post_type) { $descendant_ids = array(); $level = array((int) $parent_id); // Depth guard: hierarchies deeper than 25 levels are treated as data corruption (parent loops). for ($depth = 0; $depth < 25 && !empty($level); $depth++) { $children = get_posts(array( 'post_type' => $post_type, 'post_status' => 'any', 'post_parent__in' => $level, 'posts_per_page' => -1, 'fields' => 'ids', // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- Structural traversal: do not let language plugins hide ancestors (also get_posts()'s default). 'suppress_filters' => true, 'orderby' => 'ID', 'order' => 'ASC', )); $children = array_map('intval', array_diff($children, $descendant_ids, array((int) $parent_id))); $descendant_ids = array_merge($descendant_ids, $children); $level = $children; } return $descendant_ids; } // phpcs:disable WordPress.DB.DirectDatabaseQuery function accua_forms_trash_submission($id_sub){ global $wpdb; return $wpdb->query($wpdb->prepare("UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_status = -1 WHERE afs_id = %d", $id_sub)) !== FALSE; } function accua_forms_restore_submission($id_sub){ global $wpdb; return $wpdb->query($wpdb->prepare("UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_status = 0 WHERE afs_id = %d", $id_sub)) !== FALSE; } // phpcs:enable WordPress.DB.DirectDatabaseQuery /** * Clear accua_get_pages cache when posts are modified. * * Called when posts are created, updated, deleted, or have status changed. * This ensures that post-select dropdowns always show fresh data. * * @since 2.0.0-beta.29 * @param int $post_id Post ID that was modified. */ function accua_forms_clear_pages_cache($post_id = 0) { global $wpdb; // Delete all transients that start with 'accua_pages_' // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query required to delete transients by prefix, and we're clearing cache not reading data. $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_accua_pages_%' OR option_name LIKE '_transient_timeout_accua_pages_%'"); } // Clear cache when posts are modified add_action('save_post', 'accua_forms_clear_pages_cache'); add_action('delete_post', 'accua_forms_clear_pages_cache'); add_action('trash_post', 'accua_forms_clear_pages_cache'); add_action('untrash_post', 'accua_forms_clear_pages_cache'); function accua_forms_get_lead_statuses() { static $statuses = NULL; if ($statuses === NULL) { $statuses = array( 0 => __('Undefined', 'contact-forms'), -1 => __('Spam', 'contact-forms'), 1 => __('Job Candidate', 'contact-forms'), 2 => __('Lead', 'contact-forms'), 3 => __('Prospect', 'contact-forms'), 4 => __('Opportunity', 'contact-forms'), 5 => __('Customer', 'contact-forms'), 6 => __('Supplier', 'contact-forms'), 7 => __('Other', 'contact-forms'), ); } return $statuses; } /** * Get lead statuses help text (used in toggletip and Help Tab). */ function accua_forms_get_lead_statuses_help() { return '' . esc_html__( 'Spam', 'contact-forms' ) . ' - ' . esc_html__( 'All submissions that can be discarded immediately, including submission tests', 'contact-forms' ) . '
' . '' . esc_html__( 'Job Candidate', 'contact-forms' ) . ' - ' . esc_html__( 'Includes spontaneous and specific job applications', 'contact-forms' ) . '
' . '' . esc_html__( 'Lead', 'contact-forms' ) . ' - ' . esc_html__( 'Unclear (general info request)', 'contact-forms' ) . '
' . '' . esc_html__( 'Prospect', 'contact-forms' ) . ' - ' . esc_html__( 'A qualified lead passed to Sales', 'contact-forms' ) . '
' . '' . esc_html__( 'Opportunity', 'contact-forms' ) . ' - ' . esc_html__( 'Quote / Pricing request that must be followed up', 'contact-forms' ) . '
' . '' . esc_html__( 'Customer', 'contact-forms' ) . ' - ' . esc_html__( 'Has already purchased', 'contact-forms' ) . '
' . '' . esc_html__( 'Supplier', 'contact-forms' ) . ' - ' . esc_html__( 'Contact whose role is or can only be supplier of goods and services', 'contact-forms' ) . '
' . '' . esc_html__( 'Other', 'contact-forms' ) . ' - ' . esc_html__( 'Contact is valid but not within lead generation', 'contact-forms' ); } function accua_forms_select_lead_status($subid, $original_lead_status) { $subid = absint( $subid ); // (int), not absint(): -1 is the Spam lead status and must not collapse onto 1. $original_lead_status = (int) $original_lead_status; $nonce = esc_attr( wp_json_encode( wp_create_nonce( "set_lead_status_$subid" ) ) ); $ret = ''; return $ret; } add_action( 'wp_ajax_accua-forms-set-lead-status' , 'accua_forms_set_lead_status'); function accua_forms_set_lead_status() { if (!current_user_can('manage_options')){ wp_die(0, 403); } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification happens after subid is extracted via check_ajax_referer() $post = $_POST + array( 'subid' => 0, 'lead_status' => 0, ); $subid = (int) $post['subid']; if ($subid) { check_ajax_referer("set_lead_status_$subid", '_nonce_set_lead_status'); $lead_status = (int) $post['lead_status']; $statuses = accua_forms_get_lead_statuses(); if (isset($statuses[$lead_status])) { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery $ret = $wpdb->update( "{$wpdb->prefix}accua_forms_submissions", array('afs_lead_status' => $lead_status), array('afs_id' => $subid), array('%d'), array('%d') ); // phpcs:enable WordPress.DB.DirectDatabaseQuery if ($ret !== FALSE) { wp_die(1); } } } wp_die(0, 500); } add_action( 'wp_ajax_accua-forms-add-note', 'accua_forms_ajax_add_note' ); function accua_forms_ajax_add_note() { $sub_id = isset( $_POST['subid'] ) ? (int) $_POST['subid'] : 0; $text = isset( $_POST['text'] ) ? sanitize_textarea_field( wp_unslash( $_POST['text'] ) ) : ''; if ( ! $sub_id || ! $text ) { wp_send_json_error(); } check_ajax_referer( "submission_{$sub_id}_note_add", '_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error(); } require_once __DIR__ . '/admin/single-submission.php'; $result = accua_forms_add_submission_note( $sub_id, $text ); if ( ! $result ) { wp_send_json_error(); } $result['del_nonce'] = wp_create_nonce( "submission_{$sub_id}_note_del" ); wp_send_json_success( $result ); } add_action( 'wp_ajax_accua-forms-delete-note', 'accua_forms_ajax_delete_note' ); function accua_forms_ajax_delete_note() { $sub_id = isset( $_POST['subid'] ) ? (int) $_POST['subid'] : 0; $date = isset( $_POST['date'] ) ? sanitize_text_field( wp_unslash( $_POST['date'] ) ) : ''; if ( ! $sub_id || ! $date ) { wp_send_json_error(); } check_ajax_referer( "submission_{$sub_id}_note_del", '_nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error(); } require_once __DIR__ . '/admin/single-submission.php'; if ( ! accua_forms_delete_submission_note( $sub_id, $date ) ) { wp_send_json_error(); } wp_send_json_success(); }