SVG for the filter definition.
*/
public static function get_filter_svg_from_preset( $preset ) {
_deprecated_function( __FUNCTION__, '6.3.0' );
$filter_id = self::get_filter_id_from_preset( $preset );
return self::get_filter_svg( $filter_id, $preset['colors'] );
}
/**
* Get the SVGs for the duotone filters.
*
* Example output:
*
*
* @internal
*
* @since 6.3.0
*
* @param array $sources The duotone presets.
* @return string The SVGs for the duotone filters.
*/
private static function get_svg_definitions( $sources ) {
$svgs = '';
foreach ( $sources as $filter_id => $filter_data ) {
$colors = $filter_data['colors'];
$svgs .= self::get_filter_svg( $filter_id, $colors );
}
return $svgs;
}
/**
* Get the CSS for global styles.
*
* Example output:
* body{--wp--preset--duotone--blue-orange:url('#wp-duotone-blue-orange');}
*
* @internal
*
* @since 6.3.0
*
* @param array $sources The duotone presets.
* @return string The CSS for global styles.
*/
private static function get_global_styles_presets( $sources ) {
$css = 'body{';
foreach ( $sources as $filter_id => $filter_data ) {
$slug = $filter_data['slug'];
$colors = $filter_data['colors'];
$css_property_name = self::get_css_custom_property_name( $slug );
$declaration_value = is_string( $colors ) ? $colors : self::get_filter_url( $filter_id );
$css .= "$css_property_name:$declaration_value;";
}
$css .= '}';
return $css;
}
/**
* Enqueue a block CSS declaration for the page.
*
* This does not include any SVGs.
*
* @internal
*
* @since 6.3.0
*
* @param string $filter_id The filter ID. e.g. 'wp-duotone-000000-ffffff-2'.
* @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'.
* @param string $filter_value The filter CSS value. e.g. 'url(#wp-duotone-000000-ffffff-2)' or 'unset'.
*/
private static function enqueue_block_css( $filter_id, $duotone_selector, $filter_value ) {
// Build the CSS selectors to which the filter will be applied.
$selectors = explode( ',', $duotone_selector );
$selectors_scoped = array();
foreach ( $selectors as $selector_part ) {
/*
* Assuming the selector part is a subclass selector (not a tag name)
* so we can prepend the filter id class. If we want to support elements
* such as `img` or namespaces, we'll need to add a case for that here.
*/
$selectors_scoped[] = '.' . $filter_id . trim( $selector_part );
}
$selector = implode( ', ', $selectors_scoped );
self::$block_css_declarations[] = array(
'selector' => $selector,
'declarations' => array(
'filter' => $filter_value,
),
);
}
/**
* Enqueue custom filter assets for the page.
*
* Includes an SVG filter and block CSS declaration.
*
* @internal
*
* @since 6.3.0
*
* @param string $filter_id The filter ID. e.g. 'wp-duotone-000000-ffffff-2'.
* @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'.
* @param string $filter_value The filter CSS value. e.g. 'url(#wp-duotone-000000-ffffff-2)' or 'unset'.
* @param array $filter_data Duotone filter data with 'slug' and 'colors' keys.
*/
private static function enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $filter_data ) {
self::$used_svg_filter_data[ $filter_id ] = $filter_data;
self::enqueue_block_css( $filter_id, $duotone_selector, $filter_value );
}
/**
* Enqueue preset assets for the page.
*
* Includes a CSS custom property, SVG filter, and block CSS declaration.
*
* @internal
*
* @since 6.3.0
*
* @param string $filter_id The filter ID. e.g. 'wp-duotone-blue-orange'.
* @param string $duotone_selector The block's duotone selector. e.g. '.wp-block-image img'.
* @param string $filter_value The filter CSS value. e.g. 'url(#wp-duotone-blue-orange)' or 'unset'.
*/
private static function enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value ) {
$global_styles_presets = self::get_all_global_styles_presets();
if ( ! array_key_exists( $filter_id, $global_styles_presets ) ) {
$error_message = sprintf(
/* translators: %s: duotone filter ID */
__( 'The duotone id "%s" is not registered in theme.json settings' ),
$filter_id
);
_doing_it_wrong( __METHOD__, $error_message, '6.3.0' );
return;
}
self::$used_global_styles_presets[ $filter_id ] = $global_styles_presets[ $filter_id ];
self::enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $global_styles_presets[ $filter_id ] );
}
/**
* Registers the style and colors block attributes for block types that support it.
*
* Block support is added with `supports.filter.duotone` in block.json.
*
* @since 6.3.0
*
* @param WP_Block_Type $block_type Block Type.
*/
public static function register_duotone_support( $block_type ) {
/*
* Previous `color.__experimentalDuotone` support flag is migrated
* to `filter.duotone` via `block_type_metadata_settings` filter.
*/
if ( block_has_support( $block_type, array( 'filter', 'duotone' ), null ) ) {
if ( ! $block_type->attributes ) {
$block_type->attributes = array();
}
if ( ! array_key_exists( 'style', $block_type->attributes ) ) {
$block_type->attributes['style'] = array(
'type' => 'object',
);
}
}
}
/**
* Get the CSS selector for a block type.
*
* This handles selectors defined in `color.__experimentalDuotone` support
* if `filter.duotone` support is not defined.
*
* @internal
* @since 6.3.0
*
* @param WP_Block_Type $block_type Block type to check for support.
* @return string|null The CSS selector or null if there is no support.
*/
private static function get_selector( $block_type ) {
if ( ! ( $block_type instanceof WP_Block_Type ) ) {
return null;
}
/*
* Backward compatibility with `supports.color.__experimentalDuotone`
* is provided via the `block_type_metadata_settings` filter. If
* `supports.filter.duotone` has not been set and the experimental
* property has been, the experimental property value is copied into
* `supports.filter.duotone`.
*/
$duotone_support = block_has_support( $block_type, array( 'filter', 'duotone' ) );
if ( ! $duotone_support ) {
return null;
}
/*
* If the experimental duotone support was set, that value is to be
* treated as a selector and requires scoping.
*/
$experimental_duotone = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false );
if ( $experimental_duotone ) {
$root_selector = wp_get_block_css_selector( $block_type );
return is_string( $experimental_duotone )
? WP_Theme_JSON::scope_selector( $root_selector, $experimental_duotone )
: $root_selector;
}
// Regular filter.duotone support uses filter.duotone selectors with fallbacks.
return wp_get_block_css_selector( $block_type, array( 'filter', 'duotone' ), true );
}
/**
* Scrape all possible duotone presets from global and theme styles and
* store them in self::$global_styles_presets.
*
* Used in conjunction with self::render_duotone_support for blocks that
* use duotone preset filters.
*
* @since 6.3.0
*
* @return array An array of global styles presets, keyed on the filter ID.
*/
private static function get_all_global_styles_presets() {
if ( isset( self::$global_styles_presets ) ) {
return self::$global_styles_presets;
}
// Get the per block settings from the theme.json.
$tree = wp_get_global_settings();
$presets_by_origin = _wp_array_get( $tree, array( 'color', 'duotone' ), array() );
self::$global_styles_presets = array();
foreach ( $presets_by_origin as $presets ) {
foreach ( $presets as $preset ) {
$filter_id = self::get_filter_id( _wp_to_kebab_case( $preset['slug'] ) );
self::$global_styles_presets[ $filter_id ] = $preset;
}
}
return self::$global_styles_presets;
}
/**
* Scrape all block names from global styles and store in self::$global_styles_block_names.
*
* Used in conjunction with self::render_duotone_support to output the
* duotone filters defined in the theme.json global styles.
*
* @since 6.3.0
*
* @return string[] An array of global style block slugs, keyed on the block name.
*/
private static function get_all_global_style_block_names() {
if ( isset( self::$global_styles_block_names ) ) {
return self::$global_styles_block_names;
}
// Get the per block settings from the theme.json.
$tree = WP_Theme_JSON_Resolver::get_merged_data();
$block_nodes = $tree->get_styles_block_nodes();
$theme_json = $tree->get_raw_data();
self::$global_styles_block_names = array();
foreach ( $block_nodes as $block_node ) {
// This block definition doesn't include any duotone settings. Skip it.
if ( empty( $block_node['duotone'] ) ) {
continue;
}
// Value looks like this: 'var(--wp--preset--duotone--blue-orange)' or 'var:preset|duotone|blue-orange'.
$duotone_attr_path = array_merge( $block_node['path'], array( 'filter', 'duotone' ) );
$duotone_attr = _wp_array_get( $theme_json, $duotone_attr_path, array() );
if ( empty( $duotone_attr ) ) {
continue;
}
// If it has a duotone filter preset, save the block name and the preset slug.
$slug = self::get_slug_from_attribute( $duotone_attr );
if ( $slug && $slug !== $duotone_attr ) {
self::$global_styles_block_names[ $block_node['name'] ] = $slug;
}
}
return self::$global_styles_block_names;
}
/**
* Render out the duotone CSS styles and SVG.
*
* The hooks self::set_global_style_block_names and self::set_global_styles_presets
* must be called before this function.
*
* @since 6.3.0
*
* @param string $block_content Rendered block content.
* @param array $block Block object.
* @param WP_Block $wp_block The block instance.
* @return string Filtered block content.
*/
public static function render_duotone_support( $block_content, $block, $wp_block ) {
if ( empty( $block_content ) || ! $block['blockName'] ) {
return $block_content;
}
$duotone_selector = self::get_selector( $wp_block->block_type );
if ( ! $duotone_selector ) {
return $block_content;
}
$global_styles_block_names = self::get_all_global_style_block_names();
// The block should have a duotone attribute or have duotone defined in its theme.json to be processed.
$has_duotone_attribute = isset( $block['attrs']['style']['color']['duotone'] );
$has_global_styles_duotone = array_key_exists( $block['blockName'], $global_styles_block_names );
if ( ! $has_duotone_attribute && ! $has_global_styles_duotone ) {
return $block_content;
}
// Generate the pieces needed for rendering a duotone to the page.
if ( $has_duotone_attribute ) {
/*
* Possible values for duotone attribute:
* 1. Array of colors - e.g. array('#000000', '#ffffff').
* 2. Variable for an existing Duotone preset - e.g. 'var:preset|duotone|blue-orange' or 'var(--wp--preset--duotone--blue-orange)''
* 3. A CSS string - e.g. 'unset' to remove globally applied duotone.
*/
$duotone_attr = $block['attrs']['style']['color']['duotone'];
$is_preset = is_string( $duotone_attr ) && self::is_preset( $duotone_attr );
$is_css = is_string( $duotone_attr ) && ! $is_preset;
$is_custom = is_array( $duotone_attr );
if ( $is_preset ) {
$slug = self::get_slug_from_attribute( $duotone_attr ); // e.g. 'blue-orange'.
$filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-blue-orange'.
$filter_value = self::get_css_var( $slug ); // e.g. 'var(--wp--preset--duotone--blue-orange)'.
// CSS custom property, SVG filter, and block CSS.
self::enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value );
} elseif ( $is_css ) {
$slug = wp_unique_id( sanitize_key( $duotone_attr . '-' ) ); // e.g. 'unset-1'.
$filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-unset-1'.
$filter_value = $duotone_attr; // e.g. 'unset'.
// Just block CSS.
self::enqueue_block_css( $filter_id, $duotone_selector, $filter_value );
} elseif ( $is_custom ) {
$slug = wp_unique_id( sanitize_key( implode( '-', $duotone_attr ) . '-' ) ); // e.g. '000000-ffffff-2'.
$filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-000000-ffffff-2'.
$filter_value = self::get_filter_url( $filter_id ); // e.g. 'url(#wp-duotone-filter-000000-ffffff-2)'.
$filter_data = array(
'slug' => $slug,
'colors' => $duotone_attr,
);
// SVG filter and block CSS.
self::enqueue_custom_filter( $filter_id, $duotone_selector, $filter_value, $filter_data );
}
} elseif ( $has_global_styles_duotone ) {
$slug = $global_styles_block_names[ $block['blockName'] ]; // e.g. 'blue-orange'.
$filter_id = self::get_filter_id( $slug ); // e.g. 'wp-duotone-filter-blue-orange'.
$filter_value = self::get_css_var( $slug ); // e.g. 'var(--wp--preset--duotone--blue-orange)'.
// CSS custom property, SVG filter, and block CSS.
self::enqueue_global_styles_preset( $filter_id, $duotone_selector, $filter_value );
}
// Like the layout hook, this assumes the hook only applies to blocks with a single wrapper.
$tags = new WP_HTML_Tag_Processor( $block_content );
if ( $tags->next_tag() ) {
$tags->add_class( $filter_id );
}
return $tags->get_updated_html();
}
/**
* Appends the used block duotone filter declarations to the inline block supports CSS.
*
* Uses the declarations saved in earlier calls to self::enqueue_block_css.
*
* @since 6.3.0
*/
public static function output_block_styles() {
if ( ! empty( self::$block_css_declarations ) ) {
wp_style_engine_get_stylesheet_from_css_rules(
self::$block_css_declarations,
array(
'context' => 'block-supports',
)
);
}
}
/**
* Appends the used global style duotone filter presets (CSS custom
* properties) to the inline global styles CSS.
*
* Uses the declarations saved in earlier calls to self::enqueue_global_styles_preset.
*
* @since 6.3.0
*/
public static function output_global_styles() {
if ( ! empty( self::$used_global_styles_presets ) ) {
wp_add_inline_style( 'global-styles', self::get_global_styles_presets( self::$used_global_styles_presets ) );
}
}
/**
* Outputs all necessary SVG for duotone filters, CSS for classic themes.
*
* Uses the declarations saved in earlier calls to self::enqueue_global_styles_preset
* and self::enqueue_custom_filter.
*
* @since 6.3.0
*/
public static function output_footer_assets() {
if ( ! empty( self::$used_svg_filter_data ) ) {
echo self::get_svg_definitions( self::$used_svg_filter_data );
}
// In block themes, the CSS is added in the head via wp_add_inline_style in the wp_enqueue_scripts action.
if ( ! wp_is_block_theme() ) {
$style_tag_id = 'core-block-supports-duotone';
wp_register_style( $style_tag_id, false );
if ( ! empty( self::$used_global_styles_presets ) ) {
wp_add_inline_style( $style_tag_id, self::get_global_styles_presets( self::$used_global_styles_presets ) );
}
if ( ! empty( self::$block_css_declarations ) ) {
wp_add_inline_style( $style_tag_id, wp_style_engine_get_stylesheet_from_css_rules( self::$block_css_declarations ) );
}
wp_enqueue_style( $style_tag_id );
}
}
/**
* Adds the duotone SVGs and CSS custom properties to the editor settings.
*
* This allows the properties to be pulled in by the EditorStyles component
* in JS and rendered in the post editor.
*
* @since 6.3.0
*
* @param array $settings The block editor settings from the `block_editor_settings_all` filter.
* @return array The editor settings with duotone SVGs and CSS custom properties.
*/
public static function add_editor_settings( $settings ) {
$global_styles_presets = self::get_all_global_styles_presets();
if ( ! empty( $global_styles_presets ) ) {
if ( ! isset( $settings['styles'] ) ) {
$settings['styles'] = array();
}
$settings['styles'][] = array(
// For the editor we can add all of the presets by default.
'assets' => self::get_svg_definitions( $global_styles_presets ),
// The 'svgs' type is new in 6.3 and requires the corresponding JS changes in the EditorStyles component to work.
'__unstableType' => 'svgs',
// These styles not generated by global styles, so this must be false or they will be stripped out in wp_get_block_editor_settings.
'isGlobalStyles' => false,
);
$settings['styles'][] = array(
// For the editor we can add all of the presets by default.
'css' => self::get_global_styles_presets( $global_styles_presets ),
// This must be set and must be something other than 'theme' or they will be stripped out in the post editor component.
'__unstableType' => 'presets',
// These styles are no longer generated by global styles, so this must be false or they will be stripped out in wp_get_block_editor_settings.
'isGlobalStyles' => false,
);
}
return $settings;
}
/**
* Migrates the experimental duotone support flag to the stabilized location.
*
* This moves `supports.color.__experimentalDuotone` to `supports.filter.duotone`.
*
* @since 6.3.0
*
* @param array $settings Current block type settings.
* @param array $metadata Block metadata as read in via block.json.
* @return array Filtered block type settings.
*/
public static function migrate_experimental_duotone_support_flag( $settings, $metadata ) {
$duotone_support = _wp_array_get( $metadata, array( 'supports', 'color', '__experimentalDuotone' ), null );
if ( ! isset( $settings['supports']['filter']['duotone'] ) && null !== $duotone_support ) {
_wp_array_set( $settings, array( 'supports', 'filter', 'duotone' ), (bool) $duotone_support );
}
return $settings;
}
/**
* Gets the CSS filter property value from a preset.
*
* Exported for the deprecated function wp_get_duotone_filter_id().
*
* @internal
*
* @since 6.3.0
* @deprecated 6.3.0
*
* @param array $preset The duotone preset.
* @return string The CSS filter property value.
*/
public static function get_filter_css_property_value_from_preset( $preset ) {
_deprecated_function( __FUNCTION__, '6.3.0' );
if ( isset( $preset['colors'] ) && is_string( $preset['colors'] ) ) {
return $preset['colors'];
}
$filter_id = self::get_filter_id_from_preset( $preset );
return 'url(#' . $filter_id . ')';
}
}
ty( $date_bits[7] ) ? iso8601_timezone_to_offset( $date_bits[7] ) : wc_timezone_offset();
$timestamp = gmmktime( $date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1] ) - $offset;
} else {
$timestamp = wc_string_to_timestamp( get_gmt_from_date( gmdate( 'Y-m-d H:i:s', wc_string_to_timestamp( $time_string ) ) ) );
}
$datetime = new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) );
// Set local timezone or offset.
if ( get_option( 'timezone_string' ) ) {
$datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) );
} else {
$datetime->set_utc_offset( wc_timezone_offset() );
}
return $datetime;
}
/**
* WooCommerce Timezone - helper to retrieve the timezone string for a site until.
* a WP core method exists (see https://core.trac.wordpress.org/ticket/24730).
*
* Adapted from https://secure.php.net/manual/en/function.timezone-name-from-abbr.php#89155.
*
* @since 2.1
* @return string PHP timezone string for the site
*/
function wc_timezone_string() {
// Added in WordPress 5.3 Ref https://developer.wordpress.org/reference/functions/wp_timezone_string/.
if ( function_exists( 'wp_timezone_string' ) ) {
return wp_timezone_string();
}
// If site timezone string exists, return it.
$timezone = get_option( 'timezone_string' );
if ( $timezone ) {
return $timezone;
}
// Get UTC offset, if it isn't set then return UTC.
$utc_offset = floatval( get_option( 'gmt_offset', 0 ) );
if ( ! is_numeric( $utc_offset ) || 0.0 === $utc_offset ) {
return 'UTC';
}
// Adjust UTC offset from hours to seconds.
$utc_offset = (int) ( $utc_offset * 3600 );
// Attempt to guess the timezone string from the UTC offset.
$timezone = timezone_name_from_abbr( '', $utc_offset );
if ( $timezone ) {
return $timezone;
}
// Last try, guess timezone string manually.
foreach ( timezone_abbreviations_list() as $abbr ) {
foreach ( $abbr as $city ) {
// 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.
if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
return $city['timezone_id'];
}
}
}
// Fallback to UTC.
return 'UTC';
}
/**
* Get timezone offset in seconds.
*
* @since 3.0.0
* @return float
*/
function wc_timezone_offset() {
$timezone = get_option( 'timezone_string' );
if ( $timezone ) {
$timezone_object = new DateTimeZone( $timezone );
return $timezone_object->getOffset( new DateTime( 'now' ) );
} else {
return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS;
}
}
/**
* Callback which can flatten post meta (gets the first value if it's an array).
*
* @since 3.0.0
* @param array $value Value to flatten.
* @return mixed
*/
function wc_flatten_meta_callback( $value ) {
return is_array( $value ) ? current( $value ) : $value;
}
if ( ! function_exists( 'wc_rgb_from_hex' ) ) {
/**
* Convert RGB to HEX.
*
* @param mixed $color Color.
*
* @return array
*/
function wc_rgb_from_hex( $color ) {
$color = str_replace( '#', '', $color );
// Convert shorthand colors to full format, e.g. "FFF" -> "FFFFFF".
$color = preg_replace( '~^(.)(.)(.)$~', '$1$1$2$2$3$3', $color );
$rgb = array();
$rgb['R'] = hexdec( $color[0] . $color[1] );
$rgb['G'] = hexdec( $color[2] . $color[3] );
$rgb['B'] = hexdec( $color[4] . $color[5] );
return $rgb;
}
}
if ( ! function_exists( 'wc_hex_darker' ) ) {
/**
* Make HEX color darker.
*
* @param mixed $color Color.
* @param int $factor Darker factor.
* Defaults to 30.
* @return string
*/
function wc_hex_darker( $color, $factor = 30 ) {
$base = wc_rgb_from_hex( $color );
$color = '#';
foreach ( $base as $k => $v ) {
$amount = $v / 100;
$amount = NumberUtil::round( $amount * $factor );
$new_decimal = $v - $amount;
$new_hex_component = dechex( $new_decimal );
if ( strlen( $new_hex_component ) < 2 ) {
$new_hex_component = '0' . $new_hex_component;
}
$color .= $new_hex_component;
}
return $color;
}
}
if ( ! function_exists( 'wc_hex_lighter' ) ) {
/**
* Make HEX color lighter.
*
* @param mixed $color Color.
* @param int $factor Lighter factor.
* Defaults to 30.
* @return string
*/
function wc_hex_lighter( $color, $factor = 30 ) {
$base = wc_rgb_from_hex( $color );
$color = '#';
foreach ( $base as $k => $v ) {
$amount = 255 - $v;
$amount = $amount / 100;
$amount = NumberUtil::round( $amount * $factor );
$new_decimal = $v + $amount;
$new_hex_component = dechex( $new_decimal );
if ( strlen( $new_hex_component ) < 2 ) {
$new_hex_component = '0' . $new_hex_component;
}
$color .= $new_hex_component;
}
return $color;
}
}
if ( ! function_exists( 'wc_hex_is_light' ) ) {
/**
* Determine whether a hex color is light.
*
* @param mixed $color Color.
* @return bool True if a light color.
*/
function wc_hex_is_light( $color ) {
$hex = str_replace( '#', '', $color );
$c_r = hexdec( substr( $hex, 0, 2 ) );
$c_g = hexdec( substr( $hex, 2, 2 ) );
$c_b = hexdec( substr( $hex, 4, 2 ) );
$brightness = ( ( $c_r * 299 ) + ( $c_g * 587 ) + ( $c_b * 114 ) ) / 1000;
return $brightness > 155;
}
}
if ( ! function_exists( 'wc_light_or_dark' ) ) {
/**
* Detect if we should use a light or dark color on a background color.
*
* @param mixed $color Color.
* @param string $dark Darkest reference.
* Defaults to '#000000'.
* @param string $light Lightest reference.
* Defaults to '#FFFFFF'.
* @return string
*/
function wc_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) {
return wc_hex_is_light( $color ) ? $dark : $light;
}
}
if ( ! function_exists( 'wc_format_hex' ) ) {
/**
* Format string as hex.
*
* @param string $hex HEX color.
* @return string|null
*/
function wc_format_hex( $hex ) {
$hex = trim( str_replace( '#', '', $hex ?? '' ) );
if ( strlen( $hex ) === 3 ) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
}
return $hex ? '#' . $hex : null;
}
}
/**
* Format the postcode according to the country and length of the postcode.
*
* @param string $postcode Unformatted postcode.
* @param string $country Base country.
* @return string
*/
function wc_format_postcode( $postcode, $country ) {
$postcode = wc_normalize_postcode( $postcode );
switch ( $country ) {
case 'CA':
case 'GB':
$postcode = substr_replace( $postcode, ' ', -3, 0 );
break;
case 'IE':
$postcode = substr_replace( $postcode, ' ', 3, 0 );
break;
case 'BR':
case 'PL':
$postcode = substr_replace( $postcode, '-', -3, 0 );
break;
case 'JP':
$postcode = substr_replace( $postcode, '-', 3, 0 );
break;
case 'PT':
$postcode = substr_replace( $postcode, '-', 4, 0 );
break;
case 'PR':
case 'US':
$postcode = rtrim( substr_replace( $postcode, '-', 5, 0 ), '-' );
break;
case 'NL':
$postcode = substr_replace( $postcode, ' ', 4, 0 );
break;
case 'LV':
if ( preg_match( '/(?:LV)?-?(\d+)/i', $postcode, $matches ) ) {
$postcode = count( $matches ) >= 2 ? "LV-$matches[1]" : $postcode;
}
break;
case 'DK':
$postcode = preg_replace( '/^(DK)(.+)$/', '${1}-${2}', $postcode );
break;
}
return apply_filters( 'woocommerce_format_postcode', trim( $postcode ), $country );
}
/**
* Normalize postcodes.
*
* Remove spaces and convert characters to uppercase.
*
* @since 2.6.0
* @param string $postcode Postcode.
* @return string
*/
function wc_normalize_postcode( $postcode ) {
return preg_replace( '/[\s\-]/', '', trim( wc_strtoupper( $postcode ) ) );
}
/**
* Format phone numbers.
*
* @param string $phone Phone number.
* @return string
*/
function wc_format_phone_number( $phone ) {
if ( ! WC_Validation::is_phone( $phone ) ) {
return '';
}
return preg_replace( '/[^0-9\+\-\(\)\s]/', '-', preg_replace( '/[\x00-\x1F\x7F-\xFF]/', '', $phone ) );
}
/**
* Sanitize phone number.
* Allows only numbers and "+" (plus sign).
*
* @since 3.6.0
* @param string $phone Phone number.
* @return string
*/
function wc_sanitize_phone_number( $phone ) {
return preg_replace( '/[^\d+]/', '', $phone );
}
/**
* Wrapper for mb_strtoupper which see's if supported first.
*
* @since 3.1.0
* @param string $string String to format.
* @return string
*/
function wc_strtoupper( $string ) {
return function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $string ) : strtoupper( $string );
}
/**
* Make a string lowercase.
* Try to use mb_strtolower() when available.
*
* @since 2.3
* @param string $string String to format.
* @return string
*/
function wc_strtolower( $string ) {
return function_exists( 'mb_strtolower' ) ? mb_strtolower( $string ) : strtolower( $string );
}
/**
* Trim a string and append a suffix.
*
* @param string $string String to trim.
* @param integer $chars Amount of characters.
* Defaults to 200.
* @param string $suffix Suffix.
* Defaults to '...'.
* @return string
*/
function wc_trim_string( $string, $chars = 200, $suffix = '...' ) {
if ( strlen( $string ) > $chars ) {
if ( function_exists( 'mb_substr' ) ) {
$string = mb_substr( $string, 0, ( $chars - mb_strlen( $suffix ) ) ) . $suffix;
} else {
$string = substr( $string, 0, ( $chars - strlen( $suffix ) ) ) . $suffix;
}
}
return $string;
}
/**
* Format content to display shortcodes.
*
* @since 2.3.0
* @param string $raw_string Raw string.
* @return string
*/
function wc_format_content( $raw_string ) {
return apply_filters( 'woocommerce_format_content', apply_filters( 'woocommerce_short_description', $raw_string ), $raw_string );
}
/**
* Format product short description.
* Adds support for Jetpack Markdown.
*
* @codeCoverageIgnore
* @since 2.4.0
* @param string $content Product short description.
* @return string
*/
function wc_format_product_short_description( $content ) {
// Add support for Jetpack Markdown.
if ( class_exists( 'WPCom_Markdown' ) ) {
$markdown = WPCom_Markdown::get_instance();
return wpautop(
$markdown->transform(
$content,
array(
'unslash' => false,
)
)
);
}
return $content;
}
/**
* Formats curency symbols when saved in settings.
*
* @codeCoverageIgnore
* @param string $value Option value.
* @param array $option Option name.
* @param string $raw_value Raw value.
* @return string
*/
function wc_format_option_price_separators( $value, $option, $raw_value ) {
return wp_kses_post( $raw_value );
}
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_decimal_sep', 'wc_format_option_price_separators', 10, 3 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_thousand_sep', 'wc_format_option_price_separators', 10, 3 );
/**
* Formats decimals when saved in settings.
*
* @codeCoverageIgnore
* @param string $value Option value.
* @param array $option Option name.
* @param string $raw_value Raw value.
* @return string
*/
function wc_format_option_price_num_decimals( $value, $option, $raw_value ) {
return is_null( $raw_value ) ? 2 : absint( $raw_value );
}
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_num_decimals', 'wc_format_option_price_num_decimals', 10, 3 );
/**
* Formats hold stock option and sets cron event up.
*
* @codeCoverageIgnore
* @param string $value Option value.
* @param array $option Option name.
* @param string $raw_value Raw value.
* @return string
*/
function wc_format_option_hold_stock_minutes( $value, $option, $raw_value ) {
$value = ! empty( $raw_value ) ? absint( $raw_value ) : ''; // Allow > 0 or set to ''.
wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' );
if ( '' !== $value ) {
$cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $value ) );
wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' );
}
return $value;
}
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_hold_stock_minutes', 'wc_format_option_hold_stock_minutes', 10, 3 );
/**
* Sanitize terms from an attribute text based.
*
* @since 2.4.5
* @param string $term Term value.
* @return string
*/
function wc_sanitize_term_text_based( $term ) {
return trim( wp_strip_all_tags( wp_unslash( $term ) ) );
}
if ( ! function_exists( 'wc_make_numeric_postcode' ) ) {
/**
* Make numeric postcode.
*
* Converts letters to numbers so we can do a simple range check on postcodes.
* E.g. PE30 becomes 16050300 (P = 16, E = 05, 3 = 03, 0 = 00)
*
* @since 2.6.0
* @param string $postcode Regular postcode.
* @return string
*/
function wc_make_numeric_postcode( $postcode ) {
$postcode = str_replace( array( ' ', '-' ), '', $postcode );
$postcode_length = strlen( $postcode );
$letters_to_numbers = array_merge( array( 0 ), range( 'A', 'Z' ) );
$letters_to_numbers = array_flip( $letters_to_numbers );
$numeric_postcode = '';
for ( $i = 0; $i < $postcode_length; $i ++ ) {
if ( is_numeric( $postcode[ $i ] ) ) {
$numeric_postcode .= str_pad( $postcode[ $i ], 2, '0', STR_PAD_LEFT );
} elseif ( isset( $letters_to_numbers[ $postcode[ $i ] ] ) ) {
$numeric_postcode .= str_pad( $letters_to_numbers[ $postcode[ $i ] ], 2, '0', STR_PAD_LEFT );
} else {
$numeric_postcode .= '00';
}
}
return $numeric_postcode;
}
}
/**
* Format the stock amount ready for display based on settings.
*
* @since 3.0.0
* @param WC_Product $product Product object for which the stock you need to format.
* @return string
*/
function wc_format_stock_for_display( $product ) {
$display = __( 'In stock', 'woocommerce' );
$stock_amount = $product->get_stock_quantity();
switch ( get_option( 'woocommerce_stock_format' ) ) {
case 'low_amount':
if ( $stock_amount <= wc_get_low_stock_amount( $product ) ) {
/* translators: %s: stock amount */
$display = sprintf( __( 'Only %s left in stock', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product ) );
}
break;
case '':
/* translators: %s: stock amount */
$display = sprintf( __( '%s in stock', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product ) );
break;
}
if ( $product->backorders_allowed() && $product->backorders_require_notification() ) {
$display .= ' ' . __( '(can be backordered)', 'woocommerce' );
}
return $display;
}
/**
* Format the stock quantity ready for display.
*
* @since 3.0.0
* @param int $stock_quantity Stock quantity.
* @param WC_Product $product Product instance so that we can pass through the filters.
* @return string
*/
function wc_format_stock_quantity_for_display( $stock_quantity, $product ) {
return apply_filters( 'woocommerce_format_stock_quantity', $stock_quantity, $product );
}
/**
* Format a sale price for display.
*
* @since 3.0.0
* @param string $regular_price Regular price.
* @param string $sale_price Sale price.
* @return string
*/
function wc_format_sale_price( $regular_price, $sale_price ) {
$price = '' . ( is_numeric( $regular_price ) ? wc_price( $regular_price ) : $regular_price ) . ' ' . ( is_numeric( $sale_price ) ? wc_price( $sale_price ) : $sale_price ) . '';
return apply_filters( 'woocommerce_format_sale_price', $price, $regular_price, $sale_price );
}
/**
* Format a price range for display.
*
* @param string $from Price from.
* @param string $to Price to.
* @return string
*/
function wc_format_price_range( $from, $to ) {
/* translators: 1: price from 2: price to */
$price = sprintf( _x( '%1$s – %2$s', 'Price range: from-to', 'woocommerce' ), is_numeric( $from ) ? wc_price( $from ) : $from, is_numeric( $to ) ? wc_price( $to ) : $to );
return apply_filters( 'woocommerce_format_price_range', $price, $from, $to );
}
/**
* Format a weight for display.
*
* @since 3.0.0
* @param float $weight Weight.
* @return string
*/
function wc_format_weight( $weight ) {
$weight_string = wc_format_localized_decimal( $weight );
if ( ! empty( $weight_string ) ) {
$weight_string .= ' ' . get_option( 'woocommerce_weight_unit' );
} else {
$weight_string = __( 'N/A', 'woocommerce' );
}
return apply_filters( 'woocommerce_format_weight', $weight_string, $weight );
}
/**
* Format dimensions for display.
*
* @since 3.0.0
* @param array $dimensions Array of dimensions.
* @return string
*/
function wc_format_dimensions( $dimensions ) {
$dimension_string = implode( ' × ', array_filter( array_map( 'wc_format_localized_decimal', $dimensions ) ) );
if ( ! empty( $dimension_string ) ) {
$dimension_string .= ' ' . get_option( 'woocommerce_dimension_unit' );
} else {
$dimension_string = __( 'N/A', 'woocommerce' );
}
return apply_filters( 'woocommerce_format_dimensions', $dimension_string, $dimensions );
}
/**
* Format a date for output.
*
* @since 3.0.0
* @param WC_DateTime $date Instance of WC_DateTime.
* @param string $format Data format.
* Defaults to the wc_date_format function if not set.
* @return string
*/
function wc_format_datetime( $date, $format = '' ) {
if ( ! $format ) {
$format = wc_date_format();
}
if ( ! is_a( $date, 'WC_DateTime' ) ) {
return '';
}
return $date->date_i18n( $format );
}
/**
* Process oEmbeds.
*
* @since 3.1.0
* @param string $content Content.
* @return string
*/
function wc_do_oembeds( $content ) {
global $wp_embed;
$content = $wp_embed->autoembed( $content );
return $content;
}
/**
* Get part of a string before :.
*
* Used for example in shipping methods ids where they take the format
* method_id:instance_id
*
* @since 3.2.0
* @param string $string String to extract.
* @return string
*/
function wc_get_string_before_colon( $string ) {
return trim( current( explode( ':', (string) $string ) ) );
}
/**
* Array merge and sum function.
*
* Source: https://gist.github.com/Nickology/f700e319cbafab5eaedc
*
* @since 3.2.0
* @return array
*/
function wc_array_merge_recursive_numeric() {
$arrays = func_get_args();
// If there's only one array, it's already merged.
if ( 1 === count( $arrays ) ) {
return $arrays[0];
}
// Remove any items in $arrays that are NOT arrays.
foreach ( $arrays as $key => $array ) {
if ( ! is_array( $array ) ) {
unset( $arrays[ $key ] );
}
}
// We start by setting the first array as our final array.
// We will merge all other arrays with this one.
$final = array_shift( $arrays );
foreach ( $arrays as $b ) {
foreach ( $final as $key => $value ) {
// If $key does not exist in $b, then it is unique and can be safely merged.
if ( ! isset( $b[ $key ] ) ) {
$final[ $key ] = $value;
} else {
// If $key is present in $b, then we need to merge and sum numeric values in both.
if ( is_numeric( $value ) && is_numeric( $b[ $key ] ) ) {
// If both values for these keys are numeric, we sum them.
$final[ $key ] = $value + $b[ $key ];
} elseif ( is_array( $value ) && is_array( $b[ $key ] ) ) {
// If both values are arrays, we recursively call ourself.
$final[ $key ] = wc_array_merge_recursive_numeric( $value, $b[ $key ] );
} else {
// If both keys exist but differ in type, then we cannot merge them.
// In this scenario, we will $b's value for $key is used.
$final[ $key ] = $b[ $key ];
}
}
}
// Finally, we need to merge any keys that exist only in $b.
foreach ( $b as $key => $value ) {
if ( ! isset( $final[ $key ] ) ) {
$final[ $key ] = $value;
}
}
}
return $final;
}
/**
* Implode and escape HTML attributes for output.
*
* @since 3.3.0
* @param array $raw_attributes Attribute name value pairs.
* @return string
*/
function wc_implode_html_attributes( $raw_attributes ) {
$attributes = array();
foreach ( $raw_attributes as $name => $value ) {
$attributes[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"';
}
return implode( ' ', $attributes );
}
/**
* Escape JSON for use on HTML or attribute text nodes.
*
* @since 3.5.5
* @param string $json JSON to escape.
* @param bool $html True if escaping for HTML text node, false for attributes. Determines how quotes are handled.
* @return string Escaped JSON.
*/
function wc_esc_json( $json, $html = false ) {
return _wp_specialchars(
$json,
$html ? ENT_NOQUOTES : ENT_QUOTES, // Escape quotes in attribute nodes only.
'UTF-8', // json_encode() outputs UTF-8 (really just ASCII), not the blog's charset.
true // Double escape entities: `&` -> `&`.
);
}
/**
* Parse a relative date option from the settings API into a standard format.
*
* @since 3.4.0
* @param mixed $raw_value Value stored in DB.
* @return array Nicely formatted array with number and unit values.
*/
function wc_parse_relative_date_option( $raw_value ) {
$periods = array(
'days' => __( 'Day(s)', 'woocommerce' ),
'weeks' => __( 'Week(s)', 'woocommerce' ),
'months' => __( 'Month(s)', 'woocommerce' ),
'years' => __( 'Year(s)', 'woocommerce' ),
);
$value = wp_parse_args(
(array) $raw_value,
array(
'number' => '',
'unit' => 'days',
)
);
$value['number'] = ! empty( $value['number'] ) ? absint( $value['number'] ) : '';
if ( ! in_array( $value['unit'], array_keys( $periods ), true ) ) {
$value['unit'] = 'days';
}
return $value;
}
/**
* Format the endpoint slug, strip out anything not allowed in a url.
*
* @since 3.5.0
* @param string $raw_value The raw value.
* @return string
*/
function wc_sanitize_endpoint_slug( $raw_value ) {
return sanitize_title( $raw_value );
}
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_checkout_pay_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_checkout_order_received_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_add_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_delete_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_set_default_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_orders_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_view_order_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_downloads_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_account_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_address_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_payment_methods_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_lost_password_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_logout_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );