Passed
Push — master ( 6176aa...f7c939 )
by Mike
03:08
created

AbstractTermsContoller::check_permissions()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 10
nc 4
nop 2
dl 0
loc 20
rs 8.8333
c 0
b 0
f 0
1
<?php
2
/**
3
 * Abstract Rest Terms Controller
4
 *
5
 * @package WooCommerce/RestApi
6
 */
7
8
namespace WooCommerce\RestApi\Controllers\Version4;
9
10
defined( 'ABSPATH' ) || exit;
11
12
use \WooCommerce\RestApi\Controllers\Version4\Utilities\Permissions;
13
14
/**
15
 * Terms controller class.
16
 */
17
abstract class AbstractTermsContoller extends AbstractController {
18
19
	/**
20
	 * Route base.
21
	 *
22
	 * @var string
23
	 */
24
	protected $rest_base = '';
25
26
	/**
27
	 * Taxonomy.
28
	 *
29
	 * @var string
30
	 */
31
	protected $taxonomy = '';
32
33
	/**
34
	 * Store total terms.
35
	 *
36
	 * @var integer
37
	 */
38
	protected $total_terms = 0;
39
40
	/**
41
	 * Store sort column.
42
	 *
43
	 * @var string
44
	 */
45
	protected $sort_column = '';
46
47
	/**
48
	 * Register the routes for terms.
49
	 */
50
	public function register_routes() {
51
		register_rest_route(
52
			$this->namespace,
53
			'/' . $this->rest_base,
54
			array(
55
				array(
56
					'methods'             => \WP_REST_Server::READABLE,
57
					'callback'            => array( $this, 'get_items' ),
58
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
59
					'args'                => $this->get_collection_params(),
60
				),
61
				array(
62
					'methods'             => \WP_REST_Server::CREATABLE,
63
					'callback'            => array( $this, 'create_item' ),
64
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
65
					'args'                => array_merge(
66
						$this->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ),
67
						array(
68
							'name' => array(
69
								'type'        => 'string',
70
								'description' => __( 'Name for the resource.', 'woocommerce' ),
71
								'required'    => true,
72
							),
73
						)
74
					),
75
				),
76
				'schema' => array( $this, 'get_public_item_schema' ),
77
			),
78
			true
79
		);
80
81
		register_rest_route(
82
			$this->namespace,
83
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
84
			array(
85
				'args'   => array(
86
					'id' => array(
87
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
88
						'type'        => 'integer',
89
					),
90
				),
91
				array(
92
					'methods'             => \WP_REST_Server::READABLE,
93
					'callback'            => array( $this, 'get_item' ),
94
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
95
					'args'                => array(
96
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
97
					),
98
				),
99
				array(
100
					'methods'             => \WP_REST_Server::EDITABLE,
101
					'callback'            => array( $this, 'update_item' ),
102
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
103
					'args'                => $this->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ),
104
				),
105
				array(
106
					'methods'             => \WP_REST_Server::DELETABLE,
107
					'callback'            => array( $this, 'delete_item' ),
108
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
109
					'args'                => array(
110
						'force' => array(
111
							'default'     => false,
112
							'type'        => 'boolean',
113
							'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
114
						),
115
					),
116
				),
117
				'schema' => array( $this, 'get_public_item_schema' ),
118
			),
119
			true
120
		);
121
122
		$this->register_batch_route();
123
	}
124
125
	/**
126
	 * Check if a given request has access to read the terms.
127
	 *
128
	 * @param  \WP_REST_Request $request Full details about the request.
129
	 * @return \WP_Error|boolean
130
	 */
131
	public function get_items_permissions_check( $request ) {
132
		if ( ! Permissions::check_taxonomy( $this->taxonomy, 'read' ) ) {
133
			return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
134
		}
135
		return true;
136
	}
137
138
	/**
139
	 * Check if a given request has access to create a term.
140
	 *
141
	 * @param  \WP_REST_Request $request Full details about the request.
142
	 * @return \WP_Error|boolean
143
	 */
