Completed
Push — master ( 7f2ea5...5630d2 )
by Mike
09:46
created

ProductVariations::prepare_object_for_response()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 2
dl 0
loc 19
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * REST API variations controller
4
 *
5
 * Handles requests to the /products/<product_id>/variations endpoints.
6
 *
7
 * @package WooCommerce/RestApi
8
 */
9
10
namespace WooCommerce\RestApi\Controllers\Version4;
11
12
defined( 'ABSPATH' ) || exit;
13
14
use WooCommerce\RestApi\Controllers\Version4\Schema\ProductVariationSchema;
15
16
/**
17
 * REST API variations controller class.
18
 */
19
class ProductVariations extends Products {
20
21
	/**
22
	 * Route base.
23
	 *
24
	 * @var string
25
	 */
26
	protected $rest_base = 'products/(?P<product_id>[\d]+)/variations';
27
28
	/**
29
	 * Post type.
30
	 *
31
	 * @var string
32
	 */
33
	protected $post_type = 'product_variation';
34
35
	/**
36
	 * Register the routes for products.
37
	 */
38
	public function register_routes() {
39
		register_rest_route(
40
			$this->namespace,
41
			'/' . $this->rest_base,
42
			array(
43
				'args'   => array(
44
					'product_id' => array(
45
						'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
46
						'type'        => 'integer',
47
					),
48
				),
49
				array(
50
					'methods'             => \WP_REST_Server::READABLE,
51
					'callback'            => array( $this, 'get_items' ),
52
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
53
					'args'                => $this->get_collection_params(),
54
				),
55
				array(
56
					'methods'             => \WP_REST_Server::CREATABLE,
57
					'callback'            => array( $this, 'create_item' ),
58
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
59
					'args'                => $this->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ),
60
				),
61
				'schema' => array( $this, 'get_public_item_schema' ),
62
			),
63
			true
64
		);
65
		register_rest_route(
66
			$this->namespace,
67
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
68
			array(
69
				'args'   => array(
70
					'product_id' => array(
71
						'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
72
						'type'        => 'integer',
73
					),
74
					'id'         => array(
75
						'description' => __( 'Unique identifier for the variation.', 'woocommerce' ),
76
						'type'        => 'integer',
77
					),
78
				),
79
				array(
80
					'methods'             => \WP_REST_Server::READABLE,
81
					'callback'            => array( $this, 'get_item' ),
82
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
83
					'args'                => array(
84
						'context' => $this->get_context_param(
85
							array(
86
								'default' => 'view',
87
							)
88
						),
89
					),
90
				),
91
				array(
92
					'methods'             => \WP_REST_Server::EDITABLE,
93
					'callback'            => array( $this, 'update_item' ),
94
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
95
					'args'                => $this->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ),
96
				),
97
				array(
98
					'methods'             => \WP_REST_Server::DELETABLE,
99
					'callback'            => array( $this, 'delete_item' ),
100
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
101
					'args'                => array(
102
						'force' => array(
103
							'default'     => false,
104
							'type'        => 'boolean',
105
							'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
106
						),
107
					),
108
				),
109
				'schema' => array( $this, 'get_public_item_schema' ),
110
			),
111
			true
112
		);
113
		register_rest_route(
114
			$this->namespace,
115
			'/' . $this->rest_base . '/batch',
116
			array(
117
				'args'   => array(
118
					'product_id' => array(
119
						'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
120
						'type'        => 'integer',
121
					),
122
				),
123
				array(
124
					'methods'             => \WP_REST_Server::EDITABLE,
125
					'callback'            => array( $this, 'batch_items' ),
126
					'permission_callback' => array( $this, 'batch_items_permissions_check' ),
127
					'args'                => $this->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ),
128
				),
129
				'schema' => array( $this, 'get_public_batch_schema' ),
130
			),
131
			true
132
		);
133
	}
134
135
	/**
136
	 * Get the Variation's schema, conforming to JSON Schema.
137
	 *
138
	 * @return array
139
	 */
140
	public function get_item_schema() {
141
		$schema = ProductVariationSchema::get_schema();
142
		return $this->add_additional_fields_schema( $schema );
143
	}
144
145
	/**
146
	 * Get the query params for collections of attachments.
147
	 *
148
	 * @return array
149
	 */
150
	public function get_collection_params() {
151
		$params = parent::get_collection_params();
152
153
		unset(
154
			$params['in_stock'],
155
			$params['type'],
156
			$params['featured'],
157
			$params['category'],
158
			$params['tag'],
159
			$params['shipping_class'],
160
			$params['attribute'],
161
			$params['attribute_term']
162
		);
163
164
		$params['stock_status'] = array(
165
			'description'       => __( 'Limit result set to products with specified stock status.', 'woocommerce' ),
166
			'type'              => 'string',
167
			'enum'              => array_keys( wc_get_product_stock_status_options() ),
168
			'sanitize_callback' => 'sanitize_text_field',
169
			'validate_callback' => 'rest_validate_request_arg',
170
		);
171
172
		$params['search'] = array(
173
			'description'       => __( 'Search by similar product name or sku.', 'woocommerce' ),
174
			'type'              => 'string',
175
			'validate_callback' => 'rest_validate_request_arg',
176
		);
177
178
		return $params;
179
	}
180
181
	/**
182
	 * Get object.
183
	 *
184
	 * @since  3.0.0
185
	 * @param  int $id Object ID.
186
	 * @return \WC_Data|bool
187
	 */
188
	protected function get_object( $id ) {
189
		return wc_get_product( $id );
190
	}
191
192
	/**
193
	 * Check if a given request has access to update an item.
194
	 *
195
	 * @param  \WP_REST_Request $request Full details about the request.
196
	 * @return \WP_Error|boolean
197
	 */
198
	public function update_item_permissions_check( $request ) {
199
		$object = $this->get_object( (int) $request['id'] );
200
201
		if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) {
202
			return new \WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
203
		}
204
205
		// Check if variation belongs to the correct parent product.
206
		if ( $object && 0 !== $object->get_parent_id() && absint( $request['product_id'] ) !== $object->get_parent_id() ) {
207
			return new \WP_Error( 'woocommerce_rest_cannot_edit', __( 'Parent product does not match current variation.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
208
		}
209
210
		return true;
211
	}
212
213
	/**
214
	 * Prepare a single variation output for response.
215
	 *
216
	 * @param  \WC_Data         $object  Object data.
217
	 * @param  \WP_REST_Request $request Request object.
218
	 * @return \WP_REST_Response
219
	 */
220
	public function prepare_object_for_response( $object, $request ) {
221
		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
222
		$data     = ProductVariationSchema::object_to_schema( $object, $context );
223
		$data     = $this->add_additional_fields_to_object( $data, $request );
224
		$data     = $this->filter_response_by_context( $data, $context );
225
		$response = rest_ensure_response( $data );
226
		$response->add_links( $this->prepare_links( $object, $request ) );
227
228
		/**
229
		 * Filter the data for a response.
230
		 *
231
		 * The dynamic portion of the hook name, $this->post_type,
232
		 * refers to object type being prepared for the response.
233
		 *
234
		 * @param \WP_REST_Response $response The response object.
235
		 * @param \WC_Data          $object   Object data.
236
		 * @param \WP_REST_Request  $request  Request object.
237
		 */
238
		return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request );
239
	}
240
241
	/**
242
	 * Prepare objects query.
243
	 *
244
	 * @since  3.0.0
245
	 * @param  \WP_REST_Request $request Full details about the request.
246
	 * @return array
247
	 */
248
	protected function prepare_objects_query( $request ) {
249
		$args = parent::prepare_objects_query( $request );
250
251
		// Set post_status.
252
		$args['post_status'] = $request['status'];
253
254
		// Set custom args to handle later during clauses.
255
		$custom_keys = array(
256
			'sku',
257
			'min_price',
258
			'max_price',
259
			'stock_status',
260
			'low_in_stock',
261
		);
262
		foreach ( $custom_keys as $key ) {
263
			if ( ! empty( $request[ $key ] ) ) {
264
				$args[ $key ] = $request[ $key ];
265
			}
266
		}
267
268
		// Filter by tax class.
269
		if ( ! empty( $request['tax_class'] ) ) {
270
			$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
271
				$args,
272
				array(
273
					'key'   => '_tax_class',
274
					'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '',
275
				)
276
			);
277
		}
278
279
		// Filter by on sale products.
280
		if ( is_bool( $request['on_sale'] ) ) {
281
			$on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in';
282
			$on_sale_ids = wc_get_product_ids_on_sale();
283
284
			// Use 0 when there's no on sale products to avoid return all products.
285
			$on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids;
286
287
			$args[ $on_sale_key ] += $on_sale_ids;
288
		}
289
290
		// Force the post_type argument, since it's not a user input variable.
291
		if ( ! empty( $request['sku'] ) ) {
292
			$args['post_type'] = array( 'product', 'product_variation' );
293
		} else {
294
			$args['post_type'] = $this->post_type;
295
		}
296
297
		$args['post_parent'] = $request['product_id'];
298
299
		if ( ! empty( $request['search'] ) ) {
300
			$args['search'] = $request['search'];
301
			unset( $args['s'] );
302
		}
303
304
		return $args;
305
	}
306
307
	/**
308
	 * Prepare a single variation for create or update.
309
	 *
310
	 * @param  \WP_REST_Request $request Request object.
311
	 * @param  bool             $creating If is creating a new object.
312
	 * @return \WP_Error|\WC_Data
313
	 */
314
	protected function prepare_object_for_database( $request, $creating = false ) {
315
		$variation = ProductVariationSchema::schema_to_object( $request );
316
317
		/**
318
		 * Filters an object before it is inserted via the REST API.
319
		 *
320
		 * The dynamic portion of the hook name, `$this->post_type`,
321
		 * refers to the object type slug.
322
		 *
323
		 * @param \WC_Data         $variation Object object.
324
		 * @param \WP_REST_Request $request   Request object.
325
		 * @param bool            $creating  If is creating a new object.
326
		 */
327
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating );
328
	}
