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

Allow template duplication + concept of active templates #67125

Open
wants to merge 9 commits into
base: trunk
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions backport-changelog/6.8/8063.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/8063

* https://github.com/WordPress/gutenberg/pull/67125
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

class Gutenberg_REST_Static_Templates_Controller extends Gutenberg_REST_Templates_Controller_6_7 {
public function register_routes() {
// Lists all templates.
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);

// Lists/updates a single template based on the given id.
register_rest_route(
$this->namespace,
// The route.
sprintf(
'/%s/(?P<id>%s%s)',
$this->rest_base,
/*
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
* Excludes invalid directory name characters: `/:<>*?"|`.
*/
'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
// Matches the template name.
'[\/\w%-]+'
),
array(
'args' => array(
'id' => array(
'description' => __( 'The id of a template' ),
'type' => 'string',
'sanitize_callback' => array( $this, '_sanitize_template_id' ),
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}

public function get_item_schema() {
$schema = parent::get_item_schema();
$schema['properties']['is_custom'] = array(
'description' => __( 'Whether a template is a custom template.' ),
'type' => 'bool',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
);
$schema['properties']['plugin'] = array(
'type' => 'string',
'description' => __( 'Plugin that registered the template.' ),
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
);
return $schema;
}

public function get_items( $request ) {
$query = array();
if ( isset( $request['area'] ) ) {
$query['area'] = $request['area'];
}
if ( isset( $request['post_type'] ) ) {
$query['post_type'] = $request['post_type'];
}
$template_files = _get_block_templates_files( 'wp_template', $query );
$query_result = array();
foreach ( $template_files as $template_file ) {
$query_result[] = _build_block_template_result_from_file( $template_file, 'wp_template' );
}

// Add templates registered in the template registry. Filtering out the ones which have a theme file.
$registered_templates = WP_Block_Templates_Registry::get_instance()->get_by_query( $query );
$matching_registered_templates = array_filter(
$registered_templates,
function ( $registered_template ) use ( $template_files ) {
foreach ( $template_files as $template_file ) {
if ( $template_file['slug'] === $registered_template->slug ) {
return false;
}
}
return true;
}
);

$query_result = array_merge( $query_result, $matching_registered_templates );

$templates = array();
foreach ( $query_result as $template ) {
$item = $this->prepare_item_for_response( $template, $request );
$item->data['type'] = '_wp_static_template';
$templates[] = $this->prepare_response_for_collection( $item );
}

return rest_ensure_response( $templates );
}

public function get_item( $request ) {
$template = get_block_file_template( $request['id'], 'wp_template' );

if ( ! $template ) {
return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
}

$item = $this->prepare_item_for_response( $template, $request );
// adjust the template type here instead
$item->data['type'] = '_wp_static_template';
return rest_ensure_response( $item );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

class Gutenberg_REST_Templates_Controller extends WP_REST_Posts_Controller {
protected function handle_status_param( $status, $request ) {
if ( 'auto-draft' === $status ) {
return $status;
}
return parent::handle_status_param( $status, $request );
}
protected function add_additional_fields_schema( $schema ) {
$schema = parent::add_additional_fields_schema( $schema );

$schema['properties']['status']['enum'][] = 'auto-draft';
return $schema;
}
}
7 changes: 5 additions & 2 deletions lib/compat/wordpress-6.8/preload.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ function gutenberg_block_editor_preload_paths_6_8( $paths, $context ) {
'url',
)
);
$paths[] = '/wp/v2/templates/lookup?slug=front-page';
$paths[] = '/wp/v2/templates/lookup?slug=home';
// There's an issue where preloaded data does not invalidate.
// $paths[] = '/wp/v2/templates/lookup?slug=front-page';
// $paths[] = '/wp/v2/templates/lookup?slug=home';

$paths[] = '/wp/v2/_wp_static_template?context=edit';
}

// Preload theme and global styles paths.
Expand Down
166 changes: 166 additions & 0 deletions lib/compat/wordpress-6.8/template-activate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

// How does this work?
// 1. For wp_template, we remove the custom templates controller, so it becomes
// a normal posts endpoint, modified slightly to allow auto-drafts.
add_filter( 'register_post_type_args', 'gutenberg_modify_wp_template_post_type_args', 10, 2 );
function gutenberg_modify_wp_template_post_type_args( $args, $post_type ) {
if ( 'wp_template' === $post_type ) {
$args['rest_base'] = 'wp_template';
$args['rest_controller_class'] = 'Gutenberg_REST_Templates_Controller';
$args['autosave_rest_controller_class'] = null;
$args['revisions_rest_controller_class'] = null;
}
return $args;
}

// 2. We maintain the routes for /templates and /templates/lookup. I think we'll
// need to deprecate /templates eventually, but we'll still want to be able
// to lookup the active template for a specific slug, and probably get a list
// of all _active_ templates. For that we can keep /lookup.
add_action( 'rest_api_init', 'gutenberg_maintain_templates_routes' );
function gutenberg_maintain_templates_routes() {
// This should later be changed in core so we don't need initialise
// WP_REST_Templates_Controller with a post type.
global $wp_post_types;
$wp_post_types['wp_template']->rest_base = 'templates';
$controller = new Gutenberg_REST_Templates_Controller_6_7( 'wp_template' );
$wp_post_types['wp_template']->rest_base = 'wp_template';
$controller->register_routes();
}

// 3. We need a route to get that raw static templates from themes and plugins.
// I registered this as a post type route because right now the
// EditorProvider assumes templates are posts.
add_action( 'init', 'gutenberg_setup_static_template' );
function gutenberg_setup_static_template() {
global $wp_post_types;
$wp_post_types['_wp_static_template'] = clone $wp_post_types['wp_template'];
$wp_post_types['_wp_static_template']->name = '_wp_static_template';
$wp_post_types['_wp_static_template']->rest_base = '_wp_static_template';
$wp_post_types['_wp_static_template']->rest_controller_class = 'Gutenberg_REST_Static_Templates_Controller';

register_setting(
'reading',
'active_templates',
array(
'type' => 'object',
'show_in_rest' => array(
'schema' => array(
'type' => 'object',
'additionalProperties' => true,
),
),
'default' => array(),
'label' => 'Active Templates',
)
);
}

add_filter( 'pre_wp_unique_post_slug', 'gutenberg_allow_template_slugs_to_be_duplicated', 10, 5 );
function gutenberg_allow_template_slugs_to_be_duplicated( $override, $slug, $post_id, $post_status, $post_type ) {
return 'wp_template' === $post_type ? $slug : $override;
}

add_filter( 'pre_get_block_templates', 'gutenberg_pre_get_block_templates', 10, 3 );
function gutenberg_pre_get_block_templates( $output, $query, $template_type ) {
if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) {
$active_templates = get_option( 'active_templates', array() );
$slugs = $query['slug__in'];
$output = array();
foreach ( $slugs as $slug ) {
if ( isset( $active_templates[ $slug ] ) ) {
if ( false !== $active_templates[ $slug ] ) {
$post = get_post( $active_templates[ $slug ] );
if ( $post && 'publish' === $post->post_status ) {
$output[] = _build_block_template_result_from_post( $post );
}
} else {
// Deactivated template, fall back to next slug.
$output[] = array();
}
}
}
if ( empty( $output ) ) {
$output = null;
}
}
return $output;
}

// Whenever templates are queried by slug, never return any user templates.
// We are handling that in gutenberg_pre_get_block_templates.
function gutenberg_remove_tax_query_for_templates( $query ) {
if ( isset( $query->query['post_type'] ) && 'wp_template' === $query->query['post_type'] ) {
// We don't have templates with this status, that's the point. We want
// this query to not return any user templates.
$query->set( 'post_status', array( 'pending' ) );
}
}

add_filter( 'pre_get_block_templates', 'gutenberg_tax_pre_get_block_templates', 10, 3 );
function gutenberg_tax_pre_get_block_templates( $output, $query, $template_type ) {
// Do not remove the tax query when querying for a specific slug.
if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) {
add_action( 'pre_get_posts', 'gutenberg_remove_tax_query_for_templates' );
}
return $output;
}

add_filter( 'get_block_templates', 'gutenberg_tax_get_block_templates', 10, 3 );
function gutenberg_tax_get_block_templates( $output, $query, $template_type ) {
if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) {
remove_action( 'pre_get_posts', 'gutenberg_remove_tax_query_for_templates' );
}
return $output;
}

