From 748e20335bbfa38b3fee1037442c823d75445ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Fri, 10 May 2024 15:35:25 +0200 Subject: [PATCH] Add plugin template registration API --- lib/block-templates.php | 14 + lib/class-wp-block-templates-registry.php | 246 ++++++++++++++++++ lib/compat/wordpress-6.5/rest-api.php | 4 +- .../wordpress-6.6/block-template-utils.php | 14 + lib/load.php | 2 + .../src/utils/is-template-removable.js | 6 +- .../src/utils/is-template-revertable.js | 3 +- packages/editor/src/store/private-actions.js | 2 +- .../src/store/utils/is-template-revertable.js | 3 +- 9 files changed, 288 insertions(+), 6 deletions(-) create mode 100644 lib/block-templates.php create mode 100644 lib/class-wp-block-templates-registry.php diff --git a/lib/block-templates.php b/lib/block-templates.php new file mode 100644 index 00000000000000..04f53e5aa5e736 --- /dev/null +++ b/lib/block-templates.php @@ -0,0 +1,14 @@ +register( $template_name, 'wp_template', $args ); +} + +function gutenberg_register_block_template_part( $template_name, $args = array() ) { + return WP_Block_Templates_Registry::get_instance()->register( $template_name, 'wp_template_part', $args ); +} diff --git a/lib/class-wp-block-templates-registry.php b/lib/class-wp-block-templates-registry.php new file mode 100644 index 00000000000000..86a0a3d6b42fde --- /dev/null +++ b/lib/class-wp-block-templates-registry.php @@ -0,0 +1,246 @@ + array(), 'wp_template_part' => array() ); + private static $instance = null; + + public function register( $template_name, $template_type, $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 strings.' ), + '6.6.0' + ); + return false; + } + + if ( 'wp_template' !== $template_type && 'wp_template_part' !== $template_type ) { + _doing_it_wrong( + __METHOD__, + __( 'Block templates need to be of `wp_template` or `wp_template_part` type.' ), + '6.6.0' + ); + return false; + } + + if ( preg_match( '/[A-Z]+/', $template_name ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Block template names must not contain uppercase characters.' ), + '6.6.0' + ); + return false; + } + + $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' ), + '6.6.0' + ); + return false; + } + + if ( $this->is_registered( $template_type, $template_name ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Template name. */ + sprintf( __( 'Template "%s" is already registered.' ), $template_name ), + '6.6.0' + ); + return false; + } + + if ( ! $template ) { + $theme_name = get_stylesheet(); + $template = new WP_Block_Template(); + $template->id = $theme_name . '//' . $args['slug']; + $template->theme = $theme_name; // @todo If not attached to the theme, this should be the plugin URI. + $template->plugin = array_key_exists( 'plugin', $args ) ? $args[ 'plugin' ] : ''; + $template->author = null; + $template->content = array_key_exists( 'path', $args ) ? file_get_contents( $args[ 'path' ] ) : ''; + $template->source = 'plugin'; + $template->slug = array_key_exists( 'slug', $args ) ? $args['slug'] : ''; + $template->type = $template_type; + $template->title = array_key_exists( 'title', $args ) ? $args['title'] : ''; + $template->description = array_key_exists( 'description', $args ) ? $args['description'] : ''; + $template->status = 'publish'; + $template->has_theme_file = true; + $template->origin = 'plugin'; + $template->is_custom = false; + $template->post_types = array_key_exists( 'post_types', $args ) ? $args['post_types'] : ''; + $template->area = $template_type === 'wp_template_part' && array_key_exists( 'area', $args ) ? $args['area'] : ''; + } + + $this->registered_block_templates[ $template_type ][ $template_name ] = $template; + + return $template; + } + + public function get_by_slug( $template_type, $template_slug ) { + $all_templates = $this->get_all_registered( $template_type ); + + if ( ! $all_templates ) { + return null; + } + + foreach( $all_templates as $template_name => $template ) { + if ( $template->slug === $template_slug ) { + return $template; + } + } + + return null; + } + + /** + * Retrieves a registered template. + * + * @since 6.6.0 + * + * @param string $template_type Template type, either `wp_template` or `wp_template_part`. + * @param string $template_name Block type name including namespace. + * @return WP_Block_Type|null The registered block type, or null if it is not registered. + */ + public function get_registered( $template_type, $template_name ) { + if ( 'wp_template' !== $template_type && 'wp_template_part' !== $template_type ) { + _doing_it_wrong( + __METHOD__, + __( 'Only valid block template types are `wp_template` and `wp_template_part`.' ), + '6.6.0' + ); + return false; + } + + if ( ! $this->is_registered( $template_type, $template_name ) ) { + return null; + } + + return $this->registered_block_templates[ $template_type ][ $template_name ]; + } + + /** + * Retrieves all registered block templates by type. + * + * @since 6.6.0 + * + * @param string $template_type Template type, either `wp_template` or `wp_template_part`. + * @return WP_Block_Template[] Associative array of `$block_type_name => $block_type` pairs. + */ + public function get_all_registered( $template_type ) { + if ( 'wp_template' !== $template_type && 'wp_template_part' !== $template_type ) { + _doing_it_wrong( + __METHOD__, + __( 'Only valid block template types are `wp_template` and `wp_template_part`.' ), + '6.6.0' + ); + return false; + } + + return $this->registered_block_templates[ $template_type ]; + } + + /** + * Retrieves all registered block templates by type. + * + * @since 6.6.0 + * + * @param string $template_type Template type. Either 'wp_template' or 'wp_template_part'. + * @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 $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. + * } + */ + public function get_by_query( $template_type, $query = array() ) { + $all_templates = $this->get_all_registered( $template_type ); + + if ( ! $all_templates ) { + return array(); + } + + $slugs_to_include = isset( $query['slug__in'] ) ? $query['slug__in'] : array(); + $slugs_to_skip = isset( $query['slug__not_in'] ) ? $query['slug__not_in'] : array(); + $area = isset( $query['area'] ) ? $query['area'] : null; + $post_type = isset( $query['post_type'] ) ? $query['post_type'] : ''; + + foreach( $all_templates as $template_name => $template ) { + if ( ! empty( $slugs_to_include ) && ! in_array( $template->slug, $slugs_to_include ) ) { + unset( $all_templates[ $template_name ] ); + } + + if ( ! empty( $slugs_to_skip ) && in_array( $template->slug, $slugs_to_skip ) ) { + unset( $all_templates[ $template_name ] ); + } + + if ( 'wp_template_part' === $template_type && $template->area !== $area ) { + unset( $all_templates[ $template_name ] ); + } + + if ( ! empty( $post_type ) && ! in_array( $post_type, $template->post_types ) ) { + unset( $all_templates[ $template_name ] ); + } + } + + return $all_templates; + } + + /** + * Checks if a block template is registered. + * + * @since 6.6.0 + * + * @param string $template_type Template type, either `wp_template` or `wp_template_part`. + * @param string $template_name Block type name including namespace. + * @return bool True if the template is registered, false otherwise. + */ + public function is_registered( $template_type, $template_name ) { + if ( 'wp_template' !== $template_type && 'wp_template_part' !== $template_type ) { + _doing_it_wrong( + __METHOD__, + __( 'Only valid block template types are `wp_template` and `wp_template_part`.' ), + '6.6.0' + ); + return false; + } + + return isset( $this->registered_block_templates[ $template_type ][ $template_name ] ); + } + + public function unregister( $template_name ) { + // @todo + } + + /** + * Utility method to retrieve the main instance of the class. + * + * The instance will be created if it does not exist yet. + * + * @since 6.6.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.5/rest-api.php b/lib/compat/wordpress-6.5/rest-api.php index 12d789fb58b869..faf38bd6aaca38 100644 --- a/lib/compat/wordpress-6.5/rest-api.php +++ b/lib/compat/wordpress-6.5/rest-api.php @@ -86,7 +86,9 @@ function _gutenberg_get_wp_templates_author_text_field( $template_object ) { 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' ) ) ]; + // $plugin_name = array_key_exists('plugin', $template_object ) ? $template_object['plugin'] : $template_object['theme']; + $plugin_name = array_key_exists( 'plugin', $template_object ) ? $template_object['plugin'] . '/' . $template_object['plugin'] : 'woocommerce/woocommerce'; + $plugin = $plugins[ plugin_basename( sanitize_text_field( $plugin_name . '.php' ) ) ]; return empty( $plugin['Name'] ) ? $template_object['theme'] : $plugin['Name']; case 'site': return get_bloginfo( 'name' ); diff --git a/lib/compat/wordpress-6.6/block-template-utils.php b/lib/compat/wordpress-6.6/block-template-utils.php index 953f6bf20c077e..78f8a2eb5b2440 100644 --- a/lib/compat/wordpress-6.6/block-template-utils.php +++ b/lib/compat/wordpress-6.6/block-template-utils.php @@ -340,6 +340,20 @@ function gutenberg_get_block_templates( $query = array(), $template_type = 'wp_t foreach ( $template_files as $template_file ) { $query_result[] = _build_block_template_result_from_file( $template_file, $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( $template_type, $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 ); } /** diff --git a/lib/load.php b/lib/load.php index d556dd7f21b435..e75c3ab6b4da0d 100644 --- a/lib/load.php +++ b/lib/load.php @@ -195,7 +195,9 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/class-wp-theme-json-resolver-gutenberg.php'; require __DIR__ . '/class-wp-theme-json-schema-gutenberg.php'; require __DIR__ . '/class-wp-duotone-gutenberg.php'; +require __DIR__ . '/class-wp-block-templates-registry.php'; require __DIR__ . '/blocks.php'; +require __DIR__ . '/block-templates.php'; require __DIR__ . '/block-editor-settings.php'; require __DIR__ . '/client-assets.php'; require __DIR__ . '/demo.php'; diff --git a/packages/edit-site/src/utils/is-template-removable.js b/packages/edit-site/src/utils/is-template-removable.js index 9cb1de23daab75..c363a979fa989f 100644 --- a/packages/edit-site/src/utils/is-template-removable.js +++ b/packages/edit-site/src/utils/is-template-removable.js @@ -7,7 +7,7 @@ import { TEMPLATE_ORIGINS } from './constants'; * Check if a template is removable. * * @param {Object} template The template entity to check. - * @return {boolean} Whether the template is revertable. + * @return {boolean} Whether the template is removable. */ export default function isTemplateRemovable( template ) { if ( ! template ) { @@ -15,6 +15,8 @@ export default function isTemplateRemovable( template ) { } return ( - template.source === TEMPLATE_ORIGINS.custom && ! template.has_theme_file + template.source === TEMPLATE_ORIGINS.custom && + template.origin !== 'plugin' && // @todo check `editor_visiblity` value here. + ! template.has_theme_file ); } diff --git a/packages/edit-site/src/utils/is-template-revertable.js b/packages/edit-site/src/utils/is-template-revertable.js index a6274d07ebebb6..e9b65df0580c41 100644 --- a/packages/edit-site/src/utils/is-template-revertable.js +++ b/packages/edit-site/src/utils/is-template-revertable.js @@ -15,7 +15,8 @@ export default function isTemplateRevertable( template ) { } /* eslint-disable camelcase */ return ( - template?.source === TEMPLATE_ORIGINS.custom && template?.has_theme_file + template?.source === TEMPLATE_ORIGINS.custom && + ( template?.origin === 'plugin' || template?.has_theme_file ) ); /* eslint-enable camelcase */ } diff --git a/packages/editor/src/store/private-actions.js b/packages/editor/src/store/private-actions.js index c3fca0798e8b70..ffe05cce1a99d0 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 efe4647f212801..f366f5c4d7a440 100644 --- a/packages/editor/src/store/utils/is-template-revertable.js +++ b/packages/editor/src/store/utils/is-template-revertable.js @@ -17,7 +17,8 @@ export default function isTemplateRevertable( template ) { } /* eslint-disable camelcase */ return ( - template?.source === TEMPLATE_ORIGINS.custom && template?.has_theme_file + template?.source === TEMPLATE_ORIGINS.custom && + ( template?.origin === 'plugin' || template?.has_theme_file ) ); /* eslint-enable camelcase */ }