Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backport-changelog/6.9/10305.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/10305

* https://github.com/WordPress/gutenberg/pull/72165
106 changes: 0 additions & 106 deletions lib/compat/wordpress-6.9/entity-block-bindings.php

This file was deleted.

28 changes: 26 additions & 2 deletions lib/compat/wordpress-6.9/post-data-block-bindings.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,29 @@ function gutenberg_block_bindings_post_data_get_value( array $source_args, $bloc
return null;
}

if ( empty( $block_instance->context['postId'] ) ) {
/*
* BACKWARDS COMPATIBILITY: Hardcoded exception for navigation blocks.
* Required for WordPress 6.9+ navigation blocks. DO NOT REMOVE.
*/
Comment on lines +26 to +29
Copy link
Contributor

Choose a reason for hiding this comment

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

For full clarity, could we also add an END block so we know the full section of code that should never be removed?

$block_name = $block_instance->name ?? '';
$is_navigation_block = in_array(
$block_name,
array( 'core/navigation-link', 'core/navigation-submenu' ),
true
);

if ( $is_navigation_block ) {
// Navigation blocks: read from block attributes
$post_id = $block_instance->attributes['id'] ?? null;
} else {
// All other blocks: use context
$post_id = $block_instance->context['postId'] ?? null;
}

// If we don't have an entity ID, bail early.
if ( empty( $post_id ) ) {
return null;
}
$post_id = $block_instance->context['postId'];

// If a post isn't public, we need to prevent unauthorized users from accessing the post data.
$post = get_post( $post_id );
Expand All @@ -46,6 +65,11 @@ function gutenberg_block_bindings_post_data_get_value( array $source_args, $bloc
return '';
}
}

if ( 'link' === $source_args['key'] ) {
$permalink = get_permalink( $post_id );
return false === $permalink ? null : esc_url( $permalink );
}
}

/**
Expand Down
119 changes: 119 additions & 0 deletions lib/compat/wordpress-6.9/term-data-block-bindings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php
/**
* Term Data source for the block bindings.
*
* @since 6.9.0
* @package gutenberg
* @subpackage Block Bindings
*/

