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

ProductAttributes   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 490
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 215
dl 0
loc 490
rs 9.6
c 0
b 0
f 0
wmc 35

14 Methods

Rating   Name   Duplication   Size   Complexity  
A flush_rewrite_rules() 0 2 1
A delete_item() 0 40 5
A prepare_item_for_response() 0 28 2
A register_routes() 0 73 1
A get_item() 0 10 2
A validate_attribute_slug() 0 13 5
A create_item() 0 42 5
A get_attribute() 0 19 3
A get_items() 0 10 2
A prepare_links() 0 12 1
A get_taxonomy() 0 12 3
A get_item_schema() 0 52 1
A get_collection_params() 0 5 1
A update_item() 0 41 3
1
<?php
2
/**
3
 * REST API Product Attributes controller
4
 *
5
 * Handles requests to the products/attributes endpoint.
6
 *
7
 * @package WooCommerce/RestApi
8
 */
9
10
namespace WooCommerce\RestApi\Controllers\Version4;
11
12
defined( 'ABSPATH' ) || exit;
13
14
/**
15
 * REST API Product Attributes controller class.
16
 */
17
class ProductAttributes extends AbstractController {
18
19
	/**
20
	 * Route base.
21
	 *
22
	 * @var string
23
	 */
24
	protected $rest_base = 'products/attributes';
25
26
	/**
27
	 * Permission to check.
28
	 *
29
	 * @var string
30
	 */
31
	protected $resource_type = 'attributes';
32
33
	/**
34
	 * Attribute name.
35
	 *
36
	 * @var string
37
	 */
38
	protected $attribute = '';
39
40
	/**
41
	 * Register the routes for product attributes.
42
	 */
43
	public function register_routes() {
44
		register_rest_route(
45
			$this->namespace,
46
			'/' . $this->rest_base,
47
			array(
48
				array(
49
					'methods'             => \WP_REST_Server::READABLE,
50
					'callback'            => array( $this, 'get_items' ),
51
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
52
					'args'                => $this->get_collection_params(),
53
				),
54
				array(
55
					'methods'             => \WP_REST_Server::CREATABLE,
56
					'callback'            => array( $this, 'create_item' ),
57
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
58
					'args'                => array_merge(
59
						$this->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ),
60
						array(
61
							'name' => array(
62
								'description' => __( 'Name for the resource.', 'woocommerce' ),
63
								'type'        => 'string',
64
								'required'    => true,
65
							),
66
						)
67
					),
68
				),
69
				'schema' => array( $this, 'get_public_item_schema' ),
70
			),
71
			true
72
		);
73
74
		register_rest_route(
75
			$this->namespace,
76
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
77
			array(
78
				'args' => array(
79
					'id' => array(
80
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
81
						'type'        => 'integer',
82
					),
83
				),
84
				array(
85
					'methods'             => \WP_REST_Server::READABLE,
86
					'callback'            => array( $this, 'get_item' ),
87
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
88
					'args'                => array(
89
						'context'         => $this->get_context_param( array( 'default' => 'view' ) ),
90
					),
91
				),
92
				array(
93
					'methods'             => \WP_REST_Server::EDITABLE,
94
					'callback'            => array( $this, 'update_item' ),
95
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
96
					'args'                => $this->get_endpoint_args_for_item_schema( \WP_REST_Server::EDITABLE ),
97
				),
98
				array(
99
					'methods'             => \WP_REST_Server::DELETABLE,
100
					'callback'            => array( $this, 'delete_item' ),
101
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
102
					'args'                => array(
103
						'force' => array(
104
							'default'     => true,
105
							'type'        => 'boolean',
106
							'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
107
						),
108
					),
109
				),
110
				'schema' => array( $this, 'get_public_item_schema' ),
111
			),
112
			true
113
		);
114
115
		$this->register_batch_route();
116
	}
117
118
	/**
119
	 * Get all attributes.
120
	 *
121
	 * @param \WP_REST_Request $request Request params.
122
	 * @return array
123
	 */
124
	public function get_items( $request ) {
125
		$attributes = wc_get_attribute_taxonomies();
126
		$data       = array();
127
		foreach ( $attributes as $attribute_obj ) {
128
			$attribute = $this->prepare_item_for_response( $attribute_obj, $request );
129
			$attribute = $this->prepare_response_for_collection( $attribute );
130
			$data[]    = $attribute;
131
		}
132
133
		return rest_ensure_response( $data );
134
	}
135
136
	/**
137
	 * Create a single attribute.
138
	 *
139
	 * @param \WP_REST_Request $request Full details about the request.
140
	 * @return \WP_REST_Request|\WP_Error
141
	 */
