diff --git a/phpcs.xml.dist b/phpcs.xml.dist index db1a76d17e72..32c6acc06d8b 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -237,6 +237,7 @@ + diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index 54413d809bc7..a16fe2914c82 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -147,7 +147,7 @@ function get_default_block_template_types() { ), 'category' => array( 'title' => _x( 'Category', 'Template name' ), - 'description' => __( 'Displays latest posts in single post category.' ), + 'description' => __( 'Displays latest posts from a single post category.' ), ), 'taxonomy' => array( 'title' => _x( 'Taxonomy', 'Template name' ), @@ -555,7 +555,8 @@ function _build_block_template_result_from_post( $post ) { $template_file = _get_block_template_file( $post->post_type, $post->post_name ); $has_theme_file = wp_get_theme()->get_stylesheet() === $theme && null !== $template_file; - $origin = get_post_meta( $post->ID, 'origin', true ); + $origin = get_post_meta( $post->ID, 'origin', true ); + $is_wp_suggestion = get_post_meta( $post->ID, 'is_wp_suggestion', true ); $template = new WP_Block_Template(); $template->wp_id = $post->ID; @@ -570,7 +571,7 @@ function _build_block_template_result_from_post( $post ) { $template->title = $post->post_title; $template->status = $post->post_status; $template->has_theme_file = $has_theme_file; - $template->is_custom = true; + $template->is_custom = empty( $is_wp_suggestion ); $template->author = $post->post_author; if ( 'wp_template' === $post->post_type && $has_theme_file && isset( $template_file['postTypes'] ) ) { @@ -679,7 +680,8 @@ function get_block_templates( $query = array(), $template_type = 'wp_template' ) continue; } - if ( $post_type && + if ( + $post_type && isset( $template->post_types ) && ! in_array( $post_type, $template->post_types, true ) ) { @@ -912,9 +914,10 @@ function block_footer_area() { * @return Bool Whether this file is in an ignored directory. */ function wp_is_theme_directory_ignored( $path ) { - $directories_to_ignore = array( '.svn', '.git', '.hg', '.bzr', 'node_modules', 'vendor' ); + $directories_to_ignore = array( '.DS_Store', '.svn', '.git', '.hg', '.bzr', 'node_modules', 'vendor' ); + foreach ( $directories_to_ignore as $directory ) { - if ( strpos( $path, $directory ) === 0 ) { + if ( str_starts_with( $path, $directory ) ) { return true; } } @@ -1023,3 +1026,74 @@ function wp_generate_block_templates_export_file() { return $filename; } + +/** + * Gets the template hierarchy for the given template slug to be created. + * + * + * Note: Always add `index` as the last fallback template. + * + * @since 6.1.0 + * + * @param string $slug The template slug to be created. + * @param boolean $is_custom Optional. Indicates if a template is custom or + * part of the template hierarchy. Default false. + * @param string $template_prefix Optional. The template prefix for the created template. + * Used to extract the main template type, e.g. + * in `taxonomy-books` the `taxonomy` is extracted. + * Default empty string. + * @return string[] The template hierarchy. + */ +function get_template_hierarchy( $slug, $is_custom = false, $template_prefix = '' ) { + if ( 'index' === $slug ) { + return array( 'index' ); + } + if ( $is_custom ) { + return array( 'page', 'singular', 'index' ); + } + if ( 'front-page' === $slug ) { + return array( 'front-page', 'home', 'index' ); + } + + $template_hierarchy = array( $slug ); + + // Most default templates don't have `$template_prefix` assigned. + if ( $template_prefix ) { + list( $type ) = explode( '-', $template_prefix ); + // These checks are needed because the `$slug` above is always added. + if ( ! in_array( $template_prefix, array( $slug, $type ), true ) ) { + $template_hierarchy[] = $template_prefix; + } + if ( $slug !== $type ) { + $template_hierarchy[] = $type; + } + } + + // Handle `archive` template. + if ( + str_starts_with( $slug, 'author' ) || + str_starts_with( $slug, 'taxonomy' ) || + str_starts_with( $slug, 'category' ) || + str_starts_with( $slug, 'tag' ) || + 'date' === $slug + ) { + $template_hierarchy[] = 'archive'; + } + // Handle `single` template. + if ( 'attachment' === $slug ) { + $template_hierarchy[] = 'single'; + } + + // Handle `singular` template. + if ( + str_starts_with( $slug, 'single' ) || + str_starts_with( $slug, 'page' ) || + 'attachment' === $slug + ) { + $template_hierarchy[] = 'singular'; + } + + $template_hierarchy[] = 'index'; + + return $template_hierarchy; +}; diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php index cb2daa4b292c..8223f1dd6f38 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php @@ -186,54 +186,58 @@ public function prepare_item_for_response( $item, $request ) { $fields = $this->get_fields_for_response( $request ); $data = array(); - if ( in_array( 'capabilities', $fields, true ) ) { + if ( rest_is_field_included( 'capabilities', $fields ) ) { $data['capabilities'] = $post_type->cap; } - if ( in_array( 'description', $fields, true ) ) { + if ( rest_is_field_included( 'description', $fields ) ) { $data['description'] = $post_type->description; } - if ( in_array( 'hierarchical', $fields, true ) ) { + if ( rest_is_field_included( 'hierarchical', $fields ) ) { $data['hierarchical'] = $post_type->hierarchical; } - if ( in_array( 'visibility', $fields, true ) ) { + if ( rest_is_field_included( 'visibility', $fields ) ) { $data['visibility'] = array( 'show_in_nav_menus' => (bool) $post_type->show_in_nav_menus, 'show_ui' => (bool) $post_type->show_ui, ); } - if ( in_array( 'viewable', $fields, true ) ) { + if ( rest_is_field_included( 'viewable', $fields ) ) { $data['viewable'] = is_post_type_viewable( $post_type ); } - if ( in_array( 'labels', $fields, true ) ) { + if ( rest_is_field_included( 'labels', $fields ) ) { $data['labels'] = $post_type->labels; } - if ( in_array( 'name', $fields, true ) ) { + if ( rest_is_field_included( 'name', $fields ) ) { $data['name'] = $post_type->label; } - if ( in_array( 'slug', $fields, true ) ) { + if ( rest_is_field_included( 'slug', $fields ) ) { $data['slug'] = $post_type->name; } - if ( in_array( 'supports', $fields, true ) ) { + if ( rest_is_field_included( 'icon', $fields ) ) { + $data['icon'] = $post_type->menu_icon; + } + + if ( rest_is_field_included( 'supports', $fields ) ) { $data['supports'] = $supports; } - if ( in_array( 'taxonomies', $fields, true ) ) { + if ( rest_is_field_included( 'taxonomies', $fields ) ) { $data['taxonomies'] = array_values( $taxonomies ); } - if ( in_array( 'rest_base', $fields, true ) ) { + if ( rest_is_field_included( 'rest_base', $fields ) ) { $data['rest_base'] = $base; } - if ( in_array( 'rest_namespace', $fields, true ) ) { + if ( rest_is_field_included( 'rest_namespace', $fields ) ) { $data['rest_namespace'] = $namespace; } @@ -287,6 +291,7 @@ protected function prepare_links( $post_type ) { * @since 4.7.0 * @since 4.8.0 The `supports` property was added. * @since 5.9.0 The `visibility` and `rest_namespace` properties were added. + * @since 6.1.0 The `icon` property was added. * * @return array Item schema data. */ @@ -385,6 +390,12 @@ public function get_item_schema() { ), ), ), + 'icon' => array( + 'description' => __( 'The icon for the post type.' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), ), ); 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 7e6067489064..e26b4d17e5eb 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 @@ -42,6 +42,7 @@ public function __construct( $post_type ) { * Registers the controllers routes. * * @since 5.8.0 + * @since 6.1.0 Endpoint for fallback template content. */ public function register_routes() { // Lists all templates. @@ -65,6 +66,34 @@ public function register_routes() { ) ); + // Get fallback template content. + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/lookup', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_template_fallback' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'slug' => array( + 'description' => __( 'The slug of the template to get the fallback for' ), + 'type' => 'string', + 'required' => true, + ), + 'is_custom' => array( + 'description' => __( ' Indicates if a template is custom or part of the template hierarchy' ), + 'type' => 'boolean', + ), + 'template_prefix' => array( + 'description' => __( 'The template prefix for the created template. This is used to extract the main template type, e.g. in `taxonomy-books` extracts the `taxonomy`' ), + 'type' => 'string', + ), + ), + ), + ) + ); + // Lists/updates a single template based on the given id. register_rest_route( $this->namespace, @@ -117,6 +146,21 @@ public function register_routes() { ); } + /** + * Returns the fallback template for the given slug. + * + * @since 6.1.0 + * + * @param WP_REST_Request $request The request instance. + * @return WP_REST_Response|WP_Error + */ + public function get_template_fallback( $request ) { + $hierarchy = get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] ); + $fallback_template = resolve_block_template( $request['slug'], $hierarchy, '' ); + $response = $this->prepare_item_for_response( $fallback_template, $request ); + return rest_ensure_response( $response ); + } + /** * Checks if the user has permissions to make the request. * @@ -525,6 +569,15 @@ protected function prepare_item_for_database( $request ) { $changes->post_excerpt = $template->description; } + if ( 'wp_template' === $this->post_type && isset( $request['is_wp_suggestion'] ) ) { + $changes->meta_input = wp_parse_args( + array( + 'is_wp_suggestion' => $request['is_wp_suggestion'], + ), + $changes->meta_input = array() + ); + } + if ( 'wp_template_part' === $this->post_type ) { if ( isset( $request['area'] ) ) { $changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $request['area'] ); diff --git a/tests/phpunit/tests/block-templates/base.php b/tests/phpunit/tests/block-templates/base.php new file mode 100644 index 000000000000..f6a5a9f4dfce --- /dev/null +++ b/tests/phpunit/tests/block-templates/base.php @@ -0,0 +1,86 @@ +post->create_and_get( + array( + 'post_type' => 'wp_template', + 'post_name' => 'my_template', + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template', + 'tax_input' => array( + 'wp_theme' => array( + 'this-theme-should-not-resolve', + ), + ), + ) + ); + + wp_set_post_terms( self::$template_post->ID, 'this-theme-should-not-resolve', 'wp_theme' ); + + // Set up template post. + self::$template_post = $factory->post->create_and_get( + array( + 'post_type' => 'wp_template', + 'post_name' => 'my_template', + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + ), + ) + ); + + wp_set_post_terms( self::$template_post->ID, self::TEST_THEME, 'wp_theme' ); + + // Set up template part post. + self::$template_part_post = $factory->post->create_and_get( + array( + 'post_type' => 'wp_template_part', + 'post_name' => 'my_template_part', + 'post_title' => 'My Template Part', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template part', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + 'wp_template_part_area' => array( + WP_TEMPLATE_PART_AREA_HEADER, + ), + ), + ) + ); + + wp_set_post_terms( self::$template_part_post->ID, WP_TEMPLATE_PART_AREA_HEADER, 'wp_template_part_area' ); + wp_set_post_terms( self::$template_part_post->ID, self::TEST_THEME, 'wp_theme' ); + } + + public static function wpTearDownAfterClass() { + wp_delete_post( self::$template_post->ID ); + wp_delete_post( self::$template_part_post->ID ); + } + + public function set_up() { + parent::set_up(); + switch_theme( self::TEST_THEME ); + } +} diff --git a/tests/phpunit/tests/block-templates/getTemplateHierarchy.php b/tests/phpunit/tests/block-templates/getTemplateHierarchy.php new file mode 100644 index 000000000000..e550b2277841 --- /dev/null +++ b/tests/phpunit/tests/block-templates/getTemplateHierarchy.php @@ -0,0 +1,124 @@ +assertSame( $expected, get_template_hierarchy( ...$args ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_template_hierarchy() { + return array( + 'front-page' => array( + 'args' => array( 'front-page' ), + 'expected' => array( 'front-page', 'home', 'index' ), + ), + 'custom template' => array( + 'args' => array( 'whatever-slug', true ), + 'expected' => array( 'page', 'singular', 'index' ), + ), + 'page' => array( + 'args' => array( 'page' ), + 'expected' => array( 'page', 'singular', 'index' ), + ), + 'tag' => array( + 'args' => array( 'tag' ), + 'expected' => array( 'tag', 'archive', 'index' ), + ), + 'author' => array( + 'args' => array( 'author' ), + 'expected' => array( 'author', 'archive', 'index' ), + ), + 'date' => array( + 'args' => array( 'date' ), + 'expected' => array( 'date', 'archive', 'index' ), + ), + 'taxonomy' => array( + 'args' => array( 'taxonomy' ), + 'expected' => array( 'taxonomy', 'archive', 'index' ), + ), + 'attachment' => array( + 'args' => array( 'attachment' ), + 'expected' => array( 'attachment', 'single', 'singular', 'index' ), + ), + 'singular' => array( + 'args' => array( 'singular' ), + 'expected' => array( 'singular', 'index' ), + ), + 'single' => array( + 'args' => array( 'single' ), + 'expected' => array( 'single', 'singular', 'index' ), + ), + 'archive' => array( + 'args' => array( 'archive' ), + 'expected' => array( 'archive', 'index' ), + ), + 'index' => array( + 'args' => array( 'index' ), + 'expected' => array( 'index' ), + ), + 'specific taxonomies' => array( + 'args' => array( 'taxonomy-books', false, 'taxonomy-books' ), + 'expected' => array( 'taxonomy-books', 'taxonomy', 'archive', 'index' ), + ), + 'single word categories' => array( + 'args' => array( 'category-fruits', false, 'category' ), + 'expected' => array( 'category-fruits', 'category', 'archive', 'index' ), + ), + 'multi word categories' => array( + 'args' => array( 'category-fruits-yellow', false, 'category' ), + 'expected' => array( 'category-fruits-yellow', 'category', 'archive', 'index' ), + ), + 'single word taxonomy and term' => array( + 'args' => array( 'taxonomy-books-action', false, 'taxonomy-books' ), + 'expected' => array( 'taxonomy-books-action', 'taxonomy-books', 'taxonomy', 'archive', 'index' ), + ), + 'single word taxonomy and multi word term' => array( + 'args' => array( 'taxonomy-books-action-adventure', false, 'taxonomy-books' ), + 'expected' => array( 'taxonomy-books-action-adventure', 'taxonomy-books', 'taxonomy', 'archive', 'index' ), + ), + 'multi word taxonomy and term' => array( + 'args' => array( 'taxonomy-greek-books-action-adventure', false, 'taxonomy-greek-books' ), + 'expected' => array( 'taxonomy-greek-books-action-adventure', 'taxonomy-greek-books', 'taxonomy', 'archive', 'index' ), + ), + 'single word post type' => array( + 'args' => array( 'single-book', false, 'single-book' ), + 'expected' => array( 'single-book', 'single', 'singular', 'index' ), + ), + 'multi word post type' => array( + 'args' => array( 'single-art-project', false, 'single-art-project' ), + 'expected' => array( 'single-art-project', 'single', 'singular', 'index' ), + ), + 'single post with multi word post type' => array( + 'args' => array( 'single-art-project-imagine', false, 'single-art-project' ), + 'expected' => array( 'single-art-project-imagine', 'single-art-project', 'single', 'singular', 'index' ), + ), + 'single page' => array( + 'args' => array( 'page-hi', false, 'page' ), + 'expected' => array( 'page-hi', 'page', 'singular', 'index' ), + ), + 'authors' => array( + 'args' => array( 'author-rigas', false, 'author' ), + 'expected' => array( 'author-rigas', 'author', 'archive', 'index' ), + ), + ); + } +} diff --git a/tests/phpunit/tests/rest-api/rest-post-types-controller.php b/tests/phpunit/tests/rest-api/rest-post-types-controller.php index 0ab2a7b93700..982fb4e04a23 100644 --- a/tests/phpunit/tests/rest-api/rest-post-types-controller.php +++ b/tests/phpunit/tests/rest-api/rest-post-types-controller.php @@ -156,24 +156,31 @@ public function test_prepare_item_limit_fields() { ); } + /** + * @ticket 56467 + * + * @covers WP_REST_Post_Types_Controller::get_item_schema + */ public function test_get_item_schema() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/types' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 12, $properties ); - $this->assertArrayHasKey( 'capabilities', $properties ); - $this->assertArrayHasKey( 'description', $properties ); - $this->assertArrayHasKey( 'hierarchical', $properties ); - $this->assertArrayHasKey( 'viewable', $properties ); - $this->assertArrayHasKey( 'labels', $properties ); - $this->assertArrayHasKey( 'name', $properties ); - $this->assertArrayHasKey( 'slug', $properties ); - $this->assertArrayHasKey( 'supports', $properties ); - $this->assertArrayHasKey( 'taxonomies', $properties ); - $this->assertArrayHasKey( 'rest_base', $properties ); - $this->assertArrayHasKey( 'rest_namespace', $properties ); - $this->assertArrayHasKey( 'visibility', $properties ); + + $this->assertCount( 13, $properties, 'Schema should have 13 properties' ); + $this->assertArrayHasKey( 'capabilities', $properties, '`capabilities` should be included in the schema' ); + $this->assertArrayHasKey( 'description', $properties, '`description` should be included in the schema' ); + $this->assertArrayHasKey( 'hierarchical', $properties, '`hierarchical` should be included in the schema' ); + $this->assertArrayHasKey( 'viewable', $properties, '`viewable` should be included in the schema' ); + $this->assertArrayHasKey( 'labels', $properties, '`labels` should be included in the schema' ); + $this->assertArrayHasKey( 'name', $properties, '`name` should be included in the schema' ); + $this->assertArrayHasKey( 'slug', $properties, '`slug` should be included in the schema' ); + $this->assertArrayHasKey( 'supports', $properties, '`supports` should be included in the schema' ); + $this->assertArrayHasKey( 'taxonomies', $properties, '`taxonomies` should be included in the schema' ); + $this->assertArrayHasKey( 'rest_base', $properties, '`rest_base` should be included in the schema' ); + $this->assertArrayHasKey( 'rest_namespace', $properties, '`rest_namespace` should be included in the schema' ); + $this->assertArrayHasKey( 'visibility', $properties, '`visibility` should be included in the schema' ); + $this->assertArrayHasKey( 'icon', $properties, '`icon` should be included in the schema' ); } public function test_get_additional_field_registration() { diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index a1e4330c0cc4..afc1ebc12583 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -150,12 +150,14 @@ public function test_expected_routes_in_schema() { '/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/template-parts/lookup', '/wp/v2/templates', '/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/templates/lookup', '/wp/v2/themes', '/wp/v2/themes/(?P[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', '/wp/v2/plugins', diff --git a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php index c53701f9b6a8..0ddd023c012f 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php @@ -56,6 +56,7 @@ public static function wpTearDownAfterClass() { /** * @covers WP_REST_Templates_Controller::register_routes * @ticket 54596 + * @ticket 56467 */ public function test_register_routes() { $routes = rest_get_server()->get_routes(); @@ -69,6 +70,11 @@ public function test_register_routes() { $routes, 'Single template based on the given ID route does not exist' ); + $this->assertArrayHasKey( + '/wp/v2/templates/lookup', + $routes, + 'Get template fallback content route does not exist' + ); } /** @@ -678,4 +684,109 @@ protected function find_and_normalize_template_by_id( $templates, $id ) { return null; } + /** + * @dataProvider data_create_item_with_is_wp_suggestion + * @ticket 56467 + * @covers WP_REST_Templates_Controller::create_item + * + * @param array $body_params Data set to test. + * @param array $expected Expected results. + */ + public function test_create_item_with_is_wp_suggestion( array $body_params, array $expected ) { + // Set up the user. + $body_params['author'] = self::$admin_id; + $expected['author'] = self::$admin_id; + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/templates' ); + $request->set_body_params( $body_params ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + unset( $data['_links'] ); + unset( $data['wp_id'] ); + + $this->assertSame( $expected, $data ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_create_item_with_is_wp_suggestion() { + $expected = array( + 'id' => 'default//page-rigas', + 'theme' => 'default', + 'content' => array( + 'raw' => 'Content', + ), + 'slug' => 'page-rigas', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Just a description', + 'title' => array( + 'raw' => 'My Template', + 'rendered' => 'My Template', + ), + 'status' => 'publish', + 'has_theme_file' => false, + 'is_custom' => false, + 'author' => null, + ); + + return array( + 'is_wp_suggestion: true' => array( + 'body_params' => array( + 'slug' => 'page-rigas', + 'description' => 'Just a description', + 'title' => 'My Template', + 'content' => 'Content', + 'is_wp_suggestion' => true, + 'author' => null, + ), + 'expected' => $expected, + ), + 'is_wp_suggestion: false' => array( + 'body_params' => array( + 'slug' => 'page-hi', + 'description' => 'Just a description', + 'title' => 'My Template', + 'content' => 'Content', + 'is_wp_suggestion' => false, + 'author' => null, + ), + 'expected' => array_merge( + $expected, + array( + 'id' => 'default//page-hi', + 'slug' => 'page-hi', + 'is_custom' => true, + ) + ), + ), + ); + } + + /** + * @ticket 56467 + * @covers WP_REST_Templates_Controller::get_template_fallback + */ + public function test_get_template_fallback() { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/lookup' ); + // Should fallback to `index.html`. + $request->set_param( 'slug', 'tag-status' ); + $request->set_param( 'is_custom', false ); + $request->set_param( 'template_prefix', 'tag' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 'index', $response->get_data()['slug'], 'Should fallback to `index.html`.' ); + // Should fallback to `page.html`. + $request->set_param( 'slug', 'page-hello' ); + $request->set_param( 'is_custom', false ); + $request->set_param( 'template_prefix', 'page' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 'page', $response->get_data()['slug'], 'Should fallback to `page.html`.' ); + } } diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 4020b25da844..051f176fc4a7 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -5140,6 +5140,43 @@ mockedApiResponse.Schema = { ] } }, + "/wp/v2/templates/lookup": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "slug": { + "description": "The slug of the template to get the fallback for", + "type": "string", + "required": true + }, + "is_custom": { + "description": " Indicates if a template is custom or part of the template hierarchy", + "type": "boolean", + "required": false + }, + "template_prefix": { + "description": "The template prefix for the created template. This is used to extract the main template type, e.g. in `taxonomy-books` extracts the `taxonomy`", + "type": "string", + "required": false + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/templates/lookup" + } + ] + } + }, "/wp/v2/templates/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w-]+)": { "namespace": "wp/v2", "methods": [ @@ -5792,6 +5829,43 @@ mockedApiResponse.Schema = { ] } }, + "/wp/v2/template-parts/lookup": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "slug": { + "description": "The slug of the template to get the fallback for", + "type": "string", + "required": true + }, + "is_custom": { + "description": " Indicates if a template is custom or part of the template hierarchy", + "type": "boolean", + "required": false + }, + "template_prefix": { + "description": "The template prefix for the created template. This is used to extract the main template type, e.g. in `taxonomy-books` extracts the `taxonomy`", + "type": "string", + "required": false + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/template-parts/lookup" + } + ] + } + }, "/wp/v2/template-parts/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w-]+)": { "namespace": "wp/v2", "methods": [ @@ -11591,6 +11665,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": false, "name": "Posts", "slug": "post", + "icon": "dashicons-admin-post", "taxonomies": [ "category", "post_tag" @@ -11622,6 +11697,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": true, "name": "Pages", "slug": "page", + "icon": "dashicons-admin-page", "taxonomies": [], "rest_base": "pages", "rest_namespace": "wp/v2", @@ -11650,6 +11726,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": false, "name": "Media", "slug": "attachment", + "icon": "dashicons-admin-media", "taxonomies": [], "rest_base": "media", "rest_namespace": "wp/v2", @@ -11678,6 +11755,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": false, "name": "Navigation Menu Items", "slug": "nav_menu_item", + "icon": null, "taxonomies": [ "nav_menu" ], @@ -11708,6 +11786,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": false, "name": "Reusable blocks", "slug": "wp_block", + "icon": null, "taxonomies": [], "rest_base": "blocks", "rest_namespace": "wp/v2", @@ -11736,6 +11815,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": false, "name": "Templates", "slug": "wp_template", + "icon": null, "taxonomies": [], "rest_base": "templates", "rest_namespace": "wp/v2", @@ -11764,6 +11844,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": false, "name": "Template Parts", "slug": "wp_template_part", + "icon": null, "taxonomies": [], "rest_base": "template-parts", "rest_namespace": "wp/v2", @@ -11792,6 +11873,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": false, "name": "Navigation Menus", "slug": "wp_navigation", + "icon": null, "taxonomies": [], "rest_base": "navigation", "rest_namespace": "wp/v2", @@ -11822,6 +11904,7 @@ mockedApiResponse.TypeModel = { "hierarchical": false, "name": "Posts", "slug": "post", + "icon": "dashicons-admin-post", "taxonomies": [ "category", "post_tag"