diff options
-rw-r--r-- | src/wp-includes/rest-api.php | 48 | ||||
-rw-r--r-- | src/wp-includes/rest-api/class-wp-rest-request.php | 24 | ||||
-rw-r--r-- | src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php | 14 | ||||
-rw-r--r-- | tests/phpunit/tests/rest-api/rest-posts-controller.php | 60 | ||||
-rw-r--r-- | tests/phpunit/tests/rest-api/rest-schema-sanitization.php | 55 | ||||
-rw-r--r-- | tests/phpunit/tests/rest-api/rest-schema-validation.php | 32 | ||||
-rw-r--r-- | tests/qunit/fixtures/wp-api-generated.js | 110 |
7 files changed, 319 insertions, 24 deletions
diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 8c809c89ba..a0bb8fba3a 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -1209,6 +1209,20 @@ function rest_get_avatar_sizes() { * @return true|WP_Error */ function rest_validate_value_from_schema( $value, $args, $param = '' ) { + if ( is_array( $args['type'] ) ) { + foreach ( $args['type'] as $type ) { + $type_args = $args; + $type_args['type'] = $type; + + if ( true === rest_validate_value_from_schema( $value, $type_args, $param ) ) { + return true; + } + } + + /* translators: 1: Parameter, 2: List of types. */ + return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s' ), $param, implode( ',', $args['type'] ) ) ); + } + if ( 'array' === $args['type'] ) { if ( ! is_null( $value ) ) { $value = wp_parse_list( $value ); @@ -1261,6 +1275,15 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { } } + if ( 'null' === $args['type'] ) { + if ( null !== $value ) { + /* translators: 1: Parameter, 2: Type name. */ + return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'null' ) ); + } + + return true; + } + if ( ! empty( $args['enum'] ) ) { if ( ! in_array( $value, $args['enum'], true ) ) { /* translators: 1: Parameter, 2: List of valid values. */ @@ -1365,6 +1388,27 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) { * @return true|WP_Error */ function rest_sanitize_value_from_schema( $value, $args ) { + if ( is_array( $args['type'] ) ) { + // Determine which type the value was validated against, and use that type when performing sanitization + $validated_type = ''; + + foreach ( $args['type'] as $type ) { + $type_args = $args; + $type_args['type'] = $type; + + if ( ! is_wp_error( rest_validate_value_from_schema( $value, $type_args ) ) ) { + $validated_type = $type; + break; + } + } + + if ( ! $validated_type ) { + return null; + } + + $args['type'] = $validated_type; + } + if ( 'array' === $args['type'] ) { if ( empty( $args['items'] ) ) { return (array) $value; @@ -1407,6 +1451,10 @@ function rest_sanitize_value_from_schema( $value, $args ) { return $value; } + if ( 'null' === $args['type'] ) { + return null; + } + if ( 'integer' === $args['type'] ) { return (int) $value; } diff --git a/src/wp-includes/rest-api/class-wp-rest-request.php b/src/wp-includes/rest-api/class-wp-rest-request.php index 81a5564464..0910ff6f28 100644 --- a/src/wp-includes/rest-api/class-wp-rest-request.php +++ b/src/wp-includes/rest-api/class-wp-rest-request.php @@ -398,6 +398,30 @@ class WP_REST_Request implements ArrayAccess { } /** + * Checks if a parameter exists in the request. + * + * This allows distinguishing between an omitted parameter, + * and a parameter specifically set to null. + * + * @since 5.3.0 + * + * @param string $key Parameter name. + * + * @return bool True if a param exists for the given key. + */ + public function has_param( $key ) { + $order = $this->get_parameter_order(); + + foreach ( $order as $type ) { + if ( array_key_exists( $key, $this->params[ $type ] ) ) { + return true; + } + } + + return false; + } + + /** * Sets a parameter on the request. * * @since 4.4.0 diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index 883e9b31fe..b1e2836af2 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -1036,6 +1036,16 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { } } + // Sending a null date or date_gmt value resets date and date_gmt to their + // default values (`0000-00-00 00:00:00`). + if ( + ( ! empty( $schema['properties']['date_gmt'] ) && $request->has_param( 'date_gmt' ) && null === $request['date_gmt'] ) || + ( ! empty( $schema['properties']['date'] ) && $request->has_param( 'date' ) && null === $request['date'] ) + ) { + $prepared_post->post_date_gmt = null; + $prepared_post->post_date = null; + } + // Post slug. if ( ! empty( $schema['properties']['slug'] ) && isset( $request['slug'] ) ) { $prepared_post->post_name = $request['slug']; @@ -1891,13 +1901,13 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { 'properties' => array( 'date' => array( 'description' => __( "The date the object was published, in the site's timezone." ), - 'type' => 'string', + 'type' => array( 'string', 'null' ), 'format' => 'date-time', 'context' => array( 'view', 'edit', 'embed' ), ), 'date_gmt' => array( 'description' => __( 'The date the object was published, as GMT.' ), - 'type' => 'string', + 'type' => array( 'string', 'null' ), 'format' => 'date-time', 'context' => array( 'view', 'edit' ), ), diff --git a/tests/phpunit/tests/rest-api/rest-posts-controller.php b/tests/phpunit/tests/rest-api/rest-posts-controller.php index 95e8ee11c2..9596dd3ccd 100644 --- a/tests/phpunit/tests/rest-api/rest-posts-controller.php +++ b/tests/phpunit/tests/rest-api/rest-posts-controller.php @@ -2625,6 +2625,66 @@ class WP_Test_REST_Posts_Controller extends WP_Test_REST_Post_Type_Controller_Te $this->assertEquals( $params['excerpt'], $post->post_excerpt ); } + /** + * Verify that updating a post with a `null` date or date_gmt results in a reset post, where all + * date values are equal (date, date_gmt, date_modified and date_modofied_gmt) in the API response. + * In the database, the post_date_gmt field is reset to the default `0000-00-00 00:00:00`. + * + * @ticket 44975 + */ + public function test_rest_update_post_with_empty_date() { + // Create a new test post. + $post_id = $this->factory->post->create(); + wp_set_current_user( self::$editor_id ); + + // Set the post date to the future. + $future_date = '2919-07-29T18:00:00'; + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->add_header( 'content-type', 'application/json' ); + $params = $this->set_post_data( + array( + 'date_gmt' => $future_date, + 'date' => $future_date, + 'title' => 'update', + 'status' => 'draft', + ) + ); + $request->set_body( wp_json_encode( $params ) ); + $response = rest_get_server()->dispatch( $request ); + $this->check_update_post_response( $response ); + $new_data = $response->get_data(); + + // Verify the post is set to the future date. + $this->assertEquals( $new_data['date_gmt'], $future_date ); + $this->assertEquals( $new_data['date'], $future_date ); + $this->assertNotEquals( $new_data['date_gmt'], $new_data['modified_gmt'] ); + $this->assertNotEquals( $new_data['date'], $new_data['modified'] ); + + // Update post with a blank field (date or date_gmt). + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->add_header( 'content-type', 'application/json' ); + $params = $this->set_post_data( + array( + 'date_gmt' => null, + 'title' => 'test', + 'status' => 'draft', + ) + ); + $request->set_body( wp_json_encode( $params ) ); + $response = rest_get_server()->dispatch( $request ); + + // Verify the date field values are reset in the API response. + $this->check_update_post_response( $response ); + $new_data = $response->get_data(); + $this->assertEquals( $new_data['date_gmt'], $new_data['date'] ); + $this->assertNotEquals( $new_data['date_gmt'], $future_date ); + + $post = get_post( $post_id, 'ARRAY_A' ); + $this->assertEquals( $post['post_date_gmt'], '0000-00-00 00:00:00' ); + $this->assertNotEquals( $new_data['date_gmt'], $future_date ); + $this->assertNotEquals( $new_data['date'], $future_date ); + } + public function test_rest_update_post_raw() { wp_set_current_user( self::$editor_id ); diff --git a/tests/phpunit/tests/rest-api/rest-schema-sanitization.php b/tests/phpunit/tests/rest-api/rest-schema-sanitization.php index 4f9f2c9242..1b2fd1d90c 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-sanitization.php +++ b/tests/phpunit/tests/rest-api/rest-schema-sanitization.php @@ -292,4 +292,59 @@ class WP_Test_REST_Schema_Sanitization extends WP_UnitTestCase { $this->assertEquals( 1.10, rest_sanitize_value_from_schema( 1.10, $schema ) ); $this->assertEquals( 1, rest_sanitize_value_from_schema( 1, $schema ) ); } + + public function test_nullable_date() { + $schema = array( + 'type' => array( 'string', 'null' ), + 'format' => 'date-time', + ); + + $this->assertNull( rest_sanitize_value_from_schema( null, $schema ) ); + $this->assertEquals( '2019-09-19T18:00:00', rest_sanitize_value_from_schema( '2019-09-19T18:00:00', $schema ) ); + $this->assertNull( rest_sanitize_value_from_schema( 'lalala', $schema ) ); + } + + public function test_object_or_string() { + $schema = array( + 'type' => array( 'object', 'string' ), + 'properties' => array( + 'raw' => array( + 'type' => 'string', + ), + ), + ); + + $this->assertEquals( 'My Value', rest_sanitize_value_from_schema( 'My Value', $schema ) ); + $this->assertEquals( array( 'raw' => 'My Value' ), rest_sanitize_value_from_schema( array( 'raw' => 'My Value' ), $schema ) ); + $this->assertNull( rest_sanitize_value_from_schema( array( 'raw' => 1 ), $schema ) ); + } + + public function test_object_or_bool() { + $schema = array( + 'type' => array( 'object', 'boolean' ), + 'properties' => array( + 'raw' => array( + 'type' => 'boolean', + ), + ), + ); + + $this->assertTrue( rest_sanitize_value_from_schema( true, $schema ) ); + $this->assertTrue( rest_sanitize_value_from_schema( '1', $schema ) ); + $this->assertTrue( rest_sanitize_value_from_schema( 1, $schema ) ); + + $this->assertFalse( rest_sanitize_value_from_schema( false, $schema ) ); + $this->assertFalse( rest_sanitize_value_from_schema( '0', $schema ) ); + $this->assertFalse( rest_sanitize_value_from_schema( 0, $schema ) ); + + $this->assertEquals( array( 'raw' => true ), rest_sanitize_value_from_schema( array( 'raw' => true ), $schema ) ); + $this->assertEquals( array( 'raw' => true ), rest_sanitize_value_from_schema( array( 'raw' => '1' ), $schema ) ); + $this->assertEquals( array( 'raw' => true ), rest_sanitize_value_from_schema( array( 'raw' => 1 ), $schema ) ); + + $this->assertEquals( array( 'raw' => false ), rest_sanitize_value_from_schema( array( 'raw' => false ), $schema ) ); + $this->assertEquals( array( 'raw' => false ), rest_sanitize_value_from_schema( array( 'raw' => '0' ), $schema ) ); + $this->assertEquals( array( 'raw' => false ), rest_sanitize_value_from_schema( array( 'raw' => 0 ), $schema ) ); + + $this->assertNull( rest_sanitize_value_from_schema( array( 'raw' => 'something non boolean' ), $schema ) ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-schema-validation.php b/tests/phpunit/tests/rest-api/rest-schema-validation.php index df16a894ad..9c18a846dc 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-validation.php +++ b/tests/phpunit/tests/rest-api/rest-schema-validation.php @@ -296,4 +296,36 @@ class WP_Test_REST_Schema_Validation extends WP_UnitTestCase { $this->assertTrue( rest_validate_value_from_schema( 1, $schema ) ); $this->assertTrue( rest_validate_value_from_schema( array(), $schema ) ); } + + public function test_type_null() { + $this->assertTrue( rest_validate_value_from_schema( null, array( 'type' => 'null' ) ) ); + $this->assertWPError( rest_validate_value_from_schema( '', array( 'type' => 'null' ) ) ); + $this->assertWPError( rest_validate_value_from_schema( 'null', array( 'type' => 'null' ) ) ); + } + + public function test_nullable_date() { + $schema = array( + 'type' => array( 'string', 'null' ), + 'format' => 'date-time', + ); + + $this->assertTrue( rest_validate_value_from_schema( null, $schema ) ); + $this->assertTrue( rest_validate_value_from_schema( '2019-09-19T18:00:00', $schema ) ); + $this->assertWPError( rest_validate_value_from_schema( 'some random string', $schema ) ); + } + + public function test_object_or_string() { + $schema = array( + 'type' => array( 'object', 'string' ), + 'properties' => array( + 'raw' => array( + 'type' => 'string', + ), + ), + ); + + $this->assertTrue( rest_validate_value_from_schema( 'My Value', $schema ) ); + $this->assertTrue( rest_validate_value_from_schema( array( 'raw' => 'My Value' ), $schema ) ); + $this->assertWPError( rest_validate_value_from_schema( array( 'raw' => array( 'a list' ) ), $schema ) ); + } } diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 46fcb69370..57e92cb885 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -373,12 +373,18 @@ mockedApiResponse.Schema = { "date": { "required": false, "description": "The date the object was published, in the site's timezone.", - "type": "string" + "type": [ + "string", + "null" + ] }, "date_gmt": { "required": false, "description": "The date the object was published, as GMT.", - "type": "string" + "type": [ + "string", + "null" + ] }, "slug": { "required": false, @@ -553,12 +559,18 @@ mockedApiResponse.Schema = { "date": { "required": false, "description": "The date the object was published, in the site's timezone.", - "type": "string" + "type": [ + "string", + "null" + ] }, "date_gmt": { "required": false, "description": "The date the object was published, as GMT.", - "type": "string" + "type": [ + "string", + "null" + ] }, "slug": { "required": false, @@ -893,12 +905,18 @@ mockedApiResponse.Schema = { "date": { "required": false, "description": "The date the object was published, in the site's timezone.", - "type": "string" + "type": [ + "string", + "null" + ] }, "date_gmt": { "required": false, "description": "The date the object was published, as GMT.", - "type": "string" + "type": [ + "string", + "null" + ] }, "slug": { "required": false, @@ -1238,12 +1256,18 @@ mockedApiResponse.Schema = { "date": { "required": false, "description": "The date the object was published, in the site's timezone.", - "type": "string" + "type": [ + "string", + "null" + ] }, "date_gmt": { "required": false, "description": "The date the object was published, as GMT.", - "type": "string" + "type": [ + "string", + "null" + ] }, "slug": { "required": false, @@ -1390,12 +1414,18 @@ mockedApiResponse.Schema = { "date": { "required": false, "description": "The date the object was published, in the site's timezone.", - "type": "string" + "type": [ + "string", + "null" + ] }, "date_gmt": { "required": false, "description": "The date the object was published, as GMT.", - "type": "string" + "type": [ + "string", + "null" + ] }, "slug": { "required": false, @@ -1702,12 +1732,18 @@ mockedApiResponse.Schema = { "date": { "required": false, "description": "The date the object was published, in the site's timezone.", - "type": "string" + "type": [ + "string", + "null" + ] }, "date_gmt": { "required": false, "description": "The date the object was published, as GMT.", - "type": "string" + "type": [ + "string", + "null" + ] }, "slug": { "required": false, @@ -2015,12 +2051,18 @@ mockedApiResponse.Schema = { "date": { "required": false, "description": "The date the object was published, in the site's timezone.", - "type": "string" + "type": [ + "string", + "null" + ] }, "date_gmt": { "required": false, "description": "The date the object was published, as GMT.", - "type": "string" + "type": [ + "string", + "null" + ] }, "slug": { "required": false, @@ -2152,12 +2194,18 @@ mockedApiResponse.Schema = { "date": { "required": false, "description": "The date the object was published, in the site's timezone.", - "type": "string" + "type": [ + "string", + "null" + ] }, "date_gmt": { "required": false, "description": "The date the object was published, as GMT.", - "type": "string" + "type": [ + "string", + "null" + ] }, "slug": { "required": false, @@ -2399,12 +2447,18 @@ mockedApiResponse.Schema = { "date": { "required": false, "description": "The date the object was published, in the site's timezone.", - "type": "string" + "type": [ + "string", + "null" + ] }, "date_gmt": { "required": false, "description": "The date the object was published, as GMT.", - "type": "string" + "type": [ + "string", + "null" + ] }, "slug": { "required": false, @@ -2503,12 +2557,18 @@ mockedApiResponse.Schema = { "date": { "required": false, "description": "The date the object was published, in the site's timezone.", - "type": "string" + "type": [ + "string", + "null" + ] }, "date_gmt": { "required": false, "description": "The date the object was published, as GMT.", - "type": "string" + "type": [ + "string", + "null" + ] }, "slug": { "required": false, @@ -2612,12 +2672,18 @@ mockedApiResponse.Schema = { "date": { "required": false, "description": "The date the object was published, in the site's timezone.", - "type": "string" + "type": [ + "string", + "null" + ] }, "date_gmt": { "required": false, "description": "The date the object was published, as GMT.", - "type": "string" + "type": [ + "string", + "null" + ] }, "slug": { "required": false, |