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  */
 12 
 13 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
 14 
 15 class WC_API_Resource {
 16 
 17     /** @var WC_API_Server the API server */
 18     protected $server;
 19 
 20     /** @var string sub-classes override this to set a resource-specific base route */
 21     protected $base;
 22 
 23     /**
 24      * Setup class
 25      *
 26      * @since 2.1
 27      * @param WC_API_Server $server
 28      * @return WC_API_Resource
 29      */
 30     public function __construct( WC_API_Server $server ) {
 31 
 32         $this->server = $server;
 33 
 34         // automatically register routes for sub-classes
 35         add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) );
 36 
 37         // remove fields from responses when requests specify certain fields
 38         // note these are hooked at a later priority so data added via filters (e.g. customer data to the order response)
 39         // still has the fields filtered properly
 40         foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) {
 41 
 42             add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 );
 43             add_filter( "woocommerce_api_{$resource}_response", array( $this, 'filter_response_fields' ), 20, 3 );
 44         }
 45     }
 46 
 47     /**
 48      * Validate the request by checking:
 49      *
 50      * 1) the ID is a valid integer
 51      * 2) the ID returns a valid post object and matches the provided post type
 52      * 3) the current user has the proper permissions to read/edit/delete the post
 53      *
 54      * @since 2.1
 55      * @param string|int $id the post ID
 56      * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product`
 57      * @param string $context the context of the request, either `read`, `edit` or `delete`
 58      * @return int|WP_Error valid post ID or WP_Error if any of the checks fails
 59      */
 60     protected function validate_request( $id, $type, $context ) {
 61 
 62         if ( 'shop_order' === $type || 'shop_coupon' === $type )
 63             $resource_name = str_replace( 'shop_', '', $type );
 64         else
 65             $resource_name = $type;
 66 
 67         $id = absint( $id );
 68 
 69         // validate ID
 70         if ( empty( $id ) )
 71             return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) );
 72 
 73         // only custom post types have per-post type/permission checks
 74         if ( 'customer' !== $type ) {
 75 
 76             $post = get_post( $id );
 77 
 78             // for checking permissions, product variations are the same as the product post type
 79             $post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type;
 80 
 81             // validate post type
 82             if ( $type !== $post_type )
 83                 return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) );
 84 
 85             // validate permissions
 86             switch ( $context ) {
 87 
 88                 case 'read':
 89                     if ( ! $this->is_readable( $post ) )
 90                         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 ) );
 91                     break;
 92 
 93                 case 'edit':
 94                     if ( ! $this->is_editable( $post ) )
 95                         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 ) );
 96                     break;
 97 
 98                 case 'delete':
 99                     if ( ! $this->is_deletable( $post ) )
