Completed
Push — master ( 07eadf...5397e4 )
by Mike
20:38 queued 11s
created

data-stores/class-wc-product-data-store-cpt.php (3 issues)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * WC_Product_Data_Store_CPT class file.
4
 *
5
 * @package WooCommerce/Classes
6
 */
7
8
if ( ! defined( 'ABSPATH' ) ) {
9
	exit;
10
}
11
12
/**
13
 * WC Product Data Store: Stored in CPT.
14
 *
15
 * @version  3.0.0
16
 */
17
class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface, WC_Product_Data_Store_Interface {
18
19
	/**
20
	 * Data stored in meta keys, but not considered "meta".
21
	 *
22
	 * @since 3.0.0
23
	 * @var array
24
	 */
25
	protected $internal_meta_keys = array(
26
		'_visibility',
27
		'_sku',
28
		'_price',
29
		'_regular_price',
30
		'_sale_price',
31
		'_sale_price_dates_from',
32
		'_sale_price_dates_to',
33
		'total_sales',
34
		'_tax_status',
35
		'_tax_class',
36
		'_manage_stock',
37
		'_stock',
38
		'_stock_status',
39
		'_backorders',
40
		'_low_stock_amount',
41
		'_sold_individually',
42
		'_weight',
43
		'_length',
44
		'_width',
45
		'_height',
46
		'_upsell_ids',
47
		'_crosssell_ids',
48
		'_purchase_note',
49
		'_default_attributes',
50
		'_product_attributes',
51
		'_virtual',
52
		'_downloadable',
53
		'_download_limit',
54
		'_download_expiry',
55
		'_featured',
56
		'_downloadable_files',
57
		'_wc_rating_count',
58
		'_wc_average_rating',
59
		'_wc_review_count',
60
		'_variation_description',
61
		'_thumbnail_id',
62
		'_file_paths',
63
		'_product_image_gallery',
64
		'_product_version',
65
		'_wp_old_slug',
66
		'_edit_last',
67
		'_edit_lock',
68
	);
69
70
	/**
71
	 * If we have already saved our extra data, don't do automatic / default handling.
72
	 *
73
	 * @var bool
74
	 */
75
	protected $extra_data_saved = false;
76
77
	/**
78
	 * Stores updated props.
79
	 *
80
	 * @var array
81
	 */
82
	protected $updated_props = array();
83
84
	/*
85
	|--------------------------------------------------------------------------
86
	| CRUD Methods
87
	|--------------------------------------------------------------------------
88
	*/
89
90
	/**
91
	 * Method to create a new product in the database.
92
	 *
93
	 * @param WC_Product $product Product object.
94
	 */
95 381
	public function create( &$product ) {
96 381
		if ( ! $product->get_date_created( 'edit' ) ) {
97 381
			$product->set_date_created( current_time( 'timestamp', true ) );
98
		}
99
100 381
		$id = wp_insert_post(
101 381
			apply_filters(
102 381
				'woocommerce_new_product_data',
103 381
				array(
104 381
					'post_type'      => 'product',
105 381
					'post_status'    => $product->get_status() ? $product->get_status() : 'publish',
106 381
					'post_author'    => get_current_user_id(),
107 381
					'post_title'     => $product->get_name() ? $product->get_name() : __( 'Product', 'woocommerce' ),
108 381
					'post_content'   => $product->get_description(),
109 381
					'post_excerpt'   => $product->get_short_description(),
110 381
					'post_parent'    => $product->get_parent_id(),
111 381
					'comment_status' => $product->get_reviews_allowed() ? 'open' : 'closed',
112 381
					'ping_status'    => 'closed',
113 381
					'menu_order'     => $product->get_menu_order(),
114 381
					'post_password'  => $product->get_post_password( 'edit' ),
115 381
					'post_date'      => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
116
					'post_date_gmt'  => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
117 381
					'post_name'      => $product->get_slug( 'edit' ),
118
				)
119
			),
120 381
			true
121 381
		);
122
123 381 View Code Duplication
		if ( $id && ! is_wp_error( $id ) ) {
124 381
			$product->set_id( $id );
125 381
126 381
			$this->update_post_meta( $product, true );
127 381
			$this->update_terms( $product, true );
128 381
			$this->update_visibility( $product, true );
129
			$this->update_attributes( $product, true );
130 381
			$this->update_version_and_type( $product );
131 381
			$this->handle_updated_props( $product );
132
			$this->clear_caches( $product );
133 381
134
			$product->save_meta_data();
135 381
			$product->apply_changes();
136
137
			do_action( 'woocommerce_new_product', $id );
138
		}
139
	}
140
141
	/**
142
	 * Method to read a product from the database.
143
	 *
144
	 * @param WC_Product $product Product object.
145 383
	 * @throws Exception If invalid product.
146 383
	 */
147 383
	public function read( &$product ) {
148
		$product->set_defaults();
149 383
		$post_object = get_post( $product->get_id() );
150 6
151 View Code Duplication
		if ( ! $product->get_id() || ! $post_object || 'product' !== $post_object->post_type ) {
152
			throw new Exception( __( 'Invalid product.', 'woocommerce' ) );
153 381
		}
154
155 381
		$product->set_props(
156 381
			array(
157 381
				'name'              => $post_object->post_title,
158 381
				'slug'              => $post_object->post_name,
159 381
				'date_created'      => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
160 381
				'date_modified'     => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
161 381
				'status'            => $post_object->post_status,
162 381
				'description'       => $post_object->post_content,
163 381
				'short_description' => $post_object->post_excerpt,
164 381
				'parent_id'         => $post_object->post_parent,
165
				'menu_order'        => $post_object->menu_order,
166
				'post_password'     => $post_object->post_password,
167
				'reviews_allowed'   => 'open' === $post_object->comment_status,
168 381
			)
169 381
		);
170 381
171 381
		$this->read_attributes( $product );
172 381
		$this->read_downloads( $product );
173 381
		$this->read_visibility( $product );
174
		$this->read_product_data( $product );
175
		$this->read_extra_data( $product );
176
		$product->set_object_read( true );
177
	}
178
179
	/**
180
	 * Method to update a product in the database.
181 85
	 *
182 85
	 * @param WC_Product $product Product object.
183 85
	 */
184
	public function update( &$product ) {
185
		$product->save_meta_data();
186 85
		$changes = $product->get_changes();
187
188 13
		// Only update the post when the post data changes.
189 13
		if ( array_intersect( array( 'description', 'short_description', 'name', 'parent_id', 'reviews_allowed', 'status', 'menu_order', 'date_created', 'date_modified', 'slug' ), array_keys( $changes ) ) ) {
190 13
			$post_data = array(
191 13
				'post_content'   => $product->get_description( 'edit' ),
192 13
				'post_excerpt'   => $product->get_short_description( 'edit' ),
193 13
				'post_title'     => $product->get_name( 'edit' ),
194 13
				'post_parent'    => $product->get_parent_id( 'edit' ),
195 13
				'comment_status' => $product->get_reviews_allowed( 'edit' ) ? 'open' : 'closed',
196 13
				'post_status'    => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish',
197
				'menu_order'     => $product->get_menu_order( 'edit' ),
198 13
				'post_password'  => $product->get_post_password( 'edit' ),
199 13
				'post_name'      => $product->get_slug( 'edit' ),
200 13
				'post_type'      => 'product',
201
			);
202 13
			if ( $product->get_date_created( 'edit' ) ) {
203 2
				$post_data['post_date']     = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() );
204 2
				$post_data['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() );
205
			}
206 11
			if ( isset( $changes['date_modified'] ) && $product->get_date_modified( 'edit' ) ) {
207 11
				$post_data['post_modified']     = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() );
208
				$post_data['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() );
209
			} else {
210
				$post_data['post_modified']     = current_time( 'mysql' );
211
				$post_data['post_modified_gmt'] = current_time( 'mysql', 1 );
212
			}
213
214
			/**
215
			 * When updating this object, to prevent infinite loops, use $wpdb
216
			 * to update data, since wp_update_post spawns more calls to the
217
			 * save_post action.
218 13
			 *
219
			 * This ensures hooks are fired by either WP itself (admin screen save),
220
			 * or an update purely from CRUD.
221
			 */
222 13 View Code Duplication
			if ( doing_action( 'save_post' ) ) {
223
				$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) );
224 13
				clean_post_cache( $product->get_id() );
225
			} else {
226
				wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) );
227 78
			}
228 78
			$product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
229
230 78 View Code Duplication
		} else { // Only update post modified time to record this save event.
231 78
			$GLOBALS['wpdb']->update(
232
				$GLOBALS['wpdb']->posts,
233
				array(
234 78
					'post_modified'     => current_time( 'mysql' ),
235
					'post_modified_gmt' => current_time( 'mysql', 1 ),
236
				),
237 78
				array(
238
					'ID' => $product->get_id(),
239
				)
240 85
			);
241 85
			clean_post_cache( $product->get_id() );
242 85
		}
243 85
244 85
		$this->update_post_meta( $product );
245 85
		$this->update_terms( $product );
246
		$this->update_visibility( $product );
247 85
		$this->update_attributes( $product );
248
		$this->update_version_and_type( $product );
249 85
		$this->handle_updated_props( $product );
250
		$this->clear_caches( $product );
251 85
252
		$product->apply_changes();
253
254
		do_action( 'woocommerce_update_product', $product->get_id() );