329
330
331
	/**
332
	 * Delete a variation.
333
	 *
334
	 * @param \WP_REST_Request $request Full details about the request.
335
	 *
336
	 * @return bool|\WP_Error|\WP_REST_Response
337
	 */
338
	public function delete_item( $request ) {
339
		$force  = (bool) $request['force'];
340
		$object = $this->get_object( (int) $request['id'] );
341
		$result = false;
342
343
		if ( ! $object || 0 === $object->get_id() ) {
0 ignored issues
show
introduced by
$object is of type WC_Product, thus it always evaluated to true.
Loading history...
344
			return new \WP_Error(
345
				"woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array(
346
					'status' => 404,
347
				)
348
			);
349
		}
350
351
		$supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) );
352
353
		/**
354
		 * Filter whether an object is trashable.
355
		 *
356
		 * Return false to disable trash support for the object.
357
		 *
358
		 * @param boolean  $supports_trash Whether the object type support trashing.
359
		 * @param \WC_Data $object         The object being considered for trashing support.
360
		 */
361
		$supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object );
362
363
		if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) {
364
			return new \WP_Error(
365
				/* translators: %s: post type */
366
				"woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array(
367
					'status' => rest_authorization_required_code(),
368
				)
369
			);
370
		}
371
372
		$request->set_param( 'context', 'edit' );
373
374
		// If we're forcing, then delete permanently.
375
		if ( $force ) {
376
			$previous = $this->prepare_object_for_response( $object, $request );
377
378
			$object->delete( true );
379
380
			$result   = 0 === $object->get_id();
381
			$response = new \WP_REST_Response();
382
			$response->set_data(
383
				array(
384
					'deleted'  => true,
385
					'previous' => $previous->get_data(),
386
				)
387
			);
388
		} else {
389
			// If we don't support trashing for this type, error out.
390
			if ( ! $supports_trash ) {
391
				return new \WP_Error(
392
					/* translators: %s: post type */
393
					'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array(
394
						'status' => 501,
395
					)
396
				);
397
			}
398
399
			// Otherwise, only trash if we haven't already.
400
			if ( is_callable( array( $object, 'get_status' ) ) ) {
401
				if ( 'trash' === $object->get_status() ) {
402
					return new \WP_Error(
403
						/* translators: %s: post type */
404
						'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array(
405
							'status' => 410,
406
						)
407
					);
408
				}
409
410
				$object->delete();
411
				$result = 'trash' === $object->get_status();
412
			}
413
414
			$response = $this->prepare_object_for_response( $object, $request );
415
		}
416
417
		if ( ! $result ) {
418
			return new \WP_Error(
419
				/* translators: %s: post type */
420
				'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array(
421
					'status' => 500,
422
				)
423
			);
424
		}
425
426
		/**
427
		 * Fires after a single object is deleted or trashed via the REST API.
428
		 *
429
		 * @param \WC_Data          $object   The deleted or trashed object.
430
		 * @param \WP_REST_Response $response The response data.
431
		 * @param \WP_REST_Request  $request  The request sent to the API.
432
		 */
433
		do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request );
434
435
		return $response;
436
	}
437
438
	/**
439
	 * Get batch of items from requst.
440
	 *
441
	 * @param \WP_REST_Request $request Full details about the request.
442
	 * @param string           $batch_type Batch type; one of create, update, delete.
443
	 * @return array
444
	 */
445
	protected function get_batch_of_items_from_request( $request, $batch_type ) {
446
		$params     = $request->get_params();
447
		$url_params = $request->get_url_params();
448
		$product_id = $url_params['product_id'];
449
450
		if ( ! isset( $params[ $batch_type ] ) ) {
451
			return array();
452
		}
453
454
		$items = array_filter( $params[ $batch_type ] );
455
456
		if ( 'update' === $batch_type || 'create' === $batch_type ) {
457
			foreach ( $items as $key => $item ) {
458
				$items[ $key ] = array_merge(
459
					array(
460
						'product_id' => $product_id,
461
					),
462
					$item
463
				);
464
			}
465
		}
466
467
		return array_filter( $items );
468
	}
469
470
	/**
471
	 * Prepare links for the request.
472
	 *
473
	 * @param \WC_Data         $object  Object data.
474
	 * @param \WP_REST_Request $request Request object.
475
	 * @return array                   Links for the given post.
476
	 */
477
	protected function prepare_links( $object, $request ) {
478
		$product_id = (int) $request['product_id'];
479
		$base       = str_replace( '(?P<product_id>[\d]+)', $product_id, $this->rest_base );
480
		$links      = array(
481
			'self'       => array(
482
				'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $object->get_id() ) ),
483
			),
484
			'collection' => array(
485
				'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
486
			),
487
			'up'         => array(
488
				'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ),
489
			),
490
		);
491
		return $links;
492
	}
493
}
494