[ 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 $is_valid = $ability->validate_input( $input ); 164 if ( is_wp_error( $is_valid ) ) { 165 $is_valid->add_data( array( 'status' => 400 ) ); 166 return $is_valid; 167 } 168 169 $result = $ability->check_permissions( $input ); 170 if ( is_wp_error( $result ) ) { 171 $result->add_data( array( 'status' => rest_authorization_required_code() ) ); 172 return $result; 173 } 174 if ( ! $result ) { 175 return new WP_Error( 176 'rest_ability_cannot_execute', 177 __( 'Sorry, you are not allowed to execute this ability.' ), 178 array( 'status' => rest_authorization_required_code() ) 179 ); 180 } 181 182 return true; 183 } 184 185 /** 186 * Extracts input parameters from the request. 187 * 188 * @since 6.9.0 189 * 190 * @param WP_REST_Request $request The request object. 191 * @return mixed|null The input parameters. 192 */ 193 private function get_input_from_request( $request ) { 194 if ( in_array( $request->get_method(), array( 'GET', 'DELETE' ), true ) ) { 195 // For GET and DELETE requests, look for 'input' query parameter. 196 $query_params = $request->get_query_params(); 197 return $query_params['input'] ?? null; 198 } 199 200 // For POST requests, look for 'input' in JSON body. 201 $json_params = $request->get_json_params(); 202 return $json_params['input'] ?? null; 203 } 204 205 /** 206 * Retrieves the arguments for ability execution endpoint. 207 * 208 * @since 6.9.0 209 * 210 * @return array<string, mixed> Arguments for the run endpoint. 211 */ 212 public function get_run_args(): array { 213 return array( 214 'input' => array( 215 'description' => __( 'Input parameters for the ability execution.' ), 216 'type' => array( 'integer', 'number', 'boolean', 'string', 'array', 'object', 'null' ), 217 'default' => null, 218 ), 219 ); 220 } 221 222 /** 223 * Retrieves the schema for ability execution endpoint. 224 * 225 * @since 6.9.0 226 * 227 * @return array<string, mixed> Schema for the run endpoint. 228 */ 229 public function get_run_schema(): array { 230 return array( 231 '$schema' => 'http://json-schema.org/draft-04/schema#', 232 'title' => 'ability-execution', 233 'type' => 'object', 234 'properties' => array( 235 'result' => array( 236 'description' => __( 'The result of the ability execution.' ), 237 'type' => array( 'integer', 'number', 'boolean', 'string', 'array', 'object', 'null' ), 238 'context' => array( 'view', 'edit' ), 239 'readonly' => true, 240 ), 241 ), 242 ); 243 } 244 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Thu Oct 23 08:20:05 2025 | Cross-referenced by PHPXref |