255
	}
256
257
	/**
258
	 * Method to delete a product from the database.
259
	 *
260 27
	 * @param WC_Product $product Product object.
261 27
	 * @param array      $args Array of args to pass to the delete method.
262 27
	 */
263
	public function delete( &$product, $args = array() ) {
264 27
		$id        = $product->get_id();
265 27
		$post_type = $product->is_type( 'variation' ) ? 'product_variation' : 'product';
266 27
267
		$args = wp_parse_args(
268
			$args,
269
			array(
270 27
				'force_delete' => false,
271
			)
272
		);
273
274 27
		if ( ! $id ) {
275 25
			return;
276 25
		}
277 25
278 25
		if ( $args['force_delete'] ) {
279
			do_action( 'woocommerce_before_delete_' . $post_type, $id );
280 2
			wp_delete_post( $id );
281 2
			$product->set_id( 0 );
282 2
			do_action( 'woocommerce_delete_' . $post_type, $id );
283
		} else {
284
			wp_trash_post( $id );
285
			$product->set_status( 'trash' );
286
			do_action( 'woocommerce_trash_' . $post_type, $id );
287
		}
288
	}
289
290
	/*
291
	|--------------------------------------------------------------------------
292
	| Additional Methods
293
	|--------------------------------------------------------------------------
294
	*/
295
296
	/**
297
	 * Read product data. Can be overridden by child classes to load other props.
298 381
	 *
299 381
	 * @param WC_Product $product Product object.
300 381
	 * @since 3.0.0
301 381
	 */
302 381
	protected function read_product_data( &$product ) {
303
		$id                = $product->get_id();
304 381
		$post_meta_values  = get_post_meta( $id );
305 2
		$meta_key_to_props = array(
306
			'_sku'                   => 'sku',
307 381
			'_regular_price'         => 'regular_price',
308
			'_sale_price'            => 'sale_price',
309
			'_price'                 => 'price',
310 381
			'_sale_price_dates_from' => 'date_on_sale_from',
311 2
			'_sale_price_dates_to'   => 'date_on_sale_to',
312
			'total_sales'            => 'total_sales',
313 381
			'_tax_status'            => 'tax_status',
314
			'_tax_class'             => 'tax_class',
315
			'_manage_stock'          => 'manage_stock',
316 381
			'_backorders'            => 'backorders',
317 2
			'_low_stock_amount'      => 'low_stock_amount',
318
			'_sold_individually'     => 'sold_individually',
319 381
			'_weight'                => 'weight',
320
			'_length'                => 'length',
321
			'_width'                 => 'width',
322 381
			'_height'                => 'height',
323
			'_upsell_ids'            => 'upsell_ids',
324 381
			'_crosssell_ids'         => 'cross_sell_ids',
325 381
			'_purchase_note'         => 'purchase_note',
326 381
			'_default_attributes'    => 'default_attributes',
327 381
			'_virtual'               => 'virtual',
328 381
			'_downloadable'          => 'downloadable',
329 381
			'_download_limit'        => 'download_limit',
330 381
			'_download_expiry'       => 'download_expiry',
331 381
			'_thumbnail_id'          => 'image_id',
332 381
			'_stock'                 => 'stock_quantity',
333 381
			'_stock_status'          => 'stock_status',
334 381
			'_wc_average_rating'     => 'average_rating',
335 381
			'_wc_rating_count'       => 'rating_counts',
336 381
			'_wc_review_count'       => 'review_count',
337 381
			'_product_image_gallery' => 'gallery_image_ids',
338 381
		);
339 381
340 381
		$set_props = array();
341 381
342 381
		foreach ( $meta_key_to_props as $meta_key => $prop ) {
343 381
			$meta_value         = isset( $post_meta_values[ $meta_key ][0] ) ? $post_meta_values[ $meta_key ][0] : '';
344 381
			$set_props[ $prop ] = maybe_unserialize( $meta_value ); // get_post_meta only unserializes single values.
345 381
		}
346 381
347 381
		$set_props['category_ids']      = $this->get_term_ids( $product, 'product_cat' );
348 381
		$set_props['tag_ids']           = $this->get_term_ids( $product, 'product_tag' );
349 381
		$set_props['shipping_class_id'] = current( $this->get_term_ids( $product, 'product_shipping_class' ) );
350 381
		$set_props['gallery_image_ids'] = array_filter( explode( ',', $set_props['gallery_image_ids'] ) );
351 381
352 381
		if ( '' === $set_props['review_count'] ) {
353 381
			unset( $set_props['review_count'] );
354 381
			WC_Comments::get_review_count_for_product( $product );
355 381
		}
356
357
		if ( '' === $set_props['rating_counts'] ) {
358
			unset( $set_props['rating_counts'] );
359
			WC_Comments::get_rating_counts_for_product( $product );
360 381
		}
361
362
		if ( '' === $set_props['average_rating'] ) {
363
			unset( $set_props['average_rating'] );
364
			WC_Comments::get_average_rating_for_product( $product );
365
		}
366
367
		$product->set_props( $set_props );
368
369
		// Handle sale dates on the fly in case of missed cron schedule.
370
		if ( $product->is_type( 'simple' ) && $product->is_on_sale( 'edit' ) && $product->get_sale_price( 'edit' ) !== $product->get_price( 'edit' ) ) {
371
			update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) );
372 382
			$product->set_price( $product->get_sale_price( 'edit' ) );
373 382
		}
374 21
	}
375 21
376 21
	/**
377
	 * Read extra data associated with the product, like button text or product URL for external products.
378
	 *
379
	 * @param WC_Product $product Product object.
380
	 * @since 3.0.0
381
	 */
382 View Code Duplication
	protected function read_extra_data( &$product ) {
383
		foreach ( $product->get_extra_data_keys() as $key ) {
384
			$function = 'set_' . $key;
385
			if ( is_callable( array( $product, $function ) ) ) {
386
				$product->{$function}( get_post_meta( $product->get_id(), '_' . $key, true ) );
387
			}
388 381
		}
389 381
	}
390 381
391 381
	/**
392 381
	 * Convert visibility terms to props.
393 381
	 * Catalog visibility valid values are 'visible', 'catalog', 'search', and 'hidden'.
394
	 *
395 381
	 * @param WC_Product $product Product object.
396 1
	 * @since 3.0.0
397 381
	 */
398
	protected function read_visibility( &$product ) {
399 381
		$terms           = get_the_terms( $product->get_id(), 'product_visibility' );
400 2
		$term_names      = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array();
401
		$featured        = in_array( 'featured', $term_names, true );
402 380
		$exclude_search  = in_array( 'exclude-from-search', $term_names, true );
403
		$exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true );
404
405 381 View Code Duplication
		if ( $exclude_search && $exclude_catalog ) {
406
			$catalog_visibility = 'hidden';
407 381
		} elseif ( $exclude_search ) {
408 381
			$catalog_visibility = 'catalog';
409
		} elseif ( $exclude_catalog ) {
410
			$catalog_visibility = 'search';
411
		} else {
412
			$catalog_visibility = 'visible';
413
		}
414
415
		$product->set_props(
416
			array(
417
				'featured'           => $featured,
418 337
				'catalog_visibility' => $catalog_visibility,
419 337
			)
420
		);
421 337
	}
422 3
423 3
	/**
424 3
	 * Read attributes from post meta.
425
	 *
426 3
	 * @param WC_Product $product Product object.
427
	 */
428
	protected function read_attributes( &$product ) {
429
		$meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true );
430
431
		if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) {
432 3
			$attributes = array();
433
			foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) {
434
				$meta_value = array_merge(
435
					array(
436 3
						'name'         => '',
437 1
						'value'        => '',
438
						'position'     => 0,
439
						'is_visible'   => 0,
440 1
						'is_variation' => 0,
441 1
						'is_taxonomy'  => 0,
442
					),
443 3
					(array) $meta_attribute_value
444 3
				);
445
446
				// Check if is a taxonomy attribute.
447 3 View Code Duplication
				if ( ! empty( $meta_value['is_taxonomy'] ) ) {
448 3
					if ( ! taxonomy_exists( $meta_value['name'] ) ) {
449 3
						continue;
450 3
					}
451 3
					$id      = wc_attribute_taxonomy_id_by_name( $meta_value['name'] );
452 3
					$options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' );
453 3
				} else {
454 3
					$id      = 0;
455
					$options = wc_get_text_attributes( $meta_value['value'] );
456 3
				}
457
458
				$attribute = new WC_Product_Attribute();
459
				$attribute->set_id( $id );
460
				$attribute->set_name( $meta_value['name'] );
461
				$attribute->set_options( $options );
462
				$attribute->set_position( $meta_value['position'] );
463
				$attribute->set_visible( $meta_value['is_visible'] );
464
				$attribute->set_variation( $meta_value['is_variation'] );
465
				$attributes[] = $attribute;
466 382
			}
467 382
			$product->set_attributes( $attributes );
468
		}
469 382
	}
470 3
471 3
	/**
472 3
	 * Read downloads from post meta.
473
	 *
474
	 * @param WC_Product $product Product object.
475 3
	 * @since 3.0.0
476 3
	 */
