|
1
|
|
|
<?php |
|
|
|
|
|
|
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 |
|
|
|
|
|
|
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 ) { |
|
|
|
|
|
|
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 ) { |
|
|
|
|
|
|
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 ) { |
|
|
|
|
|
|
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 ) { |
|
|
|
|
|
|
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
|
|
|
|
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.