// We need to set the theme for the template when it's created. See:
// https://github.com/WordPress/wordpress-develop/blob/b2c8d8d2c8754cab5286b06efb4c11e2b6aa92d5/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php#L571-L578
add_action( 'rest_pre_insert_wp_template', 'gutenberg_set_active_template_theme', 10, 2 );
function gutenberg_set_active_template_theme( $changes, $request ) {
$template = $request['id'] ? get_block_template( $request['id'], 'wp_template' ) : null;
if ( $template ) {
return $changes;
}
$changes->tax_input = array(
'wp_theme' => isset( $request['theme'] ) ? $request['theme'] : get_stylesheet(),
);
return $changes;
}

// Migrate existing "edited" templates. By existing, it means that the template
// is active.
add_action( 'init', 'gutenberg_migrate_existing_templates' );
function gutenberg_migrate_existing_templates() {
$active_templates = get_option( 'active_templates' );

if ( $active_templates ) {
return;
}

// Query all templates in the database. See `get_block_templates`.
$wp_query_args = array(
'post_status' => 'publish',
'post_type' => 'wp_template',
'posts_per_page' => -1,
'no_found_rows' => true,
'lazy_load_term_meta' => false,
'tax_query' => array(
array(
'taxonomy' => 'wp_theme',
'field' => 'name',
'terms' => get_stylesheet(),
),
),
);

$template_query = new WP_Query( $wp_query_args );
$active_templates = array();

foreach ( $template_query->posts as $post ) {
$active_templates[ $post->post_name ] = $post->ID;
}

update_option( 'active_templates', $active_templates );
}
3 changes: 3 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/compat/wordpress-6.7/rest-api.php';

