Changeset 62449
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-list-controller.php
r62447 r62449 226 226 * Transforms an ability schema for REST response output. 227 227 * 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 * 228 235 * Ability schemas may include WordPress-internal properties or unsupported 229 236 * schema keywords that should not be exposed in REST responses. This method 230 237 * strips keys not recognized by the REST API schema handling. It also 231 238 * 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. 233 242 * 234 243 * @since 7.1.0 … … 256 265 257 266 $schema = array_intersect_key( $schema, $allowed_keywords ); 267 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 } 258 301 259 302 // Sub-schema maps: keys are user-defined, values are sub-schemas. -
trunk/tests/phpunit/tests/rest-api/rest-schema-validation.php
r59040 r62449 1507 1507 1508 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 1533 /** 1509 1534 * @ticket 51023 1510 1535 */ -
trunk/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php
r62447 r62449 1148 1148 $this->assertSame( 'boolean', $data['output_schema']['additionalItems']['type'] ); 1149 1149 } 1150 1151 /** 1152 * Test that per-property `required` booleans become a draft-04 `required` array. 1153 * 1154 * @ticket 64955 1155 */ 1156 public function test_required_property_booleans_converted_to_draft_04_array(): void { 1157 $this->register_test_ability( 1158 'test/required-booleans', 1159 array( 1160 'label' => 'Required Booleans', 1161 'description' => 'Tests conversion of per-property required booleans.', 1162 'category' => 'general', 1163 'input_schema' => array( 1164 'type' => 'object', 1165 'properties' => array( 1166 'title' => array( 1167 'type' => 'string', 1168 'required' => true, 1169 ), 1170 'content' => array( 1171 'type' => 'string', 1172 'required' => true, 1173 ), 1174 'optional' => array( 1175 'type' => 'string', 1176 ), 1177 ), 1178 ), 1179 'output_schema' => array( 1180 'type' => 'object', 1181 'properties' => array( 1182 'id' => array( 1183 'type' => 'integer', 1184 'required' => true, 1185 ), 1186 ), 1187 ), 1188 'execute_callback' => static function (): array { 1189 return array( 'id' => 1 ); 1190 }, 1191 'permission_callback' => '__return_true', 1192 'meta' => array( 'show_in_rest' => true ), 1193 ) 1194 ); 1195 1196 $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/required-booleans' ); 1197 $response = $this->server->dispatch( $request ); 1198 1199 $this->assertSame( 200, $response->get_status() ); 1200 1201 $data = $response->get_data(); 1202 1203 // The `required` array lists the names of the properties flagged as required. 1204 $this->assertArrayHasKey( 'required', $data['input_schema'] ); 1205 $this->assertSameSets( array( 'title', 'content' ), $data['input_schema']['required'] ); 1206 1207 // The boolean flag is removed from each property sub-schema. 1208 $this->assertArrayNotHasKey( 'required', $data['input_schema']['properties']['title'] ); 1209 $this->assertArrayNotHasKey( 'required', $data['input_schema']['properties']['content'] ); 1210 $this->assertArrayNotHasKey( 'required', $data['input_schema']['properties']['optional'] ); 1211 1212 // Output schemas are normalized the same way. 1213 $this->assertSame( array( 'id' ), $data['output_schema']['required'] ); 1214 $this->assertArrayNotHasKey( 'required', $data['output_schema']['properties']['id'] ); 1215 } 1216 1217 /** 1218 * Test that per-property `required` booleans are converted in nested object schemas. 1219 * 1220 * @ticket 64955 1221 */ 1222 public function test_required_booleans_converted_in_nested_object_schemas(): void { 1223 $this->register_test_ability( 1224 'test/required-nested', 1225 array( 1226 'label' => 'Required Nested', 1227 'description' => 'Tests conversion within nested object schemas.', 1228 'category' => 'general', 1229 'input_schema' => array( 1230 'type' => 'object', 1231 'properties' => array( 1232 'address' => array( 1233 'type' => 'object', 1234 'required' => true, 1235 'properties' => array( 1236 'street' => array( 1237 'type' => 'string', 1238 'required' => true, 1239 ), 1240 'city' => array( 1241 'type' => 'string', 1242 ), 1243 ), 1244 ), 1245 ), 1246 ), 1247 'execute_callback' => static function () { 1248 return null; 1249 }, 1250 'permission_callback' => '__return_true', 1251 'meta' => array( 'show_in_rest' => true ), 1252 ) 1253 ); 1254 1255 $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/required-nested' ); 1256 $response = $this->server->dispatch( $request ); 1257 1258 $this->assertSame( 200, $response->get_status() ); 1259 1260 $data = $response->get_data(); 1261 $address = $data['input_schema']['properties']['address']; 1262 1263 // The outer object lists the nested object as a required property. 1264 $this->assertSame( array( 'address' ), $data['input_schema']['required'] ); 1265 1266 // The nested object's own boolean flag is replaced by a draft-04 array 1267 // collecting its own required properties (proving the boolean was converted). 1268 $this->assertSame( array( 'street' ), $address['required'] ); 1269 $this->assertArrayNotHasKey( 'required', $address['properties']['street'] ); 1270 $this->assertArrayNotHasKey( 'required', $address['properties']['city'] ); 1271 } 1272 1273 /** 1274 * Test that `required: false` is removed without emitting an empty `required` array. 1275 * 1276 * @ticket 64955 1277 */ 1278 public function test_required_false_booleans_removed_without_required_array(): void { 1279 $this->register_test_ability( 1280 'test/required-false', 1281 array( 1282 'label' => 'Required False', 1283 'description' => 'Tests that required:false is stripped.', 1284 'category' => 'general', 1285 'input_schema' => array( 1286 'type' => 'object', 1287 'properties' => array( 1288 'maybe' => array( 1289 'type' => 'string', 1290 'required' => false, 1291 ), 1292 ), 1293 ), 1294 'execute_callback' => static function () { 1295 return null; 1296 }, 1297 'permission_callback' => '__return_true', 1298 'meta' => array( 'show_in_rest' => true ), 1299 ) 1300 ); 1301 1302 $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/required-false' ); 1303 $response = $this->server->dispatch( $request ); 1304 1305 $this->assertSame( 200, $response->get_status() ); 1306 1307 $data = $response->get_data(); 1308 1309 $this->assertArrayNotHasKey( 'required', $data['input_schema'] ); 1310 $this->assertArrayNotHasKey( 'required', $data['input_schema']['properties']['maybe'] ); 1311 } 1312 1313 /** 1314 * Test that an existing draft-04 `required` array takes precedence over per-property booleans. 1315 * 1316 * This mirrors rest_validate_object_value_from_schema(), which ignores 1317 * per-property `required` booleans when a draft-04 `required` array is 1318 * present, so the published schema matches what is actually enforced. 1319 * 1320 * @ticket 64955 1321 */ 1322 public function test_required_draft_04_array_takes_precedence_over_booleans(): void { 1323 $this->register_test_ability( 1324 'test/required-mixed', 1325 array( 1326 'label' => 'Required Mixed', 1327 'description' => 'Tests precedence of a draft-04 array over draft-03 booleans.', 1328 'category' => 'general', 1329 'input_schema' => array( 1330 'type' => 'object', 1331 'required' => array( 'title' ), 1332 'properties' => array( 1333 'title' => array( 1334 'type' => 'string', 1335 'required' => true, 1336 ), 1337 'content' => array( 1338 'type' => 'string', 1339 'required' => true, 1340 ), 1341 ), 1342 ), 1343 'execute_callback' => static function () { 1344 return null; 1345 }, 1346 'permission_callback' => '__return_true', 1347 'meta' => array( 'show_in_rest' => true ), 1348 ) 1349 ); 1350 1351 $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/required-mixed' ); 1352 $response = $this->server->dispatch( $request ); 1353 1354 $this->assertSame( 200, $response->get_status() ); 1355 1356 $data = $response->get_data(); 1357 1358 // The draft-04 array wins: the `content` boolean is ignored, not merged in. 1359 $this->assertSame( array( 'title' ), $data['input_schema']['required'] ); 1360 1361 // The per-property booleans are still stripped from the output. 1362 $this->assertArrayNotHasKey( 'required', $data['input_schema']['properties']['title'] ); 1363 $this->assertArrayNotHasKey( 'required', $data['input_schema']['properties']['content'] ); 1364 } 1365 1366 /** 1367 * Test that a boolean `required` with no draft-04 equivalent (e.g. on a scalar) is dropped. 1368 * 1369 * @ticket 64955 1370 */ 1371 public function test_required_boolean_on_scalar_schema_removed(): void { 1372 $this->register_test_ability( 1373 'test/required-scalar', 1374 array( 1375 'label' => 'Required Scalar', 1376 'description' => 'Tests stripping of a boolean required on a scalar schema.', 1377 'category' => 'general', 1378 'input_schema' => array( 1379 'type' => 'string', 1380 'description' => 'The text to analyze.', 1381 'required' => true, 1382 ), 1383 'output_schema' => array( 1384 'type' => 'string', 1385 'required' => true, 1386 ), 1387 'execute_callback' => static function ( $input ) { 1388 return $input; 1389 }, 1390 'permission_callback' => '__return_true', 1391 'meta' => array( 'show_in_rest' => true ), 1392 ) 1393 ); 1394 1395 $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/required-scalar' ); 1396 $response = $this->server->dispatch( $request ); 1397 1398 $this->assertSame( 200, $response->get_status() ); 1399 1400 $data = $response->get_data(); 1401 1402 $this->assertArrayNotHasKey( 'required', $data['input_schema'] ); 1403 $this->assertSame( 'string', $data['input_schema']['type'] ); 1404 $this->assertArrayNotHasKey( 'required', $data['output_schema'] ); 1405 } 1406 1407 /** 1408 * Test that per-property `required` booleans are converted in an array's `items` object. 1409 * 1410 * @ticket 64955 1411 */ 1412 public function test_required_booleans_converted_in_array_items_object_schemas(): void { 1413 $this->register_test_ability( 1414 'test/required-array-items', 1415 array( 1416 'label' => 'Required Array Items', 1417 'description' => 'Tests conversion within array item object schemas.', 1418 'category' => 'general', 1419 'input_schema' => array( 1420 'type' => 'array', 1421 'items' => array( 1422 'type' => 'object', 1423 'properties' => array( 1424 'id' => array( 1425 'type' => 'integer', 1426 'required' => true, 1427 ), 1428 'label' => array( 1429 'type' => 'string', 1430 ), 1431 ), 1432 ), 1433 ), 1434 'execute_callback' => static function () { 1435 return null; 1436 }, 1437 'permission_callback' => '__return_true', 1438 'meta' => array( 'show_in_rest' => true ), 1439 ) 1440 ); 1441 1442 $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/required-array-items' ); 1443 $response = $this->server->dispatch( $request ); 1444 1445 $this->assertSame( 200, $response->get_status() ); 1446 1447 $data = $response->get_data(); 1448 $items = $data['input_schema']['items']; 1449 1450 // The object schema inside `items` collects its own required properties 1451 // into a draft-04 array, and the per-property boolean is removed. 1452 $this->assertSame( array( 'id' ), $items['required'] ); 1453 $this->assertArrayNotHasKey( 'required', $items['properties']['id'] ); 1454 $this->assertArrayNotHasKey( 'required', $items['properties']['label'] ); 1455 } 1150 1456 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)