[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

   1  <?php
   2  /**
   3   * REST API run 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 execute abilities via the REST API.
  14   *
  15   * @since 6.9.0
  16   *
  17   * @see WP_REST_Controller
  18   */
  19  class WP_REST_Abilities_V1_Run_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 ability execution.
  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 . '/(?P<name>[a-zA-Z0-9\-\/]+?)/run',
  48              array(
  49                  'args'   => array(
  50                      'name' => array(
  51                          'description' => __( 'Unique identifier for the ability.' ),
  52                          'type'        => 'string',
  53                          'pattern'     => '^[a-zA-Z0-9\-\/]+$',
  54                      ),
  55                  ),
  56  
  57                  // TODO: We register ALLMETHODS because at route registration time, we don't know which abilities
  58                  // exist or their annotations (`destructive`, `idempotent`, `readonly`). This is due to WordPress
  59                  // load order - routes are registered early, before plugins have registered their abilities.
  60                  // This approach works but could be improved with lazy route registration or a different
  61                  // architecture that allows type-specific routes after abilities are registered.
  62                  // This was the same issue that we ended up seeing with the Feature API.
  63                  array(
  64                      'methods'             => WP_REST_Server::ALLMETHODS,
  65                      'callback'            => array( $this, 'execute_ability' ),
  66                      'permission_callback' => array( $this, 'check_ability_permissions' ),
  67                      'args'                => $this->get_run_args(),
  68  
  69                      /*
  70                       * Serve the ability result as returned, so it follows the ability's output
  71                       * schema whether it is an object, an array, or a scalar value: `_fields`,
  72                       * `_embed` and response links leave it unchanged.
  73                       */
  74                      'data_processing'     => false,
  75                  ),
  76                  'schema' => array( $this, 'get_run_schema' ),
  77              )
  78          );
  79      }
  80  
  81      /**
  82       * Executes an ability.
  83       *
  84       * @since 6.9.0
  85       *
  86       * @param WP_REST_Request $request Full details about the request.
  87       * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  88       */
  89  	public function execute_ability( $request ) {
  90          $ability = wp_has_ability( $request['name'] ) ? wp_get_ability( $request['name'] ) : null;
  91          if ( ! $ability ) {
  92              return new WP_Error(
  93                  'rest_ability_not_found',
  94                  __( 'Ability not found.' ),
  95                  array( 'status' => 404 )
  96              );
  97          }
  98  
  99          $input  = $this->get_input_from_request( $request );
 100          $result = $ability->execute( $input );
 101          if ( is_wp_error( $result ) ) {
 102              return $result;
 103          }
 104  
 105          /*
 106           * Preserve empty objects when encoding the result as JSON. Preparation can
 107           * convert empty PHP arrays to stdClass, also within object results.
 108           */
 109          $result = wp_prepare_json_value_for_client( $result, $ability->get_output_schema() );
 110  
 111          return rest_ensure_response( $result );
 112      }
 113  
 114      /**
 115       * Validates if the HTTP method matches the expected method for the ability based on its annotations.
 116       *
 117       * @since 6.9.0
 118       *
 119       * @param string                     $request_method The HTTP method of the request.
 120       * @param array<string, (null|bool)> $annotations    The ability annotations.
 121       * @return true|WP_Error True on success, or WP_Error object on failure.
 122       */
 123  	public function validate_request_method( string $request_method, array $annotations ) {
 124          $expected_method = 'POST';
 125          if ( ! empty( $annotations['readonly'] ) ) {
 126              $expected_method = 'GET';
 127          } elseif ( ! empty( $annotations['destructive'] ) && ! empty( $annotations['idempotent'] ) ) {
 128              $expected_method = 'DELETE';
 129          }
 130  
 131          if ( $expected_method === $request_method ) {
 132              return true;
 133          }
 134  
 135          $error_message = __( 'Abilities that perform updates require POST method.' );
 136          if ( 'GET' === $expected_method ) {
 137              $error_message = __( 'Read-only abilities require GET method.' );
 138          } elseif ( 'DELETE' === $expected_method ) {
 139              $error_message = __( 'Abilities that perform destructive actions require DELETE method.' );
 140          }
 141          return new WP_Error(
 142              'rest_ability_invalid_method',
 143              $error_message,
 144              array( 'status' => 405 )
 145          );
 146      }
 147  
 148      /**
 149       * Checks if a given request has permission to execute a specific ability.
 150       *
 151       * @since 6.9.0
 152       *
 153       * @param WP_REST_Request $request Full details about the request.
 154       * @return true|WP_Error True if the request has execution permission, WP_Error object otherwise.
 155       */
 156  	public function check_ability_permissions( $request ) {
 157          $ability = wp_has_ability( $request['name'] ) ? wp_get_ability( $request['name'] ) : null;
 158          if ( ! $ability || ! $ability->get_meta_item( 'show_in_rest' ) ) {
 159              return new WP_Error(
 160                  'rest_ability_not_found',
 161                  __( 'Ability not found.' ),
 162                  array( 'status' => 404 )
 163              );
 164          }
 165  
 166          $is_valid = $this->validate_request_method(
 167              $request->get_method(),
 168              $ability->get_meta_item( 'annotations' )
 169          );
 170          if ( is_wp_error( $is_valid ) ) {
 171              return $is_valid;
 172          }
 173  
 174          $input = $this->get_input_from_request( $request );
 175          $input = $ability->normalize_input( $input );
 176          if ( is_wp_error( $input ) ) {
 177              return $this->ensure_error_status( $input, 400 );
 178          }
 179  
 180          $is_valid = $ability->validate_input( $input );
 181          if ( is_wp_error( $is_valid ) ) {
 182              return $this->ensure_error_status( $is_valid, 400 );
 183          }
 184  
 185          $input = $ability->coerce_input( $input );
 186          if ( is_wp_error( $input ) ) {
 187              return $this->ensure_error_status( $input, 400 );
 188          }
 189  
 190          $result = $ability->check_permissions( $input );
 191          if ( is_wp_error( $result ) ) {
 192              $result->add_data( array( 'status' => rest_authorization_required_code() ) );
 193              return $result;
 194          }
 195          if ( ! $result ) {
 196              return new WP_Error(
 197                  'rest_ability_cannot_execute',
 198                  __( 'Sorry, you are not allowed to execute this ability.' ),
 199                  array( 'status' => rest_authorization_required_code() )
 200              );
 201          }
 202  
 203          return true;
 204      }
 205  
 206      /**
 207       * Ensures a WP_Error object carries an HTTP status, adding a default when none is set.
 208       *
 209       * @since 7.1.0
 210       *
 211       * @param WP_Error $error  Error object to update.
 212       * @param int      $status HTTP status code to add if not already present.
 213       * @return WP_Error The error object, with a default status when needed.
 214       */
 215  	private function ensure_error_status( WP_Error $error, int $status ): WP_Error {
 216          $error_data = $error->get_error_data();
 217          if ( ! is_array( $error_data ) || ! isset( $error_data['status'] ) ) {
 218              $error->add_data( array( 'status' => $status ) );
 219          }
 220  
 221          return $error;
 222      }
 223  
 224      /**
 225       * Extracts input parameters from the request.
 226       *
 227       * @since 6.9.0
 228       *
 229       * @param WP_REST_Request $request The request object.
 230       * @return mixed|null The input parameters.
 231       */
 232  	private function get_input_from_request( $request ) {
 233          if ( in_array( $request->get_method(), array( 'GET', 'DELETE' ), true ) ) {
 234              // For GET and DELETE requests, look for 'input' query parameter.
 235              $query_params = $request->get_query_params();
 236              return $query_params['input'] ?? null;
 237          }
 238  
 239          // For POST requests, look for 'input' in JSON body.
 240          $json_params = $request->get_json_params();
 241          return $json_params['input'] ?? null;
 242      }
 243  
 244      /**
 245       * Sanitizes the run input by coercing it to the ability's input schema.
 246       *
 247       * Registered as the `input` argument `sanitize_callback` so request input is coerced
 248       * before any route callback runs, regardless of transport. `check_ability_permissions()`
 249       * and `WP_Ability::execute()` coerce the input again after normalization and validation,
 250       * so schema defaults and values from normalization filters are coerced too.
 251       *
 252       * @since 7.1.0
 253       *
 254       * @see WP_Ability::coerce_input()
 255       *
 256       * @param mixed           $input   Raw input extracted from the request.
 257       * @param WP_REST_Request $request The request object.
 258       * @return mixed|WP_Error Coerced input, raw input when validation fails, or WP_Error when coercion fails.
 259       */
 260  	public function sanitize_input_for_ability( $input, $request ) {
 261          $ability = wp_has_ability( $request['name'] ) ? wp_get_ability( $request['name'] ) : null;
 262          if ( ! $ability instanceof WP_Ability ) {
 263              return $input;
 264          }
 265  
 266          // Validate before coercion so sanitization cannot rescue invalid input.
 267          if ( null === $input || empty( $ability->get_input_schema() ) || is_wp_error( $ability->validate_input( $input ) ) ) {
 268              return $input;
 269          }
 270  
 271          return $ability->coerce_input( $input );
 272      }
 273  
 274      /**
 275       * Retrieves the arguments for ability execution endpoint.
 276       *
 277       * @since 6.9.0
 278       *
 279       * @return array<string, mixed> Arguments for the run endpoint.
 280       */
 281  	public function get_run_args(): array {
 282          return array(
 283              'input' => array(
 284                  'description'       => __( 'Input parameters for the ability execution.' ),
 285                  'type'              => array( 'integer', 'number', 'boolean', 'string', 'array', 'object', 'null' ),
 286                  'default'           => null,
 287                  'sanitize_callback' => array( $this, 'sanitize_input_for_ability' ),
 288              ),
 289          );
 290      }
 291  
 292      /**
 293       * Retrieves the schema for ability execution endpoint.
 294       *
 295       * @since 6.9.0
 296       *
 297       * @return array<string, mixed> Schema for the run endpoint.
 298       */
 299  	public function get_run_schema(): array {
 300          return array(
 301              '$schema'    => 'http://json-schema.org/draft-04/schema#',
 302              'title'      => 'ability-execution',
 303              'type'       => 'object',
 304              'properties' => array(
 305                  'result' => array(
 306                      'description' => __( 'The result of the ability execution.' ),
 307                      'type'        => array( 'integer', 'number', 'boolean', 'string', 'array', 'object', 'null' ),
 308                      'context'     => array( 'view', 'edit' ),
 309                      'readonly'    => true,
 310                  ),
 311              ),
 312          );
 313      }
 314  }


Generated : Wed Nov 4 08:20:38 2026 Cross-referenced by PHPXref