// WordPress 6.8 compat.
require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-static-templates-controller.php';
require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-templates-controller.php';
require __DIR__ . '/compat/wordpress-6.8/template-activate.php';
require __DIR__ . '/compat/wordpress-6.8/block-comments.php';
require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-comment-controller-6-8.php';
require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-post-types-controller-6-8.php';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const icons = {
post,
page,
wp_template: layout,
_wp_static_template: layout,
wp_template_part: symbolFilled,
};

Expand Down Expand Up @@ -169,7 +170,7 @@ const getNavigationCommandLoaderPerTemplate = ( templateType ) =>
return {
isBlockBasedTheme:
select( coreStore ).getCurrentTheme()?.is_block_theme,
canCreateTemplate: select( coreStore ).canUser( 'create', {
canCreateTemplate: select( coreStore ).canUser( 'read', {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did we change the perm? Seems natural to check for create in canCreate...

Copy link
Member Author

Choose a reason for hiding this comment

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

Why? The commands are meant to navigate to templates, not create new templates?

Copy link
Member Author

Choose a reason for hiding this comment

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

But yes this variable should be renamed 😃

Copy link
Member

Choose a reason for hiding this comment

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

I agree that using the create capability check here is odd but necessary. Users who can read templates might not have access to the Site Editor.

See #60317 (comment).

P.S. We should extract similar minor and unrelated fixes into separate PRs. There's already a lot going on here and that would make reviewing bit easier.

Copy link
Member Author

Choose a reason for hiding this comment

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

Then this check should be called differently and we should use something else to check if the user has access to the site editor. This is not a good proxy for checking site editor access, it is more restrictive now. Also we should then use it as a condition to prevent all site editor commands from loading?

Copy link
Member

Choose a reason for hiding this comment

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

Also we should then use it as a condition to prevent all site editor commands from loading?

It's hard to generalize it. The Site Editor needs more granular capability checks, but those should be mapped out separately - maybe even incorporated into the router.

As I mentioned, I would remove and extract similar fixes into separate PRs.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, here the change is necessary because you cannot create static templates through the endpoint, you can only create custom templates.

Copy link
Member Author

Choose a reason for hiding this comment

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

So I guess we need another additional check, not sure what

kind: 'postType',
name: templateType,
} ),
Expand Down Expand Up @@ -420,6 +421,10 @@ export function useSiteEditorNavigationCommands() {
name: 'core/edit-site/navigate-templates',
hook: getNavigationCommandLoaderPerTemplate( 'wp_template' ),
} );
useCommandLoader( {
name: 'core/edit-site/navigate-templates',
hook: getNavigationCommandLoaderPerTemplate( '_wp_static_template' ),
} );
useCommandLoader( {
name: 'core/edit-site/navigate-template-parts',
hook: getNavigationCommandLoaderPerTemplate( 'wp_template_part' ),
Expand Down
Loading
Loading