Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the Block Bindings API #5888

Closed
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
8720e12
Add the Block Bindings API
michalczaplinski Jan 17, 2024
8891169
Add tests
artemiomorales Jan 17, 2024
eac5b22
Move tests to different directory
artemiomorales Jan 17, 2024
39e52ed
Fix bug wherein block content was erased
artemiomorales Jan 17, 2024
82e70ff
Update comment format for consistency
artemiomorales Jan 17, 2024
6486471
Move process_block_bindings_function to class-wp-block
michalczaplinski Jan 17, 2024
048330e
WordPress capitalization dangit
michalczaplinski Jan 17, 2024
4a4b825
PHPDoc formatting
michalczaplinski Jan 18, 2024
9a662e4
Add a better PHPDoc
michalczaplinski Jan 18, 2024
c92b6ac
Add a new unit test covering the _process_block_bindings() function
michalczaplinski Jan 18, 2024
abb56fb
More fixes for formatting, capitalization and PHPDocs
michalczaplinski Jan 18, 2024
e8b1195
Add @since to block-bindings.php
michalczaplinski Jan 18, 2024
cc6c2c2
Add version and since tags to class and methods
michalczaplinski Jan 18, 2024
206b6b7
Add a comment on the block class
michalczaplinski Jan 18, 2024
dc8377a
Update assertions in WP_Block_Bindings_Test class
michalczaplinski Jan 18, 2024
751a459
Update block binding registration function comment
michalczaplinski Jan 18, 2024
423bb66
Change the signature of `wp_block_bindings_register_source` & WP_Bloc…
michalczaplinski Jan 23, 2024
d1b0d2e
Add type hints for `register_source()` & `replace_html()`
michalczaplinski Jan 23, 2024
9f2274d
Add the missing array keys
michalczaplinski Jan 23, 2024
02f0ea3
Apply https://github.com/WordPress/gutenberg/pull/58055/
michalczaplinski Jan 23, 2024
9303e02
Make processing of bindings private to the class
artemiomorales Jan 25, 2024
30f4ed7
Update class name and method names
artemiomorales Jan 25, 2024
940fb82
Remove the `wp_block_bindings()` function and create a static get_ins…
michalczaplinski Jan 25, 2024
dbda027
Rename `register_block_bindings_source()` to `register()`
michalczaplinski Jan 25, 2024
34d9dd7
Move the process_block_bindings & replace_html methods to the WP_bloc…
michalczaplinski Jan 25, 2024
74bfae7
Rename WP_Block_Bindings to WP_Block_Bindings_Registry in missing places
michalczaplinski Jan 25, 2024
013535e
Rename class-wp-block-bindings.php to class-wp-block-bindings-registr…
michalczaplinski Jan 25, 2024
89e9833
Rename $source_args to $source_properties to align with other similar…
michalczaplinski Jan 25, 2024
0c931e0
Add `is_registered()` & `get_registered()`
michalczaplinski Jan 25, 2024
7b1ebe2
Register block bindings on 'init' action hook
michalczaplinski Jan 25, 2024
b0c300f
Merge branch 'trunk' into feature/block-bindings-api
michalczaplinski Jan 25, 2024
e0b3883
Fix incorrect loading of block bindings class
artemiomorales Jan 26, 2024
2900bf5
Add validation to prevent sources from being registered twice
artemiomorales Jan 26, 2024
26da23e
Add 'core' namespace to built-in sources
artemiomorales Jan 26, 2024
29213d8
Fix incorrect reading of sources
artemiomorales Jan 26, 2024
4c3d892
Rename 'apply' to 'get_value_callback'
artemiomorales Jan 26, 2024
06a3930
Fix incorrect reading of registered sources
artemiomorales Jan 26, 2024
f53121d
Ensure unauthorized users don't read post meta
artemiomorales Jan 26, 2024
5b8eb4a
Fix PHPCS errors
artemiomorales Jan 26, 2024
45a96f8
Rename sources using naming pattern for block types
artemiomorales Jan 26, 2024
08ddb0a
Add context to translation
artemiomorales Jan 26, 2024
3a16941
Update comments and revise to use proper array annotation
artemiomorales Jan 26, 2024
1729f65
Update callback comment with return type details; fix PHPCS
artemiomorales Jan 26, 2024
9ea2c9a
Code quality improvements for the Block Bindings Registry
gziolo Jan 29, 2024
fd090ab
Ensure that code comments fit into the line length limit
gziolo Jan 29, 2024
6ea69cd
Updates to the test
michalczaplinski Jan 25, 2024
15dea88
Merge branch 'trunk' into feature/block-bindings-api
michalczaplinski Jan 29, 2024
5893867
Remove the files already committed in #5965 and #5966.
michalczaplinski Jan 29, 2024
d10b165
Update block bindings function call in class-wp-block.php
michalczaplinski Jan 29, 2024
c324e65
Add set_up to block-bindings tests
michalczaplinski Jan 29, 2024
a0d550a
Refactor block bindings tests
michalczaplinski Jan 29, 2024
3f242d3
Fix block bindings registration unit test and test setup
michalczaplinski Jan 29, 2024
d202e5d
Move tests to block-bindings directory
michalczaplinski Jan 30, 2024
2a20dee
Remove unnecessary attributes from the test
michalczaplinski Jan 30, 2024
2b4cf33
Incorporate PHP changes from https://github.com/WordPress/gutenberg/p…
michalczaplinski Jan 30, 2024
8604e82
Fix binding source arguments in WP_Block class
michalczaplinski Jan 30, 2024
65a7c09
Test passing arguments to the source
michalczaplinski Jan 30, 2024
1ceef1b
Undo the changes in register.php
michalczaplinski Jan 30, 2024
3d97457
Merge branch 'trunk' into feature/block-bindings-api
michalczaplinski Jan 30, 2024
5f397aa
Update the docstring for `process_block_bindings`
michalczaplinski Jan 30, 2024
f2a611b
Update source_name parameter documentation in block-bindings.php and …
michalczaplinski Jan 30, 2024
a1e1bac
Refactor post_meta_source_callback function to use is_post_publicly_v…
michalczaplinski Jan 30, 2024
26571a7
Remove unnecessary condition for unregistering block bindings sources
michalczaplinski Jan 30, 2024
d189f50
Fix access control logic in post_meta_source_callback function
michalczaplinski Jan 30, 2024
d7201ce
Optimize call to post_password_required()
artemiomorales Jan 30, 2024
69ed754
Update pattern source label and PHPDoc string.
michalczaplinski Jan 31, 2024
951ca16
Merge branch 'trunk' into feature/block-bindings-api
michalczaplinski Jan 31, 2024
f10a4fe
Update block bindings source labels
michalczaplinski Jan 31, 2024
0bec833
Wrap the block binding registrations with 'init' actions
michalczaplinski Feb 1, 2024
3134faa
Use $this->block_type directly in replace_html method
michalczaplinski Feb 1, 2024
9491be3
Mark functions passed to init actions in block binding sources as pri…
michalczaplinski Feb 1, 2024
c95b92f
Prefix block binding function names with `_` to mark as private
michalczaplinski Feb 1, 2024
3619e18
Update the function names again
michalczaplinski Feb 1, 2024
c8232ac
Prevent pattern and post meta sources from registering in unit tests
michalczaplinski Feb 1, 2024
871cedc
Use `call_user_func_array` to call the source binding callback in WP_…
michalczaplinski Feb 1, 2024
604f7a2
Validate that `$block['attrs']['metadata']['bindings']` is an array i…
michalczaplinski Feb 1, 2024
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
23 changes: 23 additions & 0 deletions src/wp-includes/block-bindings/sources/pattern.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
/**
* The "pattern" source for the Block Bindings API. This source is used by the
* Partially Synced Patterns.
michalczaplinski marked this conversation as resolved.
Show resolved Hide resolved
*
* @since 6.5.0
* @package WordPress
*/
function pattern_source_callback( $source_attrs, $block_instance, $attribute_name ) {
if ( ! _wp_array_get( $block_instance->attributes, array( 'metadata', 'id' ), false ) ) {
return null;
}
$block_id = $block_instance->attributes['metadata']['id'];
return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, $attribute_name ), null );
}