144
	public function create_item_permissions_check( $request ) {
145
		if ( ! Permissions::check_taxonomy( $this->taxonomy, 'create' ) ) {
146
			return new \WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
147
		}
148
		return true;
149
	}
150
151
	/**
152
	 * Check if a given request has access to read a term.
153
	 *
154
	 * @param  \WP_REST_Request $request Full details about the request.
155
	 * @return \WP_Error|boolean
156
	 */
157
	public function get_item_permissions_check( $request ) {
158
		$id = $request->get_param( 'id' );
159
160
		if ( ! Permissions::check_taxonomy( $this->taxonomy, 'read', $id ) ) {
161
			return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you are not allowed to view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
162
		}
163
		return true;
164
	}
165
166
	/**
167
	 * Check if a given request has access to update a term.
168
	 *
169
	 * @param  \WP_REST_Request $request Full details about the request.
170
	 * @return \WP_Error|boolean
171
	 */
172
	public function update_item_permissions_check( $request ) {
173
		$id = $request->get_param( 'id' );
174
175
		if ( ! Permissions::check_taxonomy( $this->taxonomy, 'edit', $id ) ) {
176
			return new \WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
177
		}
178
		return true;
179
	}
180
181
	/**
182
	 * Check if a given request has access to delete a term.
183
	 *
184
	 * @param  \WP_REST_Request $request Full details about the request.
185
	 * @return \WP_Error|boolean
186
	 */
187
	public function delete_item_permissions_check( $request ) {
188
		$id = $request->get_param( 'id' );
189
190
		if ( ! Permissions::check_taxonomy( $this->taxonomy, 'delete', $id ) ) {
191
			return new \WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
192
		}
193
		return true;
194
	}
195
196
	/**
197
	 * Check if a given request has access batch create, update and delete items.
198
	 *
199
	 * @param  \WP_REST_Request $request Full details about the request.
200
	 * @return boolean|\WP_Error
201
	 */
202
	public function batch_items_permissions_check( $request ) {
203
		if ( ! Permissions::check_taxonomy( $this->taxonomy, 'batch' ) ) {
204
			return new \WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
205
		}
206
		return true;
207
	}
208
209
	/**
210
	 * Get terms associated with a taxonomy.
211
	 *
212
	 * @param \WP_REST_Request $request Full details about the request.
213
	 * @return \WP_REST_Response|\WP_Error
214
	 */
