Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,50 @@ protected function prepare_value( $value, $schema ) {
*/
public function update_item( $request ) {
$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 ) ) {
Expand Down
51 changes: 51 additions & 0 deletions tests/phpunit/tests/rest-api/rest-settings-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -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( '' );

$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 );
}
Comment on lines +800 to +849
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for a critical scenario: the PR description states that "global parameters (like _locale) still allow the request to succeed," but there's no test verifying that a request with both a valid setting AND an internal parameter (e.g., title + _locale) successfully updates the setting. This is important for verifying backward compatibility. Consider adding a test like: test_update_item_with_valid_setting_and_internal_param_succeeds()

Copilot uses AI. Check for mistakes.
}
Loading