Skip to content

Commit 32d3736

Browse files
authored
Allow template duplication + concept of active templates (#67125)
Co-authored-by: ellatrix <ellatrix@git.wordpress.org> Co-authored-by: youknowriad <youknowriad@git.wordpress.org> Co-authored-by: draganescu <andraganescu@git.wordpress.org> Co-authored-by: fabiankaegy <fabiankaegy@git.wordpress.org> Co-authored-by: jameskoster <jameskoster@git.wordpress.org> Co-authored-by: carlomanf <manfcarlo@git.wordpress.org> Co-authored-by: mtias <matveb@git.wordpress.org> Co-authored-by: annezazu <annezazu@git.wordpress.org> Co-authored-by: richtabor <richtabor@git.wordpress.org> Co-authored-by: skorasaurus <skorasaurus@git.wordpress.org> Co-authored-by: jordesign <jordesign@git.wordpress.org> Co-authored-by: talldan <talldanwp@git.wordpress.org> Co-authored-by: ramonjd <ramonopoly@git.wordpress.org> Co-authored-by: mrfoxtalbot <mrfoxtalbot@git.wordpress.org> Co-authored-by: Andrew-Starr <uxl@git.wordpress.org> Co-authored-by: carolinan <poena@git.wordpress.org> Co-authored-by: nerrad <nerrad@git.wordpress.org> Co-authored-by: Aljullu <aljullu@git.wordpress.org> Co-authored-by: sethrubenstein <smrubenstein@git.wordpress.org> Co-authored-by: justintadlock <greenshady@git.wordpress.org> Co-authored-by: scruffian <scruffian@git.wordpress.org> Co-authored-by: nickpagz <nickpagz@git.wordpress.org> Co-authored-by: liviopv <liviopv@git.wordpress.org> Unlinked contributors: erikjoling.
1 parent 6c0284b commit 32d3736

File tree

61 files changed

+1173
-610
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1173
-610
lines changed

backport-changelog/6.8/8063.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
https://github.com/WordPress/wordpress-develop/pull/8063
2+
3+
* https://github.com/WordPress/gutenberg/pull/67125
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
3+
class Gutenberg_REST_Static_Templates_Controller extends WP_REST_Templates_Controller {
4+
public function register_routes() {
5+
// Lists all templates.
6+
register_rest_route(
7+
$this->namespace,
8+
'/' . $this->rest_base,
9+
array(
10+
array(
11+
'methods' => WP_REST_Server::READABLE,
12+
'callback' => array( $this, 'get_items' ),
13+
'permission_callback' => array( $this, 'get_items_permissions_check' ),
14+
'args' => $this->get_collection_params(),
15+
),
16+
'schema' => array( $this, 'get_public_item_schema' ),
17+
)
18+
);
19+
20+
// Lists/updates a single template based on the given id.
21+
register_rest_route(
22+
$this->namespace,
23+
// The route.
24+
sprintf(
25+
'/%s/(?P<id>%s%s)',
26+
$this->rest_base,
27+
/*
28+
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
29+
* Excludes invalid directory name characters: `/:<>*?"|`.
30+
*/
31+
'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
32+
// Matches the template name.
33+
'[\/\w%-]+'
34+
),
35+
array(
36+
'args' => array(
37+
'id' => array(
38+
'description' => __( 'The id of a template' ),
39+
'type' => 'string',
40+
'sanitize_callback' => array( $this, '_sanitize_template_id' ),
41+
),
42+
),
43+
array(
44+
'methods' => WP_REST_Server::READABLE,
45+
'callback' => array( $this, 'get_item' ),
46+
'permission_callback' => array( $this, 'get_item_permissions_check' ),
47+
'args' => array(
48+
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
49+
),
50+
),
51+
'schema' => array( $this, 'get_public_item_schema' ),
52+
)
53+
);
54+
}
55+
56+
public function get_item_schema() {
57+
$schema = parent::get_item_schema();
58+
$schema['properties']['is_custom'] = array(
59+
'description' => __( 'Whether a template is a custom template.' ),
60+
'type' => 'bool',
61+
'context' => array( 'embed', 'view', 'edit' ),
62+
'readonly' => true,
63+
);
64+
$schema['properties']['plugin'] = array(
65+
'type' => 'string',
66+
'description' => __( 'Plugin that registered the template.' ),
67+
'readonly' => true,
68+
'context' => array( 'view', 'edit', 'embed' ),
69+
);
70+
return $schema;
71+
}
72+
73+
public function get_items( $request ) {
74+
$query = array();
75+
if ( isset( $request['area'] ) ) {
76+
$query['area'] = $request['area'];
77+
}
78+
if ( isset( $request['post_type'] ) ) {
79+
$query['post_type'] = $request['post_type'];
80+
}
81+
$template_files = _get_block_templates_files( 'wp_template', $query );
82+
$query_result = array();
83+
foreach ( $template_files as $template_file ) {
84+
$query_result[] = _build_block_template_result_from_file( $template_file, 'wp_template' );
85+
}
86+
87+
// Add templates registered in the template registry. Filtering out the ones which have a theme file.
88+
$registered_templates = WP_Block_Templates_Registry::get_instance()->get_by_query( $query );
89+
$matching_registered_templates = array_filter(
90+
$registered_templates,
91+
function ( $registered_template ) use ( $template_files ) {
92+
foreach ( $template_files as $template_file ) {
93+
if ( $template_file['slug'] === $registered_template->slug ) {
94+
return false;
95+
}
96+
}
97+
return true;
98+
}
99+
);
100+
101+
$query_result = array_merge( $query_result, $matching_registered_templates );
102+
103+
/**
104+
* Filters the array of queried block templates array after they've been fetched.
105+
*
106+
* @since 5.9.0
107+
*
108+
* @param WP_Block_Template[] $query_result Array of found block templates.
109+
* @param array $query {
110+
* Arguments to retrieve templates. All arguments are optional.
111+
*
112+
* @type string[] $slug__in List of slugs to include.
113+
* @type int $wp_id Post ID of customized template.
114+
* @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only).
115+
* @type string $post_type Post type to get the templates for.
116+
* }
117+
* @param string $template_type wp_template or wp_template_part.
118+
*/
119+
$query_result = apply_filters( 'get_block_templates', $query_result, $query, 'wp_template' );
120+
121+
$templates = array();
122+
foreach ( $query_result as $template ) {
123+
$item = $this->prepare_item_for_response( $template, $request );
124+
$item->data['type'] = 'wp_registered_template';
125+
$templates[] = $this->prepare_response_for_collection( $item );
126+
}
127+
128+
return rest_ensure_response( $templates );
129+
}
130+
131+
public function get_item( $request ) {
132+
$template = get_block_file_template( $request['id'], 'wp_template' );
133+
134+
if ( ! $template ) {
135+
return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
136+
}
137+
138+
$item = $this->prepare_item_for_response( $template, $request );
139+
// adjust the template type here instead
140+
$item->data['type'] = 'wp_registered_template';
141+
return rest_ensure_response( $item );
142+
}
143+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
class Gutenberg_REST_Templates_Controller extends WP_REST_Posts_Controller {
4+
protected function handle_status_param( $status, $request ) {
5+
if ( 'auto-draft' === $status ) {
6+
return $status;
7+
}
8+
return parent::handle_status_param( $status, $request );
9+
}
10+
protected function add_additional_fields_schema( $schema ) {
11+
$schema = parent::add_additional_fields_schema( $schema );
12+
13+
$schema['properties']['status']['enum'][] = 'auto-draft';
14+
return $schema;
15+
}
16+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/**
4+
* Preload necessary resources for the editors.
5+
*
6+
* @param array $paths REST API paths to preload.
7+
* @param WP_Block_Editor_Context $context Current block editor context
8+
*
9+
* @return array Filtered preload paths.
10+
*/
11+
function gutenberg_block_editor_preload_paths_6_9( $paths, $context ) {
12+
if ( 'core/edit-site' === $context->name ) {
13+
// Only prefetch for the root. If we preload it for all pages and it's not used
14+
// it won't be possible to invalidate.
15+
// To do: perhaps purge all preloaded paths when client side navigating.
16+
if ( '/' !== $_GET['p'] ) {
17+
$paths = array_filter(
18+
$paths,
19+
function ( $path ) {
20+
return '/wp/v2/templates/lookup?slug=front-page' !== $path && '/wp/v2/templates/lookup?slug=home' !== $path;
21+
}
22+
);
23+
}
24+
25+
$paths[] = '/wp/v2/wp_registered_template?context=edit';
26+
}
27+
28+
return $paths;
29+
}
30+
add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_6_9', 10, 2 );
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
3+
// How does this work?
4+
// 1. For wp_template, we remove the custom templates controller, so it becomes
5+
// a normal posts endpoint, modified slightly to allow auto-drafts.
6+
add_filter( 'register_post_type_args', 'gutenberg_modify_wp_template_post_type_args', 10, 2 );
7+
function gutenberg_modify_wp_template_post_type_args( $args, $post_type ) {
8+
if ( 'wp_template' === $post_type ) {
9+
$args['rest_base'] = 'wp_template';
10+
$args['rest_controller_class'] = 'Gutenberg_REST_Templates_Controller';
11+
$args['autosave_rest_controller_class'] = null;
12+
$args['revisions_rest_controller_class'] = null;
13+
}
14+
return $args;
15+
}
16+
17+
// 2. We maintain the routes for /templates and /templates/lookup. I think we'll
18+
// need to deprecate /templates eventually, but we'll still want to be able
19+
// to lookup the active template for a specific slug, and probably get a list
20+
// of all _active_ templates. For that we can keep /lookup.
21+
add_action( 'rest_api_init', 'gutenberg_maintain_templates_routes' );
22+
function gutenberg_maintain_templates_routes() {
23+
// This should later be changed in core so we don't need initialise
24+
// WP_REST_Templates_Controller with a post type.
25+
global $wp_post_types;
26+
$wp_post_types['wp_template']->rest_base = 'templates';
27+
$controller = new WP_REST_Templates_Controller( 'wp_template' );
28+
$wp_post_types['wp_template']->rest_base = 'wp_template';
29+
$controller->register_routes();
30+
}
31+
32+
// 3. We need a route to get that raw static templates from themes and plugins.
33+
// I registered this as a post type route because right now the
34+
// EditorProvider assumes templates are posts.
35+
add_action( 'init', 'gutenberg_setup_static_template' );
36+
function gutenberg_setup_static_template() {
37+
global $wp_post_types;
38+
$wp_post_types['wp_registered_template'] = clone $wp_post_types['wp_template'];
39+
$wp_post_types['wp_registered_template']->name = 'wp_registered_template';
40+
$wp_post_types['wp_registered_template']->rest_base = 'wp_registered_template';
41+
$wp_post_types['wp_registered_template']->rest_controller_class = 'Gutenberg_REST_Static_Templates_Controller';
42+
43+
register_setting(
44+
'reading',
45+
'active_templates',
46+
array(
47+
'type' => 'object',
48+
'show_in_rest' => array(
49+
'schema' => array(
50+
'type' => 'object',
51+
// properties can be integers or false (deactivated).
52+
'additionalProperties' => true,
53+
),
54+
),
55+
'default' => array(),
56+
'label' => 'Active Templates',
57+
)
58+
);
59+
}
60+
61+
add_filter( 'pre_wp_unique_post_slug', 'gutenberg_allow_template_slugs_to_be_duplicated', 10, 5 );
62+
function gutenberg_allow_template_slugs_to_be_duplicated( $override, $slug, $post_id, $post_status, $post_type ) {
63+
return 'wp_template' === $post_type ? $slug : $override;
64+
}
65+
66+
add_filter( 'pre_get_block_templates', 'gutenberg_pre_get_block_templates', 10, 3 );
67+
function gutenberg_pre_get_block_templates( $output, $query, $template_type ) {
68+
if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) {
69+
$active_templates = (array) get_option( 'active_templates', array() );
70+
$slugs = $query['slug__in'];
71+
$output = array();
72+
foreach ( $slugs as $slug ) {
73+
if ( isset( $active_templates[ $slug ] ) ) {
74+
if ( false !== $active_templates[ $slug ] ) {
75+
$post = get_post( $active_templates[ $slug ] );
76+
if ( $post && 'publish' === $post->post_status ) {
77+
$output[] = _build_block_template_result_from_post( $post );
78+
}
79+
} else {
80+
// Deactivated template, fall back to next slug.
81+
$output[] = array();
82+
}
83+
}
84+
}
85+
if ( empty( $output ) ) {
86+
$output = null;
87+
}
88+
}
89+
return $output;
90+
}
91+
92+
// Whenever templates are queried by slug, never return any user templates.
93+
// We are handling that in gutenberg_pre_get_block_templates.
94+
function gutenberg_remove_tax_query_for_templates( $query ) {
95+
if ( isset( $query->query['post_type'] ) && 'wp_template' === $query->query['post_type'] ) {
96+
// We don't have templates with this status, that's the point. We want
97+
// this query to not return any user templates.
98+
$query->set( 'post_status', array( 'pending' ) );
99+
}
100+
}
101+
102+
add_filter( 'pre_get_block_templates', 'gutenberg_tax_pre_get_block_templates', 10, 3 );
103+
function gutenberg_tax_pre_get_block_templates( $output, $query, $template_type ) {
104+
// Do not remove the tax query when querying for a specific slug.
105+
if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) {
106+
add_action( 'pre_get_posts', 'gutenberg_remove_tax_query_for_templates' );
107+
}
108+
return $output;
109+
}
110+
111+
add_filter( 'get_block_templates', 'gutenberg_tax_get_block_templates', 10, 3 );
112+
function gutenberg_tax_get_block_templates( $output, $query, $template_type ) {
113+
if ( 'wp_template' === $template_type && ! empty( $query['slug__in'] ) ) {
114+
remove_action( 'pre_get_posts', 'gutenberg_remove_tax_query_for_templates' );
115+
}
116+
return $output;
117+
}
118+
119+
// We need to set the theme for the template when it's created. See:
120+
// https://github.com/WordPress/wordpress-develop/blob/b2c8d8d2c8754cab5286b06efb4c11e2b6aa92d5/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php#L571-L578
121+
// Priority 9 so it runs before default hooks like
122+
// `inject_ignored_hooked_blocks_metadata_attributes`.
123+
add_action( 'rest_pre_insert_wp_template', 'gutenberg_set_active_template_theme', 9, 2 );
124+
function gutenberg_set_active_template_theme( $changes, $request ) {
125+
$template = $request['id'] ? get_block_template( $request['id'], 'wp_template' ) : null;
126+
if ( $template ) {
127+
return $changes;
128+
}
129+
$changes->tax_input = array(
130+
'wp_theme' => isset( $request['theme'] ) ? $request['theme'] : get_stylesheet(),
131+
);
132+
return $changes;
133+
}
134+
135+
// Migrate existing "edited" templates. By existing, it means that the template
136+
// is active.
137+
add_action( 'init', 'gutenberg_migrate_existing_templates' );
138+
function gutenberg_migrate_existing_templates() {
139+
$active_templates = get_option( 'active_templates' );
140+
141+
if ( $active_templates ) {
142+
return;
143+
}
144+
145+
// Query all templates in the database. See `get_block_templates`.
146+
$wp_query_args = array(
147+
'post_status' => 'publish',
148+
'post_type' => 'wp_template',
149+
'posts_per_page' => -1,
150+
'no_found_rows' => true,
151+
'lazy_load_term_meta' => false,
152+
'tax_query' => array(
153+
array(
154+
'taxonomy' => 'wp_theme',
155+
'field' => 'name',
156+
'terms' => get_stylesheet(),
157+
),
158+
),
159+
);
160+
161+
$template_query = new WP_Query( $wp_query_args );
162+
$active_templates = array();
163+
164+
foreach ( $template_query->posts as $post ) {
165+
$active_templates[ $post->post_name ] = $post->ID;
166+
}
167+
168+
update_option( 'active_templates', $active_templates );
169+
}

lib/load.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ function gutenberg_is_experiment_enabled( $name ) {
3939
require __DIR__ . '/compat/wordpress-6.8/rest-api.php';
4040

4141
// WordPress 6.9 compat.
42+
require __DIR__ . '/compat/wordpress-6.9/class-gutenberg-rest-static-templates-controller.php';
43+
require __DIR__ . '/compat/wordpress-6.9/class-gutenberg-rest-templates-controller.php';
44+
require __DIR__ . '/compat/wordpress-6.9/template-activate.php';
4245
require __DIR__ . '/compat/wordpress-6.9/block-bindings.php';
4346
require __DIR__ . '/compat/wordpress-6.9/post-data-block-bindings.php';
4447
require __DIR__ . '/compat/wordpress-6.9/term-data-block-bindings.php';
@@ -84,6 +87,7 @@ function gutenberg_is_experiment_enabled( $name ) {
8487
// WordPress 6.9 compat.
8588
require __DIR__ . '/compat/wordpress-6.9/customizer-preview-custom-css.php';
8689
require __DIR__ . '/compat/wordpress-6.9/command-palette.php';
90+
require __DIR__ . '/compat/wordpress-6.9/preload.php';
8791

8892
// Experimental features.
8993
require __DIR__ . '/experimental/block-editor-settings-mobile.php';

0 commit comments

Comments
 (0)