From 96979a754ab2fa94528fe1ac609af7dca5498c42 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 28 Aug 2025 18:18:51 +0200 Subject: [PATCH 01/29] Block Bindings: Add fallback --- lib/compat/wordpress-6.9/block-bindings.php | 308 ++++++++++++++++++++ lib/load.php | 1 + 2 files changed, 309 insertions(+) create mode 100644 lib/compat/wordpress-6.9/block-bindings.php diff --git a/lib/compat/wordpress-6.9/block-bindings.php b/lib/compat/wordpress-6.9/block-bindings.php new file mode 100644 index 00000000000000..09d3e1e05eefcd --- /dev/null +++ b/lib/compat/wordpress-6.9/block-bindings.php @@ -0,0 +1,308 @@ +parsed_block['attrs']; + // Process the block bindings and get attributes updated with the values from the sources. + $computed_attributes = gutenberg_process_block_bindings( $instance ); + if ( empty( $computed_attributes ) ) { + return $block_content; + } + + // Merge the computed attributes with the original attributes. + $instance->attributes = array_merge( $attributes, $computed_attributes ); + + /** + * This filter is called from WP_Block::render(), after the block content has + * already been rendered. However, dynamic blocks expect their render() method + * to receive block attributes to have their bound values. This means that we have + * to re-render the block here. + * To do so, we'll set a flag that this filter checks when invoked to avoid infinite + * recursion. Furthermore, we can unset all of the block's bindings, as we know that + * they have been processed by the time we reach this point. + */ + unset( $instance->parsed_block['attrs']['metadata']['bindings'] ); + $inside_block_bindings_render = true; + $block_content = $instance->render(); + $inside_block_bindings_render = false; + + if ( ! empty( $computed_attributes ) && ! empty( $block_content ) ) { + foreach ( $computed_attributes as $attribute_name => $source_value ) { + $block_content = gutenberg_replace_html( $block_content, $attribute_name, $source_value, $instance->block_type ); + } + } + + return $block_content; +} +add_filter( 'render_block', 'gutenberg_block_bindings_render_block', 10, 3 ); + +/** + * Processes the block bindings and updates the block attributes with the values from the sources. + * + * A block might contain bindings in its attributes. Bindings are mappings + * between an attribute of the block and a source. A "source" is a function + * registered with `register_block_bindings_source()` that defines how to + * retrieve a value from outside the block, e.g. from post meta. + * + * This function will process those bindings and update the block's attributes + * with the values coming from the bindings. + * + * ### Example + * + * The "bindings" property for an Image block might look like this: + * + * ```json + * { + * "metadata": { + * "bindings": { + * "title": { + * "source": "core/post-meta", + * "args": { "key": "text_custom_field" } + * }, + * "url": { + * "source": "core/post-meta", + * "args": { "key": "url_custom_field" } + * } + * } + * } + * } + * ``` + * + * The above example will replace the `title` and `url` attributes of the Image + * block with the values of the `text_custom_field` and `url_custom_field` post meta. + * + * @since 6.9.0 + * + * @param WP_Block $instance The block instance. + * @return array The computed block attributes for the provided block bindings. + */ +function gutenberg_process_block_bindings( $instance ) { + $attributes = $instance->parsed_block['attrs']; + $computed_attributes = array(); + + if ( ! isset( $attributes['metadata']['bindings'] ) ) { + return array(); + } + + // List of block attributes supported by Block Bindings in WP 6.8. + $block_bindings_supported_attributes_6_8 = array( + 'core/paragraph' => array( 'content' ), + 'core/heading' => array( 'content' ), + 'core/image' => array( 'id', 'url', 'title', 'alt' ), + 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), + ); + + $block_type = $instance->name; + $bindings = $attributes['metadata']['bindings']; + + // if ( isset( $block_bindings_supported_attributes_6_8[ $block_type ] ) ) { + // $supported_block_attributes = $block_bindings_supported_attributes_6_8[ $block_type ]; + // // Remove attributes that we know are processed by WP 6.8 from the list. + // // $bindings = array_diff_key( $bindings, $supported_block_attributes ); + // } + + $supported_block_attributes = + $block_bindings_supported_attributes_6_8[ $block_type ] ?? + array(); + + /** + * Filters the supported block attributes for block bindings. + * + * The dynamic portion of the hook name, `$block_type`, refers to the block type + * whose attributes are being filtered. + * + * @since 6.9.0 + * + * @param string[] $supported_block_attributes The block's attributes that are supported by block bindings. + */ + $supported_block_attributes = apply_filters( + "block_bindings_supported_attributes_{$block_type}", + $supported_block_attributes + ); + + // if ( empty( $bindings ) ) { + // return array(); + // } + + // TODO: Include pattern overrides. + + // TODO: Review the following. + foreach ( $bindings as $attribute_name => $block_binding ) { + + // If the attribute is not in the supported list, process next attribute. + if ( ! in_array( $attribute_name, $supported_block_attributes, true ) ) { + continue; + } + + // If the attribute was already processed by Core, process next attribute. + if ( + isset( $block_bindings_supported_attributes_6_8[ $block_type ] ) && + in_array( $attribute_name, $block_bindings_supported_attributes_6_8[ $block_type ], true ) + ) { + continue; + } + + // If no source is provided, or that source is not registered, process next attribute. + if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) ) { + continue; + } + + $block_binding_source = get_block_bindings_source( $block_binding['source'] ); + if ( null === $block_binding_source ) { + continue; + } + + // Problem: No access to available_context. Can we use refresh_context_dependents()? + // That uses $this->block_type->uses_context rather than $instance->block_type->uses_context :/ + + // Adds the necessary context defined by the source. + if ( ! empty( $block_binding_source->uses_context ) ) { + foreach ( $block_binding_source->uses_context as $context_name ) { + // if ( array_key_exists( $context_name, $this->available_context ) ) { + // $instance->context[ $context_name ] = $this->available_context[ $context_name ]; + // } + } + } + + $source_args = ! empty( $block_binding['args'] ) && is_array( $block_binding['args'] ) ? $block_binding['args'] : array(); + $source_value = $block_binding_source->get_value( $source_args, $instance, $attribute_name ); + + // If the value is not null, process the HTML based on the block and the attribute. + if ( ! is_null( $source_value ) ) { + $computed_attributes[ $attribute_name ] = $source_value; + } + } + + return $computed_attributes; +} + +/** + * Depending on the block attribute name, replace its value in the HTML based on the value provided. + * + * @since 6.5.0 + * + * @param string $block_content Block content. + * @param string $attribute_name The attribute name to replace. + * @param mixed $source_value The value used to replace in the HTML. + * @param WP_Block_Type $block_type The block type. + * @return string The modified block content. + */ +function gutenberg_replace_html( string $block_content, string $attribute_name, $source_value, WP_Block_Type $block_type ) { + if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) { + return $block_content; + } + + // Depending on the attribute source, the processing will be different. + switch ( $block_type->attributes[ $attribute_name ]['source'] ) { + case 'html': + case 'rich-text': + $block_reader = gutenberg_get_block_bindings_processor( $block_content ); + + // TODO: Support for CSS selectors whenever they are ready in the HTML API. + // In the meantime, support comma-separated selectors by exploding them into an array. + $selectors = explode( ',', $block_type->attributes[ $attribute_name ]['selector'] ); + // Add a bookmark to the first tag to be able to iterate over the selectors. + $block_reader->next_tag(); + $block_reader->set_bookmark( 'iterate-selectors' ); + + foreach ( $selectors as $selector ) { + // If the parent tag, or any of its children, matches the selector, replace the HTML. + if ( strcasecmp( $block_reader->get_tag(), $selector ) === 0 || $block_reader->next_tag( + array( + 'tag_name' => $selector, + ) + ) ) { + // TODO: Use `WP_HTML_Processor::set_inner_html` method once it's available. + $block_reader->release_bookmark( 'iterate-selectors' ); + $block_reader->replace_rich_text( wp_kses_post( $source_value ) ); + return $block_reader->get_updated_html(); + } else { + $block_reader->seek( 'iterate-selectors' ); + } + } + $block_reader->release_bookmark( 'iterate-selectors' ); + return $block_content; + + case 'attribute': + $amended_content = new WP_HTML_Tag_Processor( $block_content ); + if ( ! $amended_content->next_tag( + array( + // TODO: build the query from CSS selector. + 'tag_name' => $block_type->attributes[ $attribute_name ]['selector'], + ) + ) ) { + return $block_content; + } + $amended_content->set_attribute( $block_type->attributes[ $attribute_name ]['attribute'], $source_value ); + return $amended_content->get_updated_html(); + + default: + return $block_content; + } +} + +function gutenberg_get_block_bindings_processor( string $block_content ) { + $internal_processor_class = new class('', WP_HTML_Processor::CONSTRUCTOR_UNLOCK_CODE) extends WP_HTML_Processor { + /** + * Replace the rich text content between a tag opener and matching closer. + * + * When stopped on a tag opener, replace the content enclosed by it and its + * matching closer with the provided rich text. + * + * @param string $rich_text The rich text to replace the original content with. + * @return bool True on success. + */ + public function replace_rich_text( $rich_text ) { + if ( $this->is_tag_closer() || ! $this->expects_closer() ) { + return false; + } + + $depth = $this->get_current_depth(); + + $this->set_bookmark( '_wp_block_bindings_tag_opener' ); + // The bookmark names are prefixed with `_` so the key below has an extra `_`. + $tag_opener = $this->bookmarks['__wp_block_bindings_tag_opener']; + $start = $tag_opener->start + $tag_opener->length; + $this->release_bookmark( '_wp_block_bindings_tag_opener' ); + + // Find matching tag closer. + while ( $this->next_token() && $this->get_current_depth() >= $depth ) { + } + + $this->set_bookmark( '_wp_block_bindings_tag_closer' ); + $tag_closer = $this->bookmarks['__wp_block_bindings_tag_closer']; + $end = $tag_closer->start; + $this->release_bookmark( '_wp_block_bindings_tag_closer' ); + + $this->lexical_updates[] = new WP_HTML_Text_Replacement( + $start, + $end - $start, + $rich_text + ); + + return true; + } + }; + + return $internal_processor_class::create_fragment( $block_content ); +} diff --git a/lib/load.php b/lib/load.php index 304487f0336a4a..ecf434c37b5e9c 100644 --- a/lib/load.php +++ b/lib/load.php @@ -39,6 +39,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.8/rest-api.php'; // WordPress 6.9 compat. + require __DIR__ . '/compat/wordpress-6.9/block-bindings.php'; require __DIR__ . '/compat/wordpress-6.9/post-data-block-bindings.php'; require __DIR__ . '/compat/wordpress-6.9/rest-api.php'; require __DIR__ . '/compat/wordpress-6.9/class-gutenberg-hierarchical-sort.php'; From 123894dfc1b630a71bcb88a07a374dad67ace7e3 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 28 Aug 2025 18:19:26 +0200 Subject: [PATCH 02/29] Block Bindings: Use fallback for Date block --- .../block-library/src/post-date/index.php | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/block-library/src/post-date/index.php b/packages/block-library/src/post-date/index.php index f2afe770ab8b5a..271c7156031c61 100644 --- a/packages/block-library/src/post-date/index.php +++ b/packages/block-library/src/post-date/index.php @@ -20,19 +20,11 @@ function render_block_core_post_date( $attributes, $content, $block ) { $classes = array(); if ( - isset( $attributes['metadata']['bindings']['datetime']['source'] ) && - isset( $attributes['metadata']['bindings']['datetime']['args'] ) + ! isset( $attributes['datetime'] ) && ! ( + isset( $attributes['metadata']['bindings']['datetime']['source'] ) && + isset( $attributes['metadata']['bindings']['datetime']['args'] ) + ) ) { - /* - * We might be running on a version of WordPress that doesn't support binding the block's `datetime` attribute - * to a Block Bindings source. In this case, we need to manually set the `datetime` attribute to its correct value. - * This branch can be removed once the minimum required WordPress version is 6.9 or newer. - */ - $source = get_block_bindings_source( $attributes['metadata']['bindings']['datetime']['source'] ); - $source_args = $attributes['metadata']['bindings']['datetime']['args']; - - $attributes['datetime'] = $source->get_value( $source_args, $block, 'datetime' ); - } elseif ( ! isset( $attributes['datetime'] ) ) { /* * This is the legacy version of the block that didn't have the `datetime` attribute. * This branch needs to be kept for backward compatibility. @@ -61,7 +53,7 @@ function render_block_core_post_date( $attributes, $content, $block ) { // (See https://github.com/WordPress/gutenberg/pull/46839 where this logic was originally // implemented.) // In this case, we have to respect and return the empty value. - return $attributes['datetime']; + return ''; } $unformatted_date = $attributes['datetime']; @@ -112,5 +104,18 @@ function register_block_core_post_date() { 'render_callback' => 'render_block_core_post_date', ) ); + + // The following filter can be removed once the minimum required WordPress version is 6.9 or newer. + add_filter( + 'block_bindings_supported_attributes_core/post-date', + function ( $attributes ) { + if ( ! in_array( 'datetime', $attributes, true ) ) { + $attributes[] = 'datetime'; + } + return $attributes; + }, + 10, + 3 + ); } add_action( 'init', 'register_block_core_post_date' ); From 38564118004bbd3b94928dce47ed0c57ae9559e7 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 28 Aug 2025 18:25:49 +0200 Subject: [PATCH 03/29] Add backport changelog --- backport-changelog/6.9/9469.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 backport-changelog/6.9/9469.md diff --git a/backport-changelog/6.9/9469.md b/backport-changelog/6.9/9469.md new file mode 100644 index 00000000000000..26fa84c5dd18fe --- /dev/null +++ b/backport-changelog/6.9/9469.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/9469 + +* https://github.com/WordPress/gutenberg/pull/71389 From 572967653e802e54b2a2617a430862f9aae07ddd Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Mon, 1 Sep 2025 14:51:25 +0200 Subject: [PATCH 04/29] In tests, set datetime attribute indirectly. --- phpunit/blocks/render-block-core-post-date.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit/blocks/render-block-core-post-date.php b/phpunit/blocks/render-block-core-post-date.php index b12359d432750d..39f1141eff4797 100644 --- a/phpunit/blocks/render-block-core-post-date.php +++ b/phpunit/blocks/render-block-core-post-date.php @@ -94,7 +94,7 @@ public function test_render_with_date_attribute_binding( $field, $expected_date_ ); // Now verify that a fallback value is overridden by Block Bindings. - $block->attributes['datetime'] = '2025-01-01 00:00:00'; + $block->parsed_block['attrs']['datetime'] = '2025-01-01 00:00:00'; $output = $block->render(); $this->assertStringContainsString( From 6993e7338764dd7f5fadcb8e9115bf3655f3d062 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Mon, 1 Sep 2025 16:10:50 +0200 Subject: [PATCH 05/29] Extract and process context --- lib/compat/wordpress-6.9/block-bindings.php | 25 ++++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/compat/wordpress-6.9/block-bindings.php b/lib/compat/wordpress-6.9/block-bindings.php index 09d3e1e05eefcd..db6d2e50f22ff8 100644 --- a/lib/compat/wordpress-6.9/block-bindings.php +++ b/lib/compat/wordpress-6.9/block-bindings.php @@ -1,4 +1,4 @@ -block_type->uses_context rather than $instance->block_type->uses_context :/ + if ( ! class_exists( 'WP_Block_Context_Extractor' ) ) { + // phpcs:ignore Gutenberg.Commenting.SinceTag.MissingClassSinceTag + class WP_Block_Context_Extractor extends WP_Block { + /** + * Static methods of subclasses have access to protected properties + * of instances of the parent class. + * In this case, this gives us access to `available_context`. + */ + // phpcs:ignore Gutenberg.Commenting.SinceTag.MissingMethodSinceTag + public static function get_available_context( $instance ) { + return $instance->available_context; + } + } + } + $available_context = WP_Block_Context_Extractor::get_available_context( $instance ); // Adds the necessary context defined by the source. if ( ! empty( $block_binding_source->uses_context ) ) { foreach ( $block_binding_source->uses_context as $context_name ) { - // if ( array_key_exists( $context_name, $this->available_context ) ) { - // $instance->context[ $context_name ] = $this->available_context[ $context_name ]; - // } + if ( array_key_exists( $context_name, $available_context ) ) { + $instance->context[ $context_name ] = $available_context[ $context_name ]; + } } } From d0ddc0bf5c4b54e74099cdf4c4a55122a529c721 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Wed, 3 Sep 2025 19:37:21 +0200 Subject: [PATCH 06/29] Add test coverage for new Block Bindings features --- phpunit/block-bindings-test.php | 523 ++++++++++++++++++++++++++++++++ 1 file changed, 523 insertions(+) create mode 100644 phpunit/block-bindings-test.php diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php new file mode 100644 index 00000000000000..b7d68dcdc943f2 --- /dev/null +++ b/phpunit/block-bindings-test.php @@ -0,0 +1,523 @@ + 'Test source', + ); + + /** + * Sets up shared fixtures. + * + * @since 6.9.0 + */ + public static function wpSetUpBeforeClass() { + register_block_type( + 'test/block', + array( + 'attributes' => array( + 'myAttribute' => array( + 'type' => 'string', + ), + ), + 'render_callback' => function ( $attributes ) { + return '

