skyverge/wc-plugin-framework

View on GitHub
woocommerce/class-sv-wc-helper.php

Summary

Maintainability
F
4 days
Test Coverage
<?php
/**
 * WooCommerce Plugin Framework
 *
 * This source file is subject to the GNU General Public License v3.0
 * that is bundled with this package in the file license.txt.
 * It is also available through the world-wide-web at this URL:
 * http://www.gnu.org/licenses/gpl-3.0.html
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@skyverge.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade the plugin to newer
 * versions in the future. If you wish to customize the plugin for your
 * needs please refer to http://www.skyverge.com
 *
 * @package   SkyVerge/WooCommerce/Plugin/Classes
 * @author    SkyVerge
 * @copyright Copyright (c) 2013-2024, SkyVerge, Inc.
 * @license   http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0
 */

namespace SkyVerge\WooCommerce\PluginFramework\v5_12_3;

defined( 'ABSPATH' ) or exit;

if ( ! class_exists( '\\SkyVerge\\WooCommerce\\PluginFramework\\v5_12_3\\SV_WC_Helper' ) ) :


/**
 * SkyVerge Helper Class
 *
 * The purpose of this class is to centralize common utility functions that
 * are commonly used in SkyVerge plugins
 *
 * @since 2.2.0
 */
#[\AllowDynamicProperties]
class SV_WC_Helper {


    /** encoding used for mb_*() string functions */
    const MB_ENCODING = 'UTF-8';


    /** String manipulation functions (all multi-byte safe) ***************/

    /**
     * Returns true if the haystack string starts with needle
     *
     * Note: case-sensitive
     *
     * @since 2.2.0
     * @param string $haystack
     * @param string $needle
     * @return bool
     */
    public static function str_starts_with( $haystack, $needle ) {

        if ( self::multibyte_loaded() ) {

            if ( '' === $needle ) {
                return true;
            }

            return 0 === mb_strpos( $haystack, $needle, 0, self::MB_ENCODING );

        } else {

            $needle = self::str_to_ascii( $needle );

            if ( '' === $needle ) {
                return true;
            }

            return 0 === strpos( self::str_to_ascii( $haystack ), self::str_to_ascii( $needle ) );
        }
    }


    /**
     * Return true if the haystack string ends with needle
     *
     * Note: case-sensitive
     *
     * @since 2.2.0
     * @param string $haystack
     * @param string $needle
     * @return bool
     */
    public static function str_ends_with( $haystack, $needle ) {

        if ( '' === $needle ) {
            return true;
        }

        if ( self::multibyte_loaded() ) {

            return mb_substr( $haystack, -mb_strlen( $needle, self::MB_ENCODING ), null, self::MB_ENCODING ) === $needle;

        } else {

            $haystack = self::str_to_ascii( $haystack );
            $needle   = self::str_to_ascii( $needle );

            return substr( $haystack, -strlen( $needle ) ) === $needle;
        }
    }


    /**
     * Returns true if the needle exists in haystack
     *
     * Note: case-sensitive
     *
     * @since 2.2.0
     * @param string $haystack
     * @param string $needle
     * @return bool
     */
    public static function str_exists( $haystack, $needle ) {

        if ( self::multibyte_loaded() ) {

            if ( '' === $needle ) {
                return false;
            }

            return false !== mb_strpos( $haystack, $needle, 0, self::MB_ENCODING );

        } else {

            $needle = self::str_to_ascii( $needle );

            if ( '' === $needle ) {
                return false;
            }

            return false !== strpos( self::str_to_ascii( $haystack ), self::str_to_ascii( $needle ) );
        }
    }


