<?php
/**
* REST API: WP_REST_Block_Editor_Settings_Controller class
*
* @package WordPress
* @subpackage REST_API
*/
if ( ! class_exists( 'WP_REST_Block_Editor_Settings_Controller' ) ) {
/**
* Core class used to retrieve the block editor settings via the REST API.
*
* @see WP_REST_Controller
*/
class WP_REST_Block_Editor_Settings_Controller extends WP_REST_Controller {
/**
* Constructs the controller.
*/
public function __construct() {
$this->namespace = 'wp-block-editor/v1';
$this->rest_base = 'settings';
}
/**
* Registers the necessary REST API 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' ),
)
);
register_rest_route(
$this->namespace,
'/assets',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_assets' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_assets_schema' ),
)
);
}
/**
* Checks whether a given request has permission to read block editor settings
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_Error|bool 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_settings',
__( 'Sorry, you are not allowed to read the block editor settings.', 'gutenberg' ),
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Returns the block editor's settings
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis
// Simplified context handling: mobile vs default.
// Only 'mobile' context is special; everything else uses the default editor context.
$context_param = $request->get_param( 'context' );
$editor_context_name = 'mobile' === $context_param ? 'core/mobile' : 'core/edit-post';
// Apply mobile-specific settings filter only for mobile context.
if ( 'mobile' === $context_param ) {
add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings_mobile', PHP_INT_MAX );
}
$editor_context = new WP_Block_Editor_Context( array( 'name' => $editor_context_name ) );
$settings = get_block_editor_settings( array(), $editor_context );
if ( 'mobile' === $context_param ) {
remove_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings_mobile', PHP_INT_MAX );
}
return rest_ensure_response( $settings );
}
/**
* Retrieves the block editor's settings schema, conforming to JSON Schema.
*
* @since 5.8.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'block-editor-settings-item',
'type' => 'object',
'properties' => array(
'__unstableEnableFullSiteEditingBlocks' => array(
'description' => __( 'Enables experimental Site Editor blocks', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
'styles' => array(
'description' => __( 'Editor styles', 'gutenberg' ),
'type' => 'array',
'context' => array( 'default' ),
),
'supportsTemplateMode' => array(
'description' => __( 'Indicates whether the current theme supports block-based templates.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
'supportsLayout' => array(
'description' => __( 'Enable/disable layouts support in container blocks.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
'widgetTypesToHideFromLegacyWidgetBlock' => array(
'description' => __( 'Widget types to hide from Legacy Widget block.', 'gutenberg' ),
'type' => 'array',
'context' => array( 'default' ),
),
'__experimentalFeatures' => array(
'description' => __( 'Settings consolidated from core, theme, and user origins.', 'gutenberg' ),
'type' => 'object',
'context' => array( 'default', 'mobile' ),
),
'__experimentalStyles' => array(
'description' => __( 'Styles consolidated from core, theme, and user origins.', 'gutenberg' ),
'type' => 'object',
'context' => array( 'mobile' ),
),
'__experimentalEnableQuoteBlockV2' => array(
'description' => __( 'Whether the V2 of the quote block that uses inner blocks should be enabled.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'mobile' ),
),
'__experimentalEnableListBlockV2' => array(
'description' => __( 'Whether the V2 of the list block that uses inner blocks should be enabled.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'mobile' ),
),
'alignWide' => array(
'description' => __( 'Enable/Disable Wide/Full Alignments.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
'allowedBlockTypes' => array(
'description' => __( 'List of allowed block types.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
'allowedMimeTypes' => array(
'description' => __( 'List of allowed mime types and file extensions.', 'gutenberg' ),
'type' => 'object',
'context' => array( 'default' ),
),
'blockCategories' => array(
'description' => __( 'Returns all the categories for block types that will be shown in the block editor.', 'gutenberg' ),
'type' => 'array',
'context' => array( 'default' ),
),
'blockInspectorTabs' => array(
'description' => __( 'Block inspector tab display overrides.', 'gutenberg' ),
'type' => 'object',
'context' => array( 'default' ),
),
'disableCustomColors' => array(
'description' => __( 'Disables custom colors.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
'disableCustomFontSizes' => array(
'description' => __( 'Disables custom font size.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
'disableCustomGradients' => array(
'description' => __( 'Disables custom font size.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
'disableLayoutStyles' => array(
'description' => __( 'Disables output of layout styles.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
'enableCustomLineHeight' => array(
'description' => __( 'Enables custom line height.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
'enableCustomSpacing' => array(
'description' => __( 'Enables custom spacing.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
'enableCustomUnits' => array(
'description' => __( 'Enables custom units.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
'isRTL' => array(
'description' => __( 'Determines whether the current locale is right-to-left (RTL).', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
'imageDefaultSize' => array(
'description' => __( 'Default size for images.', 'gutenberg' ),
'type' => 'string',
'context' => array( 'default' ),
),
'imageDimensions' => array(
'description' => __( 'Available image dimensions.', 'gutenberg' ),
'type' => 'object',
'context' => array( 'default' ),
),
'imageEditing' => array(
'description' => __( 'Determines whether the image editing feature is enabled.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
'imageSizes' => array(
'description' => __( 'Available image sizes.', 'gutenberg' ),
'type' => 'array',
'context' => array( 'default' ),
),
'maxUploadFileSize' => array(
'description' => __( 'Maximum upload size in bytes allowed for the site.', 'gutenberg' ),
'type' => 'number',
'context' => array( 'default' ),
),
'colors' => array(
'description' => __( 'Active theme color palette.', 'gutenberg' ),
'type' => 'array',
'context' => array( 'default' ),
),
'fontSizes' => array(
'description' => __( 'Active theme font sizes.', 'gutenberg' ),
'type' => 'array',
'context' => array( 'default' ),
),
'gradients' => array(
'description' => __( 'Active theme gradients.', 'gutenberg' ),
'type' => 'array',
'context' => array( 'default' ),
),
'spacingSizes' => array(
'description' => __( 'Active theme spacing sizes.', 'gutenberg' ),
'type' => 'array',
'context' => array( 'default' ),
),
'spacingScale' => array(
'description' => __( 'Active theme spacing scale.', 'gutenberg' ),
'type' => 'array',
'context' => array( 'default' ),
),
'disableCustomSpacingSizes' => array(
'description' => __( 'Disables custom spacing sizes.', 'gutenberg' ),
'type' => 'boolean',
'context' => array( 'default' ),
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves editor assets (scripts, styles, and inline content).
*
* @since Gutenberg 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_assets( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
// Load WordPress admin environment.
$this->load_admin_environment();
// Set up the block editor context.
set_current_screen();
$current_screen = get_current_screen();
if ( $current_screen ) {
$current_screen->is_block_editor( true );
}
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$hook_suffix = 'block-editor-assets';
// Remove unwanted scripts/styles.
remove_action( 'admin_enqueue_scripts', 'gutenberg_enqueue_command_palette_assets', 9 );
remove_action( 'admin_enqueue_scripts', 'gutenberg_enqueue_command_palette_assets' );
remove_action( 'admin_enqueue_scripts', 'wp_auth_check_load' );
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
remove_action( 'admin_print_styles', 'print_emoji_styles' );
// Preload blocks - this creates inline scripts.
$server_block_settings = get_block_editor_server_block_settings();
wp_add_inline_script(
'wp-block-library',
'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( $server_block_settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) . ');',
'before'
);
// Preload server-registered block bindings sources.
$registered_sources = get_all_registered_block_bindings_sources();
if ( ! empty( $registered_sources ) ) {
$filtered_sources = array();
foreach ( $registered_sources as $source ) {
$filtered_sources[] = array(
'name' => $source->name,
'label' => $source->label,
'usesContext' => $source->uses_context,
);
}
$script = sprintf( 'for ( const source of %s ) { wp.blocks.registerBlockBindingsSource( source ); }', wp_json_encode( $filtered_sources, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) );
wp_add_inline_script(
'wp-block-library',
$script,
'before'
);
}
$load_blocks_scripts = 'wp.blockLibrary.registerCoreBlocks( wp.blockLibrary.__experimentalGetCoreBlocks().filter( ( block ) => block.name !== "core/freeform" ) );';
wp_add_inline_script(
'wp-block-library',
$load_blocks_scripts,
'after'
);
// Enqueue editor assets.
wp_enqueue_script( 'wp-format-library' );
wp_enqueue_script( 'wp-block-library' );
wp_enqueue_style( 'wp-editor' );
wp_enqueue_style( 'wp-block-library' );
wp_enqueue_style( 'wp-format-library' );
wp_enqueue_media();
do_action( 'enqueue_block_editor_assets' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
// Never allow old admin styles as a dependency.
wp_deregister_style( 'common' );
wp_deregister_style( 'forms' );
// Trigger rendering hooks to capture all assets.
ob_start();
do_action( 'admin_enqueue_scripts', $hook_suffix ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
do_action( "admin_print_styles-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
do_action( 'admin_print_styles' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
do_action( "admin_print_scripts-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
do_action( 'admin_print_scripts' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
do_action( "admin_head-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
do_action( 'admin_head' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
do_action( 'admin_footer', '' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
do_action( 'wp_print_footer_scripts' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores,WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
do_action( "admin_print_footer_scripts-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
do_action( 'admin_print_footer_scripts' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
do_action( "admin_footer-{$hook_suffix}" ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
$html_output = ob_get_clean();
// Extract all script tags with type="text/html" (WordPress media templates use text/html, not text/template).
// Templates are already in html_output from the admin_footer hook.
$html_templates = array();
if ( preg_match_all( '/<script[^>]*type\s*=\s*["\']text\/html["\'][^>]*>.*?<\/script>/is', $html_output, $matches ) ) {
$html_templates = $matches[0];
}
// Process the captured assets.
return rest_ensure_response( $this->process_assets( $html_templates ) );
}
/**
* Process WordPress assets and return structured data.
*
* @since Gutenberg 5.8.0
*
* @global WP_Scripts $wp_scripts WordPress scripts objects.
* @global WP_Styles $wp_styles WordPress styles objects.
*
* @param array $html_templates Optional. Array of HTML template strings.
* @return array Structured asset data.
*/
private function process_assets( $html_templates = array() ) {
global $wp_scripts, $wp_styles;
$styles_data = array();
$scripts_data = array();
$inline_styles = array(
'before' => array(),
'after' => array(),
);
$inline_scripts = array(
'before' => array(),
'after' => array(),
);
// Get boot module asset file for dependencies.
$boot_asset_file = include __DIR__ . '/../../build/modules/boot/index.min.asset.php';
$boot_dependencies = $boot_asset_file['dependencies'] ?? array();
// Get all dependencies that should be excluded (boot dependencies + their deep dependencies).
$excluded_scripts = $this->get_all_dependencies( $boot_dependencies, $wp_scripts );
$excluded_styles = $this->get_all_dependencies( array( 'wp-components', 'wp-commands', 'dashicons' ), $wp_styles );
// Process styles - include all dependencies of queued styles.
$all_needed_styles = array();
foreach ( $wp_styles->queue as $handle ) {
$all_needed_styles = array_merge( $all_needed_styles, $this->get_all_dependencies( array( $handle ), $wp_styles ) );
}
$all_needed_styles = array_unique( $all_needed_styles );
foreach ( $all_needed_styles as $handle ) {
// Skip styles that are already loaded by the boot module.
if ( in_array( $handle, $excluded_styles, true ) ) {
continue;
}
if ( isset( $wp_styles->registered[ $handle ] ) ) {
$style = $wp_styles->registered[ $handle ];
$styles_data[ $handle ] = array(
'src' => $style->src,
'deps' => $style->deps,
'version' => $style->ver,
'media' => $style->args,
);
$inline_style = $wp_styles->get_data( $handle, 'after' );
if ( ! empty( $inline_style ) ) {
$inline_styles['after'][ $handle ] = $inline_style;
}
$inline_style = $wp_styles->get_data( $handle, 'before' );
if ( ! empty( $inline_style ) ) {
$inline_styles['before'][ $handle ] = $inline_style;
}
}
}
// Process scripts - include all dependencies of queued scripts.
$all_needed_scripts = array();
foreach ( $wp_scripts->queue as $handle ) {
$all_needed_scripts = array_merge( $all_needed_scripts, $this->get_all_dependencies( array( $handle ), $wp_scripts ) );
}
$all_needed_scripts = array_unique( $all_needed_scripts );
foreach ( $all_needed_scripts as $handle ) {
// Skip scripts that are already loaded by the boot module.
if ( in_array( $handle, $excluded_scripts, true ) ) {
continue;
}
if ( isset( $wp_scripts->registered[ $handle ] ) ) {
$script = $wp_scripts->registered[ $handle ];
$scripts_data[ $handle ] = array(
'src' => $script->src,
'deps' => $script->deps,
'version' => $script->ver,
'in_footer' => isset( $script->extra['group'] ) && 1 === $script->extra['group'],
);
// Get inline scripts using proper WordPress methods.
$before_script = $wp_scripts->get_inline_script_data( $handle, 'before' );
$after_script = $wp_scripts->get_inline_script_data( $handle, 'after' );
// Get extra script data (localized data).
$extra_script = $wp_scripts->print_extra_script( $handle, false );
// Store before scripts (includes extra/localized data).
$before_content = '';
if ( ! empty( $before_script ) ) {
$before_content .= $before_script . "\n";
}
if ( ! empty( $extra_script ) ) {
$before_content .= $extra_script . "\n";
}
if ( ! empty( $before_content ) ) {
$inline_scripts['before'][ $handle ] = trim( $before_content );
}
// Store after scripts separately.
if ( ! empty( $after_script ) ) {
$inline_scripts['after'][ $handle ] = trim( $after_script );
}
}
}
// Build an import map from registered script modules.
// the import map need to be dynamically extended in the frontend.
$script_modules = wp_script_modules();
$registered = array();
$reflection = new ReflectionClass( $script_modules );
$property = $reflection->getProperty( 'registered' );
/*
* ReflectionProperty::setAccessible is:
* - needed until 8.1.0, as property `registered` is private.
* - redundant as of 8.1.0, which made non-public reflection accessible by default.
* - deprecated as of 8.5.0.
*/
if ( PHP_VERSION_ID < 80100 ) {
$property->setAccessible( true );
}
$registered = $property->getValue( $script_modules );
$import_map = array();
foreach ( $registered as $id => $module ) {
// Handle both array and object formats.
if ( is_array( $module ) && isset( $module['src'] ) ) {
$import_map[ $id ] = $module['src'];
} elseif ( is_object( $module ) && isset( $module->src ) ) {
$import_map[ $id ] = $module->src;
}
}
return array(
'scripts' => $scripts_data,
'styles' => $styles_data,
'inline_scripts' => $inline_scripts,
'inline_styles' => $inline_styles,
'html_templates' => $html_templates,
'script_modules' => $import_map,
);
}
/**
* Get all dependencies recursively (works for both scripts and styles).
*
* @since Gutenberg 5.8.0
*
* @param array|string $handles The handles to get dependencies for.
* @param WP_Dependencies $wp_assets The WordPress dependencies object.
* @return array Array of all dependencies.
*/
private function get_all_dependencies( $handles, $wp_assets ) {
$all_deps = array();
$handles = (array) $handles;
foreach ( $handles as $handle ) {
if ( isset( $wp_assets->registered[ $handle ] ) ) {
$asset = $wp_assets->registered[ $handle ];
$all_deps[] = $handle;
if ( ! empty( $asset->deps ) ) {
$child_deps = $this->get_all_dependencies( $asset->deps, $wp_assets );
$all_deps = array_merge( $all_deps, $child_deps );
}
}
}
return array_unique( $all_deps );
}
/**
* Load WordPress admin environment and required functions.
*
* This mirrors the initialization that happens in wp-admin context
* to ensure all necessary functions and hooks are available.
*
* @since Gutenberg 5.8.0
*/
private function load_admin_environment() {
// Load core admin files that provide essential functions.
require_once ABSPATH . 'wp-admin/includes/admin.php';
}
/**
* Retrieves the editor assets schema, conforming to JSON Schema.
*
* @since Gutenberg 5.8.0
*
* @return array Item schema data.
*/
public function get_assets_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'block-editor-assets',
'type' => 'object',
'properties' => array(
'scripts' => array(
'description' => __( 'Editor scripts data.', 'gutenberg' ),
'type' => 'object',
'readonly' => true,
),
'styles' => array(
'description' => __( 'Editor styles data.', 'gutenberg' ),
'type' => 'object',
'readonly' => true,
),
'inline_scripts' => array(
'description' => __( 'Inline scripts for editor assets.', 'gutenberg' ),
'type' => 'object',
'readonly' => true,
),
'inline_styles' => array(
'description' => __( 'Inline styles for editor assets.', 'gutenberg' ),
'type' => 'object',
'readonly' => true,
),
'script_modules' => array(
'description' => __( 'Script modules to load into the import map.', 'gutenberg' ),
'type' => 'object',
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
}