diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index fb9076c42e8f4..29007811727f6 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -592,6 +592,15 @@ function _build_block_template_result_from_file( $template_file, $template_type $template->is_custom = true; $template->modified = null; + if ( 'wp_template' === $template_type ) { + $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $template_file['slug'] ); + if ( $registered_template ) { + $template->plugin = $registered_template->plugin; + $template->title = empty( $template->title ) || $template->title === $template->slug ? $registered_template->title : $template->title; + $template->description = empty( $template->description ) ? $registered_template->description : $template->description; + } + } + if ( 'wp_template' === $template_type && isset( $default_template_types[ $template_file['slug'] ] ) ) { $template->description = $default_template_types[ $template_file['slug'] ]['description']; $template->title = $default_template_types[ $template_file['slug'] ]['title']; @@ -1014,6 +1023,19 @@ function _build_block_template_result_from_post( $post ) { } } + if ( 'wp_template' === $post->post_type ) { + $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $template->slug ); + if ( $registered_template ) { + $template->plugin = $registered_template->plugin; + $template->origin = + 'theme' !== $template->origin && 'theme' !== $template->source ? + 'plugin' : + $template->origin; + $template->title = empty( $template->title ) || $template->title === $template->slug ? $registered_template->title : $template->title; + $template->description = empty( $template->description ) ? $registered_template->description : $template->description; + } + } + $hooked_blocks = get_hooked_blocks(); if ( ! empty( $hooked_blocks ) || has_filter( 'hooked_block_types' ) ) { $before_block_visitor = make_before_block_visitor( $hooked_blocks, $template, 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata' ); @@ -1157,6 +1179,23 @@ function get_block_templates( $query = array(), $template_type = 'wp_template' ) foreach ( $template_files as $template_file ) { $query_result[] = _build_block_template_result_from_file( $template_file, $template_type ); } + + if ( 'wp_template' === $template_type ) { + // Add templates registered in the template registry. Filtering out the ones which have a theme file. + $registered_templates = WP_Block_Templates_Registry::get_instance()->get_by_query( $query ); + $matching_registered_templates = array_filter( + $registered_templates, + function ( $registered_template ) use ( $template_files ) { + foreach ( $template_files as $template_file ) { + if ( $template_file['slug'] === $registered_template->slug ) { + return false; + } + } + return true; + } + ); + $query_result = array_merge( $query_result, $matching_registered_templates ); + } } /** @@ -1287,18 +1326,17 @@ function get_block_file_template( $id, $template_type = 'wp_template' ) { } list( $theme, $slug ) = $parts; - if ( get_stylesheet() !== $theme ) { - /** This filter is documented in wp-includes/block-template-utils.php */ - return apply_filters( 'get_block_file_template', null, $id, $template_type ); - } + if ( get_stylesheet() === $theme ) { + $template_file = _get_block_template_file( $template_type, $slug ); + if ( null !== $template_file ) { + $block_template = _build_block_template_result_from_file( $template_file, $template_type ); - $template_file = _get_block_template_file( $template_type, $slug ); - if ( null === $template_file ) { - /** This filter is documented in wp-includes/block-template-utils.php */ - return apply_filters( 'get_block_file_template', null, $id, $template_type ); + /** This filter is documented in wp-includes/block-template-utils.php */ + return apply_filters( 'get_block_file_template', $block_template, $id, $template_type ); + } } - $block_template = _build_block_template_result_from_file( $template_file, $template_type ); + $block_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $slug ); /** * Filters the block template object after it has been (potentially) fetched from the theme file. @@ -1665,12 +1703,12 @@ function inject_ignored_hooked_blocks_metadata_attributes( $changes, $deprecated ); } - $content = get_comment_delimited_block_content( + $content = get_comment_delimited_block_content( 'core/template-part', $attributes, $changes->post_content ); - $content = apply_block_hooks_to_content( $content, $template, 'set_ignored_hooked_blocks_metadata' ); + $content = apply_block_hooks_to_content( $content, $template, 'set_ignored_hooked_blocks_metadata' ); $changes->post_content = remove_serialized_parent_block( $content ); $wrapper_block_markup = extract_serialized_parent_block( $content ); diff --git a/src/wp-includes/block-template.php b/src/wp-includes/block-template.php index 0a4d85ac718a9..d42a74479c4e3 100644 --- a/src/wp-includes/block-template.php +++ b/src/wp-includes/block-template.php @@ -358,3 +358,38 @@ function _resolve_template_for_new_post( $wp_query ) { $wp_query->set( 'post_status', 'auto-draft' ); } } + +/** + * Register a block template. + * + * @since 6.7.0 + * + * @param string $template_name Template name in the form of `plugin_uri//template_name`. + * @param array|string $args { + * @type string $title Optional. Title of the template as it will be shown in the Site Editor + * and other UI elements. + * @type string $description Optional. Description of the template as it will be shown in the Site + * Editor. + * @type string $content Optional. Default content of the template that will be used when the + * template is rendered or edited in the editor. + * @type string[] $post_types Optional. Array of post types to which the template should be available. + * @type string $plugin Optional. Slug of the plugin that registers the template. + * } + * @return WP_Block_Template|WP_Error The registered template object on success, WP_Error object on failure. + */ +function wp_register_block_template( $template_name, $args = array() ) { + return WP_Block_Templates_Registry::get_instance()->register( $template_name, $args ); +} + +/** + * Unregister a block template. + * + * @since 6.7.0 + * + * @param string $template_name Template name in the form of `plugin_uri//template_name`. + * @return WP_Block_Template|WP_Error The unregistered template object on success, WP_Error object on failure or if the + * template doesn't exist. + */ +function wp_unregister_block_template( $template_name ) { + return WP_Block_Templates_Registry::get_instance()->unregister( $template_name ); +} diff --git a/src/wp-includes/class-wp-block-template.php b/src/wp-includes/class-wp-block-template.php index 8149935203220..822302d4c4d85 100644 --- a/src/wp-includes/class-wp-block-template.php +++ b/src/wp-includes/class-wp-block-template.php @@ -131,6 +131,14 @@ class WP_Block_Template { */ public $author; + /** + * Plugin. + * + * @since 6.7.0 + * @var string|null + */ + public $plugin; + /** * Post types. * diff --git a/src/wp-includes/class-wp-block-templates-registry.php b/src/wp-includes/class-wp-block-templates-registry.php new file mode 100644 index 0000000000000..368c517fc9e27 --- /dev/null +++ b/src/wp-includes/class-wp-block-templates-registry.php @@ -0,0 +1,256 @@ + $instance` pairs. + * + * @since 6.7.0 + * @var WP_Block_Template[] $registered_block_templates Registered templates. + */ + private $registered_templates = array(); + + /** + * Container for the main instance of the class. + * + * @since 6.7.0 + * @var WP_Block_Templates_Registry|null + */ + private static $instance = null; + + /** + * Registers a template. + * + * @since 6.7.0 + * + * @param string $template_name Template name including namespace. + * @param array $args Optional. Array of template arguments. + * @return WP_Block_Template|WP_Error The registered template on success, or WP_Error on failure. + */ + public function register( $template_name, $args = array() ) { + + $template = null; + + $error_message = ''; + $error_code = ''; + + if ( ! is_string( $template_name ) ) { + $error_message = __( 'Template names must be strings.' ); + $error_code = 'template_name_no_string'; + } elseif ( preg_match( '/[A-Z]+/', $template_name ) ) { + $error_message = __( 'Template names must not contain uppercase characters.' ); + $error_code = 'template_name_no_uppercase'; + } elseif ( ! preg_match( '/^[a-z0-9-]+\/\/[a-z0-9-]+$/', $template_name ) ) { + $error_message = __( 'Template names must contain a namespace prefix. Example: my-plugin//my-custom-template' ); + $error_code = 'template_no_prefix'; + } elseif ( $this->is_registered( $template_name ) ) { + /* translators: %s: Template name. */ + $error_message = sprintf( __( 'Template "%s" is already registered.' ), $template_name ); + $error_code = 'template_already_registered'; + } + + if ( $error_message ) { + _doing_it_wrong( + __METHOD__, + $error_message, + '6.7.0' + ); + return new WP_Error( $error_code, $error_message ); + } + + if ( ! $template ) { + $theme_name = get_stylesheet(); + list( $plugin, $slug ) = explode( '//', $template_name ); + $default_template_types = get_default_block_template_types(); + + $template = new WP_Block_Template(); + $template->id = $theme_name . '//' . $slug; + $template->theme = $theme_name; + $template->plugin = $plugin; + $template->author = null; + $template->content = isset( $args['content'] ) ? $args['content'] : ''; + $template->source = 'plugin'; + $template->slug = $slug; + $template->type = 'wp_template'; + $template->title = isset( $args['title'] ) ? $args['title'] : $template_name; + $template->description = isset( $args['description'] ) ? $args['description'] : ''; + $template->status = 'publish'; + $template->origin = 'plugin'; + $template->is_custom = ! isset( $default_template_types[ $template_name ] ); + $template->post_types = isset( $args['post_types'] ) ? $args['post_types'] : array(); + } + + $this->registered_templates[ $template_name ] = $template; + + return $template; + } + + /** + * Retrieves all registered templates. + * + * @since 6.7.0 + * + * @return WP_Block_Template[] Associative array of `$template_name => $template` pairs. + */ + public function get_all_registered() { + return $this->registered_templates; + } + + /** + * Retrieves a registered template by its name. + * + * @since 6.7.0 + * + * @param string $template_name Template name including namespace. + * @return WP_Block_Template|null The registered template, or null if it is not registered. + */ + public function get_registered( $template_name ) { + if ( ! $this->is_registered( $template_name ) ) { + return null; + } + + return $this->registered_templates[ $template_name ]; + } + + /** + * Retrieves a registered template by its slug. + * + * @since 6.7.0 + * + * @param string $template_slug Slug of the template. + * @return WP_Block_Template|null The registered template, or null if it is not registered. + */ + public function get_by_slug( $template_slug ) { + $all_templates = $this->get_all_registered(); + + if ( ! $all_templates ) { + return null; + } + + foreach ( $all_templates as $template ) { + if ( $template->slug === $template_slug ) { + return $template; + } + } + + return null; + } + + /** + * Retrieves registered templates matching a query. + * + * @since 6.7.0 + * + * @param array $query { + * Arguments to retrieve templates. Optional, empty by default. + * + * @type string[] $slug__in List of slugs to include. + * @type string[] $slug__not_in List of slugs to skip. + * @type string $post_type Post type to get the templates for. + * } + * @return WP_Block_Template[] Associative array of `$template_name => $template` pairs. + */ + public function get_by_query( $query = array() ) { + $all_templates = $this->get_all_registered(); + + if ( ! $all_templates ) { + return array(); + } + + $query = wp_parse_args( + $query, + array( + 'slug__in' => array(), + 'slug__not_in' => array(), + 'post_type' => '', + ) + ); + $slugs_to_include = $query['slug__in']; + $slugs_to_skip = $query['slug__not_in']; + $post_type = $query['post_type']; + + $matching_templates = array(); + foreach ( $all_templates as $template_name => $template ) { + if ( $slugs_to_include && ! in_array( $template->slug, $slugs_to_include, true ) ) { + continue; + } + + if ( $slugs_to_skip && in_array( $template->slug, $slugs_to_skip, true ) ) { + continue; + } + + if ( $post_type && ! in_array( $post_type, $template->post_types, true ) ) { + continue; + } + + $matching_templates[ $template_name ] = $template; + } + + return $matching_templates; + } + + /** + * Checks if a template is registered. + * + * @since 6.7.0 + * + * @param string $template_name Template name. + * @return bool True if the template is registered, false otherwise. + */ + public function is_registered( $template_name ) { + return isset( $this->registered_templates[ $template_name ] ); + } + + /** + * Unregisters a template. + * + * @since 6.7.0 + * + * @param string $template_name Template name including namespace. + * @return WP_Block_Template|WP_Error The unregistered template on success, or WP_Error on failure. + */ + public function unregister( $template_name ) { + if ( ! $this->is_registered( $template_name ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Template name. */ + sprintf( __( 'Template "%s" is not registered.' ), $template_name ), + '6.7.0' + ); + /* translators: %s: Template name. */ + return new WP_Error( 'template_not_registered', __( 'Template "%s" is not registered.' ) ); + } + + $unregistered_template = $this->registered_templates[ $template_name ]; + unset( $this->registered_templates[ $template_name ] ); + + return $unregistered_template; + } + + /** + * Utility method to retrieve the main instance of the class. + * + * The instance will be created if it does not exist yet. + * + * @since 6.7.0 + * + * @return WP_Block_Templates_Registry The main instance. + */ + public static function get_instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } +} diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php index e2063a450de9c..43780fb4e677b 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php @@ -326,7 +326,7 @@ public function get_item_permissions_check( $request ) { * @return WP_REST_Response|WP_Error */ public function get_item( $request ) { - if ( isset( $request['source'] ) && 'theme' === $request['source'] ) { + if ( isset( $request['source'] ) && ( 'theme' === $request['source'] || 'plugin' === $request['source'] ) ) { $template = get_block_file_template( $request['id'], $this->post_type ); } else { $template = get_block_template( $request['id'], $this->post_type ); @@ -776,6 +776,13 @@ public function prepare_item_for_response( $item, $request ) { $data['original_source'] = self::get_wp_templates_original_source_field( $template ); } + if ( rest_is_field_included( 'plugin', $fields ) ) { + $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $template->slug ); + if ( $registered_template ) { + $data['plugin'] = $registered_template->plugin; + } + } + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); @@ -831,7 +838,7 @@ private static function get_wp_templates_original_source_field( $template_object } // Added by plugin. - if ( $template_object->has_theme_file && 'plugin' === $template_object->origin ) { + if ( 'plugin' === $template_object->origin ) { return 'plugin'; } @@ -865,9 +872,41 @@ private static function get_wp_templates_author_text_field( $template_object ) { $theme_name = wp_get_theme( $template_object->theme )->get( 'Name' ); return empty( $theme_name ) ? $template_object->theme : $theme_name; case 'plugin': - $plugins = get_plugins(); - $plugin = $plugins[ plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) ) ]; - return empty( $plugin['Name'] ) ? $template_object->theme : $plugin['Name']; + if ( ! function_exists( 'get_plugins' ) || ! function_exists( 'get_plugin_data' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + if ( isset( $template_object->plugin ) ) { + $plugins = wp_get_active_and_valid_plugins(); + + foreach ( $plugins as $plugin_file ) { + $plugin_basename = plugin_basename( $plugin_file ); + // Split basename by '/' to get the plugin slug. + list( $plugin_slug, ) = explode( '/', $plugin_basename ); + + if ( $plugin_slug === $template_object->plugin ) { + $plugin_data = get_plugin_data( $plugin_file ); + + if ( ! empty( $plugin_data['Name'] ) ) { + return $plugin_data['Name']; + } + + break; + } + } + } + + /* + * Fall back to the theme name if the plugin is not defined. That's needed to keep backwards + * compatibility with templates that were registered before the plugin attribute was added. + */ + $plugins = get_plugins(); + $plugin_basename = plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) ); + if ( isset( $plugins[ $plugin_basename ] ) && isset( $plugins[ $plugin_basename ]['Name'] ) ) { + return $plugins[ $plugin_basename ]['Name']; + } + return isset( $template_object->plugin ) ? + $template_object->plugin : + $template_object->theme; case 'site': return get_bloginfo( 'name' ); case 'user': @@ -1134,6 +1173,12 @@ public function get_item_schema() { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ); + $schema['properties']['plugin'] = array( + 'type' => 'string', + 'description' => __( 'Plugin that registered the template.' ), + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ); } if ( 'wp_template_part' === $this->post_type ) { diff --git a/src/wp-settings.php b/src/wp-settings.php index d3dfe5776ee88..4643892ada0d6 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -192,6 +192,7 @@ require ABSPATH . WPINC . '/class-wp-duotone.php'; require ABSPATH . WPINC . '/global-styles-and-settings.php'; require ABSPATH . WPINC . '/class-wp-block-template.php'; +require ABSPATH . WPINC . '/class-wp-block-templates-registry.php'; require ABSPATH . WPINC . '/block-template-utils.php'; require ABSPATH . WPINC . '/block-template.php'; require ABSPATH . WPINC . '/theme-templates.php'; diff --git a/tests/phpunit/tests/block-template.php b/tests/phpunit/tests/block-template.php index 7b91b3f2ea185..9ec506a414a46 100644 --- a/tests/phpunit/tests/block-template.php +++ b/tests/phpunit/tests/block-template.php @@ -433,6 +433,47 @@ public function test_get_block_templates_paths_dir_doesnt_exists() { $this->assertSame( array(), $template_paths ); } + /** + * Tests that get_block_templates() returns plugin-registered templates. + * + * @ticket 61804 + * + * @covers ::get_block_templates + */ + public function test_get_block_templates_from_registry() { + $template_name = 'test-plugin//test-template'; + + wp_register_block_template( $template_name ); + + $templates = get_block_templates(); + + $this->assertArrayHasKey( $template_name, $templates ); + + wp_unregister_block_template( $template_name ); + } + + /** + * Tests that get_block_template() returns plugin-registered templates. + * + * @ticket 61804 + * + * @covers ::get_block_template + */ + public function test_get_block_template_from_registry() { + $template_name = 'test-plugin//test-template'; + $args = array( + 'title' => 'Test Template', + ); + + wp_register_block_template( $template_name, $args ); + + $template = get_block_template( 'block-theme//test-template' ); + + $this->assertSame( 'Test Template', $template->title ); + + wp_unregister_block_template( $template_name ); + } + /** * Registers a test block to log `in_the_loop()` results. * diff --git a/tests/phpunit/tests/block-templates/WpBlockTemplatesRegistry.php b/tests/phpunit/tests/block-templates/WpBlockTemplatesRegistry.php new file mode 100644 index 0000000000000..cc44d265e9dc3 --- /dev/null +++ b/tests/phpunit/tests/block-templates/WpBlockTemplatesRegistry.php @@ -0,0 +1,270 @@ +register( $template_name ); + + $this->assertSame( $template->slug, 'test-template' ); + + self::$registry->unregister( $template_name ); + } + + /** + * Tests that register() returns an error if template name is not a string. + * + * @ticket 61804 + * + * @covers ::register + */ + public function test_register_template_invalid_name() { + // Try to register a template with invalid name (non-string). + $template_name = array( 'invalid-template-name' ); + + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( $template_name ); + + $this->assertWPError( $result, 'Template registration is expected to trigger an error.' ); + $this->assertSame( 'template_name_no_string', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertSame( 'Template names must be strings.', $result->get_error_message(), 'Error message mismatch.' ); + } + + /** + * Tests that register() returns an error if template name contains + * uppercase characters. + * + * @ticket 61804 + * + * @covers ::register + */ + public function test_register_template_invalid_name_uppercase() { + // Try to register a template with uppercase characters in the name. + $template_name = 'test-plugin//Invalid-Template-Name'; + + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( $template_name ); + + $this->assertWPError( $result, 'Template registration is expected to trigger an error.' ); + $this->assertSame( 'template_name_no_uppercase', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertSame( 'Template names must not contain uppercase characters.', $result->get_error_message(), 'Error message mismatch.' ); + } + + /** + * Tests that register() returns an error if template name has no prefix. + * + * @ticket 61804 + * + * @covers ::register + */ + public function test_register_template_no_prefix() { + // Try to register a template without a namespace. + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( 'template-no-plugin', array() ); + + $this->assertWPError( $result, 'Template registration is expected to trigger an error.' ); + $this->assertSame( 'template_no_prefix', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertSame( 'Template names must contain a namespace prefix. Example: my-plugin//my-custom-template', $result->get_error_message(), 'Error message mismatch.' ); + } + + /** + * Tests that register() returns an error if template already exists. + * + * @ticket 61804 + * + * @covers ::register + */ + public function test_register_template_already_exists() { + // Register the template for the first time. + $template_name = 'test-plugin//duplicate-template'; + self::$registry->register( $template_name ); + + // Try to register the same template again. + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( $template_name ); + + $this->assertWPError( $result, 'Template registration is expected to trigger an error.' ); + $this->assertSame( 'template_already_registered', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertStringContainsString( 'Template "test-plugin//duplicate-template" is already registered.', $result->get_error_message(), 'Error message mismatch.' ); + + self::$registry->unregister( $template_name ); + } + + /** + * Tests that get_all_registered() returns all registered templates. + * + * @ticket 61804 + * + * @covers ::get_all_registered + */ + public function test_get_all_registered() { + $template_name_1 = 'test-plugin//template-1'; + $template_name_2 = 'test-plugin//template-2'; + self::$registry->register( $template_name_1 ); + self::$registry->register( $template_name_2 ); + + $all_templates = self::$registry->get_all_registered(); + + $this->assertIsArray( $all_templates, 'Registered templates should be an array.' ); + $this->assertCount( 2, $all_templates, 'Registered templates should contain 2 items.' ); + $this->assertArrayHasKey( 'test-plugin//template-1', $all_templates, 'Registered templates should contain "test-plugin//template-1".' ); + $this->assertArrayHasKey( 'test-plugin//template-2', $all_templates, 'Registered templates should contain "test-plugin//template-2".' ); + + self::$registry->unregister( $template_name_1 ); + self::$registry->unregister( $template_name_2 ); + } + + /** + * Tests that get_registered() returns the correct registered template. + * + * @ticket 61804 + * + * @covers ::get_registered + */ + public function test_get_registered() { + $template_name = 'test-plugin//registered-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Registered Template', + 'description' => 'Description of registered template', + 'post_types' => array( 'post', 'page' ), + ); + self::$registry->register( $template_name, $args ); + + $registered_template = self::$registry->get_registered( $template_name ); + + $this->assertSame( 'default', $registered_template->theme, 'Template theme mismatch.' ); + $this->assertSame( 'registered-template', $registered_template->slug, 'Template slug mismatch.' ); + $this->assertSame( 'default//registered-template', $registered_template->id, 'Template ID mismatch.' ); + $this->assertSame( 'Registered Template', $registered_template->title, 'Template title mismatch.' ); + $this->assertSame( 'Template content', $registered_template->content, 'Template content mismatch.' ); + $this->assertSame( 'Description of registered template', $registered_template->description, 'Template description mismatch.' ); + $this->assertSame( 'plugin', $registered_template->source, "Template source should be 'plugin'." ); + $this->assertSame( 'plugin', $registered_template->origin, "Template origin should be 'plugin'." ); + $this->assertSameSets( array( 'post', 'page' ), $registered_template->post_types, 'Template post types mismatch.' ); + $this->assertSame( 'test-plugin', $registered_template->plugin, 'Plugin name mismatch.' ); + + self::$registry->unregister( $template_name ); + } + + /** + * Tests that get_by_slug() returns the correct template by slug. + * + * @ticket 61804 + * + * @covers ::get_by_slug + */ + public function test_get_by_slug() { + $slug = 'slug-template'; + $template_name = 'test-plugin//' . $slug; + $args = array( + 'content' => 'Template content', + 'title' => 'Slug Template', + ); + self::$registry->register( $template_name, $args ); + + $registered_template = self::$registry->get_by_slug( $slug ); + + $this->assertNotNull( $registered_template, 'Registered template should not be null.' ); + $this->assertSame( $slug, $registered_template->slug, 'Template slug mismatch.' ); + + self::$registry->unregister( $template_name ); + } + + /** + * Tests that get_by_query() returns the correct templates based on the query. + * + * @ticket 61804 + * + * @covers ::get_by_query + */ + public function test_get_by_query() { + $template_name_1 = 'test-plugin//query-template-1'; + $template_name_2 = 'test-plugin//query-template-2'; + $args_1 = array( + 'content' => 'Template content 1', + 'title' => 'Query Template 1', + ); + $args_2 = array( + 'content' => 'Template content 2', + 'title' => 'Query Template 2', + ); + self::$registry->register( $template_name_1, $args_1 ); + self::$registry->register( $template_name_2, $args_2 ); + + $query = array( + 'slug__in' => array( 'query-template-1' ), + ); + $results = self::$registry->get_by_query( $query ); + + $this->assertCount( 1, $results, 'Query result should contain 1 item.' ); + $this->assertArrayHasKey( $template_name_1, $results, 'Query result should contain "test-plugin//query-template-1".' ); + + self::$registry->unregister( $template_name_1 ); + self::$registry->unregister( $template_name_2 ); + } + + /** + * Tests that is_registered() correctly identifies registered templates. + * + * @ticket 61804 + * + * @covers ::is_registered + */ + public function test_is_registered() { + $template_name = 'test-plugin//is-registered-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Is Registered Template', + ); + self::$registry->register( $template_name, $args ); + + $this->assertTrue( self::$registry->is_registered( $template_name ) ); + + self::$registry->unregister( $template_name ); + } + + /** + * Tests that unregister() correctly unregisters a registered template. + * + * @ticket 61804 + * + * @covers ::unregister + */ + public function test_unregister() { + $template_name = 'test-plugin//unregister-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Unregister Template', + ); + $template = self::$registry->register( $template_name, $args ); + + $unregistered_template = self::$registry->unregister( $template_name ); + + $this->assertEquals( $template, $unregistered_template, 'Unregistered template should be the same as the registered one.' ); + $this->assertFalse( self::$registry->is_registered( $template_name ), 'Template should not be registered after unregistering.' ); + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php b/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php index 430bc87cc21f8..550eb8e5fe999 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php @@ -310,7 +310,7 @@ public function test_get_item_schema() { $properties = $data['schema']['properties']; - $this->assertCount( 18, $properties ); + $this->assertCount( 19, $properties ); $this->assertArrayHasKey( 'id', $properties, 'ID key should exist in properties.' ); $this->assertArrayHasKey( 'slug', $properties, 'Slug key should exist in properties.' ); $this->assertArrayHasKey( 'theme', $properties, 'Theme key should exist in properties.' ); @@ -328,6 +328,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'parent', $properties, 'Parent key should exist in properties.' ); $this->assertArrayHasKey( 'author_text', $properties, 'author_text key should exist in properties.' ); $this->assertArrayHasKey( 'original_source', $properties, 'original_source key should exist in properties.' ); + $this->assertArrayHasKey( 'plugin', $properties, 'plugin key should exist in properties.' ); } /** diff --git a/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php b/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php index 9362dd42a72f2..014a191f0a13f 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php @@ -449,7 +449,7 @@ public function test_get_item_schema() { $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 18, $properties ); + $this->assertCount( 19, $properties ); $this->assertArrayHasKey( 'id', $properties, 'ID key should exist in properties.' ); $this->assertArrayHasKey( 'slug', $properties, 'Slug key should exist in properties.' ); $this->assertArrayHasKey( 'theme', $properties, 'Theme key should exist in properties.' ); @@ -467,6 +467,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'parent', $properties, 'Parent key should exist in properties.' ); $this->assertArrayHasKey( 'author_text', $properties, 'author_text key should exist in properties.' ); $this->assertArrayHasKey( 'original_source', $properties, 'original_source key should exist in properties.' ); + $this->assertArrayHasKey( 'plugin', $properties, 'plugin key should exist in properties.' ); } /** diff --git a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php index d06295a1bbcc6..db0183aa78986 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php @@ -528,6 +528,53 @@ public function data_get_item_with_valid_theme_dirname() { ); } + /** + * Tests that get_item() returns plugin-registered templates. + * + * @ticket 61804 + * + * @covers WP_REST_Templates_Controller::get_item + */ + public function test_get_item_from_registry() { + wp_set_current_user( self::$admin_id ); + + $template_name = 'test-plugin//test-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Test Template', + 'description' => 'Description of test template', + 'post_types' => array( 'post', 'page' ), + ); + + wp_register_block_template( $template_name, $args ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/test-plugin//test-template' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertNotWPError( $response, "Fetching a registered template shouldn't cause an error." ); + + $data = $response->get_data(); + + $this->assertSame( 'default//test-template', $data['id'], 'Template ID mismatch.' ); + $this->assertSame( 'default', $data['theme'], 'Template theme mismatch.' ); + $this->assertSame( 'Template content', $data['content']['raw'], 'Template content mismatch.' ); + $this->assertSame( 'test-template', $data['slug'], 'Template slug mismatch.' ); + $this->assertSame( 'plugin', $data['source'], "Template source should be 'plugin'." ); + $this->assertSame( 'plugin', $data['origin'], "Template origin should be 'plugin'." ); + $this->assertSame( 'test-plugin', $data['author_text'], 'Template author text mismatch.' ); + $this->assertSame( 'Description of test template', $data['description'], 'Template description mismatch.' ); + $this->assertSame( 'Test Template', $data['title']['rendered'], 'Template title mismatch.' ); + $this->assertSame( 'test-plugin', $data['plugin'], 'Plugin name mismatch.' ); + + wp_unregister_block_template( $template_name ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/test-plugin//test-template' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertNotWPError( $response, "Fetching an unregistered template shouldn't cause an error." ); + $this->assertSame( 404, $response->get_status(), 'Fetching an unregistered template should return 404.' ); + } + /** * @ticket 54507 * @dataProvider data_sanitize_template_id @@ -863,7 +910,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 17, $properties ); + $this->assertCount( 18, $properties ); $this->assertArrayHasKey( 'id', $properties ); $this->assertArrayHasKey( 'description', $properties ); $this->assertArrayHasKey( 'slug', $properties ); @@ -882,6 +929,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'modified', $properties ); $this->assertArrayHasKey( 'author_text', $properties ); $this->assertArrayHasKey( 'original_source', $properties ); + $this->assertArrayHasKey( 'plugin', $properties ); } protected function find_and_normalize_template_by_id( $templates, $id ) {