PluginProbe
Contact Forms by Cimatti / 2.3.0
Contact Forms by Cimatti v2.3.0
2.3.6 2.3.5 2.3.0 2.2.32 2.2.4 2.2.0 2.1.2 2.1.1 trunk 1.0 1.1 1.2 1.2.1 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 All 62 releases
contact-forms / contact-forms.php

contact-forms.php in Contact Forms by Cimatti 2.3.0, at contact-forms.php

1,333 lines 43.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: Contact Forms by Cimatti
4 Description: Create accessible contact forms with drag-and-drop. WCAG 2.2 compliant with screen reader support, keyboard navigation, and clear error messages.
5 Version: 2.3.0
6 Plugin URI: https://www.cimatti.it/en/wordpress-plugins/contact-forms/
7 Author: Cimatti
8 Author URI: https://www.cimatti.it
9 Text Domain: contact-forms
10 Domain Path: /languages
11 Requires at least: 5.9
12 Requires PHP: 7.4
13 License: GPLv2 or later
14 License URI: http://www.gnu.org/licenses/gpl-2.0.html
15 */
16
17 /*
18 Contact Forms by Cimatti
19 Copyright (c) 2011-2026 Andrea Cimatti
20
21 This program is free software; you can redistribute it and/or
22 modify it under the terms of the GNU General Public License
23 as published by the Free Software Foundation; either version 2
24 of the License, or (at your option) any later version.
25
26 This program is distributed in the hope that it will be useful,
27 but WITHOUT ANY WARRANTY; without even the implied warranty of
28 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 GNU General Public License for more details.
30
31 You should have received a copy of the GNU General Public License
32 along with this program; if not, write to the Free Software
33 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
34
35
36 The full copy of the GNU General Public License is available here: http://www.gnu.org/licenses/gpl.txt
37
38 */
39
40 if ( ! defined( 'ABSPATH' ) ) exit;
41
42 define('ACCUA_FORMS_DB_VERSION', '19');
43 define('ACCUA_FORMS_CSS_VERSION', 205);
44 define('ACCUA_FORMS_JS_VERSION', '133');
45 define('ACCUA_FORMS_FILE', __FILE__);
46 define('ACCUA_FORMS_DIR_URL', plugin_dir_url(ACCUA_FORMS_FILE));
47 define('ACCUA_FORMS_DIR', dirname(ACCUA_FORMS_FILE));
48
49 require_once('accua-form-api.php');
50 require_once('accua-forms.php');
51 require_once('admin/shortcode-button.php');
52 require_once('admin/theme-helper.php');
53 require_once('block-editor.php');
54
55 function accua_forms_dashboard_page_head()
56 {
57 $screen = get_current_screen();
58 $screen->add_help_tab(array(
59 'id' => 'accua_help_tab',
60 'title' => __('Contact Forms Dashboard', 'contact-forms'),
61 'content' => '<p>' . __('Marketing tools for WordPress', 'contact-forms') . '</p>',
62 ));
63
64 wp_enqueue_script( 'accua-forms-expandable-cells', plugins_url( 'assets/js/admin/expandable-cells.js', ACCUA_FORMS_FILE ), array(), ACCUA_FORMS_JS_VERSION, true );
65 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);
66 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);
67 wp_enqueue_style('accua-forms-admin', plugins_url('assets/css/admin.css', ACCUA_FORMS_FILE), array(), ACCUA_FORMS_CSS_VERSION);
68
69 /* Tentativo x drag and drop
70 $page_hook_id = fx_smb_setings_page_id();
71 /* Load the JavaScript needed for the settings screen. * /
72 add_action( 'admin_enqueue_scripts', 'fx_smb_enqueue_scripts' );
73 add_action( "admin_footer-{$page_hook_id}", 'fx_smb_footer_scripts' );
74
75 /* Set number of column available. * /
76 add_filter( 'screen_layout_columns', 'fx_smb_screen_layout_column', 10, 2 );
77 */
78 }
79
80 /**
81 * Add Contact Forms dashboard widget.
82 *
83 * Only visible to users with the 'manage_options' capability, the same
84 * capability required by the plugin admin pages. wp_dashboard_setup only
85 * fires on the dashboard screen, and the widget code is loaded on demand
86 * here, so admin/dashboard-widget.php is never included on the frontend
87 * or on other admin pages.
88 */
89 add_action('wp_dashboard_setup', 'accua_contact_forms_dashboard_add_widgets');
90 function accua_contact_forms_dashboard_add_widgets()
91 {
92 if (!current_user_can('manage_options')) {
93 return;
94 }
95 require_once ACCUA_FORMS_DIR . '/admin/dashboard-widget.php';
96 wp_add_dashboard_widget('accua_contact_forms_dashboard_widget_news', __('Contact Forms', 'contact-forms'), 'accua_contact_forms_dashboard_widget_news_handler');
97 }
98
99 function accua_forms_dashboard_page()
100 {
101 require_once ACCUA_FORMS_DIR . '/admin/dashboard-page.php';
102 return _accua_forms_dashboard_page();
103 }
104
105
106 register_activation_hook(ACCUA_FORMS_FILE, 'accua_forms_install');
107 function accua_forms_install()
108 {
109 static $ran = false;
110 if ( $ran ) {
111 return;
112 }
113 $ran = true;
114
115 $modified = false;
116 $keys = get_option('accua_form_api_keys', array());
117 if (!isset($keys['hash'])) {
118 $modified = true;
119 $keys['hash'] = wp_generate_password(64, true, true);
120 }
121 if (!isset($keys['aes'])) {
122 $modified = true;
123 $keys['aes'] = wp_generate_password(64, true, true);
124 }
125 if ($modified) {
126 update_option('accua_form_api_keys', $keys);
127 }
128
129 global $wpdb;
130
131 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
132
133 $charset_collate = '';
134 if (! empty($wpdb->charset))
135 $charset_collate = "DEFAULT CHARACTER SET $wpdb->charset";
136 if (! empty($wpdb->collate))
137 $charset_collate .= " COLLATE $wpdb->collate";
138
139 $old_db_version = (int) get_option('accua_forms_db_version', 0);
140
141 if ($old_db_version < 3) {
142 $wpdb_suppress_errors_status = $wpdb->suppress_errors();
143 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Schema migration, runs once
144 $wpdb->query("ALTER TABLE `{$wpdb->prefix}accua_forms_submissions` DROP INDEX uri");
145 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Schema migration, runs once
146 $wpdb->query("ALTER TABLE `{$wpdb->prefix}accua_forms_submissions` DROP INDEX referrer");
147 $wpdb->suppress_errors($wpdb_suppress_errors_status);
148 }
149
150 $sql = "CREATE TABLE `{$wpdb->prefix}accua_forms_submissions` (
151 afs_id BIGINT(20) NOT NULL AUTO_INCREMENT,
152 afs_form_id VARCHAR(77) NOT NULL DEFAULT '',
153 afs_post_id BIGINT(20) NOT NULL DEFAULT 0,
154 afs_ip VARCHAR(255) NOT NULL DEFAULT '',
155 afs_uri TEXT NOT NULL,
156 afs_referrer TEXT NOT NULL,
157 afs_lang VARCHAR(60) NOT NULL DEFAULT '',
158 afs_created TIMESTAMP NOT NULL DEFAULT 0,
159 afs_submitted TIMESTAMP NOT NULL DEFAULT 0,
160 afs_status TINYINT(1) NOT NULL DEFAULT 0,
161 afs_anonymized TINYINT(1) NOT NULL DEFAULT 0,
162 afs_stats TEXT NOT NULL,
163 afs_lead_status TINYINT(1) NOT NULL DEFAULT 0,
164 PRIMARY KEY (afs_id),
165 KEY form (afs_form_id, afs_status),
166 KEY uri (afs_uri(190)),
167 KEY pid (afs_post_id),
168 KEY referrer (afs_referrer(190)),
169 KEY status (afs_status, afs_id),
170 KEY submitted (afs_submitted),
171 KEY lead_status (afs_lead_status, afs_status)
172 ) $charset_collate;";
173 dbDelta($sql);
174
175 $sql = "CREATE TABLE `{$wpdb->prefix}accua_forms_submissions_values` (
176 afsv_sub_id BIGINT(20) NOT NULL,
177 afsv_field_id VARCHAR(77) NOT NULL,
178 afsv_type varchar(255) NOT NULL DEFAULT '',
179 afsv_value TEXT NOT NULL,
180 PRIMARY KEY (afsv_sub_id, afsv_field_id),
181 KEY value_index (afsv_field_id(77), afsv_value(114))
182 ) $charset_collate;";
183 dbDelta($sql);
184
185 /* creazione tabella per le note dell'utente
186 sono relative a una compilazione
187 possono esserci più note per ogni compilazione in date diverse
188
189 */
190 $sql = "CREATE TABLE `{$wpdb->prefix}accua_forms_submissions_notes` (
191 afsn_sub_id BIGINT(20) NOT NULL,
192 afsn_date TIMESTAMP NOT NULL DEFAULT 0,
193 afsn_text TEXT NOT NULL,
194 afsn_user VARCHAR(100) NOT NULL DEFAULT '',
195 PRIMARY KEY (afsn_sub_id, afsn_date),
196 KEY afsn_sub_id (afsn_sub_id)
197 ) $charset_collate;";
198 dbDelta($sql);
199
200 // DB 18: make field slugs case-sensitive in the submission values table.
201 // Runs right after dbDelta, so a fresh install (version 0) is converted at
202 // creation time too.
203 //
204 // Deliberately NOT gated on the stored DB version: dbDelta above rewrites
205 // afsv_field_id back to the table's default (case-insensitive) collation
206 // whenever it runs, because the column no longer matches the definition it
207 // compares against. install() runs on every version change AND on every
208 // activation, so a version check here would leave the column case-insensitive
209 // again after the next update or a deactivate/reactivate - with the values of
210 // two case-differing slugs silently colliding once more. The conversion is
211 // idempotent and returns early when the collation is already binary, so
212 // re-asserting it on each run costs one information_schema lookup.
213 accua_forms_make_field_slugs_case_sensitive();
214
215 // Data migration: run after dbDelta so new columns exist
216 if ($old_db_version < 14) {
217 // Migrate: afs_status=-2 (old "anonymized" status) → afs_anonymized=1 + restore original active status
218 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time migration
219 $wpdb->query("UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_anonymized = 1, afs_status = 0 WHERE afs_status = -2");
220
221 // Migrate saved forms from 1.x format to 2.0 format
222 $saved_forms = get_option('accua_forms_saved_forms', array());
223 if (is_array($saved_forms)) {
224 $forms_updated = false;
225 foreach ($saved_forms as $fid => &$form) {
226 // Rename legacy 'name' key to 'title' (changed in 2.0)
227 if (isset($form['name']) && !isset($form['title'])) {
228 $form['title'] = $form['name'];
229 unset($form['name']);
230 $forms_updated = true;
231 }
232 // Convert 'fields' from comma-separated string to array of instances.
233 // Keyed by instance id, exactly as the form editor writes them: the
234 // submission handler looks each posted field up as
235 // $form_data['fields'][$name] (accua-forms.php), so an instance stored
236 // under a numeric key is never found and its value is dropped.
237 if (isset($form['fields']) && is_string($form['fields'])) {
238 $field_slugs = array_filter(array_map('trim', explode(',', $form['fields'])));
239 $form['fields'] = array();
240 foreach ($field_slugs as $slug) {
241 $istance_id = accua_forms_unique_istance_id($slug, $form['fields']);
242 $form['fields'][$istance_id] = array(
243 'version' => 2,
244 'istance_id' => $istance_id,
245 'widget_number' => '',
246 'ref' => $slug,
247 'required' => false,
248 );
249 }
250 unset($form['fieldnum']);
251 $forms_updated = true;
252 }
253 }
254 unset($form);
255 if ($forms_updated) {
256 update_option('accua_forms_saved_forms', $saved_forms);
257 }
258 }
259 }
260
261 // DB 19: repair field instances an earlier 1.x -> 2.0 migration left unusable.
262 // That conversion (in 2.0 through 2.3.0) appended array('ref' => $slug) with
263 // no instance id, so the instances ended up under numeric keys. Such a form
264 // still renders - the render loop reads 'ref' - but the submission handler
265 // matches each posted field by array key, finds nothing, and stores the
266 // submission with none of its values. Silent, and only on forms that came
267 // from a pre-2.0 install, which is why it went unnoticed for so long.
268 if ($old_db_version < 19) {
269 accua_forms_repair_field_instances('accua_forms_saved_forms');
270 // Trashed forms can be restored, so they must come back working too.
271 accua_forms_repair_field_instances('accua_forms_trash_forms');
272 }
273
274
275 // An unreadable field list must never be silently replaced by the defaults:
276 // try to recover it first, and keep a copy of the original either way.
277 accua_forms_recover_avail_fields();
278
279 $avail_fields = get_option('accua_forms_avail_fields', array());
280
281 if (!is_array($avail_fields)) {
282 $avail_fields = array();
283 }
284
285 if (empty($avail_fields) || ($old_db_version === 0)) {
286 $avail_fields += array(
287 // essential_column: shown by the submissions list "Essential Columns"
288 // button — sensible defaults for a contact form: who wrote (name,
289 // email) and what they wrote (message).
290 'first_name' => array(
291 'id' => 'first_name',
292 'name' => 'First Name',
293 'type' => 'textfield',
294 'description' => '',
295 'default_value' => '',
296 'allowed_values' => '',
297 'essential_column' => 1,
298 ),
299 'last_name' => array(
300 'id' => 'last_name',
301 'name' => 'Last Name',
302 'type' => 'textfield',
303 'description' => '',
304 'default_value' => '',
305 'allowed_values' => '',
306 'essential_column' => 1,
307 ),
308 'email' => array(
309 'id' => 'email',
310 'name' => 'Email',
311 'type' => 'autoreply_email',
312 'description' => '',
313 'default_value' => '',
314 'allowed_values' => '',
315 'essential_column' => 1,
316 ),
317 'address' => array(
318 'id' => 'address',
319 'name' => 'Address',
320 'type' => 'textfield',
321 'description' => '',
322 'default_value' => '',
323 'allowed_values' => '',
324 ),
325 'city' => array(
326 'id' => 'city',
327 'name' => 'City',
328 'type' => 'textfield',
329 'description' => '',
330 'default_value' => '',
331 'allowed_values' => '',
332 ),
333 'state_province' => array(
334 'id' => 'state_province',
335 'name' => 'State/Province',
336 'type' => 'textfield',
337 'description' => '',
338 'default_value' => '',
339 'allowed_values' => '',
340 ),
341 'country' => array(
342 'id' => 'country',
343 'name' => 'Country',
344 'type' => 'select',
345 'description' => '',
346 'default_value' => '-',
347 'allowed_values' => "-|Select...
348 Afghanistan
349 �
350 land Islands
351 Albania
352 Algeria
353 American Samoa
354 Andorra
355 Angola
356 Anguilla
357 Antarctica
358 Antigua And Barbuda
359 Argentina
360 Armenia
361 Aruba
362 Australia
363 Austria
364 Azerbaijan
365 Bahamas
366 Bahrain
367 Bangladesh
368 Barbados
369 Belarus
370 Belgium
371 Belize
372 Benin
373 Bermuda
374 Bhutan
375 Bolivia
376 Bosnia And Herzegovina
377 Botswana
378 Bouvet Island
379 Brazil
380 British Indian Ocean Territory
381 Brunei Darussalam
382 Bulgaria
383 Burkina Faso
384 Burundi
385 Cambodia
386 Cameroon
387 Canada
388 Cape Verde
389 Cayman Islands
390 Central African Republic
391 Chad
392 Chile
393 China
394 Christmas Island
395 Cocos (Keeling) Islands
396 Colombia
397 Comoros
398 Congo
399 Congo, The Democratic Republic Of The
400 Cook Islands
401 Costa Rica
402 Côte D'Ivoire
403 Croatia
404 Cuba
405 Cyprus
406 Czech Republic
407 Denmark
408 Djibouti
409 Dominica
410 Dominican Republic
411 Ecuador
412 Egypt
413 El Salvador
414 Equatorial Guinea
415 Eritrea
416 Estonia
417 Ethiopia
418 Falkland Islands (Malvinas)
419 Faroe Islands
420 Fiji
421 Finland
422 France
423 French Guiana
424 French Polynesia
425 French Southern Territories
426 Gabon
427 Gambia
428 Georgia
429 Germany
430 Ghana
431 Gibraltar
432 Greece
433 Greenland
434 Grenada
435 Guadeloupe
436 Guam
437 Guatemala
438 Guernsey
439 Guinea
440 Guinea-Bissau
441 Guyana
442 Haiti
443 Heard Island And Mcdonald Islands
444 Holy See (Vatican City State)
445 Honduras
446 Hong Kong
447 Hungary
448 Iceland
449 India
450 Indonesia
451 Iran, Islamic Republic Of
452 Iraq
453 Ireland
454 Isle Of Man
455 Israel
456 Italy
457 Jamaica
458 Japan
459 Jersey
460 Jordan
461 Kazakhstan
462 Kenya
463 Kiribati
464 Korea, Democratic People'S Republic Of
465 Korea, Republic Of
466 Kuwait
467 Kyrgyzstan
468 Lao People'S Democratic Republic
469 Latvia
470 Lebanon
471 Lesotho
472 Liberia
473 Libyan Arab Jamahiriya
474 Liechtenstein
475 Lithuania
476 Luxembourg
477 Macao
478 Macedonia, The Former Yugoslav Republic Of
479 Madagascar
480 Malawi
481 Malaysia
482 Maldives
483 Mali
484 Malta
485 Marshall Islands
486 Martinique
487 Mauritania
488 Mauritius
489 Mayotte
490 Mexico
491 Micronesia, Federated States Of
492 Moldova, Republic Of
493 Monaco
494 Mongolia
495 Montenegro
496 Montserrat
497 Morocco
498 Mozambique
499 Myanmar
500 Namibia
501 Nauru
502 Nepal
503 Netherlands
504 Netherlands Antilles
505 New Caledonia
506 New Zealand
507 Nicaragua
508 Niger
509 Nigeria
510 Niue
511 Norfolk Island
512 Northern Mariana Islands
513 Norway
514 Oman
515 Pakistan
516 Palau
517 Palestinian Territory, Occupied
518 Panama
519 Papua New Guinea
520 Paraguay
521 Peru
522 Philippines
523 Pitcairn
524 Poland
525 Portugal
526 Puerto Rico
527 Qatar
528 Réunion
529 Romania
530 Russian Federation
531 Rwanda
532 Saint Barthélemy
533 Saint Helena
534 Saint Kitts And Nevis
535 Saint Lucia
536 Saint Martin
537 Saint Pierre And Miquelon
538 Saint Vincent And The Grenadines
539 Samoa
540 San Marino
541 Sao Tome And Principe
542 Saudi Arabia
543 Senegal
544 Serbia
545 Seychelles
546 Sierra Leone
547 Singapore
548 Slovakia
549 Slovenia
550 Solomon Islands
551 Somalia
552 South Africa
553 South Georgia And The South Sandwich Islands
554 Spain
555 Sri Lanka
556 Sudan
557 Suriname
558 Svalbard And Jan Mayen
559 Swaziland
560 Sweden
561 Switzerland
562 Syrian Arab Republic
563 Taiwan, Province Of China
564 Tajikistan
565 Tanzania, United Republic Of
566 Thailand
567 Timor-Leste
568 Togo
569 Tokelau
570 Tonga
571 Trinidad And Tobago
572 Tunisia
573 Turkey
574 Turkmenistan
575 Turks And Caicos Islands
576 Tuvalu
577 Uganda
578 Ukraine
579 United Arab Emirates
580 United Kingdom
581 United States
582 United States Minor Outlying Islands
583 Uruguay
584 Uzbekistan
585 Vanuatu
586 Venezuela, Bolivarian Republic Of
587 Viet Nam
588 Virgin Islands, British
589 Virgin Islands, U.S.
590 Wallis And Futuna
591 Western Sahara
592 Yemen
593 Zambia
594 Zimbabwe",
595 ),
596 'message' => array(
597 'id' => 'message',
598 'name' => 'Message',
599 'type' => 'textarea',
600 'description' => '',
601 'default_value' => '',
602 'allowed_values' => '',
603 'essential_column' => 1,
604 ),
605 'captcha' => array(
606 'id' => 'captcha',
607 'name' => 'Captcha',
608 'type' => 'captcha',
609 'description' => '',
610 'default_value' => '',
611 'allowed_values' => '',
612 ),
613 'turnstile' => array(
614 'id' => 'turnstile',
615 'name' => 'Turnstile',
616 'type' => 'turnstile',
617 'description' => '',
618 'default_value' => '',
619 'allowed_values' => '',
620 ),
621 'captcha_v3' => array(
622 'id' => 'captcha_v3',
623 'name' => 'Captcha (reCAPTCHA v3)',
624 'type' => 'captcha_v3',
625 'description' => '',
626 'default_value' => '',
627 'allowed_values' => '',
628 ),
629 'cap' => array(
630 'id' => 'cap',
631 'name' => 'Captcha (Cap)',
632 'type' => 'cap',
633 'description' => '',
634 'default_value' => '',
635 'allowed_values' => '',
636 ),
637 );
638 update_option('accua_forms_avail_fields', $avail_fields);
639 }
640
641 // Add Turnstile field for existing installations (backward compatibility)
642 // Only add if it doesn't exist yet
643 if (!isset($avail_fields['turnstile'])) {
644 $avail_fields['turnstile'] = array(
645 'id' => 'turnstile',
646 'name' => 'Turnstile',
647 'type' => 'turnstile',
648 'description' => '',
649 'default_value' => '',
650 'allowed_values' => '',
651 );
652 update_option('accua_forms_avail_fields', $avail_fields);
653 }
654
655 // Add reCAPTCHA v3 field for existing installations (backward compatibility)
656 // Only add if it doesn't exist yet
657 if (!isset($avail_fields['captcha_v3'])) {
658 $avail_fields['captcha_v3'] = array(
659 'id' => 'captcha_v3',
660 'name' => 'Captcha (reCAPTCHA v3)',
661 'type' => 'captcha_v3',
662 'description' => '',
663 'default_value' => '',
664 'allowed_values' => '',
665 );
666 update_option('accua_forms_avail_fields', $avail_fields);
667 }
668
669 // Add Cap field for existing installations (backward compatibility)
670 // Only add if it doesn't exist yet
671 if (!isset($avail_fields['cap'])) {
672 $avail_fields['cap'] = array(
673 'id' => 'cap',
674 'name' => 'Captcha (Cap)',
675 'type' => 'cap',
676 'description' => '',
677 'default_value' => '',
678 'allowed_values' => '',
679 );
680 update_option('accua_forms_avail_fields', $avail_fields);
681 }
682
683 // Add Telephone field for existing installations (backward compatibility)
684 // Only add if it doesn't exist yet
685 if (!isset($avail_fields['telephone'])) {
686 $avail_fields['telephone'] = array(
687 'id' => 'telephone',
688 'name' => 'Telephone',
689 'type' => 'telephone',
690 'description' => '',
691 'default_value' => '+39 ',
692 'allowed_values' => '',
693 );
694 update_option('accua_forms_avail_fields', $avail_fields);
695 }
696
697 // 2.2.47 (DB 17): per-field "Show in essential columns" flag. Existing
698 // installations keep the previous behavior of the submissions list
699 // "Essential Columns" button, whose hardcoded list contained one field
700 // column: email. Guarded by isset so a later uncheck (the Fields page save
701 // always writes the key) is never overridden on reactivation; fresh
702 // installs already flag email (and name/message) in the defaults above.
703 if ($old_db_version < 17 && isset($avail_fields['email']) && !isset($avail_fields['email']['essential_column'])) {
704 $avail_fields['email']['essential_column'] = 1;
705 update_option('accua_forms_avail_fields', $avail_fields);
706 }
707
708 $form_data = get_option('accua_forms_default_form_data', array());
709 if (!is_array($form_data)) {
710 $form_data = array();
711 }
712
713 // Use the centralized defaults function
714 $form_data += accua_forms_get_default_form_data();
715
716 update_option('accua_forms_default_form_data', $form_data);
717 update_option('accua_forms_db_version', ACCUA_FORMS_DB_VERSION);
718 }
719
720 /**
721 * An instance id that is free in this form's field list.
722 *
723 * Field instances are keyed by their instance id, which is normally the field
724 * slug; a form may hold the same field twice, so a taken slug gets a numeric
725 * suffix the way the form editor's own widget numbering does.
726 *
727 * @since 2.3.0
728 * @param string $slug Field slug the instance refers to.
729 * @param array $fields Instances already placed in this form.
730 * @return string
731 */
732 function accua_forms_unique_istance_id($slug, $fields) {
733 if (!isset($fields[$slug])) {
734 return $slug;
735 }
736 $n = 2;
737 while (isset($fields[$slug . '-' . $n])) {
738 $n++;
739 }
740 return $slug . '-' . $n;
741 }
742
743 /**
744 * Give every field instance of every form a usable instance id.
745 *
746 * Instances converted from the pre-2.0 format carry only a 'ref' and sit under
747 * a numeric key, which makes the submission handler drop their values (it looks
748 * each posted field up by array key). Rebuilds those keyed by instance id and
749 * leaves everything else exactly as it is - a normally saved form is untouched,
750 * so this is safe to run over any install and is idempotent.
751 *
752 * @since 2.3.0
753 * @param string $option Option holding forms ('accua_forms_saved_forms' or the trash).
754 * @return int Number of forms repaired.
755 */
756 function accua_forms_repair_field_instances($option) {
757 $forms = get_option($option, array());
758 if (!is_array($forms)) {
759 return 0;
760 }
761
762 $repaired = 0;
763 foreach ($forms as $fid => $form) {
764 if (empty($form['fields']) || !is_array($form['fields'])) {
765 continue;
766 }
767
768 // Nothing to do unless some instance is missing its id.
769 $needs_repair = false;
770 foreach ($form['fields'] as $key => $istance_data) {
771 if (!is_array($istance_data) || empty($istance_data['istance_id'])) {
772 $needs_repair = true;
773 break;
774 }
775 }
776 if (!$needs_repair) {
777 continue;
778 }
779
780 $rebuilt = array();
781 foreach ($form['fields'] as $key => $istance_data) {
782 // A bare slug instead of an instance: another shape old data turns up in,
783 // and recoverable, so it is rebuilt rather than dropped.
784 if (is_string($istance_data) && '' !== $istance_data) {
785 $istance_data = array('ref' => $istance_data);
786 }
787 if (!is_array($istance_data)) {
788 continue;
789 }
790 if (!empty($istance_data['istance_id'])) {
791 // Already valid: keep it under its own instance id.
792 $rebuilt[$istance_data['istance_id']] = $istance_data;
793 continue;
794 }
795 // The slug is in 'ref'; a numeric key carries no information, but a
796 // string key was the instance id in some hand-edited data, so prefer it.
797 $ref = !empty($istance_data['ref'])
798 ? $istance_data['ref']
799 : (is_string($key) ? $key : '');
800 if ('' === $ref) {
801 continue;
802 }
803 $istance_id = accua_forms_unique_istance_id(is_string($key) ? $key : $ref, $rebuilt);
804 $rebuilt[$istance_id] = array(
805 'version' => 2,
806 'istance_id' => $istance_id,
807 'widget_number' => '',
808 'ref' => $ref,
809 'required' => !empty($istance_data['required']),
810 ) + $istance_data;
811 }
812
813 $forms[$fid]['fields'] = $rebuilt;
814 $repaired++;
815 }
816
817 if ($repaired) {
818 update_option($option, $forms);
819 }
820
821 return $repaired;
822 }
823
824 /**
825 * Repair the string lengths of a serialized value whose text was altered
826 * without updating them.
827 *
828 * The usual cause is a migration or search-and-replace tool that rewrote the
829 * option text (classically normalising CRLF to LF) without recomputing the
830 * `s:<length>:` headers. PHP then refuses the whole structure and unserialize()
831 * returns false, so the value reads as missing even though the data is intact.
832 *
833 * Each declared length is trusted first and only shortened when it does not
834 * land on the closing quote, so a healthy value is returned untouched.
835 *
836 * @since 2.3.0
837 * @param string $data Serialized value.
838 * @return string|false Repaired serialized string, or false if it cannot be walked.
839 */
840 function accua_forms_fix_serialized_lengths( $data ) {
841 $out = '';
842 $i = 0;
843 $len = strlen( $data );
844
845 while ( $i < $len ) {
846 if ( 's:' === substr( $data, $i, 2 ) && preg_match( '/^s:(\d+):"/', substr( $data, $i, 24 ), $m ) ) {
847 $declared = (int) $m[1];
848 $start = $i + strlen( $m[0] );
849 $actual = null;
850
851 // The declared length wins when it is correct; otherwise take the
852 // longest shorter run that still terminates the string properly.
853 for ( $try = $declared; $try >= 0; $try-- ) {
854 if ( '";' === substr( $data, $start + $try, 2 ) ) {
855 $actual = $try;
856 break;
857 }
858 }
859
860 if ( null === $actual ) {
861 return false;
862 }
863
864 $string = substr( $data, $start, $actual );
865 $out .= 's:' . strlen( $string ) . ':"' . $string . '";';
866 $i = $start + $actual + 2;
867 continue;
868 }
869
870 $out .= $data[ $i ];
871 ++$i;
872 }
873
874 return $out;
875 }
876
877 /**
878 * Recover accua_forms_avail_fields when PHP cannot unserialize it.
879 *
880 * accua_forms_install() treats an unreadable field list as an empty one and
881 * writes the shipped defaults over it, which would discard every field the
882 * site had defined - labels, options and all - with no way back. That only
883 * bites on an upgrade, because the install routine runs when the stored DB
884 * version changes, which is exactly when a site carrying such a value meets
885 * it. Observed in the wild on a site whose option had lost its carriage
886 * returns while keeping the original lengths.
887 *
888 * The repaired value is accepted only when it unserializes into something that
889 * actually looks like a field list, and the untouched original is copied to
890 * accua_forms_avail_fields_corrupt_backup either way, so nothing is lost even
891 * when the repair does not work.
892 *
893 * @since 2.3.0
894 * @return bool True when a broken value was recovered.
895 */
896 function accua_forms_recover_avail_fields() {
897 global $wpdb;
898
899 $stored = get_option( 'accua_forms_avail_fields', array() );
900 if ( is_array( $stored ) ) {
901 return false; // Healthy, or genuinely absent.
902 }
903
904 // 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
905 $raw = $wpdb->get_var(
906 $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", 'accua_forms_avail_fields' )
907 );
908
909 if ( ! is_string( $raw ) || '' === $raw || 0 !== strpos( $raw, 'a:' ) ) {
910 return false;
911 }
912
913 // Keep the original before anything else touches it.
914 if ( false === get_option( 'accua_forms_avail_fields_corrupt_backup' ) ) {
915 add_option( 'accua_forms_avail_fields_corrupt_backup', $raw, '', 'no' );
916 }
917
918 $fixed = accua_forms_fix_serialized_lengths( $raw );
919 if ( false === $fixed ) {
920 return false;
921 }
922
923 $recovered = @unserialize( $fixed ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- A failed repair is an expected outcome, handled below
924 if ( ! is_array( $recovered ) || empty( $recovered ) ) {
925 return false;
926 }
927
928 // Only accept something shaped like a field list.
929 foreach ( $recovered as $slug => $definition ) {
930 if ( ! is_string( $slug ) || ! is_array( $definition ) || ! isset( $definition['type'] ) ) {
931 return false;
932 }
933 }
934
935 update_option( 'accua_forms_avail_fields', $recovered );
936
937 return true;
938 }
939
940 /**
941 * Give afsv_field_id the binary collation of its own charset, so field slugs
942 * that differ only in case are distinct values.
943 *
944 * The table is created with the site's default (case-insensitive) collation,
945 * under which 'role' and 'Role' are the same string to MySQL. Two field
946 * definitions differing only in case are perfectly legal - the slugs live in a
947 * PHP array, where keys are case-sensitive - and the consequences in the
948 * database were severe: the submissions list built its field columns with
949 * SELECT DISTINCT afsv_field_id, which collapsed the pair into a single
950 * column, and the PRIMARY KEY (afsv_sub_id, afsv_field_id) treated the second
951 * field of a submission as a duplicate and rejected its value outright, losing
952 * it silently.
953 *
954 * Converting only this column keeps its indexes usable (no per-query COLLATE,
955 * which would prevent the index from being used) and leaves stored values
956 * untouched. The conversion is also safe in this direction: values that were
957 * equal under the case-insensitive collation could never both exist, so
958 * relaxing the comparison cannot produce a duplicate key.
959 *
960 * An explicit ALTER TABLE is used because dbDelta does not compare collations;
961 * WordPress core changes collations the same way (maybe_convert_table_to_utf8mb4).
962 *
963 * @since 2.3.0
964 * @return bool True when the column ends up case-sensitive, false otherwise.
965 */
966 function accua_forms_make_field_slugs_case_sensitive() {
967 global $wpdb;
968
969 $table = $wpdb->prefix . 'accua_forms_submissions_values';
970
971 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema inspection, runs once per upgrade
972 $column = $wpdb->get_row($wpdb->prepare(
973 "SELECT CHARACTER_SET_NAME, COLLATION_NAME FROM information_schema.COLUMNS
974 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = 'afsv_field_id'",
975 $table
976 ));
977
978 if (!$column || empty($column->CHARACTER_SET_NAME)) {
979 return false;
980 }
981
982 // Already case-sensitive (binary collation): nothing to do.
983 if (!empty($column->COLLATION_NAME) && substr($column->COLLATION_NAME, -4) === '_bin') {
984 return true;
985 }
986
987 $collation = $column->CHARACTER_SET_NAME . '_bin';
988
989 // The collation name goes into the ALTER unquoted, so accept it only when the
990 // server itself reports it as a real collation of that charset.
991 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema inspection, runs once per upgrade
992 $known = $wpdb->get_var($wpdb->prepare(
993 "SELECT COLLATION_NAME FROM information_schema.COLLATIONS
994 WHERE COLLATION_NAME = %s AND CHARACTER_SET_NAME = %s",
995 $collation,
996 $column->CHARACTER_SET_NAME
997 ));
998
999 if (!$known || !preg_match('/^[A-Za-z0-9_]+$/', $known)) {
1000 return false;
1001 }
1002
1003 // 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
1004 $wpdb->query("ALTER TABLE `{$table}` MODIFY `afsv_field_id` VARCHAR(77) COLLATE {$known} NOT NULL");
1005
1006 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Verifying the schema change
1007 $now = $wpdb->get_var($wpdb->prepare(
1008 "SELECT COLLATION_NAME FROM information_schema.COLUMNS
1009 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = 'afsv_field_id'",
1010 $table
1011 ));
1012
1013 return (is_string($now) && substr($now, -4) === '_bin');
1014 }
1015
1016 // Runs at init priority 1 (before accua_form_init at priority 5) so the DB schema
1017 // is always up to date before form processing begins. Also ensures translations used
1018 // in accua_forms_get_default_form_data() are loaded when a fresh install is detected.
1019 add_action('init', 'accua_forms_check_db_version_and_update', 1);
1020 function accua_forms_check_db_version_and_update()
1021 {
1022 if ( get_transient( '_accua_forms_data_deleted' ) ) {
1023 return;
1024 }
1025 $db_version = get_option('accua_forms_db_version', '');
1026 if (ACCUA_FORMS_DB_VERSION != $db_version) {
1027 accua_forms_install();
1028 }
1029 }
1030
1031 add_filter('robots_txt', 'accua_forms_robots_txt', 10, 2);
1032 function accua_forms_robots_txt($output, $public)
1033 {
1034 if ($public) {
1035 $site_url = wp_parse_url(site_url());
1036 $path = (!empty($site_url['path'])) ? $site_url['path'] : '';
1037 $output .= "\nUser-agent: *\n";
1038 $output .= "Allow: $path/wp-admin/js/\n";
1039 $output .= "Allow: $path/wp-admin/css/\n";
1040 }
1041 return $output;
1042 }
1043
1044 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function with underscore prefix
1045 function _accua_forms_json_encode($item)
1046 {
1047 static $version = NULL;
1048 if ($version === NULL) {
1049 $version = version_compare(PHP_VERSION, '5.3.0', '>=');
1050 }
1051 if ($version) {
1052 return json_encode($item, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
1053 } else {
1054 return strtr(
1055 json_encode($item),
1056 array(
1057 '&' => '\\u0026',
1058 '<' => '\\u003C',
1059 '>' => '\\u003E',
1060 )
1061 );
1062 }
1063 }
1064
1065 /* per ripulire eventuali token impostati nella versione 1.9.4 */
1066 function accua_forms_cleanup_meta_tokens()
1067 {
1068 global $wpdb;
1069 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time cleanup on activation
1070 $post_ids = $wpdb->get_col("SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_accua_download_token'");
1071
1072 if (!empty($post_ids)) {
1073 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time cleanup on activation
1074 $wpdb->query("DELETE FROM {$wpdb->postmeta} WHERE meta_key = '_accua_download_token'");
1075 }
1076 }
1077 register_activation_hook(__FILE__, 'accua_forms_cleanup_meta_tokens');
1078
1079 // Schedule retention cleanup cron on activation
1080 register_activation_hook( __FILE__, 'accua_forms_activate_retention_cron' );
1081 function accua_forms_activate_retention_cron() {
1082 if ( ! wp_next_scheduled( 'accua_forms_retention_cleanup' ) ) {
1083 wp_schedule_event( time(), 'daily', 'accua_forms_retention_cleanup' );
1084 }
1085 }
1086
1087 // Clear retention cleanup cron on deactivation
1088 register_deactivation_hook( __FILE__, 'accua_forms_deactivate_retention_cron' );
1089 function accua_forms_deactivate_retention_cron() {
1090 wp_clear_scheduled_hook( 'accua_forms_retention_cleanup' );
1091 }
1092
1093 // Add REST API compatibility
1094 add_action('rest_api_init', 'accua_forms_rest_compatibility');
1095
1096 /**
1097 * Ensures proper REST API compatibility by closing any open PHP sessions
1098 * This prevents timeouts when WordPress is making internal REST API requests
1099 */
1100 function accua_forms_rest_compatibility()
1101 {
1102 // Check if a session is active and close it for REST requests
1103 if (session_id() && session_status() === PHP_SESSION_ACTIVE) {
1104 session_write_close();
1105 }
1106
1107 // Remove hooks that might interfere with REST API requests
1108 remove_action('init', 'accua_forms_init_session', 1);
1109 }
1110
1111 /**
1112 * Get the default form data values.
1113 *
1114 * This function returns all default values for form settings including messages,
1115 * email templates, and styling options. Used during installation and for the
1116 * "Restore to Default" functionality in the settings page.
1117 *
1118 * @since 2.0.0-beta.6
1119 * @return array Default form data values.
1120 */
1121 function accua_forms_get_default_form_data()
1122 {
1123 return array(
1124 'success_message' => '<div style="font-family: Arial, Helvetica, sans-serif;">
1125 <h2>' . __('Thank you', 'contact-forms') . ' {first_name} {last_name},</h2>
1126 ' . __('We have received your contact request. Check your inbox for the confirmation message.', 'contact-forms') . '
1127
1128 <strong>' . __('Can\'t find the email?', 'contact-forms') . '</strong>
1129
1130 ' . __('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') . '
1131
1132 ' . __('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') . '
1133
1134 ' . __('For any other issues, please don\'t hesitate to contact us.', 'contact-forms') . '
1135
1136 </div>',
1137 'error_message' => '<div style="font-family: Arial, Helvetica, sans-serif;">
1138 <h2>' . __('Oops! Something went wrong.', 'contact-forms') . '</h2>
1139 ' . __('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') . '
1140
1141 ' . __('Please try filling in the form again.', 'contact-forms') . '
1142
1143 ' . __('For any other issues, please don\'t hesitate to contact us.', 'contact-forms') . '
1144
1145 </div>',
1146 'emails_from_name' => '',
1147 'emails_from' => '',
1148 'admin_emails_to' => get_option('admin_email', ''),
1149 'emails_bcc' => '',
1150 'admin_emails_subject' => __('New contact request from your site', 'contact-forms'),
1151 '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">
1152 <tbody>
1153 <tr>
1154 <td>
1155 <table style="max-width:700px;" border="0" cellspacing="0" cellpadding="0" align="center">
1156 <tbody>
1157 <tr>
1158 <td>
1159 <table style="padding: 10px; border: 1px solid #dfdfdf; max-width:700px;" border="0" cellspacing="0" cellpadding="0" align="center">
1160 <tbody>
1161 <tr valign="top">
1162 <td style="max-width:22px;"></td>
1163 <td style="font-family: Arial, Helvetica, sans-serif; padding: 0px 30px 10px; max-width: 645px;">
1164 <table style="width: 100%; table-layout: fixed;">
1165 <tbody>
1166 <tr>
1167 <td>' . __('On', 'contact-forms') . ' {__submitted_day_month_year} ' . __('at', 'contact-forms') . ' {__submitted_hour} ' . __('the following form was filled in.', 'contact-forms') . '
1168
1169 <hr />
1170
1171 <strong>' . __('Page where the form was filled in', 'contact-forms') . '</strong>
1172 <a href="{__url}" style="word-break: break-all; overflow-wrap: break-word;">{__url}</a></td>
1173 </tr>
1174 <tr><td>
1175 <strong>' . __('Submission review page', 'contact-forms') . '</strong>
1176 <a href="{__review_submission_url}" style="word-break: break-all; overflow-wrap: break-word;">{__review_submission_url}</a>
1177 </td></tr>
1178 <tr>
1179 <td id="submitted_html" style="overflow-wrap: break-word; word-break: normal;">{__submitted_html}</td>
1180 </tr>
1181 <tr>
1182 <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>
1183 </tr>
1184 <tr>
1185 <td><strong>' . __('Contact IP', 'contact-forms') . '</strong> {__anonymized_ip}
1186 <hr />
1187 </td>
1188 </tr>
1189 <tr>
1190 <td>[form_if {__autoreply} [' . __('A confirmation email was sent to', 'contact-forms') . ' <a href="mailto:{email}">{email}</a> ' . __('with the following message', 'contact-forms') . ':
1191 <table>
1192 <tbody>
1193 <tr>
1194 <td>{__confirmation_emails_message}</td>
1195 </tr>
1196 </tbody>
1197 </table> ]]
1198 </td>
1199 </tr>
1200 </tbody>
1201 </table>
1202 </td>
1203 <td style="max-width:23px"></td>
1204 </tr>
1205 </tbody>
1206 </table>
1207 </td>
1208 </tr>
1209 </tbody>
1210 </table>
1211 </td>
1212 </tr>
1213 </tbody>
1214 </table>',
1215 'confirmation_emails_subject' => __('Your message', 'contact-forms'),
1216 'confirmation_emails_message' => '<div style="font-family: Arial, Helvetica, sans-serif;">
1217
1218 <h2>' . __('Thank you', 'contact-forms') . ' {first_name} {last_name},</h2>
1219 ' . __('Thank you for your contact request.', 'contact-forms') . '
1220
1221 ' . __('We will contact you as soon as possible.', 'contact-forms') . '
1222
1223 ' . __('Sincerely,', 'contact-forms') . '
1224
1225 ' . __('The Website Team', 'contact-forms') . '
1226
1227 <hr />
1228 </div>',
1229
1230 'layout' => 'sidebyside',
1231 'style_margin' => '',
1232 'style_border_color' => '',
1233 'style_border_width' => '',
1234 'style_border_radius' => '',
1235 'style_background_color' => '',
1236 'style_padding' => '',
1237 'style_color' => '',
1238 'style_font_size' => '',
1239 'style_field_spacing' => '',
1240 'style_field_border_color' => '',
1241 'style_field_border_width' => '',
1242 'style_field_border_radius' => '',
1243 'style_field_background_color' => '',
1244 'style_field_padding' => '',
1245 'style_field_color' => '',
1246 'style_submit_border_color' => '',
1247 'style_submit_border_width' => '',
1248 'style_submit_border_radius' => '',
1249 'style_submit_background_color' => '',
1250 'style_submit_padding' => '',
1251 'style_submit_color' => '',
1252 'style_submit_font_size' => '',
1253 );
1254 }
1255
1256 /**
1257 * Get all countries with localized names for phone field country selector.
1258 *
1259 * Uses PHP Intl extension to generate localized country names from ISO 3166-1 alpha-2 codes.
1260 * Falls back to country codes if Intl extension is unavailable.
1261 *
1262 * @since 2.0.0-beta.34
1263 * @param string|null $display_locale Locale for display names (default: current WordPress locale).
1264 * @return array Associative array ['US' => 'United States', 'IT' => 'Italy', ...] sorted alphabetically.
1265 */
1266 function accua_forms_get_countries( $display_locale = null ) {
1267 // ISO 3166-1 alpha-2 codes - complete list (249 countries/territories)
1268 $country_codes = array(
1269 'AF', 'AX', 'AL', 'DZ', 'AS', 'AD', 'AO', 'AI', 'AQ', 'AG', 'AR', 'AM', 'AW', 'AU', 'AT', 'AZ',
1270 'BS', 'BH', 'BD', 'BB', 'BY', 'BE', 'BZ', 'BJ', 'BM', 'BT', 'BO', 'BQ', 'BA', 'BW', 'BV', 'BR',
1271 'IO', 'BN', 'BG', 'BF', 'BI', 'KH', 'CM', 'CA', 'CV', 'KY', 'CF', 'TD', 'CL', 'CN', 'CX', 'CC',
1272 'CO', 'KM', 'CG', 'CD', 'CK', 'CR', 'CI', 'HR', 'CU', 'CW', 'CY', 'CZ', 'DK', 'DJ', 'DM', 'DO',
1273 'EC', 'EG', 'SV', 'GQ', 'ER', 'EE', 'ET', 'FK', 'FO', 'FJ', 'FI', 'FR', 'GF', 'PF', 'TF', 'GA',
1274 'GM', 'GE', 'DE', 'GH', 'GI', 'GR', 'GL', 'GD', 'GP', 'GU', 'GT', 'GG', 'GN', 'GW', 'GY', 'HT',
1275 'HM', 'VA', 'HN', 'HK', 'HU', 'IS', 'IN', 'ID', 'IR', 'IQ', 'IE', 'IM', 'IL', 'IT', 'JM', 'JP',
1276 'JE', 'JO', 'KZ', 'KE', 'KI', 'KP', 'KR', 'KW', 'KG', 'LA', 'LV', 'LB', 'LS', 'LR', 'LY', 'LI',
1277 'LT', 'LU', 'MO', 'MK', 'MG', 'MW', 'MY', 'MV', 'ML', 'MT', 'MH', 'MQ', 'MR', 'MU', 'YT', 'MX',
1278 'FM', 'MD', 'MC', 'MN', 'ME', 'MS', 'MA', 'MZ', 'MM', 'NA', 'NR', 'NP', 'NL', 'NC', 'NZ', 'NI',
1279 'NE', 'NG', 'NU', 'NF', 'MP', 'NO', 'OM', 'PK', 'PW', 'PS', 'PA', 'PG', 'PY', 'PE', 'PH', 'PN',
1280 'PL', 'PT', 'PR', 'QA', 'RE', 'RO', 'RU', 'RW', 'BL', 'SH', 'KN', 'LC', 'MF', 'PM', 'VC', 'WS',
1281 'SM', 'ST', 'SA', 'SN', 'RS', 'SC', 'SL', 'SG', 'SX', 'SK', 'SI', 'SB', 'SO', 'ZA', 'GS', 'SS',
1282 'ES', 'LK', 'SD', 'SR', 'SJ', 'SZ', 'SE', 'CH', 'SY', 'TW', 'TJ', 'TZ', 'TH', 'TL', 'TG', 'TK',
1283 'TO', 'TT', 'TN', 'TR', 'TM', 'TC', 'TV', 'UG', 'UA', 'AE', 'GB', 'US', 'UM', 'UY', 'UZ', 'VU',
1284 'VE', 'VN', 'VG', 'VI', 'WF', 'EH', 'YE', 'ZM', 'ZW',
1285 );
1286
1287 // Use WordPress locale if not specified
1288 if ( null === $display_locale ) {
1289 $display_locale = get_locale();
1290 }
1291
1292 // Check if Intl extension is available
1293 if ( ! class_exists( 'Locale' ) ) {
1294 // Fallback: return codes as both key and value
1295 return array_combine( $country_codes, $country_codes );
1296 }
1297
1298 $countries = array();
1299 foreach ( $country_codes as $code ) {
1300 // The trick: prepend '-' to convert region code to locale format
1301 $name = Locale::getDisplayRegion( '-' . $code, $display_locale );
1302 // If Intl returns the code itself (unknown region), use the code
1303 $countries[ $code ] = ( $name && $name !== $code ) ? $name : $code;
1304 }
1305
1306 // Sort alphabetically by localized name
1307 asort( $countries, SORT_LOCALE_STRING );
1308
1309 return $countries;
1310 }
1311
1312 /**
1313 * Get the translated label for a layout value.
1314 *
1315 * Returns the human-readable, translated label for a given layout value.
1316 * Used in layout dropdowns throughout the admin interface.
1317 *
1318 * @since 2.0.0-beta.29
1319 * @param string $layout Layout value: 'sidebyside', 'toplabel', or 'inlinelabel'.
1320 * @return string Translated layout label.
1321 */
1322 function accua_forms_get_layout_label( $layout ) {
1323 switch ( $layout ) {
1324 case 'toplabel':
1325 return __( 'Labels on top of the fields', 'contact-forms' );
1326 case 'inlinelabel':
1327 return __( 'Inline labels', 'contact-forms' );
1328 case 'sidebyside':
1329 default:
1330 return __( 'Labels on the left of the fields', 'contact-forms' );
1331 }
1332 }
1333