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 9d72c7fb05..d9ff86e1fa 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,14 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { // List themes global styles. register_rest_route( $this->namespace, - '/' . $this->rest_base . '/themes/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', + // The route. + sprintf( + '/%s/themes/(?P%s)', + $this->rest_base, + // Matches theme's directory: `/themes///` or `/themes//`. + // Excludes invalid directory name characters: `/:<>*?"|`. + '[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?' + ), array( array( 'methods' => WP_REST_Server::READABLE, @@ -61,7 +68,7 @@ class WP_REST_Global_Styles_Controller extends WP_REST_Controller { // Lists/updates a single global style variation based on the given id. register_rest_route( $this->namespace, - '/' . $this->rest_base . '/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', + '/' . $this->rest_base . '/(?P[\/\w-]+)', array( array( 'methods' => WP_REST_Server::READABLE, @@ -88,8 +95,8 @@ 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`. + * For example, `wp/v2/global-styles/twentytwentytwo%200.4.0` + * would be decoded to `twentytwentytwo 0.4.0`. * * @since 5.9.0 * 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 dae9d88931..8306774160 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,16 @@ 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[\/\s%\w\.\(\)\[\]\@_\-]+)', + // The route. + sprintf( + '/%s/(?P%s%s)', + $this->rest_base, + // Matches theme's directory: `/themes///` or `/themes//`. + // Excludes invalid directory name characters: `/:<>*?"|`. + '([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', + // Matches the template name. + '[\/\w-]+' + ), array( 'args' => array( 'id' => array( @@ -149,7 +158,6 @@ 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, '/' ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-themes-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-themes-controller.php index 0d6ffd0da2..05cd65c47a 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-themes-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-themes-controller.php @@ -16,7 +16,11 @@ */ class WP_REST_Themes_Controller extends WP_REST_Controller { - const PATTERN = '[^.\/]+(?:\/[^.\/]+)?'; + /** + * Matches theme's directory: `/themes///` or `/themes//`. + * Excludes invalid directory name characters: `/:<>*?"|`. + */ + const PATTERN = '[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?'; /** * Constructor. @@ -56,8 +60,9 @@ class WP_REST_Themes_Controller extends WP_REST_Controller { array( 'args' => array( 'stylesheet' => array( - 'description' => __( "The theme's stylesheet. This uniquely identifies the theme." ), - 'type' => 'string', + 'description' => __( "The theme's stylesheet. This uniquely identifies the theme." ), + 'type' => 'string', + 'sanitize_callback' => array( $this, '_sanitize_stylesheet_callback' ), ), ), array( @@ -70,6 +75,18 @@ class WP_REST_Themes_Controller extends WP_REST_Controller { ); } + /** + * Sanitize the stylesheet to decode endpoint. + * + * @since 5.9.0 + * + * @param string $stylesheet The stylesheet name. + * @return string Sanitized stylesheet. + */ + public function _sanitize_stylesheet_callback( $stylesheet ) { + return urldecode( $stylesheet ); + } + /** * Checks if a given request has access to read the theme. * diff --git a/tests/phpunit/data/themedir1/block_theme-[0.4.0]/index.php b/tests/phpunit/data/themedir1/block_theme-[0.4.0]/index.php new file mode 100644 index 0000000000..589adcefcd --- /dev/null +++ b/tests/phpunit/data/themedir1/block_theme-[0.4.0]/index.php @@ -0,0 +1,4 @@ + +

Large Héader Témplaté Part

+ diff --git a/tests/phpunit/data/themedir1/block_theme-[0.4.0]/style.css b/tests/phpunit/data/themedir1/block_theme-[0.4.0]/style.css new file mode 100644 index 0000000000..fe3ff14ba9 --- /dev/null +++ b/tests/phpunit/data/themedir1/block_theme-[0.4.0]/style.css @@ -0,0 +1,7 @@ +/* +Theme Name: Block Theme [0.4.0] +Theme URI: https://wordpress.org/ +Description: Has different characters in theme directory name for testing purposes. +Version: 0.4.0 +Text Domain: block-theme +*/ diff --git a/tests/phpunit/data/themedir1/block_theme-[0.4.0]/templates/page-large-header.html b/tests/phpunit/data/themedir1/block_theme-[0.4.0]/templates/page-large-header.html new file mode 100644 index 0000000000..c5218f8739 --- /dev/null +++ b/tests/phpunit/data/themedir1/block_theme-[0.4.0]/templates/page-large-header.html @@ -0,0 +1,9 @@ + + + +
+ +
+ + + diff --git a/tests/phpunit/data/themedir1/block_theme-[0.4.0]/theme.json b/tests/phpunit/data/themedir1/block_theme-[0.4.0]/theme.json new file mode 100644 index 0000000000..38fcb1d9dd --- /dev/null +++ b/tests/phpunit/data/themedir1/block_theme-[0.4.0]/theme.json @@ -0,0 +1,71 @@ +{ + "version": 1, + "settings": { + "color": { + "palette": [ + { + "slug": "light", + "name": "Light", + "color": "#f5f7f9" + }, + { + "slug": "dark", + "name": "Dark", + "color": "#000" + } + ], + "gradients": [ + { + "name": "Custom gradient", + "gradient": "linear-gradient(135deg,rgba(0,0,0) 0%,rgb(0,0,0) 100%)", + "slug": "custom-gradient" + } + ], + "custom": false, + "customGradient": false + }, + "typography": { + "fontSizes": [ + { + "name": "Custom", + "slug": "custom", + "size": "100px" + } + ], + "customFontSize": false, + "customLineHeight": true + }, + "spacing": { + "units": [ + "rem" + ], + "customPadding": true + }, + "blocks": { + "core/paragraph": { + "color": { + "palette": [ + { + "slug": "light", + "name": "Light", + "color": "#f5f7f9" + } + ] + } + } + } + }, + "customTemplates": [ + { + "name": "page-home", + "title": "Homepage template" + } + ], + "templateParts": [ + { + "name": "small-header", + "title": "Small Header", + "area": "header" + } + ] +} diff --git a/tests/phpunit/data/themedir1/subdir/block_theme-[1.0.0]/index.php b/tests/phpunit/data/themedir1/subdir/block_theme-[1.0.0]/index.php new file mode 100644 index 0000000000..589adcefcd --- /dev/null +++ b/tests/phpunit/data/themedir1/subdir/block_theme-[1.0.0]/index.php @@ -0,0 +1,4 @@ +get_routes(); $this->assertArrayHasKey( - '/wp/v2/global-styles/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', + '/wp/v2/global-styles/(?P[\/\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\.\(\)\[\]\@_\-]+)'], + $routes['/wp/v2/global-styles/(?P[\/\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\.\(\)\[\]\@_\-]+)', + '/wp/v2/global-styles/themes/(?P[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', $routes, 'Theme global styles route does not exist' ); $this->assertCount( 1, - $routes['/wp/v2/global-styles/themes/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)'], + $routes['/wp/v2/global-styles/themes/(?P[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)'], 'Theme global styles route does not have exactly one element' ); } @@ -164,14 +164,17 @@ 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 + * + * @param string $theme_dirname Theme directory to test. + * @param string $expected Expected error code. */ - public function test_get_theme_item_invalid_theme_dirname( $theme_dirname ) { + public function test_get_theme_item_invalid_theme_dirname( $theme_dirname, $expected ) { wp_set_current_user( self::$admin_id ); 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 ); + $this->assertErrorResponse( $expected, $response, 404 ); } /** @@ -181,12 +184,39 @@ class WP_REST_Global_Styles_Controller_Test extends WP_Test_REST_Controller_Test */ 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' ), + '+' => array( + 'theme_dirname' => 'my+theme+', + 'expected' => 'rest_theme_not_found', + ), + ':' => array( + 'theme_dirname' => 'my:theme:', + 'expected' => 'rest_no_route', + ), + '<>' => array( + 'theme_dirname' => 'my', + 'expected' => 'rest_no_route', + ), + '*' => array( + 'theme_dirname' => 'my*theme*', + 'expected' => 'rest_no_route', + ), + '?' => array( + 'theme_dirname' => 'my?theme?', + 'expected' => 'rest_no_route', + ), + '"' => array( + 'theme_dirname' => 'my"theme?"', + 'expected' => 'rest_no_route', + ), + '| (invalid on Windows)' => array( + 'theme_dirname' => 'my|theme|', + 'expected' => 'rest_no_route', + ), + // Themes deep in subdirectories. + '2 subdirectories deep' => array( + 'theme_dirname' => 'subdir/subsubdir/mytheme', + 'expected' => 'rest_global_styles_not_found', + ), ); } @@ -194,6 +224,8 @@ class WP_REST_Global_Styles_Controller_Test extends WP_Test_REST_Controller_Test * @dataProvider data_get_theme_item * @covers WP_REST_Global_Styles_Controller::get_theme_item * @ticket 54596 + * + * @param string $theme Theme directory to test. */ public function test_get_theme_item( $theme ) { wp_set_current_user( self::$admin_id ); @@ -216,18 +248,34 @@ class WP_REST_Global_Styles_Controller_Test extends WP_Test_REST_Controller_Test */ 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' ), + 'alphabetic' => array( 'mytheme' ), + 'alphanumeric' => array( 'mythemev1' ), + 'àáâãäåæç' => array( 'àáâãäåæç' ), + 'space' => array( 'my theme' ), + '-_.' => array( 'my_theme-0.1' ), + '[]' => array( 'my[theme]' ), + '()' => array( 'my(theme)' ), + '{}' => array( 'my{theme}' ), + '&=#@!$,^~%' => array( 'theme &=#@!$,^~%' ), + 'all combined' => array( 'thémé {}&=@!$,^~%[0.1](-_-)' ), + + // Themes in a subdirectory. + 'subdir: alphabetic' => array( 'subdir/mytheme' ), + 'subdir: alphanumeric in theme' => array( 'subdir/mythemev1' ), + 'subdir: alphanumeric in subdir' => array( 'subdirv1/mytheme' ), + 'subdir: alphanumeric in both' => array( 'subdirv1/mythemev1' ), + 'subdir: àáâãäåæç in theme' => array( 'subdir/àáâãäåæç' ), + 'subdir: àáâãäåæç in subdir' => array( 'àáâãäåæç/mythemev1' ), + 'subdir: àáâãäåæç in both' => array( 'àáâãäåæç/àáâãäåæç' ), + 'subdir: space in theme' => array( 'subdir/my theme' ), + 'subdir: space in subdir' => array( 'sub dir/mytheme' ), + 'subdir: space in both' => array( 'sub dir/my theme' ), + 'subdir: -_. in theme' => array( 'subdir/my_theme-0.1' ), + 'subdir: -_. in subdir' => array( 'sub_dir-0.1/mytheme' ), + 'subdir: -_. in both' => array( 'sub_dir-0.1/my_theme-0.1' ), + 'subdir: all combined in theme' => array( 'subdir/thémé {}&=@!$,^~%[0.1](-_-)' ), + 'subdir: all combined in subdir' => array( 'sűbdīr {}&=@!$,^~%[0.1](-_-)/mytheme' ), + 'subdir: all combined in both' => array( 'sűbdīr {}&=@!$,^~%[0.1](-_-)/thémé {}&=@!$,^~%[0.1](-_-)' ), ); } diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index b1131c329a..3bec8d45d4 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -135,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[\/\s%\w\.\(\)\[\]\@_\-]+)', - '/wp/v2/global-styles/themes/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', + '/wp/v2/global-styles/(?P[\/\w-]+)', + '/wp/v2/global-styles/themes/(?P[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', '/wp/v2/search', '/wp/v2/block-renderer/(?P[a-z0-9-]+/[a-z0-9-]+)', '/wp/v2/block-types', @@ -144,19 +144,19 @@ 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[\/\s%\w\.\(\)\[\]\@_\-]+)', '/wp/v2/template-parts/(?P[\d]+)/autosaves', + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w-]+)', '/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[\/\s%\w\.\(\)\[\]\@_\-]+)', '/wp/v2/templates/(?P[\d]+)/autosaves', + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w-]+)', '/wp/v2/templates/(?P[\d]+)/autosaves/(?P[\d]+)', '/wp/v2/templates/(?P[\d]+)/revisions', '/wp/v2/templates/(?P[\d]+)/revisions/(?P[\d]+)', '/wp/v2/themes', - '/wp/v2/themes/(?P[^.\/]+(?:\/[^.\/]+)?)', + '/wp/v2/themes/(?P[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', '/wp/v2/plugins', '/wp/v2/plugins/(?P[^.\/]+(?:\/[^.\/]+)?)', '/wp/v2/block-directory/search', diff --git a/tests/phpunit/tests/rest-api/rest-themes-controller.php b/tests/phpunit/tests/rest-api/rest-themes-controller.php index 73a8b4c3cc..6737a707bd 100644 --- a/tests/phpunit/tests/rest-api/rest-themes-controller.php +++ b/tests/phpunit/tests/rest-api/rest-themes-controller.php @@ -1285,15 +1285,86 @@ class WP_Test_REST_Themes_Controller extends WP_Test_REST_Controller_Testcase { } /** - * @ticket 54349 + * @dataProvider data_get_item_non_subdir_theme + * @ticket 54596 + * @covers WP_REST_Themes_Controller::get_item + * + * @param string $theme_dir Theme directory to test. + * @param string $expected_name Expected theme name. */ - public function test_get_item_subdirectory_theme() { + public function test_get_item_non_subdir_theme( $theme_dir, $expected_name ) { wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', self::$themes_route . '/subdir/theme2' ); + $request = new WP_REST_Request( 'GET', self::$themes_route . $theme_dir ); $response = rest_do_request( $request ); $this->assertSame( 200, $response->get_status() ); - $this->assertSame( 'My Subdir Theme', $response->get_data()['name']['raw'] ); + $this->assertSame( $expected_name, $response->get_data()['name']['raw'] ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_item_non_subdir_theme() { + return array( + 'parent theme' => array( + 'theme_dir' => '/block-theme', + 'expected_name' => 'Block Theme', + ), + 'child theme' => array( + 'theme_dir' => '/block-theme-child', + 'expected_name' => 'Block Theme Child Theme', + ), + 'theme with _-[]. characters' => array( + 'theme_dir' => '/block_theme-[0.4.0]', + 'expected_name' => 'Block Theme [0.4.0]', + ), + ); + } + + /** + * @dataProvider data_get_item_subdirectory_theme + * @ticket 54349 + * @ticket 54596 + * @covers WP_REST_Themes_Controller::get_item + * + * @param string $theme_dir Theme directory to test. + * @param string $expected_name Expected theme name. + */ + public function test_get_item_subdirectory_theme( $theme_dir, $expected_name ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', self::$themes_route . $theme_dir ); + $response = rest_do_request( $request ); + + $this->assertSame( + 200, + $response->get_status(), + 'A 200 OK status was not returned.' + ); + $this->assertSame( + $expected_name, + $response->get_data()['name']['raw'], + 'The actual theme name was not the expected theme name.' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_item_subdirectory_theme() { + return array( + 'theme2' => array( + 'theme_dir' => '/subdir/theme2', + 'expected_name' => 'My Subdir Theme', + ), + 'theme with _-[]. characters' => array( + 'theme_dir' => '/subdir/block_theme-[1.0.0]', + 'expected_name' => 'Block Theme [1.0.0] in subdirectory', + ), + ); } /** diff --git a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php index 504227faff..477467d644 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php @@ -65,7 +65,7 @@ class Tests_REST_WpRestTemplatesController extends WP_Test_REST_Controller_Testc 'Templates route does not exist' ); $this->assertArrayHasKey( - '/wp/v2/templates/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)', + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w-]+)', $routes, 'Single template based on the given ID route does not exist' ); @@ -208,6 +208,119 @@ class Tests_REST_WpRestTemplatesController extends WP_Test_REST_Controller_Testc ); } + /** + * @dataProvider data_get_item_with_valid_theme_dirname + * @covers WP_REST_Templates_Controller::get_item + * @ticket 54596 + * + * @param string $theme_dir Theme directory to test. + * @param string $template Template to test. + * @param array $args Arguments to create the 'wp_template" post. + */ + public function test_get_item_with_valid_theme_dirname( $theme_dir, $template, array $args ) { + wp_set_current_user( self::$admin_id ); + switch_theme( $theme_dir ); + + // Set up template post. + $args['post_type'] = 'wp_template'; + $args['tax_input'] = array( + 'wp_theme' => array( + get_stylesheet(), + ), + ); + $post = self::factory()->post->create_and_get( $args ); + wp_set_post_terms( $post->ID, get_stylesheet(), 'wp_theme' ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/templates/{$theme_dir}//{$template}" ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + unset( $data['content'] ); + unset( $data['_links'] ); + + $this->assertSameSetsWithIndex( + array( + 'id' => "{$theme_dir}//{$template}", + 'theme' => $theme_dir, + 'slug' => $template, + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => $args['post_excerpt'], + 'title' => array( + 'raw' => $args['post_title'], + 'rendered' => $args['post_title'], + ), + 'status' => 'publish', + 'wp_id' => $post->ID, + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => self::$admin_id, + ), + $data + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_item_with_valid_theme_dirname() { + $theme_root_dir = DIR_TESTDATA . '/themedir1/'; + return array( + 'template parts: parent theme' => array( + 'theme_dir' => 'themedir1/block-theme', + 'template' => 'small-header', + 'args' => array( + 'post_name' => 'small-header', + 'post_title' => 'Small Header Template', + 'post_content' => file_get_contents( $theme_root_dir . '/block-theme/parts/small-header.html' ), + 'post_excerpt' => 'Description of small header template.', + ), + ), + 'template: parent theme' => array( + 'theme_dir' => 'themedir1/block-theme', + 'template' => 'page-home', + 'args' => array( + 'post_name' => 'page-home', + 'post_title' => 'Home Page Template', + 'post_content' => file_get_contents( $theme_root_dir . 'block-theme/templates/page-home.html' ), + 'post_excerpt' => 'Description of page home template.', + ), + ), + 'template: child theme' => array( + 'theme_dir' => 'themedir1/block-theme-child', + 'template' => 'page-1', + 'args' => array( + 'post_name' => 'page-1', + 'post_title' => 'Page 1 Template', + 'post_content' => file_get_contents( $theme_root_dir . 'block-theme-child/templates/page-1.html' ), + 'post_excerpt' => 'Description of page 1 template.', + ), + ), + 'template part: subdir with _-[]. characters' => array( + 'theme_dir' => 'themedir1/block_theme-[0.4.0]', + 'template' => 'large-header', + 'args' => array( + 'post_name' => 'large-header', + 'post_title' => 'Large Header Template Part', + 'post_content' => file_get_contents( $theme_root_dir . 'block_theme-[0.4.0]/parts/large-header.html' ), + 'post_excerpt' => 'Description of large header template.', + ), + ), + 'template: subdir with _-[]. characters' => array( + 'theme_dir' => 'themedir1/block_theme-[0.4.0]', + 'template' => 'page-large-header', + 'args' => array( + 'post_name' => 'page-large-header', + 'post_title' => 'Page Large Template', + 'post_content' => file_get_contents( $theme_root_dir . 'block_theme-[0.4.0]/templates/page-large-header.html' ), + 'post_excerpt' => 'Description of page large template.', + ), + ), + ); + } + /** * @ticket 54507 * @dataProvider get_template_ids_to_sanitize diff --git a/tests/phpunit/tests/theme/themeDir.php b/tests/phpunit/tests/theme/themeDir.php index 9311f6643a..377ff79fc1 100644 --- a/tests/phpunit/tests/theme/themeDir.php +++ b/tests/phpunit/tests/theme/themeDir.php @@ -163,6 +163,8 @@ class Tests_Theme_ThemeDir extends WP_UnitTestCase { 'REST Theme', 'Block Theme', 'Block Theme Child Theme', + 'Block Theme [0.4.0]', + 'Block Theme [1.0.0] in subdirectory', ); sort( $theme_names ); diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index e565ddd7cb..c0b7b39ed2 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -5140,7 +5140,7 @@ mockedApiResponse.Schema = { ] } }, - "/wp/v2/templates/(?P[\\/\\s%\\w\\.\\(\\)\\[\\]\\@_\\-]+)": { + "/wp/v2/templates/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w-]+)": { "namespace": "wp/v2", "methods": [ "GET", @@ -5792,7 +5792,7 @@ mockedApiResponse.Schema = { ] } }, - "/wp/v2/template-parts/(?P[\\/\\s%\\w\\.\\(\\)\\[\\]\\@_\\-]+)": { + "/wp/v2/template-parts/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w-]+)": { "namespace": "wp/v2", "methods": [ "GET", @@ -9424,7 +9424,7 @@ mockedApiResponse.Schema = { } ] }, - "/wp/v2/global-styles/themes/(?P[\\/\\s%\\w\\.\\(\\)\\[\\]\\@_\\-]+)": { + "/wp/v2/global-styles/themes/(?P[^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)": { "namespace": "wp/v2", "methods": [ "GET" @@ -9444,7 +9444,7 @@ mockedApiResponse.Schema = { } ] }, - "/wp/v2/global-styles/(?P[\\/\\s%\\w\\.\\(\\)\\[\\]\\@_\\-]+)": { + "/wp/v2/global-styles/(?P[\\/\\w-]+)": { "namespace": "wp/v2", "methods": [ "GET", @@ -9668,7 +9668,7 @@ mockedApiResponse.Schema = { "self": "http://example.org/index.php?rest_route=/wp/v2/themes" } }, - "/wp/v2/themes/(?P[^.\\/]+(?:\\/[^.\\/]+)?)": { + "/wp/v2/themes/(?P[^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)": { "namespace": "wp/v2", "methods": [ "GET"