215
	public function get_items( $request ) {
216
		$taxonomy      = $this->get_taxonomy( $request );
217
		$prepared_args = array(
218
			'exclude'    => $request['exclude'],
219
			'include'    => $request['include'],
220
			'order'      => $request['order'],
221
			'orderby'    => $request['orderby'],
222
			'product'    => $request['product'],
223
			'hide_empty' => $request['hide_empty'],
224
			'number'     => $request['per_page'],
225
			'search'     => $request['search'],
226
			'slug'       => $request['slug'],
227
		);
228
229
		if ( ! empty( $request['offset'] ) ) {
230
			$prepared_args['offset'] = $request['offset'];
231
		} else {
232
			$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
233
		}
234
235
		$taxonomy_obj = get_taxonomy( $taxonomy );
236
237
		if ( $taxonomy_obj->hierarchical && isset( $request['parent'] ) ) {
238
			if ( 0 === $request['parent'] ) {
239
				// Only query top-level terms.
240
				$prepared_args['parent'] = 0;
241
			} else {
242
				if ( $request['parent'] ) {
243
					$prepared_args['parent'] = $request['parent'];
244
				}
245
			}
246
		}
247
248
		/**
249
		 * Filter the query arguments, before passing them to `get_terms()`.
250
		 *
251
		 * Enables adding extra arguments or setting defaults for a terms
252
		 * collection request.
253
		 *
254
		 * @see https://developer.wordpress.org/reference/functions/get_terms/
255
		 *
256
		 * @param array           $prepared_args Array of arguments to be
257
		 *                                       passed to get_terms.
258
		 * @param \WP_REST_Request $request       The current request.
259
		 */
260
		$prepared_args = apply_filters( "woocommerce_rest_{$taxonomy}_query", $prepared_args, $request );
261
262
		if ( ! empty( $prepared_args['product'] ) ) {
263
			$query_result = $this->get_terms_for_product( $prepared_args, $request );
264
			$total_terms  = $this->total_terms;
265
		} else {
266
			$query_result = get_terms( $taxonomy, $prepared_args );
267
268
			$count_args = $prepared_args;
269
			unset( $count_args['number'] );
270
			unset( $count_args['offset'] );
271
			$total_terms = wp_count_terms( $taxonomy, $count_args );
272
273
			// Ensure we don't return results when offset is out of bounds.
274
			// See https://core.trac.wordpress.org/ticket/35935.
275
			if ( $prepared_args['offset'] && $prepared_args['offset'] >= $total_terms ) {
276
				$query_result = array();
277
			}
278
279
			// wp_count_terms can return a falsy value when the term has no children.
280
			if ( ! $total_terms ) {
281
				$total_terms = 0;
282
			}
283
		}
284
		$response = array();
285
		foreach ( $query_result as $term ) {
286
			$data       = $this->prepare_item_for_response( $term, $request );
287
			$response[] = $this->prepare_response_for_collection( $data );
0 ignored issues
show
Bug introduced by
$data of type WP_Error is incompatible with the type WP_REST_Response expected by parameter $response of WP_REST_Controller::prep...sponse_for_collection(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

287
			$response[] = $this->prepare_response_for_collection( /** @scrutinizer ignore-type */ $data );
Loading history...
288
		}
289
290
		$response = rest_ensure_response( $response );
291
292
		// Store pagination values for headers then unset for count query.
293
		$per_page = (int) $prepared_args['number'];
294
		$page     = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
295
296
		$response->header( 'X-WP-Total', (int) $total_terms );
297
		$max_pages = ceil( $total_terms / $per_page );
298
		$response->header( 'X-WP-TotalPages', (int) $max_pages );
299
300
		$base  = str_replace( '(?P<attribute_id>[\d]+)', $request['attribute_id'], $this->rest_base );
301
		$base = add_query_arg( $request->get_query_params(), rest_url( '/' . $this->namespace . '/' . $base ) );
302
		if ( $page > 1 ) {
303
			$prev_page = $page - 1;
304
			if ( $prev_page > $max_pages ) {
305
				$prev_page = $max_pages;
306
			}
307
			$prev_link = add_query_arg( 'page', $prev_page, $base );
308
			$response->link_header( 'prev', $prev_link );
309
		}
310
		if ( $max_pages > $page ) {
311
			$next_page = $page + 1;
312
			$next_link = add_query_arg( 'page', $next_page, $base );
313
			$response->link_header( 'next', $next_link );
314
		}
315
316
		return $response;
317
	}
318
319
	/**
320
	 * Create a single term for a taxonomy.
321
	 *
322
	 * @param \WP_REST_Request $request Full details about the request.
323
	 * @return \WP_REST_Request|\WP_Error
324
	 */
325
	public function create_item( $request ) {
326
		$taxonomy = $this->get_taxonomy( $request );
327
		$name     = $request['name'];
328
		$args     = array();
329
		$schema   = $this->get_item_schema();
330
331
		if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) {
332
			$args['description'] = $request['description'];
333
		}
334
		if ( isset( $request['slug'] ) ) {
335
			$args['slug'] = $request['slug'];
336
		}
337
		if ( isset( $request['parent'] ) ) {
338
			if ( ! is_taxonomy_hierarchical( $taxonomy ) ) {
339
				return new \WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) );
340
			}
341
			$args['parent'] = $request['parent'];
342
		}
343
344
		$term = wp_insert_term( $name, $taxonomy, $args );
345
		if ( is_wp_error( $term ) ) {
346
			$error_data = array( 'status' => 400 );
347
348
			// If we're going to inform the client that the term exists,
349
			// give them the identifier they can actually use.
350
			$term_id = $term->get_error_data( 'term_exists' );
351
			if ( $term_id ) {
352
				$error_data['resource_id'] = $term_id;
353
			}
354
355
			return new \WP_Error( $term->get_error_code(), $term->get_error_message(), $error_data );
356
		}