477 3
	protected function read_downloads( &$product ) {
478 3
		$meta_values = array_filter( (array) get_post_meta( $product->get_id(), '_downloadable_files', true ) );
479 3
480
		if ( $meta_values ) {
481 3
			$downloads = array();
482
			foreach ( $meta_values as $key => $value ) {
483
				if ( ! isset( $value['name'], $value['file'] ) ) {
484
					continue;
485
				}
486
				$download = new WC_Product_Download();
487
				$download->set_id( $key );
488
				$download->set_name( $value['name'] ? $value['name'] : wc_get_filename_from_url( $value['file'] ) );
489
				$download->set_file( apply_filters( 'woocommerce_file_download_path', $value['file'], $product, $key ) );
490
				$downloads[] = $download;
491
			}
492 382
			$product->set_downloads( $downloads );
493
		}
494 382
	}
495
496
	/**
497
	 * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class.
498
	 *
499
	 * @param WC_Product $product Product object.
500
	 * @param bool       $force Force update. Used during create.
501
	 * @since 3.0.0
502
	 */
503
	protected function update_post_meta( &$product, $force = false ) {
504
		$meta_key_to_props = array(
505
			'_sku'                   => 'sku',
506
			'_regular_price'         => 'regular_price',
507
			'_sale_price'            => 'sale_price',
508
			'_sale_price_dates_from' => 'date_on_sale_from',
509
			'_sale_price_dates_to'   => 'date_on_sale_to',
510
			'total_sales'            => 'total_sales',
511
			'_tax_status'            => 'tax_status',
512
			'_tax_class'             => 'tax_class',
513
			'_manage_stock'          => 'manage_stock',
514
			'_backorders'            => 'backorders',
515
			'_low_stock_amount'      => 'low_stock_amount',
516
			'_sold_individually'     => 'sold_individually',
517
			'_weight'                => 'weight',
518
			'_length'                => 'length',
519
			'_width'                 => 'width',
520
			'_height'                => 'height',
521
			'_upsell_ids'            => 'upsell_ids',
522
			'_crosssell_ids'         => 'cross_sell_ids',
523
			'_purchase_note'         => 'purchase_note',
524
			'_default_attributes'    => 'default_attributes',
525
			'_virtual'               => 'virtual',
526
			'_downloadable'          => 'downloadable',
527
			'_product_image_gallery' => 'gallery_image_ids',
528 382
			'_download_limit'        => 'download_limit',
529
			'_download_expiry'       => 'download_expiry',
530 382
			'_thumbnail_id'          => 'image_id',
531 21
			'_stock'                 => 'stock_quantity',
532
			'_stock_status'          => 'stock_status',
533
			'_wc_average_rating'     => 'average_rating',
534 382
			'_wc_rating_count'       => 'rating_counts',
535
			'_wc_review_count'       => 'review_count',
536 382
		);
537 382
538 382
		// Make sure to take extra data (like product url or text for external products) into account.
539
		$extra_data_keys = $product->get_extra_data_keys();
540 382
541 382
		foreach ( $extra_data_keys as $key ) {
542 382
			$meta_key_to_props[ '_' . $key ] = $key;
543 382
		}
544 382
545 382
		$props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props );
546 382
547 382
		foreach ( $props_to_update as $meta_key => $prop ) {
548 382
			$value = $product->{"get_$prop"}( 'edit' );
549 382
			$value = is_string( $value ) ? wp_slash( $value ) : $value;
550 382
			switch ( $prop ) {
551 10
				case 'virtual':
552
				case 'downloadable':
553 381
				case 'manage_stock':
554
				case 'sold_individually':
555 382
					$updated = update_post_meta( $product->get_id(), $meta_key, wc_bool_to_string( $value ) );
556 382
					break;
557 382
				case 'gallery_image_ids':
558 382
					$updated = update_post_meta( $product->get_id(), $meta_key, implode( ',', $value ) );
559 382
					break;
560 382
				case 'image_id':
561
					if ( ! empty( $value ) ) {
562 382
						set_post_thumbnail( $product->get_id(), $value );
563 382
					} else {
564
						delete_post_meta( $product->get_id(), '_thumbnail_id' );
565 382
					}
566 382
					$updated = true;
567
					break;
568
				case 'date_on_sale_from':
569
				case 'date_on_sale_to':
570
					$updated = update_post_meta( $product->get_id(), $meta_key, $value ? $value->getTimestamp() : '' );
571 382
					break;
572 382
				default:
573 21
					$updated = update_post_meta( $product->get_id(), $meta_key, $value );
574 1
					break;
575
			}
576 21
			if ( $updated ) {
577 21
				$this->updated_props[] = $prop;
578 21
			}
579 21
		}
580
581 21
		// Update extra data associated with the product like button text or product URL for external products.
582 21
		if ( ! $this->extra_data_saved ) {
583
			foreach ( $extra_data_keys as $key ) {
584
				if ( ! array_key_exists( '_' . $key, $props_to_update ) ) {
585
					continue;
586
				}
587
				$function = 'get_' . $key;
588 382
				if ( is_callable( array( $product, $function ) ) ) {
589 382
					$value = $product->{$function}( 'edit' );
590
					$value = is_string( $value ) ? wp_slash( $value ) : $value;
591
592
					if ( update_post_meta( $product->get_id(), '_' . $key, $value ) ) {
593
						$this->updated_props[] = $key;
594
					}
595
				}
596
			}
597
		}
598
599 382
		if ( $this->update_downloads( $product, $force ) ) {
600 382
			$this->updated_props[] = 'downloads';
601
		}
602 382
	}
603 379
604 379
	/**
605 40
	 * Handle updated meta props after updating meta data.
606 40
	 *
607
	 * @since 3.0.0
608
	 * @param WC_Product $product Product Object.
609
	 */
610 379
	protected function handle_updated_props( &$product ) {
611 379
		$price_is_synced = $product->is_type( array( 'variable', 'grouped' ) );
612 16
613 16
		if ( ! $price_is_synced ) {
614
			if ( in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) ) {
615 379
				if ( $product->get_sale_price( 'edit' ) >= $product->get_regular_price( 'edit' ) ) {
616 379
					update_post_meta( $product->get_id(), '_sale_price', '' );
617
					$product->set_sale_price( '' );
618
				}
619
			}
620
621 382
			if ( in_array( 'date_on_sale_from', $this->updated_props, true ) || in_array( 'date_on_sale_to', $this->updated_props, true ) || in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) || in_array( 'product_type', $this->updated_props, true ) ) {
622 382
				if ( $product->is_on_sale( 'edit' ) ) {
623 60
					update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) );
624
					$product->set_price( $product->get_sale_price( 'edit' ) );
625 381
				} else {
626
					update_post_meta( $product->get_id(), '_price', $product->get_regular_price( 'edit' ) );
627
					$product->set_price( $product->get_regular_price( 'edit' ) );
628
				}
629 382
			}
630 382
		}
631 60
632
		if ( in_array( 'stock_quantity', $this->updated_props, true ) ) {
633 381
			if ( $product->is_type( 'variation' ) ) {
634
				do_action( 'woocommerce_variation_set_stock', $product );
635
			} else {
636
				do_action( 'woocommerce_product_set_stock', $product );
637
			}
638 382
		}
639
640
		if ( in_array( 'stock_status', $this->updated_props, true ) ) {
641 382
			if ( $product->is_type( 'variation' ) ) {
642
				do_action( 'woocommerce_variation_set_stock_status', $product->get_id(), $product->get_stock_status(), $product );
643
			} else {
644
				do_action( 'woocommerce_product_set_stock_status', $product->get_id(), $product->get_stock_status(), $product );
645
			}
646
		}
647
648
		// Trigger action so 3rd parties can deal with updated props.
649
		do_action( 'woocommerce_product_object_updated_props', $product, $this->updated_props );
650
651 381
		// After handling, we can reset the props array.
652 381
		$this->updated_props = array();
653
	}
654 381
655 381
	/**
656
	 * For all stored terms in all taxonomies, save them to the DB.
657 381
	 *
658 380
	 * @param WC_Product $product Product object.
659
	 * @param bool       $force Force update. Used during create.
660
	 * @since 3.0.0
661 381
	 */
662
	protected function update_terms( &$product, $force = false ) {
663 381
		$changes = $product->get_changes();
664 381
665
		if ( $force || array_key_exists( 'category_ids', $changes ) ) {
666 381
			$categories = $product->get_category_ids( 'edit' );
667 381
668
			if ( empty( $categories ) && get_option( 'default_product_cat', 0 ) ) {
669
				$categories = array( get_option( 'default_product_cat', 0 ) );
670
			}
671
672
			wp_set_post_terms( $product->get_id(), $categories, 'product_cat', false );
673
		}
674
		if ( $force || array_key_exists( 'tag_ids', $changes ) ) {
675
			wp_set_post_terms( $product->get_id(), $product->get_tag_ids( 'edit' ), 'product_tag', false );
676
		}
677 View Code Duplication
		if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) {
678
			wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false );
679 381
		}
680 381
	}
681
682 381
	/**
683 381
	 * Update visibility terms based on props.
684
	 *
685 381
	 * @since 3.0.0
686 4
	 *
687
	 * @param WC_Product $product Product object.
688
	 * @param bool       $force Force update. Used during create.
689 381
	 */