    /**
     * Truncates a given $string after a given $length if string is longer than
     * $length. The last characters will be replaced with the $omission string
     * for a total length not exceeding $length
     *
     * @since 2.2.0
     * @param string $string text to truncate
     * @param int $length total desired length of string, including omission
     * @param string $omission omission text, defaults to '...'
     * @return string
     */
    public static function str_truncate( $string, $length, $omission = '...' ) {

        if ( self::multibyte_loaded() ) {

            // bail if string doesn't need to be truncated
            if ( mb_strlen( $string, self::MB_ENCODING ) <= $length ) {
                return $string;
            }

            $length -= mb_strlen( $omission, self::MB_ENCODING );

            return mb_substr( $string, 0, $length, self::MB_ENCODING ) . $omission;

        } else {

            $string = self::str_to_ascii( $string );

            // bail if string doesn't need to be truncated
            if ( strlen( $string ) <= $length ) {
                return $string;
            }

            $length -= strlen( $omission );

            return substr( $string, 0, $length ) . $omission;
        }
    }


    /**
     * Returns a string with all non-ASCII characters removed.
     *
     * This is useful for any string functions that expect only ASCII chars and can't safely handle UTF-8.
     * Note this only allows ASCII chars in the range 33-126 (newlines/carriage returns are stripped).
     *
     * @since 2.2.0
     *
     * @param string|mixed $string string to make ASCII
     * @return string|false
     */
    public static function str_to_ascii( $string ) {

        // strip ASCII chars 32 and under
        $string = filter_var( $string, FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW );

        // strip ASCII chars 127 and higher
        return filter_var( $string, FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_FLAG_STRIP_HIGH );
    }


    /**
     * Return a string with insane UTF-8 characters removed, like invisible
     * characters, unused code points, and other weirdness. It should
     * accept the common types of characters defined in Unicode.
     *
     * The following are allowed characters:
     *
     * p{L} - any kind of letter from any language
     * p{Mn} - a character intended to be combined with another character without taking up extra space (e.g. accents, umlauts, etc.)
     * p{Mc} - a character intended to be combined with another character that takes up extra space (vowel signs in many Eastern languages)
     * p{Nd} - a digit zero through nine in any script except ideographic scripts
     * p{Zs} - a whitespace character that is invisible, but does take up space
     * p{P} - any kind of punctuation character
     * p{Sm} - any mathematical symbol
     * p{Sc} - any currency sign
     *
     * pattern definitions from http://www.regular-expressions.info/unicode.html
     *
     * @since 4.0.0
     *
     * @param string $string
     * @return string
     */
    public static function str_to_sane_utf8( $string ) {

        $sane_string = preg_replace( '/[^\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Zs}\p{P}\p{Sm}\p{Sc}]/u', '', $string );

        // preg_replace with the /u modifier can return null or false on failure
        return ( is_null( $sane_string ) || false === $sane_string ) ? $string : $sane_string;
    }


    /**
     * Formats a number as a percentage.
     *
     * @since 5.10.9
     *
     * @NOTE The second and third parameter below are directly passed to {@see wc_format_decimal()} in case the decimal output or rounding needs to be tweaked.
     *
     * @param float|int|string $fraction the fraction to format as percentage
     * @param int|string|false number of decimal points to use, empty string to use {@see woocommerce_price_num_decimals(), or false to avoid rounding (optional, default).
     * @param bool $trim_zeros from end of string (optional, default false)
     * @return string fraction formatted as percentage
     */
    public static function format_percentage( $fraction, $decimal_points = false, $trim_zeros = false ) {

        return sprintf( '%s%%', (string) wc_format_decimal( $fraction * 100, $decimal_points, $trim_zeros ) );
    }


    /**
     * Helper method to check if the multibyte extension is loaded, which
     * indicates it's safe to use the mb_*() string methods
     *
     * @since 2.2.0
     * @return bool
     */
    protected static function multibyte_loaded() {

        return extension_loaded( 'mbstring' );
    }


    /** Array functions ***************************************************/


