Templates: add PHP changes required for the template activation feature.

* Adds the `active_templates` setting, which is an object holding the template slug as a key and template post ID as the value.
* To maintain backwards compatibility, any `wp_template` (post type) not created through the new API will be activated.
* `get_block_template` and `get_block_templates` have been adjusted to check `active_templates`. These functions should never return inactive templates, just like before, to maintain backwards compatibility.
* The pre-existing `/templates` endpoint and sub-endpoints remain and work exactly as before.
* A new endpoint `/wp_template` has been added, but this is just a regular posts controller (`WP_REST_Posts_Controller`). We do register an additional `theme` field and expose the `is_wp_suggestion` meta.
* Another new endpoint `/wp_registered_template` has been added, which is read-only and lists the registered templates from themes and plugin (un-edited, without activations applied).

These changes are to be iterated on.

See https://github.com/WordPress/wordpress-develop/pull/8063.

Props ellatrix, shailu25, ntsekouras.
Fixes #62755.
Built from https://develop.svn.wordpress.org/trunk@61029


git-svn-id: http://core.svn.wordpress.org/trunk@60365 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
ellatrix
2025-10-21 13:43:58 +00:00
parent 6782a70a92
commit 82c917225c
10 changed files with 294 additions and 14 deletions

View File

@@ -182,6 +182,7 @@ $preload_paths = array(
array( rest_get_route_for_post_type_items( 'attachment' ), 'OPTIONS' ),
array( rest_get_route_for_post_type_items( 'page' ), 'OPTIONS' ),
'/wp/v2/types?context=view',
'/wp/v2/wp_registered_template?context=edit',
'/wp/v2/types/wp_template?context=edit',
'/wp/v2/types/wp_template_part?context=edit',
'/wp/v2/templates?context=edit&per_page=-1',
@@ -244,7 +245,9 @@ if ( $block_editor_context->post ) {
);
}
}
} else {
} elseif ( isset( $_GET['p'] ) && '/' !== $_GET['p'] ) {
// Only prefetch for the root. If we preload it for all pages and it's not
// used it won't be possible to invalidate.
$preload_paths[] = '/wp/v2/templates/lookup?slug=front-page';
$preload_paths[] = '/wp/v2/templates/lookup?slug=home';
}

View File