690 66
	protected function update_visibility( &$product, $force = false ) {
691
		$changes = $product->get_changes();
692
693 381
		if ( $force || array_intersect( array( 'featured', 'stock_status', 'average_rating', 'catalog_visibility' ), array_keys( $changes ) ) ) {
694
			$terms = array();
695 381
696 1
			if ( $product->get_featured() ) {
697
				$terms[] = 'featured';
698
			}
699 381
700 381
			if ( 'outofstock' === $product->get_stock_status() ) {
701 1
				$terms[] = 'outofstock';
702 1
			}
703 1
704 381
			$rating = min( 5, round( $product->get_average_rating(), 0 ) );
705
706
			if ( $rating > 0 ) {
707 381
				$terms[] = 'rated-' . $rating;
708 2
			}
709 2
710
			switch ( $product->get_catalog_visibility() ) {
711
				case 'hidden':
712 381
					$terms[] = 'exclude-from-search';
713 381
					$terms[] = 'exclude-from-catalog';
714 381
					break;
715
				case 'catalog':
716
					$terms[] = 'exclude-from-search';
717
					break;
718
				case 'search':
719
					$terms[] = 'exclude-from-catalog';
720
					break;
721
			}
722
723
			if ( ! is_wp_error( wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ) ) ) {
724
				do_action( 'woocommerce_product_set_visibility', $product->get_id(), $product->get_catalog_visibility() );
725
			}
726 381
		}
727 381
	}
728
729 381
	/**
730 381
	 * Update attributes which are a mix of terms and meta data.
731 381
	 *
732
	 * @param WC_Product $product Product object.
733 381
	 * @param bool       $force Force update. Used during create.
734 53
	 * @since 3.0.0
735 53
	 */
736
	protected function update_attributes( &$product, $force = false ) {
737 53
		$changes = $product->get_changes();
738
739 53
		if ( $force || array_key_exists( 'attributes', $changes ) ) {
740
			$attributes  = $product->get_attributes();
741
			$meta_values = array();
742
743
			if ( $attributes ) {
744
				foreach ( $attributes as $attribute_key => $attribute ) {
745
					$value = '';
746 53
747 46
					if ( is_null( $attribute ) ) {
748
						if ( taxonomy_exists( $attribute_key ) ) {
749 10
							// Handle attributes that have been unset.
750
							wp_set_object_terms( $product->get_id(), array(), $attribute_key );
751
						} elseif ( taxonomy_exists( urldecode( $attribute_key ) ) ) {
752
							// Handle attributes that have been unset.
753 53
							wp_set_object_terms( $product->get_id(), array(), urldecode( $attribute_key ) );
754 53
						}
755 53
						continue;
756 53
757 53
					} elseif ( $attribute->is_taxonomy() ) {
758 53
						wp_set_object_terms( $product->get_id(), wp_list_pluck( (array) $attribute->get_terms(), 'term_id' ), $attribute->get_name() );
759 53
					} else {
760
						$value = wc_implode_text_attributes( $attribute->get_options() );
761
					}
762
763 381
					// Store in format WC uses in meta.
764
					$meta_values[ $attribute_key ] = array(
765
						'name'         => $attribute->get_name(),
766
						'value'        => $value,
767
						'position'     => $attribute->get_position(),
768
						'is_visible'   => $attribute->get_visible() ? 1 : 0,
769
						'is_variation' => $attribute->get_variation() ? 1 : 0,
770
						'is_taxonomy'  => $attribute->is_taxonomy() ? 1 : 0,
771
					);
772
				}
773
			}
774
			// Note, we use wp_slash to add extra level of escaping. See https://codex.wordpress.org/Function_Reference/update_post_meta#Workaround.
775 382
			update_post_meta( $product->get_id(), '_product_attributes', wp_slash( $meta_values ) );
776 382
		}
777
	}
778 382
779 382
	/**
780 382
	 * Update downloads.
781
	 *
782 382
	 * @since 3.0.0
783 3
	 * @param WC_Product $product Product object.
784
	 * @param bool       $force Force update. Used during create.
785 3
	 * @return bool If updated or not.
786
	 */
787
	protected function update_downloads( &$product, $force = false ) {
788
		$changes = $product->get_changes();
789 382
790 60
		if ( $force || array_key_exists( 'downloads', $changes ) ) {
791
			$downloads   = $product->get_downloads();
792 381
			$meta_values = array();
793
794
			if ( $downloads ) {
795 382
				foreach ( $downloads as $key => $download ) {
796
					// Store in format WC uses in meta.
797 95
					$meta_values[ $key ] = $download->get_data();
798
				}
799
			}
800
801
			if ( $product->is_type( 'variation' ) ) {
802
				do_action( 'woocommerce_process_product_file_download_paths', $product->get_parent_id(), $product->get_id(), $downloads );
803
			} else {
804
				do_action( 'woocommerce_process_product_file_download_paths', $product->get_id(), 0, $downloads );
805
			}
806 381
807 381
			return update_post_meta( $product->get_id(), '_downloadable_files', $meta_values );
808 381
		}
809
		return false;
810 381
	}
811 381
812
	/**
813
	 * Make sure we store the product type and version (to track data changes).
814 381
	 *
815 72
	 * @param WC_Product $product Product object.
816 72
	 * @since 3.0.0
817
	 */
818
	protected function update_version_and_type( &$product ) {
819
		$old_type = WC_Product_Factory::get_product_type( $product->get_id() );
820
		$new_type = $product->get_type();
821
822
		wp_set_object_terms( $product->get_id(), $new_type, 'product_type' );
823
		update_post_meta( $product->get_id(), '_product_version', WC_VERSION );
824
825
		// Action for the transition.
826 382
		if ( $old_type !== $new_type ) {
827 382
			$this->updated_props[] = 'product_type';
828 382
			do_action( 'woocommerce_product_type_changed', $product, $old_type, $new_type );
829
		}
830
	}
831
832
	/**
833
	 * Clear any caches.
834
	 *
835
	 * @param WC_Product $product Product object.
836
	 * @since 3.0.0
837
	 */
838
	protected function clear_caches( &$product ) {
839
		wc_delete_product_transients( $product->get_id() );
840
		if ( $product->get_parent_id( 'edit' ) ) {
841
			wc_delete_product_transients( $product->get_parent_id( 'edit' ) );
842
			WC_Cache_Helper::incr_cache_prefix( 'product_' . $product->get_parent_id( 'edit' ) );
843
		}
844 4
		WC_Cache_Helper::invalidate_attribute_count( array_keys( $product->get_attributes() ) );
845
		WC_Cache_Helper::incr_cache_prefix( 'product_' . $product->get_id() );
846
	}
847 4
848 4
	/*
849 4
	|--------------------------------------------------------------------------
850 4
	| wc-product-functions.php methods
851 4
	|--------------------------------------------------------------------------
852 4
	*/
853
854 4
	/**
855
	 * Returns an array of on sale products, as an array of objects with an
856
	 * ID and parent_id present. Example: $return[0]->id, $return[0]->parent_id.
857
	 *
858 4
	 * @return array
859
	 * @since 3.0.0
860
	 */
861
	public function get_on_sale_products() {
862
		global $wpdb;
863
864 4
		$decimals                    = absint( wc_get_price_decimals() );
865 4
		$exclude_term_ids            = array();
866
		$outofstock_join             = '';
867
		$outofstock_where            = '';
868
		$non_published_where         = '';
869
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();
870 4
871 1 View Code Duplication
		if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) {
872
			$exclude_term_ids[] = $product_visibility_term_ids['outofstock'];
873
		}
874 4
875
		if ( count( $exclude_term_ids ) ) {
876 4
			$outofstock_join  = " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = id';
877 4
			$outofstock_where = ' AND exclude_join.object_id IS NULL';
878 4
		}
879 4
880 4
		// Fetch a list of non-published parent products and exlude them, quicker than joining in the main query below.
881
		$non_published_products = $wpdb->get_col(
882
			"SELECT post.ID as id FROM `$wpdb->posts` AS post
883
			WHERE post.post_type = 'product'
884
			AND post.post_parent = 0
885
			AND post.post_status != 'publish'"
886
		);
887
		if ( 0 < count( $non_published_products ) ) {
888 4
			$non_published_where = ' AND post.post_parent NOT IN ( ' . implode( ',', $non_published_products ) . ')';
889 4
		}
890
891 4
		return $wpdb->get_results(
892 4
			// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
893
			$wpdb->prepare(
894
				"SELECT post.ID as id, post.post_parent as parent_id FROM `$wpdb->posts` AS post
895
				LEFT JOIN `$wpdb->postmeta` AS meta ON post.ID = meta.post_id
896
				LEFT JOIN `$wpdb->postmeta` AS meta2 ON post.ID = meta2.post_id
897
				$outofstock_join
898
				WHERE post.post_type IN ( 'product', 'product_variation' )
899
					AND post.post_status = 'publish'
900
					AND meta.meta_key = '_sale_price'
901
					AND meta2.meta_key = '_price'
902
					AND CAST( meta.meta_value AS DECIMAL ) >= 0
903
					AND CAST( meta.meta_value AS CHAR ) != ''
904
					AND CAST( meta.meta_value AS DECIMAL( 10, %d ) ) = CAST( meta2.meta_value AS DECIMAL( 10, %d ) )
905
					$outofstock_where
906 2
					$non_published_where
907 2
				GROUP BY post.ID",
908
				$decimals,
909 2
				$decimals
910
			)
911 2
			// phpcs:enable
912
		);
913 2
	}
