Skip to content

Commit 63bf1d9

Browse files
committed
Abilities API: Normalize required schema shape for REST responses
Ability schemas are a public contract that REST clients, including the `@wordpress/abilities` JavaScript client, validate against as standard JSON Schema. The `required` keyword must therefore use the draft-04 array-of-property-names form, not the draft-03 per-property boolean that `rest_validate_value_from_schema()` also accepts on the server. In `WP_REST_Abilities_V1_List_Controller::prepare_schema_for_response()`, collect per-property `required: true` booleans into a parent `required` array, recursively and inside array `items`. Strip `required: false` and boolean `required` values with no draft-04 equivalent, and honor `rest_validate_object_value_from_schema()` precedence where an existing draft-04 array wins. Only the REST response copy is rewritten; stored schemas and server-side validation are unchanged. Props gziolo, westonruter, jorgefilipecosta. See #64955. git-svn-id: https://develop.svn.wordpress.org/trunk@62449 602fd350-edb4-49c9-b593-d223f7449a82
1 parent f2359ee commit 63bf1d9

3 files changed

Lines changed: 375 additions & 1 deletion

File tree

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,20 @@ private function is_associative_array( $value ): bool {
225225
/**
226226
* Transforms an ability schema for REST response output.
227227
*
228+
* The input and output schemas are a public contract: REST clients (such as
229+
* the `@wordpress/abilities` JS client) consume them as standard JSON Schema
230+
* and validate ability input and output against them. The response must
231+
* therefore use JSON Schema draft-04 forms that standard validators
232+
* understand, not the WordPress-internal conventions that
233+
* `rest_validate_value_from_schema()` also accepts on the server.
234+
*
228235
* Ability schemas may include WordPress-internal properties or unsupported
229236
* schema keywords that should not be exposed in REST responses. This method
230237
* strips keys not recognized by the REST API schema handling. It also
231238
* converts empty array defaults to objects when the schema type is 'object'
232-
* to ensure proper JSON serialization as {} instead of [].
239+
* to ensure proper JSON serialization as {} instead of [], and normalizes
240+
* the `required` keyword from the draft-03 per-property boolean form into
241+
* the draft-04 array of property names.
233242
*
234243
* @since 7.1.0
235244
*
@@ -256,6 +265,40 @@ private function prepare_schema_for_response( array $schema ): array {
256265

257266
$schema = array_intersect_key( $schema, $allowed_keywords );
258267

268+
// Collect draft-03 per-property `required: true` flags into a draft-04
269+
// `required` array of property names on the parent object schema.
270+
//
271+
// This mirrors rest_validate_object_value_from_schema(), where a draft-04
272+
// `required` array takes precedence: when one is present, per-property
273+
// booleans are ignored during validation. They are therefore left out of
274+
// the array here as well (but still stripped from the output) so the
275+
// published schema describes exactly what gets enforced.
276+
if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
277+
$has_required_array = isset( $schema['required'] ) && is_array( $schema['required'] );
278+
$required = array();
279+
foreach ( $schema['properties'] as $property => &$property_schema ) {
280+
if ( $this->is_associative_array( $property_schema ) && isset( $property_schema['required'] ) && is_bool( $property_schema['required'] ) ) {
281+
if ( ! $has_required_array && true === $property_schema['required'] ) {
282+
$required[] = (string) $property;
283+
}
284+
unset( $property_schema['required'] );
285+
}
286+
}
287+
unset( $property_schema );
288+
289+
// Property keys are unique, so the collected list needs no deduplication.
290+
// When a draft-04 array is already present, leave it untouched.
291+
if ( ! $has_required_array && count( $required ) > 0 ) {
292+
$schema['required'] = $required;
293+
}
294+
}
295+
296+
// A boolean `required` outside of an object's property list has no draft-04
297+
// equivalent, so drop it rather than emit an invalid keyword.
298+
if ( isset( $schema['required'] ) && is_bool( $schema['required'] ) ) {
299+
unset( $schema['required'] );
300+
}
301+
259302
// Sub-schema maps: keys are user-defined, values are sub-schemas.
260303
// Note: 'dependencies' values can also be property-dependency arrays
261304
// (numeric arrays of strings) which are skipped via wp_is_numeric_array().

tests/phpunit/tests/rest-api/rest-schema-validation.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1505,6 +1505,31 @@ public function data_required_deeply_nested_property() {
15051505
);
15061506
}
15071507

1508+
/**
1509+
* A draft-04 `required` array takes precedence over per-property
1510+
* `required` booleans on the same object node: the booleans are ignored.
1511+
*
1512+
* @ticket 64955
1513+
*/
1514+
public function test_required_v4_array_takes_precedence_over_v3_booleans() {
1515+
$schema = array(
1516+
'type' => 'object',
1517+
'required' => array( 'listed' ),
1518+
'properties' => array(
1519+
'listed' => array( 'type' => 'string' ),
1520+
'flagged' => array(
1521+
'type' => 'string',
1522+
'required' => true, // Ignored because the array is present.
1523+
),
1524+
),
1525+
);
1526+
1527+
// Missing the array-listed prop fails.
1528+
$this->assertWPError( rest_validate_value_from_schema( array( 'flagged' => 'x' ), $schema ) );
1529+
// Missing only the boolean-flagged prop passes — the boolean is not enforced.
1530+
$this->assertTrue( rest_validate_value_from_schema( array( 'listed' => 'x' ), $schema ) );
1531+
}
1532+
15081533
/**
15091534
* @ticket 51023
15101535
*/

0 commit comments

Comments
 (0)