357
358
		$term = get_term( $term['term_id'], $taxonomy );
359
360
		$this->update_additional_fields_for_object( $term, $request );
0 ignored issues
show
Bug introduced by
It seems like $term can also be of type WP_Error and WP_Term; however, parameter $object of WP_REST_Controller::upda...nal_fields_for_object() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

360
		$this->update_additional_fields_for_object( /** @scrutinizer ignore-type */ $term, $request );
Loading history...
361
362
		// Add term data.
363
		$meta_fields = $this->update_term_meta_fields( $term, $request );
364
		if ( is_wp_error( $meta_fields ) ) {
365
			wp_delete_term( $term->term_id, $taxonomy );
0 ignored issues
show
Bug introduced by
The property term_id does not seem to exist on WP_Error.
Loading history...
366
367
			return $meta_fields;
368
		}
369
370
		/**
371
		 * Fires after a single term is created or updated via the REST API.
372
		 *
373
		 * @param WP_Term         $term      Inserted Term object.
374
		 * @param \WP_REST_Request $request   Request object.
375
		 * @param boolean         $creating  True when creating term, false when updating.
376
		 */
377
		do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, true );
378
379
		$request->set_param( 'context', 'edit' );
380
		$response = $this->prepare_item_for_response( $term, $request );
381
		$response = rest_ensure_response( $response );
382
		$response->set_status( 201 );
0 ignored issues
show
Bug introduced by
The method set_status() does not exist on WP_Error. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

382
		$response->/** @scrutinizer ignore-call */ 
383
             set_status( 201 );

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
383
384
		$base = '/' . $this->namespace . '/' . $this->rest_base;
385
		if ( ! empty( $request['attribute_id'] ) ) {
386
			$base = str_replace( '(?P<attribute_id>[\d]+)', (int) $request['attribute_id'], $base );
387
		}
388
389
		$response->header( 'Location', rest_url( $base . '/' . $term->term_id ) );
0 ignored issues
show
Bug introduced by
The method header() does not exist on WP_Error. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

389
		$response->/** @scrutinizer ignore-call */ 
390
             header( 'Location', rest_url( $base . '/' . $term->term_id ) );

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
390
391
		return $response;
392
	}
393
394
	/**
395
	 * Get a single term from a taxonomy.
396
	 *
397
	 * @param \WP_REST_Request $request Full details about the request.
398
	 * @return \WP_REST_Request|\WP_Error
399
	 */
400
	public function get_item( $request ) {
401
		$taxonomy = $this->get_taxonomy( $request );
402
		$term     = get_term( (int) $request['id'], $taxonomy );
403
404
		if ( is_wp_error( $term ) ) {
405
			return $term;
406
		}
407
408
		$response = $this->prepare_item_for_response( $term, $request );
409
410
		return rest_ensure_response( $response );
411
	}
412
413
	/**
414
	 * Update a single term from a taxonomy.
415
	 *
416
	 * @param \WP_REST_Request $request Full details about the request.
417
	 * @return \WP_REST_Request|\WP_Error
418
	 */
419
	public function update_item( $request ) {
420
		$taxonomy      = $this->get_taxonomy( $request );
421
		$term          = get_term( (int) $request['id'], $taxonomy );
422
		$schema        = $this->get_item_schema();
423
		$prepared_args = array();
424
425
		if ( isset( $request['name'] ) ) {
426
			$prepared_args['name'] = $request['name'];
427
		}
428
		if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) {
429
			$prepared_args['description'] = $request['description'];
430
		}
431
		if ( isset( $request['slug'] ) ) {
432
			$prepared_args['slug'] = $request['slug'];
433
		}
434
		if ( isset( $request['parent'] ) ) {
435
			if ( ! is_taxonomy_hierarchical( $taxonomy ) ) {
436
				return new \WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) );
437
			}
438
			$prepared_args['parent'] = $request['parent'];
