diff --git a/src/wp-includes/class-wp-block-metadata-registry.php b/src/wp-includes/class-wp-block-metadata-registry.php index 17bbf320d5..4b567d7d6f 100644 --- a/src/wp-includes/class-wp-block-metadata-registry.php +++ b/src/wp-includes/class-wp-block-metadata-registry.php @@ -38,20 +38,12 @@ class WP_Block_Metadata_Registry { private static $last_matched_collection = null; /** - * Stores the WordPress 'wp-includes' directory path. + * Stores the default allowed collection root paths. * - * @since 6.7.0 - * @var string|null + * @since 6.7.2 + * @var string[]|null */ - private static $wpinc_dir = null; - - /** - * Stores the normalized WordPress plugin directory path. - * - * @since 6.7.0 - * @var string|null - */ - private static $plugin_dir = null; + private static $default_collection_roots = null; /** * Registers a block metadata collection. @@ -92,29 +84,50 @@ class WP_Block_Metadata_Registry { public static function register_collection( $path, $manifest ) { $path = wp_normalize_path( rtrim( $path, '/' ) ); - $wpinc_dir = self::get_wpinc_dir(); - $plugin_dir = self::get_plugin_dir(); + $collection_roots = self::get_default_collection_roots(); + + /** + * Filters the root directory paths for block metadata collections. + * + * Any block metadata collection that is registered must not use any of these paths, or any parent directory + * path of them. Most commonly, block metadata collections should reside within one of these paths, though in + * some scenarios they may also reside in entirely different directories (e.g. in case of symlinked plugins). + * + * Example: + * * It is allowed to register a collection with path `WP_PLUGIN_DIR . '/my-plugin'`. + * * It is not allowed to register a collection with path `WP_PLUGIN_DIR`. + * * It is not allowed to register a collection with path `dirname( WP_PLUGIN_DIR )`. + * + * The default list encompasses the `wp-includes` directory, as well as the root directories for plugins, + * must-use plugins, and themes. This filter can be used to expand the list, e.g. to custom directories that + * contain symlinked plugins, so that these root directories cannot be used themselves for a block metadata + * collection either. + * + * @since 6.7.2 + * + * @param string[] $collection_roots List of allowed metadata collection root paths. + */ + $collection_roots = apply_filters( 'wp_allowed_block_metadata_collection_roots', $collection_roots ); + + $collection_roots = array_unique( + array_map( + static function ( $allowed_root ) { + return rtrim( $allowed_root, '/' ); + }, + $collection_roots + ) + ); // Check if the path is valid: - if ( str_starts_with( $path, $plugin_dir ) ) { - // For plugins, ensure the path is within a specific plugin directory and not the base plugin directory. - $relative_path = substr( $path, strlen( $plugin_dir ) + 1 ); - $plugin_name = strtok( $relative_path, '/' ); - - if ( empty( $plugin_name ) || $plugin_name === $relative_path ) { - _doing_it_wrong( - __METHOD__, - __( 'Block metadata collections can only be registered for a specific plugin. The provided path is neither a core path nor a valid plugin path.' ), - '6.7.0' - ); - return false; - } - } elseif ( ! str_starts_with( $path, $wpinc_dir ) ) { - // If it's neither a plugin directory path nor within 'wp-includes', the path is invalid. + if ( ! self::is_valid_collection_path( $path, $collection_roots ) ) { _doing_it_wrong( __METHOD__, - __( 'Block metadata collections can only be registered for a specific plugin. The provided path is neither a core path nor a valid plugin path.' ), - '6.7.0' + sprintf( + /* translators: %s: list of allowed collection roots */ + __( 'Block metadata collections cannot be registered as one of the following directories or their parent directories: %s' ), + esc_html( implode( wp_get_list_item_separator(), $collection_roots ) ) + ), + '6.7.2' ); return false; } @@ -244,30 +257,58 @@ class WP_Block_Metadata_Registry { } /** - * Gets the WordPress 'wp-includes' directory path. + * Checks whether the given block metadata collection path is valid against the list of collection roots. * - * @since 6.7.0 + * @since 6.7.2 * - * @return string The WordPress 'wp-includes' directory path. + * @param string $path Block metadata collection path, without trailing slash. + * @param string[] $collection_roots List of collection root paths, without trailing slashes. + * @return bool True if the path is allowed, false otherwise. */ - private static function get_wpinc_dir() { - if ( ! isset( self::$wpinc_dir ) ) { - self::$wpinc_dir = wp_normalize_path( ABSPATH . WPINC ); + private static function is_valid_collection_path( $path, $collection_roots ) { + foreach ( $collection_roots as $allowed_root ) { + // If the path matches any root exactly, it is invalid. + if ( $allowed_root === $path ) { + return false; + } + + // If the path is a parent path of any of the roots, it is invalid. + if ( str_starts_with( $allowed_root, $path ) ) { + return false; + } } - return self::$wpinc_dir; + + return true; } /** - * Gets the normalized WordPress plugin directory path. + * Gets the default collection root directory paths. * - * @since 6.7.0 + * @since 6.7.2 * - * @return string The normalized WordPress plugin directory path. + * @return string[] List of directory paths within which metadata collections are allowed. */ - private static function get_plugin_dir() { - if ( ! isset( self::$plugin_dir ) ) { - self::$plugin_dir = wp_normalize_path( WP_PLUGIN_DIR ); + private static function get_default_collection_roots() { + if ( isset( self::$default_collection_roots ) ) { + return self::$default_collection_roots; } - return self::$plugin_dir; + + $collection_roots = array( + wp_normalize_path( ABSPATH . WPINC ), + wp_normalize_path( WP_CONTENT_DIR ), + wp_normalize_path( WPMU_PLUGIN_DIR ), + wp_normalize_path( WP_PLUGIN_DIR ), + ); + + $theme_roots = get_theme_roots(); + if ( ! is_array( $theme_roots ) ) { + $theme_roots = array( $theme_roots ); + } + foreach ( $theme_roots as $theme_root ) { + $collection_roots[] = trailingslashit( wp_normalize_path( WP_CONTENT_DIR ) ) . ltrim( wp_normalize_path( $theme_root ), '/' ); + } + + self::$default_collection_roots = array_unique( $collection_roots ); + return self::$default_collection_roots; } } diff --git a/tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php b/tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php index 3f0ec006b0..bc62131c6a 100644 --- a/tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php +++ b/tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php @@ -80,12 +80,112 @@ class Tests_Blocks_WpBlockMetadataRegistry extends WP_UnitTestCase { $this->assertFalse( $result, 'Invalid plugin path should not be registered' ); } - public function test_register_collection_with_non_existent_path() { - $non_existent_path = '/path/that/does/not/exist'; + /** + * @ticket 62140 + */ + public function test_register_collection_with_valid_muplugin_path() { + $plugin_path = WPMU_PLUGIN_DIR . '/my-plugin/blocks'; + $result = WP_Block_Metadata_Registry::register_collection( $plugin_path, $this->temp_manifest_file ); + $this->assertTrue( $result, 'Valid must-use plugin path should be registered successfully' ); + } + + /** + * @ticket 62140 + */ + public function test_register_collection_with_invalid_muplugin_path() { + $invalid_plugin_path = WPMU_PLUGIN_DIR; $this->setExpectedIncorrectUsage( 'WP_Block_Metadata_Registry::register_collection' ); - $result = WP_Block_Metadata_Registry::register_collection( $non_existent_path, $this->temp_manifest_file ); - $this->assertFalse( $result, 'Non-existent path should not be registered' ); + $result = WP_Block_Metadata_Registry::register_collection( $invalid_plugin_path, $this->temp_manifest_file ); + $this->assertFalse( $result, 'Invalid must-use plugin path should not be registered' ); + } + + /** + * @ticket 62140 + */ + public function test_register_collection_with_valid_theme_path() { + $theme_path = WP_CONTENT_DIR . '/themes/my-theme/blocks'; + $result = WP_Block_Metadata_Registry::register_collection( $theme_path, $this->temp_manifest_file ); + $this->assertTrue( $result, 'Valid theme path should be registered successfully' ); + } + + /** + * @ticket 62140 + */ + public function test_register_collection_with_invalid_theme_path() { + $invalid_theme_path = WP_CONTENT_DIR . '/themes'; + + $this->setExpectedIncorrectUsage( 'WP_Block_Metadata_Registry::register_collection' ); + + $result = WP_Block_Metadata_Registry::register_collection( $invalid_theme_path, $this->temp_manifest_file ); + $this->assertFalse( $result, 'Invalid theme path should not be registered' ); + } + + /** + * @ticket 62140 + */ + public function test_register_collection_with_arbitrary_path() { + $arbitrary_path = '/var/arbitrary/path'; + $result = WP_Block_Metadata_Registry::register_collection( $arbitrary_path, $this->temp_manifest_file ); + $this->assertTrue( $result, 'Arbitrary path should be registered successfully' ); + } + + /** + * @ticket 62140 + */ + public function test_register_collection_with_arbitrary_path_and_collection_roots_filter() { + $arbitrary_path = '/var/arbitrary/path'; + add_filter( + 'wp_allowed_block_metadata_collection_roots', + static function ( $paths ) use ( $arbitrary_path ) { + $paths[] = $arbitrary_path; + return $paths; + } + ); + + $this->setExpectedIncorrectUsage( 'WP_Block_Metadata_Registry::register_collection' ); + + $result = WP_Block_Metadata_Registry::register_collection( $arbitrary_path, $this->temp_manifest_file ); + $this->assertFalse( $result, 'Arbitrary path should not be registered if it matches a collection root' ); + + $result = WP_Block_Metadata_Registry::register_collection( dirname( $arbitrary_path ), $this->temp_manifest_file ); + $this->assertFalse( $result, 'Arbitrary path should not be registered if it is a parent directory of a collection root' ); + + $result = WP_Block_Metadata_Registry::register_collection( $arbitrary_path . '/my-plugin/blocks', $this->temp_manifest_file ); + $this->assertTrue( $result, 'Arbitrary path should be registered successfully if it is within a collection root' ); + } + + /** + * @ticket 62140 + */ + public function test_register_collection_with_wp_content_parent_directory_path() { + $invalid_path = dirname( WP_CONTENT_DIR ); + + $this->setExpectedIncorrectUsage( 'WP_Block_Metadata_Registry::register_collection' ); + + $result = WP_Block_Metadata_Registry::register_collection( $invalid_path, $this->temp_manifest_file ); + $this->assertFalse( $result, 'Invalid path (parent directory of "wp-content") should not be registered' ); + } + + /** + * @ticket 62140 + */ + public function test_register_collection_with_wp_includes_parent_directory_path() { + $invalid_path = ABSPATH; + + $this->setExpectedIncorrectUsage( 'WP_Block_Metadata_Registry::register_collection' ); + + $result = WP_Block_Metadata_Registry::register_collection( $invalid_path, $this->temp_manifest_file ); + $this->assertFalse( $result, 'Invalid path (parent directory of "wp-includes") should not be registered' ); + } + + public function test_register_collection_with_non_existent_manifest() { + $non_existent_manifest = '/path/that/does/not/exist/block-manifest.php'; + + $this->setExpectedIncorrectUsage( 'WP_Block_Metadata_Registry::register_collection' ); + + $result = WP_Block_Metadata_Registry::register_collection( '/var/arbitrary/path', $non_existent_manifest ); + $this->assertFalse( $result, 'Non-existent manifest should not be registered' ); } }