| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 'schema' => array( $this, 'get_run_schema' ), 70 ) 71 ); 72 } 73 74 /** 75 * Executes an ability. 76 * 77 * @since 6.9.0 78 * 79 * @param WP_REST_Request $request Full details about the request. 80 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 81 */ 82 public function execute_ability( $request ) { 83 $ability = wp_get_ability( $request['name'] ); 84 if ( ! $ability ) { 85 return new WP_Error( 86 'rest_ability_not_found', 87 __( 'Ability not found.' ), 88 array( 'status' => 404 ) 89 ); 90 } 91 92 $input = $this->get_input_from_request( $request ); 93 $result = $ability->execute( $input ); 94 if ( is_wp_error( $result ) ) { 95 return $result; 96 } 97 98 return rest_ensure_response( $result ); 99 } 100 101 /** 102 * Validates if the HTTP method matches the expected method for the ability based on its annotations. 103 * 104 * @since 6.9.0 105 * 106 * @param string $request_method The HTTP method of the request. 107 * @param array<string, (null|bool)> $annotations The ability annotations. 108 * @return true|WP_Error True on success, or WP_Error object on failure. 109 */ 110 public function validate_request_method( string $request_method, array $annotations ) { 111 $expected_method = 'POST'; 112 if ( ! empty( $annotations['readonly'] ) ) { 113 $expected_method = 'GET'; 114 } elseif ( ! empty( $annotations['destructive'] ) && ! empty( $annotations['idempotent'] ) ) { 115 $expected_method = 'DELETE'; 116 } 117 118 if ( $expected_method === $request_method ) { 119 return true; 120 } 121 122 $error_message = __( 'Abilities that perform updates require POST method.' ); 123 if ( 'GET' === $expected_method ) { 124 $error_message = __( 'Read-only abilities require GET method.' ); 125 } elseif ( 'DELETE' === $expected_method ) { 126 $error_message = __( 'Abilities that perform destructive actions require DELETE method.' ); 127 } 128 return new WP_Error( 129 'rest_ability_invalid_method', 130 $error_message, 131 array( 'status' => 405 ) 132 ); 133 } 134 135 /** 136 * Checks if a given request has permission to execute a specific ability. 137 * 138 * @since 6.9.0 139 * 140 * @param WP_REST_Request $request Full details about the request. 141 * @return true|WP_Error True if the request has execution permission, WP_Error object otherwise. 142 */ 143 public function check_ability_permissions( $request ) { 144 $ability = wp_get_ability( $request['name'] ); 145 if ( ! $ability || ! $ability->get_meta_item( 'show_in_rest' ) ) { 146 return new WP_Error( 147 'rest_ability_not_found', 148 __( 'Ability not found.' ), 149 array( 'status' => 404 ) 150 ); 151 } 152 153 $is_valid = $this->validate_request_method( 154 $request->get_method(), 155 $ability->get_meta_item( 'annotations' ) 156 ); 157 if ( is_wp_error( $is_valid ) ) { 158 return $is_valid; 159 } 160 161 $input = $this->get_input_from_request( $request ); 162 $input = $ability->normalize_input( $input ); 163 if ( is_wp_error( $input ) ) { 164 return $this->ensure_error_status( $input, 400 ); 165 } 166 167 $is_valid = $ability->validate_input( $input ); 168 if ( is_wp_error( $is_valid ) ) { 169 return $this->ensure_error_status( $is_valid, 400 ); 170 } 171 172 $result = $ability->check_permissions( $input ); 173 if ( is_wp_error( $result ) ) { 174 $result->add_data( array( 'status' => rest_authorization_required_code() ) ); 175 return $result; 176 } 177 if ( ! $result ) { 178 return new WP_Error( 179 'rest_ability_cannot_execute', 180 __( 'Sorry, you are not allowed to execute this ability.' ), 181 array( 'status' => rest_authorization_required_code() ) 182 ); 183 } 184 185 return true; 186 } 187 188 /** 189 * Ensures a WP_Error object carries an HTTP status, adding a default when none is set. 190 * 191 * @since 7.1.0 192 * 193 * @param WP_Error $error Error object to update. 194 * @param int $status HTTP status code to add if not already present. 195 * @return WP_Error The error object, with a default status when needed. 196 */ 197 private function ensure_error_status( WP_Error $error, int $status ): WP_Error { 198 $error_data = $error->get_error_data(); 199 if ( ! is_array( $error_data ) || ! isset( $error_data['status'] ) ) { 200 $error->add_data( array( 'status' => $status ) ); 201 } 202 203 return $error; 204 } 205 206 /** 207 * Extracts input parameters from the request. 208 * 209 * @since 6.9.0 210 * 211 * @param WP_REST_Request $request The request object. 212 * @return mixed|null The input parameters. 213 */ 214 private function get_input_from_request( $request ) { 215 if ( in_array( $request->get_method(), array( 'GET', 'DELETE' ), true ) ) { 216 // For GET and DELETE requests, look for 'input' query parameter. 217 $query_params = $request->get_query_params(); 218 return $query_params['input'] ?? null; 219 } 220 221 // For POST requests, look for 'input' in JSON body. 222 $json_params = $request->get_json_params(); 223 return $json_params['input'] ?? null; 224 } 225 226 /** 227 * Retrieves the arguments for ability execution endpoint. 228 * 229 * @since 6.9.0 230 * 231 * @return array<string, mixed> Arguments for the run endpoint. 232 */ 233 public function get_run_args(): array { 234 return array( 235 'input' => array( 236 'description' => __( 'Input parameters for the ability execution.' ), 237 'type' => array( 'integer', 'number', 'boolean', 'string', 'array', 'object', 'null' ), 238 'default' => null, 239 ), 240 ); 241 } 242 243 /** 244 * Retrieves the schema for ability execution endpoint. 245 * 246 * @since 6.9.0 247 * 248 * @return array<string, mixed> Schema for the run endpoint. 249 */ 250 public function get_run_schema(): array { 251 return array( 252 '$schema' => 'http://json-schema.org/draft-04/schema#', 253 'title' => 'ability-execution', 254 'type' => 'object', 255 'properties' => array( 256 'result' => array( 257 'description' => __( 'The result of the ability execution.' ), 258 'type' => array( 'integer', 'number', 'boolean', 'string', 'array', 'object', 'null' ), 259 'context' => array( 'view', 'edit' ), 260 'readonly' => true, 261 ), 262 ), 263 ); 264 } 265 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Mon Jun 15 08:20:09 2026 | Cross-referenced by PHPXref |