Completed
Push — master ( f47a1d...37f03f )
by Claudio
28:06
created

WC_API_Resource::delete()   C

Complexity

Conditions 8
Paths 16

Size

Total Lines 36
Code Lines 20

Duplication

Lines 36
Ratio 100 %
Metric Value
dl 36
loc 36
rs 5.3846
nc 16
cc 8
eloc 20
nop 3
1
<?php
1 ignored issue
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 18 and the first side effect is on line 15.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
/**
3
 * WooCommerce API Resource class
4
 *
5
 * Provides shared functionality for resource-specific API classes
6
 *
7
 * @author      WooThemes
8
 * @category    API
9
 * @package     WooCommerce/API
10
 * @since       2.1
11
 * @version     2.1
12
 */
13
14
if ( ! defined( 'ABSPATH' ) ) {
15
	exit; // Exit if accessed directly
16
}
17
18
class WC_API_Resource {
19
20
	/** @var WC_API_Server the API server */
21
	protected $server;
22
23
	/** @var string sub-classes override this to set a resource-specific base route */
24
	protected $base;
25
26
	/**
27
	 * Setup class
28
	 *
29
	 * @since 2.1
30
	 * @param WC_API_Server $server
31
	 * @return WC_API_Resource
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
32
	 */
33
	public function __construct( WC_API_Server $server ) {
34
35
		$this->server = $server;
36
37
		// automatically register routes for sub-classes
38
		add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) );
39
40
		// remove fields from responses when requests specify certain fields
41
		// note these are hooked at a later priority so data added via filters (e.g. customer data to the order response)
42
		// still has the fields filtered properly
43
		foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {
44
45
			add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
46
			add_filter( "woocommerce_api_{$resource}_response", array( $this, 'filter_response_fields' ), 20, 3 );
47
		}
48
	}
49
50
	/**
51
	 * Validate the request by checking:
52
	 *
53
	 * 1) the ID is a valid integer
54
	 * 2) the ID returns a valid post object and matches the provided post type
55
	 * 3) the current user has the proper permissions to read/edit/delete the post
56
	 *
57
	 * @since 2.1
58
	 * @param string|int $id the post ID
59
	 * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product`
60
	 * @param string $context the context of the request, either `read`, `edit` or `delete`
61
	 * @return int|WP_Error valid post ID or WP_Error if any of the checks fails
62
	 */
63
	protected function validate_request( $id, $type, $context ) {
64
65
		if ( 'shop_order' === $type || 'shop_coupon' === $type )
66
			$resource_name = str_replace( 'shop_', '', $type );
67
		else
68
			$resource_name = $type;
69
70
		$id = absint( $id );
71
72
		// validate ID
73
		if ( empty( $id ) )
74
			return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
75
76
		// only custom post types have per-post type/permission checks
77
		if ( 'customer' !== $type ) {
78
79
			$post = get_post( $id );
80
81
			// for checking permissions, product variations are the same as the product post type
82
			$post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type;
83
84
			// validate post type
85
			if ( $type !== $post_type )
86
				return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) );
87
88
			// validate permissions
89
			switch ( $context ) {
90
91
				case 'read':
92
					if ( ! $this->is_readable( $post ) )
93
						return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
94
					break;
95
96
				case 'edit':
97
					if ( ! $this->is_editable( $post ) )
98
						return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
99
					break;
100
101
				case 'delete':
102
					if ( ! $this->is_deletable( $post ) )
103
						return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) );
104
					break;
105
			}
106
		}
107
108
		return $id;
109
	}
110
111
	/**
112
	 * Add common request arguments to argument list before WP_Query is run
113
	 *
114
	 * @since 2.1
115
	 * @param array $base_args required arguments for the query (e.g. `post_type`, etc)
116
	 * @param array $request_args arguments provided in the request
117
	 * @return array
118
	 */
