From 2fbdb47863dad4067fb4ca72d03b4ea27ab5b90c Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Mon, 27 Jul 2026 11:45:13 +0530 Subject: [PATCH 01/12] 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/12] 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/12] 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 a86697aa06894d19daab8e0538c2f48a7ce1f637 Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Mon, 27 Jul 2026 13:28:45 +0530 Subject: [PATCH 04/12] 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 94f8b3da849cb..3e6dd8ac2a144 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -5006,6 +5006,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 3cea7f1ea937e19e8c2796e7118d8cce713ab555 Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Mon, 27 Jul 2026 14:00:48 +0530 Subject: [PATCH 05/12] 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 4ed2157f51bf6a0bc13ddca5eeaca7275f45aaf0 Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Mon, 27 Jul 2026 14:26:16 +0530 Subject: [PATCH 06/12] 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 f7b390b6caedc0d077b6b09b86e2733be1670e31 Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Mon, 27 Jul 2026 14:26:54 +0530 Subject: [PATCH 07/12] 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 df8a54214b4eed97a18df96b740150474a67f190 Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Tue, 28 Jul 2026 15:24:55 +0530 Subject: [PATCH 08/12] 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 fd141581eb6386589c4bb4bd5ac7d829d0a9caab Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Tue, 28 Jul 2026 15:26:25 +0530 Subject: [PATCH 09/12] 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 8b3c21d48a786f027adf71c0bf7278cd2696f204 Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Tue, 28 Jul 2026 15:26:46 +0530 Subject: [PATCH 10/12] 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 981ca3dc684b6..53905312a2295 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 a62817fb5004586229518cffafe6e7794d931f2e Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Tue, 28 Jul 2026 16:21:31 +0530 Subject: [PATCH 11/12] 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 4a2d5a3ac7ea8..7cd8110977381 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -11143,6 +11143,8 @@ mockedApiResponse.Schema = { "title": "Title", "description": "Site title.", "type": "string", + "minLength": 1, + "pattern": "\\S", "required": false }, "description": { From ac27abae5dac1aa7e9766dc6e29889807d19982a Mon Sep 17 00:00:00 2001 From: Himanshu Pathak Date: Fri, 21 Aug 2026 13:22:39 +0530 Subject: [PATCH 12/12] Constrain max-width for blogname and site_name fields in network settings --- src/wp-admin/css/forms.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/wp-admin/css/forms.css b/src/wp-admin/css/forms.css index f7e6accfce0fd..00e2fa3c955e6 100644 --- a/src/wp-admin/css/forms.css +++ b/src/wp-admin/css/forms.css @@ -1222,6 +1222,10 @@ table.form-table td .updated p { 20.0 - Settings ------------------------------------------------------------------------------*/ +.form-field #blogname { + max-width: 25em; +} + .timezone-info code { white-space: nowrap; } @@ -1317,7 +1321,8 @@ table.form-table td .updated p { .form-field #admin-email, .form-field #path, .form-field #blog_registered, -.form-field #blog_last_updated { +.form-field #blog_last_updated, +.form-field #site_name { max-width: 25em; }