From 82c917225c1118f324a841e4e7ffa043cb753654 Mon Sep 17 00:00:00 2001 From: ellatrix Date: Tue, 21 Oct 2025 13:43:58 +0000 Subject: [PATCH] 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 --- wp-admin/site-editor.php | 5 +- wp-includes/block-template-utils.php | 111 ++++++++++++++++++++++++++- wp-includes/block-template.php | 74 +++++++++++++++++- wp-includes/default-filters.php | 5 ++ wp-includes/option.php | 19 +++++ wp-includes/post.php | 19 ++++- wp-includes/rest-api.php | 67 ++++++++++++++++ wp-includes/theme-templates.php | 5 ++ wp-includes/version.php | 2 +- wp-settings.php | 1 + 10 files changed, 294 insertions(+), 14 deletions(-) diff --git a/wp-admin/site-editor.php b/wp-admin/site-editor.php index b0bb4e2bb1..d27f461c9f 100644 --- a/wp-admin/site-editor.php +++ b/wp-admin/site-editor.php @@ -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'; } diff --git a/wp-includes/block-template-utils.php b/wp-includes/block-template-utils.php index be15b8c398..b0ca8c6caf 100644 --- a/wp-includes/block-template-utils.php +++ b/wp-includes/block-template-utils.php @@ -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 ); +} diff --git a/wp-includes/block-template.php b/wp-includes/block-template.php index eecbe2d61d..2dd7b2e3c0 100644 --- a/wp-includes/block-template.php +++ b/wp-includes/block-template.php @@ -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. diff --git a/wp-includes/default-filters.php b/wp-includes/default-filters.php index 52830054ff..ba27765063 100644 --- a/wp-includes/default-filters.php +++ b/wp-includes/default-filters.php @@ -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' ); diff --git a/wp-includes/option.php b/wp-includes/option.php index 7cb4736c28..58217ce317 100644 --- a/wp-includes/option.php +++ b/wp-includes/option.php @@ -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', + ) + ); } /** diff --git a/wp-includes/post.php b/wp-includes/post.php index 19cd927c5c..d73088f0ed 100644 --- a/wp-includes/post.php +++ b/wp-includes/post.php @@ -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, + ) + ); } diff --git a/wp-includes/rest-api.php b/wp-includes/rest-api.php index 836e0e5ec8..8402f2d8e3 100644 --- a/wp-includes/rest-api.php +++ b/wp-includes/rest-api.php @@ -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(); diff --git a/wp-includes/theme-templates.php b/wp-includes/theme-templates.php index eed0fb9b2b..7ebc3cdc1a 100644 --- a/wp-includes/theme-templates.php +++ b/wp-includes/theme-templates.php @@ -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; } diff --git a/wp-includes/version.php b/wp-includes/version.php index 289bb28422..e4889fd8df 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -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. diff --git a/wp-settings.php b/wp-settings.php index b67b385e79..579765028c 100644 --- a/wp-settings.php +++ b/wp-settings.php @@ -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';