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