914
915
	/**
916 2
	 * Returns a list of product IDs ( id as key => parent as value) that are
917
	 * featured. Uses get_posts instead of wc_get_products since we want
918 2
	 * some extra meta queries and ALL products (posts_per_page = -1).
919 2
	 *
920 2
	 * @return array
921
	 * @since 3.0.0
922
	 */
923 2
	public function get_featured_product_ids() {
924 2
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();
925 2
926 2
		return get_posts(
927
			array(
928
				'post_type'      => array( 'product', 'product_variation' ),
929 2
				'posts_per_page' => -1,
930
				'post_status'    => 'publish',
931
				'tax_query'      => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
932
					'relation' => 'AND',
933
					array(
934
						'taxonomy' => 'product_visibility',
935
						'field'    => 'term_taxonomy_id',
936
						'terms'    => array( $product_visibility_term_ids['featured'] ),
937
					),
938
					array(
939
						'taxonomy' => 'product_visibility',
940
						'field'    => 'term_taxonomy_id',
941
						'terms'    => array( $product_visibility_term_ids['exclude-from-catalog'] ),
942 328
						'operator' => 'NOT IN',
943
					),
944
				),
945
				'fields'         => 'id=>parent',
946 328
			)
947 328
		);
948 328
	}
949 328
950 328
	/**
951 328
	 * Check if product sku is found for any other product IDs.
952 328
	 *
953 328
	 * @since 3.0.0
954 328
	 * @param int    $product_id Product ID.
955
	 * @param string $sku Will be slashed to work around https://core.trac.wordpress.org/ticket/27421.
956 328
	 * @return bool
957
	 */
958
	public function is_existing_sku( $product_id, $sku ) {
959
		global $wpdb;
960
961
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
962
		return $wpdb->get_var(
963
			$wpdb->prepare(
964
				"SELECT $wpdb->posts.ID
965
				FROM $wpdb->posts
966
				LEFT JOIN $wpdb->postmeta ON ( $wpdb->posts.ID = $wpdb->postmeta.post_id )
967
				WHERE $wpdb->posts.post_type IN ( 'product', 'product_variation' )
968 82
					AND $wpdb->posts.post_status != 'trash'
969
					AND $wpdb->postmeta.meta_key = '_sku' AND $wpdb->postmeta.meta_value = %s
970
					AND $wpdb->postmeta.post_id <> %d
971
				LIMIT 1",
972 82
				wp_slash( $sku ),
973 82
				$product_id
974
			)
975 82
		);
976 82
	}
977
978
	/**
979
	 * Return product ID based on SKU.
980
	 *
981
	 * @since 3.0.0
982 82
	 * @param string $sku Product SKU.
983
	 * @return int
984
	 */
985 View Code Duplication
	public function get_product_id_by_sku( $sku ) {
986 82
		global $wpdb;
987
988
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
989
		$id = $wpdb->get_var(
990
			$wpdb->prepare(
991
				"SELECT posts.ID
992
				FROM $wpdb->posts AS posts
993
				LEFT JOIN $wpdb->postmeta AS postmeta ON ( posts.ID = postmeta.post_id )
994
				WHERE posts.post_type IN ( 'product', 'product_variation' )
995
					AND posts.post_status != 'trash'
996
					AND postmeta.meta_key = '_sku'
997
					AND postmeta.meta_value = %s
998
				LIMIT 1",
999
				$sku
1000
			)
1001
		);
1002
1003
		return (int) apply_filters( 'woocommerce_get_product_id_by_sku', $id, $sku );
1004
	}
1005
1006
	/**
1007
	 * Returns an array of IDs of products that have sales starting soon.
1008
	 *
1009
	 * @since 3.0.0
1010
	 * @return array
1011
	 */
1012 View Code Duplication
	public function get_starting_sales() {
1013
		global $wpdb;
1014
1015
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1016
		return $wpdb->get_col(
1017
			$wpdb->prepare(
1018
				"SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta
1019
				LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id
1020
				LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id
1021
				WHERE postmeta.meta_key = '_sale_price_dates_from'
1022
					AND postmeta_2.meta_key = '_price'
1023
					AND postmeta_3.meta_key = '_sale_price'
1024
					AND postmeta.meta_value > 0
1025
					AND postmeta.meta_value < %s
1026
					AND postmeta_2.meta_value != postmeta_3.meta_value",
1027
				current_time( 'timestamp', true )
1028
			)
1029
		);
1030
	}
1031
1032
	/**
1033
	 * Returns an array of IDs of products that have sales which are due to end.
1034
	 *
1035
	 * @since 3.0.0
1036
	 * @return array
1037
	 */
1038 View Code Duplication
	public function get_ending_sales() {
1039
		global $wpdb;
1040
1041
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1042
		return $wpdb->get_col(
1043
			$wpdb->prepare(
1044
				"SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta
1045
				LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id
1046
				LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id
1047
				WHERE postmeta.meta_key = '_sale_price_dates_to'
1048
					AND postmeta_2.meta_key = '_price'
1049
					AND postmeta_3.meta_key = '_regular_price'
1050
					AND postmeta.meta_value > 0
1051
					AND postmeta.meta_value < %s
1052
					AND postmeta_2.meta_value != postmeta_3.meta_value",
1053
				current_time( 'timestamp', true ) - DAY_IN_SECONDS
1054
			)
1055
		);
1056
	}
1057
1058
	/**
1059
	 * Find a matching (enabled) variation within a variable product.
1060
	 *
1061
	 * @since  3.0.0
1062
	 * @param  WC_Product $product Variable product.
1063
	 * @param  array      $match_attributes Array of attributes we want to try to match.
1064
	 * @return int Matching variation ID or 0.
1065
	 */
1066
	public function find_matching_product_variation( $product, $match_attributes = array() ) {
1067
		global $wpdb;
1068
1069
		$meta_attribute_names = array();
1070
1071
		// Get attributes to match in meta.
1072
		foreach ( $product->get_attributes() as $attribute ) {
1073
			if ( ! $attribute->get_variation() ) {
1074
				continue;
1075
			}
1076
1077
			$attribute_field_name = 'attribute_' . sanitize_title( $attribute->get_name() );
1078
1079
			if ( ! isset( $match_attributes[ $attribute_field_name ] ) ) {
1080
				return 0;
1081
			}
1082
1083
			$meta_attribute_names[] = $attribute_field_name;
1084
		}
1085
1086
		// Get the attributes of the variations.
1087
		$query = $wpdb->prepare(
1088
			"
1089
			SELECT post_id, meta_key, meta_value FROM {$wpdb->postmeta}
1090
			WHERE post_id IN (
1091
				SELECT ID FROM {$wpdb->posts}
1092
				WHERE {$wpdb->posts}.post_parent = %d
1093
				AND {$wpdb->posts}.post_status = 'publish'
1094
				AND {$wpdb->posts}.post_type = 'product_variation'
1095
				ORDER BY menu_order ASC, ID ASC
1096
			)
1097
			",
1098
			$product->get_id()
1099
		);
1100
1101
		$query .= ' AND meta_key IN ( "' . implode( '","', array_map( 'esc_sql', $meta_attribute_names ) ) . '" );';
1102
1103
		$attributes = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
0 ignored issues
show
Usage of a direct database call is discouraged.
Loading history...
Usage of a direct database call without caching is prohibited. Use wp_cache_get / wp_cache_set.
Loading history...
1104
1105
		if ( ! $attributes ) {
1106
			return 0;
1107
		}
1108
1109
		$sorted_meta = array();
1110
1111
		foreach ( $attributes as $m ) {
1112
			$sorted_meta[ $m->post_id ][ $m->meta_key ] = $m->meta_value; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
0 ignored issues
show
Detected usage of meta_key, possible slow query.
Loading history...
1113
		}
1114
1115
		/**
1116
		 * Check each variation to find the one that matches the $match_attributes.
1117
		 *
1118
		 * Note: Not all meta fields will be set which is why we check existance.
1119
		 */
1120
		foreach ( $sorted_meta as $variation_id => $variation ) {
1121
			$match = true;
1122
1123
			foreach ( $match_attributes as $attribute_key => $attribute_value ) {
1124
				if ( array_key_exists( $attribute_key, $variation ) ) {
1125
					if ( $variation[ $attribute_key ] !== $attribute_value && ! empty( $variation[ $attribute_key ] ) ) {
1126
						$match = false;
1127
					}
1128
				}
1129
			}
1130
1131
			if ( true === $match ) {
1132
				return $variation_id;
1133
			}
1134
		}
1135
1136
		if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) {
1137
			/**
1138
			 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
1139
			 * Fallback is here because there are cases where data will be 'synced' but the product version will remain the same.
1140
			 */
1141 20
			return ( array_map( 'sanitize_title', $match_attributes ) === $match_attributes ) ? 0 : $this->find_matching_product_variation( $product, array_map( 'sanitize_title', $match_attributes ) );
1142
		}
1143
	}
1144
1145 20
	/**
1146 20
	 * Make sure all variations have a sort order set so they can be reordered correctly.
1147 20
	 *
1148 20
	 * @param int $parent_id Product ID.
1149
	 */
1150
	public function sort_all_product_variations( $parent_id ) {
1151 20
		global $wpdb;
1152
1153
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1154 20
		$ids   = $wpdb->get_col(
1155
			$wpdb->prepare(
1156
				"SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product_variation' AND post_parent = %d AND post_status = 'publish' ORDER BY menu_order ASC, ID ASC",
1157
				$parent_id
1158
			)
1159
		);
1160
		$index = 1;
1161
1162
		foreach ( $ids as $id ) {
1163
			// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1164
			$wpdb->update( $wpdb->posts, array( 'menu_order' => ( $index++ ) ), array( 'ID' => absint( $id ) ) );
1165
		}
1166
	}
