Skip to content

Commit

Permalink
Editor: Relax restrictions around registration of block metadata coll…
Browse files Browse the repository at this point in the history
…ections.

This changeset allows for block metadata collections to be registered for almost any source, such as MU plugins, themes, or custom directories with e.g. symlinked plugins or symlinked themes. Prior to the change, block metadata collections could only be registered for plugins and WordPress Core.

There are still safeguards in place to prevent registration of collections in locations that would cause conflicts. For example, it is not possible to register a collection for the entire `wp-content/plugins` directory or the entire `wp-content/themes` directory, since such a collection would conflict with any specific plugin's or theme's collection. In case developers would like to enable this safeguard for their own custom directories, they can use the new `wp_allowed_block_metadata_collection_roots` filter.

Props assassinateur, bowedk, desrosj, dougwollison, flixos90, glynnquelch, gziolo, jorbin, mreishus, swissspidy.
Fixes #62140.


git-svn-id: https://develop.svn.wordpress.org/trunk@59730 602fd350-edb4-49c9-b593-d223f7449a82
  • Loading branch information
felixarntz committed Jan 29, 2025
1 parent 9acdbb9 commit 3fd9378
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 49 deletions.
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' );
}
}

0 comments on commit 3fd9378

Please sign in to comment.