@@ -1074,6 +1074,46 @@ function _build_block_template_result_from_post( $post ) {
return $template;
}
function get_registered_block_templates( $query ) {
$template_files = _get_block_templates_files( 'wp_template', $query );
$query_result = array();
// _get_block_templates_files seems broken, it does not obey the query.
if ( isset( $query['slug__in'] ) && is_array( $query['slug__in'] ) ) {
$template_files = array_filter(
$template_files,
function ( $template_file ) use ( $query ) {
return in_array( $template_file['slug'], $query['slug__in'], true );
}
);
}
foreach ( $template_files as $template_file ) {
$query_result[] = _build_block_template_result_from_file( $template_file, 'wp_template' );
}
// Add templates registered through 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 added by PHP filter also count as registered templates.
/** This filter is documented in wp-includes/block-template-utils.php */
return apply_filters( 'get_block_templates', $query_result, $query, 'wp_template' );
}
/**
* Retrieves a list of unified template objects based on a query.
*
@@ -1152,6 +1192,8 @@ function get_block_templates( $query = array(), $template_type = 'wp_template' )
$wp_query_args['post_status'] = 'publish';
}
$active_templates = get_option( 'active_templates', array() );
$template_query = new WP_Query( $wp_query_args );
$query_result = array();
foreach ( $template_query->posts as $post ) {
@@ -1173,7 +1215,14 @@ function get_block_templates( $query = array(), $template_type = 'wp_template' )
continue;
}
$query_result[] = $template;
if ( $template->is_custom || isset( $query['wp_id'] ) ) {
// Custom templates don't need to be activated, leave them be.
// Also don't filter out templates when querying by wp_id.
$query_result[] = $template;
} elseif ( isset( $active_templates[ $template->slug ] ) && $active_templates[ $template->slug ] === $post->ID ) {
// Only include active templates.
$query_result[] = $template;
}
}
if ( ! isset( $query['wp_id'] ) ) {
@@ -1296,7 +1345,25 @@ function get_block_template( $id, $template_type = 'wp_template' ) {
return null;
}
list( $theme, $slug ) = $parts;
$wp_query_args = array(
$active_templates = get_option( 'active_templates', array() );
if ( ! empty( $active_templates[ $slug ] ) ) {
if ( is_int( $active_templates[ $slug ] ) ) {
$post = get_post( $active_templates[ $slug ] );
if ( $post && 'publish' === $post->post_status ) {
$template = _build_block_template_result_from_post( $post );
if ( ! is_wp_error( $template ) && $theme === $template->theme ) {
return $template;
}
}
} elseif ( false === $active_templates[ $slug ] ) {
return null;
}
}
$wp_query_args = array(
'post_name__in' => array( $slug ),
'post_type' => $template_type,
'post_status' => array( 'auto-draft', 'draft', 'publish', 'trash' ),
@@ -1310,12 +1377,18 @@ function get_block_template( $id, $template_type = 'wp_template' ) {
),
),
);
$template_query = new WP_Query( $wp_query_args );
$posts = $template_query->posts;
$template_query = new WP_Query( $wp_query_args );
$posts = $template_query->posts;
if ( count( $posts ) > 0 ) {
$template = _build_block_template_result_from_post( $posts[0] );
// Custom templates don't need to be activated, so if it's a custom
// template, return it.
if ( ! is_wp_error( $template ) && $template->is_custom ) {
return $template;
}
if ( ! is_wp_error( $template ) ) {
return $template;
}
@@ -1779,3 +1852,33 @@ function inject_ignored_hooked_blocks_metadata_attributes( $changes, $deprecated
return $changes;
}
function wp_assign_new_template_to_theme( $changes, $request ) {
// Do not run this for templates created through the old enpoint.
$template = $request['id'] ? get_block_template( $request['id'], 'wp_template' ) : null;
if ( $template ) {
return $changes;
}
if ( ! isset( $changes->tax_input ) ) {
$changes->tax_input = array();
}
$changes->tax_input['wp_theme'] = isset( $request['theme'] ) ? $request['theme'] : get_stylesheet();
// All new templates saved will receive meta so we can distinguish between
// templates created the old way as edits and templates created the new way.
if ( ! isset( $changes->meta_input ) ) {
$changes->meta_input = array();
}
$changes->meta_input['is_inactive_by_default'] = true;
return $changes;
}
function wp_maybe_activate_template( $post_id ) {
$post = get_post( $post_id );
$is_inactive_by_default = get_post_meta( $post_id, 'is_inactive_by_default', true );
if ( $is_inactive_by_default ) {
return;
}
$active_templates = get_option( 'active_templates', array() );
$active_templates[ $post->post_name ] = $post->ID;
update_option( 'active_templates', $active_templates );
}

View File

@@ -164,11 +164,77 @@ function resolve_block_template( $template_type, $template_hierarchy, $fallback_
$template_hierarchy
);
// Find all potential templates 'wp_template' post matching the hierarchy.
$query = array(
'slug__in' => $slugs,
$object = get_queried_object();
$specific_template = $object ? get_page_template_slug( $object ) : null;
$active_templates = (array) get_option( 'active_templates', array() );
// Remove templates slugs that are deactivated, except if it's the specific
// template or index.
$slugs = array_filter(
$slugs,
function ( $slug ) use ( $specific_template, $active_templates ) {
$should_ignore = $slug === $specific_template || 'index' === $slug;
return $should_ignore || ( ! isset( $active_templates[ $slug ] ) || false !== $active_templates[ $slug ] );
}
);
$templates = get_block_templates( $query );
// We expect one template for each slug. Use the active template if it is
// set and exists. Otherwise use the static template.
$templates = array();
$remaining_slugs = array();
foreach ( $slugs as $slug ) {
if ( $slug === $specific_template || empty( $active_templates[ $slug ] ) ) {
$remaining_slugs[] = $slug;
continue;
}
// TODO: it need to be possible to set a static template as active.
$post = get_post( $active_templates[ $slug ] );
if ( ! $post || 'publish' !== $post->post_status ) {
$remaining_slugs[] = $slug;
continue;
}
$template = _build_block_template_result_from_post( $post );
// Ensure the active templates are associated with the active theme.
// See _build_block_template_object_from_post_object.
if ( get_stylesheet() !== $template->theme ) {
$remaining_slugs[] = $slug;
continue;
}
$templates[] = $template;
}
// Apply the filter to the active templates for backward compatibility.
/** This filter is documented in wp-includes/block-template-utils.php */
if ( ! empty( $templates ) ) {
$templates = apply_filters(
'get_block_templates',
$templates,
array(
'slug__in' => array_map(
function ( $template ) {
return $template->slug;
},
$templates
),
),
'wp_template'
);
}
// For any remaining slugs, use the static template.
$query = array(
'slug__in' => $remaining_slugs,
);
$templates = array_merge( $templates, get_registered_block_templates( $query ) );
if ( $specific_template ) {
$templates = array_merge( $templates, get_block_templates( array( 'slug__in' => array( $specific_template ) ) ) );
}
// Order these templates per slug priority.
// Build map of template slugs to their priority in the current hierarchy.