1167
1168
	/**
1169 20
	 * Return a list of related products (using data like categories and IDs).
1170
	 *
1171
	 * @since 3.0.0
1172 20
	 * @param array $cats_array  List of categories IDs.
1173 20
	 * @param array $tags_array  List of tags IDs.
1174 20
	 * @param array $exclude_ids Excluded IDs.
1175
	 * @param int   $limit       Limit of results.
1176 20
	 * @param int   $product_id  Product ID.
1177 20
	 * @return array
1178
	 */
1179
	public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ) {
1180 20
		global $wpdb;
1181
1182
		$args = array(
1183
			'categories'  => $cats_array,
1184
			'tags'        => $tags_array,
1185
			'exclude_ids' => $exclude_ids,
1186 20
			'limit'       => $limit + 10,
1187
		);
1188 20
1189 20
		$related_product_query = (array) apply_filters( 'woocommerce_product_related_posts_query', $this->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit + 10 ), $product_id, $args );
1190
1191
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
1192
		return $wpdb->get_col( implode( ' ', $related_product_query ) );
1193
	}
1194
1195
	/**
1196 20
	 * Builds the related posts query.
1197
	 *
1198
	 * @since 3.0.0
1199
	 *
1200 20
	 * @param array $cats_array  List of categories IDs.
1201 20
	 * @param array $tags_array  List of tags IDs.
1202 20
	 * @param array $exclude_ids Excluded IDs.
1203
	 * @param int   $limit       Limit of results.
1204
	 *
1205 20
	 * @return array
1206 20
	 */
1207
	public function get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ) {
1208
		global $wpdb;
1209 20
1210 20
		$include_term_ids            = array_merge( $cats_array, $tags_array );
1211
		$exclude_term_ids            = array();
1212
		$product_visibility_term_ids = wc_get_product_visibility_term_ids();
1213 20
1214
		if ( $product_visibility_term_ids['exclude-from-catalog'] ) {
1215
			$exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog'];
1216
		}
1217
1218 View Code Duplication
		if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) {
1219
			$exclude_term_ids[] = $product_visibility_term_ids['outofstock'];
1220
		}
1221
1222
		$query = array(
1223
			'fields' => "
1224
				SELECT DISTINCT ID FROM {$wpdb->posts} p
1225
			",
1226 2
			'join'   => '',
1227
			'where'  => "
1228 2
				WHERE 1=1
1229
				AND p.post_status = 'publish'
1230
				AND p.post_type = 'product'
1231
1232 2
			",
1233
			'limits' => '
1234 1
				LIMIT ' . absint( $limit ) . '
1235 1
			',
1236 1
		);
1237
1238 View Code Duplication
		if ( count( $exclude_term_ids ) ) {
1239 1
			$query['join']  .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID';
1240 2
			$query['where'] .= ' AND exclude_join.object_id IS NULL';
1241
		}
1242 1
1243 1 View Code Duplication
		if ( count( $include_term_ids ) ) {
1244 1
			$query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $include_term_ids ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID';
1245
		}
1246
1247 1
		if ( count( $exclude_ids ) ) {
1248
			$query['where'] .= ' AND p.ID NOT IN ( ' . implode( ',', array_map( 'absint', $exclude_ids ) ) . ' )';
1249
		}
1250 2
1251 2
		return $query;
1252 2
	}
1253
1254
	/**
1255 2
	 * Update a product's stock amount directly.
1256
	 *
1257
	 * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues).
1258 2
	 *
1259
	 * @since  3.0.0 this supports set, increase and decrease.
1260
	 * @param  int      $product_id_with_stock Product ID.
1261
	 * @param  int|null $stock_quantity Stock quantity.
1262
	 * @param  string   $operation Set, increase and decrease.
1263
	 */
1264
	public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ) {
1265
		global $wpdb;
1266
		add_post_meta( $product_id_with_stock, '_stock', 0, true );
1267
1268
		// Update stock in DB directly.
1269
		switch ( $operation ) {
1270
			case 'increase':
1271 13
				$sql = $wpdb->prepare(
1272
					"UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='_stock'",
1273 13
					$stock_quantity,
1274
					$product_id_with_stock
1275
				);
1276
				break;
1277 13
			case 'decrease':
1278
				$sql = $wpdb->prepare(
1279 13
					"UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='_stock'",
1280 13
					$stock_quantity,
1281 13
					$product_id_with_stock
1282
				);
1283
				break;
1284 13
			default:
1285
				$sql = $wpdb->prepare(
1286
					"UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'",
1287
					$stock_quantity,
1288
					$product_id_with_stock
1289
				);
1290
				break;
1291
		}
1292
1293
		$sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $stock_quantity, $operation );
1294
1295
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
1296
		$wpdb->query( $sql );
1297
1298
		wp_cache_delete( $product_id_with_stock, 'post_meta' );
1299
	}
1300
1301
	/**
1302
	 * Update a product's sale count directly.
1303 13
	 *
1304
	 * Uses queries rather than update_post_meta so we can do this in one query for performance.
1305
	 *
1306
	 * @since  3.0.0 this supports set, increase and decrease.
1307
	 * @param  int      $product_id Product ID.
1308
	 * @param  int|null $quantity Quantity.
1309
	 * @param  string   $operation set, increase and decrease.
1310
	 */
1311
	public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ) {
1312 22
		global $wpdb;
1313 22
		add_post_meta( $product_id, 'total_sales', 0, true );
1314 22
1315
		// Update stock in DB directly.
1316
		switch ( $operation ) {
1317
			case 'increase':
1318
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1319
				$wpdb->query(
1320
					$wpdb->prepare(
1321
						"UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='total_sales'",
1322
						$quantity,
1323 22
						$product_id
1324 22
					)
1325
				);
1326
				break;
1327
			case 'decrease':
1328
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1329
				$wpdb->query(
1330
					$wpdb->prepare(
1331
						"UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='total_sales'",
1332
						$quantity,
1333 22
						$product_id
1334 22
					)
1335
				);
1336
				break;
1337
			default:
1338
				// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1339
				$wpdb->query(
1340
					$wpdb->prepare(
1341
						"UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='total_sales'",
1342
						$quantity,
1343
						$product_id
1344 2
					)
1345 2
				);
1346 2
				break;
1347 2
		}
1348
1349
		wp_cache_delete( $product_id, 'post_meta' );
1350
	}
1351
1352
	/**
1353
	 * Update a products average rating meta.
1354
	 *
1355
	 * @since 3.0.0
1356
	 * @param WC_Product $product Product object.
1357
	 */
1358
	public function update_average_rating( $product ) {
1359
		update_post_meta( $product->get_id(), '_wc_average_rating', $product->get_average_rating( 'edit' ) );
1360
		self::update_visibility( $product, true );
1361
	}
1362
1363
	/**
1364
	 * Update a products review count meta.
1365
	 *
1366
	 * @since 3.0.0
1367
	 * @param WC_Product $product Product object.
1368
	 */
1369
	public function update_review_count( $product ) {
1370
		update_post_meta( $product->get_id(), '_wc_review_count', $product->get_review_count( 'edit' ) );
1371
	}
1372
1373
	/**
1374
	 * Update a products rating counts.
1375 1
	 *
1376
	 * @since 3.0.0
1377
	 * @param WC_Product $product Product object.
1378 1
	 */
1379 1
	public function update_rating_counts( $product ) {
1380 1
		update_post_meta( $product->get_id(), '_wc_rating_count', $product->get_rating_counts( 'edit' ) );
1381 1
	}
1382 1
1383 1
	/**
1384 1
	 * Get shipping class ID by slug.
1385
	 *
1386
	 * @since 3.0.0
1387 1
	 * @param string $slug Product shipping class slug.
1388 1
	 * @return int|false
1389
	 */
1390 1
	public function get_shipping_class_id_by_slug( $slug ) {
1391
		$shipping_class_term = get_term_by( 'slug', $slug, 'product_shipping_class' );
1392
		if ( $shipping_class_term ) {
1393 1
			return $shipping_class_term->term_id;
1394 1
		} else {
1395
			return false;
1396 1
		}
1397
	}
1398 1
1399 1
	/**
1400 1
	 * Returns an array of products.
1401
	 *
1402
	 * @param  array $args Args to pass to WC_Product_Query().
1403 1
	 * @return array|object
1404 1
	 * @see wc_get_products
1405
	 */
1406
	public function get_products( $args = array() ) {
1407
		$query = new WC_Product_Query( $args );
1408
		return $query->get_products();
1409
	}
1410 1
1411 1
	/**
1412
	 * Search product data for a term and return ids.
1413 1
	 *
1414 1
	 * @param  string   $term Search term.
1415 1
	 * @param  string   $type Type of product.
1416 1
	 * @param  bool     $include_variations Include variations in search or not.
1417
	 * @param  bool     $all_statuses Should we search all statuses or limit to published.
1418
	 * @param  null|int $limit Limit returned results. @since 3.5.0.
1419 1
	 * @return array of ids
1420 1
	 */