    /**
     * Insert the given element after the given key in the array
     *
     * Sample usage:
     *
     * given
     *
     * array( 'item_1' => 'foo', 'item_2' => 'bar' )
     *
     * array_insert_after( $array, 'item_1', array( 'item_1.5' => 'w00t' ) )
     *
     * becomes
     *
     * array( 'item_1' => 'foo', 'item_1.5' => 'w00t', 'item_2' => 'bar' )
     *
     * @since 2.2.0
     * @param array $array array to insert the given element into
     * @param string $insert_key key to insert given element after
     * @param array $element element to insert into array
     * @return array
     */
    public static function array_insert_after( Array $array, $insert_key, Array $element ) {

        $new_array = array();

        foreach ( $array as $key => $value ) {

            $new_array[ $key ] = $value;

            if ( $insert_key == $key ) {

                foreach ( $element as $k => $v ) {
                    $new_array[ $k ] = $v;
                }
            }
        }

        return $new_array;
    }


    /**
     * Convert array into XML by recursively generating child elements
     *
     * First instantiate a new XML writer object:
     *
     * $xml = new XMLWriter();
     *
     * Open in memory (alternatively you can use a local URI for file output)
     *
     * $xml->openMemory();
     *
     * Then start the document
     *
     * $xml->startDocument( '1.0', 'UTF-8' );
     *
     * Don't forget to end the document and output the memory
     *
     * $xml->endDocument();
     *
     * $your_xml_string = $xml->outputMemory();
     *
     * @since 2.2.0
     *
     * @param \XMLWriter $xml_writer XML writer instance
     * @param string|array $element_key name for element, e.g. <per_page>
     * @param string|array $element_value value for element, e.g. 100
     */
    public static function array_to_xml( $xml_writer, $element_key, $element_value = array() ) {

        if ( is_array( $element_value ) ) {

            // handle attributes
            if ( '@attributes' === $element_key ) {

                foreach ( $element_value as $attribute_key => $attribute_value ) {

                    $xml_writer->startAttribute( $attribute_key );
                    $xml_writer->text( $attribute_value );
                    $xml_writer->endAttribute();
                }

                return;
            }

            // handle multi-elements (e.g. multiple <Order> elements)
            if ( is_numeric( key( $element_value ) ) ) {

                // recursively generate child elements
                foreach ( $element_value as $child_element_key => $child_element_value ) {

                    $xml_writer->startElement( $element_key );

                    foreach ( $child_element_value as $sibling_element_key => $sibling_element_value ) {
                        self::array_to_xml( $xml_writer, $sibling_element_key, $sibling_element_value );
                    }

                    $xml_writer->endElement();
                }

            } else {

                // start root element
                $xml_writer->startElement( $element_key );

                // recursively generate child elements
                foreach ( $element_value as $child_element_key => $child_element_value ) {
                    self::array_to_xml( $xml_writer, $child_element_key, $child_element_value );
                }

                // end root element
                $xml_writer->endElement();
            }

        } else {

            // handle single elements
            if ( '@value' === $element_key ) {

                $xml_writer->text( $element_value );

            } else {

                // wrap element in CDATA tags if it contains illegal characters
                if ( false !== strpos( $element_value, '<' ) || false !== strpos( $element_value, '>' ) ) {

                    $xml_writer->startElement( $element_key );
                    $xml_writer->writeCdata( $element_value );
                    $xml_writer->endElement();

                } else {

                    $xml_writer->writeElement( $element_key, $element_value );
                }
            }
        }
    }


    /**
     * Lists an array as text.
     *
     * Takes an array and returns a list like "one, two, three, and four"
     * with a (mandatory) oxford comma.
     *
     * @since 5.2.0
     *
     * @param array $items items to list
     * @param string|null $conjunction coordinating conjunction, like "or" or "and"
     * @param string $separator list separator, like a comma
     * @return string
     */
    public static function list_array_items( array $items, $conjunction = null, $separator = '' ) {

        if ( ! is_string( $conjunction ) ) {
            $conjunction = _x( 'and', 'coordinating conjunction for a list of items: a, b, and c', 'woocommerce-plugin-framework' );
        }

        // append the conjunction to the last item
        if ( count( $items ) > 1 ) {

            $last_item = array_pop( $items );

            array_push( $items, trim( "{$conjunction} {$last_item}" ) );

            // only use a comma if needed and no separator was passed
            if ( count( $items ) < 3 ) {
                $separator = ' ';
            } elseif ( ! is_string( $separator ) || '' === $separator ) {
                $separator = ', ';
            }
        }

        return implode( $separator, $items );
    }


