From 20a008ef31c6fac1d44e2e9790b4db14a3a5e9e5 Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Mon, 27 Jul 2026 11:45:13 +0530 Subject: [PATCH 01/11] Multisite, Settings: prevent saving an empty site title (blogname) --- src/wp-admin/network/site-settings.php | 43 +++++++++++++++++++------- src/wp-admin/options-general.php | 4 +-- src/wp-admin/options.php | 12 +++++++ 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/wp-admin/network/site-settings.php b/src/wp-admin/network/site-settings.php index 8b1248f6b13b2..e8b3cf0589c06 100644 --- a/src/wp-admin/network/site-settings.php +++ b/src/wp-admin/network/site-settings.php @@ -40,6 +40,13 @@ switch_to_blog( $id ); $skip_options = array( 'allowedthemes' ); // Don't update these options since they are handled elsewhere in the form. + + // Ensure the site title (blogname) is not left empty. + if ( isset( $_POST['option']['blogname'] ) && '' === trim( $_POST['option']['blogname'] ) ) { + $skip_options[] = 'blogname'; + $update_error = __( 'The site title cannot be empty.' ); + } + foreach ( (array) $_POST['option'] as $key => $val ) { $key = wp_unslash( $key ); $val = wp_unslash( $val ); @@ -60,12 +67,15 @@ do_action( 'wpmu_update_blog_options', $id ); restore_current_blog(); + + $redirect_args = array( + 'update' => isset( $update_error ) ? 'error' : 'updated', + 'id' => $id, + ); + wp_redirect( add_query_arg( - array( - 'update' => 'updated', - 'id' => $id, - ), + $redirect_args, 'site-settings.php' ) ); @@ -75,7 +85,15 @@ if ( isset( $_GET['update'] ) ) { $messages = array(); if ( 'updated' === $_GET['update'] ) { - $messages[] = __( 'Site options updated.' ); + $messages[] = array( + 'text' => __( 'Site options updated.' ), + 'type' => 'success', + ); + } elseif ( 'error' === $_GET['update'] ) { + $messages[] = array( + 'text' => __( 'The site title cannot be empty.' ), + 'type' => 'error', + ); } } @@ -104,14 +122,15 @@ ); if ( ! empty( $messages ) ) { - $notice_args = array( - 'type' => 'success', - 'dismissible' => true, - 'id' => 'message', - ); - foreach ( $messages as $msg ) { - wp_admin_notice( $msg, $notice_args ); + wp_admin_notice( + $msg['text'], + array( + 'type' => $msg['type'], + 'dismissible' => true, + 'id' => 'message', + ) + ); } } ?> diff --git a/src/wp-admin/options-general.php b/src/wp-admin/options-general.php index 6a9a6d2f3812d..1fd22375a920d 100644 --- a/src/wp-admin/options-general.php +++ b/src/wp-admin/options-general.php @@ -72,9 +72,9 @@ - + - + Date: Mon, 27 Jul 2026 13:25:20 +0530 Subject: [PATCH 02/11] Add aria-req attribute --- src/wp-admin/options-general.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/options-general.php b/src/wp-admin/options-general.php index 1fd22375a920d..a7758c7320866 100644 --- a/src/wp-admin/options-general.php +++ b/src/wp-admin/options-general.php @@ -74,7 +74,7 @@ - + Date: Mon, 27 Jul 2026 13:25:38 +0530 Subject: [PATCH 03/11] Remove field from `options.php` --- src/wp-admin/options.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/wp-admin/options.php b/src/wp-admin/options.php index e7acd33dfc374..b45cbb00387ce 100644 --- a/src/wp-admin/options.php +++ b/src/wp-admin/options.php @@ -303,18 +303,6 @@ ); } - // Ensure the site title is not left empty. - if ( isset( $_POST['blogname'] ) && '' === trim( $_POST['blogname'] ) ) { - $_POST['blogname'] = get_option( 'blogname' ); - - add_settings_error( - 'general', - 'blogname', - __( 'The site title cannot be empty.' ), - 'error' - ); - } - // Handle translation installation. if ( ! empty( $_POST['WPLANG'] ) && current_user_can( 'install_languages' ) ) { require_once ABSPATH . 'wp-admin/includes/translation-install.php'; From 7cfc8eb3ec702ae0495115495f8c71d91b17f754 Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Mon, 27 Jul 2026 13:28:45 +0530 Subject: [PATCH 04/11] Adds empty string validation for `blogname` inside `sanitize_option()` --- src/wp-admin/network/settings.php | 51 ++++++++++++++---- src/wp-admin/network/site-settings.php | 75 +++++++++++++++++--------- src/wp-includes/formatting.php | 5 ++ 3 files changed, 94 insertions(+), 37 deletions(-) diff --git a/src/wp-admin/network/settings.php b/src/wp-admin/network/settings.php index 1e13084a858c1..9b5c53853132e 100644 --- a/src/wp-admin/network/settings.php +++ b/src/wp-admin/network/settings.php @@ -116,6 +116,13 @@ } } + // Ensure the network title (site_name) is not left empty. + $network_title_error = ''; + if ( isset( $_POST['site_name'] ) && '' === trim( wp_unslash( $_POST['site_name'] ) ) ) { + unset( $_POST['site_name'] ); + $network_title_error = __( 'The network title cannot be empty. Please enter a title for your network.' ); + } + foreach ( $options as $option_name ) { if ( ! isset( $_POST[ $option_name ] ) ) { continue; @@ -131,21 +138,43 @@ */ do_action( 'update_wpmu_options' ); - wp_redirect( add_query_arg( 'updated', 'true', network_admin_url( 'settings.php' ) ) ); + if ( $network_title_error ) { + set_transient( 'network_settings_errors', array( $network_title_error ), 30 ); + wp_redirect( add_query_arg( 'updated', 'error', network_admin_url( 'settings.php' ) ) ); + } else { + wp_redirect( add_query_arg( 'updated', 'true', network_admin_url( 'settings.php' ) ) ); + } exit; } require_once ABSPATH . 'wp-admin/admin-header.php'; if ( isset( $_GET['updated'] ) ) { - wp_admin_notice( - __( 'Settings saved.' ), - array( - 'type' => 'success', - 'dismissible' => true, - 'id' => 'message', - ) - ); + if ( 'true' === $_GET['updated'] ) { + wp_admin_notice( + __( 'Settings saved.' ), + array( + 'type' => 'success', + 'dismissible' => true, + 'id' => 'message', + ) + ); + } elseif ( 'error' === $_GET['updated'] ) { + $network_settings_errors = get_transient( 'network_settings_errors' ); + delete_transient( 'network_settings_errors' ); + + $error_messages = is_array( $network_settings_errors ) ? $network_settings_errors : array( __( 'Some settings could not be saved.' ) ); + foreach ( $error_messages as $error_message ) { + wp_admin_notice( + $error_message, + array( + 'type' => 'error', + 'dismissible' => true, + 'id' => 'message', + ) + ); + } + } } ?> @@ -155,10 +184,10 @@

