From 4ae0e4220f0c01005afa0dcbb74979c0d5cb9abc Mon Sep 17 00:00:00 2001 From: youknowriad Date: Tue, 25 May 2021 14:20:57 +0000 Subject: [PATCH] Block Editor: Introduce block templates for classic themes. With this patch, users will be able to create custom block based templates and assign them to specific pages/posts. Themes can also opt-out of this feature Props bernhard-reiter, carlomanf. Fixes #53176. Built from https://develop.svn.wordpress.org/trunk@51003 git-svn-id: http://core.svn.wordpress.org/trunk@50612 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-admin/edit-form-blocks.php | 1 + wp-includes/block-template-utils.php | 144 +++++ wp-includes/block-template.php | 228 +++++++ wp-includes/class-wp-block-template.php | 103 +++ wp-includes/class-wp-theme.php | 10 + wp-includes/default-filters.php | 8 + wp-includes/post.php | 60 ++ .../class-wp-rest-templates-controller.php | 603 ++++++++++++++++++ wp-includes/script-loader.php | 62 ++ wp-includes/taxonomy.php | 19 + wp-includes/template-canvas.php | 28 + wp-includes/template.php | 2 + wp-includes/theme-templates.php | 163 +++++ wp-includes/version.php | 2 +- wp-settings.php | 5 + 15 files changed, 1437 insertions(+), 1 deletion(-) create mode 100644 wp-includes/block-template-utils.php create mode 100644 wp-includes/block-template.php create mode 100644 wp-includes/class-wp-block-template.php create mode 100644 wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php create mode 100644 wp-includes/template-canvas.php create mode 100644 wp-includes/theme-templates.php 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; + } + ?> + + + + + +