From 5fca0b1bba679af8bd0287c621a431a5d561506e Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Tue, 22 Sep 2026 21:59:34 +0200 Subject: [PATCH 01/10] Refactor settings_errors() to use wp_admin_notice() internally. --- src/wp-admin/includes/template.php | 43 +++++++++++++++++------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/wp-admin/includes/template.php b/src/wp-admin/includes/template.php index b535b3acbfc66..c776d12ce1392 100644 --- a/src/wp-admin/includes/template.php +++ b/src/wp-admin/includes/template.php @@ -1974,6 +1974,9 @@ function get_settings_errors( $setting = '', $sanitize = false ) { * @since 3.0.0 * @since 5.3.0 Legacy `error` and `updated` CSS classes are mapped to * `notice-error` and `notice-success`. + * @since 7.2.0 Uses `wp_admin_notice()` to echo the notices so that the CSS + * classes are handled based on the notice type and the hook + * `wp_admin_notice` is fired, for consistency with other notices. * * @param string $setting Optional slug title of a specific setting whose errors you want. * @param bool $sanitize Whether to re-sanitize the setting value before returning errors. @@ -1992,32 +1995,36 @@ function settings_errors( $setting = '', $sanitize = false, $hide_on_update = fa return; } - $output = ''; - foreach ( $settings_errors as $key => $details ) { - if ( 'updated' === $details['type'] ) { - $details['type'] = 'success'; - } + $type = $details['type']; - if ( in_array( $details['type'], array( 'error', 'success', 'warning', 'info' ), true ) ) { - $details['type'] = 'notice-' . $details['type']; + if ( 'updated' === $type ) { + $type = 'success'; } - $css_id = sprintf( + /* + * Note that the setting error code contains underscores, for example: + * `settings_updated`. This will build a CSS ID selector that contains + * underscores, which are currently not allowed by the WordPress CSS + * Coding Standards. Kept for backwards compatibility. + */ + $css_id = sprintf( 'setting-error-%s', - esc_attr( $details['code'] ) - ); - $css_class = sprintf( - 'notice %s settings-error is-dismissible', - esc_attr( $details['type'] ) + esc_attr( $details['code'] ), ); - $output .= "
\n"; - $output .= "

{$details['message']}

"; - $output .= "
\n"; + wp_admin_notice( + "{$details['message']}", + array( + 'type' => $type, + 'code' => $details['code'], + 'id' => $css_id, + 'paragraph_wrap' => true, + 'dismissible' => true, + 'additional_classes' => array( 'settings-error' ), + ) + ); } - - echo $output; } /** From 4900e9c6f752d0920a3a4e86aa127a35847d6c96 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Wed, 23 Sep 2026 00:34:30 +0200 Subject: [PATCH 02/10] Adjustments to make tests pass and for backward compatibility. --- src/wp-admin/includes/template.php | 17 +++++++++++++++-- src/wp-includes/user.php | 1 + tests/phpunit/tests/admin/includesTemplate.php | 7 ++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/includes/template.php b/src/wp-admin/includes/template.php index c776d12ce1392..53fa5cfe7721e 100644 --- a/src/wp-admin/includes/template.php +++ b/src/wp-admin/includes/template.php @@ -1996,12 +1996,25 @@ function settings_errors( $setting = '', $sanitize = false, $hide_on_update = fa } foreach ( $settings_errors as $key => $details ) { - $type = $details['type']; + // Backward compatibility for passed types that may wrongly contain `notice-`. + $type = str_replace( 'notice-', '', trim( $details['type'] ) ); if ( 'updated' === $type ) { $type = 'success'; } + $additional_classes = array( 'settings-error' ); + + // Backward compatibility for types that are strings with spaces see Trac ticket #44941. + // For example, a string like `error my-own-css-class hello world`. + if ( str_contains( $type, ' ' ) ) { + // Use ths substrings as additional classes. + $additional_classes = explode( ' ', $type . ' settings-error' ); + // Set the type to empty string. + $type = ''; + + } + /* * Note that the setting error code contains underscores, for example: * `settings_updated`. This will build a CSS ID selector that contains @@ -2021,7 +2034,7 @@ function settings_errors( $setting = '', $sanitize = false, $hide_on_update = fa 'id' => $css_id, 'paragraph_wrap' => true, 'dismissible' => true, - 'additional_classes' => array( 'settings-error' ), + 'additional_classes' => $additional_classes, ) ); } diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 92d091165525b..9f81e9845ddbb 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -4013,6 +4013,7 @@ function send_confirmation_on_profile_email( $user_id = 0 ) { * @global string $pagenow The filename of the current screen. */ function new_user_email_admin_notice() { + global $pagenow; if ( 'profile.php' === $pagenow && isset( $_GET['updated'] ) ) { diff --git a/tests/phpunit/tests/admin/includesTemplate.php b/tests/phpunit/tests/admin/includesTemplate.php index 4b9b8bc68034e..b9b44bdcae5e7 100644 --- a/tests/phpunit/tests/admin/includesTemplate.php +++ b/tests/phpunit/tests/admin/includesTemplate.php @@ -442,7 +442,12 @@ public function test_settings_errors_css_classes( $type, $expected ) { $wp_settings_errors = null; - $expected = sprintf( 'notice %s settings-error is-dismissible', $expected ); + if ( str_contains( $type, ' ' ) ) { + // Backward compatibility for types that are strings with spaces see Trac ticket #44941. + $expected = sprintf( 'notice is-dismissible %s settings-error', $expected ); + } else { + $expected = sprintf( 'notice %s is-dismissible settings-error', $expected ); + } $this->assertStringContainsString( $expected, $output ); $this->assertStringNotContainsString( 'notice-notice-', $output ); From 4b3febfc32865be1b015212385f4a5f8ddcfa24c Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Wed, 23 Sep 2026 22:11:21 +0200 Subject: [PATCH 03/10] Better address backward compatibility for custom admin notice types. --- src/wp-admin/includes/template.php | 21 ++++++++++--------- .../phpunit/tests/admin/includesTemplate.php | 13 ++++++++---- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/wp-admin/includes/template.php b/src/wp-admin/includes/template.php index 53fa5cfe7721e..8384a0b348b3b 100644 --- a/src/wp-admin/includes/template.php +++ b/src/wp-admin/includes/template.php @@ -1996,8 +1996,7 @@ function settings_errors( $setting = '', $sanitize = false, $hide_on_update = fa } foreach ( $settings_errors as $key => $details ) { - // Backward compatibility for passed types that may wrongly contain `notice-`. - $type = str_replace( 'notice-', '', trim( $details['type'] ) ); + $type = trim( $details['type'] ); if ( 'updated' === $type ) { $type = 'success'; @@ -2005,14 +2004,16 @@ function settings_errors( $setting = '', $sanitize = false, $hide_on_update = fa $additional_classes = array( 'settings-error' ); - // Backward compatibility for types that are strings with spaces see Trac ticket #44941. - // For example, a string like `error my-own-css-class hello world`. - if ( str_contains( $type, ' ' ) ) { - // Use ths substrings as additional classes. - $additional_classes = explode( ' ', $type . ' settings-error' ); - // Set the type to empty string. - $type = ''; - + /* + * Backward compatibility: for Core admin notice types, the related CSS + * classes are rendered in the default order determined by `wp_admin_notice()`. + * This includes the old type `updated`. Instead, anything else e.g. + * custom notice types or strings with spaces is now passed as + * additional_classes and appended to the end of the string, + */ + if ( ! in_array( $type, array( 'error', 'success', 'warning', 'info' ), true ) ) { + $additional_classes = array_merge( $additional_classes, explode( ' ', $type ) ); + $type = ''; } /* diff --git a/tests/phpunit/tests/admin/includesTemplate.php b/tests/phpunit/tests/admin/includesTemplate.php index b9b44bdcae5e7..37150fae166bf 100644 --- a/tests/phpunit/tests/admin/includesTemplate.php +++ b/tests/phpunit/tests/admin/includesTemplate.php @@ -442,11 +442,13 @@ public function test_settings_errors_css_classes( $type, $expected ) { $wp_settings_errors = null; - if ( str_contains( $type, ' ' ) ) { - // Backward compatibility for types that are strings with spaces see Trac ticket #44941. - $expected = sprintf( 'notice is-dismissible %s settings-error', $expected ); - } else { + if ( in_array( $type, array( 'error', 'success', 'updated', 'warning', 'info' ), true ) ) { + // Backward compatibility: Core admin notice types build the CSS classes in this order. $expected = sprintf( 'notice %s is-dismissible settings-error', $expected ); + } else { + // Anything else e.g. custom types or strings with spaces is now passed as additional_classes. + // As such, the classes are appended to the end of the string. + $expected = sprintf( 'notice is-dismissible settings-error %s', $expected ); } $this->assertStringContainsString( $expected, $output ); @@ -461,7 +463,10 @@ public function data_settings_errors_css_classes() { array( 'info', 'notice-info' ), array( 'updated', 'notice-success' ), array( 'notice-error', 'notice-error' ), + array( 'custom', 'custom' ), + array( 'notice-custom', 'notice-custom' ), array( 'error my-own-css-class hello world', 'error my-own-css-class hello world' ), + array( 'error my-notice-is-awesome hello world', 'error my-notice-is-awesome hello world' ), ); } From e83cce5e79298e55da5af7c2fbc64b2506dc9d83 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Wed, 23 Sep 2026 22:19:35 +0200 Subject: [PATCH 04/10] Remove no longer necessary esc_attr. --- src/wp-admin/includes/template.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/template.php b/src/wp-admin/includes/template.php index 8384a0b348b3b..60dfabc860b12 100644 --- a/src/wp-admin/includes/template.php +++ b/src/wp-admin/includes/template.php @@ -2024,7 +2024,7 @@ function settings_errors( $setting = '', $sanitize = false, $hide_on_update = fa */ $css_id = sprintf( 'setting-error-%s', - esc_attr( $details['code'] ), + $details['code'], ); wp_admin_notice( From e7e9cc0366cf48d2a1ef32ba80916aa085f9cf62 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Wed, 23 Sep 2026 22:49:01 +0200 Subject: [PATCH 05/10] Simplify test by expecting the full string with CSS classes in the expected order. --- .../phpunit/tests/admin/includesTemplate.php | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/tests/phpunit/tests/admin/includesTemplate.php b/tests/phpunit/tests/admin/includesTemplate.php index 37150fae166bf..bf24f86a28e5c 100644 --- a/tests/phpunit/tests/admin/includesTemplate.php +++ b/tests/phpunit/tests/admin/includesTemplate.php @@ -442,31 +442,25 @@ public function test_settings_errors_css_classes( $type, $expected ) { $wp_settings_errors = null; - if ( in_array( $type, array( 'error', 'success', 'updated', 'warning', 'info' ), true ) ) { - // Backward compatibility: Core admin notice types build the CSS classes in this order. - $expected = sprintf( 'notice %s is-dismissible settings-error', $expected ); - } else { - // Anything else e.g. custom types or strings with spaces is now passed as additional_classes. - // As such, the classes are appended to the end of the string. - $expected = sprintf( 'notice is-dismissible settings-error %s', $expected ); - } - $this->assertStringContainsString( $expected, $output ); $this->assertStringNotContainsString( 'notice-notice-', $output ); } + // Backward compatibility: Core admin notice types build the CSS classes in this order. + // Anything else e.g. custom types or strings with spaces is now passed as additional_classes. + // As such, the classes are appended to the end of the string. public function data_settings_errors_css_classes() { return array( - array( 'error', 'notice-error' ), - array( 'success', 'notice-success' ), - array( 'warning', 'notice-warning' ), - array( 'info', 'notice-info' ), - array( 'updated', 'notice-success' ), - array( 'notice-error', 'notice-error' ), - array( 'custom', 'custom' ), - array( 'notice-custom', 'notice-custom' ), - array( 'error my-own-css-class hello world', 'error my-own-css-class hello world' ), - array( 'error my-notice-is-awesome hello world', 'error my-notice-is-awesome hello world' ), + array( 'error', 'notice notice-error is-dismissible settings-error' ), + array( 'success', 'notice notice-success is-dismissible settings-error' ), + array( 'warning', 'notice notice-warning is-dismissible settings-error' ), + array( 'info', 'notice notice-info is-dismissible settings-error' ), + array( 'updated', 'notice notice-success is-dismissible settings-error' ), + array( 'notice-error', 'notice is-dismissible settings-error notice-error' ), + array( 'custom', 'is-dismissible settings-error custom' ), + array( 'notice-custom', 'is-dismissible settings-error notice-custom' ), + array( 'error my-own-css-class hello world', 'is-dismissible settings-error error my-own-css-class hello world' ), + array( 'error my-notice-is-awesome hello world', 'is-dismissible settings-error error my-notice-is-awesome hello world' ), ); } From b6ad7ef16d3d0c6496e4aa51ebf1524a9dad1de1 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Wed, 23 Sep 2026 23:19:56 +0200 Subject: [PATCH 06/10] Revert unintentional change to user.php file. --- src/wp-includes/user.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-includes/user.php b/src/wp-includes/user.php index 9f81e9845ddbb..92d091165525b 100644 --- a/src/wp-includes/user.php +++ b/src/wp-includes/user.php @@ -4013,7 +4013,6 @@ function send_confirmation_on_profile_email( $user_id = 0 ) { * @global string $pagenow The filename of the current screen. */ function new_user_email_admin_notice() { - global $pagenow; if ( 'profile.php' === $pagenow && isset( $_GET['updated'] ) ) { From 7b31839a0c65969dbfba4251432a467d745eb550 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Thu, 24 Sep 2026 00:24:08 +0200 Subject: [PATCH 07/10] Add the notice class to the expected output where missing. --- tests/phpunit/tests/admin/includesTemplate.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/admin/includesTemplate.php b/tests/phpunit/tests/admin/includesTemplate.php index bf24f86a28e5c..a05147bbffead 100644 --- a/tests/phpunit/tests/admin/includesTemplate.php +++ b/tests/phpunit/tests/admin/includesTemplate.php @@ -457,10 +457,10 @@ public function data_settings_errors_css_classes() { array( 'info', 'notice notice-info is-dismissible settings-error' ), array( 'updated', 'notice notice-success is-dismissible settings-error' ), array( 'notice-error', 'notice is-dismissible settings-error notice-error' ), - array( 'custom', 'is-dismissible settings-error custom' ), - array( 'notice-custom', 'is-dismissible settings-error notice-custom' ), - array( 'error my-own-css-class hello world', 'is-dismissible settings-error error my-own-css-class hello world' ), - array( 'error my-notice-is-awesome hello world', 'is-dismissible settings-error error my-notice-is-awesome hello world' ), + array( 'custom', 'notice is-dismissible settings-error custom' ), + array( 'notice-custom', 'notice is-dismissible settings-error notice-custom' ), + array( 'error my-own-css-class hello world', 'notice is-dismissible settings-error error my-own-css-class hello world' ), + array( 'error my-notice-is-awesome hello world', 'notice is-dismissible settings-error error my-notice-is-awesome hello world' ), ); } From 85bafe516e48276567d8453b745d57a90d6ab36c Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Fri, 25 Sep 2026 09:09:07 +0200 Subject: [PATCH 08/10] Clarify comment. --- src/wp-admin/includes/template.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/wp-admin/includes/template.php b/src/wp-admin/includes/template.php index 60dfabc860b12..6261a4facbae2 100644 --- a/src/wp-admin/includes/template.php +++ b/src/wp-admin/includes/template.php @@ -2005,11 +2005,10 @@ function settings_errors( $setting = '', $sanitize = false, $hide_on_update = fa $additional_classes = array( 'settings-error' ); /* - * Backward compatibility: for Core admin notice types, the related CSS - * classes are rendered in the default order determined by `wp_admin_notice()`. - * This includes the old type `updated`. Instead, anything else e.g. - * custom notice types or strings with spaces is now passed as - * additional_classes and appended to the end of the string, + * Backward compatibility: `wp_admin_notice()` generates the admin notice + * CSS classes based on the passed message type. For example, 'error', + * 'success', 'warning', 'info'. Custom types will be treated as + * additional classes and appended to the end of the CSS classes. */ if ( ! in_array( $type, array( 'error', 'success', 'warning', 'info' ), true ) ) { $additional_classes = array_merge( $additional_classes, explode( ' ', $type ) ); From 0ec409b7c7bd1a93e1e0a0c0a69d0d9dee974866 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Fri, 25 Sep 2026 09:24:54 +0200 Subject: [PATCH 09/10] Remove redundant test case. --- tests/phpunit/tests/admin/includesTemplate.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/phpunit/tests/admin/includesTemplate.php b/tests/phpunit/tests/admin/includesTemplate.php index a05147bbffead..f906a9b1174a4 100644 --- a/tests/phpunit/tests/admin/includesTemplate.php +++ b/tests/phpunit/tests/admin/includesTemplate.php @@ -458,7 +458,6 @@ public function data_settings_errors_css_classes() { array( 'updated', 'notice notice-success is-dismissible settings-error' ), array( 'notice-error', 'notice is-dismissible settings-error notice-error' ), array( 'custom', 'notice is-dismissible settings-error custom' ), - array( 'notice-custom', 'notice is-dismissible settings-error notice-custom' ), array( 'error my-own-css-class hello world', 'notice is-dismissible settings-error error my-own-css-class hello world' ), array( 'error my-notice-is-awesome hello world', 'notice is-dismissible settings-error error my-notice-is-awesome hello world' ), ); From 8ce4b17efa9af77b08860cca287c8ceafbef1f07 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Sat, 26 Sep 2026 14:31:32 +0200 Subject: [PATCH 10/10] Improve comments. --- src/wp-admin/includes/template.php | 3 +-- tests/phpunit/tests/admin/includesTemplate.php | 10 +++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/wp-admin/includes/template.php b/src/wp-admin/includes/template.php index 6261a4facbae2..93428dcc4ae6d 100644 --- a/src/wp-admin/includes/template.php +++ b/src/wp-admin/includes/template.php @@ -1975,8 +1975,7 @@ function get_settings_errors( $setting = '', $sanitize = false ) { * @since 5.3.0 Legacy `error` and `updated` CSS classes are mapped to * `notice-error` and `notice-success`. * @since 7.2.0 Uses `wp_admin_notice()` to echo the notices so that the CSS - * classes are handled based on the notice type and the hook - * `wp_admin_notice` is fired, for consistency with other notices. + * classes, filters, and hooks are handled consistently. * * @param string $setting Optional slug title of a specific setting whose errors you want. * @param bool $sanitize Whether to re-sanitize the setting value before returning errors. diff --git a/tests/phpunit/tests/admin/includesTemplate.php b/tests/phpunit/tests/admin/includesTemplate.php index f906a9b1174a4..5e50cf8180e98 100644 --- a/tests/phpunit/tests/admin/includesTemplate.php +++ b/tests/phpunit/tests/admin/includesTemplate.php @@ -446,16 +446,20 @@ public function test_settings_errors_css_classes( $type, $expected ) { $this->assertStringNotContainsString( 'notice-notice-', $output ); } - // Backward compatibility: Core admin notice types build the CSS classes in this order. - // Anything else e.g. custom types or strings with spaces is now passed as additional_classes. - // As such, the classes are appended to the end of the string. public function data_settings_errors_css_classes() { return array( + // The admin notices default types will generate CSS classes in this order. array( 'error', 'notice notice-error is-dismissible settings-error' ), array( 'success', 'notice notice-success is-dismissible settings-error' ), array( 'warning', 'notice notice-warning is-dismissible settings-error' ), array( 'info', 'notice notice-info is-dismissible settings-error' ), array( 'updated', 'notice notice-success is-dismissible settings-error' ), + /* + * Backward compatibility: custom types and anything else (e.g. + * strings with spaces) are now passed as additional_classes. As + * such, the classes will be appended to the end of the generated + * class attribute. + */ array( 'notice-error', 'notice is-dismissible settings-error notice-error' ), array( 'custom', 'notice is-dismissible settings-error custom' ), array( 'error my-own-css-class hello world', 'notice is-dismissible settings-error error my-own-css-class hello world' ),