' . esc_html( $attributes['myAttribute'] ) . '

'; + }, + ) + ); + + add_filter( 'render_block', 'gutenberg_block_bindings_render_block', 10, 3 ); + } + + /** + * Tear down after each test. + * + * @since 6.5.0 + */ + public function tear_down() { + remove_filter( 'render_block', 'gutenberg_block_bindings_render_block', 10 ); + + foreach ( get_all_registered_block_bindings_sources() as $source_name => $source_properties ) { + if ( str_starts_with( $source_name, 'test/' ) ) { + unregister_block_bindings_source( $source_name ); + } + } + + parent::tear_down(); + } + + /** + * Tear down after class. + * + * @since 6.9.0 + */ + public static function wpTearDownAfterClass() { + unregister_block_type( 'test/block' ); + } + + public function data_update_block_with_value_from_source() { + return array( + 'paragraph block' => array( + 'content', + << +

This should not appear

+ +HTML + , + '

test source value

', + ), + 'button block' => array( + 'text', + << + + +HTML + , + '', + ), + ); + } + + /** + * Test if the block content is updated with the value returned by the source. + * + * @ticket 60282 + * + * @covers ::register_block_bindings_source + * + * @dataProvider data_update_block_with_value_from_source + */ + public function test_update_block_with_value_from_source( $bound_attribute, $block_content, $expected_result ) { + $get_value_callback = function () { + return 'test source value'; + }; + + register_block_bindings_source( + self::SOURCE_NAME, + array( + 'label' => self::SOURCE_LABEL, + 'get_value_callback' => $get_value_callback, + ) + ); + + $parsed_blocks = parse_blocks( $block_content ); + + $parsed_blocks[0]['attrs']['metadata'] = array( + 'bindings' => array( + $bound_attribute => array( + 'source' => self::SOURCE_NAME, + ), + ), + ); + + $block = new WP_Block( $parsed_blocks[0] ); + $result = $block->render(); + + $this->assertSame( + 'test source value', + $block->attributes[ $bound_attribute ], + "The '{$bound_attribute}' attribute should be updated with the value returned by the source." + ); + $this->assertSame( + $expected_result, + trim( $result ), + 'The block content should be updated with the value returned by the source.' + ); + } + + /** + * Test if the block_bindings_supported_attributes_{$block_type} filter is applied correctly. + * + * @ticket 62090 + */ + public function test_filter_block_bindings_supported_attributes() { + $get_value_callback = function () { + return 'test source value'; + }; + + register_block_bindings_source( + self::SOURCE_NAME, + array( + 'label' => self::SOURCE_LABEL, + 'get_value_callback' => $get_value_callback, + ) + ); + + add_filter( + 'block_bindings_supported_attributes_test/block', + function ( $supported_attributes ) { + $supported_attributes[] = 'myAttribute'; + return $supported_attributes; + } + ); + + $block_content = << +

