: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
$css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }';
// Right and left padding are applied to the first container with `.has-global-padding` class.
$css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
// Alignfull children of the container with left and right padding have negative margins so they can still be full width.
$css .= '.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }';
// Nested children of the container with left and right padding that are not full aligned do not get padding, unless they are direct children of an alignfull flow container.
$css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) { padding-right: 0; padding-left: 0; }';
// Alignfull direct children of the containers that are targeted by the rule above do not need negative margins.
$css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) > .alignfull { margin-left: 0; margin-right: 0; }';
$css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
$css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
$css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
// Block gap styles will be output unless explicitly set to `null`. See static::PROTECTED_PROPERTIES.
if ( isset( $this->theme_json['settings']['spacing']['blockGap'] ) ) {
$block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) );
$css .= ":where(.wp-site-blocks) > * { margin-block-start: $block_gap_value; margin-block-end: 0; }";
$css .= ':where(.wp-site-blocks) > :first-child { margin-block-start: 0; }';
$css .= ':where(.wp-site-blocks) > :last-child { margin-block-end: 0; }';
// For backwards compatibility, ensure the legacy block gap CSS variable is still available.
$css .= static::ROOT_CSS_PROPERTIES_SELECTOR . " { --wp--style--block-gap: $block_gap_value; }";
$css .= $this->get_layout_styles( $block_metadata );
* For metadata values that can either be booleans or paths to booleans, gets the value.
* 'defaultPalette' => true
* static::get_metadata_boolean( $data, false );
* static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) );
* @param array $data The data to inspect.
* @param bool|array $path Boolean or path to a boolean.
* @param bool $default_value Default value if the referenced path is missing.
* @return bool Value of boolean metadata.
protected static function get_metadata_boolean( $data, $path, $default_value = false ) {
if ( is_bool( $path ) ) {
if ( is_array( $path ) ) {
$value = _wp_array_get( $data, $path );
* Merges new incoming data.
* @since 5.9.0 Duotone preset also has origins.
* @param WP_Theme_JSON $incoming Data to merge.
public function merge( $incoming ) {
$incoming_data = $incoming->get_raw_data();
$this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
* Recompute all the spacing sizes based on the new hierarchy of data. In the constructor
* spacingScale and spacingSizes are both keyed by origin and VALID_ORIGINS is ordered, so
* we can allow partial spacingScale data to inherit missing data from earlier layers when
* computing the spacing sizes.
* This happens before the presets are merged to ensure that default spacing sizes can be
* removed from the theme origin if $prevent_override is true.
$flattened_spacing_scale = array();
foreach ( static::VALID_ORIGINS as $origin ) {
$scale_path = array( 'settings', 'spacing', 'spacingScale', $origin );
// Apply the base spacing scale to the current layer.
$base_spacing_scale = _wp_array_get( $this->theme_json, $scale_path, array() );
$flattened_spacing_scale = array_replace( $flattened_spacing_scale, $base_spacing_scale );
$spacing_scale = _wp_array_get( $incoming_data, $scale_path, null );
if ( ! isset( $spacing_scale ) ) {
// Allow partial scale settings by merging with lower layers.
$flattened_spacing_scale = array_replace( $flattened_spacing_scale, $spacing_scale );
// Generate and merge the scales for this layer.
$sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin );
$spacing_sizes = _wp_array_get( $incoming_data, $sizes_path, array() );
$spacing_scale_sizes = static::compute_spacing_sizes( $flattened_spacing_scale );
$merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );
_wp_array_set( $incoming_data, $sizes_path, $merged_spacing_sizes );
* The array_replace_recursive algorithm merges at the leaf level,
* but we don't want leaf arrays to be merged, so we overwrite it.
* For leaf values that are sequential arrays it will use the numeric indexes for replacement.
* We rather replace the existing with the incoming value, if it exists.
* This is the case of spacing.units.
* For leaf values that are associative arrays it will merge them as expected.
* This is also not the behavior we want for the current associative arrays (presets).
* We rather replace the existing with the incoming value, if it exists.
* This happens, for example, when we merge data from theme.json upon existing
* theme supports or when we merge anything coming from the same source twice.
* This is the case of color.palette, color.gradients, color.duotone,
* typography.fontSizes, or typography.fontFamilies.
* Additionally, for some preset types, we also want to make sure the
* values they introduce don't conflict with default values. We do so
* by checking the incoming slugs for theme presets and compare them
* with the equivalent default presets: if a slug is present as a default
* we remove it from the theme presets.
$nodes = static::get_setting_nodes( $incoming_data );
$slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) );
foreach ( $nodes as $node ) {
// Replace the spacing.units.
$content = _wp_array_get( $incoming_data, $path, null );
if ( isset( $content ) ) {
_wp_array_set( $this->theme_json, $path, $content );
foreach ( static::PRESETS_METADATA as $preset_metadata ) {
$prevent_override = $preset_metadata['prevent_override'];
if ( is_array( $prevent_override ) ) {
$prevent_override = _wp_array_get( $this->theme_json['settings'], $preset_metadata['prevent_override'] );
foreach ( static::VALID_ORIGINS as $origin ) {
$base_path = $node['path'];
foreach ( $preset_metadata['path'] as $leaf ) {
$content = _wp_array_get( $incoming_data, $path, null );
if ( ! isset( $content ) ) {
// Set names for theme presets based on the slug if they are not set and can use default names.
if ( 'theme' === $origin && $preset_metadata['use_default_names'] ) {
foreach ( $content as $key => $item ) {
if ( ! isset( $item['name'] ) ) {
$name = static::get_name_from_defaults( $item['slug'], $base_path );
$content[ $key ]['name'] = $name;
// Filter out default slugs from theme presets when defaults should not be overridden.
if ( 'theme' === $origin && $prevent_override ) {
$slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] );
$preset_global = _wp_array_get( $slugs_global, $preset_metadata['path'], array() );
$preset_node = _wp_array_get( $slugs_node, $preset_metadata['path'], array() );
$preset_slugs = array_merge_recursive( $preset_global, $preset_node );
$content = static::filter_slugs( $content, $preset_slugs );
_wp_array_set( $this->theme_json, $path, $content );
* Converts all filter (duotone) presets into SVGs.
* @param array $origins List of origins to process.
* @return string SVG filters.
public function get_svg_filters( $origins ) {
$blocks_metadata = static::get_blocks_metadata();
$setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );
foreach ( $setting_nodes as $metadata ) {
$node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
if ( empty( $node['color']['duotone'] ) ) {
$duotone_presets = $node['color']['duotone'];
foreach ( $origins as $origin ) {
if ( ! isset( $duotone_presets[ $origin ] ) ) {
foreach ( $duotone_presets[ $origin ] as $duotone_preset ) {
$filters .= wp_get_duotone_filter_svg( $duotone_preset );
* Determines whether a presets should be overridden or not.
* @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead.
* @param array $theme_json The theme.json like structure to inspect.
* @param array $path Path to inspect.
* @param bool|array $override Data to compute whether to override the preset.
protected static function should_override_preset( $theme_json, $path, $override ) {
_deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' );
if ( is_bool( $override ) ) {
* The relationship between whether to override the defaults
* and whether the defaults are enabled is inverse:
* - If defaults are enabled => theme presets should not be overridden
* - If defaults are disabled => theme presets should be overridden
* For example, a theme sets defaultPalette to false,
* making the default palette hidden from the user.
* In that case, we want all the theme presets to be present,
* so they should override the defaults.
if ( is_array( $override ) ) {
$value = _wp_array_get( $theme_json, array_merge( $path, $override ) );
// Search the top-level key if none was found for this node.
$value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) );
* Returns the default slugs for all the presets in an associative array
* whose keys are the preset paths and the leaves is the list of slugs.
* 'palette' => array( 'slug-1', 'slug-2' ),
* 'gradients' => array( 'slug-3', 'slug-4' ),
* @param array $data A theme.json like structure.
* @param array $node_path The path to inspect. It's 'settings' by default.
protected static function get_default_slugs( $data, $node_path ) {
foreach ( static::PRESETS_METADATA as $metadata ) {
foreach ( $metadata['path'] as $leaf ) {
$preset = _wp_array_get( $data, $path, null );
if ( ! isset( $preset ) ) {
$slugs_for_preset = array();
foreach ( $preset as $item ) {
if ( isset( $item['slug'] ) ) {
$slugs_for_preset[] = $item['slug'];
_wp_array_set( $slugs, $metadata['path'], $slugs_for_preset );
* Gets a `default`'s preset name by a provided slug.
* @param string $slug The slug we want to find a match from default presets.
* @param array $base_path The path to inspect. It's 'settings' by default.
protected function get_name_from_defaults( $slug, $base_path ) {
$default_content = _wp_array_get( $this->theme_json, $path, null );
if ( ! $default_content ) {
foreach ( $default_content as $item ) {
if ( $slug === $item['slug'] ) {
* Removes the preset values whose slug is equal to any of given slugs.
* @param array $node The node with the presets to validate.
* @param array $slugs The slugs that should not be overridden.
* @return array The new node.
protected static function filter_slugs( $node, $slugs ) {
foreach ( $node as $value ) {
if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) {
* Removes insecure data from theme.json.
* @since 6.3.2 Preserves global styles block variations when securing styles.
* @since 6.6.0 Updated to allow variation element styles and $origin parameter.
* @param array $theme_json Structure to sanitize.
* @param string $origin Optional. What source of data this object represents.
* One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
* @return array Sanitized structure.
public static function remove_insecure_properties( $theme_json, $origin = 'theme' ) {
if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
$theme_json = WP_Theme_JSON_Schema::migrate( $theme_json, $origin );
$valid_block_names = array_keys( static::get_blocks_metadata() );
$valid_element_names = array_keys( static::ELEMENTS );
$valid_variations = static::get_valid_block_style_variations();
$theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations );
$blocks_metadata = static::get_blocks_metadata();
$style_options = array( 'include_block_style_variations' => true ); // Allow variations data.
$style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata, $style_options );
foreach ( $style_nodes as $metadata ) {
$input = _wp_array_get( $theme_json, $metadata['path'], array() );
// The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability.
if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) {
$output = static::remove_insecure_styles( $input );
* Get a reference to element name from path.
* $metadata['path'] = array( 'styles', 'elements', 'link' );
$current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ];
* $output is stripped of pseudo selectors. Re-add and process them
* or insecure styles here.
if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) {
foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) {
if ( isset( $input[ $pseudo_selector ] ) ) {
$output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] );
if ( ! empty( $output ) ) {
_wp_array_set( $sanitized, $metadata['path'], $output );
if ( isset( $metadata['variations'] ) ) {
foreach ( $metadata['variations'] as $variation ) {
$variation_input = _wp_array_get( $theme_json, $variation['path'], array() );
if ( empty( $variation_input ) ) {
$variation_output = static::remove_insecure_styles( $variation_input );
// Process a variation's elements and element pseudo selector styles.
if ( isset( $variation_input['elements'] ) ) {
foreach ( $valid_element_names as $element_name ) {
$element_input = $variation_input['elements'][ $element_name ] ?? null;
$element_output = static::remove_insecure_styles( $element_input );
if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) {
foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) {
if ( isset( $element_input[ $pseudo_selector ] ) ) {
$element_output[ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $pseudo_selector ] );
if ( ! empty( $element_output ) ) {
_wp_array_set( $variation_output, array( 'elements', $element_name ), $element_output );
if ( ! empty( $variation_output ) ) {
_wp_array_set( $sanitized, $variation['path'], $variation_output );
$setting_nodes = static::get_setting_nodes( $theme_json );
foreach ( $setting_nodes as $metadata ) {
$input = _wp_array_get( $theme_json, $metadata['path'], array() );
$output = static::remove_insecure_settings( $input );
if ( ! empty( $output ) ) {
_wp_array_set( $sanitized, $metadata['path'], $output );
if ( empty( $sanitized['styles'] ) ) {
unset( $theme_json['styles'] );
$theme_json['styles'] = $sanitized['styles'];
if ( empty( $sanitized['settings'] ) ) {
unset( $theme_json['settings'] );
$theme_json['settings'] = $sanitized['settings'];