PluginProbe
Contact Forms by Cimatti / 1.9.2
Contact Forms by Cimatti v1.9.2
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
← All changes | AccuaForm.php +93 -1245 2.3.61.9.2 View file →
@@ -1,8 +1,5 @@
1 1 <?php
2 -if ( ! defined( 'ABSPATH' ) ) exit;
3 -
4 -// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped, WordPress.Security.EscapeOutput.HeredocOutputNotEscaped, PluginCheck.CodeAnalysis.Heredoc.NotAllowed, WordPress.PHP.DevelopmentFunctions.error_log_trigger_error, WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.WP.AlternativeFunctions.rename_rename -- PFBC Form extension class with controlled HTML output, deprecation notices, debug logging, file operations
5 2 class AccuaForm extends Form {
6 3 protected static $submitted = null;
7 4 protected static $valid = null;
8 5 protected static $submittedID = null;
@@ -10,15 +7,9 @@
10 7 protected static $submittedForm = null;
11 8 protected static $submittedFormUsed = false;
12 9 protected static $submittedData = null;
13 10 protected static $rawData = null;
14 - /**
15 - * Per-form submitted messages array.
16 - * Keys are form IDs, values are message strings.
17 - * This allows multiple forms on the same page to have separate messages.
18 - * @var array<string, string>
19 - */
20 - protected static $submittedMessages = array();
11 + protected static $submittedMessages = '';
21 12
22 13 protected $formID = null;
23 14 protected $buildID = null;
24 15 protected $validate_functions = array();
@@ -29,9 +20,8 @@
29 20 protected $locale = null;
30 21 protected $language = null;
31 22 protected $files = array();
32 23 protected $ga_track = array();
33 - protected $gads_conversion_tracking_code = '';
34 24
35 25 protected $original_locale = null;
36 26 protected $original_language = null;
37 27 protected $original_l10n = null;
@@ -51,12 +41,10 @@
51 41 unset($GLOBALS['wp_locale']);
52 42 unset($GLOBALS['l10n']);
53 43 $GLOBALS['l10n'] = array();
54 44
55 - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- qTranslate plugin integration
56 45 $GLOBALS['q_config']['language'] = $this->language;
57 46 load_default_textdomain();
58 - // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- Intentionally forces language at runtime for qTranslate email delivery
59 47 load_plugin_textdomain( 'contact-forms', false, ACCUA_FORM_API_PLUGIN_TEXTDOMAIN_PATH);
60 48 require_once( ABSPATH . WPINC . '/locale.php' );
61 49 $GLOBALS['wp_locale'] = new WP_Locale();
62 50 $GLOBALS['wp_locale']->register_globals();
@@ -70,9 +58,8 @@
70 58 if ($this->forced_language) {
71 59 unset($GLOBALS['wp_locale']);
72 60 unset($GLOBALS['l10n']);
73 61
74 - // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- qTranslate plugin integration
75 62 $GLOBALS['q_config']['language'] = $this->original_language;
76 63 $GLOBALS['l10n'] =& $this->original_l10n;
77 64 $GLOBALS['wp_locale'] =& $this->original_locale;
78 65 if ($GLOBALS['wp_locale']) {
@@ -146,10 +133,8 @@
146 133 error_log("headers already sent at {$file}:{$line}");
147 134 }
148 135 }
149 136 $sessionid = session_id();
150 - // Close the session immediately after getting the ID to prevent REST API interference
151 - session_write_close();
152 137 }
153 138 return $sessionid;
154 139 }
155 140
@@ -155,17 +140,13 @@
155 140
156 141 public static function getBaseURL() {
157 142 static $ret = null;
158 143 if ($ret === null) {
159 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Building internal URL from server variables
160 144 $s = empty($_SERVER["HTTPS"]) ? '' : (($_SERVER["HTTPS"] == "on") ? "s" : "");
161 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Building internal URL from server variables
162 - $sp = isset($_SERVER["SERVER_PROTOCOL"]) ? strtolower(sanitize_text_field(wp_unslash($_SERVER["SERVER_PROTOCOL"]))) : 'http/1.1';
145 + $sp = strtolower($_SERVER["SERVER_PROTOCOL"]);
163 146 $protocol = substr($sp, 0, strpos($sp, "/")) . $s;
164 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Building internal URL from server variables
165 - $port = (!isset($_SERVER["SERVER_PORT"]) || $_SERVER["SERVER_PORT"] == "80") ? "" : (":" . absint($_SERVER["SERVER_PORT"]));
166 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Building internal URL from server variables
167 - $ret = $protocol . "://" . (isset($_SERVER['SERVER_NAME']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_NAME'])) : 'localhost') . $port;
147 + $port = ($_SERVER["SERVER_PORT"] == "80") ? "" : (":".$_SERVER["SERVER_PORT"]);
148 + $ret = $protocol . "://" . $_SERVER['SERVER_NAME'] . $port;
168 149 }
169 150 return $ret;
170 151 }
171 152
@@ -204,9 +185,8 @@
204 185 'layout' => 'sidebyside',
205 186 'title' => '',
206 187 'track_submit' => false,
207 188 'track_fields' => false,
208 - 'gads_conversion_tracking_code' => '',
209 189 );
210 190 if (is_array($params)) {
211 191 $params += $predefined_params;
212 192 $width = $params['width'];
@@ -214,11 +194,8 @@
214 194 $width = $params;
215 195 $params = $predefined_params;
216 196 $params['width'] = $width;
217 197 }
218 - // Always initialize the property to avoid undefined property warnings
219 - $this->gads_conversion_tracking_code = $params['gads_conversion_tracking_code'];
220 -
221 198 $class = 'accua-form ' . $id;
222 199 switch ($params['layout']) {
223 200 case 'toplabel':
224 201 $this->view = new AccuaForm_View_Standard();
@@ -223,14 +200,8 @@
223 200 case 'toplabel':
224 201 $this->view = new AccuaForm_View_Standard();
225 202 $class .= ' accua-form-view-standard';
226 203 break;
227 - case 'inlinelabel':
228 - $this->view = new AccuaForm_View_InlineLabel();
229 - $class .= ' accua-form-view-inlinelabel';
230 - // Prevent PFBC from adding inline width styles for inline label layout
231 - $width = '';
232 - break;
233 204 case 'sidebyside':
234 205 default:
235 206 $this->view = new AccuaForm_View_SideBySide(
236 207 '19',
@@ -241,11 +212,9 @@
241 212 $this->error = new AccuaForm_Error_Standard(array(
242 213 'errorfound' => '',
243 214 'errorsfound' => '',
244 215 ));
245 - // For inline label view, we don't set default width
246 - // The CSS handles all widths with width: 100%
247 - if ($width === "" && $params['layout'] !== 'inlinelabel'){
216 + if ($width === ""){
248 217 $width = "100%";
249 218 }
250 219 $this->attributes = array(
251 220 'class' => $class,
@@ -277,10 +246,11 @@
277 246 * - Quando ricevi il form, decripta _AccuaForm_serialized e controlla che sia valido
278 247 *
279 248 * */
280 249
250 + //$this->addElement(new Element_Hidden('PHPSESSID', self::sessionID()));
281 251 $this->prevent = array('jQuery', 'jQueryUI', 'jQueryUIButtons', 'focus', 'style');
282 - $this->configure(array('action' => '#'));
252 + $this->configure(array('action' => ''));
283 253
284 254 if (function_exists('qtrans_getLanguage')) {
285 255 $this->language = qtrans_getLanguage();
286 256 $this->locale = $GLOBALS['q_config']['locale'][$this->language];
@@ -292,26 +262,18 @@
292 262
293 263 global $post;
294 264 $pid = empty($post->ID) ? 0 : $post->ID;
295 265
296 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- URL stored for logging, escaped on output
297 - $uri = isset($GLOBALS['q_config']['url_info']['original_url']) ? $GLOBALS['q_config']['url_info']['original_url'] : (isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '');
266 + $uri = isset($GLOBALS['q_config']['url_info']['original_url']) ? $GLOBALS['q_config']['url_info']['original_url'] : $_SERVER['REQUEST_URI'];
298 267
299 268 $url = self::getBaseURL() . $uri ;
300 - // Set the form action to the current page URL so non-AJAX forms submit cleanly
301 - // (AJAX forms override this later in the JS to admin-ajax.php)
302 - $this->configure(array('action' => $url));
303 269
304 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Referrer stored for logging, escaped on output
305 - $referrer = isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : '';
270 + $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
306 271
307 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- IP stored for logging, escaped on output
308 - $remote_addr = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
309 -
310 272 $this->stats = array(
311 273 'pid' => $pid,
312 - 'ip' => $remote_addr, //updated after submit
313 - 'original_ip' => $remote_addr, //unreliable if using a static page caching system
274 + 'ip' => $_SERVER['REMOTE_ADDR'], //updated after submit
275 + 'original_ip' => $_SERVER['REMOTE_ADDR'], //unreliable if using a static page caching system
314 276 'uri' => $uri,
315 277 'url' => $url,
316 278 'referrer' => $referrer, //updated after submit using javascript
317 279 'original_referrer' => $referrer, //unreliable if using a static page caching system
@@ -335,44 +297,41 @@
335 297 }
336 298 self::$submitted = false;
337 299 $sessid = self::sessionID();
338 300
339 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- Checking request method for form handling
340 - $request_method = isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) : 'GET';
341 - if ($request_method === 'POST') {
301 + if($_SERVER['REQUEST_METHOD'] == 'POST') {
342 302 self::$rawData = stripslashes_deep($_POST);
343 303 } else {
344 304 self::$rawData = stripslashes_deep($_GET);
345 305 }
346 306
347 - // Ensure basic fields for nonce verification are present.
348 - if (empty(self::$rawData['_AccuaForm_buildID']) || empty(self::$rawData['_AccuaForm_wpnonce'])) {
307 + if (empty(self::$rawData['_AccuaForm_ID']) || empty(self::$rawData['_AccuaForm_buildID']) || empty(self::$rawData['_AccuaForm_wpnonce']) /* || empty(self::$rawData['PHPSESSID']) */
308 + || empty(self::$rawData['_AccuaForm_hash']) || empty(self::$rawData['_AccuaForm_iv']) || empty(self::$rawData['_AccuaForm_data'])) {
349 309 return false;
350 310 }
351 311
352 - // Verify nonce for CSRF protection (moved earlier).
353 - // The $id variable (action for nonce) is self::$rawData['_AccuaForm_buildID'].
354 - $requested_buildID = self::$rawData['_AccuaForm_buildID'];
355 - if (!wp_verify_nonce(self::$rawData['_AccuaForm_wpnonce'], $requested_buildID)) {
312 + /*
313 + if (self::$rawData['PHPSESSID'] !== $sessid) {
356 314 return false;
357 315 }
316 + */
358 317
359 - // Now check for fields required for form recovery and other validations.
360 - if (empty(self::$rawData['_AccuaForm_ID'])
361 - || empty(self::$rawData['_AccuaForm_hash']) || empty(self::$rawData['_AccuaForm_iv']) || empty(self::$rawData['_AccuaForm_data'])) {
318 + $id = self::$rawData['_AccuaForm_buildID'];
319 + $form = self::wp_recover(self::$rawData['_AccuaForm_hash'], self::$rawData['_AccuaForm_iv'], self::$rawData['_AccuaForm_data']);
320 +
321 + if (empty($form)) {
362 322 return false;
363 323 }
364 324
365 - // $id was self::$rawData['_AccuaForm_buildID'], now using $requested_buildID for clarity.
366 - $form = self::wp_recover(self::$rawData['_AccuaForm_hash'], self::$rawData['_AccuaForm_iv'], self::$rawData['_AccuaForm_data']);
367 -
368 - if (empty($form)) {
325 + if ($form->formID !== self::$rawData['_AccuaForm_ID'] || $form->buildID !== $id ) {
369 326 return false;
370 327 }
371 328
372 - if ($form->formID !== self::$rawData['_AccuaForm_ID'] || $form->buildID !== $requested_buildID ) {
329 + /*
330 + if (!wp_verify_nonce(self::$rawData['_AccuaForm_wpnonce'], $id)) {
373 331 return false;
374 332 }
333 + */
375 334
376 335 /* check if already submitted from uuid and other parameters */
377 336 $already_submitted = false;
378 337 if (!empty(self::$rawData['_AccuaForm_jsuuid'])) {
@@ -377,20 +336,13 @@
377 336 $already_submitted = false;
378 337 if (!empty(self::$rawData['_AccuaForm_jsuuid'])) {
379 338 if (preg_match("/^[a-z0-9]{25}$/", self::$rawData['_AccuaForm_jsuuid'])) {
380 339 $already_submitted_data = get_transient('accuaformsub_'.self::$rawData['_AccuaForm_jsuuid']);
381 - if ($already_submitted_data && $already_submitted_data['buildID'] == $form->buildID) {
382 - // Only use cached result for successful submissions to prevent duplicate processing
383 - if (!empty($already_submitted_data['valid'])) {
384 - $form = $already_submitted_data['form'];
385 - self::$submittedMessages = $already_submitted_data['submittedMessages'];
386 - self::$valid = true;
387 - $already_submitted = true;
388 - } else {
389 - // For failed validations, delete the transient so form can be re-validated
390 - // This fixes the bug where correcting a field and resubmitting caused form to disappear
391 - delete_transient('accuaformsub_'.self::$rawData['_AccuaForm_jsuuid']);
392 - }
340 + if ($already_submitted_data && $already_submitted_data['buildID'] == $form->buildID) {
341 + $form = $already_submitted_data['form'];
342 + self::$submittedMessages = $already_submitted_data['submittedMessages'];
343 + self::$valid = true;
344 + $already_submitted = true;
393 345 }
394 346 } else {
395 347 return false;
396 348 }
@@ -397,10 +349,9 @@
397 349 }
398 350
399 351 if (!$already_submitted) {
400 352 $form->stats['submitted'] = time();
401 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- IP stored for logging, escaped on output
402 - $form->stats['ip'] = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
353 + $form->stats['ip'] = $_SERVER['REMOTE_ADDR'];
403 354 foreach (array('referrer', 'jsuuid', 'user_agent', 'platform', 'tentatives', 'submit_method') as $i){
404 355 $form->stats[$i] = isset(self::$rawData["_AccuaForm_{$i}"]) ? ((string)self::$rawData["_AccuaForm_{$i}"]) : '';
405 356 }
406 357 }
@@ -447,16 +398,14 @@
447 398 not $_GET or $_POST.*/
448 399 if($element instanceof AccuaForm_Element_File) {
449 400 if (!empty($form->files[$name]['name'])) {
450 401 $value = $form->files[$name]['name'];
451 - // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- File handling uses $_FILES, nonce verified earlier
452 - } else if ((!empty($_FILES[$name])) && (isset($_FILES[$name]['error']) && $_FILES[$name]['error'] != UPLOAD_ERR_NO_FILE)) {
402 + } else if ((!empty($_FILES[$name])) && ($_FILES[$name]['error'] != UPLOAD_ERR_NO_FILE)) {
453 403 //$file = $_FILES[$name];
454 404 //include_once( ABSPATH . '/wp-admin/includes/file.php' );
455 405 //$overrides = array( 'test_form' => false );
456 406 //$file = wp_handle_upload( $file, $overrides );
457 407
458 - // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- File array passed to WordPress handler
459 408 $file = $element->handle_upload($_FILES[$name]);
460 409 $value = $file['name'];
461 410
462 411 if (empty($file['errors'])) {
@@ -469,20 +418,17 @@
469 418 } else {
470 419 $value = null;
471 420 }
472 421 } else if ($element instanceof Element_File){
473 - // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Legacy file handling
474 - $value = isset($_FILES[$name]["name"]) ? sanitize_file_name($_FILES[$name]["name"]) : '';
422 + $value = $_FILES[$name]["name"];
475 423 } else if (isset(self::$rawData[$name])) {
476 424 $value = self::$rawData[$name];
477 425 if(is_array($value)) {
478 426 foreach($value as $key => $value_i) {
479 - // Sanitize null bytes to prevent mail() ValueError in PHP 8+
480 - $value[$key] = str_replace("\0", '', (string) $value_i);
427 + $value[$key] = (string) $value_i;
481 428 }
482 429 } else {
483 - // Sanitize null bytes to prevent mail() ValueError in PHP 8+
484 - $value = str_replace("\0", '', (string) $value);
430 + $value = (string) $value;
485 431 }
486 432 } else {
487 433 $value = null;
488 434 }
@@ -499,11 +445,12 @@
499 445 }
500 446 }
501 447
502 448 if($invalidFile) {
503 - // File cleanup is handled in handle_upload() when validation fails
504 - // The uploaded tmp file is already deleted by PHP after request completes
505 - unset($form->files[$name]);
449 + if (isset($file['tmp_name'])) {
450 + unlink($element->getDestPath . $file['tmp_name']);
451 + unset($form->files[$name]);
452 + }
506 453 } else if($name !== '') {
507 454 self::$submittedData[$name] = $value;
508 455 }
509 456 }
@@ -535,38 +482,28 @@
535 482 }
536 483 }
537 484 }
538 485
539 - /*Apply errors from session to form elements for accessibility (ARIA attributes).
540 - This must be done BEFORE session_write_close() is called, so errors persist in element objects.*/
541 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Session data for form error handling
542 - if (!$valid && !empty($_SESSION["pfbc"][$id]["errors"])) {
543 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Session data for form error handling
544 - $sessionErrors = $_SESSION["pfbc"][$id]["errors"];
545 - $elements = $form->getElements();
546 - if (!empty($elements)) {
547 - foreach ($elements as $element) {
548 - $name = $element->getName();
549 - if (substr($name, -2) == "[]")
550 - $name = substr($name, 0, -2);
551 -
552 - if (isset($sessionErrors[$name]) && is_array($sessionErrors[$name])) {
553 - $element->setErrors($sessionErrors[$name]);
554 - }
555 - }
556 - }
486 + /*If no validation errors were found, the form's session values are cleared.*/
487 + /*
488 + if($valid) {
489 + if($clearValues)
490 + self::clearValues($id);
491 + self::clearErrors($id);
557 492 }
493 + */
558 494
495 + //TODO: Should i save here?
496 + //$form->save();
497 + //$_SESSION["pfbc"][$id]["form"] = serialize($form);
498 +
559 499 $form->restore_language();
560 500
561 - /*Store form with errors in transient for accessibility on redirect.
562 - Both successful and failed submissions are stored so the form can be properly re-rendered with ARIA attributes.*/
563 - if (self::$rawData['_AccuaForm_jsuuid'] ) {
501 + if ($valid && self::$rawData['_AccuaForm_jsuuid'] ) {
564 502 set_transient('accuaformsub_'.self::$rawData['_AccuaForm_jsuuid'], array(
565 503 'buildID' => self::$submittedBuildID,
566 504 'form' => self::$submittedForm,
567 505 'submittedMessages' => self::$submittedMessages,
568 - 'valid' => $valid,
569 506 ), 86400);
570 507 }
571 508
572 509 return self::$valid = (bool)$valid;
@@ -685,11 +622,9 @@
685 622 '_AccuaForm_hash' => base64_encode($hash),
686 623 '_AccuaForm_iv' => base64_encode($iv),
687 624 '_AccuaForm_data' => base64_encode($data),
688 625 );
689 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Session data for form values
690 626 if (isset($_SESSION["pfbc"][$this->buildID]["values"])) {
691 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Session data for form values
692 627 $_SESSION["pfbc"][$this->buildID]["values"] = $ret + $_SESSION["pfbc"][$this->buildID]["values"];
693 628 }
694 629 $this->setValues($ret);
695 630 return $ret;
@@ -710,53 +645,18 @@
710 645 public function addSubmitFunction($function_name, $priority = 0) {
711 646 $this->submit_functions[$function_name] = $priority;
712 647 }
713 648
714 - /**
715 - * Get submitted messages for a specific form or all forms.
716 - * @param string|null $formId Optional form ID to get message for specific form
717 - * @return string|array If $formId provided, returns string message (or empty string). Otherwise returns all messages as array.
718 - */
719 - public static function getSubmittedMessages($formId = null){
720 - if ($formId !== null) {
721 - return isset(self::$submittedMessages[$formId]) ? self::$submittedMessages[$formId] : '';
722 - }
723 - // Backwards compatibility: if old code expects a string, join all messages
724 - if (is_array(self::$submittedMessages)) {
725 - return implode('', self::$submittedMessages);
726 - }
649 + public static function getSubmittedMessages(){
727 650 return self::$submittedMessages;
728 651 }
729 652
730 - /**
731 - * Set submitted message for a specific form.
732 - * @param string $msg The message content
733 - * @param string|null $formId Optional form ID. If null, uses currently submitted form ID.
734 - * @return string The message that was set
735 - */
736 - public static function setSubmittedMessages($msg, $formId = null){
737 - if ($formId === null) {
738 - $formId = self::$submittedID ?: '__default__';
739 - }
740 - self::$submittedMessages[$formId] = $msg;
741 - return $msg;
653 + public static function setSubmittedMessages($msg){
654 + return self::$submittedMessages = $msg;
742 655 }
743 656
744 - /**
745 - * Append to submitted message for a specific form.
746 - * @param string $msg The message content to append
747 - * @param string|null $formId Optional form ID. If null, uses currently submitted form ID.
748 - * @return string The full message after appending
749 - */
750 - public static function appendSubmittedmessages($msg, $formId = null){
751 - if ($formId === null) {
752 - $formId = self::$submittedID ?: '__default__';
753 - }
754 - if (!isset(self::$submittedMessages[$formId])) {
755 - self::$submittedMessages[$formId] = '';
756 - }
757 - self::$submittedMessages[$formId] .= $msg;
758 - return self::$submittedMessages[$formId];
657 + public static function appendSubmittedmessages($msg){
658 + return self::$submittedMessages .= $msg;
759 659 }
760 660
761 661 protected static function get_anchor_id($id) {
762 662 static $used_anchor_id = array();
@@ -771,38 +671,23 @@
771 671 $id = $id . '-' . $used_anchor_id[$id];
772 672 }
773 673 return $id;
774 674 }
675 +
775 676 public function render($returnHTML = false) {
776 677 $this->wp_save();
777 678 if($returnHTML) {
778 679 ob_start();
779 680 }
780 -
781 681 parent::render(false);
782 682 if (!empty($this->accua_ajax)) {
783 683 $ajax_url = _accua_forms_json_encode(admin_url('admin-ajax.php?action=accua_form_submit'));
784 - $submit_fail_message = _accua_forms_json_encode('<li>'.__('Form submission failed. Please try again.', 'contact-forms').'</li>');
785 -
786 - /* translators: %s is the field name/label */
787 - $required_message_template = _accua_forms_json_encode(__('%s is a required field', 'contact-forms'));
788 - /* translators: %s is the field name/label */
789 - $valid_mail_message_template = _accua_forms_json_encode(__('%s: please enter a valid email address', 'contact-forms'));
790 - /* translators: %s is the field name/label */
791 - $valid_phone_message_template = _accua_forms_json_encode(__('%s: please enter a valid phone number (e.g. +39 333 1234567)', 'contact-forms'));
792 - $error_summary_header = _accua_forms_json_encode(__('Check the following fields to continue:', 'contact-forms'));
793 - $error_type_required = _accua_forms_json_encode(__('required field', 'contact-forms'));
794 - $error_type_invalid_email = _accua_forms_json_encode(__('invalid email address', 'contact-forms'));
795 - $error_type_invalid_phone = _accua_forms_json_encode(__('invalid phone number', 'contact-forms'));
796 - $success_message = _accua_forms_json_encode(__('All fields are valid. Ready to submit!', 'contact-forms'));
797 - $sending_message = _accua_forms_json_encode(__('Sending...', 'contact-forms'));
798 - $loading_summary_message = _accua_forms_json_encode(__('Submitting your form, please wait...', 'contact-forms'));
684 + $submit_fail_message = _accua_forms_json_encode('<li>'.__('Unable to submit the form, please retry', 'contact-forms').'</li>');
685 + $required_message = _accua_forms_json_encode(__('Please fill in all required fields', 'contact-forms'));
686 + $valid_mail_message = _accua_forms_json_encode(__('You have to enter a valid email address where required', 'contact-forms'));
799 687 $js_buildid = preg_replace('/[^a-zA-Z0-9_]/m','_',$this->buildID);
800 688 $formid = $this->getId();
801 - // $hostname = _accua_forms_json_encode($_SERVER['SERVER_NAME']); - rimosso perché non affidabile con proxy reversi e CDN
802 - // Extract hostname from ajax_url for proper cross-domain detection
803 - $ajax_url_parsed = wp_parse_url(admin_url('admin-ajax.php?action=accua_form_submit'));
804 - $ajax_hostname = _accua_forms_json_encode($ajax_url_parsed['host']);
689 + $hostname = _accua_forms_json_encode($_SERVER['SERVER_NAME']);
805 690
806 691 $post_url = _accua_forms_json_encode($this->stats['url']);
807 692 $js_ga_track = _accua_forms_json_encode($this->ga_track);
808 693
@@ -818,28 +703,18 @@
818 703 var _handle_ajax_submit_response_{$js_buildid} = function() {}
819 704
820 705 jQuery(function($) {
821 706 var thisform = $("#{$this->buildID}");
822 - var ajax_enabled = {$ajax_hostname} == location.hostname ;
707 + var ajax_enabled = {$hostname} == location.hostname ;
823 708 var anchor_id = $anchor_id ;
824 709
825 710 var response_messages = $("#_response_messages_{$this->buildID}");
826 711 if (! response_messages.length) {
827 - // Create all three anchors for success, invalid, and error states
828 - thisform.before('\\x3Ca id="formSubmitSuccess-'+anchor_id+'" name="formSubmitSuccess-'+anchor_id+'" class="accua-form-anchor" /\\x3E');
829 - thisform.before('\\x3Ca id="formSubmitInvalid-'+anchor_id+'" name="formSubmitInvalid-'+anchor_id+'" class="accua-form-anchor" /\\x3E');
830 - thisform.before('\\x3Ca id="formSubmitError-'+anchor_id+'" name="formSubmitError-'+anchor_id+'" class="accua-form-anchor" /\\x3E');
712 + thisform.before('\\x3Ca id="formSubmitSuccess-'+anchor_id+'" name="formSubmitSuccess-'+anchor_id+'" /\\x3E');
831 713 response_messages = $('\\x3Cdiv id="_response_messages_{$this->buildID}" class="accua-form-messages"\\x3E\\x3C/div\\x3E');
832 714 thisform.before(response_messages);
833 715 }
834 -
835 - // Smooth scroll helper function
836 - var smoothScrollToElement = function(elementId) {
837 - var target = document.getElementById(elementId);
838 - if (target) {
839 - target.scrollIntoView({ behavior: 'smooth', block: 'start' });
840 - }
841 - };
716 + var throbbler = $("\\x3Cspan class='accua_form_api_throbbler'\\x3E\\x3C/span\\x3E");
842 717
843 718 var _ajax_submitting_{$js_buildid} = false;
844 719 var timeout_handler = false;
845 720 var timeout_count = 0;
@@ -844,11 +719,8 @@
844 719 var timeout_handler = false;
845 720 var timeout_count = 0;
846 721 var fail_count = 0;
847 722 var disabled_fields = false;
848 - var submitBtn = $('button[type="submit"]', thisform);
849 - var submitBtnOriginalText = submitBtn.text();
850 - var submitBtnSendingText = $sending_message;
851 723
852 724 var jsuuid_field = $('input[name="_AccuaForm_jsuuid"]', thisform);
853 725 var jsuuid = jsuuid_field.val();
854 726 if (jsuuid == '') {
@@ -862,13 +734,8 @@
862 734
863 735 var ga_track = {$js_ga_track} ;
864 736 var ga_event, ga_submit_event, ga_field_event, ga_field_events_fired = {};
865 737 ga_event = function(eventCategory, eventAction){
866 - /* matomo */
867 - if (window._mtm && (typeof window._mtm.push == 'function')) {
868 - window._mtm.push({'event': 'ContactForms', 'eventAction': eventAction, 'eventCategory': eventCategory, 'eventLabel': ga_track.title});
869 - }
870 -
871 738 if (typeof window.gtag == 'function') {
872 739 window.gtag('event', eventAction, {'event_category': eventCategory, 'event_label': ga_track.title});
873 740 } else if (window.dataLayer && (typeof window.dataLayer.push == 'function')) {
874 741 dataLayer.push({'event': 'ContactForms', 'eventAction': eventAction, 'eventCategory': eventCategory, 'eventLabel': ga_track.title});
@@ -903,383 +770,11 @@
903 770 $('input, textarea, select', thisform).change(function(){
904 771 ga_field_event($(this).attr('name'));
905 772 });
906 773
907 - // Disable submit button during form submission
908 - var disableSubmitButton = function() {
909 - submitBtn.prop('disabled', true).attr('aria-busy', 'true').text(submitBtnSendingText);
910 - };
911 -
912 - // Re-enable submit button (on failure or completion)
913 - var enableSubmitButton = function() {
914 - submitBtn.prop('disabled', false).removeAttr('aria-busy').text(submitBtnOriginalText);
915 - };
916 -
917 - // Get field label from the field's container element
918 - var getFieldLabel = function(field) {
919 - var container = field.closest('.pfbc-element');
920 - if (!container.length) {
921 - return '';
922 - }
923 -
924 - // Try floating label first (inline labels mode)
925 - var label = container.find('.pfbc-floating-label').first();
926 - if (!label.length) {
927 - // Try standard label in .pfbc-label
928 - label = container.find('.pfbc-label label').first();
929 - }
930 - if (!label.length) {
931 - // Try any label element
932 - label = container.find('label').first();
933 - }
934 -
935 - if (label.length) {
936 - // Get text content, removing the required asterisk
937 - var text = label.clone().find('.pfbc-required').remove().end().text().trim();
938 - return text;
939 - }
940 -
941 - return '';
942 - };
943 -
944 - // Build required field error message with field name
945 - // Checks per-field data-custom-required-msg attribute first, then falls back to global template
946 - var getRequiredMessage = function(fieldLabel, field) {
947 - var template = $required_message_template;
948 - if (field) {
949 - var custom = field.attr('data-custom-required-msg');
950 - if (custom) {
951 - template = custom;
952 - }
953 - }
954 - if (fieldLabel) {
955 - return template.replace('%s', fieldLabel);
956 - }
957 - return template.replace('%s', '');
958 - };
959 -
960 - // Build email validation error message with field name
961 - // Checks per-field data-custom-format-msg attribute first, then falls back to global template
962 - var getEmailMessage = function(fieldLabel, field) {
963 - var template = $valid_mail_message_template;
964 - if (field) {
965 - var custom = field.attr('data-custom-format-msg');
966 - if (custom) {
967 - template = custom;
968 - }
969 - }
970 - if (fieldLabel) {
971 - return template.replace('%s', fieldLabel);
972 - }
973 - return template.replace('%s', '');
974 - };
975 -
976 - // Build phone validation error message with field name
977 - // Checks per-field data-custom-format-msg attribute first, then falls back to global template
978 - var getPhoneMessage = function(fieldLabel, field) {
979 - var template = $valid_phone_message_template;
980 - if (field) {
981 - var custom = field.attr('data-custom-format-msg');
982 - if (custom) {
983 - template = custom;
984 - }
985 - }
986 - if (fieldLabel) {
987 - return template.replace('%s', fieldLabel);
988 - }
989 - return template.replace('%s', '');
990 - };
991 -
992 - // Check if a telephone field contains only a country prefix (e.g. "+39")
993 - // These values should be treated as empty - the user hasn't entered a real number.
994 - var isTelephonePrefixOnly = function(field) {
995 - if (!field.hasClass('accuaform-telephone')) return false;
996 - var val = field.val();
997 - if (!val) return false;
998 - val = val.trim();
999 - if (val === '' || val.charAt(0) !== '+') return false;
1000 - return val.replace(/\D/g, '').length <= 4;
1001 - };
1002 -
1003 - // Scroll to first invalid field, then focus after scroll completes
1004 - var focusFirstInvalidField = function() {
1005 - var firstInvalidContainer = $('#{$this->buildID} .pfbc-element-has-error').first();
1006 - if (!firstInvalidContainer.length) {
1007 - return;
1008 - }
1009 -
1010 - // Try to find a focusable element in the invalid container
1011 - var focusTarget = null;
1012 -
1013 - // Check for radio/checkbox groups first
1014 - var radioOrCheckbox = firstInvalidContainer.find('input[type="radio"], input[type="checkbox"]').first();
1015 - if (radioOrCheckbox.length) {
1016 - focusTarget = radioOrCheckbox;
1017 - }
1018 -
1019 - // Check for file input (focus the dropzone button if available)
1020 - if (!focusTarget) {
1021 - var fileDropzone = firstInvalidContainer.find('.accua-file-dropzone');
1022 - if (fileDropzone.length) {
1023 - focusTarget = fileDropzone;
1024 - } else {
1025 - var fileInput = firstInvalidContainer.find('input[type="file"]');
1026 - if (fileInput.length) {
1027 - focusTarget = fileInput;
1028 - }
1029 - }
1030 - }
1031 -
1032 - // Check for custom select buttons (post select, etc.)
1033 - if (!focusTarget) {
1034 - var customSelectBtn = firstInvalidContainer.find('.pfbc-post-select-trigger, button[aria-haspopup="listbox"]').first();
1035 - if (customSelectBtn.length) {
1036 - focusTarget = customSelectBtn;
1037 - }
1038 - }
1039 -
1040 - // Standard inputs (text, email, select, textarea)
1041 - if (!focusTarget) {
1042 - focusTarget = firstInvalidContainer.find('input:not([type="hidden"]), textarea, select').first();
1043 - }
1044 -
1045 - if (focusTarget && focusTarget.length) {
1046 - // Scroll into view first with smooth animation
1047 - if (focusTarget[0].scrollIntoView) {
1048 - focusTarget[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
1049 - }
1050 - // Focus after scroll animation completes (typical scroll animation is ~300-500ms)
1051 - setTimeout(function() {
1052 - focusTarget.focus();
1053 - }, 500);
1054 - }
1055 - };
1056 -
1057 - // Scroll to a specific field by ID and focus it
1058 - var scrollToFieldAndFocus = function(fieldId) {
1059 - var element = $('#' + fieldId);
1060 - if (!element.length) {
1061 - return;
1062 - }
1063 -
1064 - var focusTarget = element;
1065 -
1066 - // If it's already an input/select/textarea, use it directly
1067 - if (element.is('input, select, textarea')) {
1068 - focusTarget = element;
1069 - }
1070 - // For containers (pfbc-element), find the first focusable element
1071 - else if (element.hasClass('pfbc-element')) {
1072 - var radioOrCheckbox = element.find('input[type="radio"], input[type="checkbox"]').first();
1073 - if (radioOrCheckbox.length) {
1074 - focusTarget = radioOrCheckbox;
1075 - } else {
1076 - var fileDropzone = element.find('.accua-file-dropzone');
1077 - if (fileDropzone.length) {
1078 - focusTarget = fileDropzone;
1079 - } else {
1080 - focusTarget = element.find('input:not([type="hidden"]), textarea, select').first();
1081 - }
1082 - }
1083 - }
1084 - // For file dropzone wrapper, focus the dropzone
1085 - else if (element.find('.accua-file-dropzone').length) {
1086 - focusTarget = element.find('.accua-file-dropzone').first();
1087 - }
1088 -
1089 - if (focusTarget && focusTarget.length) {
1090 - if (focusTarget[0].scrollIntoView) {
1091 - focusTarget[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
1092 - }
1093 - setTimeout(function() {
1094 - focusTarget.focus();
1095 - }, 500);
1096 - }
1097 - };
1098 -
1099 - // Track if form submission has been attempted
1100 - var submitAttempted = false;
1101 -
1102 - // Array to collect field errors for summary
1103 - var fieldErrorsList = [];
1104 -
1105 - // Get the error ID for a radio/checkbox group (DRY helper)
1106 - var getGroupErrorId = function(field) {
1107 - var groupWrapper = field.closest('.pfbc-radio-buttons, .pfbc-checkboxes').parent();
1108 - var groupId = groupWrapper.attr('id') || field.attr('id');
1109 - return groupId + '-error';
1110 - };
1111 -
1112 - // Remove an error element with smooth animation (generic helper for any field type)
1113 - var removeErrorAnimated = function(errorId) {
1114 - var errorEl = $('#' + errorId);
1115 - if (errorEl.length) {
1116 - errorEl.addClass('pfbc-error-removing');
1117 - setTimeout(function() {
1118 - errorEl.remove();
1119 - }, 150); // Match CSS animation duration
1120 - }
1121 - };
1122 -
1123 - // Remove all existing errors for a group with smooth fade-out (handles both potential IDs)
1124 - var removeGroupErrors = function(field, fieldName, animate) {
1125 - var groupContainer = field.closest('.pfbc-element, .pfbc-fieldwrap');
1126 - var errors = groupContainer.find('.pfbc-inline-error');
1127 -
1128 - // Also collect errors by potential IDs
1129 - var groupWrapper = field.closest('.pfbc-radio-buttons, .pfbc-checkboxes').parent();
1130 - var wrapperId = groupWrapper.attr('id');
1131 - if (wrapperId) {
1132 - errors = errors.add($('#' + wrapperId + '-error'));
1133 - }
1134 - var firstField = $("[name='"+fieldName+"']", thisform).first();
1135 - if (firstField.attr('id')) {
1136 - errors = errors.add($('#' + firstField.attr('id') + '-error'));
1137 - }
1138 -
1139 - if (animate && errors.length) {
1140 - // Smooth fade-out animation
1141 - errors.addClass('pfbc-error-removing');
1142 - setTimeout(function() {
1143 - errors.remove();
1144 - }, 150); // Match CSS transition duration
1145 - } else {
1146 - errors.remove();
1147 - }
1148 - };
1149 -
1150 - // Show or update the error/success/loading summary area
1151 - // state: true (success), false (error), 'loading' (submitting)
1152 - var updateSummaryArea = function(state) {
1153 - var summaryArea = $('#{$this->buildID}-validation-summary');
1154 -
1155 - if (!submitAttempted && state !== 'loading') {
1156 - summaryArea.remove();
1157 - return;
1158 - }
1159 -
1160 - // Ensure summary area exists
1161 - if (!summaryArea.length) {
1162 - summaryArea = $('<div id="{$this->buildID}-validation-summary" class="pfbc-validation-summary" role="status" aria-live="polite"></div>');
1163 - thisform.find('.pfbc-error').remove();
1164 - thisform.append(summaryArea);
1165 - }
1166 -
1167 - if (state === 'loading') {
1168 - // Show loading state - neutral blue with spinner and text
1169 - summaryArea
1170 - .removeClass('pfbc-validation-error pfbc-validation-success')
1171 - .addClass('pfbc-validation-loading')
1172 - .attr('role', 'status')
1173 - .attr('aria-live', 'polite')
1174 - .attr('aria-busy', 'true')
1175 - .html('<span class="pfbc-summary-spinner" aria-hidden="true"></span>' + $loading_summary_message);
1176 - } else if (state === true) {
1177 - // Show success state
1178 - summaryArea
1179 - .removeClass('pfbc-validation-error pfbc-validation-loading')
1180 - .addClass('pfbc-validation-success')
1181 - .attr('role', 'status')
1182 - .attr('aria-live', 'polite')
1183 - .removeAttr('aria-busy')
1184 - .html('<span class="pfbc-summary-icon">✓</span> ' + $success_message);
1185 - } else {
1186 - // Show error state with field list
1187 - summaryArea
1188 - .removeClass('pfbc-validation-success pfbc-validation-loading')
1189 - .addClass('pfbc-validation-error')
1190 - .attr('role', 'alert')
1191 - .attr('aria-live', 'assertive')
1192 - .removeAttr('aria-busy');
1193 -
1194 - var html = '<p class="pfbc-summary-header">' + $error_summary_header + '</p><ul class="pfbc-summary-list">';
1195 - for (var i = 0; i < fieldErrorsList.length; i++) {
1196 - var err = fieldErrorsList[i];
1197 - html += '<li><a href="#' + err.fieldId + '" class="pfbc-summary-link" data-field-id="' + err.fieldId + '">' + err.label + '</a> - ' + err.errorType + '</li>';
1198 - }
1199 - html += '</ul>';
1200 - summaryArea.html(html);
1201 -
1202 - // Attach click handlers to links
1203 - summaryArea.find('.pfbc-summary-link').on('click', function(e) {
1204 - e.preventDefault();
1205 - var fieldId = $(this).data('field-id');
1206 - scrollToFieldAndFocus(fieldId);
1207 - });
1208 - }
1209 - };
1210 -
1211 774 var show_error_messages = function(message) {
1212 - // Show error message in the validation summary area
1213 - // Used when AJAX submission fails (network error, server error, etc.)
1214 - var summaryArea = thisform.find('.pfbc-validation-summary');
1215 - if (summaryArea.length) {
1216 - summaryArea.html('<div class="pfbc-validation-error" role="alert"><strong>' + accua_forms_i18n.check_fields + '</strong><ul>' + message + '</ul></div>').show();
1217 - }
775 + thisform.append('\\x3Cdiv class="pfbc-error ui-state-error ui-corner-all"\\x3E\\x3Cul\\x3E' + message + '\\x3C/ul\\x3E\\x3C/div\\x3E');
1218 776 }
1219 -
1220 - // Update summary area with server-side errors (called from AJAX error response)
1221 - var updateSummaryWithServerErrors = function(errors, elementErrors) {
1222 - // Collect server errors for the summary
1223 - fieldErrorsList = [];
1224 -
1225 - if (elementErrors) {
1226 - jQuery.each(elementErrors, function(fieldName, fieldErrors) {
1227 - var field = jQuery('[name="' + fieldName + '"]', thisform);
1228 - if (!field.length) {
1229 - // Elements with no named input in the DOM (e.g. reCAPTCHA v2) expose
1230 - // their name on the widget container via data-accua-name
1231 - field = jQuery('[data-accua-name="' + fieldName + '"]', thisform);
1232 - }
1233 - var fieldId = field.attr('id') || fieldName;
1234 - var fieldLabel = getFieldLabel(field);
1235 -
1236 - // If no label found, try to get from the field container or use field name
1237 - if (!fieldLabel || fieldLabel === fieldName) {
1238 - var container = field.closest('.pfbc-element, .pfbc-fieldwrap');
1239 - fieldLabel = container.find('label').first().text().replace(/\s*\*\s*$/, '').trim();
1240 - if (!fieldLabel) {
1241 - // Fallback: humanize the field name
1242 - fieldLabel = fieldName.replace(/[-_]/g, ' ').replace(/\b\w/g, function(l){ return l.toUpperCase(); });
1243 - }
1244 - }
1245 -
1246 - for (var i = 0; i < fieldErrors.length; i++) {
1247 - fieldErrorsList.push({
1248 - fieldId: fieldId,
1249 - label: fieldLabel,
1250 - errorType: fieldErrors[i]
1251 - });
1252 - }
1253 - });
1254 - }
1255 -
1256 - // Also add any general errors from the errors array
1257 - if (errors && errors.length > 0) {
1258 - for (var i = 0; i < errors.length; i++) {
1259 - // Check if this error is already in fieldErrorsList
1260 - var alreadyAdded = false;
1261 - for (var j = 0; j < fieldErrorsList.length; j++) {
1262 - if (fieldErrorsList[j].errorType === errors[i]) {
1263 - alreadyAdded = true;
1264 - break;
1265 - }
1266 - }
1267 - if (!alreadyAdded) {
1268 - fieldErrorsList.push({
1269 - fieldId: '',
1270 - label: '',
1271 - errorType: errors[i]
1272 - });
1273 - }
1274 - }
1275 - }
1276 -
1277 - // Update the summary area to show errors
1278 - if (fieldErrorsList.length > 0) {
1279 - updateSummaryArea(false);
1280 - }
1281 - }
1282 777
1283 778 _handle_ajax_submit_{$js_buildid} = function() {
1284 779 if (_ajax_submitting_{$js_buildid}) {
1285 780 return false;
@@ -1288,129 +783,36 @@
1288 783 JS;
1289 784 $this->error->clear();
1290 785 echo <<<JS
1291 786
1292 - // Mark that submit was attempted
1293 - submitAttempted = true;
1294 -
1295 787 var valid_empty = true;
1296 788 var valid_mail = true;
1297 - var valid_phone = true;
1298 - var fieldErrors = {};
1299 -
1300 - // Reset field errors list for summary
1301 - fieldErrorsList = [];
1302 789
1303 790 $("#{$this->buildID} .pfbc-element").removeClass('pfbc-invalid');
1304 - $("#{$this->buildID} .pfbc-element").removeClass('pfbc-element-has-error');
1305 - $("#{$this->buildID} input, #{$this->buildID} textarea, #{$this->buildID} select").attr('aria-invalid', 'false');
1306 - $("#{$this->buildID} .pfbc-inline-error").remove();
1307 791
1308 - var processedGroups = {}; // Track radio/checkbox groups to avoid duplicate errors
1309 -
1310 792 $('.accuaforms-field-required', thisform).each(function(){
1311 793 var field = $(this);
1312 794 var type = field.attr('type');
1313 - var fieldName = field.attr('name');
1314 795
1315 796 if (type === 'checkbox' || type === 'radio') {
1316 - // Skip if we've already processed this group
1317 - if (processedGroups[fieldName]) {
797 + if ($("[name='"+field.attr("name")+"']:checked", "#{$this->buildID}").length > 0) {
1318 798 return true;
1319 799 }
1320 - processedGroups[fieldName] = true;
1321 -
1322 - if ($("[name='"+fieldName+"']:checked", "#{$this->buildID}").length > 0) {
1323 - return true;
1324 - }
1325 -
1326 - valid_empty = false;
1327 -
1328 - // Find the container for the radio/checkbox group
1329 - var groupContainer = field.closest('.pfbc-element, .pfbc-fieldwrap');
1330 - groupContainer.addClass('pfbc-invalid pfbc-element-has-error');
1331 -
1332 - // Remove any existing errors before adding new one (prevents duplicates)
1333 - removeGroupErrors(field, fieldName);
1334 -
1335 - // Get consistent error ID using helper
1336 - var errorId = getGroupErrorId(field);
1337 -
1338 - // Find the last radio/checkbox in the group
1339 - var lastInGroup = $("[name='"+fieldName+"']", "#{$this->buildID}").last();
1340 -
1341 - // Apply ARIA attributes to all inputs in the group
1342 - $("[name='"+fieldName+"']", "#{$this->buildID}").attr('aria-invalid', 'true');
1343 - $("[name='"+fieldName+"']", "#{$this->buildID}").attr('aria-describedby', errorId);
1344 -
1345 - // Get field label and build error message
1346 - var fieldLabel = getFieldLabel(field);
1347 - var firstInput = $("[name='"+fieldName+"']", "#{$this->buildID}").first();
1348 - var errorMessage = getRequiredMessage(fieldLabel, firstInput);
1349 -
1350 - // Add to field errors list for summary (use first input ID for focusing)
1351 - fieldErrorsList.push({
1352 - fieldId: firstInput.attr('id'),
1353 - label: fieldLabel,
1354 - errorType: $error_type_required
1355 - });
1356 -
1357 - // Add error message after the last item in the group
1358 - var inlineError = $('<div class="pfbc-inline-error" id="' + errorId + '" role="alert" aria-live="polite"><div class="pfbc-error-message">' + errorMessage + '</div></div>');
1359 -
1360 - // Insert after the last radio/checkbox wrapper
1361 - var lastWrapper = lastInGroup.closest('.pfbc-radio, .pfbc-checkbox');
1362 - if (lastWrapper.length) {
1363 - lastWrapper.after(inlineError);
1364 - } else {
1365 - lastInGroup.after(inlineError);
1366 - }
1367 800 } else {
1368 801 var val = field.val();
1369 802 if (typeof(val) == "string") {
1370 - // Treat "-" and "Select..." as invalid only for dropdowns
1371 - // For telephone fields, prefix-only values (e.g. "+39") are also empty
1372 - var isSelect = field.is('select');
1373 - if (! val.match(/^\s*$/) && (!isSelect || (val !== "Select..." && val !== "-")) && !isTelephonePrefixOnly(field)) {
803 + if (! val.match(/^\s*$/)) {
1374 804 return true;
1375 805 }
1376 - } else if (Array.isArray(val)) {
1377 - // Multiselect: empty array means no selection
1378 - if (val.length > 0) {
1379 - return true;
1380 - }
806 + } else if ((typeof(val) == "object") && val && (val.length > 0)) {
807 + return true;
1381 808 } else if (val) {
1382 809 return true;
1383 810 }
811 + }
1384 812
1385 - valid_empty = false;
1386 - var parent = field.closest('.pfbc-element, .pfbc-fieldwrap');
1387 - parent.addClass('pfbc-invalid pfbc-element-has-error');
1388 -
1389 - // Apply ARIA attributes
1390 - field.attr('aria-invalid', 'true');
1391 - var errorId = field.attr('id') + '-error';
1392 - field.attr('aria-describedby', errorId);
1393 -
1394 - // Get field label and build error message
1395 - var fieldLabel = getFieldLabel(field);
1396 - var errorMessage = getRequiredMessage(fieldLabel, field);
1397 - fieldErrorsList.push({
1398 - fieldId: field.attr('id'),
1399 - label: fieldLabel,
1400 - errorType: $error_type_required
1401 - });
1402 -
1403 - var inlineError = $('<div class="pfbc-inline-error" id="' + errorId + '" role="alert" aria-live="polite"><div class="pfbc-error-message">' + errorMessage + '</div></div>');
1404 -
1405 - // For file inputs with help text, insert error after help text
1406 - var helpText = field.siblings('.pfbc-help').last();
1407 - if (field.is('[type="file"]') && helpText.length) {
1408 - helpText.after(inlineError);
1409 - } else {
1410 - field.after(inlineError);
1411 - }
1412 - }
813 + valid_empty = false;
814 + field.parents("#{$this->buildID} .pfbc-element").addClass('pfbc-invalid');
1413 815 });
1414 816
1415 817 $('.pfbc-textbox[type="email"]', thisform).each(function(){
1416 818 var field = $(this);
@@ -1423,139 +825,15 @@
1423 825 return true;
1424 826 }
1425 827
1426 828 valid_mail = false;
1427 - var parent = field.closest('.pfbc-element, .pfbc-fieldwrap');
1428 - parent.addClass('pfbc-invalid pfbc-element-has-error');
1429 -
1430 - // Apply ARIA attributes
1431 - field.attr('aria-invalid', 'true');
1432 - var errorId = field.attr('id') + '-error';
1433 - field.attr('aria-describedby', errorId);
1434 -
1435 - // Get field label and build error message
1436 - var fieldLabel = getFieldLabel(field);
1437 - var errorMessage = getEmailMessage(fieldLabel, field);
1438 -
1439 - // Add to field errors list for summary (use field ID for focusing)
1440 - fieldErrorsList.push({
1441 - fieldId: field.attr('id'),
1442 - label: fieldLabel,
1443 - errorType: $error_type_invalid_email
1444 - });
1445 -
1446 - var inlineError = $('<div class=\"pfbc-inline-error\" id=\"' + errorId + '\" role=\"alert\" aria-live=\"polite\"><div class=\"pfbc-error-message\">' + errorMessage + '</div></div>');
1447 -
1448 - // For file inputs with help text, insert error after help text
1449 - var helpText = field.siblings('.pfbc-help').last();
1450 - if (field.is('[type=\"file\"]') && helpText.length) {
1451 - helpText.after(inlineError);
1452 - } else {
1453 - field.after(inlineError);
1454 - }
829 + field.parents("#{$this->buildID} .pfbc-element").addClass('pfbc-invalid');
1455 830
1456 831 });
1457 832
1458 - // Phone validation - runs for all non-empty phone fields
1459 - $('.accuaform-telephone', thisform).each(function(){
1460 - var field = $(this);
1461 - var value = field.val();
1462 -
1463 - // Skip empty fields (Required validation handles mandatory)
1464 - if (!value || value.trim() === '') {
1465 - return true;
1466 - }
1467 -
1468 - // Skip prefix-only values (e.g. "+39") - treated as empty
1469 - var phoneTrimmed = value.trim();
1470 - if (phoneTrimmed.charAt(0) === '+' && phoneTrimmed.replace(/\D/g, '').length <= 4) {
1471 - return true;
1472 - }
1473 -
1474 - // Get country code from data attribute
1475 - var countryCode = field.attr('data-country') || 'IT';
1476 -
1477 - // Use AccuaPhoneValidation if available, otherwise skip validation
1478 - if (typeof window.AccuaPhoneValidation !== 'undefined' && window.AccuaPhoneValidation.isValid) {
1479 - if (window.AccuaPhoneValidation.isValid(value, countryCode)) {
1480 - return true;
1481 - }
1482 - } else {
1483 - // Fallback: basic validation matching server-side Phone.php
1484 - var trimmed = value.trim();
1485 - // Check for invalid characters
1486 - if (!/^[\d\s\-\.\/\(\)\+]+$/.test(trimmed)) {
1487 - // Invalid characters - fail validation
1488 - } else {
1489 - var plusIndex = trimmed.indexOf('+');
1490 - if (plusIndex > 0 || (trimmed.match(/\+/g) || []).length > 1) {
1491 - // Plus in wrong position or multiple plus signs - fail validation
1492 - } else {
1493 - var digitCount = trimmed.replace(/\D/g, '').length;
1494 - // Prefix-only (1-4 digits with +) treated as empty
1495 - if (trimmed.charAt(0) === '+' && digitCount <= 4) {
1496 - return true;
1497 - }
1498 - // Valid if 5-15 digits (matches server-side Phone.php)
1499 - if (digitCount >= 5 && digitCount <= 15) {
1500 - return true;
1501 - }
1502 - }
1503 - }
1504 - }
1505 -
1506 - valid_phone = false;
1507 - var parent = field.closest('.pfbc-element, .pfbc-fieldwrap');
1508 - parent.addClass('pfbc-invalid pfbc-element-has-error');
1509 -
1510 - // Remove any existing blur validation error to avoid duplicates
1511 - $('#' + field.attr('id') + '-phone-error').remove();
1512 -
1513 - // Apply ARIA attributes
1514 - field.attr('aria-invalid', 'true');
1515 - var errorId = field.attr('id') + '-error';
1516 - field.attr('aria-describedby', errorId);
1517 -
1518 - // Get field label and build error message
1519 - var fieldLabel = getFieldLabel(field);
1520 - var errorMessage = getPhoneMessage(fieldLabel, field);
1521 -
1522 - // Add to field errors list for summary
1523 - fieldErrorsList.push({
1524 - fieldId: field.attr('id'),
1525 - label: fieldLabel,
1526 - errorType: $error_type_invalid_phone
1527 - });
1528 -
1529 - var inlineError = $('<div class="pfbc-inline-error pfbc-phone-format-error" id="' + errorId + '" role="alert" aria-live="polite"><div class="pfbc-error-message">' + errorMessage + '</div></div>');
1530 -
1531 - var helpText = field.siblings('.pfbc-help').last();
1532 - if (helpText.length) {
1533 - helpText.after(inlineError);
1534 - } else {
1535 - field.after(inlineError);
1536 - }
1537 - });
1538 -
1539 - if (valid_empty && valid_mail && valid_phone) {
1540 - // Show loading state in summary during AJAX submission
1541 - updateSummaryArea('loading');
1542 -
1543 - // reCAPTCHA v3: tokens are fetched asynchronously at submit time (they
1544 - // are single-use and expire after ~2 minutes). Cancel this submit, get
1545 - // a fresh token, then re-trigger: the re-entered handler finds the
1546 - // token in place (needsToken() false) and proceeds normally. On token
1547 - // failure the submit proceeds anyway - the server rejects it (fail-closed).
1548 - if (((typeof accuaformRecaptcha3) != 'undefined') && accuaformRecaptcha3.needsToken(thisform[0])) {
1549 - accuaformRecaptcha3.getToken(thisform[0]).then(
1550 - function(){ thisform.trigger('submit'); },
1551 - function(){ thisform.trigger('submit'); }
1552 - );
1553 - return false;
1554 - }
1555 -
833 + if (valid_empty && valid_mail) {
834 + thisform.append(throbbler);
1556 835 _ajax_submitting_{$js_buildid} = true;
1557 - disableSubmitButton();
1558 836 $('input[name="_AccuaForm_tentatives"]', thisform).val(fail_count);
1559 837 disabled_fields = $("input, textarea, button, select", thisform).not('[type="submit"]').not(':disabled');
1560 838 disabled_fields.attr('readonly','readonly');
1561 839 timeout_count = 0;
@@ -1565,20 +843,17 @@
1565 843 }
1566 844 return true;
1567 845 } else {
1568 846 ga_submit_event('formSubmitInvalid');
1569 -
1570 - // Update URL hash to reflect invalid state (for GA tracking and bookmarkability)
1571 - if (history.replaceState) {
1572 - history.replaceState(null, '', '#formSubmitInvalid-'+anchor_id);
847 + location.href = "#formSubmitInvalid-"+anchor_id;
848 + var message = '';
849 + if (!valid_empty) {
850 + message += '\\x3Cli\\x3E' + $required_message + '\\x3C/li\\x3E';
1573 851 }
1574 -
1575 - // Update summary area with error list
1576 - updateSummaryArea(false);
1577 -
1578 - // Focus on first invalid field for accessibility
1579 - focusFirstInvalidField();
1580 -
852 + if (!valid_mail) {
853 + message += '\\x3Cli\\x3E' + $valid_mail_message + '\\x3C/li\\x3E';
854 + }
855 + show_error_messages(message);
1581 856 return false;
1582 857 }
1583 858 }
1584 859
@@ -1620,10 +895,9 @@
1620 895 if (_ajax_submitting_{$js_buildid}) {
1621 896 var response = false;
1622 897 try {
1623 898 response = $.parseJSON(message.data);
1624 - // Accept response if jsuuid matches AND buildID matches or is null (server rejection)
1625 - if (response.jsuuid != jsuuid || (response.buildID != null && response.buildID != "{$this->buildID}")) {
899 + if (response.jsuuid != jsuuid || response.buildID != "{$this->buildID}") {
1626 900 response = false;
1627 901 }
1628 902 } catch (err) {
1629 903 response = false;
@@ -1636,25 +910,13 @@
1636 910
1637 911 _handle_ajax_submit_response_{$js_buildid} = function(response) {
1638 912 if (_ajax_submitting_{$js_buildid}) {
1639 913 if(response && typeof(response) == "object" && typeof(response.submitted) == "boolean") {
1640 - // Only show message container if there's actual content
1641 - if (response.messages && response.messages.trim() !== '') {
1642 - response_messages.html(response.messages).show();
1643 - } else {
1644 - response_messages.empty().hide();
1645 - }
914 + response_messages.html(response.messages);
1646 915 if (response.submitted) {
1647 916 if (response.valid) {
1648 - var gads_track_code = "{$this->gads_conversion_tracking_code}";
1649 - if(gads_track_code != ''){
1650 - gtag('event', 'conversion', {'send_to': gads_track_code});
1651 - }
1652 -
1653 917 ga_submit_event('formSubmitSuccess');
1654 - smoothScrollToElement('formSubmitSuccess-'+anchor_id);
1655 -
1656 -
918 + location.href = "#formSubmitSuccess-"+anchor_id;
1657 919 JS;
1658 920 /*A callback function can be specified to handle any post submission events.*/
1659 921 if(!empty($this->ajaxCallback)) {
1660 922 echo $this->ajaxCallback, "(response);";
@@ -1663,9 +925,9 @@
1663 925 }
1664 926 echo <<<JS
1665 927 } else {
1666 928 ga_submit_event('formSubmitInvalid');
1667 - smoothScrollToElement('formSubmitInvalid-'+anchor_id);
929 + location.href = "#formSubmitInvalid-"+anchor_id;
1668 930 JS;
1669 931 if (method_exists($this->error,'applyAjaxErrorResponseUsingShowErrorMessages')) {
1670 932 $this->error->applyAjaxErrorResponseUsingShowErrorMessages();
1671 933 } else {
@@ -1681,28 +943,13 @@
1681 943 $("input[name='_AccuaForm_iv']", thisform).val(response._AccuaForm_iv);
1682 944 $("input[name='_AccuaForm_data']",thisform).val(response._AccuaForm_data);
1683 945
1684 946 disabled_fields.removeAttr('readonly');
1685 - enableSubmitButton();
1686 947 }
1687 - } else {
1688 - // Server did not recognize the form submission (e.g. expired nonce, stale cached page)
1689 - ga_submit_event('formSubmitError');
1690 - smoothScrollToElement('formSubmitError-'+anchor_id);
1691 - fail_count++;
1692 - if (fail_count > 2) {
1693 - ajax_enabled = false;
1694 - thisform.attr("action", {$post_url} );
1695 - thisform.removeAttr("target");
1696 - $('input[name="_AccuaForm_submit_method"]', thisform).val('fallback');
1697 - }
1698 - show_error_messages( $submit_fail_message );
1699 - disabled_fields.removeAttr('readonly');
1700 - enableSubmitButton();
1701 948 }
1702 949 } else {
1703 950 ga_submit_event('formSubmitError');
1704 - smoothScrollToElement('formSubmitError-'+anchor_id);
951 + location.href = "#formSubmitError-"+anchor_id;
1705 952 fail_count++;
1706 953 if (fail_count > 2) {
1707 954 ajax_enabled = false;
1708 955 thisform.attr("action", {$post_url} );
@@ -1709,30 +956,16 @@
1709 956 thisform.removeAttr("target");
1710 957 $('input[name="_AccuaForm_submit_method"]', thisform).val('fallback');
1711 958 }
1712 959 show_error_messages( $submit_fail_message );
1713 - enableSubmitButton();
1714 960 }
961 + $('.accua_forms_show_recaptcha_button', thisform).click();
1715 962 if (((typeof accuaform_recaptcha2_initialized) != 'undefined') && accuaform_recaptcha2_initialized) {
1716 963 $('.accua_forms_recaptcha2_container', thisform).each(function(){
1717 964 accua_forms_reload_recaptcha2($(this).attr('id'));
1718 965 });
1719 966 }
1720 - if ((typeof accuaformRecaptcha3) != 'undefined') {
1721 - // v3 tokens are single-use: clear them so a retry fetches a fresh one
1722 - accuaformRecaptcha3.reset(thisform[0]);
1723 - }
1724 - if ((typeof accuaformCap) != 'undefined') {
1725 - // Cap tokens are single-use too: clear them and reset the widget
1726 - accuaformCap.reset(thisform[0]);
1727 - }
1728 - if ((typeof turnstile) != 'undefined') {
1729 - // Turnstile tokens are single-use too: reset each widget so a retry gets a fresh one
1730 - // (.cf-turnstile is the widget div rendered by the Simple Cloudflare Turnstile plugin)
1731 - $('.accua-forms-turnstile-container .cf-turnstile', thisform).each(function(){
1732 - try { turnstile.reset('#' + $(this).attr('id')); } catch (e) { }
1733 - });
1734 - }
967 + throbbler.remove();
1735 968 _ajax_submitting_{$js_buildid} = false;
1736 969 if (timeout_handler) {
1737 970 clearTimeout(timeout_handler);
1738 971 timeout_handler = false;
@@ -1750,172 +983,12 @@
1750 983 } else {
1751 984 thisform.attr("action", {$post_url} );
1752 985 }
1753 986 thisform.attr("onsubmit","return _handle_ajax_submit_{$js_buildid}()");
1754 -
1755 - // Real-time validation for better UX - use change only for checkbox/radio to avoid double-firing
1756 - $('.accuaforms-field-required', thisform).on('change', function() {
1757 - var field = $(this);
1758 - var type = field.attr('type');
1759 - if (type !== 'checkbox' && type !== 'radio') return; // Only handle checkbox/radio on change
1760 -
1761 - var fieldName = field.attr('name');
1762 - var isChecked = $("[name='"+fieldName+"']:checked", thisform).length > 0;
1763 - var groupContainer = field.closest('.pfbc-element, .pfbc-fieldwrap');
1764 - var hasError = groupContainer.hasClass('pfbc-element-has-error');
1765 -
1766 - // Only act if state actually changed to avoid flashing
1767 - if (isChecked && hasError) {
1768 - // Valid now - remove error with animation
1769 - groupContainer.removeClass('pfbc-invalid pfbc-element-has-error');
1770 - $("[name='"+fieldName+"']", thisform).attr('aria-invalid', 'false');
1771 - $("[name='"+fieldName+"']", thisform).removeAttr('aria-describedby');
1772 - removeGroupErrors(field, fieldName, true); // animate=true
1773 - } else if (!isChecked && !hasError && submitAttempted) {
1774 - // Invalid now and we've attempted submit - show error
1775 - var errorId = getGroupErrorId(field);
1776 - groupContainer.addClass('pfbc-invalid pfbc-element-has-error');
1777 - $("[name='"+fieldName+"']", thisform).attr('aria-invalid', 'true');
1778 - $("[name='"+fieldName+"']", thisform).attr('aria-describedby', errorId);
1779 -
1780 - var fieldLabel = getFieldLabel(groupContainer);
1781 - var firstField = $("[name='"+fieldName+"']", thisform).first();
1782 - var errorMessage = getRequiredMessage(fieldLabel, firstField);
1783 - var inlineError = $('<div class="pfbc-inline-error" id="' + errorId + '" role="alert" aria-live="polite"><div class="pfbc-error-message">' + errorMessage + '</div></div>');
1784 -
1785 - var lastInGroup = $("[name='"+fieldName+"']", thisform).last();
1786 - var lastWrapper = lastInGroup.closest('.pfbc-radio, .pfbc-checkbox');
1787 - if (lastWrapper.length) {
1788 - lastWrapper.after(inlineError);
1789 - } else {
1790 - lastInGroup.after(inlineError);
1791 - }
1792 - }
1793 - });
1794 -
1795 - // Blur handler for text-like fields only
1796 - $('.accuaforms-field-required', thisform).on('blur', function() {
1797 - var field = $(this);
1798 - var type = field.attr('type');
1799 - var fieldName = field.attr('name');
1800 - var isEmpty = false;
1801 -
1802 - // Skip checkbox/radio - handled by change event above
1803 - if (type === 'checkbox' || type === 'radio') return;
1804 -
1805 - var val = field.val();
1806 - if (typeof(val) == "string") {
1807 - // Treat "-" and "Select..." as invalid only for dropdowns
1808 - // For telephone fields, prefix-only values (e.g. "+39") are also empty
1809 - var isSelect = field.is('select');
1810 - isEmpty = val.match(/^\s*$/) || (isSelect && (val === "Select..." || val === "-")) || isTelephonePrefixOnly(field);
1811 - } else if (typeof(val) == "object") {
1812 - isEmpty = !val || val.length === 0;
1813 - } else {
1814 - isEmpty = !val;
1815 - }
1816 -
1817 - var parent = field.closest('.pfbc-element, .pfbc-fieldwrap');
1818 - var errorId = field.attr('id') + '-error';
1819 -
1820 - if (isEmpty) {
1821 - // For telephone: phone-validation.js blur handler runs AFTER this one and may
1822 - // still have pfbc-element-has-error set from a previous format error. Check for
1823 - // the specific required error div instead of the parent class to avoid skipping.
1824 - var alreadyHasError = field.hasClass('accuaform-telephone')
1825 - ? $('#' + errorId).length > 0
1826 - : parent.hasClass('pfbc-element-has-error');
1827 - if (!alreadyHasError) {
1828 - // For telephone: remove leftover phone format error since the field is now
1829 - // empty (required error takes priority). phone-validation.js will also clean
1830 - // up on its blur, but this handler runs first.
1831 - if (field.hasClass('accuaform-telephone')) {
1832 - $('#' + field.attr('id') + '-phone-error').remove();
1833 - }
1834 - parent.addClass('pfbc-invalid pfbc-element-has-error');
1835 - field.attr('aria-invalid', 'true');
1836 - field.attr('aria-describedby', errorId);
1837 -
1838 - // Get field label for error message
1839 - var fieldLabel = getFieldLabel(parent);
1840 - var errorMessage = getRequiredMessage(fieldLabel, field);
1841 - var inlineError = $('<div class="pfbc-inline-error" id="' + errorId + '" role="alert" aria-live="polite"><div class="pfbc-error-message">' + errorMessage + '</div></div>');
1842 -
1843 - // For file inputs with help text, insert error after help text
1844 - var helpText = field.siblings('.pfbc-help').last();
1845 - if (field.is('[type="file"]') && helpText.length) {
1846 - helpText.after(inlineError);
1847 - } else {
1848 - field.after(inlineError);
1849 - }
1850 - }
1851 - } else {
1852 - // Field is not empty - clear required-related errors.
1853 - if (field.hasClass('accuaform-telephone')) {
1854 - // For telephone fields: clear only the required error ({id}-error).
1855 - // phone-validation.js has already run on this same blur event and set the
1856 - // correct error state (phone-error or clean). We must not undo its work.
1857 - // Only remove the required-error div; preserve phone-validation.js state.
1858 - $('#' + errorId).remove();
1859 - // If phone-validation.js left no errors, clear the container state too
1860 - if (!parent.find('.pfbc-inline-error').length) {
1861 - parent.removeClass('pfbc-invalid pfbc-element-has-error');
1862 - field.attr('aria-invalid', 'false');
1863 - field.removeAttr('aria-describedby');
1864 - }
1865 - } else {
1866 - // For non-telephone fields: animated removal for smoother UX
1867 - removeErrorAnimated(errorId);
1868 - parent.removeClass('pfbc-invalid pfbc-element-has-error');
1869 - field.attr('aria-invalid', 'false');
1870 - field.removeAttr('aria-describedby');
1871 - }
1872 - }
1873 - });
1874 -
1875 - $('.pfbc-textbox[type="email"]', thisform).on('blur change', function() {
1876 - var field = $(this);
1877 - var val = field.val();
1878 -
1879 - if (val.match(/^\s*$/)) {
1880 - return; // Empty is handled by required validation
1881 - }
1882 -
1883 - var isValid = val.match(/^([a-zA-Z0-9_.+%-])+@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9])+$/);
1884 - var parent = field.closest('.pfbc-element, .pfbc-fieldwrap');
1885 - var errorId = field.attr('id') + '-error';
1886 -
1887 - if (!isValid) {
1888 - if (!$('#' + errorId + ':not(.pfbc-error-removing)').length) {
1889 - $('#' + errorId).remove();
1890 - parent.addClass('pfbc-invalid pfbc-element-has-error');
1891 - field.attr('aria-invalid', 'true');
1892 - field.attr('aria-describedby', errorId);
1893 -
1894 - // Get field label for error message
1895 - var fieldLabel = getFieldLabel(parent);
1896 - var errorMessage = getEmailMessage(fieldLabel, field);
1897 - var inlineError = $('<div class=\"pfbc-inline-error\" id=\"' + errorId + '\" role=\"alert\" aria-live=\"polite\"><div class=\"pfbc-error-message\">' + errorMessage + '</div></div>');
1898 -
1899 - // For file inputs with help text, insert error after help text
1900 - var helpText = field.siblings('.pfbc-help').last();
1901 - if (field.is('[type=\"file\"]') && helpText.length) {
1902 - helpText.after(inlineError);
1903 - } else {
1904 - field.after(inlineError);
1905 - }
1906 - }
1907 - } else {
1908 - parent.removeClass('pfbc-invalid pfbc-element-has-error');
1909 - field.attr('aria-invalid', 'false');
1910 - field.removeAttr('aria-describedby');
1911 - removeErrorAnimated(errorId);
1912 - }
1913 - });
1914 987 });
1915 988 // -->
1916 989 </script>
1917 -<iframe id="submit_target_{$js_buildid}" title="Notification Message" name="submit_target_{$js_buildid}" onload="_handle_ajax_submit_complete_{$js_buildid}()" onerror="_handle_ajax_submit_complete_{$js_buildid}()" style="width:0;height:0;border:0px solid #fff"></iframe>
990 +<iframe id="submit_target_{$js_buildid}" title="" name="submit_target_{$js_buildid}" onload="_handle_ajax_submit_complete_{$js_buildid}()" onerror="_handle_ajax_submit_complete_{$js_buildid}()" style="width:0;height:0;border:0px solid #fff"></iframe>
1918 991 JS;
1919 992 }
1920 993
1921 994 echo <<<JSREFERRER
@@ -1957,28 +1030,8 @@
1957 1030 echo 'jQuery(document).ready(function() {';
1958 1031 /*jQuery is used to set the focus of the form's initial element.*/
1959 1032 if(!in_array("focus", $this->prevent))
1960 1033 echo 'jQuery("#', $id, ' :input:visible:enabled:first").focus();';
1961 -
1962 - // Accessibility: Focus management for validation errors on page load
1963 - echo <<<JS
1964 -
1965 - // If there are errors on page load, focus the error summary or first invalid field
1966 - if (jQuery('.pfbc-error', '#{$id}').length > 0) {
1967 - setTimeout(function() {
1968 - var errorContainer = jQuery('.pfbc-error', '#{$id}').first();
1969 - errorContainer.attr('tabindex', '-1').focus();
1970 - }, 100);
1971 - } else if (jQuery('.pfbc-element-has-error', '#{$id}').length > 0) {
1972 - setTimeout(function() {
1973 - var firstInvalidField = jQuery('.pfbc-element-has-error :input:visible:enabled:first', '#{$id}').first();
1974 - if (firstInvalidField.length) {
1975 - firstInvalidField.focus();
1976 - }
1977 - }, 100);
1978 - }
1979 -
1980 -JS;
1981 1034
1982 1035 $this->view->jQueryDocumentReady();
1983 1036 foreach($this->elements as $element) {
1984 1037 $element->jQueryDocumentReady();
@@ -2020,202 +1073,9 @@
2020 1073 });
2021 1074 // -->
2022 1075 </script>
2023 1076 JS;
2024 -
2025 - // Add JavaScript for inline label floating behavior
2026 - if (strpos($this->attributes['class'], 'accua-form-view-inlinelabel') !== false) {
2027 - echo <<<INLINELABELJS
2028 -<script type="text/javascript">
2029 -<!--
2030 -jQuery(function($) {
2031 - var form = $('#{$this->attributes["id"]}');
2032 -
2033 - /**
2034 - * Inline Label Floating Behavior
2035 - *
2036 - * Handles the Material Design floating label animation:
2037 - * - Floats label up when field is focused
2038 - * - Keeps label up when field has value
2039 - * - Returns label to inline position when field is empty and unfocused
2040 - *
2041 - * Accessibility features:
2042 - * - Maintains proper ARIA relationships
2043 - * - Works with keyboard navigation
2044 - * - Compatible with screen readers
2045 - * - Supports autofill detection
2046 - */
2047 -
2048 - // Function to check if field has value
2049 - function hasValue(field) {
2050 - var val = field.val();
2051 - // For select elements, check if selected value is not empty
2052 - if (field.is('select')) {
2053 - return val !== null && val !== '' && val !== undefined;
2054 - }
2055 - // For date inputs, check if value is set (format: YYYY-MM-DD)
2056 - if (field.attr('type') === 'date') {
2057 - return val !== null && val !== '' && val !== undefined;
2058 - }
2059 - // For text inputs and textareas
2060 - return val !== null && val !== '' && val.trim() !== '';
2061 1077 }
2062 -
2063 - // Function to update wrapper state
2064 - function updateWrapperState(wrapper) {
2065 - var field = wrapper.find('.pfbc-textbox, .pfbc-textarea, .pfbc-select').first();
2066 - var postSelectWrapper = wrapper.find('.pfbc-post-select-wrapper');
2067 -
2068 - // Check if this is a post-select field
2069 - if (postSelectWrapper.length && postSelectWrapper.attr('data-enhanced') === 'true') {
2070 - // For enhanced post-select, check the hidden native select for value
2071 - var nativeSelect = postSelectWrapper.find('select');
2072 - var trigger = postSelectWrapper.find('.pfbc-post-select-trigger');
2073 - var container = postSelectWrapper.find('.pfbc-post-select-container');
2074 -
2075 - var isFocused = trigger.is(':focus') || container.hasClass('open');
2076 - var fieldHasValue = nativeSelect.length && hasValue(nativeSelect);
2077 -
2078 - wrapper.toggleClass('is-focused', isFocused);
2079 - wrapper.toggleClass('has-value', fieldHasValue);
2080 - return;
2081 - }
2082 -
2083 - if (!field.length) return;
2084 -
2085 - var isFocused = field.is(':focus');
2086 - var fieldHasValue = hasValue(field);
2087 -
2088 - // Update wrapper classes
2089 - wrapper.toggleClass('is-focused', isFocused);
2090 - wrapper.toggleClass('has-value', fieldHasValue);
2091 -
2092 - // For date inputs, add class directly to field for Firefox CSS support
2093 - if (field.attr('type') === 'date') {
2094 - field.toggleClass('has-value', fieldHasValue);
2095 - }
2096 -
2097 - // Accessibility: Update ARIA state
2098 - var label = wrapper.find('.pfbc-floating-label');
2099 - if (label.length) {
2100 - // Ensure label is always associated with field
2101 - var fieldId = field.attr('id');
2102 - if (fieldId && !field.attr('aria-labelledby')) {
2103 - // Label is already associated via for/id
2104 - // Additional ARIA not needed, but we ensure proper semantics
2105 - }
2106 - }
2107 - }
2108 -
2109 - // Initialize all inline label wrappers
2110 - form.find('.pfbc-inline-label-wrapper').each(function() {
2111 - var wrapper = $(this);
2112 - updateWrapperState(wrapper);
2113 - });
2114 -
2115 - // Handle focus events
2116 - form.on('focus', '.pfbc-inline-label-wrapper .pfbc-textbox, .pfbc-inline-label-wrapper .pfbc-textarea, .pfbc-inline-label-wrapper .pfbc-select', function() {
2117 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2118 - updateWrapperState(wrapper);
2119 - });
2120 -
2121 - // Handle blur events
2122 - form.on('blur', '.pfbc-inline-label-wrapper .pfbc-textbox, .pfbc-inline-label-wrapper .pfbc-textarea, .pfbc-inline-label-wrapper .pfbc-select', function() {
2123 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2124 - // Small delay to allow value to be set
2125 - setTimeout(function() {
2126 - updateWrapperState(wrapper);
2127 - }, 10);
2128 - });
2129 -
2130 - // Handle input/change events to detect value changes
2131 - form.on('input change', '.pfbc-inline-label-wrapper .pfbc-textbox, .pfbc-inline-label-wrapper .pfbc-textarea, .pfbc-inline-label-wrapper .pfbc-select', function() {
2132 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2133 - updateWrapperState(wrapper);
2134 - });
2135 -
2136 - // Handle post-select trigger focus/blur events
2137 - form.on('focus', '.pfbc-inline-label-wrapper .pfbc-post-select-trigger', function() {
2138 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2139 - updateWrapperState(wrapper);
2140 - });
2141 -
2142 - form.on('blur', '.pfbc-inline-label-wrapper .pfbc-post-select-trigger', function() {
2143 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2144 - setTimeout(function() {
2145 - updateWrapperState(wrapper);
2146 - }, 50);
2147 - });
2148 -
2149 - // Handle post-select value changes (native select change event)
2150 - form.on('change', '.pfbc-inline-label-wrapper .pfbc-post-select-wrapper select', function() {
2151 - var wrapper = $(this).closest('.pfbc-inline-label-wrapper');
2152 - updateWrapperState(wrapper);
2153 - });
2154 -
2155 - // Observe post-select container for open/close state changes
2156 - if (window.MutationObserver) {
2157 - form.find('.pfbc-inline-label-wrapper .pfbc-post-select-container').each(function() {
2158 - var container = this;
2159 - var wrapper = $(container).closest('.pfbc-inline-label-wrapper');
2160 - var containerObserver = new MutationObserver(function(mutations) {
2161 - mutations.forEach(function(mutation) {
2162 - if (mutation.attributeName === 'class') {
2163 - updateWrapperState(wrapper);
2164 - }
2165 - });
2166 - });
2167 - containerObserver.observe(container, {
2168 - attributes: true,
2169 - attributeFilter: ['class']
2170 - });
2171 - });
2172 - }
2173 -
2174 - // Handle browser autofill (multiple browser support)
2175 - // Chrome/Safari autofill detection
2176 - if (window.MutationObserver) {
2177 - var observer = new MutationObserver(function(mutations) {
2178 - mutations.forEach(function(mutation) {
2179 - if (mutation.attributeName === 'value' || mutation.attributeName === 'class') {
2180 - var target = $(mutation.target);
2181 - if (target.hasClass('pfbc-textbox') || target.hasClass('pfbc-textarea') || target.hasClass('pfbc-select')) {
2182 - var wrapper = target.closest('.pfbc-inline-label-wrapper');
2183 - if (wrapper.length) {
2184 - updateWrapperState(wrapper);
2185 - }
2186 - }
2187 - }
2188 - });
2189 - });
2190 -
2191 - form.find('.pfbc-inline-label-wrapper .pfbc-textbox, .pfbc-inline-label-wrapper .pfbc-textarea, .pfbc-inline-label-wrapper .pfbc-select').each(function() {
2192 - observer.observe(this, {
2193 - attributes: true,
2194 - attributeFilter: ['value', 'class']
2195 - });
2196 - });
2197 - }
2198 -
2199 - // Fallback autofill detection with animation frame checking
2200 - setTimeout(function() {
2201 - form.find('.pfbc-inline-label-wrapper').each(function() {
2202 - updateWrapperState($(this));
2203 - });
2204 - }, 100);
2205 -
2206 - // Additional check for autofill after a short delay
2207 - setTimeout(function() {
2208 - form.find('.pfbc-inline-label-wrapper').each(function() {
2209 - updateWrapperState($(this));
2210 - });
2211 - }, 500);
2212 -});
2213 -// -->
2214 -</script>
2215 -INLINELABELJS;
2216 - }
2217 - }
2218 1078
2219 1079 protected function renderCSS() {
2220 1080
2221 1081 }
@@ -2269,8 +1129,9 @@
2269 1129
2270 1130 public static function ajaxSubmit() {
2271 1131 $ret = array(
2272 1132 'valid' => false,
1133 + 'messages' => self::getSubmittedMessages(),
2273 1134 'jsuuid' => self::$rawData['_AccuaForm_jsuuid'],
2274 1135 'buildID' => self::$submittedBuildID,
2275 1136 'files' => array(),
2276 1137 );
@@ -2277,16 +1138,9 @@
2277 1138 if ($ret['submitted'] = self::isSubmit()){
2278 1139 if ($ret['valid'] = self::isValid()) {
2279 1140
2280 1141 } else {
2281 - $errorResponse = self::getAjaxErrorResponse();
2282 - // Support modern error format with both flat list and structured data for ARIA
2283 - if (is_array($errorResponse) && isset($errorResponse['errors'])) {
2284 - $ret = array_merge($ret, $errorResponse);
2285 - } else {
2286 - // Backwards compatibility: old format returned just the flat array
2287 - $ret['errors'] = $errorResponse;
2288 - }
1142 + $ret['errors'] = self::getAjaxErrorResponse();
2289 1143 }
2290 1144 $form = self::$submittedForm;
2291 1145 foreach ($form->files as $fieldname => $file) {
2292 1146 if (!empty($file['name'])) {
@@ -2293,14 +1147,8 @@
2293 1147 $ret['files'][$fieldname] = $form->getElementByName($fieldname)->getAlreadySubmittedText();
2294 1148 }
2295 1149 }
2296 1150 $ret += $form->wp_save();
2297 - // Get messages AFTER isValid() and wp_save() have completed
2298 - // This ensures email sending results are captured in the messages
2299 - $ret['messages'] = self::getSubmittedMessages(self::$submittedID);
2300 - } else {
2301 - // Form not submitted yet - no messages to show
2302 - $ret['messages'] = '';
2303 1151 }
2304 1152 return $ret;
2305 1153 }
2306 1154 }