1 <?php
  2 /**
  3  * WooCommerce API Products Class
  4  *
  5  * Handles requests to the /products endpoint
  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_Products extends WC_API_Resource {
 16 
 17     /** @var string $base the route base */
 18     protected $base = '/products';
 19 
 20     /**
 21      * Register the routes for this class
 22      *
 23      * GET /products
 24      * GET /products/count
 25      * GET /products/<id>
 26      * GET /products/<id>/reviews
 27      *
 28      * @since 2.1
 29      * @param array $routes
 30      * @return array
 31      */
 32     public function register_routes( $routes ) {
 33 
 34         # GET /products
 35         $routes[ $this->base ] = array(
 36             array( array( $this, 'get_products' ),     WC_API_Server::READABLE ),
 37         );
 38 
 39         # GET /products/count
 40         $routes[ $this->base . '/count'] = array(
 41             array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ),
 42         );
 43 
 44         # GET /products/<id>
 45         $routes[ $this->base . '/(?P<id>\d+)' ] = array(
 46             array( array( $this, 'get_product' ),  WC_API_Server::READABLE ),
 47         );
 48 
 49         # GET /products/<id>/reviews
 50         $routes[ $this->base . '/(?P<id>\d+)/reviews' ] = array(
 51             array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ),
 52         );
 53 
 54         return $routes;
 55     }
 56 
 57     /**
 58      * Get all products
 59      *
 60      * @since 2.1
 61      * @param string $fields
 62      * @param string $type
 63      * @param array $filter
 64      * @param int $page
 65      * @return array
 66      */
 67     public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) {
 68 
 69         if ( ! empty( $type ) )
 70             $filter['type'] = $type;
 71 
 72         $filter['page'] = $page;
 73 
 74         $query = $this->query_products( $filter );
 75 
 76         $products = array();
 77 
 78         foreach( $query->posts as $product_id ) {
 79 
 80             if ( ! $this->is_readable( $product_id ) )
 81                 continue;
 82 
 83             $products[] = current( $this->get_product( $product_id, $fields ) );
 84         }
 85 
 86         $this->server->add_pagination_headers( $query );
 87 
 88         return array( 'products' => $products );
 89     }
 90 
 91     /**
 92      * Get the product for the given ID
 93      *
 94      * @since 2.1
 95      * @param int $id the product ID
 96      * @param string $fields
 97      * @return array
 98      */
 99     public function get_product( $id, $fields = null ) {
100 
101         $id = $this->validate_request( $id, 'product', 'read' );
102 
103         if ( is_wp_error( $id ) )
104             return $id;
105 
106         $product = get_product( $id );
107 
108         // add data that applies to every product type
109         $product_data = $this->get_product_data( $product );
110 
111         // add variations to variable products
112         if ( $product->is_type( 'variable' ) && $product->has_child() ) {
113 
114             $product_data['variations'] = $this->get_variation_data( $product );
115         }
116 
117         // add the parent product data to an individual variation
118         if ( $product->is_type( 'variation' ) ) {
119 
120             $product_data['parent'] = $this->get_product_data( $product->parent );
121         }
122 
123         return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) );
124     }
125 
126     /**
127      * Get the total number of orders
128      *
129      * @since 2.1
130      * @param string $type
131      * @param array $filter
132      * @return array
133      */
134     public function get_products_count( $type = null, $filter = array() ) {
135 
136         if ( ! empty( $type ) )
137             $filter['type'] = $type;
138 
139         if ( ! current_user_can( 'read_private_products' ) )
140             return new WP_Error( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), array( 'status' => 401 ) );
141 
142         $query = $this->query_products( $filter );
143 
144         return array( 'count' => (int) $query->found_posts );
145     }
146 
147     /**
148      * Edit a product
149      *
150      * @TODO implement in 2.2
151      * @param int $id the product ID
152      * @param array $data
153      * @return array
154      */
155     public function edit_product( $id, $data ) {
156 
157         $id = $this->validate_request( $id, 'product', 'edit' );
158 
159         if ( is_wp_error( $id ) )
160             return $id;
161 
162         return $this->get_product( $id );
163     }
164 
165     /**
166      * Delete a product
167      *
168      * @TODO enable along with PUT/POST in 2.2
169      * @param int $id the product ID
170      * @param bool $force true to permanently delete order, false to move to trash
171      * @return array
172      */
173     public function delete_product( $id, $force = false ) {
174 
175         $id = $this->validate_request( $id, 'product', 'delete' );
176 
177         if ( is_wp_error( $id ) )
178             return $id;
179 
180         return $this->delete( $id, 'product', ( 'true' === $force ) );
181     }
182 
183     /**
184      * Get the reviews for a product
185      *
186      * @since 2.1
187      * @param int $id the product ID to get reviews for
188      * @param string $fields fields to include in response
189      * @return array
190      */
191     public function get_product_reviews( $id, $fields = null ) {
192 
193         $id = $this->validate_request( $id, 'product', 'read' );
194 
195         if ( is_wp_error( $id ) )
196             return $id;
197 
198         $args = array(
199             'post_id' => $id,
200             'approve' => 'approve',
201         );
202 
203         $comments = get_comments( $args );
204 
205         $reviews = array();
206 
207         foreach ( $comments as $comment ) {
208 
209             $reviews[] = array(
210                 'id'             => $comment->comment_ID,
211                 'created_at'     => $this->server->format_datetime( $comment->comment_date_gmt ),
212                 'review'         => $comment->comment_content,
213                 'rating'         => get_comment_meta( $comment->comment_ID, 'rating', true ),
214                 'reviewer_name'  => $comment->comment_author,
215                 'reviewer_email' => $comment->comment_author_email,
216                 'verified'       => (bool) wc_customer_bought_product( $comment->comment_author_email, $comment->user_id, $id ),
217             );
218         }
219 
220         return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) );
221     }
222 
223     /**
224      * Helper method to get product post objects
225      *
226      * @since 2.1
227      * @param array $args request arguments for filtering query
228      * @return WP_Query
229      */
230     private function query_products( $args ) {
231 
232         // set base query arguments
233         $query_args = array(
234             'fields'      => 'ids',
235             'post_type'   => 'product',
236             'post_status' => 'publish',
237             'meta_query'  => array(),
238         );
239 
240         if ( ! empty( $args['type'] ) ) {
241 
242             $types = explode( ',', $args['type'] );
243 
244             $query_args['tax_query'] = array(
245                 array(
246                     'taxonomy' => 'product_type',
247                     'field'    => 'slug',
248                     'terms'    => $types,
249                 ),
250             );
251 
252             unset( $args['type'] );
253         }
254 
255         $query_args = $this->merge_query_args( $query_args, $args );
256 
257         return new WP_Query( $query_args );
258     }
259 
260     /**
261      * Get standard product data that applies to every product type
262      *
263      * @since 2.1
264      * @param WC_Product $product
265      * @return array
266      */
267     private function get_product_data( $product ) {
268 
269         return array(
270             'title'              => $product->get_title(),
271             'id'                 => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id,
272             'created_at'         => $this->server->format_datetime( $product->get_post_data()->post_date_gmt ),
273             'updated_at'         => $this->server->format_datetime( $product->get_post_data()->post_modified_gmt ),
274             'type'               => $product->product_type,
275             'status'             => $product->get_post_data()->post_status,
276             'downloadable'       => $product->is_downloadable(),
277             'virtual'            => $product->is_virtual(),
278             'permalink'          => $product->get_permalink(),
279             'sku'                => $product->get_sku(),
280             'price'              => wc_format_decimal( $product->get_price(), 2 ),
281             'regular_price'      => wc_format_decimal( $product->get_regular_price(), 2 ),
282             'sale_price'         => $product->get_sale_price() ? wc_format_decimal( $product->get_sale_price(), 2 ) : null,
283             'price_html'         => $product->get_price_html(),
284             'taxable'            => $product->is_taxable(),
285             'tax_status'         => $product->get_tax_status(),
286             'tax_class'          => $product->get_tax_class(),
287             'managing_stock'     => $product->managing_stock(),
288             'stock_quantity'     => (int) $product->get_stock_quantity(),
289             'in_stock'           => $product->is_in_stock(),
290             'backorders_allowed' => $product->backorders_allowed(),
291             'backordered'        => $product->is_on_backorder(),
292             'sold_individually'  => $product->is_sold_individually(),
293             'purchaseable'       => $product->is_purchasable(),
294             'featured'           => $product->is_featured(),
295             'visible'            => $product->is_visible(),
296             'catalog_visibility' => $product->visibility,
297             'on_sale'            => $product->is_on_sale(),
298             'weight'             => $product->get_weight() ? wc_format_decimal( $product->get_weight(), 2 ) : null,
299             'dimensions'         => array(
300                 'length' => $product->length,
301                 'width'  => $product->width,
302                 'height' => $product->height,
303                 'unit'   => get_option( 'woocommerce_dimension_unit' ),
304             ),
305             'shipping_required'  => $product->needs_shipping(),
306             'shipping_taxable'   => $product->is_shipping_taxable(),
307             'shipping_class'     => $product->get_shipping_class(),
308             'shipping_class_id'  => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null,
309             'description'        => apply_filters( 'the_content', $product->get_post_data()->post_content ),
310             'short_description'  => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ),
311             'reviews_allowed'    => ( 'open' === $product->get_post_data()->comment_status ),
312             'average_rating'     => wc_format_decimal( $product->get_average_rating(), 2 ),
313             'rating_count'       => (int) $product->get_rating_count(),
314             'related_ids'        => array_map( 'absint', array_values( $product->get_related() ) ),
315             'upsell_ids'         => array_map( 'absint', $product->get_upsells() ),
316             'cross_sell_ids'     => array_map( 'absint', $product->get_cross_sells() ),
317             'categories'         => wp_get_post_terms( $product->id, 'product_cat', array( 'fields' => 'names' ) ),
318             'tags'               => wp_get_post_terms( $product->id, 'product_tag', array( 'fields' => 'names' ) ),
319             'images'             => $this->get_images( $product ),
320             'featured_src'       => wp_get_attachment_url( get_post_thumbnail_id( $product->is_type( 'variation' ) ? $product->variation_id : $product->id ) ),
321             'attributes'         => $this->get_attributes( $product ),
322             'downloads'          => $this->get_downloads( $product ),
323             'download_limit'     => (int) $product->download_limit,
324             'download_expiry'    => (int) $product->download_expiry,
325             'download_type'      => $product->download_type,
326             'purchase_note'      => apply_filters( 'the_content', $product->purchase_note ),
327             'total_sales'        => metadata_exists( 'post', $product->id, 'total_sales' ) ? (int) get_post_meta( $product->id, 'total_sales', true ) : 0,
328             'variations'         => array(),
329             'parent'             => array(),
330         );
331     }
332 
333     /**
334      * Get an individual variation's data
335      *
336      * @since 2.1
337      * @param WC_Product $product
338      * @return array
339      */
340     private function get_variation_data( $product ) {
341 
342         $variations = array();
343 
344         foreach ( $product->get_children() as $child_id ) {
345 
346             $variation = $product->get_child( $child_id );
347 
348             if ( ! $variation->exists() )
349                 continue;
350 
351             $variations[] = array(
352                 'id'                => $variation->get_variation_id(),
353                 'created_at'        => $this->server->format_datetime( $variation->get_post_data()->post_date_gmt ),
354                 'updated_at'        => $this->server->format_datetime( $variation->get_post_data()->post_modified_gmt ),
355                 'downloadable'      => $variation->is_downloadable(),
356                 'virtual'           => $variation->is_virtual(),
357                 'permalink'         => $variation->get_permalink(),
358                 'sku'               => $variation->get_sku(),
359                 'price'             => wc_format_decimal( $variation->get_price(), 2 ),
360                 'regular_price'     => wc_format_decimal( $variation->get_regular_price(), 2 ),
361                 'sale_price'        => $variation->get_sale_price() ? wc_format_decimal( $variation->get_sale_price(), 2 ) : null,
362                 'taxable'           => $variation->is_taxable(),
363                 'tax_status'        => $variation->get_tax_status(),
364                 'tax_class'         => $variation->get_tax_class(),
365                 'stock_quantity'    => (int) $variation->get_stock_quantity(),
366                 'in_stock'          => $variation->is_in_stock(),
367                 'backordered'       => $variation->is_on_backorder(),
368                 'purchaseable'      => $variation->is_purchasable(),
369                 'visible'           => $variation->variation_is_visible(),
370                 'on_sale'           => $variation->is_on_sale(),
371                 'weight'            => $variation->get_weight() ? wc_format_decimal( $variation->get_weight(), 2 ) : null,
372                 'dimensions'        => array(
373                     'length' => $variation->length,
374                     'width'  => $variation->width,
375                     'height' => $variation->height,
376                     'unit'   => get_option( 'woocommerce_dimension_unit' ),
377                 ),
378                 'shipping_class'    => $variation->get_shipping_class(),
379                 'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null,
380                 'image'             => $this->get_images( $variation ),
381                 'attributes'        => $this->get_attributes( $variation ),
382                 'downloads'         => $this->get_downloads( $variation ),
383                 'download_limit'    => (int) $product->download_limit,
384                 'download_expiry'   => (int) $product->download_expiry,
385             );
386         }
387 
388         return $variations;
389     }
390 
391     /**
392      * Get the images for a product or product variation
393      *
394      * @since 2.1
395      * @param WC_Product|WC_Product_Variation $product
396      * @return array
397      */
398     private function get_images( $product ) {
399 
400         $images = $attachment_ids = array();
401 
402         if ( $product->is_type( 'variation' ) ) {
403 
404             if ( has_post_thumbnail( $product->get_variation_id() ) ) {
405 
406                 // add variation image if set
407                 $attachment_ids[] = get_post_thumbnail_id( $product->get_variation_id() );
408 
409             } elseif ( has_post_thumbnail( $product->id ) ) {
410 
411                 // otherwise use the parent product featured image if set
412                 $attachment_ids[] = get_post_thumbnail_id( $product->id );
413             }
414 
415         } else {
416 
417             // add featured image
418             if ( has_post_thumbnail( $product->id ) ) {
419                 $attachment_ids[] = get_post_thumbnail_id( $product->id );
420             }
421 
422             // add gallery images
423             $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_attachment_ids() );
424         }
425 
426         // build image data
427         foreach ( $attachment_ids as $position => $attachment_id ) {
428 
429             $attachment_post = get_post( $attachment_id );
430 
431             if ( is_null( $attachment_post ) )
432                 continue;
433 
434             $attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
435 
436             if ( ! is_array( $attachment ) )
437                 continue;
438 
439             $images[] = array(
440                 'id'         => (int) $attachment_id,
441                 'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ),
442                 'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ),
443                 'src'        => current( $attachment ),
444                 'title'      => get_the_title( $attachment_id ),
445                 'alt'        => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
446                 'position'   => $position,
447             );
448         }
449 
450         // set a placeholder image if the product has no images set
451         if ( empty( $images ) ) {
452 
453             $images[] = array(
454                 'id'         => 0,
455                 'created_at' => $this->server->format_datetime( time() ), // default to now
456                 'updated_at' => $this->server->format_datetime( time() ),
457                 'src'        => wc_placeholder_img_src(),
458                 'title'      => __( 'Placeholder', 'woocommerce' ),
459                 'alt'        => __( 'Placeholder', 'woocommerce' ),
460                 'position'   => 0,
461             );
462         }
463 
464         return $images;
465     }
466 
467     /**
468      * Get the attributes for a product or product variation
469      *
470      * @since 2.1
471      * @param WC_Product|WC_Product_Variation $product
472      * @return array
473      */
474     private function get_attributes( $product ) {
475 
476         $attributes = array();
477 
478         if ( $product->is_type( 'variation' ) ) {
479 
480             // variation attributes
481             foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) {
482 
483                 // taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`
484                 $attributes[] = array(
485                     'name'   => ucwords( str_replace( 'attribute_', '', str_replace( 'pa_', '', $attribute_name ) ) ),
486                     'option' => $attribute,
487                 );
488             }
489 
490         } else {
491 
492             foreach ( $product->get_attributes() as $attribute ) {
493 
494                 // taxonomy-based attributes are comma-separated, others are pipe (|) separated
495                 if ( $attribute['is_taxonomy'] )
496                     $options = explode( ',', $product->get_attribute( $attribute['name'] ) );
497                 else
498                     $options = explode( '|', $product->get_attribute( $attribute['name'] ) );
499 
500                 $attributes[] = array(
501                     'name'      => ucwords( str_replace( 'pa_', '', $attribute['name'] ) ),
502                     'position'  => $attribute['position'],
503                     'visible'   => (bool) $attribute['is_visible'],
504                     'variation' => (bool) $attribute['is_variation'],
505                     'options'   => array_map( 'trim', $options ),
506                 );
507             }
508         }
509 
510         return $attributes;
511     }
512 
513     /**
514      * Get the downloads for a product or product variation
515      *
516      * @since 2.1
517      * @param WC_Product|WC_Product_Variation $product
518      * @return array
519      */
520     private function get_downloads( $product ) {
521 
522         $downloads = array();
523 
524         if ( $product->is_downloadable() ) {
525 
526             foreach ( $product->get_files() as $file_id => $file ) {
527 
528                 $downloads[] = array(
529                     'id'   => $file_id, // do not cast as int as this is a hash
530                     'name' => $file['name'],
531                     'file' => $file['file'],
532                 );
533             }
534         }
535 
536         return $downloads;
537     }
538 
539 }
540 
WooCommerce API documentation generated by ApiGen 2.8.0