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 004f5851a271f..311315ff46e40 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 @@ -141,11 +141,53 @@ protected function prepare_value( $value, $schema ) { * * @param WP_REST_Request $request Full details about the request. * @return array|WP_Error Array on success, or error object on failure. - */ + */ public function update_item( $request ) { - $options = $this->get_registered_options(); + $options = $this->get_registered_options(); + $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 ); - $params = $request->get_params(); + 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 e8f90b53f20f1..1b60180f6f02c 100644 --- a/tests/phpunit/tests/rest-api/rest-settings-controller.php +++ b/tests/phpunit/tests/rest-api/rest-settings-controller.php @@ -796,4 +796,55 @@ 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( array() ); + + $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 ); + } }