/**
* Gets value for Term Data source.
*
* @since 6.9.0
* @access private
*
* @param array $source_args Array containing source arguments used to look up the override value.
* Example: array( "key" => "name" ).
* @param WP_Block $block_instance The block instance.
* @return mixed The value computed for the source.
*/
function gutenberg_block_bindings_term_data_get_value( array $source_args, $block_instance ) {
if ( empty( $source_args['key'] ) ) {
return null;
}

/*
* BACKWARDS COMPATIBILITY: Hardcoded exception for navigation blocks.
* Required for WordPress 6.9+ navigation blocks. DO NOT REMOVE.
*/
$block_name = $block_instance->name ?? '';
$is_navigation_block = in_array(
$block_name,
array( 'core/navigation-link', 'core/navigation-submenu' ),
true
);

if ( $is_navigation_block ) {
// Navigation blocks: read from block attributes
$term_id = $block_instance->attributes['id'] ?? null;
$type = $block_instance->attributes['type'] ?? '';
// Map UI shorthand to taxonomy slug when using attributes.
$taxonomy = ( 'tag' === $type ) ? 'post_tag' : $type;
} else {
// All other blocks: use context
$term_id = $block_instance->context['termId'] ?? null;
$taxonomy = $block_instance->context['taxonomy'] ?? '';
}

// If we don't have required identifiers, bail early.
if ( empty( $term_id ) || empty( $taxonomy ) ) {
return null;
}

// Get the term data.
$term = get_term( $term_id, $taxonomy );
if ( is_wp_error( $term ) || ! $term ) {
return null;
}

// Check if taxonomy exists and is publicly queryable.
$taxonomy_object = get_taxonomy( $taxonomy );
if ( ! $taxonomy_object || ! $taxonomy_object->publicly_queryable ) {
if ( ! current_user_can( 'read' ) ) {
return null;
}
}

switch ( $source_args['key'] ) {
case 'id':
return esc_html( (string) $term_id );

case 'name':
return esc_html( $term->name );

case 'link':
// Only taxonomy entities are supported by Term Data.
$term_link = get_term_link( $term );
return is_wp_error( $term_link ) ? null : esc_url( $term_link );

case 'slug':
return esc_html( $term->slug );
Comment on lines +75 to +81
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ockham Is there overlap here? I guess slug might be used separately from link so maybe it's ok?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds okay to me -- especially since we don't currently have any way to "post-process" values from block bindings sources (e.g. we can't concatenate a BB source value with a string literal).


case 'description':
return wp_kses_post( $term->description );

case 'parent':
return esc_html( (string) $term->parent );

case 'count':
return esc_html( (string) $term->count );

default:
return null;
}
}

/**
* Registers Term Data source in the block bindings registry.
*
* @since 6.9.0
* @access private
*/
function gutenberg_register_block_bindings_term_data_source() {
if ( get_block_bindings_source( 'core/term-data' ) ) {
// The source is already registered.
return;
}

register_block_bindings_source(
'core/term-data',
array(
'label' => _x( 'Term Data', 'block bindings source' ),
'get_value_callback' => 'gutenberg_block_bindings_term_data_get_value',
'uses_context' => array( 'termId', 'taxonomy' ),
)
);
}

add_action( 'init', 'gutenberg_register_block_bindings_term_data_source' );
2 changes: 1 addition & 1 deletion lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/compat/wordpress-6.9/template-activate.php';
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/entity-block-bindings.php';
require __DIR__ . '/compat/wordpress-6.9/term-data-block-bindings.php';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These bindings were removed in #72275 but we need them so this PR reinstates them 😓

cc @ntsekouras @mikachan

require __DIR__ . '/compat/wordpress-6.9/rest-api.php';
require __DIR__ . '/compat/wordpress-6.9/class-gutenberg-hierarchical-sort.php';
require __DIR__ . '/compat/wordpress-6.9/block-comments.php';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1518,17 +1518,18 @@ describe( 'Creating Entities (eg: Posts, Pages)', () => {

await user.click( createButton );

searchInput = screen.getByRole( 'combobox', {
name: 'Search or type URL',
} );

const errorNotice = screen.getAllByText(
// Wait for the error message to appear after the async operation fails
const errorNotice = await screen.findByText(
'API response returned invalid entity.'
)[ 1 ];
);

// Catch the error in the test to avoid test failures.
expect( throwsError ).toThrow( Error );

searchInput = screen.getByRole( 'combobox', {
name: 'Search or type URL',
} );

// Check human readable error notice is perceivable.
expect( errorNotice ).toBeVisible();
// eslint-disable-next-line testing-library/no-node-access
Expand Down
9 changes: 6 additions & 3 deletions packages/block-library/src/navigation-link/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,10 @@ export default function NavigationLinkEdit( {
anchor={ popoverAnchor }
onRemove={ removeLink }
onChange={ ( updatedValue ) => {
const { isEntityLink } = updateAttributes(
const {
isEntityLink,
attributes: updatedAttributes,
} = updateAttributes(
updatedValue,
setAttributes,
attributes
Expand All @@ -553,9 +556,9 @@ export default function NavigationLinkEdit( {
// Only create bindings for entity links (posts, pages, taxonomies)
// Never create bindings for custom links (manual URLs)
if ( isEntityLink ) {
createBinding();
createBinding( updatedAttributes );
} else {
clearBinding();
clearBinding( updatedAttributes );
}
} }
/>
Expand Down
6 changes: 4 additions & 2 deletions packages/block-library/src/navigation-link/link-ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,12 @@ function UnforwardedLinkUI( props, ref ) {
name: postType,
} );

// Check if there's a URL binding with the core/entity source
// Check if there's a URL binding with the new binding sources
// Only enable handleEntities when there's actually a binding present
const hasUrlBinding =
metadata?.bindings?.url?.source === 'core/entity' && !! id;
( metadata?.bindings?.url?.source === 'core/post-data' ||
metadata?.bindings?.url?.source === 'core/term-data' ) &&
Comment on lines +84 to +85
Copy link
Contributor

Choose a reason for hiding this comment

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

Could do

Suggested change
( metadata?.bindings?.url?.source === 'core/post-data' ||
metadata?.bindings?.url?.source === 'core/term-data' ) &&
[ 'core/term-data', 'core/post-data' ].includes( metadata?.bindings?.url?.source ) &&

!! id;

// Memoize link value to avoid overriding the LinkControl's internal state.
// This is a temporary fix. See https://github.com/WordPress/gutenberg/issues/50976#issuecomment-1568226407.
Expand Down
15 changes: 12 additions & 3 deletions packages/block-library/src/navigation-link/shared/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { useInstanceId } from '@wordpress/compose';
import { safeDecodeURI } from '@wordpress/url';
import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
import { linkOff as unlinkIcon } from '@wordpress/icons';
import { useDispatch } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';

/**
* Internal dependencies
Expand Down Expand Up @@ -79,12 +81,19 @@ export function Controls( { attributes, setAttributes, clientId } ) {
attributes,
} );

// Get direct store dispatch to bypass setBoundAttributes wrapper
const { updateBlockAttributes } = useDispatch( blockEditorStore );

const editBoundLink = () => {
// Remove the binding
// Clear the binding first
clearBinding();

// Clear url and id to allow picking a new entity (keep type and kind)
setAttributes( { url: undefined, id: undefined } );
// Use direct store dispatch to bypass block bindings safeguards
// which prevent updates to bound attributes when calling setAttributes.
// setAttributes is actually setBoundAttributes, a wrapper function that
// processes attributes through the binding system.
// See: packages/block-editor/src/components/block-edit/edit.js
updateBlockAttributes( clientId, { url: '', id: undefined } );
};

return (
Expand Down
Loading
Loading