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

Blocks: Refactor the check against self for block hooks #5218

Closed

Conversation

gziolo
Copy link
Member

@gziolo gziolo commented Sep 14, 2023

Trac ticket: https://core.trac.wordpress.org/ticket/59346

I added some refactorings discussed in #5203 (review):


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

Copy link
Contributor

@ockham ockham left a comment

Choose a reason for hiding this comment

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

See my comment -- I really think we should keep the check for self-insertion in register_block_type_from_metadata.

Comment on lines 525 to 538
// Avoid infinite recursion in block hooks (hooking to itself).
if ( 'block_hooks' === $property_name ) {
if ( ! is_array( $property_value ) ) {
continue;
}
if ( array_key_exists( $this->name, $property_value ) ) {
_doing_it_wrong(
__METHOD__,
__( 'Cannot hook block to itself.' ),
'6.4.0'
);
unset( $property_value[ $this->name ] );
}
}
Copy link
Contributor

@ockham ockham Sep 14, 2023

Choose a reason for hiding this comment

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

Sorry, I really think we should keep this in register_block_type_from_metadata 😬 AFAICS, no other field is being validated here in WP_Block_Type -- they're all processed in register_block_type_from_metadata, so I'd rather keep it consistent.

Note that register_block_type actually calls register_block_type_from_metadata for block registration, so any block type registered via register_block_type will in fact undergo the validation in register_block_type_from_metadata:

function register_block_type( $block_type, $args = array() ) {
if ( is_string( $block_type ) && file_exists( $block_type ) ) {
return register_block_type_from_metadata( $block_type, $args );
}
return WP_Block_Type_Registry::get_instance()->register( $block_type, $args );
}

Finally, note that there's even an "inner" inconsistency with this PR, as validation of the allowed relative positions is still done in register_block_type_from_metadata 😅

Copy link
Member Author

Choose a reason for hiding this comment

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

I left a #5218 (comment) with the details from my debugging after trying to use the test with the version before refactoring.

Finally, note that there's even an "inner" inconsistency with this PR, as validation of the allowed relative positions is still done in register_block_type_from_metadata 😅

Yes, that could get improved, but we first need to settle on the responsibilities of processing block.json files. So far, it is mostly about mapping the JSON object to the matching fields in WP_Block_Type.

AFAICS, no other field is being validated here in WP_Block_Type

There are some issues filed in Gutenberg to address it:

Comment on lines +649 to +654
array(
'tests/before' => 'before',
'tests/after' => 'after',
'tests/first-child' => 'first_child',
'tests/last-child' => 'last_child',
),
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@gziolo
Copy link
Member Author

gziolo commented Sep 15, 2023

See my comment -- I really think we should keep the check for self-insertion in register_block_type_from_metadata.

I tried locally reverting the changes for the code moved to WP_Block_Type::set_props. I basically used the current trunk with the same unit test but modified @expectedIncorrectUsage line:

diff --git a/tests/phpunit/tests/blocks/register.php b/tests/phpunit/tests/blocks/register.php
index 55faf297a5..2c1cf64937 100644
--- a/tests/phpunit/tests/blocks/register.php
+++ b/tests/phpunit/tests/blocks/register.php
@@ -1069,4 +1069,29 @@ class Tests_Blocks_Register extends WP_UnitTestCase {
 		$actual = register_block_style( 'core/query', $block_styles );
 		$this->assertTrue( $actual );
 	}
+
+	/**
+	 * @ticket 59346
+	 *
+	 * @covers ::register_block_type
+	 *
+	 * @expectedIncorrectUsage register_block_type_from_metadata
+	 */
+	public function test_register_block_hooks_targeting_itself() {
+		$block_name = 'tests/block-name';
+		$block_type = register_block_type(
+			$block_name,
+			array(
+				'block_hooks' => array(
+					$block_name         => 'first',
+					'tests/other-block' => 'last',
+				),
+			)
+		);
+
+		$this->assertSameSets(
+			array( 'tests/other-block' => 'last' ),
+			$block_type->block_hooks
+		);
+	}
 }

The test fails:

Screenshot 2023-09-15 at 09 01 14

No changes get applied.


When inspecting the code it turns out that register_block_type_from_metadata isn't the universal way to register block type:

function register_block_type( $block_type, $args = array() ) {
if ( is_string( $block_type ) && file_exists( $block_type ) ) {
return register_block_type_from_metadata( $block_type, $args );
}
return WP_Block_Type_Registry::get_instance()->register( $block_type, $args );
}

