add_help_tab(array( 'id' => 'accua_help_tab', 'title' => __('Contact Forms Dashboard', 'contact-forms'), 'content' => '

' . __('Marketing tools for WordPress', 'contact-forms') . '

', )); wp_enqueue_script( 'accua-forms-expandable-cells', plugins_url( 'assets/js/admin/expandable-cells.js', ACCUA_FORMS_FILE ), array(), ACCUA_FORMS_JS_VERSION, true ); wp_enqueue_script('accua-forms-set-lead-status', plugins_url('assets/js/admin/set-lead-status.js', ACCUA_FORMS_FILE), array('jquery'), ACCUA_FORMS_JS_VERSION, true); wp_enqueue_script('accua_chart_js', plugins_url('assets/vendor/chartjs/chart.umd.min.js', ACCUA_FORMS_FILE), array('jquery'), ACCUA_FORMS_JS_VERSION, true); wp_enqueue_style('accua-forms-admin', plugins_url('assets/css/admin.css', ACCUA_FORMS_FILE), array(), ACCUA_FORMS_CSS_VERSION); /* Tentativo x drag and drop $page_hook_id = fx_smb_setings_page_id(); /* Load the JavaScript needed for the settings screen. * / add_action( 'admin_enqueue_scripts', 'fx_smb_enqueue_scripts' ); add_action( "admin_footer-{$page_hook_id}", 'fx_smb_footer_scripts' ); /* Set number of column available. * / add_filter( 'screen_layout_columns', 'fx_smb_screen_layout_column', 10, 2 ); */ } /** * Add Contact Forms dashboard widget. * * Only visible to users with the 'manage_options' capability, the same * capability required by the plugin admin pages. wp_dashboard_setup only * fires on the dashboard screen, and the widget code is loaded on demand * here, so admin/dashboard-widget.php is never included on the frontend * or on other admin pages. */ add_action('wp_dashboard_setup', 'accua_contact_forms_dashboard_add_widgets'); function accua_contact_forms_dashboard_add_widgets() { if (!current_user_can('manage_options')) { return; } require_once ACCUA_FORMS_DIR . '/admin/dashboard-widget.php'; wp_add_dashboard_widget('accua_contact_forms_dashboard_widget_news', __('Contact Forms', 'contact-forms'), 'accua_contact_forms_dashboard_widget_news_handler'); } function accua_forms_dashboard_page() { require_once ACCUA_FORMS_DIR . '/admin/dashboard-page.php'; return _accua_forms_dashboard_page(); } register_activation_hook(ACCUA_FORMS_FILE, 'accua_forms_install'); function accua_forms_install() { static $ran = false; if ( $ran ) { return; } $ran = true; $modified = false; $keys = get_option('accua_form_api_keys', array()); if (!isset($keys['hash'])) { $modified = true; $keys['hash'] = wp_generate_password(64, true, true); } if (!isset($keys['aes'])) { $modified = true; $keys['aes'] = wp_generate_password(64, true, true); } if ($modified) { update_option('accua_form_api_keys', $keys); } global $wpdb; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); $charset_collate = ''; if (! empty($wpdb->charset)) $charset_collate = "DEFAULT CHARACTER SET $wpdb->charset"; if (! empty($wpdb->collate)) $charset_collate .= " COLLATE $wpdb->collate"; $old_db_version = (int) get_option('accua_forms_db_version', 0); if ($old_db_version < 3) { $wpdb_suppress_errors_status = $wpdb->suppress_errors(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Schema migration, runs once $wpdb->query("ALTER TABLE `{$wpdb->prefix}accua_forms_submissions` DROP INDEX uri"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Schema migration, runs once $wpdb->query("ALTER TABLE `{$wpdb->prefix}accua_forms_submissions` DROP INDEX referrer"); $wpdb->suppress_errors($wpdb_suppress_errors_status); } $sql = "CREATE TABLE `{$wpdb->prefix}accua_forms_submissions` ( afs_id BIGINT(20) NOT NULL AUTO_INCREMENT, afs_form_id VARCHAR(77) NOT NULL DEFAULT '', afs_post_id BIGINT(20) NOT NULL DEFAULT 0, afs_ip VARCHAR(255) NOT NULL DEFAULT '', afs_uri TEXT NOT NULL, afs_referrer TEXT NOT NULL, afs_lang VARCHAR(60) NOT NULL DEFAULT '', afs_created TIMESTAMP NOT NULL DEFAULT 0, afs_submitted TIMESTAMP NOT NULL DEFAULT 0, afs_status TINYINT(1) NOT NULL DEFAULT 0, afs_anonymized TINYINT(1) NOT NULL DEFAULT 0, afs_stats TEXT NOT NULL, afs_lead_status TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (afs_id), KEY form (afs_form_id, afs_status), KEY uri (afs_uri(190)), KEY pid (afs_post_id), KEY referrer (afs_referrer(190)), KEY status (afs_status, afs_id), KEY submitted (afs_submitted), KEY lead_status (afs_lead_status, afs_status) ) $charset_collate;"; dbDelta($sql); $sql = "CREATE TABLE `{$wpdb->prefix}accua_forms_submissions_values` ( afsv_sub_id BIGINT(20) NOT NULL, afsv_field_id VARCHAR(77) NOT NULL, afsv_type varchar(255) NOT NULL DEFAULT '', afsv_value TEXT NOT NULL, PRIMARY KEY (afsv_sub_id, afsv_field_id), KEY value_index (afsv_field_id(77), afsv_value(114)) ) $charset_collate;"; dbDelta($sql); /* creazione tabella per le note dell'utente sono relative a una compilazione possono esserci più note per ogni compilazione in date diverse */ $sql = "CREATE TABLE `{$wpdb->prefix}accua_forms_submissions_notes` ( afsn_sub_id BIGINT(20) NOT NULL, afsn_date TIMESTAMP NOT NULL DEFAULT 0, afsn_text TEXT NOT NULL, afsn_user VARCHAR(100) NOT NULL DEFAULT '', PRIMARY KEY (afsn_sub_id, afsn_date), KEY afsn_sub_id (afsn_sub_id) ) $charset_collate;"; dbDelta($sql); // DB 18: make field slugs case-sensitive in the submission values table. // Runs right after dbDelta, so a fresh install (version 0) is converted at // creation time too. // // Deliberately NOT gated on the stored DB version: dbDelta above rewrites // afsv_field_id back to the table's default (case-insensitive) collation // whenever it runs, because the column no longer matches the definition it // compares against. install() runs on every version change AND on every // activation, so a version check here would leave the column case-insensitive // again after the next update or a deactivate/reactivate - with the values of // two case-differing slugs silently colliding once more. The conversion is // idempotent and returns early when the collation is already binary, so // re-asserting it on each run costs one information_schema lookup. accua_forms_make_field_slugs_case_sensitive(); // Data migration: run after dbDelta so new columns exist if ($old_db_version < 14) { // Migrate: afs_status=-2 (old "anonymized" status) → afs_anonymized=1 + restore original active status // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time migration $wpdb->query("UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_anonymized = 1, afs_status = 0 WHERE afs_status = -2"); // Migrate saved forms from 1.x format to 2.0 format $saved_forms = get_option('accua_forms_saved_forms', array()); if (is_array($saved_forms)) { $forms_updated = false; foreach ($saved_forms as $fid => &$form) { // Rename legacy 'name' key to 'title' (changed in 2.0) if (isset($form['name']) && !isset($form['title'])) { $form['title'] = $form['name']; unset($form['name']); $forms_updated = true; } // Convert 'fields' from comma-separated string to array of instances. // Keyed by instance id, exactly as the form editor writes them: the // submission handler looks each posted field up as // $form_data['fields'][$name] (accua-forms.php), so an instance stored // under a numeric key is never found and its value is dropped. if (isset($form['fields']) && is_string($form['fields'])) { $field_slugs = array_filter(array_map('trim', explode(',', $form['fields']))); $form['fields'] = array(); foreach ($field_slugs as $slug) { $istance_id = accua_forms_unique_istance_id($slug, $form['fields']); $form['fields'][$istance_id] = array( 'version' => 2, 'istance_id' => $istance_id, 'widget_number' => '', 'ref' => $slug, 'required' => false, ); } unset($form['fieldnum']); $forms_updated = true; } } unset($form); if ($forms_updated) { update_option('accua_forms_saved_forms', $saved_forms); } } } // DB 19: repair field instances an earlier 1.x -> 2.0 migration left unusable. // That conversion (in 2.0 through 2.3.0) appended array('ref' => $slug) with // no instance id, so the instances ended up under numeric keys. Such a form // still renders - the render loop reads 'ref' - but the submission handler // matches each posted field by array key, finds nothing, and stores the // submission with none of its values. Silent, and only on forms that came // from a pre-2.0 install, which is why it went unnoticed for so long. if ($old_db_version < 19) { accua_forms_repair_field_instances('accua_forms_saved_forms'); // Trashed forms can be restored, so they must come back working too. accua_forms_repair_field_instances('accua_forms_trash_forms'); } // An unreadable field list must never be silently replaced by the defaults: // try to recover it first, and keep a copy of the original either way. accua_forms_recover_avail_fields(); $avail_fields = get_option('accua_forms_avail_fields', array()); if (!is_array($avail_fields)) { $avail_fields = array(); } if (empty($avail_fields) || ($old_db_version === 0)) { $avail_fields += array( // essential_column: shown by the submissions list "Essential Columns" // button - sensible defaults for a contact form: who wrote (name, // email) and what they wrote (message). 'first_name' => array( 'id' => 'first_name', 'name' => 'First Name', 'type' => 'textfield', 'description' => '', 'default_value' => '', 'allowed_values' => '', 'essential_column' => 1, ), 'last_name' => array( 'id' => 'last_name', 'name' => 'Last Name', 'type' => 'textfield', 'description' => '', 'default_value' => '', 'allowed_values' => '', 'essential_column' => 1, ), 'email' => array( 'id' => 'email', 'name' => 'Email', 'type' => 'autoreply_email', 'description' => '', 'default_value' => '', 'allowed_values' => '', 'essential_column' => 1, ), 'address' => array( 'id' => 'address', 'name' => 'Address', 'type' => 'textfield', 'description' => '', 'default_value' => '', 'allowed_values' => '', ), 'city' => array( 'id' => 'city', 'name' => 'City', 'type' => 'textfield', 'description' => '', 'default_value' => '', 'allowed_values' => '', ), 'state_province' => array( 'id' => 'state_province', 'name' => 'State/Province', 'type' => 'textfield', 'description' => '', 'default_value' => '', 'allowed_values' => '', ), 'country' => array( 'id' => 'country', 'name' => 'Country', 'type' => 'select', 'description' => '', 'default_value' => '-', 'allowed_values' => "-|Select... Afghanistan Åland Islands Albania Algeria American Samoa Andorra Angola Anguilla Antarctica Antigua And Barbuda Argentina Armenia Aruba Australia Austria Azerbaijan Bahamas Bahrain Bangladesh Barbados Belarus Belgium Belize Benin Bermuda Bhutan Bolivia Bosnia And Herzegovina Botswana Bouvet Island Brazil British Indian Ocean Territory Brunei Darussalam Bulgaria Burkina Faso Burundi Cambodia Cameroon Canada Cape Verde Cayman Islands Central African Republic Chad Chile China Christmas Island Cocos (Keeling) Islands Colombia Comoros Congo Congo, The Democratic Republic Of The Cook Islands Costa Rica Côte D'Ivoire Croatia Cuba Cyprus Czech Republic Denmark Djibouti Dominica Dominican Republic Ecuador Egypt El Salvador Equatorial Guinea Eritrea Estonia Ethiopia Falkland Islands (Malvinas) Faroe Islands Fiji Finland France French Guiana French Polynesia French Southern Territories Gabon Gambia Georgia Germany Ghana Gibraltar Greece Greenland Grenada Guadeloupe Guam Guatemala Guernsey Guinea Guinea-Bissau Guyana Haiti Heard Island And Mcdonald Islands Holy See (Vatican City State) Honduras Hong Kong Hungary Iceland India Indonesia Iran, Islamic Republic Of Iraq Ireland Isle Of Man Israel Italy Jamaica Japan Jersey Jordan Kazakhstan Kenya Kiribati Korea, Democratic People'S Republic Of Korea, Republic Of Kuwait Kyrgyzstan Lao People'S Democratic Republic Latvia Lebanon Lesotho Liberia Libyan Arab Jamahiriya Liechtenstein Lithuania Luxembourg Macao Macedonia, The Former Yugoslav Republic Of Madagascar Malawi Malaysia Maldives Mali Malta Marshall Islands Martinique Mauritania Mauritius Mayotte Mexico Micronesia, Federated States Of Moldova, Republic Of Monaco Mongolia Montenegro Montserrat Morocco Mozambique Myanmar Namibia Nauru Nepal Netherlands Netherlands Antilles New Caledonia New Zealand Nicaragua Niger Nigeria Niue Norfolk Island Northern Mariana Islands Norway Oman Pakistan Palau Palestinian Territory, Occupied Panama Papua New Guinea Paraguay Peru Philippines Pitcairn Poland Portugal Puerto Rico Qatar Réunion Romania Russian Federation Rwanda Saint Barthélemy Saint Helena Saint Kitts And Nevis Saint Lucia Saint Martin Saint Pierre And Miquelon Saint Vincent And The Grenadines Samoa San Marino Sao Tome And Principe Saudi Arabia Senegal Serbia Seychelles Sierra Leone Singapore Slovakia Slovenia Solomon Islands Somalia South Africa South Georgia And The South Sandwich Islands Spain Sri Lanka Sudan Suriname Svalbard And Jan Mayen Swaziland Sweden Switzerland Syrian Arab Republic Taiwan, Province Of China Tajikistan Tanzania, United Republic Of Thailand Timor-Leste Togo Tokelau Tonga Trinidad And Tobago Tunisia Turkey Turkmenistan Turks And Caicos Islands Tuvalu Uganda Ukraine United Arab Emirates United Kingdom United States United States Minor Outlying Islands Uruguay Uzbekistan Vanuatu Venezuela, Bolivarian Republic Of Viet Nam Virgin Islands, British Virgin Islands, U.S. Wallis And Futuna Western Sahara Yemen Zambia Zimbabwe", ), 'message' => array( 'id' => 'message', 'name' => 'Message', 'type' => 'textarea', 'description' => '', 'default_value' => '', 'allowed_values' => '', 'essential_column' => 1, ), 'captcha' => array( 'id' => 'captcha', 'name' => 'Captcha', 'type' => 'captcha', 'description' => '', 'default_value' => '', 'allowed_values' => '', ), 'turnstile' => array( 'id' => 'turnstile', 'name' => 'Turnstile', 'type' => 'turnstile', 'description' => '', 'default_value' => '', 'allowed_values' => '', ), 'captcha_v3' => array( 'id' => 'captcha_v3', 'name' => 'Captcha (reCAPTCHA v3)', 'type' => 'captcha_v3', 'description' => '', 'default_value' => '', 'allowed_values' => '', ), 'cap' => array( 'id' => 'cap', 'name' => 'Captcha (Cap)', 'type' => 'cap', 'description' => '', 'default_value' => '', 'allowed_values' => '', ), ); update_option('accua_forms_avail_fields', $avail_fields); } // Add Turnstile field for existing installations (backward compatibility) // Only add if it doesn't exist yet if (!isset($avail_fields['turnstile'])) { $avail_fields['turnstile'] = array( 'id' => 'turnstile', 'name' => 'Turnstile', 'type' => 'turnstile', 'description' => '', 'default_value' => '', 'allowed_values' => '', ); update_option('accua_forms_avail_fields', $avail_fields); } // Add reCAPTCHA v3 field for existing installations (backward compatibility) // Only add if it doesn't exist yet if (!isset($avail_fields['captcha_v3'])) { $avail_fields['captcha_v3'] = array( 'id' => 'captcha_v3', 'name' => 'Captcha (reCAPTCHA v3)', 'type' => 'captcha_v3', 'description' => '', 'default_value' => '', 'allowed_values' => '', ); update_option('accua_forms_avail_fields', $avail_fields); } // Add Cap field for existing installations (backward compatibility) // Only add if it doesn't exist yet if (!isset($avail_fields['cap'])) { $avail_fields['cap'] = array( 'id' => 'cap', 'name' => 'Captcha (Cap)', 'type' => 'cap', 'description' => '', 'default_value' => '', 'allowed_values' => '', ); update_option('accua_forms_avail_fields', $avail_fields); } // Add Telephone field for existing installations (backward compatibility) // Only add if it doesn't exist yet if (!isset($avail_fields['telephone'])) { $avail_fields['telephone'] = array( 'id' => 'telephone', 'name' => 'Telephone', 'type' => 'telephone', 'description' => '', 'default_value' => '+39 ', 'allowed_values' => '', ); update_option('accua_forms_avail_fields', $avail_fields); } // 2.2.47 (DB 17): per-field "Show in essential columns" flag. Existing // installations keep the previous behavior of the submissions list // "Essential Columns" button, whose hardcoded list contained one field // column: email. Guarded by isset so a later uncheck (the Fields page save // always writes the key) is never overridden on reactivation; fresh // installs already flag email (and name/message) in the defaults above. if ($old_db_version < 17 && isset($avail_fields['email']) && !isset($avail_fields['email']['essential_column'])) { $avail_fields['email']['essential_column'] = 1; update_option('accua_forms_avail_fields', $avail_fields); } $form_data = get_option('accua_forms_default_form_data', array()); if (!is_array($form_data)) { $form_data = array(); } // Use the centralized defaults function $form_data += accua_forms_get_default_form_data(); update_option('accua_forms_default_form_data', $form_data); update_option('accua_forms_db_version', ACCUA_FORMS_DB_VERSION); } /** * An instance id that is free in this form's field list. * * Field instances are keyed by their instance id, which is normally the field * slug; a form may hold the same field twice, so a taken slug gets a numeric * suffix the way the form editor's own widget numbering does. * * @since 2.3.0 * @param string $slug Field slug the instance refers to. * @param array $fields Instances already placed in this form. * @return string */ function accua_forms_unique_istance_id($slug, $fields) { if (!isset($fields[$slug])) { return $slug; } $n = 2; while (isset($fields[$slug . '-' . $n])) { $n++; } return $slug . '-' . $n; } /** * Give every field instance of every form a usable instance id. * * Instances converted from the pre-2.0 format carry only a 'ref' and sit under * a numeric key, which makes the submission handler drop their values (it looks * each posted field up by array key). Rebuilds those keyed by instance id and * leaves everything else exactly as it is - a normally saved form is untouched, * so this is safe to run over any install and is idempotent. * * @since 2.3.0 * @param string $option Option holding forms ('accua_forms_saved_forms' or the trash). * @return int Number of forms repaired. */ function accua_forms_repair_field_instances($option) { $forms = get_option($option, array()); if (!is_array($forms)) { return 0; } $repaired = 0; foreach ($forms as $fid => $form) { if (empty($form['fields']) || !is_array($form['fields'])) { continue; } // Nothing to do unless some instance is missing its id. $needs_repair = false; foreach ($form['fields'] as $key => $istance_data) { if (!is_array($istance_data) || empty($istance_data['istance_id'])) { $needs_repair = true; break; } } if (!$needs_repair) { continue; } $rebuilt = array(); foreach ($form['fields'] as $key => $istance_data) { // A bare slug instead of an instance: another shape old data turns up in, // and recoverable, so it is rebuilt rather than dropped. if (is_string($istance_data) && '' !== $istance_data) { $istance_data = array('ref' => $istance_data); } if (!is_array($istance_data)) { continue; } if (!empty($istance_data['istance_id'])) { // Already valid: keep it under its own instance id. $rebuilt[$istance_data['istance_id']] = $istance_data; continue; } // The slug is in 'ref'; a numeric key carries no information, but a // string key was the instance id in some hand-edited data, so prefer it. $ref = !empty($istance_data['ref']) ? $istance_data['ref'] : (is_string($key) ? $key : ''); if ('' === $ref) { continue; } $istance_id = accua_forms_unique_istance_id(is_string($key) ? $key : $ref, $rebuilt); $rebuilt[$istance_id] = array( 'version' => 2, 'istance_id' => $istance_id, 'widget_number' => '', 'ref' => $ref, 'required' => !empty($istance_data['required']), ) + $istance_data; } $forms[$fid]['fields'] = $rebuilt; $repaired++; } if ($repaired) { update_option($option, $forms); } return $repaired; } /** * Repair the string lengths of a serialized value whose text was altered * without updating them. * * The usual cause is a migration or search-and-replace tool that rewrote the * option text (classically normalising CRLF to LF) without recomputing the * `s::` headers. PHP then refuses the whole structure and unserialize() * returns false, so the value reads as missing even though the data is intact. * * Each declared length is trusted first and only shortened when it does not * land on the closing quote, so a healthy value is returned untouched. * * @since 2.3.0 * @param string $data Serialized value. * @return string|false Repaired serialized string, or false if it cannot be walked. */ function accua_forms_fix_serialized_lengths( $data ) { $out = ''; $i = 0; $len = strlen( $data ); while ( $i < $len ) { if ( 's:' === substr( $data, $i, 2 ) && preg_match( '/^s:(\d+):"/', substr( $data, $i, 24 ), $m ) ) { $declared = (int) $m[1]; $start = $i + strlen( $m[0] ); $actual = null; // The declared length wins when it is correct; otherwise take the // longest shorter run that still terminates the string properly. for ( $try = $declared; $try >= 0; $try-- ) { if ( '";' === substr( $data, $start + $try, 2 ) ) { $actual = $try; break; } } if ( null === $actual ) { return false; } $string = substr( $data, $start, $actual ); $out .= 's:' . strlen( $string ) . ':"' . $string . '";'; $i = $start + $actual + 2; continue; } $out .= $data[ $i ]; ++$i; } return $out; } /** * Recover accua_forms_avail_fields when PHP cannot unserialize it. * * accua_forms_install() treats an unreadable field list as an empty one and * writes the shipped defaults over it, which would discard every field the * site had defined - labels, options and all - with no way back. That only * bites on an upgrade, because the install routine runs when the stored DB * version changes, which is exactly when a site carrying such a value meets * it. Observed in the wild on a site whose option had lost its carriage * returns while keeping the original lengths. * * The repaired value is accepted only when it unserializes into something that * actually looks like a field list, and the untouched original is copied to * accua_forms_avail_fields_corrupt_backup either way, so nothing is lost even * when the repair does not work. * * @since 2.3.0 * @return bool True when a broken value was recovered. */ function accua_forms_recover_avail_fields() { global $wpdb; $stored = get_option( 'accua_forms_avail_fields', array() ); if ( is_array( $stored ) ) { return false; // Healthy, or genuinely absent. } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reading the raw row on purpose: get_option() cannot return a value it fails to unserialize $raw = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", 'accua_forms_avail_fields' ) ); if ( ! is_string( $raw ) || '' === $raw || 0 !== strpos( $raw, 'a:' ) ) { return false; } // Keep the original before anything else touches it. if ( false === get_option( 'accua_forms_avail_fields_corrupt_backup' ) ) { add_option( 'accua_forms_avail_fields_corrupt_backup', $raw, '', 'no' ); } $fixed = accua_forms_fix_serialized_lengths( $raw ); if ( false === $fixed ) { return false; } $recovered = @unserialize( $fixed ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- A failed repair is an expected outcome, handled below if ( ! is_array( $recovered ) || empty( $recovered ) ) { return false; } // Only accept something shaped like a field list. foreach ( $recovered as $slug => $definition ) { if ( ! is_string( $slug ) || ! is_array( $definition ) || ! isset( $definition['type'] ) ) { return false; } } update_option( 'accua_forms_avail_fields', $recovered ); return true; } /** * Give afsv_field_id the binary collation of its own charset, so field slugs * that differ only in case are distinct values. * * The table is created with the site's default (case-insensitive) collation, * under which 'role' and 'Role' are the same string to MySQL. Two field * definitions differing only in case are perfectly legal - the slugs live in a * PHP array, where keys are case-sensitive - and the consequences in the * database were severe: the submissions list built its field columns with * SELECT DISTINCT afsv_field_id, which collapsed the pair into a single * column, and the PRIMARY KEY (afsv_sub_id, afsv_field_id) treated the second * field of a submission as a duplicate and rejected its value outright, losing * it silently. * * Converting only this column keeps its indexes usable (no per-query COLLATE, * which would prevent the index from being used) and leaves stored values * untouched. The conversion is also safe in this direction: values that were * equal under the case-insensitive collation could never both exist, so * relaxing the comparison cannot produce a duplicate key. * * An explicit ALTER TABLE is used because dbDelta does not compare collations; * WordPress core changes collations the same way (maybe_convert_table_to_utf8mb4). * * @since 2.3.0 * @return bool True when the column ends up case-sensitive, false otherwise. */ function accua_forms_make_field_slugs_case_sensitive() { global $wpdb; $table = $wpdb->prefix . 'accua_forms_submissions_values'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema inspection, runs once per upgrade $column = $wpdb->get_row($wpdb->prepare( "SELECT CHARACTER_SET_NAME, COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = 'afsv_field_id'", $table )); if (!$column || empty($column->CHARACTER_SET_NAME)) { return false; } // Already case-sensitive (binary collation): nothing to do. if (!empty($column->COLLATION_NAME) && substr($column->COLLATION_NAME, -4) === '_bin') { return true; } $collation = $column->CHARACTER_SET_NAME . '_bin'; // The collation name goes into the ALTER unquoted, so accept it only when the // server itself reports it as a real collation of that charset. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema inspection, runs once per upgrade $known = $wpdb->get_var($wpdb->prepare( "SELECT COLLATION_NAME FROM information_schema.COLLATIONS WHERE COLLATION_NAME = %s AND CHARACTER_SET_NAME = %s", $collation, $column->CHARACTER_SET_NAME )); if (!$known || !preg_match('/^[A-Za-z0-9_]+$/', $known)) { return false; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and the collation name is validated against information_schema above; neither can be bound as a parameter $wpdb->query("ALTER TABLE `{$table}` MODIFY `afsv_field_id` VARCHAR(77) COLLATE {$known} NOT NULL"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Verifying the schema change $now = $wpdb->get_var($wpdb->prepare( "SELECT COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = 'afsv_field_id'", $table )); return (is_string($now) && substr($now, -4) === '_bin'); } // Runs at init priority 1 (before accua_form_init at priority 5) so the DB schema // is always up to date before form processing begins. Also ensures translations used // in accua_forms_get_default_form_data() are loaded when a fresh install is detected. add_action('init', 'accua_forms_check_db_version_and_update', 1); function accua_forms_check_db_version_and_update() { if ( get_transient( '_accua_forms_data_deleted' ) ) { return; } $db_version = get_option('accua_forms_db_version', ''); if (ACCUA_FORMS_DB_VERSION != $db_version) { accua_forms_install(); } } add_filter('robots_txt', 'accua_forms_robots_txt', 10, 2); function accua_forms_robots_txt($output, $public) { if ($public) { $site_url = wp_parse_url(site_url()); $path = (!empty($site_url['path'])) ? $site_url['path'] : ''; $output .= "\nUser-agent: *\n"; $output .= "Allow: $path/wp-admin/js/\n"; $output .= "Allow: $path/wp-admin/css/\n"; } return $output; } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function with underscore prefix function _accua_forms_json_encode($item) { static $version = NULL; if ($version === NULL) { $version = version_compare(PHP_VERSION, '5.3.0', '>='); } if ($version) { return json_encode($item, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); } else { return strtr( json_encode($item), array( '&' => '\\u0026', '<' => '\\u003C', '>' => '\\u003E', ) ); } } /* per ripulire eventuali token impostati nella versione 1.9.4 */ function accua_forms_cleanup_meta_tokens() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time cleanup on activation $post_ids = $wpdb->get_col("SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_accua_download_token'"); if (!empty($post_ids)) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time cleanup on activation $wpdb->query("DELETE FROM {$wpdb->postmeta} WHERE meta_key = '_accua_download_token'"); } } register_activation_hook(__FILE__, 'accua_forms_cleanup_meta_tokens'); // Schedule retention cleanup cron on activation register_activation_hook( __FILE__, 'accua_forms_activate_retention_cron' ); function accua_forms_activate_retention_cron() { if ( ! wp_next_scheduled( 'accua_forms_retention_cleanup' ) ) { wp_schedule_event( time(), 'daily', 'accua_forms_retention_cleanup' ); } } // Clear retention cleanup cron on deactivation register_deactivation_hook( __FILE__, 'accua_forms_deactivate_retention_cron' ); function accua_forms_deactivate_retention_cron() { wp_clear_scheduled_hook( 'accua_forms_retention_cleanup' ); } // Add REST API compatibility add_action('rest_api_init', 'accua_forms_rest_compatibility'); /** * Ensures proper REST API compatibility by closing any open PHP sessions * This prevents timeouts when WordPress is making internal REST API requests */ function accua_forms_rest_compatibility() { // Check if a session is active and close it for REST requests if (session_id() && session_status() === PHP_SESSION_ACTIVE) { session_write_close(); } // Remove hooks that might interfere with REST API requests remove_action('init', 'accua_forms_init_session', 1); } /** * Get the default form data values. * * This function returns all default values for form settings including messages, * email templates, and styling options. Used during installation and for the * "Restore to Default" functionality in the settings page. * * @since 2.0.0-beta.6 * @return array Default form data values. */ function accua_forms_get_default_form_data() { return array( 'success_message' => '

' . __('Thank you', 'contact-forms') . ' {first_name} {last_name},

' . __('We have received your contact request. Check your inbox for the confirmation message.', 'contact-forms') . ' ' . __('Can\'t find the email?', 'contact-forms') . ' ' . __('It doesn\'t happen often but your mailbox could apply strict spam rules that block our email from reaching your inbox. Try checking your spam folder.', 'contact-forms') . ' ' . __('Also check that the email you entered in the form', 'contact-forms') . ' ({email}) ' . __('is correct. If it is not, you can submit the form again.', 'contact-forms') . ' ' . __('For any other issues, please don\'t hesitate to contact us.', 'contact-forms') . '
', 'error_message' => '

' . __('Oops! Something went wrong.', 'contact-forms') . '

' . __('Internet is an awfully complex place and even though we take every precaution to make sure things run smoothly every once in a while things can go wrong that are not under our control.', 'contact-forms') . ' ' . __('Please try filling in the form again.', 'contact-forms') . ' ' . __('For any other issues, please don\'t hesitate to contact us.', 'contact-forms') . '
', 'emails_from_name' => '', 'emails_from' => '', 'admin_emails_to' => get_option('admin_email', ''), 'emails_bcc' => '', 'admin_emails_subject' => __('New contact request from your site', 'contact-forms'), 'admin_emails_message' => '
' . __('On', 'contact-forms') . ' {__submitted_day_month_year} ' . __('at', 'contact-forms') . ' {__submitted_hour} ' . __('the following form was filled in.', 'contact-forms') . '
' . __('Page where the form was filled in', 'contact-forms') . ' {__url}
' . __('Submission review page', 'contact-forms') . ' {__review_submission_url}
{__submitted_html}
[form_if {__referrer} [' . __('Referrer', 'contact-forms') . ' - ' . __('where the visitor came from', 'contact-forms') . ' ' . __('before reaching the page', 'contact-forms') . ':
{__referrer}]]
' . __('Contact IP', 'contact-forms') . ' {__anonymized_ip}
[form_if {__autoreply} [' . __('A confirmation email was sent to', 'contact-forms') . ' {email} ' . __('with the following message', 'contact-forms') . ':
{__confirmation_emails_message}
]]
', 'confirmation_emails_subject' => __('Your message', 'contact-forms'), 'confirmation_emails_message' => '