- + diff --git a/src/wp-admin/network/site-settings.php b/src/wp-admin/network/site-settings.php index e8b3cf0589c06..ea646af68d3e5 100644 --- a/src/wp-admin/network/site-settings.php +++ b/src/wp-admin/network/site-settings.php @@ -40,13 +40,6 @@ switch_to_blog( $id ); $skip_options = array( 'allowedthemes' ); // Don't update these options since they are handled elsewhere in the form. - - // Ensure the site title (blogname) is not left empty. - if ( isset( $_POST['option']['blogname'] ) && '' === trim( $_POST['option']['blogname'] ) ) { - $skip_options[] = 'blogname'; - $update_error = __( 'The site title cannot be empty.' ); - } - foreach ( (array) $_POST['option'] as $key => $val ) { $key = wp_unslash( $key ); $val = wp_unslash( $val ); @@ -68,32 +61,51 @@ restore_current_blog(); - $redirect_args = array( - 'update' => isset( $update_error ) ? 'error' : 'updated', - 'id' => $id, - ); + $update = 'updated'; + + /* + * sanitize_option() rejects invalid values (such as an empty, required + * Site Title) by registering a settings error and keeping the stored value. + * Carry those errors across the redirect so an error notice can be shown + * instead of a success message. + */ + $settings_errors = get_settings_errors(); + if ( ! empty( $settings_errors ) ) { + set_transient( 'settings_errors', $settings_errors, 30 ); + $update = 'not-updated'; + } wp_redirect( add_query_arg( - $redirect_args, + array( + 'update' => $update, + 'id' => $id, + ), 'site-settings.php' ) ); exit; } +$messages = array(); +$error_messages = array(); + if ( isset( $_GET['update'] ) ) { - $messages = array(); if ( 'updated' === $_GET['update'] ) { - $messages[] = array( - 'text' => __( 'Site options updated.' ), - 'type' => 'success', - ); - } elseif ( 'error' === $_GET['update'] ) { - $messages[] = array( - 'text' => __( 'The site title cannot be empty.' ), - 'type' => 'error', - ); + $messages[] = __( 'Site options updated.' ); + } elseif ( 'not-updated' === $_GET['update'] ) { + $settings_errors = get_transient( 'settings_errors' ); + delete_transient( 'settings_errors' ); + + if ( is_array( $settings_errors ) ) { + foreach ( $settings_errors as $settings_error ) { + $error_messages[] = $settings_error['message']; + } + } + + if ( empty( $error_messages ) ) { + $error_messages[] = __( 'Some settings could not be saved.' ); + } } } @@ -121,18 +133,29 @@ ) ); -if ( ! empty( $messages ) ) { - foreach ( $messages as $msg ) { +if ( ! empty( $error_messages ) ) { + foreach ( $error_messages as $msg ) { wp_admin_notice( - $msg['text'], + $msg, array( - 'type' => $msg['type'], + 'type' => 'error', 'dismissible' => true, 'id' => 'message', ) ); } } + +if ( ! empty( $messages ) ) { + wp_admin_notice( + $messages[0], + array( + 'type' => 'success', + 'dismissible' => true, + 'id' => 'message', + ) + ); +} ?> diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 74a28109b6536..1d8d188dbf109 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -4996,6 +4996,11 @@ function sanitize_option( $option, $value ) { $error = $value->get_error_message(); } else { $value = esc_html( $value ); + + // The site title (blogname) cannot be empty. + if ( 'blogname' === $option && '' === trim( $value ) ) { + $error = __( 'The site title cannot be empty. Please enter a title for your site.' ); + } } break; From 6943f98f711b1f45e306aea39b8798e32871b5ee Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Mon, 27 Jul 2026 14:00:48 +0530 Subject: [PATCH 05/11] Omit the id for repeated notices --- src/wp-admin/network/site-settings.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-admin/network/site-settings.php b/src/wp-admin/network/site-settings.php index ea646af68d3e5..68bff1d222be0 100644 --- a/src/wp-admin/network/site-settings.php +++ b/src/wp-admin/network/site-settings.php @@ -140,7 +140,6 @@ array( 'type' => 'error', 'dismissible' => true, - 'id' => 'message', ) ); } From bffd8ea05d44da673984279e52e28049beddaa7e Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Mon, 27 Jul 2026 14:26:16 +0530 Subject: [PATCH 06/11] Prevent fatal error on non-scalar network titles --- src/wp-admin/network/settings.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/wp-admin/network/settings.php b/src/wp-admin/network/settings.php index 9b5c53853132e..4523a3539be77 100644 --- a/src/wp-admin/network/settings.php +++ b/src/wp-admin/network/settings.php @@ -118,7 +118,10 @@ // Ensure the network title (site_name) is not left empty. $network_title_error = ''; - if ( isset( $_POST['site_name'] ) && '' === trim( wp_unslash( $_POST['site_name'] ) ) ) { + if ( isset( $_POST['site_name'] ) ) { + $site_name = wp_unslash( $_POST['site_name'] ); + + if ( ! is_scalar( $site_name ) || '' === trim( (string) $site_name ) ) { unset( $_POST['site_name'] ); $network_title_error = __( 'The network title cannot be empty. Please enter a title for your network.' ); } From 11fc6e867810aab0f356f7a77c0f58fb1c6d2f38 Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Mon, 27 Jul 2026 14:26:54 +0530 Subject: [PATCH 07/11] Omit the id for repeated notices --- src/wp-admin/network/settings.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/network/settings.php b/src/wp-admin/network/settings.php index 4523a3539be77..a0d933a90ed77 100644 --- a/src/wp-admin/network/settings.php +++ b/src/wp-admin/network/settings.php @@ -122,8 +122,9 @@ $site_name = wp_unslash( $_POST['site_name'] ); if ( ! is_scalar( $site_name ) || '' === trim( (string) $site_name ) ) { - unset( $_POST['site_name'] ); - $network_title_error = __( 'The network title cannot be empty. Please enter a title for your network.' ); + unset( $_POST['site_name'] ); + $network_title_error = __( 'The network title cannot be empty. Please enter a title for your network.' ); + } } foreach ( $options as $option_name ) { @@ -173,7 +174,6 @@ array( 'type' => 'error', 'dismissible' => true, - 'id' => 'message', ) ); } From ee0b27d67d222a3c31139f427de147c4fda0b80e Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Tue, 28 Jul 2026 15:24:55 +0530 Subject: [PATCH 08/11] Add REST schema validation and settings controller checks --- src/wp-includes/option.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index 8bd6a1821162e..35fb609998e00 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -2746,7 +2746,11 @@ function register_initial_settings() { 'blogname', array( 'show_in_rest' => array( - 'name' => 'title', + 'name' => 'title', + 'schema' => array( + 'minLength' => 1, + 'pattern' => '\S', + ), ), 'type' => 'string', 'label' => __( 'Title' ), From 79d42f38dda5ce43c5c05dd0c0e4f5adb1161f53 Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Tue, 28 Jul 2026 15:26:25 +0530 Subject: [PATCH 09/11] Prevent updating site title (blogname) to empty or whitespace values in REST API settings controller --- .../endpoints/class-wp-rest-settings-controller.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php index 142836c7c8921..69e30b3352fc4 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-settings-controller.php @@ -198,6 +198,14 @@ public function update_item( $request ) { ); } + if ( 'blogname' === $args['option_name'] ) { + return new WP_Error( + 'rest_invalid_param', + __( 'The site title cannot be empty. Please enter a title for your site.' ), + array( 'status' => 400 ) + ); + } + delete_option( $args['option_name'] ); } else { update_option( $args['option_name'], $request[ $name ] ); From e9dcd79fe94490c49f55874b465b25cb292e20b2 Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Tue, 28 Jul 2026 15:26:46 +0530 Subject: [PATCH 10/11] Add unit tests for validating invalid blog titles in REST settings controller --- .../rest-api/rest-settings-controller.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/phpunit/tests/rest-api/rest-settings-controller.php b/tests/phpunit/tests/rest-api/rest-settings-controller.php index e8f90b53f20f1..6f8d04d5242b2 100644 --- a/tests/phpunit/tests/rest-api/rest-settings-controller.php +++ b/tests/phpunit/tests/rest-api/rest-settings-controller.php @@ -598,6 +598,36 @@ public function test_update_item_with_invalid_type() { $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } + /** + * @ticket 65718 + * + * @dataProvider data_update_item_with_invalid_title + */ + public function test_update_item_with_invalid_title( $invalid_title ) { + wp_set_current_user( self::$administrator ); + $original_title = get_option( 'blogname' ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' ); + $request->set_param( 'title', $invalid_title ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertSame( $original_title, get_option( 'blogname' ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_update_item_with_invalid_title() { + return array( + 'empty string' => array( '' ), + 'whitespace only' => array( ' ' ), + 'null' => array( null ), + ); + } + public function test_update_item_with_integer() { wp_set_current_user( self::$administrator ); $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' ); From 7a2880c443617b79ed24308d90345ed551b0d266 Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Tue, 28 Jul 2026 16:21:31 +0530 Subject: [PATCH 11/11] Rebuild fixtures --- tests/qunit/fixtures/wp-api-generated.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 1b0eaac6cccd1..4b896df076368 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -11134,6 +11134,8 @@ mockedApiResponse.Schema = { "title": "Title", "description": "Site title.", "type": "string", + "minLength": 1, + "pattern": "\\S", "required": false }, "description": {