119
	protected function merge_query_args( $base_args, $request_args ) {
120
121
		$args = array();
122
123
		// date
124
		if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) {
125
126
			$args['date_query'] = array();
127
128
			// resources created after specified date
129
			if ( ! empty( $request_args['created_at_min'] ) )
130
				$args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true );
131
132
			// resources created before specified date
133
			if ( ! empty( $request_args['created_at_max'] ) )
134
				$args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true );
135
136
			// resources updated after specified date
137
			if ( ! empty( $request_args['updated_at_min'] ) )
138
				$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true );
139
140
			// resources updated before specified date
141
			if ( ! empty( $request_args['updated_at_max'] ) )
142
				$args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true );
143
		}
144
145
		// search
146
		if ( ! empty( $request_args['q'] ) )
147
			$args['s'] = $request_args['q'];
148
149
		// resources per response
150
		if ( ! empty( $request_args['limit'] ) )
151
			$args['posts_per_page'] = $request_args['limit'];
152
153
		// resource offset
154
		if ( ! empty( $request_args['offset'] ) )
155
			$args['offset'] = $request_args['offset'];
156
157
		// resource page
158
		$args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1;
159
160
		return array_merge( $base_args, $args );
161
	}
162
163
	/**
164
	 * Add meta to resources when requested by the client. Meta is added as a top-level
165
	 * `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs
166
	 *
167
	 * @since 2.1
168
	 * @param array $data the resource data
169
	 * @param object $resource the resource object (e.g WC_Order)
170
	 * @return mixed
171
	 */
172 View Code Duplication
	public function maybe_add_meta( $data, $resource ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
173
174
		if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) {
175
176
			// don't attempt to add meta more than once
177
			if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) )
178
				return $data;
179
180
			// define the top-level property name for the meta
181
			switch ( get_class( $resource ) ) {
182
183
				case 'WC_Order':
184
					$meta_name = 'order_meta';
185
					break;
186
187
				case 'WC_Coupon':
188
					$meta_name = 'coupon_meta';
189
					break;
190
191
				case 'WP_User':
192
					$meta_name = 'customer_meta';
193
					break;
194
195
				default:
196
					$meta_name = 'product_meta';
197
					break;
198
			}
199
200
			if ( is_a( $resource, 'WP_User' ) ) {
201
202
				// customer meta
203
				$meta = (array) get_user_meta( $resource->ID );
204
205
			} elseif ( is_a( $resource, 'WC_Product_Variation' ) ) {
206
207
				// product variation meta
208
				$meta = (array) get_post_meta( $resource->get_variation_id() );
209
210
			} else {
211
212
				// coupon/order/product meta
213
				$meta = (array) get_post_meta( $resource->id );
214
			}
215
216
			foreach( $meta as $meta_key => $meta_value ) {
217
218
				// don't add hidden meta by default
219
				if ( ! is_protected_meta( $meta_key ) ) {
220
					$data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] );
221
				}
222
			}
223
224
		}
225
226
		return $data;
227
	}
228
229
	/**
230
	 * Restrict the fields included in the response if the request specified certain only certain fields should be returned
231
	 *
232
	 * @since 2.1
233
	 * @param array $data the response data
234
	 * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order
235
	 * @param array|string the requested list of fields to include in the response
236
	 * @return array response data
237
	 */
238 View Code Duplication
	public function filter_response_fields( $data, $resource, $fields ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
239
240
		if ( ! is_array( $data ) || empty( $fields ) )
241
			return $data;
242
243
		$fields = explode( ',', $fields );
244
		$sub_fields = array();
245
246
		// get sub fields
247
		foreach ( $fields as $field ) {
248
249
			if ( false !== strpos( $field, '.' ) ) {
250
251
				list( $name, $value ) = explode( '.', $field );
252
253
				$sub_fields[ $name ] = $value;
254
			}
255
		}
256
257
		// iterate through top-level fields
258
		foreach ( $data as $data_field => $data_value ) {
259
260
			// if a field has sub-fields and the top-level field has sub-fields to filter
261
			if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) {
262
263
				// iterate through each sub-field
264
				foreach ( $data_value as $sub_field => $sub_field_value ) {
265
266
					// remove non-matching sub-fields
267
					if ( ! in_array( $sub_field, $sub_fields ) ) {
268
						unset( $data[ $data_field ][ $sub_field ] );
269
					}
270
				}
271
272
			} else {
273
274
				// remove non-matching top-level fields
275
				if ( ! in_array( $data_field, $fields ) ) {
276
					unset( $data[ $data_field ] );
277
				}
278
			}
279
		}