' . __('Thank you', 'contact-forms') . ' {first_name} {last_name},

' . __('Thank you for your contact request.', 'contact-forms') . ' ' . __('We will contact you as soon as possible.', 'contact-forms') . ' ' . __('Sincerely,', 'contact-forms') . ' ' . __('The Website Team', 'contact-forms') . '
', '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' => '', ); } /** * Get all countries with localized names for phone field country selector. * * Uses PHP Intl extension to generate localized country names from ISO 3166-1 alpha-2 codes. * Falls back to country codes if Intl extension is unavailable. * * @since 2.0.0-beta.34 * @param string|null $display_locale Locale for display names (default: current WordPress locale). * @return array Associative array ['US' => 'United States', 'IT' => 'Italy', ...] sorted alphabetically. */ function accua_forms_get_countries( $display_locale = null ) { // ISO 3166-1 alpha-2 codes - complete list (249 countries/territories) $country_codes = array( 'AF', 'AX', 'AL', 'DZ', 'AS', 'AD', 'AO', 'AI', 'AQ', 'AG', 'AR', 'AM', 'AW', 'AU', 'AT', 'AZ', 'BS', 'BH', 'BD', 'BB', 'BY', 'BE', 'BZ', 'BJ', 'BM', 'BT', 'BO', 'BQ', 'BA', 'BW', 'BV', 'BR', 'IO', 'BN', 'BG', 'BF', 'BI', 'KH', 'CM', 'CA', 'CV', 'KY', 'CF', 'TD', 'CL', 'CN', 'CX', 'CC', 'CO', 'KM', 'CG', 'CD', 'CK', 'CR', 'CI', 'HR', 'CU', 'CW', 'CY', 'CZ', 'DK', 'DJ', 'DM', 'DO', 'EC', 'EG', 'SV', 'GQ', 'ER', 'EE', 'ET', 'FK', 'FO', 'FJ', 'FI', 'FR', 'GF', 'PF', 'TF', 'GA', 'GM', 'GE', 'DE', 'GH', 'GI', 'GR', 'GL', 'GD', 'GP', 'GU', 'GT', 'GG', 'GN', 'GW', 'GY', 'HT', 'HM', 'VA', 'HN', 'HK', 'HU', 'IS', 'IN', 'ID', 'IR', 'IQ', 'IE', 'IM', 'IL', 'IT', 'JM', 'JP', 'JE', 'JO', 'KZ', 'KE', 'KI', 'KP', 'KR', 'KW', 'KG', 'LA', 'LV', 'LB', 'LS', 'LR', 'LY', 'LI', 'LT', 'LU', 'MO', 'MK', 'MG', 'MW', 'MY', 'MV', 'ML', 'MT', 'MH', 'MQ', 'MR', 'MU', 'YT', 'MX', 'FM', 'MD', 'MC', 'MN', 'ME', 'MS', 'MA', 'MZ', 'MM', 'NA', 'NR', 'NP', 'NL', 'NC', 'NZ', 'NI', 'NE', 'NG', 'NU', 'NF', 'MP', 'NO', 'OM', 'PK', 'PW', 'PS', 'PA', 'PG', 'PY', 'PE', 'PH', 'PN', 'PL', 'PT', 'PR', 'QA', 'RE', 'RO', 'RU', 'RW', 'BL', 'SH', 'KN', 'LC', 'MF', 'PM', 'VC', 'WS', 'SM', 'ST', 'SA', 'SN', 'RS', 'SC', 'SL', 'SG', 'SX', 'SK', 'SI', 'SB', 'SO', 'ZA', 'GS', 'SS', 'ES', 'LK', 'SD', 'SR', 'SJ', 'SZ', 'SE', 'CH', 'SY', 'TW', 'TJ', 'TZ', 'TH', 'TL', 'TG', 'TK', 'TO', 'TT', 'TN', 'TR', 'TM', 'TC', 'TV', 'UG', 'UA', 'AE', 'GB', 'US', 'UM', 'UY', 'UZ', 'VU', 'VE', 'VN', 'VG', 'VI', 'WF', 'EH', 'YE', 'ZM', 'ZW', ); // Use WordPress locale if not specified if ( null === $display_locale ) { $display_locale = get_locale(); } // Check if Intl extension is available if ( ! class_exists( 'Locale' ) ) { // Fallback: return codes as both key and value return array_combine( $country_codes, $country_codes ); } $countries = array(); foreach ( $country_codes as $code ) { // The trick: prepend '-' to convert region code to locale format $name = Locale::getDisplayRegion( '-' . $code, $display_locale ); // If Intl returns the code itself (unknown region), use the code $countries[ $code ] = ( $name && $name !== $code ) ? $name : $code; } // Sort alphabetically by localized name asort( $countries, SORT_LOCALE_STRING ); return $countries; } /** * Get the translated label for a layout value. * * Returns the human-readable, translated label for a given layout value. * Used in layout dropdowns throughout the admin interface. * * @since 2.0.0-beta.29 * @param string $layout Layout value: 'sidebyside', 'toplabel', or 'inlinelabel'. * @return string Translated layout label. */ function accua_forms_get_layout_label( $layout ) { switch ( $layout ) { case 'toplabel': return __( 'Labels on top of the fields', 'contact-forms' ); case 'inlinelabel': return __( 'Inline labels', 'contact-forms' ); case 'sidebyside': default: return __( 'Labels on the left of the fields', 'contact-forms' ); } }