diff --git a/wp-admin/edit-form-blocks.php b/wp-admin/edit-form-blocks.php
index eaf0138412..3aead40a7b 100644
--- a/wp-admin/edit-form-blocks.php
+++ b/wp-admin/edit-form-blocks.php
@@ -220,6 +220,7 @@ $editor_settings = array(
'supportsLayout' => WP_Theme_JSON_Resolver::theme_has_support(),
'__experimentalBlockPatterns' => WP_Block_Patterns_Registry::get_instance()->get_all_registered(),
'__experimentalBlockPatternCategories' => WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered(),
+ 'supportsTemplateMode' => current_theme_supports( 'block-templates' ),
// Whether or not to load the 'postcustom' meta box is stored as a user meta
// field so that we're not always loading its assets.
diff --git a/wp-includes/block-template-utils.php b/wp-includes/block-template-utils.php
new file mode 100644
index 0000000000..fa32393290
--- /dev/null
+++ b/wp-includes/block-template-utils.php
@@ -0,0 +1,144 @@
+name;
+
+ $template = new WP_Block_Template();
+ $template->wp_id = $post->ID;
+ $template->id = $theme . '//' . $post->post_name;
+ $template->theme = $theme;
+ $template->content = $post->post_content;
+ $template->slug = $post->post_name;
+ $template->source = 'custom';
+ $template->type = $post->post_type;
+ $template->description = $post->post_excerpt;
+ $template->title = $post->post_title;
+ $template->status = $post->post_status;
+ $template->has_theme_file = false;
+
+ return $template;
+}
+
+/**
+ * Retrieves a list of unified template objects based on a query.
+ *
+ * @since 5.8.0
+ *
+ * @param array $query {
+ * Optional. Arguments to retrieve templates.
+ *
+ * @type array $slug__in List of slugs to include.
+ * @type int $wp_id Post ID of customized template.
+ * }
+ * @param string $template_type wp_template.
+ *
+ * @return array Templates.
+ */
+function get_block_templates( $query = array(), $template_type = 'wp_template' ) {
+ $wp_query_args = array(
+ 'post_status' => array( 'auto-draft', 'draft', 'publish' ),
+ 'post_type' => $template_type,
+ 'posts_per_page' => -1,
+ 'no_found_rows' => true,
+ 'tax_query' => array(
+ array(
+ 'taxonomy' => 'wp_theme',
+ 'field' => 'name',
+ 'terms' => wp_get_theme()->get_stylesheet(),
+ ),
+ ),
+ );
+
+ if ( isset( $query['slug__in'] ) ) {
+ $wp_query_args['post_name__in'] = $query['slug__in'];
+ }
+
+ // This is only needed for the regular templates CPT listing and editor.
+ if ( isset( $query['wp_id'] ) ) {
+ $wp_query_args['p'] = $query['wp_id'];
+ } else {
+ $wp_query_args['post_status'] = 'publish';
+ }
+
+ $template_query = new WP_Query( $wp_query_args );
+ $query_result = array();
+ foreach ( $template_query->get_posts() as $post ) {
+ $template = _build_template_result_from_post( $post );
+
+ if ( ! is_wp_error( $template ) ) {
+ $query_result[] = $template;
+ }
+ }
+
+ return $query_result;
+}
+
+/**
+ * Retrieves a single unified template object using its id.
+ *
+ * @since 5.8.0
+ *
+ * @param string $id Template unique identifier (example: theme_slug//template_slug).
+ * @param string $template_type wp_template.
+ *
+ * @return WP_Block_Template|null Template.
+ */
+function get_block_template( $id, $template_type = 'wp_template' ) {
+ $parts = explode( '//', $id, 2 );
+ if ( count( $parts ) < 2 ) {
+ return null;
+ }
+ list( $theme, $slug ) = $parts;
+ $wp_query_args = array(
+ 'post_name__in' => array( $slug ),
+ 'post_type' => $template_type,
+ 'post_status' => array( 'auto-draft', 'draft', 'publish', 'trash' ),
+ 'posts_per_page' => 1,
+ 'no_found_rows' => true,
+ 'tax_query' => array(
+ array(
+ 'taxonomy' => 'wp_theme',
+ 'field' => 'name',
+ 'terms' => $theme,
+ ),
+ ),
+ );
+ $template_query = new WP_Query( $wp_query_args );
+ $posts = $template_query->get_posts();
+
+ if ( count( $posts ) > 0 ) {
+ $template = _build_template_result_from_post( $posts[0] );
+
+ if ( ! is_wp_error( $template ) ) {
+ return $template;
+ }
+ }
+
+ return null;
+}
diff --git a/wp-includes/block-template.php b/wp-includes/block-template.php
new file mode 100644
index 0000000000..c93a083472
--- /dev/null
+++ b/wp-includes/block-template.php
@@ -0,0 +1,228 @@
+content ) && is_user_logged_in() ) {
+ $_wp_current_template_content =
+ sprintf(
+ /* translators: %s: Template title */
+ __( 'Empty template: %s' ),
+ $block_template->title
+ );
+ } elseif ( ! empty( $block_template->content ) ) {
+ $_wp_current_template_content = $block_template->content;
+ }
+ if ( isset( $_GET['_wp-find-template'] ) ) {
+ wp_send_json_success( $block_template );
+ }
+ } else {
+ if ( $template ) {
+ return $template;
+ }
+
+ if ( 'index' === $type ) {
+ if ( isset( $_GET['_wp-find-template'] ) ) {
+ wp_send_json_error( array( 'message' => __( 'No matching template found.' ) ) );
+ }
+ } else {
+ return ''; // So that the template loader keeps looking for templates.
+ }
+ }
+
+ // Add hooks for template canvas.
+ // Add viewport meta tag.
+ add_action( 'wp_head', '_block_template_viewport_meta_tag', 0 );
+
+ // Render title tag with content, regardless of whether theme has title-tag support.
+ remove_action( 'wp_head', '_wp_render_title_tag', 1 ); // Remove conditional title tag rendering...
+ add_action( 'wp_head', '_block_template_render_title_tag', 1 ); // ...and make it unconditional.
+
+ // This file will be included instead of the theme's template file.
+ return ABSPATH . WPINC . '/template-canvas.php';
+}
+
+/**
+ * Return the correct 'wp_template' to render for the request template type.
+ *
+ * @access private
+ * @since 5.8.0
+ *
+ * Accepts an optional $template_hierarchy argument as a hint.
+ *
+ * @param string $template_type The current template type.
+ * @param string[] $template_hierarchy (optional) The current template hierarchy, ordered by priority.
+ * @return WP_Block_Template|null template A template object, or null if none could be found.
+ */
+function resolve_block_template( $template_type, $template_hierarchy ) {
+ if ( ! $template_type ) {
+ return null;
+ }
+
+ if ( empty( $template_hierarchy ) ) {
+ $template_hierarchy = array( $template_type );
+ }
+
+ $slugs = array_map(
+ '_strip_template_file_suffix',
+ $template_hierarchy
+ );
+
+ // Find all potential templates 'wp_template' post matching the hierarchy.
+ $query = array(
+ 'theme' => wp_get_theme()->get_stylesheet(),
+ 'slug__in' => $slugs,
+ );
+ $templates = get_block_templates( $query );
+
+ // Order these templates per slug priority.
+ // Build map of template slugs to their priority in the current hierarchy.
+ $slug_priorities = array_flip( $slugs );
+
+ usort(
+ $templates,
+ function ( $template_a, $template_b ) use ( $slug_priorities ) {
+ return $slug_priorities[ $template_a->slug ] - $slug_priorities[ $template_b->slug ];
+ }
+ );
+
+ return count( $templates ) ? $templates[0] : null;
+}
+
+/**
+ * Displays title tag with content, regardless of whether theme has title-tag support.
+ *
+ * @access private
+ * @since 5.8.0
+ *
+ * @see _wp_render_title_tag()
+ */
+function _block_template_render_title_tag() {
+ echo '
' . wp_get_document_title() . '' . "\n";
+}
+
+/**
+ * Returns the markup for the current template.
+ *
+ * @access private
+ * @since 5.8.0
+ *
+ * @return string block tempate markup.
+ */
+function get_the_block_template_html() {
+ global $_wp_current_template_content;
+ global $wp_embed;
+
+ if ( ! $_wp_current_template_content ) {
+ if ( is_user_logged_in() ) {
+ return '' . esc_html__( 'No matching template found' ) . '
';
+ }
+ return;
+ }
+
+ $content = $wp_embed->run_shortcode( $_wp_current_template_content );
+ $content = $wp_embed->autoembed( $content );
+ $content = do_blocks( $content );
+ $content = wptexturize( $content );
+ if ( function_exists( 'wp_filter_content_tags' ) ) {
+ $content = wp_filter_content_tags( $content );
+ } else {
+ $content = wp_make_content_images_responsive( $content );
+ }
+ $content = str_replace( ']]>', ']]>', $content );
+
+ // Wrap block template in .wp-site-blocks to allow for specific descendant styles
+ // (e.g. `.wp-site-blocks > *`).
+ return '' . $content . '
';
+}
+
+/**
+ * Renders a 'viewport' meta tag.
+ *
+ * @access private
+ * @since 5.8.0
+ *
+ * This is hooked into {@see 'wp_head'} to decouple its output from the default template canvas.
+ */
+function _block_template_viewport_meta_tag() {
+ echo '' . "\n";
+}
+
+/**
+ * Strips .php or .html suffix from template file names.
+ *
+ * @access private
+ * @since 5.8.0
+ *
+ * @param string $template_file Template file name.
+ * @return string Template file name without extension.
+ */
+function _strip_template_file_suffix( $template_file ) {
+ return preg_replace( '/\.(php|html)$/', '', $template_file );
+}
+
+/**
+ * Removes post details from block context when rendering a block template.
+ *
+ * @access private
+ * @since 5.8.0
+ *
+ * @param array $context Default context.
+ *
+ * @return array Filtered context.
+ */
+function _block_template_render_without_post_block_context( $context ) {
+ /*
+ * When loading a template directly and not through a page
+ * that resolves it, the top-level post ID and type context get set to that
+ * of the template. Templates are just the structure of a site, and
+ * they should not be available as post context because blocks like Post
+ * Content would recurse infinitely.
+ */
+ if ( isset( $context['postType'] ) && 'wp_template' === $context['postType'] ) {
+ unset( $context['postId'] );
+ unset( $context['postType'] );
+ }
+
+ return $context;
+}
diff --git a/wp-includes/class-wp-block-template.php b/wp-includes/class-wp-block-template.php
new file mode 100644
index 0000000000..704a2afc45
--- /dev/null
+++ b/wp-includes/class-wp-block-template.php
@@ -0,0 +1,103 @@
+ true ) ) as $type ) {
+ foreach ( $block_templates as $block_template ) {
+ $post_templates[ $type ][ $block_template->slug ] = $block_template->title;
+ }
+ }
+ }
+
$this->cache_add( 'post_templates', $post_templates );
}
diff --git a/wp-includes/default-filters.php b/wp-includes/default-filters.php
index d828a9fd17..535ed7261c 100644
--- a/wp-includes/default-filters.php
+++ b/wp-includes/default-filters.php
@@ -555,6 +555,9 @@ add_filter( 'style_loader_src', 'wp_style_loader_src', 10, 2 );
add_action( 'wp_head', 'wp_maybe_inline_styles', 1 ); // Run for styles enqueued in .
add_action( 'wp_footer', 'wp_maybe_inline_styles', 1 ); // Run for late-loaded styles in the footer.
+add_action( 'admin_footer-post.php', 'wp_add_iframed_editor_assets_html' );
+add_action( 'admin_footer-post-new.php', 'wp_add_iframed_editor_assets_html' );
+
// Taxonomy.
add_action( 'init', 'create_initial_taxonomies', 0 ); // Highest priority.
add_action( 'change_locale', 'create_initial_taxonomies' );
@@ -633,4 +636,9 @@ add_filter( 'user_has_cap', 'wp_maybe_grant_install_languages_cap', 1 );
add_filter( 'user_has_cap', 'wp_maybe_grant_resume_extensions_caps', 1 );
add_filter( 'user_has_cap', 'wp_maybe_grant_site_health_caps', 1, 4 );
+// Block Templates CPT 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( 'wp_footer', 'the_block_template_skip_link' );
+
unset( $filter, $action );
diff --git a/wp-includes/post.php b/wp-includes/post.php
index 81d152f638..e184298410 100644
--- a/wp-includes/post.php
+++ b/wp-includes/post.php
@@ -310,6 +310,66 @@ function create_initial_post_types() {
)
);
+ register_post_type(
+ 'wp_template',
+ array(
+ 'labels' => array(
+ 'name' => __( 'Templates' ),
+ 'singular_name' => __( 'Template' ),
+ 'menu_name' => _x( 'Templates', 'Admin Menu text' ),
+ 'add_new' => _x( 'Add New', 'Template' ),
+ 'add_new_item' => __( 'Add New Template' ),
+ 'new_item' => __( 'New Template' ),
+ 'edit_item' => __( 'Edit Template' ),
+ 'view_item' => __( 'View Template' ),
+ 'all_items' => __( 'All Templates' ),
+ 'search_items' => __( 'Search Templates' ),
+ 'parent_item_colon' => __( 'Parent Template:' ),
+ 'not_found' => __( 'No templates found.' ),
+ 'not_found_in_trash' => __( 'No templates found in Trash.' ),
+ 'archives' => __( 'Template archives' ),
+ 'insert_into_item' => __( 'Insert into template' ),
+ 'uploaded_to_this_item' => __( 'Uploaded to this template' ),
+ 'filter_items_list' => __( 'Filter templates list' ),
+ 'items_list_navigation' => __( 'Templates list navigation' ),
+ 'items_list' => __( 'Templates list' ),
+ ),
+ 'description' => __( 'Templates to include in your theme.' ),
+ 'public' => false,
+ 'has_archive' => false,
+ 'show_ui' => false,
+ 'show_in_menu' => false,
+ 'show_in_admin_bar' => false,
+ 'show_in_rest' => true,
+ 'rewrite' => false,
+ 'rest_base' => 'templates',
+ 'rest_controller_class' => 'WP_REST_Templates_Controller',
+ 'capability_type' => array( 'template', 'templates' ),
+ 'capabilities' => array(
+ 'create_posts' => 'edit_theme_options',
+ 'delete_posts' => 'edit_theme_options',
+ 'delete_others_posts' => 'edit_theme_options',
+ 'delete_private_posts' => 'edit_theme_options',
+ 'delete_published_posts' => 'edit_theme_options',
+ 'edit_posts' => 'edit_theme_options',
+ 'edit_others_posts' => 'edit_theme_options',
+ 'edit_private_posts' => 'edit_theme_options',
+ 'edit_published_posts' => 'edit_theme_options',
+ 'publish_posts' => 'edit_theme_options',
+ 'read' => 'edit_theme_options',
+ 'read_private_posts' => 'edit_theme_options',
+ ),
+ 'map_meta_cap' => true,
+ 'supports' => array(
+ 'title',
+ 'slug',
+ 'excerpt',
+ 'editor',
+ 'revisions',
+ ),
+ )
+ );
+
register_post_status(
'publish',
array(
diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php
new file mode 100644
index 0000000000..ff71dc024a
--- /dev/null
+++ b/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php
@@ -0,0 +1,603 @@
+post_type = $post_type;
+ $this->namespace = 'wp/v2';
+ $obj = get_post_type_object( $post_type );
+ $this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
+ }
+
+ /**
+ * Registers the controllers routes.
+ *
+ * @since 5.8.0
+ *
+ * @return void
+ */
+ 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(),
+ ),
+ array(
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => array( $this, 'create_item' ),
+ 'permission_callback' => array( $this, 'create_item_permissions_check' ),
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+
+ // Lists/updates a single template based on the given id.
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/(?P[\/\w-]+)',
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_item' ),
+ 'permission_callback' => array( $this, 'get_item_permissions_check' ),
+ 'args' => array(
+ 'id' => array(
+ 'description' => __( 'The id of a template' ),
+ 'type' => 'string',
+ ),
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $this, 'update_item' ),
+ 'permission_callback' => array( $this, 'update_item_permissions_check' ),
+ 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::DELETABLE,
+ 'callback' => array( $this, 'delete_item' ),
+ 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
+ 'args' => array(
+ 'force' => array(
+ 'type' => 'boolean',
+ 'default' => false,
+ 'description' => __( 'Whether to bypass Trash and force deletion.' ),
+ ),
+ ),
+ ),
+ 'schema' => array( $this, 'get_public_item_schema' ),
+ )
+ );
+ }
+
+ /**
+ * Checks if the user has permissions to make the request.
+ *
+ * @since 5.8.0
+ *
+ * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
+ */
+ protected function permissions_check() {
+ // Verify if the current user has edit_theme_options capability.
+ // This capability is required to edit/view/delete templates.
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
+ return new WP_Error(
+ 'rest_cannot_manage_templates',
+ __( 'Sorry, you are not allowed to access the templates on this site.' ),
+ array(
+ 'status' => rest_authorization_required_code(),
+ )
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if a given request has access to read templates.
+ *
+ * @since 5.8.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
+ */
+ public function get_items_permissions_check( $request ) {
+ return $this->permissions_check( $request );
+ }
+
+ /**
+ * Returns a list of templates.
+ *
+ * @since 5.8.0
+ *
+ * @param WP_REST_Request $request The request instance.
+ *
+ * @return WP_REST_Response
+ */
+ public function get_items( $request ) {
+ $query = array();
+ if ( isset( $request['wp_id'] ) ) {
+ $query['wp_id'] = $request['wp_id'];
+ }
+ if ( isset( $request['area'] ) ) {
+ $query['area'] = $request['area'];
+ }
+ $templates = array();
+ foreach ( get_block_templates( $query, $this->post_type ) as $template ) {
+ $data = $this->prepare_item_for_response( $template, $request );
+ $templates[] = $this->prepare_response_for_collection( $data );
+ }
+
+ return rest_ensure_response( $templates );
+ }
+
+ /**
+ * Checks if a given request has access to read a single template.
+ *
+ * @since 5.8.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
+ */
+ public function get_item_permissions_check( $request ) {
+ return $this->permissions_check( $request );
+ }
+
+ /**
+ * Returns the given template
+ *
+ * @since 5.8.0
+ *
+ * @param WP_REST_Request $request The request instance.
+ *
+ * @return WP_REST_Response|WP_Error
+ */
+ public function get_item( $request ) {
+ $template = get_block_template( $request['id'], $this->post_type );
+
+ if ( ! $template ) {
+ return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
+ }
+
+ return $this->prepare_item_for_response( $template, $request );
+ }
+
+ /**
+ * Checks if a given request has access to write a single template.
+ *
+ * @since 5.8.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
+ */
+ public function update_item_permissions_check( $request ) {
+ return $this->permissions_check( $request );
+ }
+
+ /**
+ * Updates a single template.
+ *
+ * @since 5.8.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function update_item( $request ) {
+ $template = get_block_template( $request['id'], $this->post_type );
+ if ( ! $template ) {
+ return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
+ }
+
+ $changes = $this->prepare_item_for_database( $request );
+
+ if ( 'custom' === $template->source ) {
+ $result = wp_update_post( wp_slash( (array) $changes ), true );
+ } else {
+ $result = wp_insert_post( wp_slash( (array) $changes ), true );
+ }
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
+
+ $template = get_block_template( $request['id'], $this->post_type );
+ $fields_update = $this->update_additional_fields_for_object( $template, $request );
+ if ( is_wp_error( $fields_update ) ) {
+ return $fields_update;
+ }
+
+ return $this->prepare_item_for_response(
+ get_block_template( $request['id'], $this->post_type ),
+ $request
+ );
+ }
+
+ /**
+ * Checks if a given request has access to create a template.
+ *
+ * @since 5.8.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
+ */
+ public function create_item_permissions_check( $request ) {
+ return $this->permissions_check( $request );
+ }
+
+ /**
+ * Creates a single template.
+ *
+ * @since 5.8.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function create_item( $request ) {
+ $changes = $this->prepare_item_for_database( $request );
+ $changes->post_name = $request['slug'];
+ $result = wp_insert_post( wp_slash( (array) $changes ), true );
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
+ $posts = get_block_templates( array( 'wp_id' => $result ), $this->post_type );
+ if ( ! count( $posts ) ) {
+ return new WP_Error( 'rest_template_insert_error', __( 'No templates exist with that id.' ) );
+ }
+ $id = $posts[0]->id;
+ $template = get_block_template( $id, $this->post_type );
+ $fields_update = $this->update_additional_fields_for_object( $template, $request );
+ if ( is_wp_error( $fields_update ) ) {
+ return $fields_update;
+ }
+
+ return $this->prepare_item_for_response(
+ get_block_template( $id, $this->post_type ),
+ $request
+ );
+ }
+
+ /**
+ * Checks if a given request has access to delete a single template.
+ *
+ * @since 5.8.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return true|WP_Error True if the request has delete access for the item, WP_Error object otherwise.
+ */
+ public function delete_item_permissions_check( $request ) {
+ return $this->permissions_check( $request );
+ }
+
+ /**
+ * Deletes a single template.
+ *
+ * @since 5.8.0
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function delete_item( $request ) {
+ $template = get_block_template( $request['id'], $this->post_type );
+ if ( ! $template ) {
+ return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
+ }
+ if ( 'custom' !== $template->source ) {
+ return new WP_Error( 'rest_invalid_template', __( 'Templates based on theme files can\'t be removed.' ), array( 'status' => 400 ) );
+ }
+
+ $id = $template->wp_id;
+ $force = (bool) $request['force'];
+
+ // If we're forcing, then delete permanently.
+ if ( $force ) {
+ $previous = $this->prepare_item_for_response( $template, $request );
+ wp_delete_post( $id, true );
+ $response = new WP_REST_Response();
+ $response->set_data(
+ array(
+ 'deleted' => true,
+ 'previous' => $previous->get_data(),
+ )
+ );
+
+ return $response;
+ }
+
+ // Otherwise, only trash if we haven't already.
+ if ( 'trash' === $template->status ) {
+ return new WP_Error(
+ 'rest_template_already_trashed',
+ __( 'The template has already been deleted.' ),
+ array( 'status' => 410 )
+ );
+ }
+
+ wp_trash_post( $id );
+ $template->status = 'trash';
+ return $this->prepare_item_for_response( $template, $request );
+ }
+
+ /**
+ * Prepares a single template for create or update.
+ *
+ * @since 5.8.0
+ *
+ * @param WP_REST_Request $request Request object.
+ * @return stdClass Changes to pass to wp_update_post.
+ */
+ protected function prepare_item_for_database( $request ) {
+ $template = $request['id'] ? get_block_template( $request['id'], $this->post_type ) : null;
+ $changes = new stdClass();
+ if ( null === $template ) {
+ $changes->post_type = $this->post_type;
+ $changes->post_status = 'publish';
+ $changes->tax_input = array(
+ 'wp_theme' => isset( $request['theme'] ) ? $request['theme'] : wp_get_theme()->get_stylesheet(),
+ );
+ } elseif ( 'custom' !== $template->source ) {
+ $changes->post_name = $template->slug;
+ $changes->post_type = $this->post_type;
+ $changes->post_status = 'publish';
+ $changes->tax_input = array(
+ 'wp_theme' => $template->theme,
+ );
+ } else {
+ $changes->post_name = $template->slug;
+ $changes->ID = $template->wp_id;
+ $changes->post_status = 'publish';
+ }
+ if ( isset( $request['content'] ) ) {
+ $changes->post_content = $request['content'];
+ } elseif ( null !== $template && 'custom' !== $template->source ) {
+ $changes->post_content = $template->content;
+ }
+ if ( isset( $request['title'] ) ) {
+ $changes->post_title = $request['title'];
+ } elseif ( null !== $template && 'custom' !== $template->source ) {
+ $changes->post_title = $template->title;
+ }
+ if ( isset( $request['description'] ) ) {
+ $changes->post_excerpt = $request['description'];
+ } elseif ( null !== $template && 'custom' !== $template->source ) {
+ $changes->post_excerpt = $template->description;
+ }
+
+ return $changes;
+ }
+
+ /**
+ * Prepare a single template output for response
+ *
+ * @since 5.8.0
+ *
+ * @param WP_Block_Template $template Template instance.
+ * @param WP_REST_Request $request Request object.
+ *
+ * @return WP_REST_Response $data
+ */
+ public function prepare_item_for_response( $template, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ $result = array(
+ 'id' => $template->id,
+ 'theme' => $template->theme,
+ 'content' => array( 'raw' => $template->content ),
+ 'slug' => $template->slug,
+ 'source' => $template->source,
+ 'type' => $template->type,
+ 'description' => $template->description,
+ 'title' => array(
+ 'raw' => $template->title,
+ 'rendered' => $template->title,
+ ),
+ 'status' => $template->status,
+ 'wp_id' => $template->wp_id,
+ 'has_theme_file' => $template->has_theme_file,
+ );
+
+ if ( 'wp_template_part' === $template->type ) {
+ $result['area'] = $template->area;
+ }
+
+ $result = $this->add_additional_fields_to_object( $result, $request );
+
+ $response = rest_ensure_response( $result );
+ $links = $this->prepare_links( $template->id );
+ $response->add_links( $links );
+ if ( ! empty( $links['self']['href'] ) ) {
+ $actions = $this->get_available_actions();
+ $self = $links['self']['href'];
+ foreach ( $actions as $rel ) {
+ $response->add_link( $rel, $self );
+ }
+ }
+
+ return $response;
+ }
+
+
+ /**
+ * Prepares links for the request.
+ *
+ * @since 5.8.0
+ *
+ * @param integer $id ID.
+ * @return array Links for the given post.
+ */
+ protected function prepare_links( $id ) {
+ $base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
+
+ $links = array(
+ 'self' => array(
+ 'href' => rest_url( trailingslashit( $base ) . $id ),
+ ),
+ 'collection' => array(
+ 'href' => rest_url( $base ),
+ ),
+ 'about' => array(
+ 'href' => rest_url( 'wp/v2/types/' . $this->post_type ),
+ ),
+ );
+
+ return $links;
+ }
+
+ /**
+ * Get the link relations available for the post and current user.
+ *
+ * @since 5.8.0
+ *
+ * @return array List of link relations.
+ */
+ protected function get_available_actions() {
+ $rels = array();
+
+ $post_type = get_post_type_object( $this->post_type );
+
+ if ( current_user_can( $post_type->cap->publish_posts ) ) {
+ $rels[] = 'https://api.w.org/action-publish';
+ }
+
+ if ( current_user_can( 'unfiltered_html' ) ) {
+ $rels[] = 'https://api.w.org/action-unfiltered-html';
+ }
+
+ return $rels;
+ }
+
+ /**
+ * Retrieves the query params for the posts collection.
+ *
+ * @since 5.8.0
+ *
+ * @return array Collection parameters.
+ */
+ public function get_collection_params() {
+ return array(
+ 'context' => $this->get_context_param(),
+ 'wp_id' => array(
+ 'description' => __( 'Limit to the specified post id.' ),
+ 'type' => 'integer',
+ ),
+ );
+ }
+
+ /**
+ * Retrieves the block type' schema, conforming to JSON Schema.
+ *
+ * @since 5.8.0
+ *
+ * @return array Item schema data.
+ */
+ public function get_item_schema() {
+ if ( $this->schema ) {
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => $this->post_type,
+ 'type' => 'object',
+ 'properties' => array(
+ 'id' => array(
+ 'description' => __( 'ID of template.' ),
+ 'type' => 'string',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'slug' => array(
+ 'description' => __( 'Unique slug identifying the template.' ),
+ 'type' => 'string',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ 'required' => true,
+ 'minLength' => 1,
+ 'pattern' => '[a-zA-Z_\-]+',
+ ),
+ 'theme' => array(
+ 'description' => __( 'Theme identifier for the template.' ),
+ 'type' => 'string',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ ),
+ 'source' => array(
+ 'description' => __( 'Source of template' ),
+ 'type' => 'string',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'content' => array(
+ 'description' => __( 'Content of template.' ),
+ 'type' => array( 'object', 'string' ),
+ 'default' => '',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ ),
+ 'title' => array(
+ 'description' => __( 'Title of template.' ),
+ 'type' => array( 'object', 'string' ),
+ 'default' => '',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ ),
+ 'description' => array(
+ 'description' => __( 'Description of template.' ),
+ 'type' => 'string',
+ 'default' => '',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ ),
+ 'status' => array(
+ 'description' => __( 'Status of template.' ),
+ 'type' => 'string',
+ 'default' => 'publish',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ ),
+ 'wp_id' => array(
+ 'description' => __( 'Post ID.' ),
+ 'type' => 'integer',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'has_theme_file' => array(
+ 'description' => __( 'Theme file exists.' ),
+ 'type' => 'bool',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ ),
+ );
+
+ $this->schema = $schema;
+
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+}
diff --git a/wp-includes/script-loader.php b/wp-includes/script-loader.php
index 8a77a441b3..62936447b9 100644
--- a/wp-includes/script-loader.php
+++ b/wp-includes/script-loader.php
@@ -2662,3 +2662,65 @@ function wp_maybe_inline_styles() {
}
}
}
+
+/**
+ * Inject the block editor assets that need to be loaded into the editor's iframe as an inline script.
+ *
+ * @since 5.8.0
+ */
+function wp_add_iframed_editor_assets_html() {
+ $script_handles = array();
+ $style_handles = array(
+ 'wp-block-editor',
+ 'wp-block-library',
+ 'wp-block-library-theme',
+ 'wp-edit-blocks',
+ );
+
+ $block_registry = WP_Block_Type_Registry::get_instance();
+
+ foreach ( $block_registry->get_all_registered() as $block_type ) {
+ if ( ! empty( $block_type->style ) ) {
+ $style_handles[] = $block_type->style;
+ }
+
+ if ( ! empty( $block_type->editor_style ) ) {
+ $style_handles[] = $block_type->editor_style;
+ }
+
+ if ( ! empty( $block_type->script ) ) {
+ $script_handles[] = $block_type->script;
+ }
+ }
+
+ $style_handles = array_unique( $style_handles );
+ $done = wp_styles()->done;
+
+ ob_start();
+
+ wp_styles()->done = array();
+ wp_styles()->do_items( $style_handles );
+ wp_styles()->done = $done;
+
+ $styles = ob_get_clean();
+
+ $script_handles = array_unique( $script_handles );
+ $done = wp_scripts()->done;
+
+ ob_start();
+
+ wp_scripts()->done = array();
+ wp_scripts()->do_items( $script_handles );
+ wp_scripts()->done = $done;
+
+ $scripts = ob_get_clean();
+
+ $editor_assets = wp_json_encode(
+ array(
+ 'styles' => $styles,
+ 'scripts' => $scripts,
+ )
+ );
+
+ echo "";
+}
diff --git a/wp-includes/taxonomy.php b/wp-includes/taxonomy.php
index 7d7785407c..0e7f1312dd 100644
--- a/wp-includes/taxonomy.php
+++ b/wp-includes/taxonomy.php
@@ -172,6 +172,25 @@ function create_initial_taxonomies() {
'show_in_nav_menus' => current_theme_supports( 'post-formats' ),
)
);
+
+ register_taxonomy(
+ 'wp_theme',
+ array( 'wp_template' ),
+ array(
+ 'public' => false,
+ 'hierarchical' => false,
+ 'labels' => array(
+ 'name' => __( 'Themes' ),
+ 'singular_name' => __( 'Theme' ),
+ ),
+ 'query_var' => false,
+ 'rewrite' => false,
+ 'show_ui' => false,
+ '_builtin' => true,
+ 'show_in_nav_menus' => false,
+ 'show_in_rest' => false,
+ )
+ );
}
/**
diff --git a/wp-includes/template-canvas.php b/wp-includes/template-canvas.php
new file mode 100644
index 0000000000..2ce5b12ca6
--- /dev/null
+++ b/wp-includes/template-canvas.php
@@ -0,0 +1,28 @@
+ so that blocks can add scripts and styles in wp_head().
+ */
+$template_html = get_the_block_template_html();
+?>
+
+>
+
+
+
+
+
+>
+
+
+
+
+
+
+
diff --git a/wp-includes/template.php b/wp-includes/template.php
index b5b5619af5..891e77748d 100644
--- a/wp-includes/template.php
+++ b/wp-includes/template.php
@@ -63,6 +63,8 @@ function get_query_template( $type, $templates = array() ) {
$template = locate_template( $templates );
+ $template = locate_block_template( $template, $type, $templates );
+
/**
* Filters the path of the queried template by type.
*
diff --git a/wp-includes/theme-templates.php b/wp-includes/theme-templates.php
new file mode 100644
index 0000000000..5cce35cd99
--- /dev/null
+++ b/wp-includes/theme-templates.php
@@ -0,0 +1,163 @@
+get_stylesheet();
+ $terms = get_the_terms( $post_ID, 'wp_theme' );
+ if ( $terms && ! is_wp_error( $terms ) ) {
+ $theme = $terms[0]->name;
+ }
+
+ $check_query_args = array(
+ 'post_name__in' => array( $override_slug ),
+ 'post_type' => $post_type,
+ 'posts_per_page' => 1,
+ 'no_found_rows' => true,
+ 'post__not_in' => array( $post_ID ),
+ 'tax_query' => array(
+ array(
+ 'taxonomy' => 'wp_theme',
+ 'field' => 'name',
+ 'terms' => $theme,
+ ),
+ ),
+ );
+ $check_query = new WP_Query( $check_query_args );
+ $posts = $check_query->get_posts();
+
+ if ( count( $posts ) > 0 ) {
+ $suffix = 2;
+ do {
+ $query_args = $check_query_args;
+ $alt_post_name = _truncate_post_slug( $override_slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix";
+ $query_args['post_name__in'] = array( $alt_post_name );
+ $query = new WP_Query( $query_args );
+ $suffix++;
+ } while ( count( $query->get_posts() ) > 0 );
+ $override_slug = $alt_post_name;
+ }
+
+ return $override_slug;
+}
+
+/**
+ * Print the skip-link script & styles.
+ *
+ * @access private
+ * @since 5.8.0
+ *
+ * @return void
+ */
+function the_block_template_skip_link() {
+
+ // Early exit if not an FSE theme.
+ if ( ! current_theme_supports( 'block-templates' ) ) {
+ return;
+ }
+ ?>
+
+
+
+
+
+