Skip to content
131 changes: 86 additions & 45 deletions src/wp-includes/class-wp-block-metadata-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
private static $wpinc_dir = null;

/**
* Stores the normalized WordPress plugin directory path.
*
* @since 6.7.0
* @var string|null
* @since 6.7.2
* @var string[]|null
*/
private static $plugin_dir = null;
private static $default_collection_roots = null;

/**
* Registers a block metadata collection.
Expand Down Expand Up @@ -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();

// 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, '/' );
/**
* 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 );

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.
$collection_roots = array_unique(
array_map(
static function ( $allowed_root ) {
return rtrim( $allowed_root, '/' );
},
$collection_roots
)
);

// Check if the path is valid:
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;
}
Expand Down Expand Up @@ -244,30 +257,58 @@ private static function default_identifier_callback( $path ) {
}

/**
* 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;
}
}
108 changes: 104 additions & 4 deletions tests/phpunit/tests/blocks/wpBlockMetadataRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,112 @@ public function test_register_collection_with_invalid_plugin_path() {
$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( $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( $non_existent_path, $this->temp_manifest_file );
$this->assertFalse( $result, 'Non-existent path should not be registered' );
$result = WP_Block_Metadata_Registry::register_collection( '/var/arbitrary/path', $non_existent_manifest );
$this->assertFalse( $result, 'Non-existent manifest should not be registered' );
}
}
Loading