View File

@@ -740,6 +740,7 @@ add_filter( 'user_has_cap', 'wp_maybe_grant_site_health_caps', 1, 4 );
// Block templates post type and rendering.
add_filter( 'render_block_context', '_block_template_render_without_post_block_context' );
add_filter( 'pre_wp_unique_post_slug', 'wp_filter_wp_template_unique_post_slug', 10, 5 );
add_action( 'save_post_wp_template', 'wp_maybe_activate_template' );
add_action( 'save_post_wp_template_part', 'wp_set_unique_slug_on_create_template_part' );
add_action( 'wp_enqueue_scripts', 'wp_enqueue_block_template_skip_link' );
add_action( 'wp_footer', 'the_block_template_skip_link' ); // Retained for backwards-compatibility. Unhooked by wp_enqueue_block_template_skip_link().
@@ -780,6 +781,10 @@ add_action( 'init', '_wp_register_default_font_collections' );
add_filter( 'rest_pre_insert_wp_template', 'inject_ignored_hooked_blocks_metadata_attributes' );
add_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes' );
// Assign the wp_theme term to any newly created wp_template with the new endpoint.
// Must run before `inject_ignored_hooked_blocks_metadata_attributes`.
add_action( 'rest_pre_insert_wp_template', 'wp_assign_new_template_to_theme', 9, 2 );
// Update ignoredHookedBlocks postmeta for some post types.
add_filter( 'rest_pre_insert_page', 'update_ignored_hooked_blocks_postmeta' );
add_filter( 'rest_pre_insert_post', 'update_ignored_hooked_blocks_postmeta' );

View File

