Skip to content

Commit da7588b

Browse files
committed
REST API: Allow-list ability schema response keywords
Switch the Abilities REST API schema response preparation from an internal-keyword deny-list to an allow-list, so ability input and output schemas expose only keywords from `rest_get_allowed_schema_keywords()\` plus a small set of additional draft-04 keywords. Unknown or WordPress-internal keywords (e.g. `sanitize_callback`, `example`, `context`, `readonly`) are no longer exposed to REST clients by default. Props gziolo, jorgefilipecosta. See #64955. git-svn-id: https://develop.svn.wordpress.org/trunk@62447 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 026d3e2 commit da7588b

2 files changed

Lines changed: 67 additions & 26 deletions

File tree

src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -189,15 +189,23 @@ public function get_item_permissions_check( $request ) {
189189
}
190190

191191
/**
192-
* WordPress-internal schema keywords to strip from REST responses.
192+
* Additional schema keywords to preserve in REST responses.
193193
*
194-
* @since 7.0.0
195-
* @var array<string, true>
194+
* Ability schemas are exposed to clients as JSON Schema. Preserve additional
195+
* draft-04 keywords so clients can validate richer schemas, even when some
196+
* of those keywords are not enforced by the server-side REST schema validator.
197+
*
198+
* @since 7.1.0
199+
* @var string[]
196200
*/
197-
private const INTERNAL_SCHEMA_KEYWORDS = array(
198-
'sanitize_callback' => true,
199-
'validate_callback' => true,
200-
'arg_options' => true,
201+
private const ADDITIONAL_ALLOWED_SCHEMA_KEYWORDS = array(
202+
'required',
203+
'allOf',
204+
'not',
205+
'$ref',
206+
'definitions',
207+
'dependencies',
208+
'additionalItems',
201209
);
202210

203211
/**
@@ -217,12 +225,11 @@ private function is_associative_array( $value ): bool {
217225
/**
218226
* Transforms an ability schema for REST response output.
219227
*
220-
* Ability schemas may include WordPress-internal properties like
221-
* `sanitize_callback`, `validate_callback`, and `arg_options` that are
222-
* used server-side but are not valid JSON Schema keywords. This method
223-
* removes those specific keys so they are not exposed in REST responses.
224-
* It also converts empty array defaults to objects when the schema type is
225-
* 'object' to ensure proper JSON serialization as {} instead of [].
228+
* Ability schemas may include WordPress-internal properties or unsupported
229+
* schema keywords that should not be exposed in REST responses. This method
230+
* strips keys not recognized by the REST API schema handling. It also
231+
* converts empty array defaults to objects when the schema type is 'object'
232+
* to ensure proper JSON serialization as {} instead of [].
226233
*
227234
* @since 7.1.0
228235
*
@@ -237,7 +244,17 @@ private function prepare_schema_for_response( array $schema ): array {
237244
}
238245
}
239246

240-
$schema = array_diff_key( $schema, self::INTERNAL_SCHEMA_KEYWORDS );
247+
// Computed once and reused across the recursive calls for every schema node.
248+
static $allowed_keywords = null;
249+
$allowed_keywords ??= array_fill_keys(
250+
array_merge(
251+
rest_get_allowed_schema_keywords(),
252+
self::ADDITIONAL_ALLOWED_SCHEMA_KEYWORDS
253+
),
254+
true
255+
);
256+
257+
$schema = array_intersect_key( $schema, $allowed_keywords );
241258

242259
// Sub-schema maps: keys are user-defined, values are sub-schemas.
243260
// Note: 'dependencies' values can also be property-dependency arrays

tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -829,23 +829,28 @@ public function test_filter_by_namespace_still_respects_show_in_rest(): void {
829829
}
830830

831831
/**
832-
* Test that WordPress-internal schema keywords are stripped from ability schemas in REST response.
832+
* Test that schema keywords outside the allow-list are stripped from ability schemas in REST response.
833833
*
834834
* @ticket 65035
835835
*/
836-
public function test_internal_schema_keywords_stripped_from_response(): void {
836+
public function test_unsupported_schema_keywords_stripped_from_response(): void {
837837
$this->register_test_ability(
838-
'test/with-internal-keywords',
838+
'test/with-unsupported-keywords',
839839
array(
840-
'label' => 'Test Internal Keywords',
841-
'description' => 'Tests stripping of internal schema keywords',
840+
'label' => 'Test Unsupported Keywords',
841+
'description' => 'Tests stripping of unsupported schema keywords',
842842
'category' => 'general',
843843
'input_schema' => array(
844844
'type' => 'object',
845+
'required' => array( 'content' ),
845846
'properties' => array(
846847
'content' => array(
847848
'type' => 'string',
848849
'description' => 'The content value.',
850+
'example' => 'example content',
851+
'examples' => array( 'example content' ),
852+
'context' => array( 'view', 'edit', 'embed' ),
853+
'readonly' => true,
849854
'sanitize_callback' => 'sanitize_text_field',
850855
'validate_callback' => 'is_string',
851856
'arg_options' => array( 'sanitize_callback' => 'wp_kses_post' ),
@@ -854,7 +859,13 @@ public function test_internal_schema_keywords_stripped_from_response(): void {
854859
),
855860
'output_schema' => array(
856861
'type' => 'string',
862+
'example' => 'example output',
863+
'examples' => array( 'example output' ),
864+
'context' => array( 'view', 'edit', 'embed' ),
865+
'readonly' => true,
857866
'sanitize_callback' => 'sanitize_text_field',
867+
'validate_callback' => 'is_string',
868+
'arg_options' => array( 'sanitize_callback' => 'wp_kses_post' ),
858869
),
859870
'execute_callback' => static function ( $input ) {
860871
return $input['content'];
@@ -864,7 +875,7 @@ public function test_internal_schema_keywords_stripped_from_response(): void {
864875
)
865876
);
866877

867-
$request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/with-internal-keywords' );
878+
$request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/with-unsupported-keywords' );
868879
$response = $this->server->dispatch( $request );
869880

870881
$this->assertSame( 200, $response->get_status() );
@@ -875,18 +886,29 @@ public function test_internal_schema_keywords_stripped_from_response(): void {
875886
$this->assertArrayHasKey( 'content', $data['input_schema']['properties'] );
876887
$this->assertArrayHasKey( 'output_schema', $data );
877888

878-
// Verify internal keywords are stripped from input_schema properties.
889+
// Verify unsupported schema keywords are stripped from input_schema properties.
879890
$content_schema = $data['input_schema']['properties']['content'];
880891
$this->assertArrayNotHasKey( 'sanitize_callback', $content_schema );
881892
$this->assertArrayNotHasKey( 'validate_callback', $content_schema );
882893
$this->assertArrayNotHasKey( 'arg_options', $content_schema );
894+
$this->assertArrayNotHasKey( 'example', $content_schema );
895+
$this->assertArrayNotHasKey( 'examples', $content_schema );
896+
$this->assertArrayNotHasKey( 'context', $content_schema );
897+
$this->assertArrayNotHasKey( 'readonly', $content_schema );
883898

884899
// Verify valid JSON Schema keywords are preserved.
885900
$this->assertSame( 'string', $content_schema['type'] );
886901
$this->assertSame( 'The content value.', $content_schema['description'] );
902+
$this->assertSame( array( 'content' ), $data['input_schema']['required'] );
887903

888904
// Verify internal keywords are stripped from output_schema.
889905
$this->assertArrayNotHasKey( 'sanitize_callback', $data['output_schema'] );
906+
$this->assertArrayNotHasKey( 'validate_callback', $data['output_schema'] );
907+
$this->assertArrayNotHasKey( 'arg_options', $data['output_schema'] );
908+
$this->assertArrayNotHasKey( 'example', $data['output_schema'] );
909+
$this->assertArrayNotHasKey( 'examples', $data['output_schema'] );
910+
$this->assertArrayNotHasKey( 'context', $data['output_schema'] );
911+
$this->assertArrayNotHasKey( 'readonly', $data['output_schema'] );
890912
$this->assertSame( 'string', $data['output_schema']['type'] );
891913
}
892914

@@ -947,19 +969,20 @@ public function test_nested_empty_object_schema_defaults_prepared_for_response()
947969
}
948970

949971
/**
950-
* Test that internal schema keywords are stripped from nested sub-schema locations.
972+
* Test that schema keywords outside the allow-list are stripped from nested sub-schema locations.
951973
*
952974
* @ticket 64098
953975
*/
954-
public function test_internal_schema_keywords_stripped_from_nested_sub_schemas(): void {
976+
public function test_unsupported_schema_keywords_stripped_from_nested_sub_schemas(): void {
955977
$this->register_test_ability(
956-
'test/nested-internal-keywords',
978+
'test/nested-unsupported-keywords',
957979
array(
958-
'label' => 'Test Nested Keywords',
980+
'label' => 'Test Nested Unsupported Keywords',
959981
'description' => 'Tests stripping from all sub-schema locations',
960982
'category' => 'general',
961983
'input_schema' => array(
962984
'type' => 'object',
985+
'$ref' => '#/definitions/address',
963986
'anyOf' => array(
964987
array(
965988
'type' => 'object',
@@ -1053,14 +1076,15 @@ public function test_internal_schema_keywords_stripped_from_nested_sub_schemas()
10531076
)
10541077
);
10551078

1056-
$request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/nested-internal-keywords' );
1079+
$request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/nested-unsupported-keywords' );
10571080
$response = $this->server->dispatch( $request );
10581081

10591082
$this->assertSame( 200, $response->get_status() );
10601083

10611084
$data = $response->get_data();
10621085

10631086
// Verify internal keywords are stripped from anyOf sub-schemas.
1087+
$this->assertSame( '#/definitions/address', $data['input_schema']['$ref'] );
10641088
$this->assertArrayHasKey( 'anyOf', $data['input_schema'] );
10651089
$this->assertArrayNotHasKey( 'sanitize_callback', $data['input_schema']['anyOf'][0] );
10661090
$this->assertSame( 'object', $data['input_schema']['anyOf'][0]['type'] );

0 commit comments

Comments
 (0)