1421
	public function search_products( $term, $type = '', $include_variations = false, $all_statuses = false, $limit = null ) {
1422
		global $wpdb;
1423
1424 1
		$custom_results = apply_filters( 'woocommerce_product_pre_search_products', false, $term, $type, $include_variations, $all_statuses, $limit );
1425 1
1426
		if ( is_array( $custom_results ) ) {
1427
			return $custom_results;
1428 1
		}
1429
1430
		$post_types    = $include_variations ? array( 'product', 'product_variation' ) : array( 'product' );
1431
		$post_statuses = current_user_can( 'edit_private_products' ) ? array( 'private', 'publish' ) : array( 'publish' );
1432
		$type_join     = '';
1433 1
		$type_where    = '';
1434
		$status_where  = '';
1435
		$limit_query   = '';
1436
		$term          = wc_strtolower( $term );
1437 1
1438
		// See if search term contains OR keywords.
1439
		if ( strstr( $term, ' or ' ) ) {
1440
			$term_groups = explode( ' or ', $term );
1441
		} else {
1442 1
			$term_groups = array( $term );
1443
		}
1444 1
1445 1
		$search_where   = '';
1446 1
		$search_queries = array();
1447 1
1448 1
		foreach ( $term_groups as $term_group ) {
1449 1
			// Parse search terms.
1450 1
			if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $term_group, $matches ) ) {
1451
				$search_terms = $this->get_valid_search_terms( $matches[0] );
1452 1
				$count        = count( $search_terms );
1453
1454
				// if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence.
1455
				if ( 9 < $count || 0 === $count ) {
1456
					$search_terms = array( $term_group );
1457 1
				}
1458
			} else {
1459 1
				$search_terms = array( $term_group );
1460
			}
1461
1462
			$term_group_query = '';
1463
			$searchand        = '';
1464
1465
			foreach ( $search_terms as $search_term ) {
1466
				$like              = '%' . $wpdb->esc_like( $search_term ) . '%';
1467
				$term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( postmeta.meta_key = '_sku' AND postmeta.meta_value LIKE %s ) )", $like, $like, $like, $like ); // @codingStandardsIgnoreLine.
1468
				$searchand         = ' AND ';
1469
			}
1470
1471
			if ( $term_group_query ) {
1472 1
				$search_queries[] = $term_group_query;
1473
			}
1474
		}
1475
1476
		if ( ! empty( $search_queries ) ) {
1477
			$search_where = 'AND (' . implode( ') OR (', $search_queries ) . ')';
1478
		}
1479
1480
		if ( $type && in_array( $type, array( 'virtual', 'downloadable' ), true ) ) {
1481
			$type_join  = " LEFT JOIN {$wpdb->postmeta} postmeta_type ON posts.ID = postmeta_type.post_id ";
1482 384
			$type_where = " AND ( postmeta_type.meta_key = '_{$type}' AND postmeta_type.meta_value = 'yes' ) ";
1483 384
		}
1484 384
1485 60
		if ( ! $all_statuses ) {
1486 383
			$status_where = " AND posts.post_status IN ('" . implode( "','", $post_statuses ) . "') ";
1487 381
		}
1488 381
1489
		if ( $limit ) {
1490 6
			$limit_query = $wpdb->prepare( ' LIMIT %d ', $limit );
1491
		}
1492
1493
		// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
1494
		$search_results = $wpdb->get_results(
1495
			// phpcs:disable
1496
			"SELECT DISTINCT posts.ID as product_id, posts.post_parent as parent_id FROM {$wpdb->posts} posts
1497
			LEFT JOIN {$wpdb->postmeta} postmeta ON posts.ID = postmeta.post_id
1498
			$type_join
1499
			WHERE posts.post_type IN ('" . implode( "','", $post_types ) . "')
1500
			$search_where
1501
			$status_where
1502 1
			$type_where
1503
			ORDER BY posts.post_parent ASC, posts.post_title ASC
1504
			$limit_query
1505 1
			"
1506 1
			// phpcs:enable
1507 1
		);
1508
1509 1
		$product_ids = wp_parse_id_list( array_merge( wp_list_pluck( $search_results, 'product_id' ), wp_list_pluck( $search_results, 'parent_id' ) ) );
1510
1511
		if ( is_numeric( $term ) ) {
1512
			$post_id   = absint( $term );
1513 1
			$post_type = get_post_type( $post_id );
1514
1515
			if ( 'product_variation' === $post_type && $include_variations ) {
1516
				$product_ids[] = $post_id;
1517
			} elseif ( 'product' === $post_type ) {
1518
				$product_ids[] = $post_id;
1519
			}
1520
1521
			$product_ids[] = wp_get_post_parent_id( $post_id );
1522
		}
1523 14
1524
		return wp_parse_id_list( $product_ids );
1525
	}
1526
1527 14
	/**
1528
	 * Get the product type based on product ID.
1529
	 *
1530
	 * @since 3.0.0
1531
	 * @param int $product_id Product ID.
1532
	 * @return bool|string
1533
	 */
1534 14
	public function get_product_type( $product_id ) {
1535 14
		$cache_key    = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . '_type_' . $product_id;
1536 14
		$product_type = wp_cache_get( $cache_key, 'products' );
1537 14
1538
		if ( $product_type ) {
1539
			return $product_type;
1540
		}
1541
1542
		$post_type = get_post_type( $product_id );
1543 14
1544
		if ( 'product_variation' === $post_type ) {
1545
			$product_type = 'variation';
1546
		} elseif ( 'product' === $post_type ) {
1547
			$terms        = get_the_terms( $product_id, 'product_type' );
1548 14
			$product_type = ! empty( $terms ) ? sanitize_title( current( $terms )->name ) : 'simple';
1549 14
		} else {
1550 14
			$product_type = false;
1551
		}
1552
1553
		wp_cache_set( $cache_key, $product_type, 'products' );
1554
1555
		return $product_type;
1556 14
	}
1557
1558
	/**
1559
	 * Add ability to get products by 'reviews_allowed' in WC_Product_Query.
1560 14
	 *
1561 14
	 * @since 3.2.0
1562 14
	 * @param string   $where Where clause.
1563 14
	 * @param WP_Query $wp_query WP_Query instance.
1564
	 * @return string
1565
	 */
1566
	public function reviews_allowed_query_where( $where, $wp_query ) {
1567 14
		global $wpdb;
1568
1569 14
		if ( isset( $wp_query->query_vars['reviews_allowed'] ) && is_bool( $wp_query->query_vars['reviews_allowed'] ) ) {
1570 14
			if ( $wp_query->query_vars['reviews_allowed'] ) {
1571
				$where .= " AND $wpdb->posts.comment_status = 'open'";
1572 14
			} else {
1573
				$where .= " AND $wpdb->posts.comment_status = 'closed'";
1574
			}
1575
		}
1576
1577
		return $where;
1578 14
	}
1579 1
1580 14
	/**
1581 1
	 * Get valid WP_Query args from a WC_Product_Query's query variables.
1582 1
	 *
1583 1
	 * @since 3.2.0
1584
	 * @param array $query_vars Query vars from a WC_Product_Query.
1585 1
	 * @return array
1586 1
	 */
1587 1
	protected function get_wp_query_args( $query_vars ) {
1588
1589
		// Map query vars to ones that get_wp_query_args or WP_Query recognize.
1590
		$key_mapping = array(
1591
			'status'         => 'post_status',
1592
			'page'           => 'paged',
1593
			'include'        => 'post__in',
1594
			'stock_quantity' => 'stock',
1595
			'average_rating' => 'wc_average_rating',
1596 13
			'review_count'   => 'wc_review_count',
1597 13
		);
1598 13 View Code Duplication
		foreach ( $key_mapping as $query_key => $db_key ) {
1599 13
			if ( isset( $query_vars[ $query_key ] ) ) {
1600 13
				$query_vars[ $db_key ] = $query_vars[ $query_key ];
1601
				unset( $query_vars[ $query_key ] );
1602
			}
1603
		}
1604
1605 14
		// Map boolean queries that are stored as 'yes'/'no' in the DB to 'yes' or 'no'.
1606 1
		$boolean_queries = array(
1607 1
			'virtual',
1608 1
			'downloadable',
1609 1
			'sold_individually',
1610
			'manage_stock',
1611
		);
1612
		foreach ( $boolean_queries as $boolean_query ) {
1613
			if ( isset( $query_vars[ $boolean_query ] ) && '' !== $query_vars[ $boolean_query ] ) {
1614 14
				$query_vars[ $boolean_query ] = $query_vars[ $boolean_query ] ? 'yes' : 'no';
1615 1
			}
1616 1
		}
1617 1
1618 1
		// These queries cannot be auto-generated so we have to remove them and build them manually.
1619 1
		$manual_queries = array(
1620
			'sku'        => '',
1621
			'featured'   => '',
1622
			'visibility' => '',
1623
		);
1624 14
		foreach ( $manual_queries as $key => $manual_query ) {
1625 1
			if ( isset( $query_vars[ $key ] ) ) {
1626 1
				$manual_queries[ $key ] = $query_vars[ $key ];
1627 1
				unset( $query_vars[ $key ] );
1628 1
			}
1629
		}
1630
1631
		$wp_query_args = parent::get_wp_query_args( $query_vars );
1632
1633
		if ( ! isset( $wp_query_args['date_query'] ) ) {
1634 14
			$wp_query_args['date_query'] = array();
1635 1
		}
1636 1
		if ( ! isset( $wp_query_args['meta_query'] ) ) {
1637 1
			$wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
1638 1
		}
1639
1640
		// Handle product types.
1641
		if ( 'variation' === $query_vars['type'] ) {
1642
			$wp_query_args['post_type'] = 'product_variation';
1643 14
		} elseif ( is_array( $query_vars['type'] ) && in_array( 'variation', $query_vars['type'], true ) ) {
1644
			$wp_query_args['post_type']   = array( 'product_variation', 'product' );
1645 2
			$wp_query_args['tax_query'][] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1646 1
				'relation' => 'OR',
1647
				array(
1648 1
					'taxonomy' => 'product_type',
1649
					'field'    => 'slug',
1650
					'terms'    => $query_vars['type'],
1651
				),
1652
				array(
1653
					'taxonomy' => 'product_type',
1654
					'field'    => 'id',
1655
					'operator' => 'NOT EXISTS',
1656
				),
1657
			);
1658 2 View Code Duplication
		} else {
1659 2
			$wp_query_args['post_type']   = 'product';
1660 2
			$wp_query_args['tax_query'][] = array(
1661 2
				'taxonomy' => 'product_type',
1662
				'field'    => 'slug',
1663
				'terms'    => $query_vars['type'],
1664
			);
1665
		}