142
	public function create_item( $request ) {
143
		global $wpdb;
144
145
		$id = wc_create_attribute(
146
			array(
147
				'name'         => $request['name'],
148
				'slug'         => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ),
149
				'type'         => ! empty( $request['type'] ) ? $request['type'] : 'select',
150
				'order_by'     => ! empty( $request['order_by'] ) ? $request['order_by'] : 'menu_order',
151
				'has_archives' => true === $request['has_archives'],
152
			)
153
		);
154
155
		// Checks for errors.
156
		if ( is_wp_error( $id ) ) {
157
			return new \WP_Error( 'woocommerce_rest_cannot_create', $id->get_error_message(), array( 'status' => 400 ) );
158
		}
159
160
		$attribute = $this->get_attribute( $id );
0 ignored issues
show
Bug introduced by
It seems like $id can also be of type WP_Error; however, parameter $id of WooCommerce\RestApi\Cont...ibutes::get_attribute() does only seem to accept integer, 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

160
		$attribute = $this->get_attribute( /** @scrutinizer ignore-type */ $id );
Loading history...
161
162
		if ( is_wp_error( $attribute ) ) {
163
			return $attribute;
164
		}
165
166
		$this->update_additional_fields_for_object( $attribute, $request );
0 ignored issues
show
Bug introduced by
$attribute of type WP_Error|WooCommerce\Res...llers\Version4\stdClass is incompatible with the type array expected by parameter $object of WP_REST_Controller::upda...nal_fields_for_object(). ( Ignorable by Annotation )

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

166
		$this->update_additional_fields_for_object( /** @scrutinizer ignore-type */ $attribute, $request );
Loading history...
167
168
		/**
169
		 * Fires after a single product attribute is created or updated via the REST API.
170
		 *
171
		 * @param stdObject       $attribute Inserted attribute object.
172
		 * @param \WP_REST_Request $request   Request object.
173
		 * @param boolean         $creating  True when creating attribute, false when updating.
174
		 */
175
		do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, true );
176
177
		$request->set_param( 'context', 'edit' );
178
		$response = $this->prepare_item_for_response( $attribute, $request );
179
		$response = rest_ensure_response( $response );
180
		$response->set_status( 201 );
181
		$response->header( 'Location', rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/' . $attribute->attribute_id ) );
0 ignored issues
show
Bug introduced by
The property attribute_id does not seem to exist on WP_Error.
Loading history...
182
183
		return $response;
184
	}
185
186
	/**
187
	 * Get a single attribute.
188
	 *
189
	 * @param \WP_REST_Request $request Full details about the request.
190
	 * @return \WP_REST_Request|\WP_Error
191
	 */
192
	public function get_item( $request ) {
193
		$attribute = $this->get_attribute( (int) $request['id'] );
194
195
		if ( is_wp_error( $attribute ) ) {
196
			return $attribute;
197
		}
198
199
		$response = $this->prepare_item_for_response( $attribute, $request );
200
201
		return rest_ensure_response( $response );
202
	}
203
204
	/**
205
	 * Update a single term from a taxonomy.
206
	 *
207
	 * @param \WP_REST_Request $request Full details about the request.
208
	 * @return \WP_REST_Request|\WP_Error
209
	 */
210
	public function update_item( $request ) {
211
		global $wpdb;
212
213
		$id     = (int) $request['id'];
214
		$edited = wc_update_attribute(
215
			$id,
216
			array(
217
				'name'         => $request['name'],
218
				'slug'         => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ),
219
				'type'         => $request['type'],
220
				'order_by'     => $request['order_by'],
221
				'has_archives' => $request['has_archives'],
222
			)
223
		);
224
225
		// Checks for errors.
226
		if ( is_wp_error( $edited ) ) {
227
			return new \WP_Error( 'woocommerce_rest_cannot_edit', $edited->get_error_message(), array( 'status' => 400 ) );
228
		}
229
230
		$attribute = $this->get_attribute( $id );
231
232
		if ( is_wp_error( $attribute ) ) {
233
			return $attribute;
234
		}
235
236
		$this->update_additional_fields_for_object( $attribute, $request );
0 ignored issues
show
Bug introduced by
$attribute of type WP_Error|WooCommerce\Res...llers\Version4\stdClass is incompatible with the type array expected by parameter $object of WP_REST_Controller::upda...nal_fields_for_object(). ( Ignorable by Annotation )

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

236
		$this->update_additional_fields_for_object( /** @scrutinizer ignore-type */ $attribute, $request );