280
281
		return $data;
282
	}
283
284
	/**
285
	 * Delete a given resource
286
	 *
287
	 * @since 2.1
288
	 * @param int $id the resource ID
289
	 * @param string $type the resource post type, or `customer`
290
	 * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`)
291
	 * @return array|WP_Error
292
	 */
293 View Code Duplication
	protected function delete( $id, $type, $force = false ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
294
295
		if ( 'shop_order' === $type || 'shop_coupon' === $type )
296
			$resource_name = str_replace( 'shop_', '', $type );
297
		else
298
			$resource_name = $type;
299
300
		if ( 'customer' === $type ) {
301
302
			$result = wp_delete_user( $id );
303
304
			if ( $result )
305
				return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) );
306
			else
307
				return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) );
308
309
		} else {
310
311
			// delete order/coupon/product
312
313
			$result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id );
314
315
			if ( ! $result )
316
				return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) );
317
318
			if ( $force ) {
319
				return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) );
320
321
			} else {
322
323
				$this->server->send_status( '202' );
324
325
				return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) );
326
			}
327
		}
328
	}
329
330
331
	/**
332
	 * Checks if the given post is readable by the current user
333
	 *
334
	 * @since 2.1
335
	 * @see WC_API_Resource::check_permission()
336
	 * @param WP_Post|int $post
337
	 * @return bool
338
	 */
339
	protected function is_readable( $post ) {
340
341
		return $this->check_permission( $post, 'read' );
342
	}
343
344
	/**
345
	 * Checks if the given post is editable by the current user
346
	 *
347
	 * @since 2.1
348
	 * @see WC_API_Resource::check_permission()
349
	 * @param WP_Post|int $post
350
	 * @return bool
351
	 */
352
	protected function is_editable( $post ) {
353
354
		return $this->check_permission( $post, 'edit' );
355
356
	}
357
358
	/**
359
	 * Checks if the given post is deletable by the current user
360
	 *
361
	 * @since 2.1
362
	 * @see WC_API_Resource::check_permission()
363
	 * @param WP_Post|int $post
364
	 * @return bool
365
	 */
366
	protected function is_deletable( $post ) {
367
368
		return $this->check_permission( $post, 'delete' );
369
	}
370
371
	/**
372
	 * Checks the permissions for the current user given a post and context
373
	 *
374
	 * @since 2.1
375
	 * @param WP_Post|int $post
376
	 * @param string $context the type of permission to check, either `read`, `write`, or `delete`
377
	 * @return bool true if the current user has the permissions to perform the context on the post
378
	 */
379 View Code Duplication
	private function check_permission( $post, $context ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
380
381
		if ( ! is_a( $post, 'WP_Post' ) )
382
			$post = get_post( $post );
383
384
		if ( is_null( $post ) )
385
			return false;
386
387
		$post_type = get_post_type_object( $post->post_type );
388
389
		if ( 'read' === $context )
390
			return current_user_can( $post_type->cap->read_private_posts, $post->ID );
391
392
		elseif ( 'edit' === $context )
393
			return current_user_can( $post_type->cap->edit_post, $post->ID );
394
395
		elseif ( 'delete' === $context )
396
			return current_user_can( $post_type->cap->delete_post, $post->ID );
397
398
		else
399
			return false;
400
	}
401
402
}
403