PluginProbe
Contact Forms by Cimatti / 2.2.32
Contact Forms by Cimatti v2.2.32
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 / accua-forms.php

accua-forms.php in Contact Forms by Cimatti 2.2.32, at accua-forms.php

4,122 lines 175.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) exit;
3
4 require_once __DIR__ . '/admin/fields-page.php';
5 require_once __DIR__ . '/admin/settings-page.php';
6 require_once __DIR__ . '/admin/form-editor.php';
7 require_once __DIR__ . '/includes/privacy.php';
8
9 /**
10 * Base fill colour for the admin sidebar menu icon.
11 *
12 * WordPress core (svg-painter.js) recolours base64 SVG menu icons to the active
13 * admin colour scheme on load. We resolve that same base colour here and bake it
14 * into the icon so the first server-rendered paint already matches the painted
15 * result, avoiding a brief flash of a differently coloured icon before the JS
16 * repaint. Colour schemes are registered on admin_init (priority 1) which runs
17 * after admin_menu, so the exact colour is applied later by
18 * accua_forms_paint_menu_icon() rather than at menu-registration time.
19 */
20 function accua_forms_admin_menu_icon_color(){
21 global $_wp_admin_css_colors;
22 $scheme = get_user_option('admin_color');
23
24 if ( empty($scheme) || ! isset($_wp_admin_css_colors[$scheme]) ) {
25 $scheme = 'modern';
26 }
27
28 if ( ! empty($_wp_admin_css_colors[$scheme]->icon_colors['base']) ) {
29 return $_wp_admin_css_colors[$scheme]->icon_colors['base'];
30 }
31
32 if ( ! empty($_wp_admin_css_colors['modern']->icon_colors['base']) ) {
33 return $_wp_admin_css_colors['modern']->icon_colors['base'];
34 }
35
36 return '#a7aaad'; // WordPress default menu icon base colour.
37 }
38
39 /**
40 * Monochrome sidebar menu icon as a base64 data URI.
41 *
42 * The standalone brand icon (assets/img/accua-contacts-forms.svg) stays coloured
43 * and is used unchanged in page headers and other contexts; only the sidebar menu
44 * icon is neutral, per the WordPress.org plugin guidelines.
45 */
46 function accua_forms_admin_menu_icon(){
47 $color = accua_forms_admin_menu_icon_color();
48 $svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44.46 44.46"><path fill="' . esc_attr($color) . '" d="m23.97,28.72c1.85,0,2.93-.11,2.93-.11,1.49-.15,2.75.95,2.8,2.44l.01.14c.05,1.5-1.11,2.96-2.58,3.25,0,0-2.53.5-5.25.5-7.24,0-10.11-3.84-10.11-12.98,0-8.6,3.01-12.44,10.26-12.44,2.82,0,5.11.47,5.11.47,1.47.3,2.63,1.78,2.57,3.27l-.01.15c-.05,1.5-1.31,2.6-2.8,2.45,0,0-1.13-.12-2.98-.12-4.67,0-5.54,1.6-5.54,6.22,0,5.2.92,6.76,5.59,6.76M33.36,1.93c-1.06-1.06-3.15-1.93-4.65-1.93H15.75c-1.5,0-3.59.87-4.65,1.93L1.93,11.1C.87,12.16,0,14.25,0,15.75v12.97c0,1.5.87,3.59,1.93,4.65l9.17,9.17c1.06,1.06,3.15,1.93,4.65,1.93h12.97c1.5,0,3.59-.87,4.65-1.93l9.17-9.17c1.06-1.06,1.93-3.15,1.93-4.65V15.75c0-1.5-.87-3.59-1.93-4.65z"/></svg>';
49 return 'data:image/svg+xml;base64,' . base64_encode($svg);
50 }
51
52 /**
53 * Repaint the sidebar menu icon with the active colour scheme's base colour.
54 *
55 * Runs on admin_init (priority 20, after register_admin_color_schemes at 1) when
56 * the colour schemes are available. The icon set at menu-registration time uses
57 * the fallback colour; here we overwrite it in the $menu global with the exact
58 * scheme colour so the first paint matches svg-painter.js and there is no flash.
59 */
60 add_action('admin_init', 'accua_forms_paint_menu_icon', 20);
61 function accua_forms_paint_menu_icon(){
62 global $menu;
63 if ( ! is_array($menu) ) {
64 return;
65 }
66 foreach ( $menu as $i => $item ) {
67 if ( isset($item[2]) && 'accua_forms' === $item[2] ) {
68 $menu[$i][6] = accua_forms_admin_menu_icon();
69 break;
70 }
71 }
72 }
73
74 add_action('admin_menu', 'accua_forms_menu', -95);
75 function accua_forms_menu(){
76 $dashboard_admin_page=add_menu_page('Contact Forms by Cimatti', 'Contact Forms', 'manage_options', 'accua_forms', 'accua_forms_dashboard_page', accua_forms_admin_menu_icon(), '90.90300');
77 add_action('load-'.$dashboard_admin_page, 'accua_forms_dashboard_page_head');
78
79 add_submenu_page('accua_forms', 'Contact Forms by Cimatti', 'Dashboard', 'manage_options', "accua_forms", 'accua_forms_dashboard_page');
80
81 $form_edit_page = add_submenu_page('accua_forms', 'Forms', 'Forms', 'manage_options', "accua_forms_list", 'accua_forms_list_page');
82 add_action('admin_head-'.$form_edit_page, 'accua_forms_edit_page_head');
83 add_action( 'admin_print_styles-'.$form_edit_page, 'accua_forms_edit_page_head_styles');
84 add_action( 'admin_print_scripts-'.$form_edit_page, 'accua_forms_edit_page_head_scripts');
85
86 $form_add_page = add_submenu_page('accua_forms', __('Add new form', 'contact-forms'), __('Add new', 'contact-forms'), 'manage_options', "accua_forms_add", 'accua_forms_add_page');
87 add_action('admin_head-'.$form_add_page, 'accua_forms_edit_page_head');
88 add_action( 'admin_print_styles-'.$form_add_page, 'accua_forms_edit_page_head_styles');
89 add_action( 'admin_print_scripts-'.$form_add_page, 'accua_forms_edit_page_head_scripts');
90
91 $form_submissions_page = add_submenu_page('accua_forms', __('Forms submissions', 'contact-forms') , __('Submissions', 'contact-forms'), 'manage_options', "accua_forms_submissions_list", '__accua_forms_submissions_list_page');
92 add_action('load-'.$form_submissions_page, 'accua_forms_submissions_list_page_load');
93 add_action('admin_head-'.$form_submissions_page, 'accua_forms_submissions_list_page_head');
94 add_action( 'admin_print_styles-'.$form_submissions_page, 'accua_forms_edit_page_head_styles');
95
96 $form_fields_page = add_submenu_page('accua_forms', __( 'Form fields', 'contact-forms'), __('Fields', 'contact-forms'), 'manage_options', "accua_forms_fields", 'accua_forms_fields_page');
97 add_action( 'admin_print_styles-'.$form_fields_page, 'accua_forms_edit_page_head_styles');
98 add_action( 'admin_print_scripts-'.$form_fields_page, 'accua_forms_fields_page_enqueue_scripts');
99
100 $settings_page = add_submenu_page('accua_forms', __( 'Default Forms settings', 'contact-forms'), __('Settings', 'contact-forms'), 'manage_options', "accua_forms_settings", 'accua_forms_settings_page');
101 add_action( 'admin_print_styles-'.$settings_page, 'accua_forms_edit_page_head_styles');
102 add_action( 'admin_print_scripts-'.$settings_page, 'accua_forms_settings_page_head_scripts');
103
104 wp_enqueue_script('jquery-form');
105 wp_enqueue_script('jquery-color');
106 wp_enqueue_script('jquery-ui-core');
107 wp_enqueue_script('jquery-ui-sortable');
108 wp_enqueue_script('jquery-ui-draggable');
109 wp_enqueue_script('jquery-ui-droppable');
110 wp_enqueue_script('jquery-ui-selectable');
111 wp_enqueue_script('jquery-ui-resizable');
112 wp_enqueue_script('jquery-ui-dialog');
113 wp_enqueue_style('wp-jquery-ui-dialog');
114 }
115
116 function accua_forms_report_page_head(){
117 $column_list = array(
118 'month' => __( 'Month', 'contact-forms'),
119 'unique_submissions' => __( 'Unique submissions', 'contact-forms') ,
120 'submissions' => __('Total submissions', 'contact-forms'),
121 );
122 global $hook_suffix;
123 register_column_headers($hook_suffix, $column_list);
124 }
125
126
127 function accua_forms_report_page() {
128 ?>
129 <div id="accua_forms_report_page" class="accua_forms_admin_page wrap">
130 <h2>Forms submissions report</h2>
131
132 <?php
133 global $wpdb, $hook_suffix;
134 $months = array(1 => 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December');
135
136 $query = "SELECT YEAR(sub_date) AS `year`, MONTH(sub_date) AS `month`, COUNT(DISTINCT `email`) AS `unique_submissions`, COUNT(*) AS `submissions`
137 FROM `{$wpdb->prefix}cformssubmissions`
138 GROUP BY `year`, `month`
139 ORDER BY `year` DESC, `month` DESC";
140
141 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- No user input in query
142 $results = $wpdb->get_results($query);
143
144 if ($results) {
145 ?>
146 <style type="text/css">
147 .column-submissions, .column-unique_submissions {
148 text-align: right !important;
149 }
150 </style>
151 <table class="widefat" id="stnl_review_reviewed">
152 <thead>
153 <tr><?php print_column_headers($hook_suffix); ?></tr>
154 </thead>
155
156 <tfoot>
157 <tr><?php print_column_headers($hook_suffix, false); ?></tr>
158 </tfoot>
159
160 <tbody>
161 <?php
162 $alternate = false;
163 $hidden = get_hidden_columns($hook_suffix);
164 foreach ($results as $result){
165 $month = esc_html( $months[$result->month] );
166 $year = esc_html( $result->year );
167 $unique_submissions = esc_html( $result->unique_submissions );
168 $submissions = esc_html( $result->submissions );
169 $alternate_class = ( $alternate = ! $alternate ) ? 'alternate' : '';
170 $month_style = in_array( 'month', $hidden, true ) ? " style='display:none;'" : '';
171 $unique_style = in_array( 'unique_submissions', $hidden, true ) ? " style='display:none;'" : '';
172 $sub_style = in_array( 'submissions', $hidden, true ) ? " style='display:none;'" : '';
173 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- All variables pre-escaped above
174 echo "<tr class='iedit " . esc_attr( $alternate_class ) . "'>\n";
175 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $month, $year pre-escaped with esc_html()
176 echo "<td class='column-month'" . $month_style . ">" . $month . " " . $year . "</td>\n";
177 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $unique_submissions pre-escaped with esc_html()
178 echo "<td class='column-unique_submissions'" . $unique_style . ">" . $unique_submissions . "</td>\n";
179 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $submissions pre-escaped with esc_html()
180 echo "<td class='column-submissions'" . $sub_style . ">" . $submissions . "</td>\n";
181 echo "</tr>\n";
182 }
183 ?>
184 </tbody>
185 </table>
186 <?php
187 }
188 ?>
189
190 </div>
191 <?php
192 }
193
194 function accua_forms_edit_page_head_styles() {
195 wp_enqueue_style( 'accua-forms-admin', plugins_url('assets/css/admin.css', ACCUA_FORMS_FILE), array(), ACCUA_FORMS_CSS_VERSION); //
196 }
197
198 add_action( 'admin_enqueue_scripts', 'accua_forms_enqueue_deactivation_modal' );
199 function accua_forms_enqueue_deactivation_modal( $hook ) {
200 if ( $hook !== 'plugins.php' ) {
201 return;
202 }
203
204 wp_enqueue_script(
205 'accua-forms-deactivation-modal',
206 plugins_url( 'assets/js/admin/deactivation-modal.js', ACCUA_FORMS_FILE ),
207 array( 'jquery' ),
208 ACCUA_FORMS_JS_VERSION,
209 true
210 );
211
212 wp_localize_script( 'accua-forms-deactivation-modal', 'accuaFormsDeactivation', array(
213 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
214 'nonce' => wp_create_nonce( 'accua_forms_deactivation_cleanup' ),
215 'pluginBasename' => plugin_basename( ACCUA_FORMS_FILE ),
216 'i18n' => array(
217 'title' => __( 'What would you like to do with your Contact Forms data?', 'contact-forms' ),
218 'description' => __( 'You are about to deactivate Contact Forms. Choose what to do with your existing data:', 'contact-forms' ),
219 'deleteAll' => __( 'Delete all data', 'contact-forms' ),
220 'deleteAllDesc' => __( 'Permanently remove all forms, submissions, settings, and uploaded files. This cannot be undone.', 'contact-forms' ),
221 'anonymizeAll' => __( 'Anonymize all submissions', 'contact-forms' ),
222 'anonymizeAllDesc'=> __( 'Replace personal data with placeholders and set IPs to 0.0.0.0. Forms and settings will be kept. This cannot be undone.', 'contact-forms' ),
223 'skip' => __( 'Just deactivate', 'contact-forms' ),
224 'skipDesc' => __( 'Keep all data. You can reactivate the plugin later.', 'contact-forms' ),
225 'confirmDelete' => __( 'Are you sure? This will permanently delete ALL forms, submissions, settings, and uploaded files. This cannot be undone.', 'contact-forms' ),
226 'confirmAnonymize'=> __( 'Are you sure? This will anonymize ALL submissions, replacing personal data with placeholders. This cannot be undone.', 'contact-forms' ),
227 'processing' => __( 'Processing…', 'contact-forms' ),
228 'cancel' => __( 'Cancel', 'contact-forms' ),
229 ),
230 ) );
231 }
232
233 function accua_forms_form_fields_order() {
234 if (!current_user_can('manage_options')){
235 wp_die( -1, 403 );
236 }
237 check_ajax_referer('edit_form', '_nonce_edit_form');
238
239 $post = stripslashes_deep($_POST);
240
241 if (empty($post['sidebars'])) {
242 die('-1');
243 }
244
245 // Save to draft instead of directly to database
246 foreach ($post['sidebars'] as $sidebar_id => $order) {
247 if (strpos($sidebar_id, 'cimatti-accua-fields-form-area-') !== 0){
248 die('-1');
249 }
250 $fid = substr($sidebar_id, 31);
251
252 // Get draft data for this form
253 $draft_data = _accua_forms_get_draft_data($fid);
254
255 if (empty($draft_data['fields'])) {
256 die('-1');
257 }
258
259 $old_fields = $draft_data['fields'];
260 unset($draft_data['fields']);
261 $new_fields = array();
262
263 $order = explode(',', $order);
264
265 foreach ($order as $i) {
266 $i = preg_replace('/^(new-)?widget-\\d+_/', '', $i);
267 if (isset($old_fields[$i])) {
268 $new_fields[$i] = $old_fields[$i];
269 unset($old_fields[$i]);
270 }
271 }
272
273 if($old_fields){
274 $new_fields += $old_fields;
275 }
276 $draft_data['fields'] = $new_fields;
277
278 // Save to draft (not to live database)
279 _accua_forms_save_draft($fid, $draft_data);
280 }
281
282 die('1');
283 }
284
285 add_action( 'wp_ajax_accua-save-form-field', 'accua_forms_save_form_field');
286 /* azione dove vengono salvati i campi dei un form - saves to draft */
287 function accua_forms_save_form_field() {
288 if (!current_user_can('manage_options')){
289 wp_die( -1, 403 );
290 }
291 check_ajax_referer('edit_form', '_nonce_edit_form');
292
293 $post = stripslashes_deep($_POST);
294
295 $fid = $post['form-id'];
296 if (accua_forms_validate_form_id($fid) !== '') {
297 die('-1');
298 }
299
300 // Get draft data instead of live data
301 $draft_data = _accua_forms_get_draft_data($fid);
302 if (empty($draft_data['fields'])) {
303 $draft_data['fields'] = array();
304 }
305
306 $avail_fields = get_option('accua_forms_avail_fields', array());
307 @ $wid = (string) $post['widget-id'];
308 if (isset($avail_fields[$wid])) {
309 $check_ref = $wid;
310 } else {
311 if (preg_match('/^(__html|__fieldset-begin|__fieldset-end)-\d+$/', $wid, $matches)) {
312 $check_ref = $matches[1];
313 } else {
314 die('-1');
315 }
316 }
317 if (empty($post['delete_widget'])) {
318 @ $ref = $post['id_base'];
319 if ($ref !== $check_ref) {
320 die('-1');
321 }
322 $required = !empty($post["form-field-{$wid}-required"]);
323 $widget_number = empty($post['multi_number']) ? (empty($post['widget_number']) ? '' : (int)$post['widget_number']) : (int)$post['multi_number'];
324
325 if (isset($draft_data['fields'][$wid])) {
326 $old_istance_data = $draft_data['fields'][$wid];
327 } else {
328 $old_istance_data = array();
329 }
330
331 $draft_data['fields'][$wid] = array (
332 'version' => 2,
333 'istance_id' => $wid,
334 'widget_number' => $widget_number,
335 'ref' => $ref,
336 'required' => $required,
337 );
338
339 if (!empty($post["form-field-{$wid}-override-label"])) {
340 @ $label = (string) $post["form-field-{$wid}-label"];
341 if (!current_user_can('unfiltered_html')) {
342 $label = wp_kses($label, 'post');
343 }
344 $draft_data['fields'][$wid]['label'] = $label;
345 }
346
347 $is_file = false;
348 $is_date = false;
349 if (isset($avail_fields[$wid]['type'])) {
350 if ($avail_fields[$wid]['type'] == 'file') {
351 $is_file = true;
352 } elseif ($avail_fields[$wid]['type'] == 'date') {
353 $is_date = true;
354 }
355 }
356
357 if (!empty($post["form-field-{$wid}-override-default-value"])) {
358 @ $default_value = (string) $post["form-field-{$wid}-default-value"];
359 if ($is_date) {
360 $default_value = accua_forms_filter_date($default_value);
361 } elseif (!current_user_can('unfiltered_html')) {
362 //This is filtered in any case because field type can change
363 $default_value = wp_kses($default_value, 'post');
364 }
365 $draft_data['fields'][$wid]['default_value'] = $default_value;
366 }
367
368 if (!empty($post["form-field-{$wid}-override-allowed-values"])) {
369 @ $allowed_values = (string) $post["form-field-{$wid}-allowed-values"];
370 if ($is_file){
371 $draft_data['fields'][$wid]['allowed_extensions'] = accua_forms_filter_extensions($allowed_values);
372 } else {
373 $draft_data['fields'][$wid]['allowed_values'] = $allowed_values;
374 }
375 }
376 if (!empty($post["form-field-{$wid}-override-datemin-values"])) {
377 @ $mindate_values = (string) $post["form-field-{$wid}-min-of-date"];
378 $draft_data['fields'][$wid]['min_date'] = accua_forms_filter_date($mindate_values);
379 }
380 if (!empty($post["form-field-{$wid}-override-datemax-values"])) {
381 @ $maxdate_values = (string) $post["form-field-{$wid}-max-of-date"];
382 $draft_data['fields'][$wid]['max_date'] = accua_forms_filter_date($maxdate_values);
383 }
384
385 // Save post_type for post-select and post-multicheckbox fields
386 if (isset($post["form-field-{$wid}-post-type"])) {
387 @ $post_type_value = (string) $post["form-field-{$wid}-post-type"];
388 // Validate post type
389 $valid_post_types = get_post_types(array('public' => true));
390 if (isset($valid_post_types[$post_type_value])) {
391 $draft_data['fields'][$wid]['post_type'] = $post_type_value;
392 }
393 }
394
395 // Save country_code for telephone fields (for libphonenumber validation)
396 if (isset($post["form-field-{$wid}-country-code"])) {
397 $country_code = strtoupper(sanitize_text_field($post["form-field-{$wid}-country-code"]));
398 // Validate against the list of countries
399 $valid_countries = accua_forms_get_countries();
400 if (isset($valid_countries[$country_code])) {
401 $draft_data['fields'][$wid]['country_code'] = $country_code;
402 }
403 }
404
405 /**
406 * Filter field instance data before saving to draft.
407 *
408 * @param array $field_instance The field instance data being saved.
409 * @param string $widget_id The field widget ID.
410 * @param array $post_data The raw POST data (already stripslashed).
411 * @param array $field_def The field definition from avail_fields.
412 */
413 $draft_data['fields'][$wid] = apply_filters(
414 'accua_forms_save_field_data',
415 $draft_data['fields'][$wid],
416 $wid,
417 $post,
418 isset($avail_fields[$wid]) ? $avail_fields[$wid] : array()
419 );
420
421 // Save custom CSS class for the field wrapper
422 if (isset($post["form-field-{$wid}-css-class"])) {
423 $css_class_raw = sanitize_text_field($post["form-field-{$wid}-css-class"]);
424 if ($css_class_raw !== '') {
425 // Sanitize each class individually
426 $classes = array_filter(array_map('sanitize_html_class', explode(' ', $css_class_raw)));
427 $draft_data['fields'][$wid]['css_class'] = implode(' ', $classes);
428 } else {
429 $draft_data['fields'][$wid]['css_class'] = '';
430 }
431 }
432
433 // Save custom CSS ID for the field wrapper
434 if (isset($post["form-field-{$wid}-css-id"])) {
435 $css_id_raw = sanitize_text_field($post["form-field-{$wid}-css-id"]);
436 $draft_data['fields'][$wid]['css_id'] = sanitize_html_class($css_id_raw);
437 }
438
439 // Save fieldset style (fieldset-begin only)
440 if (isset($post["form-field-{$wid}-fieldset-style"])) {
441 $allowed_fieldset_styles = array(
442 'border-off-title-off', 'border-on-title-off',
443 'border-on-title-inline', 'border-on-title-outside',
444 'border-on-title-inside', 'border-off-title-on',
445 );
446 $fs = sanitize_text_field($post["form-field-{$wid}-fieldset-style"]);
447 if (in_array($fs, $allowed_fieldset_styles, true)) {
448 $draft_data['fields'][$wid]['fieldset_style'] = $fs;
449 }
450 }
451
452 // Save custom required message override
453 if (!empty($post["form-field-{$wid}-override-required-msg"])) {
454 $draft_data['fields'][$wid]['custom_required_message'] = sanitize_text_field($post["form-field-{$wid}-custom-required-msg"]);
455 }
456
457 // Save custom format message override (email/phone)
458 if (!empty($post["form-field-{$wid}-override-format-msg"])) {
459 $draft_data['fields'][$wid]['custom_format_message'] = sanitize_text_field($post["form-field-{$wid}-custom-format-msg"]);
460 }
461 } else {
462 unset($draft_data['fields'][$wid]);
463 }
464
465 // Save to draft (not to live database)
466 _accua_forms_save_draft($fid, $draft_data);
467
468 die('1');
469 }
470
471 function accua_forms_filter_text($text) {
472 if ( is_object( $text ) || is_array( $text ) ) {
473 return '';
474 }
475
476 $text = (string) $text;
477 $text = wp_check_invalid_utf8( $text );
478 $text = preg_replace( '/[ \t\n\r\0\x0B]+/', ' ', $text );
479 $text = trim( $text );
480
481 return $text;
482 }
483
484 function accua_forms_filter_email($email) {
485 $parts = explode('<', $email);
486 if (isset($parts[1])) {
487 $email = accua_forms_filter_text(trim($parts[1], "<> \t\n\r\0\x0B"));
488 $display_name = accua_forms_filter_text(trim($parts[0]));
489 return "$display_name <$email>";
490 } else {
491 return accua_forms_filter_text($email);
492 }
493 }
494
495 function accua_forms_filter_emails($emails) {
496 $split_emails = preg_split("/\s*[,;]\s*/", $emails);
497 $emails = array();
498 foreach ($split_emails as $email) {
499 $emails[] = accua_forms_filter_email($email);
500 }
501 return implode(', ', $emails);
502 }
503
504 function accua_forms_filter_extensions($extensions) {
505 $cleaned_extensions = array();
506 $mimes = get_allowed_mime_types();
507 $extensions = explode("\n", $extensions);
508 foreach ($extensions as $extension) {
509 $extension = strtolower( trim( ltrim( trim( $extension ), '.' ) ) );
510 if ($extension !== '') {
511 foreach ( $mimes as $ext_preg => $mime_match ) {
512 $ext_preg = '!^' . $ext_preg . '$!i';
513 if ( preg_match( $ext_preg, $extension ) ) {
514 $cleaned_extensions[] = $extension;
515 break;
516 }
517 }
518 }
519 }
520 return implode("\n", $cleaned_extensions);
521 }
522
523 function accua_forms_filter_settings($form_settings) {
524 foreach($form_settings as $k => $v) {
525 switch ($k) {
526 case 'success_message_no_message':
527 case 'error_message_no_message':
528 case 'admin_emails_message_no_message':
529 case 'confirmation_emails_message_no_message':
530 case 'use_ajax':
531 //boolean
532 $form_settings[$k] = (bool) $v;
533 break;
534 case 'layout':
535 // Only set if valid layout value, otherwise remove to use default
536 if ($v === 'toplabel' || $v === 'inlinelabel' || $v === 'sidebyside') {
537 $form_settings[$k] = $v;
538 } else {
539 unset($form_settings[$k]); // Reset to default
540 }
541 break;
542 case 'emails_from':
543 // single email
544 $form_settings[$k] = accua_forms_filter_email($v);
545 break;
546 case 'admin_emails_to':
547 case 'emails_bcc':
548 //emails list
549 $form_settings[$k] = accua_forms_filter_email($v);
550 break;
551 case 'success_message':
552 case 'error_message':
553 case 'admin_emails_message':
554 case 'confirmation_emails_message':
555 //HTML
556 if (!current_user_can('unfiltered_html')) {
557 $form_settings[$k] = wp_kses($v, 'post');
558 }
559 break;
560 //case 'title':
561 //case 'emails_from_name':
562 //case 'admin_emails_subject':
563 //case 'confirmation_emails_subject':
564 //case 'style_*':
565 default:
566 // text field
567 $form_settings[$k] = accua_forms_filter_text($v);
568 }
569 }
570 return $form_settings;
571 }
572
573 /**
574 * AJAX handler to restore default message values.
575 *
576 * Restores the default content for a specific message section:
577 * - success_message: On-screen success message
578 * - error_message: On-screen error message
579 * - admin_emails: Admin notification email (subject + message only)
580 * - confirmation_emails: Confirmation email (subject + message only)
581 *
582 * @since 2.0.0-beta.6
583 */
584 add_action('wp_ajax_accua_forms_restore_default_message', 'accua_forms_restore_default_message');
585 function accua_forms_restore_default_message() {
586 if (!current_user_can('manage_options')) {
587 wp_send_json_error(array('message' => __('Permission denied.', 'contact-forms')), 403);
588 }
589
590 check_ajax_referer('accua_forms_restore_default', 'nonce');
591
592 $message_type = isset($_POST['message_type']) ? sanitize_key($_POST['message_type']) : '';
593
594 // Get default values
595 $defaults = accua_forms_get_default_form_data();
596
597 // Define which fields to restore for each message type
598 $restore_map = array(
599 'success_message' => array('success_message'),
600 'error_message' => array('error_message'),
601 'admin_emails' => array('admin_emails_subject', 'admin_emails_message'),
602 'confirmation_emails' => array('confirmation_emails_subject', 'confirmation_emails_message'),
603 );
604
605 if (!isset($restore_map[$message_type])) {
606 wp_send_json_error(array('message' => __('Invalid message type.', 'contact-forms')), 400);
607 }
608
609 // Get current form data
610 $form_data = get_option('accua_forms_default_form_data', array());
611 if (!is_array($form_data)) {
612 $form_data = array();
613 }
614
615 // Restore the specified fields
616 $restored_values = array();
617 foreach ($restore_map[$message_type] as $field) {
618 $form_data[$field] = $defaults[$field];
619 $restored_values[$field] = $defaults[$field];
620 }
621
622 // Save updated form data
623 update_option('accua_forms_default_form_data', $form_data);
624
625 wp_send_json_success(array(
626 'message' => __('Default values restored successfully.', 'contact-forms'),
627 'values' => $restored_values,
628 ));
629 }
630
631 add_action( 'wp_ajax_accua-save-form-settings', 'accua_forms_save_form_settings');
632 function accua_forms_save_form_settings() {
633 if (!current_user_can('manage_options')){
634 wp_die( -1, 403 );
635 }
636 check_ajax_referer('edit_form', '_nonce_edit_form');
637
638 $post = stripslashes_deep($_POST);
639
640 $fid = $post['form-id'];
641 if (accua_forms_validate_form_id($fid) !== '') {
642 die('-1');
643 }
644
645 // Get draft data instead of live data
646 $draft_data = _accua_forms_get_draft_data($fid);
647
648 $settings = array(
649 'title',
650 'success_message',
651 'success_message_no_message',
652 'error_message',
653 'error_message_no_message',
654 'emails_from_name',
655 'emails_from',
656 'admin_emails_to',
657 'emails_bcc',
658 'admin_emails_subject',
659 'admin_emails_message',
660 'admin_emails_message_no_message',
661 'confirmation_emails_subject',
662 'confirmation_emails_message',
663 'confirmation_emails_message_no_message',
664 'gads_conversion_tracking_code',
665 //'use_ajax',
666
667 'layout',
668 'style_margin',
669 'style_border_color',
670 'style_border_width',
671 'style_border_radius',
672 'style_background_color',
673 'style_padding',
674 'style_color',
675 'style_font_size',
676 'style_field_spacing',
677 'style_field_border_color',
678 'style_field_border_width',
679 'style_field_border_radius',
680 'style_field_background_color',
681 'style_field_padding',
682 'style_field_color',
683 'style_submit_border_color',
684 'style_submit_border_width',
685 'style_submit_border_radius',
686 'style_submit_background_color',
687 'style_submit_padding',
688 'style_submit_color',
689 'style_submit_font_size',
690 'submission_retention_value',
691 'submission_retention_unit',
692 'submission_retention_mode',
693 );
694
695 // print_r($post);
696
697 $new_form_settings = array();
698 foreach($settings as $i) {
699 if (isset($post[$i])) {
700 $new_form_settings[$i] = $post[$i];
701 }
702 if (isset($draft_data[$i])) {
703 unset($draft_data[$i]);
704 }
705 }
706
707 $draft_data += accua_forms_filter_settings($new_form_settings);
708
709 $draft_data['use_ajax'] = !empty($post['use_ajax']);
710 $draft_data['submission_retention_override'] = !empty($post['submission_retention_override']);
711
712 // Save to draft (not to live database)
713 _accua_forms_save_draft($fid, $draft_data);
714
715 // Return JSON response for AJAX handler
716 wp_send_json_success($draft_data);
717 }
718
719 /**
720 * AJAX handler to publish draft to live database.
721 * Called when user clicks the Save button.
722 */
723 add_action( 'wp_ajax_accua-publish-form-draft', 'accua_forms_publish_form_draft');
724 function accua_forms_publish_form_draft() {
725 if (!current_user_can('manage_options')){
726 wp_die( -1, 403 );
727 }
728 check_ajax_referer('edit_form', '_nonce_edit_form');
729
730 $post = stripslashes_deep($_POST);
731 $fid = isset($post['form-id']) ? $post['form-id'] : '';
732
733 if (accua_forms_validate_form_id($fid) !== '') {
734 wp_send_json_error(array('message' => __('Invalid form ID.', 'contact-forms')), 400);
735 }
736
737 // Publish the draft
738 $result = _accua_forms_publish_draft($fid);
739
740 if ($result) {
741 wp_send_json_success(array('message' => __('Form saved successfully.', 'contact-forms')));
742 } else {
743 // Draft might not exist (nothing to publish) - this is OK for a new form
744 // Check if form exists in database
745 $forms_data = get_option('accua_forms_saved_forms', array());
746 if (isset($forms_data[$fid])) {
747 wp_send_json_success(array('message' => __('No changes to save.', 'contact-forms')));
748 } else {
749 wp_send_json_error(array('message' => __('Failed to save form.', 'contact-forms')), 500);
750 }
751 }
752 }
753
754 /**
755 * AJAX handler to discard draft and reload from published data.
756 * Called when user clicks "Discard changes".
757 */
758 add_action( 'wp_ajax_accua-discard-form-draft', 'accua_forms_discard_form_draft');
759 function accua_forms_discard_form_draft() {
760 if (!current_user_can('manage_options')){
761 wp_die( -1, 403 );
762 }
763 check_ajax_referer('edit_form', '_nonce_edit_form');
764
765 $post = stripslashes_deep($_POST);
766 $fid = isset($post['form-id']) ? $post['form-id'] : '';
767
768 if (accua_forms_validate_form_id($fid) !== '') {
769 wp_send_json_error(array('message' => __('Invalid form ID.', 'contact-forms')), 400);
770 }
771
772 // Delete the draft
773 _accua_forms_delete_draft($fid);
774
775 wp_send_json_success(array('message' => __('Changes discarded.', 'contact-forms')));
776 }
777
778 /**
779 * Filter an admin-configured post_status value for post fields down to the
780 * statuses those fields may expose in a public dropdown: publish and private.
781 *
782 * Draft/pending/future content is never exposed, regardless of configuration.
783 *
784 * @since 2.2.27
785 * @param string|array $post_status Comma-separated string or array of statuses.
786 * @return array Allowed statuses (may be empty).
787 */
788 function accua_forms_filter_field_post_status($post_status) {
789 if (!is_array($post_status)) {
790 $post_status = explode(',', (string) $post_status);
791 }
792 $post_status = array_map('trim', $post_status);
793 return array_values(array_intersect($post_status, array('publish', 'private')));
794 }
795
796 /**
797 * Check whether an extra_args string received from the AJAX endpoint matches a
798 * post-select / post-multicheckbox configuration actually stored by an admin,
799 * for the post type the request resolved to.
800 *
801 * The extra_args string is echoed into the form markup and sent back by the
802 * browser, so it is client-controlled. Privileged parameters (post_status=private)
803 * are only honored when the exact string exists in a saved field configuration —
804 * otherwise any visitor could craft a request that enumerates private post titles.
805 * The post type is part of the match: a query string saved for one post type must
806 * not unlock private posts of a different type (the field's post type lives in a
807 * separate setting, so the string alone does not identify what it exposes).
808 *
809 * @since 2.2.27
810 * @param string $extra_args Sanitized extra_args string from the request.
811 * @param string $post_type Post type the request resolved to (after the
812 * post_type override inside extra_args, if any).
813 * @return bool True when a stored field configuration matches both.
814 */
815 function accua_forms_extra_args_is_saved_config($extra_args, $post_type) {
816 $extra_args = trim($extra_args);
817 if ($extra_args === '') {
818 return false;
819 }
820
821 // Candidate configurations: array of (allowed_values, configured post type).
822 $candidates = array();
823 $avail_fields = get_option('accua_forms_avail_fields', array());
824 foreach ($avail_fields as $field) {
825 if (!empty($field['type']) && ($field['type'] === 'post-select' || $field['type'] === 'post-multicheckbox') && isset($field['allowed_values'])) {
826 $candidates[] = array($field['allowed_values'], isset($field['post_type']) ? $field['post_type'] : 'page');
827 }
828 }
829 $forms = get_option('accua_forms_saved_forms', array());
830 foreach ($forms as $form) {
831 if (empty($form['fields']) || !is_array($form['fields'])) {
832 continue;
833 }
834 foreach ($form['fields'] as $inst) {
835 if (!is_array($inst) || empty($inst['ref']) || !isset($avail_fields[$inst['ref']]['type'])) {
836 continue;
837 }
838 $type = $avail_fields[$inst['ref']]['type'];
839 if (($type === 'post-select' || $type === 'post-multicheckbox') && isset($inst['allowed_values'])) {
840 $inst_post_type = isset($inst['post_type']) ? $inst['post_type']
841 : (isset($avail_fields[$inst['ref']]['post_type']) ? $avail_fields[$inst['ref']]['post_type'] : 'page');
842 $candidates[] = array($inst['allowed_values'], $inst_post_type);
843 }
844 }
845 }
846
847 foreach ($candidates as $candidate) {
848 list($candidate_args, $candidate_post_type) = $candidate;
849 if (trim(sanitize_text_field($candidate_args)) !== $extra_args) {
850 continue;
851 }
852 // Resolve the candidate's effective post type the same way the request
853 // does: a post_type override inside the string wins over the field setting.
854 $candidate_extra = array();
855 wp_parse_str($extra_args, $candidate_extra);
856 if (!empty($candidate_extra['post_type'])) {
857 $candidate_post_type = sanitize_text_field($candidate_extra['post_type']);
858 }
859 if ($candidate_post_type === $post_type) {
860 return true;
861 }
862 }
863 return false;
864 }
865
866 /**
867 * AJAX handler to get posts for post-select fields with pagination.
868 * Available to both logged-in and anonymous users (for frontend forms).
869 *
870 * @since 2.0.0-beta.29
871 */
872 add_action('wp_ajax_accua_forms_get_posts', 'accua_forms_ajax_get_posts');
873 add_action('wp_ajax_nopriv_accua_forms_get_posts', 'accua_forms_ajax_get_posts');
874 function accua_forms_ajax_get_posts() {
875 // Verify nonce
876 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verification
877 if (!isset($_REQUEST['_nonce']) || !wp_verify_nonce($_REQUEST['_nonce'], 'accua_forms_get_posts')) {
878 wp_send_json_error(array('message' => __('Security check failed.', 'contact-forms')), 403);
879 }
880
881 // Sanitize inputs
882 $post_type = isset($_REQUEST['post_type']) ? sanitize_text_field(wp_unslash($_REQUEST['post_type'])) : 'page';
883 $search = isset($_REQUEST['search']) ? sanitize_text_field(wp_unslash($_REQUEST['search'])) : '';
884 $page = isset($_REQUEST['page']) ? absint($_REQUEST['page']) : 1;
885 $per_page = isset($_REQUEST['per_page']) ? min(absint($_REQUEST['per_page']), 100) : 50;
886 $extra_args = isset($_REQUEST['extra_args']) ? sanitize_text_field(wp_unslash($_REQUEST['extra_args'])) : '';
887 $selected = isset($_REQUEST['selected']) ? sanitize_text_field(wp_unslash($_REQUEST['selected'])) : '';
888
889 // Validate post type
890 $valid_post_types = get_post_types(array('public' => true));
891 if (!isset($valid_post_types[$post_type])) {
892 $post_type = 'page';
893 }
894
895 // Calculate offset
896 $offset = ($page - 1) * $per_page;
897
898 // Statuses the response may contain (extended below when the field
899 // configuration explicitly requests private posts).
900 $allowed_statuses = array('publish');
901
902 // Build query arguments
903 $args = array(
904 'post_type' => $post_type,
905 'number' => $per_page + 1, // Get one extra to check if there are more
906 'offset' => $offset,
907 's' => $search,
908 );
909
910 // Parse extra arguments (backward compatibility with allowed_values textarea)
911 if (!empty($extra_args)) {
912 // Parse the query string format
913 $extra = array();
914 wp_parse_str($extra_args, $extra);
915
916 // Allow post_type override from extra_args (backward compatibility)
917 if (isset($extra['post_type'])) {
918 $override_post_type = sanitize_text_field($extra['post_type']);
919 // Validate the overridden post type
920 if (isset($valid_post_types[$override_post_type])) {
921 $post_type = $override_post_type;
922 $args['post_type'] = $post_type;
923 }
924 }
925
926 // Merge only safe parameters
927 $safe_params = array('meta_key', 'meta_value', 'authors', 'parent', 'child_of', 'exclude', 'include', 'sort_column', 'sort_order');
928 foreach ($safe_params as $param) {
929 if (isset($extra[$param])) {
930 $args[$param] = $extra[$param];
931 }
932 }
933
934 // post_status is a privileged parameter: only publish/private are ever
935 // honored, and 'private' only when the extra_args string matches a field
936 // configuration stored by an admin (or the user can read private posts,
937 // e.g. the form editor preview). Otherwise a visitor could craft a request
938 // that enumerates private post titles.
939 if (!empty($extra['post_status'])) {
940 $requested_statuses = accua_forms_filter_field_post_status($extra['post_status']);
941 if (in_array('private', $requested_statuses, true)
942 && !current_user_can('read_private_posts')
943 && !accua_forms_extra_args_is_saved_config($extra_args, $post_type)) {
944 $requested_statuses = array('publish');
945 }
946 if (!empty($requested_statuses)) {
947 $args['post_status'] = $requested_statuses;
948 $allowed_statuses = $requested_statuses;
949 }
950 }
951 }
952
953 // Get posts using WPML-compatible function
954 $posts = accua_get_pages($args);
955
956 // Check if there are more results
957 $has_more = count($posts) > $per_page;
958 if ($has_more) {
959 array_pop($posts); // Remove the extra item
960 }
961
962 // Format results for the dropdown
963 $results = array();
964 foreach ($posts as $post) {
965 $results[] = array(
966 'id' => $post->ID,
967 'text' => $post->post_title,
968 );
969 }
970
971 // If this is the first page and we have a selected value, ensure it's in the list
972 if ($page === 1 && !empty($selected) && is_numeric($selected)) {
973 $selected_id = absint($selected);
974 $found = false;
975 foreach ($results as $result) {
976 if ($result['id'] === $selected_id) {
977 $found = true;
978 break;
979 }
980 }
981 // If selected post not in results, fetch it separately and prepend.
982 // Only statuses the field is allowed to expose (publish, plus private when
983 // explicitly configured): this endpoint is available to anonymous visitors,
984 // so it must not disclose titles of other drafts/private/pending posts.
985 if (!$found) {
986 $selected_post = get_post($selected_id);
987 if ($selected_post && $selected_post->post_type === $post_type && in_array($selected_post->post_status, $allowed_statuses, true)) {
988 array_unshift($results, array(
989 'id' => $selected_post->ID,
990 'text' => $selected_post->post_title,
991 ));
992 }
993 }
994 }
995
996 wp_send_json_success(array(
997 'results' => $results,
998 'more' => $has_more,
999 'page' => $page,
1000 ));
1001 }
1002
1003 /**
1004 * Get available public post types for the post-select field editor.
1005 *
1006 * @since 2.0.0-beta.29
1007 * @return array Array of post type slug => label pairs.
1008 */
1009 function accua_forms_get_public_post_types() {
1010 $post_types = get_post_types(array('public' => true), 'objects');
1011 $options = array();
1012 foreach ($post_types as $post_type) {
1013 // Skip attachments
1014 if ($post_type->name === 'attachment') {
1015 continue;
1016 }
1017 $options[$post_type->name] = $post_type->labels->singular_name;
1018 }
1019 return $options;
1020 }
1021
1022 function accua_forms_field_settings_form_counter() {
1023 static $i = 0;
1024 $i++;
1025 return $i;
1026 }
1027
1028 /* impostazioni dei campi */
1029 function accua_forms_field_text_settings_form($fid, $field_data=array(), $istance_data=array()){
1030 static $html_multi_number = 0;
1031 $i = accua_forms_field_settings_form_counter();
1032
1033 $hidden = '';
1034 if (!is_array($istance_data)) {
1035 if ($istance_data === 'hidden') {
1036 $hidden = 'style="display:none"';
1037 }
1038 $istance_data = array();
1039 $empty_istance = true;
1040 } else {
1041 $empty_istance = empty($istance_data);
1042 }
1043
1044 if (!is_array($field_data)){
1045 $field_data = array();
1046 }
1047
1048 $field_data += array(
1049 'version' => '1',
1050 'id' => '__html',
1051 'name' => __( 'Custom HTML content', 'contact-forms'),
1052 'type' => 'html',
1053 'description' => __('Use this special field to inject raw HTML in the form. You can use this multiple times.', 'contact-forms'),
1054 'default_value' => '',
1055 'allowed_values' => '',
1056 'allowed_extensions' => '',
1057 );
1058
1059 $override_label = isset($istance_data['label']) ? 'checked="checked"' : '';
1060 $override_default_value = isset($istance_data['default_value']) ? 'checked="checked"' : '';
1061 $override_allowed_values = isset($istance_data['allowed_values']) ? 'checked="checked"' : '';
1062 $override_allowed_extensions = '';
1063 $override_custom_required_msg = isset($istance_data['custom_required_message']) ? 'checked="checked"' : '';
1064 $override_custom_format_msg = isset($istance_data['custom_format_message']) ? 'checked="checked"' : '';
1065
1066 if ($field_data['type'] == 'file') {
1067 if (isset($istance_data['version']) && $istance_data['version'] >= 2) {
1068 if (isset($istance_data['allowed_extensions'])) {
1069 $override_allowed_extensions = 'checked="checked"';
1070 }
1071 } else {
1072 if (isset($istance_data['allowed_values'])) {
1073 $istance_data['allowed_extensions'] = $istance_data['allowed_values'];
1074 $override_allowed_extensions = 'checked="checked"';
1075 }
1076 }
1077
1078 if ($field_data['version'] < 2) {
1079 $field_data['allowed_extensions'] = $field_data['allowed_values'];
1080 }
1081
1082 if ($field_data['allowed_extensions'] === '') {
1083 $file_data = get_option('accua_forms_default_file_field_data',array());
1084 if (isset($file_data['valid_extensions'])){
1085 $field_data['allowed_extensions'] = $file_data['valid_extensions'];
1086 }
1087 }
1088 }
1089
1090 if ($field_data['type'] == 'date') {
1091 $override_mindate_values = isset($istance_data['min_date']) ? 'checked="checked"' : '';
1092 $override_maxdate_values = isset($istance_data['max_date']) ? 'checked="checked"' : '';
1093 $istance_data += array(
1094 'min_date' => $field_data['min_date'],
1095 'max_date' => $field_data['max_date'],
1096 'default_value' => $field_data['default_date_value'],
1097 );
1098 }
1099
1100 $istance_data += array(
1101 //'version' => 1,
1102 'istance_id' => $field_data['id'],
1103 'widget_number' => '',
1104 'ref' => $field_data['id'],
1105 'label' => $field_data['name'],
1106 'default_value' => $field_data['default_value'],
1107 'allowed_values' => $field_data['allowed_values'],
1108 'allowed_extensions' => $field_data['allowed_extensions'],
1109 'required' => false,
1110 'post_type' => 'page', // Default post type for post-select fields
1111 'css_class' => '',
1112 'css_id' => '',
1113 'custom_required_message' => '',
1114 'custom_format_message' => '',
1115 'fieldset_style' => 'border-off-title-off',
1116 );
1117
1118 foreach ($istance_data as $key => $value) {
1119 $istance_data[$key] = esc_attr($istance_data[$key]);
1120 }
1121
1122 foreach ($field_data as $key => $value) {
1123 $field_data[$key] = esc_attr($field_data[$key]);
1124 }
1125
1126 $multi_number = '';
1127 $add_new = '';
1128
1129 if (in_array($istance_data['ref'], array('__html','__fieldset-begin','__fieldset-end'))) {
1130 $forceoverride_field = true;
1131 if ($empty_istance) {
1132 $add_new = 'multi';
1133 $istance_data['istance_id'] .= '-__i__';
1134 $istance_data['widget_number'] = 1;
1135 $multi_number = 1 + $html_multi_number;
1136 } else {
1137 if ($html_multi_number < $istance_data['widget_number']) {
1138 $html_multi_number = $istance_data['widget_number'];
1139 }
1140 }
1141 } else {
1142 $forceoverride_field = false;
1143 $add_new = $empty_istance ? 'single' : '';
1144 }
1145
1146 $fid = esc_attr($fid);
1147 $testi_eot = array (
1148 'label' => __( 'Label', 'contact-forms'),
1149 'override' => __( 'override', 'contact-forms'),
1150 'default_value' => __( 'Default value', 'contact-forms'),
1151 'default_values' => __( 'Default value(s)', 'contact-forms'),
1152 'desc_def' => __( 'For multiple default values, use | as separator.', 'contact-forms'),
1153 'allowed_values' => __( 'Allowed values', 'contact-forms'),
1154 'desc_all' => __( 'The possible values this field can contain. Enter one value per line, in the format key|label. The key is the value that will be stored in the database. The label is optional, and the key will be used as the label if no label is specified.', 'contact-forms'),
1155 'allowed_extensions' => __( 'Allowed extensions', 'contact-forms'),
1156 'desc_all_ext' => __( 'Accepted file extensions. One per line, without dots.', 'contact-forms'),
1157 'required' => __( 'Required', 'contact-forms'),
1158 'custom_HTML_content' => __( 'Custom HTML content', 'contact-forms'),
1159 'refresh_preview' => __( 'Refresh Preview', 'contact-forms'),
1160 'add' => __( 'Add field', 'contact-forms'),
1161 'remove' => __( 'Remove', 'contact-forms'),
1162 'close' => __( 'Close', 'contact-forms'),
1163 'save' => __( 'Save', 'contact-forms'),
1164 'min-of-date' => __( 'Min date', 'contact-forms'),
1165 'max-of-date' => __( 'Max date', 'contact-forms')
1166 );
1167
1168 if ($forceoverride_field) {
1169 $override_begin = '';
1170 $override_type = 'hidden';
1171 $override_end = '';
1172 } else {
1173 $override_begin = "({$testi_eot['override']}: ";
1174 $override_type = 'checkbox';
1175 $override_end = ')';
1176 }
1177
1178 // phpcs:disable PluginCheck.CodeAnalysis.Heredoc.NotAllowed, WordPress.Security.EscapeOutput.HeredocOutputNotEscaped -- Heredoc used for HTML templates with pre-escaped variables
1179 $content = <<<EOT
1180 <p><label for="widget-{$istance_data['istance_id']}-label">{$testi_eot['label']}:</label>
1181 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-label" value="1" {$override_label} />{$override_end}<br>
1182 <input type="text" value="{$istance_data['label']}" name="form-field-{$istance_data['istance_id']}-label" id="widget-{$istance_data['istance_id']}-label" class="widefat"></p>
1183 EOT;
1184
1185 $default_value = <<<EOT
1186 <p><label for="widget-{$istance_data['istance_id']}-default-value">{$testi_eot['default_value']}:</label>
1187 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-default-value" value="1" {$override_default_value} />{$override_end}<br>
1188 <input type="text" value="{$istance_data['default_value']}" name="form-field-{$istance_data['istance_id']}-default-value" id="widget-{$istance_data['istance_id']}-default-value" class="widefat"></p>
1189 EOT;
1190
1191 $default_values = <<<EOT
1192 <p><label for="widget-{$istance_data['istance_id']}-default-value">{$testi_eot['default_value']}:</label>
1193 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-default-value" value="1" {$override_default_value} />{$override_end}<br>
1194 <input type="text" value="{$istance_data['default_value']}" name="form-field-{$istance_data['istance_id']}-default-value" id="widget-{$istance_data['istance_id']}-default-value" class="widefat"><br />
1195 </p>
1196 EOT;
1197
1198 $allowed_values = <<<EOT
1199 <p><label for="widget-{$istance_data['istance_id']}-allowed-values">{$testi_eot['allowed_values']}:</label>
1200 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-allowed-values" value="1" {$override_allowed_values} />{$override_end}<br>
1201 <textarea rows="6" cols="50" name="form-field-{$istance_data['istance_id']}-allowed-values" id="widget-{$istance_data['istance_id']}-allowed-values" class="widefat">{$istance_data['allowed_values']}</textarea><br />
1202 {$testi_eot['desc_all']}</p>
1203 EOT;
1204
1205 $allowed_ext = <<<EOT
1206 <p><label for="widget-{$istance_data['istance_id']}-allowed-values">{$testi_eot['allowed_extensions']}:</label>
1207 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-allowed-values" value="1" {$override_allowed_extensions} />{$override_end}<br>
1208 <textarea rows="6" cols="50" name="form-field-{$istance_data['istance_id']}-allowed-values" id="widget-{$istance_data['istance_id']}-allowed-values" class="widefat">{$istance_data['allowed_extensions']}</textarea><br />
1209 {$testi_eot['desc_all_ext']}</p>
1210 EOT;
1211
1212 $required_checked = empty($istance_data['required']) ? '' : 'checked="checked"';
1213 $required = <<<EOT
1214 <p><label for="widget-{$istance_data['istance_id']}-required">{$testi_eot['required']}:</label>
1215 <input type="checkbox" value="1" {$required_checked} name="form-field-{$istance_data['istance_id']}-required" id="widget-{$istance_data['istance_id']}-required"></p>
1216 EOT;
1217
1218 // Custom required message override (checkbox + text input, same pattern as custom label)
1219 $custom_required_msg_label = __( 'Custom required message', 'contact-forms');
1220 // translators: %s is the field name/label
1221 $custom_required_msg_desc = __( 'Overrides the default "required" error message. Use %s for the field name.', 'contact-forms');
1222 $custom_required_msg = <<<EOT
1223 <p><label for="widget-{$istance_data['istance_id']}-custom-required-msg">{$custom_required_msg_label}:</label>
1224 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-required-msg" value="1" {$override_custom_required_msg} />{$override_end}<br>
1225 <input type="text" value="{$istance_data['custom_required_message']}" name="form-field-{$istance_data['istance_id']}-custom-required-msg" id="widget-{$istance_data['istance_id']}-custom-required-msg" class="widefat"><br>
1226 <small>{$custom_required_msg_desc}</small></p>
1227 EOT;
1228
1229 // Custom format message override for email and telephone fields
1230 $custom_format_msg = '';
1231 if ($field_data['type'] === 'email' || $field_data['type'] === 'autoreply_email') {
1232 $custom_format_msg_label = __( 'Custom invalid email message', 'contact-forms');
1233 // translators: %s is the field name/label
1234 $custom_format_msg_desc = __( 'Overrides the default email format error message. Use %s for the field name.', 'contact-forms');
1235 $custom_format_msg = <<<EOT
1236 <p><label for="widget-{$istance_data['istance_id']}-custom-format-msg">{$custom_format_msg_label}:</label>
1237 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-format-msg" value="1" {$override_custom_format_msg} />{$override_end}<br>
1238 <input type="text" value="{$istance_data['custom_format_message']}" name="form-field-{$istance_data['istance_id']}-custom-format-msg" id="widget-{$istance_data['istance_id']}-custom-format-msg" class="widefat"><br>
1239 <small>{$custom_format_msg_desc}</small></p>
1240 EOT;
1241 } elseif ($field_data['type'] === 'telephone') {
1242 $custom_format_msg_label = __( 'Custom invalid phone message', 'contact-forms');
1243 // translators: %s is the field name/label
1244 $custom_format_msg_desc = __( 'Overrides the default phone format error message. Use %s for the field name.', 'contact-forms');
1245 $custom_format_msg = <<<EOT
1246 <p><label for="widget-{$istance_data['istance_id']}-custom-format-msg">{$custom_format_msg_label}:</label>
1247 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-format-msg" value="1" {$override_custom_format_msg} />{$override_end}<br>
1248 <input type="text" value="{$istance_data['custom_format_message']}" name="form-field-{$istance_data['istance_id']}-custom-format-msg" id="widget-{$istance_data['istance_id']}-custom-format-msg" class="widefat"><br>
1249 <small>{$custom_format_msg_desc}</small></p>
1250 EOT;
1251 }
1252
1253 if ($field_data['type'] == 'date'){
1254 $default_date_value = <<<EOT
1255 <p><label for="widget-{$istance_data['istance_id']}-default-value">{$testi_eot['default_value']}:</label>
1256 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-default-value" value="1" {$override_default_value} />{$override_end}<br>
1257 <input type="date" value="{$istance_data['default_value']}" name="form-field-{$istance_data['istance_id']}-default-value" id="widget-{$istance_data['istance_id']}-default-value" class="widefat"></p>
1258 EOT;
1259 $min_date = <<<EOT
1260 <p><label for="widget-{$istance_data['istance_id']}-min-of-date">{$testi_eot['min-of-date']}:</label>
1261 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-datemin-values" value="1" {$override_mindate_values} />{$override_end}<br>
1262 <input type="date" value="{$istance_data['min_date']}" name="form-field-{$istance_data['istance_id']}-min-of-date" id="widget-{$istance_data['istance_id']}-min-of-date"></p>
1263 EOT;
1264 $max_date = <<<EOT
1265 <p><label for="widget-{$istance_data['istance_id']}-max-of-date">{$testi_eot['max-of-date']}:</label>
1266 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-datemax-values" value="1" {$override_maxdate_values} />{$override_end}<br>
1267 <input type="date" value="{$istance_data['max_date']}" name="form-field-{$istance_data['istance_id']}-max-of-date" id="widget-{$istance_data['istance_id']}-max-of-date"></p>
1268 EOT;
1269 }
1270
1271 // Post type selector for post-select and post-multicheckbox fields
1272 $post_type_selector = '';
1273 if ($field_data['type'] === 'post-select' || $field_data['type'] === 'post-multicheckbox') {
1274 $override_post_type = isset($istance_data['post_type']) && $istance_data['post_type'] !== 'page' ? 'checked="checked"' : '';
1275 $post_types = accua_forms_get_public_post_types();
1276 $post_type_options = '';
1277 $current_post_type = esc_attr($istance_data['post_type']);
1278 foreach ($post_types as $pt_slug => $pt_label) {
1279 $selected = ($pt_slug === $current_post_type) ? ' selected="selected"' : '';
1280 $post_type_options .= '<option value="' . esc_attr($pt_slug) . '"' . $selected . '>' . esc_html($pt_label) . '</option>';
1281 }
1282 $post_type_label = __('Post type', 'contact-forms');
1283 $post_type_desc = __('Select which post type to show in the dropdown.', 'contact-forms');
1284 $query_params_label = __('Additional query parameters', 'contact-forms');
1285 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- This is example help text, not actual code.
1286 $query_params_desc = __('Optional: Filter posts using query parameters (e.g., authors=admin or meta_key=featured&meta_value=1). Add post_status=publish,private to also include private posts (their titles become visible to all visitors of this form). Leave empty for all published posts of the selected type.', 'contact-forms');
1287 $post_type_selector = <<<EOT
1288 <p><label for="widget-{$istance_data['istance_id']}-post-type">{$post_type_label}:</label>
1289 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-post-type" value="1" {$override_post_type} />{$override_end}<br>
1290 <select name="form-field-{$istance_data['istance_id']}-post-type" id="widget-{$istance_data['istance_id']}-post-type" class="widefat">{$post_type_options}</select><br />
1291 {$post_type_desc}</p>
1292 <p><label for="widget-{$istance_data['istance_id']}-allowed-values">{$query_params_label}:</label>
1293 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-allowed-values" value="1" {$override_allowed_values} />{$override_end}<br>
1294 <textarea rows="3" cols="50" name="form-field-{$istance_data['istance_id']}-allowed-values" id="widget-{$istance_data['istance_id']}-allowed-values" class="widefat">{$istance_data['allowed_values']}</textarea><br />
1295 {$query_params_desc}</p>
1296 EOT;
1297 }
1298
1299 // Country selector for telephone fields (for libphonenumber validation)
1300 $country_selector = '';
1301 if ($field_data['type'] === 'telephone') {
1302 $countries = accua_forms_get_countries();
1303 $current_country = isset($istance_data['country_code']) ? esc_attr($istance_data['country_code']) : 'IT';
1304 $override_country = isset($istance_data['country_code']) && $istance_data['country_code'] !== 'IT' ? 'checked="checked"' : '';
1305 $country_options = '';
1306 foreach ($countries as $code => $country_name) {
1307 $selected = ($code === $current_country) ? ' selected="selected"' : '';
1308 $country_options .= '<option value="' . esc_attr($code) . '"' . $selected . '>' . esc_html($country_name) . '</option>';
1309 }
1310 $country_label = __('Default country', 'contact-forms');
1311 // translators: Help text for phone field country selector in form editor
1312 $country_desc = __('For numbers without international prefix, validation assumes this country.', 'contact-forms');
1313 $country_selector = <<<EOT
1314 <p><label for="widget-{$istance_data['istance_id']}-country-code">{$country_label}:</label>
1315 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-country-code" value="1" {$override_country} />{$override_end}<br>
1316 <select name="form-field-{$istance_data['istance_id']}-country-code" id="widget-{$istance_data['istance_id']}-country-code" class="widefat">{$country_options}</select><br />
1317 {$country_desc}</p>
1318 EOT;
1319 }
1320
1321 // CSS Class and CSS ID fields (universal, apply to all field types)
1322 $css_class_label = __( 'CSS Class', 'contact-forms');
1323 $css_id_label = __( 'CSS ID', 'contact-forms');
1324 // translators: Help text for CSS Class field in form editor
1325 $css_class_desc = __( 'Custom CSS class(es) for the field wrapper. Separate multiple classes with spaces.', 'contact-forms');
1326 // translators: Help text for CSS ID field in form editor
1327 $css_id_desc = __( 'Custom CSS ID for the field wrapper. Must be unique on the page.', 'contact-forms');
1328 $css_class_field = <<<EOT
1329 <p><label for="widget-{$istance_data['istance_id']}-css-class">{$css_class_label}:</label><br>
1330 <input type="text" value="{$istance_data['css_class']}" name="form-field-{$istance_data['istance_id']}-css-class" id="widget-{$istance_data['istance_id']}-css-class" class="widefat"><br>
1331 <small>{$css_class_desc}</small></p>
1332 EOT;
1333 $css_id_field = <<<EOT
1334 <p><label for="widget-{$istance_data['istance_id']}-css-id">{$css_id_label}:</label><br>
1335 <input type="text" value="{$istance_data['css_id']}" name="form-field-{$istance_data['istance_id']}-css-id" id="widget-{$istance_data['istance_id']}-css-id" class="widefat"><br>
1336 <small>{$css_id_desc}</small></p>
1337 EOT;
1338
1339 switch ($field_data['type']) {
1340 case 'textarea':
1341 $content .= <<<EOT
1342 <p><label for="widget-{$istance_data['istance_id']}-default-value">{$testi_eot['default_value']}:</label>
1343 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-default-value" value="1" {$override_default_value} />{$override_end}<br>
1344 <textarea rows="6" cols="50" name="form-field-{$istance_data['istance_id']}-default-value" id="widget-{$istance_data['istance_id']}-default-value" class="widefat">{$istance_data['default_value']}</textarea></p>
1345 $required
1346 $custom_required_msg
1347 EOT;
1348 break;
1349 case 'hidden':
1350 $content = $default_value;
1351 break;
1352 case 'checkbox':
1353 $content .= $default_value . $required . $custom_required_msg;
1354 break;
1355 case 'select':
1356 case 'radio':
1357 $content .= $default_value . $allowed_values . $required . $custom_required_msg;
1358 break;
1359 case 'post-select':
1360 $content .= $default_value . $post_type_selector . $required . $custom_required_msg;
1361 break;
1362 case 'multiselect':
1363 case 'multicheckbox':
1364 $content .= $default_values . $allowed_values . $required . $custom_required_msg;
1365 break;
1366 case 'post-multicheckbox':
1367 $content .= $default_values . $post_type_selector . $required . $custom_required_msg;
1368 break;
1369 case 'file':
1370 $content .= $allowed_ext . $required . $custom_required_msg;
1371 break;
1372 case 'submit':
1373 //just the label
1374 break;
1375 case 'fieldset-begin':
1376 $fs_label_text = __('Border and Title', 'contact-forms');
1377 $gt_label_text = __('Group Title', 'contact-forms');
1378 // translators: Help text under the Group Title field for fieldset groups in the form editor
1379 $gt_desc_text = __('Section heading. Shown in the form when a title option is selected.', 'contact-forms');
1380 $fs_opts_map = array(
1381 'border-off-title-off' => __('Border OFF | Title OFF', 'contact-forms'),
1382 'border-on-title-off' => __('Border ON | Title OFF', 'contact-forms'),
1383 'border-on-title-inline' => __('Border ON | Title ON (inline)', 'contact-forms'),
1384 'border-on-title-outside' => __('Border ON | Title ON (outside)', 'contact-forms'),
1385 'border-on-title-inside' => __('Border ON | Title ON (inside)', 'contact-forms'),
1386 'border-off-title-on' => __('Border OFF | Title ON', 'contact-forms'),
1387 );
1388 $fs_options_html = '';
1389 foreach ($fs_opts_map as $opt_val => $opt_label) {
1390 $opt_selected = ($istance_data['fieldset_style'] === $opt_val) ? ' selected="selected"' : '';
1391 $fs_options_html .= '<option value="' . esc_attr($opt_val) . '"' . $opt_selected . '>' . esc_html($opt_label) . '</option>';
1392 }
1393 $content = <<<EOT
1394 <p><label for="widget-{$istance_data['istance_id']}-label">{$gt_label_text}:</label><br>
1395 <input type="hidden" name="form-field-{$istance_data['istance_id']}-override-label" value="1">
1396 <input type="text" value="{$istance_data['label']}" name="form-field-{$istance_data['istance_id']}-label" id="widget-{$istance_data['istance_id']}-label" class="widefat"><br>
1397 <small>{$gt_desc_text}</small></p>
1398 <p><label for="widget-{$istance_data['istance_id']}-fieldset-style">{$fs_label_text}:</label><br>
1399 <select name="form-field-{$istance_data['istance_id']}-fieldset-style" id="widget-{$istance_data['istance_id']}-fieldset-style" class="widefat accua-fieldset-style-select">{$fs_options_html}</select></p>
1400 EOT;
1401 $content .= $css_class_field . $css_id_field;
1402 break;
1403 case 'fieldset-end':
1404 //Nothing!
1405 $content = '';
1406 break;
1407 case 'html':
1408 $content = <<<EOT
1409 <p><label for="widget-{$istance_data['istance_id']}-default-value">{$testi_eot['custom_HTML_content']}</label>
1410 {$override_begin}<input type="{$override_type}" name="form-field-{$istance_data['istance_id']}-override-default-value" value="1" {$override_default_value} />{$override_end}<br>
1411 <textarea rows="6" cols="50" name="form-field-{$istance_data['istance_id']}-default-value" id="widget-{$istance_data['istance_id']}-default-value" class="widefat">{$istance_data['default_value']}</textarea></p>
1412 <p><a href="#" class="accua-refresh-preview">{$testi_eot['refresh_preview']}</a></p>
1413 EOT;
1414 break;
1415 case 'date':
1416 $content .= $default_date_value . $min_date . $max_date . $required . $custom_required_msg;
1417 break;
1418 case 'telephone':
1419 $content .= $default_value . $country_selector . $required . $custom_required_msg . $custom_format_msg;
1420 break;
1421 case 'email':
1422 case 'autoreply_email':
1423 $content .= $default_value . $required . $custom_required_msg . $custom_format_msg;
1424 break;
1425 case 'textfield':
1426 case 'colorpicker':
1427 case 'datepicker':
1428 case 'dateselect':
1429 default:
1430 /**
1431 * Action to render additional field settings in the form editor.
1432 *
1433 * @param string $field_type The field type identifier.
1434 * @param array $field_data The field definition.
1435 * @param array $istance_data The field instance data.
1436 * @param string $content The current settings HTML (passed by reference via output buffering).
1437 */
1438 ob_start();
1439 do_action( 'accua_forms_field_settings', $field_data['type'], $field_data, $istance_data );
1440 $extra_settings = ob_get_clean();
1441 $content .= $default_value . $extra_settings . $required . $custom_required_msg;
1442 break;
1443 }
1444
1445 // Append CSS Class and CSS ID fields to all types except fieldset-end (which has no settings)
1446 if ($field_data['type'] !== 'fieldset-end' && $field_data['type'] !== 'fieldset-begin') {
1447 $content .= $css_class_field . $css_id_field;
1448 }
1449
1450 $adminurl = admin_url();
1451
1452 return <<<EOT
1453 <div class="widget ui-draggable" id="widget-{$i}_{$istance_data['istance_id']}" data-field-type="{$field_data['type']}" $hidden> <div class="widget-top">
1454 <div class="widget-title-action">
1455 <a href="#add-field" class="widget-add-action hide-if-no-js" title="{$testi_eot['add']}" aria-label="{$testi_eot['add']}"></a>
1456 <a href="#available-widgets" class="widget-action hide-if-no-js"></a>
1457 </div>
1458 <div class="widget-title"><h4>{$field_data['name']}<span class="in-widget-title"></span></h4></div>
1459 </div>
1460
1461 <div class="widget-inside">
1462 <form method="post" action="">
1463 <div class="widget-content">
1464 $content
1465 </div>
1466 <input type="hidden" value="{$fid}" name="form-id">
1467 <input type="hidden" value="{$istance_data['istance_id']}" class="widget-id" name="widget-id">
1468 <input type="hidden" value="{$field_data['id']}" class="id_base" name="id_base">
1469 <input type="hidden" value="250" class="widget-width" name="widget-width">
1470 <input type="hidden" value="200" class="widget-height" name="widget-height">
1471 <input type="hidden" value="{$istance_data['widget_number']}" class="widget_number" name="widget_number">
1472 <input type="hidden" value="{$multi_number}" class="multi_number" name="multi_number">
1473 <input type="hidden" value="{$add_new}" class="add_new" name="add_new">
1474
1475 <div class="widget-control-actions">
1476 <div class="alignleft">
1477 <a href="#remove" class="widget-control-remove delete">{$testi_eot['remove']}</a> |
1478 <a href="#close" class="widget-control-close">{$testi_eot['close']}</a>
1479 </div>
1480 <div class="alignright">
1481 <input type="submit" value="{$testi_eot['save']}" class="button button-primary widget-control-save accua-field-save-btn" id="widget-{$istance_data['istance_id']}-savewidget" name="savewidget">
1482 </div>
1483 <br class="clear">
1484 </div>
1485 </form>
1486 </div>
1487
1488 <!--<div class="widget-description">
1489 {$field_data['description']}
1490 </div>-->
1491 </div>
1492
1493 EOT;
1494 // phpcs:enable PluginCheck.CodeAnalysis.Heredoc.NotAllowed, WordPress.Security.EscapeOutput.HeredocOutputNotEscaped
1495 }
1496
1497
1498 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function, underscore prefix indicates private
1499 function _accua_forms_get_abs_dest_path($dest_path = '') {
1500 if ($dest_path === '') {
1501 return realpath(ABSPATH) . '/wp-content/uploads/accua-forms';
1502 } elseif (substr($dest_path,0,1) === '/') {
1503 return $dest_path;
1504 } else {
1505 return realpath(ABSPATH) . '/' . $dest_path;
1506 }
1507 }
1508
1509 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function, underscore prefix indicates private
1510 function _accua_forms_get_form_data($fid = false, $return_empty = true, $restore_trash = false){
1511 $empty_form_data = array(
1512 'fields' => array(),
1513 'title' => '',
1514 'success_message' => '',
1515 'error_message' => '',
1516 'emails_from_name' => '',
1517 'emails_from' => '',
1518 'admin_emails_to' => '',
1519 'emails_bcc' => '',
1520 'admin_emails_subject' => '',
1521 'admin_emails_message' => '',
1522 'confirmation_emails_subject' => '',
1523 'confirmation_emails_message' => '',
1524 'use_ajax' => true,
1525 'gads_conversion_tracking_code' => '',
1526 'layout' => 'sidebyside',
1527 'style_margin' => '',
1528 'style_border_color' => '',
1529 'style_border_width' => '',
1530 'style_border_radius' => '',
1531 'style_background_color' => '',
1532 'style_padding' => '',
1533 'style_color' => '',
1534 'style_font_size' => '',
1535 'style_field_spacing' => '',
1536 'style_field_border_color' => '',
1537 'style_field_border_width' => '',
1538 'style_field_border_radius' => '',
1539 'style_field_background_color' => '',
1540 'style_field_padding' => '',
1541 'style_field_color' => '',
1542 'style_submit_border_color' => '',
1543 'style_submit_border_width' => '',
1544 'style_submit_border_radius' => '',
1545 'style_submit_background_color' => '',
1546 'style_submit_padding' => '',
1547 'style_submit_color' => '',
1548 'style_submit_font_size' => '',
1549 'submission_retention_override' => false,
1550 'submission_retention_value' => 0,
1551 'submission_retention_unit' => 'months',
1552 'submission_retention_mode' => 'anonymize',
1553 );
1554
1555 if ($fid === false) {
1556 return $empty_form_data;
1557 }
1558
1559 $default_form_data = get_option('accua_forms_default_form_data',array());
1560
1561 if ($fid === null) {
1562 return $default_form_data + $empty_form_data;
1563 }
1564
1565 $forms_data = get_option('accua_forms_saved_forms', array());
1566 if ($restore_trash) {
1567 $trash_data = get_option('accua_forms_trash_forms', array());
1568 if (isset($trash_data[$fid])) {
1569 $forms_data[$fid] = $trash_data[$fid];
1570 update_option('accua_forms_saved_forms', $forms_data);
1571 unset($trash_data[$fid]);
1572 update_option('accua_forms_trash_forms', $trash_data);
1573 }
1574 }
1575
1576 if (isset($forms_data[$fid])) {
1577 $form_data = array(
1578 '_overrided' => $forms_data[$fid]
1579 ) + $forms_data[$fid] + $default_form_data + $empty_form_data;
1580 return $form_data;
1581 } elseif ($return_empty) {
1582 return array(
1583 '_overrided' => array()
1584 ) + $default_form_data + $empty_form_data;
1585 } else {
1586 return null;
1587 }
1588
1589 }
1590
1591 /**
1592 * Draft System Functions
1593 *
1594 * The draft system allows users to make changes to forms in the admin editor
1595 * without immediately affecting the live/published form. Changes are stored in
1596 * a transient until the user clicks Save, which publishes the draft.
1597 *
1598 * Pattern follows WordPress auto-draft system.
1599 */
1600
1601 /**
1602 * Get the transient key for a form's draft data.
1603 *
1604 * @param string|int $fid Form ID.
1605 * @return string Transient key.
1606 */
1607 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function
1608 function _accua_forms_get_draft_key( $fid ) {
1609 return 'accua_forms_draft_' . $fid;
1610 }
1611
1612 /**
1613 * Initialize or get existing draft for a form.
1614 * Called when the form editor is loaded.
1615 *
1616 * If a draft exists, returns it.
1617 * If no draft exists, creates one from published data.
1618 *
1619 * @param string|int $fid Form ID.
1620 * @return array Draft data array.
1621 */
1622 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function
1623 function _accua_forms_init_draft( $fid ) {
1624 $draft_key = _accua_forms_get_draft_key( $fid );
1625
1626 // Check for existing draft
1627 $draft_data = get_transient( $draft_key );
1628
1629 if ( $draft_data !== false ) {
1630 // Draft exists - return it
1631 return $draft_data;
1632 }
1633
1634 // No draft - create from published data
1635 $forms_data = get_option( 'accua_forms_saved_forms', array() );
1636
1637 if ( isset( $forms_data[ $fid ] ) ) {
1638 $draft_data = $forms_data[ $fid ];
1639 } else {
1640 // New form - initialize empty structure
1641 $draft_data = array( 'fields' => array() );
1642 }
1643
1644 // Store as draft with 24 hour expiry
1645 set_transient( $draft_key, $draft_data, DAY_IN_SECONDS );
1646
1647 return $draft_data;
1648 }
1649
1650 /**
1651 * Get draft data for a form (creating if necessary).
1652 * Used by AJAX handlers to read current draft state.
1653 *
1654 * @param string|int $fid Form ID.
1655 * @return array Draft data array.
1656 */
1657 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function
1658 function _accua_forms_get_draft_data( $fid ) {
1659 $draft_key = _accua_forms_get_draft_key( $fid );
1660 $draft_data = get_transient( $draft_key );
1661
1662 if ( $draft_data === false ) {
1663 // Initialize draft from published data
1664 $draft_data = _accua_forms_init_draft( $fid );
1665 }
1666
1667 return $draft_data;
1668 }
1669
1670 /**
1671 * Save data to draft transient.
1672 * Called by AJAX handlers when fields are edited.
1673 *
1674 * @param string|int $fid Form ID.
1675 * @param array $draft_data Complete draft data to save.
1676 * @return bool True on success.
1677 */
1678 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function
1679 function _accua_forms_save_draft( $fid, $draft_data ) {
1680 $draft_key = _accua_forms_get_draft_key( $fid );
1681 return set_transient( $draft_key, $draft_data, DAY_IN_SECONDS );
1682 }
1683
1684 /**
1685 * Publish draft to live data.
1686 * Called when user clicks Save button.
1687 *
1688 * @param string|int $fid Form ID.
1689 * @return bool True on success.
1690 */
1691 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function
1692 function _accua_forms_publish_draft( $fid ) {
1693 $draft_key = _accua_forms_get_draft_key( $fid );
1694 $draft_data = get_transient( $draft_key );
1695
1696 if ( $draft_data === false ) {
1697 // No draft to publish - shouldn't happen normally
1698 return false;
1699 }
1700
1701 // Get current published data
1702 $forms_data = get_option( 'accua_forms_saved_forms', array() );
1703
1704 // Update with draft
1705 $forms_data[ $fid ] = $draft_data;
1706
1707 // Save to database
1708 $result = update_option( 'accua_forms_saved_forms', $forms_data );
1709
1710 if ( $result ) {
1711 // Clear draft after successful publish
1712 delete_transient( $draft_key );
1713 }
1714
1715 return $result;
1716 }
1717
1718 /**
1719 * Delete draft for a form.
1720 * Called when discarding changes or after successful publish.
1721 *
1722 * @param string|int $fid Form ID.
1723 * @return bool True on success.
1724 */
1725 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function
1726 function _accua_forms_delete_draft( $fid ) {
1727 $draft_key = _accua_forms_get_draft_key( $fid );
1728 return delete_transient( $draft_key );
1729 }
1730
1731 /**
1732 * Check if a draft exists and differs from published data.
1733 * Used to show "unsaved changes" warning.
1734 *
1735 * @param string|int $fid Form ID.
1736 * @return bool True if draft exists and differs from published.
1737 */
1738 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function
1739 function _accua_forms_has_unsaved_draft( $fid ) {
1740 $draft_key = _accua_forms_get_draft_key( $fid );
1741 $draft_data = get_transient( $draft_key );
1742
1743 if ( $draft_data === false ) {
1744 return false;
1745 }
1746
1747 // Compare with published data
1748 $forms_data = get_option( 'accua_forms_saved_forms', array() );
1749 $published_data = isset( $forms_data[ $fid ] ) ? $forms_data[ $fid ] : array();
1750
1751 // Deep comparison (serialize for simplicity)
1752 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Used for comparison only
1753 return serialize( $draft_data ) !== serialize( $published_data );
1754 }
1755
1756 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function, underscore prefix indicates private
1757 function _accua_forms_style_parameters($params) {
1758 $ret = '';
1759 foreach ($params as $key => $value) {
1760 $value = trim($value);
1761 if ($value !== '') {
1762 if (is_numeric($value)) {
1763 $ret .= "{$key}:{$value}px;";
1764 } else {
1765 $ret .= "{$key}:{$value};";
1766 }
1767 if ($key == 'border-width') {
1768 $ret .= "border-style:solid;";
1769 }
1770 }
1771 }
1772 return $ret;
1773 }
1774
1775 /*
1776 * generazione del form (e della preview)
1777 * */
1778 add_action('accua_form_alter', 'accua_forms_form_generate', -999, 2);
1779 function accua_forms_form_generate($baseid, $form) {
1780 if (substr($baseid, 0, 14) == '__accua-form__') {
1781 $fid = substr($baseid,14);
1782
1783 // Check if we're in admin preview mode - if so, read from draft
1784 $use_draft = apply_filters('accua_forms_use_draft_for_preview', false);
1785 if ($use_draft) {
1786 // Get draft data and merge with defaults
1787 $draft_data = _accua_forms_get_draft_data($fid);
1788 $default_form_data = get_option('accua_forms_default_form_data', array());
1789 $empty_form_data = _accua_forms_get_form_data(false); // Get empty structure
1790 $form_data = array(
1791 '_overrided' => $draft_data
1792 ) + $draft_data + $default_form_data + $empty_form_data;
1793 } else {
1794 // Frontend: read from published data
1795 $form_data = _accua_forms_get_form_data($fid, false);
1796 }
1797 /*
1798 echo '<!-- fid = ';
1799 print_r($fid);
1800 echo "\n\nform_data = ";
1801 print_r($form_data);
1802 echo "\n-->";
1803 */
1804 if ($form_data) {
1805 // Check for preview order override (allows live preview of field reorder before save)
1806 $preview_order_override = apply_filters('accua_forms_preview_order_override', null);
1807 if ($preview_order_override && !empty($form_data['fields'])) {
1808 // Find the sidebar key for this form (format: cimatti-accua-fields-form-area-{fid})
1809 $sidebar_key = 'cimatti-accua-fields-form-area-' . $fid;
1810 if (isset($preview_order_override[$sidebar_key])) {
1811 $order_string = $preview_order_override[$sidebar_key];
1812 $order_array = explode(',', $order_string);
1813
1814 // Reorder fields according to preview order
1815 $old_fields = $form_data['fields'];
1816 $new_fields = array();;
1817
1818 foreach ($order_array as $widget_id) {
1819 // Extract instance ID from widget ID (format: widget-{type}_{instance_id})
1820 $instance_id = preg_replace('/^(new-)?widget-\\d+_/', '', $widget_id);
1821 if (isset($old_fields[$instance_id])) {
1822 $new_fields[$instance_id] = $old_fields[$instance_id];
1823 unset($old_fields[$instance_id]);
1824 }
1825 }
1826
1827 // Append any remaining fields not in order
1828 if ($old_fields) {
1829 $new_fields += $old_fields;
1830 }
1831
1832 $form_data['fields'] = $new_fields;
1833 }
1834 }
1835
1836 $form_style = _accua_forms_style_parameters(array(
1837 'margin' => $form_data['style_margin'],
1838 'border-color' => $form_data['style_border_color'],
1839 'border-width' => $form_data['style_border_width'],
1840 'border-radius' => $form_data['style_border_radius'],
1841 'background-color' => $form_data['style_background_color'],
1842 'padding' => $form_data['style_padding'],
1843 'color' => $form_data['style_color'],
1844 'font-size' => $form_data['style_font_size'],
1845 ));
1846
1847 $field_style = _accua_forms_style_parameters(array(
1848 'margin-bottom' => $form_data['style_field_spacing'],
1849 'border-color' => $form_data['style_field_border_color'],
1850 'border-width' => $form_data['style_field_border_width'],
1851 'border-radius' => $form_data['style_field_border_radius'],
1852 'background-color' => $form_data['style_field_background_color'],
1853 'padding' => $form_data['style_field_padding'],
1854 'color' => (trim($form_data['style_field_color']) === '')?$form_data['style_color']:$form_data['style_field_color'],
1855 'font-size' => $form_data['style_font_size'],
1856 ));
1857 $field_properties = array();
1858 if ($field_style !== '') {
1859 $field_properties['style'] = $field_style;
1860 }
1861 // These will be set per-field in the loop below, initialized empty here
1862 $field_properties['wrapperCssClass'] = '';
1863 $field_properties['wrapperCssId'] = '';
1864
1865 $submit_style = _accua_forms_style_parameters(array(
1866 'border-color' => $form_data['style_submit_border_color'],
1867 'border-width' => $form_data['style_submit_border_width'],
1868 'border-radius' => $form_data['style_submit_border_radius'],
1869 'background-color' => $form_data['style_submit_background_color'],
1870 'padding' => $form_data['style_submit_padding'],
1871 'color' => $form_data['style_submit_color'],
1872 'font-size' => $form_data['style_submit_font_size'],
1873 ));
1874 $submit_properties = array();
1875 if ($submit_style !== '') {
1876 $submit_properties['style'] = $submit_style;
1877 }
1878
1879 if ($form_style !== '') {
1880 $form->configure(array('style' => $form_style));
1881 }
1882
1883 if (!empty($form_data['use_ajax'])){
1884 $form->configure(array(
1885 "accua_ajax" => 1,
1886 ));
1887 }
1888
1889 $add_submit = true;
1890 $fieldset_open = false;
1891 $avail_fields = get_option('accua_forms_avail_fields', array());
1892
1893 foreach ($form_data['fields'] as $istance_data) {
1894 if (empty($avail_fields[$istance_data['ref']])) {
1895 $field_data = array();
1896 if (!empty($istance_data['ref'])) {
1897 if ($istance_data['ref'] == '__fieldset-begin') {
1898 $field_data = array(
1899 'id' => '__fieldset-begin',
1900 'name' => __('Fieldset begin', 'contact-forms'),
1901 'type' => 'fieldset-begin',
1902 'description' => '',
1903 );
1904 } elseif ($istance_data['ref'] == '__fieldset-end') {
1905 $field_data = array(
1906 'id' => '__fieldset-end',
1907 'name' => __('Fieldset end', 'contact-forms'),
1908 'type' => 'fieldset-end',
1909 'description' => '',
1910 );
1911 }
1912 }
1913 } else {
1914 $field_data = $avail_fields[$istance_data['ref']];
1915 }
1916
1917 $field_data += array(
1918 'version' => 1,
1919 'id' => '__html',
1920 'name' => __( 'Custom HTML content', 'contact-forms'),
1921 'type' => 'html',
1922 'description' => __('Use this special field to inject raw HTML in the form. You can use this multiple times.', 'contact-forms'),
1923 'default_value' => '',
1924 'allowed_values' => '',
1925 'allowed_extensions' => '',
1926 'custom_required_message' => '',
1927 'custom_format_message' => '',
1928 );
1929
1930 $istance_data += array(
1931 'version' => 1,
1932 );
1933
1934 if ($field_data['type'] == 'file') {
1935 if ($field_data['version'] < 2) {
1936 $field_data['allowed_extensions'] = $field_data['allowed_values'];
1937 }
1938 if ($istance_data['version'] < 2 && isset($istance_data['allowed_values'])) {
1939 $istance_data['allowed_extensions'] = $istance_data['allowed_values'];
1940 }
1941 }
1942
1943 /* prendo i dati relativi ai campi di tipo date
1944 lo faccio prima, così default value in questo modo viene sovrascritto se un campo è di un tipo diverso
1945
1946 NB: se i campi sono già sovrascritti, si trovano già in istance_data */
1947 if ($field_data['type'] == 'date'){
1948 $istance_data += array(
1949 'default_value' => $field_data['default_date_value'],
1950 'min_date' => $field_data['min_date'],
1951 'max_date' => $field_data['max_date'],
1952 );
1953 }
1954
1955 $istance_data += array(
1956 'istance_id' => $field_data['id'],
1957 'widget_number' => '',
1958 'ref' => $field_data['id'],
1959 'label' => $field_data['name'],
1960 'default_value' => $field_data['default_value'], /* viene impostato il valore di defualt se non è un campo data */
1961 'allowed_values' => $field_data['allowed_values'],
1962 'allowed_extensions' => $field_data['allowed_extensions'],
1963 'post_type' => 'page', // Default for post-select fields
1964 'css_class' => '',
1965 'css_id' => '',
1966 'custom_required_message' => '',
1967 'custom_format_message' => '',
1968 );
1969
1970 $element = NULL;
1971 $element_conf = NULL;
1972
1973 $allowed_val = trim($istance_data['allowed_values']);
1974
1975 // For post-select fields, we use lazy loading via AJAX, so don't pre-load posts here
1976 if ($field_data['type'] == 'post-multicheckbox') {
1977 // Post-multicheckbox still needs pre-loaded options for checkbox rendering
1978 $post_type = isset($istance_data['post_type']) ? $istance_data['post_type'] : 'page';
1979 $query_args = array();
1980 if (!empty($allowed_val)) {
1981 wp_parse_str($allowed_val, $query_args);
1982 }
1983 // Let a post_type in the query parameters override the field's
1984 // post-type setting, matching post-select. The value comes from the
1985 // saved field configuration (not a client request) and is validated
1986 // against public post types.
1987 if (!empty($query_args['post_type'])) {
1988 $mc_public_types = get_post_types(array('public' => true));
1989 $mc_override = sanitize_text_field($query_args['post_type']);
1990 if (isset($mc_public_types[$mc_override])) {
1991 $post_type = $mc_override;
1992 }
1993 }
1994 $query_args['post_type'] = $post_type;
1995 // Only publish/private may be exposed, even if the admin configured other statuses.
1996 if (!empty($query_args['post_status'])) {
1997 $mc_statuses = accua_forms_filter_field_post_status($query_args['post_status']);
1998 if (!empty($mc_statuses)) {
1999 $query_args['post_status'] = $mc_statuses;
2000 } else {
2001 unset($query_args['post_status']);
2002 }
2003 }
2004 $posts = accua_get_pages($query_args);
2005 $allowed_values = array();
2006 foreach ($posts as $p) {
2007 $allowed_values[$p->ID] = $p->post_title;
2008 }
2009 } elseif ($field_data['type'] == 'post-select') {
2010 // Post-select uses lazy loading - just set empty options, JS will fetch
2011 $allowed_values = array();
2012 } else {
2013 if ($field_data['type'] == 'file') {
2014 $filedata = get_option('accua_forms_default_file_field_data',array());
2015 $filedata += array(
2016 'valid_extensions' => '',
2017 'max_filesize' => '',
2018 'dest_path' => '',
2019 );
2020 $allowed_val = accua_forms_filter_extensions($istance_data['allowed_extensions']);
2021 if ($allowed_val == '') {
2022 $allowed_val = accua_forms_filter_extensions($filedata['valid_extensions']);
2023 }
2024 }
2025
2026 $allowed_val = explode("\n",$allowed_val);
2027 $allowed_values = array();
2028 foreach ($allowed_val as $val) {
2029 $val = explode('|',$val,2);
2030 $val[0] = trim($val[0]);
2031 /*
2032 if (empty($val[0])) {
2033 continue;
2034 }
2035 */
2036 if ((!isset($val[1])) || (trim($val[1])==='')) {
2037 if ($val[0] === '') {
2038 continue;
2039 } else {
2040 $val[1] = $val[0];
2041 }
2042 }
2043 $allowed_values[$val[0]] = $val[1];
2044 }
2045 }
2046
2047 // Set per-field wrapper CSS class and ID
2048 $field_properties['wrapperCssClass'] = isset($istance_data['css_class']) ? $istance_data['css_class'] : '';
2049 $field_properties['wrapperCssId'] = isset($istance_data['css_id']) ? $istance_data['css_id'] : '';
2050
2051 // Resolve per-field custom validation messages (per-form instance → field definition → default)
2052 $resolved_required_msg = '';
2053 if (!empty($istance_data['custom_required_message'])) {
2054 $resolved_required_msg = $istance_data['custom_required_message'];
2055 } elseif (!empty($field_data['custom_required_message'])) {
2056 $resolved_required_msg = $field_data['custom_required_message'];
2057 }
2058
2059 $resolved_format_msg = '';
2060 if (!empty($istance_data['custom_format_message'])) {
2061 $resolved_format_msg = $istance_data['custom_format_message'];
2062 } elseif (!empty($field_data['custom_format_message'])) {
2063 $resolved_format_msg = $field_data['custom_format_message'];
2064 }
2065
2066 // Add data attributes for client-side custom messages (reset each iteration)
2067 unset($field_properties['data-custom-required-msg']);
2068 unset($field_properties['data-custom-format-msg']);
2069 if ($resolved_required_msg !== '') {
2070 $field_properties['data-custom-required-msg'] = $resolved_required_msg;
2071 }
2072 if ($resolved_format_msg !== '') {
2073 $field_properties['data-custom-format-msg'] = $resolved_format_msg;
2074 }
2075
2076 switch ($field_data['type']) {
2077 case 'textarea':
2078 $element = new Element_Textarea($istance_data['label'], $istance_data['istance_id'], $field_properties+array('cols' => '50', 'value'=>$istance_data['default_value']));
2079 break;
2080 case 'hidden':
2081 $hidden_props = array();
2082 if ( !empty($field_properties['wrapperCssId']) ) {
2083 $hidden_props['id'] = $field_properties['wrapperCssId'];
2084 }
2085 $element = new Element_Hidden($istance_data['istance_id'], $istance_data['default_value'], !empty($hidden_props) ? $hidden_props : null);
2086 break;
2087 case 'checkbox':
2088 $lab = $istance_data['label'];
2089 if (!empty($istance_data['required'])) {
2090 $lab .= ' <strong>*</strong>';
2091 }
2092 if ($allowed_values) {
2093 reset($allowed_values);
2094 $val = (string) key($allowed_values);
2095 $defval = trim($istance_data['default_value']);
2096 } elseif ($istance_data['default_value'] == '1') {
2097 $defval = $val = '1';
2098 } else {
2099 $val = empty($istance_data['default_value'])?'1':$istance_data['default_value'];
2100 $defval = '';
2101 }
2102 $element = new AccuaForm_Element_Checkbox('', $istance_data['istance_id'], array($val => $lab), $field_properties+array('value' => $defval));
2103 break;
2104 case 'select':
2105 if (!isset($allowed_values[''])) {
2106 $allowed_values = array('' => '') + $allowed_values;
2107 }
2108 $defval = trim($istance_data['default_value']);
2109 $element = new AccuaForm_Element_Select($istance_data['label'], $istance_data['istance_id'], $allowed_values, $field_properties+array('value'=>$defval));
2110 break;
2111 case 'post-select':
2112 $post_type = isset($istance_data['post_type']) ? $istance_data['post_type'] : 'page';
2113 $extra_args = trim($istance_data['allowed_values']);
2114 $defval = trim($istance_data['default_value']);
2115 $element = new AccuaForm_Element_PostSelect($istance_data['label'], $istance_data['istance_id'], $post_type, $extra_args, $field_properties+array('value'=>$defval));
2116 break;
2117 case 'radio':
2118 $defval = trim($istance_data['default_value']);
2119 $element = new AccuaForm_Element_Radio($istance_data['label'], $istance_data['istance_id'], $allowed_values, $field_properties+array('value'=>$defval));
2120 break;
2121 case 'multiselect':
2122 $defval = explode('|', $istance_data['default_value']);
2123 foreach ($defval as $k => $v) {
2124 $defval[$k] = trim($v);
2125 }
2126 $element = new AccuaForm_Element_Select($istance_data['label'], $istance_data['istance_id'], $allowed_values, $field_properties+array('multiple' => true, 'value'=>$defval));
2127 break;
2128 case 'multicheckbox':
2129 case 'post-multicheckbox':
2130 $defval = explode('|', $istance_data['default_value']);
2131 foreach ($defval as $k => $v) {
2132 $defval[$k] = trim($v);
2133 }
2134 $element = new AccuaForm_Element_Checkbox($istance_data['label'], $istance_data['istance_id'], $allowed_values, $field_properties+array('value'=>$defval));
2135 break;
2136 case 'file':
2137 $fdata = array();
2138
2139 if ($allowed_values) {
2140 $fdata['validExtensions'] = array_keys($allowed_values);
2141 }
2142
2143 if (!empty($filedata['max_size'])){
2144 $fdata['maxSize'] = $filedata['max_size'];
2145 }
2146
2147 $fdata['destPath'] = _accua_forms_get_abs_dest_path($filedata['dest_path']);
2148
2149 $element = new AccuaForm_Element_File($istance_data['label'], $istance_data['istance_id'], $field_properties+$fdata);
2150 break;
2151 case 'html':
2152 $element = new Element_HTML($istance_data['default_value'], $field_properties);
2153 break;
2154 case 'email':
2155 case 'autoreply_email':
2156 $email_props = $field_properties+array('value'=>$istance_data['default_value']);
2157 if ($resolved_format_msg !== '') {
2158 $email_props['custom_format_message'] = $resolved_format_msg;
2159 }
2160 $element = new AccuaForm_Element_Email($istance_data['label'], $istance_data['istance_id'], $email_props);
2161 break;
2162 case 'colorpicker':
2163 $element = new AccuaForm_Element_ColorPicker($istance_data['label'], $istance_data['istance_id'], $field_properties+array('value'=>$istance_data['default_value']));
2164 break;
2165 case 'fieldset-begin':
2166 if ($fieldset_open) {
2167 $form->addElement(new AccuaForm_Element_FieldsetEnd());
2168 } else {
2169 $fieldset_open = true;
2170 }
2171 $fs_props = $field_properties;
2172 $fs_props['fieldset_style'] = isset($istance_data['fieldset_style']) ? $istance_data['fieldset_style'] : 'border-off-title-off';
2173 $element = new AccuaForm_Element_FieldsetBegin($istance_data['label'], $istance_data['istance_id'], $fs_props);
2174 break;
2175 case 'fieldset-end':
2176 if ($fieldset_open) {
2177 $element = new AccuaForm_Element_FieldsetEnd();
2178 $fieldset_open = false;
2179 }
2180 break;
2181 case 'submit':
2182 $add_submit = false;
2183 $element = new Element_Button($istance_data['label'], 'submit', $submit_properties+array('name' => $istance_data['istance_id'], 'value' => $istance_data['default_value']));
2184 break;
2185 case 'captcha':
2186 $empty_captcha_data = array(
2187 'recaptcha_force_v1' => '',
2188 'recaptcha_public_key' => '',
2189 'recaptcha_private_key' => '',
2190 );
2191 $captcha_data = get_option('accua_forms_default_captcha_field_data',array()) + $empty_captcha_data;
2192 $captcha_properties = array("description" => "");
2193 $captcha_use_v1 = true;
2194 if (($captcha_data['recaptcha_public_key'] !== '') && ($captcha_data['recaptcha_private_key'] !== '')) {
2195 $captcha_properties['privateKey'] = $captcha_data['recaptcha_private_key'];
2196 $captcha_properties['publicKey'] = $captcha_data['recaptcha_public_key'];
2197 $captcha_use_v1 = $captcha_data['recaptcha_force_v1'];
2198 }
2199 if ($captcha_use_v1) {
2200 $element = new Element_HTML("\n\n<!-- ReCaptcha 1 is discontinued, please go to Contact Forms settings page and set reCaptcha v2 keys -->\n\n");
2201 } else {
2202 $element = new AccuaForm_Element_Captcha2 ($istance_data['label'], '', $field_properties+$captcha_properties);
2203 }
2204 break;
2205 case 'turnstile':
2206 $element = new AccuaForm_Element_Turnstile($istance_data['label'], $istance_data['istance_id'], $field_properties+array("description" => ""));
2207 break;
2208 case 'password':
2209 $element = new Element_Password($istance_data['label'], $istance_data['istance_id'], $field_properties+array('value'=>$istance_data['default_value']));
2210 break;
2211 case 'password-and-confirm':
2212 $id_2 = "___{$istance_data['istance_id']}___confirmpass";
2213 $element = new Element_Password($istance_data['label'], $istance_data['istance_id'], $field_properties+array('value'=>$istance_data['default_value']));
2214 $element_conf = new Element_Password(__("Confirm password", 'contact-forms'), $id_2, $field_properties+array('value'=>$istance_data['default_value']));
2215 $element_conf_validator = new AccuaForm_Validation_Password();
2216 $element_conf_validator->configure(array('otherPasswordFieldName'=>$istance_data['istance_id']));
2217 $element_conf->setValidation($element_conf_validator);
2218 break;
2219 case 'date':
2220 $element = new AccuaForm_Element_Date($istance_data['label'], $istance_data['istance_id'], $field_properties+array('value'=>$istance_data['default_value'], 'minDate'=>$istance_data['min_date'], 'maxDate'=>$istance_data['max_date']));
2221 break;
2222 case 'telephone':
2223 $phone_country = isset($istance_data['country_code']) ? $istance_data['country_code'] : 'IT';
2224 $phone_props = $field_properties+array('value'=>$istance_data['default_value'], 'country_code'=>$phone_country);
2225 if ($resolved_format_msg !== '') {
2226 $phone_props['custom_format_message'] = $resolved_format_msg;
2227 }
2228 $element = new AccuaForm_Element_Telephone($istance_data['label'], $istance_data['istance_id'], $phone_props);
2229 break;
2230 //case 'textfield':
2231 default:
2232 /**
2233 * Filter to create a custom Element for an external field type.
2234 *
2235 * @param Element|null $element Null by default; return an Element to override.
2236 * @param string $field_type The field type identifier.
2237 * @param array $field_data The field definition from avail_fields.
2238 * @param array $istance_data The field instance data (label, required, etc.).
2239 * @param array $field_properties Common properties (description, shortDesc, etc.).
2240 */
2241 $element = apply_filters( 'accua_forms_render_field_element', null, $field_data['type'], $field_data, $istance_data, $field_properties );
2242 if ( ! $element ) {
2243 $element = new Element_Textbox($istance_data['label'], $istance_data['istance_id'], $field_properties+array('value'=>$istance_data['default_value']));
2244 }
2245 break;
2246 }
2247 if ($element) {
2248 if (!empty($istance_data['required'])) {
2249 $element->setClass('accuaforms-field-required');
2250 if ($field_data['type'] === 'captcha' && empty($captcha_use_v1)) {
2251 //nothing
2252 } elseif ($field_data['type'] === 'turnstile') {
2253 //nothing - turnstile has its own validation set in the Element constructor
2254 } elseif ($field_data['type'] === 'password-and-confirm') {
2255 if ($resolved_required_msg !== '') {
2256 $req_msg = str_replace(array('%s', '%element%'), $istance_data['label'], $resolved_required_msg);
2257 } else {
2258 /* translators: Password field required error */
2259 $req_msg = __( 'Password is required', 'contact-forms' );
2260 }
2261 $element->setValidation(new Validation_Required($req_msg));
2262 } else {
2263 if ($resolved_required_msg !== '') {
2264 $req_msg = str_replace(array('%s', '%element%'), $istance_data['label'], $resolved_required_msg);
2265 } else {
2266 /* translators: %element% is the field label, replaced with str_replace() */
2267 // phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment -- Translators comment is above
2268 $req_msg = str_replace('%element%', $istance_data['label'], __( '%element% is required', 'contact-forms' ));
2269 }
2270 $element->setValidation(new Validation_Required($req_msg));
2271 }
2272 }
2273
2274 if ($elementName = $element->getName()) {
2275 $element->setClass('accuaform-fieldname-'.$elementName);
2276 }
2277
2278 if ($field_data['type']) {
2279 $element->setClass('accuaform-fieldtype-'.$field_data['type']);
2280 }
2281
2282 $form->addElement($element);
2283 if($element_conf!=NULL) {
2284 $form->addElement($element_conf);
2285 $element_conf=NULL;
2286 }
2287 }
2288 }
2289 if ($fieldset_open) {
2290 $form->addElement(new AccuaForm_Element_FieldsetEnd());
2291 $fieldset_open = false;
2292 }
2293 if ($add_submit) {
2294 $form->addElement(new Element_Button(__('Submit', 'contact-forms'), 'submit', $submit_properties));
2295 }
2296 }
2297
2298 }
2299 }
2300
2301 function accua_forms_aggregate_submitted_data(&$replace_map, $params = array()) {
2302 if (empty($params['txt']) && empty($params['html']) && empty($params['json']) && empty($params['email'])) {
2303 $params += array(
2304 'txt' => true,
2305 'html' => true,
2306 'json' => true,
2307 'email' => true,
2308 );
2309 }
2310
2311 if (!empty($params['txt'])) {
2312 $replace_map['__submitted_txt'] = implode("\n",$replace_map['__submitted_txt_raw']);
2313 }
2314 if (!empty($params['html'])) {
2315 $replace_map['__submitted_html'] = implode("</td></tr>\n<tr>\n<td style='white-space:nowrap;vertical-align:top;padding:4px 10px 4px 0;'>",$replace_map['__submitted_html_raw']);
2316 }
2317 if (!empty($params['json'])) {
2318 $replace_map['__submitted_json'] = _accua_forms_json_encode($replace_map['__submitted_json_raw']);
2319 }
2320 if (!empty($params['email'])) {
2321 $replace_map['__autoreply_email'] = implode('; ', $replace_map['__autoreply_email_raw']);
2322 }
2323
2324 }
2325
2326 add_filter('accua_form_validate', 'accua_forms_validation_handler', 10, 4);
2327 function accua_forms_validation_handler($valid, $submittedID, $submittedData, $form){
2328 if (substr($submittedID, 0, 14) == '__accua-form__') {
2329 $fid = substr($submittedID,14);
2330 $form_data = _accua_forms_get_form_data($fid, false);
2331 if ($form_data) {
2332 return apply_filters('accua_forms_validation', $valid, $fid, $submittedData, $form);
2333 }
2334 }
2335 return $valid;
2336 }
2337
2338 function accua_forms_anonymize_ip($ip) {
2339 $ip = (string) $ip;
2340 $anonymize_ip_data = get_option('accua_forms_anonymize_ip_data',array());
2341 if (empty($anonymize_ip_data['anonymize_ip_bytes'])) {
2342 return $ip;
2343 }
2344 switch ($anonymize_ip_data['anonymize_ip_bytes']) {
2345 case 1:
2346 $ip = preg_replace('/\.[^.]+$/', '.xxx', $ip);
2347 break;
2348 case 2:
2349 $ip = preg_replace('/\.[^.]+\.[^.]+$/', '.xxx.xxx', $ip);
2350 break;
2351 case 3:
2352 $ip = preg_replace('/\.[^.]+\.[^.]+\.[^.]+$/', '.xxx.xxx.xxx', $ip);
2353 break;
2354 case 4:
2355 $ip = '';
2356 }
2357 return $ip;
2358 }
2359
2360 add_action('accua_form_submit', 'accua_forms_form_submission_handler', -10, 3);
2361 function accua_forms_form_submission_handler($submittedID, $submittedData, $form) {
2362 if (empty($GLOBALS['wp_rewrite'])) {
2363 $GLOBALS['wp_rewrite'] = new WP_Rewrite();
2364 }
2365
2366 if (!class_exists('AccuaConditionalReplacer')){
2367 require_once('AccuaConditionalReplacer.php');
2368 }
2369
2370 if (substr($submittedID, 0, 14) == '__accua-form__') {
2371 $fid = substr($submittedID,14);
2372 $form_data = _accua_forms_get_form_data($fid, false);
2373 if ($form_data) {
2374 global $wpdb;
2375
2376 $time = time();
2377
2378 $afs_stats = _accua_forms_json_encode(array(
2379 'user_agent' => $form->stats['user_agent'],
2380 'platform' => $form->stats['platform'],
2381 'tentatives' => $form->stats['tentatives'],
2382 'submit_method' => $form->stats['submit_method'],
2383 ));
2384
2385 $anonymized_ip = accua_forms_anonymize_ip($form->stats['ip']);
2386
2387 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Form submission insert requires direct query
2388 $insert_ret = $wpdb->insert(
2389 $wpdb->prefix . 'accua_forms_submissions',
2390 array (
2391 'afs_form_id' => (string) $fid,
2392 'afs_post_id' => (string) $form->stats['pid'],
2393 'afs_ip' => $anonymized_ip,
2394 'afs_uri' => (string) $form->stats['uri'],
2395 'afs_referrer' => (string) $form->stats['referrer'],
2396 'afs_lang' => (string) $form->stats['lang'],
2397 'afs_created' => (string) gmdate('Y-m-d H:i:s', $form->stats['created']),
2398 'afs_submitted' => (string) gmdate('Y-m-d H:i:s', $time),
2399 'afs_stats' => (string) $afs_stats,
2400 )
2401 );
2402
2403 if ($insert_ret) {
2404 $submission_id = $form->stats['submission_id'] = $wpdb->insert_id;
2405 } else {
2406 $submission_id = $form->stats['submission_id'] = 0;
2407 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Legitimate error logging for failed DB insert
2408 error_log("[Contact Forms] unable to save submitted form data");
2409 }
2410
2411 $review_submission_url = admin_url('admin.php').'?page=accua_forms_submissions_list&sid='.$submission_id;
2412
2413 $replace_map = array(
2414 '__fid' => $fid,
2415 '__subid' => $submission_id,
2416 '__pid' => $form->stats['pid'],
2417 '__ip' => $form->stats['ip'],
2418 '__anonymized_ip' => $anonymized_ip,
2419 '__uri' => $form->stats['uri'],
2420 '__url' => $form->stats['url'],
2421 '__referrer' => $form->stats['referrer'],
2422 '__lang' => $form->stats['lang'],
2423 '__locale' => $form->stats['locale'],
2424 '__created' => $form->stats['created'],
2425 '__created_day' => wp_date('l j F Y', $form->stats['created']),
2426 '__created_day_month_year' => wp_date('j F Y', $form->stats['created']),
2427 '__created_hour' => wp_date('G:i', $form->stats['created']),
2428 '__submitted' => $time,
2429 '__submitted_day' => wp_date('l j F Y', $time),
2430 '__submitted_day_month_year' => wp_date('j F Y', $time),
2431 '__submitted_hour' => wp_date('G:i', $time),
2432 '__confirmation_emails_message' => $form_data['confirmation_emails_message'],
2433 '__user_agent' => $form->stats['user_agent'],
2434 '__platform' => $form->stats['platform'],
2435 '__tentatives' => $form->stats['tentatives'],
2436 '__submit_method' => $form->stats['submit_method'],
2437 '__review_submission_url' => $review_submission_url,
2438 );
2439
2440 $avail_fields = get_option('accua_forms_avail_fields', array());
2441
2442 $replace_map['__autoreply_email_raw'] = array();
2443 $replace_map['__submitted_txt_raw'] = array();
2444 $replace_map['__submitted_html_raw'] = array();
2445 $replace_map['__submitted_json_raw'] = array();
2446
2447 $_field_data = array();
2448 $_istance_data = array();
2449 $field_data = array();
2450
2451 foreach ($submittedData as $istance_id => $value) {
2452 if (empty($form_data['fields'][$istance_id])) {
2453 continue;
2454 }
2455 $istance_data = $form_data['fields'][$istance_id];
2456
2457 if (empty($avail_fields[$istance_data['ref']])) {
2458 $fieldset_data = array();
2459 if (!empty($istance_data['ref'])) {
2460 if ($istance_data['ref'] == '__fieldset-begin') {
2461 $field_data = array(
2462 'id' => '__fieldset-begin',
2463 'name' => __('Fieldset begin', 'contact-forms'),
2464 'type' => 'fieldset-begin',
2465 'description' => '',
2466 );
2467 } elseif ($istance_data['ref'] == '__fieldset-end') {
2468 $field_data = array(
2469 'id' => '__fieldset-end',
2470 'name' => __('Fieldset end', 'contact-forms'),
2471 'type' => 'fieldset-end',
2472 'description' => '',
2473 );
2474 }
2475 }
2476 } else {
2477 $field_data = $avail_fields[$istance_data['ref']];
2478 }
2479
2480 $field_data += array(
2481 'id' => '__html',
2482 'name' => __( 'Custom HTML content', 'contact-forms'),
2483 'type' => 'html',
2484 'description' => __( 'Use this special field to inject raw HTML in the form. You can use this multiple times.', 'contact-forms'),
2485 'default_value' => '',
2486 'allowed_values' => '',
2487 );
2488
2489 /*
2490 echo "<!-- istance_data: "
2491 , print_r($istance_data, true)
2492 , "\nfield_data: "
2493 , print_r($field_data, true)
2494 , "\nvalue: "
2495 , print_r($value, true)
2496 , "\n-->\n";
2497 */
2498
2499 $istance_data += array(
2500 'istance_id' => $field_data['id'],
2501 'widget_number' => '',
2502 'ref' => $field_data['id'],
2503 'label' => $field_data['name'],
2504 'default_value' => $field_data['default_value'],
2505 'allowed_values' => $field_data['allowed_values'],
2506 );
2507
2508 $type = $field_data['type'];
2509 $file_download_url = '';
2510
2511 switch ($field_data['type']) {
2512 case 'checkbox':
2513 case 'submit':
2514 if (empty($value)) {
2515 $replace_map[$istance_data['istance_id']] = $value = '';
2516 } else {
2517 if (empty($istance_data['default_value'])) {
2518 $replace_map[$istance_data['istance_id']] = 'Checked';
2519 $value = '1';
2520 } else {
2521 $replace_map[$istance_data['istance_id']] = $value = $istance_data['default_value'];
2522 }
2523 }
2524 break;
2525 case 'multiselect':
2526 case 'multicheckbox':
2527 case 'select':
2528 case 'radio':
2529 $el = $form->getElementByName($istance_id);
2530 $opts = $el->getOptions();
2531 if (!is_array($value)) {
2532 $value = array($value);
2533 }
2534 $label = array();
2535 foreach ($value as $val) {
2536 if (isset($opts[$val])) {
2537 $label[] = $opts[$val];
2538 }
2539 }
2540 $replace_map['__label_'.$istance_data['istance_id']] = implode(", ", $label);
2541 $replace_map[$istance_data['istance_id']] = implode(', ',$value);
2542 $value = implode('|',$value);
2543 break;
2544 case 'post-multicheckbox':
2545 if (is_array($value) && $value) {
2546 $el = $form->getElementByName($istance_id);
2547 $opts = $el->getOptions();
2548 $value2 = array();
2549 $titles = array();
2550 $ids = array();
2551 $urls = array();
2552 foreach ($value as $val) {
2553 if (isset($opts[$val])) {
2554 $titles[] = $opts[$val];
2555 $ids[] = $val;
2556 $urls[] = get_permalink($val);
2557 $value2[] = $val . ': ' . trim(preg_replace('/[\s\n\r]+/', ' ', $opts[$val]));
2558 }
2559 }
2560 $replace_map['__label_'.$istance_data['istance_id']] = $replace_map['__post_title_'.$istance_data['istance_id']] = implode("\n", $titles);
2561 $replace_map['__post_id_'.$istance_data['istance_id']] = implode("\n", $ids);
2562 $replace_map['__post_url_'.$istance_data['istance_id']] = implode("\n", $urls);
2563 $replace_map[$istance_data['istance_id']] = $value = implode("\n", $value2);
2564 } else {
2565 $replace_map['__label_'.$istance_data['istance_id']] = $replace_map['__post_title_'.$istance_data['istance_id']] = '';
2566 $replace_map['__post_id_'.$istance_data['istance_id']] = '';
2567 $replace_map['__post_url_'.$istance_data['istance_id']] = '';
2568 $replace_map[$istance_data['istance_id']] = $value = '';
2569 }
2570 break;
2571 case 'post-select':
2572 if ($value !== '') {
2573 $post = get_post(absint($value));
2574 // The submitted ID must belong to the post type the field is
2575 // configured for and have a status the field may expose (publish,
2576 // plus private when explicitly configured); otherwise any post ID
2577 // would be accepted.
2578 $expected_post_type = isset($istance_data['post_type']) ? $istance_data['post_type'] : 'page';
2579 $ps_allowed_statuses = array('publish');
2580 $ps_element = $form->getElementByName($istance_id);
2581 if ($ps_element instanceof AccuaForm_Element_PostSelect) {
2582 $expected_post_type = $ps_element->getEffectivePostType();
2583 $ps_allowed_statuses = $ps_element->getAllowedPostStatuses();
2584 }
2585 if ($post && in_array($post->post_status, $ps_allowed_statuses, true) && $post->post_type === $expected_post_type) {
2586 $title = $post->post_title;
2587 $replace_map['__label_'.$istance_data['istance_id']] = $replace_map['__post_title_'.$istance_data['istance_id']] = $title;
2588 $replace_map['__post_id_'.$istance_data['istance_id']] = $value;
2589 $replace_map['__post_url_'.$istance_data['istance_id']] = get_permalink($value);
2590 $value = $value . ': ' . trim(preg_replace('/[\s\n\r]+/', ' ', $title));
2591 } else {
2592 $replace_map['__label_'.$istance_data['istance_id']] = $replace_map['__post_title_'.$istance_data['istance_id']] = '';
2593 $replace_map['__post_id_'.$istance_data['istance_id']] = '';
2594 $replace_map['__post_url_'.$istance_data['istance_id']] = '';
2595 $value = '';
2596 }
2597 }
2598 $replace_map[$istance_data['istance_id']] = $value;
2599 break;
2600 case 'autoreply_email':
2601 if ($value !== '') {
2602 $replace_map['__autoreply_email_raw'][] = $value;
2603 }
2604 $replace_map[$istance_data['istance_id']] = $value;
2605 break;
2606 case 'file':
2607 //TODO: move temp file to "{$submission_id}_{$field_data['id']}_{$file['name']}"; value is $file['name']
2608 $buildid = $submittedData['_AccuaForm_buildID'];
2609 $file = $form->getFile($istance_id);
2610 if ($value !== null && $value !== '' && $file) {
2611 if ($form->renameFile($istance_id, "{$submission_id}_{$field_data['id']}_{$file['name']}")) {
2612 $urlfield = rawurlencode($istance_data['istance_id']);
2613 $urlfile = rawurlencode($value);
2614 $token = accua_forms_generate_download_token($submission_id);
2615 $file_download_url = admin_url('admin-ajax.php') . "?action=accua_forms_download_submitted_file&subid={$submission_id}&field={$urlfield}&file={$urlfile}&nonce=" . wp_create_nonce('accua_forms_download_nonce')."&token={$token}&_wpnonce=" . wp_create_nonce('download_file_' . $submission_id . '_' . $urlfield);
2616 }
2617 }
2618 $replace_map[$istance_data['istance_id']] = $value;
2619 $replace_map['__download_'.$istance_data['istance_id']] = $file_download_url;
2620 break;
2621 case 'password':
2622 case 'password-and-confirm':
2623 $value = trim($value);
2624 $replace_map[$istance_data['istance_id']] = $value;
2625 $type = 'hashed-password';
2626 if ($value !== '') {
2627 $value = wp_hash_password($value);
2628 }
2629 $replace_map['__hashed_'.$istance_data['istance_id']] = $value;
2630 break;
2631 default:
2632 $replace_map[$istance_data['istance_id']] = $value;
2633 }
2634
2635 switch ($field_data['type']) {
2636 case 'fieldset-begin':
2637 $fieldset_label = !empty($istance_data['label']) ? esc_html($istance_data['label']) : esc_html($istance_data['istance_id']);
2638 $replace_map['__submitted_txt_raw'][$istance_data['istance_id']] = "\n--- {$istance_data['label']} ---";
2639 $replace_map['__submitted_json_raw'][$istance_data['istance_id']] = $value;
2640 $replace_map['__submitted_html_raw'][$istance_data['istance_id']] = "<strong style='font-size:14px;'>{$fieldset_label}</strong></td><td class='valori_submitted'>";
2641 break;
2642 case 'fieldset-end':
2643 break;
2644 case 'file':
2645 $replace_map['__submitted_txt_raw'][$istance_data['istance_id']] = "{$istance_data['istance_id']}\t$value\t$file_download_url";
2646 $replace_map['__submitted_json_raw'][$istance_data['istance_id']] = "$value\t$file_download_url";
2647 $replace_map['__submitted_html_raw'][$istance_data['istance_id']] = "<strong>{$istance_data['istance_id']}</strong></td><td class='valori_submitted'><a href='".esc_url($file_download_url)."'>".esc_html($value)."</a>";
2648 break;
2649
2650 case 'email':
2651 case 'autoreply_email':
2652 $replace_map['__submitted_txt_raw'][$istance_data['istance_id']] = "{$istance_data['istance_id']}\t$value";
2653 $replace_map['__submitted_json_raw'][$istance_data['istance_id']] = $value;
2654 $replace_map['__submitted_html_raw'][$istance_data['istance_id']] = "<strong>{$istance_data['istance_id']}</strong></td><td class='valori_submitted'><a href='mailto:".esc_attr($value)."'>".esc_html($value)."</a>";
2655 break;
2656 case 'submit':
2657 break;
2658 case 'colorpicker':
2659 $replace_map['__submitted_txt_raw'][$istance_data['istance_id']] = "{$istance_data['istance_id']}\t$value";
2660 $replace_map['__submitted_json_raw'][$istance_data['istance_id']] = $value;
2661 if ($value === '') {
2662 $value_html = '';
2663 } else {
2664 $value_esc = esc_attr($value);
2665 $value_html = "<span style='color: $value_esc'><font color='$value_esc'>&#9608;</font></span> $value_esc";
2666 }
2667 $replace_map['__submitted_html_raw'][$istance_data['istance_id']] = "<strong>{$istance_data['istance_id']}</strong></td><td class='valori_submitted'>$value_html";
2668 break;
2669 case 'password':
2670 case 'password-and-confirm':
2671 break;
2672 default:
2673 $replace_map['__submitted_txt_raw'][$istance_data['istance_id']] = "{$istance_data['istance_id']}\t$value";
2674 $replace_map['__submitted_json_raw'][$istance_data['istance_id']] = $value;
2675 $replace_map['__submitted_html_raw'][$istance_data['istance_id']] = "<strong>{$istance_data['istance_id']}</strong></td><td class='valori_submitted'>".esc_html($value);
2676 } if ($submission_id) {
2677 // Ensure value is never NULL to prevent database errors
2678 $safe_value = $value === null ? '' : $value;
2679
2680 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Form field values insert requires direct query
2681 $wpdb->insert(
2682 $wpdb->prefix . 'accua_forms_submissions_values',
2683 array (
2684 'afsv_sub_id' => $submission_id,
2685 'afsv_field_id' => $istance_data['istance_id'],
2686 'afsv_type' => $type,
2687 'afsv_value' => $safe_value,
2688 ),
2689 array('%d','%s','%s','%s')
2690 );
2691 }
2692
2693 $_field_data[$istance_data['istance_id']] = $field_data;
2694 $_istance_data[$istance_data['istance_id']] = $istance_data;
2695
2696 }
2697
2698 $replace_map['__autoreply'] = (bool) ($replace_map['__autoreply_email_raw']
2699 && $form_data['confirmation_emails_subject']
2700 && $form_data['confirmation_emails_message']);
2701
2702 accua_forms_aggregate_submitted_data($replace_map);
2703
2704 //Older undocumented filter, maintained for backward compatibility. In fact filter accua_forms_form_submission_handler is called before accua_forms_submission, but for the rest they are the same
2705 $replace_map = apply_filters('accua_forms_form_submission_handler', $replace_map, $fid, $submittedData, $form, $_field_data, $_istance_data);
2706
2707 //Newer filter, with an easier name
2708 $replace_map = apply_filters('accua_forms_submission', $replace_map, $fid, $submittedData, $form, $_field_data, $_istance_data);
2709
2710 $submitted_html = "<table style='width:100%;border-collapse:collapse;'>\n<tr>\n<td style='white-space:nowrap;vertical-align:top;padding:4px 10px 4px 0;'>" . $replace_map['__submitted_html'] . "</td></tr></table>";
2711 $submitted_html = str_replace("class='valori_submitted'", "class='valori_submitted' style='vertical-align:top;padding:4px 0;overflow-wrap:break-word;word-break:break-word;'", $submitted_html);
2712 $confirmation_emails_message = $replace_map['__confirmation_emails_message'];
2713 unset($replace_map['__submitted_html'], $replace_map['__confirmation_emails_message'], $replace_map['__submitted_txt_raw'], $replace_map['__submitted_html_raw'], $replace_map['__submitted_json_raw'], $replace_map['__autoreply_email_raw']);
2714
2715 $replace_map_html = array();
2716 foreach($replace_map as $key => $value) {
2717 $replace_map_html["!$key"] = wp_kses($value, 'post');
2718 $replace_map_html[$key] = esc_attr($value);
2719 }
2720
2721 $replace_map['__submitted_html'] = $replace_map_html['__submitted_html'] = $replace_map_html['!__submitted_html'] = $submitted_html;
2722 $replacer_html = new AccuaConditionalReplacer($replace_map_html);
2723
2724 $confirmation_emails_message = $replace_map['__confirmation_emails_message'] = $replacer_html->doReplace($confirmation_emails_message);
2725 $replacer_html->appendPattern(array('__confirmation_emails_message' => $confirmation_emails_message, '!__confirmation_emails_message' => $confirmation_emails_message));
2726 $replacer = new AccuaConditionalReplacer($replace_map);
2727
2728 $form_data_replaced = array();
2729
2730 $settings = array(
2731 'emails_from_name',
2732 'emails_from',
2733 'admin_emails_to',
2734 'emails_bcc',
2735 'admin_emails_subject',
2736 'confirmation_emails_subject',
2737 );
2738
2739 foreach($settings as $i) {
2740 $form_data_replaced[$i] = $replacer->doReplace($form_data[$i]);
2741 }
2742
2743 $settings_html = array(
2744 'success_message',
2745 'error_message',
2746 'admin_emails_message',
2747 );
2748
2749 foreach($settings_html as $i) {
2750 $form_data_replaced[$i] = $replacer_html->doReplace($form_data[$i]);
2751 }
2752
2753 // Track mail sending success for showing appropriate message
2754 $mail_success = true;
2755 $mail1 = true;
2756 $mail2 = true;
2757
2758 $header = array("Content-Type: text/html; charset=".get_option('blog_charset'));
2759
2760 $emails_from = trim($form_data_replaced['emails_from']);
2761 if (strpos($emails_from, '@') !== false) {
2762 if ((strpos($emails_from, '<') === false) && is_email($emails_from)) {
2763 $emails_from_name = trim($form_data_replaced['emails_from_name']);
2764 if ($emails_from_name !== '') {
2765 $emails_from = "=?" . get_bloginfo('charset') . "?B?" . base64_encode($emails_from_name) . "?= <$emails_from>";
2766 }
2767 }
2768 $header[] = 'From: '.$emails_from;
2769 }
2770
2771 if ($form_data_replaced['emails_bcc']) {
2772 $header[] = 'Bcc: '.$form_data_replaced['emails_bcc'];
2773 }
2774
2775 if ($form_data_replaced['admin_emails_to']
2776 && $form_data_replaced['admin_emails_subject']) {
2777 /*
2778 $admin_tos = explode(',', strtr($form_data_replaced['admin_emails_to'], "\n\t\r;", ',,,,'));
2779 foreach ($admin_tos as $admin_to) {
2780 $mail1 = wp_mail(trim($admin_to), $form_data_replaced['admin_emails_subject'], $form_data_replaced['admin_emails_message'], $header);
2781 }
2782 */
2783 $mail1 = wp_mail($form_data_replaced['admin_emails_to'], $form_data_replaced['admin_emails_subject'],'<html><head></head><body style="background:#f9f8f8;font-size: 12px;font-family: &quot;Lucida Sans&quot;,&quot;Lucida Grande&quot;, Verdana, Arial, Sans-Serif;">'.wpautop($form_data_replaced['admin_emails_message']).'</body></html>', $header);
2784 if (!$mail1) {
2785 $mail_success = false;
2786 }
2787 }
2788
2789 if ($replace_map['__autoreply'] && $replace_map['__autoreply_email']
2790 && $form_data_replaced['confirmation_emails_subject']
2791 && $confirmation_emails_message) {
2792 $mail2 = wp_mail($replace_map['__autoreply_email'], $form_data_replaced['confirmation_emails_subject'], '<html><head></head><body>'.wpautop($confirmation_emails_message).'</body></html>', $header);
2793 if (!$mail2) {
2794 $mail_success = false;
2795 }
2796 }
2797
2798 // Determine which message to show based on mail success and user settings
2799 if ($mail_success) {
2800 // Show success message unless "Don't show any messages" is selected
2801 if (empty($form_data['success_message_no_message'])) {
2802 $message_content = trim($form_data_replaced['success_message']);
2803 if ($message_content !== '') {
2804 AccuaForm::appendSubmittedMessages(wpautop($message_content));
2805 }
2806 }
2807 } else {
2808 // Mail failed - show error message unless "Don't show any messages" is selected
2809 if (empty($form_data['error_message_no_message'])) {
2810 $error_content = trim($form_data_replaced['error_message']);
2811 if ($error_content !== '') {
2812 AccuaForm::appendSubmittedMessages(wpautop($error_content));
2813 }
2814 }
2815 }
2816
2817 /*
2818 echo "<!-- replace_map: "
2819 , print_r($replace_map, true)
2820 , "\nreplace_map: "
2821 , print_r($replace_map, true)
2822 , "\nform_data_replaced: "
2823 , print_r($form_data_replaced, true)
2824 , "\nautoreply_email: "
2825 , print_r($autoreply_email, true)
2826 , "\nmail1: "
2827 , print_r($mail1, true)
2828 , "\nmail2: "
2829 , print_r($mail2, true)
2830 ,"\n-->";
2831 */
2832 }
2833 }
2834 }
2835
2836 function accua_forms_get_submission_data($subid, $options = array()){
2837 //TODO: Completa e usa per sostituire i campi
2838 global $wpdb;
2839 $subid = (int) $subid;
2840 $options += array(
2841 'extra' => true,
2842 'file_format' => 'name',
2843 );
2844 $ret = array();
2845 if ($options['extra']) {
2846 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Submission data lookup requires direct query
2847 $query1 = $wpdb->prepare(
2848 "SELECT *
2849 FROM `{$wpdb->prefix}accua_forms_submissions`
2850 WHERE afs_id = %d",
2851 $subid
2852 );
2853 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- $query1 is prepared above, submission lookup requires direct query
2854 $data = $wpdb->get_row($query1);
2855 if (!empty($data)) {
2856 $created = $data->afs_created;
2857 $created[10] = 'T';
2858 $created.='.00+00:00';
2859 $created = strtotime($created);
2860 $submitted = $data->afs_submitted;
2861 $submitted[10] = 'T';
2862 $submitted.='.00+00:00';
2863 $submitted = strtotime($submitted);
2864 if ($data->afs_stats) {
2865 $stats = json_decode($data->afs_stats, true);
2866 if (!$stats) {
2867 $stats = array();
2868 }
2869 } else {
2870 $stats = array();
2871 }
2872 $stats += array(
2873 'user_agent' => '',
2874 'platform' => '',
2875 'tentatives' => '',
2876 'submit_method' => '',
2877 );
2878 $ret += array(
2879 '__fid' => $data->afs_form_id,
2880 '__subid' => $subid,
2881 '__pid' => $data->afs_post_id,
2882 '__ip' => $data->afs_ip,
2883 '__anonymized_ip' => accua_forms_anonymize_ip($data->afs_ip),
2884 '__uri' => $data->afs_uri,
2885 '__referrer' => $data->afs_referrer,
2886 '__lang' => $data->afs_lang,
2887 '__created' => $created,
2888 '__created_day' => wp_date('l j F Y', $created),
2889 '__created_hour' => wp_date('G:i', $created),
2890 '__submitted' => $submitted,
2891 '__submitted_day' => wp_date('l j F Y', $submitted),
2892 '__submitted_hour' => wp_date('G:i', $submitted),
2893 '__user_agent' => $stats['user_agent'],
2894 '__platform' => $stats['platform'],
2895 '__tentatives' => $stats['tentatives'],
2896 '__submit_method' => $stats['submit_method'],
2897 );
2898 }
2899 }
2900
2901 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Submission values lookup requires direct query
2902 $query2 = $wpdb->prepare(
2903 "SELECT *
2904 FROM `{$wpdb->prefix}accua_forms_submissions_values`
2905 WHERE afsv_sub_id = %d",
2906 $subid
2907 );
2908
2909 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- $query2 is prepared above, submission values lookup requires direct query
2910 $data2 = $wpdb->get_results($query2, OBJECT);
2911
2912 foreach ($data2 as $row) {
2913 switch ($row->afsv_type) {
2914 case 'file' :
2915 if ($options['file_format'] == 'url' || $options['file_format'] == 'link') {
2916 $fieldid = rawurlencode($row->afsv_field_id);
2917 $filename = rawurlencode($row->afsv_value);
2918 $url = admin_url('admin-ajax.php') . "?action=accua_forms_download_submitted_file&subid={$row->afsv_sub_id}&field={$fieldid}&file={$filename}&nonce=" . wp_create_nonce('accua_forms_download_nonce') . "&_wpnonce=" . wp_create_nonce('download_file_' . $row->afsv_sub_id . '_' . $fieldid);
2919 if(isset($options['token'])){
2920 $url .= '&token='.$options['token'];
2921 }
2922 if ($options['file_format'] == 'link'){
2923 $url = esc_url($url);
2924 $filename = esc_html($row->afsv_value);
2925 $fielddata = "<a href='{$url}' target='_blank'>{$filename}</a>";
2926 } else {
2927 $fielddata = $url;
2928 }
2929 } else { // $options['file_format'] == 'name'
2930 $fielddata = $row->afsv_value;
2931 }
2932 break;
2933 default:
2934 $fielddata = $row->afsv_value;
2935 }
2936 $ret[$row->afsv_field_id] = $fielddata;
2937 }
2938 return $ret;
2939 }
2940
2941 add_shortcode( 'accua-form', 'accua_forms_shortcode_handler' );
2942 function accua_forms_shortcode_handler($atts, $content = '', $code = '') {
2943 if (empty($atts['fid'])) {
2944 return '';
2945 }
2946
2947 $fid = $atts['fid'];
2948 $form_data = _accua_forms_get_form_data($fid, false);
2949
2950 if (! $form_data) {
2951 // In preview mode, allow unsaved (new) forms to render using draft + defaults
2952 if (! apply_filters('accua_forms_use_draft_for_preview', false)) {
2953 return '';
2954 }
2955 $default_form_data = get_option('accua_forms_default_form_data', array());
2956 $empty_form_data = _accua_forms_get_form_data(false);
2957 $form_data = array('_overrided' => array()) + $default_form_data + $empty_form_data;
2958 }
2959
2960 // Note: Preview field order override is handled in accua_forms_form_generate()
2961 // which applies the filter there for live preview
2962
2963 $fid = '__accua-form__'.$fid;
2964
2965 $out = '';
2966
2967 if (AccuaForm::getSubmittedID() == $fid) {
2968 /* return "<pre>Form submitted.\n\nData: " . print_r(AccuaForm::getSubmittedData(), true) . '</pre>'; */
2969 // Get per-form messages (supports multiple forms on same page)
2970 $messages = AccuaForm::getSubmittedMessages($fid);
2971 if ($messages && trim($messages) !== '') {
2972 $out .= '<div id="_response_messages_'.$fid.'" class="accua-form-messages">'.$messages.'</div>';
2973 }
2974
2975 // Non-AJAX fallback: set URL hash and scroll to result messages on page load.
2976 // The anchor elements are only created by the AJAX JS block (which is not output for
2977 // non-AJAX forms), so we scroll to the messages div by class instead.
2978 $anchor_suffix = preg_replace('/[^a-zA-Z0-9]+/', '_', str_replace('__accua-form__', '', $fid));
2979 $hash_type = AccuaForm::isValid() ? 'formSubmitSuccess' : 'formSubmitInvalid';
2980 $anchor_full = esc_js($hash_type . '-' . $anchor_suffix);
2981 $out .= '<script>document.addEventListener("DOMContentLoaded",function(){'
2982 . 'if(history.replaceState)history.replaceState(null,"","#' . $anchor_full . '");'
2983 . 'var m=document.querySelector(".accua-form-messages");'
2984 . 'if(m)m.scrollIntoView({behavior:"smooth",block:"start"})'
2985 . '});</script>';
2986
2987 if (AccuaForm::isValid()) {
2988 return $out;
2989 }
2990 $form = AccuaForm::getSubmittedForm();
2991 } else {
2992 $analytics_data = get_option('accua_forms_default_analytics_data',array());
2993
2994 // Check for preview layout override (allows live preview of layout changes before save)
2995 $layout = $form_data['layout'];
2996 $preview_layout_override = apply_filters('accua_forms_preview_layout_override', '');
2997 if ($preview_layout_override) {
2998 $layout = $preview_layout_override;
2999 }
3000
3001 // If layout is empty (meaning "use default"), resolve to the global default layout
3002 if (empty($layout)) {
3003 $default_form_data = get_option('accua_forms_default_form_data', array());
3004 $layout = !empty($default_form_data['layout']) ? $default_form_data['layout'] : 'sidebyside';
3005 }
3006
3007 $params = array(
3008 'layout' => $layout,
3009 'title' => $form_data['title'],
3010 'track_submit' => !empty($analytics_data['analytics_track_submit']),
3011 'track_fields' => !empty($analytics_data['analytics_track_fields']),
3012 'gads_conversion_tracking_code' => $form_data['gads_conversion_tracking_code'],
3013 );
3014 $form = AccuaForm::create($fid, $params);
3015 }
3016
3017 $out .= $form->render(true);
3018
3019 $doing_ajax = function_exists('wp_doing_ajax') ? wp_doing_ajax() : (defined( 'DOING_AJAX' ) && DOING_AJAX);
3020 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check for Yoast SEO compatibility, strips HTML for preview
3021 if ($doing_ajax && isset($_REQUEST['action']) && ($_REQUEST['action'] === 'wpseo_filter_shortcodes')) {
3022 $strip_regexp = '/(<iframe[^>]*>(.*?)<\/iframe>|<script[^>]*>(.*?)<\/script>|<input([^>]*)type="hidden"[^>]*>)/is';
3023 $out = preg_replace($strip_regexp, '', $out);
3024 }
3025
3026 return $out;
3027
3028 }
3029
3030 function accua_forms_include($fid, $atts=array(), $content = '', $code = '') {
3031 $atts['fid'] = $fid;
3032 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Shortcode handler manages its own escaping
3033 echo accua_forms_shortcode_handler($atts, $content, $code);
3034 }
3035
3036 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function, double underscore prefix indicates private
3037 function __accua_forms_submissions_list_page(){
3038 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing, actual actions have nonce checks
3039 if(isset($_GET['sid'])) {
3040 accua_forms_single_submission();
3041 } else {
3042 accua_forms_submissions_list_page();
3043 }
3044 }
3045 function accua_forms_submissions_list_page_load(){
3046 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing for screen option registration
3047 if(isset($_GET['sid'])) {
3048 // Handle GET-based trash/restore actions here (before any output is sent)
3049 require_once __DIR__ . '/admin/single-submission.php';
3050 $sid = (int) $_GET['sid'];
3051 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified below before processing
3052 if ( $sid && isset( $_GET['action'] ) ) {
3053 if ( $_GET['action'] === 'trash' ) {
3054 check_admin_referer( 'del_sub_form_' . $sid );
3055 accua_forms_trash_submission( $sid );
3056 wp_safe_redirect( admin_url( 'admin.php?page=accua_forms_submissions_list&trashed=1' ) );
3057 exit;
3058 }
3059 if ( $_GET['action'] === 'restore' ) {
3060 check_admin_referer( 'restore_sub_form_' . $sid );
3061 accua_forms_restore_submission( $sid );
3062 wp_safe_redirect( admin_url( 'admin.php?page=accua_forms_submissions_list&restored=1' ) );
3063 exit;
3064 }
3065 }
3066 return;
3067 }
3068 add_screen_option('per_page', array(
3069 'default' => 100,
3070 'option' => 'accua_forms_submissions_per_page',
3071 ));
3072
3073 // Set default hidden columns for first-time users: hide non-essential columns.
3074 add_filter( 'default_hidden_columns', function( $hidden ) {
3075 $non_essential = [ 'form_id', 'pid', 'created', 'lead_status' ];
3076 $avail_fields = get_option( 'accua_forms_avail_fields', [] );
3077 foreach ( array_keys( $avail_fields ) as $slug ) {
3078 if ( $slug !== 'email' ) {
3079 $non_essential[] = '_field_' . $slug;
3080 }
3081 }
3082 return array_unique( array_merge( $hidden, $non_essential ) );
3083 } );
3084
3085 $screen = get_current_screen();
3086 $screen->add_help_tab( array(
3087 'id' => 'accua_forms_lead_statuses',
3088 'title' => __( 'Lead Statuses', 'contact-forms' ),
3089 'content' => '<p>' . accua_forms_get_lead_statuses_help() . '</p>',
3090 ) );
3091 }
3092 add_filter('set_screen_option_accua_forms_submissions_per_page', function($status, $option, $value) {
3093 return (int) $value;
3094 }, 10, 3);
3095 function accua_forms_submissions_list_page_head(){
3096 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing, actual actions have nonce checks
3097 if(isset($_GET['sid'])) {
3098 require_once __DIR__ . '/admin/single-submission.php';
3099 accua_forms_single_submission(true);
3100 } else {
3101 require_once __DIR__ . '/admin/submissions-list-page.php';
3102 accua_forms_submissions_list_page(true);
3103 }
3104
3105 }
3106
3107 /* Generiamo token di sicurezza per poter accedere anche da anonimo - email */
3108 function accua_forms_generate_download_token($subid) {
3109 global $wpdb;
3110 $token = wp_generate_password(32, false); // Token casuale di 32 caratteri
3111
3112 // Controlla se esiste già un token per questo sub_id
3113 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Token lookup requires direct query
3114 $existing_token = $wpdb->get_var($wpdb->prepare(
3115 "SELECT afsv_value FROM `{$wpdb->prefix}accua_forms_submissions_values` WHERE afsv_sub_id = %d AND afsv_field_id = '_accua_download_token'",
3116 $subid
3117 ));
3118 if ($existing_token) {
3119 return $existing_token;
3120 } else{
3121 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Token insert requires direct query
3122 $wpdb->insert(
3123 $wpdb->prefix . 'accua_forms_submissions_values',
3124 [
3125 'afsv_sub_id' => $subid,
3126 'afsv_field_id' => '_accua_download_token',
3127 'afsv_type' => 'token',
3128 'afsv_value' => $token
3129 ],
3130 ['%d', '%s', '%s', '%s']
3131 );
3132 return $token;
3133 }
3134 }
3135
3136 function accua_forms_check_download_token($subid, $get_token) {
3137 global $wpdb;
3138
3139 $subid = (int) $subid; // Cast to integer for security
3140
3141 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Token verification requires direct query
3142 $saved_token = $wpdb->get_var($wpdb->prepare(
3143 "SELECT afsv_value FROM `{$wpdb->prefix}accua_forms_submissions_values` WHERE afsv_sub_id = %d AND afsv_field_id = '_accua_download_token'",
3144 $subid
3145 ));
3146
3147 // Debug logging for token verification (comment out in production)
3148 // error_log("Token check: Submission ID: $subid, Provided token: $get_token, Saved token: $saved_token");
3149
3150 return isset($get_token) && $get_token === $saved_token;
3151 }
3152
3153 /**
3154 * Gestisce il download di un file inviato tramite un modulo.
3155 *
3156 * Questa funzione viene eseguita tramite una richiesta AJAX e permette agli utenti di scaricare
3157 * un file precedentemente caricato con un modulo. Controlla i parametri della richiesta per verificare
3158 * la presenza di un file associato a un determinato ID di invio e campo del modulo.
3159 *
3160 * - Se il parametro "html" è presente, genera una pagina HTML con un link di reindirizzamento automatico.
3161 * - Recupera le informazioni del file dal database per verificarne l'esistenza.
3162 * - Se il file esiste e può essere letto, restituisce il contenuto con gli appropriati header HTTP.
3163 * - Se il file non viene trovato, restituisce un errore 404.
3164 *
3165 * Sicurezza:
3166 * - Nonce
3167 * - Utilizza `stripslashes_deep` per sanificare i dati in ingresso.
3168 * - Protegge il database utilizzando `wpdb->prepare` per prevenire SQL Injection.
3169 * - Determina il tipo MIME del file per un download sicuro.
3170 * - Aggiunto token di verifica per utenti
3171 */
3172
3173 add_action('wp_ajax_accua_forms_download_submitted_file', 'accua_forms_download_submitted_file');
3174 add_action('wp_ajax_nopriv_accua_forms_download_submitted_file', 'accua_forms_download_submitted_file');
3175 function accua_forms_download_submitted_file(){
3176 $get = stripslashes_deep($_GET);
3177 $token_valid = false;
3178 $nonce_valid = false;
3179 $subid = '';
3180
3181 if(isset($get['subid'])){
3182 $subid = rawurlencode($get['subid']);
3183 }
3184
3185 // First verify WordPress nonce for CSRF protection (for logged-in users)
3186 if (isset($get['_wpnonce']) && wp_verify_nonce($get['_wpnonce'], 'download_file_' . $subid . '_' . $get['field'])) {
3187 $nonce_valid = true;
3188 }
3189
3190 // For backward compatibility with older URL format that use 'nonce' instead of '_wpnonce'
3191 if (!$nonce_valid && isset($get['nonce']) && check_ajax_referer('accua_forms_download_nonce', 'nonce', false)) {
3192 $nonce_valid = true;
3193 }
3194
3195 // Check for token-based authentication (for email links and unauthenticated users)
3196 if (isset($get['token']) && $subid != '') {
3197 if (accua_forms_check_download_token($subid, $get['token']) == 1) {
3198 $token_valid = true;
3199 }
3200 }
3201
3202 // If both authentication methods fail, deny access
3203 if (!$nonce_valid && !$token_valid) {
3204 wp_die(esc_html__('Security check failed.', 'contact-forms'), 403);
3205 }
3206 // Additional permission check for admin users
3207 if(!$token_valid && !$nonce_valid && $subid != ''){
3208 // If neither token nor nonce is valid, check for logged-in admin permissions
3209 if (!is_user_logged_in() || !current_user_can('manage_options')) {
3210 wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'contact-forms'));
3211 }
3212 }
3213 if (isset($get['subid'],$get['field'],$get['file'])) {
3214 if (!empty($get['html'])) { /* export xls*/
3215 header("Content-type: text/html");
3216 $subid = rawurlencode($get['subid']);
3217 $fieldid = rawurlencode($get['field']);
3218 $filename = rawurlencode($get['file']);
3219 $url = admin_url('admin-ajax.php') . "?action=accua_forms_download_submitted_file&subid={$subid}&field={$fieldid}&file={$filename}&nonce=" . wp_create_nonce('accua_forms_download_nonce') . "&_wpnonce=" . wp_create_nonce('download_file_' . $subid . '_' . $fieldid);
3220 if(isset($get['token'])){
3221 $url .= '&token='.$get['token'];
3222 }
3223 $url = esc_url($url);
3224 $filename = esc_html($get['file']);
3225 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $filename and $url are pre-escaped above
3226 die("<html><head><title>{$filename}</title><meta http-equiv='refresh' content='0;URL={$url}'></head><body><a href='{$url}'>{$filename}</a></body></html>");
3227 }
3228 global $wpdb;
3229 $subid = (int) $get['subid'];
3230 $field = $get['field'];
3231 $file = $get['file'];
3232 $query = $wpdb->prepare(
3233 "SELECT *
3234 FROM `{$wpdb->prefix}accua_forms_submissions_values`
3235 WHERE afsv_sub_id = %d
3236 AND afsv_field_id = %s
3237 AND afsv_value = %s",
3238 $subid,
3239 $field,
3240 $file
3241 );
3242 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- $query is prepared above, file download verification requires direct query
3243 $subval = $wpdb->get_results($query, OBJECT);
3244 if ($subval) {
3245 $file_data = get_option('accua_forms_default_file_field_data',array()) + array('dest_path' => '');
3246 $dest_path = _accua_forms_get_abs_dest_path($file_data['dest_path']);
3247 $filename = "{$dest_path}/{$subid}_{$field}_{$file}";
3248 if (is_file($filename) && is_readable($filename)){
3249 if (function_exists('finfo_open')){
3250 @ $finfo = finfo_open(FILEINFO_MIME_TYPE);
3251 if ($finfo) {
3252 @ $filetype = finfo_file($finfo, $filename);
3253 @ finfo_close($finfo);
3254 }
3255 }
3256 if (empty($filetype) && function_exists('mime_content_type')){
3257 @ $filetype = mime_content_type($filename);
3258 }
3259 if (empty($filetype)) {
3260 $filetype = "application/octet-stream";
3261 }
3262 // Clean any output buffers to prevent stale content from being sent before the file
3263 while (ob_get_level()) {
3264 ob_end_clean();
3265 }
3266 // Remove all pre-set headers (admin-ajax.php sets Content-Type: text/html early)
3267 header_remove();
3268 nocache_headers();
3269 header("Content-Type: $filetype");
3270 header("Content-Length: ".filesize($filename));
3271 if (empty($_GET['view'])) {
3272 header("Content-Disposition: attachment; filename=\"$file\"");
3273 }
3274 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- WP_Filesystem not suitable for binary file streaming
3275 readfile($filename);
3276 exit;
3277 }
3278 }
3279 }
3280 header("HTTP/1.0 404 Not Found");
3281 //header("Status: 404 Not Found");
3282 die('File not found');
3283 }
3284
3285 function accua_forms_buildConditionalReplacer($map = array()) {
3286 if (!class_exists('AccuaConditionalReplacer')){
3287 require_once('AccuaConditionalReplacer.php');
3288 }
3289 return new AccuaConditionalReplacer($map);
3290 }
3291
3292
3293 add_action('wp_ajax_accua_forms_preview', 'accua_forms_preview');
3294 function accua_forms_preview() {
3295 if (!current_user_can('manage_options')){
3296 die ('');
3297 }
3298
3299 // Check nonce for CSRF protection
3300 $nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';
3301 if ( ! wp_verify_nonce( $nonce, 'accua_forms_preview' ) ) {
3302 wp_die( esc_html__( 'Security check failed.', 'contact-forms' ), 403 );
3303 }
3304
3305 // Enqueue form styles before printing them
3306 accua_form_enqueue_scripts_and_styles();
3307
3308 // Accept temporary layout override for live preview (before save)
3309 // This allows real-time preview when user changes layout dropdown
3310 $preview_layout = '';
3311 if ( ! empty( $_REQUEST['preview_layout'] ) ) {
3312 $layout_input = sanitize_text_field( wp_unslash( $_REQUEST['preview_layout'] ) );
3313 $allowed_layouts = array( 'toplabel', 'sidebyside', 'inlinelabel' );
3314 if ( in_array( $layout_input, $allowed_layouts, true ) ) {
3315 $preview_layout = $layout_input;
3316 } elseif ( 'default' === $layout_input ) {
3317 // 'default' means use the global default layout
3318 $default_form_data = get_option( 'accua_forms_default_form_data', array() );
3319 $preview_layout = ! empty( $default_form_data['layout'] ) ? $default_form_data['layout'] : 'sidebyside';
3320 }
3321 }
3322
3323 // Accept temporary field order for live preview (before save)
3324 // This allows preview to show reordered fields without saving to database
3325 $preview_order = null;
3326 if ( ! empty( $_REQUEST['preview_order'] ) ) {
3327 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON decoded and validated below
3328 $order_json = wp_unslash( $_REQUEST['preview_order'] );
3329 $preview_order = json_decode( $order_json, true );
3330 if ( json_last_error() !== JSON_ERROR_NONE ) {
3331 $preview_order = null;
3332 }
3333 }
3334
3335 // Store the preview layout override in a filter so shortcode handler can use it
3336 if ($preview_layout) {
3337 add_filter('accua_forms_preview_layout_override', function() use ($preview_layout) {
3338 return $preview_layout;
3339 });
3340 }
3341
3342 // Store the preview order override in a filter so shortcode handler can use it
3343 if ($preview_order) {
3344 add_filter('accua_forms_preview_order_override', function() use ($preview_order) {
3345 return $preview_order;
3346 });
3347 }
3348
3349 // Signal that we're in admin preview mode - form generator should read from draft
3350 add_filter('accua_forms_use_draft_for_preview', '__return_true');
3351
3352 echo '<html><head>
3353 <style>
3354 *, *::before, *::after { box-sizing: border-box; }
3355 body {
3356 font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
3357 margin: 0;
3358 padding: 16px;
3359 background: #fff;
3360 font-size: 14px;
3361 line-height: 1.5;
3362 }
3363 </style>';
3364 wp_print_styles();
3365 wp_print_head_scripts();
3366 echo '</head><body>';
3367 $preview_fid = isset($_REQUEST['fid']) ? sanitize_text_field(wp_unslash($_REQUEST['fid'])) : '';
3368
3369 // Check if the form has any fields — show placeholder if empty
3370 $draft_data = _accua_forms_get_draft_data($preview_fid);
3371 if (empty($draft_data['fields'])) {
3372 echo '<p style="color:#50575e;text-align:center;margin-top:40px;">' . esc_html__('Add fields to the form to see the preview.', 'contact-forms') . '</p>';
3373 } else {
3374 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Shortcode handler manages its own escaping
3375 echo accua_forms_shortcode_handler(array('fid' => $preview_fid));
3376 }
3377 wp_print_footer_scripts();
3378 echo '</body></html>';
3379 die('');
3380 }
3381
3382 //salvataggio file excel
3383 add_action('wp_ajax_accua_forms_submission_page_save_excel', 'accua_forms_submission_page_save_excel');
3384 //add_action('wp_ajax_nopriv_accua_forms_submission_page_save_excel', 'accua_forms_submission_page_save_excel');
3385
3386 function accua_forms_submission_page_save_excel() {
3387 if (!current_user_can('manage_options')){
3388 header("HTTP/1.0 401 Access Denied");
3389 //header("Status: 401 Access Denied");
3390 die('You are not authorized to access this page.');
3391 }
3392
3393 // Check nonce for CSRF protection
3394 if (!isset($_REQUEST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['_wpnonce'])), 'accua_forms_export_excel')) {
3395 wp_die(esc_html__('Security check failed.', 'contact-forms'), 403);
3396 }
3397
3398 require_once __DIR__ . '/admin/submissions-list-page.php';
3399 $listTable = new Accua_Forms_Submissions_List_Table();
3400 $listTable->export_xls = true;
3401 $listTable->prepare_items(true);
3402 // Sanitize column selection input
3403 $show_col_input = isset($_GET['accua_show_field']) ? sanitize_text_field(wp_unslash($_GET['accua_show_field'])) : '';
3404 $show_col = array_map('sanitize_key', explode(',', $show_col_input));
3405 $show_col = array_diff($show_col, array('singlesub'));
3406 header('Content-disposition: attachment; filename=downloads-report.xls');
3407 header('Content-type: application/vnd.ms-excel');
3408 accua_forms_submission_page_save_excel_general($listTable,$show_col);
3409 die('');
3410 }
3411
3412
3413
3414 function accua_forms_submission_page_save_excel_general(Accua_Forms_Submissions_List_Table $listTable,array $show_col,array $options=array()) {
3415 global $wpdb;
3416
3417 $content_type = 'text/html; charset=' . get_option('blog_charset');
3418
3419 //creo il file excel
3420 ?><html xmlns:o="urn:schemas-microsoft-com:office:office"
3421 xmlns:x="urn:schemas-microsoft-com:office:excel"
3422 xmlns="http://www.w3.org/TR/REC-html40">
3423 <head>
3424 <meta http-equiv=Content-Type content="<?php echo esc_attr( $content_type ); ?>" />
3425 <meta name=ProgId content=Excel.Sheet />
3426 <style>
3427 <!--
3428 td {vertical-align:top;}
3429 .head_row {font-weight:bold;}
3430 .column-date { mso-number-format:"Short Date"; }
3431 .datetime_cell { mso-number-format:"yyyy\\-mm\\-dd\\ hh\:mm\:ss"; }
3432 -->
3433 </style>
3434 <!--[if gte mso 9]><xml>
3435 <x:ExcelWorkbook>
3436 <x:ExcelWorksheets>
3437 <x:ExcelWorksheet>
3438 <x:WorksheetOptions>
3439 <x:FreezePanes/>
3440 <x:FilterOn/>
3441 <x:SplitHorizontal>1</x:SplitHorizontal>
3442 <x:TopRowBottomPane>1</x:TopRowBottomPane>
3443 <x:ActivePane>2</x:ActivePane>
3444 <x:Panes>
3445 <x:Pane>
3446 <x:Number>3</x:Number>
3447 </x:Pane>
3448 <x:Pane>
3449 <x:Number>2</x:Number>
3450 </x:Pane>
3451 </x:Panes>
3452 </x:WorksheetOptions>
3453 </x:ExcelWorksheet>
3454 </x:ExcelWorksheets>
3455 </x:ExcelWorkbook>
3456 </xml><![endif]-->
3457 </head>
3458 <body>
3459 <table x:str border=1 >
3460 <tr class='head-row'>
3461 <?php
3462 $cols = $listTable->get_columns();
3463 foreach($cols as $col_key=>$col_value) {
3464 if(in_array($col_key, $show_col)) { ?>
3465 <td x:autofilter="all"><?php echo esc_html( $col_value ); ?></td>
3466 <?php }
3467 } ?>
3468 </tr>
3469
3470 <?php
3471 $lead_statuses = accua_forms_get_lead_statuses();
3472
3473 foreach($listTable->items as $id_submission=>$single_submission) {
3474 // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Required to prevent timeout during large exports
3475 @ set_time_limit(10);
3476 echo "<tr>";
3477 foreach($cols as $col_key=>$col_value) {
3478 if(in_array($col_key, $show_col)) {
3479 echo '<td class="' . esc_attr($col_key) . '">';
3480 if ($col_key == 'lead_status') {
3481 if (isset($lead_statuses[$single_submission['lead_status']])) {
3482 echo esc_html($lead_statuses[$single_submission['lead_status']]);
3483 }
3484 } elseif(isset($single_submission[$col_key])) {
3485 if ( method_exists( $listTable, 'column_' . $col_key ) ) {
3486 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- List table column methods handle their own escaping
3487 echo call_user_func( array( &$listTable, 'column_' . $col_key ), $single_submission );
3488 } else {
3489 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- List table column_default handles escaping
3490 echo $listTable->column_default( $single_submission, $col_key );
3491 }
3492 }
3493 echo "</td>";
3494 }
3495
3496 }
3497 echo "</tr>";
3498 }
3499
3500 ?>
3501 </table>
3502 </body>
3503 </html>
3504 <?php
3505 }
3506
3507 function accua_forms_print_tokens() {
3508 $avail_fields = get_option('accua_forms_avail_fields', array());
3509 $tokens = '';
3510 foreach($avail_fields as $key=>$value) {
3511 $field_name = $value['name'] ?? $value['label'] ?? $key;
3512 $tokens .= $field_name . ": {" . $key . "}\n";
3513 switch ($value['type']) {
3514 case 'file':
3515 $tokens .= $field_name . " (download link): {__download_" . $key . "}\n";
3516 break;
3517 case 'multiselect':
3518 case 'multicheckbox':
3519 $tokens .= $field_name . " (labels): {__label_" . $key . "}\n";
3520 break;
3521 case 'select':
3522 case 'radio':
3523 $tokens .= $field_name . " (label): {__label_" . $key . "}\n";
3524 break;
3525 case 'post-multicheckbox':
3526 $tokens .= $field_name . " (posts titles): {__label_" . $key . "}\n";
3527 $tokens .= $field_name . " (posts ids): {__post_id_" . $key . "}\n";
3528 $tokens .= $field_name . " (posts urls): {__post_url_" . $key . "}\n";
3529 break;
3530 case 'post-select':
3531 $tokens .= $field_name . " (post title): {__label_" . $key . "}\n";
3532 $tokens .= $field_name . " (post id): {__post_id_" . $key . "}\n";
3533 $tokens .= $field_name . " (post url): {__post_url_" . $key . "}\n";
3534 break;
3535 }
3536 }
3537
3538 // phpcs:disable PluginCheck.CodeAnalysis.Heredoc.NotAllowed, WordPress.Security.EscapeOutput.HeredocOutputNotEscaped -- Heredoc for tokens help HTML
3539 echo <<<EOT
3540 <div class="accua_forms_token_list">
3541 <h2>Tokens</h2>
3542 <em>In HTML text, use {!token_name} to insert unfiltered token value</em>
3543 <h3>Fields</h3>
3544 <em>These tokens are available only if the field is added to the form</em>
3545 <pre>$tokens</pre>
3546 <h3>Generic tokens</h3>
3547 <pre>{__fid}
3548 {__subid}
3549 {__pid}
3550 {__ip}
3551 {__anonymized_ip}
3552 {__uri}
3553 {__url}
3554 {__referrer}
3555 {__lang}
3556 {__locale}
3557 {__created}
3558 {__created_day}
3559 {__created_day_month_year}
3560 {__created_hour}
3561 {__submitted}
3562 {__submitted_day}
3563 {__submitted_day_month_year}
3564 {__submitted_hour}
3565 {__user_agent}
3566 {__platform}
3567 {__tentatives}
3568 {__submit_method}
3569 {__submitted_txt}
3570 {__submitted_html}
3571 {__submitted_json}
3572 {__autoreply}
3573 {__autoreply_email}
3574 {__confirmation_emails_message}
3575 {__review_submission_url}</pre>
3576 </div>
3577 EOT;
3578 // phpcs:enable PluginCheck.CodeAnalysis.Heredoc.NotAllowed, WordPress.Security.EscapeOutput.HeredocOutputNotEscaped
3579 do_action('accua_forms_print_tokens');
3580 }
3581
3582 /**
3583 * Get posts/pages for post-select fields using get_posts() for WPML compatibility.
3584 *
3585 * Uses WordPress get_posts() instead of direct SQL to ensure WPML and other
3586 * language plugins can filter results to current language automatically.
3587 *
3588 * Performance considerations:
3589 * - Results are cached using transients (5 minute TTL) to reduce database queries
3590 * - meta_key/meta_value queries are necessary for filtering by custom fields
3591 * - post__not_in is used only when exclude is explicitly requested by admin
3592 * - Default limit of 500 posts prevents runaway queries
3593 *
3594 * Hierarchy handling:
3595 * - child_of returns all descendants of the given post; exclude_tree removes a
3596 * post and all its descendants. Both are resolved to explicit ID lists via
3597 * accua_forms_get_post_descendant_ids() before querying, so they remain
3598 * correct with pagination and search.
3599 * - hierarchical only affects ordering (parents before children) and only when
3600 * the result set is complete and title-sorted; it never drops posts whose
3601 * parent is unavailable (e.g. published children of draft parents).
3602 *
3603 * @since 2.0.0-beta.29
3604 * @param string|array $args Query arguments (backward compatible with old function).
3605 * @return array Array of post objects.
3606 */
3607 function accua_get_pages($args = '') {
3608 // phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- These are function parameter defaults, not actual query execution.
3609 $defaults = array(
3610 'child_of' => 0,
3611 'sort_order' => 'ASC',
3612 'sort_column' => 'post_title',
3613 'hierarchical' => 1,
3614 'exclude' => array(),
3615 'include' => array(),
3616 'meta_key' => '',
3617 'meta_value' => '',
3618 'meta_value_lt' => '',
3619 'meta_value_gt' => '',
3620 'meta_value_le' => '',
3621 'meta_value_ge' => '',
3622 'meta_value_like' => '',
3623 'meta_value_format' => 'string',
3624 'authors' => '',
3625 'parent' => -1,
3626 'exclude_tree' => '',
3627 'number' => 500, // Default limit for performance
3628 'offset' => 0,
3629 'post_type' => 'page',
3630 'post_status' => 'publish',
3631 'suppress_filters' => false, // IMPORTANT: Allow WPML to filter by language
3632 's' => '', // Search term (new parameter for AJAX search)
3633 );
3634 // phpcs:enable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
3635
3636 $r = wp_parse_args($args, $defaults);
3637
3638 // Generate cache key based on arguments and current language
3639 $cache_key_data = $r;
3640 // Add current language to cache key for WPML/Polylang compatibility
3641 if (function_exists('pll_current_language')) {
3642 $cache_key_data['_lang'] = pll_current_language();
3643 } elseif (defined('ICL_LANGUAGE_CODE')) {
3644 $cache_key_data['_lang'] = ICL_LANGUAGE_CODE;
3645 }
3646 $cache_key = 'accua_pages_' . md5(wp_json_encode($cache_key_data));
3647
3648 // Check transient cache first (skip for search queries and random ordering)
3649 $use_cache = empty($r['s']) && $r['sort_column'] !== 'rand';
3650 if ($use_cache) {
3651 $cached = get_transient($cache_key);
3652 if ($cached !== false) {
3653 return $cached;
3654 }
3655 }
3656
3657 // Validate post type
3658 $post_type = $r['post_type'];
3659 if (!is_array($post_type)) {
3660 $post_type = array_map('trim', explode(',', $post_type));
3661 }
3662 $valid_post_types = get_post_types();
3663 $post_type = array_filter($post_type, function($pt) use ($valid_post_types) {
3664 return in_array($pt, $valid_post_types, true);
3665 });
3666 if (empty($post_type)) {
3667 return array();
3668 }
3669
3670 // Validate post status
3671 $post_status = $r['post_status'];
3672 if (!is_array($post_status)) {
3673 $post_status = array_map('trim', explode(',', $post_status));
3674 }
3675 $valid_statuses = get_post_stati();
3676 $post_status = array_filter($post_status, function($ps) use ($valid_statuses) {
3677 return in_array($ps, $valid_statuses, true);
3678 });
3679 if (empty($post_status)) {
3680 $post_status = array('publish');
3681 }
3682
3683 // Map sort_column to orderby
3684 $orderby_map = array(
3685 'post_title' => 'title',
3686 'title' => 'title',
3687 'post_date' => 'date',
3688 'date' => 'date',
3689 'post_modified' => 'modified',
3690 'modified' => 'modified',
3691 'menu_order' => 'menu_order',
3692 'post_name' => 'name',
3693 'name' => 'name',
3694 'post_parent' => 'parent',
3695 'parent' => 'parent',
3696 'ID' => 'ID',
3697 'rand' => 'rand',
3698 'comment_count' => 'comment_count',
3699 'post_author' => 'author',
3700 'author' => 'author',
3701 );
3702 $sort_column = $r['sort_column'];
3703 $orderby = isset($orderby_map[$sort_column]) ? $orderby_map[$sort_column] : 'title';
3704
3705 // Build get_posts arguments
3706 $query_args = array(
3707 'post_type' => $post_type,
3708 'post_status' => $post_status,
3709 'orderby' => $orderby,
3710 'order' => strtoupper($r['sort_order']) === 'DESC' ? 'DESC' : 'ASC',
3711 'posts_per_page' => !empty($r['number']) ? (int) $r['number'] : 500,
3712 'offset' => (int) $r['offset'],
3713 'suppress_filters' => (bool) $r['suppress_filters'],
3714 );
3715
3716 // Search term
3717 if (!empty($r['s'])) {
3718 $query_args['s'] = sanitize_text_field($r['s']);
3719 }
3720
3721 // Include specific posts (overrides other filters)
3722 if (!empty($r['include'])) {
3723 $include = wp_parse_id_list($r['include']);
3724 if (!empty($include)) {
3725 $query_args['post__in'] = $include;
3726 $query_args['orderby'] = 'post__in'; // Preserve include order
3727 }
3728 } else {
3729 // Exclude posts - only used when admin explicitly configures exclusions.
3730 // exclude_tree also removes all descendants of the given post, resolved
3731 // against the full tree so it works with pagination and search.
3732 $exclude_ids = array();
3733 if (!empty($r['exclude'])) {
3734 $exclude_ids = wp_parse_id_list($r['exclude']);
3735 }
3736 if (!empty($r['exclude_tree'])) {
3737 $exclude_tree = (int) $r['exclude_tree'];
3738 $exclude_ids = array_merge($exclude_ids, array($exclude_tree), accua_forms_get_post_descendant_ids($exclude_tree, $post_type));
3739 }
3740
3741 // Child of: restrict to all descendants of the given post, like core get_pages().
3742 // Resolved to an explicit ID list so it stays correct with pagination and search.
3743 if (!empty($r['child_of'])) {
3744 $descendant_ids = accua_forms_get_post_descendant_ids((int) $r['child_of'], $post_type);
3745 // post__in cannot be combined with post__not_in, so exclusions are applied to the list itself.
3746 $descendant_ids = array_values(array_diff($descendant_ids, $exclude_ids));
3747 $query_args['post__in'] = !empty($descendant_ids) ? $descendant_ids : array(0);
3748 } elseif (!empty($exclude_ids)) {
3749 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Exclusion is an optional admin-configured feature, not default behavior.
3750 $query_args['post__not_in'] = $exclude_ids;
3751 }
3752
3753 // Parent filter (direct children only)
3754 if ((int) $r['parent'] >= 0) {
3755 $query_args['post_parent'] = (int) $r['parent'];
3756 }
3757
3758 // Authors filter
3759 if (!empty($r['authors'])) {
3760 $author_ids = array();
3761 $post_authors = preg_split('/[\s,]+/', $r['authors']);
3762 foreach ($post_authors as $post_author) {
3763 $post_author = trim($post_author);
3764 if (empty($post_author)) {
3765 continue;
3766 }
3767 if (is_numeric($post_author)) {
3768 $author_ids[] = (int) $post_author;
3769 } else {
3770 $user = get_user_by('login', $post_author);
3771 if ($user && !empty($user->ID)) {
3772 $author_ids[] = $user->ID;
3773 }
3774 }
3775 }
3776 if (!empty($author_ids)) {
3777 $query_args['author__in'] = $author_ids;
3778 }
3779 }
3780
3781 // Build meta_query for advanced meta comparisons
3782 $meta_query = array();
3783
3784 // Standard meta_key/meta_value - used for filtering posts by custom field.
3785 // This is an optional admin-configured feature for advanced post filtering.
3786 if (!empty($r['meta_key'])) {
3787 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Required for custom field filtering feature.
3788 $query_args['meta_key'] = stripslashes($r['meta_key']);
3789 if (!empty($r['meta_value'])) {
3790 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Required for custom field filtering feature.
3791 $query_args['meta_value'] = stripslashes($r['meta_value']);
3792 }
3793 }
3794
3795 // Advanced meta comparisons (lt, gt, le, ge, like)
3796 if (!empty($r['meta_key']) && (
3797 !empty($r['meta_value_lt']) || !empty($r['meta_value_gt']) ||
3798 !empty($r['meta_value_le']) || !empty($r['meta_value_ge']) ||
3799 !empty($r['meta_value_like'])
3800 )) {
3801 $meta_key = stripslashes($r['meta_key']);
3802 $meta_type = 'CHAR';
3803 switch ($r['meta_value_format']) {
3804 case 'int':
3805 $meta_type = 'NUMERIC';
3806 break;
3807 case 'float':
3808 $meta_type = 'DECIMAL';
3809 break;
3810 case 'timestamp':
3811 $meta_type = 'DATETIME';
3812 break;
3813 }
3814
3815 if (!empty($r['meta_value_lt'])) {
3816 $value = stripslashes($r['meta_value_lt']);
3817 if ($r['meta_value_format'] === 'timestamp') {
3818 $value = gmdate('Y-m-d H:i:s', strtotime($value));
3819 }
3820 $meta_query[] = array(
3821 'key' => $meta_key,
3822 'value' => $value,
3823 'compare' => '<',
3824 'type' => $meta_type,
3825 );
3826 }
3827 if (!empty($r['meta_value_gt'])) {
3828 $value = stripslashes($r['meta_value_gt']);
3829 if ($r['meta_value_format'] === 'timestamp') {
3830 $value = gmdate('Y-m-d H:i:s', strtotime($value));
3831 }
3832 $meta_query[] = array(
3833 'key' => $meta_key,
3834 'value' => $value,
3835 'compare' => '>',
3836 'type' => $meta_type,
3837 );
3838 }
3839 if (!empty($r['meta_value_le'])) {
3840 $value = stripslashes($r['meta_value_le']);
3841 if ($r['meta_value_format'] === 'timestamp') {
3842 $value = gmdate('Y-m-d H:i:s', strtotime($value));
3843 }
3844 $meta_query[] = array(
3845 'key' => $meta_key,
3846 'value' => $value,
3847 'compare' => '<=',
3848 'type' => $meta_type,
3849 );
3850 }
3851 if (!empty($r['meta_value_ge'])) {
3852 $value = stripslashes($r['meta_value_ge']);
3853 if ($r['meta_value_format'] === 'timestamp') {
3854 $value = gmdate('Y-m-d H:i:s', strtotime($value));
3855 }
3856 $meta_query[] = array(
3857 'key' => $meta_key,
3858 'value' => $value,
3859 'compare' => '>=',
3860 'type' => $meta_type,
3861 );
3862 }
3863 if (!empty($r['meta_value_like'])) {
3864 $meta_query[] = array(
3865 'key' => $meta_key,
3866 'value' => stripslashes($r['meta_value_like']),
3867 'compare' => 'LIKE',
3868 );
3869 }
3870
3871 if (!empty($meta_query)) {
3872 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required for advanced meta comparison operators (lt, gt, like, etc.).
3873 $query_args['meta_query'] = $meta_query;
3874 // Remove simple meta_value if we're using meta_query
3875 unset($query_args['meta_value']);
3876 }
3877 }
3878 }
3879
3880 // Get posts using WordPress function (WPML automatically filters by current language)
3881 $pages = get_posts($query_args);
3882
3883 if (empty($pages)) {
3884 // Cache empty results too (5 minutes)
3885 if ($use_cache) {
3886 set_transient($cache_key, array(), 5 * MINUTE_IN_SECONDS);
3887 }
3888 /**
3889 * Filters the list of pages retrieved from accua_get_pages.
3890 *
3891 * @since 2.0.0-beta.29
3892 *
3893 * @param array $pages List of page objects.
3894 * @param array $r Arguments passed to accua_get_pages.
3895 */
3896 return apply_filters('accua_forms_get_pages', array(), $r);
3897 }
3898
3899 // Hierarchical (tree) ordering: list parents before their children, like core
3900 // get_pages(). Only applied when the result set is complete (no search, no
3901 // offset, not truncated by the limit) and title-sorted — reordering a paginated
3902 // or filtered slice would drop children whose parent is not in the same slice.
3903 if ($r['hierarchical'] && empty($r['s']) && (int) $r['offset'] === 0
3904 && count($pages) < (int) $query_args['posts_per_page']
3905 && $orderby === 'title'
3906 && function_exists('get_page_children')) {
3907 $tree_ordered = get_page_children((int) $r['child_of'], $pages);
3908 if (count($tree_ordered) < count($pages)) {
3909 // Posts whose ancestors are not part of the result set (e.g. published
3910 // children of a draft parent) go at the end instead of being dropped.
3911 $tree_ids = array();
3912 foreach ($tree_ordered as $page) {
3913 $tree_ids[$page->ID] = true;
3914 }
3915 foreach ($pages as $page) {
3916 if (!isset($tree_ids[$page->ID])) {
3917 $tree_ordered[] = $page;
3918 }
3919 }
3920 }
3921 $pages = $tree_ordered;
3922 }
3923
3924 // Cache results for 5 minutes to improve performance
3925 if ($use_cache) {
3926 set_transient($cache_key, $pages, 5 * MINUTE_IN_SECONDS);
3927 }
3928
3929 /** This filter is documented above */
3930 return apply_filters('accua_forms_get_pages', $pages, $r);
3931 }
3932
3933 /**
3934 * Get the IDs of all descendants of a post by traversing the post_parent tree.
3935 *
3936 * Used to resolve the child_of and exclude_tree arguments of accua_get_pages()
3937 * to an explicit ID list, so the main query stays correct with pagination and
3938 * search. Traverses posts of any status so that e.g. a published grandchild of
3939 * a draft child is still found (the main query applies its own status filter).
3940 *
3941 * @since 2.2.27
3942 * @param int $parent_id Root post ID (not included in the result).
3943 * @param string|array $post_type Post type(s) to traverse.
3944 * @return int[] Descendant post IDs.
3945 */
3946 function accua_forms_get_post_descendant_ids($parent_id, $post_type) {
3947 $descendant_ids = array();
3948 $level = array((int) $parent_id);
3949 // Depth guard: hierarchies deeper than 25 levels are treated as data corruption (parent loops).
3950 for ($depth = 0; $depth < 25 && !empty($level); $depth++) {
3951 $children = get_posts(array(
3952 'post_type' => $post_type,
3953 'post_status' => 'any',
3954 'post_parent__in' => $level,
3955 'posts_per_page' => -1,
3956 'fields' => 'ids',
3957 'suppress_filters' => true, // Structural traversal: do not let language plugins hide ancestors.
3958 'orderby' => 'ID',
3959 'order' => 'ASC',
3960 ));
3961 $children = array_map('intval', array_diff($children, $descendant_ids, array((int) $parent_id)));
3962 $descendant_ids = array_merge($descendant_ids, $children);
3963 $level = $children;
3964 }
3965 return $descendant_ids;
3966 }
3967
3968 // phpcs:disable WordPress.DB.DirectDatabaseQuery
3969 function accua_forms_trash_submission($id_sub){
3970 global $wpdb;
3971 return $wpdb->query($wpdb->prepare("UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_status = -1 WHERE afs_id = %d", $id_sub)) !== FALSE;
3972 }
3973
3974 function accua_forms_restore_submission($id_sub){
3975 global $wpdb;
3976 return $wpdb->query($wpdb->prepare("UPDATE `{$wpdb->prefix}accua_forms_submissions` SET afs_status = 0 WHERE afs_id = %d", $id_sub)) !== FALSE;
3977 }
3978 // phpcs:enable WordPress.DB.DirectDatabaseQuery
3979
3980 /**
3981 * Clear accua_get_pages cache when posts are modified.
3982 *
3983 * Called when posts are created, updated, deleted, or have status changed.
3984 * This ensures that post-select dropdowns always show fresh data.
3985 *
3986 * @since 2.0.0-beta.29
3987 * @param int $post_id Post ID that was modified.
3988 */
3989 function accua_forms_clear_pages_cache($post_id = 0) {
3990 global $wpdb;
3991 // Delete all transients that start with 'accua_pages_'
3992 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query required to delete transients by prefix, and we're clearing cache not reading data.
3993 $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_accua_pages_%' OR option_name LIKE '_transient_timeout_accua_pages_%'");
3994 }
3995 // Clear cache when posts are modified
3996 add_action('save_post', 'accua_forms_clear_pages_cache');
3997 add_action('delete_post', 'accua_forms_clear_pages_cache');
3998 add_action('trash_post', 'accua_forms_clear_pages_cache');
3999 add_action('untrash_post', 'accua_forms_clear_pages_cache');
4000
4001 function accua_forms_get_lead_statuses() {
4002 static $statuses = NULL;
4003 if ($statuses === NULL) {
4004 $statuses = array(
4005 0 => __('Undefined', 'contact-forms'),
4006 -1 => __('Spam', 'contact-forms'),
4007 1 => __('Job Candidate', 'contact-forms'),
4008 2 => __('Lead', 'contact-forms'),
4009 3 => __('Prospect', 'contact-forms'),
4010 4 => __('Opportunity', 'contact-forms'),
4011 5 => __('Customer', 'contact-forms'),
4012 6 => __('Supplier', 'contact-forms'),
4013 7 => __('Other', 'contact-forms'),
4014 );
4015 }
4016 return $statuses;
4017 }
4018
4019 /**
4020 * Get lead statuses help text (used in toggletip and Help Tab).
4021 */
4022 function accua_forms_get_lead_statuses_help() {
4023 return '<strong>' . esc_html__( 'Spam', 'contact-forms' ) . '</strong> – ' . esc_html__( 'All submissions that can be discarded immediately, including submission tests', 'contact-forms' ) . '<br>'
4024 . '<strong>' . esc_html__( 'Job Candidate', 'contact-forms' ) . '</strong> – ' . esc_html__( 'Includes spontaneous and specific job applications', 'contact-forms' ) . '<br>'
4025 . '<strong>' . esc_html__( 'Lead', 'contact-forms' ) . '</strong> – ' . esc_html__( 'Unclear (general info request)', 'contact-forms' ) . '<br>'
4026 . '<strong>' . esc_html__( 'Prospect', 'contact-forms' ) . '</strong> – ' . esc_html__( 'A qualified lead passed to Sales', 'contact-forms' ) . '<br>'
4027 . '<strong>' . esc_html__( 'Opportunity', 'contact-forms' ) . '</strong> – ' . esc_html__( 'Quote / Pricing request that must be followed up', 'contact-forms' ) . '<br>'
4028 . '<strong>' . esc_html__( 'Customer', 'contact-forms' ) . '</strong> – ' . esc_html__( 'Has already purchased', 'contact-forms' ) . '<br>'
4029 . '<strong>' . esc_html__( 'Supplier', 'contact-forms' ) . '</strong> – ' . esc_html__( 'Contact whose role is or can only be supplier of goods and services', 'contact-forms' ) . '<br>'
4030 . '<strong>' . esc_html__( 'Other', 'contact-forms' ) . '</strong> – ' . esc_html__( 'Contact is valid but not within lead generation', 'contact-forms' );
4031 }
4032
4033 function accua_forms_select_lead_status($subid, $original_lead_status) {
4034 $subid = absint( $subid );
4035 $original_lead_status = absint( $original_lead_status );
4036 $nonce = esc_attr( wp_json_encode( wp_create_nonce( "set_lead_status_$subid" ) ) );
4037 $ret = '<select onchange="accua_forms_set_lead_status(this, ' . $subid . ', ' . $nonce . ', ' . $original_lead_status . ')">';
4038 $statuses = accua_forms_get_lead_statuses();
4039 foreach ($statuses as $k => $l) {
4040 $selected = ( (int) $k === $original_lead_status ) ? ' selected="selected" ' : '';
4041 $ret .= '<option value="' . esc_attr( $k ) . '"' . $selected . '>' . esc_html( $l ) . '</option>';
4042 }
4043 $ret .= '</select>';
4044 return $ret;
4045 }
4046
4047 add_action( 'wp_ajax_accua-forms-set-lead-status' , 'accua_forms_set_lead_status');
4048 function accua_forms_set_lead_status() {
4049 if (!current_user_can('manage_options')){
4050 wp_die(0, 403);
4051 }
4052 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification happens after subid is extracted via check_ajax_referer()
4053 $post = $_POST + array(
4054 'subid' => 0,
4055 'lead_status' => 0,
4056 );
4057 $subid = (int) $post['subid'];
4058
4059 if ($subid) {
4060 check_ajax_referer("set_lead_status_$subid", '_nonce_set_lead_status');
4061 $lead_status = (int) $post['lead_status'];
4062 $statuses = accua_forms_get_lead_statuses();
4063 if (isset($statuses[$lead_status])) {
4064 global $wpdb;
4065 // phpcs:disable WordPress.DB.DirectDatabaseQuery
4066 $ret = $wpdb->update(
4067 "{$wpdb->prefix}accua_forms_submissions",
4068 array('afs_lead_status' => $lead_status),
4069 array('afs_id' => $subid),
4070 array('%d'),
4071 array('%d')
4072 );
4073 // phpcs:enable WordPress.DB.DirectDatabaseQuery
4074 if ($ret !== FALSE) {
4075 wp_die(1);
4076 }
4077 }
4078 }
4079 wp_die(0, 500);
4080 }
4081
4082 add_action( 'wp_ajax_accua-forms-add-note', 'accua_forms_ajax_add_note' );
4083 function accua_forms_ajax_add_note() {
4084 $sub_id = isset( $_POST['subid'] ) ? (int) $_POST['subid'] : 0;
4085 $text = isset( $_POST['text'] ) ? sanitize_textarea_field( wp_unslash( $_POST['text'] ) ) : '';
4086
4087 if ( ! $sub_id || ! $text ) {
4088 wp_send_json_error();
4089 }
4090 check_ajax_referer( "submission_{$sub_id}_note_add", '_nonce' );
4091 if ( ! current_user_can( 'manage_options' ) ) {
4092 wp_send_json_error();
4093 }
4094
4095 require_once __DIR__ . '/admin/single-submission.php';
4096 $result = accua_forms_add_submission_note( $sub_id, $text );
4097 if ( ! $result ) {
4098 wp_send_json_error();
4099 }
4100 $result['del_nonce'] = wp_create_nonce( "submission_{$sub_id}_note_del" );
4101 wp_send_json_success( $result );
4102 }
4103
4104 add_action( 'wp_ajax_accua-forms-delete-note', 'accua_forms_ajax_delete_note' );
4105 function accua_forms_ajax_delete_note() {
4106 $sub_id = isset( $_POST['subid'] ) ? (int) $_POST['subid'] : 0;
4107 $date = isset( $_POST['date'] ) ? sanitize_text_field( wp_unslash( $_POST['date'] ) ) : '';
4108
4109 if ( ! $sub_id || ! $date ) {
4110 wp_send_json_error();
4111 }
4112 check_ajax_referer( "submission_{$sub_id}_note_del", '_nonce' );
4113 if ( ! current_user_can( 'manage_options' ) ) {
4114 wp_send_json_error();
4115 }
4116
4117 require_once __DIR__ . '/admin/single-submission.php';
4118 if ( ! accua_forms_delete_submission_note( $sub_id, $date ) ) {
4119 wp_send_json_error();
4120 }
4121 wp_send_json_success();
4122 }