diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php index 0d98990103..9d72c7fb05 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php @@ -41,7 +41,7 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { // List themes global styles. register_rest_route( $this->namespace, - '/' . $this->rest_base . '/themes/(?P[^.\/]+(?:\/[^.\/]+)?)', + '/' . $this->rest_base . '/themes/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', array( array( 'methods' => WP_REST_Server::READABLE, @@ -49,18 +49,19 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { 'permission_callback' => array( $this, 'get_theme_item_permissions_check' ), 'args' => array( 'stylesheet' => array( - 'description' => __( 'The theme identifier' ), - 'type' => 'string', + 'description' => __( 'The theme identifier' ), + 'type' => 'string', + 'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ), ), ), ), ) ); - // Lists/updates a single gloval style variation based on the given id. + // Lists/updates a single global style variation based on the given id. register_rest_route( $this->namespace, - '/' . $this->rest_base . '/(?P[\/\w-]+)', + '/' . $this->rest_base . '/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', array( array( 'methods' => WP_REST_Server::READABLE, @@ -68,8 +69,9 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { 'permission_callback' => array( $this, 'get_item_permissions_check' ), 'args' => array( 'id' => array( - 'description' => __( 'The id of a template' ), - 'type' => 'string', + 'description' => __( 'The id of a template' ), + 'type' => 'string', + 'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ), ), ), ), @@ -84,6 +86,20 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { ); } + /** + * Sanitize the global styles ID or stylesheet to decode endpoint. + * For example, `wp/v2/global-styles/templatetwentytwo%200.4.0` + * would be decoded to `templatetwentytwo 0.4.0`. + * + * @since 5.9.0 + * + * @param string $id_or_stylesheet Global styles ID or stylesheet. + * @return string Sanitized global styles ID or stylesheet. + */ + public function _sanitize_global_styles_callback( $id_or_stylesheet ) { + return urldecode( $id_or_stylesheet ); + } + /** * Checks if a given request has access to read a single global style. * @@ -519,7 +535,6 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { * @since 5.9.0 * * @param WP_REST_Request $request The request instance. - * * @return WP_REST_Response|WP_Error */ public function get_theme_item( $request ) { diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php index 02bfe5ce24..dae9d88931 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php @@ -68,7 +68,7 @@ class WP_REST_Templates_Controller extends WP_REST_Controller { // Lists/updates a single template based on the given id. register_rest_route( $this->namespace, - '/' . $this->rest_base . '/(?P[\/\w-]+)', + '/' . $this->rest_base . '/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', array( 'args' => array( 'id' => array( @@ -149,6 +149,9 @@ class WP_REST_Templates_Controller extends WP_REST_Controller { * @return string Sanitized template ID. */ public function _sanitize_template_id( $id ) { + // Decode empty space. + $id = urldecode( $id ); + $last_slash_pos = strrpos( $id, '/' ); if ( false === $last_slash_pos ) { return $id; diff --git a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php index ff1da17fb6..5974ad95ee 100644 --- a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php +++ b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php @@ -93,13 +93,30 @@ class WP_REST_Global_Styles_Controller_Test extends WP_Test_REST_Controller_Test /** * @covers WP_REST_Global_Styles_Controller::register_routes + * @ticket 54596 */ public function test_register_routes() { $routes = rest_get_server()->get_routes(); - $this->assertArrayHasKey( '/wp/v2/global-styles/(?P[\/\w-]+)', $routes ); - $this->assertCount( 2, $routes['/wp/v2/global-styles/(?P[\/\w-]+)'] ); - $this->assertArrayHasKey( '/wp/v2/global-styles/themes/(?P[^.\/]+(?:\/[^.\/]+)?)', $routes ); - $this->assertCount( 1, $routes['/wp/v2/global-styles/themes/(?P[^.\/]+(?:\/[^.\/]+)?)'] ); + $this->assertArrayHasKey( + '/wp/v2/global-styles/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', + $routes, + 'Single global style based on the given ID route does not exist' + ); + $this->assertCount( + 2, + $routes['/wp/v2/global-styles/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)'], + 'Single global style based on the given ID route does not have exactly two elements' + ); + $this->assertArrayHasKey( + '/wp/v2/global-styles/themes/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', + $routes, + 'Theme global styles route does not exist' + ); + $this->assertCount( + 1, + $routes['/wp/v2/global-styles/themes/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)'], + 'Theme global styles route does not have exactly one element' + ); } public function test_context_param() { @@ -132,7 +149,6 @@ class WP_REST_Global_Styles_Controller_Test extends WP_Test_REST_Controller_Test $this->assertErrorResponse( 'rest_cannot_manage_global_styles', $response, 403 ); } - /** * @covers WP_REST_Global_Styles_Controller::get_theme_item * @ticket 54516 @@ -145,18 +161,74 @@ class WP_REST_Global_Styles_Controller_Test extends WP_Test_REST_Controller_Test } /** + * @dataProvider data_get_theme_item_invalid_theme_dirname * @covers WP_REST_Global_Styles_Controller::get_theme_item + * @ticket 54596 */ - public function test_get_theme_item() { + public function test_get_theme_item_invalid_theme_dirname( $theme_dirname ) { wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/tt1-blocks' ); + switch_theme( $theme_dirname ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/' . $theme_dirname ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_no_route', $response, 404 ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_theme_item_invalid_theme_dirname() { + return array( + 'with |' => array( 'my|theme' ), + 'with +' => array( 'my+theme' ), + 'with {}' => array( 'my{theme}' ), + 'with #' => array( 'my#theme' ), + 'with !' => array( 'my!theme' ), + 'multiple invalid chars' => array( 'mytheme-[_(+@)]#! v4.0' ), + ); + } + + /** + * @dataProvider data_get_theme_item + * @covers WP_REST_Global_Styles_Controller::get_theme_item + * @ticket 54596 + */ + public function test_get_theme_item( $theme ) { + wp_set_current_user( self::$admin_id ); + switch_theme( $theme ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/' . $theme ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $links = $response->get_links(); - $this->assertArrayHasKey( 'settings', $data ); - $this->assertArrayHasKey( 'styles', $data ); - $this->assertArrayHasKey( 'self', $links ); - $this->assertStringContainsString( '/wp/v2/global-styles/themes/tt1-blocks', $links['self'][0]['href'] ); + $this->assertArrayHasKey( 'settings', $data, 'Data does not have "settings" key' ); + $this->assertArrayHasKey( 'styles', $data, 'Data does not have "styles" key' ); + $this->assertArrayHasKey( 'self', $links, 'Links do not have a "self" key' ); + $this->assertStringContainsString( '/wp/v2/global-styles/themes/' . $theme, $links['self'][0]['href'] ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_theme_item() { + return array( + 'alphabetic chars' => array( 'mytheme' ), + 'alphanumeric chars' => array( 'mythemev1' ), + 'space' => array( 'my theme' ), + '-' => array( 'my-theme' ), + '_' => array( 'my_theme' ), + '.' => array( 'mytheme0.1' ), + '- and .' => array( 'my-theme-0.1' ), + 'space and .' => array( 'mytheme v0.1' ), + 'space, -, _, .' => array( 'my-theme-v0.1' ), + '[]' => array( 'my[theme]' ), + '()' => array( 'my(theme)' ), + '@' => array( 'my@theme' ), + ); } /** diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 1ed1d89611..b1131c329a 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -69,6 +69,9 @@ class WP_Test_REST_Schema_Initialization extends WP_Test_REST_TestCase { } } + /** + * @ticket 54596 + */ public function test_expected_routes_in_schema() { $routes = rest_get_server()->get_routes(); @@ -132,8 +135,8 @@ class WP_Test_REST_Schema_Initialization extends WP_Test_REST_TestCase { '/wp/v2/users/(?P(?:[\\d]+|me))/application-passwords/(?P[\\w\\-]+)', '/wp/v2/comments', '/wp/v2/comments/(?P[\\d]+)', - '/wp/v2/global-styles/(?P[\/\w-]+)', - '/wp/v2/global-styles/themes/(?P[^.\/]+(?:\/[^.\/]+)?)', + '/wp/v2/global-styles/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', + '/wp/v2/global-styles/themes/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', '/wp/v2/search', '/wp/v2/block-renderer/(?P[a-z0-9-]+/[a-z0-9-]+)', '/wp/v2/block-types', @@ -141,13 +144,13 @@ class WP_Test_REST_Schema_Initialization extends WP_Test_REST_TestCase { '/wp/v2/block-types/(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z0-9_-]+)', '/wp/v2/settings', '/wp/v2/template-parts', - '/wp/v2/template-parts/(?P[\/\w-]+)', + '/wp/v2/template-parts/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', '/wp/v2/template-parts/(?P[\d]+)/autosaves', '/wp/v2/template-parts/(?P[\d]+)/autosaves/(?P[\d]+)', '/wp/v2/template-parts/(?P[\d]+)/revisions', '/wp/v2/template-parts/(?P[\d]+)/revisions/(?P[\d]+)', '/wp/v2/templates', - '/wp/v2/templates/(?P[\/\w-]+)', + '/wp/v2/templates/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', '/wp/v2/templates/(?P[\d]+)/autosaves', '/wp/v2/templates/(?P[\d]+)/autosaves/(?P[\d]+)', '/wp/v2/templates/(?P[\d]+)/revisions', diff --git a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php index 36f59374d6..504227faff 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php @@ -53,10 +53,22 @@ class Tests_REST_WpRestTemplatesController extends WP_Test_REST_Controller_Testc wp_delete_post( self::$post->ID ); } + /** + * @covers WP_REST_Templates_Controller::register_routes + * @ticket 54596 + */ public function test_register_routes() { $routes = rest_get_server()->get_routes(); - $this->assertArrayHasKey( '/wp/v2/templates', $routes ); - $this->assertArrayHasKey( '/wp/v2/templates/(?P[\/\w-]+)', $routes ); + $this->assertArrayHasKey( + '/wp/v2/templates', + $routes, + 'Templates route does not exist' + ); + $this->assertArrayHasKey( + '/wp/v2/templates/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', + $routes, + 'Single template based on the given ID route does not exist' + ); } /** diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index d6e11af973..9b71d950fc 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -5134,7 +5134,7 @@ mockedApiResponse.Schema = { ] } }, - "/wp/v2/templates/(?P[\\/\\w-]+)": { + "/wp/v2/templates/(?P[\\/\\s%\\w\\.\\(\\)\\[\\]\\@_\\-]+)": { "namespace": "wp/v2", "methods": [ "GET", @@ -5786,7 +5786,7 @@ mockedApiResponse.Schema = { ] } }, - "/wp/v2/template-parts/(?P[\\/\\w-]+)": { + "/wp/v2/template-parts/(?P[\\/\\s%\\w\\.\\(\\)\\[\\]\\@_\\-]+)": { "namespace": "wp/v2", "methods": [ "GET", @@ -9418,7 +9418,7 @@ mockedApiResponse.Schema = { } ] }, - "/wp/v2/global-styles/themes/(?P[^.\\/]+(?:\\/[^.\\/]+)?)": { + "/wp/v2/global-styles/themes/(?P[\\/\\s%\\w\\.\\(\\)\\[\\]\\@_\\-]+)": { "namespace": "wp/v2", "methods": [ "GET" @@ -9438,7 +9438,7 @@ mockedApiResponse.Schema = { } ] }, - "/wp/v2/global-styles/(?P[\\/\\w-]+)": { + "/wp/v2/global-styles/(?P[\\/\\s%\\w\\.\\(\\)\\[\\]\\@_\\-]+)": { "namespace": "wp/v2", "methods": [ "GET",