[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/rest-api/endpoints/ -> class-wp-rest-abilities-v1-list-controller.php (source)

   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  }


Generated : Mon Jun 15 08:20:09 2026 Cross-referenced by PHPXref