Loading history...
237
238
		/**
239
		 * Fires after a single product attribute is created or updated via the REST API.
240
		 *
241
		 * @param stdObject       $attribute Inserted attribute object.
242
		 * @param \WP_REST_Request $request   Request object.
243
		 * @param boolean         $creating  True when creating attribute, false when updating.
244
		 */
245
		do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, false );
246
247
		$request->set_param( 'context', 'edit' );
248
		$response = $this->prepare_item_for_response( $attribute, $request );
249
250
		return rest_ensure_response( $response );
251
	}
252
253
	/**
254
	 * Delete a single attribute.
255
	 *
256
	 * @param \WP_REST_Request $request Full details about the request.
257
	 * @return \WP_REST_Response|\WP_Error
258
	 */
259
	public function delete_item( $request ) {
260
		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
261
262
		// We don't support trashing for this type, error out.
263
		if ( ! $force ) {
264
			return new \WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
265
		}
266
267
		$attribute = $this->get_attribute( (int) $request['id'] );
268
269
		if ( is_wp_error( $attribute ) ) {
270
			return $attribute;
271
		}
272
273
		$request->set_param( 'context', 'edit' );
274
		$previous = $this->prepare_item_for_response( $attribute, $request );
275
		$deleted  = wc_delete_attribute( $attribute->attribute_id );
0 ignored issues
show
Bug introduced by
The property attribute_id does not seem to exist on WP_Error.
Loading history...
276
277
		if ( false === $deleted ) {
278
			return new \WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
279
		}
280
281
		$response = new \WP_REST_Response();
282
		$response->set_data(
283
			array(
284
				'deleted'  => true,
285
				'previous' => $previous->get_data(),
286
			)
287
		);
288
289
		/**
290
		 * Fires after a single attribute is deleted via the REST API.
291
		 *
292
		 * @param stdObject        $attribute     The deleted attribute.
293
		 * @param \WP_REST_Response $response The response data.
294
		 * @param \WP_REST_Request  $request  The request sent to the API.
295
		 */
296
		do_action( 'woocommerce_rest_delete_product_attribute', $attribute, $response, $request );
297
298
		return $response;
299
	}
300
301
	/**
302
	 * Prepare a single product attribute output for response.
303
	 *
304
	 * @param obj             $item Term object.
0 ignored issues
show
Bug introduced by
The type WooCommerce\RestApi\Controllers\Version4\obj was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
305
	 * @param \WP_REST_Request $request Request params.
306
	 * @return \WP_REST_Response $response
307
	 */
308
	public function prepare_item_for_response( $item, $request ) {
309
		$data = array(
310
			'id'           => (int) $item->attribute_id,
311
			'name'         => $item->attribute_label,
312
			'slug'         => wc_attribute_taxonomy_name( $item->attribute_name ),
313
			'type'         => $item->attribute_type,
314
			'order_by'     => $item->attribute_orderby,
315
			'has_archives' => (bool) $item->attribute_public,
316
		);
317
318
		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
319
		$data    = $this->add_additional_fields_to_object( $data, $request );
320
		$data    = $this->filter_response_by_context( $data, $context );
321
322
		$response = rest_ensure_response( $data );
323
324
		$response->add_links( $this->prepare_links( $item ) );
325
326
		/**
327
		 * Filter a attribute item returned from the API.
328
		 *
329
		 * Allows modification of the product attribute data right before it is returned.
330
		 *
331
		 * @param \WP_REST_Response  $response  The response object.
332
		 * @param object            $item      The original attribute object.
333
		 * @param \WP_REST_Request   $request   Request used to generate the response.
334
		 */
335
		return apply_filters( 'woocommerce_rest_prepare_product_attribute', $response, $item, $request );
336
	}
337
338
	/**
339
	 * Prepare links for the request.
340
	 *
341
	 * @param object $attribute Attribute object.
342
	 * @return array Links for the given attribute.
343
	 */
344
	protected function prepare_links( $attribute ) {
345
		$base  = '/' . $this->namespace . '/' . $this->rest_base;
346
		$links = array(
347
			'self' => array(
348
				'href' => rest_url( trailingslashit( $base ) . $attribute->attribute_id ),
349
			),
350
			'collection' => array(
351
				'href' => rest_url( $base ),
352
			),
353
		);
354
355
		return $links;
356
	}
357
358
	/**
359
	 * Get the Attribute's schema, conforming to JSON Schema.
360
	 *
361
	 * @return array
362
	 */