If no path to the block.json file gets passed as the first param, then the registration goes directly through the WP_Block_Type_Registry. It's also what register_block_type_from_metadata calls after all transformations applied to the loaded block.json file:

return WP_Block_Type_Registry::get_instance()->register(
$metadata['name'],
$settings
);

We can keep the implementation as it is currently works in trunk and refactor the test to use some mocked block.json file. The drawback is that _doing_it_wrong check won't run in the case when someone opts into registering the block type using the syntax that has been present in WordPress core since version 5.0. The same applies to the case when someone uses the registry directly with WP_Block_Type_Registry::get_instance()->register(). Let me know which approach you prefer, given all the above.

@ockham
Copy link
Contributor

ockham commented Sep 18, 2023

When inspecting the code it turns out that register_block_type_from_metadata isn't the universal way to register block type:

function register_block_type( $block_type, $args = array() ) {
if ( is_string( $block_type ) && file_exists( $block_type ) ) {
return register_block_type_from_metadata( $block_type, $args );
}
return WP_Block_Type_Registry::get_instance()->register( $block_type, $args );
}

If no path to the block.json file gets passed as the first param, then the registration goes directly through the WP_Block_Type_Registry.

You're of course right 🤦‍♂️ My apologies, I'd read that code but totally glossed over the if ( is_string( $block_type ) && file_exists( $block_type ) ) conditional.

It's also what register_block_type_from_metadata calls after all transformations applied to the loaded block.json file:

return WP_Block_Type_Registry::get_instance()->register(
$metadata['name'],
$settings
);

We can keep the implementation as it is currently works in trunk and refactor the test to use some mocked block.json file. The drawback is that _doing_it_wrong check won't run in the case when someone opts into registering the block type using the syntax that has been present in WordPress core since version 5.0. The same applies to the case when someone uses the registry directly with WP_Block_Type_Registry::get_instance()->register(). Let me know which approach you prefer, given all the above.

I think since there's no precedent of doing any processing or validation in WP_Block_Type, I'd like to keep it that way, even if it means that people could bypass the check in register_block_type_from_metadata. In any case, I'd like to have a check that prevents infinite recursion by disallowing self-insertion later in insert_hooked_block (or whatever we're going to call it), regardless of how the block was registered.

@gziolo
Copy link
Member Author

gziolo commented Sep 18, 2023

Thanks for the feedback @ockham, I will refactor the code accordingly.

@gziolo gziolo force-pushed the update/refactor-block-hooks-checks-self branch from 6e3aaba to 2089f54 Compare September 18, 2023 09:47
Copy link
Contributor

@ockham ockham left a comment

Choose a reason for hiding this comment

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

Thank you! LGTM :shipit:

@gziolo
Copy link
Member Author

gziolo commented Sep 18, 2023

Committed with https://core.trac.wordpress.org/changeset/56607.

@gziolo gziolo closed this Sep 18, 2023
@gziolo gziolo deleted the update/refactor-block-hooks-checks-self branch September 18, 2023 10:33
@ockham
Copy link
Contributor

ockham commented Sep 27, 2023

Just a short note: The method we landed on for inserting hooked blocks into their designated positions should make infinite loops of blocks inserting themselves next to each other impossible, if I'm not mistaken.

The crux is that we no longer insert parsed hooked blocks into the block tree, where they might be traversed themselves by traverse_and_serialized, and blocks that are hooked to them would be inserted, potentially causing infinite recursion. Instead, we now only concatenate the hooked block's serialized markup during the traversal and serialization step to its anchor block's. That serialized markup is not subjected to any further parsing or traversal that could cause recursive block insertion.

In other words, while it's nice to prohibit "self-insertion" at block type registration stage, it's not technically required -- things won't break if a block does end up registering itself for "self-insertion".

@gziolo
Copy link
Member Author

gziolo commented Sep 28, 2023

In other words, while it's nice to prohibit "self-insertion" at block type registration stage, it's not technically required -- things won't break if a block does end up registering itself for "self-insertion".

That's another excellent side-effect of the revised implementation. I really like how you integrated Block Hooks into WordPress core, taking advantage of the access to all internals. It seems fine to leave the validation as is. By the way, there is an open issue to add validation for every argument to WP_Block_Type: WordPress/gutenberg#48039.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants