PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.8.0-beta.2
WooCommerce v10.8.0-beta.2
10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / includes / wc-formatting-functions.php
woocommerce / includes Last commit date
abstracts 4 weeks ago admin 4 weeks ago blocks 10 months ago cli 7 months ago customizer 3 months ago data-stores 3 weeks ago emails 3 weeks ago export 1 year ago gateways 2 months ago import 2 months ago integrations 4 weeks ago interfaces 3 months ago legacy 3 months ago libraries 1 year ago log-handlers 1 year ago payment-tokens 5 years ago product-usage 1 year ago queue 3 months ago react-admin 3 months ago rest-api 4 weeks ago shipping 2 months ago shortcodes 2 months ago theme-support 2 years ago tracks 3 months ago traits 5 years ago walkers 5 years ago wccom-site 4 weeks ago widgets 4 weeks ago class-wc-ajax.php 4 weeks ago class-wc-auth.php 1 year ago class-wc-autoloader.php 7 months ago class-wc-background-emailer.php 4 weeks ago class-wc-background-updater.php 5 years ago class-wc-brands-brand-settings-manager.php 1 year ago class-wc-brands-coupons.php 1 year ago class-wc-brands.php 4 months ago class-wc-breadcrumb.php 3 months ago class-wc-cache-helper.php 4 weeks ago class-wc-cart-fees.php 2 years ago class-wc-cart-session.php 2 months ago class-wc-cart-totals.php 10 months ago class-wc-cart.php 2 months ago class-wc-checkout.php 4 weeks ago class-wc-cli.php 9 months ago class-wc-comments.php 3 months ago class-wc-countries.php 4 weeks ago class-wc-coupon.php 4 weeks ago class-wc-customer-download-log.php 5 years ago class-wc-customer-download.php 1 year ago class-wc-customer.php 4 weeks ago class-wc-data-exception.php 8 years ago class-wc-data-store.php 3 years ago class-wc-datetime.php 4 years ago class-wc-deprecated-action-hooks.php 2 years ago class-wc-deprecated-filter-hooks.php 2 months ago class-wc-discounts.php 10 months ago class-wc-download-handler.php 1 year ago class-wc-emails.php 3 weeks ago class-wc-embed.php 1 year ago class-wc-form-handler.php 2 months ago class-wc-frontend-scripts.php 4 weeks ago class-wc-geo-ip.php 7 months ago class-wc-geolite-integration.php 6 years ago class-wc-geolocation.php 4 weeks ago class-wc-https.php 2 years ago class-wc-install.php 3 weeks ago class-wc-integrations.php 5 years ago class-wc-log-levels.php 2 years ago class-wc-logger.php 3 months ago class-wc-meta-data.php 4 years ago class-wc-order-factory.php 4 weeks ago class-wc-order-item-coupon.php 4 years ago class-wc-order-item-fee.php 4 months ago class-wc-order-item-meta.php 4 years ago class-wc-order-item-product.php 4 weeks ago class-wc-order-item-shipping.php 4 months ago class-wc-order-item-tax.php 4 years ago class-wc-order-item.php 4 months ago class-wc-order-query.php 3 months ago class-wc-order-refund.php 1 year ago class-wc-order.php 3 weeks ago class-wc-payment-gateways.php 4 weeks ago class-wc-payment-tokens.php 3 years ago class-wc-post-data.php 4 weeks ago class-wc-post-types.php 4 weeks ago class-wc-privacy-background-process.php 1 year ago class-wc-privacy-erasers.php 9 months ago class-wc-privacy-exporters.php 4 years ago class-wc-privacy.php 11 months ago class-wc-product-attribute.php 3 months ago class-wc-product-download.php 3 months ago class-wc-product-external.php 1 year ago class-wc-product-factory.php 2 months ago class-wc-product-grouped.php 2 months ago class-wc-product-query.php 3 months ago class-wc-product-simple.php 10 months ago class-wc-product-variable.php 2 months ago class-wc-product-variation.php 1 year ago class-wc-query.php 4 weeks ago class-wc-rate-limiter.php 4 years ago class-wc-regenerate-images-request.php 3 years ago class-wc-regenerate-images.php 1 year ago class-wc-register-wp-admin-settings.php 4 years ago class-wc-rest-authentication.php 1 year ago class-wc-rest-exception.php 5 years ago class-wc-session-handler.php 2 months ago class-wc-shipping-rate.php 11 months ago class-wc-shipping-zone.php 5 years ago class-wc-shipping-zones.php 6 months ago class-wc-shipping.php 4 weeks ago class-wc-shortcodes.php 1 year ago class-wc-structured-data.php 4 weeks ago class-wc-tax.php 4 weeks ago class-wc-template-loader.php 6 months ago class-wc-tracker.php 7 months ago class-wc-validation.php 2 years ago class-wc-webhook.php 4 weeks ago class-woocommerce.php 3 weeks ago wc-account-functions.php 6 months ago wc-attribute-functions.php 4 weeks ago wc-brands-functions.php 1 year ago wc-cart-functions.php 4 months ago wc-conditional-functions.php 10 months ago wc-core-functions.php 4 weeks ago wc-coupon-functions.php 4 months ago wc-deprecated-functions.php 3 months ago wc-formatting-functions.php 6 months ago wc-interactivity-api-functions.php 4 weeks ago wc-notice-functions.php 4 months ago wc-order-functions.php 3 weeks ago wc-order-item-functions.php 3 years ago wc-order-step-logger-functions.php 3 months ago wc-page-functions.php 3 weeks ago wc-product-functions.php 4 weeks ago wc-rest-functions.php 6 months ago wc-stock-functions.php 6 months ago wc-template-functions.php 4 weeks ago wc-template-hooks.php 9 months ago wc-term-functions.php 4 weeks ago wc-update-functions.php 3 weeks ago wc-user-functions.php 4 weeks ago wc-webhook-functions.php 4 weeks ago wc-widget-functions.php 5 years ago
wc-formatting-functions.php
1692 lines
1 <?php
2 /**
3 * WooCommerce Formatting
4 *
5 * Functions for formatting data.
6 *
7 * @package WooCommerce\Functions
8 * @version 2.1.0
9 */
10
11 use Automattic\WooCommerce\Utilities\I18nUtil;
12 use Automattic\WooCommerce\Utilities\NumberUtil;
13
14 defined( 'ABSPATH' ) || exit;
15
16 // Once WooCommerce requires PHP 7.4, the "$x = $x ?? ''" constructs can be replaced with "$x ??= ''".
17
18 /**
19 * Converts a string (e.g. 'yes' or 'no') to a bool.
20 *
21 * @since 3.0.0
22 * @param string|bool $string String to convert. If a bool is passed it will be returned as-is.
23 * @return bool
24 */
25 function wc_string_to_bool( $string ) {
26 $string = $string ?? '';
27 return is_bool( $string ) ? $string : ( 'yes' === strtolower( $string ) || 1 === $string || 'true' === strtolower( $string ) || '1' === $string );
28 }
29
30 /**
31 * Converts a bool to a 'yes' or 'no'.
32 *
33 * @since 3.0.0
34 * @param bool|string $bool Bool to convert. If a string is passed it will first be converted to a bool.
35 * @return string
36 */
37 function wc_bool_to_string( $bool ) {
38 if ( ! is_bool( $bool ) ) {
39 $bool = wc_string_to_bool( $bool );
40 }
41 return true === $bool ? 'yes' : 'no';
42 }
43
44 /**
45 * Explode a string into an array by $delimiter and remove empty values.
46 *
47 * @since 3.0.0
48 * @param string $string String to convert.
49 * @param string $delimiter Delimiter, defaults to ','.
50 * @return array
51 */
52 function wc_string_to_array( $string, $delimiter = ',' ) {
53 $string = $string ?? '';
54 return is_array( $string ) ? $string : array_filter( explode( $delimiter, $string ) );
55 }
56
57 /**
58 * Sanitize taxonomy names. Slug format (no spaces, lowercase).
59 * Urldecode is used to reverse munging of UTF8 characters.
60 *
61 * @param string $taxonomy Taxonomy name.
62 * @return string
63 */
64 function wc_sanitize_taxonomy_name( $taxonomy ) {
65 return apply_filters( 'sanitize_taxonomy_name', urldecode( sanitize_title( urldecode( $taxonomy ?? '' ) ) ), $taxonomy );
66 }
67
68 /**
69 * Sanitize permalink values before insertion into DB.
70 *
71 * Cannot use wc_clean because it sometimes strips % chars and breaks the user's setting.
72 *
73 * @since 2.6.0
74 * @param string $value Permalink.
75 * @return string
76 */
77 function wc_sanitize_permalink( $value ) {
78 global $wpdb;
79
80 $value = $wpdb->strip_invalid_text_for_column( $wpdb->options, 'option_value', $value ?? '' );
81
82 if ( is_wp_error( $value ) ) {
83 $value = '';
84 }
85
86 $value = esc_url_raw( trim( $value ) );
87 $value = str_replace( 'http://', '', $value );
88 return untrailingslashit( $value );
89 }
90
91 /**
92 * Gets the filename part of a download URL.
93 *
94 * @param string $file_url File URL.
95 * @return string
96 */
97 function wc_get_filename_from_url( $file_url ) {
98 $parts = wp_parse_url( $file_url );
99 if ( isset( $parts['path'] ) ) {
100 return basename( $parts['path'] );
101 }
102 }
103
104 /**
105 * Normalise dimensions, unify to cm then convert to wanted unit value.
106 *
107 * Usage:
108 * wc_get_dimension( 55, 'in' );
109 * wc_get_dimension( 55, 'in', 'm' );
110 *
111 * @param int|float $dimension Dimension.
112 * @param string $to_unit Unit to convert to.
113 * Options: 'in', 'mm', 'cm', 'm'.
114 * @param string $from_unit Unit to convert from.
115 * Defaults to ''.
116 * Options: 'in', 'mm', 'cm', 'm'.
117 * @return float
118 */
119 function wc_get_dimension( $dimension, $to_unit, $from_unit = '' ) {
120 $to_unit = strtolower( $to_unit );
121
122 if ( empty( $from_unit ) ) {
123 $from_unit = strtolower( get_option( 'woocommerce_dimension_unit' ) );
124 }
125
126 // Unify all units to cm first.
127 if ( $from_unit !== $to_unit ) {
128 switch ( $from_unit ) {
129 case 'in':
130 $dimension *= 2.54;
131 break;
132 case 'm':
133 $dimension *= 100;
134 break;
135 case 'mm':
136 $dimension *= 0.1;
137 break;
138 case 'yd':
139 $dimension *= 91.44;
140 break;
141 }
142
143 // Output desired unit.
144 switch ( $to_unit ) {
145 case 'in':
146 $dimension *= 0.3937;
147 break;
148 case 'm':
149 $dimension *= 0.01;
150 break;
151 case 'mm':
152 $dimension *= 10;
153 break;
154 case 'yd':
155 $dimension *= 0.010936133;
156 break;
157 }
158 }
159
160 return ( $dimension < 0 ) ? 0 : $dimension;
161 }
162
163 /**
164 * Normalise weights, unify to kg then convert to wanted unit value.
165 *
166 * Usage:
167 * wc_get_weight(55, 'kg');
168 * wc_get_weight(55, 'kg', 'lbs');
169 *
170 * @param int|float $weight Weight.
171 * @param string $to_unit Unit to convert to.
172 * Options: 'g', 'kg', 'lbs', 'oz'.
173 * @param string $from_unit Unit to convert from.
174 * Defaults to ''.
175 * Options: 'g', 'kg', 'lbs', 'oz'.
176 * @return float
177 */
178 function wc_get_weight( $weight, $to_unit, $from_unit = '' ) {
179 $weight = (float) $weight;
180 $to_unit = strtolower( $to_unit );
181
182 if ( empty( $from_unit ) ) {
183 $from_unit = strtolower( get_option( 'woocommerce_weight_unit' ) );
184 }
185
186 // Unify all units to kg first.
187 if ( $from_unit !== $to_unit ) {
188 switch ( $from_unit ) {
189 case 'g':
190 $weight *= 0.001;
191 break;
192 case 'lbs':
193 $weight *= 0.453592;
194 break;
195 case 'oz':
196 $weight *= 0.0283495;
197 break;
198 }
199
200 // Output desired unit.
201 switch ( $to_unit ) {
202 case 'g':
203 $weight *= 1000;
204 break;
205 case 'lbs':
206 $weight *= 2.20462;
207 break;
208 case 'oz':
209 $weight *= 35.274;
210 break;
211 }
212 }
213
214 return ( $weight < 0 ) ? 0 : $weight;
215 }
216
217 /**
218 * Trim trailing zeros off prices.
219 *
220 * @param string|float|int $price Price.
221 * @return string
222 */
223 function wc_trim_zeros( $price ) {
224 return preg_replace( '/' . preg_quote( wc_get_price_decimal_separator(), '/' ) . '0++$/', '', $price ?? '' );
225 }
226
227 /**
228 * Round a tax amount.
229 *
230 * @param double $value Amount to round.
231 * @param int $precision DP to round. Defaults to wc_get_price_decimals.
232 * @return float
233 */
234 function wc_round_tax_total( $value, $precision = null ) {
235 $precision = is_null( $precision ) ? wc_get_price_decimals() : intval( $precision );
236 $rounded_tax = NumberUtil::round( $value, $precision, wc_get_tax_rounding_mode() ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.round_modeFound
237
238 return apply_filters( 'wc_round_tax_total', $rounded_tax, $value, $precision, WC_TAX_ROUNDING_MODE );
239 }
240
241 /**
242 * Round half down in PHP 5.2.
243 *
244 * @since 3.2.6
245 * @param float $value Value to round.
246 * @param int $precision Precision to round down to.
247 * @return float
248 */
249 function wc_legacy_round_half_down( $value, $precision ) {
250 $value = wc_float_to_string( $value ) ?? '';
251
252 if ( false !== strstr( $value, '.' ) ) {
253 $value = explode( '.', $value );
254
255 if ( strlen( $value[1] ) > $precision && substr( $value[1], -1 ) === '5' ) {
256 $value[1] = substr( $value[1], 0, -1 ) . '4';
257 }
258
259 $value = implode( '.', $value );
260 }
261
262 return NumberUtil::round( floatval( $value ), $precision );
263 }
264
265 /**
266 * Make a refund total negative.
267 *
268 * @param float $amount Refunded amount.
269 *
270 * @return float
271 */
272 function wc_format_refund_total( $amount ) {
273 return $amount * -1;
274 }
275
276 /**
277 * Format decimal numbers ready for DB storage.
278 *
279 * Sanitize, optionally remove decimals, and optionally round + trim off zeros.
280 *
281 * This function does not remove thousands - this should be done before passing a value to the function.
282 *
283 * @param float|string $number Expects either a float or a string with a decimal separator only (no thousands).
284 * @param mixed $dp number Number of decimal points to use, blank to use woocommerce_price_num_decimals, or false to avoid all rounding.
285 * @param bool $trim_zeros From end of string.
286 * @return string
287 */
288 function wc_format_decimal( $number, $dp = false, $trim_zeros = false ) {
289 $number = $number ?? '';
290
291 if ( '' === $number ) {
292 return '';
293 }
294
295 $locale = localeconv();
296 $decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'] );
297
298 // Remove locale from string.
299 if ( ! is_float( $number ) ) {
300 $number = str_replace( $decimals, '.', $number );
301
302 // Convert multiple dots to just one.
303 $number = preg_replace( '/\.(?![^.]+$)|[^0-9.-]/', '', wc_clean( $number ) );
304 }
305
306 if ( false !== $dp ) {
307 $dp = intval( '' === $dp ? wc_get_price_decimals() : $dp );
308 $number = number_format( floatval( $number ), $dp, '.', '' );
309 } elseif ( is_float( $number ) ) {
310 // DP is false - don't use number format, just return a string using whatever is given. Remove scientific notation using sprintf.
311 $number = str_replace( $decimals, '.', sprintf( '%.' . wc_get_rounding_precision() . 'f', $number ) );
312 // We already had a float, so trailing zeros are not needed.
313 $trim_zeros = true;
314 }
315
316 if ( $trim_zeros && strstr( $number, '.' ) ) {
317 $number = rtrim( rtrim( $number, '0' ), '.' );
318 }
319
320 return $number;
321 }
322
323 /**
324 * Convert a float to a string without locale formatting which PHP adds when changing floats to strings.
325 *
326 * @param float $float Float value to format.
327 * @return string
328 */
329 function wc_float_to_string( $float ) {
330 if ( ! is_float( $float ) ) {
331 return $float;
332 }
333
334 $locale = localeconv();
335 $string = strval( $float );
336 $string = str_replace( $locale['decimal_point'], '.', $string );
337
338 return $string;
339 }
340
341 /**
342 * Format a price with WC Currency Locale settings.
343 *
344 * @param string $value Price to localize.
345 * @return string
346 */
347 function wc_format_localized_price( $value ) {
348 return apply_filters( 'woocommerce_format_localized_price', str_replace( '.', wc_get_price_decimal_separator(), strval( $value ) ), $value );
349 }
350
351 /**
352 * Format a decimal with the decimal separator for prices or PHP Locale settings.
353 *
354 * @param string $value Decimal to localize.
355 * @return string
356 */
357 function wc_format_localized_decimal( $value ) {
358 $locale = localeconv();
359 $decimal_point = isset( $locale['decimal_point'] ) ? $locale['decimal_point'] : '.';
360 $decimal = ( ! empty( wc_get_price_decimal_separator() ) ) ? wc_get_price_decimal_separator() : $decimal_point;
361 return apply_filters( 'woocommerce_format_localized_decimal', str_replace( '.', $decimal, strval( $value ) ), $value );
362 }
363
364 /**
365 * Format a coupon code.
366 *
367 * @since 3.0.0
368 * @param string $value Coupon code to format.
369 * @return string
370 */
371 function wc_format_coupon_code( $value ) {
372 return apply_filters( 'woocommerce_coupon_code', $value );
373 }
374
375 /**
376 * Sanitize a coupon code.
377 *
378 * Uses sanitize_post_field since coupon codes are stored as post_titles - the sanitization and escaping must match.
379 *
380 * Due to the unfiltered_html captability that some (admin) users have, we need to account for slashes.
381 *
382 * The html_entity_decode() call handles coupon codes that contain special characters like ampersands (&), quotes ("),
383 * and other HTML entities. Without this decoding step, coupon codes with special characters would fail to match
384 * during application, causing legitimate coupons to be rejected.
385 *
386 * @see WC_Cart_Test::test_coupon_codes_with_special_characters
387 *
388 * @since 3.6.0
389 * @since 10.0.0 Decode HTML entities here instead of via woocommerce_coupon_code filter.
390 * @param string $value Coupon code to format.
391 * @return string
392 */
393 function wc_sanitize_coupon_code( $value ) {
394 $value = wp_kses( sanitize_post_field( 'post_title', html_entity_decode( $value ?? '', ENT_COMPAT, get_bloginfo( 'charset' ) ), 0, 'db' ), 'entities' );
395 return current_user_can( 'unfiltered_html' ) ? $value : stripslashes( $value );
396 }
397
398 /**
399 * Clean variables using sanitize_text_field. Arrays are cleaned recursively.
400 * Non-scalar values are ignored.
401 *
402 * @param string|array $var Data to sanitize.
403 * @return string|array
404 */
405 function wc_clean( $var ) {
406 if ( is_array( $var ) ) {
407 return array_map( 'wc_clean', $var );
408 } else {
409 return is_scalar( $var ) ? sanitize_text_field( $var ) : $var;
410 }
411 }
412
413 /**
414 * Function wp_check_invalid_utf8 with recursive array support.
415 *
416 * @param string|array $var Data to sanitize.
417 * @return string|array
418 */
419 function wc_check_invalid_utf8( $var ) {
420 if ( is_array( $var ) ) {
421 return array_map( 'wc_check_invalid_utf8', $var );
422 } else {
423 return wp_check_invalid_utf8( $var );
424 }
425 }
426
427 /**
428 * Run wc_clean over posted textarea but maintain line breaks.
429 *
430 * @since 3.0.0
431 * @param string $var Data to sanitize.
432 * @return string
433 */
434 function wc_sanitize_textarea( $var ) {
435 return implode( "\n", array_map( 'wc_clean', explode( "\n", $var ?? '' ) ) );
436 }
437
438 /**
439 * Sanitize a string destined to be a tooltip.
440 *
441 * @since 2.3.10 Tooltips are encoded with htmlspecialchars to prevent XSS. Should not be used in conjunction with esc_attr()
442 * @param string $var Data to sanitize.
443 * @return string
444 */
445 function wc_sanitize_tooltip( $var ) {
446 return htmlspecialchars(
447 wp_kses(
448 html_entity_decode( $var ?? '' ),
449 array(
450 'br' => array(),
451 'em' => array(),
452 'strong' => array(),
453 'small' => array(),
454 'span' => array(),
455 'ul' => array(),
456 'li' => array(),
457 'ol' => array(),
458 'p' => array(),
459 )
460 )
461 );
462 }
463
464 /**
465 * Merge two arrays.
466 *
467 * @param array $a1 First array to merge.
468 * @param array $a2 Second array to merge.
469 * @return array
470 */
471 function wc_array_overlay( $a1, $a2 ) {
472 foreach ( $a1 as $k => $v ) {
473 if ( ! array_key_exists( $k, $a2 ) ) {
474 continue;
475 }
476 if ( is_array( $v ) && is_array( $a2[ $k ] ) ) {
477 $a1[ $k ] = wc_array_overlay( $v, $a2[ $k ] );
478 } else {
479 $a1[ $k ] = $a2[ $k ];
480 }
481 }
482 return $a1;
483 }
484
485 /**
486 * Formats a stock amount by running it through a filter.
487 *
488 * @param int|float $amount Stock amount.
489 * @return int|float
490 */
491 function wc_stock_amount( $amount ) {
492 /**
493 * Filter the stock amount. If an invalid value is returned by hooks, falls back to intval( $amount ).
494 *
495 * @since 2.3
496 * @param int|float $amount Stock amount.
497 * @return int|float
498 */
499 return NumberUtil::normalize( apply_filters( 'woocommerce_stock_amount', $amount ), intval( $amount ) );
500 }
501
502 /**
503 * Check if the stock amount is an integer.
504 *
505 * @since 10.1.0
506 * @return bool
507 */
508 function wc_is_stock_amount_integer() {
509 return wc_stock_amount( 1 ) === 1;
510 }
511
512 /**
513 * Get the price format depending on the currency position.
514 *
515 * @return string
516 */
517 function get_woocommerce_price_format() {
518 $currency_pos = get_option( 'woocommerce_currency_pos' );
519 $format = '%1$s%2$s';
520
521 switch ( $currency_pos ) {
522 case 'left':
523 $format = '%1$s%2$s';
524 break;
525 case 'right':
526 $format = '%2$s%1$s';
527 break;
528 case 'left_space':
529 $format = '%1$s&nbsp;%2$s';
530 break;
531 case 'right_space':
532 $format = '%2$s&nbsp;%1$s';
533 break;
534 }
535
536 return apply_filters( 'woocommerce_price_format', $format, $currency_pos );
537 }
538
539 /**
540 * Return the thousand separator for prices.
541 *
542 * @since 2.3
543 * @return string
544 */
545 function wc_get_price_thousand_separator() {
546 return stripslashes( apply_filters( 'wc_get_price_thousand_separator', get_option( 'woocommerce_price_thousand_sep' ) ) );
547 }
548
549 /**
550 * Return the decimal separator for prices.
551 *
552 * @since 2.3
553 * @return string
554 */
555 function wc_get_price_decimal_separator() {
556 $separator = apply_filters( 'wc_get_price_decimal_separator', get_option( 'woocommerce_price_decimal_sep' ) );
557 return $separator ? stripslashes( $separator ) : '.';
558 }
559
560 /**
561 * Return the number of decimals after the decimal point.
562 *
563 * @since 2.3
564 * @return int
565 */
566 function wc_get_price_decimals() {
567 return absint( apply_filters( 'wc_get_price_decimals', get_option( 'woocommerce_price_num_decimals', 2 ) ) );
568 }
569
570 /**
571 * Format the price with a currency symbol.
572 *
573 * @param float $price Raw price.
574 * @param array $args Arguments to format a price {
575 * Array of arguments.
576 * Defaults to empty array.
577 *
578 * @type bool $ex_tax_label Adds exclude tax label.
579 * Defaults to false.
580 * @type string $currency Currency code.
581 * Defaults to empty string (Use the result from get_woocommerce_currency()).
582 * @type string $decimal_separator Decimal separator.
583 * Defaults the result of wc_get_price_decimal_separator().
584 * @type string $thousand_separator Thousand separator.
585 * Defaults the result of wc_get_price_thousand_separator().
586 * @type string $decimals Number of decimals.
587 * Defaults the result of wc_get_price_decimals().
588 * @type string $price_format Price format depending on the currency position.
589 * Defaults the result of get_woocommerce_price_format().
590 * @type bool $in_span Whether to enclose the formatted price in an HTML <span> element.
591 * Defaults to true.
592 * }
593 * @return string
594 */
595 function wc_price( $price, $args = array() ) {
596 $args = apply_filters(
597 'wc_price_args',
598 wp_parse_args(
599 $args,
600 array(
601 'ex_tax_label' => false,
602 'currency' => '',
603 'decimal_separator' => wc_get_price_decimal_separator(),
604 'thousand_separator' => wc_get_price_thousand_separator(),
605 'decimals' => wc_get_price_decimals(),
606 'price_format' => get_woocommerce_price_format(),
607 'in_span' => true,
608 'aria-hidden' => false,
609 )
610 )
611 );
612
613 $original_price = $price;
614
615 // Convert to float to avoid issues on PHP 8.
616 $price = (float) $price;
617
618 $unformatted_price = $price;
619 $negative = $price < 0;
620
621 /**
622 * Filter raw price.
623 *
624 * @param float $raw_price Raw price.
625 * @param float|string $original_price Original price as float, or empty string. Since 5.0.0.
626 */
627 $price = apply_filters( 'raw_woocommerce_price', $negative ? $price * -1 : $price, $original_price );
628
629 /**
630 * Filter formatted price.
631 *
632 * @param float $formatted_price Formatted price.
633 * @param float $price Unformatted price.
634 * @param int $decimals Number of decimals.
635 * @param string $decimal_separator Decimal separator.
636 * @param string $thousand_separator Thousand separator.
637 * @param float|string $original_price Original price as float, or empty string. Since 5.0.0.
638 */
639 $price = apply_filters( 'formatted_woocommerce_price', number_format( $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'] ), $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'], $original_price );
640
641 if ( apply_filters( 'woocommerce_price_trim_zeros', false ) && $args['decimals'] > 0 ) {
642 $price = wc_trim_zeros( $price );
643 }
644
645 if ( $args['in_span'] ) {
646 $formatted_price = ( $negative ? '-' : '' ) . sprintf( $args['price_format'], '<span class="woocommerce-Price-currencySymbol">' . get_woocommerce_currency_symbol( $args['currency'] ) . '</span>', $price );
647 $aria_hidden = $args['aria-hidden'] ? ' aria-hidden="true"' : '';
648 $return = '<span class="woocommerce-Price-amount amount"' . $aria_hidden . '><bdi>' . $formatted_price . '</bdi></span>';
649 } else {
650 $formatted_price = ( $negative ? '-' : '' ) . sprintf( $args['price_format'], get_woocommerce_currency_symbol( $args['currency'] ), $price );
651 $return = $formatted_price;
652 }
653
654 if ( $args['ex_tax_label'] && wc_tax_enabled() ) {
655 $return .= ' <small class="woocommerce-Price-taxLabel tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>';
656 }
657
658 /**
659 * Filters the string of price markup.
660 *
661 * @param string $return Price HTML markup.
662 * @param string $price Formatted price.
663 * @param array $args Pass on the args.
664 * @param float $unformatted_price Price as float to allow plugins custom formatting. Since 3.2.0.
665 * @param float|string $original_price Original price as float, or empty string. Since 5.0.0.
666 */
667 return apply_filters( 'wc_price', $return, $price, $args, $unformatted_price, $original_price );
668 }
669
670 /**
671 * Notation to numbers.
672 *
673 * This function transforms the php.ini notation for numbers (like '2M') to an integer.
674 *
675 * @param string $size Size value.
676 * @return int
677 */
678 function wc_let_to_num( $size ) {
679 $size = $size ?? '';
680
681 $l = substr( $size, -1 );
682 $ret = (int) substr( $size, 0, -1 );
683 switch ( strtoupper( $l ) ) {
684 case 'P':
685 $ret *= 1024;
686 // No break.
687 case 'T':
688 $ret *= 1024;
689 // No break.
690 case 'G':
691 $ret *= 1024;
692 // No break.
693 case 'M':
694 $ret *= 1024;
695 // No break.
696 case 'K':
697 $ret *= 1024;
698 // No break.
699 }
700 return $ret;
701 }
702
703 /**
704 * WooCommerce Date Format - Allows to change date format for everything WooCommerce.
705 *
706 * @return string
707 */
708 function wc_date_format() {
709 $date_format = get_option( 'date_format' );
710 if ( empty( $date_format ) ) {
711 // Return default date format if the option is empty.
712 $date_format = 'F j, Y';
713 }
714 return apply_filters( 'woocommerce_date_format', $date_format );
715 }
716
717 /**
718 * WooCommerce Time Format - Allows to change time format for everything WooCommerce.
719 *
720 * @return string
721 */
722 function wc_time_format() {
723 $time_format = get_option( 'time_format' );
724 if ( empty( $time_format ) ) {
725 // Return default time format if the option is empty.
726 $time_format = 'g:i a';
727 }
728 return apply_filters( 'woocommerce_time_format', $time_format );
729 }
730
731 /**
732 * Convert mysql datetime to PHP timestamp, forcing UTC. Wrapper for strtotime.
733 *
734 * Based on wcs_strtotime_dark_knight() from WC Subscriptions by Prospress.
735 *
736 * @since 3.0.0
737 * @param string $time_string Time string.
738 * @param int|null $from_timestamp Timestamp to convert from.
739 * @return int
740 */
741 function wc_string_to_timestamp( $time_string, $from_timestamp = null ) {
742 $time_string = $time_string ?? '';
743
744 $original_timezone = date_default_timezone_get();
745
746 // @codingStandardsIgnoreStart
747 date_default_timezone_set( 'UTC' );
748
749 if ( null === $from_timestamp ) {
750 $next_timestamp = strtotime( $time_string );
751 } else {
752 $next_timestamp = strtotime( $time_string, $from_timestamp );
753 }
754
755 date_default_timezone_set( $original_timezone );
756 // @codingStandardsIgnoreEnd
757
758 return $next_timestamp;
759 }
760
761 /**
762 * Convert a date string to a WC_DateTime.
763 *
764 * @since 3.1.0
765 * @param string $time_string Time string.
766 * @return WC_DateTime
767 */
768 function wc_string_to_datetime( $time_string ) {
769 $time_string = $time_string ?? '';
770
771 // Strings are defined in local WP timezone. Convert to UTC.
772 if ( 1 === preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|((-|\+)\d{2}:\d{2}))$/', $time_string, $date_bits ) ) {
773 $offset = ! empty( $date_bits[7] ) ? iso8601_timezone_to_offset( $date_bits[7] ) : wc_timezone_offset();
774 $timestamp = gmmktime( $date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1] ) - $offset;
775 } else {
776 $timestamp = wc_string_to_timestamp( get_gmt_from_date( gmdate( 'Y-m-d H:i:s', wc_string_to_timestamp( $time_string ) ) ) );
777 }
778 $datetime = new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) );
779
780 // Set local timezone or offset.
781 if ( get_option( 'timezone_string' ) ) {
782 $datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) );
783 } else {
784 $datetime->set_utc_offset( wc_timezone_offset() );
785 }
786
787 return $datetime;
788 }
789
790 /**
791 * WooCommerce Timezone - helper to retrieve the timezone string for a site until.
792 * a WP core method exists (see https://core.trac.wordpress.org/ticket/24730).
793 *
794 * Adapted from https://secure.php.net/manual/en/function.timezone-name-from-abbr.php#89155.
795 *
796 * @since 2.1
797 * @return string PHP timezone string for the site
798 */
799 function wc_timezone_string() {
800 // Added in WordPress 5.3 Ref https://developer.wordpress.org/reference/functions/wp_timezone_string/.
801 if ( function_exists( 'wp_timezone_string' ) ) {
802 return wp_timezone_string();
803 }
804
805 // If site timezone string exists, return it.
806 $timezone = get_option( 'timezone_string' );
807 if ( $timezone ) {
808 return $timezone;
809 }
810
811 // Get UTC offset, if it isn't set then return UTC.
812 $utc_offset = floatval( get_option( 'gmt_offset', 0 ) );
813 if ( ! is_numeric( $utc_offset ) || 0.0 === $utc_offset ) {
814 return 'UTC';
815 }
816
817 // Adjust UTC offset from hours to seconds.
818 $utc_offset = (int) ( $utc_offset * 3600 );
819
820 // Attempt to guess the timezone string from the UTC offset.
821 $timezone = timezone_name_from_abbr( '', $utc_offset );
822 if ( $timezone ) {
823 return $timezone;
824 }
825
826 // Last try, guess timezone string manually.
827 foreach ( timezone_abbreviations_list() as $abbr ) {
828 foreach ( $abbr as $city ) {
829 // WordPress restrict the use of date(), since it's affected by timezone settings, but in this case is just what we need to guess the correct timezone.
830 if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
831 return $city['timezone_id'];
832 }
833 }
834 }
835
836 // Fallback to UTC.
837 return 'UTC';
838 }
839
840 /**
841 * Get timezone offset in seconds.
842 *
843 * @since 3.0.0
844 * @return float
845 */
846 function wc_timezone_offset() {
847 $timezone = get_option( 'timezone_string' );
848
849 if ( $timezone ) {
850 $timezone_object = new DateTimeZone( $timezone );
851 return $timezone_object->getOffset( new DateTime( 'now' ) );
852 } else {
853 return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS;
854 }
855 }
856
857 /**
858 * Callback which can flatten post meta (gets the first value if it's an array).
859 *
860 * @since 3.0.0
861 * @param array $value Value to flatten.
862 * @return mixed
863 */
864 function wc_flatten_meta_callback( $value ) {
865 return is_array( $value ) ? current( $value ) : $value;
866 }
867
868 if ( ! function_exists( 'wc_rgb_from_hex' ) ) {
869
870 /**
871 * Convert RGB to HEX.
872 *
873 * @param mixed $color Color.
874 *
875 * @return array
876 */
877 function wc_rgb_from_hex( $color ) {
878 $color = str_replace( '#', '', $color ?? '000' );
879 // Convert shorthand colors to full format, e.g. "FFF" -> "FFFFFF".
880 $color = preg_replace( '~^(.)(.)(.)$~', '$1$1$2$2$3$3', $color );
881
882 $rgb = array();
883 $rgb['R'] = hexdec( $color[0] . $color[1] );
884 $rgb['G'] = hexdec( $color[2] . $color[3] );
885 $rgb['B'] = hexdec( $color[4] . $color[5] );
886
887 return $rgb;
888 }
889 }
890
891 if ( ! function_exists( 'wc_hex_darker' ) ) {
892
893 /**
894 * Make HEX color darker.
895 *
896 * @param mixed $color Color.
897 * @param int $factor Darker factor.
898 * Defaults to 30.
899 * @return string
900 */
901 function wc_hex_darker( $color, $factor = 30 ) {
902 $base = wc_rgb_from_hex( $color );
903 $color = '#';
904
905 foreach ( $base as $k => $v ) {
906 $amount = $v / 100;
907 $amount = NumberUtil::round( $amount * $factor );
908 $new_decimal = $v - $amount;
909
910 $new_hex_component = dechex( $new_decimal );
911 if ( strlen( $new_hex_component ) < 2 ) {
912 $new_hex_component = '0' . $new_hex_component;
913 }
914 $color .= $new_hex_component;
915 }
916
917 return $color;
918 }
919 }
920
921 if ( ! function_exists( 'wc_hex_lighter' ) ) {
922
923 /**
924 * Make HEX color lighter.
925 *
926 * @param mixed $color Color.
927 * @param int $factor Lighter factor.
928 * Defaults to 30.
929 * @return string
930 */
931 function wc_hex_lighter( $color, $factor = 30 ) {
932 $base = wc_rgb_from_hex( $color );
933 $color = '#';
934
935 foreach ( $base as $k => $v ) {
936 $amount = 255 - $v;
937 $amount = $amount / 100;
938 $amount = NumberUtil::round( $amount * $factor );
939 $new_decimal = $v + $amount;
940
941 $new_hex_component = dechex( $new_decimal );
942 if ( strlen( $new_hex_component ) < 2 ) {
943 $new_hex_component = '0' . $new_hex_component;
944 }
945 $color .= $new_hex_component;
946 }
947
948 return $color;
949 }
950 }
951
952 if ( ! function_exists( 'wc_hex_is_light' ) ) {
953
954 /**
955 * Determine whether a hex color is light.
956 *
957 * @param mixed $color Color.
958 * @return bool True if a light color.
959 */
960 function wc_hex_is_light( $color ) {
961 $hex = str_replace( '#', '', $color ?? '' );
962
963 $c_r = hexdec( substr( $hex, 0, 2 ) );
964 $c_g = hexdec( substr( $hex, 2, 2 ) );
965 $c_b = hexdec( substr( $hex, 4, 2 ) );
966
967 $brightness = ( ( $c_r * 299 ) + ( $c_g * 587 ) + ( $c_b * 114 ) ) / 1000;
968
969 return $brightness > 155;
970 }
971 }
972
973 if ( ! function_exists( 'wc_light_or_dark' ) ) {
974
975 /**
976 * Detect if we should use a light or dark color on a background color.
977 *
978 * @param mixed $color Color.
979 * @param string $dark Darkest reference.
980 * Defaults to '#000000'.
981 * @param string $light Lightest reference.
982 * Defaults to '#FFFFFF'.
983 * @return string
984 */
985 function wc_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) {
986 return wc_hex_is_light( $color ) ? $dark : $light;
987 }
988 }
989
990 if ( ! function_exists( 'wc_format_hex' ) ) {
991
992 /**
993 * Format string as hex.
994 *
995 * @param string $hex HEX color.
996 * @return string|null
997 */
998 function wc_format_hex( $hex ) {
999 $hex = trim( str_replace( '#', '', $hex ?? '' ) );
1000
1001 if ( strlen( $hex ) === 3 ) {
1002 $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
1003 }
1004
1005 return $hex ? '#' . $hex : null;
1006 }
1007 }
1008
1009 /**
1010 * Format the postcode according to the country and length of the postcode.
1011 *
1012 * @param string $postcode Unformatted postcode.
1013 * @param string $country Base country.
1014 * @return string
1015 */
1016 function wc_format_postcode( $postcode, $country ) {
1017 $postcode = wc_normalize_postcode( $postcode ?? '' );
1018
1019 switch ( $country ) {
1020 case 'SE':
1021 $postcode = substr_replace( $postcode, ' ', -2, 0 );
1022 break;
1023 case 'CA':
1024 case 'GB':
1025 $postcode = substr_replace( $postcode, ' ', -3, 0 );
1026 break;
1027 case 'IE':
1028 $postcode = substr_replace( $postcode, ' ', 3, 0 );
1029 break;
1030 case 'BR':
1031 case 'PL':
1032 $postcode = substr_replace( $postcode, '-', -3, 0 );
1033 break;
1034 case 'JP':
1035 $postcode = substr_replace( $postcode, '-', 3, 0 );
1036 break;
1037 case 'PT':
1038 $postcode = substr_replace( $postcode, '-', 4, 0 );
1039 break;
1040 case 'PR':
1041 case 'US':
1042 case 'MN':
1043 $postcode = rtrim( substr_replace( $postcode, '-', 5, 0 ), '-' );
1044 break;
1045 case 'NL':
1046 $postcode = substr_replace( $postcode, ' ', 4, 0 );
1047 break;
1048 case 'LV':
1049 $postcode = preg_replace( '/^(LV)?-?(\d+)$/', 'LV-${2}', $postcode );
1050 break;
1051 case 'CZ':
1052 case 'SK':
1053 $postcode = preg_replace( "/^({$country})-?(\d+)$/", '${1}-${2}', $postcode );
1054 $postcode = substr_replace( $postcode, ' ', -2, 0 );
1055 break;
1056 case 'DK':
1057 $postcode = preg_replace( '/^(DK)(.+)$/', '${1}-${2}', $postcode );
1058 break;
1059 }
1060
1061 return apply_filters( 'woocommerce_format_postcode', trim( $postcode ), $country );
1062 }
1063
1064 /**
1065 * Normalize postcodes.
1066 *
1067 * Remove spaces and convert characters to uppercase.
1068 *
1069 * @since 2.6.0
1070 * @param string $postcode Postcode.
1071 * @return string
1072 */
1073 function wc_normalize_postcode( $postcode ) {
1074 return preg_replace( '/[\s\-]/', '', trim( wc_strtoupper( $postcode ?? '' ) ) );
1075 }
1076
1077 /**
1078 * Format phone numbers.
1079 *
1080 * @param string $phone Phone number.
1081 * @return string
1082 */
1083 function wc_format_phone_number( $phone ) {
1084 $phone = $phone ?? '';
1085
1086 if ( ! WC_Validation::is_phone( $phone ) ) {
1087 return '';
1088 }
1089 return preg_replace( '/[^0-9\+\-\(\)\s]/', '-', preg_replace( '/[\x00-\x1F\x7F-\xFF]/', '', $phone ) );
1090 }
1091
1092 /**
1093 * Sanitize phone number.
1094 * Allows only numbers and "+" (plus sign).
1095 *
1096 * @since 3.6.0
1097 * @param string $phone Phone number.
1098 * @return string
1099 */
1100 function wc_sanitize_phone_number( $phone ) {
1101 return preg_replace( '/[^\d+]/', '', $phone ?? '' );
1102 }
1103
1104 /**
1105 * Wrapper for mb_strtoupper which see's if supported first.
1106 *
1107 * @since 3.1.0
1108 * @param string $string String to format.
1109 * @return string
1110 */
1111 function wc_strtoupper( $string ) {
1112 $string = $string ?? '';
1113 return function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $string ) : strtoupper( $string );
1114 }
1115
1116 /**
1117 * Make a string lowercase.
1118 * Try to use mb_strtolower() when available.
1119 *
1120 * @since 2.3
1121 * @param string $string String to format.
1122 * @return string
1123 */
1124 function wc_strtolower( $string ) {
1125 $string = $string ?? '';
1126 return function_exists( 'mb_strtolower' ) ? mb_strtolower( $string ) : strtolower( $string );
1127 }
1128
1129 /**
1130 * Trim a string and append a suffix.
1131 *
1132 * @param string $string String to trim.
1133 * @param integer $chars Amount of characters.
1134 * Defaults to 200.
1135 * @param string $suffix Suffix.
1136 * Defaults to '...'.
1137 * @return string
1138 */
1139 function wc_trim_string( $string, $chars = 200, $suffix = '...' ) {
1140 $string = $string ?? '';
1141
1142 if ( strlen( $string ) > $chars ) {
1143 if ( function_exists( 'mb_substr' ) ) {
1144 $string = mb_substr( $string, 0, ( $chars - mb_strlen( $suffix ) ) ) . $suffix;
1145 } else {
1146 $string = substr( $string, 0, ( $chars - strlen( $suffix ) ) ) . $suffix;
1147 }
1148 }
1149 return $string;
1150 }
1151
1152 /**
1153 * Format content to display shortcodes.
1154 *
1155 * @since 2.3.0
1156 * @param string $raw_string Raw string.
1157 * @return string
1158 */
1159 function wc_format_content( $raw_string ) {
1160 $raw_string = $raw_string ?? '';
1161 return apply_filters( 'woocommerce_format_content', apply_filters( 'woocommerce_short_description', $raw_string ), $raw_string );
1162 }
1163
1164 /**
1165 * Format product short description.
1166 * Adds support for Jetpack Markdown.
1167 *
1168 * @codeCoverageIgnore
1169 * @since 2.4.0
1170 * @param string $content Product short description.
1171 * @return string
1172 */
1173 function wc_format_product_short_description( $content ) {
1174 // Add support for Jetpack Markdown.
1175 if ( class_exists( 'WPCom_Markdown' ) ) {
1176 $markdown = WPCom_Markdown::get_instance();
1177
1178 return wpautop(
1179 $markdown->transform(
1180 $content,
1181 array(
1182 'unslash' => false,
1183 )
1184 )
1185 );
1186 }
1187
1188 return $content;
1189 }
1190
1191 /**
1192 * Formats curency symbols when saved in settings.
1193 *
1194 * @codeCoverageIgnore
1195 * @param string $value Option value.
1196 * @param array $option Option name.
1197 * @param string $raw_value Raw value.
1198 * @return string
1199 */
1200 function wc_format_option_price_separators( $value, $option, $raw_value ) {
1201 return wp_kses_post( $raw_value ?? '' );
1202 }
1203 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_decimal_sep', 'wc_format_option_price_separators', 10, 3 );
1204 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_thousand_sep', 'wc_format_option_price_separators', 10, 3 );
1205
1206 /**
1207 * Formats decimals when saved in settings.
1208 *
1209 * @codeCoverageIgnore
1210 * @param string $value Option value.
1211 * @param array $option Option name.
1212 * @param string $raw_value Raw value.
1213 * @return string
1214 */
1215 function wc_format_option_price_num_decimals( $value, $option, $raw_value ) {
1216 return is_null( $raw_value ) ? 2 : absint( $raw_value );
1217 }
1218 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_num_decimals', 'wc_format_option_price_num_decimals', 10, 3 );
1219
1220 /**
1221 * Formats hold stock option and sets cron event up.
1222 *
1223 * @codeCoverageIgnore
1224 * @param string $value Option value.
1225 * @param array $option Option name.
1226 * @param string $raw_value Raw value.
1227 * @return string
1228 */
1229 function wc_format_option_hold_stock_minutes( $value, $option, $raw_value ) {
1230 $value = ! empty( $raw_value ) ? absint( $raw_value ) : ''; // Allow > 0 or set to ''.
1231
1232 // Clear existing scheduled events.
1233 if ( function_exists( 'as_unschedule_all_actions' ) ) {
1234 as_unschedule_all_actions( 'woocommerce_cancel_unpaid_orders' );
1235 } else {
1236 wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
1237 }
1238
1239 if ( '' !== $value ) {
1240 /**
1241 * Filters the interval at which to cancel unpaid orders in minutes.
1242 *
1243 * @since 5.1.0
1244 *
1245 * @param int $cancel_unpaid_interval The interval at which to cancel unpaid orders in minutes.
1246 */
1247 $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $value ) );
1248
1249 if ( function_exists( 'as_schedule_single_action' ) ) {
1250 as_schedule_single_action( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders', array(), 'woocommerce', true );
1251 } else {
1252 wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' );
1253 }
1254 }
1255
1256 return $value;
1257 }
1258 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_hold_stock_minutes', 'wc_format_option_hold_stock_minutes', 10, 3 );
1259
1260 /**
1261 * Sanitize terms from an attribute text based.
1262 *
1263 * @since 2.4.5
1264 * @param string $term Term value.
1265 * @return string
1266 */
1267 function wc_sanitize_term_text_based( $term ) {
1268 return trim( wp_strip_all_tags( wp_unslash( $term ?? '' ) ) );
1269 }
1270
1271 if ( ! function_exists( 'wc_make_numeric_postcode' ) ) {
1272 /**
1273 * Make numeric postcode.
1274 *
1275 * Converts letters to numbers so we can do a simple range check on postcodes.
1276 * E.g. PE30 becomes 16050300 (P = 16, E = 05, 3 = 03, 0 = 00)
1277 *
1278 * @since 2.6.0
1279 * @param string $postcode Regular postcode.
1280 * @return string
1281 */
1282 function wc_make_numeric_postcode( $postcode ) {
1283 $postcode = str_replace( array( ' ', '-' ), '', $postcode ?? '' );
1284 $postcode_length = strlen( $postcode );
1285 $letters_to_numbers = array_merge( array( 0 ), range( 'A', 'Z' ) );
1286 $letters_to_numbers = array_flip( $letters_to_numbers );
1287 $numeric_postcode = '';
1288
1289 for ( $i = 0; $i < $postcode_length; $i++ ) {
1290 if ( is_numeric( $postcode[ $i ] ) ) {
1291 $numeric_postcode .= str_pad( $postcode[ $i ], 2, '0', STR_PAD_LEFT );
1292 } elseif ( isset( $letters_to_numbers[ $postcode[ $i ] ] ) ) {
1293 $numeric_postcode .= str_pad( $letters_to_numbers[ $postcode[ $i ] ], 2, '0', STR_PAD_LEFT );
1294 } else {
1295 $numeric_postcode .= '00';
1296 }
1297 }
1298
1299 return $numeric_postcode;
1300 }
1301 }
1302
1303 /**
1304 * Format the stock amount ready for display based on settings.
1305 *
1306 * @since 3.0.0
1307 * @param WC_Product $product Product object for which the stock you need to format.
1308 * @return string
1309 */
1310 function wc_format_stock_for_display( $product ) {
1311 $display = __( 'In stock', 'woocommerce' );
1312 $stock_amount = $product->get_stock_quantity();
1313
1314 switch ( get_option( 'woocommerce_stock_format' ) ) {
1315 case 'low_amount':
1316 if ( $stock_amount <= wc_get_low_stock_amount( $product ) ) {
1317 /* translators: %s: stock amount */
1318 $display = sprintf( __( 'Only %s left in stock', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product ) );
1319 }
1320 break;
1321 case '':
1322 /* translators: %s: stock amount */
1323 $display = sprintf( __( '%s in stock', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product ) );
1324 break;
1325 }
1326
1327 if ( $product->backorders_allowed() && $product->backorders_require_notification() ) {
1328 $display .= ' ' . __( '(can be backordered)', 'woocommerce' );
1329 }
1330
1331 return $display;
1332 }
1333
1334 /**
1335 * Format the stock quantity ready for display.
1336 *
1337 * @since 3.0.0
1338 * @param int $stock_quantity Stock quantity.
1339 * @param WC_Product $product Product instance so that we can pass through the filters.
1340 * @return string
1341 */
1342 function wc_format_stock_quantity_for_display( $stock_quantity, $product ) {
1343 return apply_filters( 'woocommerce_format_stock_quantity', $stock_quantity, $product );
1344 }
1345
1346 /**
1347 * Format a sale price for display.
1348 *
1349 * @since 3.0.0
1350 * @param string $regular_price Regular price.
1351 * @param string $sale_price Sale price.
1352 * @return string
1353 */
1354 function wc_format_sale_price( $regular_price, $sale_price ) {
1355 // Format the prices.
1356 $formatted_regular_price = is_numeric( $regular_price ) ? wc_price( $regular_price ) : $regular_price;
1357 $formatted_sale_price = is_numeric( $sale_price ) ? wc_price( $sale_price ) : $sale_price;
1358
1359 // Strikethrough pricing.
1360 $price = '<del aria-hidden="true">' . $formatted_regular_price . '</del> ';
1361
1362 // For accessibility (a11y) we'll also display that information to screen readers.
1363 $price .= '<span class="screen-reader-text">';
1364 // translators: %s is a product's regular price.
1365 $price .= esc_html( sprintf( __( 'Original price was: %s.', 'woocommerce' ), wp_strip_all_tags( $formatted_regular_price ) ) );
1366 $price .= '</span>';
1367
1368 // Add the sale price.
1369 $price .= '<ins aria-hidden="true">' . $formatted_sale_price . '</ins>';
1370
1371 // For accessibility (a11y) we'll also display that information to screen readers.
1372 $price .= '<span class="screen-reader-text">';
1373 // translators: %s is a product's current (sale) price.
1374 $price .= esc_html( sprintf( __( 'Current price is: %s.', 'woocommerce' ), wp_strip_all_tags( $formatted_sale_price ) ) );
1375 $price .= '</span>';
1376
1377 return apply_filters( 'woocommerce_format_sale_price', $price, $regular_price, $sale_price );
1378 }
1379
1380 /**
1381 * Format a price range for display.
1382 *
1383 * @param string $from Price from.
1384 * @param string $to Price to.
1385 * @return string
1386 */
1387 function wc_format_price_range( $from, $to ) {
1388 /* translators: 1: price from 2: price to */
1389 $price = sprintf( _x( '%1$s <span aria-hidden="true">&ndash;</span> %2$s', 'Price range: from-to', 'woocommerce' ), is_numeric( $from ) ? wc_price( $from, array( 'aria-hidden' => true ) ) : $from, is_numeric( $to ) ? wc_price( $to, array( 'aria-hidden' => true ) ) : $to );
1390 $price .= '<span class="screen-reader-text">';
1391 $price .= sprintf(
1392 /* translators: 1: price from 2: price to */
1393 __( 'Price range: %1$s through %2$s', 'woocommerce' ),
1394 is_numeric( $from ) ? wp_strip_all_tags( wc_price( $from ) ) : wp_strip_all_tags( $from ),
1395 is_numeric( $to ) ? wp_strip_all_tags( wc_price( $to ) ) : wp_strip_all_tags( $to )
1396 );
1397 $price .= '</span>';
1398 return apply_filters( 'woocommerce_format_price_range', $price, $from, $to );
1399 }
1400
1401 /**
1402 * Format a weight for display.
1403 *
1404 * @since 3.0.0
1405 * @param float $weight Weight.
1406 * @return string
1407 */
1408 function wc_format_weight( $weight ) {
1409 $weight_string = wc_format_localized_decimal( $weight );
1410
1411 if ( ! empty( $weight_string ) ) {
1412 $weight_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit' ) );
1413
1414 $weight_string = sprintf(
1415 // translators: 1. A formatted number; 2. A label for a weight unit of measure. E.g. 2.72 kg.
1416 _x( '%1$s %2$s', 'formatted weight', 'woocommerce' ),
1417 $weight_string,
1418 $weight_label
1419 );
1420 } else {
1421 $weight_string = __( 'N/A', 'woocommerce' );
1422 }
1423
1424 return apply_filters( 'woocommerce_format_weight', $weight_string, $weight );
1425 }
1426
1427 /**
1428 * Format dimensions for display.
1429 *
1430 * @since 3.0.0
1431 * @param array $dimensions Array of dimensions.
1432 * @return string
1433 */
1434 function wc_format_dimensions( $dimensions ) {
1435 $dimension_string = implode( ' &times; ', array_filter( array_map( 'wc_format_localized_decimal', $dimensions ) ) );
1436
1437 if ( ! empty( $dimension_string ) ) {
1438 $dimension_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit' ) );
1439
1440 $dimension_string = sprintf(
1441 // translators: 1. A formatted number; 2. A label for a dimensions unit of measure. E.g. 3.14 cm.
1442 _x( '%1$s %2$s', 'formatted dimensions', 'woocommerce' ),
1443 $dimension_string,
1444 $dimension_label
1445 );
1446 } else {
1447 $dimension_string = __( 'N/A', 'woocommerce' );
1448 }
1449
1450 return apply_filters( 'woocommerce_format_dimensions', $dimension_string, $dimensions );
1451 }
1452
1453 /**
1454 * Format a date for output.
1455 *
1456 * @since 3.0.0
1457 * @param WC_DateTime $date Instance of WC_DateTime.
1458 * @param string $format Data format.
1459 * Defaults to the wc_date_format function if not set.
1460 * @return string
1461 */
1462 function wc_format_datetime( $date, $format = '' ) {
1463 if ( ! $format ) {
1464 $format = wc_date_format();
1465 }
1466 if ( ! is_a( $date, 'WC_DateTime' ) ) {
1467 return '';
1468 }
1469 return $date->date_i18n( $format );
1470 }
1471
1472 /**
1473 * Process oEmbeds.
1474 *
1475 * @since 3.1.0
1476 * @param string $content Content.
1477 * @return string
1478 */
1479 function wc_do_oembeds( $content ) {
1480 global $wp_embed;
1481
1482 $content = $wp_embed->autoembed( $content ?? '' );
1483
1484 return $content;
1485 }
1486
1487 /**
1488 * Get part of a string before :.
1489 *
1490 * Used for example in shipping methods ids where they take the format
1491 * method_id:instance_id
1492 *
1493 * @since 3.2.0
1494 * @param string $string String to extract.
1495 * @return string
1496 */
1497 function wc_get_string_before_colon( $string ) {
1498 return trim( current( explode( ':', (string) $string ) ) );
1499 }
1500
1501 /**
1502 * Array merge and sum function.
1503 *
1504 * Source: https://gist.github.com/Nickology/f700e319cbafab5eaedc
1505 *
1506 * @since 3.2.0
1507 * @return array
1508 */
1509 function wc_array_merge_recursive_numeric() {
1510 $arrays = func_get_args();
1511
1512 // If there's only one array, it's already merged.
1513 if ( 1 === count( $arrays ) ) {
1514 return $arrays[0];
1515 }
1516
1517 // Remove any items in $arrays that are NOT arrays.
1518 foreach ( $arrays as $key => $array ) {
1519 if ( ! is_array( $array ) ) {
1520 unset( $arrays[ $key ] );
1521 }
1522 }
1523
1524 // We start by setting the first array as our final array.
1525 // We will merge all other arrays with this one.
1526 $final = array_shift( $arrays );
1527
1528 foreach ( $arrays as $b ) {
1529 foreach ( $final as $key => $value ) {
1530 // If $key does not exist in $b, then it is unique and can be safely merged.
1531 if ( ! isset( $b[ $key ] ) ) {
1532 $final[ $key ] = $value;
1533 } else {
1534 // If $key is present in $b, then we need to merge and sum numeric values in both.
1535 if ( is_numeric( $value ) && is_numeric( $b[ $key ] ) ) {
1536 // If both values for these keys are numeric, we sum them.
1537 $final[ $key ] = $value + $b[ $key ];
1538 } elseif ( is_array( $value ) && is_array( $b[ $key ] ) ) {
1539 // If both values are arrays, we recursively call ourself.
1540 $final[ $key ] = wc_array_merge_recursive_numeric( $value, $b[ $key ] );
1541 } else {
1542 // If both keys exist but differ in type, then we cannot merge them.
1543 // In this scenario, we will $b's value for $key is used.
1544 $final[ $key ] = $b[ $key ];
1545 }
1546 }
1547 }
1548
1549 // Finally, we need to merge any keys that exist only in $b.
1550 foreach ( $b as $key => $value ) {
1551 if ( ! isset( $final[ $key ] ) ) {
1552 $final[ $key ] = $value;
1553 }
1554 }
1555 }
1556
1557 return $final;
1558 }
1559
1560 /**
1561 * Implode and escape HTML attributes for output.
1562 *
1563 * @since 3.3.0
1564 * @param array $raw_attributes Attribute name value pairs.
1565 * @return string
1566 */
1567 function wc_implode_html_attributes( $raw_attributes ) {
1568 $attributes = array();
1569 foreach ( $raw_attributes as $name => $value ) {
1570 $attributes[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"';
1571 }
1572 return implode( ' ', $attributes );
1573 }
1574
1575 /**
1576 * Escape JSON for use on HTML or attribute text nodes.
1577 *
1578 * @since 3.5.5
1579 * @param string $json JSON to escape.
1580 * @param bool $html True if escaping for HTML text node, false for attributes. Determines how quotes are handled.
1581 * @return string Escaped JSON.
1582 */
1583 function wc_esc_json( $json, $html = false ) {
1584 return _wp_specialchars(
1585 $json,
1586 $html ? ENT_NOQUOTES : ENT_QUOTES, // Escape quotes in attribute nodes only.
1587 'UTF-8', // json_encode() outputs UTF-8 (really just ASCII), not the blog's charset.
1588 true // Double escape entities: `&amp;` -> `&amp;amp;`.
1589 );
1590 }
1591
1592 /**
1593 * Parse a relative date option from the settings API into a standard format.
1594 *
1595 * @since 3.4.0
1596 * @param mixed $raw_value Value stored in DB.
1597 * @return array Nicely formatted array with number and unit values.
1598 */
1599 function wc_parse_relative_date_option( $raw_value ) {
1600 $periods = array(
1601 'days' => __( 'Day(s)', 'woocommerce' ),
1602 'weeks' => __( 'Week(s)', 'woocommerce' ),
1603 'months' => __( 'Month(s)', 'woocommerce' ),
1604 'years' => __( 'Year(s)', 'woocommerce' ),
1605 );
1606
1607 $value = wp_parse_args(
1608 (array) $raw_value,
1609 array(
1610 'number' => '',
1611 'unit' => 'days',
1612 )
1613 );
1614
1615 $value['number'] = ! empty( $value['number'] ) ? absint( $value['number'] ) : '';
1616
1617 if ( ! in_array( $value['unit'], array_keys( $periods ), true ) ) {
1618 $value['unit'] = 'days';
1619 }
1620
1621 return $value;
1622 }
1623
1624 /**
1625 * Format the endpoint slug, strip out anything not allowed in a url.
1626 *
1627 * @since 3.5.0
1628 * @param string $raw_value The raw value.
1629 * @return string
1630 */
1631 function wc_sanitize_endpoint_slug( $raw_value ) {
1632 return sanitize_title( $raw_value ?? '' );
1633 }
1634
1635 /**
1636 * Removes useless non-displayable and problematic Unicode characters from a string.
1637 *
1638 * This function eliminates characters that can cause formatting issues, invisible text,
1639 * or unexpected behavior in copy-pasted text. Specifically, it removes:
1640 *
1641 * - **Soft hyphen (`U+00AD`)** – Invisible unless text is broken across lines.
1642 * - **Zero-width spaces & joiners (`U+200B–U+200D`)** – Invisible and can cause copy/paste issues.
1643 * - **Directional markers (`U+200E–U+200F`, `U+202A–U+202E`)** – Can affect text rendering.
1644 * - **Byte Order Mark (BOM) (`U+FEFF`)** – Can interfere with encoding.
1645 * - **Interlinear annotation characters (`U+FFF9–U+FFFB`)** – Rarely used and unnecessary in checkout fields.
1646 *
1647 * It does **not** remove:
1648 *
1649 * - **Non-breaking space (`U+00A0`)** – Useful for preventing line breaks in addresses.
1650 * - **Word joiner (`U+2060`)** – Sometimes needed for proper text rendering in certain scripts.
1651 *
1652 * @param string $raw_value The input string to sanitize.
1653 *
1654 * @return string The sanitized string without problematic characters.
1655 * @since 9.9.0
1656 */
1657 function wc_remove_non_displayable_chars( string $raw_value ): string {
1658 $remove_chars = array(
1659 "\u{00AD}", // Soft Hyphen.
1660 "\u{200B}", // Zero Width Space.
1661 "\u{200C}", // Zero Width Non-Joiner.
1662 "\u{200D}", // Zero Width Joiner.
1663 "\u{200E}", // Left-to-Right Mark.
1664 "\u{200F}", // Right-to-Left Mark.
1665 "\u{202A}", // Left-to-Right Embedding.
1666 "\u{202B}", // Right-to-Left Embedding.
1667 "\u{202C}", // Pop Directional Formatting.
1668 "\u{202D}", // Left-to-Right Override.
1669 "\u{202E}", // Right-to-Left Override.
1670 "\u{FEFF}", // Byte Order Mark (BOM).
1671 "\u{FFF9}", // Interlinear Annotation Anchor.
1672 "\u{FFFA}", // Interlinear Annotation Separator.
1673 "\u{FFFB}", // Interlinear Annotation Terminator.
1674 );
1675
1676 return str_replace( $remove_chars, '', $raw_value );
1677 }
1678
1679 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_checkout_pay_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
1680 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_checkout_order_received_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
1681 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_add_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
1682 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_delete_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
1683 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_set_default_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
1684 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_orders_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
1685 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_view_order_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
1686 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_downloads_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
1687 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_account_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
1688 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_address_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
1689 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_payment_methods_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
1690 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_lost_password_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
1691 add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_logout_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
1692