Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,16 @@ class WPCOM_REST_API_V2_Endpoint_Block_Editor_Assets extends WP_REST_Controller
const CACHE_BUSTER = '2025-02-28';

/**
* List of allowed plugins whose assets should be preserved.
* Each entry should be a unique identifier that appears in the asset URL.
* List of allowed plugin handle prefixes whose assets should be preserved.
* Each entry should be a handle prefix that identifies assets from allowed plugins.
*
* @var array
*/
const ALLOWED_PLUGINS = array(
'/plugins/gutenberg/', // Default plugin location
'/plugins/gutenberg-core/', // WPCOM Simple site location
'/plugins/jetpack/', // Default plugin location
'/plugins/jetpack-dev/', // Used for loading in-progress work
'/mu-plugins/jetpack-mu-wpcom-plugin/', // WPCOM Simple site location
'/mu-plugins/wpcomsh/', // WoA helpers, including Jetpack assets in vendor directories
Comment on lines -22 to -28
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path-based allow list meant that inline script tags were erroneously disallowed, as inline scripts do not have a path assigned to a src attribute.

const ALLOWED_PLUGIN_HANDLE_PREFIXES = array(
'jetpack-', // E.g., jetpack-blocks-editor, jetpack-connection
'jp-', // E.g., jp-forms-blocks
'videopress-', // E.g., videopress-add-resumable-upload-support
'wp-', // E.g., wp-block-styles, wp-jp-i18n-loader
);

/**
Expand Down Expand Up @@ -277,43 +275,45 @@ private function unregister_disallowed_plugin_assets() {
// Unregister disallowed plugin scripts
foreach ( $wp_scripts->registered as $handle => $script ) {
// Skip core scripts and protected handles
if ( $this->is_core_asset( $script->src ) || $this->is_protected_handle( $handle ) ) {
if ( $this->is_core_or_gutenberg_asset( $script->src ) || $this->is_protected_handle( $handle ) ) {
continue;
}

if ( ! $this->is_allowed_plugin_asset( $script->src ) ) {
if ( ! $this->is_allowed_plugin_handle( $handle ) ) {
unset( $wp_scripts->registered[ $handle ] );
}
}

// Unregister disallowed plugin styles
foreach ( $wp_styles->registered as $handle => $style ) {
// Skip core styles and protected handles
if ( $this->is_core_asset( $style->src ) || $this->is_protected_handle( $handle ) ) {
if ( $this->is_core_or_gutenberg_asset( $style->src ) || $this->is_protected_handle( $handle ) ) {
continue;
}

if ( ! $this->is_allowed_plugin_asset( $style->src ) ) {
if ( ! $this->is_allowed_plugin_handle( $handle ) ) {
unset( $wp_styles->registered[ $handle ] );
}
}
}

/**
* Check if an asset is a core asset.
* Check if an asset is a core or Gutenberg asset.
*
* @param string $src The asset source URL.
* @return bool True if the asset is a core asset, false otherwise.
* @return bool True if the asset is a core or Gutenberg asset, false otherwise.
*/
private function is_core_asset( $src ) {
private function is_core_or_gutenberg_asset( $src ) {
if ( ! is_string( $src ) ) {
return false;
}

return empty( $src ) ||
$src[0] === '/' ||
strpos( $src, 'wp-includes/' ) !== false ||
strpos( $src, 'wp-admin/' ) !== false;
strpos( $src, 'wp-admin/' ) !== false ||
strpos( $src, 'plugins/gutenberg/' ) !== false ||
strpos( $src, 'plugins/gutenberg-core/' ) !== false; // WPCOM-specific path
}

/**
Expand All @@ -327,18 +327,18 @@ private function is_protected_handle( $handle ) {
}

/**
* Check if an asset is from an allowed plugin.
* Check if a handle is from an allowed plugin.
*
* @param string $src The asset source URL.
* @return bool True if the asset is from an allowed plugin, false otherwise.
* @param string $handle The asset handle.
* @return bool True if the handle is from an allowed plugin, false otherwise.
*/
private function is_allowed_plugin_asset( $src ) {
if ( ! is_string( $src ) || empty( $src ) ) {
private function is_allowed_plugin_handle( $handle ) {
if ( ! is_string( $handle ) || empty( $handle ) ) {
return false;
}

foreach ( self::ALLOWED_PLUGINS as $allowed_plugin ) {
if ( strpos( $src, $allowed_plugin ) !== false ) {
foreach ( self::ALLOWED_PLUGIN_HANDLE_PREFIXES as $allowed_prefix ) {
if ( strpos( $handle, $allowed_prefix ) === 0 ) {
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: other

Editor assets endpoint: reinstate missing Jetpack assets via handle-based exclusion logic
Original file line number Diff line number Diff line change
Expand Up @@ -29,36 +29,9 @@ public function set_up() {
parent::set_up();
$this->instance = new WPCOM_REST_API_V2_Endpoint_Block_Editor_Assets();

// Mock the enqueue_block_editor_assets action to prevent loading non-existent files
// Remove existing actions to prevent failed loading of files that may or
// may not exist depending on the build output.
remove_all_actions( 'enqueue_block_editor_assets' );
add_action( 'enqueue_block_editor_assets', array( $this, 'mock_block_editor_assets' ) );
}

/**
* Clean up after each test.
*/
public function tear_down() {
// Remove our mock action
remove_action( 'enqueue_block_editor_assets', array( $this, 'mock_block_editor_assets' ) );
parent::tear_down();
}

/**
* Mock function for block editor assets.
* This provides minimal required assets without loading actual files.
*/
public function mock_block_editor_assets() {
// Register minimal mock assets that don't require actual files
wp_register_script( 'mock-editor-script', 'http://example.org/plugins/jetpack/mock-editor.js', array(), '1.0', true );
wp_register_style( 'mock-editor-style', 'http://example.org/plugins/jetpack/mock-editor.css', array(), '1.0' );
wp_register_script( 'disallowed-plugin-script', 'http://example.org/plugins/disallowed-plugin/script.js', array(), '1.0', true );
wp_register_script( 'disallowed-plugin-style', 'http://example.org/plugins/disallowed-plugin/style.css', array(), '1.0', true );

// Enqueue our mock assets
wp_enqueue_script( 'mock-editor-script' );
wp_enqueue_style( 'mock-editor-style' );
wp_enqueue_script( 'disallowed-plugin-script' );
wp_enqueue_style( 'disallowed-plugin-style' );
Comment on lines -46 to -61
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relocated these mocks to be co-located with related test cases to better communicate intent.

}

/**
Expand Down Expand Up @@ -178,13 +151,30 @@ function ( $block_name ) {
public function test_get_items_returns_allowed_plugin_assets() {
wp_set_current_user( self::factory()->user->create( array( 'role' => 'editor' ) ) );

add_action( 'enqueue_block_editor_assets', array( $this, 'mock_allowed_plugin_assets' ) );

$request = new WP_REST_Request( Requests::GET, '/wpcom/v2/editor-assets' );
$response = $this->server->dispatch( $request );
$data = $response->get_data();

// Verify the allowed plugin script and style are in the output
$this->assertStringContainsString( 'mock-editor-script', $data['scripts'] );
$this->assertStringContainsString( 'mock-editor-style', $data['styles'] );
$this->assertStringContainsString( 'jetpack-mock-script', $data['scripts'] );
$this->assertStringContainsString( 'jetpack-mock-style', $data['styles'] );

remove_action( 'enqueue_block_editor_assets', array( $this, 'mock_allowed_plugin_assets' ) );
}

/**
* Enqueue allowed plugin assets.
*/
public function mock_allowed_plugin_assets() {
// Register minimal mock assets that don't require actual files
wp_register_script( 'jetpack-mock-script', 'http://example.org/mock-editor.js', array(), '1.0', true );
wp_register_style( 'jetpack-mock-style', 'http://example.org/mock-editor.css', array(), '1.0' );

// Enqueue our mock assets
wp_enqueue_script( 'jetpack-mock-script' );
wp_enqueue_style( 'jetpack-mock-style' );
}

/**
Expand All @@ -193,13 +183,28 @@ public function test_get_items_returns_allowed_plugin_assets() {
public function test_disallowed_plugin_assets_are_filtered() {
wp_set_current_user( self::factory()->user->create( array( 'role' => 'editor' ) ) );

add_action( 'enqueue_block_editor_assets', array( $this, 'mock_disallowed_plugin_assets' ) );

$request = new WP_REST_Request( Requests::GET, '/wpcom/v2/editor-assets' );
$response = $this->server->dispatch( $request );
$data = $response->get_data();

// Verify the disallowed plugin script and style are not in the output
$this->assertStringNotContainsString( 'disallowed-plugin-script', $data['scripts'] );
$this->assertStringNotContainsString( 'disallowed-plugin-style', $data['styles'] );

remove_action( 'enqueue_block_editor_assets', array( $this, 'mock_disallowed_plugin_assets' ) );
}

/**
* Enqueue disallowed plugin assets.
*/
public function mock_disallowed_plugin_assets() {
wp_register_script( 'disallowed-plugin-script', 'http://example.org/script.js', array(), '1.0', true );
wp_register_style( 'disallowed-plugin-style', 'http://example.org/style.css', array(), '1.0' );

wp_enqueue_script( 'disallowed-plugin-script' );
wp_enqueue_style( 'disallowed-plugin-style' );
}

/**
Expand All @@ -216,6 +221,38 @@ public function test_protected_handles_are_preserved() {
$this->assertStringContainsString( 'jquery', $data['scripts'] );
}

/**
* Test that WPCOM-specific Gutenberg assets are preserved.
*/
public function test_wpcom_gutenberg_assets_are_preserved() {
wp_set_current_user( self::factory()->user->create( array( 'role' => 'editor' ) ) );

add_action( 'enqueue_block_editor_assets', array( $this, 'mock_wpcom_gutenberg_assets' ) );

$request = new WP_REST_Request( Requests::GET, '/wpcom/v2/editor-assets' );
$response = $this->server->dispatch( $request );
$data = $response->get_data();

// Verify the WPCOM Gutenberg assets are preserved in the output
$this->assertStringContainsString( 'wpcom-gutenberg-script', $data['scripts'] );
$this->assertStringContainsString( 'wpcom-gutenberg-style', $data['styles'] );
$this->assertStringContainsString( 'plugins/gutenberg-core/script.js', $data['scripts'] );
$this->assertStringContainsString( 'plugins/gutenberg-core/style.css', $data['styles'] );

remove_action( 'enqueue_block_editor_assets', array( $this, 'mock_wpcom_gutenberg_assets' ) );
}

/**
* Enqueue assets using WPCOM's specific Gutenberg paths.
*/
public function mock_wpcom_gutenberg_assets() {
wp_register_script( 'wpcom-gutenberg-script', 'http://example.org/plugins/gutenberg-core/script.js', array(), '1.0', true );
wp_register_style( 'wpcom-gutenberg-style', 'http://example.org/plugins/gutenberg-core/style.css', array(), '1.0' );

wp_enqueue_script( 'wpcom-gutenberg-script' );
wp_enqueue_style( 'wpcom-gutenberg-style' );
}

/**
* Test that required WordPress actions are triggered.
*/
Expand Down
Loading