439
		}
440
441
		// Only update the term if we haz something to update.
442
		if ( ! empty( $prepared_args ) ) {
443
			$update = wp_update_term( $term->term_id, $term->taxonomy, $prepared_args );
0 ignored issues
show
Bug introduced by
The property term_id does not seem to exist on WP_Error.
Loading history...
Bug introduced by
The property taxonomy does not seem to exist on WP_Error.
Loading history...
444
			if ( is_wp_error( $update ) ) {
445
				return $update;
446
			}
447
		}
448
449
		$term = get_term( (int) $request['id'], $taxonomy );
450
451
		$this->update_additional_fields_for_object( $term, $request );
0 ignored issues
show
Bug introduced by
It seems like $term can also be of type WP_Error and WP_Term; however, parameter $object of WP_REST_Controller::upda...nal_fields_for_object() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

451
		$this->update_additional_fields_for_object( /** @scrutinizer ignore-type */ $term, $request );
Loading history...
452
453
		// Update term data.
454
		$meta_fields = $this->update_term_meta_fields( $term, $request );
455
		if ( is_wp_error( $meta_fields ) ) {
456
			return $meta_fields;
457
		}
458
459
		/**
460
		 * Fires after a single term is created or updated via the REST API.
461
		 *
462
		 * @param WP_Term         $term      Inserted Term object.
463
		 * @param \WP_REST_Request $request   Request object.
464
		 * @param boolean         $creating  True when creating term, false when updating.
465
		 */
466
		do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, false );
467
468
		$request->set_param( 'context', 'edit' );
469
		$response = $this->prepare_item_for_response( $term, $request );
470
		return rest_ensure_response( $response );
471
	}
472
473
	/**
474
	 * Delete a single term from a taxonomy.
475
	 *
476
	 * @param \WP_REST_Request $request Full details about the request.
477
	 * @return \WP_REST_Response|\WP_Error
478
	 */