    /**
     * Joins the array elements into a string using natural language.
     *
     * For example, the array `['US', 'Canada', 'Mexico']` would become `'US, Canada, and Mexico'`.
     *
     * When using this method to create user-facing text, it is recommended to supply a localized conjunction.
     *
     * @since 5.11.8
     *
     * @param array<scalar> $array
     * @param string|null $conjunction one of 'and' or 'or'
     * @param string|null $pattern a custom sprintf pattern, with placeholders %1$s and %2$s
     * @return string
     */
    public static function array_join_natural( array $array, ?string $conjunction = 'and', ?string $pattern = '' ) : string
    {
        $last = array_pop( $array );

        if ( $array ) {
            if ( ! $pattern ) {
                switch ( $conjunction ) {
                    case 'or':
                        /* translators: A list of items, for example: "US or Canada", or "US, Canada, or Mexico". English uses Oxford comma before the conjunction ("or") if there are at least 2 items preceding it - hence the use of plural forms. If your locale does not use Oxford comma, you can just provide the same translation to all plural forms. Placeholders: %1$s - a comma-separated list of item, %2$s - the final item in the list */
                        $pattern = _n( '%1$s or %2$s', '%1$s, or %2$s', count( $array ), 'woocommerce-plugin-framework' );
                        break;

                    case 'and':
                    default:
                        /* translators: A list of items, for example: "US and Canada", or "US, Canada, and Mexico". English uses Oxford comma before the conjunction ("and") if there are at least 2 items preceding it - hence the use of plural forms. If your locale does not use Oxford comma, you can just provide the same translation to all plural forms. Placeholders: %1$s - a comma-separated list of items, %2$s - the final item in the list */
                        $pattern = _n( '%1$s and %2$s', '%1$s, and %2$s', count( $array ), 'woocommerce-plugin-framework' );
                        break;
                }
            }

            return sprintf( $pattern, implode( ', ', $array ), $last );
        }

        return (string) $last;
    }


    /** Number helper functions *******************************************/


    /**
     * Format a number with 2 decimal points, using a period for the decimal
     * separator and no thousands separator.
     *
     * Commonly used for payment gateways which require amounts in this format.
     *
     * @since 3.0.0
     * @param float $number
     * @return string
     */
    public static function number_format( $number ) {

        return number_format( (float) $number, 2, '.', '' );
    }


    /** WooCommerce helper functions **************************************/


    /**
     * Gets order line items (products) as an array of objects.
     *
     * Object properties:
     *
     * + id          - item ID
     * + name        - item name, usually product title, processed through htmlentities()
     * + description - formatted item meta (e.g. Size: Medium, Color: blue), processed through htmlentities()
     * + quantity    - item quantity
     * + item_total  - item total (line total divided by quantity, excluding tax & rounded)
     * + line_total  - line item total (excluding tax & rounded)
     * + meta        - formatted item meta array
     * + product     - item product or null if getting product from item failed
     * + item        - raw item array
     *
     * @since 3.0.0
     *
     * @param \WC_Order $order
     * @return \stdClass[] array of line item objects
     */
    public static function get_order_line_items( $order ): array {

        $line_items = [];

        /** @var \WC_Order_Item_Product $item */
        foreach ( $order->get_items() as $id => $item ) {

            $line_item = new \stdClass();
            $product   = $item->get_product();
            $name      = $item->get_name();
            $quantity  = $item->get_quantity();
            $sku       = $product instanceof \WC_Product ? $product->get_sku() : '';
            $item_desc = [];

            // add SKU to description if available
            if ( ! empty( $sku ) ) {
                $item_desc[] = sprintf( 'SKU: %s', $sku );
            }

            $meta_data = $item->get_formatted_meta_data( '_', true );
            $item_meta = [];

            foreach ( $meta_data as $meta ) {
                $item_meta[] = array(
                    'label' => $meta->display_key,
                    'value' => $meta->value,
                );
            }

            if ( ! empty( $item_meta ) ) {
                foreach ( $item_meta as $meta ) {
                    $item_desc[] = sprintf( '%s: %s', $meta['label'], $meta['value'] );
                }
            }

            $item_desc = implode( ', ', $item_desc );

            $line_item->id          = $id;
            $line_item->name        = htmlentities( $name, ENT_QUOTES, 'UTF-8', false );
            $line_item->description = htmlentities( $item_desc, ENT_QUOTES, 'UTF-8', false );
            $line_item->quantity    = $quantity;
            $line_item->item_total  = $item['recurring_line_total'] ?? $order->get_item_total( $item );
            $line_item->line_total  = $order->get_line_total( $item );
            $line_item->meta        = $item_meta;
            $line_item->product     = is_object( $product ) ? $product : null;
            $line_item->item        = $item;

            $line_items[] = $line_item;
        }

        return $line_items;
    }