100                         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 ) );
101                     break;
102             }
103         }
104 
105         return $id;
106     }
107 
108     /**
109      * Add common request arguments to argument list before WP_Query is run
110      *
111      * @since 2.1
112      * @param array $base_args required arguments for the query (e.g. `post_type`, etc)
113      * @param array $request_args arguments provided in the request
114      * @return array
115      */
116     protected function merge_query_args( $base_args, $request_args ) {
117 
118         $args = array();
119 
120         // date
121         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'] ) ) {
122 
123             $args['date_query'] = array();
124 
125             // resources created after specified date
126             if ( ! empty( $request_args['created_at_min'] ) )
127                 $args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true );
128 
129             // resources created before specified date
130             if ( ! empty( $request_args['created_at_max'] ) )
131                 $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true );
132 
133             // resources updated after specified date
134             if ( ! empty( $request_args['updated_at_min'] ) )
135                 $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true );
136 
137             // resources updated before specified date
138             if ( ! empty( $request_args['updated_at_max'] ) )
139                 $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true );
140         }
141 
142         // search
143         if ( ! empty( $request_args['q'] ) )
144             $args['s'] = $request_args['q'];
145 
146         // resources per response
147         if ( ! empty( $request_args['limit'] ) )
148             $args['posts_per_page'] = $request_args['limit'];
149 
150         // resource offset
151         if ( ! empty( $request_args['offset'] ) )
152             $args['offset'] = $request_args['offset'];
153 
154         // resource page
155         $args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1;
156 
157         return array_merge( $base_args, $args );
158     }
159 
160     /**
161      * Add meta to resources when requested by the client. Meta is added as a top-level
162      * `<resource_name>_meta` attribute (e.g. `order_meta`) as a list of key/value pairs
163      *
164      * @since 2.1
165      * @param array $data the resource data
166      * @param object $resource the resource object (e.g WC_Order)
167      * @return mixed
168      */
169     public function maybe_add_meta( $data, $resource ) {
170 
171         if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) {
172 
173             // don't attempt to add meta more than once
174             if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) )
175                 return $data;
176 
177             // define the top-level property name for the meta
178             switch ( get_class( $resource ) ) {
179 
180                 case 'WC_Order':
181                     $meta_name = 'order_meta';
182                     break;
183 
184                 case 'WC_Coupon':
185                     $meta_name = 'coupon_meta';
186                     break;
187 
188                 case 'WP_User':
189                     $meta_name = 'customer_meta';
190                     break;
191 
192                 default:
193                     $meta_name = 'product_meta';
194                     break;
195             }
196 
197             if ( is_a( $resource, 'WP_User' ) ) {
198 
199                 // customer meta
200                 $meta = (array) get_user_meta( $resource->ID );
201 
202             } elseif ( is_a( $resource, 'WC_Product_Variation' ) ) {
203 
204                 // product variation meta
205                 $meta = (array) get_post_meta( $resource->get_variation_id() );
206 
207             } else {
208 
209                 // coupon/order/product meta
210                 $meta = (array) get_post_meta( $resource->id );
211             }
212 
213             foreach( $meta as $meta_key => $meta_value ) {
214 
215                 // don't add hidden meta by default
216                 if ( ! is_protected_meta( $meta_key ) ) {
217                     $data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] );
218                 }
219             }
220 
221         }
222 
223         return $data;
224     }
225 
226     /**
227      * Restrict the fields included in the response if the request specified certain only certain fields should be returned
228      *
229      * @since 2.1
230      * @param array $data the response data
231      * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order
232      * @param array|string the requested list of fields to include in the response
233      * @return array response data
234      */
235     public function filter_response_fields( $data, $resource, $fields ) {
236 
237         if ( ! is_array( $data ) || empty( $fields ) )
238             return $data;
239 
240         $fields = explode( ',', $fields );
241         $sub_fields = array();
242 
243         // get sub fields
244         foreach ( $fields as $field ) {
245 
246             if ( false !== strpos( $field, '.' ) ) {
247 
248                 list( $name, $value ) = explode( '.', $field );
249 
250                 $sub_fields[ $name ] = $value;
251             }
252         }
253 
254         // iterate through top-level fields
255         foreach ( $data as $data_field => $data_value ) {
256 
257             // if a field has sub-fields and the top-level field has sub-fields to filter
258             if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) {
259 
260                 // iterate through each sub-field
261                 foreach ( $data_value as $sub_field => $sub_field_value ) {
262 
263                     // remove non-matching sub-fields
264                     if ( ! in_array( $sub_field, $sub_fields ) ) {
265                         unset( $data[ $data_field ][ $sub_field ] );
266                     }
267                 }
268 
269             } else {
270 
271                 // remove non-matching top-level fields
272                 if ( ! in_array( $data_field, $fields ) ) {
273                     unset( $data[ $data_field ] );
274                 }
275             }
276         }
277 
278         return $data;
279     }
280 
281     /**
282      * Delete a given resource
283      *
284      * @since 2.1
285      * @param int $id the resource ID
286      * @param string $type the resource post type, or `customer`
287      * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`)
288      * @return array|WP_Error
289      */
290     protected function delete( $id, $type, $force = false ) {
291 
292         if ( 'shop_order' === $type || 'shop_coupon' === $type )
293             $resource_name = str_replace( 'shop_', '', $type );
294         else
295             $resource_name = $type;
296 
297         if ( 'customer' === $type ) {
298 
299             $result = wp_delete_user( $id );
300 
301             if ( $result )
302                 return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) );
303             else
304                 return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) );
305 
306         } else {
307 
308             // delete order/coupon/product
309 
310             $result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id );
311 
312             if ( ! $result )
313                 return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) );
314 
315             if ( $force ) {
316                 return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) );
317 
318             } else {
319 
320                 $this->server->send_status( '202' );
321 
322                 return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) );
323             }
324         }
325     }
326 
327 
328     /**
329      * Checks if the given post is readable by the current user
330      *
331      * @since 2.1
332      * @see WC_API_Resource::check_permission()
333      * @param WP_Post|int $post
334      * @return bool
335      */
336     protected function is_readable( $post ) {
337 
338         return $this->check_permission( $post, 'read' );
339     }
340 
341     /**
342      * Checks if the given post is editable by the current user
343      *
344      * @since 2.1
345      * @see WC_API_Resource::check_permission()
346      * @param WP_Post|int $post
347      * @return bool
348      */
349     protected function is_editable( $post ) {
350 
351         return $this->check_permission( $post, 'edit' );
352 
353     }
354 
355     /**
356      * Checks if the given post is deletable by the current user
357      *
358      * @since 2.1
359      * @see WC_API_Resource::check_permission()
360      * @param WP_Post|int $post
361      * @return bool
362      */
363     protected function is_deletable( $post ) {
364 
365         return $this->check_permission( $post, 'delete' );
366     }
367 
368     /**
369      * Checks the permissions for the current user given a post and context
370      *
371      * @since 2.1
372      * @param WP_Post|int $post
373      * @param string $context the type of permission to check, either `read`, `write`, or `delete`
374      * @return bool true if the current user has the permissions to perform the context on the post
375      */
376     private function check_permission( $post, $context ) {
377 
378         if ( ! is_a( $post, 'WP_Post' ) )
379             $post = get_post( $post );
380 
381         if ( is_null( $post ) )
382             return false;
383 
384         $post_type = get_post_type_object( $post->post_type );
385 
386         if ( 'read' === $context )
387             return current_user_can( $post_type->cap->read_private_posts, $post->ID );
388 
389         elseif ( 'edit' === $context )
390             return current_user_can( $post_type->cap->edit_post, $post->ID );
391 
392         elseif ( 'delete' === $context )
393             return current_user_can( $post_type->cap->delete_post, $post->ID );
394 
395         else
396             return false;
397     }
398 
399 }
400 
WooCommerce API documentation generated by ApiGen 2.8.0