diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php index 78c3567147..2fb5221c9a 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php @@ -545,7 +545,7 @@ abstract class WP_REST_Controller { $endpoint_args[ $field_id ]['required'] = true; } - foreach ( array( 'type', 'format', 'enum', 'items' ) as $schema_prop ) { + foreach ( array( 'type', 'format', 'enum', 'items', 'properties' ) as $schema_prop ) { if ( isset( $params[ $schema_prop ] ) ) { $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ]; } 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 1618e3597c..21e467f318 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 @@ -119,23 +119,13 @@ class WP_REST_Settings_Controller extends WP_REST_Controller { * @return mixed The prepared value. */ protected function prepare_value( $value, $schema ) { - // If the value is not a scalar, it's not possible to cast it to anything. - if ( ! is_scalar( $value ) ) { + // If the value is not valid by the schema, set the value to null. Null + // values are specifcally non-destructive so this will not cause overwriting + // the current invalid value to null. + if ( is_wp_error( rest_validate_value_from_schema( $value, $schema ) ) ) { return null; } - - switch ( $schema['type'] ) { - case 'string': - return (string) $value; - case 'integer': - return (int) $value; - case 'number': - return (float) $value; - case 'boolean': - return (bool) $value; - default: - return null; - } + return rest_sanitize_value_from_schema( $value, $schema ); } /** @@ -148,6 +138,7 @@ class WP_REST_Settings_Controller extends WP_REST_Controller { */ public function update_item( $request ) { $options = $this->get_registered_options(); + $params = $request->get_params(); foreach ( $options as $name => $args ) { @@ -187,12 +178,12 @@ class WP_REST_Settings_Controller extends WP_REST_Controller { * * To protect clients from accidentally including the null * values from a response object in a request, we do not allow - * options with non-scalar values to be updated to null. + * options with values that don't pass validation to be updated to null. * Without this added protection a client could mistakenly - * delete all options that have non-scalar values from the + * delete all options that have invalid values from the * database. */ - if ( ! is_scalar( get_option( $args['option_name'], false ) ) ) { + if ( is_wp_error( rest_validate_value_from_schema( get_option( $args['option_name'], false ), $args['schema'] ) ) ) { return new WP_Error( 'rest_invalid_stored_value', sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ), array( 'status' => 500 ) ); @@ -253,7 +244,7 @@ class WP_REST_Settings_Controller extends WP_REST_Controller { * Whitelist the supported types for settings, as we don't want invalid types * to be updated with arbitrary values that we can't do decent sanitizing for. */ - if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'integer', 'string', 'boolean' ), true ) ) { + if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'integer', 'string', 'boolean', 'array', 'object' ), true ) ) { continue; } diff --git a/tests/phpunit/tests/rest-api/rest-settings-controller.php b/tests/phpunit/tests/rest-api/rest-settings-controller.php index 853a013d5c..b4d7c68b5d 100644 --- a/tests/phpunit/tests/rest-api/rest-settings-controller.php +++ b/tests/phpunit/tests/rest-api/rest-settings-controller.php @@ -127,6 +127,93 @@ class WP_Test_REST_Settings_Controller extends WP_Test_REST_Controller_Testcase unregister_setting( 'somegroup', 'mycustomsetting' ); } + public function test_get_item_with_custom_array_setting() { + wp_set_current_user( self::$administrator ); + + register_setting( 'somegroup', 'mycustomsetting', array( + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ), + ), + 'type' => 'array', + ) ); + + // Array is cast to correct types. + update_option( 'mycustomsetting', array( '1', '2' ) ); + $request = new WP_REST_Request( 'GET', '/wp/v2/settings' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( array( 1, 2 ), $data['mycustomsetting'] ); + + // Empty array works as expected. + update_option( 'mycustomsetting', array() ); + $request = new WP_REST_Request( 'GET', '/wp/v2/settings' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( array(), $data['mycustomsetting'] ); + + // Invalid value + update_option( 'mycustomsetting', array( array( 1 ) ) ); + $request = new WP_REST_Request( 'GET', '/wp/v2/settings' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( null, $data['mycustomsetting'] ); + + // No option value + delete_option( 'mycustomsetting' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/settings' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( null, $data['mycustomsetting'] ); + + unregister_setting( 'somegroup', 'mycustomsetting' ); + } + + public function test_get_item_with_custom_object_setting() { + wp_set_current_user( self::$administrator ); + + register_setting( 'somegroup', 'mycustomsetting', array( + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'a' => array( + 'type' => 'integer', + ), + ), + ), + ), + 'type' => 'object', + ) ); + + // Object is cast to correct types. + update_option( 'mycustomsetting', array( 'a' => '1' ) ); + $request = new WP_REST_Request( 'GET', '/wp/v2/settings' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( array( 'a' => 1 ), $data['mycustomsetting'] ); + + // Empty array works as expected. + update_option( 'mycustomsetting', array() ); + $request = new WP_REST_Request( 'GET', '/wp/v2/settings' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( array(), $data['mycustomsetting'] ); + + // Invalid value + update_option( 'mycustomsetting', array( 'a' => 1, 'b' => 2 ) ); + $request = new WP_REST_Request( 'GET', '/wp/v2/settings' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( array( 'a' => 1 ), $data['mycustomsetting'] ); + + unregister_setting( 'somegroup', 'mycustomsetting' ); + } + public function get_setting_custom_callback( $result, $name, $args ) { switch ( $name ) { case 'mycustomsetting1': @@ -215,6 +302,7 @@ class WP_Test_REST_Settings_Controller extends WP_Test_REST_Controller_Testcase $response = $this->server->dispatch( $request ); $data = $response->get_data(); $this->assertEquals( null, $data['mycustomsettinginrest'] ); + unregister_setting( 'somegroup', 'mycustomsetting' ); } @@ -242,6 +330,91 @@ class WP_Test_REST_Settings_Controller extends WP_Test_REST_Controller_Testcase return false; } + public function test_update_item_with_array() { + register_setting( 'somegroup', 'mycustomsetting', array( + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ), + ), + 'type' => 'array', + ) ); + + // We have to re-register the route, as the args changes based off registered settings. + $this->server->override_by_default = true; + $this->endpoint->register_routes(); + wp_set_current_user( self::$administrator ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' ); + $request->set_param( 'mycustomsetting', array( '1', '2' ) ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( array( 1, 2 ), $data['mycustomsetting'] ); + $this->assertEquals( array( 1, 2 ), get_option( 'mycustomsetting' ) ); + + // Setting an empty array. + $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' ); + $request->set_param( 'mycustomsetting', array() ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( array(), $data['mycustomsetting'] ); + $this->assertEquals( array(), get_option( 'mycustomsetting' ) ); + + // Setting an invalid array. + $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' ); + $request->set_param( 'mycustomsetting', array( 'invalid' ) ); + $response = $this->server->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + unregister_setting( 'somegroup', 'mycustomsetting' ); + } + + public function test_update_item_with_object() { + register_setting( 'somegroup', 'mycustomsetting', array( + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'a' => array( + 'type' => 'integer', + ), + ), + ), + ), + 'type' => 'object', + ) ); + + // We have to re-register the route, as the args changes based off registered settings. + $this->server->override_by_default = true; + $this->endpoint->register_routes(); + wp_set_current_user( self::$administrator ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' ); + $request->set_param( 'mycustomsetting', array( 'a' => 1 ) ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( array( 'a' => 1 ), $data['mycustomsetting'] ); + $this->assertEquals( array( 'a' => 1 ), get_option( 'mycustomsetting' ) ); + + // Setting an empty object. + $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' ); + $request->set_param( 'mycustomsetting', array() ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( array(), $data['mycustomsetting'] ); + $this->assertEquals( array(), get_option( 'mycustomsetting' ) ); + + // Setting an invalid object. + $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' ); + $request->set_param( 'mycustomsetting', array( 'a' => 'invalid' ) ); + $response = $this->server->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + unregister_setting( 'somegroup', 'mycustomsetting' ); + } + public function test_update_item_with_filter() { wp_set_current_user( self::$administrator );