    /**
     * Determines if an order contains only virtual products.
     *
     * @since 4.5.0
     *
     * @param \WC_Order $order the order object
     * @return bool
     */
    public static function is_order_virtual( \WC_Order $order ) {

        $is_virtual = true;

        /** @var \WC_Order_Item_Product $item */
        foreach ( $order->get_items() as $item ) {

            $product = $item->get_product();

            // once we've found one non-virtual product we know we're done, break out of the loop
            if ( $product && ! $product->is_virtual() ) {

                $is_virtual = false;
                break;
            }
        }

        return $is_virtual;
    }


    /**
     * Determines if a shop has any published virtual products.
     *
     * @since 5.10.0
     *
     * @return bool
     */
    public static function shop_has_virtual_products() {

        $virtual_products = wc_get_products( [
            'virtual' => true,
            'status'  => 'publish',
            'limit'   => 1,
        ] );

        return sizeof( $virtual_products ) > 0;
    }


    /**
     * Safely gets a value from $_POST.
     *
     * If the expected data is a string also trims it.
     *
     * @since 5.5.0
     *
     * @param string $key posted data key
     * @param int|float|array|bool|null|string $default default data type to return (default empty string)
     * @return int|float|array|bool|null|string posted data value if key found, or default
     */
    public static function get_posted_value( $key, $default = '' ) {

        $value = $default;

        if ( isset( $_POST[ $key ] ) ) {
            $value = is_string( $_POST[ $key ] ) ? trim( $_POST[ $key ] ) : $_POST[ $key ];
        }

        return $value;
    }


    /**
     * Safely gets a value from $_REQUEST.
     *
     * If the expected data is a string also trims it.
     *
     * @since 5.5.0
     *
     * @param string $key posted data key
     * @param int|float|array|bool|null|string $default default data type to return (default empty string)
     * @return int|float|array|bool|null|string posted data value if key found, or default
     */
    public static function get_requested_value( $key, $default = '' ) {

        $value = $default;

        if ( isset( $_REQUEST[ $key ] ) ) {
            $value = is_string( $_REQUEST[ $key ] ) ? trim( $_REQUEST[ $key ] ) : $_REQUEST[ $key ];
        }

        return $value;
    }


    /**
     * Get the count of notices added, either for all notices (default) or for one
     * particular notice type specified by $notice_type.
     *
     * WC notice functions are not available in the admin
     *
     * @since 3.0.2
     * @param string $notice_type The name of the notice type - either error, success or notice. [optional]
     * @return int
     */
    public static function wc_notice_count( $notice_type = '' ) {

        if ( function_exists( 'wc_notice_count' ) ) {
            return wc_notice_count( $notice_type );
        }

        return 0;
    }


    /**
     * Add and store a notice.
     *
     * WC notice functions are not available in the admin
     *
     * @since 3.0.2
     * @param string $message The text to display in the notice.
     * @param string $notice_type The singular name of the notice type - either error, success or notice. [optional]
     */
    public static function wc_add_notice( $message, $notice_type = 'success' ) {

        // the session sanity check is necessary as WC doesn't provide one of its own
        if ( function_exists( 'wc_add_notice' ) && ! empty( WC()->session ) ) {
            wc_add_notice( $message, $notice_type );
        }
    }


