Skip to content

Commit 859d7fe

Browse files
committed
REST API: Add a shared helper for JSON Schema allowed keywords.
Introduce `wp_get_json_schema_allowed_keywords()` as the single place to decide which JSON Schema keywords may stay in schema output. It supports two profiles: `rest-api` (the default, used for REST route output) and `draft-04` (a wider draft-04 set used when publishing schemas to clients such as the Abilities API). Route schema output now flows through the helper and the new `wp_json_schema_allowed_keywords` filter, replacing the Abilities-specific special casing in the REST list controller. Validation internals still call `rest_get_allowed_schema_keywords()` directly, so REST validation behavior does not change. Props gziolo, apermo. See #64955. git-svn-id: https://develop.svn.wordpress.org/trunk@62549 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 27a0d86 commit 859d7fe

7 files changed

Lines changed: 206 additions & 35 deletions

File tree

src/wp-includes/json-schema.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
/**
3+
* JSON Schema API: shared functions for working with JSON Schema.
4+
*
5+
* @package WordPress
6+
* @subpackage JSON_Schema
7+
* @since 7.1.0
8+
*/
9+
10+
/**
11+
* Gets the JSON Schema keywords allowed for a given schema profile.
12+
*
13+
* Use the returned list to decide which keywords to keep when a schema is
14+
* output as JSON. Both profiles describe JSON Schema draft-04 output, also
15+
* called JSON Schema Version 4. They differ only in how much of the keyword
16+
* vocabulary stays in the result.
17+
*
18+
* - 'rest-api' returns the subset of draft-04 that the WordPress REST API
19+
* uses for route output. This is the default.
20+
* - 'draft-04' returns the larger draft-04 set used when publishing a schema
21+
* as a standalone document to clients, such as the Abilities API. It keeps
22+
* documentation and passthrough keywords like '$ref', 'definitions',
23+
* 'allOf', 'not', 'dependencies', and 'additionalItems'.
24+
*
25+
* The keywords are allowed to stay in the schema output. This does not mean
26+
* WordPress validates or sanitizes values against them.
27+
*
28+
* @since 7.1.0
29+
*
30+
* @param string $schema_profile Optional. Name of the schema profile to get keywords for.
31+
* Accepts 'rest-api' or 'draft-04'. Any other value falls
32+
* back to the 'rest-api' profile. Default 'rest-api'.
33+
* @return string[] Allowed JSON Schema keywords.
34+
*/
35+
function wp_get_json_schema_allowed_keywords( string $schema_profile = 'rest-api' ): array {
36+
$rest_keywords = rest_get_allowed_schema_keywords();
37+
38+
$keywords_by_profile = array(
39+
'rest-api' => $rest_keywords,
40+
'draft-04' => array_merge(
41+
array(
42+
'$schema',
43+
'id',
44+
'$ref',
45+
),
46+
$rest_keywords,
47+
array(
48+
'required',
49+
'allOf',
50+
'not',
51+
'definitions',
52+
'dependencies',
53+
'additionalItems',
54+
)
55+
),
56+
);
57+
58+
$allowed_keywords = $keywords_by_profile[ $schema_profile ] ?? $rest_keywords;
59+
60+
/**
61+
* Filters the JSON Schema keywords allowed for a given schema profile.
62+
*
63+
* Adding a keyword lets it stay in the schema output for that profile.
64+
* It does not make WordPress validate or sanitize values against the keyword.
65+
*
66+
* @since 7.1.0
67+
*
68+
* @param string[] $allowed_keywords Allowed JSON Schema keywords.
69+
* @param string $schema_profile The schema profile the keywords are for.
70+
*/
71+
return apply_filters( 'wp_json_schema_allowed_keywords', $allowed_keywords, $schema_profile );
72+
}

src/wp-includes/rest-api/class-wp-rest-server.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1649,7 +1649,7 @@ public function get_data_for_route( $route, $callbacks, $context = 'view' ) {
16491649
}
16501650
}
16511651

1652-
$allowed_schema_keywords = array_flip( rest_get_allowed_schema_keywords() );
1652+
$allowed_schema_keywords = array_flip( wp_get_json_schema_allowed_keywords( 'rest-api' ) );
16531653

