PluginProbe
Contact Forms by Cimatti / trunk
Contact Forms by Cimatti vtrunk
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 trunk, at accua-forms.php

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