    /**
     * Print a single notice immediately
     *
     * WC notice functions are not available in the admin
     *
     * @since 3.0.2
     * @param string $message The text to display in the notice.
     * @param string $notice_type The singular name of the notice type - either error, success or notice. [optional]
     */
    public static function wc_print_notice( $message, $notice_type = 'success' ) {

        if ( function_exists( 'wc_print_notice' ) ) {
            wc_print_notice( $message, $notice_type );
        }
    }


    /**
     * Gets the full URL to the log file for a given $handle
     *
     * @since 4.0.0
     * @param string $handle log handle
     * @return string URL to the WC log file identified by $handle
     */
    public static function get_wc_log_file_url( $handle ) {
        return admin_url( sprintf( 'admin.php?page=wc-status&tab=logs&log_file=%s-%s-log', $handle, sanitize_file_name( wp_hash( $handle ) ) ) );
    }


    /**
     * Gets the current WordPress site name.
     *
     * This is helpful for retrieving the actual site name instead of the
     * network name on multisite installations.
     *
     * @since 4.6.0
     * @return string
     */
    public static function get_site_name() {

        return ( is_multisite() ) ? get_blog_details()->blogname : get_bloginfo( 'name' );
    }


    /** JavaScript helper functions ***************************************/


    /**
     * Enhanced search JavaScript (Select2)
     *
     * Enqueues JavaScript required for AJAX search with Select2.
     *
     * @codeCoverageIgnore no need to unit test this since it's mostly JavaScript
     *
     * @since 3.1.0
     */
    public static function render_select2_ajax() {

        if ( ! did_action( 'sv_wc_select2_ajax_rendered' ) ) {

            $javascript = "( function(){
                if ( ! $().select2 ) return;
            ";

            // Ensure localized strings are used.
            $javascript .= "

                function getEnhancedSelectFormatString() {

                    if ( 'undefined' !== typeof wc_select_params ) {
                        wc_enhanced_select_params = wc_select_params;
                    }

                    if ( 'undefined' === typeof wc_enhanced_select_params ) {
                        return {};
                    }

                    var formatString = {
                        formatMatches: function( matches ) {
                            if ( 1 === matches ) {
                                return wc_enhanced_select_params.i18n_matches_1;
                            }

                            return wc_enhanced_select_params.i18n_matches_n.replace( '%qty%', matches );
                        },
                        formatNoMatches: function() {
                            return wc_enhanced_select_params.i18n_no_matches;
                        },
                        formatAjaxError: function( jqXHR, textStatus, errorThrown ) {
                            return wc_enhanced_select_params.i18n_ajax_error;
                        },
                        formatInputTooShort: function( input, min ) {
                            var number = min - input.length;

                            if ( 1 === number ) {
                                return wc_enhanced_select_params.i18n_input_too_short_1
                            }

                            return wc_enhanced_select_params.i18n_input_too_short_n.replace( '%qty%', number );
                        },
                        formatInputTooLong: function( input, max ) {
                            var number = input.length - max;

                            if ( 1 === number ) {
                                return wc_enhanced_select_params.i18n_input_too_long_1
                            }

                            return wc_enhanced_select_params.i18n_input_too_long_n.replace( '%qty%', number );
                        },
                        formatSelectionTooBig: function( limit ) {
                            if ( 1 === limit ) {
                                return wc_enhanced_select_params.i18n_selection_too_long_1;
                            }

                            return wc_enhanced_select_params.i18n_selection_too_long_n.replace( '%qty%', number );
                        },
                        formatLoadMore: function( pageNumber ) {
                            return wc_enhanced_select_params.i18n_load_more;
                        },
                        formatSearching: function() {
                            return wc_enhanced_select_params.i18n_searching;
                        }
                    };

                    return formatString;
                }
            ";

            $javascript .= "

                $( 'select.sv-wc-enhanced-search' ).filter( ':not(.enhanced)' ).each( function() {

                    var select2_args = {
                        allowClear:         $( this ).data( 'allow_clear' ) ? true : false,
                        placeholder:        $( this ).data( 'placeholder' ),
                        minimumInputLength: $( this ).data( 'minimum_input_length' ) ? $( this ).data( 'minimum_input_length' ) : '3',
                        escapeMarkup:       function( m ) {
                            return m;
                        },
                        ajax:               {
                            url:            '" . esc_js( admin_url( 'admin-ajax.php' ) ) . "',
                            dataType:       'json',
                            cache:          true,
                            delay:          250,
                            data:           function( params ) {
                                return {
                                    term:         params.term,
                                    request_data: $( this ).data( 'request_data' ) ? $( this ).data( 'request_data' ) : {},
                                    action:       $( this ).data( 'action' ) || 'woocommerce_json_search_products_and_variations',
                                    security:     $( this ).data( 'nonce' )
                                };
                            },
                            processResults: function( data, params ) {
                                var terms = [];
                                if ( data ) {
                                    $.each( data, function( id, text ) {
                                        terms.push( { id: id, text: text } );
                                    });
                                }
                                return { results: terms };
                            }
                        }
                    };

                    select2_args = $.extend( select2_args, getEnhancedSelectFormatString() );

                    $( this ).select2( select2_args ).addClass( 'enhanced' );
                } );
            ";

            $javascript .= '} )();';

            wc_enqueue_js( $javascript );

            /**
             * WC Select2 Ajax Rendered Action.
             *
             * Fired when an Ajax select2 is rendered.
             *
             * @since 3.1.0
             */
            do_action( 'sv_wc_select2_ajax_rendered' );
        }
    }


