From 121f3e7e82671688e1daf5a0f39b56c68c90b944 Mon Sep 17 00:00:00 2001 From: liaisontw Date: Thu, 30 Apr 2026 13:05:34 +0800 Subject: [PATCH] REST API: Ensure empty meta is returned as an object in view context --- .../class-wp-rest-blocks-controller.php | 11 ++- .../fields/class-wp-rest-meta-fields.php | 12 ++- .../tests/rest-api/rest-blocks-controller.php | 87 +++++++++++++++++++ 3 files changed, 107 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-blocks-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-blocks-controller.php index b7286bea746e4..0066e720ad5f2 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-blocks-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-blocks-controller.php @@ -58,8 +58,15 @@ public function filter_response_by_context( $data, $context ) { unset( $data['content']['rendered'] ); // Add the core wp_pattern_sync_status meta as top level property to the response. - $data['wp_pattern_sync_status'] = $data['meta']['wp_pattern_sync_status'] ?? ''; - unset( $data['meta']['wp_pattern_sync_status'] ); + $meta = (array) $data['meta']; + $data['wp_pattern_sync_status'] = $meta['wp_pattern_sync_status'] ?? ''; + + if ( is_object( $data['meta'] ) ) { + unset( $data['meta']->wp_pattern_sync_status ); + } else { + unset( $data['meta']['wp_pattern_sync_status'] ); + } + return $data; } diff --git a/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php b/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php index a9c3fbcde831a..e35abde61d88e 100644 --- a/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php +++ b/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php @@ -74,7 +74,8 @@ public function register_field() { * * @param int $object_id Object ID to fetch meta for. * @param WP_REST_Request $request Full details about the request. - * @return array Array containing the meta values keyed by name. + * @return array|object Array containing the meta values keyed by name, + * or an empty object if to ensure JSON object encoding. */ public function get_value( $object_id, $request ) { $fields = $this->get_registered_fields(); @@ -105,6 +106,11 @@ public function get_value( $object_id, $request ) { $response[ $name ] = $value; } + // Use stdClass so that JSON result is {} and not []. + if ( empty( $response ) ) { + return (object) array(); + } + return $response; } @@ -582,6 +588,10 @@ public static function prepare_value( $value, $request, $args ) { * @return array|false The meta array, if valid, false otherwise. */ public function check_meta_is_array( $value, $request, $param ) { + if ( is_object( $value ) ) { + $value = (array) $value; + } + if ( ! is_array( $value ) ) { return false; } diff --git a/tests/phpunit/tests/rest-api/rest-blocks-controller.php b/tests/phpunit/tests/rest-api/rest-blocks-controller.php index 43a181683c788..63ae262834410 100644 --- a/tests/phpunit/tests/rest-api/rest-blocks-controller.php +++ b/tests/phpunit/tests/rest-api/rest-blocks-controller.php @@ -255,4 +255,91 @@ public function test_wp_patterns_sync_status_post_meta() { $this->assertArrayHasKey( 'wp_pattern_sync_status', $data ); $this->assertArrayNotHasKey( 'wp_pattern_sync_status', $data['meta'] ); } + + /** + * Tests that the Blocks Controller handles an empty object (stdClass) for meta correctly. + * + * This verifies the compatibility of filter_response_by_context with object types + * introduced in the patch. + * + * @ticket 54484 + */ + public function test_filter_response_by_context_with_empty_object_meta() { + wp_set_current_user( self::$user_ids['editor'] ); + + // Register meta for testing. + register_post_meta( + 'wp_block', + 'wp_pattern_sync_status', + array( + 'single' => true, + 'type' => 'string', + 'show_in_rest' => true, + ) + ); + + // Force simulate meta being converted to an object (simulating Patch B's get_value behavior). + add_filter( + 'rest_prepare_wp_block', + function ( $response ) { + $data = $response->get_data(); + // Simulate when meta is empty or cast to an object. + if ( empty( $data['meta'] ) ) { + $data['meta'] = new stdClass(); + } + $response->set_data( $data ); + return $response; + }, + 10 + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/blocks/' . self::$post_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + // Verify: Top-level property should exist even if meta is an object. + $this->assertArrayHasKey( 'wp_pattern_sync_status', $data ); + + // Verify: Internal meta property should be correctly unset (regardless of being an object or array). + if ( is_object( $data['meta'] ) ) { + $this->assertObjectNotHasAttribute( 'wp_pattern_sync_status', $data['meta'] ); + } else { + $this->assertArrayNotHasKey( 'wp_pattern_sync_status', $data['meta'] ); + } + } + + /** + * Tests that check_meta_is_array correctly handles object types. + * + * @ticket 54484 + */ + public function test_check_meta_is_array_accepts_object() { + wp_set_current_user( self::$user_ids['editor'] ); + + // Implement all abstract methods required by WP_REST_Meta_Fields. + $meta_fields = new class( 'post' ) extends WP_REST_Meta_Fields { + protected function get_meta_type() { + return 'post'; + } + + protected function get_rest_field_type() { + return 'post'; + } + }; + + // Simulate an object parsed from JSON (stdClass). + $object_value = new stdClass(); + $object_value->test_key = 'test_value'; + + // Execute the method from the patch. + $result = $meta_fields->check_meta_is_array( $object_value, new WP_REST_Request(), 'meta' ); + + // Verify: The object should be converted to an array to pass validation. + $this->assertIsArray( $result, 'The object should be converted to an array to pass validation.' ); + $this->assertArrayHasKey( 'test_key', $result ); + $this->assertSame( 'test_value', $result['test_key'] ); + + // Verify: Values that are neither object nor array (e.g., string) should still return false. + $this->assertFalse( $meta_fields->check_meta_is_array( 'not-an-array', new WP_REST_Request(), 'meta' ) ); + } }