PluginProbe
Image Optimizer – Compress Images and Convert to WebP or AVIF / 1.7.7
Image Optimizer – Compress Images and Convert to WebP or AVIF v1.7.7
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.7, at classes/rest/route.php

437 lines 13.5 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 * Looks up inherited methods so module Route_Base manage_options gates are honoured.
121 *
122 * @param string $method The REST method name
123 *
124 * @return callable If a method called (rest-method)_permission_callback exists, returns a reference to it,
125 * otherwise get_permission_callback when present, otherwise permission_callback.
126 */
127 public function get_permission_callback_method( string $method ): callable {
128 $method_name = strtolower( $method );
129 $permission_callback_method = $method_name . '_permission_callback';
130
131 if ( method_exists( $this, $permission_callback_method ) ) {
132 return [ $this, $permission_callback_method ];
133 }
134
135 if ( method_exists( $this, 'get_permission_callback' ) ) {
136 return [ $this, 'get_permission_callback' ];
137 }
138
139 return [ $this, 'permission_callback' ];
140 }
141
142 /**
143 * maybe_add_args_to_config
144 *
145 * Checks if the class has a method call (rest-method)_args.
146 * If it does, the function calls it and adds its response to the config object passed to the function, under the /args/ key.
147 * if the function (rest-method)_consumes exists, it will add the response to the config object under the /consumes/ key.
148 * if the function (rest-method)_produces exists, it will add the response to the config object under the /produces/ key.
149 * if the function (rest-method)_summary exists, it will add the response to the config object under the /summary/ key.
150 * if the function (rest-method)_description exists, it will add the response to the config object under the /description/ key.
151 * @param string $method The REST method name being configured
152 * @param array $config The configuration object for the method
153 *
154 * @return array The configuration object for the method, possibly after being amended
155 */
156 public function maybe_add_args_to_config( string $method, array $config ): array {
157 $method_name = strtolower( $method );
158 $method_args = $method_name . '_args';
159 if ( $this->method_exists_in_current_class( $method_args ) ) {
160 $config['args'] = $this->{$method_args}();
161 }
162 $config['consumes'] = [ 'application/json' ];
163 if ( $this->method_exists_in_current_class( $method . '_consumes' ) ) {
164 $config['consumes'] = $this->{$method . '_consumes'}();
165 }
166 $config['produces'] = [ 'application/json' ];
167 if ( $this->method_exists_in_current_class( $method . '_produces' ) ) {
168 $config['produces'] = $this->{$method . '_produces'}();
169 }
170 if ( $this->method_exists_in_current_class( $method . '_summary' ) ) {
171 $config['summary'] = $this->{$method . '_summary'}();
172 }
173 if ( $this->method_exists_in_current_class( $method . '_description' ) ) {
174 $config['description'] = $this->{$method . '_description'}();
175 }
176 return $config;
177 }
178
179 /**
180 * maybe_add_response_to_swagger
181 *
182 * If the function method /(rest-method)_response_callback/ exists, adds the filter
183 * /swagger_api_response_(namespace with slashes replaced with underscores)_(endpoint with slashes replaced with underscores)/
184 * with the aforementioned function method.
185 * This filter is used with the WP API Swagger UI plugin to create documentation for the API.
186 * The value being passed is an array: [
187 '200' => ['description' => 'OK'],
188 '404' => ['description' => 'Not Found'],
189 '400' => ['description' => 'Bad Request']
190 ]
191 * @param string $method REST method name
192 */
193 public function maybe_add_response_to_swagger( string $method ): void {
194 $method_name = strtolower( $method );
195 $method_response_callback = $method_name . '_response_callback';
196 if ( $this->method_exists_in_current_class( $method_response_callback ) ) {
197 $response_filter = $method_name . '_' . str_replace(
198 '/',
199 '_',
200 $this->namespace . '/' . $this->get_endpoint()
201 );
202 add_filter( 'swagger_api_responses_' . $response_filter, [ $this, $method_response_callback ] );
203 }
204 }
205
206 /**
207 * build_endpoint_method_config
208 *
209 * Builds a configuration array for the endpoint based on the presence of the callback, permission, additional parameters,
210 * and response to Swagger member functions.
211 * @param string $method The REST method for the endpoint
212 *
213 * @return array The endpoint configuration for the method specified by the parameter
214 */
215 private function build_endpoint_method_config( string $method ): array {
216 $config = [
217 'methods' => $method,
218 'callback' => $this->get_callback_method( $method ),
219 'permission_callback' => $this->get_permission_callback_method( $method ),
220 ];
221 $this->maybe_add_response_to_swagger( $method );
222 return $this->maybe_add_args_to_config( $method, $config );
223 }
224
225 /**
226 * method_exists_in_current_class
227 *
228 * Uses reflection to check if this class has the /method/ method.
229 * @param string $method The name of the method being checked.
230 *
231 * @return bool TRUE if the class has the /method/ method, FALSE otherwise.
232 */
233 private function method_exists_in_current_class( string $method ): bool {
234 $class_name = get_class( $this );
235 try {
236 $reflection = new ReflectionClass( $class_name );
237 } catch ( \ReflectionException $e ) {
238 return false;
239 }
240 if ( ! $reflection->hasMethod( $method ) ) {
241 return false;
242 }
243 $method_ref = $reflection->getMethod( $method );
244
245 return ( $method_ref && $class_name === $method_ref->class );
246 }
247
248 /**
249 * permission_callback
250 * Permissions callback fallback for the endpoint
251 * Gets the current user ID and sets the /current_user_id/ property.
252 * If the /auth/ property is set to /true/ will make sure that the user is logged in (has an id greater than 0)
253 *
254 * @param WP_REST_Request $request unused
255 *
256 * @return bool TRUE, if permission granted, FALSE otherwise
257 */
258 public function permission_callback( WP_REST_Request $request ): bool {
259 // try to get current user
260 $this->current_user_id = get_current_user_id();
261 if ( $this->auth ) {
262 return $this->current_user_id > 0;
263 }
264
265 return true;
266 }
267
268 /**
269 * callback
270 * Fallback callback function, returns a response consisting of the string /ok/.
271 *
272 * @param WP_REST_Request $request unused
273 *
274 * @return WP_REST_Response Default Response of the string 'ok'.
275 */
276 public function callback( WP_REST_Request $request ): WP_REST_Response {
277 return rest_ensure_response( [ 'OK' ] );
278 }
279
280 /**
281 * respond_wrong_method
282 *
283 * Creates a WordPress error object with the /rest_no_route/ code and the message and code supplied or the defaults.
284 * @param null $message The error message for the wrong method.
285 * Optional.
286 * Defaults to null, which makes sets the message to /No route was found matching the URL and request method/
287 * @param int $code The HTTP status code.
288 * Optional.
289 * Defaults to 404 (Not found).
290 *
291 * @return WP_Error The WordPress error object with the error message and status code supplied
292 */
293 public function respond_wrong_method( $message = null, int $code = 404 ): WP_Error {
294 if ( null === $message ) {
295 $message = __( 'No route was found matching the URL and request method', 'image-optimization' );
296 }
297
298 return new WP_Error( 'rest_no_route', $message, [ 'status' => $code ] );
299 }
300
301 /**
302 * respond_with_code
303 * Create a new /WP_REST_Response/ object with the specified data and HTTP response code.
304 *
305 * @param array|null $data The data to return in this response
306 * @param int $code The HTTP response code.
307 * Optional.
308 * Defaults to 200 (OK).
309 *
310 * @return WP_REST_Response The WordPress response object loaded with the data and the response code.
311 */
312 public function respond_with_code( ?array $data = null, int $code = 200 ): WP_REST_Response {
313 return new WP_REST_Response( $data, $code );
314 }
315
316 /**
317 * get_user_from_request
318 *
319 * Returns the current user object.
320 * Depends on the property /current_user_id/ to be set.
321 * @return WP_User|false The user object or false if not found or on error.
322 */
323 public function get_user_from_request() {
324 return get_user_by( 'id', $this->current_user_id );
325 }
326
327 /**
328 * get_arguments
329 * Rest Endpoint extra arguments
330 * @return array Additional arguments for the route configuration
331 */
332 public function get_arguments(): array {
333 return [];
334 }
335
336 /**
337 * get_endpoint
338 * Rest route Endpoint
339 * @return string Endpoint uri component (comes after the route namespace)
340 */
341 abstract public function get_endpoint(): string;
342
343 /**
344 * get_name
345 * @return string The name of the route
346 */
347 abstract public function get_name(): string;
348
349 /**
350 * get_self_url
351 *
352 * @param string $endpoint
353 *
354 * @return string
355 */
356 public function get_self_url( string $endpoint = '' ): string {
357 return rest_url( $this->namespace . '/' . $endpoint );
358 }
359
360 public function respond_success_json( $data = [] ): WP_REST_Response {
361 return new WP_REST_Response([
362 'success' => true,
363 'data' => $data,
364 ]);
365 }
366
367 /**
368 * @param array{message: string, code: string} $data
369 *
370 * @return WP_Error
371 */
372 public function respond_error_json( array $data ): WP_Error {
373 if ( ! isset( $data['message'] ) || ! isset( $data['code'] ) ) {
374 _doing_it_wrong(
375 __FUNCTION__,
376 esc_html__( 'Both `message` and `code` keys must be provided', 'image-optimization' ),
377 '1.0.0'
378 ); // @codeCoverageIgnore
379 }
380
381 return new WP_Error(
382 $data['code'] ?? 'internal_server_error',
383 $data['message'] ?? esc_html__( 'Internal server error', 'image-optimization' ),
384 );
385 }
386
387 public function verify_nonce( $nonce = '', $name = '' ) {
388 if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $nonce ) ), $name ) ) {
389 return $this->respond_error_json([
390 'message' => esc_html__( 'Invalid nonce', 'image-optimization' ),
391 'code' => 'bad_request',
392 ]);
393 }
394 }
395
396 public function verify_capability( $capability = 'manage_options' ) {
397 if ( ! current_user_can( $capability ) ) {
398 return $this->respond_error_json([
399 'message' => esc_html__( 'You do not have sufficient permissions to access this data.', 'image-optimization' ),
400 'code' => 'bad_request',
401 ]);
402 }
403 }
404
405 public function verify_nonce_and_capability( $nonce = '', $name = '', $capability = 'manage_options' ) {
406 $valid = $this->verify_nonce( $nonce, $name );
407
408 if ( is_wp_error( $valid ) ) {
409 return $valid;
410 }
411
412 if ( ! current_user_can( $capability ) ) {
413 return $this->respond_error_json([
414 'message' => esc_html__( 'You do not have sufficient permissions to access this data.', 'image-optimization' ),
415 'code' => 'bad_request',
416 ]);
417 }
418 }
419
420 public function trigger_internal( $method, $endpoint, $args = [] ): array {
421 $request = new WP_REST_Request( $method, $this->get_self_url( $endpoint ) );
422 if ( ! empty( $args['body'] ) ) {
423 $request->set_body_params( $args['body'] );
424 }
425 if ( ! empty( $args['headers'] ) ) {
426 $request->set_headers( $args['headers'] );
427 }
428 if ( ! empty( $args['params'] ) ) {
429 $request->set_query_params( $args['params'] );
430 }
431
432 $response = rest_do_request( $request );
433 $server = rest_get_server();
434 return $server->response_to_data( $response, false );
435 }
436 }
437