register_block_bindings_source(
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
'core/pattern-attributes',
michalczaplinski marked this conversation as resolved.
Show resolved Hide resolved
array(
'label' => __( 'Pattern Attributes' ),
'get_value_callback' => 'pattern_source_callback',
)
);
33 changes: 33 additions & 0 deletions src/wp-includes/block-bindings/sources/post-meta.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
/**
* Add the post_meta source to the Block Bindings API.
*
* @since 6.5.0
* @package WordPress
*/
function post_meta_source_callback( $source_attrs ) {
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
// Use the postId attribute if available
gziolo marked this conversation as resolved.
Show resolved Hide resolved
if ( isset( $source_attrs['postId'] ) ) {
$post_id = $source_attrs['postId'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have use-cases where the post id is an attribute and not something in the context?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SantosGuillamot might know better. I would assume we rather read it from the context or other block attributes through $block_instance param.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was to allow people to select a specific ID to connect to. So, instead of connecting to the post title of the context, you connect to a specific post title.

I added it more as an example of other attributes that could make sense at some point.

} else {
// $block_instance->context['postId'] is not available in the Image block.
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
$post_id = get_the_ID();
Comment on lines +17 to +18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there might be a bug here.

Instead of completely omitting the usage of the $block_instance->context['postId'] here, we should check if the value isset and if so use it over the ID we get from get_the_ID.

Something like this:

$post_id = isset( $block_instance->context['postId'] ) ? $block_instance->context['postId'] : get_the_ID();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wonder if we should always use the context here. as if I'm not wrong the default context should be set as get_the_ID if there's no parent query block.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I tested, $block_instance->context is an empty array for the heading, button, and image block, so that's why we couldn't get the postId from there. Is that not expected?

We could definitely use the conditional suggested. However, I'd like to understand if just using $block_instance->context should work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we have an issue somewhere but in the client the block context has some "default context values" like the post ID if I'm not wrong, and in the server you're saying that these are not present. IMO, the block context should be the same for the client and the server.

I think it's fine if we defer the potential fix to a dedicated issue but we might want to track it somewhere.

}

Copy link

@sc0ttkclark sc0ttkclark Jan 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SantosGuillamot @gziolo Heads up here --

This is lacking any access rights checks and finding/fixing this now means we avoid having to patch the eventual release. If someone provides any post ID, it could pull meta for any post ID of any post type of any status including password-protected posts.

Something like this could help, maybe only in the conditional above where a custom post ID is set but to be safe we could have it here before the return.

if ( ! current_user_can( 'read_post', $post_id ) || post_password_required( $post_id ) ) {
	return '';
}

We may also want to check whether the associated post type is public + publicly_queryable to ensure that we follow the same constraints established by the Query Loop block for dynamically embedding a list posts themselves.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ( ! current_user_can( 'read_post', $post_id ) || post_password_required( $post_id ) ) {
return '';
}

Copy link

@sc0ttkclark sc0ttkclark Jan 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the block editor itself, this logic runs through the REST API which already does this sort of logic.

This only impacts the render on the server-side.

Copy link

@artemiomorales artemiomorales Jan 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sc0ttkclark That makes sense to me. I added the permissions check in f53121d and had to check the post status too to make sure it works as intended.

(Pardon the PHPCS errors — those have all been fixed in later commits)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on this on the line where this was implemented: https://github.com/WordPress/wordpress-develop/pull/5888/files#r1471516195

// If a post isn't public, we need to prevent
// unauthorized users from accessing the post meta.
$post = get_post( $post_id );
if ( ( $post && 'publish' !== $post->post_status && ! current_user_can( 'read_post', $post_id ) ) || post_password_required( $post_id ) ) {
michalczaplinski marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

return get_post_meta( $post_id, $source_attrs['value'], true );
}

register_block_bindings_source(
'core/post-meta',
array(
'label' => _x( 'Post Meta', 'Post metadata to be read and used to substitute block content' ),
michalczaplinski marked this conversation as resolved.
Show resolved Hide resolved
'get_value_callback' => 'post_meta_source_callback',
)
);
202 changes: 202 additions & 0 deletions src/wp-includes/class-wp-block.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,204 @@ public function __get( $name ) {
return null;
}

/**
* Processes the block bindings in block's attributes.
*
* 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 `$this->register()` that defines how to
* retrieve a value from outside the block, e.g. from post meta.
*
* This function will process those bindings and replace the HTML with the value of the binding.
* The value is retrieved from the source of the binding.
*
* ### Example
*
* The "bindings" property for an Image block might look like this:
*
* ```json
* {
* "metadata": {
* "bindings": {
* "title": {
* "source": {
* "name": "post_meta",
* "attributes": { "value": "text_custom_field" }
* }
* },
* "url": {
* "source": {
* "name": "post_meta",
* "attributes": { "value": "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.
*
* @access private
* @since 6.5.0
*
* @param string $block_content Block content.
* @param array $block The full block, including name and attributes.
gziolo marked this conversation as resolved.
Show resolved Hide resolved
*/
private function process_block_bindings( $block_content ) {
$block = $this->parsed_block;

// Allowed blocks that support block bindings.
// TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes?
$allowed_blocks = array(
'core/paragraph' => array( 'content' ),
'core/heading' => array( 'content' ),
'core/image' => array( 'url', 'title', 'alt' ),
'core/button' => array( 'url', 'text' ),
);

// If the block doesn't have the bindings property or isn't one of the allowed block types, return.
if ( ! isset( $block['attrs']['metadata']['bindings'] ) || ! isset( $allowed_blocks[ $this->name ] ) ) {
return $block_content;
}

$block_bindings_sources = get_all_registered_block_bindings_sources();
$modified_block_content = $block_content;
foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) {
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
// If the attribute is not in the list, process next attribute.
if ( ! in_array( $binding_attribute, $allowed_blocks[ $this->name ], true ) ) {
continue;
}
// If no source is provided, or that source is not registered, process next attribute.
if ( ! isset( $binding_source['source'] ) || ! isset( $binding_source['source']['name'] ) || ! isset( $block_bindings_sources[ $binding_source['source']['name'] ] ) ) {
continue;
}

$source_callback = $block_bindings_sources[ $binding_source['source']['name'] ]['get_value_callback'];
// Get the value based on the source.
if ( ! isset( $binding_source['source']['attributes'] ) ) {
$source_args = array();
} else {
$source_args = $binding_source['source']['attributes'];
}
$source_value = $source_callback( $source_args, $this, $binding_attribute );
michalczaplinski marked this conversation as resolved.
Show resolved Hide resolved
// If the value is null, process next attribute.
if ( is_null( $source_value ) ) {
continue;
}

// Process the HTML based on the block and the attribute.
$modified_block_content = $this->replace_html( $modified_block_content, $this->name, $binding_attribute, $source_value );
}
return $modified_block_content;
}

/**
* Depending on the block attributes, replace the HTML based on the value returned by the source.
*
* @since 6.5.0
*
* @param string $block_content Block content.
* @param string $block_name The name of the block to process.
* @param string $block_attr The attribute of the block we want to process.
* @param string $source_value The value used to replace the HTML.
gziolo marked this conversation as resolved.
Show resolved Hide resolved
*/
private function replace_html( string $block_content, string $block_name, string $block_attr, string $source_value ) {
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name );
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
if ( null === $block_type || ! isset( $block_type->attributes[ $block_attr ] ) ) {
return $block_content;
}

// Depending on the attribute source, the processing will be different.
switch ( $block_type->attributes[ $block_attr ]['source'] ) {
case 'html':
case 'rich-text':
$block_reader = new WP_HTML_Tag_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[ $block_attr ]['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' );

// TODO: This shouldn't be needed when the `set_inner_html` function is ready.
// Store the parent tag and its attributes to be able to restore them later in the button.
// The button block has a wrapper while the paragraph and heading blocks don't.
if ( 'core/button' === $block_name ) {
$button_wrapper = $block_reader->get_tag();
$button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' );
$button_wrapper_attrs = array();
foreach ( $button_wrapper_attribute_names as $name ) {
$button_wrapper_attrs[ $name ] = $block_reader->get_attribute( $name );
}
}

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 ), $selector ) === 0 || $block_reader->next_tag(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_tag() doesn't take any argument, and it could return NULL, which would throw an error when passed as the first argument of strcasecmp. I think we should guard against it first.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is currently an error being triggered in trunk:

Deprecated: strcasecmp(): Passing null to parameter #1 ($string1) of type string is deprecated in /var/www/html/wp-includes/class-wp-block.php on line 330

To reproduce:

  1. Create a new post
  2. Add a heading block and add the text 'default' to it
  3. Using the block settings menu, click 'Create pattern'.
  4. In the resulting modal, give the pattern a name and click 'Create'
  5. Click the 'Edit original' button that appears on the resulting pattern block
  6. Select the heading block and check the 'Allow instance overrides' option in the advanced inspector tools
  7. Save and click the 'Back' button from the topbar
  8. Now you're back in the post editor, change the heading text 'default' to 'override'

Preview the post
Expected: The preview should show the heading with the text 'override'
Actual: It still shows 'default' and the above error is shown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe having something like this should solve the issue:

$parent_tag = $block_reader->get_tag();
if ( ! is_null( $parent_tag ) || strcasecmp( $parent_tag, $selector ) === 0 || $block_reader->next_tag(
    array(
	'tag_name' => $selector,
    )
) )

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when using next_tag() we will always get a string for get_tag() unless no tag was found.

it would be better to check the result of next_tag() so we don't descend into this function as if a tag were matched.

if ( ! $block_reader->next_tag() ) {
	continue; // or whatever control flow is appropriate
}

array(
'tag_name' => $selector,
)
) ) {
$block_reader->release_bookmark( 'iterate-selectors' );

// TODO: Use `set_inner_html` method whenever it's ready in the HTML API.
// Until then, it is hardcoded for the paragraph, heading, and button blocks.
// Store the tag and its attributes to be able to restore them later.
$selector_attribute_names = $block_reader->get_attribute_names_with_prefix( '' );
$selector_attrs = array();
foreach ( $selector_attribute_names as $name ) {
$selector_attrs[ $name ] = $block_reader->get_attribute( $name );
}
$selector_markup = "<$selector>" . wp_kses_post( $source_value ) . "</$selector>";
$amended_content = new WP_HTML_Tag_Processor( $selector_markup );
$amended_content->next_tag();
foreach ( $selector_attrs as $attribute_key => $attribute_value ) {
$amended_content->set_attribute( $attribute_key, $attribute_value );
}
if ( 'core/paragraph' === $block_name || 'core/heading' === $block_name ) {
return $amended_content->get_updated_html();
}
if ( 'core/button' === $block_name ) {
$button_markup = "<$button_wrapper>{$amended_content->get_updated_html()}</$button_wrapper>";
$amended_button = new WP_HTML_Tag_Processor( $button_markup );
$amended_button->next_tag();
foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) {
$amended_button->set_attribute( $attribute_key, $attribute_value );
}
return $amended_button->get_updated_html();
}
} else {
$block_reader->seek( 'iterate-selectors' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seek() could return false which means we fail to seek the bookmark. This could happen when we reach the end the of input.

I think there's currently a bug in WP_HTML_Tag_Processor API that seek() after next_tag() won't correctly put the cursor at the right place.

$p = new WP_HTML_Tag_Processor( '<h2></h2>' );
$p->next_tag();
$p->set_bookmark( 'bookmark' );
$p->next_tag();
var_dump( $p->seek( 'bookmark' ) );
var_dump( $p->get_tag() );

For instance, I expect the above snippet to output true and H2, but instead it outputs false and NULL. @dmsnell might know better if it's a bug or not 🙇.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevin940726, what was the user interaction, and what HTML was saved for the block that triggered the issue? It's a great opportunity to add a unit test that will help fix the issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some further details on the comment above - #5888 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the find @kevin940726 - pushed out #6021 to fix it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevin940726 fixed in trunk now 👍

}
}
$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[ $block_attr ]['selector'],
)
) ) {
return $block_content;
}
$amended_content->set_attribute( $block_type->attributes[ $block_attr ]['attribute'], esc_attr( $source_value ) );
michalczaplinski marked this conversation as resolved.
Show resolved Hide resolved
return $amended_content->get_updated_html();
break;

