1 <?php
  2 /**
  3  * WooCommerce Product Functions
  4  *
  5  * Functions for product specific things.
  6  *
  7  * @author      WooThemes
  8  * @category    Core
  9  * @package     WooCommerce/Functions
 10  * @version     2.1.0
 11  */
 12 
 13 /**
 14  * Main function for returning products, uses the WC_Product_Factory class.
 15  *
 16  * @param mixed $the_product Post object or post ID of the product.
 17  * @param array $args (default: array()) Contains all arguments to be used to get this product.
 18  * @return WC_Product
 19  */
 20 function get_product( $the_product = false, $args = array() ) {
 21     return WC()->product_factory->get_product( $the_product, $args );
 22 }
 23 
 24 /**
 25  * Update a product's stock amount
 26  *
 27  * @param  int $product_id
 28  * @param  int $new_stock_level
 29  */
 30 function wc_update_product_stock( $product_id, $new_stock_level ) {
 31     $product = get_product( $product_id );
 32 
 33     if ( $product->is_type( 'variation' ) )
 34         $product->set_stock( $new_stock_level, true );
 35     else
 36         $product->set_stock( $new_stock_level );
 37 }
 38 
 39 /**
 40  * Update a product's stock status
 41  *
 42  * @param  int $product_id
 43  * @param  int $status
 44  */
 45 function wc_update_product_stock_status( $product_id, $status ) {
 46     $product = get_product( $product_id );
 47     $product-> set_stock_status( $status );
 48 }
 49 
 50 /**
 51  * Returns whether or not SKUS are enabled.
 52  * @return bool
 53  */
 54 function wc_product_sku_enabled() {
 55     return apply_filters( 'wc_product_sku_enabled', true );
 56 }
 57 
 58 /**
 59  * Returns whether or not product weights are enabled.
 60  * @return bool
 61  */
 62 function wc_product_weight_enabled() {
 63     return apply_filters( 'wc_product_weight_enabled', true );
 64 }
 65 
 66 /**
 67  * Returns whether or not product dimensions (HxWxD) are enabled.
 68  * @return bool
 69  */
 70 function wc_product_dimensions_enabled() {
 71     return apply_filters( 'wc_product_dimensions_enabled', true );
 72 }
 73 
 74 /**
 75  * Clear all transients cache for product data.
 76  *
 77  * @param int $post_id (default: 0)
 78  */
 79 function wc_delete_product_transients( $post_id = 0 ) {
 80     global $wpdb;
 81 
 82     if ( wp_using_ext_object_cache() ) {
 83         wp_cache_flush(); // There isn't a reliable method of looking up the names, so flush the cache.
 84         return;
 85     }
 86 
 87     $post_id = absint( $post_id );
 88 
 89     // Clear core transients
 90     $transients_to_clear = array(
 91         'wc\_products\_onsale',
 92         'wc\_hidden\_product\_ids',
 93         'wc\_hidden\_product\_ids\_search',
 94         'wc\_attribute\_taxonomies',
 95         'wc\_term\_counts',
 96         'wc\_featured\_products'
 97     );
 98 
 99     // Clear transients for which we don't have the name
100     $wpdb->query( "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE ('\_transient\_wc\_uf\_pid\_%') OR `option_name` LIKE ('\_transient\_timeout\_wc\_uf\_pid\_%')" );
101     $wpdb->query( "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE ('\_transient\_wc\_ln\_count\_%') OR `option_name` LIKE ('\_transient\_timeout\_wc\_ln\_count\_%')" );
102     $wpdb->query( "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE ('\_transient\_wc\_ship\_%') OR `option_name` LIKE ('\_transient\_timeout\_wc\_ship\_%')" );
103     $wpdb->query( "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE ('\_transient\_wc\_products\_will\_display\_%') OR `option_name` LIKE ('\_transient\_timeout\_wc\_products\_will\_display\_%')" );
104 
105     // Clear product specific transients
106     $post_transient_names = array(
107         'wc_product_children_ids_',
108         'wc_product_total_stock_',
109         'wc_average_rating_',
110         'wc_rating_count_'
111     );
112 
113     if ( $post_id > 0 ) {
114         foreach( $post_transient_names as $transient ) {
115             $transients_to_clear[] = $transient . $post_id;
116         }
117     } else {
118         foreach( $post_transient_names as $transient ) {
119             $transient = str_replace('_', '\_', $transient);
120             $wpdb->query( $wpdb->prepare( "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE %s OR `option_name` LIKE %s", '\_transient\_' . $transient . '%', '\_transient\_timeout\_' . $transient . '%' ) );
121         }
122     }
123 
124     // Delete transients
125     foreach( $transients_to_clear as $transient ) {
126         delete_transient( $transient );
127     }
128 
129     do_action( 'woocommerce_delete_product_transients', $post_id );
130 }
131 
132 /**
133  * Function that returns an array containing the IDs of the products that are on sale.
134  *
135  * @since 2.0
136  * @access public
137  * @return array
138  */
139 function wc_get_product_ids_on_sale() {
140     global $wpdb;
141 
142     // Load from cache
143     $product_ids_on_sale = get_transient( 'wc_products_onsale' );
144 
145     // Valid cache found
146     if ( false !== $product_ids_on_sale )
147         return $product_ids_on_sale;
148 
149     $on_sale_posts = $wpdb->get_results( "
150         SELECT post.ID, post.post_parent FROM `$wpdb->posts` AS post
151         LEFT JOIN `$wpdb->postmeta` AS meta ON post.ID = meta.post_id
152         LEFT JOIN `$wpdb->postmeta` AS meta2 ON post.ID = meta2.post_id
153         WHERE post.post_type IN ( 'product', 'product_variation' )
154             AND post.post_status = 'publish'
155             AND meta.meta_key = '_sale_price'
156             AND meta2.meta_key = '_price'
157             AND CAST( meta.meta_value AS DECIMAL ) >= 0
158             AND CAST( meta.meta_value AS CHAR ) != ''
159             AND CAST( meta.meta_value AS DECIMAL ) = CAST( meta2.meta_value AS DECIMAL )
160         GROUP BY post.ID;
161     " );
162 
163     $product_ids_on_sale = array_unique( array_map( 'absint', array_merge( wp_list_pluck( $on_sale_posts, 'ID' ), array_diff( wp_list_pluck( $on_sale_posts, 'post_parent' ), array( 0 ) ) ) ) );
164 
165     set_transient( 'wc_products_onsale', $product_ids_on_sale, YEAR_IN_SECONDS );
166 
167     return $product_ids_on_sale;
168 }
169 
170 /**
171  * Function that returns an array containing the IDs of the featured products.
172  *
173  * @since 2.1
174  * @access public
175  * @return array
176  */
177 function wc_get_featured_product_ids() {
178 
179     // Load from cache
180     $featured_product_ids = get_transient( 'wc_featured_products' );
181 
182     // Valid cache found
183     if ( false !== $featured_product_ids )
184         return $featured_product_ids;
185 
186     $featured = get_posts( array(
187         'post_type'      => array( 'product', 'product_variation' ),
188         'posts_per_page' => -1,
189         'post_status'    => 'publish',
190         'meta_query'     => array(
191             array(
192                 'key'       => '_visibility',
193                 'value'     => array('catalog', 'visible'),
194                 'compare'   => 'IN'
195             ),
196             array(
197                 'key'   => '_featured',
198                 'value' => 'yes'
199             )
200         ),
201         'fields' => 'id=>parent'
202     ) );
203 
204     $product_ids          = array_keys( $featured );
205     $parent_ids           = array_values( $featured );
206     $featured_product_ids = array_unique( array_merge( $product_ids, $parent_ids ) );
207 
208     set_transient( 'wc_featured_products', $featured_product_ids, YEAR_IN_SECONDS );
209 
210     return $featured_product_ids;
211 }
212 
213 /**
214  * Filter to allow product_cat in the permalinks for products.
215  *
216  * @access public
217  * @param string $permalink The existing permalink URL.
218  * @param object $post
219  * @return string
220  */
221 function wc_product_post_type_link( $permalink, $post ) {
222     // Abort if post is not a product
223     if ( $post->post_type !== 'product' )
224         return $permalink;
225 
226     // Abort early if the placeholder rewrite tag isn't in the generated URL
227     if ( false === strpos( $permalink, '%' ) )
228         return $permalink;
229 
230     // Get the custom taxonomy terms in use by this post
231     $terms = get_the_terms( $post->ID, 'product_cat' );
232 
233     if ( empty( $terms ) ) {
234         // If no terms are assigned to this post, use a string instead (can't leave the placeholder there)
235         $product_cat = _x( 'uncategorized', 'slug', 'woocommerce' );
236     } else {
237         // Replace the placeholder rewrite tag with the first term's slug
238         $first_term = array_shift( $terms );
239         $product_cat = $first_term->slug;
240     }
241 
242     $find = array(
243         '%year%',
244         '%monthnum%',
245         '%day%',
246         '%hour%',
247         '%minute%',
248         '%second%',
249         '%post_id%',
250         '%category%',
251         '%product_cat%'
252     );
253 
254     $replace = array(
255         date_i18n( 'Y', strtotime( $post->post_date ) ),
256         date_i18n( 'm', strtotime( $post->post_date ) ),
257         date_i18n( 'd', strtotime( $post->post_date ) ),
258         date_i18n( 'H', strtotime( $post->post_date ) ),
259         date_i18n( 'i', strtotime( $post->post_date ) ),
260         date_i18n( 's', strtotime( $post->post_date ) ),
261         $post->ID,
262         $product_cat,
263         $product_cat
264     );
265 
266     $replace = array_map( 'sanitize_title', $replace );
267 
268     $permalink = str_replace( $find, $replace, $permalink );
269 
270     return $permalink;
271 }
272 add_filter( 'post_type_link', 'wc_product_post_type_link', 10, 2 );
273 
274 
275 /**
276  * Get the placeholder image URL for products etc
277  *
278  * @access public
279  * @return string
280  */
281 function wc_placeholder_img_src() {
282     return apply_filters( 'woocommerce_placeholder_img_src', WC()->plugin_url() . '/assets/images/placeholder.png' );
283 }
284 
285 /**
286  * Get the placeholder image
287  *
288  * @access public
289  * @return string
290  */
291 function wc_placeholder_img( $size = 'shop_thumbnail' ) {
292     $dimensions = wc_get_image_size( $size );
293 
294     return apply_filters('woocommerce_placeholder_img', '<img src="' . wc_placeholder_img_src() . '" alt="Placeholder" width="' . esc_attr( $dimensions['width'] ) . '" class="woocommerce-placeholder wp-post-image" height="' . esc_attr( $dimensions['height'] ) . '" />' );
295 }
296 
297 /**
298  * Variation Formatting
299  *
300  * Gets a formatted version of variation data or item meta
301  *
302  * @access public
303  * @param array $variation
304  * @param bool $flat (default: false)
305  * @return string
306  */
307 function wc_get_formatted_variation( $variation, $flat = false ) {
308     $return = '';
309     if ( is_array( $variation ) ) {
310 
311         if ( ! $flat ) {
312             $return = '<dl class="variation">';
313         }
314 
315         $variation_list = array();
316 
317         foreach ( $variation as $name => $value ) {
318             if ( ! $value ) {
319                 continue;
320             }
321 
322             // If this is a term slug, get the term's nice name
323             if ( taxonomy_exists( esc_attr( str_replace( 'attribute_', '', $name ) ) ) ) {
324                 $term = get_term_by( 'slug', $value, esc_attr( str_replace( 'attribute_', '', $name ) ) );
325                 if ( ! is_wp_error( $term ) && $term->name )
326                     $value = $term->name;
327             }
328 
329             if ( $flat ) {
330                 $variation_list[] = wc_attribute_label( str_replace( 'attribute_', '', $name ) ) . ': ' . urldecode( $value );
331             } else {
332                 $variation_list[] = '<dt>' . wc_attribute_label( str_replace( 'attribute_', '', $name ) ) . ':</dt><dd>' . urldecode( $value ) . '</dd>';
333             }
334         }
335 
336         if ( $flat ) {
337             $return .= implode( ', ', $variation_list );
338         } else {
339             $return .= implode( '', $variation_list );
340         }
341 
342         if ( ! $flat ) {
343             $return .= '</dl>';
344         }
345     }
346     return $return;
347 }
348 
349 /**
350  * Function which handles the start and end of scheduled sales via cron.
351  *
352  * @access public
353  * @return void
354  */
355 function wc_scheduled_sales() {
356     global $wpdb;
357 
358     // Sales which are due to start
359     $product_ids = $wpdb->get_col( $wpdb->prepare( "
360         SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta
361         LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id
362         LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id
363         WHERE postmeta.meta_key = '_sale_price_dates_from'
364         AND postmeta_2.meta_key = '_price'
365         AND postmeta_3.meta_key = '_sale_price'
366         AND postmeta.meta_value > 0
367         AND postmeta.meta_value < %s
368         AND postmeta_2.meta_value != postmeta_3.meta_value
369     ", current_time( 'timestamp' ) ) );
370 
371     if ( $product_ids ) {
372         foreach ( $product_ids as $product_id ) {
373             $sale_price = get_post_meta( $product_id, '_sale_price', true );
374 
375             if ( $sale_price ) {
376                 update_post_meta( $product_id, '_price', $sale_price );
377             } else {
378                 // No sale price!
379                 update_post_meta( $product_id, '_sale_price_dates_from', '' );
380                 update_post_meta( $product_id, '_sale_price_dates_to', '' );
381             }
382 
383             wc_delete_product_transients( $product_id );
384 
385             $parent = wp_get_post_parent_id( $product_id );
386 
387             // Sync parent
388             if ( $parent ) {
389                 // We can force variable product prices to sync up by removing their min price meta
390                 delete_post_meta( $parent, '_min_price_variation_id' );
391 
392                 // Grouped products need syncing via a function
393                 $this_product = get_product( $product_id );
394                 if ( $this_product->is_type( 'simple' ) )
395                     $this_product->grouped_product_sync();
396 
397                 wc_delete_product_transients( $parent );
398             }
399         }
400     }
401 
402     // Sales which are due to end
403     $product_ids = $wpdb->get_col( $wpdb->prepare( "
404         SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta
405         LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id
406         LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id
407         WHERE postmeta.meta_key = '_sale_price_dates_to'
408         AND postmeta_2.meta_key = '_price'
409         AND postmeta_3.meta_key = '_regular_price'
410         AND postmeta.meta_value > 0
411         AND postmeta.meta_value < %s
412         AND postmeta_2.meta_value != postmeta_3.meta_value
413     ", current_time( 'timestamp' ) ) );
414 
415     if ( $product_ids ) {
416         foreach ( $product_ids as $product_id ) {
417             $regular_price = get_post_meta( $product_id, '_regular_price', true );
418 
419             update_post_meta( $product_id, '_price', $regular_price );
420             update_post_meta( $product_id, '_sale_price', '' );
421             update_post_meta( $product_id, '_sale_price_dates_from', '' );
422             update_post_meta( $product_id, '_sale_price_dates_to', '' );
423 
424             wc_delete_product_transients( $product_id );
425 
426             $parent = wp_get_post_parent_id( $product_id );
427 
428             // Sync parent
429             if ( $parent ) {
430                 // We can force variable product price to sync up by removing their min price meta
431                 delete_post_meta( $parent, '_min_variation_price' );
432 
433                 // Grouped products need syncing via a function
434                 $this_product = get_product( $product_id );
435                 if ( $this_product->is_type( 'simple' ) )
436                     $this_product->grouped_product_sync();
437 
438                 wc_delete_product_transients( $parent );
439             }
440         }
441     }
442 }
443 add_action( 'woocommerce_scheduled_sales', 'wc_scheduled_sales' );
444 
445 /**
446  * wc_get_attachment_image_attributes function.
447  *
448  * @access public
449  * @param array $attr
450  * @return array
451  */
452 function wc_get_attachment_image_attributes( $attr ) {
453     if ( strstr( $attr['src'], 'woocommerce_uploads/' ) )
454         $attr['src'] = wc_placeholder_img_src();
455 
456     return $attr;
457 }
458 add_filter( 'wp_get_attachment_image_attributes', 'wc_get_attachment_image_attributes' );
459 
460 
461 /**
462  * wc_prepare_attachment_for_js function.
463  *
464  * @access public
465  * @param array $response
466  * @return array
467  */
468 function wc_prepare_attachment_for_js( $response ) {
469 
470     if ( isset( $response['url'] ) && strstr( $response['url'], 'woocommerce_uploads/' ) ) {
471         $response['full']['url'] = wc_placeholder_img_src();
472         if ( isset( $response['sizes'] ) ) {
473             foreach( $response['sizes'] as $size => $value ) {
474                 $response['sizes'][ $size ]['url'] = wc_placeholder_img_src();
475             }
476         }
477     }
478 
479     return $response;
480 }
481 add_filter( 'wp_prepare_attachment_for_js', 'wc_prepare_attachment_for_js' );
482 
483 /**
484  * Track product views
485  */
486 function wc_track_product_view() {
487     if ( ! is_singular( 'product' ) )
488         return;
489 
490     global $post, $product;
491 
492     if ( empty( $_COOKIE['woocommerce_recently_viewed'] ) )
493         $viewed_products = array();
494     else
495         $viewed_products = (array) explode( '|', $_COOKIE['woocommerce_recently_viewed'] );
496 
497     if ( ! in_array( $post->ID, $viewed_products ) )
498         $viewed_products[] = $post->ID;
499 
500     if ( sizeof( $viewed_products ) > 15 )
501         array_shift( $viewed_products );
502 
503     // Store for session only
504     wc_setcookie( 'woocommerce_recently_viewed', implode( '|', $viewed_products ) );
505 }
506 
507 add_action( 'template_redirect', 'wc_track_product_view', 20 );
508 
WooCommerce API documentation generated by ApiGen 2.8.0