From a73ffcb4c6a0c33b538d65db5b00ba187f2d1e88 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 28 May 2026 13:24:20 +0200 Subject: [PATCH 1/3] REST API: Allow-list ability schema response keywords. --- ...s-wp-rest-abilities-v1-list-controller.php | 42 ++++++++++++------- .../wpRestAbilitiesV1ListController.php | 2 + 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php index 85464fd9dd302..5beb7389aaeae 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php @@ -189,15 +189,21 @@ public function get_item_permissions_check( $request ) { } /** - * WordPress-internal schema keywords to strip from REST responses. + * Additional schema keywords to preserve in REST responses. * - * @since 7.0.0 - * @var array + * These are not included in rest_get_allowed_schema_keywords(), but are + * still recognized as schema traversal locations for ability schemas. + * + * @since 7.1.0 + * @var string[] */ - private const INTERNAL_SCHEMA_KEYWORDS = array( - 'sanitize_callback' => true, - 'validate_callback' => true, - 'arg_options' => true, + private const ADDITIONAL_ALLOWED_SCHEMA_KEYWORDS = array( + 'required', + 'allOf', + 'not', + 'definitions', + 'dependencies', + 'additionalItems', ); /** @@ -217,12 +223,11 @@ private function is_associative_array( $value ): bool { /** * Transforms an ability schema for REST response output. * - * Ability schemas may include WordPress-internal properties like - * `sanitize_callback`, `validate_callback`, and `arg_options` that are - * used server-side but are not valid JSON Schema keywords. This method - * removes those specific keys so they are not exposed in REST responses. - * It also converts empty array defaults to objects when the schema type is - * 'object' to ensure proper JSON serialization as {} instead of []. + * Ability schemas may include WordPress-internal properties or unsupported + * schema keywords that should not be exposed in REST responses. This method + * strips keys not recognized by the REST API schema handling. It also + * converts empty array defaults to objects when the schema type is 'object' + * to ensure proper JSON serialization as {} instead of []. * * @since 7.1.0 * @@ -237,7 +242,16 @@ private function prepare_schema_for_response( array $schema ): array { } } - $schema = array_diff_key( $schema, self::INTERNAL_SCHEMA_KEYWORDS ); + $schema = array_intersect_key( + $schema, + array_fill_keys( + array_merge( + rest_get_allowed_schema_keywords(), + self::ADDITIONAL_ALLOWED_SCHEMA_KEYWORDS + ), + true + ) + ); // Sub-schema maps: keys are user-defined, values are sub-schemas. // Note: 'dependencies' values can also be property-dependency arrays diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php index 5ba688cb57c79..9a1c95e6589fd 100644 --- a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php @@ -846,6 +846,7 @@ public function test_internal_schema_keywords_stripped_from_response(): void { 'content' => array( 'type' => 'string', 'description' => 'The content value.', + 'examples' => array( 'example content' ), 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'is_string', 'arg_options' => array( 'sanitize_callback' => 'wp_kses_post' ), @@ -880,6 +881,7 @@ public function test_internal_schema_keywords_stripped_from_response(): void { $this->assertArrayNotHasKey( 'sanitize_callback', $content_schema ); $this->assertArrayNotHasKey( 'validate_callback', $content_schema ); $this->assertArrayNotHasKey( 'arg_options', $content_schema ); + $this->assertArrayNotHasKey( 'examples', $content_schema ); // Verify valid JSON Schema keywords are preserved. $this->assertSame( 'string', $content_schema['type'] ); From 7d7328f08fb2458e103890225865e6e7bd304d15 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 28 May 2026 14:13:58 +0200 Subject: [PATCH 2/3] REST API: Preserve client-side ability schema keywords --- .../class-wp-rest-abilities-v1-list-controller.php | 6 ++++-- .../tests/rest-api/wpRestAbilitiesV1ListController.php | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php index 5beb7389aaeae..faf712e212ae7 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php @@ -191,8 +191,9 @@ public function get_item_permissions_check( $request ) { /** * Additional schema keywords to preserve in REST responses. * - * These are not included in rest_get_allowed_schema_keywords(), but are - * still recognized as schema traversal locations for ability schemas. + * Ability schemas are exposed to clients as JSON Schema. Preserve additional + * draft-04 keywords so clients can validate richer schemas, even when some + * of those keywords are not enforced by the server-side REST schema validator. * * @since 7.1.0 * @var string[] @@ -201,6 +202,7 @@ public function get_item_permissions_check( $request ) { 'required', 'allOf', 'not', + '$ref', 'definitions', 'dependencies', 'additionalItems', diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php index 9a1c95e6589fd..38f251b00ef2f 100644 --- a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php @@ -962,6 +962,7 @@ public function test_internal_schema_keywords_stripped_from_nested_sub_schemas() 'category' => 'general', 'input_schema' => array( 'type' => 'object', + '$ref' => '#/definitions/address', 'anyOf' => array( array( 'type' => 'object', @@ -1063,6 +1064,7 @@ public function test_internal_schema_keywords_stripped_from_nested_sub_schemas() $data = $response->get_data(); // Verify internal keywords are stripped from anyOf sub-schemas. + $this->assertSame( '#/definitions/address', $data['input_schema']['$ref'] ); $this->assertArrayHasKey( 'anyOf', $data['input_schema'] ); $this->assertArrayNotHasKey( 'sanitize_callback', $data['input_schema']['anyOf'][0] ); $this->assertSame( 'object', $data['input_schema']['anyOf'][0]['type'] ); From 4145fcec200145d7eb62737cb3105bd7ce8c9d7e Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Thu, 28 May 2026 18:14:17 +0200 Subject: [PATCH 3/3] REST API: Expand ability schema keyword tests --- .../wpRestAbilitiesV1ListController.php | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php index 38f251b00ef2f..0a34f9a40f8d5 100644 --- a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php @@ -842,11 +842,15 @@ public function test_internal_schema_keywords_stripped_from_response(): void { 'category' => 'general', 'input_schema' => array( 'type' => 'object', + 'required' => array( 'content' ), 'properties' => array( 'content' => array( 'type' => 'string', 'description' => 'The content value.', + 'example' => 'example content', 'examples' => array( 'example content' ), + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'is_string', 'arg_options' => array( 'sanitize_callback' => 'wp_kses_post' ), @@ -855,7 +859,13 @@ public function test_internal_schema_keywords_stripped_from_response(): void { ), 'output_schema' => array( 'type' => 'string', + 'example' => 'example output', + 'examples' => array( 'example output' ), + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'is_string', + 'arg_options' => array( 'sanitize_callback' => 'wp_kses_post' ), ), 'execute_callback' => static function ( $input ) { return $input['content']; @@ -876,19 +886,29 @@ public function test_internal_schema_keywords_stripped_from_response(): void { $this->assertArrayHasKey( 'content', $data['input_schema']['properties'] ); $this->assertArrayHasKey( 'output_schema', $data ); - // Verify internal keywords are stripped from input_schema properties. + // Verify unsupported schema keywords are stripped from input_schema properties. $content_schema = $data['input_schema']['properties']['content']; $this->assertArrayNotHasKey( 'sanitize_callback', $content_schema ); $this->assertArrayNotHasKey( 'validate_callback', $content_schema ); $this->assertArrayNotHasKey( 'arg_options', $content_schema ); + $this->assertArrayNotHasKey( 'example', $content_schema ); $this->assertArrayNotHasKey( 'examples', $content_schema ); + $this->assertArrayNotHasKey( 'context', $content_schema ); + $this->assertArrayNotHasKey( 'readonly', $content_schema ); // Verify valid JSON Schema keywords are preserved. $this->assertSame( 'string', $content_schema['type'] ); $this->assertSame( 'The content value.', $content_schema['description'] ); + $this->assertSame( array( 'content' ), $data['input_schema']['required'] ); // Verify internal keywords are stripped from output_schema. $this->assertArrayNotHasKey( 'sanitize_callback', $data['output_schema'] ); + $this->assertArrayNotHasKey( 'validate_callback', $data['output_schema'] ); + $this->assertArrayNotHasKey( 'arg_options', $data['output_schema'] ); + $this->assertArrayNotHasKey( 'example', $data['output_schema'] ); + $this->assertArrayNotHasKey( 'examples', $data['output_schema'] ); + $this->assertArrayNotHasKey( 'context', $data['output_schema'] ); + $this->assertArrayNotHasKey( 'readonly', $data['output_schema'] ); $this->assertSame( 'string', $data['output_schema']['type'] ); }