16541654
$route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );
16551655

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

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ class WP_REST_Abilities_V1_List_Controller extends WP_REST_Controller {
3434
*/
3535
protected $rest_base = 'abilities';
3636

37+
/**
38+
* Lookup map of allowed schema keywords for preparing ability schemas in REST responses.
39+
*
40+
* Keyword names are stored as keys so they can be matched with
41+
* array_intersect_key(). Computed lazily on first use and reused while
42+
* preparing nested schemas.
43+
*
44+
* @since 7.1.0
45+
* @var array<string, true>
46+
*/
47+
private array $allowed_schema_keyword_lookup;
48+
3749
/**
3850
* Registers the routes for abilities.
3951
*
@@ -193,26 +205,6 @@ public function get_item_permissions_check( $request ) {
193205
return current_user_can( 'read' );
194206
}
195207

196-
/**
197-
* Additional schema keywords to preserve in REST responses.
198-
*
199-
* Ability schemas are exposed to clients as JSON Schema. Preserve additional
200-
* draft-04 keywords so clients can validate richer schemas, even when some
201-
* of those keywords are not enforced by the server-side REST schema validator.
202-
*
203-
* @since 7.1.0
204-
* @var string[]
205-
*/
206-
private const ADDITIONAL_ALLOWED_SCHEMA_KEYWORDS = array(
207-
'required',
208-
'allOf',
209-
'not',
210-
'$ref',
211-
'definitions',
212-
'dependencies',
213-
'additionalItems',
214-
);
215-
216208
/**
217209
* Determines whether the value is an associative array.
218210
*
@@ -227,6 +219,26 @@ private function is_associative_array( $value ): bool {
227219
return is_array( $value ) && ! wp_is_numeric_array( $value );
228220
}
229221

222+
/**
223+
* Gets the allowed schema keywords for preparing ability schemas in REST responses.
224+
*
225+
* Uses the fuller draft-04 keyword set, not the smaller REST API subset.
226+
* The published schema is consumed by clients that re-validate values
227+
* against standard draft-04, so it keeps the keywords those validators
228+
* expect.
229+
*
230+
* @since 7.1.0
231+
*
232+
* @return array<string, true> Allowed schema keywords.
233+
*/
234+
private function get_allowed_schema_keywords_for_response(): array {
235+
if ( ! isset( $this->allowed_schema_keyword_lookup ) ) {
236+
$this->allowed_schema_keyword_lookup = array_fill_keys( wp_get_json_schema_allowed_keywords( 'draft-04' ), true );
237+
}
238+
239+
return $this->allowed_schema_keyword_lookup;
240+
}
241+
230242
/**
231243
* Transforms an ability schema for REST response output.
232244
*
@@ -258,17 +270,7 @@ private function prepare_schema_for_response( array $schema ): array {
258270
}
259271
}
260272

261-
// Computed once and reused across the recursive calls for every schema node.
262-
static $allowed_keywords = null;
263-
$allowed_keywords ??= array_fill_keys(
264-
array_merge(
265-
rest_get_allowed_schema_keywords(),
266-
self::ADDITIONAL_ALLOWED_SCHEMA_KEYWORDS
267-
),
268-
true
269-
);
270-
271-
$schema = array_intersect_key( $schema, $allowed_keywords );
273+
$schema = array_intersect_key( $schema, $this->get_allowed_schema_keywords_for_response() );
272274

273275
// Collect draft-03 per-property `required: true` flags into a draft-04
274276
// `required` array of property names on the parent object schema.

src/wp-settings.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@
313313
require ABSPATH . WPINC . '/abilities-api.php';
314314
require ABSPATH . WPINC . '/abilities.php';
315315
require ABSPATH . WPINC . '/rest-api.php';
316+
require ABSPATH . WPINC . '/json-schema.php';
316317
require ABSPATH . WPINC . '/rest-api/class-wp-rest-server.php';
317318
require ABSPATH . WPINC . '/rest-api/class-wp-rest-response.php';
318319
require ABSPATH . WPINC . '/rest-api/class-wp-rest-request.php';

tests/phpunit/tests/abilities-api/wpRegisterCoreAbilities.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,9 +385,7 @@ public function test_core_get_environment_info_rejects_invalid_fields(): void {
385385
* @ticket 64384
386386
*/
387387
public function test_core_abilities_schemas_use_only_valid_keywords(): void {
388-
$allowed_keywords = rest_get_allowed_schema_keywords();
389-
// Add 'required' which is valid at the property level for draft-04.
390-
$allowed_keywords[] = 'required';
388+
$allowed_keywords = wp_get_json_schema_allowed_keywords( 'draft-04' );
391389

392390
$abilities = wp_get_abilities();
393391

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
/**
3+
* JSON Schema functions.
4+
*
5+
* @package WordPress
6+
* @subpackage JSON_Schema
7+
*/
8+
9+
/**
10+
* @group json-schema
11+
*/
12+
class Tests_JSON_Schema extends WP_UnitTestCase {
13+
14+
/**
15+
* @ticket 64955
16+
*/
17+
public function test_wp_get_json_schema_allowed_keywords_uses_rest_keywords_by_default() {
18+
$this->assertSame( rest_get_allowed_schema_keywords(), wp_get_json_schema_allowed_keywords() );
19+
$this->assertSame( rest_get_allowed_schema_keywords(), wp_get_json_schema_allowed_keywords( 'rest-api' ) );
20+
$this->assertSame( rest_get_allowed_schema_keywords(), wp_get_json_schema_allowed_keywords( 'unknown-context' ) );
21+
}
22+
23+
/**
24+
* @ticket 64955
25+
*/
26+
public function test_wp_get_json_schema_allowed_keywords_includes_draft_04_keywords() {
27+
$keywords = wp_get_json_schema_allowed_keywords( 'draft-04' );
28+
29+
// Keywords the draft-04 profile adds on top of the REST keyword set.
30+
foreach ( array( '$schema', 'id', '$ref', 'required', 'allOf', 'not', 'definitions', 'dependencies', 'additionalItems' ) as $keyword ) {
31+
$this->assertContains( $keyword, $keywords );
32+
}
33+
34+
// 'type' is a base REST keyword, not a draft-04 addition. Checking it
35+
// confirms the draft-04 profile is a superset that keeps the REST keywords.
36+
$this->assertContains( 'type', $keywords );
37+
}
38+
39+
/**
40+
* @ticket 64955
41+
*/
42+
public function test_wp_get_json_schema_allowed_keywords_filter_receives_schema_profile() {
43+
$schema_profiles = array();
44+
$filter = static function ( $keywords, $schema_profile ) use ( &$schema_profiles ) {
45+
$schema_profiles[] = $schema_profile;
46+
$keywords[] = 'xCustomKeyword';
47+
48+
return $keywords;
49+
};
50+
51+
add_filter( 'wp_json_schema_allowed_keywords', $filter, 10, 2 );
52+
$keywords = wp_get_json_schema_allowed_keywords( 'draft-04' );
53+
54+
$this->assertContains( 'xCustomKeyword', $keywords );
55+
$this->assertSame( array( 'draft-04' ), $schema_profiles );
56+
}
57+
}

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2519,6 +2519,47 @@ public function test_get_data_for_route_includes_permitted_schema_keywords() {
25192519
$this->assertSameSetsWithIndex( $expected, $args['param'] );
25202520
}
25212521

2522+
/**
2523+
* @ticket 64955
2524+
*/
2525+
public function test_get_data_for_route_includes_filtered_json_schema_keywords() {
2526+
$filter = static function ( $keywords, $schema_profile ) {
2527+
if ( 'rest-api' === $schema_profile ) {
2528+
$keywords[] = 'xRestApiKeyword';
2529+
}
2530+
2531+
return $keywords;
2532+
};
2533+
2534+
add_filter( 'wp_json_schema_allowed_keywords', $filter, 10, 2 );
2535+
2536+
register_rest_route(
2537+
'test-ns/v1',
2538+
'/test',
2539+
array(
2540+
'methods' => 'POST',
2541+
'callback' => static function () {
2542+
return new WP_REST_Response( 'test' );
2543+
},
2544+
'permission_callback' => '__return_true',
2545+
'args' => array(
2546+
'param' => array(
2547+
'type' => 'string',
2548+
'xRestApiKeyword' => true,
2549+
'invalid' => true,
2550+
),
2551+
),
2552+
)
2553+
);
2554+
2555+
$response = rest_do_request( new WP_REST_Request( 'OPTIONS', '/test-ns/v1/test' ) );
2556+
2557+
$args = $response->get_data()['endpoints'][0]['args'];
2558+
2559+
$this->assertArrayHasKey( 'xRestApiKeyword', $args['param'] );
2560+
$this->assertArrayNotHasKey( 'invalid', $args['param'] );
2561+
}
2562+
25222563
/**
25232564
* @ticket 53056
25242565
*/

0 commit comments

Comments
 (0)