diff --git a/src/wp-includes/block-bindings/post-meta.php b/src/wp-includes/block-bindings/post-meta.php index 5aa6bf048d07d..75062f5ca3ea9 100644 --- a/src/wp-includes/block-bindings/post-meta.php +++ b/src/wp-includes/block-bindings/post-meta.php @@ -34,6 +34,19 @@ function _block_bindings_post_meta_get_value( array $source_args, $block_instanc return null; } + // Check if the meta field is protected. + if ( is_protected_meta( $source_args['key'], 'post' ) ) { + return null; + } + + // Check if the meta field is registered to be shown in REST. + $meta_keys = get_registered_meta_keys( 'post', $block_instance->context['postType'] ); + // Add fields registered for all subtypes. + $meta_keys = array_merge( $meta_keys, get_registered_meta_keys( 'post', '' ) ); + if ( empty( $meta_keys[ $source_args['key'] ]['show_in_rest'] ) ) { + return null; + } + return get_post_meta( $post_id, $source_args['key'], true ); } diff --git a/tests/phpunit/tests/block-bindings/postMetaSource.php b/tests/phpunit/tests/block-bindings/postMetaSource.php new file mode 100644 index 0000000000000..81a8a5861adc3 --- /dev/null +++ b/tests/phpunit/tests/block-bindings/postMetaSource.php @@ -0,0 +1,269 @@ +post_content = $content; + return apply_filters( 'the_content', $GLOBALS['post']->post_content ); + } + + /** + * Sets up shared fixtures. + */ + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + self::$post = $factory->post->create_and_get(); + self::$wp_meta_keys_saved = isset( $GLOBALS['wp_meta_keys'] ) ? $GLOBALS['wp_meta_keys'] : array(); + } + + /** + * Tear down after class. + */ + public static function wpTearDownAfterClass() { + $GLOBALS['wp_meta_keys'] = self::$wp_meta_keys_saved; + } + + /** + * Set up before each test. + * + * @since 6.5.0 + */ + public function set_up() { + parent::set_up(); + // Needed because tear_down() will reset it between tests. + $GLOBALS['post'] = self::$post; + } + + /** + * Tests that a block connected to a custom field renders its value. + * + * @ticket 60651 + */ + public function test_custom_field_value_is_rendered() { + register_meta( + 'post', + 'tests_custom_field', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'default' => 'Custom field value', + ) + ); + + $content = $this->get_modified_post_content( '
Fallback value
' ); + $this->assertSame( + 'Custom field value
', + $content, + 'The post content should show the value of the custom field . ' + ); + } + + /** + * Tests that an html attribute connected to a custom field renders its value. + * + * @ticket 60651 + */ + public function test_html_attribute_connected_to_custom_field_value_is_rendered() { + register_meta( + 'post', + 'tests_url_custom_field', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'default' => 'https://example.com/foo.png', + ) + ); + + $content = $this->get_modified_post_content( '' ); + $this->assertSame( + '', + $content, + 'The image src should point to the value of the custom field . ' + ); + } + + /** + * Tests that a blocks connected in a password protected post don't render the value. + * + * @ticket 60651 + */ + public function test_custom_field_value_is_not_shown_in_password_protected_posts() { + register_meta( + 'post', + 'tests_custom_field', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'default' => 'Custom field value', + ) + ); + + add_filter( 'post_password_required', '__return_true' ); + + $content = $this->get_modified_post_content( 'Fallback value
' ); + + remove_filter( 'post_password_required', '__return_true' ); + + $this->assertSame( + 'Fallback value
', + $content, + 'The post content should show the fallback value instead of the custom field value.' + ); + } + + /** + * Tests that a blocks connected in a post that is not publicly viewable don't render the value. + * + * @ticket 60651 + */ + public function test_custom_field_value_is_not_shown_in_non_viewable_posts() { + register_meta( + 'post', + 'tests_custom_field', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'default' => 'Custom field value', + ) + ); + + add_filter( 'is_post_status_viewable', '__return_false' ); + + $content = $this->get_modified_post_content( 'Fallback value
' ); + + remove_filter( 'is_post_status_viewable', '__return_false' ); + + $this->assertSame( + 'Fallback value
', + $content, + 'The post content should show the fallback value instead of the custom field value.' + ); + } + + /** + * Tests that a block connected to a meta key that doesn't exist renders the fallback. + * + * @ticket 60651 + */ + public function test_binding_to_non_existing_meta_key() { + $content = $this->get_modified_post_content( 'Fallback value
' ); + + $this->assertSame( + 'Fallback value
', + $content, + 'The post content should show the fallback value.' + ); + } + + /** + * Tests that a block connected without specifying the custom field renders the fallback. + * + * @ticket 60651 + */ + public function test_binding_without_key_renders_the_fallback() { + $content = $this->get_modified_post_content( 'Fallback value
' ); + + $this->assertSame( + 'Fallback value
', + $content, + 'The post content should show the fallback value.' + ); + } + + /** + * Tests that a block connected to a protected field doesn't show the value. + * + * @ticket 60651 + */ + public function test_protected_field_value_is_not_shown() { + register_meta( + 'post', + '_tests_protected_field', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'default' => 'Protected value', + ) + ); + + $content = $this->get_modified_post_content( 'Fallback value
' ); + + $this->assertSame( + 'Fallback value
', + $content, + 'The post content should show the fallback value instead of the protected value.' + ); + } + + /** + * Tests that a block connected to a field not exposed in the REST API doesn't show the value. + * + * @ticket 60651 + */ + public function test_custom_field_not_exposed_in_rest_api_is_not_shown() { + register_meta( + 'post', + 'tests_show_in_rest_false_field', + array( + 'show_in_rest' => false, + 'single' => true, + 'type' => 'string', + 'default' => 'Protected value', + ) + ); + + $content = $this->get_modified_post_content( 'Fallback value
' ); + + $this->assertSame( + 'Fallback value
', + $content, + 'The post content should show the fallback value instead of the protected value.' + ); + } + + /** + * Tests that meta key with unsafe HTML is sanitized. + * + * @ticket 60651 + */ + public function test_custom_field_with_unsafe_html_is_sanitized() { + register_meta( + 'post', + 'tests_unsafe_html_field', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'default' => '', + ) + ); + + $content = $this->get_modified_post_content( 'Fallback value
' ); + + $this->assertSame( + 'alert(“Unsafe HTML”)
', + $content, + 'The post content should not include the script tag.' + ); + } +} diff --git a/tests/phpunit/tests/block-bindings/render.php b/tests/phpunit/tests/block-bindings/render.php index 03da454e87b3d..aac4c417fd43f 100644 --- a/tests/phpunit/tests/block-bindings/render.php +++ b/tests/phpunit/tests/block-bindings/render.php @@ -198,4 +198,40 @@ public function test_update_block_with_value_from_source_image_placeholder() { '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.' + ); + } }