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..8e2c8fdc85cfa 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 @@ -144,8 +144,50 @@ protected function prepare_value( $value, $schema ) { */ public function update_item( $request ) { $options = $this->get_registered_options(); + $params = $request->get_params(); - $params = $request->get_params(); + /* + * Validate that the request contains only registered settings and internal + * WordPress parameters. + * + * This ensures the settings endpoint returns a 400 Bad Request when sent + * unknown properties or an empty body, aligning it with other REST + * API controllers. + * + * @see https://core.trac.wordpress.org/ticket/41604 + */ + $internal_params = array( + '_wpnonce', + '_method', + '_envelope', + '_jsonp', + '_locale', + '_fields', // Used for sparse fieldsets. + '_embed', // Used to embed linked resources. + ); + + $request_keys = array_keys( $params ); + $allowed_keys = array_merge( array_keys( $options ), $internal_params ); + $unknown = array_diff( $request_keys, $allowed_keys ); + + if ( ! empty( $unknown ) ) { + return new WP_Error( + 'rest_invalid_param', + /* translators: %s: List of invalid parameters. */ + sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', $unknown ) ), + array( 'status' => 400 ) + ); + } + + $provided_settings = array_intersect( $request_keys, array_keys( $options ) ); + + if ( empty( $provided_settings ) ) { + return new WP_Error( + 'rest_empty_request', + __( 'No valid settings provided for update.' ), + array( 'status' => 400 ) + ); + } foreach ( $options as $name => $args ) { if ( ! array_key_exists( $name, $params ) ) { diff --git a/tests/phpunit/tests/rest-api/rest-settings-controller.php b/tests/phpunit/tests/rest-api/rest-settings-controller.php index dd79885d2b16d..a4a45a24071b6 100644 --- a/tests/phpunit/tests/rest-api/rest-settings-controller.php +++ b/tests/phpunit/tests/rest-api/rest-settings-controller.php @@ -797,4 +797,111 @@ public function test_provides_setting_metadata_in_schema() { $this->assertSame( 'Site title.', $title['description'] ); $this->assertSame( null, $title['default'] ); } + + /** + * Test that sending an empty body returns 400. + * + * @ticket 41604 + */ + public function test_update_item_with_empty_body_returns_400() { + wp_set_current_user( self::$administrator ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/settings' ); + $request->set_body( '' ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_empty_request', $response, 400 ); + } + + /** + * Test that sending ONLY internal params (like _locale) still returns 400 + * because no actual settings were changed. + * + * @ticket 41604 + */ + public function test_update_item_with_only_internal_params_returns_400() { + wp_set_current_user( self::$administrator ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/settings' ); + $request->set_query_params( array( '_locale' => 'en_US' ) ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_empty_request', $response, 400 ); + } + + /** + * Test that sending a mix of valid settings and invalid parameters returns 400. + * + * @ticket 41604 + */ + public function test_update_item_with_mixed_valid_and_invalid_params_returns_400() { + wp_set_current_user( self::$administrator ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/settings' ); + $request->set_query_params( array( 'title' => 'New Title' ) ); + $request->set_body( json_encode( array( 'junk' => 'data' ) ) ); + $request->set_header( 'Content-Type', 'application/json' ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Test that sending only an unregistered setting key returns 400. + * + * @ticket 41604 + */ + public function test_update_item_with_only_unknown_setting_returns_400() { + wp_set_current_user( self::$administrator ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/settings' ); + $request->set_query_params( array( 'this_setting_does_not_exist' => 'value' ) ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Test that sending an unregistered setting key via PUT returns 400. + * + * @ticket 41604 + */ + public function test_update_item_via_put_with_unknown_setting_returns_400() { + wp_set_current_user( self::$administrator ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' ); + $request->set_query_params( array( 'this_setting_does_not_exist' => 'value' ) ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Test that a valid setting combined with an internal parameter (e.g. `_locale`) + * still succeeds and updates the setting, confirming internal params don't get + * mistaken for unknown settings. + * + * @ticket 41604 + */ + public function test_update_item_with_valid_setting_and_internal_param_succeeds() { + wp_set_current_user( self::$administrator ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/settings' ); + $request->set_query_params( array( '_locale' => 'en_US' ) ); + $request->set_body( json_encode( array( 'title' => 'New Title' ) ) ); + $request->set_header( 'Content-Type', 'application/json' ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertNotWPError( $response->as_error() ); + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 'New Title', $data['title'] ); + $this->assertSame( 'New Title', get_option( 'blogname' ) ); + } }