    /** Framework translation functions ***********************************/


    /**
     * Gettext `__()` wrapper for framework-translated strings
     *
     * Warning! This function should only be used if an existing
     * translation from the framework is to be used. It should
     * never be called for plugin-specific or untranslated strings!
     * Untranslated = not registered via string literal.
     *
     * @since 4.1.0
     * @param string $text
     * @return string translated text
     */
    public static function f__( $text ) {

        return __( $text, 'woocommerce-plugin-framework' );
    }


    /**
     * Gettext `_e()` wrapper for framework-translated strings
     *
     * Warning! This function should only be used if an existing
     * translation from the framework is to be used. It should
     * never be called for plugin-specific or untranslated strings!
     * Untranslated = not registered via string literal.
     *
     * @since 4.1.0
     * @param string $text
     */
    public static function f_e( $text ) {

        _e( $text, 'woocommerce-plugin-framework' );
    }


    /**
     * Gettext `_x()` wrapper for framework-translated strings
     *
     * Warning! This function should only be used if an existing
     * translation from the framework is to be used. It should
     * never be called for plugin-specific or untranslated strings!
     * Untranslated = not registered via string literal.
     *
     * @since 4.1.0
     *
     * @param string $text
     * @param string $context
     * @return string translated text
     */
    public static function f_x( $text, $context ) {

        return _x( $text, $context, 'woocommerce-plugin-framework' );
    }


    /** Misc functions ****************************************************/


    /**
     * Gets the WordPress current screen.
     *
     * @see get_current_screen() replacement which is always available, unlike the WordPress core function
     *
     * @since 5.4.2
     *
     * @return \WP_Screen|null
     */
    public static function get_current_screen() {
        global $current_screen;

        return $current_screen ?: null;
    }


    /**
     * Checks if the current screen matches a specified ID.
     *
     * This helps avoiding using the get_current_screen() function which is not always available,
     * or setting the substitute global $current_screen every time a check needs to be performed.
     *
     * @since 5.4.2
     *
     * @param string $id id (or property) to compare
     * @param string $prop optional property to compare, defaults to screen id
     * @return bool
     */
    public static function is_current_screen( $id, $prop = 'id' ) {
        global $current_screen;

        return isset( $current_screen->$prop ) && $id === $current_screen->$prop;
    }