1666
1667 14
		// Handle product categories.
1668 1 View Code Duplication
		if ( ! empty( $query_vars['category'] ) ) {
1669 1
			$wp_query_args['tax_query'][] = array(
1670 1
				'taxonomy' => 'product_cat',
1671 1
				'field'    => 'slug',
1672 1
				'terms'    => $query_vars['category'],
1673 1
			);
1674
		}
1675 1
1676 1
		// Handle product tags.
1677 1 View Code Duplication
		if ( ! empty( $query_vars['tag'] ) ) {
1678 1
			unset( $wp_query_args['tag'] );
1679 1
			$wp_query_args['tax_query'][] = array(
1680
				'taxonomy' => 'product_tag',
1681
				'field'    => 'slug',
1682 1
				'terms'    => $query_vars['tag'],
1683 1
			);
1684 1
		}
1685 1
1686 1
		// Handle shipping classes.
1687 View Code Duplication
		if ( ! empty( $query_vars['shipping_class'] ) ) {
1688
			$wp_query_args['tax_query'][] = array(
1689
				'taxonomy' => 'product_shipping_class',
1690
				'field'    => 'slug',
1691
				'terms'    => $query_vars['shipping_class'],
1692 14
			);
1693 1
		}
1694 1
1695 1
		// Handle total_sales.
1696
		// This query doesn't get auto-generated since the meta key doesn't have the underscore prefix.
1697
		if ( isset( $query_vars['total_sales'] ) && '' !== $query_vars['total_sales'] ) {
1698
			$wp_query_args['meta_query'][] = array(
1699
				'key'     => 'total_sales',
1700
				'value'   => absint( $query_vars['total_sales'] ),
1701 1
				'compare' => '=',
1702 1
			);
1703
		}
1704
1705
		// Handle SKU.
1706
		if ( $manual_queries['sku'] ) {
1707
			// Check for existing values if wildcard is used.
1708
			if ( '*' === $manual_queries['sku'] ) {
1709
				$wp_query_args['meta_query'][] = array(
1710 1
					array(
1711 1
						'key'     => '_sku',
1712
						'compare' => 'EXISTS',
1713
					),
1714
					array(
1715
						'key'     => '_sku',
1716
						'value'   => '',
1717 1
						'compare' => '!=',
1718 1
					),
1719 1
				);
1720
			} else {
1721
				$wp_query_args['meta_query'][] = array(
1722
					'key'     => '_sku',
1723
					'value'   => $manual_queries['sku'],
1724
					'compare' => 'LIKE',
1725 1
				);
1726
			}
1727
		}
1728
1729
		// Handle featured.
1730
		if ( '' !== $manual_queries['featured'] ) {
1731 14
			$product_visibility_term_ids = wc_get_product_visibility_term_ids();
1732
			if ( $manual_queries['featured'] ) {
1733
				$wp_query_args['tax_query'][] = array(
1734
					'taxonomy' => 'product_visibility',
1735
					'field'    => 'term_taxonomy_id',
1736 14
					'terms'    => array( $product_visibility_term_ids['featured'] ),
1737 14
				);
1738
				$wp_query_args['tax_query'][] = array(
1739
					'taxonomy' => 'product_visibility',
1740 1
					'field'    => 'term_taxonomy_id',
1741 1
					'terms'    => array( $product_visibility_term_ids['exclude-from-catalog'] ),
1742
					'operator' => 'NOT IN',
1743
				);
1744
			} else {
1745 14
				$wp_query_args['tax_query'][] = array(
1746
					'taxonomy' => 'product_visibility',
1747
					'field'    => 'term_taxonomy_id',
1748
					'terms'    => array( $product_visibility_term_ids['featured'] ),
1749
					'operator' => 'NOT IN',
1750 14
				);
1751 13
			}
1752
		}
1753
1754
		// Handle visibility.
1755 14
		if ( $manual_queries['visibility'] ) {
1756 1
			switch ( $manual_queries['visibility'] ) {
1757 View Code Duplication
				case 'search':
1758
					$wp_query_args['tax_query'][] = array(
1759 14
						'taxonomy' => 'product_visibility',
1760
						'field'    => 'slug',
1761
						'terms'    => array( 'exclude-from-search' ),
1762
						'operator' => 'NOT IN',
1763
					);
1764
					break;
1765 View Code Duplication
				case 'catalog':
1766
					$wp_query_args['tax_query'][] = array(
1767
						'taxonomy' => 'product_visibility',
1768
						'field'    => 'slug',
1769
						'terms'    => array( 'exclude-from-catalog' ),
1770
						'operator' => 'NOT IN',
1771 14
					);
1772 14
					break;
1773 View Code Duplication
				case 'visible':
1774 14
					$wp_query_args['tax_query'][] = array(
1775
						'taxonomy' => 'product_visibility',
1776
						'field'    => 'slug',
1777
						'terms'    => array( 'exclude-from-catalog', 'exclude-from-search' ),
1778
						'operator' => 'NOT IN',
1779
					);
1780
					break;
1781 14 View Code Duplication
				case 'hidden':
1782
					$wp_query_args['tax_query'][] = array(
1783
						'taxonomy' => 'product_visibility',
1784 14
						'field'    => 'slug',
1785
						'terms'    => array( 'exclude-from-catalog', 'exclude-from-search' ),
1786 4
						'operator' => 'AND',
1787
					);
1788
					break;
1789 14
			}
1790
		}
1791 14
1792
		// Handle date queries.
1793 2
		$date_queries = array(
1794 2
			'date_created'      => 'post_date',
1795 2
			'date_modified'     => 'post_modified',
1796
			'date_on_sale_from' => '_sale_price_dates_from',
1797
			'date_on_sale_to'   => '_sale_price_dates_to',
1798
		);
1799 13
		foreach ( $date_queries as $query_var_key => $db_key ) {
1800
			if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) {
1801
1802
				// Remove any existing meta queries for the same keys to prevent conflicts.
1803
				$existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true );
1804
				foreach ( $existing_queries as $query_index => $query_contents ) {
1805
					unset( $wp_query_args['meta_query'][ $query_index ] );
1806
				}
1807
1808
				$wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args );
1809
			}
1810
		}
1811
1812
		// Handle paginate.
1813 View Code Duplication
		if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) {
1814
			$wp_query_args['no_found_rows'] = true;
1815
		}
1816
1817
		// Handle reviews_allowed.
1818
		if ( isset( $query_vars['reviews_allowed'] ) && is_bool( $query_vars['reviews_allowed'] ) ) {
1819
			add_filter( 'posts_where', array( $this, 'reviews_allowed_query_where' ), 10, 2 );
1820
		}
1821
1822
		return apply_filters( 'woocommerce_product_data_store_cpt_get_products_query', $wp_query_args, $query_vars, $this );
1823
	}
1824
1825
	/**
1826
	 * Query for Products matching specific criteria.
1827
	 *
1828
	 * @since 3.2.0
1829
	 *
1830
	 * @param array $query_vars Query vars from a WC_Product_Query.
1831
	 *
1832
	 * @return array|object
1833
	 */
1834
	public function query( $query_vars ) {
1835
		$args = $this->get_wp_query_args( $query_vars );
1836
1837 View Code Duplication
		if ( ! empty( $args['errors'] ) ) {
1838
			$query = (object) array(
1839
				'posts'         => array(),
1840
				'found_posts'   => 0,
1841
				'max_num_pages' => 0,
1842
			);
1843
		} else {
1844
			$query = new WP_Query( $args );
1845
		}
1846
1847
		if ( isset( $query_vars['return'] ) && 'objects' === $query_vars['return'] && ! empty( $query->posts ) ) {
1848
			// Prime caches before grabbing objects.
1849
			update_post_caches( $query->posts, array( 'product', 'product_variation' ) );
1850
		}
1851
1852
		$products = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_product', $query->posts ) );
1853
1854 View Code Duplication
		if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) {
1855
			return (object) array(
1856
				'products'      => $products,
1857
				'total'         => $query->found_posts,
1858
				'max_num_pages' => $query->max_num_pages,
1859
			);
1860
		}
1861
1862
		return $products;
1863
	}
1864
}
1865