diff --git a/lib/block-templates.php b/lib/block-templates.php new file mode 100644 index 0000000000000..c866d4b21323b --- /dev/null +++ b/lib/block-templates.php @@ -0,0 +1,10 @@ +register( $template_name, $args ); +} diff --git a/lib/class-wp-block-templates-registry.php b/lib/class-wp-block-templates-registry.php new file mode 100644 index 0000000000000..2ba5d43a2d0e1 --- /dev/null +++ b/lib/class-wp-block-templates-registry.php @@ -0,0 +1,272 @@ + $instance` pairs. + * + * @since 6.7.0 + * @var WP_Block_Template[] $registered_block_templates Registered block templates. + */ + private $registered_block_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 block template. + * + * @since 6.7.0 + * + * @param string|WP_Block_Template $template_name Block template name including namespace, or alternatively + * a complete WP_Block_Template instance. In case a WP_Block_Template + * is provided, the $args parameter will be ignored. + * @param array $args Optional. Array of block template arguments. + * @return WP_Block_Template|false The registered block template on success, or false on failure. + */ + public function register( $template_name, $args = array() ) { + + $template = null; + if ( $template_name instanceof WP_Block_Template ) { + $template = $template_name; + $template_name = $template->name; + } + + if ( ! is_string( $template_name ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Block template names must be a string.', 'gutenberg' ), + '6.7.0' + ); + return new WP_Error( 'template_name_no_string', __( 'Block template names must be a string.', 'gutenberg' ) ); + } + + if ( preg_match( '/[A-Z]+/', $template_name ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Block template names must not contain uppercase characters.', 'gutenberg' ), + '6.7.0' + ); + return new WP_Error( 'template_name_no_uppercase', __( 'Block template names must not contain uppercase characters.', 'gutenberg' ) ); + } + + $name_matcher = '/^[a-z0-9-]+\/\/[a-z0-9-]+$/'; + if ( ! preg_match( $name_matcher, $template_name ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Block template names must contain a namespace prefix. Example: my-plugin//my-custom-template', 'gutenberg' ), + '6.7.0' + ); + return new WP_Error( 'template_no_prefix', __( 'Block template names must contain a namespace prefix. Example: my-plugin//my-custom-template', 'gutenberg' ) ); + } + + if ( $this->is_registered( $template_name ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Template name. */ + sprintf( __( 'Template "%s" is already registered.', 'gutenberg' ), $template_name ), + '6.7.0' + ); + /* translators: %s: Template name. */ + return new WP_Error( 'template_already_registered', __( 'Template "%s" is already registered.', 'gutenberg' ) ); + } + + if ( ! $template ) { + $theme_name = get_stylesheet(); + $slug = isset( $args['slug'] ) ? $args['slug'] : explode( '//', $template_name )[1]; + + $template = new WP_Block_Template(); + $template->id = $theme_name . '//' . $slug; + $template->theme = $theme_name; + $template->plugin = isset( $args['plugin'] ) ? $args['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->description = isset( $args['description'] ) ? $args['description'] : ''; + $template->status = 'publish'; + $template->has_theme_file = true; + $template->origin = 'plugin'; + $template->is_custom = true; + $template->post_types = isset( $args['post_types'] ) ? $args['post_types'] : ''; + } + + $this->registered_block_templates[ $template_name ] = $template; + + return $template; + } + + /** + * Retrieves all registered block templates. + * + * @since 6.7.0 + * + * @return WP_Block_Template[]|false Associative array of `$block_template_name => $block_template` pairs. + */ + public function get_all_registered() { + return $this->registered_block_templates; + } + + /** + * Retrieves a registered template by its and name. + * + * @since 6.7.0 + * + * @param string $template_name Block template name including namespace. + * @return WP_Block_Template|null|false The registered block 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_block_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 block 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 block 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. + * } + */ + 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']; + + foreach ( $all_templates as $template_name => $template ) { + if ( ! empty( $slugs_to_include ) && ! in_array( $template->slug, $slugs_to_include, true ) ) { + unset( $all_templates[ $template_name ] ); + } + + if ( ! empty( $slugs_to_skip ) && in_array( $template->slug, $slugs_to_skip, true ) ) { + unset( $all_templates[ $template_name ] ); + } + + if ( ! empty( $post_type ) && ! in_array( $post_type, $template->post_types, true ) ) { + unset( $all_templates[ $template_name ] ); + } + } + + return $all_templates; + } + + /** + * Checks if a block template is registered. + * + * @since 6.7.0 + * + * @param string $template_name Block template name including namespace. + * @return bool True if the template is registered, false otherwise. + */ + public function is_registered( $template_name ) { + return isset( $this->registered_block_templates[ $template_name ] ); + } + + /** + * Unregisters a block template. + * + * @since 6.7.0 + * + * @param string $name Block template name including namespace. + * @return WP_Block_Template|false The unregistered block template on success, or false 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.', 'gutenberg' ), $template_name ), + '6.7.0' + ); + /* translators: %s: Template name. */ + return new WP_Error( 'template_not_registered', __( 'Template "%s" is not registered.', 'gutenberg' ) ); + } + + $unregistered_block_template = $this->registered_block_templates[ $template_name ]; + unset( $this->registered_block_templates[ $template_name ] ); + + return $unregistered_block_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/lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php b/lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php new file mode 100644 index 0000000000000..37de68f5f8240 --- /dev/null +++ b/lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php @@ -0,0 +1,188 @@ +post_type ); + // @core-merge: Add a special case for plugin templates. + } elseif ( isset( $request['source'] ) && 'plugin' === $request['source'] ) { + list( , $slug ) = explode( '//', $request['id'] ); + $template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $slug ); + // @core-merge: End of changes to merge in core. + } else { + $template = get_block_template( $request['id'], $this->post_type ); + } + + if ( ! $template ) { + return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) ); + } + + return $this->prepare_item_for_response( $template, $request ); + } + + /** + * Prepare a single template output for response + * + * @param WP_Block_Template $item Template instance. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. + */ + public function prepare_item_for_response( $item, $request ) { + $template = $item; + + $fields = $this->get_fields_for_response( $request ); + + if ( 'plugin' !== $item->origin ) { + return parent::prepare_item_for_response( $item, $request ); + } + // @core-merge: Fix wrong author in plugin templates. + $cloned_item = clone $item; + // Set the origin as theme when calling the previous `prepare_item_for_response()` to prevent warnings when generating the author text. + $cloned_item->origin = 'theme'; + $response = parent::prepare_item_for_response( $cloned_item, $request ); + $data = $response->data; + + if ( rest_is_field_included( 'origin', $fields ) ) { + $data['origin'] = 'plugin'; + } + + if ( rest_is_field_included( 'author_text', $fields ) ) { + $data['author_text'] = $this->get_wp_templates_author_text_field( $template ); + } + + if ( rest_is_field_included( 'original_source', $fields ) ) { + $data['original_source'] = $this->get_wp_templates_original_source_field( $template ); + } + + $response = rest_ensure_response( $data ); + + if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { + $links = $this->prepare_links( $template->id ); + $response->add_links( $links ); + if ( ! empty( $links['self']['href'] ) ) { + $actions = $this->get_available_actions(); + $self = $links['self']['href']; + foreach ( $actions as $rel ) { + $response->add_link( $rel, $self ); + } + } + } + + return $response; + } + + /** + * Returns the source from where the template originally comes from. + * + * @param WP_Block_Template $template_object Template instance. + * @return string Original source of the template one of theme, plugin, site, or user. + */ + // @core-merge: Nothing has changed in this function, the only reason to include it here is that it's a private function. + private static function get_wp_templates_original_source_field( $template_object ) { + if ( 'wp_template' === $template_object->type || 'wp_template_part' === $template_object->type ) { + // Added by theme. + // Template originally provided by a theme, but customized by a user. + // Templates originally didn't have the 'origin' field so identify + // older customized templates by checking for no origin and a 'theme' + // or 'custom' source. + if ( $template_object->has_theme_file && + ( 'theme' === $template_object->origin || ( + empty( $template_object->origin ) && in_array( + $template_object->source, + array( + 'theme', + 'custom', + ), + true + ) ) + ) + ) { + return 'theme'; + } + + // Added by plugin. + if ( $template_object->has_theme_file && 'plugin' === $template_object->origin ) { + return 'plugin'; + } + + // Added by site. + // Template was created from scratch, but has no author. Author support + // was only added to templates in WordPress 5.9. Fallback to showing the + // site logo and title. + if ( empty( $template_object->has_theme_file ) && 'custom' === $template_object->source && empty( $template_object->author ) ) { + return 'site'; + } + } + + // Added by user. + return 'user'; + } + + /** + * Returns a human readable text for the author of the template. + * + * @param WP_Block_Template $template_object Template instance. + * @return string Human readable text for the author. + */ + private static function get_wp_templates_author_text_field( $template_object ) { + $original_source = self::get_wp_templates_original_source_field( $template_object ); + switch ( $original_source ) { + case 'theme': + $theme_name = wp_get_theme( $template_object->theme )->get( 'Name' ); + return empty( $theme_name ) ? $template_object->theme : $theme_name; + case 'plugin': + // @core-merge: Prioritize plugin name instead of theme name for plugin-registered templates. + 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; + } + } + } + // @core-merge: End of changes to merge in core. + + /* + * Fallback 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 = $plugins[ plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) ) ]; + return empty( $plugin['Name'] ) ? $template_object->theme : $plugin['Name']; + case 'site': + return get_bloginfo( 'name' ); + case 'user': + $author = get_user_by( 'id', $template_object->author ); + if ( ! $author ) { + return __( 'Unknown author' ); + } + return $author->get( 'display_name' ); + } + } +} diff --git a/lib/compat/wordpress-6.7/compat.php b/lib/compat/wordpress-6.7/compat.php new file mode 100644 index 0000000000000..4d8f8074a2bef --- /dev/null +++ b/lib/compat/wordpress-6.7/compat.php @@ -0,0 +1,76 @@ +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 ); + } + + return $query_result; +} +add_filter( 'get_block_templates', '_gutenberg_add_block_templates_from_registry', 10, 3 ); + +/** + * Hooks into `get_block_file_template` so templates from the registry are also returned. + * + * @param WP_Block_Template|null $block_template The found block template, or null if there is none. + * @param string $id Template unique identifier (example: 'theme_slug//template_slug'). + * @return WP_Block_Template|null The block template that was already found or from the registry. In case the template was already found, add the necessary details from the registry. + */ +function _gutenberg_add_block_file_templates_from_registry( $block_template, $id ) { + if ( $block_template ) { + $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $block_template->slug ); + if ( $registered_template ) { + $block_template->plugin = $registered_template->plugin; + } + return $block_template; + } + + $parts = explode( '//', $id, 2 ); + + if ( count( $parts ) < 2 ) { + return $block_template; + } + + list( , $slug ) = $parts; + return WP_Block_Templates_Registry::get_instance()->get_by_slug( $slug ); +} +add_filter( 'get_block_file_template', '_gutenberg_add_block_file_templates_from_registry', 10, 2 ); diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php new file mode 100644 index 0000000000000..f8c7b079700d6 --- /dev/null +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -0,0 +1,31 @@ + = { return ( isTemplateOrTemplatePart( item ) && item?.source === TEMPLATE_ORIGINS.custom && - item?.has_theme_file + ( item?.origin === TEMPLATE_ORIGINS.plugin || item?.has_theme_file ) ); }, icon: backup, diff --git a/packages/editor/src/dataviews/actions/utils.ts b/packages/editor/src/dataviews/actions/utils.ts index efbb7c590e651..fae127400f2d0 100644 --- a/packages/editor/src/dataviews/actions/utils.ts +++ b/packages/editor/src/dataviews/actions/utils.ts @@ -43,6 +43,8 @@ export function isTemplateRemovable( template: TemplateOrTemplatePart ) { return ( [ template.source, template.source ].includes( TEMPLATE_ORIGINS.custom - ) && ! template.has_theme_file + ) && + template?.origin !== TEMPLATE_ORIGINS.plugin && + ! template.has_theme_file ); } diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index 29f4358456324..fe8302fa0326a 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -16,6 +16,7 @@ export interface BasePost { export interface TemplateOrTemplatePart extends BasePost { type: 'wp_template' | 'wp_template_part'; source: string; + origin: string; has_theme_file: boolean; id: string; } diff --git a/packages/editor/src/store/private-actions.js b/packages/editor/src/store/private-actions.js index c5fe1c260071a..b261bc7eb1cb6 100644 --- a/packages/editor/src/store/private-actions.js +++ b/packages/editor/src/store/private-actions.js @@ -269,7 +269,7 @@ export const revertTemplate = const fileTemplatePath = addQueryArgs( `${ templateEntityConfig.baseURL }/${ template.id }`, - { context: 'edit', source: 'theme' } + { context: 'edit', source: template.origin } ); const fileTemplate = await apiFetch( { path: fileTemplatePath } ); diff --git a/packages/editor/src/store/utils/is-template-revertable.js b/packages/editor/src/store/utils/is-template-revertable.js index a09715af875bc..ff205ca97c46d 100644 --- a/packages/editor/src/store/utils/is-template-revertable.js +++ b/packages/editor/src/store/utils/is-template-revertable.js @@ -18,6 +18,7 @@ export default function isTemplateRevertable( templateOrTemplatePart ) { return ( templateOrTemplatePart.source === TEMPLATE_ORIGINS.custom && - templateOrTemplatePart.has_theme_file + ( templateOrTemplatePart?.origin === TEMPLATE_ORIGINS.plugin || + templateOrTemplatePart?.has_theme_file ) ); }