@@ -2959,6 +2959,25 @@ function register_initial_settings() {
'description' => __( 'Allow people to submit comments on new posts.' ),
)
);
register_setting(
'reading',
'active_templates',
array(
'type' => 'object',
// Do not set the default value to an empty array! For some reason
// that will prevent the option from being set to an empty array.
'show_in_rest' => array(
'schema' => array(
'type' => 'object',
// Properties can be integers, strings, or false
// (deactivated).
'additionalProperties' => true,
),
),
'label' => 'Active Templates',
)
);
}
/**

View File

@@ -398,10 +398,8 @@ function create_initial_post_types() {
'show_in_menu' => false,
'show_in_rest' => true,
'rewrite' => false,
'rest_base' => 'templates',
'rest_controller_class' => 'WP_REST_Templates_Controller',
'autosave_rest_controller_class' => 'WP_REST_Template_Autosaves_Controller',
'revisions_rest_controller_class' => 'WP_REST_Template_Revisions_Controller',
'rest_base' => 'wp_template',
'rest_controller_class' => 'WP_REST_Posts_Controller',
'late_route_registration' => true,
'capability_type' => array( 'template', 'templates' ),
'capabilities' => array(
@@ -426,6 +424,7 @@ function create_initial_post_types() {
'editor',
'revisions',
'author',
'custom-fields',
),
)
);
@@ -8629,4 +8628,16 @@ function wp_create_initial_post_meta() {
),
)
);
// Allow setting the is_wp_suggestion meta field, which partly determines if
// a template is a custom template.
register_post_meta(
'wp_template',
'is_wp_suggestion',
array(
'type' => 'boolean',
'show_in_rest' => true,
'single' => true,
)
);
}

View File

@@ -263,6 +263,15 @@ function rest_api_default_filters() {
* @since 4.7.0
*/
function create_initial_rest_routes() {
global $wp_post_types;
// Register the registered templates endpoint. For that we need to copy the
// wp_template post type so that it's available as an entity in core-data.
$wp_post_types['wp_registered_template'] = clone $wp_post_types['wp_template'];
$wp_post_types['wp_registered_template']->name = 'wp_registered_template';
$wp_post_types['wp_registered_template']->rest_base = 'wp_registered_template';
$wp_post_types['wp_registered_template']->rest_controller_class = 'WP_REST_Registered_Templates_Controller';
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
$controller = $post_type->get_rest_controller();
@@ -289,6 +298,64 @@ function create_initial_rest_routes() {
}
}
// Register the old templates endpoints. The WP_REST_Templates_Controller
// and sub-controllers used linked to the wp_template post type, but are no
// longer. They still require a post type object when contructing the class.
// To maintain backward and changes to these controller classes, we make use
// that the wp_template post type has the right information it needs.
$wp_post_types['wp_template']->rest_base = 'templates';
// Store the classes so they can be restored.
$original_rest_controller_class = $wp_post_types['wp_template']->rest_controller_class;
$original_autosave_rest_controller_class = $wp_post_types['wp_template']->autosave_rest_controller_class;
$original_revisions_rest_controller_class = $wp_post_types['wp_template']->revisions_rest_controller_class;
// Temporarily set the old classes.
$wp_post_types['wp_template']->rest_controller_class = 'WP_REST_Templates_Controller';
$wp_post_types['wp_template']->autosave_rest_controller_class = 'WP_REST_Template_Autosaves_Controller';
$wp_post_types['wp_template']->revisions_rest_controller_class = 'WP_REST_Template_Revisions_Controller';
// Initialize the controllers. The order is important: the autosave
// controller needs both the templates and revisions controllers.
$controller = new WP_REST_Templates_Controller( 'wp_template' );
$wp_post_types['wp_template']->rest_controller = $controller;
$revisions_controller = new WP_REST_Template_Revisions_Controller( 'wp_template' );
$wp_post_types['wp_template']->revisions_rest_controller = $revisions_controller;
$autosaves_controller = new WP_REST_Template_Autosaves_Controller( 'wp_template' );
// Unset the controller cache, it will be re-initialized when
// get_rest_controller is called.
$wp_post_types['wp_template']->rest_controller = null;
$wp_post_types['wp_template']->revisions_rest_controller = null;
// Restore the original classes.
$wp_post_types['wp_template']->rest_controller_class = $original_rest_controller_class;
$wp_post_types['wp_template']->autosave_rest_controller_class = $original_autosave_rest_controller_class;
$wp_post_types['wp_template']->revisions_rest_controller_class = $original_revisions_rest_controller_class;
// Restore the original base.
$wp_post_types['wp_template']->rest_base = 'wp_template';
// Register the old routes.
$autosaves_controller->register_routes();
$revisions_controller->register_routes();
$controller->register_routes();
register_rest_field(
'wp_template',
'theme',
array(
'get_callback' => function ( $post_arr ) {
// add_additional_fields_to_object is also called for the old
// templates controller, so we need to check if the id is an
// integer to make sure it's the proper post type endpoint.
if ( ! is_int( $post_arr['id'] ) ) {
$template = get_block_template( $post_arr['id'], 'wp_template' );
return $template ? $template->theme : null;
}
$terms = get_the_terms( $post_arr['id'], 'wp_theme' );
if ( is_wp_error( $terms ) || empty( $terms ) ) {
return null;
}
return $terms[0]->slug;
},
)
);
// Post types.
$controller = new WP_REST_Post_Types_Controller();
$controller->register_routes();

View File

@@ -49,6 +49,11 @@ function wp_filter_wp_template_unique_post_slug( $override_slug, $slug, $post_id
return $override_slug;
}
// For wp_template, slugs no longer have to be unique within the same theme.
if ( 'wp_template' !== $post_type ) {
return $override_slug;
}
if ( ! $override_slug ) {
$override_slug = $slug;
}

View File

@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
$wp_version = '6.9-alpha-61028';
$wp_version = '6.9-alpha-61029';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.

View File

@@ -325,6 +325,7 @@ require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-sidebars-controller
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-widget-types-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-widgets-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-templates-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-registered-templates-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-url-details-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-navigation-fallback-controller.php';
require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-font-families-controller.php';