PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / api / abstract-rest-api-controller.php

abstract-rest-api-controller.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/api/abstract-rest-api-controller.php

629 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST Controller
4 *
5 * This class extend `WP_REST_Controller` in order to include /batch endpoint
6 * for almost all endpoints in StoreEngine REST API.
7 *
8 * It's required to follow "Controller Classes" guide before extending this class:
9 * <https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/>
10 *
11 * NOTE THAT ONLY CODE RELEVANT FOR MOST ENDPOINTS SHOULD BE INCLUDED INTO THIS CLASS.
12 * If necessary extend this class and create new abstract classes.
13 *
14 * Base REST controller.
15 *
16 * @package StoreEngine\API
17 * @see https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/
18 */
19
20 namespace StoreEngine\API;
21
22 use Closure;
23 use Exception;
24 use stdClass;
25 use StoreEngine\Classes\AbstractCollection;
26 use StoreEngine\Classes\AbstractEntity;
27 use StoreEngine\Classes\Exceptions\StoreEngineException;
28 use StoreEngine\Classes\StoreengineDatetime;
29 use StoreEngine\Utils\StringUtil;
30 use WP_Error;
31 use WP_HTTP_Response;
32 use WP_Post;
33 use WP_REST_Controller;
34 use WP_REST_Request;
35 use WP_REST_Response;
36 use WP_REST_Server;
37
38 if ( ! defined( 'ABSPATH' ) ) {
39 exit;
40 }
41
42 /**
43 * Abstract Rest Controller Class
44 *
45 * @package StoreEngine\Api
46 * @extends WP_REST_Controller
47 */
48 abstract class AbstractRestApiController extends WP_REST_Controller {
49
50 /**
51 * Endpoint namespace.
52 *
53 * @var string
54 */
55 protected $namespace = STOREENGINE_PLUGIN_SLUG . '/v1';
56
57 /**
58 * Route base.
59 *
60 * @var string
61 */
62 protected $rest_base = '';
63
64 /**
65 * Used to cache computed return fields.
66 *
67 * @var null|array
68 */
69 private ?array $_fields = null; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
70
71 /**
72 * Used to verify if cached fields are for correct request object.
73 *
74 * @var null|WP_REST_Request
75 */
76 private ?WP_REST_Request $_request = null; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
77
78 public function __construct() {
79 if ( ! $this->rest_base ) {
80 $this->rest_base = StringUtil::get_class_slug( $this );
81 }
82 }
83
84 /**
85 * Add the schema from additional fields to an schema array.
86 *
87 * The type of object is inferred from the passed schema.
88 *
89 * @param array $schema Schema array.
90 *
91 * @return array
92 */
93 protected function add_additional_fields_schema( $schema ): array {
94 if ( empty( $schema['title'] ) ) {
95 return $schema;
96 }
97
98 /**
99 * Can't use $this->get_object_type otherwise we cause an inf loop.
100 */
101 $object_type = $schema['title'];
102
103 $additional_fields = $this->get_additional_fields( $object_type );
104
105 foreach ( $additional_fields as $field_name => $field_options ) {
106 if ( ! $field_options['schema'] ) {
107 continue;
108 }
109
110 $schema['properties'][ $field_name ] = $field_options['schema'];
111 }
112
113 $schema['properties'] = apply_filters( 'storeengine/rest_' . $object_type . '_schema', $schema['properties'] );
114
115 return $schema;
116 }
117
118 /**
119 * Get normalized rest base.
120 *
121 * @return string
122 */
123 protected function get_normalized_rest_base(): string {
124 return preg_replace( '/\(.*\)\//i', '', $this->rest_base );
125 }
126
127 /**
128 * Check batch limit.
129 *
130 * @param array $items Request items.
131 *
132 * @return bool|WP_Error
133 */
134 protected function check_batch_limit( array $items ) {
135 $limit = apply_filters( 'storeengine/rest_batch_items_limit', 100, $this->get_normalized_rest_base() );
136 $total = 0;
137
138 if ( ! empty( $items['create'] ) && is_countable( $items['create'] ) ) {
139 $total += count( $items['create'] );
140 }
141
142 if ( ! empty( $items['update'] ) && is_countable( $items['update'] ) ) {
143 $total += count( $items['update'] );
144 }
145
146 if ( ! empty( $items['delete'] ) && is_countable( $items['delete'] ) ) {
147 $total += count( $items['delete'] );
148 }
149
150 if ( $total > $limit ) {
151 /* translators: %s: items limit */
152 return new WP_Error( 'rest_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'storeengine' ), $limit ), array( 'status' => 413 ) );
153 }
154
155 return true;
156 }
157
158 /**
159 * Bulk create, update and delete items.
160 *
161 * @param WP_REST_Request $request Full details about the request.
162 *
163 * @return WP_Error|WP_Error[]|array Of WP_Error or WP_REST_Response.
164 */
165 public function batch_items( WP_REST_Request $request ) {
166 /**
167 * REST Server
168 *
169 * @var WP_REST_Server $wp_rest_server
170 */
171 global $wp_rest_server;
172
173 // Get the request params.
174 $items = array_filter( $request->get_params() );
175 $query = $request->get_query_params();
176 $response = [];
177
178 // Check batch limit.
179 $limit = $this->check_batch_limit( $items );
180 if ( is_wp_error( $limit ) ) {
181 return $limit;
182 }
183
184 if ( ! empty( $items['create'] ) ) {
185 foreach ( $items['create'] as $item ) {
186 $_item = new WP_REST_Request( 'POST', $request->get_route() );
187
188 // Default parameters.
189 $defaults = [];
190 $schema = $this->get_public_item_schema();
191 foreach ( $schema['properties'] as $arg => $options ) {
192 if ( isset( $options['default'] ) ) {
193 $defaults[ $arg ] = $options['default'];
194 }
195 }
196 $_item->set_default_params( $defaults );
197
198 // Set request parameters.
199 $_item->set_body_params( $item );
200
201 // Set query (GET) parameters.
202 $_item->set_query_params( $query );
203
204 $allowed = $this->create_item_permissions_check( $_item );
205 if ( is_wp_error( $allowed ) ) {
206 $response['create'][] = [
207 'id' => 0,
208 'error' => [
209 'code' => $allowed->get_error_code(),
210 'message' => $allowed->get_error_message(),
211 'data' => $allowed->get_error_data(),
212 ],
213 ];
214 continue;
215 }
216
217 $_response = $this->create_item( $_item );
218
219 if ( is_wp_error( $_response ) ) {
220 $response['create'][] = [
221 'id' => 0,
222 'error' => [
223 'code' => $_response->get_error_code(),
224 'message' => $_response->get_error_message(),
225 'data' => $_response->get_error_data(),
226 ],
227 ];
228 } else {
229 $response['create'][] = $wp_rest_server->response_to_data( $_response, '' );
230 }
231 }
232 }
233
234 if ( ! empty( $items['update'] ) ) {
235 foreach ( $items['update'] as $item ) {
236 $_item = new WP_REST_Request( 'PUT', $request->get_route() );
237 $_item->set_body_params( $item );
238
239 $allowed = $this->update_item_permissions_check( $_item );
240 if ( is_wp_error( $allowed ) ) {
241 $response['update'][] = [
242 'id' => $_item['id'],
243 'error' => [
244 'code' => $allowed->get_error_code(),
245 'message' => $allowed->get_error_message(),
246 'data' => $allowed->get_error_data(),
247 ],
248 ];
249 continue;
250 }
251
252 $_response = $this->update_item( $_item );
253
254 if ( is_wp_error( $_response ) ) {
255 $response['update'][] = [
256 'id' => $item['id'],
257 'error' => [
258 'code' => $_response->get_error_code(),
259 'message' => $_response->get_error_message(),
260 'data' => $_response->get_error_data(),
261 ],
262 ];
263 } else {
264 $response['update'][] = $wp_rest_server->response_to_data( $_response, '' );
265 }
266 }
267 }
268
269 if ( ! empty( $items['delete'] ) ) {
270 foreach ( $items['delete'] as $id ) {
271 $id = is_array( $id ) ? $id : (int) $id;
272
273 if ( 0 === $id ) {
274 continue;
275 }
276
277 $_item = new WP_REST_Request( 'DELETE', $request->get_route() );
278 if ( is_array( $id ) ) {
279 $id['force'] = true;
280 $_item->set_query_params( $id );
281 } else {
282 $_item->set_query_params( [
283 'id' => $id,
284 'force' => true,
285 ] );
286 }
287
288 $allowed = $this->delete_item_permissions_check( $_item );
289 if ( is_wp_error( $allowed ) ) {
290 $response['delete'][] = [
291 'id' => $id,
292 'error' => [
293 'code' => $allowed->get_error_code(),
294 'message' => $allowed->get_error_message(),
295 'data' => $allowed->get_error_data(),
296 ],
297 ];
298 continue;
299 }
300
301 $_response = $this->delete_item( $_item );
302
303 if ( is_wp_error( $_response ) ) {
304 $response['delete'][] = [
305 'id' => $id,
306 'error' => [
307 'code' => $_response->get_error_code(),
308 'message' => $_response->get_error_message(),
309 'data' => $_response->get_error_data(),
310 ],
311 ];
312 } else {
313 $response['delete'][] = $wp_rest_server->response_to_data( $_response, '' );
314 }
315 }
316 }
317
318 return $response;
319 }
320
321 /**
322 * Validate a text value for a text based setting.
323 *
324 * @param ?string $value Value.
325 * @param array $setting Setting.
326 *
327 * @return string
328 */
329 public function validate_setting_text_field( ?string $value, array $setting ): string {
330 $value = is_null( $value ) ? '' : $value;
331
332 return wp_kses_post( trim( stripslashes( $value ) ) );
333 }
334
335 /**
336 * Validate select based settings.
337 *
338 * @param string $value Value.
339 * @param array $setting Setting.
340 *
341 * @return string|WP_Error
342 */
343 public function validate_setting_select_field( string $value, array $setting ) {
344 if ( array_key_exists( $value, $setting['options'] ) ) {
345 return $value;
346 } else {
347 return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'storeengine' ), [ 'status' => 400 ] );
348 }
349 }
350
351 /**
352 * Validate multiselect based settings.
353 *
354 * @param array $values Values.
355 * @param array $setting Setting.
356 *
357 * @return array|WP_Error
358 */
359 public function validate_setting_multiselect_field( array $values, array $setting ) {
360 if ( empty( $values ) ) {
361 return [];
362 }
363
364 if ( ! is_array( $values ) ) {
365 return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'storeengine' ), array( 'status' => 400 ) );
366 }
367
368 $final_values = [];
369 foreach ( $values as $value ) {
370 if ( array_key_exists( $value, $setting['options'] ) ) {
371 $final_values[] = $value;
372 }
373 }
374
375 return $final_values;
376 }
377
378 /**
379 * Validate image_width based settings.
380 *
381 * @param array $values Values.
382 * @param array $setting Setting.
383 *
384 * @return string|WP_Error
385 */
386 public function validate_setting_image_width_field( array $values, array $setting ) {
387 if ( ! is_array( $values ) ) {
388 return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'storeengine' ), array( 'status' => 400 ) );
389 }
390
391 $current = $setting['value'];
392
393 if ( isset( $values['width'] ) ) {
394 $current['width'] = intval( $values['width'] );
395 }
396
397 if ( isset( $values['height'] ) ) {
398 $current['height'] = intval( $values['height'] );
399 }
400
401 if ( isset( $values['crop'] ) ) {
402 $current['crop'] = (bool) $values['crop'];
403 }
404
405 return $current;
406 }
407
408 /**
409 * Validate radio based settings.
410 *
411 * @param string $value Value.
412 * @param array $setting Setting.
413 *
414 * @return string|WP_Error
415 */
416 public function validate_setting_radio_field( string $value, array $setting ) {
417 return $this->validate_setting_select_field( $value, $setting );
418 }
419
420 /**
421 * Validate checkbox based settings.
422 *
423 * @param string $value Value.
424 * @param array $setting Setting.
425 *
426 * @return string|WP_Error
427 */
428 public function validate_setting_checkbox_field( string $value, array $setting ) {
429 if ( in_array( $value, [ 'yes', 'no' ], true ) ) {
430 return $value;
431 } elseif ( empty( $value ) ) {
432 return $setting['default'] ?? 'no';
433 } else {
434 return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'storeengine' ), array( 'status' => 400 ) );
435 }
436 }
437
438 /**
439 * Validate textarea based settings.
440 *
441 * @param string $value Value.
442 * @param array $setting Setting.
443 *
444 * @return string
445 */
446 public function validate_setting_textarea_field( string $value, array $setting ) {
447 $value = is_null( $value ) ? '' : $value;
448
449 return wp_kses_post( trim( stripslashes( $value ) ) );
450 }
451
452 /**
453 * Add meta query.
454 *
455 * @param array $args Query args.
456 * @param array $meta_query Meta query.
457 *
458 * @return array
459 */
460 protected function add_meta_query( array $args, array $meta_query ): array {
461 if ( empty( $args['meta_query'] ) ) {
462 $args['meta_query'] = []; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
463 }
464
465 $args['meta_query'][] = $meta_query;
466
467 return $args['meta_query'];
468 }
469
470 /**
471 * Get the batch schema, conforming to JSON Schema.
472 *
473 * @return array
474 */
475 public function get_public_batch_schema(): array {
476 return apply_filters( 'storeengine/get_public_batch_schema', [
477 '$schema' => 'http://json-schema.org/draft-04/schema#',
478 'title' => 'batch',
479 'type' => 'object',
480 'properties' => [
481 'create' => [
482 'description' => __( 'List of created resources.', 'storeengine' ),
483 'type' => 'array',
484 'context' => [ 'view', 'edit' ],
485 'items' => [
486 'type' => 'object',
487 ],
488 ],
489 'update' => [
490 'description' => __( 'List of updated resources.', 'storeengine' ),
491 'type' => 'array',
492 'context' => [ 'view', 'edit' ],
493 'items' => [
494 'type' => 'object',
495 ],
496 ],
497 'delete' => [
498 'description' => __( 'List of delete resources.', 'storeengine' ),
499 'type' => 'array',
500 'context' => [ 'view', 'edit' ],
501 'items' => [
502 'type' => 'integer',
503 ],
504 ],
505 ],
506 ] );
507 }
508
509 /**
510 * Limit the contents of the meta_data property based on certain request parameters.
511 *
512 * Note that if both `include_meta` and `exclude_meta` are present in the request,
513 * `include_meta` will take precedence.
514 *
515 * @param WP_REST_Request $request The request.
516 * @param array $meta_data All the meta data for an object.
517 *
518 * @return array
519 */
520 protected function get_meta_data_for_response( WP_REST_Request $request, array $meta_data ): array {
521 $fields = $this->get_fields_for_response( $request );
522 if ( ! in_array( 'meta_data', $fields, true ) ) {
523 return array();
524 }
525
526 $include = (array) $request['include_meta'];
527 $exclude = (array) $request['exclude_meta'];
528
529 if ( ! empty( $include ) ) {
530 $meta_data = array_filter( $meta_data, fn( $item ) => in_array( $item['key'], $include, true ) );
531 } elseif ( ! empty( $exclude ) ) {
532 $meta_data = array_filter( $meta_data, fn( $item ) => ! in_array( $item['key'], $exclude, true ) );
533 }
534
535 // Ensure the array indexes are reset so it doesn't get converted to an object in JSON.
536 return array_values( $meta_data );
537 }
538
539 /**
540 * @param AbstractEntity|WP_Post|array|stdClass $item
541 * @param WP_REST_Request $request
542 *
543 * @return array
544 */
545 protected function prepare_links( $item, WP_REST_Request $request ): array {
546 $id = null;
547 $links = [];
548
549 if ( $item instanceof AbstractEntity || is_callable( [ $item, 'get_id' ] ) ) {
550 $id = $item->get_id();
551 } elseif ( is_array( $item ) && ! empty( $item['id'] ) ) {
552 $id = $item['id'];
553 } elseif ( is_array( $item ) && ! empty( $item['ID'] ) ) {
554 $id = $item['ID'];
555 } elseif ( is_object( $item ) && isset( $item->id ) ) {
556 $id = $item->id;
557 } elseif ( is_object( $item ) && isset( $item->ID ) ) {
558 $id = $item->ID;
559 }
560
561 if ( $id ) {
562 $links['self'] = [
563 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $id ) ),
564 ];
565 }
566
567 $links['collection'] = [
568 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
569 ];
570
571 return $links;
572 }
573
574 protected function prepare_pagination_headers( WP_REST_Response $response, WP_REST_Request $request, int $current_page = 1, int $total = 0, int $total_pages = 1 ) {
575 $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
576
577 $response->header( 'X-WP-Total', $total );
578 $response->header( 'X-WP-TotalPages', $total_pages );
579
580 if ( $current_page > 1 ) {
581 $prev_page = $current_page - 1;
582 if ( $prev_page > $total_pages ) {
583 $prev_page = $total_pages;
584 }
585 $prev_link = add_query_arg( 'page', $prev_page, $base );
586 $response->link_header( 'prev', $prev_link );
587 }
588
589 if ( $total_pages > $current_page ) {
590 $next_page = $current_page + 1;
591 $next_link = add_query_arg( 'page', $next_page, $base );
592 $response->link_header( 'next', $next_link );
593 }
594 }
595
596 protected function prepare_query_response( array $data, $query, WP_REST_Request $request ) {
597 $response = rest_ensure_response( $data );
598
599 if ( $query instanceof AbstractCollection ) {
600 $this->prepare_pagination_headers(
601 $response,
602 $request,
603 $query->query['page'],
604 $query->get_found_results(),
605 $query->get_max_num_pages()
606 );
607 }
608
609 if ( $query instanceof \WP_Query ) {
610 $this->prepare_pagination_headers(
611 $response,
612 $request,
613 $query->query['paged'],
614 $query->found_posts,
615 $query->max_num_pages
616 );
617 }
618
619
620 return $response;
621 }
622
623 protected function date_as_string( ?StoreengineDatetime $date_time_object ): ?string {
624 return is_null( $date_time_object ) ? null : $date_time_object->format( 'Y-m-d H:i:s' );
625 }
626 }
627
628 // End of file abstract-rest-api-controller.php.
629