363
	public function get_item_schema() {
364
		$schema = array(
365
			'$schema'              => 'http://json-schema.org/draft-04/schema#',
366
			'title'                => 'product_attribute',
367
			'type'                 => 'object',
368
			'properties'           => array(
369
				'id' => array(
370
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
371
					'type'        => 'integer',
372
					'context'     => array( 'view', 'edit' ),
373
					'readonly'    => true,
374
				),
375
				'name' => array(
376
					'description' => __( 'Attribute name.', 'woocommerce' ),
377
					'type'        => 'string',
378
					'context'     => array( 'view', 'edit' ),
379
					'arg_options' => array(
380
						'sanitize_callback' => 'sanitize_text_field',
381
					),
382
				),
383
				'slug' => array(
384
					'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
385
					'type'        => 'string',
386
					'context'     => array( 'view', 'edit' ),
387
					'arg_options' => array(
388
						'sanitize_callback' => 'sanitize_title',
389
					),
390
				),
391
				'type' => array(
392
					'description' => __( 'Type of attribute.', 'woocommerce' ),
393
					'type'        => 'string',
394
					'default'     => 'select',
395
					'enum'        => array_keys( wc_get_attribute_types() ),
396
					'context'     => array( 'view', 'edit' ),
397
				),
398
				'order_by' => array(
399
					'description' => __( 'Default sort order.', 'woocommerce' ),
400
					'type'        => 'string',
401
					'default'     => 'menu_order',
402
					'enum'        => array( 'menu_order', 'name', 'name_num', 'id' ),
403
					'context'     => array( 'view', 'edit' ),
404
				),
405
				'has_archives' => array(
406
					'description' => __( 'Enable/Disable attribute archives.', 'woocommerce' ),
407
					'type'        => 'boolean',
408
					'default'     => false,
409
					'context'     => array( 'view', 'edit' ),
410
				),
411
			),
412
		);
413
414
		return $this->add_additional_fields_schema( $schema );
415
	}
416
417
	/**
418
	 * Get the query params for collections
419
	 *
420
	 * @return array
421
	 */
422
	public function get_collection_params() {
423
		$params = array();
424
		$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
425
426
		return $params;
427
	}
428
429
	/**
430
	 * Get attribute name.
431
	 *
432
	 * @param \WP_REST_Request $request Full details about the request.
433
	 * @return string
434
	 */
435
	protected function get_taxonomy( $request ) {
436
		if ( '' !== $this->attribute ) {
437
			return $this->attribute;
438
		}
439
440
		if ( $request['id'] ) {
441
			$name = wc_attribute_taxonomy_name_by_id( (int) $request['id'] );
442
443
			$this->attribute = $name;
444
		}
445
446
		return $this->attribute;
447
	}
448
449
	/**
450
	 * Get attribute data.
451
	 *
452
	 * @param int $id Attribute ID.
453
	 * @return stdClass|\WP_Error
0 ignored issues
show
Bug introduced by
The type WooCommerce\RestApi\Controllers\Version4\stdClass was not found. Did you mean stdClass? If so, make sure to prefix the type with \.
Loading history...
454
	 */
455
	protected function get_attribute( $id ) {
456
		global $wpdb;
457
458
		$attribute = $wpdb->get_row(
459
			$wpdb->prepare(
460
				"
461
				SELECT *
462
				FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
463
				WHERE attribute_id = %d
464
				",
465
				$id
466
			)
467
		);
468
469
		if ( is_wp_error( $attribute ) || is_null( $attribute ) ) {
470
			return new \WP_Error( 'woocommerce_rest_attribute_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
471
		}
472
473
		return $attribute;
474
	}
475
476
	/**
477
	 * Validate attribute slug.
478
	 *
479
	 * @deprecated 3.2.0
480
	 * @param string $slug Slug being set.
481
	 * @param bool   $new_data Is the data new or old.
482
	 * @return bool|\WP_Error
483
	 */
484
	protected function validate_attribute_slug( $slug, $new_data = true ) {
485
		if ( strlen( $slug ) >= 28 ) {
486
			/* Translators: %s slug. */
487
			return new \WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
488
		} elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) {
489
			/* Translators: %s slug. */
490
			return new \WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
491
		} elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) {
492
			/* Translators: %s slug. */
493
			return new \WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
494
		}
495
496
		return true;
497
	}
498
499
	/**
500
	 * Schedule to flush rewrite rules.
501
	 *
502
	 * @deprecated 3.2.0
503
	 * @since 3.0.0
504
	 */
505
	protected function flush_rewrite_rules() {
506
		wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' );
507
	}
508
}
509