diff --git a/lib/experimental/class-wp-rest-block-editor-assets-controller.php b/lib/experimental/class-wp-rest-block-editor-assets-controller.php new file mode 100644 index 00000000000000..acf463e3dec9b8 --- /dev/null +++ b/lib/experimental/class-wp-rest-block-editor-assets-controller.php @@ -0,0 +1,193 @@ +namespace = '__experimental/wp-block-editor/v1'; + $this->rest_base = 'editor-assets'; + } + + /** + * Registers the controller routes. + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Retrieves a collection of items. + * + * @param WP_REST_Request $request The request object. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + global $wp_styles, $wp_scripts; + + $current_wp_styles = $wp_styles; + $current_wp_scripts = $wp_scripts; + + $wp_styles = new WP_Styles(); + $wp_scripts = new WP_Scripts(); + + // Trigger an action frequently used by plugins to enqueue assets. + do_action( 'wp_loaded' ); + + // We generally do not need reset styles for the block editor. However, if + // it's a classic theme, margins will be added to every block, which is + // reset specifically for list items, so classic themes rely on these + // reset styles. + $wp_styles->done = + wp_theme_has_theme_json() ? array( 'wp-reset-editor-styles' ) : array(); + + wp_enqueue_script( 'wp-polyfill' ); + // Enqueue the `editorStyle` handles for all core block, and dependencies. + wp_enqueue_style( 'wp-edit-blocks' ); + + if ( current_theme_supports( 'wp-block-styles' ) ) { + wp_enqueue_style( 'wp-block-library-theme' ); + } + + // Enqueue frequent dependent, admin-only `dashicon` asset. + wp_enqueue_style( 'dashicons' ); + + // Enqueue the admin-only `postbox` asset required for the block editor. + $suffix = wp_scripts_get_suffix(); + wp_enqueue_script( 'postbox', "/wp-admin/js/postbox$suffix.js", array( 'jquery-ui-sortable', 'wp-a11y' ), false, 1 ); + + // Enqueue foundational post editor assets. + wp_enqueue_script( 'wp-edit-post' ); + wp_enqueue_style( 'wp-edit-post' ); + + // Ensure the block editor scripts and styles are enqueued. + add_filter( 'should_load_block_editor_scripts_and_styles', '__return_true' ); + do_action( 'enqueue_block_assets' ); + do_action( 'enqueue_block_editor_assets' ); + remove_filter( 'should_load_block_editor_scripts_and_styles', '__return_true' ); + + // Additionally, enqueue `editorStyle` and `editorScript` assets for all + // blocks, which contains editor-only styling for blocks (editor content). + $block_registry = WP_Block_Type_Registry::get_instance(); + foreach ( $block_registry->get_all_registered() as $block_type ) { + if ( isset( $block_type->editor_style_handles ) && is_array( $block_type->editor_style_handles ) ) { + foreach ( $block_type->editor_style_handles as $style_handle ) { + wp_enqueue_style( $style_handle ); + } + } + if ( isset( $block_type->editor_script_handles ) && is_array( $block_type->editor_script_handles ) ) { + foreach ( $block_type->editor_script_handles as $script_handle ) { + wp_enqueue_script( $script_handle ); + } + } + } + + // Remove the deprecated `print_emoji_styles` handler. It avoids breaking + // style generation with a deprecation message. + $has_emoji_styles = has_action( 'wp_print_styles', 'print_emoji_styles' ); + if ( $has_emoji_styles ) { + remove_action( 'wp_print_styles', 'print_emoji_styles' ); + } + + ob_start(); + wp_print_styles(); + $styles = ob_get_clean(); + + if ( $has_emoji_styles ) { + add_action( 'wp_print_styles', 'print_emoji_styles' ); + } + + ob_start(); + wp_print_head_scripts(); + wp_print_footer_scripts(); + $scripts = ob_get_clean(); + + $wp_styles = $current_wp_styles; + $wp_scripts = $current_wp_scripts; + + return array( + 'styles' => $styles, + 'scripts' => $scripts, + ); + } + + /** + * Checks the permissions for retrieving items. + * + * @param WP_REST_Request $request The REST request object. + * + * @return bool|WP_Error True if the request has permission, WP_Error object otherwise. + */ + public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + if ( current_user_can( 'edit_posts' ) ) { + return true; + } + + foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { + if ( current_user_can( $post_type->cap->edit_posts ) ) { + return true; + } + } + + return new WP_Error( + 'rest_cannot_read_block_editor_assets', + __( 'Sorry, you are not allowed to read the block editor assets.', 'gutenberg' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + /** + * Retrieves the block editor assets schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + 'type' => 'object', + 'properties' => array( + 'styles' => array( + 'description' => esc_html__( 'Style link tags for the block editor.', 'gutenberg' ), + 'type' => 'string', + ), + 'scripts' => array( + 'description' => esc_html__( 'Script tags for the block editor.', 'gutenberg' ), + 'type' => 'string', + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } + } +} diff --git a/lib/experimental/rest-api.php b/lib/experimental/rest-api.php index 6bb2947f889147..44fe9ed2086ec5 100644 --- a/lib/experimental/rest-api.php +++ b/lib/experimental/rest-api.php @@ -19,6 +19,15 @@ function gutenberg_register_block_editor_settings() { } add_action( 'rest_api_init', 'gutenberg_register_block_editor_settings' ); +/** + * Registers the block editor assets REST API route. + */ +function gutenberg_register_block_editor_assets() { + $editor_assets = new WP_REST_Block_Editor_Assets_Controller(); + $editor_assets->register_routes(); +} +add_action( 'rest_api_init', 'gutenberg_register_block_editor_assets' ); + /** * Shim for get_sample_permalink() to add support for auto-draft status. diff --git a/lib/load.php b/lib/load.php index b501f0abd1c978..befbf66e21d98b 100644 --- a/lib/load.php +++ b/lib/load.php @@ -34,6 +34,7 @@ function gutenberg_is_experiment_enabled( $name ) { if ( ! class_exists( 'WP_REST_Block_Editor_Settings_Controller' ) ) { require_once __DIR__ . '/experimental/class-wp-rest-block-editor-settings-controller.php'; } + require_once __DIR__ . '/experimental/class-wp-rest-block-editor-assets-controller.php'; // WordPress 6.6 compat. require __DIR__ . '/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php'; diff --git a/phpunit/experimental/class-wp-rest-block-editor-assets-controller-test.php b/phpunit/experimental/class-wp-rest-block-editor-assets-controller-test.php new file mode 100644 index 00000000000000..e730cd911bc8b4 --- /dev/null +++ b/phpunit/experimental/class-wp-rest-block-editor-assets-controller-test.php @@ -0,0 +1,136 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + + self::$subscriber_id = $factory->user->create( + array( + 'role' => 'subscriber', + ) + ); + } + + /** + * Clean up fake data. + */ + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + self::delete_user( self::$subscriber_id ); + } + + public function tear_down() { + parent::tear_down(); + } + + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + + $this->assertArrayHasKey( + '/__experimental/wp-block-editor/v1/editor-assets', + $routes + ); + } + + public function test_get_items_without_user() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'GET', '/__experimental/wp-block-editor/v1/editor-assets' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_cannot_read_block_editor_assets', $response, 401 ); + } + + public function test_get_items_without_permissions() { + wp_set_current_user( self::$subscriber_id ); + + $request = new WP_REST_Request( 'GET', '/__experimental/wp-block-editor/v1/editor-assets' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_cannot_read_block_editor_assets', $response, 403 ); + } + + public function test_get_items() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'GET', '/__experimental/wp-block-editor/v1/editor-assets' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'styles', $data, 'Editor assets should include styles.' ); + $this->assertArrayHasKey( 'scripts', $data, 'Editor assets should include scripts.' ); + } + + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/__experimental/wp-block-editor/v1/editor-assets' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertCount( 2, $properties, 'Schema properties array does not have exactly 2 elements' ); + $this->assertArrayHasKey( 'styles', $properties, 'Schema properties array does not have "id" key' ); + $this->assertArrayHasKey( 'scripts', $properties, 'Schema properties array does not have "styles" key' ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_create_item() {} + + /** + * @doesNotPerformAssertions + */ + public function test_update_item() {} + + /** + * @doesNotPerformAssertions + */ + public function test_get_item() {} + + /** + * @doesNotPerformAssertions + */ + public function test_delete_item() {} + + /** + * @doesNotPerformAssertions + */ + public function test_prepare_item() {} + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() {} +}