-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Add the Block Bindings API #5888
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
Changes from 16 commits
8720e12
8891169
eac5b22
39e52ed
82e70ff
6486471
048330e
4a4b825
9a662e4
c92b6ac
abb56fb
e8b1195
cc6c2c2
206b6b7
dc8377a
751a459
423bb66
d1b0d2e
9f2274d
02f0ea3
9303e02
30f4ed7
940fb82
dbda027
34d9dd7
74bfae7
013535e
89e9833
0c931e0
7b1ebe2
b0c300f
e0b3883
2900bf5
26da23e
29213d8
4c3d892
06a3930
f53121d
5b8eb4a
45a96f8
08ddb0a
3a16941
1729f65
9ea2c9a
fd090ab
6ea69cd
15dea88
5893867
d10b165
c324e65
a0d550a
3f242d3
d202e5d
2a20dee
2b4cf33
8604e82
65a7c09
1ceef1b
3d97457
5f397aa
f2a611b
a1e1bac
26571a7
d189f50
d7201ce
69ed754
951ca16
f10a4fe
0bec833
3134faa
9491be3
c95b92f
3619e18
c8232ac
871cedc
604f7a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| <?php | ||
| /** | ||
| * Block Bindings API | ||
| * | ||
| * This file contains functions for managing block bindings in WordPress. | ||
| * | ||
| * @since 6.5.0 | ||
| * @package WordPress | ||
| */ | ||
|
|
||
| /** | ||
| * Retrieves the singleton instance of WP_Block_Bindings. | ||
| * | ||
| * @since 6.5.0 | ||
| * | ||
| * @return WP_Block_Bindings The WP_Block_Bindings instance. | ||
| */ | ||
| function wp_block_bindings() { | ||
| static $instance = null; | ||
| if ( is_null( $instance ) ) { | ||
| $instance = new WP_Block_Bindings(); | ||
| } | ||
| return $instance; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Registers a new source for block bindings. | ||
| * | ||
| * @since 6.5.0 | ||
| * | ||
| * @param string $source_name The name of the source. | ||
| * @param string $label The label of the source. | ||
| * @param callable $apply The callback executed when the source is processed during block rendering. The callable should have the following signature: | ||
| * function (object $source_attrs, object $block_instance, string $attribute_name): string | ||
| * - object $source_attrs: Object containing source ID used to look up the override value, i.e. {"value": "{ID}"}. | ||
| * - object $block_instance: The block instance. | ||
| * - string $attribute_name: The name of an attribute used to retrieve an override value from the block context. | ||
| * The callable should return a string that will be used to override the block's original value. | ||
| * @return void | ||
| */ | ||
| function wp_block_bindings_register_source( $source_name, $label, $apply ) { | ||
| wp_block_bindings()->register_source( $source_name, $label, $apply ); | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Retrieves the list of registered block sources. | ||
| * | ||
| * @since 6.5.0 | ||
| * | ||
| * @return array The list of registered block sources. | ||
| */ | ||
| function wp_block_bindings_get_sources() { | ||
| return wp_block_bindings()->get_sources(); | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Replaces the HTML content of a block based on the provided source value. | ||
| * | ||
| * @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. | ||
| * @return string The modified block content. | ||
| */ | ||
| function wp_block_bindings_replace_html( $block_content, $block_name, $block_attr, $source_value ) { | ||
| return wp_block_bindings()->replace_html( $block_content, $block_name, $block_attr, $source_value ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| <?php | ||
michalczaplinski marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /** | ||
| * Block Bindings API: WP_Block_Bindings class. | ||
| * | ||
| * Support for overriding content in blocks by connecting them to different sources. | ||
| * | ||
| * @package WordPress | ||
| * @subpackage Block Bindings | ||
| */ | ||
|
|
||
| /** | ||
| * Core class used to define supported blocks, register sources, and populate HTML with content from those sources. | ||
| * | ||
| * @since 6.5.0 | ||
| */ | ||
| class WP_Block_Bindings { | ||
|
|
||
| /** | ||
| * Holds the registered block bindings sources, keyed by source identifier. | ||
| * | ||
| * @since 6.5.0 | ||
| * | ||
| * @var array | ||
| */ | ||
| private $sources = array(); | ||
|
|
||
| /** | ||
| * Function to register a new block binding source. | ||
| * | ||
| * Sources are used to override block's original attributes with a value | ||
| * coming from the source. Once a source is registered, it can be used by a | ||
| * block by setting its `metadata.bindings` attribute to a value that refers | ||
| * to the source. | ||
| * | ||
| * @since 6.5.0 | ||
| * | ||
| * @param string $source_name The name of the source. | ||
| * @param string $label The label of the source. | ||
| * @param callable $apply The callback executed when the source is processed during block rendering. The callable should have the following signature: | ||
| * function (object $source_attrs, object $block_instance, string $attribute_name): string | ||
| * - object $source_attrs: Object containing source ID used to look up the override value, i.e. {"value": "{ID}"}. | ||
| * - object $block_instance: The block instance. | ||
| * - string $attribute_name: The name of an attribute used to retrieve an override value from the block context. | ||
| * The callable should return a string that will be used to override the block's original value. | ||
| * | ||
| * @return void | ||
| */ | ||
| public function register_source( $source_name, $label, $apply ) { | ||
michalczaplinski marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| $this->sources[ $source_name ] = array( | ||
| 'label' => $label, | ||
| 'apply' => $apply, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * 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. | ||
| */ | ||
| public function replace_html( $block_content, $block_name, $block_attr, $source_value ) { | ||
| $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); | ||
| 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( | ||
| 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>" . esc_html( $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' ); | ||
| } | ||
| } | ||
| $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 ) ); | ||
| return $amended_content->get_updated_html(); | ||
| break; | ||
|
|
||
| default: | ||
| return $block_content; | ||
| break; | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| /** | ||
| * Retrieves the list of registered block sources. | ||
| * | ||
| * @since 6.5.0 | ||
| * | ||
| * @return array The array of registered sources. | ||
| */ | ||
| public function get_sources() { | ||
| return $this->sources; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <?php | ||
| /** | ||
| * The "pattern" source for the Block Bindings API. This source is used by the | ||
| * Partially Synced Patterns. | ||
michalczaplinski marked this conversation as resolved.
Outdated
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 ); | ||
| } | ||
|
|
||
| wp_block_bindings_register_source( | ||
michalczaplinski marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 'pattern_attributes', | ||
| __( 'Pattern Attributes' ), | ||
| 'pattern_source_callback' | ||
| ); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,24 @@ | ||||||||||||||
| <?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']; | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Something like this: $post_id = isset( $block_instance->context['postId'] ) ? $block_instance->context['postId'] : get_the_ID();
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From what I tested, We could definitely use the conditional suggested. However, I'd like to understand if just using
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||
| return get_post_meta( $post_id, $source_attrs['value'], true ); | ||||||||||||||
|
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| wp_block_bindings_register_source( | ||||||||||||||
| 'post_meta', | ||||||||||||||
| __( 'Post Meta' ), | ||||||||||||||
| 'post_meta_source_callback' | ||||||||||||||
| ); | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2083,3 +2083,98 @@ function _wp_footnotes_force_filtered_html_on_import_filter( $arg ) { | |
| } | ||
| return $arg; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * 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 `wp_block_bindings_register_source()` 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. | ||
| * @param WP_Block $block_instance The block instance. | ||
| */ | ||
| function _process_block_bindings( $block_content, $block, $block_instance ) { | ||
michalczaplinski marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // 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[ $block_instance->name ] ) ) { | ||
| return $block_content; | ||
| } | ||
|
|
||
| $block_bindings_sources = wp_block_bindings_get_sources(); | ||
| $modified_block_content = $block_content; | ||
| foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) { | ||
|
|
||
| // If the attribute is not in the list, process next attribute. | ||
| if ( ! in_array( $binding_attribute, $allowed_blocks[ $block_instance->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'] ]['apply']; | ||
| // 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, $block_instance, $binding_attribute ); | ||
| // 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 = wp_block_bindings_replace_html( $modified_block_content, $block_instance->name, $binding_attribute, $source_value ); | ||
michalczaplinski marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| return $modified_block_content; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.