From eb6df9794045d4ecdc0510a21396b7f98b9cf17e Mon Sep 17 00:00:00 2001
From: Abdi Tolessa <41271840+AbdiTolesa@users.noreply.github.com>
Date: Fri, 8 Aug 2025 13:31:56 +0300
Subject: [PATCH 01/20] Improve form errors accessibility
---
classes/helpers/FrmFormsHelper.php | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/classes/helpers/FrmFormsHelper.php b/classes/helpers/FrmFormsHelper.php
index d496d523eb..36c7f875a1 100644
--- a/classes/helpers/FrmFormsHelper.php
+++ b/classes/helpers/FrmFormsHelper.php
@@ -263,6 +263,12 @@ public static function get_invalid_error_message( $args ) {
$frm_settings = FrmAppHelper::get_settings( $settings_args );
$invalid_msg = do_shortcode( $frm_settings->invalid_msg );
+ $field_error_messages = '';
+ foreach ( $args['errors'] as $field_id => $error ) {
+ $field = (array) FrmField::getOne( str_replace( 'field', '', $field_id ) );
+ $field_error_messages .= '
' . $error . '';
+ }
+ $invalid_msg .= $field_error_messages ? '' . $field_error_messages . '
' : '';
return apply_filters( 'frm_invalid_error_message', $invalid_msg, $args );
}
From 88c94349489b17978ec8791d138d4bac83caf610 Mon Sep 17 00:00:00 2001
From: Abdi Tolessa <41271840+AbdiTolesa@users.noreply.github.com>
Date: Fri, 8 Aug 2025 14:40:53 +0300
Subject: [PATCH 02/20] Move logic that creates field error messages with links
to a new function
---
classes/helpers/FrmFormsHelper.php | 51 +++++++++++++++++++++++++++---
1 file changed, 46 insertions(+), 5 deletions(-)
diff --git a/classes/helpers/FrmFormsHelper.php b/classes/helpers/FrmFormsHelper.php
index 36c7f875a1..fe16dbcf41 100644
--- a/classes/helpers/FrmFormsHelper.php
+++ b/classes/helpers/FrmFormsHelper.php
@@ -263,15 +263,56 @@ public static function get_invalid_error_message( $args ) {
$frm_settings = FrmAppHelper::get_settings( $settings_args );
$invalid_msg = do_shortcode( $frm_settings->invalid_msg );
- $field_error_messages = '';
- foreach ( $args['errors'] as $field_id => $error ) {
- $field = (array) FrmField::getOne( str_replace( 'field', '', $field_id ) );
- $field_error_messages .= '' . $error . '';
- }
+ $field_error_messages = self::get_field_error_messages( $args );
$invalid_msg .= $field_error_messages ? '' . $field_error_messages . '
' : '';
return apply_filters( 'frm_invalid_error_message', $invalid_msg, $args );
}
+ /**
+ * @since x.x
+ *
+ * @param array $args
+ * @return string
+ */
+ private static function get_field_error_messages( $args ) {
+ $field_error_messages = '';
+ if ( empty( $args['errors'] ) ) {
+ return $field_error_messages;
+ }
+ $field_ids = array_map(
+ function ( $field_plus_id ) {
+ $field_id = str_replace( 'field', '', $field_plus_id );
+ if ( strpos( $field_id, '-' ) !== false ) {
+ $field_id = explode( '-', $field_id )[0];
+ }
+ return $field_id;
+ },
+ array_keys( $args['errors'] )
+ );
+ $field_keys = FrmDb::get_results( 'frm_fields', array( 'id' => $field_ids ), 'id,field_key,type' );
+ foreach ( $args['errors'] as $field_plus_id => $error ) {
+ $field_id = str_replace( 'field', '', $field_plus_id );
+ $row = '';
+ if ( strpos( $field_id, '-' ) !== false ) {
+ $field_id_parts = explode( '-', $field_id );
+ if ( count( $field_id_parts ) === 3 ) {
+ $field_id = $field_id_parts[0];
+ $row = '-' . $field_id_parts[2];
+ }
+ }
+ $index = array_search( $field_id, array_column( $field_keys, 'id' ), true );
+ if ( false === $index ) {
+ continue;
+ }
+ $html_id = 'field_' . $field_keys[ $index ]->field_key . $row;
+ if ( in_array( $field_keys[ $index ]->type, array( 'checkbox', 'radio' ), true ) ) {
+ $html_id .= '-0'; // Focus on the first option.
+ }
+ $field_error_messages .= '' . $error . '';
+ }
+ return $field_error_messages;
+ }
+
/**
* @param array $atts {
* The success message details.
From 924ccf8b0f2433bba1e3c3b5cd2d378ee0d44e93 Mon Sep 17 00:00:00 2001
From: Abdi Tolessa <41271840+AbdiTolesa@users.noreply.github.com>
Date: Fri, 8 Aug 2025 14:45:30 +0300
Subject: [PATCH 03/20] Fix PHPCS errpr
---
classes/helpers/FrmFormsHelper.php | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/classes/helpers/FrmFormsHelper.php b/classes/helpers/FrmFormsHelper.php
index fe16dbcf41..ab3adfca7f 100644
--- a/classes/helpers/FrmFormsHelper.php
+++ b/classes/helpers/FrmFormsHelper.php
@@ -306,10 +306,11 @@ function ( $field_plus_id ) {
}
$html_id = 'field_' . $field_keys[ $index ]->field_key . $row;
if ( in_array( $field_keys[ $index ]->type, array( 'checkbox', 'radio' ), true ) ) {
- $html_id .= '-0'; // Focus on the first option.
+ // Focus on the first option.
+ $html_id .= '-0';
}
$field_error_messages .= '' . $error . '';
- }
+ }//end foreach
return $field_error_messages;
}
From d79ac345403b8933fef6e1673aa39f8fc8a5f306 Mon Sep 17 00:00:00 2001
From: Abdi Tolessa <41271840+AbdiTolesa@users.noreply.github.com>
Date: Fri, 8 Aug 2025 15:43:29 +0300
Subject: [PATCH 04/20] Fix equal sign alignment
---
classes/helpers/FrmFormsHelper.php | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/classes/helpers/FrmFormsHelper.php b/classes/helpers/FrmFormsHelper.php
index ab3adfca7f..04f040063b 100644
--- a/classes/helpers/FrmFormsHelper.php
+++ b/classes/helpers/FrmFormsHelper.php
@@ -263,8 +263,9 @@ public static function get_invalid_error_message( $args ) {
$frm_settings = FrmAppHelper::get_settings( $settings_args );
$invalid_msg = do_shortcode( $frm_settings->invalid_msg );
+
$field_error_messages = self::get_field_error_messages( $args );
- $invalid_msg .= $field_error_messages ? '' . $field_error_messages . '
' : '';
+ $invalid_msg .= $field_error_messages ? '' . $field_error_messages . '
' : '';
return apply_filters( 'frm_invalid_error_message', $invalid_msg, $args );
}
From ce3144c0e4022bcd77370f6a7c21e4d205b7b025 Mon Sep 17 00:00:00 2001
From: Abdi Tolessa <41271840+AbdiTolesa@users.noreply.github.com>
Date: Fri, 8 Aug 2025 21:45:24 +0300
Subject: [PATCH 05/20] Add comment to function
---
classes/helpers/FrmFormsHelper.php | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/classes/helpers/FrmFormsHelper.php b/classes/helpers/FrmFormsHelper.php
index 04f040063b..e8bfd13fc6 100644
--- a/classes/helpers/FrmFormsHelper.php
+++ b/classes/helpers/FrmFormsHelper.php
@@ -264,18 +264,20 @@ public static function get_invalid_error_message( $args ) {
$frm_settings = FrmAppHelper::get_settings( $settings_args );
$invalid_msg = do_shortcode( $frm_settings->invalid_msg );
- $field_error_messages = self::get_field_error_messages( $args );
+ $field_error_messages = self::get_clickable_field_error_messages( $args );
$invalid_msg .= $field_error_messages ? '' . $field_error_messages . '
' : '';
return apply_filters( 'frm_invalid_error_message', $invalid_msg, $args );
}
/**
+ * Get clickable field error messages.
+ *
* @since x.x
*
* @param array $args
* @return string
*/
- private static function get_field_error_messages( $args ) {
+ private static function get_clickable_field_error_messages( $args ) {
$field_error_messages = '';
if ( empty( $args['errors'] ) ) {
return $field_error_messages;
@@ -307,7 +309,7 @@ function ( $field_plus_id ) {
}
$html_id = 'field_' . $field_keys[ $index ]->field_key . $row;
if ( in_array( $field_keys[ $index ]->type, array( 'checkbox', 'radio' ), true ) ) {
- // Focus on the first option.
+ // Needed to focus on the first option when error link is clicked.
$html_id .= '-0';
}
$field_error_messages .= '' . $error . '';
From e7df4102bc058d1f09ec9f5953668b1afec53eae Mon Sep 17 00:00:00 2001
From: Abdi Tolessa <41271840+AbdiTolesa@users.noreply.github.com>
Date: Fri, 5 Dec 2025 15:19:01 +0300
Subject: [PATCH 06/20] Add CSS rules for form errors list
---
classes/helpers/FrmFormsHelper.php | 2 +-
css/_single_theme.css.php | 17 +++++++++++++++++
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/classes/helpers/FrmFormsHelper.php b/classes/helpers/FrmFormsHelper.php
index e8bfd13fc6..5d26f507bf 100644
--- a/classes/helpers/FrmFormsHelper.php
+++ b/classes/helpers/FrmFormsHelper.php
@@ -262,7 +262,7 @@ public static function get_invalid_error_message( $args ) {
}
$frm_settings = FrmAppHelper::get_settings( $settings_args );
- $invalid_msg = do_shortcode( $frm_settings->invalid_msg );
+ $invalid_msg = '' . do_shortcode( $frm_settings->invalid_msg ) . '';
$field_error_messages = self::get_clickable_field_error_messages( $args );
$invalid_msg .= $field_error_messages ? '' . $field_error_messages . '
' : '';
diff --git a/css/_single_theme.css.php b/css/_single_theme.css.php
index 7c45459cc5..d5b9d5ee3d 100644
--- a/css/_single_theme.css.php
+++ b/css/_single_theme.css.php
@@ -379,6 +379,23 @@
margin-bottom:;
}
+. .frm_error_style span{
+ font-weight: bold;;
+}
+
+. .frm_error_style ul{
+ list-style: inside;;
+ color: ;
+}
+
+. .frm_error_style ul li a{
+ color: ;
+}
+
+. .frm_error_style ul li a:hover{
+ text-decoration: underline;;
+}
+
. #frm_loading .progress-striped .progress-bar{
background-image:linear-gradient(45deg, 25%, rgba(0, 0, 0, 0) 25%, rgba(0, 0, 0, 0) 50%, 50%, 75%, rgba(0, 0, 0, 0) 75%, rgba(0, 0, 0, 0));
}
From ba68f4aed709ff813617b0bade547fc747ed7014 Mon Sep 17 00:00:00 2001
From: Abdi Tolessa <41271840+AbdiTolesa@users.noreply.github.com>
Date: Fri, 5 Dec 2025 15:23:55 +0300
Subject: [PATCH 07/20] Fix PHP CS errors
---
classes/helpers/FrmFormsHelper.php | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/classes/helpers/FrmFormsHelper.php b/classes/helpers/FrmFormsHelper.php
index 4eeb7fb66a..11c106c261 100644
--- a/classes/helpers/FrmFormsHelper.php
+++ b/classes/helpers/FrmFormsHelper.php
@@ -312,16 +312,20 @@ public static function get_invalid_error_message( $args ) {
* @since x.x
*
* @param array $args
+ *
* @return string
*/
private static function get_clickable_field_error_messages( $args ) {
$field_error_messages = '';
+
if ( empty( $args['errors'] ) ) {
return $field_error_messages;
}
+
$field_ids = array_map(
function ( $field_plus_id ) {
$field_id = str_replace( 'field', '', $field_plus_id );
+
if ( strpos( $field_id, '-' ) !== false ) {
$field_id = explode( '-', $field_id )[0];
}
@@ -330,25 +334,33 @@ function ( $field_plus_id ) {
array_keys( $args['errors'] )
);
$field_keys = FrmDb::get_results( 'frm_fields', array( 'id' => $field_ids ), 'id,field_key,type' );
+
foreach ( $args['errors'] as $field_plus_id => $error ) {
$field_id = str_replace( 'field', '', $field_plus_id );
$row = '';
+
if ( strpos( $field_id, '-' ) !== false ) {
$field_id_parts = explode( '-', $field_id );
+
if ( count( $field_id_parts ) === 3 ) {
$field_id = $field_id_parts[0];
$row = '-' . $field_id_parts[2];
}
}
+
$index = array_search( $field_id, array_column( $field_keys, 'id' ), true );
+
if ( false === $index ) {
continue;
}
+
$html_id = 'field_' . $field_keys[ $index ]->field_key . $row;
+
if ( in_array( $field_keys[ $index ]->type, array( 'checkbox', 'radio' ), true ) ) {
// Needed to focus on the first option when error link is clicked.
$html_id .= '-0';
}
+
$field_error_messages .= '' . $error . '';
}//end foreach
return $field_error_messages;
From 401b972e88d30a3659ce141b49d519771d2d1000 Mon Sep 17 00:00:00 2001
From: Abdi Tolessa <41271840+AbdiTolesa@users.noreply.github.com>
Date: Fri, 5 Dec 2025 18:14:40 +0300
Subject: [PATCH 08/20] Make code more readable
---
classes/helpers/FrmFormsHelper.php | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/classes/helpers/FrmFormsHelper.php b/classes/helpers/FrmFormsHelper.php
index 11c106c261..a538fb60df 100644
--- a/classes/helpers/FrmFormsHelper.php
+++ b/classes/helpers/FrmFormsHelper.php
@@ -299,10 +299,13 @@ public static function get_invalid_error_message( $args ) {
}
$frm_settings = FrmAppHelper::get_settings( $settings_args );
- $invalid_msg = '' . do_shortcode( $frm_settings->invalid_msg ) . '';
$field_error_messages = self::get_clickable_field_error_messages( $args );
- $invalid_msg .= $field_error_messages ? '' . $field_error_messages . '
' : '';
+
+ $invalid_msg = '' . do_shortcode( $frm_settings->invalid_msg ) . '';
+ if ( $field_error_messages ) {
+ $invalid_msg .= "";
+ }
return apply_filters( 'frm_invalid_error_message', $invalid_msg, $args );
}
From 4a03c7da951c6a7eca4a071a1a016e6f132c3280 Mon Sep 17 00:00:00 2001
From: Abdi Tolessa <41271840+AbdiTolesa@users.noreply.github.com>
Date: Fri, 5 Dec 2025 20:15:15 +0300
Subject: [PATCH 09/20] Fix PHP CS errors
---
classes/helpers/FrmFormsHelper.php | 1 +
1 file changed, 1 insertion(+)
diff --git a/classes/helpers/FrmFormsHelper.php b/classes/helpers/FrmFormsHelper.php
index a538fb60df..34c71f5df5 100644
--- a/classes/helpers/FrmFormsHelper.php
+++ b/classes/helpers/FrmFormsHelper.php
@@ -303,6 +303,7 @@ public static function get_invalid_error_message( $args ) {
$field_error_messages = self::get_clickable_field_error_messages( $args );
$invalid_msg = '' . do_shortcode( $frm_settings->invalid_msg ) . '';
+
if ( $field_error_messages ) {
$invalid_msg .= "";
}
From b2603a4d9912aa7a1de639804900dc28f8206466 Mon Sep 17 00:00:00 2001
From: Abdi Tolessa <41271840+AbdiTolesa@users.noreply.github.com>
Date: Fri, 5 Dec 2025 20:35:25 +0300
Subject: [PATCH 10/20] Show field errors summary for ajax submit
---
classes/controllers/FrmEntriesAJAXSubmitController.php | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/classes/controllers/FrmEntriesAJAXSubmitController.php b/classes/controllers/FrmEntriesAJAXSubmitController.php
index 33b1af4b17..91c4fc99ba 100644
--- a/classes/controllers/FrmEntriesAJAXSubmitController.php
+++ b/classes/controllers/FrmEntriesAJAXSubmitController.php
@@ -96,7 +96,12 @@ public static function ajax_create() {
}
$response['errors'] = $obj;
- $invalid_msg = FrmFormsHelper::get_invalid_error_message( array( 'form' => $form ) );
+ $invalid_msg = FrmFormsHelper::get_invalid_error_message(
+ array(
+ 'form' => $form,
+ 'errors' => $errors,
+ )
+ );
$response['error_message'] = FrmFormsHelper::get_success_message(
array(
'message' => $invalid_msg,
From b28445d791d5b75d36b19e4f235de396005d0937 Mon Sep 17 00:00:00 2001
From: Mike Letellier
Date: Wed, 5 Aug 2026 14:32:50 -0300
Subject: [PATCH 11/20] Issue 5886 improvements
---
classes/helpers/FrmFormsHelper.php | 58 +++++----
css/_single_theme.css.php | 10 +-
tests/phpunit/forms/test_FrmFormsHelper.php | 133 ++++++++++++++++++++
3 files changed, 171 insertions(+), 30 deletions(-)
diff --git a/classes/helpers/FrmFormsHelper.php b/classes/helpers/FrmFormsHelper.php
index 0499d44384..a632b729a2 100644
--- a/classes/helpers/FrmFormsHelper.php
+++ b/classes/helpers/FrmFormsHelper.php
@@ -288,15 +288,14 @@ public static function get_invalid_error_message( $args ) {
$settings_args['current_form'] = $args['form']->id;
}
- $frm_settings = FrmAppHelper::get_settings( $settings_args );
-
+ $frm_settings = FrmAppHelper::get_settings( $settings_args );
$field_error_messages = self::get_clickable_field_error_messages( $args );
-
- $invalid_msg = '' . do_shortcode( $frm_settings->invalid_msg ) . '';
+ $invalid_msg = '' . do_shortcode( $frm_settings->invalid_msg ) . '';
if ( $field_error_messages ) {
$invalid_msg .= "";
}
+
return apply_filters( 'frm_invalid_error_message', $invalid_msg, $args );
}
@@ -310,27 +309,17 @@ public static function get_invalid_error_message( $args ) {
* @return string
*/
private static function get_clickable_field_error_messages( $args ) {
- $field_error_messages = '';
-
if ( empty( $args['errors'] ) ) {
- return $field_error_messages;
+ return '';
}
- $field_ids = array_map(
- function ( $field_plus_id ) {
- $field_id = str_replace( 'field', '', $field_plus_id );
-
- if ( strpos( $field_id, '-' ) !== false ) {
- $field_id = explode( '-', $field_id )[0];
- }
- return $field_id;
- },
- array_keys( $args['errors'] )
- );
- $field_keys = FrmDb::get_results( 'frm_fields', array( 'id' => $field_ids ), 'id,field_key,type' );
+ // Parse each error key once into its field ID and repeater row suffix, skipping
+ // non-field errors like 'form' or 'spam' that have no input to link to.
+ $parsed_errors = array();
+ $field_ids = array();
foreach ( $args['errors'] as $field_plus_id => $error ) {
- $field_id = str_replace( 'field', '', $field_plus_id );
+ $field_id = preg_replace( '/^field/', '', $field_plus_id );
$row = '';
if ( strpos( $field_id, '-' ) !== false ) {
@@ -342,21 +331,40 @@ function ( $field_plus_id ) {
}
}
- $index = array_search( $field_id, array_column( $field_keys, 'id' ), true );
+ if ( ! is_numeric( $field_id ) ) {
+ continue;
+ }
+
+ $field_ids[] = (int) $field_id;
+ $parsed_errors[] = compact( 'field_id', 'row', 'error' );
+ }
+
+ if ( ! $field_ids ) {
+ return '';
+ }
+
+ $fields = FrmDb::get_results( 'frm_fields', array( 'id' => $field_ids ), 'id,field_key,type' );
+ $ids = array_map( 'intval', array_column( $fields, 'id' ) );
+ $field_error_messages = '';
+
+ foreach ( $parsed_errors as $parsed_error ) {
+ $index = array_search( (int) $parsed_error['field_id'], $ids, true );
if ( false === $index ) {
continue;
}
- $html_id = 'field_' . $field_keys[ $index ]->field_key . $row;
+ $field = $fields[ $index ];
+ $html_id = 'field_' . $field->field_key . $parsed_error['row'];
- if ( in_array( $field_keys[ $index ]->type, array( 'checkbox', 'radio' ), true ) ) {
- // Needed to focus on the first option when error link is clicked.
+ if ( in_array( $field->type, array( 'checkbox', 'radio' ), true ) ) {
+ // Needed to focus on the first option when the error link is clicked.
$html_id .= '-0';
}
- $field_error_messages .= '' . $error . '';
+ $field_error_messages .= '' . esc_html( $parsed_error['error'] ) . '';
}//end foreach
+
return $field_error_messages;
}
diff --git a/css/_single_theme.css.php b/css/_single_theme.css.php
index 90ed2bf442..3906dcc6b6 100644
--- a/css/_single_theme.css.php
+++ b/css/_single_theme.css.php
@@ -423,20 +423,20 @@
}
. .frm_error_style span{
- font-weight: bold;;
+ font-weight: bold;
}
. .frm_error_style ul{
- list-style: inside;;
- color: ;
+ list-style: inside;
+ color: var(--error-text);
}
. .frm_error_style ul li a{
- color: ;
+ color: var(--error-text);
}
. .frm_error_style ul li a:hover{
- text-decoration: underline;;
+ text-decoration: underline;
}
diff --git a/tests/phpunit/forms/test_FrmFormsHelper.php b/tests/phpunit/forms/test_FrmFormsHelper.php
index 41a294d59d..0204b3e833 100644
--- a/tests/phpunit/forms/test_FrmFormsHelper.php
+++ b/tests/phpunit/forms/test_FrmFormsHelper.php
@@ -146,4 +146,137 @@ private function create_form_with_custom_style_value( $custom_style ) {
)
);
}
+
+ /**
+ * The invalid error message should include a list of links that jump to each field that failed validation.
+ *
+ * @covers FrmFormsHelper::get_invalid_error_message
+ */
+ public function test_get_invalid_error_message_builds_clickable_field_links() {
+ $this->form = $this->factory->form->create_and_get();
+
+ $text_id = $this->create_field_with_key( 'text', 'my_text' );
+ $select_id = $this->create_field_with_key( 'select', 'my_select' );
+ $checkbox_id = $this->create_field_with_key( 'checkbox', 'my_checkbox' );
+ $radio_id = $this->create_field_with_key( 'radio', 'my_radio' );
+
+ $message = FrmFormsHelper::get_invalid_error_message(
+ array(
+ 'form' => $this->form,
+ 'errors' => array(
+ 'field' . $text_id => 'Text is required',
+ 'field' . $select_id => 'Select is required',
+ 'field' . $checkbox_id => 'Checkbox is required',
+ 'field' . $radio_id => 'Radio is required',
+ ),
+ )
+ );
+
+ // The base invalid message is wrapped in a span, and the links are inside a list.
+ $this->assertStringContainsString( '', $message );
+ $this->assertStringContainsString( '- Text is required
', $message );
+ $this->assertStringContainsString( '- Select is required
', $message );
+
+ // Checkbox and radio links target the first option so focus lands on a real input.
+ $this->assertStringContainsString( '- Checkbox is required
', $message );
+ $this->assertStringContainsString( '- Radio is required
', $message );
+ }
+
+ /**
+ * Non-field errors such as 'form' or 'spam' have no input to link to and should be skipped.
+ *
+ * @covers FrmFormsHelper::get_invalid_error_message
+ */
+ public function test_get_invalid_error_message_skips_non_field_errors() {
+ $this->form = $this->factory->form->create_and_get();
+ $text_id = $this->create_field_with_key( 'text', 'linkable_text' );
+
+ $message = FrmFormsHelper::get_invalid_error_message(
+ array(
+ 'form' => $this->form,
+ 'errors' => array(
+ 'form' => 'There was a problem with your submission.',
+ 'spam' => 'Your entry appears to be spam!',
+ 'field' . $text_id => 'Text is required',
+ ),
+ )
+ );
+
+ $this->assertStringContainsString( '- Text is required
', $message );
+ $this->assertStringNotContainsString( 'appears to be spam', $message );
+ $this->assertStringNotContainsString( 'Repeated text is required', $message );
+ }
+
+ /**
+ * @param string $type
+ * @param string $field_key
+ *
+ * @return int
+ */
+ private function create_field_with_key( $type, $field_key ) {
+ return $this->factory->field->create(
+ array(
+ 'form_id' => $this->form->id,
+ 'type' => $type,
+ 'field_key' => $field_key,
+ )
+ );
+ }
}
From c24a1da81e2e47995e4d24553352f48a7638abb9 Mon Sep 17 00:00:00 2001
From: Mike Letellier
Date: Wed, 5 Aug 2026 15:04:43 -0300
Subject: [PATCH 12/20] Use role="alert" for AJAX errors, use kses on error
message
---
.../FrmEntriesAJAXSubmitController.php | 1 +
classes/helpers/FrmFormsHelper.php | 25 +++++-
tests/phpunit/forms/test_FrmFormsHelper.php | 83 +++++++++++++++++--
3 files changed, 101 insertions(+), 8 deletions(-)
diff --git a/classes/controllers/FrmEntriesAJAXSubmitController.php b/classes/controllers/FrmEntriesAJAXSubmitController.php
index 21286af743..3ce89a3f18 100644
--- a/classes/controllers/FrmEntriesAJAXSubmitController.php
+++ b/classes/controllers/FrmEntriesAJAXSubmitController.php
@@ -85,6 +85,7 @@ public static function ajax_create() {
'form' => $form,
'entry_id' => 0,
'class' => FrmFormsHelper::form_error_class(),
+ 'role' => 'alert',
)
);
} else {
diff --git a/classes/helpers/FrmFormsHelper.php b/classes/helpers/FrmFormsHelper.php
index a632b729a2..7439291d95 100644
--- a/classes/helpers/FrmFormsHelper.php
+++ b/classes/helpers/FrmFormsHelper.php
@@ -347,6 +347,11 @@ private static function get_clickable_field_error_messages( $args ) {
$ids = array_map( 'intval', array_column( $fields, 'id' ) );
$field_error_messages = '';
+ // Error messages are admin configured and may include shortcodes, so allow the same
+ // inline formatting Formidable permits elsewhere while stripping anything unsafe. Anchors
+ // are intentionally excluded so a message link cannot nest inside the summary link.
+ $allowed_tags = array( 'strong', 'b', 'em', 'i', 'u', 'span', 'code', 'br', 'sub', 'sup', 'mark', 'small' );
+
foreach ( $parsed_errors as $parsed_error ) {
$index = array_search( (int) $parsed_error['field_id'], $ids, true );
@@ -354,7 +359,16 @@ private static function get_clickable_field_error_messages( $args ) {
continue;
}
- $field = $fields[ $index ];
+ $field = $fields[ $index ];
+ $error = FrmAppHelper::kses( $parsed_error['error'], $allowed_tags );
+
+ if ( in_array( $field->type, array( 'hidden', 'user_id' ), true ) ) {
+ // These render as hidden inputs, which cannot receive focus, so list the
+ // error without a link that would go nowhere when clicked.
+ $field_error_messages .= '- ' . $error . '
';
+ continue;
+ }
+
$html_id = 'field_' . $field->field_key . $parsed_error['row'];
if ( in_array( $field->type, array( 'checkbox', 'radio' ), true ) ) {
@@ -362,7 +376,7 @@ private static function get_clickable_field_error_messages( $args ) {
$html_id .= '-0';
}
- $field_error_messages .= '- ' . esc_html( $parsed_error['error'] ) . '
';
+ $field_error_messages .= '- ' . $error . '
';
}//end foreach
return $field_error_messages;
@@ -376,6 +390,9 @@ private static function get_clickable_field_error_messages( $args ) {
* @type stdClass $form
* @type int $entry_id
* @type string $class
+ * @type string $role Optional. ARIA live region role for the wrapper. Defaults to 'status'.
+ * Pass 'alert' when the message reports a validation error so it is
+ * announced assertively, matching the non-ajax error wrapper.
* }
*
* @return string
@@ -403,7 +420,9 @@ public static function get_success_message( $atts ) {
}
$message = do_shortcode( $message );
- return '' . $message . '
';
+ $role = isset( $atts['role'] ) ? $atts['role'] : 'status';
+
+ return '' . $message . '
';
}
/**
diff --git a/tests/phpunit/forms/test_FrmFormsHelper.php b/tests/phpunit/forms/test_FrmFormsHelper.php
index 0204b3e833..f9bdb81661 100644
--- a/tests/phpunit/forms/test_FrmFormsHelper.php
+++ b/tests/phpunit/forms/test_FrmFormsHelper.php
@@ -222,11 +222,12 @@ public function test_get_invalid_error_message_without_errors_has_no_list() {
}
/**
- * Error text and IDs must be escaped so a malicious field key or message cannot inject markup.
+ * Error messages may contain admin HTML, so unsafe markup is stripped while safe inline
+ * formatting is kept, and anchors are removed so they cannot nest inside the summary link.
*
* @covers FrmFormsHelper::get_invalid_error_message
*/
- public function test_get_invalid_error_message_escapes_output() {
+ public function test_get_invalid_error_message_sanitizes_error_html() {
$this->form = $this->factory->form->create_and_get();
$text_id = $this->create_field_with_key( 'text', 'escape_test' );
@@ -234,13 +235,53 @@ public function test_get_invalid_error_message_escapes_output() {
array(
'form' => $this->form,
'errors' => array(
- 'field' . $text_id => '',
+ 'field' . $text_id => 'Name is requiredx',
),
)
);
- $this->assertStringNotContainsString( '', $message );
- $this->assertStringContainsString( '<script>', $message );
+ // Scripts are removed entirely.
+ $this->assertStringNotContainsString( '