From ceb98ab53657a735da83e35cd9778a37e258e8db Mon Sep 17 00:00:00 2001 From: ramon Date: Fri, 9 Jun 2023 13:14:35 +1000 Subject: [PATCH 1/9] All these file changes are so that we can add `modified` to the template and template part objects in the rest response for WP_REST_Templates_Controller For theme templates, it will add the modified value when we have a post id (which is once the template is first save after user change) --- .../wordpress-6.3/block-template-utils.php | 383 ++++++++++++++++++ .../class-gutenberg-block-template.php | 158 ++++++++ ...utenberg-rest-templates-controller-6-3.php | 226 ++++++++++- lib/load.php | 2 + phpunit/block-template-utils-test.php | 84 +++- ...tenberg-rest-templates-controller-test.php | 131 +++++- 6 files changed, 970 insertions(+), 14 deletions(-) create mode 100644 lib/compat/wordpress-6.3/block-template-utils.php create mode 100644 lib/compat/wordpress-6.3/class-gutenberg-block-template.php diff --git a/lib/compat/wordpress-6.3/block-template-utils.php b/lib/compat/wordpress-6.3/block-template-utils.php new file mode 100644 index 00000000000000..f8941def143938 --- /dev/null +++ b/lib/compat/wordpress-6.3/block-template-utils.php @@ -0,0 +1,383 @@ +name; + $template_file = _get_block_template_file( $post->post_type, $post->post_name ); + $has_theme_file = get_stylesheet() === $theme && null !== $template_file; + + $origin = get_post_meta( $post->ID, 'origin', true ); + $is_wp_suggestion = get_post_meta( $post->ID, 'is_wp_suggestion', true ); + + $template = new Gutenberg_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->origin = ! empty( $origin ) ? $origin : null; + $template->type = $post->post_type; + $template->description = $post->post_excerpt; + $template->title = $post->post_title; + $template->status = $post->post_status; + $template->has_theme_file = $has_theme_file; + $template->is_custom = empty( $is_wp_suggestion ); + $template->author = $post->post_author; + $template->modified = ! empty( $post->post_modified ) ? $post->post_modified : null; + + if ( 'wp_template' === $post->post_type && $has_theme_file && isset( $template_file['postTypes'] ) ) { + $template->post_types = $template_file['postTypes']; + } + + if ( 'wp_template' === $post->post_type && isset( $default_template_types[ $template->slug ] ) ) { + $template->is_custom = false; + } + + if ( 'wp_template_part' === $post->post_type ) { + $type_terms = get_the_terms( $post, 'wp_template_part_area' ); + if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) { + $template->area = $type_terms[0]->name; + } + } + + // Check for a block template without a description and title or with a title equal to the slug. + if ( 'wp_template' === $post->post_type && empty( $template->description ) && ( empty( $template->title ) || $template->title === $template->slug ) ) { + $matches = array(); + + // Check for a block template for a single author, page, post, tag, category, custom post type, or custom taxonomy. + if ( preg_match( '/(author|page|single|tag|category|taxonomy)-(.+)/', $template->slug, $matches ) ) { + $type = $matches[1]; + $slug_remaining = $matches[2]; + + switch ( $type ) { + case 'author': + $nice_name = $slug_remaining; + $users = get_users( + array( + 'capability' => 'edit_posts', + 'search' => $nice_name, + 'search_columns' => array( 'user_nicename' ), + 'fields' => 'display_name', + ) + ); + + if ( empty( $users ) ) { + $template->title = sprintf( + /* translators: Custom template title in the Site Editor, referencing a deleted author. %s: Author nicename. */ + __( 'Deleted author: %s' ), + $nice_name + ); + } else { + $author_name = $users[0]; + + $template->title = sprintf( + /* translators: Custom template title in the Site Editor. %s: Author name. */ + __( 'Author: %s' ), + $author_name + ); + + $template->description = sprintf( + /* translators: Custom template description in the Site Editor. %s: Author name. */ + __( 'Template for %s' ), + $author_name + ); + + $users_with_same_name = get_users( + array( + 'capability' => 'edit_posts', + 'search' => $author_name, + 'search_columns' => array( 'display_name' ), + 'fields' => 'display_name', + ) + ); + + if ( count( $users_with_same_name ) > 1 ) { + $template->title = sprintf( + /* translators: Custom template title in the Site Editor. 1: Template title of an author template, 2: Author nicename. */ + __( '%1$s (%2$s)' ), + $template->title, + $nice_name + ); + } + } + break; + case 'page': + _wp_build_title_and_description_for_single_post_type_block_template( 'page', $slug_remaining, $template ); + break; + case 'single': + $post_types = get_post_types(); + + foreach ( $post_types as $post_type ) { + $post_type_length = strlen( $post_type ) + 1; + + // If $slug_remaining starts with $post_type followed by a hyphen. + if ( 0 === strncmp( $slug_remaining, $post_type . '-', $post_type_length ) ) { + $slug = substr( $slug_remaining, $post_type_length, strlen( $slug_remaining ) ); + $found = _wp_build_title_and_description_for_single_post_type_block_template( $post_type, $slug, $template ); + + if ( $found ) { + break; + } + } + } + break; + case 'tag': + _wp_build_title_and_description_for_taxonomy_block_template( 'post_tag', $slug_remaining, $template ); + break; + case 'category': + _wp_build_title_and_description_for_taxonomy_block_template( 'category', $slug_remaining, $template ); + break; + case 'taxonomy': + $taxonomies = get_taxonomies(); + + foreach ( $taxonomies as $taxonomy ) { + $taxonomy_length = strlen( $taxonomy ) + 1; + + // If $slug_remaining starts with $taxonomy followed by a hyphen. + if ( 0 === strncmp( $slug_remaining, $taxonomy . '-', $taxonomy_length ) ) { + $slug = substr( $slug_remaining, $taxonomy_length, strlen( $slug_remaining ) ); + $found = _wp_build_title_and_description_for_taxonomy_block_template( $taxonomy, $slug, $template ); + + if ( $found ) { + break; + } + } + } + break; + } + } + } + + 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 string[] $slug__in List of slugs to include. + * @type int $wp_id Post ID of customized template. + * @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only). + * @type string $post_type Post type to get the templates for. + * } + * @param string $template_type 'wp_template' or 'wp_template_part'. + * @return WP_Block_Template[] Array of block templates. + */ +function gutenberg_get_block_templates( $query = array(), $template_type = 'wp_template' ) { + /** + * Filters the block templates array before the query takes place. + * + * Return a non-null value to bypass the WordPress queries. + * + * @param WP_Block_Template[]|null $block_templates Return an array of block templates to short-circuit the default query, + * or null to allow WP to run its normal queries. + * @param array $query { + * Arguments to retrieve templates. All arguments are optional. + * + * @type string[] $slug__in List of slugs to include. + * @type int $wp_id Post ID of customized template. + * @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only). + * @type string $post_type Post type to get the templates for. + * } + * @param string $template_type 'wp_template' or 'wp_template_part'. + * @since 5.9.0 + */ + $templates = apply_filters( 'pre_get_block_templates', null, $query, $template_type ); + if ( ! is_null( $templates ) ) { + return $templates; + } + + $post_type = isset( $query['post_type'] ) ? $query['post_type'] : ''; + $wp_query_args = array( + 'post_status' => array( 'auto-draft', 'draft', 'publish' ), + 'post_type' => $template_type, + 'posts_per_page' => -1, + 'no_found_rows' => true, + 'lazy_load_term_meta' => false, + 'tax_query' => array( + array( + 'taxonomy' => 'wp_theme', + 'field' => 'name', + 'terms' => get_stylesheet(), + ), + ), + ); + + if ( 'wp_template_part' === $template_type && isset( $query['area'] ) ) { + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'wp_template_part_area', + 'field' => 'name', + 'terms' => $query['area'], + ); + $wp_query_args['tax_query']['relation'] = 'AND'; + } + + if ( ! empty( $query['slug__in'] ) ) { + $wp_query_args['post_name__in'] = $query['slug__in']; + $wp_query_args['posts_per_page'] = count( array_unique( $query['slug__in'] ) ); + } + + // This is only needed for the regular templates/template parts post type 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->posts as $post ) { + $template = _gutenberg_build_block_template_result_from_post( $post ); + + if ( is_wp_error( $template ) ) { + continue; + } + + if ( $post_type && ! $template->is_custom ) { + continue; + } + + if ( + $post_type && + isset( $template->post_types ) && + ! in_array( $post_type, $template->post_types, true ) + ) { + continue; + } + + $query_result[] = $template; + } + + if ( ! isset( $query['wp_id'] ) ) { + /* + * If the query has found some use templates, those have priority + * over the theme-provided ones, so we skip querying and building them. + */ + $query['slug__not_in'] = wp_list_pluck( $query_result, 'slug' ); + $template_files = _get_block_templates_files( $template_type, $query ); + foreach ( $template_files as $template_file ) { + $query_result[] = _build_block_template_result_from_file( $template_file, $template_type ); + } + } + + /** + * Filters the array of queried block templates array after they've been fetched. + * + * @param WP_Block_Template[] $query_result Array of found block templates. + * @param array $query { + * Arguments to retrieve templates. All arguments are optional. + * + * @type string[] $slug__in List of slugs to include. + * @type int $wp_id Post ID of customized template. + * @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only). + * @type string $post_type Post type to get the templates for. + * } + * @param string $template_type wp_template or wp_template_part. + * @since 5.9.0 + */ + return apply_filters( 'get_block_templates', $query_result, $query, $template_type ); +} + +/** + * 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 Optional. Template type: 'wp_template' or 'wp_template_part'. + * Default 'wp_template'. + * @return WP_Block_Template|null Template. + */ +function gutenberg_get_block_template( $id, $template_type = 'wp_template' ) { + /** + * Filters the block template object before the query takes place. + * + * Return a non-null value to bypass the WordPress queries. + * + * @since 5.9.0 + * + * @param WP_Block_Template|null $block_template Return block template object to short-circuit the default query, + * or null to allow WP to run its normal queries. + * @param string $id Template unique identifier (example: 'theme_slug//template_slug'). + * @param string $template_type Template type: 'wp_template' or 'wp_template_part'. + */ + $block_template = apply_filters( 'pre_get_block_template', null, $id, $template_type ); + if ( ! is_null( $block_template ) ) { + return $block_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->posts; + + if ( count( $posts ) > 0 ) { + $template = _gutenberg_build_block_template_result_from_post( $posts[0] ); + + if ( ! is_wp_error( $template ) ) { + return $template; + } + } + + $block_template = get_block_file_template( $id, $template_type ); + + /** + * Filters the queried block template object after it's been fetched. + * + * @since 5.9.0 + * + * @param WP_Block_Template|null $block_template The found block template, or null if there isn't one. + * @param string $id Template unique identifier (example: 'theme_slug//template_slug'). + * @param array $template_type Template type: 'wp_template' or 'wp_template_part'. + */ + return apply_filters( 'get_block_template', $block_template, $id, $template_type ); +} diff --git a/lib/compat/wordpress-6.3/class-gutenberg-block-template.php b/lib/compat/wordpress-6.3/class-gutenberg-block-template.php new file mode 100644 index 00000000000000..2de5169a311754 --- /dev/null +++ b/lib/compat/wordpress-6.3/class-gutenberg-block-template.php @@ -0,0 +1,158 @@ +data['modified'] = $template->modified; + } + } + if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { $links = $this->prepare_revision_links( $template ); $response->add_links( $links ); @@ -98,7 +113,7 @@ public function prepare_item_for_response( $item, $request ) { // phpcs:ignore V /** * Adds revisions to links. * - * @since 6.2.0 + * @since 6.3.0 * * @param WP_Block_Template $template Template instance. * @return array Links for the given post. @@ -126,4 +141,211 @@ protected function prepare_revision_links( $template ) { return $links; } + + + /** + * 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 ) { + if ( isset( $request['source'] ) && 'theme' === $request['source'] ) { + $template = get_block_file_template( $request['id'], $this->post_type ); + } else { + $template = gutenberg_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 ); + } + + /** + * 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']; + } + if ( isset( $request['post_type'] ) ) { + $query['post_type'] = $request['post_type']; + } + + $templates = array(); + foreach ( gutenberg_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 ); + } + + /** + * Retrieves the block type' schema, conforming to JSON Schema. + * + * @since 5.8.0 + * @since 5.9.0 Added `'area'`. + * @since 6.3.0 Added `'modified'`. + * + * @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-Z0-9_\%-]+', + ), + 'theme' => array( + 'description' => __( 'Theme identifier for the template.' ), + 'type' => 'string', + 'context' => array( 'embed', 'view', 'edit' ), + ), + 'type' => array( + 'description' => __( 'Type of template.' ), + 'type' => 'string', + 'context' => array( 'embed', 'view', 'edit' ), + ), + 'source' => array( + 'description' => __( 'Source of template' ), + 'type' => 'string', + 'context' => array( 'embed', 'view', 'edit' ), + 'readonly' => true, + ), + 'origin' => array( + 'description' => __( 'Source of a customized 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' ), + 'properties' => array( + 'raw' => array( + 'description' => __( 'Content for the template, as it exists in the database.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'block_version' => array( + 'description' => __( 'Version of the content block format used by the template.' ), + 'type' => 'integer', + 'context' => array( 'edit' ), + 'readonly' => true, + ), + ), + ), + 'title' => array( + 'description' => __( 'Title of template.' ), + 'type' => array( 'object', 'string' ), + 'default' => '', + 'context' => array( 'embed', 'view', 'edit' ), + 'properties' => array( + 'raw' => array( + 'description' => __( 'Title for the template, as it exists in the database.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'rendered' => array( + 'description' => __( 'HTML title for the template, transformed for display.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + ), + ), + 'description' => array( + 'description' => __( 'Description of template.' ), + 'type' => 'string', + 'default' => '', + 'context' => array( 'embed', 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Status of template.' ), + 'type' => 'string', + 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ), + '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, + ), + 'author' => array( + 'description' => __( 'The ID for the author of the template.' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'modified' => array( + 'description' => __( "The date the post was last modified, in the site's timezone." ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + if ( 'wp_template' === $this->post_type ) { + $schema['properties']['is_custom'] = array( + 'description' => __( 'Whether a template is a custom template.' ), + 'type' => 'bool', + 'context' => array( 'embed', 'view', 'edit' ), + 'readonly' => true, + ); + } + + if ( 'wp_template_part' === $this->post_type ) { + $schema['properties']['area'] = array( + 'description' => __( 'Where the template part is intended for use (header, footer, etc.)' ), + 'type' => 'string', + 'context' => array( 'embed', 'view', 'edit' ), + ); + } + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } } diff --git a/lib/load.php b/lib/load.php index b0ed333185ef6a..1118b69972e043 100644 --- a/lib/load.php +++ b/lib/load.php @@ -82,6 +82,8 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.3 compat. require __DIR__ . '/compat/wordpress-6.3/get-global-styles-and-settings.php'; +require __DIR__ . '/compat/wordpress-6.3/block-template-utils.php'; +require __DIR__ . '/compat/wordpress-6.3/class-gutenberg-block-template.php'; if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { require __DIR__ . '/compat/wordpress-6.2/html-api/class-wp-html-attribute-token.php'; diff --git a/phpunit/block-template-utils-test.php b/phpunit/block-template-utils-test.php index ac2e4145cd0ed2..a9cdc94fe6e953 100644 --- a/phpunit/block-template-utils-test.php +++ b/phpunit/block-template-utils-test.php @@ -6,12 +6,18 @@ */ class Tests_Block_Template_Utils extends WP_UnitTestCase { + const TEST_THEME = 'emptytheme'; + + private static $template_post; + + private static $template_part_post; + public function set_up() { parent::set_up(); switch_theme( 'emptytheme' ); } - public static function wpSetUpBeforeClass() { + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { register_post_type( 'custom_book', array( @@ -20,6 +26,46 @@ public static function wpSetUpBeforeClass() { ) ); register_taxonomy( 'book_type', 'custom_book' ); + + // Set up template post. + self::$template_post = $factory->post->create_and_get( + array( + 'post_type' => 'wp_template', + 'post_name' => 'my_template', + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + ), + ) + ); + + wp_set_post_terms( self::$template_post->ID, self::TEST_THEME, 'wp_theme' ); + + // Set up template part post. + self::$template_part_post = $factory->post->create_and_get( + array( + 'post_type' => 'wp_template_part', + 'post_name' => 'my_template_part', + 'post_title' => 'My Template Part', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template part', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + 'wp_template_part_area' => array( + WP_TEMPLATE_PART_AREA_HEADER, + ), + ), + ) + ); + + wp_set_post_terms( self::$template_part_post->ID, WP_TEMPLATE_PART_AREA_HEADER, 'wp_template_part_area' ); + wp_set_post_terms( self::$template_part_post->ID, self::TEST_THEME, 'wp_theme' ); } public static function wpTearDownAfterClass() { @@ -250,4 +296,40 @@ public function test_get_template_hierarchy() { $hierarchy ); } + + public function test_build_block_template_result_from_post() { + $template = _gutenberg_build_block_template_result_from_post( + self::$template_post, + 'wp_template' + ); + + $this->assertNotWPError( $template ); + $this->assertSame( get_stylesheet() . '//my_template', $template->id ); + $this->assertSame( get_stylesheet(), $template->theme ); + $this->assertSame( 'my_template', $template->slug ); + $this->assertSame( 'publish', $template->status ); + $this->assertSame( 'custom', $template->source ); + $this->assertSame( 'My Template', $template->title ); + $this->assertSame( 'Description of my template', $template->description ); + $this->assertSame( 'wp_template', $template->type ); + $this->assertSame( self::$template_post->post_modified, $template->modified ); + + // Test template parts. + $template_part = _gutenberg_build_block_template_result_from_post( + self::$template_part_post, + 'wp_template_part' + ); + + $this->assertNotWPError( $template_part ); + $this->assertSame( get_stylesheet() . '//my_template_part', $template_part->id ); + $this->assertSame( get_stylesheet(), $template_part->theme ); + $this->assertSame( 'my_template_part', $template_part->slug ); + $this->assertSame( 'publish', $template_part->status ); + $this->assertSame( 'custom', $template_part->source ); + $this->assertSame( 'My Template Part', $template_part->title ); + $this->assertSame( 'Description of my template part', $template_part->description ); + $this->assertSame( 'wp_template_part', $template_part->type ); + $this->assertSame( WP_TEMPLATE_PART_AREA_HEADER, $template_part->area ); + $this->assertSame( self::$template_part_post->post_modified, $template->modified ); + } } diff --git a/phpunit/class-gutenberg-rest-templates-controller-test.php b/phpunit/class-gutenberg-rest-templates-controller-test.php index e86f9db5d848e1..c992278fc50cdc 100644 --- a/phpunit/class-gutenberg-rest-templates-controller-test.php +++ b/phpunit/class-gutenberg-rest-templates-controller-test.php @@ -5,10 +5,22 @@ class Gutenberg_REST_Templates_Controller_Test extends WP_Test_REST_Controller_T * @var int */ protected static $admin_id; + private static $post; + + protected function find_and_normalize_template_by_id( $templates, $id ) { + foreach ( $templates as $template ) { + if ( $template['id'] === $id ) { + unset( $template['content'] ); + unset( $template['_links'] ); + return $template; + } + } + + return null; + } public function set_up() { parent::set_up(); - switch_theme( 'emptytheme' ); } /** @@ -17,11 +29,28 @@ public function set_up() { * @param WP_UnitTest_Factory $factory Helper that lets us create fake data. */ public static function wpSetupBeforeClass( $factory ) { + switch_theme( 'emptytheme' ); self::$admin_id = $factory->user->create( array( 'role' => 'administrator', ) ); + + // Set up template post. + $args = array( + 'post_type' => 'wp_template', + 'post_name' => 'my_template', + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template.', + 'tax_input' => array( + 'wp_theme' => array( + get_stylesheet(), + ), + ), + ); + self::$post = self::factory()->post->create_and_get( $args ); + wp_set_post_terms( self::$post->ID, get_stylesheet(), 'wp_theme' ); } public function test_register_routes() { @@ -63,19 +92,104 @@ public function test_get_template_fallback() { } /** - * @doesNotPerformAssertions + * @covers WP_REST_Templates_Controller::get_item_schema */ - public function test_context_param() {} + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/templates' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertCount( 15, $properties ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'slug', $properties ); + $this->assertArrayHasKey( 'theme', $properties ); + $this->assertArrayHasKey( 'type', $properties ); + $this->assertArrayHasKey( 'source', $properties ); + $this->assertArrayHasKey( 'origin', $properties ); + $this->assertArrayHasKey( 'content', $properties ); + $this->assertArrayHasKey( 'title', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'status', $properties ); + $this->assertArrayHasKey( 'wp_id', $properties ); + $this->assertArrayHasKey( 'has_theme_file', $properties ); + $this->assertArrayHasKey( 'is_custom', $properties ); + $this->assertArrayHasKey( 'author', $properties ); + $this->assertArrayHasKey( 'modified', $properties ); + } /** - * @doesNotPerformAssertions + * @covers WP_REST_Templates_Controller::get_item */ - public function test_get_items() {} + public function test_get_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/emptytheme//my_template' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + unset( $data['content'] ); + unset( $data['_links'] ); + + $this->assertSame( + array( + 'id' => 'emptytheme//my_template', + 'theme' => 'emptytheme', + 'slug' => 'my_template', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Description of my template.', + 'title' => array( + 'raw' => 'My Template', + 'rendered' => 'My Template', + ), + 'status' => 'publish', + 'wp_id' => self::$post->ID, + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => 0, + 'modified' => self::$post->post_modified, + ), + $data + ); + } + + /** + * @covers WP_REST_Templates_Controller::get_items + */ + public function test_get_items() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( + array( + 'id' => 'emptytheme//my_template', + 'theme' => 'emptytheme', + 'slug' => 'my_template', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Description of my template.', + 'title' => array( + 'raw' => 'My Template', + 'rendered' => 'My Template', + ), + 'status' => 'publish', + 'wp_id' => self::$post->ID, + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => 0, + 'modified' => self::$post->post_modified, + ), + $this->find_and_normalize_template_by_id( $data, 'emptytheme//my_template' ) + ); + } /** * @doesNotPerformAssertions */ - public function test_get_item() {} + public function test_context_param() {} /** * @doesNotPerformAssertions @@ -96,9 +210,4 @@ public function test_delete_item() {} * @doesNotPerformAssertions */ public function test_prepare_item() {} - - /** - * @doesNotPerformAssertions - */ - public function test_get_item_schema() {} } From 1dd2805670a719103fbafe5ca469250454657708 Mon Sep 17 00:00:00 2001 From: ramon Date: Fri, 9 Jun 2023 13:48:58 +1000 Subject: [PATCH 2/9] Modified property is returned when updated. --- ...utenberg-rest-templates-controller-6-3.php | 69 +++++++++++++++++++ ...tenberg-rest-templates-controller-test.php | 22 ++++-- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php index ab9dc582d00bd3..fa7becf188bf96 100644 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php +++ b/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php @@ -194,6 +194,75 @@ public function get_items( $request ) { return rest_ensure_response( $templates ); } + /** + * 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 = gutenberg_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 ) ); + } + + $post_before = get_post( $template->wp_id ); + + if ( isset( $request['source'] ) && 'theme' === $request['source'] ) { + wp_delete_post( $template->wp_id, true ); + $request->set_param( 'context', 'edit' ); + + $template = gutenberg_get_block_template( $request['id'], $this->post_type ); + $response = $this->prepare_item_for_response( $template, $request ); + + return rest_ensure_response( $response ); + } + + $changes = $this->prepare_item_for_database( $request ); + + if ( is_wp_error( $changes ) ) { + return $changes; + } + + if ( 'custom' === $template->source ) { + $update = true; + $result = wp_update_post( wp_slash( (array) $changes ), false ); + } else { + $update = false; + $post_before = null; + $result = wp_insert_post( wp_slash( (array) $changes ), false ); + } + + if ( is_wp_error( $result ) ) { + if ( 'db_update_error' === $result->get_error_code() ) { + $result->add_data( array( 'status' => 500 ) ); + } else { + $result->add_data( array( 'status' => 400 ) ); + } + return $result; + } + + $template = gutenberg_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; + } + + $request->set_param( 'context', 'edit' ); + + $post = get_post( $template->wp_id ); + /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ + do_action( "rest_after_insert_{$this->post_type}", $post, $request, false ); + + wp_after_insert_post( $post, $update, $post_before ); + + $response = $this->prepare_item_for_response( $template, $request ); + + return rest_ensure_response( $response ); + } + /** * Retrieves the block type' schema, conforming to JSON Schema. * diff --git a/phpunit/class-gutenberg-rest-templates-controller-test.php b/phpunit/class-gutenberg-rest-templates-controller-test.php index c992278fc50cdc..8f37c01781f3e0 100644 --- a/phpunit/class-gutenberg-rest-templates-controller-test.php +++ b/phpunit/class-gutenberg-rest-templates-controller-test.php @@ -187,19 +187,33 @@ public function test_get_items() { } /** - * @doesNotPerformAssertions + * @covers WP_REST_Templates_Controller::update_item */ - public function test_context_param() {} + public function test_update_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/templates/emptytheme//my_template' ); + $request->set_body_params( + array( + 'title' => 'My new Index Title', + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'My new Index Title', $data['title']['raw'] ); + $this->assertSame( 'custom', $data['source'] ); + $this->assertIsString( $data['modified'] ); + } /** * @doesNotPerformAssertions */ - public function test_create_item() {} + public function test_context_param() {} /** * @doesNotPerformAssertions */ - public function test_update_item() {} + public function test_create_item() {} /** * @doesNotPerformAssertions From be9285f792612e2fcbc174df1425732ff3d95706 Mon Sep 17 00:00:00 2001 From: ramon Date: Fri, 9 Jun 2023 13:57:22 +1000 Subject: [PATCH 3/9] Modified property is returned when created. --- ...utenberg-rest-templates-controller-6-3.php | 52 +++++++++++++++++++ ...tenberg-rest-templates-controller-test.php | 47 +++++++++++++++-- 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php index fa7becf188bf96..a7ce2390c04bb8 100644 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php +++ b/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php @@ -263,6 +263,58 @@ public function update_item( $request ) { return rest_ensure_response( $response ); } + /** + * 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 ) { + $prepared_post = $this->prepare_item_for_database( $request ); + + if ( is_wp_error( $prepared_post ) ) { + return $prepared_post; + } + + $prepared_post->post_name = $request['slug']; + $post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true ); + if ( is_wp_error( $post_id ) ) { + if ( 'db_insert_error' === $post_id->get_error_code() ) { + $post_id->add_data( array( 'status' => 500 ) ); + } else { + $post_id->add_data( array( 'status' => 400 ) ); + } + + return $post_id; + } + $posts = gutenberg_get_block_templates( array( 'wp_id' => $post_id ), $this->post_type ); + if ( ! count( $posts ) ) { + return new WP_Error( 'rest_template_insert_error', __( 'No templates exist with that id.' ), array( 'status' => 400 ) ); + } + $id = $posts[0]->id; + $post = get_post( $post_id ); + $template = gutenberg_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; + } + + /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ + do_action( "rest_after_insert_{$this->post_type}", $post, $request, true ); + + wp_after_insert_post( $post, false, null ); + + $response = $this->prepare_item_for_response( $template, $request ); + $response = rest_ensure_response( $response ); + + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $template->id ) ) ); + + return $response; + } + /** * Retrieves the block type' schema, conforming to JSON Schema. * diff --git a/phpunit/class-gutenberg-rest-templates-controller-test.php b/phpunit/class-gutenberg-rest-templates-controller-test.php index 8f37c01781f3e0..3e96da92e440c0 100644 --- a/phpunit/class-gutenberg-rest-templates-controller-test.php +++ b/phpunit/class-gutenberg-rest-templates-controller-test.php @@ -206,14 +206,55 @@ public function test_update_item() { } /** - * @doesNotPerformAssertions + * @covers WP_REST_Templates_Controller::create_item */ - public function test_context_param() {} + public function test_create_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/templates' ); + $request->set_body_params( + array( + 'slug' => 'my_custom_template', + 'description' => 'Just a description', + 'title' => 'My Template', + 'content' => 'Content', + 'author' => self::$admin_id, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + unset( $data['_links'] ); + unset( $data['wp_id'] ); + + $this->assertSame( + array( + 'id' => 'emptytheme//my_custom_template', + 'theme' => 'emptytheme', + 'content' => array( + 'raw' => 'Content', + ), + 'slug' => 'my_custom_template', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Just a description', + 'title' => array( + 'raw' => 'My Template', + 'rendered' => 'My Template', + ), + 'status' => 'publish', + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => self::$admin_id, + 'modified' => $data['modified'], + ), + $data + ); + } /** * @doesNotPerformAssertions */ - public function test_create_item() {} + public function test_context_param() {} /** * @doesNotPerformAssertions From b742fb359d9ccf5ffea1df386163bb9f388ff09e Mon Sep 17 00:00:00 2001 From: ramon Date: Fri, 9 Jun 2023 15:33:40 +1000 Subject: [PATCH 4/9] _wp_build_title_and_description_for_single_post_type_block_template and _wp_build_title_and_description_for_taxonomy_block_template have to be ported across too because they expect WP_Block_Template, not Gutenberg_Block_Template --- .../wordpress-6.3/block-template-utils.php | 181 ++++++++++++++++-- 1 file changed, 169 insertions(+), 12 deletions(-) diff --git a/lib/compat/wordpress-6.3/block-template-utils.php b/lib/compat/wordpress-6.3/block-template-utils.php index f8941def143938..8c2e8274723db5 100644 --- a/lib/compat/wordpress-6.3/block-template-utils.php +++ b/lib/compat/wordpress-6.3/block-template-utils.php @@ -15,7 +15,7 @@ * @access private * * @param WP_Post $post Template post. - * @return WP_Block_Template|WP_Error Template or error object. + * @return Gutenberg_Block_Template|WP_Error Template or error object. */ function _gutenberg_build_block_template_result_from_post( $post ) { $default_template_types = get_default_block_template_types(); @@ -130,7 +130,7 @@ function _gutenberg_build_block_template_result_from_post( $post ) { } break; case 'page': - _wp_build_title_and_description_for_single_post_type_block_template( 'page', $slug_remaining, $template ); + _gutenberg_build_title_and_description_for_single_post_type_block_template( 'page', $slug_remaining, $template ); break; case 'single': $post_types = get_post_types(); @@ -141,7 +141,7 @@ function _gutenberg_build_block_template_result_from_post( $post ) { // If $slug_remaining starts with $post_type followed by a hyphen. if ( 0 === strncmp( $slug_remaining, $post_type . '-', $post_type_length ) ) { $slug = substr( $slug_remaining, $post_type_length, strlen( $slug_remaining ) ); - $found = _wp_build_title_and_description_for_single_post_type_block_template( $post_type, $slug, $template ); + $found = _gutenberg_build_title_and_description_for_single_post_type_block_template( $post_type, $slug, $template ); if ( $found ) { break; @@ -150,10 +150,10 @@ function _gutenberg_build_block_template_result_from_post( $post ) { } break; case 'tag': - _wp_build_title_and_description_for_taxonomy_block_template( 'post_tag', $slug_remaining, $template ); + _gutenberg_build_title_and_description_for_taxonomy_block_template( 'post_tag', $slug_remaining, $template ); break; case 'category': - _wp_build_title_and_description_for_taxonomy_block_template( 'category', $slug_remaining, $template ); + _gutenberg_build_title_and_description_for_taxonomy_block_template( 'category', $slug_remaining, $template ); break; case 'taxonomy': $taxonomies = get_taxonomies(); @@ -164,7 +164,7 @@ function _gutenberg_build_block_template_result_from_post( $post ) { // If $slug_remaining starts with $taxonomy followed by a hyphen. if ( 0 === strncmp( $slug_remaining, $taxonomy . '-', $taxonomy_length ) ) { $slug = substr( $slug_remaining, $taxonomy_length, strlen( $slug_remaining ) ); - $found = _wp_build_title_and_description_for_taxonomy_block_template( $taxonomy, $slug, $template ); + $found = _gutenberg_build_title_and_description_for_taxonomy_block_template( $taxonomy, $slug, $template ); if ( $found ) { break; @@ -193,7 +193,7 @@ function _gutenberg_build_block_template_result_from_post( $post ) { * @type string $post_type Post type to get the templates for. * } * @param string $template_type 'wp_template' or 'wp_template_part'. - * @return WP_Block_Template[] Array of block templates. + * @return Gutenberg_Block_Template[] Array of block templates. */ function gutenberg_get_block_templates( $query = array(), $template_type = 'wp_template' ) { /** @@ -201,7 +201,7 @@ function gutenberg_get_block_templates( $query = array(), $template_type = 'wp_t * * Return a non-null value to bypass the WordPress queries. * - * @param WP_Block_Template[]|null $block_templates Return an array of block templates to short-circuit the default query, + * @param Gutenberg_Block_Template[]|null $block_templates Return an array of block templates to short-circuit the default query, * or null to allow WP to run its normal queries. * @param array $query { * Arguments to retrieve templates. All arguments are optional. @@ -295,7 +295,7 @@ function gutenberg_get_block_templates( $query = array(), $template_type = 'wp_t /** * Filters the array of queried block templates array after they've been fetched. * - * @param WP_Block_Template[] $query_result Array of found block templates. + * @param Gutenberg_Block_Template[] $query_result Array of found block templates. * @param array $query { * Arguments to retrieve templates. All arguments are optional. * @@ -318,7 +318,7 @@ function gutenberg_get_block_templates( $query = array(), $template_type = 'wp_t * @param string $id Template unique identifier (example: 'theme_slug//template_slug'). * @param string $template_type Optional. Template type: 'wp_template' or 'wp_template_part'. * Default 'wp_template'. - * @return WP_Block_Template|null Template. + * @return Gutenberg_Block_Template|null Template. */ function gutenberg_get_block_template( $id, $template_type = 'wp_template' ) { /** @@ -328,7 +328,7 @@ function gutenberg_get_block_template( $id, $template_type = 'wp_template' ) { * * @since 5.9.0 * - * @param WP_Block_Template|null $block_template Return block template object to short-circuit the default query, + * @param Gutenberg_Block_Template|null $block_template Return block template object to short-circuit the default query, * or null to allow WP to run its normal queries. * @param string $id Template unique identifier (example: 'theme_slug//template_slug'). * @param string $template_type Template type: 'wp_template' or 'wp_template_part'. @@ -375,9 +375,166 @@ function gutenberg_get_block_template( $id, $template_type = 'wp_template' ) { * * @since 5.9.0 * - * @param WP_Block_Template|null $block_template The found block template, or null if there isn't one. + * @param Gutenberg_Block_Template|null $block_template The found block template, or null if there isn't one. * @param string $id Template unique identifier (example: 'theme_slug//template_slug'). * @param array $template_type Template type: 'wp_template' or 'wp_template_part'. */ return apply_filters( 'get_block_template', $block_template, $id, $template_type ); } + +/** + * Builds the title and description of a post-specific template based on the underlying referenced post. + * + * Mutates the underlying template object. + * + * @since 6.1.0 + * @access private + * + * @param string $post_type Post type, e.g. page, post, product. + * @param string $slug Slug of the post, e.g. a-story-about-shoes. + * @param Gutenberg_Block_Template $template Template to mutate adding the description and title computed. + * @return bool Returns true if the referenced post was found and false otherwise. + */ +function _gutenberg_build_title_and_description_for_single_post_type_block_template( $post_type, $slug, Gutenberg_Block_Template $template ) { + $post_type_object = get_post_type_object( $post_type ); + + $default_args = array( + 'post_type' => $post_type, + 'post_status' => 'publish', + 'posts_per_page' => 1, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + ); + + $args = array( + 'name' => $slug, + ); + $args = wp_parse_args( $args, $default_args ); + + $posts_query = new WP_Query( $args ); + + if ( empty( $posts_query->posts ) ) { + $template->title = sprintf( + /* translators: Custom template title in the Site Editor referencing a post that was not found. 1: Post type singular name, 2: Post type slug. */ + __( 'Not found: %1$s (%2$s)' ), + $post_type_object->labels->singular_name, + $slug + ); + + return false; + } + + $post_title = $posts_query->posts[0]->post_title; + + $template->title = sprintf( + /* translators: Custom template title in the Site Editor. 1: Post type singular name, 2: Post title. */ + __( '%1$s: %2$s' ), + $post_type_object->labels->singular_name, + $post_title + ); + + $template->description = sprintf( + /* translators: Custom template description in the Site Editor. %s: Post title. */ + __( 'Template for %s' ), + $post_title + ); + + $args = array( + 'title' => $post_title, + ); + $args = wp_parse_args( $args, $default_args ); + + $posts_with_same_title_query = new WP_Query( $args ); + + if ( count( $posts_with_same_title_query->posts ) > 1 ) { + $template->title = sprintf( + /* translators: Custom template title in the Site Editor. 1: Template title, 2: Post type slug. */ + __( '%1$s (%2$s)' ), + $template->title, + $slug + ); + } + + return true; +} + +/** + * Builds the title and description of a taxonomy-specific template based on the underlying entity referenced. + * + * Mutates the underlying template object. + * + * @since 6.1.0 + * @access private + * + * @param string $taxonomy Identifier of the taxonomy, e.g. category. + * @param string $slug Slug of the term, e.g. shoes. + * @param Gutenberg_Block_Template $template Template to mutate adding the description and title computed. + * @return bool True if the term referenced was found and false otherwise. + */ +function _gutenberg_build_title_and_description_for_taxonomy_block_template( $taxonomy, $slug, Gutenberg_Block_Template $template ) { + $taxonomy_object = get_taxonomy( $taxonomy ); + + $default_args = array( + 'taxonomy' => $taxonomy, + 'hide_empty' => false, + 'update_term_meta_cache' => false, + ); + + $term_query = new WP_Term_Query(); + + $args = array( + 'number' => 1, + 'slug' => $slug, + ); + $args = wp_parse_args( $args, $default_args ); + + $terms_query = $term_query->query( $args ); + + if ( empty( $terms_query ) ) { + $template->title = sprintf( + /* translators: Custom template title in the Site Editor, referencing a taxonomy term that was not found. 1: Taxonomy singular name, 2: Term slug. */ + __( 'Not found: %1$s (%2$s)' ), + $taxonomy_object->labels->singular_name, + $slug + ); + return false; + } + + $term_title = $terms_query[0]->name; + + $template->title = sprintf( + /* translators: Custom template title in the Site Editor. 1: Taxonomy singular name, 2: Term title. */ + __( '%1$s: %2$s' ), + $taxonomy_object->labels->singular_name, + $term_title + ); + + $template->description = sprintf( + /* translators: Custom template description in the Site Editor. %s: Term title. */ + __( 'Template for %s' ), + $term_title + ); + + $term_query = new WP_Term_Query(); + + $args = array( + 'number' => 2, + 'name' => $term_title, + ); + $args = wp_parse_args( $args, $default_args ); + + $terms_with_same_title_query = $term_query->query( $args ); + + if ( count( $terms_with_same_title_query ) > 1 ) { + $template->title = sprintf( + /* translators: Custom template title in the Site Editor. 1: Template title, 2: Term slug. */ + __( '%1$s (%2$s)' ), + $template->title, + $slug + ); + } + + return true; +} From f56656fd349bcbd7e7359434ba089c833f50f1cd Mon Sep 17 00:00:00 2001 From: ramon Date: Fri, 9 Jun 2023 16:04:25 +1000 Subject: [PATCH 5/9] Revert changes that required pulling all files across and using hooks to add the field to the response. --- .../wordpress-6.3/block-template-utils.php | 540 ------------------ .../class-gutenberg-block-template.php | 158 ----- ...utenberg-rest-templates-controller-6-3.php | 345 +---------- lib/compat/wordpress-6.3/rest-api.php | 22 + lib/load.php | 2 - phpunit/block-template-utils-test.php | 84 +-- 6 files changed, 29 insertions(+), 1122 deletions(-) delete mode 100644 lib/compat/wordpress-6.3/block-template-utils.php delete mode 100644 lib/compat/wordpress-6.3/class-gutenberg-block-template.php diff --git a/lib/compat/wordpress-6.3/block-template-utils.php b/lib/compat/wordpress-6.3/block-template-utils.php deleted file mode 100644 index 8c2e8274723db5..00000000000000 --- a/lib/compat/wordpress-6.3/block-template-utils.php +++ /dev/null @@ -1,540 +0,0 @@ -name; - $template_file = _get_block_template_file( $post->post_type, $post->post_name ); - $has_theme_file = get_stylesheet() === $theme && null !== $template_file; - - $origin = get_post_meta( $post->ID, 'origin', true ); - $is_wp_suggestion = get_post_meta( $post->ID, 'is_wp_suggestion', true ); - - $template = new Gutenberg_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->origin = ! empty( $origin ) ? $origin : null; - $template->type = $post->post_type; - $template->description = $post->post_excerpt; - $template->title = $post->post_title; - $template->status = $post->post_status; - $template->has_theme_file = $has_theme_file; - $template->is_custom = empty( $is_wp_suggestion ); - $template->author = $post->post_author; - $template->modified = ! empty( $post->post_modified ) ? $post->post_modified : null; - - if ( 'wp_template' === $post->post_type && $has_theme_file && isset( $template_file['postTypes'] ) ) { - $template->post_types = $template_file['postTypes']; - } - - if ( 'wp_template' === $post->post_type && isset( $default_template_types[ $template->slug ] ) ) { - $template->is_custom = false; - } - - if ( 'wp_template_part' === $post->post_type ) { - $type_terms = get_the_terms( $post, 'wp_template_part_area' ); - if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) { - $template->area = $type_terms[0]->name; - } - } - - // Check for a block template without a description and title or with a title equal to the slug. - if ( 'wp_template' === $post->post_type && empty( $template->description ) && ( empty( $template->title ) || $template->title === $template->slug ) ) { - $matches = array(); - - // Check for a block template for a single author, page, post, tag, category, custom post type, or custom taxonomy. - if ( preg_match( '/(author|page|single|tag|category|taxonomy)-(.+)/', $template->slug, $matches ) ) { - $type = $matches[1]; - $slug_remaining = $matches[2]; - - switch ( $type ) { - case 'author': - $nice_name = $slug_remaining; - $users = get_users( - array( - 'capability' => 'edit_posts', - 'search' => $nice_name, - 'search_columns' => array( 'user_nicename' ), - 'fields' => 'display_name', - ) - ); - - if ( empty( $users ) ) { - $template->title = sprintf( - /* translators: Custom template title in the Site Editor, referencing a deleted author. %s: Author nicename. */ - __( 'Deleted author: %s' ), - $nice_name - ); - } else { - $author_name = $users[0]; - - $template->title = sprintf( - /* translators: Custom template title in the Site Editor. %s: Author name. */ - __( 'Author: %s' ), - $author_name - ); - - $template->description = sprintf( - /* translators: Custom template description in the Site Editor. %s: Author name. */ - __( 'Template for %s' ), - $author_name - ); - - $users_with_same_name = get_users( - array( - 'capability' => 'edit_posts', - 'search' => $author_name, - 'search_columns' => array( 'display_name' ), - 'fields' => 'display_name', - ) - ); - - if ( count( $users_with_same_name ) > 1 ) { - $template->title = sprintf( - /* translators: Custom template title in the Site Editor. 1: Template title of an author template, 2: Author nicename. */ - __( '%1$s (%2$s)' ), - $template->title, - $nice_name - ); - } - } - break; - case 'page': - _gutenberg_build_title_and_description_for_single_post_type_block_template( 'page', $slug_remaining, $template ); - break; - case 'single': - $post_types = get_post_types(); - - foreach ( $post_types as $post_type ) { - $post_type_length = strlen( $post_type ) + 1; - - // If $slug_remaining starts with $post_type followed by a hyphen. - if ( 0 === strncmp( $slug_remaining, $post_type . '-', $post_type_length ) ) { - $slug = substr( $slug_remaining, $post_type_length, strlen( $slug_remaining ) ); - $found = _gutenberg_build_title_and_description_for_single_post_type_block_template( $post_type, $slug, $template ); - - if ( $found ) { - break; - } - } - } - break; - case 'tag': - _gutenberg_build_title_and_description_for_taxonomy_block_template( 'post_tag', $slug_remaining, $template ); - break; - case 'category': - _gutenberg_build_title_and_description_for_taxonomy_block_template( 'category', $slug_remaining, $template ); - break; - case 'taxonomy': - $taxonomies = get_taxonomies(); - - foreach ( $taxonomies as $taxonomy ) { - $taxonomy_length = strlen( $taxonomy ) + 1; - - // If $slug_remaining starts with $taxonomy followed by a hyphen. - if ( 0 === strncmp( $slug_remaining, $taxonomy . '-', $taxonomy_length ) ) { - $slug = substr( $slug_remaining, $taxonomy_length, strlen( $slug_remaining ) ); - $found = _gutenberg_build_title_and_description_for_taxonomy_block_template( $taxonomy, $slug, $template ); - - if ( $found ) { - break; - } - } - } - break; - } - } - } - - 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 string[] $slug__in List of slugs to include. - * @type int $wp_id Post ID of customized template. - * @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only). - * @type string $post_type Post type to get the templates for. - * } - * @param string $template_type 'wp_template' or 'wp_template_part'. - * @return Gutenberg_Block_Template[] Array of block templates. - */ -function gutenberg_get_block_templates( $query = array(), $template_type = 'wp_template' ) { - /** - * Filters the block templates array before the query takes place. - * - * Return a non-null value to bypass the WordPress queries. - * - * @param Gutenberg_Block_Template[]|null $block_templates Return an array of block templates to short-circuit the default query, - * or null to allow WP to run its normal queries. - * @param array $query { - * Arguments to retrieve templates. All arguments are optional. - * - * @type string[] $slug__in List of slugs to include. - * @type int $wp_id Post ID of customized template. - * @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only). - * @type string $post_type Post type to get the templates for. - * } - * @param string $template_type 'wp_template' or 'wp_template_part'. - * @since 5.9.0 - */ - $templates = apply_filters( 'pre_get_block_templates', null, $query, $template_type ); - if ( ! is_null( $templates ) ) { - return $templates; - } - - $post_type = isset( $query['post_type'] ) ? $query['post_type'] : ''; - $wp_query_args = array( - 'post_status' => array( 'auto-draft', 'draft', 'publish' ), - 'post_type' => $template_type, - 'posts_per_page' => -1, - 'no_found_rows' => true, - 'lazy_load_term_meta' => false, - 'tax_query' => array( - array( - 'taxonomy' => 'wp_theme', - 'field' => 'name', - 'terms' => get_stylesheet(), - ), - ), - ); - - if ( 'wp_template_part' === $template_type && isset( $query['area'] ) ) { - $wp_query_args['tax_query'][] = array( - 'taxonomy' => 'wp_template_part_area', - 'field' => 'name', - 'terms' => $query['area'], - ); - $wp_query_args['tax_query']['relation'] = 'AND'; - } - - if ( ! empty( $query['slug__in'] ) ) { - $wp_query_args['post_name__in'] = $query['slug__in']; - $wp_query_args['posts_per_page'] = count( array_unique( $query['slug__in'] ) ); - } - - // This is only needed for the regular templates/template parts post type 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->posts as $post ) { - $template = _gutenberg_build_block_template_result_from_post( $post ); - - if ( is_wp_error( $template ) ) { - continue; - } - - if ( $post_type && ! $template->is_custom ) { - continue; - } - - if ( - $post_type && - isset( $template->post_types ) && - ! in_array( $post_type, $template->post_types, true ) - ) { - continue; - } - - $query_result[] = $template; - } - - if ( ! isset( $query['wp_id'] ) ) { - /* - * If the query has found some use templates, those have priority - * over the theme-provided ones, so we skip querying and building them. - */ - $query['slug__not_in'] = wp_list_pluck( $query_result, 'slug' ); - $template_files = _get_block_templates_files( $template_type, $query ); - foreach ( $template_files as $template_file ) { - $query_result[] = _build_block_template_result_from_file( $template_file, $template_type ); - } - } - - /** - * Filters the array of queried block templates array after they've been fetched. - * - * @param Gutenberg_Block_Template[] $query_result Array of found block templates. - * @param array $query { - * Arguments to retrieve templates. All arguments are optional. - * - * @type string[] $slug__in List of slugs to include. - * @type int $wp_id Post ID of customized template. - * @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only). - * @type string $post_type Post type to get the templates for. - * } - * @param string $template_type wp_template or wp_template_part. - * @since 5.9.0 - */ - return apply_filters( 'get_block_templates', $query_result, $query, $template_type ); -} - -/** - * 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 Optional. Template type: 'wp_template' or 'wp_template_part'. - * Default 'wp_template'. - * @return Gutenberg_Block_Template|null Template. - */ -function gutenberg_get_block_template( $id, $template_type = 'wp_template' ) { - /** - * Filters the block template object before the query takes place. - * - * Return a non-null value to bypass the WordPress queries. - * - * @since 5.9.0 - * - * @param Gutenberg_Block_Template|null $block_template Return block template object to short-circuit the default query, - * or null to allow WP to run its normal queries. - * @param string $id Template unique identifier (example: 'theme_slug//template_slug'). - * @param string $template_type Template type: 'wp_template' or 'wp_template_part'. - */ - $block_template = apply_filters( 'pre_get_block_template', null, $id, $template_type ); - if ( ! is_null( $block_template ) ) { - return $block_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->posts; - - if ( count( $posts ) > 0 ) { - $template = _gutenberg_build_block_template_result_from_post( $posts[0] ); - - if ( ! is_wp_error( $template ) ) { - return $template; - } - } - - $block_template = get_block_file_template( $id, $template_type ); - - /** - * Filters the queried block template object after it's been fetched. - * - * @since 5.9.0 - * - * @param Gutenberg_Block_Template|null $block_template The found block template, or null if there isn't one. - * @param string $id Template unique identifier (example: 'theme_slug//template_slug'). - * @param array $template_type Template type: 'wp_template' or 'wp_template_part'. - */ - return apply_filters( 'get_block_template', $block_template, $id, $template_type ); -} - -/** - * Builds the title and description of a post-specific template based on the underlying referenced post. - * - * Mutates the underlying template object. - * - * @since 6.1.0 - * @access private - * - * @param string $post_type Post type, e.g. page, post, product. - * @param string $slug Slug of the post, e.g. a-story-about-shoes. - * @param Gutenberg_Block_Template $template Template to mutate adding the description and title computed. - * @return bool Returns true if the referenced post was found and false otherwise. - */ -function _gutenberg_build_title_and_description_for_single_post_type_block_template( $post_type, $slug, Gutenberg_Block_Template $template ) { - $post_type_object = get_post_type_object( $post_type ); - - $default_args = array( - 'post_type' => $post_type, - 'post_status' => 'publish', - 'posts_per_page' => 1, - 'update_post_meta_cache' => false, - 'update_post_term_cache' => false, - 'ignore_sticky_posts' => true, - 'no_found_rows' => true, - ); - - $args = array( - 'name' => $slug, - ); - $args = wp_parse_args( $args, $default_args ); - - $posts_query = new WP_Query( $args ); - - if ( empty( $posts_query->posts ) ) { - $template->title = sprintf( - /* translators: Custom template title in the Site Editor referencing a post that was not found. 1: Post type singular name, 2: Post type slug. */ - __( 'Not found: %1$s (%2$s)' ), - $post_type_object->labels->singular_name, - $slug - ); - - return false; - } - - $post_title = $posts_query->posts[0]->post_title; - - $template->title = sprintf( - /* translators: Custom template title in the Site Editor. 1: Post type singular name, 2: Post title. */ - __( '%1$s: %2$s' ), - $post_type_object->labels->singular_name, - $post_title - ); - - $template->description = sprintf( - /* translators: Custom template description in the Site Editor. %s: Post title. */ - __( 'Template for %s' ), - $post_title - ); - - $args = array( - 'title' => $post_title, - ); - $args = wp_parse_args( $args, $default_args ); - - $posts_with_same_title_query = new WP_Query( $args ); - - if ( count( $posts_with_same_title_query->posts ) > 1 ) { - $template->title = sprintf( - /* translators: Custom template title in the Site Editor. 1: Template title, 2: Post type slug. */ - __( '%1$s (%2$s)' ), - $template->title, - $slug - ); - } - - return true; -} - -/** - * Builds the title and description of a taxonomy-specific template based on the underlying entity referenced. - * - * Mutates the underlying template object. - * - * @since 6.1.0 - * @access private - * - * @param string $taxonomy Identifier of the taxonomy, e.g. category. - * @param string $slug Slug of the term, e.g. shoes. - * @param Gutenberg_Block_Template $template Template to mutate adding the description and title computed. - * @return bool True if the term referenced was found and false otherwise. - */ -function _gutenberg_build_title_and_description_for_taxonomy_block_template( $taxonomy, $slug, Gutenberg_Block_Template $template ) { - $taxonomy_object = get_taxonomy( $taxonomy ); - - $default_args = array( - 'taxonomy' => $taxonomy, - 'hide_empty' => false, - 'update_term_meta_cache' => false, - ); - - $term_query = new WP_Term_Query(); - - $args = array( - 'number' => 1, - 'slug' => $slug, - ); - $args = wp_parse_args( $args, $default_args ); - - $terms_query = $term_query->query( $args ); - - if ( empty( $terms_query ) ) { - $template->title = sprintf( - /* translators: Custom template title in the Site Editor, referencing a taxonomy term that was not found. 1: Taxonomy singular name, 2: Term slug. */ - __( 'Not found: %1$s (%2$s)' ), - $taxonomy_object->labels->singular_name, - $slug - ); - return false; - } - - $term_title = $terms_query[0]->name; - - $template->title = sprintf( - /* translators: Custom template title in the Site Editor. 1: Taxonomy singular name, 2: Term title. */ - __( '%1$s: %2$s' ), - $taxonomy_object->labels->singular_name, - $term_title - ); - - $template->description = sprintf( - /* translators: Custom template description in the Site Editor. %s: Term title. */ - __( 'Template for %s' ), - $term_title - ); - - $term_query = new WP_Term_Query(); - - $args = array( - 'number' => 2, - 'name' => $term_title, - ); - $args = wp_parse_args( $args, $default_args ); - - $terms_with_same_title_query = $term_query->query( $args ); - - if ( count( $terms_with_same_title_query ) > 1 ) { - $template->title = sprintf( - /* translators: Custom template title in the Site Editor. 1: Template title, 2: Term slug. */ - __( '%1$s (%2$s)' ), - $template->title, - $slug - ); - } - - return true; -} diff --git a/lib/compat/wordpress-6.3/class-gutenberg-block-template.php b/lib/compat/wordpress-6.3/class-gutenberg-block-template.php deleted file mode 100644 index 2de5169a311754..00000000000000 --- a/lib/compat/wordpress-6.3/class-gutenberg-block-template.php +++ /dev/null @@ -1,158 +0,0 @@ -data['modified'] = $template->modified; + // Adds modified date to the response. + if ( rest_is_field_included( 'modified', $fields ) && ! empty( $response->data['wp_id'] ) ) { + $post = get_post( $response->data['wp_id'] ); + if ( $post ) { + $response->data['modified'] = $post->post_modified; } } @@ -141,332 +136,4 @@ protected function prepare_revision_links( $template ) { return $links; } - - - /** - * 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 ) { - if ( isset( $request['source'] ) && 'theme' === $request['source'] ) { - $template = get_block_file_template( $request['id'], $this->post_type ); - } else { - $template = gutenberg_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 ); - } - - /** - * 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']; - } - if ( isset( $request['post_type'] ) ) { - $query['post_type'] = $request['post_type']; - } - - $templates = array(); - foreach ( gutenberg_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 ); - } - - /** - * 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 = gutenberg_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 ) ); - } - - $post_before = get_post( $template->wp_id ); - - if ( isset( $request['source'] ) && 'theme' === $request['source'] ) { - wp_delete_post( $template->wp_id, true ); - $request->set_param( 'context', 'edit' ); - - $template = gutenberg_get_block_template( $request['id'], $this->post_type ); - $response = $this->prepare_item_for_response( $template, $request ); - - return rest_ensure_response( $response ); - } - - $changes = $this->prepare_item_for_database( $request ); - - if ( is_wp_error( $changes ) ) { - return $changes; - } - - if ( 'custom' === $template->source ) { - $update = true; - $result = wp_update_post( wp_slash( (array) $changes ), false ); - } else { - $update = false; - $post_before = null; - $result = wp_insert_post( wp_slash( (array) $changes ), false ); - } - - if ( is_wp_error( $result ) ) { - if ( 'db_update_error' === $result->get_error_code() ) { - $result->add_data( array( 'status' => 500 ) ); - } else { - $result->add_data( array( 'status' => 400 ) ); - } - return $result; - } - - $template = gutenberg_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; - } - - $request->set_param( 'context', 'edit' ); - - $post = get_post( $template->wp_id ); - /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ - do_action( "rest_after_insert_{$this->post_type}", $post, $request, false ); - - wp_after_insert_post( $post, $update, $post_before ); - - $response = $this->prepare_item_for_response( $template, $request ); - - return rest_ensure_response( $response ); - } - - /** - * 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 ) { - $prepared_post = $this->prepare_item_for_database( $request ); - - if ( is_wp_error( $prepared_post ) ) { - return $prepared_post; - } - - $prepared_post->post_name = $request['slug']; - $post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true ); - if ( is_wp_error( $post_id ) ) { - if ( 'db_insert_error' === $post_id->get_error_code() ) { - $post_id->add_data( array( 'status' => 500 ) ); - } else { - $post_id->add_data( array( 'status' => 400 ) ); - } - - return $post_id; - } - $posts = gutenberg_get_block_templates( array( 'wp_id' => $post_id ), $this->post_type ); - if ( ! count( $posts ) ) { - return new WP_Error( 'rest_template_insert_error', __( 'No templates exist with that id.' ), array( 'status' => 400 ) ); - } - $id = $posts[0]->id; - $post = get_post( $post_id ); - $template = gutenberg_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; - } - - /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ - do_action( "rest_after_insert_{$this->post_type}", $post, $request, true ); - - wp_after_insert_post( $post, false, null ); - - $response = $this->prepare_item_for_response( $template, $request ); - $response = rest_ensure_response( $response ); - - $response->set_status( 201 ); - $response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $template->id ) ) ); - - return $response; - } - - /** - * Retrieves the block type' schema, conforming to JSON Schema. - * - * @since 5.8.0 - * @since 5.9.0 Added `'area'`. - * @since 6.3.0 Added `'modified'`. - * - * @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-Z0-9_\%-]+', - ), - 'theme' => array( - 'description' => __( 'Theme identifier for the template.' ), - 'type' => 'string', - 'context' => array( 'embed', 'view', 'edit' ), - ), - 'type' => array( - 'description' => __( 'Type of template.' ), - 'type' => 'string', - 'context' => array( 'embed', 'view', 'edit' ), - ), - 'source' => array( - 'description' => __( 'Source of template' ), - 'type' => 'string', - 'context' => array( 'embed', 'view', 'edit' ), - 'readonly' => true, - ), - 'origin' => array( - 'description' => __( 'Source of a customized 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' ), - 'properties' => array( - 'raw' => array( - 'description' => __( 'Content for the template, as it exists in the database.' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'block_version' => array( - 'description' => __( 'Version of the content block format used by the template.' ), - 'type' => 'integer', - 'context' => array( 'edit' ), - 'readonly' => true, - ), - ), - ), - 'title' => array( - 'description' => __( 'Title of template.' ), - 'type' => array( 'object', 'string' ), - 'default' => '', - 'context' => array( 'embed', 'view', 'edit' ), - 'properties' => array( - 'raw' => array( - 'description' => __( 'Title for the template, as it exists in the database.' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'rendered' => array( - 'description' => __( 'HTML title for the template, transformed for display.' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ), - ), - ), - 'description' => array( - 'description' => __( 'Description of template.' ), - 'type' => 'string', - 'default' => '', - 'context' => array( 'embed', 'view', 'edit' ), - ), - 'status' => array( - 'description' => __( 'Status of template.' ), - 'type' => 'string', - 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ), - '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, - ), - 'author' => array( - 'description' => __( 'The ID for the author of the template.' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'modified' => array( - 'description' => __( "The date the post was last modified, in the site's timezone." ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), - ); - - if ( 'wp_template' === $this->post_type ) { - $schema['properties']['is_custom'] = array( - 'description' => __( 'Whether a template is a custom template.' ), - 'type' => 'bool', - 'context' => array( 'embed', 'view', 'edit' ), - 'readonly' => true, - ); - } - - if ( 'wp_template_part' === $this->post_type ) { - $schema['properties']['area'] = array( - 'description' => __( 'Where the template part is intended for use (header, footer, etc.)' ), - 'type' => 'string', - 'context' => array( 'embed', 'view', 'edit' ), - ); - } - - $this->schema = $schema; - - return $this->add_additional_fields_schema( $this->schema ); - } } diff --git a/lib/compat/wordpress-6.3/rest-api.php b/lib/compat/wordpress-6.3/rest-api.php index 757bd317dcd548..b276273d5a86c7 100644 --- a/lib/compat/wordpress-6.3/rest-api.php +++ b/lib/compat/wordpress-6.3/rest-api.php @@ -76,3 +76,25 @@ function gutenberg_update_global_styles_rest_controller( $args, $post_type ) { return $args; } add_filter( 'register_post_type_args', 'gutenberg_update_global_styles_rest_controller', 10, 2 ); + +/** + * Add the `modified` value to the `wp_template` schema. + * + * @since 6.3.0 Added 'modified' property. + */ +function add_modified_wp_template_schema() { + register_rest_field( + array( 'wp_template', 'wp_template_part' ), + 'modified', + array( + 'schema' => array( + 'description' => __( "The date the post was last modified, in the site's timezone.", 'gutenberg' ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ) + ); +} +add_filter( 'rest_api_init', 'add_modified_wp_template_schema' ); diff --git a/lib/load.php b/lib/load.php index 1118b69972e043..b0ed333185ef6a 100644 --- a/lib/load.php +++ b/lib/load.php @@ -82,8 +82,6 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.3 compat. require __DIR__ . '/compat/wordpress-6.3/get-global-styles-and-settings.php'; -require __DIR__ . '/compat/wordpress-6.3/block-template-utils.php'; -require __DIR__ . '/compat/wordpress-6.3/class-gutenberg-block-template.php'; if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { require __DIR__ . '/compat/wordpress-6.2/html-api/class-wp-html-attribute-token.php'; diff --git a/phpunit/block-template-utils-test.php b/phpunit/block-template-utils-test.php index a9cdc94fe6e953..ac2e4145cd0ed2 100644 --- a/phpunit/block-template-utils-test.php +++ b/phpunit/block-template-utils-test.php @@ -6,18 +6,12 @@ */ class Tests_Block_Template_Utils extends WP_UnitTestCase { - const TEST_THEME = 'emptytheme'; - - private static $template_post; - - private static $template_part_post; - public function set_up() { parent::set_up(); switch_theme( 'emptytheme' ); } - public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + public static function wpSetUpBeforeClass() { register_post_type( 'custom_book', array( @@ -26,46 +20,6 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { ) ); register_taxonomy( 'book_type', 'custom_book' ); - - // Set up template post. - self::$template_post = $factory->post->create_and_get( - array( - 'post_type' => 'wp_template', - 'post_name' => 'my_template', - 'post_title' => 'My Template', - 'post_content' => 'Content', - 'post_excerpt' => 'Description of my template', - 'tax_input' => array( - 'wp_theme' => array( - self::TEST_THEME, - ), - ), - ) - ); - - wp_set_post_terms( self::$template_post->ID, self::TEST_THEME, 'wp_theme' ); - - // Set up template part post. - self::$template_part_post = $factory->post->create_and_get( - array( - 'post_type' => 'wp_template_part', - 'post_name' => 'my_template_part', - 'post_title' => 'My Template Part', - 'post_content' => 'Content', - 'post_excerpt' => 'Description of my template part', - 'tax_input' => array( - 'wp_theme' => array( - self::TEST_THEME, - ), - 'wp_template_part_area' => array( - WP_TEMPLATE_PART_AREA_HEADER, - ), - ), - ) - ); - - wp_set_post_terms( self::$template_part_post->ID, WP_TEMPLATE_PART_AREA_HEADER, 'wp_template_part_area' ); - wp_set_post_terms( self::$template_part_post->ID, self::TEST_THEME, 'wp_theme' ); } public static function wpTearDownAfterClass() { @@ -296,40 +250,4 @@ public function test_get_template_hierarchy() { $hierarchy ); } - - public function test_build_block_template_result_from_post() { - $template = _gutenberg_build_block_template_result_from_post( - self::$template_post, - 'wp_template' - ); - - $this->assertNotWPError( $template ); - $this->assertSame( get_stylesheet() . '//my_template', $template->id ); - $this->assertSame( get_stylesheet(), $template->theme ); - $this->assertSame( 'my_template', $template->slug ); - $this->assertSame( 'publish', $template->status ); - $this->assertSame( 'custom', $template->source ); - $this->assertSame( 'My Template', $template->title ); - $this->assertSame( 'Description of my template', $template->description ); - $this->assertSame( 'wp_template', $template->type ); - $this->assertSame( self::$template_post->post_modified, $template->modified ); - - // Test template parts. - $template_part = _gutenberg_build_block_template_result_from_post( - self::$template_part_post, - 'wp_template_part' - ); - - $this->assertNotWPError( $template_part ); - $this->assertSame( get_stylesheet() . '//my_template_part', $template_part->id ); - $this->assertSame( get_stylesheet(), $template_part->theme ); - $this->assertSame( 'my_template_part', $template_part->slug ); - $this->assertSame( 'publish', $template_part->status ); - $this->assertSame( 'custom', $template_part->source ); - $this->assertSame( 'My Template Part', $template_part->title ); - $this->assertSame( 'Description of my template part', $template_part->description ); - $this->assertSame( 'wp_template_part', $template_part->type ); - $this->assertSame( WP_TEMPLATE_PART_AREA_HEADER, $template_part->area ); - $this->assertSame( self::$template_part_post->post_modified, $template->modified ); - } } From 20e70fdfb240871f9041a2471d732c53f5f10183 Mon Sep 17 00:00:00 2001 From: ramon Date: Tue, 13 Jun 2023 10:59:11 +1000 Subject: [PATCH 6/9] Now adding the value of `modified` to the response using the get_callback method on the register_rest_field filter, which is the way I should have done it in the first place :) --- ...lass-gutenberg-rest-templates-controller-6-3.php | 10 +--------- lib/compat/wordpress-6.3/rest-api.php | 13 +++++++++++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php index aa85b7b180d686..7d59ccc9c71fc1 100644 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php +++ b/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php @@ -69,7 +69,7 @@ public function get_template_fallback( $request ) { /** * Add revisions to the response. * - * @since 6.3.0 Added prepare_revision_links() method to get revision links, and added `modified` property to the response. + * @since 6.3.0 Added prepare_revision_links() method to get revision links. * * @param WP_Block_Template $item Template instance. * @param WP_REST_Request $request Request object. @@ -82,14 +82,6 @@ public function prepare_item_for_response( $item, $request ) { // phpcs:ignore V $response = parent::prepare_item_for_response( $item, $request ); - // Adds modified date to the response. - if ( rest_is_field_included( 'modified', $fields ) && ! empty( $response->data['wp_id'] ) ) { - $post = get_post( $response->data['wp_id'] ); - if ( $post ) { - $response->data['modified'] = $post->post_modified; - } - } - if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { $links = $this->prepare_revision_links( $template ); $response->add_links( $links ); diff --git a/lib/compat/wordpress-6.3/rest-api.php b/lib/compat/wordpress-6.3/rest-api.php index b276273d5a86c7..a3f676531701cd 100644 --- a/lib/compat/wordpress-6.3/rest-api.php +++ b/lib/compat/wordpress-6.3/rest-api.php @@ -80,20 +80,29 @@ function gutenberg_update_global_styles_rest_controller( $args, $post_type ) { /** * Add the `modified` value to the `wp_template` schema. * - * @since 6.3.0 Added 'modified' property. + * @since 6.3.0 Added 'modified' property and response value. */ function add_modified_wp_template_schema() { register_rest_field( array( 'wp_template', 'wp_template_part' ), 'modified', array( - 'schema' => array( + 'schema' => array( 'description' => __( "The date the post was last modified, in the site's timezone.", 'gutenberg' ), 'type' => 'string', 'format' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), + 'get_callback' => function( $object ) { + if ( ! empty( $object['wp_id'] ) ) { + $post = get_post( $object['wp_id'] ); + if ( $post ) { + return $post->post_modified; + } + } + return null; + }, ) ); } From 71186e5be7341ca7ed319cc00d0fd80b5ca6cdaa Mon Sep 17 00:00:00 2001 From: ramon Date: Wed, 14 Jun 2023 08:05:34 +1000 Subject: [PATCH 7/9] Formatting date according post_modified rules found in https://developer.wordpress.org/reference/classes/wp_rest_posts_controller/prepare_item_for_response/ --- lib/compat/wordpress-6.3/rest-api.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/compat/wordpress-6.3/rest-api.php b/lib/compat/wordpress-6.3/rest-api.php index a3f676531701cd..19110d250cbac0 100644 --- a/lib/compat/wordpress-6.3/rest-api.php +++ b/lib/compat/wordpress-6.3/rest-api.php @@ -97,8 +97,8 @@ function add_modified_wp_template_schema() { 'get_callback' => function( $object ) { if ( ! empty( $object['wp_id'] ) ) { $post = get_post( $object['wp_id'] ); - if ( $post ) { - return $post->post_modified; + if ( $post && isset( $post->post_modified ) ) { + return mysql_to_rfc3339( $post->post_modified ); } } return null; From 303f528f6d9d855281cb0a66bc214c28e536dfb7 Mon Sep 17 00:00:00 2001 From: ramon Date: Wed, 14 Jun 2023 08:17:33 +1000 Subject: [PATCH 8/9] Whoops, forgot to wrap date expectations in mysql_to_rfc3339 --- phpunit/class-gutenberg-rest-templates-controller-test.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpunit/class-gutenberg-rest-templates-controller-test.php b/phpunit/class-gutenberg-rest-templates-controller-test.php index 3e96da92e440c0..0992399464c5c8 100644 --- a/phpunit/class-gutenberg-rest-templates-controller-test.php +++ b/phpunit/class-gutenberg-rest-templates-controller-test.php @@ -147,7 +147,7 @@ public function test_get_item() { 'has_theme_file' => false, 'is_custom' => true, 'author' => 0, - 'modified' => self::$post->post_modified, + 'modified' => mysql_to_rfc3339( self::$post->post_modified ), ), $data ); @@ -180,7 +180,7 @@ public function test_get_items() { 'has_theme_file' => false, 'is_custom' => true, 'author' => 0, - 'modified' => self::$post->post_modified, + 'modified' => mysql_to_rfc3339( self::$post->post_modified ), ), $this->find_and_normalize_template_by_id( $data, 'emptytheme//my_template' ) ); From b0414a6ef666ec2a5fdf51ca59773a42250a50b2 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 15 Jun 2023 16:00:15 +1000 Subject: [PATCH 9/9] Update lib/compat/wordpress-6.3/rest-api.php Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com> --- lib/compat/wordpress-6.3/rest-api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.3/rest-api.php b/lib/compat/wordpress-6.3/rest-api.php index 19110d250cbac0..e1d3026316440f 100644 --- a/lib/compat/wordpress-6.3/rest-api.php +++ b/lib/compat/wordpress-6.3/rest-api.php @@ -88,7 +88,7 @@ function add_modified_wp_template_schema() { 'modified', array( 'schema' => array( - 'description' => __( "The date the post was last modified, in the site's timezone.", 'gutenberg' ), + 'description' => __( "The date the template was last modified, in the site's timezone.", 'gutenberg' ), 'type' => 'string', 'format' => 'date-time', 'context' => array( 'view', 'edit' ),