This should not appear

+ +HTML; + $parsed_blocks = parse_blocks( $block_content ); + $block = new WP_Block( $parsed_blocks[0] ); + $result = $block->render(); + + $this->assertSame( + 'test source value', + $block->attributes['myAttribute'], + "The 'myAttribute' attribute should be updated with the value returned by the source." + ); + $this->assertSame( + '

test source value

', + trim( $result ), + 'The block content should be updated with the value returned by the source.' + ); + } + + /** + * Test passing arguments to the source. + * + * @ticket 60282 + * + * @covers ::register_block_bindings_source + */ + public function test_passing_arguments_to_source() { + $get_value_callback = function ( $source_args, $block_instance, $attribute_name ) { + $value = $source_args['key']; + return "The attribute name is '$attribute_name' and its binding has argument 'key' with value '$value'."; + }; + + register_block_bindings_source( + self::SOURCE_NAME, + array( + 'label' => self::SOURCE_LABEL, + 'get_value_callback' => $get_value_callback, + ) + ); + + $block_content = << +

This should not appear

+ +HTML; + $parsed_blocks = parse_blocks( $block_content ); + $block = new WP_Block( $parsed_blocks[0] ); + $result = $block->render(); + + $this->assertSame( + "The attribute name is 'content' and its binding has argument 'key' with value 'test'.", + $block->attributes['content'], + "The 'content' attribute should be updated with the value returned by the source." + ); + $this->assertSame( + "

The attribute name is 'content' and its binding has argument 'key' with value 'test'.

", + trim( $result ), + 'The block content should be updated with the value returned by the source.' + ); + } + + /** + * Tests passing `uses_context` as argument to the source. + * + * @ticket 60525 + * + * @covers ::register_block_bindings_source + */ + public function test_passing_uses_context_to_source() { + $get_value_callback = function ( $source_args, $block_instance, $attribute_name ) { + $value = $block_instance->context['sourceContext']; + return "Value: $value"; + }; + + register_block_bindings_source( + self::SOURCE_NAME, + array( + 'label' => self::SOURCE_LABEL, + 'get_value_callback' => $get_value_callback, + 'uses_context' => array( 'sourceContext' ), + ) + ); + + $block_content = << +

This should not appear

+ +HTML; + $parsed_blocks = parse_blocks( $block_content ); + $block = new WP_Block( $parsed_blocks[0], array( 'sourceContext' => 'source context value' ) ); + $result = $block->render(); + + $this->assertSame( + 'Value: source context value', + $block->attributes['content'], + "The 'content' should be updated with the value of the source context." + ); + $this->assertSame( + '

Value: source context value

', + trim( $result ), + 'The block content should be updated with the value of the source context.' + ); + } + + /** + * Tests that blocks can only access the context from the specific source. + * + * @ticket 61642 + * + * @covers ::register_block_bindings_source + */ + public function test_blocks_can_just_access_the_specific_uses_context() { + register_block_bindings_source( + 'test/source-one', + array( + 'label' => 'Test Source One', + 'get_value_callback' => function () { + return; + }, + 'uses_context' => array( 'contextOne' ), + ) + ); + + register_block_bindings_source( + 'test/source-two', + array( + 'label' => 'Test Source Two', + 'get_value_callback' => function ( $source_args, $block_instance, $attribute_name ) { + $value = $block_instance->context['contextTwo']; + // Try to use the context from source one, which shouldn't be available. + if ( ! empty( $block_instance->context['contextOne'] ) ) { + $value = $block_instance->context['contextOne']; + } + return "Value: $value"; + }, + 'uses_context' => array( 'contextTwo' ), + ) + ); + + $block_content = << +

