PluginProbe
Image Optimizer – Compress Images and Convert to WebP or AVIF / 1.7.4
Image Optimizer – Compress Images and Convert to WebP or AVIF v1.7.4
1.7.7 1.7.6 1.7.5 1.7.4 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.2.0 1.2.1 1.3.0 1.4.0 1.4.1 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 All 33 releases
image-optimization / classes / rest / route.php

route.php in Image Optimizer – Compress Images and Convert to WebP or AVIF 1.7.4, at classes/rest/route.php

427 lines 13.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ImageOptimization\Classes\Rest;
4
5 use ReflectionClass;
6 use WP_Error;
7 use WP_REST_Request;
8 use WP_REST_Response;
9 use WP_User;
10
11 abstract class Route {
12
13 /**
14 * Should the endpoint be validated for user authentication?
15 * If set to TRUE, the default permission callback will make sure the user is logged in and has a valid user id
16 * @var bool
17 */
18 protected $auth = true;
19
20 /**
21 * holds current authenticated user id
22 * @var int
23 */
24 protected $current_user_id;
25
26
27 /**
28 * Should the endpoint override an existing one?
29 * @var bool
30 */
31 protected bool $override = false;
32
33 /**
34 * Rest Endpoint namespace
35 * @var string
36 */
37 protected string $namespace = 'image-optimizer/v1';
38
39 /**
40 * @var array The valid HTTP methods. The list represents the general REST methods. Do not modify.
41 */
42 private array $valid_http_methods = [
43 'GET',
44 'PATCH',
45 'POST',
46 'PUT',
47 'DELETE',
48 'HEAD',
49 ];
50
51 /**
52 * Route_Base constructor.
53 */
54 public function __construct() {
55 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
56 }
57
58 /**
59 * rest_api_init
60 *
61 * Registers REST endpoints.
62 * Loops through the REST methods for this route, creates an endpoint configuration for
63 * each of them and registers all the endpoints with the WordPress system.
64 */
65 public function rest_api_init(): void {
66 $methods = $this->get_methods();
67 if ( empty( $methods ) ) {
68 return;
69 }
70
71 $callbacks = false;
72 foreach ( (array) $methods as $method ) {
73 if ( ! in_array( $method, $this->valid_http_methods ) ) {
74 continue;
75 }
76 if ( $method && ! $callbacks ) {
77 $callbacks = [];
78 }
79 $callbacks[] = $this->build_endpoint_method_config( $method );
80 }
81
82 $arguments = $this->get_arguments();
83
84 if ( ! $callbacks && empty( $arguments ) ) {
85 return;
86 }
87
88 $arguments = array_merge( $arguments, (array) $callbacks );
89 register_rest_route( $this->namespace, '/' . $this->get_endpoint() . '/', $arguments, $this->override );
90 }
91
92 /**
93 * get_methods
94 * Rest Endpoint methods
95 *
96 * Returns an array of the supported REST methods for this route
97 * @return array<string> REST methods being configured for this route.
98 */
99 abstract public function get_methods(): array;
100
101 /**
102 * get_callback
103 *
104 * Returns a reference to the callback function to handle the REST method specified by the /method/ parameter.
105 * @param string $method The REST method name
106 *
107 * @return callable A reference to a member function with the same name as the REST method being passed as a parameter,
108 * or a reference to the default function /callback/.
109 */
110 public function get_callback_method( string $method ): callable {
111 $method_name = strtolower( $method );
112 $callback = $this->method_exists_in_current_class( $method_name ) ? $method_name : 'callback';
113 return [ $this, $callback ];
114 }
115
116 /**
117 * get_permission_callback_method
118 *
119 * Returns a reference to the permission callback for the method if exists or the default one if it doesn't.
120 * @param string $method The REST method name
121 *
122 * @return callable If a method called (rest-method)_permission_callback exists, returns a reference to it, otherwise
123 * returns a reference to the default member method /permission_callback/.
124 */
125 public function get_permission_callback_method( string $method ): callable {
126 $method_name = strtolower( $method );
127 $permission_callback_method = $method_name . '_permission_callback';
128 $permission_callback = $this->method_exists_in_current_class( $permission_callback_method ) ? $permission_callback_method : 'permission_callback';
129 return [ $this, $permission_callback ];
130 }
131
132 /**
133 * maybe_add_args_to_config
134 *
135 * Checks if the class has a method call (rest-method)_args.
136 * If it does, the function calls it and adds its response to the config object passed to the function, under the /args/ key.
137 * if the function (rest-method)_consumes exists, it will add the response to the config object under the /consumes/ key.
138 * if the function (rest-method)_produces exists, it will add the response to the config object under the /produces/ key.
139 * if the function (rest-method)_summary exists, it will add the response to the config object under the /summary/ key.
140 * if the function (rest-method)_description exists, it will add the response to the config object under the /description/ key.
141 * @param string $method The REST method name being configured
142 * @param array $config The configuration object for the method
143 *
144 * @return array The configuration object for the method, possibly after being amended
145 */
146 public function maybe_add_args_to_config( string $method, array $config ): array {
147 $method_name = strtolower( $method );
148 $method_args = $method_name . '_args';
149 if ( $this->method_exists_in_current_class( $method_args ) ) {
150 $config['args'] = $this->{$method_args}();
151 }
152 $config['consumes'] = [ 'application/json' ];
153 if ( $this->method_exists_in_current_class( $method . '_consumes' ) ) {
154 $config['consumes'] = $this->{$method . '_consumes'}();
155 }
156 $config['produces'] = [ 'application/json' ];
157 if ( $this->method_exists_in_current_class( $method . '_produces' ) ) {
158 $config['produces'] = $this->{$method . '_produces'}();
159 }
160 if ( $this->method_exists_in_current_class( $method . '_summary' ) ) {
161 $config['summary'] = $this->{$method . '_summary'}();
162 }
163 if ( $this->method_exists_in_current_class( $method . '_description' ) ) {
164 $config['description'] = $this->{$method . '_description'}();
165 }
166 return $config;
167 }
168
169 /**
170 * maybe_add_response_to_swagger
171 *
172 * If the function method /(rest-method)_response_callback/ exists, adds the filter
173 * /swagger_api_response_(namespace with slashes replaced with underscores)_(endpoint with slashes replaced with underscores)/
174 * with the aforementioned function method.
175 * This filter is used with the WP API Swagger UI plugin to create documentation for the API.
176 * The value being passed is an array: [
177 '200' => ['description' => 'OK'],
178 '404' => ['description' => 'Not Found'],
179 '400' => ['description' => 'Bad Request']
180 ]
181 * @param string $method REST method name
182 */
183 public function maybe_add_response_to_swagger( string $method ): void {
184 $method_name = strtolower( $method );
185 $method_response_callback = $method_name . '_response_callback';
186 if ( $this->method_exists_in_current_class( $method_response_callback ) ) {
187 $response_filter = $method_name . '_' . str_replace(
188 '/',
189 '_',
190 $this->namespace . '/' . $this->get_endpoint()
191 );
192 add_filter( 'swagger_api_responses_' . $response_filter, [ $this, $method_response_callback ] );
193 }
194 }
195
196 /**
197 * build_endpoint_method_config
198 *
199 * Builds a configuration array for the endpoint based on the presence of the callback, permission, additional parameters,
200 * and response to Swagger member functions.
201 * @param string $method The REST method for the endpoint
202 *
203 * @return array The endpoint configuration for the method specified by the parameter
204 */
205 private function build_endpoint_method_config( string $method ): array {
206 $config = [
207 'methods' => $method,
208 'callback' => $this->get_callback_method( $method ),
209 'permission_callback' => $this->get_permission_callback_method( $method ),
210 ];
211 $this->maybe_add_response_to_swagger( $method );
212 return $this->maybe_add_args_to_config( $method, $config );
213 }
214
215 /**
216 * method_exists_in_current_class
217 *
218 * Uses reflection to check if this class has the /method/ method.
219 * @param string $method The name of the method being checked.
220 *
221 * @return bool TRUE if the class has the /method/ method, FALSE otherwise.
222 */
223 private function method_exists_in_current_class( string $method ): bool {
224 $class_name = get_class( $this );
225 try {
226 $reflection = new ReflectionClass( $class_name );
227 } catch ( \ReflectionException $e ) {
228 return false;
229 }
230 if ( ! $reflection->hasMethod( $method ) ) {
231 return false;
232 }
233 $method_ref = $reflection->getMethod( $method );
234
235 return ( $method_ref && $class_name === $method_ref->class );
236 }
237
238 /**
239 * permission_callback
240 * Permissions callback fallback for the endpoint
241 * Gets the current user ID and sets the /current_user_id/ property.
242 * If the /auth/ property is set to /true/ will make sure that the user is logged in (has an id greater than 0)
243 *
244 * @param WP_REST_Request $request unused
245 *
246 * @return bool TRUE, if permission granted, FALSE otherwise
247 */
248 public function permission_callback( WP_REST_Request $request ): bool {
249 // try to get current user
250 $this->current_user_id = get_current_user_id();
251 if ( $this->auth ) {
252 return $this->current_user_id > 0;
253 }
254
255 return true;
256 }
257
258 /**
259 * callback
260 * Fallback callback function, returns a response consisting of the string /ok/.
261 *
262 * @param WP_REST_Request $request unused
263 *
264 * @return WP_REST_Response Default Response of the string 'ok'.
265 */
266 public function callback( WP_REST_Request $request ): WP_REST_Response {
267 return rest_ensure_response( [ 'OK' ] );
268 }
269
270 /**
271 * respond_wrong_method
272 *
273 * Creates a WordPress error object with the /rest_no_route/ code and the message and code supplied or the defaults.
274 * @param null $message The error message for the wrong method.
275 * Optional.
276 * Defaults to null, which makes sets the message to /No route was found matching the URL and request method/
277 * @param int $code The HTTP status code.
278 * Optional.
279 * Defaults to 404 (Not found).
280 *
281 * @return WP_Error The WordPress error object with the error message and status code supplied
282 */
283 public function respond_wrong_method( $message = null, int $code = 404 ): WP_Error {
284 if ( null === $message ) {
285 $message = __( 'No route was found matching the URL and request method', 'image-optimization' );
286 }
287
288 return new WP_Error( 'rest_no_route', $message, [ 'status' => $code ] );
289 }
290
291 /**
292 * respond_with_code
293 * Create a new /WP_REST_Response/ object with the specified data and HTTP response code.
294 *
295 * @param array|null $data The data to return in this response
296 * @param int $code The HTTP response code.
297 * Optional.
298 * Defaults to 200 (OK).
299 *
300 * @return WP_REST_Response The WordPress response object loaded with the data and the response code.
301 */
302 public function respond_with_code( ?array $data = null, int $code = 200 ): WP_REST_Response {
303 return new WP_REST_Response( $data, $code );
304 }
305
306 /**
307 * get_user_from_request
308 *
309 * Returns the current user object.
310 * Depends on the property /current_user_id/ to be set.
311 * @return WP_User|false The user object or false if not found or on error.
312 */
313 public function get_user_from_request() {
314 return get_user_by( 'id', $this->current_user_id );
315 }
316
317 /**
318 * get_arguments
319 * Rest Endpoint extra arguments
320 * @return array Additional arguments for the route configuration
321 */
322 public function get_arguments(): array {
323 return [];
324 }
325
326 /**
327 * get_endpoint
328 * Rest route Endpoint
329 * @return string Endpoint uri component (comes after the route namespace)
330 */
331 abstract public function get_endpoint(): string;
332
333 /**
334 * get_name
335 * @return string The name of the route
336 */
337 abstract public function get_name(): string;
338
339 /**
340 * get_self_url
341 *
342 * @param string $endpoint
343 *
344 * @return string
345 */
346 public function get_self_url( string $endpoint = '' ): string {
347 return rest_url( $this->namespace . '/' . $endpoint );
348 }
349
350 public function respond_success_json( $data = [] ): WP_REST_Response {
351 return new WP_REST_Response([
352 'success' => true,
353 'data' => $data,
354 ]);
355 }
356
357 /**
358 * @param array{message: string, code: string} $data
359 *
360 * @return WP_Error
361 */
362 public function respond_error_json( array $data ): WP_Error {
363 if ( ! isset( $data['message'] ) || ! isset( $data['code'] ) ) {
364 _doing_it_wrong(
365 __FUNCTION__,
366 esc_html__( 'Both `message` and `code` keys must be provided', 'image-optimization' ),
367 '1.0.0'
368 ); // @codeCoverageIgnore
369 }
370
371 return new WP_Error(
372 $data['code'] ?? 'internal_server_error',
373 $data['message'] ?? esc_html__( 'Internal server error', 'image-optimization' ),
374 );
375 }
376
377 public function verify_nonce( $nonce = '', $name = '' ) {
378 if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $nonce ) ), $name ) ) {
379 return $this->respond_error_json([
380 'message' => esc_html__( 'Invalid nonce', 'image-optimization' ),
381 'code' => 'bad_request',
382 ]);
383 }
384 }
385
386 public function verify_capability( $capability = 'manage_options' ) {
387 if ( ! current_user_can( $capability ) ) {
388 return $this->respond_error_json([
389 'message' => esc_html__( 'You do not have sufficient permissions to access this data.', 'image-optimization' ),
390 'code' => 'bad_request',
391 ]);
392 }
393 }
394
395 public function verify_nonce_and_capability( $nonce = '', $name = '', $capability = 'manage_options' ) {
396 $valid = $this->verify_nonce( $nonce, $name );
397
398 if ( is_wp_error( $valid ) ) {
399 return $valid;
400 }
401
402 if ( ! current_user_can( $capability ) ) {
403 return $this->respond_error_json([
404 'message' => esc_html__( 'You do not have sufficient permissions to access this data.', 'image-optimization' ),
405 'code' => 'bad_request',
406 ]);
407 }
408 }
409
410 public function trigger_internal( $method, $endpoint, $args = [] ): array {
411 $request = new WP_REST_Request( $method, $this->get_self_url( $endpoint ) );
412 if ( ! empty( $args['body'] ) ) {
413 $request->set_body_params( $args['body'] );
414 }
415 if ( ! empty( $args['headers'] ) ) {
416 $request->set_headers( $args['headers'] );
417 }
418 if ( ! empty( $args['params'] ) ) {
419 $request->set_query_params( $args['params'] );
420 }
421
422 $response = rest_do_request( $request );
423 $server = rest_get_server();
424 return $server->response_to_data( $response, false );
425 }
426 }
427