PluginProbe
WooCommerce / 11.1.0-rc.2
WooCommerce v11.1.0-rc.2
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 All 648 releases
woocommerce / includes / wc-core-functions.php
wc-core-functions.php
2,686 lines 79.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WooCommerce Core Functions
4 *
5 * General core functions available on both the front-end and admin.
6 *
7 * @package WooCommerce\Functions
8 * @version 3.3.0
9 */
10
11 use Automattic\Jetpack\Constants;
12 use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
13 use Automattic\WooCommerce\Enums\DefaultCustomerAddress;
14 use Automattic\WooCommerce\Utilities\NumberUtil;
15 use Automattic\WooCommerce\Internal\Logging\OrderLogsCleanupHelper;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 // Include core functions (available in both admin and frontend).
22 require WC_ABSPATH . 'includes/wc-conditional-functions.php';
23 require WC_ABSPATH . 'includes/wc-coupon-functions.php';
24 require WC_ABSPATH . 'includes/wc-user-functions.php';
25 require WC_ABSPATH . 'includes/wc-deprecated-functions.php';
26 require WC_ABSPATH . 'includes/wc-formatting-functions.php';
27 require WC_ABSPATH . 'includes/wc-order-functions.php';
28 require WC_ABSPATH . 'includes/wc-order-item-functions.php';
29 require WC_ABSPATH . 'includes/wc-page-functions.php';
30 require WC_ABSPATH . 'includes/wc-product-functions.php';
31 require WC_ABSPATH . 'includes/wc-stock-functions.php';
32 require WC_ABSPATH . 'includes/wc-account-functions.php';
33 require WC_ABSPATH . 'includes/wc-term-functions.php';
34 require WC_ABSPATH . 'includes/wc-attribute-functions.php';
35 require WC_ABSPATH . 'includes/wc-rest-functions.php';
36 require WC_ABSPATH . 'includes/wc-widget-functions.php';
37 require WC_ABSPATH . 'includes/wc-webhook-functions.php';
38 require WC_ABSPATH . 'includes/wc-order-step-logger-functions.php';
39 require WC_ABSPATH . 'includes/wc-interactivity-api-functions.php';
40
41 /**
42 * Filters on data used in admin and frontend.
43 */
44 add_filter( 'woocommerce_coupon_code', 'wc_sanitize_coupon_code' );
45 add_filter( 'woocommerce_coupon_code', 'wc_strtolower' );
46 add_filter( 'woocommerce_stock_amount', 'intval' ); // Stock amounts are integers by default.
47 add_filter( 'woocommerce_shipping_rate_label', 'sanitize_text_field' ); // Shipping rate label.
48 add_filter( 'woocommerce_attribute_label', 'wp_kses_post', 100 );
49
50 /**
51 * Short Description (excerpt).
52 */
53 if ( function_exists( 'do_blocks' ) ) {
54 add_filter( 'woocommerce_short_description', 'do_blocks', 9 );
55 }
56 add_filter( 'woocommerce_short_description', 'wptexturize' );
57 add_filter( 'woocommerce_short_description', 'convert_smilies' );
58 add_filter( 'woocommerce_short_description', 'convert_chars' );
59 add_filter( 'woocommerce_short_description', 'wpautop' );
60 add_filter( 'woocommerce_short_description', 'shortcode_unautop' );
61 add_filter( 'woocommerce_short_description', 'prepend_attachment' );
62 add_filter( 'woocommerce_short_description', 'do_shortcode', 11 ); // After wpautop().
63 add_filter( 'woocommerce_short_description', 'wc_format_product_short_description', 9999999 );
64 add_filter( 'woocommerce_short_description', 'wc_do_oembeds' );
65 add_filter( 'woocommerce_short_description', array( $GLOBALS['wp_embed'], 'run_shortcode' ), 8 ); // Before wpautop().
66
67 /**
68 * Define a constant if it is not already defined.
69 *
70 * @since 3.0.0
71 * @param string $name Constant name.
72 * @param mixed $value Value.
73 * @return void
74 */
75 function wc_maybe_define_constant( $name, $value ) {
76 if ( ! defined( $name ) ) {
77 define( $name, $value );
78 }
79 }
80
81 /**
82 * Create a new order programmatically.
83 *
84 * Returns a new order object on success which can then be used to add additional data.
85 *
86 * @param array $args Order arguments.
87 * @return WC_Order|WP_Error
88 */
89 function wc_create_order( $args = array() ) {
90 $default_args = array(
91 'status' => null,
92 'customer_id' => null,
93 'customer_note' => null,
94 'parent' => null,
95 'created_via' => null,
96 'cart_hash' => null,
97 'order_id' => 0,
98 );
99
100 try {
101 $args = wp_parse_args( $args, $default_args );
102 $order = new WC_Order( $args['order_id'] );
103
104 // Update props that were set (not null).
105 if ( ! is_null( $args['parent'] ) ) {
106 $order->set_parent_id( absint( $args['parent'] ) );
107 }
108
109 if ( ! is_null( $args['status'] ) ) {
110 $order->set_status( $args['status'] );
111 }
112
113 if ( ! is_null( $args['customer_note'] ) ) {
114 $order->set_customer_note( $args['customer_note'] );
115 }
116
117 if ( ! is_null( $args['customer_id'] ) ) {
118 $order->set_customer_id( is_numeric( $args['customer_id'] ) ? absint( $args['customer_id'] ) : 0 );
119 }
120
121 if ( ! is_null( $args['created_via'] ) ) {
122 $order->set_created_via( sanitize_text_field( $args['created_via'] ) );
123 }
124
125 if ( ! is_null( $args['cart_hash'] ) ) {
126 $order->set_cart_hash( sanitize_text_field( $args['cart_hash'] ) );
127 }
128
129 // Set these fields when creating a new order but not when updating an existing order.
130 if ( ! $args['order_id'] ) {
131 $order->set_currency( get_woocommerce_currency() );
132 $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
133 $order->set_customer_ip_address( WC_Geolocation::get_ip_address() );
134 $order->set_customer_user_agent( wc_get_user_agent() );
135 }
136
137 // Update other order props set automatically.
138 $order->save();
139 } catch ( Exception $e ) {
140 return new WP_Error( 'error', $e->getMessage() );
141 }
142
143 return $order;
144 }
145
146 /**
147 * Update an order. Uses wc_create_order.
148 *
149 * @param array $args Order arguments.
150 * @return WC_Order|WP_Error
151 */
152 function wc_update_order( $args ) {
153 if ( empty( $args['order_id'] ) ) {
154 return new WP_Error( __( 'Invalid order ID.', 'woocommerce' ) );
155 }
156 return wc_create_order( $args );
157 }
158
159 /**
160 * Given a path, this will convert any of the subpaths into their corresponding tokens.
161 *
162 * @since 4.3.0
163 * @param string $path The absolute path to tokenize.
164 * @param array $path_tokens An array keyed with the token, containing paths that should be replaced.
165 * @return string The tokenized path.
166 */
167 function wc_tokenize_path( $path, $path_tokens ) {
168 // Order most to least specific so that the token can encompass as much of the path as possible.
169 uasort(
170 $path_tokens,
171 function ( $a, $b ) {
172 $a = strlen( $a );
173 $b = strlen( $b );
174
175 if ( $a > $b ) {
176 return -1;
177 }
178
179 if ( $b > $a ) {
180 return 1;
181 }
182
183 return 0;
184 }
185 );
186
187 foreach ( $path_tokens as $token => $token_path ) {
188 if ( 0 !== strpos( $path, $token_path ) ) {
189 continue;
190 }
191
192 $path = str_replace( $token_path, '{{' . $token . '}}', $path );
193 }
194
195 return $path;
196 }
197
198 /**
199 * Given a tokenized path, this will expand the tokens to their full path.
200 *
201 * @since 4.3.0
202 * @param string $path The absolute path to expand.
203 * @param array $path_tokens An array keyed with the token, containing paths that should be expanded.
204 * @return string The absolute path.
205 */
206 function wc_untokenize_path( $path, $path_tokens ) {
207 foreach ( $path_tokens as $token => $token_path ) {
208 $path = str_replace( '{{' . $token . '}}', $token_path, $path );
209 }
210
211 return $path;
212 }
213
214 /**
215 * Fetches an array containing all of the configurable path constants to be used in tokenization.
216 *
217 * @return array The key is the define and the path is the constant.
218 */
219 function wc_get_path_define_tokens() {
220 $defines = array(
221 'ABSPATH',
222 'WP_CONTENT_DIR',
223 'WP_PLUGIN_DIR',
224 'WPMU_PLUGIN_DIR',
225 'PLUGINDIR',
226 'WP_THEME_DIR',
227 );
228
229 $path_tokens = array();
230 foreach ( $defines as $define ) {
231 if ( defined( $define ) ) {
232 $path_tokens[ $define ] = constant( $define );
233 }
234 }
235
236 return apply_filters( 'woocommerce_get_path_define_tokens', $path_tokens );
237 }
238
239 /**
240 * Get template part (for templates like the shop-loop).
241 *
242 * WC_TEMPLATE_DEBUG_MODE will prevent overrides in themes from taking priority.
243 *
244 * @param mixed $slug Template slug.
245 * @param string $name Template name (default: '').
246 * @return void
247 */
248 function wc_get_template_part( $slug, $name = '' ) {
249 $cache_key = sanitize_key( implode( '-', array( 'template-part', $slug, $name, Constants::get_constant( 'WC_VERSION' ) ) ) );
250 $template = (string) wp_cache_get( $cache_key, 'woocommerce' );
251
252 if ( ! $template ) {
253 if ( $name ) {
254 $template = WC_TEMPLATE_DEBUG_MODE ? '' : locate_template(
255 array(
256 "{$slug}-{$name}.php",
257 WC()->template_path() . "{$slug}-{$name}.php",
258 )
259 );
260
261 if ( ! $template ) {
262 $fallback = WC()->plugin_path() . "/templates/{$slug}-{$name}.php";
263 $template = file_exists( $fallback ) ? $fallback : '';
264 }
265 }
266
267 if ( ! $template ) {
268 // If template file doesn't exist, look in yourtheme/slug.php and yourtheme/woocommerce/slug.php.
269 $template = WC_TEMPLATE_DEBUG_MODE ? '' : locate_template(
270 array(
271 "{$slug}.php",
272 WC()->template_path() . "{$slug}.php",
273 )
274 );
275 }
276
277 // Don't cache the absolute path so that it can be shared between web servers with different paths.
278 $cache_path = wc_tokenize_path( $template, wc_get_path_define_tokens() );
279
280 wc_set_template_cache( $cache_key, $cache_path );
281 } else {
282 // Make sure that the absolute path to the template is resolved.
283 $template = wc_untokenize_path( $template, wc_get_path_define_tokens() );
284 }
285
286 // Allow 3rd party plugins to filter template file from their plugin.
287 $template = apply_filters( 'wc_get_template_part', $template, $slug, $name );
288
289 if ( $template ) {
290 load_template( $template, false );
291 }
292 }
293
294 /**
295 * Get other templates (e.g. product attributes) passing attributes and including the file.
296 *
297 * @param string $template_name Template name.
298 * @param array $args Arguments. (default: array).
299 * @param string $template_path Template path. (default: '').
300 * @param string $default_path Default path. (default: '').
301 * @return void
302 */
303 function wc_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) {
304 $cache_key = sanitize_key( implode( '-', array( 'template', $template_name, $template_path, $default_path, Constants::get_constant( 'WC_VERSION' ) ) ) );
305 $template = (string) wp_cache_get( $cache_key, 'woocommerce' );
306
307 if ( ! $template ) {
308 $template = wc_locate_template( $template_name, $template_path, $default_path );
309
310 // Don't cache the absolute path so that it can be shared between web servers with different paths.
311 $cache_path = wc_tokenize_path( $template, wc_get_path_define_tokens() );
312
313 wc_set_template_cache( $cache_key, $cache_path );
314 } else {
315 // Make sure that the absolute path to the template is resolved.
316 $template = wc_untokenize_path( $template, wc_get_path_define_tokens() );
317 }
318
319 // Allow 3rd party plugin filter template file from their plugin.
320 $filter_template = apply_filters( 'wc_get_template', $template, $template_name, $args, $template_path, $default_path );
321
322 if ( $filter_template !== $template ) {
323 if ( ! file_exists( $filter_template ) ) {
324 /* translators: %s template */
325 wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%s does not exist.', 'woocommerce' ), '<code>' . $filter_template . '</code>' ), '2.1' );
326 return;
327 }
328 $template = $filter_template;
329 }
330
331 $action_args = array(
332 'template_name' => $template_name,
333 'template_path' => $template_path,
334 'located' => $template,
335 'args' => $args,
336 );
337
338 if ( ! empty( $args ) && is_array( $args ) ) {
339 if ( isset( $args['action_args'] ) ) {
340 wc_doing_it_wrong(
341 __FUNCTION__,
342 __( 'action_args should not be overwritten when calling wc_get_template.', 'woocommerce' ),
343 '3.6.0'
344 );
345 unset( $args['action_args'] );
346 }
347 extract( $args ); // @codingStandardsIgnoreLine
348 }
349
350 do_action( 'woocommerce_before_template_part', $action_args['template_name'], $action_args['template_path'], $action_args['located'], $action_args['args'] );
351
352 include $action_args['located'];
353
354 do_action( 'woocommerce_after_template_part', $action_args['template_name'], $action_args['template_path'], $action_args['located'], $action_args['args'] );
355 }
356
357 /**
358 * Like wc_get_template, but returns the HTML instead of outputting.
359 *
360 * @see wc_get_template
361 * @since 2.5.0
362 * @param string $template_name Template name.
363 * @param array $args Arguments. (default: array).
364 * @param string $template_path Template path. (default: '').
365 * @param string $default_path Default path. (default: '').
366 *
367 * @return string
368 */
369 function wc_get_template_html( $template_name, $args = array(), $template_path = '', $default_path = '' ) {
370 ob_start();
371 wc_get_template( $template_name, $args, $template_path, $default_path );
372 return ob_get_clean();
373 }
374 /**
375 * Locate a template and return the path for inclusion.
376 *
377 * This is the load order:
378 *
379 * yourtheme/$template_path/$template_name
380 * yourtheme/$template_name
381 * $default_path/$template_name
382 *
383 * @param string $template_name Template name.
384 * @param string $template_path Template path. (default: '').
385 * @param string $default_path Default path. (default: '').
386 * @return string
387 */
388 function wc_locate_template( $template_name, $template_path = '', $default_path = '' ) {
389 if ( ! $template_path ) {
390 $template_path = WC()->template_path();
391 }
392
393 if ( ! $default_path ) {
394 $default_path = WC()->plugin_path() . '/templates/';
395 }
396
397 // Look within passed path within the theme - this is priority.
398 if ( false !== strpos( $template_name, 'product_cat' ) || false !== strpos( $template_name, 'product_tag' ) ) {
399 $cs_template = str_replace( '_', '-', $template_name );
400 $template = locate_template(
401 array(
402 trailingslashit( $template_path ) . $cs_template,
403 $cs_template,
404 )
405 );
406 }
407
408 if ( empty( $template ) ) {
409 $template = locate_template(
410 array(
411 trailingslashit( $template_path ) . $template_name,
412 $template_name,
413 )
414 );
415 }
416
417 // Get default template/.
418 if ( ! $template || WC_TEMPLATE_DEBUG_MODE ) {
419 if ( empty( $cs_template ) ) {
420 $template = $default_path . $template_name;
421 } else {
422 $template = $default_path . $cs_template;
423 }
424 }
425
426 /**
427 * Filter to customize the path of a given WooCommerce template.
428 *
429 * Note: the $default_path argument was added in WooCommerce 9.5.0.
430 *
431 * @param string $template Full file path of the template.
432 * @param string $template_name Template name.
433 * @param string $template_path Template path.
434 * @param string $template_path Default WooCommerce templates path.
435 *
436 * @since 9.5.0 $default_path argument added.
437 */
438 return apply_filters( 'woocommerce_locate_template', $template, $template_name, $template_path, $default_path );
439 }
440
441 /**
442 * Add a template to the template cache.
443 *
444 * @since 4.3.0
445 * @param string $cache_key Object cache key.
446 * @param string $template Located template.
447 * @return void
448 */
449 function wc_set_template_cache( $cache_key, $template ) {
450 wp_cache_set( $cache_key, $template, 'woocommerce' );
451
452 $cached_templates = wp_cache_get( 'cached_templates', 'woocommerce' );
453 if ( is_array( $cached_templates ) ) {
454 $cached_templates[] = $cache_key;
455 } else {
456 $cached_templates = array( $cache_key );
457 }
458
459 wp_cache_set( 'cached_templates', $cached_templates, 'woocommerce' );
460 }
461
462 /**
463 * Clear the template cache.
464 *
465 * @since 4.3.0
466 * @return void
467 */
468 function wc_clear_template_cache() {
469 $cached_templates = wp_cache_get( 'cached_templates', 'woocommerce' );
470 if ( is_array( $cached_templates ) ) {
471 foreach ( $cached_templates as $cache_key ) {
472 wp_cache_delete( $cache_key, 'woocommerce' );
473 }
474
475 wp_cache_delete( 'cached_templates', 'woocommerce' );
476 }
477 }
478
479 /**
480 * Clear the system status theme info cache.
481 *
482 * @since 9.4.0
483 * @return void
484 */
485 function wc_clear_system_status_theme_info_cache() {
486 delete_transient( 'wc_system_status_theme_info' );
487 }
488
489 /**
490 * Get Base Currency Code.
491 *
492 * @return string
493 */
494 function get_woocommerce_currency() {
495 return apply_filters( 'woocommerce_currency', get_option( 'woocommerce_currency' ) );
496 }
497
498 /**
499 * Get full list of currency codes.
500 *
501 * Currency symbols and names should follow the Unicode CLDR recommendation (https://cldr.unicode.org/translation/currency-names-and-symbols)
502 *
503 * @return array
504 */
505 function get_woocommerce_currencies() {
506 static $currencies;
507
508 if ( ! isset( $currencies ) ) {
509 $currencies = array_unique(
510 /**
511 * Filters the list of available currencies.
512 *
513 * @since 2.1.0
514 * @param array $currencies Array of currency codes and names.
515 */
516 apply_filters( 'woocommerce_currencies', include WC()->plugin_path() . '/i18n/currencies.php' )
517 );
518 }
519
520 return $currencies;
521 }
522
523 /**
524 * Get all available Currency symbols.
525 *
526 * Currency symbols and names should follow the Unicode CLDR recommendation (https://cldr.unicode.org/translation/currency-names-and-symbols)
527 *
528 * @since 4.1.0
529 * @return array
530 */
531 function get_woocommerce_currency_symbols() {
532
533 $symbols = apply_filters(
534 'woocommerce_currency_symbols',
535 array(
536 'AED' => '&#x62f;.&#x625;',
537 'AFN' => '&#x60b;',
538 'ALL' => 'L',
539 'AMD' => 'AMD',
540 'ANG' => '&fnof;',
541 'AOA' => 'Kz',
542 'ARS' => '&#36;',
543 'AUD' => '&#36;',
544 'AWG' => 'Afl.',
545 'AZN' => '&#8380;',
546 'BAM' => 'KM',
547 'BBD' => '&#36;',
548 'BDT' => '&#2547;&nbsp;',
549 'BGN' => '&#1083;&#1074;.',
550 'BHD' => '.&#x62f;.&#x628;',
551 'BIF' => 'Fr',
552 'BMD' => '&#36;',
553 'BND' => '&#36;',
554 'BOB' => 'Bs.',
555 'BRL' => '&#82;&#36;',
556 'BSD' => '&#36;',
557 'BTC' => '&#3647;',
558 'BTN' => 'Nu.',
559 'BWP' => 'P',
560 'BYR' => 'Br',
561 'BYN' => 'Br',
562 'BZD' => '&#36;',
563 'CAD' => '&#36;',
564 'CDF' => 'Fr',
565 'CHF' => '&#67;&#72;&#70;',
566 'CLP' => '&#36;',
567 'CNY' => '&yen;',
568 'COP' => '&#36;',
569 'CRC' => '&#x20a1;',
570 'CUC' => '&#36;',
571 'CUP' => '&#36;',
572 'CVE' => '&#36;',
573 'CZK' => '&#75;&#269;',
574 'DJF' => 'Fr',
575 'DKK' => 'kr.',
576 'DOP' => 'RD&#36;',
577 'DZD' => '&#x62f;.&#x62c;',
578 'EGP' => 'EGP',
579 'ERN' => 'Nfk',
580 'ETB' => 'Br',
581 'EUR' => '&euro;',
582 'FJD' => '&#36;',
583 'FKP' => '&pound;',
584 'GBP' => '&pound;',
585 'GEL' => '&#x20be;',
586 'GGP' => '&pound;',
587 'GHS' => '&#x20b5;',
588 'GIP' => '&pound;',
589 'GMD' => 'D',
590 'GNF' => 'Fr',
591 'GTQ' => 'Q',
592 'GYD' => '&#36;',
593 'HKD' => '&#36;',
594 'HNL' => 'L',
595 'HRK' => 'kn',
596 'HTG' => 'G',
597 'HUF' => '&#70;&#116;',
598 'IDR' => 'Rp',
599 'ILS' => '&#8362;',
600 'IMP' => '&pound;',
601 'INR' => '&#8377;',
602 'IQD' => '&#x62f;.&#x639;',
603 'IRR' => '&#xfdfc;',
604 'IRT' => '&#x062A;&#x0648;&#x0645;&#x0627;&#x0646;',
605 'ISK' => 'kr.',
606 'JEP' => '&pound;',
607 'JMD' => '&#36;',
608 'JOD' => '&#x62f;.&#x627;',
609 'JPY' => '&yen;',
610 'KES' => 'KSh',
611 'KGS' => '&#x441;&#x43e;&#x43c;',
612 'KHR' => '&#x17db;',
613 'KMF' => 'Fr',
614 'KPW' => '&#x20a9;',
615 'KRW' => '&#8361;',
616 'KWD' => '&#x62f;.&#x643;',
617 'KYD' => '&#36;',
618 'KZT' => '&#8376;',
619 'LAK' => '&#8365;',
620 'LBP' => '&#x644;.&#x644;',
621 'LKR' => '&#xdbb;&#xdd4;',
622 'LRD' => '&#36;',
623 'LSL' => 'L',
624 'LYD' => '&#x62f;.&#x644;',
625 'MAD' => '&#x62f;.&#x645;.',
626 'MDL' => 'MDL',
627 'MGA' => 'Ar',
628 'MKD' => '&#x434;&#x435;&#x43d;',
629 'MMK' => 'Ks',
630 'MNT' => '&#x20ae;',
631 'MOP' => 'MOP&#36;',
632 'MRU' => 'UM',
633 'MUR' => '&#x20a8;',
634 'MVR' => '.&#x783;',
635 'MWK' => 'MK',
636 'MXN' => '&#36;',
637 'MYR' => '&#82;&#77;',
638 'MZN' => 'MT',
639 'NAD' => 'N&#36;',
640 'NGN' => '&#8358;',
641 'NIO' => 'C&#36;',
642 'NOK' => '&#107;&#114;',
643 'NPR' => '&#8360;',
644 'NZD' => '&#36;',
645 'OMR' => '&#x631;.&#x639;.',
646 'PAB' => 'B/.',
647 'PEN' => 'S/',
648 'PGK' => 'K',
649 'PHP' => '&#8369;',
650 'PKR' => '&#8360;',
651 'PLN' => '&#122;&#322;',
652 'PRB' => '&#x440;.',
653 'PYG' => '&#8370;',
654 'QAR' => '&#x631;.&#x642;',
655 'RMB' => '&yen;',
656 'RON' => 'lei',
657 'RSD' => '&#1088;&#1089;&#1076;',
658 'RUB' => '&#8381;',
659 'RWF' => 'Fr',
660 'SAR' => '&#x631;.&#x633;',
661 'SBD' => '&#36;',
662 'SCR' => '&#x20a8;',
663 'SDG' => '&#x62c;.&#x633;.',
664 'SEK' => '&#107;&#114;',
665 'SGD' => '&#36;',
666 'SHP' => '&pound;',
667 'SLL' => 'Le',
668 'SOS' => 'Sh',
669 'SRD' => '&#36;',
670 'SSP' => '&pound;',
671 'STN' => 'Db',
672 'SYP' => '&#x644;.&#x633;',
673 'SZL' => 'E',
674 'THB' => '&#3647;',
675 'TJS' => '&#x405;&#x41c;',
676 'TMT' => 'm',
677 'TND' => '&#x62f;.&#x62a;',
678 'TOP' => 'T&#36;',
679 'TRY' => '&#8378;',
680 'TTD' => '&#36;',
681 'TWD' => '&#78;&#84;&#36;',
682 'TZS' => 'Sh',
683 'UAH' => '&#8372;',
684 'UGX' => 'UGX',
685 'USD' => '&#36;',
686 'UYU' => '&#36;',
687 'UZS' => 'UZS',
688 'VEF' => 'Bs F',
689 'VES' => 'Bs.',
690 'VND' => '&#8363;',
691 'VUV' => 'Vt',
692 'WST' => 'T',
693 'XAF' => 'CFA',
694 'XCD' => '&#36;',
695 'XOF' => 'CFA',
696 'XPF' => 'XPF',
697 'YER' => '&#xfdfc;',
698 'ZAR' => '&#82;',
699 // CLDR uses K for en-ZM and ZK for its narrow representation.
700 'ZMW' => 'K',
701 )
702 );
703
704 return $symbols;
705 }
706
707 /**
708 * Get Currency symbol.
709 *
710 * Currency symbols and names should follow the Unicode CLDR recommendation (https://cldr.unicode.org/translation/currency-names-and-symbols)
711 *
712 * @param string $currency Currency. (default: '').
713 * @return string
714 */
715 function get_woocommerce_currency_symbol( $currency = '' ) {
716 if ( ! $currency ) {
717 $currency = get_woocommerce_currency();
718 }
719
720 $symbols = get_woocommerce_currency_symbols();
721
722 $currency_symbol = isset( $symbols[ $currency ] ) ? $symbols[ $currency ] : '';
723
724 return apply_filters( 'woocommerce_currency_symbol', $currency_symbol, $currency );
725 }
726
727 /**
728 * Send HTML emails from WooCommerce.
729 *
730 * @param mixed $to Receiver.
731 * @param mixed $subject Subject.
732 * @param mixed $message Message.
733 * @param string $headers Headers. (default: "Content-Type: text/html\r\n").
734 * @param string $attachments Attachments. (default: "").
735 * @return bool
736 */
737 function wc_mail( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = '' ) {
738 $mailer = WC()->mailer();
739
740 return $mailer->send( $to, $subject, $message, $headers, $attachments );
741 }
742
743 /**
744 * Return "theme support" values from the current theme, if set.
745 *
746 * @since 3.3.0
747 * @param string $prop Name of prop (or key::subkey for arrays of props) if you want a specific value. Leave blank to get all props as an array.
748 * @param mixed $default Optional value to return if the theme does not declare support for a prop.
749 * @return mixed Value of prop(s).
750 */
751 function wc_get_theme_support( $prop = '', $default = null ) {
752 $theme_support = get_theme_support( 'woocommerce' );
753 $theme_support = is_array( $theme_support ) ? $theme_support[0] : false;
754
755 if ( ! $theme_support ) {
756 return $default;
757 }
758
759 if ( $prop ) {
760 $prop_stack = explode( '::', $prop );
761 $prop_key = array_shift( $prop_stack );
762
763 if ( isset( $theme_support[ $prop_key ] ) ) {
764 $value = $theme_support[ $prop_key ];
765
766 if ( count( $prop_stack ) ) {
767 foreach ( $prop_stack as $prop_key ) {
768 if ( is_array( $value ) && isset( $value[ $prop_key ] ) ) {
769 $value = $value[ $prop_key ];
770 } else {
771 $value = $default;
772 break;
773 }
774 }
775 }
776 } else {
777 $value = $default;
778 }
779
780 return $value;
781 }
782
783 return $theme_support;
784 }
785
786 /**
787 * Get an image size by name or defined dimensions.
788 *
789 * The returned variable is filtered by woocommerce_get_image_size_{image_size} filter to
790 * allow 3rd party customisation.
791 *
792 * Sizes defined by the theme take priority over settings. Settings are hidden when a theme
793 * defines sizes.
794 *
795 * @param array|string $image_size Name of the image size to get, or an array of dimensions.
796 * @return array Array of dimensions including width, height, and cropping mode. Cropping mode is 0 for no crop, and 1 for hard crop.
797 */
798 function wc_get_image_size( $image_size ) {
799 $cache_key = 'size-' . ( is_array( $image_size ) ? implode( '-', $image_size ) : $image_size );
800 $size = ! is_customize_preview() ? wp_cache_get( $cache_key, 'woocommerce' ) : false;
801
802 if ( $size ) {
803 return $size;
804 }
805
806 $size = array(
807 'width' => 600,
808 'height' => 600,
809 'crop' => 1,
810 );
811
812 if ( is_array( $image_size ) ) {
813 $size = array(
814 'width' => isset( $image_size[0] ) ? absint( $image_size[0] ) : 600,
815 'height' => isset( $image_size[1] ) ? absint( $image_size[1] ) : 600,
816 'crop' => isset( $image_size[2] ) ? absint( $image_size[2] ) : 1,
817 );
818 $image_size = $size['width'] . '_' . $size['height'];
819 } else {
820 $image_size = str_replace( 'woocommerce_', '', $image_size );
821
822 if ( 'single' === $image_size ) {
823 $size['width'] = absint( wc_get_theme_support( 'single_image_width', get_option( 'woocommerce_single_image_width', 600 ) ) );
824 $size['height'] = '';
825 $size['crop'] = 0;
826
827 } elseif ( 'gallery_thumbnail' === $image_size ) {
828 $size['width'] = absint( wc_get_theme_support( 'gallery_thumbnail_image_width', 100 ) );
829 $size['height'] = $size['width'];
830 $size['crop'] = 1;
831
832 } elseif ( 'thumbnail' === $image_size ) {
833 $size['width'] = absint( wc_get_theme_support( 'thumbnail_image_width', get_option( 'woocommerce_thumbnail_image_width', 300 ) ) );
834 $cropping = get_option( 'woocommerce_thumbnail_cropping', '1:1' );
835
836 if ( 'uncropped' === $cropping ) {
837 $size['height'] = '';
838 $size['crop'] = 0;
839 } elseif ( 'custom' === $cropping ) {
840 $width = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_width', '4' ) );
841 $height = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_height', '3' ) );
842 $size['height'] = absint( NumberUtil::round( ( $size['width'] / $width ) * $height ) );
843 $size['crop'] = 1;
844 } else {
845 $cropping_split = explode( ':', $cropping );
846 $width = max( 1, (float) current( $cropping_split ) );
847 $height = max( 1, (float) end( $cropping_split ) );
848 $size['height'] = absint( NumberUtil::round( ( $size['width'] / $width ) * $height ) );
849 $size['crop'] = 1;
850 }
851 }
852 }
853
854 $size = apply_filters( 'woocommerce_get_image_size_' . $image_size, $size );
855
856 if ( is_customize_preview() ) {
857 wp_cache_delete( $cache_key, 'woocommerce' );
858 } else {
859 wp_cache_set( $cache_key, $size, 'woocommerce' );
860 }
861 return $size;
862 }
863
864 /**
865 * Queue some JavaScript code to be output in the footer.
866 *
867 * @param string $code Code.
868 * @return void
869 *
870 * @deprecated 10.4.0 Use wp_add_inline_script() instead.
871 */
872 function wc_enqueue_js( $code ) {
873 global $wc_queued_js;
874
875 wc_deprecated_function( 'wc_enqueue_js', '10.4.0', 'wp_add_inline_script' );
876
877 if ( empty( $wc_queued_js ) ) {
878 $wc_queued_js = '';
879 }
880
881 $wc_queued_js .= "\n" . $code . "\n";
882 }
883
884 /**
885 * Output any queued javascript code in the footer.
886 *
887 * @return void
888 */
889 function wc_print_js() {
890 global $wc_queued_js;
891
892 if ( ! empty( $wc_queued_js ) ) {
893 // Sanitize.
894 $wc_queued_js = wp_check_invalid_utf8( $wc_queued_js );
895 $wc_queued_js = preg_replace( '/&#(x)?0*(?(1)27|39);?/i', "'", $wc_queued_js );
896 $wc_queued_js = str_replace( "\r", '', $wc_queued_js );
897
898 $js = "<!-- WooCommerce JavaScript -->\n<script type=\"text/javascript\">\njQuery(function($) { $wc_queued_js });\n</script>\n";
899
900 /**
901 * Queued jsfilter.
902 *
903 * @since 2.6.0
904 * @param string $js JavaScript code.
905 */
906 echo apply_filters( 'woocommerce_queued_js', $js ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
907
908 unset( $wc_queued_js );
909 }
910 }
911
912 /**
913 * Set a cookie - wrapper for setcookie using WP constants.
914 *
915 * @param string $name Name of the cookie being set.
916 * @param string $value Value of the cookie.
917 * @param integer $expire Expiry of the cookie.
918 * @param bool $secure Whether the cookie should be served only over https.
919 * @param bool $httponly Whether the cookie is only accessible over HTTP, not scripting languages like JavaScript. @since 3.6.0.
920 * @return void
921 */
922 function wc_setcookie( $name, $value, $expire = 0, $secure = false, $httponly = false ) {
923 /**
924 * Controls whether the cookie should be set via wc_setcookie().
925 *
926 * @since 6.3.0
927 *
928 * @param bool $set_cookie_enabled If wc_setcookie() should set the cookie.
929 * @param string $name Cookie name.
930 * @param string $value Cookie value.
931 * @param integer $expire When the cookie should expire.
932 * @param bool $secure If the cookie should only be served over HTTPS.
933 */
934 if ( ! apply_filters( 'woocommerce_set_cookie_enabled', true, $name, $value, $expire, $secure ) ) {
935 return;
936 }
937
938 if ( ! headers_sent() ) {
939 /**
940 * Controls the options to be specified when setting the cookie.
941 *
942 * @see https://www.php.net/manual/en/function.setcookie.php
943 * @since 6.7.0
944 *
945 * @param array $cookie_options Cookie options.
946 * @param string $name Cookie name.
947 * @param string $value Cookie value.
948 */
949 $options = apply_filters(
950 'woocommerce_set_cookie_options',
951 array(
952 'expires' => $expire,
953 'secure' => $secure,
954 'path' => COOKIEPATH ? COOKIEPATH : '/',
955 'domain' => COOKIE_DOMAIN,
956 /**
957 * Controls whether the cookie should only be accessible via the HTTP protocol, or if it should also be
958 * accessible to Javascript.
959 *
960 * @see https://www.php.net/manual/en/function.setcookie.php
961 * @since 3.3.0
962 *
963 * @param bool $httponly If the cookie should only be accessible via the HTTP protocol.
964 * @param string $name Cookie name.
965 * @param string $value Cookie value.
966 * @param int $expire When the cookie should expire.
967 * @param bool $secure If the cookie should only be served over HTTPS.
968 */
969 'httponly' => apply_filters( 'woocommerce_cookie_httponly', $httponly, $name, $value, $expire, $secure ),
970 ),
971 $name,
972 $value
973 );
974
975 setcookie( $name, $value, $options );
976 } elseif ( Constants::is_true( 'WP_DEBUG' ) ) {
977 headers_sent( $file, $line );
978 trigger_error( "{$name} cookie cannot be set - headers already sent by {$file} on line {$line}", E_USER_NOTICE ); // @codingStandardsIgnoreLine
979 }
980 }
981
982 /**
983 * Recursively get page children.
984 *
985 * @param int $page_id Page ID.
986 * @return int[]
987 */
988 function wc_get_page_children( $page_id ) {
989 $page_ids = get_posts(
990 array(
991 'post_parent' => $page_id,
992 'post_type' => 'page',
993 'numberposts' => -1, // @codingStandardsIgnoreLine
994 'post_status' => 'any',
995 'fields' => 'ids',
996 )
997 );
998
999 if ( ! empty( $page_ids ) ) {
1000 foreach ( $page_ids as $page_id ) {
1001 $page_ids = array_merge( $page_ids, wc_get_page_children( $page_id ) );
1002 }
1003 }
1004
1005 return $page_ids;
1006 }
1007
1008 /**
1009 * Flushes rewrite rules when the shop page (or it's children) gets saved.
1010 *
1011 * @return void
1012 */
1013 function flush_rewrite_rules_on_shop_page_save() {
1014 $screen = get_current_screen();
1015 $screen_id = $screen ? $screen->id : '';
1016
1017 // Check if this is the edit page.
1018 if ( 'page' !== $screen_id ) {
1019 return;
1020 }
1021
1022 // Check if page is edited.
1023 if ( empty( $_GET['post'] ) || empty( $_GET['action'] ) || ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1024 return;
1025 }
1026
1027 $post_id = intval( $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1028 $shop_page_id = wc_get_page_id( 'shop' );
1029
1030 if ( $shop_page_id === $post_id || in_array( $post_id, wc_get_page_children( $shop_page_id ), true ) ) {
1031 do_action( 'woocommerce_flush_rewrite_rules' );
1032 }
1033 }
1034 add_action( 'admin_footer', 'flush_rewrite_rules_on_shop_page_save' );
1035
1036 /**
1037 * Various rewrite rule fixes.
1038 *
1039 * @since 2.2
1040 * @param array $rules Rules.
1041 * @return array
1042 */
1043 function wc_fix_rewrite_rules( $rules ) {
1044 global $wp_rewrite;
1045
1046 $permalinks = wc_get_permalink_structure();
1047
1048 // Fix the rewrite rules when the product permalink have %product_cat% flag.
1049 if ( preg_match( '`/(.+)(/%product_cat%)`', $permalinks['product_rewrite_slug'], $matches ) ) {
1050 foreach ( $rules as $rule => $rewrite ) {
1051 if ( preg_match( '`^' . preg_quote( $matches[1], '`' ) . '/\(`', $rule ) && preg_match( '/^(index\.php\?product_cat)(?!(.*product))/', $rewrite ) ) {
1052 unset( $rules[ $rule ] );
1053 }
1054 }
1055 }
1056
1057 // If the shop page is used as the base, we need to handle shop page subpages to avoid 404s.
1058 if ( ! $permalinks['use_verbose_page_rules'] ) {
1059 return $rules;
1060 }
1061
1062 $shop_page_id = wc_get_page_id( 'shop' );
1063 if ( $shop_page_id ) {
1064 $page_rewrite_rules = array();
1065 $subpages = wc_get_page_children( $shop_page_id );
1066
1067 // Subpage rules.
1068 foreach ( $subpages as $subpage ) {
1069 $uri = get_page_uri( $subpage );
1070 $page_rewrite_rules[ $uri . '/?$' ] = 'index.php?pagename=' . $uri;
1071 $wp_generated_rewrite_rules = $wp_rewrite->generate_rewrite_rules( $uri, EP_PAGES, true, true, false, false );
1072 foreach ( $wp_generated_rewrite_rules as $key => $value ) {
1073 $wp_generated_rewrite_rules[ $key ] = $value . '&pagename=' . $uri;
1074 }
1075 $page_rewrite_rules = array_merge( $page_rewrite_rules, $wp_generated_rewrite_rules );
1076 }
1077
1078 // Merge with rules.
1079 $rules = array_merge( $page_rewrite_rules, $rules );
1080 }
1081
1082 return $rules;
1083 }
1084 add_filter( 'rewrite_rules_array', 'wc_fix_rewrite_rules' );
1085
1086 /**
1087 * Prevent product attachment links from breaking when using complex rewrite structures.
1088 *
1089 * @param string $link Link.
1090 * @param int $post_id Post ID.
1091 * @return string
1092 */
1093 function wc_fix_product_attachment_link( $link, $post_id ) {
1094 $parent_type = get_post_type( wp_get_post_parent_id( $post_id ) );
1095 if ( 'product' === $parent_type || 'product_variation' === $parent_type ) {
1096 $link = home_url( '/?attachment_id=' . $post_id );
1097 }
1098 return $link;
1099 }
1100 add_filter( 'attachment_link', 'wc_fix_product_attachment_link', 10, 2 );
1101
1102 /**
1103 * Protect downloads from ms-files.php in multisite.
1104 *
1105 * @param string $rewrite rewrite rules.
1106 * @return string
1107 */
1108 function wc_ms_protect_download_rewite_rules( $rewrite ) {
1109 if ( ! is_multisite() || 'redirect' === get_option( 'woocommerce_file_download_method' ) ) {
1110 return $rewrite;
1111 }
1112
1113 $rule = "\n# WooCommerce Rules - Protect Files from ms-files.php\n\n";
1114 $rule .= "<IfModule mod_rewrite.c>\n";
1115 $rule .= "RewriteEngine On\n";
1116 $rule .= "RewriteCond %{QUERY_STRING} file=woocommerce_uploads/ [NC]\n";
1117 $rule .= "RewriteRule /ms-files.php$ - [F]\n";
1118 $rule .= "</IfModule>\n\n";
1119
1120 return $rule . $rewrite;
1121 }
1122 add_filter( 'mod_rewrite_rules', 'wc_ms_protect_download_rewite_rules' );
1123
1124 /**
1125 * Formats a string in the format COUNTRY:STATE into an array.
1126 *
1127 * @since 2.3.0
1128 * @param string $country_string Country string.
1129 * @return array
1130 */
1131 function wc_format_country_state_string( $country_string ) {
1132 if ( strstr( $country_string, ':' ) ) {
1133 list( $country, $state ) = explode( ':', $country_string );
1134 } else {
1135 $country = $country_string;
1136 $state = '';
1137 }
1138 return array(
1139 'country' => $country,
1140 'state' => $state,
1141 );
1142 }
1143
1144 /**
1145 * Get the store's base location.
1146 *
1147 * @since 2.3.0
1148 * @return array
1149 */
1150 function wc_get_base_location() {
1151 $default = apply_filters( 'woocommerce_get_base_location', get_option( 'woocommerce_default_country', 'US:CA' ) );
1152
1153 return wc_format_country_state_string( $default );
1154 }
1155
1156 /**
1157 * Uses geolocation to get the customer country and state only if they are valid values.
1158 *
1159 * @since 9.5.0
1160 * @param array $fallback Fallback location.
1161 * @return array
1162 */
1163 function wc_get_customer_geolocation( $fallback = array(
1164 'country' => '',
1165 'state' => '',
1166 ) ) {
1167 $ua = wc_get_user_agent();
1168
1169 // Exclude common bots from geolocation by user agent.
1170 if ( stripos( $ua, 'bot' ) !== false || stripos( $ua, 'spider' ) !== false || stripos( $ua, 'crawl' ) !== false ) {
1171 return $fallback;
1172 }
1173
1174 $geolocation = WC_Geolocation::geolocate_ip( '', true, false );
1175
1176 if ( empty( $geolocation['country'] ) ) {
1177 return $fallback;
1178 }
1179
1180 // Ensure geolocation is valid.
1181 $allowed_countries = WC()->countries->get_allowed_countries();
1182
1183 if ( ! isset( $allowed_countries[ $geolocation['country'] ] ) ) {
1184 return $fallback;
1185 }
1186
1187 $allowed_states = WC()->countries->get_allowed_country_states();
1188 $country_states = $allowed_states[ $geolocation['country'] ] ?? array();
1189
1190 if ( $country_states && ! isset( $country_states[ $geolocation['state'] ] ) ) {
1191 $geolocation['state'] = '';
1192 }
1193
1194 return array(
1195 'country' => $geolocation['country'],
1196 'state' => $geolocation['state'],
1197 );
1198 }
1199
1200 /**
1201 * Get the customer's default location.
1202 *
1203 * Filtered, and set to base location or left blank. If cache-busting,
1204 * this should only be used when 'location' is set in the querystring.
1205 *
1206 * @since 2.3.0
1207 * @return array
1208 */
1209 function wc_get_customer_default_location() {
1210 $set_default_location_to = get_option( 'woocommerce_default_customer_address', DefaultCustomerAddress::BASE );
1211
1212 // Unless the location should be blank, use the base location as the default.
1213 if ( DefaultCustomerAddress::NO_DEFAULT !== $set_default_location_to ) {
1214 $default_location_string = get_option( 'woocommerce_default_country', 'US:CA' );
1215 }
1216
1217 $default_location = wc_format_country_state_string(
1218 /**
1219 * Filter the customer default location before geolocation.
1220 *
1221 * @since 2.3.0
1222 * @param string $default_location_string The default location.
1223 * @return string
1224 */
1225 apply_filters( 'woocommerce_customer_default_location', $default_location_string ?? '' )
1226 );
1227
1228 // Ensure defaults are valid.
1229 $allowed_countries = WC()->countries->get_allowed_countries();
1230
1231 if ( ! in_array( $default_location['country'], array_keys( $allowed_countries ), true ) ) {
1232 $default_location = array(
1233 'country' => '',
1234 'state' => '',
1235 );
1236 }
1237
1238 // Geolocation takes priority if geolocation is possible.
1239 if ( in_array( $set_default_location_to, array( DefaultCustomerAddress::GEOLOCATION, DefaultCustomerAddress::GEOLOCATION_AJAX ), true ) ) {
1240 $default_location = wc_get_customer_geolocation( $default_location );
1241 }
1242
1243 /**
1244 * Filter the customer default location after geolocation.
1245 *
1246 * @since 2.3.0
1247 * @param array $customer_location The customer location with keys 'country' and 'state'.
1248 * @return array
1249 */
1250 return apply_filters( 'woocommerce_customer_default_location_array', $default_location );
1251 }
1252
1253 /**
1254 * Get user agent string.
1255 *
1256 * @since 3.0.0
1257 * @return string
1258 */
1259 function wc_get_user_agent() {
1260 return isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; // @codingStandardsIgnoreLine
1261 }
1262
1263 /**
1264 * Generate a rand hash.
1265 *
1266 * @since 2.4.0
1267 * @param string $prefix Prefix for the hash.
1268 * @param ?int $max_length Maximum length of the hash. Excludes the prefix.
1269 * @return string
1270 */
1271 function wc_rand_hash( $prefix = '', $max_length = null ) {
1272 try {
1273 $random = bin2hex( random_bytes( 20 ) );
1274 } catch ( Exception $e ) {
1275 if ( function_exists( 'wp_fast_hash' ) ) {
1276 $random = bin2hex( substr( wp_fast_hash( wp_rand() ), -20 ) );
1277 } else {
1278 $random = bin2hex( substr( sha1( wp_rand() ), -20 ) );
1279 }
1280 }
1281
1282 if ( $max_length && $max_length > 0 ) {
1283 $random = substr( $random, 0, $max_length );
1284 }
1285
1286 return $prefix . $random;
1287 }
1288
1289 /**
1290 * WC API - Hash.
1291 *
1292 * @since 2.4.0
1293 * @param string $data Message to be hashed.
1294 * @return string
1295 */
1296 function wc_api_hash( $data ) {
1297 return hash_hmac( 'sha256', $data, 'wc-api' );
1298 }
1299
1300 /**
1301 * Find all possible combinations of values from the input array and return in a logical order.
1302 *
1303 * @since 2.5.0
1304 * @param array $input Input.
1305 * @return array
1306 */
1307 function wc_array_cartesian( $input ) {
1308 $input = array_filter( $input );
1309 $results = array();
1310 $indexes = array();
1311 $index = 0;
1312
1313 // Generate indexes from keys and values so we have a logical sort order.
1314 foreach ( $input as $key => $values ) {
1315 foreach ( $values as $value ) {
1316 $indexes[ $key ][ $value ] = $index++;
1317 }
1318 }
1319
1320 // Loop over the 2D array of indexes and generate all combinations.
1321 foreach ( $indexes as $key => $values ) {
1322 // When result is empty, fill with the values of the first looped array.
1323 if ( empty( $results ) ) {
1324 foreach ( $values as $value ) {
1325 $results[] = array( $key => $value );
1326 }
1327 } else {
1328 // Second and subsequent input sub-array merging.
1329 foreach ( $results as $result_key => $result ) {
1330 foreach ( $values as $value ) {
1331 // If the key is not set, we can set it.
1332 if ( ! isset( $results[ $result_key ][ $key ] ) ) {
1333 $results[ $result_key ][ $key ] = $value;
1334 } else {
1335 // If the key is set, we can add a new combination to the results array.
1336 $new_combination = $results[ $result_key ];
1337 $new_combination[ $key ] = $value;
1338 $results[] = $new_combination;
1339 }
1340 }
1341 }
1342 }
1343 }
1344
1345 // Sort the indexes.
1346 arsort( $results );
1347
1348 // Convert indexes back to values.
1349 foreach ( $results as $result_key => $result ) {
1350 $converted_values = array();
1351
1352 // Sort the values.
1353 arsort( $results[ $result_key ] );
1354
1355 // Convert the values.
1356 foreach ( $results[ $result_key ] as $key => $value ) {
1357 $converted_values[ $key ] = array_search( $value, $indexes[ $key ], true );
1358 }
1359
1360 $results[ $result_key ] = $converted_values;
1361 }
1362
1363 return $results;
1364 }
1365
1366 /**
1367 * Run a MySQL transaction query, if supported.
1368 *
1369 * @since 2.5.0
1370 * @param string $type Types: start (default), commit, rollback.
1371 * @param bool $force use of transactions.
1372 * @return void
1373 */
1374 function wc_transaction_query( $type = 'start', $force = false ) {
1375 global $wpdb;
1376
1377 $wpdb->hide_errors();
1378
1379 wc_maybe_define_constant( 'WC_USE_TRANSACTIONS', true );
1380
1381 if ( Constants::is_true( 'WC_USE_TRANSACTIONS' ) || $force ) {
1382 switch ( $type ) {
1383 case 'commit':
1384 $wpdb->query( 'COMMIT' );
1385 break;
1386 case 'rollback':
1387 $wpdb->query( 'ROLLBACK' );
1388 break;
1389 default:
1390 $wpdb->query( 'START TRANSACTION' );
1391 break;
1392 }
1393 }
1394 }
1395
1396 /**
1397 * Gets the url to the cart page.
1398 *
1399 * @since 2.5.0
1400 * @since 9.3.0 To support shortcodes on other pages besides the main cart page, this returns the current URL if it is the cart page.
1401 *
1402 * @return string Url to cart page
1403 */
1404 function wc_get_cart_url() {
1405 global $post;
1406
1407 // We don't use is_cart() here because that also checks for a defined constant. We are only interested in the page.
1408 if ( CartCheckoutUtils::is_cart_page() ) {
1409 $cart_url = get_permalink( $post->ID );
1410 } else {
1411 $cart_url = wc_get_page_permalink( 'cart' );
1412 }
1413
1414 /**
1415 * Filter the cart URL.
1416 *
1417 * @since 2.5.0
1418 * @param string $cart_url Cart URL.
1419 */
1420 return apply_filters( 'woocommerce_get_cart_url', $cart_url );
1421 }
1422
1423 /**
1424 * Gets the url to the checkout page.
1425 *
1426 * @since 2.5.0
1427 *
1428 * @return string Url to checkout page
1429 */
1430 function wc_get_checkout_url() {
1431 $checkout_url = wc_get_page_permalink( 'checkout' );
1432 if ( $checkout_url ) {
1433 // Force SSL if needed.
1434 if ( is_ssl() || 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ) {
1435 $checkout_url = str_replace( 'http:', 'https:', $checkout_url );
1436 }
1437 }
1438
1439 return apply_filters( 'woocommerce_get_checkout_url', $checkout_url );
1440 }
1441
1442 /**
1443 * Register a shipping method.
1444 *
1445 * @since 1.5.7
1446 * @param string|object $shipping_method class name (string) or a class object.
1447 * @return void
1448 */
1449 function woocommerce_register_shipping_method( $shipping_method ) {
1450 WC()->shipping()->register_shipping_method( $shipping_method );
1451 }
1452
1453 if ( ! function_exists( 'wc_get_shipping_zone' ) ) {
1454 /**
1455 * Get the shipping zone matching a given package from the cart.
1456 *
1457 * @since 2.6.0
1458 * @uses WC_Shipping_Zones::get_zone_matching_package
1459 * @param array $package Shipping package.
1460 * @return WC_Shipping_Zone
1461 */
1462 function wc_get_shipping_zone( $package ) {
1463 return WC_Shipping_Zones::get_zone_matching_package( $package );
1464 }
1465 }
1466
1467 /**
1468 * Get a nice name for credit card providers.
1469 *
1470 * @since 2.6.0
1471 * @param string $type Provider Slug/Type.
1472 * @return string
1473 */
1474 function wc_get_credit_card_type_label( $type ) {
1475 // Normalize.
1476 $type = strtolower( $type );
1477 $type = str_replace( '-', ' ', $type );
1478 $type = str_replace( '_', ' ', $type );
1479
1480 $labels = apply_filters(
1481 'woocommerce_credit_card_type_labels',
1482 array(
1483 'mastercard' => _x( 'MasterCard', 'Name of credit card', 'woocommerce' ),
1484 'visa' => _x( 'Visa', 'Name of credit card', 'woocommerce' ),
1485 'discover' => _x( 'Discover', 'Name of credit card', 'woocommerce' ),
1486 'american express' => _x( 'American Express', 'Name of credit card', 'woocommerce' ),
1487 'cartes bancaires' => _x( 'Cartes Bancaires', 'Name of credit card', 'woocommerce' ),
1488 'diners' => _x( 'Diners', 'Name of credit card', 'woocommerce' ),
1489 'jcb' => _x( 'JCB', 'Name of credit card', 'woocommerce' ),
1490 )
1491 );
1492
1493 /**
1494 * Fallback to title case, uppercasing the first letter of each word.
1495 *
1496 * @since 8.9.0
1497 */
1498 return apply_filters( 'woocommerce_get_credit_card_type_label', ( array_key_exists( $type, $labels ) ? $labels[ $type ] : ucwords( $type ) ) );
1499 }
1500
1501 /**
1502 * Outputs a "back" link so admin screens can easily jump back a page.
1503 *
1504 * @param string $label Title of the page to return to.
1505 * @param string $url URL of the page to return to.
1506 * @return void
1507 */
1508 function wc_back_link( $label, $url ) {
1509 echo '<small class="wc-admin-breadcrumb"><a href="' . esc_url( $url ) . '" aria-label="' . esc_attr( $label ) . '">&#x2934;&#xfe0e;</a></small>';
1510 }
1511
1512 /**
1513 * Outputs a header with "back" link so admin screens can easily jump back a page.
1514 *
1515 * @param string $title Title of the current page.
1516 * @param string $label Label of the page to return to.
1517 * @param string $url URL of the page to return to.
1518 * @return void
1519 */
1520 function wc_back_header( $title, $label, $url ) {
1521 $arrow = is_rtl() ? 'dashicons-arrow-right-alt2' : 'dashicons-arrow-left-alt2';
1522
1523 echo '<h2 class="wc-admin-header">';
1524 echo '<small><a href="' . esc_url( $url ) . '" aria-label="' . esc_attr( $label ) . '"><span class="dashicons ' . esc_attr( $arrow ) . '" aria-hidden="true"></span></a></small>';
1525 echo esc_html( $title );
1526 echo '</h2>';
1527 }
1528
1529 /**
1530 * Display a WooCommerce help tip.
1531 *
1532 * @since 2.5.0
1533 *
1534 * @param string $tip Help tip text.
1535 * @param bool $allow_html Allow sanitized HTML if true or escape.
1536 * @return string
1537 */
1538 function wc_help_tip( $tip, $allow_html = false ) {
1539 if ( $allow_html ) {
1540 $sanitized_tip = wc_sanitize_tooltip( $tip );
1541 } else {
1542 $sanitized_tip = esc_attr( $tip );
1543 }
1544
1545 $aria_label = wp_strip_all_tags( $tip );
1546
1547 /**
1548 * Filter the help tip.
1549 *
1550 * @since 7.7.0
1551 *
1552 * @param string $tip_html Help tip HTML.
1553 * @param string $sanitized_tip Sanitized help tip text.
1554 * @param string $tip Original help tip text.
1555 * @param bool $allow_html Allow sanitized HTML if true or escape.
1556 *
1557 * @return string
1558 */
1559 return apply_filters( 'wc_help_tip', '<span class="woocommerce-help-tip" tabindex="0" aria-label="' . esc_attr( $aria_label ) . '" data-tip="' . $sanitized_tip . '"></span>', $sanitized_tip, $tip, $allow_html );
1560 }
1561
1562 /**
1563 * Return a list of potential postcodes for wildcard searching.
1564 *
1565 * @since 2.6.0
1566 * @param string $postcode Postcode.
1567 * @param string $country Country to format postcode for matching.
1568 * @return string[]
1569 */
1570 function wc_get_wildcard_postcodes( $postcode, $country = '' ) {
1571 $formatted_postcode = wc_format_postcode( $postcode, $country );
1572 $length = function_exists( 'mb_strlen' ) ? mb_strlen( $formatted_postcode ) : strlen( $formatted_postcode );
1573 $postcodes = array(
1574 $postcode,
1575 $formatted_postcode,
1576 $formatted_postcode . '*',
1577 );
1578
1579 for ( $i = 0; $i < $length; $i++ ) {
1580 $postcodes[] = ( function_exists( 'mb_substr' ) ? mb_substr( $formatted_postcode, 0, ( $i + 1 ) * -1 ) : substr( $formatted_postcode, 0, ( $i + 1 ) * -1 ) ) . '*';
1581 }
1582
1583 return $postcodes;
1584 }
1585
1586 /**
1587 * Used by shipping zones and taxes to compare a given $postcode to stored
1588 * postcodes to find matches for numerical ranges, and wildcards.
1589 *
1590 * @since 2.6.0
1591 * @param string $postcode Postcode you want to match against stored postcodes.
1592 * @param array $objects Array of postcode objects from Database.
1593 * @param string $object_id_key DB column name for the ID.
1594 * @param string $object_compare_key DB column name for the value.
1595 * @param string $country Country from which this postcode belongs. Allows for formatting.
1596 * @return array Array of matching object ID and matching values.
1597 */
1598 function wc_postcode_location_matcher( $postcode, $objects, $object_id_key, $object_compare_key, $country = '' ) {
1599 $postcode = wc_normalize_postcode( $postcode );
1600 $wildcard_postcodes = array_map( 'wc_clean', wc_get_wildcard_postcodes( $postcode, $country ) );
1601 $matches = array();
1602
1603 foreach ( $objects as $object ) {
1604 $object_id = $object->$object_id_key;
1605 $compare_against = $object->$object_compare_key;
1606
1607 // Handle postcodes containing ranges.
1608 if ( strstr( $compare_against, '...' ) ) {
1609 $range = array_map( 'trim', explode( '...', $compare_against ) );
1610
1611 if ( 2 !== count( $range ) ) {
1612 continue;
1613 }
1614
1615 list( $min, $max ) = $range;
1616
1617 // If the postcode is non-numeric, make it numeric.
1618 if ( ! is_numeric( $min ) || ! is_numeric( $max ) ) {
1619 $compare = wc_make_numeric_postcode( $postcode );
1620 $min = str_pad( wc_make_numeric_postcode( $min ), strlen( $compare ), '0' );
1621 $max = str_pad( wc_make_numeric_postcode( $max ), strlen( $compare ), '0' );
1622 } else {
1623 $compare = $postcode;
1624 }
1625
1626 if ( $compare >= $min && $compare <= $max ) {
1627 $matches[ $object_id ] = isset( $matches[ $object_id ] ) ? $matches[ $object_id ] : array();
1628 $matches[ $object_id ][] = $compare_against;
1629 }
1630 } elseif ( in_array( $compare_against, $wildcard_postcodes, true ) ) {
1631 // Wildcard and standard comparison.
1632 $matches[ $object_id ] = isset( $matches[ $object_id ] ) ? $matches[ $object_id ] : array();
1633 $matches[ $object_id ][] = $compare_against;
1634 }
1635 }
1636
1637 return $matches;
1638 }
1639
1640 /**
1641 * Gets number of shipping methods currently enabled. Used to identify if
1642 * shipping is configured.
1643 *
1644 * @since 2.6.0
1645 * @param bool $include_legacy Count legacy shipping methods too.
1646 * @param bool $enabled_only Whether non-legacy shipping methods should be
1647 * restricted to enabled ones. It doesn't affect
1648 * legacy shipping methods. @since 4.3.0.
1649 * @return int
1650 */
1651 function wc_get_shipping_method_count( $include_legacy = false, $enabled_only = false ) {
1652 global $wpdb;
1653
1654 $transient_name = 'wc_shipping_method_count';
1655 $transient_version = WC_Cache_Helper::get_transient_version( 'shipping' );
1656 $transient_value = get_transient( $transient_name );
1657 $counts = array(
1658 'legacy' => 0,
1659 'enabled' => 0,
1660 'disabled' => 0,
1661 );
1662
1663 if ( ! isset( $transient_value['legacy'], $transient_value['enabled'], $transient_value['disabled'], $transient_value['version'] ) || $transient_value['version'] !== $transient_version ) {
1664 // Count activated methods that don't support shipping zones if $include_legacy is true.
1665 $methods = WC()->shipping()->get_shipping_methods();
1666 $method_ids = array();
1667
1668 foreach ( $methods as $method ) {
1669 $method_ids[] = $method->id;
1670
1671 if ( isset( $method->enabled ) && 'yes' === $method->enabled && ! $method->supports( 'shipping-zones' ) ) {
1672 ++$counts['legacy'];
1673 }
1674 }
1675
1676 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
1677 $counts['enabled'] = absint( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE is_enabled=1 AND method_id IN ('" . implode( "','", array_map( 'esc_sql', $method_ids ) ) . "')" ) );
1678 $counts['disabled'] = absint( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE is_enabled=0 AND method_id IN ('" . implode( "','", array_map( 'esc_sql', $method_ids ) ) . "')" ) );
1679 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
1680
1681 $transient_value = array(
1682 'version' => $transient_version,
1683 'legacy' => $counts['legacy'],
1684 'enabled' => $counts['enabled'],
1685 'disabled' => $counts['disabled'],
1686 );
1687
1688 set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 );
1689 } else {
1690 $counts = $transient_value;
1691 }
1692
1693 $return = 0;
1694
1695 if ( $enabled_only ) {
1696 $return = $counts['enabled'];
1697 } else {
1698 $return = $counts['enabled'] + $counts['disabled'];
1699 }
1700
1701 if ( $include_legacy ) {
1702 $return += $counts['legacy'];
1703 }
1704
1705 return $return;
1706 }
1707
1708 /**
1709 * Wrapper for set_time_limit to see if it is enabled.
1710 *
1711 * @since 2.6.0
1712 * @param int $limit Time limit.
1713 * @return void
1714 */
1715 function wc_set_time_limit( $limit = 0 ) {
1716 if ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved
1717 @set_time_limit( $limit ); // @codingStandardsIgnoreLine
1718 }
1719 }
1720
1721 /**
1722 * Wrapper for nocache_headers which also disables page caching.
1723 *
1724 * @since 3.2.4
1725 * @return void
1726 */
1727 function wc_nocache_headers() {
1728 WC_Cache_Helper::set_nocache_constants();
1729 nocache_headers();
1730 }
1731
1732 /**
1733 * Used to sort products attributes with uasort.
1734 *
1735 * @since 2.6.0
1736 * @param array $a First attribute to compare.
1737 * @param array $b Second attribute to compare.
1738 * @return int
1739 */
1740 function wc_product_attribute_uasort_comparison( $a, $b ) {
1741 $a_position = is_null( $a ) ? null : $a['position'];
1742 $b_position = is_null( $b ) ? null : $b['position'];
1743 return wc_uasort_comparison( $a_position, $b_position );
1744 }
1745
1746 /**
1747 * Used to sort shipping zone methods with uasort.
1748 *
1749 * @since 3.0.0
1750 * @param array $a First shipping zone method to compare.
1751 * @param array $b Second shipping zone method to compare.
1752 * @return int
1753 */
1754 function wc_shipping_zone_method_order_uasort_comparison( $a, $b ) {
1755 return wc_uasort_comparison( $a->method_order, $b->method_order );
1756 }
1757
1758 /**
1759 * User to sort checkout fields based on priority with uasort.
1760 *
1761 * @since 3.5.1
1762 * @param array $a First field to compare.
1763 * @param array $b Second field to compare.
1764 * @return int
1765 */
1766 function wc_checkout_fields_uasort_comparison( $a, $b ) {
1767 /*
1768 * We are not guaranteed to get a priority
1769 * setting. So don't compare if they don't
1770 * exist.
1771 */
1772 if ( ! isset( $a['priority'], $b['priority'] ) ) {
1773 return 0;
1774 }
1775
1776 return wc_uasort_comparison( $a['priority'], $b['priority'] );
1777 }
1778
1779 /**
1780 * User to sort two values with ausort.
1781 *
1782 * @since 3.5.1
1783 * @param int $a First value to compare.
1784 * @param int $b Second value to compare.
1785 * @return int
1786 */
1787 function wc_uasort_comparison( $a, $b ) {
1788 if ( $a === $b ) {
1789 return 0;
1790 }
1791 return ( $a < $b ) ? -1 : 1;
1792 }
1793
1794 /**
1795 * Sort values based on ascii, useful for special chars in strings.
1796 *
1797 * @param string $a First value.
1798 * @param string $b Second value.
1799 * @return int
1800 */
1801 function wc_ascii_uasort_comparison( $a, $b ) {
1802 $a = remove_accents( $a );
1803 $b = remove_accents( $b );
1804 return strcmp( $a, $b );
1805 }
1806
1807 /**
1808 * Sort array according to current locale rules and maintaining index association.
1809 * By default tries to use Collator from PHP Internationalization Functions if available.
1810 * If PHP Collator class doesn't exists it fallback to removing accepts from a array
1811 * and by sorting with `uasort( $data, 'strcmp' )` giving support for ASCII values.
1812 *
1813 * @since 4.6.0
1814 * @param array $data List of values to sort.
1815 * @param string $locale Locale.
1816 * @return array
1817 */
1818 function wc_asort_by_locale( &$data, $locale = '' ) {
1819 // Use Collator if PHP Internationalization Functions (php-intl) is available.
1820 if ( class_exists( 'Collator' ) ) {
1821 try {
1822 $locale = $locale ? $locale : get_locale();
1823 $collator = new Collator( $locale );
1824 $collator->asort( $data, Collator::SORT_STRING );
1825 return $data;
1826 } catch ( IntlException $e ) {
1827 /*
1828 * Just skip if some error got caused.
1829 * It may be caused in installations that doesn't include ICU TZData.
1830 */
1831 if ( Constants::is_true( 'WP_DEBUG' ) ) {
1832 error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1833 sprintf(
1834 'An unexpected error occurred while trying to use PHP Intl Collator class, it may be caused by an incorrect installation of PHP Intl and ICU, and could be fixed by reinstallaing PHP Intl, see more details about PHP Intl installation: %1$s. Error message: %2$s',
1835 'https://www.php.net/manual/en/intl.installation.php',
1836 $e->getMessage()
1837 )
1838 );
1839 }
1840 }
1841 }
1842
1843 $raw_data = $data;
1844
1845 array_walk(
1846 $data,
1847 function ( &$value ) {
1848 $value = remove_accents( html_entity_decode( $value ) );
1849 }
1850 );
1851
1852 uasort( $data, 'strcmp' );
1853
1854 foreach ( $data as $key => $val ) {
1855 $data[ $key ] = $raw_data[ $key ];
1856 }
1857
1858 return $data;
1859 }
1860
1861 /**
1862 * Get rounding mode for internal tax calculations.
1863 *
1864 * @since 3.2.4
1865 * @return int
1866 */
1867 function wc_get_tax_rounding_mode() {
1868 $constant = WC_TAX_ROUNDING_MODE;
1869
1870 if ( 'auto' === $constant ) {
1871 return 'yes' === get_option( 'woocommerce_prices_include_tax', 'no' ) ? PHP_ROUND_HALF_DOWN : PHP_ROUND_HALF_UP;
1872 }
1873
1874 return intval( $constant );
1875 }
1876
1877 /**
1878 * Get rounding precision for internal WC calculations.
1879 * Will return the value of wc_get_price_decimals increased by 2 decimals, with WC_ROUNDING_PRECISION being the minimum.
1880 *
1881 * @since 2.6.3
1882 * @return int
1883 */
1884 function wc_get_rounding_precision() {
1885 $precision = wc_get_price_decimals() + 2;
1886 if ( $precision < absint( WC_ROUNDING_PRECISION ) ) {
1887 $precision = absint( WC_ROUNDING_PRECISION );
1888 }
1889
1890 /**
1891 * Filter the rounding precision for internal WC calculations. This is different from the number of decimals used for display.
1892 * Generally, this filter can be used to decrease the precision, but if you choose to decrease, there maybe side effects such as off by one rounding errors for certain tax rate combinations.
1893 *
1894 * @since 8.8.0
1895 *
1896 * @param int $precision The number of decimals to round to.
1897 */
1898 return apply_filters( 'woocommerce_internal_rounding_precision', $precision );
1899 }
1900
1901 /**
1902 * Add precision to a number by moving the decimal point to the right as many places as indicated by wc_get_price_decimals().
1903 * Optionally the result is rounded so that the total number of digits equals wc_get_rounding_precision() plus one.
1904 *
1905 * @since 3.2.0
1906 * @param float|null $value Number to add precision to.
1907 * @param bool $round If the result should be rounded.
1908 * @return int|float
1909 */
1910 function wc_add_number_precision( ?float $value, bool $round = true ) {
1911 if ( ! $value ) {
1912 return 0.0;
1913 }
1914
1915 // Fallback to standard rounding precision in order to cover rounding changes in PHP 8.4.
1916 $result = $value * pow( 10, wc_get_price_decimals() );
1917 $round_precision = $round ? wc_get_rounding_precision() - wc_get_price_decimals() : wc_get_rounding_precision();
1918
1919 return NumberUtil::round( $result, $round_precision );
1920 }
1921
1922 /**
1923 * Remove precision from a number and return a float.
1924 *
1925 * @since 3.2.0
1926 * @param float $value Number to add precision to.
1927 * @return float
1928 */
1929 function wc_remove_number_precision( $value ) {
1930 if ( ! $value ) {
1931 return 0.0;
1932 }
1933
1934 $cent_precision = pow( 10, wc_get_price_decimals() );
1935 return $value / $cent_precision;
1936 }
1937
1938 /**
1939 * Add precision to an array of number and return an array of int.
1940 *
1941 * @since 3.2.0
1942 * @param array $value Number to add precision to.
1943 * @param bool $round Should we round after adding precision?.
1944 * @return int|array
1945 */
1946 function wc_add_number_precision_deep( $value, $round = true ) {
1947 if ( ! is_array( $value ) ) {
1948 return wc_add_number_precision( (float) $value, $round );
1949 }
1950
1951 foreach ( $value as $key => $sub_value ) {
1952 $value[ $key ] = wc_add_number_precision_deep( $sub_value, $round );
1953 }
1954
1955 return $value;
1956 }
1957
1958 /**
1959 * Remove precision from an array of number and return an array of int.
1960 *
1961 * @since 3.2.0
1962 * @param array $value Number to add precision to.
1963 * @return int|array
1964 */
1965 function wc_remove_number_precision_deep( $value ) {
1966 if ( ! is_array( $value ) ) {
1967 return wc_remove_number_precision( $value );
1968 }
1969
1970 foreach ( $value as $key => $sub_value ) {
1971 $value[ $key ] = wc_remove_number_precision_deep( $sub_value );
1972 }
1973
1974 return $value;
1975 }
1976
1977 /**
1978 * Get a shared logger instance.
1979 *
1980 * Use the woocommerce_logging_class filter to change the logging class. You may provide one of the following:
1981 * - a class name which will be instantiated as `new $class` with no arguments
1982 * - an instance which will be used directly as the logger
1983 * In either case, the class or instance *must* implement WC_Logger_Interface.
1984 *
1985 * @return WC_Logger_Interface
1986 */
1987 function wc_get_logger() {
1988 static $logger = null;
1989
1990 $class = apply_filters( 'woocommerce_logging_class', 'WC_Logger' );
1991
1992 if ( null !== $logger && is_string( $class ) && is_a( $logger, $class ) ) {
1993 return $logger;
1994 }
1995
1996 $implements = class_implements( $class );
1997
1998 if ( is_array( $implements ) && in_array( 'WC_Logger_Interface', $implements, true ) ) {
1999 $logger = is_object( $class ) ? $class : new $class();
2000 } else {
2001 wc_doing_it_wrong(
2002 __FUNCTION__,
2003 sprintf(
2004 /* translators: 1: class name 2: woocommerce_logging_class 3: WC_Logger_Interface */
2005 __( 'The class %1$s provided by %2$s filter must implement %3$s.', 'woocommerce' ),
2006 '<code>' . esc_html( is_object( $class ) ? get_class( $class ) : $class ) . '</code>',
2007 '<code>woocommerce_logging_class</code>',
2008 '<code>WC_Logger_Interface</code>'
2009 ),
2010 '3.0'
2011 );
2012
2013 $logger = is_a( $logger, 'WC_Logger' ) ? $logger : new WC_Logger();
2014 }
2015
2016 return $logger;
2017 }
2018
2019 /**
2020 * Trigger logging cleanup using the logging class.
2021 *
2022 * @since 3.4.0
2023 * @return void
2024 */
2025 function wc_cleanup_logs() {
2026 $logger = wc_get_logger();
2027
2028 if ( is_callable( array( $logger, 'clear_expired_logs' ) ) ) {
2029 $logger->clear_expired_logs();
2030 }
2031
2032 wc_get_container()->get( OrderLogsCleanupHelper::class )->cleanup();
2033 }
2034 add_action( 'woocommerce_cleanup_logs', 'wc_cleanup_logs' );
2035
2036 /**
2037 * Prints human-readable information about a variable.
2038 *
2039 * Some server environments block some debugging functions. This function provides a safe way to
2040 * turn an expression into a printable, readable form without calling blocked functions.
2041 *
2042 * @since 3.0
2043 *
2044 * @param mixed $expression The expression to be printed.
2045 * @param bool $return Optional. Default false. Set to true to return the human-readable string.
2046 * @return string|bool False if expression could not be printed. True if the expression was printed.
2047 * If $return is true, a string representation will be returned.
2048 */
2049 function wc_print_r( $expression, $return = false ) {
2050 $alternatives = array(
2051 array(
2052 'func' => 'print_r',
2053 'args' => array( $expression, true ),
2054 ),
2055 array(
2056 'func' => 'var_export',
2057 'args' => array( $expression, true ),
2058 ),
2059 array(
2060 'func' => 'json_encode',
2061 'args' => array( $expression ),
2062 ),
2063 array(
2064 'func' => 'serialize',
2065 'args' => array( $expression ),
2066 ),
2067 );
2068
2069 $alternatives = apply_filters( 'woocommerce_print_r_alternatives', $alternatives, $expression );
2070
2071 foreach ( $alternatives as $alternative ) {
2072 if ( function_exists( $alternative['func'] ) ) {
2073 $res = $alternative['func']( ...$alternative['args'] );
2074 if ( $return ) {
2075 return $res; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
2076 }
2077
2078 echo $res; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
2079 return true;
2080 }
2081 }
2082
2083 return false;
2084 }
2085
2086 /**
2087 * Based on wp_list_pluck, this calls a method instead of returning a property.
2088 *
2089 * @since 3.0.0
2090 * @param array $list List of objects or arrays.
2091 * @param int|string $callback_or_field Callback method from the object to place instead of the entire object.
2092 * @param int|string $index_key Optional. Field from the object to use as keys for the new array.
2093 * Default null.
2094 * @return array Array of values.
2095 */
2096 function wc_list_pluck( $list, $callback_or_field, $index_key = null ) {
2097 // Use wp_list_pluck if this isn't a callback.
2098 $first_el = current( $list );
2099 if ( ! is_object( $first_el ) || ! is_callable( array( $first_el, $callback_or_field ) ) ) {
2100 return wp_list_pluck( $list, $callback_or_field, $index_key );
2101 }
2102 if ( ! $index_key ) {
2103 /*
2104 * This is simple. Could at some point wrap array_column()
2105 * if we knew we had an array of arrays.
2106 */
2107 foreach ( $list as $key => $value ) {
2108 $list[ $key ] = $value->{$callback_or_field}();
2109 }
2110 return $list;
2111 }
2112
2113 /*
2114 * When index_key is not set for a particular item, push the value
2115 * to the end of the stack. This is how array_column() behaves.
2116 */
2117 $newlist = array();
2118 foreach ( $list as $value ) {
2119 // Get index. @since 3.2.0 this supports a callback.
2120 if ( is_callable( array( $value, $index_key ) ) ) {
2121 $newlist[ $value->{$index_key}() ] = $value->{$callback_or_field}();
2122 } elseif ( isset( $value->$index_key ) ) {
2123 $newlist[ $value->$index_key ] = $value->{$callback_or_field}();
2124 } else {
2125 $newlist[] = $value->{$callback_or_field}();
2126 }
2127 }
2128 return $newlist;
2129 }
2130
2131 /**
2132 * Get permalink settings for things like products and taxonomies.
2133 *
2134 * As of 3.3.0, the permalink settings are stored to the option instead of
2135 * being blank and inheritting from the locale. This speeds up page loading
2136 * times by negating the need to switch locales on each page load.
2137 *
2138 * This is more inline with WP core behavior which does not localize slugs.
2139 *
2140 * @since 3.0.0
2141 * @return array
2142 */
2143 function wc_get_permalink_structure() {
2144 $saved_permalinks = (array) get_option( 'woocommerce_permalinks', array() );
2145 $permalinks = wp_parse_args(
2146 array_filter( $saved_permalinks ),
2147 array(
2148 'product_base' => _x( 'product', 'slug', 'woocommerce' ),
2149 'category_base' => _x( 'product-category', 'slug', 'woocommerce' ),
2150 'tag_base' => _x( 'product-tag', 'slug', 'woocommerce' ),
2151 'attribute_base' => '',
2152 'use_verbose_page_rules' => false,
2153 )
2154 );
2155
2156 if ( $saved_permalinks !== $permalinks ) {
2157 update_option( 'woocommerce_permalinks', $permalinks );
2158 }
2159
2160 $permalinks['product_rewrite_slug'] = untrailingslashit( $permalinks['product_base'] );
2161 $permalinks['category_rewrite_slug'] = untrailingslashit( $permalinks['category_base'] );
2162 $permalinks['tag_rewrite_slug'] = untrailingslashit( $permalinks['tag_base'] );
2163 $permalinks['attribute_rewrite_slug'] = untrailingslashit( $permalinks['attribute_base'] );
2164
2165 return $permalinks;
2166 }
2167
2168 /**
2169 * Switch WooCommerce to site language.
2170 *
2171 * @since 3.1.0
2172 * @return void
2173 */
2174 function wc_switch_to_site_locale() {
2175 global $wp_locale_switcher;
2176
2177 if ( function_exists( 'switch_to_locale' ) && isset( $wp_locale_switcher ) ) {
2178 switch_to_locale( get_locale() );
2179
2180 // Filter on plugin_locale so load_plugin_textdomain loads the correct locale.
2181 add_filter( 'plugin_locale', 'get_locale' );
2182
2183 // Init WC locale.
2184 WC()->load_plugin_textdomain();
2185 }
2186 }
2187
2188 /**
2189 * Switch WooCommerce language to original.
2190 *
2191 * @since 3.1.0
2192 * @return void
2193 */
2194 function wc_restore_locale() {
2195 global $wp_locale_switcher;
2196
2197 if ( function_exists( 'restore_previous_locale' ) && isset( $wp_locale_switcher ) ) {
2198 restore_previous_locale();
2199
2200 // Remove filter.
2201 remove_filter( 'plugin_locale', 'get_locale' );
2202
2203 // Init WC locale.
2204 WC()->load_plugin_textdomain();
2205 }
2206 }
2207
2208 /**
2209 * Convert plaintext phone number to clickable phone number.
2210 *
2211 * Remove formatting and allow "+".
2212 * Example and specs: https://developer.mozilla.org/en/docs/Web/HTML/Element/a#Creating_a_phone_link
2213 *
2214 * @since 3.1.0
2215 *
2216 * @param string $phone Content to convert phone number.
2217 * @return string Content with converted phone number.
2218 */
2219 function wc_make_phone_clickable( $phone ) {
2220 $number = trim( preg_replace( '/[^\d|\+]/', '', $phone ) );
2221
2222 return $number ? '<a href="tel:' . esc_attr( $number ) . '">' . esc_html( $phone ) . '</a>' : '';
2223 }
2224
2225 /**
2226 * Get an item of post data if set, otherwise return a default value.
2227 *
2228 * @since 3.0.9
2229 * @param string $key Meta key.
2230 * @param string $default Default value.
2231 * @return mixed Value sanitized by wc_clean.
2232 */
2233 function wc_get_post_data_by_key( $key, $default = '' ) {
2234 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing
2235 return wc_clean( wp_unslash( wc_get_var( $_POST[ $key ], $default ) ) );
2236 }
2237
2238 /**
2239 * Get data if set, otherwise return a default value or null. Prevents notices when data is not set.
2240 *
2241 * @since 3.2.0
2242 * @param mixed $var Variable.
2243 * @param string $default Default value.
2244 * @return mixed
2245 */
2246 function wc_get_var( &$var, $default = null ) {
2247 return isset( $var ) ? $var : $default;
2248 }
2249
2250 /**
2251 * Read in WooCommerce headers when reading plugin headers.
2252 *
2253 * @since 3.2.0
2254 * @param array $headers Headers.
2255 * @return array
2256 */
2257 function wc_enable_wc_plugin_headers( $headers ) {
2258 if ( ! class_exists( 'WC_Plugin_Updates' ) ) {
2259 include_once __DIR__ . '/admin/plugin-updates/class-wc-plugin-updates.php';
2260 }
2261
2262 // WC requires at least - allows developers to define which version of WooCommerce the plugin requires to run.
2263 $headers[] = WC_Plugin_Updates::VERSION_REQUIRED_HEADER;
2264
2265 // WC tested up to - allows developers to define which version of WooCommerce they have tested up to.
2266 $headers[] = WC_Plugin_Updates::VERSION_TESTED_HEADER;
2267
2268 // Woo - This is used in WooCommerce extensions and is picked up by the helper.
2269 $headers[] = 'Woo';
2270
2271 return $headers;
2272 }
2273 add_filter( 'extra_theme_headers', 'wc_enable_wc_plugin_headers' );
2274 add_filter( 'extra_plugin_headers', 'wc_enable_wc_plugin_headers' );
2275
2276 /**
2277 * Prevent auto-updating the WooCommerce plugin on major releases if there are untested extensions active.
2278 *
2279 * @since 3.2.0
2280 * @param bool $should_update If should update.
2281 * @param object $plugin Plugin data.
2282 * @return bool
2283 */
2284 function wc_prevent_dangerous_auto_updates( $should_update, $plugin ) {
2285 if ( ! isset( $plugin->plugin, $plugin->new_version ) ) {
2286 return $should_update;
2287 }
2288
2289 if ( 'woocommerce/woocommerce.php' !== $plugin->plugin ) {
2290 return $should_update;
2291 }
2292
2293 if ( ! class_exists( 'WC_Plugin_Updates' ) ) {
2294 include_once __DIR__ . '/admin/plugin-updates/class-wc-plugin-updates.php';
2295 }
2296
2297 $new_version = wc_clean( $plugin->new_version );
2298 $plugin_updates = new WC_Plugin_Updates();
2299 $version_type = Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' );
2300 if ( ! is_string( $version_type ) ) {
2301 $version_type = 'none';
2302 }
2303 $untested_plugins = $plugin_updates->get_untested_plugins( $new_version, $version_type );
2304 if ( ! empty( $untested_plugins ) ) {
2305 return false;
2306 }
2307
2308 return $should_update;
2309 }
2310 add_filter( 'auto_update_plugin', 'wc_prevent_dangerous_auto_updates', 99, 2 );
2311
2312 /**
2313 * Delete expired transients.
2314 *
2315 * Deletes all expired transients. The multi-table delete syntax is used.
2316 * to delete the transient record from table a, and the corresponding.
2317 * transient_timeout record from table b.
2318 *
2319 * Based on code inside core's upgrade_network() function.
2320 *
2321 * @since 3.2.0
2322 * @return int Number of transients that were cleared.
2323 */
2324 function wc_delete_expired_transients() {
2325 global $wpdb;
2326
2327 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
2328 $sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b
2329 WHERE a.option_name LIKE %s
2330 AND a.option_name NOT LIKE %s
2331 AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) )
2332 AND b.option_value < %d";
2333 $rows = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_transient_' ) . '%', $wpdb->esc_like( '_transient_timeout_' ) . '%', time() ) );
2334
2335 $sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b
2336 WHERE a.option_name LIKE %s
2337 AND a.option_name NOT LIKE %s
2338 AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) )
2339 AND b.option_value < %d";
2340 $rows2 = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_site_transient_' ) . '%', $wpdb->esc_like( '_site_transient_timeout_' ) . '%', time() ) );
2341 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
2342
2343 return absint( $rows + $rows2 );
2344 }
2345 add_action( 'woocommerce_installed', 'wc_delete_expired_transients' );
2346
2347 /**
2348 * Make a URL relative, if possible.
2349 *
2350 * @since 3.2.0
2351 * @param string $url URL to make relative.
2352 * @return string
2353 */
2354 function wc_get_relative_url( $url ) {
2355 return wc_is_external_resource( $url ) ? $url : str_replace( array( 'http://', 'https://' ), '//', $url );
2356 }
2357
2358 /**
2359 * See if a resource is remote.
2360 *
2361 * @since 3.2.0
2362 * @param string $url URL to check.
2363 * @return bool
2364 */
2365 function wc_is_external_resource( $url ) {
2366 $wp_base = str_replace( array( 'http://', 'https://' ), '//', get_home_url( null, '/', 'http' ) );
2367
2368 return strstr( $url, '://' ) && ! strstr( $url, $wp_base );
2369 }
2370
2371 /**
2372 * See if theme/s is activate or not.
2373 *
2374 * @since 3.3.0
2375 * @param string|array $theme Theme name or array of theme names to check.
2376 * @return boolean
2377 */
2378 function wc_is_active_theme( $theme ) {
2379 return is_array( $theme ) ? in_array( get_template(), $theme, true ) : get_template() === $theme;
2380 }
2381
2382 /**
2383 * Is the site using a default WP theme?
2384 *
2385 * @return boolean
2386 */
2387 function wc_is_wp_default_theme_active() {
2388 return wc_is_active_theme(
2389 array(
2390 'twentytwentythree',
2391 'twentytwentytwo',
2392 'twentytwentyone',
2393 'twentytwenty',
2394 'twentynineteen',
2395 'twentyseventeen',
2396 'twentysixteen',
2397 'twentyfifteen',
2398 'twentyfourteen',
2399 'twentythirteen',
2400 'twentyeleven',
2401 'twentytwelve',
2402 'twentyten',
2403 )
2404 );
2405 }
2406
2407 /**
2408 * Cleans up session data - cron callback.
2409 *
2410 * @since 3.3.0
2411 * @return void
2412 */
2413 function wc_cleanup_session_data() {
2414 $session_class = apply_filters( 'woocommerce_session_handler', 'WC_Session_Handler' );
2415 $session = new $session_class();
2416
2417 if ( is_callable( array( $session, 'cleanup_sessions' ) ) ) {
2418 $session->cleanup_sessions();
2419 }
2420 }
2421 add_action( 'woocommerce_cleanup_sessions', 'wc_cleanup_session_data' );
2422
2423 /**
2424 * Convert a decimal (e.g. 3.5) to a fraction (e.g. 7/2).
2425 * From: https://www.designedbyaturtle.co.uk/2015/converting-a-decimal-to-a-fraction-in-php/
2426 *
2427 * @param float $decimal the decimal number.
2428 * @return array|bool a 1/2 would be [1, 2] array (this can be imploded with '/' to form a string).
2429 */
2430 function wc_decimal_to_fraction( $decimal ) {
2431 if ( 0 > $decimal || ! is_numeric( $decimal ) ) {
2432 // Negative digits need to be passed in as positive numbers and prefixed as negative once the response is imploded.
2433 return false;
2434 }
2435
2436 if ( 0 === $decimal ) {
2437 return array( 0, 1 );
2438 }
2439
2440 $tolerance = 1.e-4;
2441 $numerator = 1;
2442 $h2 = 0;
2443 $denominator = 0;
2444 $k2 = 1;
2445 $b = 1 / $decimal;
2446
2447 do {
2448 $b = 1 / $b;
2449 $a = floor( $b );
2450 $aux = $numerator;
2451 $numerator = $a * $numerator + $h2;
2452 $h2 = $aux;
2453 $aux = $denominator;
2454 $denominator = $a * $denominator + $k2;
2455 $k2 = $aux;
2456 $b = $b - $a;
2457 } while ( abs( $decimal - $numerator / $denominator ) > $decimal * $tolerance );
2458
2459 return array( $numerator, $denominator );
2460 }
2461
2462 /**
2463 * Round discount.
2464 *
2465 * @param double $value Amount to round.
2466 * @param int $precision DP to round.
2467 * @return float
2468 */
2469 function wc_round_discount( $value, $precision ) {
2470 return NumberUtil::round( $value, $precision, WC_DISCOUNT_ROUNDING_MODE ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.round_modeFound
2471 }
2472
2473 /**
2474 * Return the html selected attribute if stringified $value is found in array of stringified $options
2475 * or if stringified $value is the same as scalar stringified $options.
2476 *
2477 * @param string|int $value Value to find within options.
2478 * @param string|int|array $options Options to go through when looking for value.
2479 * @return string
2480 */
2481 function wc_selected( $value, $options ) {
2482 if ( is_array( $options ) ) {
2483 $options = array_map( 'strval', $options );
2484 return selected( in_array( (string) $value, $options, true ), true, false );
2485 }
2486
2487 return selected( $value, $options, false );
2488 }
2489
2490 /**
2491 * Retrieves the MySQL server version. Based on $wpdb.
2492 *
2493 * @since 3.4.1
2494 * @return array Version information.
2495 */
2496 function wc_get_server_database_version() {
2497 global $wpdb;
2498
2499 if ( empty( $wpdb->is_mysql ) || empty( $wpdb->use_mysqli ) ) {
2500 return array(
2501 'string' => '',
2502 'number' => '',
2503 );
2504 }
2505
2506 $server_info = $wpdb->get_var( 'SELECT VERSION()' );
2507
2508 return array(
2509 'string' => $server_info,
2510 'number' => preg_replace( '/([^\d.]+).*/', '', $server_info ),
2511 );
2512 }
2513
2514 /**
2515 * Initialize and load the cart functionality.
2516 *
2517 * @since 3.6.4
2518 * @return void
2519 */
2520 function wc_load_cart() {
2521 if ( ! did_action( 'before_woocommerce_init' ) || doing_action( 'before_woocommerce_init' ) ) {
2522 /* translators: 1: wc_load_cart 2: woocommerce_init */
2523 wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%1$s should not be called before the %2$s action.', 'woocommerce' ), 'wc_load_cart', 'woocommerce_init' ), '3.7' );
2524 return;
2525 }
2526
2527 // Ensure dependencies are loaded in all contexts.
2528 include_once WC_ABSPATH . 'includes/wc-cart-functions.php';
2529 include_once WC_ABSPATH . 'includes/wc-notice-functions.php';
2530
2531 WC()->initialize_session();
2532 WC()->initialize_cart();
2533 }
2534
2535 /**
2536 * Test whether the context of execution comes from async action scheduler.
2537 *
2538 * @since 4.0.0
2539 * @return bool
2540 */
2541 function wc_is_running_from_async_action_scheduler() {
2542 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2543 return isset( $_REQUEST['action'] ) && 'as_async_request_queue_runner' === $_REQUEST['action'];
2544 }
2545
2546 /**
2547 * Polyfill for wp_cache_get_multiple for WP versions before 5.5.
2548 *
2549 * @param array $keys Array of keys to get from group.
2550 * @param string $group Optional. Where the cache contents are grouped. Default empty.
2551 * @param bool $force Optional. Whether to force an update of the local cache from the persistent
2552 * cache. Default false.
2553 * @return array|bool Array of values.
2554 */
2555 function wc_cache_get_multiple( $keys, $group = '', $force = false ) {
2556 if ( function_exists( 'wp_cache_get_multiple' ) ) {
2557 return wp_cache_get_multiple( $keys, $group, $force );
2558 }
2559 $values = array();
2560 foreach ( $keys as $key ) {
2561 $values[ $key ] = wp_cache_get( $key, $group, $force );
2562 }
2563 return $values;
2564 }
2565
2566 /**
2567 * Delete multiple transients in a single operation.
2568 *
2569 * IMPORTANT: This is a private function (internal use ONLY).
2570 *
2571 * This function efficiently deletes multiple transients at once, using a direct
2572 * database query when possible for better performance.
2573 *
2574 * @internal
2575 *
2576 * @since 9.8.0
2577 * @param array $transients Array of transient names to delete (without the '_transient_' prefix).
2578 * @return bool True on success, false on failure.
2579 */
2580 function _wc_delete_transients( $transients ) {
2581 global $wpdb;
2582
2583 if ( empty( $transients ) || ! is_array( $transients ) ) {
2584 return false;
2585 }
2586
2587 // If using external object cache, delete each transient individually.
2588 if ( wp_using_ext_object_cache() ) {
2589 foreach ( $transients as $transient ) {
2590 delete_transient( $transient );
2591 }
2592 return true;
2593 } else {
2594 // For database storage, create a list of transient option names.
2595 $transient_names = array();
2596 foreach ( $transients as $transient ) {
2597 $transient_names[] = '_transient_' . $transient;
2598 $transient_names[] = '_transient_timeout_' . $transient;
2599 }
2600
2601 // Limit the number of items in a single query to avoid exceeding database query parameter limits.
2602 if ( count( $transients ) > 199 ) {
2603 // Process in smaller chunks to reduce memory usage.
2604 $chunks = array_chunk( $transients, 100 );
2605 $success = true;
2606
2607 foreach ( $chunks as $chunk ) {
2608 $result = _wc_delete_transients( $chunk );
2609 if ( ! $result ) {
2610 $success = false;
2611 }
2612 // Force garbage collection after each chunk to free memory.
2613 gc_collect_cycles();
2614 }
2615
2616 return $success;
2617 }
2618
2619 try {
2620 // Before deleting, get the list of options to clear from cache.
2621 // Since we already have the option names we could skip this step but this mirrors WP's delete_option functionality.
2622 // It also allows us to only delete the options we know exist.
2623 $options_to_clear = array();
2624 if ( ! wp_installing() ) {
2625 $options_to_clear = $wpdb->get_col(
2626 $wpdb->prepare(
2627 'SELECT option_name FROM ' . $wpdb->options . ' WHERE option_name IN ( ' . implode( ', ', array_fill( 0, count( $transient_names ), '%s' ) ) . ' )',
2628 $transient_names
2629 )
2630 );
2631 }
2632
2633 if ( empty( $options_to_clear ) ) {
2634 // If there are no options to clear, return true immediately.
2635 return true;
2636 }
2637
2638 // Use a single query for better performance.
2639 $wpdb->query(
2640 $wpdb->prepare(
2641 'DELETE FROM ' . $wpdb->options . ' WHERE option_name IN ( ' . implode( ', ', array_fill( 0, count( $options_to_clear ), '%s' ) ) . ' )',
2642 $options_to_clear
2643 )
2644 );
2645
2646 // Lets clear our options data from the cache.
2647 // We can batch delete if available, introduced in WP 6.0.0.
2648 if ( ! wp_installing() ) {
2649 if ( function_exists( 'wp_cache_delete_multiple' ) ) {
2650 wp_cache_delete_multiple( $options_to_clear, 'options' );
2651 } else {
2652 foreach ( $options_to_clear as $option_name ) {
2653 wp_cache_delete( $option_name, 'options' );
2654 }
2655 }
2656
2657 // Also update alloptions cache if needed.
2658 // This is required to prevent phantom transients from being returned.
2659 $alloptions = wp_load_alloptions( true );
2660 $updated_alloptions = false;
2661
2662 if ( is_array( $alloptions ) ) {
2663 foreach ( $options_to_clear as $option_name ) {
2664 if ( isset( $alloptions[ $option_name ] ) ) {
2665 unset( $alloptions[ $option_name ] );
2666 $updated_alloptions = true;
2667 }
2668 }
2669
2670 if ( $updated_alloptions ) {
2671 wp_cache_set( 'alloptions', $alloptions, 'options' );
2672 }
2673 }
2674 }
2675
2676 return true;
2677 } catch ( Exception $e ) {
2678 wc_get_logger()->error(
2679 sprintf( 'Exception when deleting transients: %s', $e->getMessage() ),
2680 array( 'source' => '_wc_delete_transients' )
2681 );
2682 return false;
2683 }
2684 }
2685 }
2686