    /**
     * Determines if viewing an enhanced admin screen.
     *
     * @since 5.6.0
     *
     * @return bool
     */
    public static function is_enhanced_admin_screen() {

        return is_admin() && SV_WC_Plugin_Compatibility::is_enhanced_admin_available() && ( \Automattic\WooCommerce\Admin\Loader::is_admin_page() || \Automattic\WooCommerce\Admin\Loader::is_embed_page() );
    }


    /**
     * Determines whether the new WooCommerce enhanced navigation is supported and enabled.
     *
     * @since 5.10.6
     *
     * @return bool
     */
    public static function is_wc_navigation_enabled() {

        return
            is_callable( [ \Automattic\WooCommerce\Admin\Features\Navigation\Screen::class, 'register_post_type' ] ) &&
            is_callable( [ \Automattic\WooCommerce\Admin\Features\Navigation\Menu::class, 'add_plugin_item' ] ) &&
            is_callable( [ \Automattic\WooCommerce\Admin\Features\Navigation\Menu::class, 'add_plugin_category' ] ) &&
            is_callable( [ \Automattic\WooCommerce\Admin\Features\Features::class, 'is_enabled' ] ) &&
            \Automattic\WooCommerce\Admin\Features\Features::is_enabled( 'navigation' );
    }


    /**
     * Determines if the current request is for a WC REST API endpoint.
     *
     * @see \WooCommerce::is_rest_api_request()
     *
     * @since 5.9.0
     *
     * @return bool
     */
    public static function is_rest_api_request() {

        if ( is_callable( 'WC' ) && is_callable( [ WC(), 'is_rest_api_request' ] ) ) {
            return (bool) WC()->is_rest_api_request();
        }

        if ( empty( $_SERVER['REQUEST_URI'] ) || ! function_exists( 'rest_get_url_prefix' ) ) {
            return false;
        }

        $rest_prefix         = trailingslashit( rest_get_url_prefix() );
        $is_rest_api_request = false !== strpos( $_SERVER['REQUEST_URI'], $rest_prefix );

        /* applies WooCommerce core filter */
        return (bool) apply_filters( 'woocommerce_is_rest_api_request', $is_rest_api_request );
    }


    /**
     * Displays a notice if the provided hook has not yet run.
     *
     * @since 5.2.0
     *
     * @param string $hook action hook to check
     * @param string $method method/function name
     * @param string $version version the notice was added
     */
    public static function maybe_doing_it_early( $hook, $method, $version ) {

        if ( ! did_action( $hook ) ) {
            wc_doing_it_wrong( $method, "This should only be called after '{$hook}'", $version );
        }
    }


    /**
     * Triggers a PHP error.
     *
     * This wrapper method ensures AJAX isn't broken in the process.
     *
     * @since 4.6.0
     * @param string $message the error message
     * @param int $type Optional. The error type. Defaults to E_USER_NOTICE
     */
    public static function trigger_error( $message, $type = E_USER_NOTICE ) {

        if ( is_callable( 'wp_doing_ajax' ) && wp_doing_ajax() ) {

            switch ( $type ) {

                case E_USER_NOTICE:
                    $prefix = 'Notice: ';
                break;

                case E_USER_WARNING:
                    $prefix = 'Warning: ';
                break;

                default:
                    $prefix = '';
            }

            error_log( $prefix . $message );

        } else {

            trigger_error( $message, $type );
        }
    }


    /**
     * Converts an array of strings to a comma separated list of strings, escaped for SQL use.
     *
     * This can be safely used in SQL IN clauses.
     *
     * @since 5.10.9
     *
     * @param string[] $values
     * @return string
     */
    public static function get_escaped_string_list( array $values ) {
        global $wpdb;

        return (string) $wpdb->prepare( implode( ', ', array_fill( 0, count( $values ), '%s' ) ), $values );
    }


    /**
     * Converts an array of numerical integers into a comma separated list of IDs.
     *
     * This can be safely used for SQL IN clauses.
     *
     * @since 5.10.9
     *
     * @param int[] $ids
     * @return string
     */
    public static function get_escaped_id_list( array $ids ) {

        return implode( ',', array_unique( array_map( 'intval', $ids ) ) );
    }


}


endif;