Skip to content
Draft
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
3 changes: 3 additions & 0 deletions src/wp-includes/block-bindings.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ function get_block_bindings_source( string $source_name ) {
* @since 6.9.0
* @since 7.1.0 Added support for the List Item block.
* @since 7.2.0 Added support for the Icon block.
* @since 7.3.0 Added support for the inner blocks of the Group and Column blocks.
*
* @param string $block_type The block type whose supported attributes are being retrieved.
* @return array The list of block attributes that are supported by block bindings.
Expand All @@ -153,6 +154,8 @@ function get_block_bindings_supported_attributes( $block_type ) {
'core/navigation-link' => array( 'url' ),
'core/navigation-submenu' => array( 'url' ),
'core/icon' => array( 'icon' ),
'core/group' => array( 'innerBlocks' ),
'core/column' => array( 'innerBlocks' ),
);

$supported_block_attributes =
Expand Down
4 changes: 3 additions & 1 deletion src/wp-includes/block-editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,8 @@ function wp_get_post_content_block_attributes() {
* Returns the contextualized block editor settings for a selected editor context.
*
* @since 5.8.0
* @since 7.3.0 Leaves `innerBlocks` out of the attributes that block bindings support,
* as the editor doesn't render its bindings yet.
*
* @param array $custom_settings Custom settings to use with the given editor type.
* @param WP_Block_Editor_Context $block_editor_context The current block editor context.
Expand All @@ -502,7 +504,7 @@ function get_block_editor_settings( array $custom_settings, $block_editor_contex

$editor_settings['__experimentalBlockBindingsSupportedAttributes'] = array();
foreach ( array_keys( WP_Block_Type_Registry::get_instance()->get_all_registered() ) as $block_type ) {
$supported_block_attributes = get_block_bindings_supported_attributes( $block_type );
$supported_block_attributes = array_values( array_diff( get_block_bindings_supported_attributes( $block_type ), array( 'innerBlocks' ) ) );
if ( ! empty( $supported_block_attributes ) ) {
$editor_settings['__experimentalBlockBindingsSupportedAttributes'][ $block_type ] = $supported_block_attributes;
}
Expand Down
303 changes: 302 additions & 1 deletion src/wp-includes/class-wp-block.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@ class WP_Block {
*/
protected $registry;

/**
* The blocks whose inner blocks a block bindings source supplied are
* rendering, outermost first, each with the request it made to the source.
*
* @since 7.3.0
* @var array[]
*/
private static $rendering_inner_blocks = array();

/**
* List of inner blocks (of this same class)
*
Expand Down Expand Up @@ -273,6 +282,8 @@ public function __get( $name ) {
* @since 6.5.0
* @since 6.6.0 Handle the `__default` attribute for pattern overrides.
* @since 6.7.0 Return any updated bindings metadata in the computed attributes.
* @since 7.3.0 Replace the inner blocks of the block for an `innerBlocks` binding,
* which the `__default` binding doesn't add.
*
* @return array The computed block attributes for the provided block bindings.
*/
Expand Down Expand Up @@ -310,6 +321,9 @@ private function process_block_bindings() {
* resulting array.
*/
foreach ( $supported_block_attributes as $attribute_name ) {
if ( 'innerBlocks' === $attribute_name && ! isset( $bindings['innerBlocks'] ) ) {
continue;
}
// Retain any non-pattern override bindings that might be present.
$updated_bindings[ $attribute_name ] = $bindings[ $attribute_name ] ?? array( 'source' => 'core/pattern-overrides' );
}
Expand Down Expand Up @@ -348,7 +362,13 @@ private function process_block_bindings() {
}
}

$source_args = ! empty( $block_binding['args'] ) && is_array( $block_binding['args'] ) ? $block_binding['args'] : array();
$source_args = ! empty( $block_binding['args'] ) && is_array( $block_binding['args'] ) ? $block_binding['args'] : array();

if ( 'innerBlocks' === $attribute_name ) {
$this->replace_inner_blocks( $block_binding_source, $source_args );
continue;
}

$source_value = $block_binding_source->get_value( $source_args, $this, $attribute_name );

// If the value is not null, process the HTML based on the block and the attribute.
Expand All @@ -360,6 +380,285 @@ private function process_block_bindings() {
return $computed_attributes;
}

/**
* Replaces the inner blocks of the block with the ones a block bindings
* source supplies, before they render.
*
* Not while the inner blocks the source supplied for the same request
* render: the block then keeps its own inner blocks, and the source isn't
* asked. This way sources whose values bind each other's, directly or
* through content that loads as it renders, like the menu of a Navigation
* block, can't render forever. Bindings that ask a source something else
* still resolve, inside supplied inner blocks too.
*
* @since 7.3.0
*
* @param WP_Block_Bindings_Source $source The source of the binding.
* @param array $source_args The arguments of the binding.
*/
private function replace_inner_blocks( WP_Block_Bindings_Source $source, array $source_args ) {
$request = $this->get_inner_blocks_request( $source, $source_args );
foreach ( self::$rendering_inner_blocks as $rendering ) {
if ( $rendering['request'] === $request ) {
return;
}
}

$value = $source->get_value( $source_args, $this, 'innerBlocks' );
if ( ! self::is_inner_blocks_value( $value ) ) {
return;
}

$blocks = $this->parse_bound_inner_blocks( $value, $source, $source_args );
$inner_content = self::get_inner_content_with_slots( $this->parsed_block['innerContent'] ?? null, count( $blocks ) );
if ( null === $inner_content ) {
return;
}

$this->parsed_block['innerBlocks'] = $blocks;
$this->parsed_block['innerContent'] = $inner_content;
$this->parsed_block['innerHTML'] = implode( '', array_filter( $inner_content, 'is_string' ) );
$this->inner_blocks = new WP_Block_List( array() );
$this->inner_content = $inner_content;
$this->inner_html = $this->parsed_block['innerHTML'];
$this->refresh_parsed_block_dependents();

self::$rendering_inner_blocks[] = array(
'block' => $this,
'request' => $request,
);
}

/**
* Returns what the block asks its block bindings source for `innerBlocks`:
* the source, the arguments of the binding, the context the source uses,
* and the attributes of the block.
*
* Two blocks with the same request get the same inner blocks from the
* source.
*
* @since 7.3.0
*
* @param WP_Block_Bindings_Source $source The source of the binding.
* @param array $source_args The arguments of the binding.
* @return array The request.
*/
private function get_inner_blocks_request( WP_Block_Bindings_Source $source, array $source_args ): array {
$context = array();
foreach ( (array) $source->uses_context as $context_name ) {
if ( array_key_exists( $context_name, $this->context ) ) {
$context[ $context_name ] = $this->context[ $context_name ];
}
}
return array( $source->name, $source_args, $context, $this->attributes );
}

/**
* Stops tracking the block, once it has rendered, if the inner blocks a
* source supplied for it were rendering.
*
* The blocks still rendering inside it, if any, stopped with it: a block
* that doesn't finish rendering, because of an exception, never gets here,
* so they are forgotten along with it.
*
* @since 7.3.0
*/
private function forget_rendered_inner_blocks() {
foreach ( self::$rendering_inner_blocks as $index => $rendering ) {
if ( $rendering['block'] === $this ) {
self::$rendering_inner_blocks = array_slice( self::$rendering_inner_blocks, 0, $index );
return;
}
}
}

/**
* Determines whether the value of a block bindings source supplies inner
* blocks.
*
* Serialized block markup supplies them, and an empty string supplies none.
* Markup supplies them when the first block delimiter the block parser
* finds, after any whitespace, opens a block. Any other value, like the
* plain text of a field, leaves the block with its own inner blocks.
*
* @since 7.3.0
*
* @param mixed $value The value of the source.
* @return bool Whether the value supplies inner blocks.
*/
private static function is_inner_blocks_value( $value ): bool {
if ( '' === $value ) {
return true;
}
if ( ! is_string( $value ) ) {
return false;
}

$processor = new WP_Block_Processor( $value );
while ( $processor->next_token() ) {
if ( $processor->is_html() && ! $processor->is_non_whitespace_html() ) {
continue;
}
return ! $processor->is_html() && $processor->opens_block();
}
return false;
}

/**
* Parses the inner blocks a block bindings source supplies.
*
* The markup never went through the filters that sanitize post content on
* save, and the user who supplied it may not be allowed unfiltered HTML, so
* by default it is sanitized as on save, attributes of its blocks included.
* Its embeds are expanded as the embeds of the content were, before its
* blocks render.
*
* @since 7.3.0
*
* @global WP_Embed $wp_embed WordPress Embed object.
*
* @param string $markup The serialized blocks.
* @param WP_Block_Bindings_Source $source The source of the binding.
* @param array $source_args The arguments of the binding.
* @return array[] The parsed blocks.
*/
private function parse_bound_inner_blocks( string $markup, WP_Block_Bindings_Source $source, array $source_args ): array {
global $wp_embed;

/**
* Filters whether the inner blocks a block bindings source supplies are
* sanitized before they render.
*
* By default the markup is sanitized as the post content of a user
* without the `unfiltered_html` capability, since the user who supplied
* it is unknown: `wp_kses_post()` removes the HTML that user may not
* publish, in the attributes of the blocks included. A source whose
* markup comes from trusted code, not from users, may keep it as
* supplied. Custom CSS is removed from the blocks either way.
*
* Sanitizing keeps the block delimiters that `serialize_blocks()`
* writes, which escapes the characters `wp_kses_post()` rewrites
* inside comments.
*
* @since 7.3.0
*
* @param bool $sanitize Whether to sanitize the markup. Default true.
* @param string $source_name The name of the source.
* @param array $source_args The arguments of the binding.
* @param WP_Block $block_instance The block whose inner blocks the source supplies.
*/
if ( apply_filters( 'block_bindings_sanitize_inner_blocks', true, $source->name, $source_args, $this ) ) {
$markup = wp_kses_post( $markup );
}
if ( $wp_embed instanceof WP_Embed ) {
$markup = $wp_embed->run_shortcode( $markup );
$markup = $wp_embed->autoembed( $markup );
}

return self::prepare_bound_inner_blocks( parse_blocks( $markup ) );
}

/**
* Prepares the blocks a block bindings source supplies, and their inner
* blocks, to render.
*
* Removes their custom CSS, which only users who can edit CSS may add:
* whoever supplied the value is unknown.
*
* @since 7.3.0
*
* @param array[] $blocks The parsed blocks.
* @return array[] The parsed blocks.
*/
private static function prepare_bound_inner_blocks( array $blocks ): array {
foreach ( $blocks as &$block ) {
if ( isset( $block['attrs']['style'] ) ) {
$block['attrs']['style'] = wp_strip_custom_css_from_style_array( $block['attrs']['style'] );
if ( empty( $block['attrs']['style'] ) ) {
unset( $block['attrs']['style'] );
}
}

$block['innerBlocks'] = self::prepare_bound_inner_blocks( $block['innerBlocks'] );
}
return $blocks;
}

/**
* Returns the inner content of a block with slots for the given number of
* inner blocks.
*
* The slots replace the block's own inner blocks and the HTML between them.
* A block without inner blocks gets them before the closing tag of the
* element that wraps its content.
*
* @since 7.3.0
*
* @param array|null $inner_content The inner content of the block.
* @param int $count The number of inner blocks.
* @return array|null The inner content, or null if there's no element to put
* the inner blocks in.
*/
private static function get_inner_content_with_slots( $inner_content, int $count ): ?array {
if ( ! is_array( $inner_content ) ) {
return null;
}

$slots = array_fill( 0, $count, null );
$own_slots = array_keys( $inner_content, null, true );
if ( $own_slots ) {
$before_first_slot = array_slice( $inner_content, 0, $own_slots[0] );
$after_last_slot = array_slice( $inner_content, end( $own_slots ) + 1 );
return array_merge( $before_first_slot, $slots, $after_last_slot );
}

$wrapper_parts = self::split_at_wrapper_closer( implode( '', $inner_content ) );
if ( null === $wrapper_parts ) {
return null;
}
return array_merge( array( $wrapper_parts[0] ), $slots, array( $wrapper_parts[1] ) );
}

/**
* Splits the HTML of an element that wraps a block's content before the
* element's closing tag.
*
* @since 7.3.0
*
* @param string $html The HTML of the wrapper element.
* @return string[]|null The HTML before the closing tag and the HTML from it,
* or null if the HTML has no wrapper element to split.
*/
private static function split_at_wrapper_closer( string $html ): ?array {
$processor = WP_HTML_Processor::create_fragment( $html );
if ( null === $processor ) {
return null;
}

$before = '';
$after = '';
$depth_outside_wrapper = null;
$is_at_wrapper_closer = false;
while ( $processor->next_token() ) {
if ( null === $depth_outside_wrapper && '#tag' === $processor->get_token_type() && ! $processor->is_tag_closer() ) {
$depth_outside_wrapper = $processor->get_current_depth() - 1;
} elseif ( $processor->is_tag_closer() && $processor->get_current_depth() === $depth_outside_wrapper ) {
$is_at_wrapper_closer = true;
}

if ( $is_at_wrapper_closer ) {
$after .= $processor->serialize_token();
} else {
$before .= $processor->serialize_token();
}
}

if ( ! $is_at_wrapper_closer || null !== $processor->get_last_error() || $processor->paused_at_incomplete_token() ) {
return null;
}
return array( $before, $after );
}

/**
* Depending on the block attribute name, replace its value in the HTML based on the value provided.
*
Expand Down Expand Up @@ -704,6 +1003,8 @@ public function render( $options = array() ) {
}
}

$this->forget_rendered_inner_blocks();

/**
* Filters the content of a single block.
*
Expand Down
Loading
Loading