Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport: Block template utils and rest templates controller #3221

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@

<!-- Test case parent classes outside of the "includes" folder. -->
<element value="Tests_Query_Conditionals"/>
<element value="WP_Block_Templates_UnitTestCase"/>
<element value="WP_Filesystem_UnitTestCase"/>
<element value="WP_HTTP_UnitTestCase"/>
<element value="WP_Image_UnitTestCase"/>
Expand Down
86 changes: 80 additions & 6 deletions src/wp-includes/block-template-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ),
Expand Down Expand Up @@ -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;
Expand All @@ -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'] ) ) {
Expand Down Expand Up @@ -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 )
) {
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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 = '' ) {
hellofromtonya marked this conversation as resolved.
Show resolved Hide resolved
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 );
hellofromtonya marked this conversation as resolved.
Show resolved Hide resolved

// 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.
hellofromtonya marked this conversation as resolved.
Show resolved Hide resolved
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.
hellofromtonya marked this conversation as resolved.
Show resolved Hide resolved
if (
str_starts_with( $slug, 'single' ) ||
str_starts_with( $slug, 'page' ) ||
'attachment' === $slug
) {
$template_hierarchy[] = 'singular';
}

$template_hierarchy[] = 'index';
hellofromtonya marked this conversation as resolved.
Show resolved Hide resolved

return $template_hierarchy;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this value is null, should it maybe default to something?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's okay to be null since it's not required and the consumers can decide what to do with it. For example in php core for post types we assign some default icon if it's in the admin menu, but in the site editor we assign different default 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;
}

Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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' ),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be in the view context? The icon is not public right now?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean public? I think it would be good to have in view as it can be useful in some cases. Do you see any drawback being in view context?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you make a property as view it means it can't be viewed pubilically. Do we want to expose this data publically?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no harm in it, but if you have a strong opinion on this, I can remove it.

'readonly' => true,
),
),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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' ),
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
'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,
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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'] );
Expand Down
86 changes: 86 additions & 0 deletions tests/phpunit/tests/block-templates/base.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

/**
* @group block-templates
* @covers ::get_template_hierarchy
*/
abstract class WP_Block_Templates_UnitTestCase extends WP_UnitTestCase {
const TEST_THEME = 'block-theme';

protected static $template_post;
protected static $template_part_post;

public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
/*
* Set up a template post corresponding to a different theme.
* Do this to ensure resolution and slug creation works as expected,
* even with another post of that same name present for another theme.
*/
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(
'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 );
}
}
Loading