Default content

+ +HTML; + $parsed_blocks = parse_blocks( $block_content ); + $block = new WP_Block( + $parsed_blocks[0], + array( + 'contextOne' => 'source one context value', + 'contextTwo' => 'source two context value', + ) + ); + $result = $block->render(); + + $this->assertSame( + 'Value: source two context value', + $block->attributes['content'], + "The 'content' should be updated with the value of the second source context value." + ); + $this->assertSame( + '

Value: source two context value

', + trim( $result ), + 'The block content should be updated with the value of the source context.' + ); + } + + /** + * Tests if the block content is updated with the value returned by the source + * for the Image block in the placeholder state. + * + * @ticket 60282 + * + * @covers ::register_block_bindings_source + */ + public function test_update_block_with_value_from_source_image_placeholder() { + $get_value_callback = function () { + return 'https://example.com/image.jpg'; + }; + + register_block_bindings_source( + self::SOURCE_NAME, + array( + 'label' => self::SOURCE_LABEL, + 'get_value_callback' => $get_value_callback, + ) + ); + + $block_content = << +
+ +HTML; + $parsed_blocks = parse_blocks( $block_content ); + $block = new WP_Block( $parsed_blocks[0] ); + $result = $block->render(); + + $this->assertSame( + 'https://example.com/image.jpg', + $block->attributes['url'], + "The 'url' attribute should be updated with the value returned by the source." + ); + $this->assertSame( + '
', + trim( $result ), + 'The block content should be updated with the value returned by the source.' + ); + } + + /** + * Tests if the block content is sanitized when unsafe HTML is passed. + * + * @ticket 60651 + * + * @covers ::register_block_bindings_source + */ + public function test_source_value_with_unsafe_html_is_sanitized() { + $get_value_callback = function () { + return ''; + }; + + register_block_bindings_source( + self::SOURCE_NAME, + array( + 'label' => self::SOURCE_LABEL, + 'get_value_callback' => $get_value_callback, + ) + ); + + $block_content = << +

This should not appear

+ +HTML; + $parsed_blocks = parse_blocks( $block_content ); + $block = new WP_Block( $parsed_blocks[0] ); + $result = $block->render(); + + $this->assertSame( + '

alert("Unsafe HTML")

', + trim( $result ), + 'The block content should be updated with the value returned by the source.' + ); + } + + /** + * Tests that including symbols and numbers works well with bound attributes. + * + * @ticket 61385 + * + * @covers WP_Block::process_block_bindings + */ + public function test_using_symbols_in_block_bindings_value() { + $get_value_callback = function () { + return '$12.50'; + }; + + register_block_bindings_source( + self::SOURCE_NAME, + array( + 'label' => self::SOURCE_LABEL, + 'get_value_callback' => $get_value_callback, + ) + ); + + $block_content = << +

Default content

+ +HTML; + $parsed_blocks = parse_blocks( $block_content ); + $block = new WP_Block( $parsed_blocks[0] ); + $result = $block->render(); + + $this->assertSame( + '

$12.50

', + trim( $result ), + 'The block content should properly show the symbol and numbers.' + ); + } + + /** + * Tests if the `__default` attribute is replaced with real attributes for + * pattern overrides. + * + * @ticket 61333 + * @ticket 62069 + * + * @covers WP_Block::process_block_bindings + */ + public function test_default_binding_for_pattern_overrides() { + $block_content = << +

This should not appear

+ +HTML; + + $expected_content = 'This is the content value'; + $parsed_blocks = parse_blocks( $block_content ); + $block = new WP_Block( $parsed_blocks[0], array( 'pattern/overrides' => array( 'Test' => array( 'content' => $expected_content ) ) ) ); + + $result = $block->render(); + + $this->assertSame( + "

$expected_content

", + trim( $result ), + 'The `__default` attribute should be replaced with the real attribute prior to the callback.' + ); + + $expected_bindings_metadata = array( + 'content' => array( 'source' => 'core/pattern-overrides' ), + ); + $this->assertSame( + $expected_bindings_metadata, + $block->attributes['metadata']['bindings'], + 'The __default binding should be updated with the individual binding attributes in the block metadata.' + ); + } + + /** + * Tests that filter `block_bindings_source_value` is applied. + * + * @ticket 61181 + */ + public function test_filter_block_bindings_source_value() { + register_block_bindings_source( + self::SOURCE_NAME, + array( + 'label' => self::SOURCE_LABEL, + 'get_value_callback' => function () { + return ''; + }, + ) + ); + + $filter_value = function ( $value, $source_name, $source_args, $block_instance, $attribute_name ) { + if ( self::SOURCE_NAME !== $source_name ) { + return $value; + } + return "Filtered value: {$source_args['test_key']}. Block instance: {$block_instance->name}. Attribute name: {$attribute_name}."; + }; + + add_filter( 'block_bindings_source_value', $filter_value, 10, 5 ); + + $block_content = << +

Default content

+ +HTML; + $parsed_blocks = parse_blocks( $block_content ); + $block = new WP_Block( $parsed_blocks[0] ); + $result = $block->render(); + + remove_filter( 'block_bindings_source_value', $filter_value ); + + $this->assertSame( + '

Filtered value: test_arg. Block instance: core/paragraph. Attribute name: content.

', + trim( $result ), + 'The block content should show the filtered value.' + ); + } +} From abc4bf6cb3a958ee75d8cef76433632cd6fde2a8 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Wed, 3 Sep 2025 19:37:40 +0200 Subject: [PATCH 07/29] Apply new Block Bindings logic to core/button block --- lib/compat/wordpress-6.9/block-bindings.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.9/block-bindings.php b/lib/compat/wordpress-6.9/block-bindings.php index db6d2e50f22ff8..6e75480375bc31 100644 --- a/lib/compat/wordpress-6.9/block-bindings.php +++ b/lib/compat/wordpress-6.9/block-bindings.php @@ -157,7 +157,8 @@ function gutenberg_process_block_bindings( $instance ) { // If the attribute was already processed by Core, process next attribute. if ( isset( $block_bindings_supported_attributes_6_8[ $block_type ] ) && - in_array( $attribute_name, $block_bindings_supported_attributes_6_8[ $block_type ], true ) + in_array( $attribute_name, $block_bindings_supported_attributes_6_8[ $block_type ], true ) && + 'core/button' !== $block_type // ... except for the Button block, as WP 6.8 capitalizes the tag name (e.g.
). ) { continue; } From 67a1f32213aa17e6743ba6a5c33d81f03851d5a5 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Wed, 3 Sep 2025 19:56:40 +0200 Subject: [PATCH 08/29] Move remove_filter() from tear_down to wpTearDownAfterClass --- phpunit/block-bindings-test.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index b7d68dcdc943f2..de41c6a65d2824 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -40,8 +40,6 @@ public static function wpSetUpBeforeClass() { * @since 6.5.0 */ public function tear_down() { - remove_filter( 'render_block', 'gutenberg_block_bindings_render_block', 10 ); - foreach ( get_all_registered_block_bindings_sources() as $source_name => $source_properties ) { if ( str_starts_with( $source_name, 'test/' ) ) { unregister_block_bindings_source( $source_name ); @@ -57,6 +55,8 @@ public function tear_down() { * @since 6.9.0 */ public static function wpTearDownAfterClass() { + remove_filter( 'render_block', 'gutenberg_block_bindings_render_block', 10 ); + unregister_block_type( 'test/block' ); } From 57d8cbebf120f177bf053d2be3b0e66394c05b82 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Wed, 3 Sep 2025 20:04:19 +0200 Subject: [PATCH 09/29] Guard render_callback against unset attribute --- phpunit/block-bindings-test.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index de41c6a65d2824..a543eaaad76f1d 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -26,7 +26,9 @@ public static function wpSetUpBeforeClass() { ), ), 'render_callback' => function ( $attributes ) { - return '

' . esc_html( $attributes['myAttribute'] ) . '

'; + if ( isset( $attributes['myAttribute'] ) ) { + return '

' . esc_html( $attributes['myAttribute'] ) . '

'; + } }, ) ); From 378c48c789c310bebec3019a903398408e58a97e Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 10:04:19 +0200 Subject: [PATCH 10/29] Minor comment clarification --- lib/compat/wordpress-6.9/block-bindings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.9/block-bindings.php b/lib/compat/wordpress-6.9/block-bindings.php index 6e75480375bc31..9291c63a7eebcc 100644 --- a/lib/compat/wordpress-6.9/block-bindings.php +++ b/lib/compat/wordpress-6.9/block-bindings.php @@ -158,7 +158,7 @@ function gutenberg_process_block_bindings( $instance ) { if ( isset( $block_bindings_supported_attributes_6_8[ $block_type ] ) && in_array( $attribute_name, $block_bindings_supported_attributes_6_8[ $block_type ], true ) && - 'core/button' !== $block_type // ... except for the Button block, as WP 6.8 capitalizes the tag name (e.g.
). + 'core/button' !== $block_type // ... except for the Button block, as WP 6.8 capitalizes its tag name (e.g.
). ) { continue; } From f42e6d7f0e7df7b9008b8d77b82e0612a25231b7 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 10:10:24 +0200 Subject: [PATCH 11/29] No need to add filter during setup --- phpunit/block-bindings-test.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index a543eaaad76f1d..c69860b7dfbb3a 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -32,8 +32,6 @@ public static function wpSetUpBeforeClass() { }, ) ); - - add_filter( 'render_block', 'gutenberg_block_bindings_render_block', 10, 3 ); } /** @@ -57,8 +55,6 @@ public function tear_down() { * @since 6.9.0 */ public static function wpTearDownAfterClass() { - remove_filter( 'render_block', 'gutenberg_block_bindings_render_block', 10 ); - unregister_block_type( 'test/block' ); } From adcd73781e10b83de332bae61b879127d90dec43 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 10:29:09 +0200 Subject: [PATCH 12/29] Massage code to look more like Core --- lib/compat/wordpress-6.9/block-bindings.php | 36 ++++++++++++--------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/compat/wordpress-6.9/block-bindings.php b/lib/compat/wordpress-6.9/block-bindings.php index 9291c63a7eebcc..79a8b8418ea602 100644 --- a/lib/compat/wordpress-6.9/block-bindings.php +++ b/lib/compat/wordpress-6.9/block-bindings.php @@ -97,13 +97,10 @@ function gutenberg_block_bindings_render_block( $block_content, $block, $instanc * @return array The computed block attributes for the provided block bindings. */ function gutenberg_process_block_bindings( $instance ) { - $attributes = $instance->parsed_block['attrs']; + $block_type = $instance->name; + $parsed_block = $instance->parsed_block; $computed_attributes = array(); - if ( ! isset( $attributes['metadata']['bindings'] ) ) { - return array(); - } - // List of block attributes supported by Block Bindings in WP 6.8. $block_bindings_supported_attributes_6_8 = array( 'core/paragraph' => array( 'content' ), @@ -111,16 +108,6 @@ function gutenberg_process_block_bindings( $instance ) { 'core/image' => array( 'id', 'url', 'title', 'alt' ), 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), ); - - $block_type = $instance->name; - $bindings = $attributes['metadata']['bindings']; - - // if ( isset( $block_bindings_supported_attributes_6_8[ $block_type ] ) ) { - // $supported_block_attributes = $block_bindings_supported_attributes_6_8[ $block_type ]; - // // Remove attributes that we know are processed by WP 6.8 from the list. - // // $bindings = array_diff_key( $bindings, $supported_block_attributes ); - // } - $supported_block_attributes = $block_bindings_supported_attributes_6_8[ $block_type ] ?? array(); @@ -140,6 +127,25 @@ function gutenberg_process_block_bindings( $instance ) { $supported_block_attributes ); + // If the block doesn't have the bindings property, isn't one of the supported + // block types, or the bindings property is not an array, return the block content. + if ( + empty( $supported_block_attributes ) || + empty( $parsed_block['attrs']['metadata']['bindings'] ) || + ! is_array( $parsed_block['attrs']['metadata']['bindings'] ) + ) { + return $computed_attributes; + } + + $bindings = $parsed_block['attrs']['metadata']['bindings']; + + // if ( isset( $block_bindings_supported_attributes_6_8[ $block_type ] ) ) { + // $supported_block_attributes = $block_bindings_supported_attributes_6_8[ $block_type ]; + // // Remove attributes that we know are processed by WP 6.8 from the list. + // // $bindings = array_diff_key( $bindings, $supported_block_attributes ); + // } + + // if ( empty( $bindings ) ) { // return array(); // } From 3ac63b33c03513e67856a21fb38f6f476c702a79 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 10:54:34 +0200 Subject: [PATCH 13/29] Remove attributes that were handled by 6.8 early --- lib/compat/wordpress-6.9/block-bindings.php | 28 +++++++++------------ 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/lib/compat/wordpress-6.9/block-bindings.php b/lib/compat/wordpress-6.9/block-bindings.php index 79a8b8418ea602..6fdcd334f3d49b 100644 --- a/lib/compat/wordpress-6.9/block-bindings.php +++ b/lib/compat/wordpress-6.9/block-bindings.php @@ -127,6 +127,18 @@ function gutenberg_process_block_bindings( $instance ) { $supported_block_attributes ); + /* + * Remove attributes that we know are processed by WP 6.8 from the list, + * except if we're dealing with the button block, since WP 6.8 capitalizes its + * tag name (e.g.
). + */ + if ( 'core/button' !== $block_type && isset( $block_bindings_supported_attributes_6_8[ $block_type ] ) ) { + $supported_block_attributes = array_diff( + $supported_block_attributes, + $block_bindings_supported_attributes_6_8[ $block_type ] + ); + } + // If the block doesn't have the bindings property, isn't one of the supported // block types, or the bindings property is not an array, return the block content. if ( @@ -139,13 +151,6 @@ function gutenberg_process_block_bindings( $instance ) { $bindings = $parsed_block['attrs']['metadata']['bindings']; - // if ( isset( $block_bindings_supported_attributes_6_8[ $block_type ] ) ) { - // $supported_block_attributes = $block_bindings_supported_attributes_6_8[ $block_type ]; - // // Remove attributes that we know are processed by WP 6.8 from the list. - // // $bindings = array_diff_key( $bindings, $supported_block_attributes ); - // } - - // if ( empty( $bindings ) ) { // return array(); // } @@ -160,15 +165,6 @@ function gutenberg_process_block_bindings( $instance ) { continue; } - // If the attribute was already processed by Core, process next attribute. - if ( - isset( $block_bindings_supported_attributes_6_8[ $block_type ] ) && - in_array( $attribute_name, $block_bindings_supported_attributes_6_8[ $block_type ], true ) && - 'core/button' !== $block_type // ... except for the Button block, as WP 6.8 capitalizes its tag name (e.g.
). - ) { - continue; - } - // If no source is provided, or that source is not registered, process next attribute. if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) ) { continue; From 4f121844dd72fb07912c5fd34cd52d97cf1a6c36 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 11:11:03 +0200 Subject: [PATCH 14/29] Update pattern overrides test to make sure they work with newly supported block attributes --- phpunit/block-bindings-test.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index c69860b7dfbb3a..29fa0bda936a02 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -448,15 +448,23 @@ public function test_using_symbols_in_block_bindings_value() { * @covers WP_Block::process_block_bindings */ public function test_default_binding_for_pattern_overrides() { + add_filter( + 'block_bindings_supported_attributes_test/block', + function ( $supported_attributes ) { + $supported_attributes[] = 'myAttribute'; + return $supported_attributes; + } + ); + $block_content = << +

This should not appear

- + HTML; $expected_content = 'This is the content value'; $parsed_blocks = parse_blocks( $block_content ); - $block = new WP_Block( $parsed_blocks[0], array( 'pattern/overrides' => array( 'Test' => array( 'content' => $expected_content ) ) ) ); + $block = new WP_Block( $parsed_blocks[0], array( 'pattern/overrides' => array( 'Test' => array( 'myAttribute' => $expected_content ) ) ) ); $result = $block->render(); @@ -467,7 +475,7 @@ public function test_default_binding_for_pattern_overrides() { ); $expected_bindings_metadata = array( - 'content' => array( 'source' => 'core/pattern-overrides' ), + 'myAttribute' => array( 'source' => 'core/pattern-overrides' ), ); $this->assertSame( $expected_bindings_metadata, From b77c107c6b8543a5c575f3b1aeb348fa5ad1036a Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 11:11:31 +0200 Subject: [PATCH 15/29] Implement support for pattern overrides in fallback --- lib/compat/wordpress-6.9/block-bindings.php | 35 ++++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/compat/wordpress-6.9/block-bindings.php b/lib/compat/wordpress-6.9/block-bindings.php index 6fdcd334f3d49b..c864abb2d48814 100644 --- a/lib/compat/wordpress-6.9/block-bindings.php +++ b/lib/compat/wordpress-6.9/block-bindings.php @@ -151,13 +151,38 @@ function gutenberg_process_block_bindings( $instance ) { $bindings = $parsed_block['attrs']['metadata']['bindings']; - // if ( empty( $bindings ) ) { - // return array(); - // } + /* + * If the default binding is set for pattern overrides, replace it + * with a pattern override binding for all supported attributes. + */ + if ( + isset( $bindings['__default']['source'] ) && + 'core/pattern-overrides' === $bindings['__default']['source'] + ) { + $updated_bindings = array(); - // TODO: Include pattern overrides. + /* + * Build a binding array of all supported attributes. + * Note that this also omits the `__default` attribute from the + * resulting array. + */ + foreach ( $supported_block_attributes as $attribute_name ) { + // Retain any non-pattern override bindings that might be present. + $updated_bindings[ $attribute_name ] = isset( $bindings[ $attribute_name ] ) + ? $bindings[ $attribute_name ] + : array( 'source' => 'core/pattern-overrides' ); + } + $bindings = $updated_bindings; + /* + * Update the bindings metadata of the computed attributes. + * This ensures the block receives the expanded __default binding metadata when it renders. + */ + $computed_attributes['metadata'] = array_merge( + $parsed_block['attrs']['metadata'], + array( 'bindings' => $bindings ) + ); + } - // TODO: Review the following. foreach ( $bindings as $attribute_name => $block_binding ) { // If the attribute is not in the supported list, process next attribute. From 7178e53f0c62eb3dc07d8397e7d73bca1dcd6d9b Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 11:15:22 +0200 Subject: [PATCH 16/29] Whitespace tweaks --- lib/compat/wordpress-6.9/block-bindings.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/compat/wordpress-6.9/block-bindings.php b/lib/compat/wordpress-6.9/block-bindings.php index c864abb2d48814..9504cfb2330225 100644 --- a/lib/compat/wordpress-6.9/block-bindings.php +++ b/lib/compat/wordpress-6.9/block-bindings.php @@ -184,12 +184,10 @@ function gutenberg_process_block_bindings( $instance ) { } foreach ( $bindings as $attribute_name => $block_binding ) { - // If the attribute is not in the supported list, process next attribute. if ( ! in_array( $attribute_name, $supported_block_attributes, true ) ) { continue; } - // If no source is provided, or that source is not registered, process next attribute. if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) ) { continue; From 5954aaaa292f2e126f5c96c2d0ab69827e0ad75a Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 13:39:40 +0200 Subject: [PATCH 17/29] Update context test to make sure they work with newly supported block attributes --- phpunit/block-bindings-test.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index 29fa0bda936a02..b77bf3b04eaddb 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -227,6 +227,14 @@ public function test_passing_arguments_to_source() { * @covers ::register_block_bindings_source */ public function test_passing_uses_context_to_source() { + add_filter( + 'block_bindings_supported_attributes_test/block', + function ( $supported_attributes ) { + $supported_attributes[] = 'myAttribute'; + return $supported_attributes; + } + ); + $get_value_callback = function ( $source_args, $block_instance, $attribute_name ) { $value = $block_instance->context['sourceContext']; return "Value: $value"; @@ -242,9 +250,9 @@ public function test_passing_uses_context_to_source() { ); $block_content = << +

This should not appear

- + HTML; $parsed_blocks = parse_blocks( $block_content ); $block = new WP_Block( $parsed_blocks[0], array( 'sourceContext' => 'source context value' ) ); @@ -252,8 +260,8 @@ public function test_passing_uses_context_to_source() { $this->assertSame( 'Value: source context value', - $block->attributes['content'], - "The 'content' should be updated with the value of the source context." + $block->attributes['myAttribute'], + "The 'myAttribute' should be updated with the value of the source context." ); $this->assertSame( '

Value: source context value

', From 5bfa0bbe7f83d65df6f09586574b1d42013301d5 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 13:47:08 +0200 Subject: [PATCH 18/29] Add block_bindings_supported_attributes filter during setup, absorb test into data provider --- phpunit/block-bindings-test.php | 81 ++++++++------------------------- 1 file changed, 18 insertions(+), 63 deletions(-) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index b77bf3b04eaddb..091587e8e84ff5 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -32,6 +32,14 @@ public static function wpSetUpBeforeClass() { }, ) ); + + add_filter( + 'block_bindings_supported_attributes_test/block', + function ( $supported_attributes ) { + $supported_attributes[] = 'myAttribute'; + return $supported_attributes; + } + ); } /** @@ -80,6 +88,16 @@ public function data_update_block_with_value_from_source() { , '', ), + 'test block' => array( + 'myAttribute', + << +

This should not appear

+ +HTML + , + '

test source value

', + ), ); } @@ -130,53 +148,6 @@ public function test_update_block_with_value_from_source( $bound_attribute, $blo ); } - /** - * Test if the block_bindings_supported_attributes_{$block_type} filter is applied correctly. - * - * @ticket 62090 - */ - public function test_filter_block_bindings_supported_attributes() { - $get_value_callback = function () { - return 'test source value'; - }; - - register_block_bindings_source( - self::SOURCE_NAME, - array( - 'label' => self::SOURCE_LABEL, - 'get_value_callback' => $get_value_callback, - ) - ); - - add_filter( - 'block_bindings_supported_attributes_test/block', - function ( $supported_attributes ) { - $supported_attributes[] = 'myAttribute'; - return $supported_attributes; - } - ); - - $block_content = << -

This should not appear

- -HTML; - $parsed_blocks = parse_blocks( $block_content ); - $block = new WP_Block( $parsed_blocks[0] ); - $result = $block->render(); - - $this->assertSame( - 'test source value', - $block->attributes['myAttribute'], - "The 'myAttribute' attribute should be updated with the value returned by the source." - ); - $this->assertSame( - '

test source value

', - trim( $result ), - 'The block content should be updated with the value returned by the source.' - ); - } - /** * Test passing arguments to the source. * @@ -227,14 +198,6 @@ public function test_passing_arguments_to_source() { * @covers ::register_block_bindings_source */ public function test_passing_uses_context_to_source() { - add_filter( - 'block_bindings_supported_attributes_test/block', - function ( $supported_attributes ) { - $supported_attributes[] = 'myAttribute'; - return $supported_attributes; - } - ); - $get_value_callback = function ( $source_args, $block_instance, $attribute_name ) { $value = $block_instance->context['sourceContext']; return "Value: $value"; @@ -456,14 +419,6 @@ public function test_using_symbols_in_block_bindings_value() { * @covers WP_Block::process_block_bindings */ public function test_default_binding_for_pattern_overrides() { - add_filter( - 'block_bindings_supported_attributes_test/block', - function ( $supported_attributes ) { - $supported_attributes[] = 'myAttribute'; - return $supported_attributes; - } - ); - $block_content = <<

This should not appear

From dfdafb4de93e5fad7e77bd2d4699504034ade046 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 13:48:36 +0200 Subject: [PATCH 19/29] Whitespace --- lib/compat/wordpress-6.9/block-bindings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.9/block-bindings.php b/lib/compat/wordpress-6.9/block-bindings.php index 9504cfb2330225..b5c741a9171fb0 100644 --- a/lib/compat/wordpress-6.9/block-bindings.php +++ b/lib/compat/wordpress-6.9/block-bindings.php @@ -108,7 +108,7 @@ function gutenberg_process_block_bindings( $instance ) { 'core/image' => array( 'id', 'url', 'title', 'alt' ), 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), ); - $supported_block_attributes = + $supported_block_attributes = $block_bindings_supported_attributes_6_8[ $block_type ] ?? array(); From 8e22cdb46e41849bdec0a94e922acaf37cfeb2f8 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 13:56:34 +0200 Subject: [PATCH 20/29] Switch another test to test/block --- phpunit/block-bindings-test.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index 091587e8e84ff5..b37b9e0eb8c053 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -269,9 +269,9 @@ public function test_blocks_can_just_access_the_specific_uses_context() { ); $block_content = << +

Default content

- + HTML; $parsed_blocks = parse_blocks( $block_content ); $block = new WP_Block( @@ -285,8 +285,8 @@ public function test_blocks_can_just_access_the_specific_uses_context() { $this->assertSame( 'Value: source two context value', - $block->attributes['content'], - "The 'content' should be updated with the value of the second source context value." + $block->attributes['myAttribute'], + "The 'myAttribute' should be updated with the value of the second source context value." ); $this->assertSame( '

Value: source two context value

', From 8b28f221cd4c7b2add8c753fa5ac2dea34610b2b Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 14:01:35 +0200 Subject: [PATCH 21/29] Remove unnecessary source declaration --- phpunit/block-bindings-test.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index b37b9e0eb8c053..22ba9fbf43a185 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -241,17 +241,6 @@ public function test_passing_uses_context_to_source() { * @covers ::register_block_bindings_source */ public function test_blocks_can_just_access_the_specific_uses_context() { - register_block_bindings_source( - 'test/source-one', - array( - 'label' => 'Test Source One', - 'get_value_callback' => function () { - return; - }, - 'uses_context' => array( 'contextOne' ), - ) - ); - register_block_bindings_source( 'test/source-two', array( From d0db38f5618c5fad99bf1eb84961e9ec12e3535c Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 14:08:33 +0200 Subject: [PATCH 22/29] Merge context tests --- phpunit/block-bindings-test.php | 61 ++++++--------------------------- 1 file changed, 11 insertions(+), 50 deletions(-) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index 22ba9fbf43a185..e39328e12e117e 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -194,11 +194,17 @@ public function test_passing_arguments_to_source() { * Tests passing `uses_context` as argument to the source. * * @ticket 60525 + * @ticket 61642 * * @covers ::register_block_bindings_source */ public function test_passing_uses_context_to_source() { $get_value_callback = function ( $source_args, $block_instance, $attribute_name ) { + $this->assertArrayNotHasKey( + 'forbiddenSourceContext', + $block_instance->context, + "Only context that was made available through the source's uses_context property should be accessible." + ); $value = $block_instance->context['sourceContext']; return "Value: $value"; }; @@ -216,69 +222,24 @@ public function test_passing_uses_context_to_source() {

This should not appear

-HTML; - $parsed_blocks = parse_blocks( $block_content ); - $block = new WP_Block( $parsed_blocks[0], array( 'sourceContext' => 'source context value' ) ); - $result = $block->render(); - - $this->assertSame( - 'Value: source context value', - $block->attributes['myAttribute'], - "The 'myAttribute' should be updated with the value of the source context." - ); - $this->assertSame( - '

Value: source context value

', - trim( $result ), - 'The block content should be updated with the value of the source context.' - ); - } - - /** - * Tests that blocks can only access the context from the specific source. - * - * @ticket 61642 - * - * @covers ::register_block_bindings_source - */ - public function test_blocks_can_just_access_the_specific_uses_context() { - register_block_bindings_source( - 'test/source-two', - array( - 'label' => 'Test Source Two', - 'get_value_callback' => function ( $source_args, $block_instance, $attribute_name ) { - $value = $block_instance->context['contextTwo']; - // Try to use the context from source one, which shouldn't be available. - if ( ! empty( $block_instance->context['contextOne'] ) ) { - $value = $block_instance->context['contextOne']; - } - return "Value: $value"; - }, - 'uses_context' => array( 'contextTwo' ), - ) - ); - - $block_content = << -

Default content

- HTML; $parsed_blocks = parse_blocks( $block_content ); $block = new WP_Block( $parsed_blocks[0], array( - 'contextOne' => 'source one context value', - 'contextTwo' => 'source two context value', + 'sourceContext' => 'source context value', + 'forbiddenSourceContext' => 'forbidden donut', ) ); $result = $block->render(); $this->assertSame( - 'Value: source two context value', + 'Value: source context value', $block->attributes['myAttribute'], - "The 'myAttribute' should be updated with the value of the second source context value." + "The 'myAttribute' should be updated with the value of the source context." ); $this->assertSame( - '

Value: source two context value

', + '

Value: source context value

', trim( $result ), 'The block content should be updated with the value of the source context.' ); From 0e398156ce390d2210a863ef3ee4b60f035952cf Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 14:29:47 +0200 Subject: [PATCH 23/29] Merge tests for different get_value_callbacks --- phpunit/block-bindings-test.php | 114 +++++++++----------------------- 1 file changed, 30 insertions(+), 84 deletions(-) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index e39328e12e117e..62db817334339c 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -148,19 +148,42 @@ public function test_update_block_with_value_from_source( $bound_attribute, $blo ); } + public function data_different_get_value_callbacks() { + return array( + 'pass arguments to source' => array( + function ( $source_args, $block_instance, $attribute_name ) { + $value = $source_args['key']; + return "The attribute name is '$attribute_name' and its binding has argument 'key' with value '$value'."; + }, + "

The attribute name is 'content' and its binding has argument 'key' with value 'test'.

", + ), + 'unsafe HTML should be sanitized' => array( + function () { + return ''; + }, + '

alert("Unsafe HTML")

', + ), + 'symbols and numbers should be rendered correctly' => array( + function () { + return '$12.50'; + }, + '

$12.50

', + ), + ); + } + /** * Test passing arguments to the source. * * @ticket 60282 + * @ticket 60651 + * @ticket 61385 * * @covers ::register_block_bindings_source + * + * @dataProvider data_different_get_value_callbacks */ - public function test_passing_arguments_to_source() { - $get_value_callback = function ( $source_args, $block_instance, $attribute_name ) { - $value = $source_args['key']; - return "The attribute name is '$attribute_name' and its binding has argument 'key' with value '$value'."; - }; - + public function test_different_get_value_callbacks( $get_value_callback, $expected ) { register_block_bindings_source( self::SOURCE_NAME, array( @@ -179,12 +202,7 @@ public function test_passing_arguments_to_source() { $result = $block->render(); $this->assertSame( - "The attribute name is 'content' and its binding has argument 'key' with value 'test'.", - $block->attributes['content'], - "The 'content' attribute should be updated with the value returned by the source." - ); - $this->assertSame( - "

The attribute name is 'content' and its binding has argument 'key' with value 'test'.

", + $expected, trim( $result ), 'The block content should be updated with the value returned by the source.' ); @@ -287,78 +305,6 @@ public function test_update_block_with_value_from_source_image_placeholder() { ); } - /** - * Tests if the block content is sanitized when unsafe HTML is passed. - * - * @ticket 60651 - * - * @covers ::register_block_bindings_source - */ - public function test_source_value_with_unsafe_html_is_sanitized() { - $get_value_callback = function () { - return ''; - }; - - register_block_bindings_source( - self::SOURCE_NAME, - array( - 'label' => self::SOURCE_LABEL, - 'get_value_callback' => $get_value_callback, - ) - ); - - $block_content = << -

This should not appear

- -HTML; - $parsed_blocks = parse_blocks( $block_content ); - $block = new WP_Block( $parsed_blocks[0] ); - $result = $block->render(); - - $this->assertSame( - '

alert("Unsafe HTML")

', - trim( $result ), - 'The block content should be updated with the value returned by the source.' - ); - } - - /** - * Tests that including symbols and numbers works well with bound attributes. - * - * @ticket 61385 - * - * @covers WP_Block::process_block_bindings - */ - public function test_using_symbols_in_block_bindings_value() { - $get_value_callback = function () { - return '$12.50'; - }; - - register_block_bindings_source( - self::SOURCE_NAME, - array( - 'label' => self::SOURCE_LABEL, - 'get_value_callback' => $get_value_callback, - ) - ); - - $block_content = << -

Default content

- -HTML; - $parsed_blocks = parse_blocks( $block_content ); - $block = new WP_Block( $parsed_blocks[0] ); - $result = $block->render(); - - $this->assertSame( - '

$12.50

', - trim( $result ), - 'The block content should properly show the symbol and numbers.' - ); - } - /** * Tests if the `__default` attribute is replaced with real attributes for * pattern overrides. From 9f5a12e25bfe8e1e73701d53ff7d1d98b49320ab Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 21:18:56 +0200 Subject: [PATCH 24/29] Move block_bindings_supported_attributes filter to set_up() --- phpunit/block-bindings-test.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index 62db817334339c..6fc6ead80d3029 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -32,6 +32,10 @@ public static function wpSetUpBeforeClass() { }, ) ); + } + + public function set_up() { + parent::set_up(); add_filter( 'block_bindings_supported_attributes_test/block', From 16421dcf748eb196e6f55a18a059efd4861276be Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Thu, 4 Sep 2025 21:19:58 +0200 Subject: [PATCH 25/29] Add PHPDoc for set_up --- phpunit/block-bindings-test.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index 6fc6ead80d3029..643a1b51161de9 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -34,6 +34,11 @@ public static function wpSetUpBeforeClass() { ); } + /** + * Sets up the test fixture. + * + * @since 6.9.0 + */ public function set_up() { parent::set_up(); From ca7d426dd5cd21e12a932a91d0854b421c5e781b Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Fri, 5 Sep 2025 11:35:16 +0200 Subject: [PATCH 26/29] Remove @since PHPDocs --- phpunit/block-bindings-test.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index 643a1b51161de9..adf381a7790f29 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -13,8 +13,6 @@ class Tests_Block_Bindings extends WP_UnitTestCase { /** * Sets up shared fixtures. - * - * @since 6.9.0 */ public static function wpSetUpBeforeClass() { register_block_type( @@ -36,8 +34,6 @@ public static function wpSetUpBeforeClass() { /** * Sets up the test fixture. - * - * @since 6.9.0 */ public function set_up() { parent::set_up(); @@ -53,8 +49,6 @@ function ( $supported_attributes ) { /** * Tear down after each test. - * - * @since 6.5.0 */ public function tear_down() { foreach ( get_all_registered_block_bindings_sources() as $source_name => $source_properties ) { @@ -68,8 +62,6 @@ public function tear_down() { /** * Tear down after class. - * - * @since 6.9.0 */ public static function wpTearDownAfterClass() { unregister_block_type( 'test/block' ); From 4556ab5c7fa998bcdf315d35ef910c9155976a57 Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Fri, 5 Sep 2025 11:35:50 +0200 Subject: [PATCH 27/29] Remove @ticket references from PHPDoc in tests --- phpunit/block-bindings-test.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index adf381a7790f29..7c290332a89d9c 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -105,8 +105,6 @@ public function data_update_block_with_value_from_source() { /** * Test if the block content is updated with the value returned by the source. * - * @ticket 60282 - * * @covers ::register_block_bindings_source * * @dataProvider data_update_block_with_value_from_source @@ -176,10 +174,6 @@ function () { /** * Test passing arguments to the source. * - * @ticket 60282 - * @ticket 60651 - * @ticket 61385 - * * @covers ::register_block_bindings_source * * @dataProvider data_different_get_value_callbacks @@ -212,9 +206,6 @@ public function test_different_get_value_callbacks( $get_value_callback, $expect /** * Tests passing `uses_context` as argument to the source. * - * @ticket 60525 - * @ticket 61642 - * * @covers ::register_block_bindings_source */ public function test_passing_uses_context_to_source() { @@ -268,8 +259,6 @@ public function test_passing_uses_context_to_source() { * Tests if the block content is updated with the value returned by the source * for the Image block in the placeholder state. * - * @ticket 60282 - * * @covers ::register_block_bindings_source */ public function test_update_block_with_value_from_source_image_placeholder() { @@ -310,9 +299,6 @@ public function test_update_block_with_value_from_source_image_placeholder() { * Tests if the `__default` attribute is replaced with real attributes for * pattern overrides. * - * @ticket 61333 - * @ticket 62069 - * * @covers WP_Block::process_block_bindings */ public function test_default_binding_for_pattern_overrides() { @@ -346,8 +332,6 @@ public function test_default_binding_for_pattern_overrides() { /** * Tests that filter `block_bindings_source_value` is applied. - * - * @ticket 61181 */ public function test_filter_block_bindings_source_value() { register_block_bindings_source( From 292e249f38e67253282a97b2bc12f8fa04a9c9aa Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Fri, 5 Sep 2025 11:44:31 +0200 Subject: [PATCH 28/29] Shut up WPCS --- lib/compat/wordpress-6.9/block-bindings.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/compat/wordpress-6.9/block-bindings.php b/lib/compat/wordpress-6.9/block-bindings.php index b5c741a9171fb0..9d9f2dff4cbda0 100644 --- a/lib/compat/wordpress-6.9/block-bindings.php +++ b/lib/compat/wordpress-6.9/block-bindings.php @@ -311,6 +311,7 @@ function gutenberg_get_block_bindings_processor( string $block_content ) { * @param string $rich_text The rich text to replace the original content with. * @return bool True on success. */ + // phpcs:ignore Gutenberg.CodeAnalysis.GuardedFunctionAndClassNames.FunctionNotGuardedAgainstRedeclaration public function replace_rich_text( $rich_text ) { if ( $this->is_tag_closer() || ! $this->expects_closer() ) { return false; From 30680fe66e7f1149812048e1bd1be2a510fcda8c Mon Sep 17 00:00:00 2001 From: Bernie Reiter Date: Fri, 5 Sep 2025 11:49:27 +0200 Subject: [PATCH 29/29] Remove obsolete function parameter --- phpunit/block-bindings-test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php index 7c290332a89d9c..ff0d24aa190188 100644 --- a/phpunit/block-bindings-test.php +++ b/phpunit/block-bindings-test.php @@ -209,7 +209,7 @@ public function test_different_get_value_callbacks( $get_value_callback, $expect * @covers ::register_block_bindings_source */ public function test_passing_uses_context_to_source() { - $get_value_callback = function ( $source_args, $block_instance, $attribute_name ) { + $get_value_callback = function ( $source_args, $block_instance ) { $this->assertArrayNotHasKey( 'forbiddenSourceContext', $block_instance->context,