# contact-forms/trunk/contact-forms.php

Contact Forms by Cimatti, version trunk. 1,332 lines.

- Page: https://pluginprobe.com/plugins/contact-forms/trunk/code/contact-forms.php
- Raw: https://pluginprobe.com/plugins/contact-forms/trunk/raw/contact-forms.php
- Modified: 2026-09-02T14:00:10+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/contact-forms/trunk/code/contact-forms.php#L10-L20`.

```php
<?php
/*
Plugin Name: Contact Forms by Cimatti
Description: Create accessible contact forms with drag-and-drop. WCAG 2.2 compliant with screen reader support, keyboard navigation, and clear error messages.
Version: 2.3.6
Plugin URI: https://www.cimatti.it/en/wordpress-plugins/contact-forms/
Author: Cimatti
Author URI: https://www.cimatti.it
Text Domain: contact-forms
Domain Path: /languages
Requires at least: 5.9
Requires PHP: 7.4
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
*/

/*
Contact Forms by Cimatti
Copyright (c) 2011-2026 Andrea Cimatti

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.


The full copy of the GNU General Public License is available here: http://www.gnu.org/licenses/gpl.txt

*/

if ( ! defined( 'ABSPATH' ) ) exit;

define('ACCUA_FORMS_DB_VERSION', '19');
define('ACCUA_FORMS_CSS_VERSION', 207);
define('ACCUA_FORMS_JS_VERSION', '138');
define('ACCUA_FORMS_FILE', __FILE__);
define('ACCUA_FORMS_DIR_URL', plugin_dir_url(ACCUA_FORMS_FILE));
define('ACCUA_FORMS_DIR', dirname(ACCUA_FORMS_FILE));

require_once('accua-form-api.php');
require_once('accua-forms.php');
require_once('admin/shortcode-button.php');
require_once('admin/theme-helper.php');
require_once('block-editor.php');

function accua_forms_dashboard_page_head()
{
  $screen = get_current_screen();
  $screen->add_help_tab(array(
    'id'  => 'accua_help_tab',
    'title'  => __('Contact Forms Dashboard', 'contact-forms'),
    'content'  => '<p>' . __('Marketing tools for WordPress', 'contact-forms') . '</p>',
  ));

  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:<length>:` 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' => '<div style="font-family: Arial, Helvetica, sans-serif;">
<h2>' . __('Thank you', 'contact-forms') . ' {first_name} {last_name},</h2>
' . __('We have received your contact request. Check your inbox for the confirmation message.', 'contact-forms') . '

<strong>' . __('Can\'t find the email?', 'contact-forms') . '</strong>

' . __('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') . ' (<strong>{email}</strong>) ' . __('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') . '

</div>',
    'error_message' => '<div style="font-family: Arial, Helvetica, sans-serif;">
<h2>' . __('Oops! Something went wrong.', 'contact-forms') . '</h2>
' . __('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') . '

</div>',
    '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' => '<table style="font-family: Arial, Helvetica, sans-serif; background: #fff; margin-top: 10px; border: 1px solid #DDDDDD; max-width: 700px;" cellspacing="0" cellpadding="0" align="center">
<tbody>
<tr>
<td>
<table style="max-width:700px;" border="0" cellspacing="0" cellpadding="0" align="center">
<tbody>
<tr>
<td>
<table style="padding: 10px; border: 1px solid #dfdfdf; max-width:700px;" border="0" cellspacing="0" cellpadding="0" align="center">
<tbody>
<tr valign="top">
<td style="max-width:22px;"></td>
<td style="font-family: Arial, Helvetica, sans-serif; padding: 0px 30px 10px; max-width: 645px;">
<table style="width: 100%; table-layout: fixed;">
<tbody>
<tr>
<td>' . __('On', 'contact-forms') . ' {__submitted_day_month_year} ' . __('at', 'contact-forms') . ' {__submitted_hour} ' . __('the following form was filled in.', 'contact-forms') . '

<hr />

<strong>' . __('Page where the form was filled in', 'contact-forms') . '</strong>
<a href="{__url}" style="word-break: break-all; overflow-wrap: break-word;">{__url}</a></td>
</tr>
<tr><td>
<strong>' . __('Submission review page', 'contact-forms') . '</strong>
<a href="{__review_submission_url}" style="word-break: break-all; overflow-wrap: break-word;">{__review_submission_url}</a>
</td></tr>
<tr>
<td id="submitted_html" style="overflow-wrap: break-word; word-break: normal;">{__submitted_html}</td>
</tr>
<tr>
<td>[form_if {__referrer} [<strong>' . __('Referrer', 'contact-forms') . '</strong> - ' . __('where the visitor came from', 'contact-forms') . ' <em>' . __('before reaching the page', 'contact-forms') . '</em>:<br /><span style="word-break: break-all; overflow-wrap: break-word;">{__referrer}</span>]]</td>
</tr>
<tr>
<td><strong>' . __('Contact IP', 'contact-forms') . '</strong> {__anonymized_ip}
<hr />
</td>
</tr>
<tr>
<td>[form_if {__autoreply} [' . __('A confirmation email was sent to', 'contact-forms') . ' <a href="mailto:{email}">{email}</a> ' . __('with the following message', 'contact-forms') . ':
<table>
<tbody>
<tr>
<td>{__confirmation_emails_message}</td>
</tr>
</tbody>
</table> ]]
</td>
</tr>
</tbody>
</table>
</td>
<td style="max-width:23px"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>',
    'confirmation_emails_subject' => __('Your message', 'contact-forms'),
    'confirmation_emails_message' => '<div style="font-family: Arial, Helvetica, sans-serif;">

<h2>' . __('Thank you', 'contact-forms') . ' {first_name} {last_name},</h2>
' . __('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') . '

<hr />
</div>',

    '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' );
  }
}

```