479
	public function delete_item( $request ) {
480
		$taxonomy = $this->get_taxonomy( $request );
481
		$force    = isset( $request['force'] ) ? (bool) $request['force'] : false;
482
483
		// We don't support trashing for this type, error out.
484
		if ( ! $force ) {
485
			return new \WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
486
		}
487
488
		$term = get_term( (int) $request['id'], $taxonomy );
489
		// Get default category id.
490
		$default_category_id = absint( get_option( 'default_product_cat', 0 ) );
491
492
		// Prevent deleting the default product category.
493
		if ( $default_category_id === (int) $request['id'] ) {
494
			return new \WP_Error( 'woocommerce_rest_cannot_delete', __( 'Default product category cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
495
		}
496
497
		$request->set_param( 'context', 'edit' );
498
		$response = $this->prepare_item_for_response( $term, $request );
499
500
		$retval = wp_delete_term( $term->term_id, $term->taxonomy );
0 ignored issues
show
Bug introduced by
The property taxonomy does not seem to exist on WP_Error.
Loading history...
Bug introduced by
The property term_id does not seem to exist on WP_Error.
Loading history...
501
		if ( ! $retval ) {
502
			return new \WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
503
		}
504
505
		/**
506
		 * Fires after a single term is deleted via the REST API.
507
		 *
508
		 * @param WP_Term          $term     The deleted term.
509
		 * @param \WP_REST_Response $response The response data.
510
		 * @param \WP_REST_Request  $request  The request sent to the API.
511
		 */
512
		do_action( "woocommerce_rest_delete_{$taxonomy}", $term, $response, $request );
513
514
		return $response;
515
	}
516
517
	/**
518
	 * Prepare links for the request.
519
	 *
520
	 * @param object           $term   Term object.
521
	 * @param \WP_REST_Request $request Full details about the request.
522
	 * @return array Links for the given term.
523
	 */
524
	protected function prepare_links( $term, $request ) {
525
		$base = '/' . $this->namespace . '/' . $this->rest_base;
526
527
		if ( ! empty( $request['attribute_id'] ) ) {
528
			$base = str_replace( '(?P<attribute_id>[\d]+)', (int) $request['attribute_id'], $base );
529
		}
530
531
		$links = array(
532
			'self'       => array(
533
				'href' => rest_url( trailingslashit( $base ) . $term->term_id ),
534
			),
535
			'collection' => array(
536
				'href' => rest_url( $base ),
537
			),
538
		);
539
540
		if ( $term->parent ) {
541
			$parent_term = get_term( (int) $term->parent, $term->taxonomy );
542
			if ( $parent_term ) {
543
				$links['up'] = array(
544
					'href' => rest_url( trailingslashit( $base ) . $parent_term->term_id ),
0 ignored issues
show
Bug introduced by
The property term_id does not seem to exist on WP_Error.
Loading history...
545
				);
546
			}
547
		}
548
549
		return $links;
550
	}
551
552
	/**
553
	 * Update term meta fields.
554
	 *
555
	 * @param \WP_Term         $term    Term object.
556
	 * @param \WP_REST_Request $request Full details about the request.
557
	 * @return bool|\WP_Error
558
	 */
559
	protected function update_term_meta_fields( $term, $request ) {
560
		return true;
561
	}
562
563
	/**
564
	 * Get the terms attached to a product.
565
	 *
566
	 * This is an alternative to `get_terms()` that uses `get_the_terms()`
567
	 * instead, which hits the object cache. There are a few things not
568
	 * supported, notably `include`, `exclude`. In `self::get_items()` these
569
	 * are instead treated as a full query.
570
	 *
571
	 * @param array            $prepared_args Arguments for `get_terms()`.
572
	 * @param \WP_REST_Request $request       Full details about the request.
573
	 * @return array List of term objects. (Total count in `$this->total_terms`).
574
	 */
575
	protected function get_terms_for_product( $prepared_args, $request ) {
576
		$taxonomy = $this->get_taxonomy( $request );
577
578
		$query_result = get_the_terms( $prepared_args['product'], $taxonomy );
579
		if ( empty( $query_result ) ) {
580
			$this->total_terms = 0;
581
			return array();
582
		}
583
584
		// get_items() verifies that we don't have `include` set, and default.
585
		// ordering is by `name`.
586
		if ( ! in_array( $prepared_args['orderby'], array( 'name', 'none', 'include' ), true ) ) {
587
			switch ( $prepared_args['orderby'] ) {
588
				case 'id':
589
					$this->sort_column = 'term_id';
590
					break;
591
				case 'slug':
592
				case 'term_group':
593
				case 'description':
594
				case 'count':
595
					$this->sort_column = $prepared_args['orderby'];
596
					break;
597
			}
598
			usort( $query_result, array( $this, 'compare_terms' ) );
599
		}
600
		if ( strtolower( $prepared_args['order'] ) !== 'asc' ) {
601
			$query_result = array_reverse( $query_result );
602
		}
603
604
		// Pagination.
605
		$this->total_terms = count( $query_result );
606
		$query_result      = array_slice( $query_result, $prepared_args['offset'], $prepared_args['number'] );
607
608
		return $query_result;
609
	}
610
611
	/**
612
	 * Comparison function for sorting terms by a column.
613
	 *
614
	 * Uses `$this->sort_column` to determine field to sort by.
615
	 *
616
	 * @param \stdClass $left Term object.
617
	 * @param \stdClass $right Term object.
618
	 * @return int <0 if left is higher "priority" than right, 0 if equal, >0 if right is higher "priority" than left.
619
	 */
620
	protected function compare_terms( $left, $right ) {
621
		$col       = $this->sort_column;
622
		$left_val  = $left->$col;
623
		$right_val = $right->$col;
624
625
		if ( is_int( $left_val ) && is_int( $right_val ) ) {
626
			return $left_val - $right_val;
627
		}
628
629
		return strcmp( $left_val, $right_val );
630
	}
631
632
	/**
633
	 * Get the query params for collections
634
	 *
635
	 * @return array
636
	 */
637
	public function get_collection_params() {
638
		$params = parent::get_collection_params();
639
640
		if ( '' !== $this->taxonomy && taxonomy_exists( $this->taxonomy ) ) {
641
			$taxonomy = get_taxonomy( $this->taxonomy );
642
		} else {
643
			$taxonomy               = new \stdClass();
644
			$taxonomy->hierarchical = true;
645
		}
646
647
		$params['context']['default'] = 'view';
648
649
		$params['exclude'] = array(
650
			'description'       => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
651
			'type'              => 'array',
652
			'items'             => array(
653
				'type' => 'integer',
654
			),
655
			'default'           => array(),
656
			'sanitize_callback' => 'wp_parse_id_list',
657
		);
658
		$params['include'] = array(
659
			'description'       => __( 'Limit result set to specific ids.', 'woocommerce' ),
660
			'type'              => 'array',
661
			'items'             => array(
662
				'type' => 'integer',
663
			),
664
			'default'           => array(),
665
			'sanitize_callback' => 'wp_parse_id_list',
666
		);
667
		if ( ! $taxonomy->hierarchical ) {
668
			$params['offset'] = array(
669
				'description'       => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
670
				'type'              => 'integer',
671
				'sanitize_callback' => 'absint',
672
				'validate_callback' => 'rest_validate_request_arg',
673
			);
674
		}
675
		$params['order']      = array(
676
			'description'       => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
677
			'type'              => 'string',
678
			'sanitize_callback' => 'sanitize_key',
679
			'default'           => 'asc',
680
			'enum'              => array(
681
				'asc',
682
				'desc',
683
			),
684
			'validate_callback' => 'rest_validate_request_arg',
685
		);
686
		$params['orderby']    = array(
687
			'description'       => __( 'Sort collection by resource attribute.', 'woocommerce' ),
688
			'type'              => 'string',
689
			'sanitize_callback' => 'sanitize_key',
690
			'default'           => 'name',
691
			'enum'              => array(
692
				'id',
693
				'include',
694
				'name',
695
				'slug',
696
				'term_group',
697
				'description',
698
				'count',
699
			),
700
			'validate_callback' => 'rest_validate_request_arg',
701
		);
702
		$params['hide_empty'] = array(
703
			'description'       => __( 'Whether to hide resources not assigned to any products.', 'woocommerce' ),
704
			'type'              => 'boolean',
705
			'default'           => false,
706
			'validate_callback' => 'rest_validate_request_arg',
707
		);
708
		if ( $taxonomy->hierarchical ) {
709
			$params['parent'] = array(
710
				'description'       => __( 'Limit result set to resources assigned to a specific parent.', 'woocommerce' ),
711
				'type'              => 'integer',
712
				'sanitize_callback' => 'absint',
713
				'validate_callback' => 'rest_validate_request_arg',
714
			);
715
		}
716
		$params['product'] = array(
717
			'description'       => __( 'Limit result set to resources assigned to a specific product.', 'woocommerce' ),
718
			'type'              => 'integer',
719
			'default'           => null,
720
			'validate_callback' => 'rest_validate_request_arg',
721
		);
722
		$params['slug']    = array(
723
			'description'       => __( 'Limit result set to resources with a specific slug.', 'woocommerce' ),
724
			'type'              => 'string',
725
			'validate_callback' => 'rest_validate_request_arg',
726
		);
727
728
		return $params;
729
	}
730
731
	/**
732
	 * Get taxonomy.
733
	 *
734
	 * @param \WP_REST_Request $request Full details about the request.
735
	 * @return string
736
	 */
737
	protected function get_taxonomy( $request ) {
738
		// Check if taxonomy is defined.
739
		// Prevents check for attribute taxonomy more than one time for each query.
740
		if ( '' !== $this->taxonomy ) {
741
			return $this->taxonomy;
742
		}
743
744
		if ( ! empty( $request['attribute_id'] ) ) {
745
			$taxonomy = wc_attribute_taxonomy_name_by_id( (int) $request['attribute_id'] );
746
747
			$this->taxonomy = $taxonomy;
748
		}
749
750
		return $this->taxonomy;
751
	}
752
}
753