From d56906f5b0fec8cce8829bb548a8c7c7e4c64b80 Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Tue, 27 Oct 2020 16:42:38 +0000 Subject: [PATCH] REST API: Support a broader range of JSON media types. Previously, we only supported `application/json` which prevented using subtypes like `application/activity+json`. This allows for the REST API to `json_decode` the body of requests using a JSON subtype `Content-Type`. Additionally, `wp_die()` now properly sends the error as JSON when a JSON subtype is specified in the `Accept` header. Props pfefferle. Fixes #49404. git-svn-id: https://develop.svn.wordpress.org/trunk@49329 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/load.php | 22 ++++- .../rest-api/class-wp-rest-request.php | 20 +++-- tests/phpunit/tests/functions.php | 23 ++++++ tests/phpunit/tests/rest-api/rest-request.php | 81 +++++++++++++++++++ 4 files changed, 139 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index 7f4a878241..8d9161adc2 100644 --- a/src/wp-includes/load.php +++ b/src/wp-includes/load.php @@ -1595,11 +1595,11 @@ function wp_finalize_scraping_edited_file_errors( $scrape_key ) { */ function wp_is_json_request() { - if ( isset( $_SERVER['HTTP_ACCEPT'] ) && false !== strpos( $_SERVER['HTTP_ACCEPT'], 'application/json' ) ) { + if ( isset( $_SERVER['HTTP_ACCEPT'] ) && wp_is_json_media_type( $_SERVER['HTTP_ACCEPT'] ) ) { return true; } - if ( isset( $_SERVER['CONTENT_TYPE'] ) && 'application/json' === $_SERVER['CONTENT_TYPE'] ) { + if ( isset( $_SERVER['CONTENT_TYPE'] ) && wp_is_json_media_type( $_SERVER['CONTENT_TYPE'] ) ) { return true; } @@ -1635,6 +1635,24 @@ function wp_is_jsonp_request() { } +/** + * Checks whether a string is a valid JSON Media Type. + * + * @since 5.6.0 + * + * @param string $media_type A Media Type string to check. + * @return bool True if string is a valid JSON Media Type. + */ +function wp_is_json_media_type( $media_type ) { + static $cache = array(); + + if ( ! isset( $cache[ $media_type ] ) ) { + $cache[ $media_type ] = (bool) preg_match( '/(^|\s|,)application\/([\w!#\$&-\^\.\+]+\+)?json(\+oembed)?($|\s|;|,)/i', $media_type ); + } + + return $cache[ $media_type ]; +} + /** * Checks whether current request is an XML request, or is expecting an XML response. * 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 937b5684cd..d65a32eae4 100644 --- a/src/wp-includes/rest-api/class-wp-rest-request.php +++ b/src/wp-includes/rest-api/class-wp-rest-request.php @@ -323,6 +323,19 @@ class WP_REST_Request implements ArrayAccess { return $data; } + /** + * Checks if the request has specified a JSON content-type. + * + * @since 5.6.0 + * + * @return bool True if the content-type header is JSON. + */ + public function is_json_content_type() { + $content_type = $this->get_content_type(); + + return isset( $content_type['value'] ) && wp_is_json_media_type( $content_type['value'] ); + } + /** * Retrieves the parameter priority order. * @@ -335,8 +348,7 @@ class WP_REST_Request implements ArrayAccess { protected function get_parameter_order() { $order = array(); - $content_type = $this->get_content_type(); - if ( isset( $content_type['value'] ) && 'application/json' === $content_type['value'] ) { + if ( $this->is_json_content_type() ) { $order[] = 'JSON'; } @@ -658,9 +670,7 @@ class WP_REST_Request implements ArrayAccess { $this->parsed_json = true; // Check that we actually got JSON. - $content_type = $this->get_content_type(); - - if ( empty( $content_type ) || 'application/json' !== $content_type['value'] ) { + if ( ! $this->is_json_content_type() ) { return true; } diff --git a/tests/phpunit/tests/functions.php b/tests/phpunit/tests/functions.php index 9f68f22595..ffd533ebec 100644 --- a/tests/phpunit/tests/functions.php +++ b/tests/phpunit/tests/functions.php @@ -1728,4 +1728,27 @@ class Tests_Functions extends WP_UnitTestCase { array( '03:61:59', false ), // Out of bound. ); } + + /** + * @ticket 49404 + * @dataProvider data_test_wp_is_json_media_type + */ + public function test_wp_is_json_media_type( $input, $expected ) { + $this->assertEquals( $expected, wp_is_json_media_type( $input ) ); + } + + + public function data_test_wp_is_json_media_type() { + return array( + array( 'application/ld+json', true ), + array( 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', true ), + array( 'application/activity+json', true ), + array( 'application/json+oembed', true ), + array( 'application/json', true ), + array( 'application/nojson', false ), + array( 'application/no.json', false ), + array( 'text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8', false ), + array( 'application/activity+json, application/nojson', true ), + ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-request.php b/tests/phpunit/tests/rest-api/rest-request.php index d2f5021ce2..ea013d3eb0 100644 --- a/tests/phpunit/tests/rest-api/rest-request.php +++ b/tests/phpunit/tests/rest-api/rest-request.php @@ -178,6 +178,87 @@ class Tests_REST_Request extends WP_UnitTestCase { $this->assertEmpty( $this->request->get_param( 'has_json_params' ) ); } + public static function alternate_json_content_type_provider() { + return array( + array( 'application/ld+json', 'json', true ), + array( 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'json', true ), + array( 'application/activity+json', 'json', true ), + array( 'application/json+oembed', 'json', true ), + array( 'application/nojson', 'body', false ), + array( 'application/no.json', 'body', false ), + ); + } + + /** + * @ticket 49404 + * @dataProvider alternate_json_content_type_provider + * + * @param string $content_type The content-type + * @param string $source The source value. + * @param boolean $accept_json The accept_json value. + */ + public function test_alternate_json_content_type( $content_type, $source, $accept_json ) { + $this->request_with_parameters(); + + $this->request->set_method( 'POST' ); + $this->request->set_header( 'Content-Type', $content_type ); + $this->request->set_attributes( array( 'accept_json' => true ) ); + + // Check that JSON takes precedence. + $this->assertEquals( $source, $this->request->get_param( 'source' ) ); + $this->assertEquals( $accept_json, $this->request->get_param( 'has_json_params' ) ); + } + + public static function is_json_content_type_provider() { + return array( + array( 'application/ld+json', true ), + array( 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', true ), + array( 'application/activity+json', true ), + array( 'application/json+oembed', true ), + array( 'application/nojson', false ), + array( 'application/no.json', false ), + ); + } + + /** + * @ticket 49404 + * @dataProvider is_json_content_type_provider + * + * @param string $content_type The content-type + * @param boolean $is_json The is_json value. + */ + public function test_is_json_content_type( $content_type, $is_json ) { + $this->request_with_parameters(); + + $this->request->set_header( 'Content-Type', $content_type ); + + // Check for JSON content-type. + $this->assertEquals( $is_json, $this->request->is_json_content_type() ); + } + + /** + * @ticket 49404 + */ + public function test_content_type_cache() { + $this->request_with_parameters(); + $this->assertFalse( $this->request->is_json_content_type() ); + + $this->request->set_header( 'Content-Type', 'application/json' ); + $this->assertTrue( $this->request->is_json_content_type() ); + + $this->request->set_header( 'Content-Type', 'application/activity+json' ); + $this->assertTrue( $this->request->is_json_content_type() ); + + $this->request->set_header( 'Content-Type', 'application/nojson' ); + $this->assertFalse( $this->request->is_json_content_type() ); + + $this->request->set_header( 'Content-Type', 'application/json' ); + $this->assertTrue( $this->request->is_json_content_type() ); + + $this->request->remove_header( 'Content-Type' ); + $this->assertFalse( $this->request->is_json_content_type() ); + } + public function test_parameter_order_json() { $this->request_with_parameters();