| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * REST API list controller for Abilities API. 4 * 5 * @package WordPress 6 * @subpackage Abilities_API 7 * @since 6.9.0 8 */ 9 10 declare( strict_types = 1 ); 11 12 /** 13 * Core controller used to access abilities via the REST API. 14 * 15 * @since 6.9.0 16 * 17 * @see WP_REST_Controller 18 */ 19 class WP_REST_Abilities_V1_List_Controller extends WP_REST_Controller { 20 21 /** 22 * REST API namespace. 23 * 24 * @since 6.9.0 25 * @var string 26 */ 27 protected $namespace = 'wp-abilities/v1'; 28 29 /** 30 * REST API base route. 31 * 32 * @since 6.9.0 33 * @var string 34 */ 35 protected $rest_base = 'abilities'; 36 37 /** 38 * Registers the routes for abilities. 39 * 40 * @since 6.9.0 41 * 42 * @see register_rest_route() 43 */ 44 public function register_routes(): void { 45 register_rest_route( 46 $this->namespace, 47 '/' . $this->rest_base, 48 array( 49 array( 50 'methods' => WP_REST_Server::READABLE, 51 'callback' => array( $this, 'get_items' ), 52 'permission_callback' => array( $this, 'get_items_permissions_check' ), 53 'args' => $this->get_collection_params(), 54 ), 55 'schema' => array( $this, 'get_public_item_schema' ), 56 ) 57 ); 58 59 register_rest_route( 60 $this->namespace, 61 '/' . $this->rest_base . '/(?P<name>[a-zA-Z0-9\-\/]+)', 62 array( 63 'args' => array( 64 'name' => array( 65 'description' => __( 'Unique identifier for the ability.' ), 66 'type' => 'string', 67 'pattern' => '^[a-zA-Z0-9\-\/]+$', 68 ), 69 ), 70 array( 71 'methods' => WP_REST_Server::READABLE, 72 'callback' => array( $this, 'get_item' ), 73 'permission_callback' => array( $this, 'get_item_permissions_check' ), 74 ), 75 'schema' => array( $this, 'get_public_item_schema' ), 76 ) 77 ); 78 } 79 80 /** 81 * Retrieves all abilities. 82 * 83 * @since 6.9.0 84 * 85 * @param WP_REST_Request $request Full details about the request. 86 * @return WP_REST_Response Response object on success. 87 */ 88 public function get_items( $request ) { 89 $query_args = array( 90 'meta' => array( 'show_in_rest' => true ), 91 ); 92 93 if ( ! empty( $request['category'] ) ) { 94 $query_args['category'] = $request['category']; 95 } 96 97 if ( ! empty( $request['namespace'] ) ) { 98 $query_args['namespace'] = $request['namespace']; 99 } 100 101 $abilities = wp_get_abilities( $query_args ); 102 103 $page = $request['page']; 104 $per_page = $request['per_page']; 105 $offset = ( $page - 1 ) * $per_page; 106 107 $total_abilities = count( $abilities ); 108 $max_pages = (int) ceil( $total_abilities / $per_page ); 109 110 if ( $request->get_method() === 'HEAD' ) { 111 $response = new WP_REST_Response( array() ); 112 } else { 113 $abilities = array_slice( $abilities, $offset, $per_page ); 114 115 $data = array(); 116 foreach ( $abilities as $ability ) { 117 $item = $this->prepare_item_for_response( $ability, $request ); 118 $data[] = $this->prepare_response_for_collection( $item ); 119 } 120 121 $response = rest_ensure_response( $data ); 122 } 123 124 $response->header( 'X-WP-Total', (string) $total_abilities ); 125 $response->header( 'X-WP-TotalPages', (string) $max_pages ); 126 127 $query_params = $request->get_query_params(); 128 $base = add_query_arg( urlencode_deep( $query_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); 129 130 if ( $page > 1 ) { 131 $prev_page = $page - 1; 132 $prev_link = add_query_arg( 'page', $prev_page, $base ); 133 $response->link_header( 'prev', $prev_link ); 134 } 135 136 if ( $page < $max_pages ) { 137 $next_page = $page + 1; 138 $next_link = add_query_arg( 'page', $next_page, $base ); 139 $response->link_header( 'next', $next_link ); 140 } 141 142 return $response; 143 } 144 145 /** 146 * Retrieves a specific ability. 147 * 148 * @since 6.9.0 149 * 150 * @param WP_REST_Request $request Full details about the request. 151 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 152 */ 153 public function get_item( $request ) { 154 $ability = wp_get_ability( $request['name'] ); 155 if ( ! $ability || ! $ability->get_meta_item( 'show_in_rest' ) ) { 156 return new WP_Error( 157 'rest_ability_not_found', 158 __( 'Ability not found.' ), 159 array( 'status' => 404 ) 160 ); 161 } 162 163 $data = $this->prepare_item_for_response( $ability, $request ); 164 return rest_ensure_response( $data ); 165 } 166 167 /** 168 * Checks if a given request has access to read ability items. 169 * 170 * @since 6.9.0 171 * 172 * @param WP_REST_Request $request Full details about the request. 173 * @return bool True if the request has read access. 174 */ 175 public function get_items_permissions_check( $request ) { 176 return current_user_can( 'read' ); 177 } 178 179 /** 180 * Checks if a given request has access to read an ability item. 181 * 182 * @since 6.9.0 183 * 184 * @param WP_REST_Request $request Full details about the request. 185 * @return bool True if the request has read access. 186 */ 187 public function get_item_permissions_check( $request ) { 188 return current_user_can( 'read' ); 189 } 190 191 /** 192 * Additional schema keywords to preserve in REST responses. 193 * 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[] 200 */ 201 private const ADDITIONAL_ALLOWED_SCHEMA_KEYWORDS = array( 202 'required', 203 'allOf', 204 'not', 205 '$ref', 206 'definitions', 207 'dependencies', 208 'additionalItems', 209 ); 210 211 /** 212 * Determines whether the value is an associative array. 213 * 214 * @since 7.1.0 215 * 216 * @param mixed $value Value. 217 * @return bool Whether it is associative array. 218 * 219 * @phpstan-assert-if-true array<string, mixed> $value 220 */ 221 private function is_associative_array( $value ): bool { 222 return is_array( $value ) && ! wp_is_numeric_array( $value ); 223 } 224 225 /** 226 * Transforms an ability schema for REST response output. 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 * 235 * Ability schemas may include WordPress-internal properties or unsupported 236 * schema keywords that should not be exposed in REST responses. This method 237 * strips keys not recognized by the REST API schema handling. It also 238 * converts empty array defaults to objects when the schema type is 'object' 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. 242 * 243 * @since 7.1.0 244 * 245 * @param array<string, mixed> $schema The schema array. 246 * @return array<string, mixed> The transformed schema. 247 */ 248 private function prepare_schema_for_response( array $schema ): array { 249 if ( isset( $schema['type'] ) && 'object' === $schema['type'] && isset( $schema['default'] ) ) { 250 $default = $schema['default']; 251 if ( is_array( $default ) && empty( $default ) ) { 252 $schema['default'] = (object) $default; 253 } 254 } 255 256 // Computed once and reused across the recursive calls for every schema node. 257 static $allowed_keywords = null; 258 $allowed_keywords ??= array_fill_keys( 259 array_merge( 260 rest_get_allowed_schema_keywords(), 261 self::ADDITIONAL_ALLOWED_SCHEMA_KEYWORDS 262 ), 263 true 264 ); 265 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 } 301 302 // Sub-schema maps: keys are user-defined, values are sub-schemas. 303 // Note: 'dependencies' values can also be property-dependency arrays 304 // (numeric arrays of strings) which are skipped via wp_is_numeric_array(). 305 foreach ( array( 'properties', 'patternProperties', 'definitions', 'dependencies' ) as $keyword ) { 306 if ( isset( $schema[ $keyword ] ) && is_array( $schema[ $keyword ] ) ) { 307 foreach ( $schema[ $keyword ] as $key => $child_schema ) { 308 if ( $this->is_associative_array( $child_schema ) ) { 309 $schema[ $keyword ][ $key ] = $this->prepare_schema_for_response( $child_schema ); 310 } 311 } 312 } 313 } 314 315 // Single sub-schema keywords. 316 foreach ( array( 'not', 'additionalProperties', 'additionalItems' ) as $keyword ) { 317 if ( isset( $schema[ $keyword ] ) && $this->is_associative_array( $schema[ $keyword ] ) ) { 318 $schema[ $keyword ] = $this->prepare_schema_for_response( $schema[ $keyword ] ); 319 } 320 } 321 322 // Items: single schema or tuple array of schemas. 323 if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) { 324 if ( $this->is_associative_array( $schema['items'] ) ) { 325 $schema['items'] = $this->prepare_schema_for_response( $schema['items'] ); 326 } else { 327 foreach ( $schema['items'] as $index => $item_schema ) { 328 if ( $this->is_associative_array( $item_schema ) ) { 329 $schema['items'][ $index ] = $this->prepare_schema_for_response( $item_schema ); 330 } 331 } 332 } 333 } 334 335 // Array-of-schemas keywords. 336 foreach ( array( 'anyOf', 'oneOf', 'allOf' ) as $keyword ) { 337 if ( isset( $schema[ $keyword ] ) && is_array( $schema[ $keyword ] ) ) { 338 foreach ( $schema[ $keyword ] as $index => $sub_schema ) { 339 if ( $this->is_associative_array( $sub_schema ) ) { 340 $schema[ $keyword ][ $index ] = $this->prepare_schema_for_response( $sub_schema ); 341 } 342 } 343 } 344 } 345 346 return $schema; 347 } 348 349 /** 350 * Prepares an ability for response. 351 * 352 * @since 6.9.0 353 * 354 * @param WP_Ability $ability The ability object. 355 * @param WP_REST_Request $request Request object. 356 * @return WP_REST_Response Response object. 357 */ 358 public function prepare_item_for_response( $ability, $request ) { 359 $data = array( 360 'name' => $ability->get_name(), 361 'label' => $ability->get_label(), 362 'description' => $ability->get_description(), 363 'category' => $ability->get_category(), 364 'input_schema' => $this->prepare_schema_for_response( $ability->get_input_schema() ), 365 'output_schema' => $this->prepare_schema_for_response( $ability->get_output_schema() ), 366 'meta' => $ability->get_meta(), 367 ); 368 369 $context = $request['context'] ?? 'view'; 370 $data = $this->add_additional_fields_to_object( $data, $request ); 371 $data = $this->filter_response_by_context( $data, $context ); 372 373 $response = rest_ensure_response( $data ); 374 375 $fields = $this->get_fields_for_response( $request ); 376 if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { 377 $links = array( 378 'self' => array( 379 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $ability->get_name() ) ), 380 ), 381 'collection' => array( 382 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), 383 ), 384 ); 385 386 $links['wp:action-run'] = array( 387 'href' => rest_url( sprintf( '%s/%s/%s/run', $this->namespace, $this->rest_base, $ability->get_name() ) ), 388 ); 389 390 $response->add_links( $links ); 391 } 392 393 return $response; 394 } 395 396 /** 397 * Retrieves the ability's schema, conforming to JSON Schema. 398 * 399 * @since 6.9.0 400 * 401 * @return array<string, mixed> Item schema data. 402 */ 403 public function get_item_schema(): array { 404 $schema = array( 405 '$schema' => 'http://json-schema.org/draft-04/schema#', 406 'title' => 'ability', 407 'type' => 'object', 408 'properties' => array( 409 'name' => array( 410 'description' => __( 'Unique identifier for the ability.' ), 411 'type' => 'string', 412 'context' => array( 'view', 'edit', 'embed' ), 413 'readonly' => true, 414 ), 415 'label' => array( 416 'description' => __( 'Display label for the ability.' ), 417 'type' => 'string', 418 'context' => array( 'view', 'edit', 'embed' ), 419 'readonly' => true, 420 ), 421 'description' => array( 422 'description' => __( 'Description of the ability.' ), 423 'type' => 'string', 424 'context' => array( 'view', 'edit' ), 425 'readonly' => true, 426 ), 427 'category' => array( 428 'description' => __( 'Ability category this ability belongs to.' ), 429 'type' => 'string', 430 'context' => array( 'view', 'edit', 'embed' ), 431 'readonly' => true, 432 ), 433 'input_schema' => array( 434 'description' => __( 'JSON Schema for the ability input.' ), 435 'type' => 'object', 436 'context' => array( 'view', 'edit' ), 437 'readonly' => true, 438 ), 439 'output_schema' => array( 440 'description' => __( 'JSON Schema for the ability output.' ), 441 'type' => 'object', 442 'context' => array( 'view', 'edit' ), 443 'readonly' => true, 444 ), 445 'meta' => array( 446 'description' => __( 'Meta information about the ability.' ), 447 'type' => 'object', 448 'properties' => array( 449 'annotations' => array( 450 'description' => __( 'Annotations for the ability.' ), 451 'type' => array( 'boolean', 'null' ), 452 'default' => null, 453 ), 454 ), 455 'context' => array( 'view', 'edit' ), 456 'readonly' => true, 457 ), 458 ), 459 ); 460 461 return $this->add_additional_fields_schema( $schema ); 462 } 463 464 /** 465 * Retrieves the query params for collections. 466 * 467 * @since 6.9.0 468 * 469 * @return array<string, mixed> Collection parameters. 470 */ 471 public function get_collection_params(): array { 472 return array( 473 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 474 'page' => array( 475 'description' => __( 'Current page of the collection.' ), 476 'type' => 'integer', 477 'default' => 1, 478 'minimum' => 1, 479 ), 480 'per_page' => array( 481 'description' => __( 'Maximum number of items to be returned in result set.' ), 482 'type' => 'integer', 483 'default' => 50, 484 'minimum' => 1, 485 'maximum' => 100, 486 ), 487 'category' => array( 488 'description' => __( 'Limit results to abilities in specific ability category.' ), 489 'type' => 'string', 490 'sanitize_callback' => 'sanitize_key', 491 'validate_callback' => 'rest_validate_request_arg', 492 ), 493 'namespace' => array( 494 'description' => __( 'Limit results to abilities in a specific namespace.' ), 495 'type' => 'string', 496 'sanitize_callback' => 'sanitize_key', 497 'validate_callback' => 'rest_validate_request_arg', 498 ), 499 ); 500 } 501 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Mon Jun 15 08:20:09 2026 | Cross-referenced by PHPXref |