default:
return $block_content;
break;
}
return;
}


/**
* Generates the render output for the block.
*
Expand Down Expand Up @@ -280,6 +478,10 @@ public function render( $options = array() ) {
}
}

// Process the block bindings for this block, if any are registered. This
// will replace the block content with the value from a registered binding source.
$block_content = $this->process_block_bindings( $block_content );

/**
* Filters the content of a single block.
*
Expand Down
2 changes: 2 additions & 0 deletions src/wp-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@
require ABSPATH . WPINC . '/fonts.php';
require ABSPATH . WPINC . '/class-wp-script-modules.php';
require ABSPATH . WPINC . '/script-modules.php';
require ABSPATH . WPINC . '/block-bindings/sources/post-meta.php';
require ABSPATH . WPINC . '/block-bindings/sources/pattern.php';

$GLOBALS['wp_embed'] = new WP_Embed();

Expand Down
63 changes: 63 additions & 0 deletions tests/phpunit/tests/block-bindings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php
/**
* Unit tests covering WP_Block_Bindings_Registry functionality.
*
* @group block-bindings
*
* @covers WP_Block_Bindings_Registry
*
* @since 6.5.0
* @package WordPress
*/
class WP_Block_Bindings_Registry_Test extends WP_UnitTestCase {

/**
* Set up before each test.
*
* @since 6.5.0
*/
public function set_up() {
foreach ( get_all_registered_block_bindings_sources() as $source_name => $source_properties ) {
unregister_block_bindings_source( $source_name );
}

parent::set_up();
}

/**
* Test if the block content is updated with the value returned by the source.
*
* @since 6.5.0
*
* @covers WP_Block_Bindings_Registry::
*/
public function test_replace_html_for_paragraph_content() {
$source_name = 'test/source';
$label = 'Test Source';
$get_value_callback = function () {
return 'test source value';
};

register_block_bindings_source(
$source_name,
array(
'label' => $label,
'get_value_callback' => $get_value_callback,
)
);

$block_content = <<<HTML
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":{"name":"test/source","attributes":{"value":"text_custom_field"}}}}}} --><p>This should not appear</p><!-- /wp:paragraph -->
HTML;

$parsed_blocks = parse_blocks( $block_content );

$block = new WP_Block( $parsed_blocks[0] );

$expected = '<p>test source value</p>';
$result = $block->render();

// Check if the block content was updated correctly.
$this->assertEquals( $expected, $result, 'The block content should be updated with the value returned by the source.' );
}
}
16 changes: 13 additions & 3 deletions tests/phpunit/tests/block-bindings/register.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,26 @@ class Tests_Block_Bindings_Register extends WP_UnitTestCase {
'label' => 'Test source',
);

/**
* Set up before each test.
*
* @since 6.5.0
*/
public function set_up() {
foreach ( get_all_registered_block_bindings_sources() as $source_name => $source_properties ) {
unregister_block_bindings_source( $source_name );
}

parent::set_up();
}

/**
* 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 ) {
if ( str_starts_with( $source_name, 'test/' ) ) {
unregister_block_bindings_source( $source_name );
}
unregister_block_bindings_source( $source_name );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated this line because the unit tests fail otherwise. That's because in this PR we're also registering core/post-meta and core/pattern-attributes sources.

}

Expand Down
Loading