8889841cPKK[AðMÉÉclass-wc-cli.phpnu„[µü¤includes(); $this->hooks(); } /** * Load command files. */ private function includes() { require_once dirname( __FILE__ ) . '/cli/class-wc-cli-runner.php'; require_once dirname( __FILE__ ) . '/cli/class-wc-cli-rest-command.php'; require_once dirname( __FILE__ ) . '/cli/class-wc-cli-tool-command.php'; require_once dirname( __FILE__ ) . '/cli/class-wc-cli-update-command.php'; require_once dirname( __FILE__ ) . '/cli/class-wc-cli-tracker-command.php'; require_once dirname( __FILE__ ) . '/cli/class-wc-cli-com-command.php'; require_once dirname( __FILE__ ) . '/cli/class-wc-cli-com-extension-command.php'; } /** * Sets up and hooks WP CLI to our CLI code. */ private function hooks() { WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Runner::after_wp_load' ); WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Tool_Command::register_commands' ); WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Update_Command::register_commands' ); WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Tracker_Command::register_commands' ); WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_COM_Command::register_commands' ); WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_COM_Extension_Command::register_commands' ); $cli_runner = wc_get_container()->get( CLIRunner::class ); WP_CLI::add_hook( 'after_wp_load', array( $cli_runner, 'register_commands' ) ); } } new WC_CLI(); PKK[ÈÛ4´x x class-wc-cart-fees.phpnu„[µü¤cart->fees_api() which will reference this class. * * We suggest using the action woocommerce_cart_calculate_fees hook for adding fees. * * @package WooCommerce\Classes * @version 3.2.0 */ defined( 'ABSPATH' ) || exit; /** * WC_Cart_Fees class. * * @since 3.2.0 */ final class WC_Cart_Fees { /** * An array of fee objects. * * @var object[] */ private $fees = array(); /** * New fees are made out of these props. * * @var array */ private $default_fee_props = array( 'id' => '', 'name' => '', 'tax_class' => '', 'taxable' => false, 'amount' => 0, 'total' => 0, ); /** * Constructor. Reference to the cart. * * @param null $deprecated Deprecated since WooCommerce 8.2.0. * * @since 3.2.0 */ public function __construct( $deprecated = null ) { if ( isset( $deprecated ) ) { wc_doing_it_wrong( 'new WC_Cart_Fees', 'You don\'t need to pass a cart parameter to the WC_Cart_Fees constructor anymore', '8.2.0' ); } } /** * Register methods for this object on the appropriate WordPress hooks. */ public function init() {} /** * Add a fee. Fee IDs must be unique. * * @since 3.2.0 * @param array $args Array of fee properties. * @return object Either a fee object if added, or a WP_Error if it failed. */ public function add_fee( $args = array() ) { $fee_props = (object) wp_parse_args( $args, $this->default_fee_props ); $fee_props->name = $fee_props->name ? $fee_props->name : __( 'Fee', 'woocommerce' ); $fee_props->tax_class = in_array( $fee_props->tax_class, array_merge( WC_Tax::get_tax_classes(), WC_Tax::get_tax_class_slugs() ), true ) ? $fee_props->tax_class : ''; $fee_props->taxable = wc_string_to_bool( $fee_props->taxable ); $fee_props->amount = wc_format_decimal( $fee_props->amount ); if ( empty( $fee_props->id ) ) { $fee_props->id = $this->generate_id( $fee_props ); } if ( array_key_exists( $fee_props->id, $this->fees ) ) { return new WP_Error( 'fee_exists', __( 'Fee has already been added.', 'woocommerce' ) ); } $this->fees[ $fee_props->id ] = $fee_props; return $this->fees[ $fee_props->id ]; } /** * Get fees. * * @return array */ public function get_fees() { uasort( $this->fees, array( $this, 'sort_fees_callback' ) ); return $this->fees; } /** * Set fees. * * @param object[] $raw_fees Array of fees. */ public function set_fees( $raw_fees = array() ) { $this->fees = array(); foreach ( $raw_fees as $raw_fee ) { $this->add_fee( $raw_fee ); } } /** * Remove all fees. * * @since 3.2.0 */ public function remove_all_fees() { $this->set_fees(); } /** * Sort fees by amount. * * @param stdClass $a Fee object. * @param stdClass $b Fee object. * @return int */ protected function sort_fees_callback( $a, $b ) { /** * Filter sort fees callback. * * @since 3.8.0 * @param int Sort order, -1 or 1. * @param stdClass $a Fee object. * @param stdClass $b Fee object. */ return apply_filters( 'woocommerce_sort_fees_callback', $a->amount > $b->amount ? -1 : 1, $a, $b ); } /** * Generate a unique ID for the fee being added. * * @param string $fee Fee object. * @return string fee key. */ private function generate_id( $fee ) { return sanitize_title( $fee->name ); } } PKK[•P-zšÂšÂclass-wc-countries.phpnu„[µü¤get_countries(); } elseif ( 'states' === $key ) { return $this->get_states(); } elseif ( 'continents' === $key ) { return $this->get_continents(); } } /** * Get all countries. * * @return array */ public function get_countries() { if ( empty( $this->geo_cache['countries'] ) ) { /** * Allows filtering of the list of countries in WC. * * @since 1.5.3 * * @param array $countries */ $this->geo_cache['countries'] = apply_filters( 'woocommerce_countries', include WC()->plugin_path() . '/i18n/countries.php' ); if ( apply_filters( 'woocommerce_sort_countries', true ) ) { wc_asort_by_locale( $this->geo_cache['countries'] ); } } return $this->geo_cache['countries']; } /** * Check if a given code represents a valid ISO 3166-1 alpha-2 code for a country known to us. * * @since 5.1.0 * @param string $country_code The country code to check as a ISO 3166-1 alpha-2 code. * @return bool True if the country is known to us, false otherwise. */ public function country_exists( $country_code ) { return isset( $this->get_countries()[ $country_code ] ); } /** * Get all continents. * * @return array */ public function get_continents() { if ( empty( $this->geo_cache['continents'] ) ) { /** * Allows filtering of continents in WC. * * @since 2.6.0 * * @param array[array] $continents */ $this->geo_cache['continents'] = apply_filters( 'woocommerce_continents', include WC()->plugin_path() . '/i18n/continents.php' ); } return $this->geo_cache['continents']; } /** * Get continent code for a country code. * * @since 2.6.0 * @param string $cc Country code. * @return string */ public function get_continent_code_for_country( $cc ) { $cc = trim( strtoupper( $cc ) ); $continents = $this->get_continents(); $continents_and_ccs = wp_list_pluck( $continents, 'countries' ); foreach ( $continents_and_ccs as $continent_code => $countries ) { if ( false !== array_search( $cc, $countries, true ) ) { return $continent_code; } } return ''; } /** * Get calling code for a country code. * * @since 3.6.0 * @param string $cc Country code. * @return string|array Some countries have multiple. The code will be stripped of - and spaces and always be prefixed with +. */ public function get_country_calling_code( $cc ) { $codes = wp_cache_get( 'calling-codes', 'countries' ); if ( ! $codes ) { $codes = include WC()->plugin_path() . '/i18n/phone.php'; wp_cache_set( 'calling-codes', $codes, 'countries' ); } $calling_code = isset( $codes[ $cc ] ) ? $codes[ $cc ] : ''; if ( is_array( $calling_code ) ) { $calling_code = $calling_code[0]; } return $calling_code; } /** * Get continents that the store ships to. * * @since 3.6.0 * @return array */ public function get_shipping_continents() { $continents = $this->get_continents(); $shipping_countries = $this->get_shipping_countries(); $shipping_country_codes = array_keys( $shipping_countries ); $shipping_continents = array(); foreach ( $continents as $continent_code => $continent ) { if ( count( array_intersect( $continent['countries'], $shipping_country_codes ) ) ) { $shipping_continents[ $continent_code ] = $continent; } } return $shipping_continents; } /** * Load the states. * * @deprecated 3.6.0 This method was used to load state files, but is no longer needed. @see get_states(). */ public function load_country_states() { global $states; $states = include WC()->plugin_path() . '/i18n/states.php'; /** * Allows filtering of country states in WC. * * @since 1.5.3 * * @param array $states */ $this->geo_cache['states'] = apply_filters( 'woocommerce_states', $states ); } /** * Get the states for a country. * * @param string $cc Country code. * @return false|array of states */ public function get_states( $cc = null ) { if ( ! isset( $this->geo_cache['states'] ) ) { /** * Allows filtering of country states in WC. * * @since 1.5.3 * * @param array $states */ $this->geo_cache['states'] = apply_filters( 'woocommerce_states', include WC()->plugin_path() . '/i18n/states.php' ); } if ( ! is_null( $cc ) ) { return isset( $this->geo_cache['states'][ $cc ] ) ? $this->geo_cache['states'][ $cc ] : false; } else { return $this->geo_cache['states']; } } /** * Get the base address (first line) for the store. * * @since 3.1.1 * @return string */ public function get_base_address() { $base_address = get_option( 'woocommerce_store_address', '' ); return apply_filters( 'woocommerce_countries_base_address', $base_address ); } /** * Get the base address (second line) for the store. * * @since 3.1.1 * @return string */ public function get_base_address_2() { $base_address_2 = get_option( 'woocommerce_store_address_2', '' ); return apply_filters( 'woocommerce_countries_base_address_2', $base_address_2 ); } /** * Get the base country for the store. * * @return string */ public function get_base_country() { $default = wc_get_base_location(); return apply_filters( 'woocommerce_countries_base_country', $default['country'] ); } /** * Get the base state for the store. * * @return string */ public function get_base_state() { $default = wc_get_base_location(); return apply_filters( 'woocommerce_countries_base_state', $default['state'] ); } /** * Get the base city for the store. * * @version 3.1.1 * @return string */ public function get_base_city() { $base_city = get_option( 'woocommerce_store_city', '' ); return apply_filters( 'woocommerce_countries_base_city', $base_city ); } /** * Get the base postcode for the store. * * @since 3.1.1 * @return string */ public function get_base_postcode() { $base_postcode = get_option( 'woocommerce_store_postcode', '' ); return apply_filters( 'woocommerce_countries_base_postcode', $base_postcode ); } /** * Get countries that the store sells to. * * @return array */ public function get_allowed_countries() { if ( 'all' === get_option( 'woocommerce_allowed_countries' ) ) { return apply_filters( 'woocommerce_countries_allowed_countries', $this->countries ); } if ( 'all_except' === get_option( 'woocommerce_allowed_countries' ) ) { $except_countries = get_option( 'woocommerce_all_except_countries', array() ); if ( ! $except_countries ) { return $this->countries; } else { $all_except_countries = $this->countries; foreach ( $except_countries as $country ) { unset( $all_except_countries[ $country ] ); } return apply_filters( 'woocommerce_countries_allowed_countries', $all_except_countries ); } } $countries = array(); $raw_countries = get_option( 'woocommerce_specific_allowed_countries', array() ); if ( $raw_countries ) { foreach ( $raw_countries as $country ) { $countries[ $country ] = $this->countries[ $country ]; } } return apply_filters( 'woocommerce_countries_allowed_countries', $countries ); } /** * Get countries that the store ships to. * * @return array */ public function get_shipping_countries() { if ( '' === get_option( 'woocommerce_ship_to_countries' ) ) { return $this->get_allowed_countries(); } if ( 'all' === get_option( 'woocommerce_ship_to_countries' ) ) { return $this->countries; } $countries = array(); $raw_countries = get_option( 'woocommerce_specific_ship_to_countries' ); if ( $raw_countries ) { foreach ( $raw_countries as $country ) { $countries[ $country ] = $this->countries[ $country ]; } } return apply_filters( 'woocommerce_countries_shipping_countries', $countries ); } /** * Get allowed country states. * * @return array */ public function get_allowed_country_states() { if ( get_option( 'woocommerce_allowed_countries' ) !== 'specific' ) { return $this->states; } $states = array(); $raw_countries = get_option( 'woocommerce_specific_allowed_countries' ); if ( $raw_countries ) { foreach ( $raw_countries as $country ) { if ( isset( $this->states[ $country ] ) ) { $states[ $country ] = $this->states[ $country ]; } } } return apply_filters( 'woocommerce_countries_allowed_country_states', $states ); } /** * Get shipping country states. * * @return array */ public function get_shipping_country_states() { if ( get_option( 'woocommerce_ship_to_countries' ) === '' ) { return $this->get_allowed_country_states(); } if ( get_option( 'woocommerce_ship_to_countries' ) !== 'specific' ) { return $this->states; } $states = array(); $raw_countries = get_option( 'woocommerce_specific_ship_to_countries' ); if ( $raw_countries ) { foreach ( $raw_countries as $country ) { if ( ! empty( $this->states[ $country ] ) ) { $states[ $country ] = $this->states[ $country ]; } } } return apply_filters( 'woocommerce_countries_shipping_country_states', $states ); } /** * Gets an array of countries in the EU. * * @param string $type Type of countries to retrieve. Blank for EU member countries. eu_vat for EU VAT countries. * @return string[] */ public function get_european_union_countries( $type = '' ) { $countries = array( 'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK' ); if ( 'eu_vat' === $type ) { $countries[] = 'MC'; } return apply_filters( 'woocommerce_european_union_countries', $countries, $type ); } /** * Gets an array of Non-EU countries that use VAT as the Local name for their taxes based on this list - https://en.wikipedia.org/wiki/Value-added_tax#Non-European_Union_countries * * @deprecated 4.0.0 * @since 3.9.0 * @return string[] */ public function countries_using_vat() { wc_deprecated_function( 'countries_using_vat', '4.0', 'WC_Countries::get_vat_countries' ); $countries = array( 'AE', 'AL', 'AR', 'AZ', 'BB', 'BH', 'BO', 'BS', 'BY', 'CL', 'CO', 'EC', 'EG', 'ET', 'FJ', 'GH', 'GM', 'GT', 'IL', 'IN', 'IR', 'KN', 'KR', 'KZ', 'LK', 'MD', 'ME', 'MK', 'MN', 'MU', 'MX', 'NA', 'NG', 'NP', 'PS', 'PY', 'RS', 'RU', 'RW', 'SA', 'SV', 'TH', 'TR', 'UA', 'UY', 'UZ', 'VE', 'VN', 'ZA' ); return apply_filters( 'woocommerce_countries_using_vat', $countries ); } /** * Gets an array of countries using VAT. * * @since 4.0.0 * @return string[] of country codes. */ public function get_vat_countries() { $eu_countries = $this->get_european_union_countries(); $vat_countries = array( 'AE', 'AL', 'AR', 'AZ', 'BB', 'BH', 'BO', 'BS', 'BY', 'CL', 'CO', 'EC', 'EG', 'ET', 'FJ', 'GB', 'GH', 'GM', 'GT', 'IL', 'IM', 'IN', 'IR', 'KN', 'KR', 'KZ', 'LK', 'MC', 'MD', 'ME', 'MK', 'MN', 'MU', 'MX', 'NA', 'NG', 'NO', 'NP', 'PS', 'PY', 'RS', 'RU', 'RW', 'SA', 'SV', 'TH', 'TR', 'UA', 'UY', 'UZ', 'VE', 'VN', 'ZA' ); return apply_filters( 'woocommerce_vat_countries', array_merge( $eu_countries, $vat_countries ) ); } /** * Gets the correct string for shipping - either 'to the' or 'to'. * * @param string $country_code Country code. * @return string */ public function shipping_to_prefix( $country_code = '' ) { $country_code = $country_code ? $country_code : WC()->customer->get_shipping_country(); $countries = array( 'AE', 'CZ', 'DO', 'GB', 'NL', 'PH', 'US', 'USAF' ); $return = in_array( $country_code, $countries, true ) ? _x( 'to the', 'shipping country prefix', 'woocommerce' ) : _x( 'to', 'shipping country prefix', 'woocommerce' ); return apply_filters( 'woocommerce_countries_shipping_to_prefix', $return, $country_code ); } /** * Prefix certain countries with 'the'. * * @param string $country_code Country code. * @return string */ public function estimated_for_prefix( $country_code = '' ) { $country_code = $country_code ? $country_code : $this->get_base_country(); $countries = array( 'AE', 'CZ', 'DO', 'GB', 'NL', 'PH', 'US', 'USAF' ); $return = in_array( $country_code, $countries, true ) ? __( 'the', 'woocommerce' ) . ' ' : ''; return apply_filters( 'woocommerce_countries_estimated_for_prefix', $return, $country_code ); } /** * Correctly name tax in some countries VAT on the frontend. * * @return string */ public function tax_or_vat() { $return = in_array( $this->get_base_country(), $this->get_vat_countries(), true ) ? __( 'VAT', 'woocommerce' ) : __( 'Tax', 'woocommerce' ); return apply_filters( 'woocommerce_countries_tax_or_vat', $return ); } /** * Include the Inc Tax label. * * @return string */ public function inc_tax_or_vat() { $return = in_array( $this->get_base_country(), $this->get_vat_countries(), true ) ? __( '(incl. VAT)', 'woocommerce' ) : __( '(incl. tax)', 'woocommerce' ); return apply_filters( 'woocommerce_countries_inc_tax_or_vat', $return ); } /** * Include the Ex Tax label. * * @return string */ public function ex_tax_or_vat() { $return = in_array( $this->get_base_country(), $this->get_vat_countries(), true ) ? __( '(ex. VAT)', 'woocommerce' ) : __( '(ex. tax)', 'woocommerce' ); return apply_filters( 'woocommerce_countries_ex_tax_or_vat', $return ); } /** * Outputs the list of countries and states for use in dropdown boxes. * * @param string $selected_country Selected country. * @param string $selected_state Selected state. * @param bool $escape If we should escape HTML. */ public function country_dropdown_options( $selected_country = '', $selected_state = '', $escape = false ) { if ( $this->countries ) { foreach ( $this->countries as $key => $value ) { $states = $this->get_states( $key ); if ( $states ) { // Maybe default the selected state as the first one. if ( $selected_country === $key && '*' === $selected_state ) { $selected_state = key( $states ) ?? '*'; } echo ''; foreach ( $states as $state_key => $state_value ) { echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } echo ''; } else { echo '' . ( $escape ? esc_html( $value ) : $value ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } } } /** * Get country address formats. * * These define how addresses are formatted for display in various countries. * * @return array */ public function get_address_formats() { if ( empty( $this->address_formats ) ) { $this->address_formats = apply_filters( 'woocommerce_localisation_address_formats', array( 'default' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}", 'AT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'AU' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {state} {postcode}\n{country}", 'BE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'CA' => "{company}\n{name}\n{address_1}\n{address_2}\n{city} {state_code} {postcode}\n{country}", 'CH' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'CL' => "{company}\n{name}\n{address_1}\n{address_2}\n{state}\n{postcode} {city}\n{country}", 'CN' => "{country} {postcode}\n{state}, {city}, {address_2}, {address_1}\n{company}\n{name}", 'CZ' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'DE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'DK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'EE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'ES' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{state}\n{country}", 'FI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'FR' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city_upper}\n{country}", 'HK' => "{company}\n{first_name} {last_name_upper}\n{address_1}\n{address_2}\n{city_upper}\n{state_upper}\n{country}", 'HU' => "{last_name} {first_name}\n{company}\n{city}\n{address_1}\n{address_2}\n{postcode}\n{country}", 'IN' => "{company}\n{name}\n{address_1}\n{address_2}\n{city} {postcode}\n{state}, {country}", 'IS' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'IT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode}\n{city}\n{state_upper}\n{country}", 'JM' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode_upper}\n{country}", 'JP' => "{postcode}\n{state} {city} {address_1}\n{address_2}\n{company}\n{last_name} {first_name}\n{country}", 'LI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'NL' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'NO' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'NZ' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {postcode}\n{country}", 'PL' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'PR' => "{company}\n{name}\n{address_1} {address_2}\n{city} \n{country} {postcode}", 'PT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'RS' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'SE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'SI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'SK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", 'TR' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city} {state}\n{country}", 'TW' => "{company}\n{last_name} {first_name}\n{address_1}\n{address_2}\n{state}, {city} {postcode}\n{country}", 'UG' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}, {country}", 'US' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}, {state_code} {postcode}\n{country}", 'VN' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {postcode}\n{country}", ) ); } return $this->address_formats; } /** * Get country address format. * * @param array $args Arguments. * @param string $separator How to separate address lines. @since 3.5.0. * @return string */ public function get_formatted_address( $args = array(), $separator = '
' ) { $default_args = array( 'first_name' => '', 'last_name' => '', 'company' => '', 'address_1' => '', 'address_2' => '', 'city' => '', 'state' => '', 'postcode' => '', 'country' => '', ); $args = array_map( 'trim', wp_parse_args( $args, $default_args ) ); $state = $args['state']; $country = $args['country']; // Get all formats. $formats = $this->get_address_formats(); // Get format for the address' country. $format = ( $country && isset( $formats[ $country ] ) ) ? $formats[ $country ] : $formats['default']; // Handle full country name. $full_country = ( isset( $this->countries[ $country ] ) ) ? $this->countries[ $country ] : $country; // Country is not needed if the same as base. if ( $country === $this->get_base_country() && ! apply_filters( 'woocommerce_formatted_address_force_country_display', false ) ) { $format = str_replace( '{country}', '', $format ); } // Handle full state name. $full_state = ( $country && $state && isset( $this->states[ $country ][ $state ] ) ) ? $this->states[ $country ][ $state ] : $state; // Substitute address parts into the string. $replace = array_map( 'esc_html', apply_filters( 'woocommerce_formatted_address_replacements', array( '{first_name}' => $args['first_name'], '{last_name}' => $args['last_name'], '{name}' => sprintf( /* translators: 1: first name 2: last name */ _x( '%1$s %2$s', 'full name', 'woocommerce' ), $args['first_name'], $args['last_name'] ), '{company}' => $args['company'], '{address_1}' => $args['address_1'], '{address_2}' => $args['address_2'], '{city}' => $args['city'], '{state}' => $full_state, '{postcode}' => $args['postcode'], '{country}' => $full_country, '{first_name_upper}' => wc_strtoupper( $args['first_name'] ), '{last_name_upper}' => wc_strtoupper( $args['last_name'] ), '{name_upper}' => wc_strtoupper( sprintf( /* translators: 1: first name 2: last name */ _x( '%1$s %2$s', 'full name', 'woocommerce' ), $args['first_name'], $args['last_name'] ) ), '{company_upper}' => wc_strtoupper( $args['company'] ), '{address_1_upper}' => wc_strtoupper( $args['address_1'] ), '{address_2_upper}' => wc_strtoupper( $args['address_2'] ), '{city_upper}' => wc_strtoupper( $args['city'] ), '{state_upper}' => wc_strtoupper( $full_state ), '{state_code}' => wc_strtoupper( $state ), '{postcode_upper}' => wc_strtoupper( $args['postcode'] ), '{country_upper}' => wc_strtoupper( $full_country ), ), $args ) ); $formatted_address = str_replace( array_keys( $replace ), $replace, $format ); // Clean up white space. $formatted_address = preg_replace( '/ +/', ' ', trim( $formatted_address ) ); $formatted_address = preg_replace( '/\n\n+/', "\n", $formatted_address ); // Break newlines apart and remove empty lines/trim commas and white space. $formatted_address = array_filter( array_map( array( $this, 'trim_formatted_address_line' ), explode( "\n", $formatted_address ) ) ); // Add html breaks. $formatted_address = implode( $separator, $formatted_address ); // We're done! return $formatted_address; } /** * Trim white space and commas off a line. * * @param string $line Line. * @return string */ private function trim_formatted_address_line( $line ) { return trim( $line, ', ' ); } /** * Returns the fields we show by default. This can be filtered later on. * * @return array */ public function get_default_address_fields() { $address_2_label = __( 'Apartment, suite, unit, etc.', 'woocommerce' ); // If necessary, append '(optional)' to the placeholder: we don't need to worry about the // label, though, as woocommerce_form_field() takes care of that. if ( 'optional' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ) ) { $address_2_placeholder = __( 'Apartment, suite, unit, etc. (optional)', 'woocommerce' ); } else { $address_2_placeholder = $address_2_label; } $fields = array( 'first_name' => array( 'label' => __( 'First name', 'woocommerce' ), 'required' => true, 'class' => array( 'form-row-first' ), 'autocomplete' => 'given-name', 'priority' => 10, ), 'last_name' => array( 'label' => __( 'Last name', 'woocommerce' ), 'required' => true, 'class' => array( 'form-row-last' ), 'autocomplete' => 'family-name', 'priority' => 20, ), 'company' => array( 'label' => __( 'Company name', 'woocommerce' ), 'class' => array( 'form-row-wide' ), 'autocomplete' => 'organization', 'priority' => 30, 'required' => 'required' === get_option( 'woocommerce_checkout_company_field', 'optional' ), ), 'country' => array( 'type' => 'country', 'label' => __( 'Country / Region', 'woocommerce' ), 'required' => true, 'class' => array( 'form-row-wide', 'address-field', 'update_totals_on_change' ), 'autocomplete' => 'country', 'priority' => 40, ), 'address_1' => array( 'label' => __( 'Street address', 'woocommerce' ), /* translators: use local order of street name and house number. */ 'placeholder' => esc_attr__( 'House number and street name', 'woocommerce' ), 'required' => true, 'class' => array( 'form-row-wide', 'address-field' ), 'autocomplete' => 'address-line1', 'priority' => 50, ), 'address_2' => array( 'label' => $address_2_label, 'label_class' => array( 'screen-reader-text' ), 'placeholder' => esc_attr( $address_2_placeholder ), 'class' => array( 'form-row-wide', 'address-field' ), 'autocomplete' => 'address-line2', 'priority' => 60, 'required' => 'required' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ), ), 'city' => array( 'label' => __( 'Town / City', 'woocommerce' ), 'required' => true, 'class' => array( 'form-row-wide', 'address-field' ), 'autocomplete' => 'address-level2', 'priority' => 70, ), 'state' => array( 'type' => 'state', 'label' => __( 'State / County', 'woocommerce' ), 'required' => true, 'class' => array( 'form-row-wide', 'address-field' ), 'validate' => array( 'state' ), 'autocomplete' => 'address-level1', 'priority' => 80, ), 'postcode' => array( 'label' => __( 'Postcode / ZIP', 'woocommerce' ), 'required' => true, 'class' => array( 'form-row-wide', 'address-field' ), 'validate' => array( 'postcode' ), 'autocomplete' => 'postal-code', 'priority' => 90, ), ); if ( 'hidden' === get_option( 'woocommerce_checkout_company_field', 'optional' ) ) { unset( $fields['company'] ); } if ( 'hidden' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ) ) { unset( $fields['address_2'] ); } $default_address_fields = apply_filters( 'woocommerce_default_address_fields', $fields ); // Sort each of the fields based on priority. uasort( $default_address_fields, 'wc_checkout_fields_uasort_comparison' ); return $default_address_fields; } /** * Get JS selectors for fields which are shown/hidden depending on the locale. * * @return array */ public function get_country_locale_field_selectors() { $locale_fields = array( 'address_1' => '#billing_address_1_field, #shipping_address_1_field', 'address_2' => '#billing_address_2_field, #shipping_address_2_field', 'state' => '#billing_state_field, #shipping_state_field, #calc_shipping_state_field', 'postcode' => '#billing_postcode_field, #shipping_postcode_field, #calc_shipping_postcode_field', 'city' => '#billing_city_field, #shipping_city_field, #calc_shipping_city_field', ); return apply_filters( 'woocommerce_country_locale_field_selectors', $locale_fields ); } /** * Get country locale settings. * * These locales override the default country selections after a country is chosen. * * @return array */ public function get_country_locale() { if ( empty( $this->locale ) ) { $this->locale = apply_filters( 'woocommerce_get_country_locale', array( 'AE' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'state' => array( 'required' => false, ), ), 'AF' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'AL' => array( 'state' => array( 'label' => __( 'County', 'woocommerce' ), ), ), 'AO' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'AT' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'AU' => array( 'city' => array( 'label' => __( 'Suburb', 'woocommerce' ), ), 'postcode' => array( 'label' => __( 'Postcode', 'woocommerce' ), ), 'state' => array( 'label' => __( 'State', 'woocommerce' ), ), ), 'AX' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'BA' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'label' => __( 'Canton', 'woocommerce' ), 'required' => false, 'hidden' => true, ), ), 'BD' => array( 'postcode' => array( 'required' => false, ), 'state' => array( 'label' => __( 'District', 'woocommerce' ), ), ), 'BE' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'BG' => array( 'state' => array( 'required' => false, ), ), 'BH' => array( 'postcode' => array( 'required' => false, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'BI' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'BO' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'state' => array( 'label' => __( 'Department', 'woocommerce' ), ), ), 'BS' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), ), 'BZ' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'state' => array( 'required' => false, ), ), 'CA' => array( 'postcode' => array( 'label' => __( 'Postal code', 'woocommerce' ), ), 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'CH' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'label' => __( 'Canton', 'woocommerce' ), 'required' => false, ), ), 'CL' => array( 'city' => array( 'required' => true, ), 'postcode' => array( 'required' => false, // Hidden for stores within Chile. @see https://github.com/woocommerce/woocommerce/issues/36546. 'hidden' => 'CL' === $this->get_base_country(), ), 'state' => array( 'label' => __( 'Region', 'woocommerce' ), ), ), 'CN' => array( 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'CO' => array( 'postcode' => array( 'required' => false, ), 'state' => array( 'label' => __( 'Department', 'woocommerce' ), ), ), 'CR' => array( 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'CW' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'state' => array( 'required' => false, ), ), 'CZ' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'DE' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'DK' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'DO' => array( 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'EC' => array( 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'EE' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'ET' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'FI' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'FR' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'GG' => array( 'state' => array( 'required' => false, 'label' => __( 'Parish', 'woocommerce' ), ), ), 'GH' => array( 'postcode' => array( 'required' => false, ), 'state' => array( 'label' => __( 'Region', 'woocommerce' ), ), ), 'GP' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'GF' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'GR' => array( 'state' => array( 'required' => false, ), ), 'GT' => array( 'postcode' => array( 'required' => false, ), 'state' => array( 'label' => __( 'Department', 'woocommerce' ), ), ), 'HK' => array( 'postcode' => array( 'required' => false, ), 'city' => array( 'label' => __( 'Town / District', 'woocommerce' ), ), 'state' => array( 'label' => __( 'Region', 'woocommerce' ), ), ), 'HN' => array( 'state' => array( 'label' => __( 'Department', 'woocommerce' ), ), ), 'HU' => array( 'last_name' => array( 'class' => array( 'form-row-first' ), 'priority' => 10, ), 'first_name' => array( 'class' => array( 'form-row-last' ), 'priority' => 20, ), 'postcode' => array( 'class' => array( 'form-row-first', 'address-field' ), 'priority' => 65, ), 'city' => array( 'class' => array( 'form-row-last', 'address-field' ), ), 'address_1' => array( 'priority' => 71, ), 'address_2' => array( 'priority' => 72, ), 'state' => array( 'label' => __( 'County', 'woocommerce' ), 'required' => false, ), ), 'ID' => array( 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'IE' => array( 'postcode' => array( 'required' => false, 'label' => __( 'Eircode', 'woocommerce' ), ), 'state' => array( 'label' => __( 'County', 'woocommerce' ), ), ), 'IS' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'IL' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'IM' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'IN' => array( 'postcode' => array( 'label' => __( 'PIN Code', 'woocommerce' ), ), 'state' => array( 'label' => __( 'State', 'woocommerce' ), ), ), 'IR' => array( 'state' => array( 'priority' => 50, ), 'city' => array( 'priority' => 60, ), 'address_1' => array( 'priority' => 70, ), 'address_2' => array( 'priority' => 80, ), ), 'IT' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => true, 'label' => __( 'Province', 'woocommerce' ), ), ), 'JM' => array( 'city' => array( 'label' => __( 'Town / City / Post Office', 'woocommerce' ), ), 'postcode' => array( 'required' => false, 'label' => __( 'Postal Code', 'woocommerce' ), ), 'state' => array( 'required' => true, 'label' => __( 'Parish', 'woocommerce' ), ), ), 'JP' => array( 'last_name' => array( 'class' => array( 'form-row-first' ), 'priority' => 10, ), 'first_name' => array( 'class' => array( 'form-row-last' ), 'priority' => 20, ), 'postcode' => array( 'class' => array( 'form-row-first', 'address-field' ), 'priority' => 65, ), 'state' => array( 'label' => __( 'Prefecture', 'woocommerce' ), 'class' => array( 'form-row-last', 'address-field' ), 'priority' => 66, ), 'city' => array( 'priority' => 67, ), 'address_1' => array( 'priority' => 68, ), 'address_2' => array( 'priority' => 69, ), ), 'KN' => array( 'postcode' => array( 'required' => false, 'label' => __( 'Postal code', 'woocommerce' ), ), 'state' => array( 'required' => true, 'label' => __( 'Parish', 'woocommerce' ), ), ), 'KR' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'KW' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'LV' => array( 'state' => array( 'label' => __( 'Municipality', 'woocommerce' ), 'required' => false, ), ), 'LB' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'MF' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'MQ' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'MT' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'MZ' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'NI' => array( 'state' => array( 'label' => __( 'Department', 'woocommerce' ), ), ), 'NL' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'NG' => array( 'postcode' => array( 'label' => __( 'Postcode', 'woocommerce' ), 'required' => false, 'hidden' => true, ), 'state' => array( 'label' => __( 'State', 'woocommerce' ), ), ), 'NZ' => array( 'postcode' => array( 'label' => __( 'Postcode', 'woocommerce' ), ), 'state' => array( 'required' => false, 'label' => __( 'Region', 'woocommerce' ), ), ), 'NO' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'NP' => array( 'state' => array( 'label' => __( 'State / Zone', 'woocommerce' ), ), 'postcode' => array( 'required' => false, ), ), 'PA' => array( 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'PL' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'PR' => array( 'city' => array( 'label' => __( 'Municipality', 'woocommerce' ), ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'PT' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'PY' => array( 'state' => array( 'label' => __( 'Department', 'woocommerce' ), ), ), 'RE' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'RO' => array( 'state' => array( 'label' => __( 'County', 'woocommerce' ), 'required' => true, ), ), 'RS' => array( 'city' => array( 'required' => true, ), 'postcode' => array( 'required' => true, ), 'state' => array( 'label' => __( 'District', 'woocommerce' ), 'required' => false, ), ), 'RW' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'SG' => array( 'state' => array( 'required' => false, 'hidden' => true, ), 'city' => array( 'required' => false, ), ), 'SK' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'SI' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'SR' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), ), 'SV' => array( 'state' => array( 'label' => __( 'Department', 'woocommerce' ), ), ), 'ES' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'LI' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'LK' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'LU' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'MD' => array( 'state' => array( 'label' => __( 'Municipality / District', 'woocommerce' ), ), ), 'SE' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'required' => false, 'hidden' => true, ), ), 'TR' => array( 'postcode' => array( 'priority' => 65, ), 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'UG' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'city' => array( 'label' => __( 'Town / Village', 'woocommerce' ), 'required' => true, ), 'state' => array( 'label' => __( 'District', 'woocommerce' ), 'required' => true, ), ), 'US' => array( 'postcode' => array( 'label' => __( 'ZIP Code', 'woocommerce' ), ), 'state' => array( 'label' => __( 'State', 'woocommerce' ), ), ), 'UY' => array( 'state' => array( 'label' => __( 'Department', 'woocommerce' ), ), ), 'GB' => array( 'postcode' => array( 'label' => __( 'Postcode', 'woocommerce' ), ), 'state' => array( 'label' => __( 'County', 'woocommerce' ), 'required' => false, ), ), 'ST' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), 'state' => array( 'label' => __( 'District', 'woocommerce' ), ), ), 'VN' => array( 'state' => array( 'required' => false, 'hidden' => true, ), 'postcode' => array( 'priority' => 65, 'required' => false, 'hidden' => false, ), 'address_2' => array( 'required' => false, 'hidden' => false, ), ), 'WS' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), ), 'YT' => array( 'state' => array( 'required' => false, 'hidden' => true, ), ), 'ZA' => array( 'state' => array( 'label' => __( 'Province', 'woocommerce' ), ), ), 'ZW' => array( 'postcode' => array( 'required' => false, 'hidden' => true, ), ), ) ); $this->locale = array_intersect_key( $this->locale, array_merge( $this->get_allowed_countries(), $this->get_shipping_countries() ) ); // Default Locale Can be filtered to override fields in get_address_fields(). Countries with no specific locale will use default. $this->locale['default'] = apply_filters( 'woocommerce_get_country_locale_default', $this->get_default_address_fields() ); // Filter default AND shop base locales to allow overrides via a single function. These will be used when changing countries on the checkout. if ( ! isset( $this->locale[ $this->get_base_country() ] ) ) { $this->locale[ $this->get_base_country() ] = $this->locale['default']; } $this->locale['default'] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale['default'] ); $this->locale[ $this->get_base_country() ] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale[ $this->get_base_country() ] ); } return $this->locale; } /** * Apply locale and get address fields. * * @param mixed $country Country. * @param string $type Address type, defaults to 'billing_'. * @return array */ public function get_address_fields( $country = '', $type = 'billing_' ) { if ( ! $country ) { $country = $this->get_base_country(); } $fields = $this->get_default_address_fields(); $locale = $this->get_country_locale(); if ( isset( $locale[ $country ] ) ) { $fields = wc_array_overlay( $fields, $locale[ $country ] ); } // Prepend field keys. $address_fields = array(); foreach ( $fields as $key => $value ) { if ( 'state' === $key ) { $value['country_field'] = $type . 'country'; $value['country'] = $country; } $address_fields[ $type . $key ] = $value; } // Add email and phone fields. if ( 'billing_' === $type ) { if ( 'hidden' !== get_option( 'woocommerce_checkout_phone_field', 'required' ) ) { $address_fields['billing_phone'] = array( 'label' => __( 'Phone', 'woocommerce' ), 'required' => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ), 'type' => 'tel', 'class' => array( 'form-row-wide' ), 'validate' => array( 'phone' ), 'autocomplete' => 'tel', 'priority' => 100, ); } $address_fields['billing_email'] = array( 'label' => __( 'Email address', 'woocommerce' ), 'required' => true, 'type' => 'email', 'class' => array( 'form-row-wide' ), 'validate' => array( 'email' ), 'autocomplete' => 'no' === get_option( 'woocommerce_registration_generate_username' ) ? 'email' : 'email username', 'priority' => 110, ); } /** * Important note on this filter: Changes to address fields can and will be overridden by * the woocommerce_default_address_fields. The locales/default locales apply on top based * on country selection. If you want to change things like the required status of an * address field, filter woocommerce_default_address_fields instead. */ $address_fields = apply_filters( 'woocommerce_' . $type . 'fields', $address_fields, $country ); // Sort each of the fields based on priority. uasort( $address_fields, 'wc_checkout_fields_uasort_comparison' ); return $address_fields; } } PKK[-kôô class-wc-geolite-integration.phpnu„[µü¤database = $database; } /** * Get country 2-letters ISO by IP address. * Returns empty string when not able to find any ISO code. * * @param string $ip_address User IP address. * @return string * @deprecated 3.9.0 */ public function get_country_iso( $ip_address ) { wc_deprecated_function( 'get_country_iso', '3.9.0' ); $iso_code = ''; try { $reader = new MaxMind\Db\Reader( $this->database ); // phpcs:ignore PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound $data = $reader->get( $ip_address ); if ( isset( $data['country']['iso_code'] ) ) { $iso_code = $data['country']['iso_code']; } $reader->close(); } catch ( Exception $e ) { $this->log( $e->getMessage(), 'warning' ); } return sanitize_text_field( strtoupper( $iso_code ) ); } /** * Logging method. * * @param string $message Log message. * @param string $level Log level. * Available options: 'emergency', 'alert', * 'critical', 'error', 'warning', 'notice', * 'info' and 'debug'. * Defaults to 'info'. */ private function log( $message, $level = 'info' ) { if ( is_null( $this->log ) ) { $this->log = wc_get_logger(); } $this->log->log( $level, $message, array( 'source' => 'geoip' ) ); } } PKK[ !A[ÿpÿpclass-wc-cart-totals.phpnu„[µü¤ 0, 'fees_total_tax' => 0, 'items_subtotal' => 0, 'items_subtotal_tax' => 0, 'items_total' => 0, 'items_total_tax' => 0, 'total' => 0, 'shipping_total' => 0, 'shipping_tax_total' => 0, 'discounts_total' => 0, ); /** * Cache of tax rates for a given tax class. * * @var array */ protected $item_tax_rates; /** * Sets up the items provided, and calculate totals. * * @since 3.2.0 * @throws Exception If missing WC_Cart object. * @param WC_Cart $cart Cart object to calculate totals for. */ public function __construct( &$cart = null ) { if ( ! is_a( $cart, 'WC_Cart' ) ) { throw new Exception( 'A valid WC_Cart object is required' ); } $this->cart = $cart; $this->calculate_tax = wc_tax_enabled() && ! $cart->get_customer()->get_is_vat_exempt(); $this->calculate(); } /** * Run all calculation methods on the given items in sequence. * * @since 3.2.0 */ protected function calculate() { $this->calculate_item_totals(); $this->calculate_shipping_totals(); $this->calculate_fee_totals(); $this->calculate_totals(); } /** * Get default blank set of props used per item. * * @since 3.2.0 * @return array */ protected function get_default_item_props() { return (object) array( 'object' => null, 'tax_class' => '', 'taxable' => false, 'quantity' => 0, 'product' => false, 'price_includes_tax' => false, 'subtotal' => 0, 'subtotal_tax' => 0, 'subtotal_taxes' => array(), 'total' => 0, 'total_tax' => 0, 'taxes' => array(), ); } /** * Get default blank set of props used per fee. * * @since 3.2.0 * @return array */ protected function get_default_fee_props() { return (object) array( 'object' => null, 'tax_class' => '', 'taxable' => false, 'total_tax' => 0, 'taxes' => array(), ); } /** * Get default blank set of props used per shipping row. * * @since 3.2.0 * @return array */ protected function get_default_shipping_props() { return (object) array( 'object' => null, 'tax_class' => '', 'taxable' => false, 'total' => 0, 'total_tax' => 0, 'taxes' => array(), ); } /** * Handles a cart or order object passed in for calculation. Normalises data * into the same format for use by this class. * * Each item is made up of the following props, in addition to those returned by get_default_item_props() for totals. * - key: An identifier for the item (cart item key or line item ID). * - cart_item: For carts, the cart item from the cart which may include custom data. * - quantity: The qty for this line. * - price: The line price in cents. * - product: The product object this cart item is for. * * @since 3.2.0 */ protected function get_items_from_cart() { $this->items = array(); foreach ( $this->cart->get_cart() as $cart_item_key => $cart_item ) { $item = $this->get_default_item_props(); $item->key = $cart_item_key; $item->object = $cart_item; $item->tax_class = $cart_item['data']->get_tax_class(); $item->taxable = 'taxable' === $cart_item['data']->get_tax_status(); $item->price_includes_tax = wc_prices_include_tax(); $item->quantity = $cart_item['quantity']; $item->price = wc_add_number_precision_deep( (float) $cart_item['data']->get_price() * (float) $cart_item['quantity'] ); $item->product = $cart_item['data']; $item->tax_rates = $this->get_item_tax_rates( $item ); $this->items[ $cart_item_key ] = $item; } } /** * Get item costs grouped by tax class. * * @since 3.2.0 * @return array */ protected function get_tax_class_costs() { $item_tax_classes = wp_list_pluck( $this->items, 'tax_class' ); $shipping_tax_classes = wp_list_pluck( $this->shipping, 'tax_class' ); $fee_tax_classes = wp_list_pluck( $this->fees, 'tax_class' ); $costs = array_fill_keys( $item_tax_classes + $shipping_tax_classes + $fee_tax_classes, 0 ); $costs['non-taxable'] = 0; foreach ( $this->items + $this->fees + $this->shipping as $item ) { if ( 0 > $item->total ) { continue; } if ( ! $item->taxable ) { $costs['non-taxable'] += $item->total; } elseif ( 'inherit' === $item->tax_class ) { $costs[ reset( $item_tax_classes ) ] += $item->total; } else { $costs[ $item->tax_class ] += $item->total; } } return array_filter( $costs ); } /** * Get fee objects from the cart. Normalises data * into the same format for use by this class. * * @since 3.2.0 */ protected function get_fees_from_cart() { $this->fees = array(); $this->cart->calculate_fees(); $fee_running_total = 0; foreach ( $this->cart->get_fees() as $fee_key => $fee_object ) { $fee = $this->get_default_fee_props(); $fee->object = $fee_object; $fee->tax_class = $fee->object->tax_class; $fee->taxable = $fee->object->taxable; $fee->total = wc_add_number_precision_deep( $fee->object->amount ); // Negative fees should not make the order total go negative. if ( 0 > $fee->total ) { $max_discount = NumberUtil::round( $this->get_total( 'items_total', true ) + $fee_running_total + $this->get_total( 'shipping_total', true ) ) * -1; if ( $fee->total < $max_discount ) { $fee->total = $max_discount; } } $fee_running_total += $fee->total; if ( $this->calculate_tax ) { if ( 0 > $fee->total ) { // Negative fees should have the taxes split between all items so it works as a true discount. $tax_class_costs = $this->get_tax_class_costs(); $total_cost = array_sum( $tax_class_costs ); if ( $total_cost ) { foreach ( $tax_class_costs as $tax_class => $tax_class_cost ) { if ( 'non-taxable' === $tax_class ) { continue; } $proportion = $tax_class_cost / $total_cost; $cart_discount_proportion = $fee->total * $proportion; $fee->taxes = wc_array_merge_recursive_numeric( $fee->taxes, WC_Tax::calc_tax( $fee->total * $proportion, WC_Tax::get_rates( $tax_class ) ) ); } } } elseif ( $fee->object->taxable ) { $fee->taxes = WC_Tax::calc_tax( $fee->total, WC_Tax::get_rates( $fee->tax_class, $this->cart->get_customer() ), false ); } } $fee->taxes = apply_filters( 'woocommerce_cart_totals_get_fees_from_cart_taxes', $fee->taxes, $fee, $this ); $fee->total_tax = array_sum( array_map( array( $this, 'round_line_tax' ), $fee->taxes ) ); // Set totals within object. $fee->object->total = wc_remove_number_precision_deep( $fee->total ); $fee->object->tax_data = wc_remove_number_precision_deep( $fee->taxes ); $fee->object->tax = wc_remove_number_precision_deep( $fee->total_tax ); $this->fees[ $fee_key ] = $fee; } } /** * Get shipping methods from the cart and normalise. * * @since 3.2.0 */ protected function get_shipping_from_cart() { $this->shipping = array(); if ( ! $this->cart->show_shipping() ) { return; } foreach ( $this->cart->calculate_shipping() as $key => $shipping_object ) { $shipping_line = $this->get_default_shipping_props(); $shipping_line->object = $shipping_object; $shipping_line->tax_class = get_option( 'woocommerce_shipping_tax_class' ); $shipping_line->taxable = true; $shipping_line->total = wc_add_number_precision_deep( $shipping_object->cost ); $shipping_line->taxes = wc_add_number_precision_deep( $shipping_object->taxes, false ); $shipping_line->taxes = array_map( array( $this, 'round_item_subtotal' ), $shipping_line->taxes ); $shipping_line->total_tax = array_sum( $shipping_line->taxes ); $this->shipping[ $key ] = $shipping_line; } } /** * Return array of coupon objects from the cart. Normalises data * into the same format for use by this class. * * @since 3.2.0 */ protected function get_coupons_from_cart() { $this->coupons = $this->cart->get_coupons(); foreach ( $this->coupons as $coupon ) { switch ( $coupon->get_discount_type() ) { case 'fixed_product': $coupon->sort = 1; break; case 'percent': $coupon->sort = 2; break; case 'fixed_cart': $coupon->sort = 3; break; default: $coupon->sort = 0; break; } // Allow plugins to override the default order. $coupon->sort = apply_filters( 'woocommerce_coupon_sort', $coupon->sort, $coupon ); } uasort( $this->coupons, array( $this, 'sort_coupons_callback' ) ); } /** * Sort coupons so discounts apply consistently across installs. * * In order of priority; * - sort param * - usage restriction * - coupon value * - ID * * @param WC_Coupon $a Coupon object. * @param WC_Coupon $b Coupon object. * @return int */ protected function sort_coupons_callback( $a, $b ) { if ( $a->sort === $b->sort ) { if ( $a->get_limit_usage_to_x_items() === $b->get_limit_usage_to_x_items() ) { if ( $a->get_amount() === $b->get_amount() ) { return $b->get_id() - $a->get_id(); } return ( $a->get_amount() < $b->get_amount() ) ? -1 : 1; } return ( $a->get_limit_usage_to_x_items() < $b->get_limit_usage_to_x_items() ) ? -1 : 1; } return ( $a->sort < $b->sort ) ? -1 : 1; } /** * Ran to remove all base taxes from an item. Used when prices include tax, and the customer is tax exempt. * * @since 3.2.2 * @param object $item Item to adjust the prices of. * @return object */ protected function remove_item_base_taxes( $item ) { if ( $item->price_includes_tax && $item->taxable ) { if ( apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { $base_tax_rates = WC_Tax::get_base_tax_rates( $item->product->get_tax_class( 'unfiltered' ) ); } else { /** * If we want all customers to pay the same price on this store, we should not remove base taxes from a VAT exempt user's price, * but just the relevant tax rate. See issue #20911. */ $base_tax_rates = $item->tax_rates; } // Work out a new base price without the shop's base tax. $taxes = WC_Tax::calc_tax( $item->price, $base_tax_rates, true ); // Now we have a new item price (excluding TAX). $item->price = NumberUtil::round( $item->price - array_sum( $taxes ) ); $item->price_includes_tax = false; } return $item; } /** * Only ran if woocommerce_adjust_non_base_location_prices is true. * * If the customer is outside of the base location, this removes the base * taxes. This is off by default unless the filter is used. * * Uses edit context so unfiltered tax class is returned. * * @since 3.2.0 * @param object $item Item to adjust the prices of. * @return object */ protected function adjust_non_base_location_price( $item ) { if ( $item->price_includes_tax && $item->taxable ) { $base_tax_rates = WC_Tax::get_base_tax_rates( $item->product->get_tax_class( 'unfiltered' ) ); if ( $item->tax_rates !== $base_tax_rates ) { // Work out a new base price without the shop's base tax. $taxes = WC_Tax::calc_tax( $item->price, $base_tax_rates, true ); $new_taxes = WC_Tax::calc_tax( $item->price - array_sum( $taxes ), $item->tax_rates, false ); // Now we have a new item price. $item->price = $item->price - array_sum( $taxes ) + array_sum( $new_taxes ); } } return $item; } /** * Get discounted price of an item with precision (in cents). * * @since 3.2.0 * @param object $item_key Item to get the price of. * @return int */ protected function get_discounted_price_in_cents( $item_key ) { $item = $this->items[ $item_key ]; $price = isset( $this->coupon_discount_totals[ $item_key ] ) ? $item->price - $this->coupon_discount_totals[ $item_key ] : $item->price; return $price; } /** * Get tax rates for an item. Caches rates in class to avoid multiple look ups. * * @param object $item Item to get tax rates for. * @return array of taxes */ protected function get_item_tax_rates( $item ) { if ( ! wc_tax_enabled() ) { return array(); } $tax_class = $item->product->get_tax_class(); $item_tax_rates = isset( $this->item_tax_rates[ $tax_class ] ) ? $this->item_tax_rates[ $tax_class ] : $this->item_tax_rates[ $tax_class ] = WC_Tax::get_rates( $item->product->get_tax_class(), $this->cart->get_customer() ); // Allow plugins to filter item tax rates. return apply_filters( 'woocommerce_cart_totals_get_item_tax_rates', $item_tax_rates, $item, $this->cart ); } /** * Get item costs grouped by tax class. * * @since 3.2.0 * @return array */ protected function get_item_costs_by_tax_class() { $tax_classes = array( 'non-taxable' => 0, ); foreach ( $this->items + $this->fees + $this->shipping as $item ) { if ( ! isset( $tax_classes[ $item->tax_class ] ) ) { $tax_classes[ $item->tax_class ] = 0; } if ( $item->taxable ) { $tax_classes[ $item->tax_class ] += $item->total; } else { $tax_classes['non-taxable'] += $item->total; } } return $tax_classes; } /** * Get a single total with or without precision (in cents). * * @since 3.2.0 * @param string $key Total to get. * @param bool $in_cents Should the totals be returned in cents, or without precision. * @return int|float */ public function get_total( $key = 'total', $in_cents = false ) { $totals = $this->get_totals( $in_cents ); return isset( $totals[ $key ] ) ? $totals[ $key ] : 0; } /** * Set a single total. * * @since 3.2.0 * @param string $key Total name you want to set. * @param int $total Total to set. */ protected function set_total( $key, $total ) { $this->totals[ $key ] = $total; } /** * Get all totals with or without precision (in cents). * * @since 3.2.0 * @param bool $in_cents Should the totals be returned in cents, or without precision. * @return array. */ public function get_totals( $in_cents = false ) { return $in_cents ? $this->totals : wc_remove_number_precision_deep( $this->totals ); } /** * Returns array of values for totals calculation. * * @param string $field Field name. Will probably be `total` or `subtotal`. * @return array Items object */ protected function get_values_for_total( $field ) { return array_values( wp_list_pluck( $this->items, $field ) ); } /** * Get taxes merged by type. * * @since 3.2.0 * @param bool $in_cents If returned value should be in cents. * @param array|string $types Types to merge and return. Defaults to all. * @return array */ protected function get_merged_taxes( $in_cents = false, $types = array( 'items', 'fees', 'shipping' ) ) { $items = array(); $taxes = array(); if ( is_string( $types ) ) { $types = array( $types ); } foreach ( $types as $type ) { if ( isset( $this->$type ) ) { $items = array_merge( $items, $this->$type ); } } foreach ( $items as $item ) { foreach ( $item->taxes as $rate_id => $rate ) { if ( ! isset( $taxes[ $rate_id ] ) ) { $taxes[ $rate_id ] = 0; } $taxes[ $rate_id ] += $this->round_line_tax( $rate ); } } return $in_cents ? $taxes : wc_remove_number_precision_deep( $taxes ); } /** * Round merged taxes. * * @deprecated 3.9.0 `calculate_item_subtotals` should already appropriately round the tax values. * @since 3.5.4 * @param array $taxes Taxes to round. * @return array */ protected function round_merged_taxes( $taxes ) { foreach ( $taxes as $rate_id => $tax ) { $taxes[ $rate_id ] = $this->round_line_tax( $tax ); } return $taxes; } /** * Combine item taxes into a single array, preserving keys. * * @since 3.2.0 * @param array $item_taxes Taxes to combine. * @return array */ protected function combine_item_taxes( $item_taxes ) { $merged_taxes = array(); foreach ( $item_taxes as $taxes ) { foreach ( $taxes as $tax_id => $tax_amount ) { if ( ! isset( $merged_taxes[ $tax_id ] ) ) { $merged_taxes[ $tax_id ] = 0; } $merged_taxes[ $tax_id ] += $tax_amount; } } return $merged_taxes; } /* |-------------------------------------------------------------------------- | Calculation methods. |-------------------------------------------------------------------------- */ /** * Calculate item totals. * * @since 3.2.0 */ protected function calculate_item_totals() { $this->get_items_from_cart(); $this->calculate_item_subtotals(); $this->calculate_discounts(); foreach ( $this->items as $item_key => $item ) { $item->total = $this->get_discounted_price_in_cents( $item_key ); $item->total_tax = 0; if ( has_filter( 'woocommerce_get_discounted_price' ) ) { /** * Allow plugins to filter this price like in the legacy cart class. * * This is legacy and should probably be deprecated in the future. * $item->object is the cart item object. * $this->cart is the cart object. */ $item->total = wc_add_number_precision( apply_filters( 'woocommerce_get_discounted_price', wc_remove_number_precision( $item->total ), $item->object, $this->cart ) ); } if ( $this->calculate_tax && $item->product->is_taxable() ) { $total_taxes = apply_filters( 'woocommerce_calculate_item_totals_taxes', WC_Tax::calc_tax( $item->total, $item->tax_rates, $item->price_includes_tax ), $item, $this ); $item->taxes = $total_taxes; $item->total_tax = array_sum( array_map( array( $this, 'round_line_tax' ), $item->taxes ) ); if ( $item->price_includes_tax ) { // Use unrounded taxes so we can re-calculate from the orders screen accurately later. $item->total = $item->total - array_sum( $item->taxes ); } } $this->cart->cart_contents[ $item_key ]['line_tax_data']['total'] = wc_remove_number_precision_deep( $item->taxes ); $this->cart->cart_contents[ $item_key ]['line_total'] = wc_remove_number_precision( $item->total ); $this->cart->cart_contents[ $item_key ]['line_tax'] = wc_remove_number_precision( $item->total_tax ); } $items_total = $this->get_rounded_items_total( $this->get_values_for_total( 'total' ) ); $this->set_total( 'items_total', $items_total ); $this->set_total( 'items_total_tax', array_sum( array_values( wp_list_pluck( $this->items, 'total_tax' ) ) ) ); $this->cart->set_cart_contents_total( $this->get_total( 'items_total' ) ); $this->cart->set_cart_contents_tax( array_sum( $this->get_merged_taxes( false, 'items' ) ) ); $this->cart->set_cart_contents_taxes( $this->get_merged_taxes( false, 'items' ) ); } /** * Subtotals are costs before discounts. * * To prevent rounding issues we need to work with the inclusive price where possible * otherwise we'll see errors such as when working with a 9.99 inc price, 20% VAT which would * be 8.325 leading to totals being 1p off. * * Pre tax coupons come off the price the customer thinks they are paying - tax is calculated * afterwards. * * e.g. $100 bike with $10 coupon = customer pays $90 and tax worked backwards from that. * * @since 3.2.0 */ protected function calculate_item_subtotals() { $merged_subtotal_taxes = array(); // Taxes indexed by tax rate ID for storage later. $adjust_non_base_location_prices = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ); $is_customer_vat_exempt = $this->cart->get_customer()->get_is_vat_exempt(); foreach ( $this->items as $item_key => $item ) { if ( $item->price_includes_tax ) { if ( $is_customer_vat_exempt ) { $item = $this->remove_item_base_taxes( $item ); } elseif ( $adjust_non_base_location_prices ) { $item = $this->adjust_non_base_location_price( $item ); } } $item->subtotal = $item->price; if ( $this->calculate_tax && $item->product->is_taxable() ) { $item->subtotal_taxes = WC_Tax::calc_tax( $item->subtotal, $item->tax_rates, $item->price_includes_tax ); $item->subtotal_tax = array_sum( array_map( array( $this, 'round_line_tax' ), $item->subtotal_taxes ) ); if ( $item->price_includes_tax ) { // Use unrounded taxes so we can re-calculate from the orders screen accurately later. $item->subtotal = $item->subtotal - array_sum( $item->subtotal_taxes ); } foreach ( $item->subtotal_taxes as $rate_id => $rate ) { if ( ! isset( $merged_subtotal_taxes[ $rate_id ] ) ) { $merged_subtotal_taxes[ $rate_id ] = 0; } $merged_subtotal_taxes[ $rate_id ] += $this->round_line_tax( $rate ); } } $this->cart->cart_contents[ $item_key ]['line_tax_data'] = array( 'subtotal' => wc_remove_number_precision_deep( $item->subtotal_taxes ) ); $this->cart->cart_contents[ $item_key ]['line_subtotal'] = wc_remove_number_precision( $item->subtotal ); $this->cart->cart_contents[ $item_key ]['line_subtotal_tax'] = wc_remove_number_precision( $item->subtotal_tax ); } $items_subtotal = $this->get_rounded_items_total( $this->get_values_for_total( 'subtotal' ) ); // Prices are not rounded here because they should already be rounded based on settings in `get_rounded_items_total` and in `round_line_tax` method calls. $this->set_total( 'items_subtotal', $items_subtotal ); $this->set_total( 'items_subtotal_tax', array_sum( $merged_subtotal_taxes ), 0 ); $this->cart->set_subtotal( $this->get_total( 'items_subtotal' ) ); $this->cart->set_subtotal_tax( $this->get_total( 'items_subtotal_tax' ) ); } /** * Calculate COUPON based discounts which change item prices. * * @since 3.2.0 * @uses WC_Discounts class. */ protected function calculate_discounts() { $this->get_coupons_from_cart(); $discounts = new WC_Discounts( $this->cart ); // Set items directly so the discounts class can see any tax adjustments made thus far using subtotals. $discounts->set_items( $this->items ); foreach ( $this->coupons as $coupon ) { $discounts->apply_coupon( $coupon ); } $coupon_discount_amounts = $discounts->get_discounts_by_coupon( true ); $coupon_discount_tax_amounts = array(); // See how much tax was 'discounted' per item and per coupon. if ( $this->calculate_tax ) { foreach ( $discounts->get_discounts( true ) as $coupon_code => $coupon_discounts ) { $coupon_discount_tax_amounts[ $coupon_code ] = 0; foreach ( $coupon_discounts as $item_key => $coupon_discount ) { $item = $this->items[ $item_key ]; if ( $item->product->is_taxable() ) { // Item subtotals were sent, so set 3rd param. $item_tax = array_sum( WC_Tax::calc_tax( $coupon_discount, $item->tax_rates, $item->price_includes_tax ) ); // Sum total tax. $coupon_discount_tax_amounts[ $coupon_code ] += $item_tax; // Remove tax from discount total. if ( $item->price_includes_tax ) { $coupon_discount_amounts[ $coupon_code ] -= $item_tax; } } } } } $this->coupon_discount_totals = (array) $discounts->get_discounts_by_item( true ); $this->coupon_discount_tax_totals = $coupon_discount_tax_amounts; if ( wc_prices_include_tax() ) { $this->set_total( 'discounts_total', array_sum( $this->coupon_discount_totals ) - array_sum( $this->coupon_discount_tax_totals ) ); $this->set_total( 'discounts_tax_total', array_sum( $this->coupon_discount_tax_totals ) ); } else { $this->set_total( 'discounts_total', array_sum( $this->coupon_discount_totals ) ); $this->set_total( 'discounts_tax_total', array_sum( $this->coupon_discount_tax_totals ) ); } $this->cart->set_coupon_discount_totals( wc_remove_number_precision_deep( $coupon_discount_amounts ) ); $this->cart->set_coupon_discount_tax_totals( wc_remove_number_precision_deep( $coupon_discount_tax_amounts ) ); // Add totals to cart object. Note: Discount total for cart is excl tax. $this->cart->set_discount_total( $this->get_total( 'discounts_total' ) ); $this->cart->set_discount_tax( $this->get_total( 'discounts_tax_total' ) ); } /** * Triggers the cart fees API, grabs the list of fees, and calculates taxes. * * Note: This class sets the totals for the 'object' as they are calculated. This is so that APIs like the fees API can see these totals if needed. * * @since 3.2.0 */ protected function calculate_fee_totals() { $this->get_fees_from_cart(); $this->set_total( 'fees_total', array_sum( wp_list_pluck( $this->fees, 'total' ) ) ); $this->set_total( 'fees_total_tax', array_sum( wp_list_pluck( $this->fees, 'total_tax' ) ) ); $this->cart->fees_api()->set_fees( wp_list_pluck( $this->fees, 'object' ) ); $this->cart->set_fee_total( wc_remove_number_precision_deep( array_sum( wp_list_pluck( $this->fees, 'total' ) ) ) ); $this->cart->set_fee_tax( wc_remove_number_precision_deep( array_sum( wp_list_pluck( $this->fees, 'total_tax' ) ) ) ); $this->cart->set_fee_taxes( wc_remove_number_precision_deep( $this->combine_item_taxes( wp_list_pluck( $this->fees, 'taxes' ) ) ) ); } /** * Calculate any shipping taxes. * * @since 3.2.0 */ protected function calculate_shipping_totals() { $this->get_shipping_from_cart(); $this->set_total( 'shipping_total', array_sum( wp_list_pluck( $this->shipping, 'total' ) ) ); $this->set_total( 'shipping_tax_total', array_sum( wp_list_pluck( $this->shipping, 'total_tax' ) ) ); $this->cart->set_shipping_total( $this->get_total( 'shipping_total' ) ); $this->cart->set_shipping_tax( $this->get_total( 'shipping_tax_total' ) ); $this->cart->set_shipping_taxes( wc_remove_number_precision_deep( $this->combine_item_taxes( wp_list_pluck( $this->shipping, 'taxes' ) ) ) ); } /** * Main cart totals. * * @since 3.2.0 */ protected function calculate_totals() { $this->set_total( 'total', NumberUtil::round( $this->get_total( 'items_total', true ) + $this->get_total( 'fees_total', true ) + $this->get_total( 'shipping_total', true ) + array_sum( $this->get_merged_taxes( true ) ), 0 ) ); $items_tax = array_sum( $this->get_merged_taxes( false, array( 'items' ) ) ); // Shipping and fee taxes are rounded separately because they were entered excluding taxes (as opposed to item prices, which may or may not be including taxes depending upon settings). $shipping_and_fee_taxes = NumberUtil::round( array_sum( $this->get_merged_taxes( false, array( 'fees', 'shipping' ) ) ), wc_get_price_decimals() ); $this->cart->set_total_tax( $items_tax + $shipping_and_fee_taxes ); // Allow plugins to hook and alter totals before final total is calculated. if ( has_action( 'woocommerce_calculate_totals' ) ) { do_action( 'woocommerce_calculate_totals', $this->cart ); } // Allow plugins to filter the grand total, and sum the cart totals in case of modifications. $this->cart->set_total( max( 0, apply_filters( 'woocommerce_calculated_total', $this->get_total( 'total' ), $this->cart ) ) ); } } PKK[Žâ´ßE”E”wc-order-functions.phpnu„[µü¤ 'limit', 'post_type' => 'type', 'post_status' => 'status', 'post_parent' => 'parent', 'author' => 'customer', 'email' => 'billing_email', 'posts_per_page' => 'limit', 'paged' => 'page', ); foreach ( $map_legacy as $from => $to ) { if ( isset( $args[ $from ] ) ) { $args[ $to ] = $args[ $from ]; } } // Map legacy date args to modern date args. $date_before = false; $date_after = false; if ( ! empty( $args['date_before'] ) ) { $datetime = wc_string_to_datetime( $args['date_before'] ); $date_before = strpos( $args['date_before'], ':' ) ? $datetime->getOffsetTimestamp() : $datetime->date( 'Y-m-d' ); } if ( ! empty( $args['date_after'] ) ) { $datetime = wc_string_to_datetime( $args['date_after'] ); $date_after = strpos( $args['date_after'], ':' ) ? $datetime->getOffsetTimestamp() : $datetime->date( 'Y-m-d' ); } if ( $date_before && $date_after ) { $args['date_created'] = $date_after . '...' . $date_before; } elseif ( $date_before ) { $args['date_created'] = '<' . $date_before; } elseif ( $date_after ) { $args['date_created'] = '>' . $date_after; } $query = new WC_Order_Query( $args ); return $query->get_orders(); } /** * Main function for returning orders, uses the WC_Order_Factory class. * * @since 2.2 * * @param mixed $the_order Post object or post ID of the order. * * @return bool|WC_Order|WC_Order_Refund */ function wc_get_order( $the_order = false ) { if ( ! did_action( 'woocommerce_after_register_post_type' ) ) { wc_doing_it_wrong( __FUNCTION__, 'wc_get_order should not be called before post types are registered (woocommerce_after_register_post_type action)', '2.5' ); return false; } return WC()->order_factory->get_order( $the_order ); } /** * Get all order statuses. * * @since 2.2 * @used-by WC_Order::set_status * @return array */ function wc_get_order_statuses() { $order_statuses = array( 'wc-pending' => _x( 'Pending payment', 'Order status', 'woocommerce' ), 'wc-processing' => _x( 'Processing', 'Order status', 'woocommerce' ), 'wc-on-hold' => _x( 'On hold', 'Order status', 'woocommerce' ), 'wc-completed' => _x( 'Completed', 'Order status', 'woocommerce' ), 'wc-cancelled' => _x( 'Cancelled', 'Order status', 'woocommerce' ), 'wc-refunded' => _x( 'Refunded', 'Order status', 'woocommerce' ), 'wc-failed' => _x( 'Failed', 'Order status', 'woocommerce' ), ); return apply_filters( 'wc_order_statuses', $order_statuses ); } /** * See if a string is an order status. * * @param string $maybe_status Status, including any wc- prefix. * @return bool */ function wc_is_order_status( $maybe_status ) { $order_statuses = wc_get_order_statuses(); return isset( $order_statuses[ $maybe_status ] ); } /** * Get list of statuses which are consider 'paid'. * * @since 3.0.0 * @return array */ function wc_get_is_paid_statuses() { return apply_filters( 'woocommerce_order_is_paid_statuses', array( 'processing', 'completed' ) ); } /** * Get list of statuses which are consider 'pending payment'. * * @since 3.6.0 * @return array */ function wc_get_is_pending_statuses() { return apply_filters( 'woocommerce_order_is_pending_statuses', array( 'pending' ) ); } /** * Get the nice name for an order status. * * @since 2.2 * @param string $status Status. * @return string */ function wc_get_order_status_name( $status ) { $statuses = wc_get_order_statuses(); $status = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status; $status = isset( $statuses[ 'wc-' . $status ] ) ? $statuses[ 'wc-' . $status ] : $status; return $status; } /** * Generate an order key with prefix. * * @since 3.5.4 * @param string $key Order key without a prefix. By default generates a 13 digit secret. * @return string The order key. */ function wc_generate_order_key( $key = '' ) { if ( '' === $key ) { $key = wp_generate_password( 13, false ); } return 'wc_' . apply_filters( 'woocommerce_generate_order_key', 'order_' . $key ); } /** * Finds an Order ID based on an order key. * * @param string $order_key An order key has generated by. * @return int The ID of an order, or 0 if the order could not be found. */ function wc_get_order_id_by_order_key( $order_key ) { $data_store = WC_Data_Store::load( 'order' ); return $data_store->get_order_id_by_order_key( $order_key ); } /** * Get all registered order types. * * @since 2.2 * @param string $for Optionally define what you are getting order types for so * only relevant types are returned. * e.g. for 'order-meta-boxes', 'order-count'. * @return array */ function wc_get_order_types( $for = '' ) { global $wc_order_types; if ( ! is_array( $wc_order_types ) ) { $wc_order_types = array(); } $order_types = array(); switch ( $for ) { case 'order-count': foreach ( $wc_order_types as $type => $args ) { if ( ! $args['exclude_from_order_count'] ) { $order_types[] = $type; } } break; case 'order-meta-boxes': foreach ( $wc_order_types as $type => $args ) { if ( $args['add_order_meta_boxes'] ) { $order_types[] = $type; } } break; case 'view-orders': foreach ( $wc_order_types as $type => $args ) { if ( ! $args['exclude_from_order_views'] ) { $order_types[] = $type; } } break; case 'reports': foreach ( $wc_order_types as $type => $args ) { if ( ! $args['exclude_from_order_reports'] ) { $order_types[] = $type; } } break; case 'sales-reports': foreach ( $wc_order_types as $type => $args ) { if ( ! $args['exclude_from_order_sales_reports'] ) { $order_types[] = $type; } } break; case 'order-webhooks': foreach ( $wc_order_types as $type => $args ) { if ( ! $args['exclude_from_order_webhooks'] ) { $order_types[] = $type; } } break; case 'cot-migration': foreach ( $wc_order_types as $type => $args ) { if ( DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE !== $type ) { $order_types[] = $type; } } break; case 'admin-menu': $order_types = array_intersect( array_keys( $wc_order_types ), get_post_types( array( 'show_ui' => true, 'show_in_menu' => 'woocommerce', ) ) ); break; default: $order_types = array_keys( $wc_order_types ); break; } return apply_filters( 'wc_order_types', $order_types, $for ); } /** * Get an order type by post type name. * * @param string $type Post type name. * @return bool|array Details about the order type. */ function wc_get_order_type( $type ) { global $wc_order_types; if ( isset( $wc_order_types[ $type ] ) ) { return $wc_order_types[ $type ]; } return false; } /** * Register order type. Do not use before init. * * Wrapper for register post type, as well as a method of telling WC which. * post types are types of orders, and having them treated as such. * * $args are passed to register_post_type, but there are a few specific to this function: * - exclude_from_orders_screen (bool) Whether or not this order type also get shown in the main. * orders screen. * - add_order_meta_boxes (bool) Whether or not the order type gets shop_order meta boxes. * - exclude_from_order_count (bool) Whether or not this order type is excluded from counts. * - exclude_from_order_views (bool) Whether or not this order type is visible by customers when. * viewing orders e.g. on the my account page. * - exclude_from_order_reports (bool) Whether or not to exclude this type from core reports. * - exclude_from_order_sales_reports (bool) Whether or not to exclude this type from core sales reports. * * @since 2.2 * @see register_post_type for $args used in that function * @param string $type Post type. (max. 20 characters, can not contain capital letters or spaces). * @param array $args An array of arguments. * @return bool Success or failure */ function wc_register_order_type( $type, $args = array() ) { if ( post_type_exists( $type ) ) { return false; } global $wc_order_types; if ( ! is_array( $wc_order_types ) ) { $wc_order_types = array(); } // Register as a post type. if ( is_wp_error( register_post_type( $type, $args ) ) ) { return false; } // Register for WC usage. $order_type_args = array( 'exclude_from_orders_screen' => false, 'add_order_meta_boxes' => true, 'exclude_from_order_count' => false, 'exclude_from_order_views' => false, 'exclude_from_order_webhooks' => false, 'exclude_from_order_reports' => false, 'exclude_from_order_sales_reports' => false, 'class_name' => 'WC_Order', ); $args = array_intersect_key( $args, $order_type_args ); $args = wp_parse_args( $args, $order_type_args ); $wc_order_types[ $type ] = $args; return true; } /** * Return the count of processing orders. * * @return int */ function wc_processing_order_count() { return wc_orders_count( 'processing' ); } /** * Return the orders count of a specific order status. * * @param string $status Status. * @param string $type (Optional) Order type. Leave empty to include all 'for order-count' order types. @{see wc_get_order_types()}. * @return int */ function wc_orders_count( $status, string $type = '' ) { $count = 0; $legacy_statuses = array( 'draft', 'trash' ); $valid_statuses = array_merge( array_keys( wc_get_order_statuses() ), $legacy_statuses ); $status = ( ! in_array( $status, $legacy_statuses, true ) && 0 !== strpos( $status, 'wc-' ) ) ? 'wc-' . $status : $status; $valid_types = wc_get_order_types( 'order-count' ); $type = trim( $type ); if ( ! in_array( $status, $valid_statuses, true ) || ( $type && ! in_array( $type, $valid_types, true ) ) ) { return 0; } $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . $status . $type; $cached_count = wp_cache_get( $cache_key, 'counts' ); if ( false !== $cached_count ) { return $cached_count; } $types_for_count = $type ? array( $type ) : $valid_types; foreach ( $types_for_count as $type ) { $data_store = WC_Data_Store::load( 'shop_order' === $type ? 'order' : $type ); if ( $data_store ) { $count += $data_store->get_order_count( $status ); } } wp_cache_set( $cache_key, $count, 'counts' ); return $count; } /** * Grant downloadable product access to the file identified by $download_id. * * @param string $download_id File identifier. * @param int|WC_Product $product Product instance or ID. * @param WC_Order $order Order data. * @param int $qty Quantity purchased. * @param WC_Order_Item $item Item of the order. * @return int|bool insert id or false on failure. */ function wc_downloadable_file_permission( $download_id, $product, $order, $qty = 1, $item = null ) { if ( is_numeric( $product ) ) { $product = wc_get_product( $product ); } $download = new WC_Customer_Download(); $download->set_download_id( $download_id ); $download->set_product_id( $product->get_id() ); $download->set_user_id( $order->get_customer_id() ); $download->set_order_id( $order->get_id() ); $download->set_user_email( $order->get_billing_email() ); $download->set_order_key( $order->get_order_key() ); $download->set_downloads_remaining( 0 > $product->get_download_limit() ? '' : $product->get_download_limit() * $qty ); $download->set_access_granted( time() ); $download->set_download_count( 0 ); $expiry = $product->get_download_expiry(); if ( $expiry > 0 ) { $from_date = $order->get_date_completed() ? $order->get_date_completed()->format( 'Y-m-d' ) : current_time( 'mysql', true ); $download->set_access_expires( strtotime( $from_date . ' + ' . $expiry . ' DAY' ) ); } $download = apply_filters( 'woocommerce_downloadable_file_permission', $download, $product, $order, $qty, $item ); return $download->save(); } /** * Order Status completed - give downloadable product access to customer. * * @param int $order_id Order ID. * @param bool $force Force downloadable permissions. */ function wc_downloadable_product_permissions( $order_id, $force = false ) { $order = wc_get_order( $order_id ); if ( ! $order || ( $order->get_data_store()->get_download_permissions_granted( $order ) && ! $force ) ) { return; } if ( $order->has_status( 'processing' ) && 'no' === get_option( 'woocommerce_downloads_grant_access_after_payment' ) ) { return; } if ( count( $order->get_items() ) > 0 ) { foreach ( $order->get_items() as $item ) { $product = $item->get_product(); if ( $product && $product->exists() && $product->is_downloadable() ) { $downloads = $product->get_downloads(); foreach ( array_keys( $downloads ) as $download_id ) { wc_downloadable_file_permission( $download_id, $product, $order, $item->get_quantity(), $item ); } } } } $order->get_data_store()->set_download_permissions_granted( $order, true ); do_action( 'woocommerce_grant_product_download_permissions', $order_id ); } add_action( 'woocommerce_order_status_completed', 'wc_downloadable_product_permissions' ); add_action( 'woocommerce_order_status_processing', 'wc_downloadable_product_permissions' ); /** * Clear all transients cache for order data. * * @param int|WC_Order $order Order instance or ID. */ function wc_delete_shop_order_transients( $order = 0 ) { if ( is_numeric( $order ) ) { $order = wc_get_order( $order ); } $reports = WC_Admin_Reports::get_reports(); $transients_to_clear = array( 'wc_admin_report', ); foreach ( $reports as $report_group ) { foreach ( $report_group['reports'] as $report_key => $report ) { $transients_to_clear[] = 'wc_report_' . $report_key; } } foreach ( $transients_to_clear as $transient ) { delete_transient( $transient ); } // Clear customer's order related caches. if ( is_a( $order, 'WC_Order' ) ) { $order_id = $order->get_id(); delete_user_meta( $order->get_customer_id(), '_money_spent' ); delete_user_meta( $order->get_customer_id(), '_order_count' ); delete_user_meta( $order->get_customer_id(), '_last_order' ); } else { $order_id = 0; } // Increments the transient version to invalidate cache. WC_Cache_Helper::get_transient_version( 'orders', true ); // Do the same for regular cache. WC_Cache_Helper::invalidate_cache_group( 'orders' ); do_action( 'woocommerce_delete_shop_order_transients', $order_id ); } /** * See if we only ship to billing addresses. * * @return bool */ function wc_ship_to_billing_address_only() { return 'billing_only' === get_option( 'woocommerce_ship_to_destination' ); } /** * Create a new order refund programmatically. * * Returns a new refund object on success which can then be used to add additional data. * * @since 2.2 * @throws Exception Throws exceptions when fail to create, but returns WP_Error instead. * @param array $args New refund arguments. * @return WC_Order_Refund|WP_Error */ function wc_create_refund( $args = array() ) { $default_args = array( 'amount' => 0, 'reason' => null, 'order_id' => 0, 'refund_id' => 0, 'line_items' => array(), 'refund_payment' => false, 'restock_items' => false, ); try { $args = wp_parse_args( $args, $default_args ); $order = wc_get_order( $args['order_id'] ); if ( ! $order ) { throw new Exception( __( 'Invalid order ID.', 'woocommerce' ) ); } $remaining_refund_amount = $order->get_remaining_refund_amount(); $remaining_refund_items = $order->get_remaining_refund_items(); $refund_item_count = 0; $refund = new WC_Order_Refund( $args['refund_id'] ); if ( 0 > $args['amount'] || $args['amount'] > $remaining_refund_amount ) { throw new Exception( __( 'Invalid refund amount.', 'woocommerce' ) ); } $refund->set_currency( $order->get_currency() ); $refund->set_amount( $args['amount'] ); $refund->set_parent_id( absint( $args['order_id'] ) ); $refund->set_refunded_by( get_current_user_id() ? get_current_user_id() : 1 ); $refund->set_prices_include_tax( $order->get_prices_include_tax() ); if ( ! is_null( $args['reason'] ) ) { $refund->set_reason( $args['reason'] ); } // Negative line items. if ( count( $args['line_items'] ) > 0 ) { $items = $order->get_items( array( 'line_item', 'fee', 'shipping' ) ); foreach ( $items as $item_id => $item ) { if ( ! isset( $args['line_items'][ $item_id ] ) ) { continue; } $qty = isset( $args['line_items'][ $item_id ]['qty'] ) ? $args['line_items'][ $item_id ]['qty'] : 0; $refund_total = $args['line_items'][ $item_id ]['refund_total']; $refund_tax = isset( $args['line_items'][ $item_id ]['refund_tax'] ) ? array_filter( (array) $args['line_items'][ $item_id ]['refund_tax'] ) : array(); if ( empty( $qty ) && empty( $refund_total ) && empty( $args['line_items'][ $item_id ]['refund_tax'] ) ) { continue; } $class = get_class( $item ); $refunded_item = new $class( $item ); $refunded_item->set_id( 0 ); $refunded_item->add_meta_data( '_refunded_item_id', $item_id, true ); $refunded_item->set_total( wc_format_refund_total( $refund_total ) ); $refunded_item->set_taxes( array( 'total' => array_map( 'wc_format_refund_total', $refund_tax ), 'subtotal' => array_map( 'wc_format_refund_total', $refund_tax ), ) ); if ( is_callable( array( $refunded_item, 'set_subtotal' ) ) ) { $refunded_item->set_subtotal( wc_format_refund_total( $refund_total ) ); } if ( is_callable( array( $refunded_item, 'set_quantity' ) ) ) { $refunded_item->set_quantity( $qty * -1 ); } $refund->add_item( $refunded_item ); $refund_item_count += $qty; } } $refund->update_taxes(); $refund->calculate_totals( false ); $refund->set_total( $args['amount'] * -1 ); // this should remain after update_taxes(), as this will save the order, and write the current date to the db // so we must wait until the order is persisted to set the date. if ( isset( $args['date_created'] ) ) { $refund->set_date_created( $args['date_created'] ); } /** * Action hook to adjust refund before save. * * @since 3.0.0 */ do_action( 'woocommerce_create_refund', $refund, $args ); if ( $refund->save() ) { if ( $args['refund_payment'] ) { $result = wc_refund_payment( $order, $refund->get_amount(), $refund->get_reason() ); if ( is_wp_error( $result ) ) { $refund->delete(); return $result; } $refund->set_refunded_payment( true ); $refund->save(); } if ( $args['restock_items'] ) { wc_restock_refunded_items( $order, $args['line_items'] ); } /** * Trigger notification emails. * * Filter hook to modify the partially-refunded status conditions. * * @since 6.7.0 * * @param bool $is_partially_refunded Whether the order is partially refunded. * @param int $order_id The order id. * @param int $refund_id The refund id. */ if ( (bool) apply_filters( 'woocommerce_order_is_partially_refunded', ( $remaining_refund_amount - $args['amount'] ) > 0 || ( $order->has_free_item() && ( $remaining_refund_items - $refund_item_count ) > 0 ), $order->get_id(), $refund->get_id() ) ) { do_action( 'woocommerce_order_partially_refunded', $order->get_id(), $refund->get_id() ); } else { do_action( 'woocommerce_order_fully_refunded', $order->get_id(), $refund->get_id() ); $parent_status = apply_filters( 'woocommerce_order_fully_refunded_status', 'refunded', $order->get_id(), $refund->get_id() ); if ( $parent_status ) { $order->update_status( $parent_status ); } } } $order->set_date_modified( time() ); $order->save(); do_action( 'woocommerce_refund_created', $refund->get_id(), $args ); do_action( 'woocommerce_order_refunded', $order->get_id(), $refund->get_id() ); } catch ( Exception $e ) { if ( isset( $refund ) && is_a( $refund, 'WC_Order_Refund' ) ) { $refund->delete( true ); } return new WP_Error( 'error', $e->getMessage() ); } return $refund; } /** * Try to refund the payment for an order via the gateway. * * @since 3.0.0 * @throws Exception Throws exceptions when fail to refund, but returns WP_Error instead. * @param WC_Order $order Order instance. * @param string $amount Amount to refund. * @param string $reason Refund reason. * @return bool|WP_Error */ function wc_refund_payment( $order, $amount, $reason = '' ) { try { if ( ! is_a( $order, 'WC_Order' ) ) { throw new Exception( __( 'Invalid order.', 'woocommerce' ) ); } $gateway_controller = WC_Payment_Gateways::instance(); $all_gateways = $gateway_controller->payment_gateways(); $payment_method = $order->get_payment_method(); $gateway = isset( $all_gateways[ $payment_method ] ) ? $all_gateways[ $payment_method ] : false; if ( ! $gateway ) { throw new Exception( __( 'The payment gateway for this order does not exist.', 'woocommerce' ) ); } if ( ! $gateway->supports( 'refunds' ) ) { throw new Exception( __( 'The payment gateway for this order does not support automatic refunds.', 'woocommerce' ) ); } $result = $gateway->process_refund( $order->get_id(), $amount, $reason ); if ( ! $result ) { throw new Exception( __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ) ); } if ( is_wp_error( $result ) ) { throw new Exception( $result->get_error_message() ); } return true; } catch ( Exception $e ) { return new WP_Error( 'error', $e->getMessage() ); } } /** * Restock items during refund. * * @since 3.0.0 * @param WC_Order $order Order instance. * @param array $refunded_line_items Refunded items list. */ function wc_restock_refunded_items( $order, $refunded_line_items ) { if ( ! apply_filters( 'woocommerce_can_restock_refunded_items', true, $order, $refunded_line_items ) ) { return; } $line_items = $order->get_items(); foreach ( $line_items as $item_id => $item ) { if ( ! isset( $refunded_line_items[ $item_id ], $refunded_line_items[ $item_id ]['qty'] ) ) { continue; } $product = $item->get_product(); $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); $restock_refunded_items = (int) $item->get_meta( '_restock_refunded_items', true ); $qty_to_refund = $refunded_line_items[ $item_id ]['qty']; if ( ! $item_stock_reduced || ! $qty_to_refund || ! $product || ! $product->managing_stock() ) { continue; } $old_stock = $product->get_stock_quantity(); $new_stock = wc_update_product_stock( $product, $qty_to_refund, 'increase' ); // Update _reduced_stock meta to track changes. $item_stock_reduced = $item_stock_reduced - $qty_to_refund; // Keeps track of total running tally of reduced stock. $item->update_meta_data( '_reduced_stock', $item_stock_reduced ); // Keeps track of only refunded items that needs restock. $item->update_meta_data( '_restock_refunded_items', $qty_to_refund + $restock_refunded_items ); /* translators: 1: product ID 2: old stock level 3: new stock level */ $restock_note = sprintf( __( 'Item #%1$s stock increased from %2$s to %3$s.', 'woocommerce' ), $product->get_id(), $old_stock, $new_stock ); /** * Allow the restock note to be modified. * * @since 6.4.0 * * @param string $restock_note The original note. * @param int $old_stock The old stock. * @param bool|int|null $new_stock The new stock. * @param WC_Order $order The order the refund was done for. * @param bool|WC_Product $product The product the refund was done for. */ $restock_note = apply_filters( 'woocommerce_refund_restock_note', $restock_note, $old_stock, $new_stock, $order, $product ); $order->add_order_note( $restock_note ); $item->save(); do_action( 'woocommerce_restock_refunded_item', $product->get_id(), $old_stock, $new_stock, $order, $product ); } } /** * Get tax class by tax id. * * @since 2.2 * @param int $tax_id Tax ID. * @return string */ function wc_get_tax_class_by_tax_id( $tax_id ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_class FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d", $tax_id ) ); } /** * Get payment gateway class by order data. * * @since 2.2 * @param int|WC_Order $order Order instance. * @return WC_Payment_Gateway|bool */ function wc_get_payment_gateway_by_order( $order ) { if ( WC()->payment_gateways() ) { $payment_gateways = WC()->payment_gateways()->payment_gateways(); } else { $payment_gateways = array(); } if ( ! is_object( $order ) ) { $order_id = absint( $order ); $order = wc_get_order( $order_id ); } return is_a( $order, 'WC_Order' ) && isset( $payment_gateways[ $order->get_payment_method() ] ) ? $payment_gateways[ $order->get_payment_method() ] : false; } /** * When refunding an order, create a refund line item if the partial refunds do not match order total. * * This is manual; no gateway refund will be performed. * * @since 2.4 * @param int $order_id Order ID. */ function wc_order_fully_refunded( $order_id ) { $order = wc_get_order( $order_id ); $max_refund = wc_format_decimal( $order->get_total() - $order->get_total_refunded() ); if ( ! $max_refund ) { return; } // Create the refund object. wc_switch_to_site_locale(); wc_create_refund( array( 'amount' => $max_refund, 'reason' => __( 'Order fully refunded.', 'woocommerce' ), 'order_id' => $order_id, 'line_items' => array(), ) ); wc_restore_locale(); $order->add_order_note( __( 'Order status set to refunded. To return funds to the customer you will need to issue a refund through your payment gateway.', 'woocommerce' ) ); } add_action( 'woocommerce_order_status_refunded', 'wc_order_fully_refunded' ); /** * Search orders. * * @since 2.6.0 * @param string $term Term to search. * @return array List of orders ID. */ function wc_order_search( $term ) { $data_store = WC_Data_Store::load( 'order' ); return $data_store->search_orders( str_replace( 'Order #', '', wc_clean( $term ) ) ); } /** * Update total sales amount for each product within a paid order. * * @since 3.0.0 * @param int $order_id Order ID. */ function wc_update_total_sales_counts( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return; } $recorded_sales = $order->get_data_store()->get_recorded_sales( $order ); $reflected_order = in_array( $order->get_status(), array( 'cancelled', 'trash' ), true ); if ( ! $reflected_order && 'woocommerce_before_delete_order' === current_action() ) { $reflected_order = true; } if ( $recorded_sales xor $reflected_order ) { return; } $operation = $recorded_sales && $reflected_order ? 'decrease' : 'increase'; if ( count( $order->get_items() ) > 0 ) { foreach ( $order->get_items() as $item ) { $product_id = $item->get_product_id(); if ( $product_id ) { $data_store = WC_Data_Store::load( 'product' ); $data_store->update_product_sales( $product_id, absint( $item->get_quantity() ), $operation ); } } } if ( 'decrease' === $operation ) { $order->get_data_store()->set_recorded_sales( $order, false ); } else { $order->get_data_store()->set_recorded_sales( $order, true ); } /** * Called when sales for an order are recorded * * @param int $order_id order id */ do_action( 'woocommerce_recorded_sales', $order_id ); } add_action( 'woocommerce_order_status_completed', 'wc_update_total_sales_counts' ); add_action( 'woocommerce_order_status_processing', 'wc_update_total_sales_counts' ); add_action( 'woocommerce_order_status_on-hold', 'wc_update_total_sales_counts' ); add_action( 'woocommerce_order_status_completed_to_cancelled', 'wc_update_total_sales_counts' ); add_action( 'woocommerce_order_status_processing_to_cancelled', 'wc_update_total_sales_counts' ); add_action( 'woocommerce_order_status_on-hold_to_cancelled', 'wc_update_total_sales_counts' ); add_action( 'woocommerce_trash_order', 'wc_update_total_sales_counts' ); add_action( 'woocommerce_untrash_order', 'wc_update_total_sales_counts' ); add_action( 'woocommerce_before_delete_order', 'wc_update_total_sales_counts' ); /** * Update used coupon amount for each coupon within an order. * * @since 3.0.0 * @param int $order_id Order ID. */ function wc_update_coupon_usage_counts( $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return; } $has_recorded = $order->get_data_store()->get_recorded_coupon_usage_counts( $order ); if ( $order->has_status( 'cancelled' ) && $has_recorded ) { $action = 'reduce'; $order->get_data_store()->set_recorded_coupon_usage_counts( $order, false ); } elseif ( ! $order->has_status( 'cancelled' ) && ! $has_recorded ) { $action = 'increase'; $order->get_data_store()->set_recorded_coupon_usage_counts( $order, true ); } elseif ( $order->has_status( 'cancelled' ) ) { $order->get_data_store()->release_held_coupons( $order, true ); return; } else { return; } if ( count( $order->get_coupon_codes() ) > 0 ) { foreach ( $order->get_coupon_codes() as $code ) { if ( StringUtil::is_null_or_whitespace( $code ) ) { continue; } $coupon = new WC_Coupon( $code ); $used_by = $order->get_user_id(); if ( ! $used_by ) { $used_by = $order->get_billing_email(); } switch ( $action ) { case 'reduce': $coupon->decrease_usage_count( $used_by ); break; case 'increase': $coupon->increase_usage_count( $used_by, $order ); break; } } $order->get_data_store()->release_held_coupons( $order, true ); } } add_action( 'woocommerce_order_status_pending', 'wc_update_coupon_usage_counts' ); add_action( 'woocommerce_order_status_completed', 'wc_update_coupon_usage_counts' ); add_action( 'woocommerce_order_status_processing', 'wc_update_coupon_usage_counts' ); add_action( 'woocommerce_order_status_on-hold', 'wc_update_coupon_usage_counts' ); add_action( 'woocommerce_order_status_cancelled', 'wc_update_coupon_usage_counts' ); /** * Cancel all unpaid orders after held duration to prevent stock lock for those products. */ function wc_cancel_unpaid_orders() { $held_duration = get_option( 'woocommerce_hold_stock_minutes' ); // Re-schedule the event before cancelling orders // this way in case of a DB timeout or (plugin) crash the event is always scheduled for retry. wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) ); wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); if ( $held_duration < 1 || 'yes' !== get_option( 'woocommerce_manage_stock' ) ) { return; } $data_store = WC_Data_Store::load( 'order' ); $unpaid_orders = $data_store->get_unpaid_orders( strtotime( '-' . absint( $held_duration ) . ' MINUTES', current_time( 'timestamp' ) ) ); if ( $unpaid_orders ) { foreach ( $unpaid_orders as $unpaid_order ) { $order = wc_get_order( $unpaid_order ); if ( apply_filters( 'woocommerce_cancel_unpaid_order', 'checkout' === $order->get_created_via(), $order ) ) { $order->update_status( 'cancelled', __( 'Unpaid order cancelled - time limit reached.', 'woocommerce' ) ); } } } } add_action( 'woocommerce_cancel_unpaid_orders', 'wc_cancel_unpaid_orders' ); /** * Sanitize order id removing unwanted characters. * * E.g Users can sometimes try to track an order id using # with no success. * This function will fix this. * * @since 3.1.0 * @param int $order_id Order ID. */ function wc_sanitize_order_id( $order_id ) { return (int) filter_var( $order_id, FILTER_SANITIZE_NUMBER_INT ); } add_filter( 'woocommerce_shortcode_order_tracking_order_id', 'wc_sanitize_order_id' ); /** * Get an order note. * * @since 3.2.0 * @param int|WP_Comment $data Note ID (or WP_Comment instance for internal use only). * @return stdClass|null Object with order note details or null when does not exists. */ function wc_get_order_note( $data ) { if ( is_numeric( $data ) ) { $data = get_comment( $data ); } if ( ! is_a( $data, 'WP_Comment' ) ) { return null; } return (object) apply_filters( 'woocommerce_get_order_note', array( 'id' => (int) $data->comment_ID, 'date_created' => wc_string_to_datetime( $data->comment_date ), 'content' => $data->comment_content, 'customer_note' => (bool) get_comment_meta( $data->comment_ID, 'is_customer_note', true ), 'added_by' => __( 'WooCommerce', 'woocommerce' ) === $data->comment_author ? 'system' : $data->comment_author, ), $data ); } /** * Get order notes. * * @since 3.2.0 * @param array $args Query arguments { * Array of query parameters. * * @type string $limit Maximum number of notes to retrieve. * Default empty (no limit). * @type int $order_id Limit results to those affiliated with a given order ID. * Default 0. * @type array $order__in Array of order IDs to include affiliated notes for. * Default empty. * @type array $order__not_in Array of order IDs to exclude affiliated notes for. * Default empty. * @type string $orderby Define how should sort notes. * Accepts 'date_created', 'date_created_gmt' or 'id'. * Default: 'id'. * @type string $order How to order retrieved notes. * Accepts 'ASC' or 'DESC'. * Default: 'DESC'. * @type string $type Define what type of note should retrieve. * Accepts 'customer', 'internal' or empty for both. * Default empty. * } * @return stdClass[] Array of stdClass objects with order notes details. */ function wc_get_order_notes( $args ) { $key_mapping = array( 'limit' => 'number', 'order_id' => 'post_id', 'order__in' => 'post__in', 'order__not_in' => 'post__not_in', ); foreach ( $key_mapping as $query_key => $db_key ) { if ( isset( $args[ $query_key ] ) ) { $args[ $db_key ] = $args[ $query_key ]; unset( $args[ $query_key ] ); } } // Define orderby. $orderby_mapping = array( 'date_created' => 'comment_date', 'date_created_gmt' => 'comment_date_gmt', 'id' => 'comment_ID', ); $args['orderby'] = ! empty( $args['orderby'] ) && in_array( $args['orderby'], array( 'date_created', 'date_created_gmt', 'id' ), true ) ? $orderby_mapping[ $args['orderby'] ] : 'comment_ID'; // Set WooCommerce order type. if ( isset( $args['type'] ) && 'customer' === $args['type'] ) { $args['meta_query'] = array( // WPCS: slow query ok. array( 'key' => 'is_customer_note', 'value' => 1, 'compare' => '=', ), ); } elseif ( isset( $args['type'] ) && 'internal' === $args['type'] ) { $args['meta_query'] = array( // WPCS: slow query ok. array( 'key' => 'is_customer_note', 'compare' => 'NOT EXISTS', ), ); } // Set correct comment type. $args['type'] = 'order_note'; // Always approved. $args['status'] = 'approve'; // Does not support 'count' or 'fields'. unset( $args['count'], $args['fields'] ); remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); $notes = get_comments( $args ); add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); return array_filter( array_map( 'wc_get_order_note', $notes ) ); } /** * Create an order note. * * @since 3.2.0 * @param int $order_id Order ID. * @param string $note Note to add. * @param bool $is_customer_note If is a costumer note. * @param bool $added_by_user If note is create by an user. * @return int|WP_Error Integer when created or WP_Error when found an error. */ function wc_create_order_note( $order_id, $note, $is_customer_note = false, $added_by_user = false ) { $order = wc_get_order( $order_id ); if ( ! $order ) { return new WP_Error( 'invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 400 ) ); } return $order->add_order_note( $note, (int) $is_customer_note, $added_by_user ); } /** * Delete an order note. * * @since 3.2.0 * @param int $note_id Order note. * @return bool True on success, false on failure. */ function wc_delete_order_note( $note_id ) { return wp_delete_comment( $note_id, true ); } PKK[\,iTTclass-wc-product-factory.phpnu„[µü¤get_product_id( $product_id ); if ( ! $product_id ) { return false; } $product_type = $this->get_product_type( $product_id ); // Backwards compatibility. if ( ! empty( $deprecated ) ) { wc_deprecated_argument( 'args', '3.0', 'Passing args to the product factory is deprecated. If you need to force a type, construct the product class directly.' ); if ( isset( $deprecated['product_type'] ) ) { $product_type = $this->get_classname_from_product_type( $deprecated['product_type'] ); } } $classname = $this->get_product_classname( $product_id, $product_type ); try { return new $classname( $product_id, $deprecated ); } catch ( Exception $e ) { return false; } } /** * Gets a product classname and allows filtering. Returns WC_Product_Simple if the class does not exist. * * @since 3.0.0 * @param int $product_id Product ID. * @param string $product_type Product type. * @return string */ public static function get_product_classname( $product_id, $product_type ) { $classname = apply_filters( 'woocommerce_product_class', self::get_classname_from_product_type( $product_type ), $product_type, 'variation' === $product_type ? 'product_variation' : 'product', $product_id ); if ( ! $classname || ! class_exists( $classname ) ) { $classname = 'WC_Product_Simple'; } return $classname; } /** * Get the product type for a product. * * @since 3.0.0 * @param int $product_id Product ID. * @return string|false */ public static function get_product_type( $product_id ) { // Allow the overriding of the lookup in this function. Return the product type here. $override = apply_filters( 'woocommerce_product_type_query', false, $product_id ); if ( ! $override ) { return WC_Data_Store::load( 'product' )->get_product_type( $product_id ); } else { return $override; } } /** * Create a WC coding standards compliant class name e.g. WC_Product_Type_Class instead of WC_Product_type-class. * * @param string $product_type Product type. * @return string|false */ public static function get_classname_from_product_type( $product_type ) { return $product_type ? 'WC_Product_' . implode( '_', array_map( 'ucfirst', explode( '-', $product_type ) ) ) : false; } /** * Get the product ID depending on what was passed. * * @since 3.0.0 * @param WC_Product|WP_Post|int|bool $product Product instance, post instance, numeric or false to use global $post. * @return int|bool false on failure */ private function get_product_id( $product ) { global $post; if ( false === $product && isset( $post, $post->ID ) && 'product' === get_post_type( $post->ID ) ) { return absint( $post->ID ); } elseif ( is_numeric( $product ) ) { return $product; } elseif ( $product instanceof WC_Product ) { return $product->get_id(); } elseif ( ! empty( $product->ID ) ) { return $product->ID; } else { return false; } } } PKK[¶3cóó&class-wc-regenerate-images-request.phpnu„[µü¤prefix = 'wp_' . get_current_blog_id(); $this->action = 'wc_regenerate_images'; // Limit Imagick to only use 1 thread to avoid memory issues with OpenMP. if ( extension_loaded( 'imagick' ) && method_exists( Imagick::class, 'setResourceLimit' ) ) { if ( defined( 'Imagick::RESOURCETYPE_THREAD' ) ) { Imagick::setResourceLimit( Imagick::RESOURCETYPE_THREAD, 1 ); } else { Imagick::setResourceLimit( 6, 1 ); } } parent::__construct(); } /** * Is job running? * * @return boolean */ public function is_running() { return $this->is_queue_empty(); } /** * Limit each task ran per batch to 1 for image regen. * * @return bool */ protected function batch_limit_exceeded() { return true; } /** * Determines whether an attachment can have its thumbnails regenerated. * * Adapted from Regenerate Thumbnails by Alex Mills. * * @param WP_Post $attachment An attachment's post object. * @return bool Whether the given attachment can have its thumbnails regenerated. */ protected function is_regeneratable( $attachment ) { if ( 'site-icon' === get_post_meta( $attachment->ID, '_wp_attachment_context', true ) ) { return false; } if ( wp_attachment_is_image( $attachment ) ) { return true; } return false; } /** * Code to execute for each item in the queue * * @param mixed $item Queue item to iterate over. * @return bool */ protected function task( $item ) { if ( ! is_array( $item ) && ! isset( $item['attachment_id'] ) ) { return false; } $this->attachment_id = absint( $item['attachment_id'] ); $attachment = get_post( $this->attachment_id ); if ( ! $attachment || 'attachment' !== $attachment->post_type || ! $this->is_regeneratable( $attachment ) ) { return false; } if ( ! function_exists( 'wp_crop_image' ) ) { include ABSPATH . 'wp-admin/includes/image.php'; } $log = wc_get_logger(); $log->info( sprintf( // translators: %s: ID of the attachment. __( 'Regenerating images for attachment ID: %s', 'woocommerce' ), $this->attachment_id ), array( 'source' => 'wc-image-regeneration', ) ); $fullsizepath = get_attached_file( $this->attachment_id ); // Check if the file exists, if not just remove item from queue. if ( false === $fullsizepath || is_wp_error( $fullsizepath ) || ! file_exists( $fullsizepath ) ) { return false; } $old_metadata = wp_get_attachment_metadata( $this->attachment_id ); // We only want to regen WC images. add_filter( 'intermediate_image_sizes', array( $this, 'adjust_intermediate_image_sizes' ) ); // We only want to resize images if they do not already exist. add_filter( 'intermediate_image_sizes_advanced', array( $this, 'filter_image_sizes_to_only_missing_thumbnails' ), 10, 3 ); // This function will generate the new image sizes. $new_metadata = wp_generate_attachment_metadata( $this->attachment_id, $fullsizepath ); // Remove custom filters. remove_filter( 'intermediate_image_sizes', array( $this, 'adjust_intermediate_image_sizes' ) ); remove_filter( 'intermediate_image_sizes_advanced', array( $this, 'filter_image_sizes_to_only_missing_thumbnails' ), 10, 3 ); // If something went wrong lets just remove the item from the queue. if ( is_wp_error( $new_metadata ) || empty( $new_metadata ) ) { return false; } if ( ! empty( $old_metadata ) && ! empty( $old_metadata['sizes'] ) && is_array( $old_metadata['sizes'] ) ) { foreach ( $old_metadata['sizes'] as $old_size => $old_size_data ) { if ( empty( $new_metadata['sizes'][ $old_size ] ) ) { $new_metadata['sizes'][ $old_size ] = $old_metadata['sizes'][ $old_size ]; } } } // Update the meta data with the new size values. wp_update_attachment_metadata( $this->attachment_id, $new_metadata ); // We made it till the end, now lets remove the item from the queue. return false; } /** * Filters the list of thumbnail sizes to only include those which have missing files. * * @param array $sizes An associative array of registered thumbnail image sizes. * @param array $metadata An associative array of fullsize image metadata: width, height, file. * @param int $attachment_id Attachment ID. Only passed from WP 5.0+. * @return array An associative array of image sizes. */ public function filter_image_sizes_to_only_missing_thumbnails( $sizes, $metadata, $attachment_id = null ) { $attachment_id = is_null( $attachment_id ) ? $this->attachment_id : $attachment_id; if ( ! $sizes || ! $attachment_id ) { return $sizes; } $fullsizepath = get_attached_file( $attachment_id ); $editor = wp_get_image_editor( $fullsizepath ); if ( is_wp_error( $editor ) ) { return $sizes; } $metadata = wp_get_attachment_metadata( $attachment_id ); // This is based on WP_Image_Editor_GD::multi_resize() and others. foreach ( $sizes as $size => $size_data ) { if ( empty( $metadata['sizes'][ $size ] ) ) { continue; } if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) { continue; } if ( ! isset( $size_data['width'] ) ) { $size_data['width'] = null; } if ( ! isset( $size_data['height'] ) ) { $size_data['height'] = null; } if ( ! isset( $size_data['crop'] ) ) { $size_data['crop'] = false; } $image_sizes = getimagesize( $fullsizepath ); if ( false === $image_sizes ) { continue; } list( $orig_w, $orig_h ) = $image_sizes; $dimensions = image_resize_dimensions( $orig_w, $orig_h, $size_data['width'], $size_data['height'], $size_data['crop'] ); if ( ! $dimensions || ! is_array( $dimensions ) ) { continue; } $info = pathinfo( $fullsizepath ); $ext = $info['extension']; $dst_w = $dimensions[4]; $dst_h = $dimensions[5]; $suffix = "{$dst_w}x{$dst_h}"; $dst_rel_path = str_replace( '.' . $ext, '', $fullsizepath ); $thumbnail = "{$dst_rel_path}-{$suffix}.{$ext}"; if ( $dst_w === $metadata['sizes'][ $size ]['width'] && $dst_h === $metadata['sizes'][ $size ]['height'] && file_exists( $thumbnail ) ) { unset( $sizes[ $size ] ); } } return $sizes; } /** * Returns the sizes we want to regenerate. * * @param array $sizes Sizes to generate. * @return array */ public function adjust_intermediate_image_sizes( $sizes ) { // Prevent a filter loop. $unfiltered_sizes = array( 'woocommerce_thumbnail', 'woocommerce_gallery_thumbnail', 'woocommerce_single' ); static $in_filter = false; if ( $in_filter ) { return $unfiltered_sizes; } $in_filter = true; $filtered_sizes = apply_filters( 'woocommerce_regenerate_images_intermediate_image_sizes', $unfiltered_sizes ); $in_filter = false; return $filtered_sizes; } /** * This runs once the job has completed all items on the queue. * * @return void */ protected function complete() { parent::complete(); $log = wc_get_logger(); $log->info( __( 'Completed product image regeneration job.', 'woocommerce' ), array( 'source' => 'wc-image-regeneration', ) ); } } PKK[³aöΫ«'class-wc-privacy-background-process.phpnu„[µü¤prefix = 'wp_' . get_current_blog_id(); $this->action = 'wc_privacy_cleanup'; parent::__construct(); } /** * Code to execute for each item in the queue * * @param string $item Queue item to iterate over. * @return bool */ protected function task( $item ) { if ( ! $item || empty( $item['task'] ) ) { return false; } $process_count = 0; $process_limit = 20; switch ( $item['task'] ) { case 'trash_pending_orders': $process_count = WC_Privacy::trash_pending_orders( $process_limit ); break; case 'trash_failed_orders': $process_count = WC_Privacy::trash_failed_orders( $process_limit ); break; case 'trash_cancelled_orders': $process_count = WC_Privacy::trash_cancelled_orders( $process_limit ); break; case 'anonymize_completed_orders': $process_count = WC_Privacy::anonymize_completed_orders( $process_limit ); break; case 'delete_inactive_accounts': $process_count = WC_Privacy::delete_inactive_accounts( $process_limit ); break; } if ( $process_limit === $process_count ) { // Needs to run again. return $item; } return false; } } PKK[š£íº··Rintegrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.phpnu„[µü¤database_prefix = $database_prefix; } /** * Fetches the path that the database should be stored. * * @return string The local database path. */ public function get_database_path() { $uploads_dir = wp_upload_dir(); $database_path = trailingslashit( $uploads_dir['basedir'] ) . 'woocommerce_uploads/'; if ( ! empty( $this->database_prefix ) ) { $database_path .= $this->database_prefix . '-'; } $database_path .= self::DATABASE . self::DATABASE_EXTENSION; /** * Filter the geolocation database storage path. * * @param string $database_path The path to the database. * @param int $version Deprecated since 3.4.0. * @deprecated 3.9.0 */ $database_path = apply_filters_deprecated( 'woocommerce_geolocation_local_database_path', array( $database_path, 2 ), '3.9.0', 'woocommerce_maxmind_geolocation_database_path' ); /** * Filter the geolocation database storage path. * * @since 3.9.0 * @param string $database_path The path to the database. */ return apply_filters( 'woocommerce_maxmind_geolocation_database_path', $database_path ); } /** * Fetches the database from the MaxMind service. * * @param string $license_key The license key to be used when downloading the database. * @return string|WP_Error The path to the database file or an error if invalid. */ public function download_database( $license_key ) { $download_uri = add_query_arg( array( 'edition_id' => self::DATABASE, 'license_key' => urlencode( wc_clean( $license_key ) ), 'suffix' => 'tar.gz', ), 'https://download.maxmind.com/app/geoip_download' ); // Needed for the download_url call right below. require_once ABSPATH . 'wp-admin/includes/file.php'; $tmp_archive_path = download_url( esc_url_raw( $download_uri ) ); if ( is_wp_error( $tmp_archive_path ) ) { // Transform the error into something more informative. $error_data = $tmp_archive_path->get_error_data(); if ( isset( $error_data['code'] ) ) { switch ( $error_data['code'] ) { case 401: return new WP_Error( 'woocommerce_maxmind_geolocation_database_license_key', __( 'The MaxMind license key is invalid. If you have recently created this key, you may need to wait for it to become active.', 'woocommerce' ) ); } } return new WP_Error( 'woocommerce_maxmind_geolocation_database_download', __( 'Failed to download the MaxMind database.', 'woocommerce' ) ); } // Extract the database from the archive. try { $file = new PharData( $tmp_archive_path ); $tmp_database_path = trailingslashit( dirname( $tmp_archive_path ) ) . trailingslashit( $file->current()->getFilename() ) . self::DATABASE . self::DATABASE_EXTENSION; $file->extractTo( dirname( $tmp_archive_path ), trailingslashit( $file->current()->getFilename() ) . self::DATABASE . self::DATABASE_EXTENSION, true ); } catch ( Exception $exception ) { return new WP_Error( 'woocommerce_maxmind_geolocation_database_archive', $exception->getMessage() ); } finally { // Remove the archive since we only care about a single file in it. unlink( $tmp_archive_path ); } return $tmp_database_path; } /** * Fetches the ISO country code associated with an IP address. * * @param string $ip_address The IP address to find the country code for. * @return string The country code for the IP address, or empty if not found. */ public function get_iso_country_code_for_ip( $ip_address ) { $country_code = ''; if ( ! class_exists( 'MaxMind\Db\Reader' ) ) { wc_get_logger()->notice( __( 'Missing MaxMind Reader library!', 'woocommerce' ), array( 'source' => 'maxmind-geolocation' ) ); return $country_code; } $database_path = $this->get_database_path(); if ( ! file_exists( $database_path ) ) { return $country_code; } try { $reader = new MaxMind\Db\Reader( $database_path ); $data = $reader->get( $ip_address ); if ( isset( $data['country']['iso_code'] ) ) { $country_code = $data['country']['iso_code']; } $reader->close(); } catch ( Exception $e ) { wc_get_logger()->notice( $e->getMessage(), array( 'source' => 'maxmind-geolocation' ) ); } return $country_code; } } PKK[*3=Ì#Ì#Mintegrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.phpnu„[µü¤id = 'maxmind_geolocation'; $this->method_title = __( 'MaxMind Geolocation', 'woocommerce' ); $this->method_description = __( 'An integration for utilizing MaxMind to do Geolocation lookups. Please note that this integration will only do country lookups.', 'woocommerce' ); /** * Supports overriding the database service to be used. * * @since 3.9.0 * @return mixed|null The geolocation database service. */ $this->database_service = apply_filters( 'woocommerce_maxmind_geolocation_database_service', null ); if ( null === $this->database_service ) { $this->database_service = new WC_Integration_MaxMind_Database_Service( $this->get_database_prefix() ); } $this->init_form_fields(); $this->init_settings(); // Bind to the save action for the settings. add_action( 'woocommerce_update_options_integration_' . $this->id, array( $this, 'process_admin_options' ) ); // Trigger notice if license key is missing. add_action( 'update_option_woocommerce_default_customer_address', array( $this, 'display_missing_license_key_notice' ), 1000, 2 ); /** * Allows for the automatic database update to be disabled. * * @deprecated 3.9.0 * @return bool Whether or not the database should be updated periodically. */ $bind_updater = apply_filters_deprecated( 'woocommerce_geolocation_update_database_periodically', array( true ), '3.9.0', 'woocommerce_maxmind_geolocation_update_database_periodically' ); /** * Allows for the automatic database update to be disabled. * Note that MaxMind's TOS requires that the databases be updated or removed periodically. * * @since 3.9.0 * @param bool $bind_updater Whether or not the database should be updated periodically. */ $bind_updater = apply_filters( 'woocommerce_maxmind_geolocation_update_database_periodically', $bind_updater ); // Bind to the scheduled updater action. if ( $bind_updater ) { add_action( 'woocommerce_geoip_updater', array( $this, 'update_database' ) ); } // Bind to the geolocation filter for MaxMind database lookups. add_filter( 'woocommerce_get_geolocation', array( $this, 'get_geolocation' ), 10, 2 ); } /** * Override the normal options so we can print the database file path to the admin, */ public function admin_options() { parent::admin_options(); include dirname( __FILE__ ) . '/views/html-admin-options.php'; } /** * Initializes the settings fields. */ public function init_form_fields() { $this->form_fields = array( 'license_key' => array( 'title' => __( 'MaxMind License Key', 'woocommerce' ), 'type' => 'password', 'description' => sprintf( /* translators: %1$s: Documentation URL */ __( 'The key that will be used when dealing with MaxMind Geolocation services. You can read how to generate one in MaxMind Geolocation Integration documentation.', 'woocommerce' ), 'https://woo.com/document/maxmind-geolocation-integration/' ), 'desc_tip' => false, 'default' => '', ), ); } /** * Get database service. * * @return WC_Integration_MaxMind_Database_Service|null */ public function get_database_service() { return $this->database_service; } /** * Checks to make sure that the license key is valid. * * @param string $key The key of the field. * @param mixed $value The value of the field. * @return mixed * @throws Exception When the license key is invalid. */ public function validate_license_key_field( $key, $value ) { // Trim whitespaces and strip slashes. $value = $this->validate_password_field( $key, $value ); // Empty license keys have no need test downloading a database. if ( empty( $value ) ) { return $value; } // Check the license key by attempting to download the Geolocation database. $tmp_database_path = $this->database_service->download_database( $value ); if ( is_wp_error( $tmp_database_path ) ) { WC_Admin_Settings::add_error( $tmp_database_path->get_error_message() ); // Throw an exception to keep from changing this value. This will prevent // users from accidentally losing their license key, which cannot // be viewed again after generating. throw new Exception( $tmp_database_path->get_error_message() ); } // We may as well put this archive to good use, now that we've downloaded one. self::update_database( $tmp_database_path ); // Remove missing license key notice. $this->remove_missing_license_key_notice(); return $value; } /** * Updates the database used for geolocation queries. * * @param string|null $new_database_path The path to the new database file. Null will fetch a new archive. */ public function update_database( $new_database_path = null ) { // Allow us to easily interact with the filesystem. require_once ABSPATH . 'wp-admin/includes/file.php'; if ( ! WP_Filesystem() ) { wc_get_logger()->warning( __( 'Failed to initialise WC_Filesystem API while trying to update the MaxMind Geolocation database.', 'woocommerce' ) ); return; } global $wp_filesystem; // Remove any existing archives to comply with the MaxMind TOS. $target_database_path = $this->database_service->get_database_path(); // If there's no database path, we can't store the database. if ( empty( $target_database_path ) ) { return; } if ( $wp_filesystem->exists( $target_database_path ) ) { $wp_filesystem->delete( $target_database_path ); } if ( isset( $new_database_path ) ) { $tmp_database_path = $new_database_path; } else { // We can't download a database if there's no license key configured. $license_key = $this->get_option( 'license_key' ); if ( empty( $license_key ) ) { return; } $tmp_database_path = $this->database_service->download_database( $license_key ); if ( is_wp_error( $tmp_database_path ) ) { wc_get_logger()->notice( $tmp_database_path->get_error_message(), array( 'source' => 'maxmind-geolocation' ) ); return; } } // Move the new database into position. $wp_filesystem->move( $tmp_database_path, $target_database_path, true ); $wp_filesystem->delete( dirname( $tmp_database_path ) ); } /** * Performs a geolocation lookup against the MaxMind database for the given IP address. * * @param array $data Geolocation data. * @param string $ip_address The IP address to geolocate. * @return array Geolocation including country code, state, city and postcode based on an IP address. */ public function get_geolocation( $data, $ip_address ) { // WooCommerce look for headers first, and at this moment could be just enough. if ( ! empty( $data['country'] ) ) { return $data; } if ( empty( $ip_address ) ) { return $data; } $country_code = $this->database_service->get_iso_country_code_for_ip( $ip_address ); return array( 'country' => $country_code, 'state' => '', 'city' => '', 'postcode' => '', ); } /** * Fetches the prefix for the MaxMind database file. * * @return string */ private function get_database_prefix() { $prefix = $this->get_option( 'database_prefix' ); if ( empty( $prefix ) ) { $prefix = wp_generate_password( 32, false ); $this->update_option( 'database_prefix', $prefix ); } return $prefix; } /** * Add missing license key notice. */ private function add_missing_license_key_notice() { if ( ! class_exists( 'WC_Admin_Notices' ) ) { include_once WC_ABSPATH . 'includes/admin/class-wc-admin-notices.php'; } WC_Admin_Notices::add_notice( 'maxmind_license_key' ); } /** * Remove missing license key notice. */ private function remove_missing_license_key_notice() { if ( ! class_exists( 'WC_Admin_Notices' ) ) { include_once WC_ABSPATH . 'includes/admin/class-wc-admin-notices.php'; } WC_Admin_Notices::remove_notice( 'maxmind_license_key' ); } /** * Display notice if license key is missing. * * @param mixed $old_value Option old value. * @param mixed $new_value Current value. */ public function display_missing_license_key_notice( $old_value, $new_value ) { if ( ! apply_filters( 'woocommerce_maxmind_geolocation_display_notices', true ) ) { return; } if ( ! in_array( $new_value, array( 'geolocation', 'geolocation_ajax' ), true ) ) { $this->remove_missing_license_key_notice(); return; } $license_key = $this->get_option( 'license_key' ); if ( ! empty( $license_key ) ) { return; } $this->add_missing_license_key_notice(); } } PKK[{+GG=integrations/maxmind-geolocation/views/html-admin-options.phpnu„[µü¤

PKK[®©U # #class-wc-order-item-fee.phpnu„[µü¤ '', 'tax_status' => 'taxable', 'amount' => '', 'total' => '', 'total_tax' => '', 'taxes' => array( 'total' => array(), ), ); /** * Get item costs grouped by tax class. * * @since 3.2.0 * @param WC_Order $order Order object. * @return array */ protected function get_tax_class_costs( $order ) { $order_item_tax_classes = $order->get_items_tax_classes(); $costs = array_fill_keys( $order_item_tax_classes, 0 ); $costs['non-taxable'] = 0; foreach ( $order->get_items( array( 'line_item', 'fee', 'shipping' ) ) as $item ) { if ( 0 > $item->get_total() ) { continue; } if ( 'taxable' !== $item->get_tax_status() ) { $costs['non-taxable'] += $item->get_total(); } elseif ( 'inherit' === $item->get_tax_class() ) { $inherit_class = reset( $order_item_tax_classes ); $costs[ $inherit_class ] += $item->get_total(); } else { $costs[ $item->get_tax_class() ] += $item->get_total(); } } return array_filter( $costs ); } /** * Calculate item taxes. * * @since 3.2.0 * @param array $calculate_tax_for Location data to get taxes for. Required. * @return bool True if taxes were calculated. */ public function calculate_taxes( $calculate_tax_for = array() ) { if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) { return false; } // Use regular calculation unless the fee is negative. if ( 0 <= $this->get_total() ) { return parent::calculate_taxes( $calculate_tax_for ); } if ( wc_tax_enabled() && $this->get_order() ) { // Apportion taxes to order items, shipping, and fees. $order = $this->get_order(); $tax_class_costs = $this->get_tax_class_costs( $order ); $total_costs = array_sum( $tax_class_costs ); $discount_taxes = array(); if ( $total_costs ) { foreach ( $tax_class_costs as $tax_class => $tax_class_cost ) { if ( 'non-taxable' === $tax_class ) { continue; } $proportion = $tax_class_cost / $total_costs; $cart_discount_proportion = $this->get_total() * $proportion; $calculate_tax_for['tax_class'] = $tax_class; $tax_rates = WC_Tax::find_rates( $calculate_tax_for ); $discount_taxes = wc_array_merge_recursive_numeric( $discount_taxes, WC_Tax::calc_tax( $cart_discount_proportion, $tax_rates ) ); } } $this->set_taxes( array( 'total' => $discount_taxes ) ); } else { $this->set_taxes( false ); } do_action( 'woocommerce_order_item_fee_after_calculate_taxes', $this, $calculate_tax_for ); return true; } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set fee amount. * * @param string $value Amount. */ public function set_amount( $value ) { $this->set_prop( 'amount', wc_format_decimal( $value ) ); } /** * Set tax class. * * @param string $value Tax class. */ public function set_tax_class( $value ) { if ( $value && ! in_array( $value, WC_Tax::get_tax_class_slugs(), true ) ) { $this->error( 'order_item_fee_invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) ); } $this->set_prop( 'tax_class', $value ); } /** * Set tax_status. * * @param string $value Tax status. */ public function set_tax_status( $value ) { if ( in_array( $value, array( 'taxable', 'none' ), true ) ) { $this->set_prop( 'tax_status', $value ); } else { $this->set_prop( 'tax_status', 'taxable' ); } } /** * Set total. * * @param string $amount Fee amount (do not enter negative amounts). */ public function set_total( $amount ) { $this->set_prop( 'total', wc_format_decimal( $amount ) ); } /** * Set total tax. * * @param string $amount Amount. */ public function set_total_tax( $amount ) { $this->set_prop( 'total_tax', wc_format_decimal( $amount ) ); } /** * Set taxes. * * This is an array of tax ID keys with total amount values. * * @param array $raw_tax_data Raw tax data. */ public function set_taxes( $raw_tax_data ) { $raw_tax_data = maybe_unserialize( $raw_tax_data ); $tax_data = array( 'total' => array(), ); if ( ! empty( $raw_tax_data['total'] ) ) { $tax_data['total'] = array_map( 'wc_format_decimal', $raw_tax_data['total'] ); } $this->set_prop( 'taxes', $tax_data ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $this->set_total_tax( array_sum( $tax_data['total'] ) ); } else { $this->set_total_tax( array_sum( array_map( 'wc_round_tax_total', $tax_data['total'] ) ) ); } } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get fee amount. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_amount( $context = 'view' ) { return $this->get_prop( 'amount', $context ); } /** * Get order item name. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_name( $context = 'view' ) { $name = $this->get_prop( 'name', $context ); if ( 'view' === $context ) { return $name ? $name : __( 'Fee', 'woocommerce' ); } else { return $name; } } /** * Get order item type. * * @return string */ public function get_type() { return 'fee'; } /** * Get tax class. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_tax_class( $context = 'view' ) { return $this->get_prop( 'tax_class', $context ); } /** * Get tax status. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_tax_status( $context = 'view' ) { return $this->get_prop( 'tax_status', $context ); } /** * Get total fee. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_total( $context = 'view' ) { return $this->get_prop( 'total', $context ); } /** * Get total tax. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return string */ public function get_total_tax( $context = 'view' ) { return $this->get_prop( 'total_tax', $context ); } /** * Get fee taxes. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * @return array */ public function get_taxes( $context = 'view' ) { return $this->get_prop( 'taxes', $context ); } /* |-------------------------------------------------------------------------- | Array Access Methods |-------------------------------------------------------------------------- | | For backwards compatibility with legacy arrays. | */ /** * OffsetGet for ArrayAccess/Backwards compatibility. * * @param string $offset Offset. * @return mixed */ #[\ReturnTypeWillChange] public function offsetGet( $offset ) { if ( 'line_total' === $offset ) { $offset = 'total'; } elseif ( 'line_tax' === $offset ) { $offset = 'total_tax'; } elseif ( 'line_tax_data' === $offset ) { $offset = 'taxes'; } return parent::offsetGet( $offset ); } /** * OffsetSet for ArrayAccess/Backwards compatibility. * * @deprecated 4.4.0 * @param string $offset Offset. * @param mixed $value Value. */ #[\ReturnTypeWillChange] public function offsetSet( $offset, $value ) { wc_deprecated_function( 'WC_Order_Item_Fee::offsetSet', '4.4.0', '' ); if ( 'line_total' === $offset ) { $offset = 'total'; } elseif ( 'line_tax' === $offset ) { $offset = 'total_tax'; } elseif ( 'line_tax_data' === $offset ) { $offset = 'taxes'; } parent::offsetSet( $offset, $value ); } /** * OffsetExists for ArrayAccess * * @param string $offset Offset. * @return bool */ #[\ReturnTypeWillChange] public function offsetExists( $offset ) { if ( in_array( $offset, array( 'line_total', 'line_tax', 'line_tax_data' ), true ) ) { return true; } return parent::offsetExists( $offset ); } } PKK[¼­ãE‚‚$class-wc-deprecated-action-hooks.phpnu„[µü¤ 'old'. * * @var array */ protected $deprecated_hooks = array( 'woocommerce_new_order_item' => array( 'woocommerce_order_add_shipping', 'woocommerce_order_add_coupon', 'woocommerce_order_add_tax', 'woocommerce_order_add_fee', 'woocommerce_add_shipping_order_item', 'woocommerce_add_order_item_meta', 'woocommerce_add_order_fee_meta', ), 'woocommerce_update_order_item' => array( 'woocommerce_order_edit_product', 'woocommerce_order_update_coupon', 'woocommerce_order_update_shipping', 'woocommerce_order_update_fee', 'woocommerce_order_update_tax', ), 'woocommerce_new_payment_token' => 'woocommerce_payment_token_created', 'woocommerce_new_product_variation' => 'woocommerce_create_product_variation', 'woocommerce_order_details_after_order_table_items' => 'woocommerce_order_items_table', 'woocommerce_settings_advanced_page_options' => array( 'woocommerce_settings_checkout_page_options', 'woocommerce_settings_account_page_options', ), 'woocommerce_settings_advanced_page_options_end' => array( 'woocommerce_settings_checkout_page_options_end', 'woocommerce_settings_account_page_options_end', ), 'woocommerce_settings_advanced_page_options_after' => array( 'woocommerce_settings_checkout_page_options_after', 'woocommerce_settings_account_page_options_after', ), ); /** * Array of versions on each hook has been deprecated. * * @var array */ protected $deprecated_version = array( 'woocommerce_order_add_shipping' => '3.0.0', 'woocommerce_order_add_coupon' => '3.0.0', 'woocommerce_order_add_tax' => '3.0.0', 'woocommerce_order_add_fee' => '3.0.0', 'woocommerce_add_shipping_order_item' => '3.0.0', 'woocommerce_add_order_item_meta' => '3.0.0', 'woocommerce_add_order_fee_meta' => '3.0.0', 'woocommerce_order_edit_product' => '3.0.0', 'woocommerce_order_update_coupon' => '3.0.0', 'woocommerce_order_update_shipping' => '3.0.0', 'woocommerce_order_update_fee' => '3.0.0', 'woocommerce_order_update_tax' => '3.0.0', 'woocommerce_payment_token_created' => '3.0.0', 'woocommerce_create_product_variation' => '3.0.0', 'woocommerce_order_items_table' => '3.0.0', 'woocommerce_settings_checkout_page_options' => '3.4.0', 'woocommerce_settings_account_page_options' => '3.4.0', 'woocommerce_settings_checkout_page_options_end' => '3.4.0', 'woocommerce_settings_account_page_options_end' => '3.4.0', 'woocommerce_settings_checkout_page_options_after' => '3.4.0', 'woocommerce_settings_account_page_options_after' => '3.4.0', ); /** * Hook into the new hook so we can handle deprecated hooks once fired. * * @param string $hook_name Hook name. */ public function hook_in( $hook_name ) { add_action( $hook_name, array( $this, 'maybe_handle_deprecated_hook' ), -1000, 8 ); } /** * If the old hook is in-use, trigger it. * * @param string $new_hook New hook name. * @param string $old_hook Old hook name. * @param array $new_callback_args New callback args. * @param mixed $return_value Returned value. * @return mixed */ public function handle_deprecated_hook( $new_hook, $old_hook, $new_callback_args, $return_value ) { if ( has_action( $old_hook ) ) { $this->display_notice( $old_hook, $new_hook ); $return_value = $this->trigger_hook( $old_hook, $new_callback_args ); } return $return_value; } /** * Fire off a legacy hook with it's args. * * @param string $old_hook Old hook name. * @param array $new_callback_args New callback args. * @return mixed */ protected function trigger_hook( $old_hook, $new_callback_args ) { switch ( $old_hook ) { case 'woocommerce_order_add_shipping': case 'woocommerce_order_add_fee': $item_id = $new_callback_args[0]; $item = $new_callback_args[1]; $order_id = $new_callback_args[2]; if ( is_a( $item, 'WC_Order_Item_Shipping' ) || is_a( $item, 'WC_Order_Item_Fee' ) ) { do_action( $old_hook, $order_id, $item_id, $item ); } break; case 'woocommerce_order_add_coupon': $item_id = $new_callback_args[0]; $item = $new_callback_args[1]; $order_id = $new_callback_args[2]; if ( is_a( $item, 'WC_Order_Item_Coupon' ) ) { do_action( $old_hook, $order_id, $item_id, $item->get_code(), $item->get_discount(), $item->get_discount_tax() ); } break; case 'woocommerce_order_add_tax': $item_id = $new_callback_args[0]; $item = $new_callback_args[1]; $order_id = $new_callback_args[2]; if ( is_a( $item, 'WC_Order_Item_Tax' ) ) { do_action( $old_hook, $order_id, $item_id, $item->get_rate_id(), $item->get_tax_total(), $item->get_shipping_tax_total() ); } break; case 'woocommerce_add_shipping_order_item': $item_id = $new_callback_args[0]; $item = $new_callback_args[1]; $order_id = $new_callback_args[2]; if ( is_a( $item, 'WC_Order_Item_Shipping' ) ) { do_action( $old_hook, $order_id, $item_id, $item->legacy_package_key ); } break; case 'woocommerce_add_order_item_meta': $item_id = $new_callback_args[0]; $item = $new_callback_args[1]; $order_id = $new_callback_args[2]; if ( is_a( $item, 'WC_Order_Item_Product' ) ) { do_action( $old_hook, $item_id, $item->legacy_values, $item->legacy_cart_item_key ); } break; case 'woocommerce_add_order_fee_meta': $item_id = $new_callback_args[0]; $item = $new_callback_args[1]; $order_id = $new_callback_args[2]; if ( is_a( $item, 'WC_Order_Item_Fee' ) ) { do_action( $old_hook, $order_id, $item_id, $item->legacy_fee, $item->legacy_fee_key ); } break; case 'woocommerce_order_edit_product': $item_id = $new_callback_args[0]; $item = $new_callback_args[1]; $order_id = $new_callback_args[2]; if ( is_a( $item, 'WC_Order_Item_Product' ) ) { do_action( $old_hook, $order_id, $item_id, $item, $item->get_product() ); } break; case 'woocommerce_order_update_coupon': case 'woocommerce_order_update_shipping': case 'woocommerce_order_update_fee': case 'woocommerce_order_update_tax': if ( ! is_a( $item, 'WC_Order_Item_Product' ) ) { do_action( $old_hook, $order_id, $item_id, $item ); } break; default: do_action_ref_array( $old_hook, $new_callback_args ); break; } } } PKK[Iâ”M66class-wc-order-item-meta.phpnu„[µü¤legacy = true; $this->meta = array_filter( (array) $item ); return; } $this->item = $item; $this->meta = array_filter( (array) $item['item_meta'] ); $this->product = $product; } /** * Display meta in a formatted list. * * @param bool $flat Flat (default: false). * @param bool $return Return (default: false). * @param string $hideprefix Hide prefix (default: _). * @param string $delimiter Delimiter used to separate items when $flat is true. * @return string|void */ public function display( $flat = false, $return = false, $hideprefix = '_', $delimiter = ", \n" ) { $output = ''; $formatted_meta = $this->get_formatted( $hideprefix ); if ( ! empty( $formatted_meta ) ) { $meta_list = array(); foreach ( $formatted_meta as $meta ) { if ( $flat ) { $meta_list[] = wp_kses_post( $meta['label'] . ': ' . $meta['value'] ); } else { $meta_list[] = '
' . wp_kses_post( $meta['label'] ) . ':
' . wp_kses_post( wpautop( make_clickable( $meta['value'] ) ) ) . '
'; } } if ( ! empty( $meta_list ) ) { if ( $flat ) { $output .= implode( $delimiter, $meta_list ); } else { $output .= '
' . implode( '', $meta_list ) . '
'; } } } $output = apply_filters( 'woocommerce_order_items_meta_display', $output, $this, $flat ); if ( $return ) { return $output; } else { echo $output; // WPCS: XSS ok. } } /** * Return an array of formatted item meta in format e.g. * * Returns: array( * 'pa_size' => array( * 'label' => 'Size', * 'value' => 'Medium', * ) * ) * * @since 2.4 * @param string $hideprefix exclude meta when key is prefixed with this, defaults to '_'. * @return array */ public function get_formatted( $hideprefix = '_' ) { if ( $this->legacy ) { return $this->get_formatted_legacy( $hideprefix ); } $formatted_meta = array(); if ( ! empty( $this->item['item_meta_array'] ) ) { foreach ( $this->item['item_meta_array'] as $meta_id => $meta ) { if ( '' === $meta->value || is_serialized( $meta->value ) || ( ! empty( $hideprefix ) && substr( $meta->key, 0, 1 ) === $hideprefix ) ) { continue; } $attribute_key = urldecode( str_replace( 'attribute_', '', $meta->key ) ); $meta_value = $meta->value; // If this is a term slug, get the term's nice name. if ( taxonomy_exists( $attribute_key ) ) { $term = get_term_by( 'slug', $meta_value, $attribute_key ); if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { $meta_value = $term->name; } } $formatted_meta[ $meta_id ] = array( 'key' => $meta->key, 'label' => wc_attribute_label( $attribute_key, $this->product ), 'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value, $meta, $this->item ), ); } } return apply_filters( 'woocommerce_order_items_meta_get_formatted', $formatted_meta, $this ); } /** * Return an array of formatted item meta in format e.g. * Handles @deprecated args. * * @param string $hideprefix Hide prefix. * * @return array */ public function get_formatted_legacy( $hideprefix = '_' ) { if ( ! wp_doing_ajax() ) { wc_deprecated_argument( 'WC_Order_Item_Meta::get_formatted', '2.4', 'Item Meta Data is being called with legacy arguments' ); } $formatted_meta = array(); foreach ( $this->meta as $meta_key => $meta_values ) { if ( empty( $meta_values ) || ( ! empty( $hideprefix ) && substr( $meta_key, 0, 1 ) === $hideprefix ) ) { continue; } foreach ( (array) $meta_values as $meta_value ) { // Skip serialised meta. if ( is_serialized( $meta_value ) ) { continue; } $attribute_key = urldecode( str_replace( 'attribute_', '', $meta_key ) ); // If this is a term slug, get the term's nice name. if ( taxonomy_exists( $attribute_key ) ) { $term = get_term_by( 'slug', $meta_value, $attribute_key ); if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { $meta_value = $term->name; } } // Unique key required. $formatted_meta_key = $meta_key; $loop = 0; while ( isset( $formatted_meta[ $formatted_meta_key ] ) ) { $loop ++; $formatted_meta_key = $meta_key . '-' . $loop; } $formatted_meta[ $formatted_meta_key ] = array( 'key' => $meta_key, 'label' => wc_attribute_label( $attribute_key, $this->product ), 'value' => apply_filters( 'woocommerce_order_item_display_meta_value', $meta_value, $this->meta, $this->item ), ); } } return $formatted_meta; } } PKK["ø£˜•˜•class-woocommerce.phpnu„[µü¤$key(); } } /** * WooCommerce Constructor. */ public function __construct() { $this->define_constants(); $this->define_tables(); $this->includes(); $this->init_hooks(); } /** * When WP has loaded all plugins, trigger the `woocommerce_loaded` hook. * * This ensures `woocommerce_loaded` is called only after all other plugins * are loaded, to avoid issues caused by plugin directory naming changing * the load order. See #21524 for details. * * @since 3.6.0 */ public function on_plugins_loaded() { /** * Action to signal that WooCommerce has finished loading. * * @since 3.6.0 */ do_action( 'woocommerce_loaded' ); } /** * Initiali Jetpack Connection Config. * * @return void */ public function init_jetpack_connection_config() { $config = new Automattic\Jetpack\Config(); $config->ensure( 'connection', array( 'slug' => 'woocommerce', 'name' => __( 'WooCommerce', 'woocommerce' ), ) ); } /** * Hook into actions and filters. * * @since 2.3 */ private function init_hooks() { register_activation_hook( WC_PLUGIN_FILE, array( 'WC_Install', 'install' ) ); register_shutdown_function( array( $this, 'log_errors' ) ); add_action( 'plugins_loaded', array( $this, 'on_plugins_loaded' ), -1 ); add_action( 'plugins_loaded', array( $this, 'init_jetpack_connection_config' ), 1 ); add_action( 'admin_notices', array( $this, 'build_dependencies_notice' ) ); add_action( 'after_setup_theme', array( $this, 'setup_environment' ) ); add_action( 'after_setup_theme', array( $this, 'include_template_functions' ), 11 ); add_action( 'load-post.php', array( $this, 'includes' ) ); add_action( 'init', array( $this, 'init' ), 0 ); add_action( 'init', array( 'WC_Shortcodes', 'init' ) ); add_action( 'init', array( 'WC_Emails', 'init_transactional_emails' ) ); add_action( 'init', array( $this, 'add_image_sizes' ) ); add_action( 'init', array( $this, 'load_rest_api' ) ); add_action( 'init', array( 'WC_Site_Tracking', 'init' ) ); add_action( 'switch_blog', array( $this, 'wpdb_table_fix' ), 0 ); add_action( 'activated_plugin', array( $this, 'activated_plugin' ) ); add_action( 'deactivated_plugin', array( $this, 'deactivated_plugin' ) ); add_action( 'woocommerce_installed', array( $this, 'add_woocommerce_inbox_variant' ) ); add_action( 'woocommerce_updated', array( $this, 'add_woocommerce_inbox_variant' ) ); // These classes set up hooks on instantiation. $container = wc_get_container(); $container->get( ProductDownloadDirectories::class ); $container->get( DownloadPermissionsAdjuster::class ); $container->get( AssignDefaultCategory::class ); $container->get( DataRegenerator::class ); $container->get( LookupDataStore::class ); $container->get( RestockRefundedItemsAdjuster::class ); $container->get( CustomOrdersTableController::class ); $container->get( OptionSanitizer::class ); $container->get( BatchProcessingController::class ); $container->get( FeaturesController::class ); $container->get( WebhookUtil::class ); $container->get( Marketplace::class ); } /** * Add woocommerce_inbox_variant for the Remote Inbox Notification. * * P2 post can be found at https://wp.me/paJDYF-1uJ. */ public function add_woocommerce_inbox_variant() { $config_name = 'woocommerce_inbox_variant_assignment'; if ( false === get_option( $config_name, false ) ) { update_option( $config_name, wp_rand( 1, 12 ) ); } } /** * Ensures fatal errors are logged so they can be picked up in the status report. * * @since 3.2.0 */ public function log_errors() { $error = error_get_last(); if ( $error && in_array( $error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) { $logger = wc_get_logger(); $logger->critical( /* translators: 1: error message 2: file name and path 3: line number */ sprintf( __( '%1$s in %2$s on line %3$s', 'woocommerce' ), $error['message'], $error['file'], $error['line'] ) . PHP_EOL, array( 'source' => 'fatal-errors', ) ); /** * Action triggered when there are errors during shutdown. * * @since 3.2.0 */ do_action( 'woocommerce_shutdown_error', $error ); } } /** * Define WC Constants. */ private function define_constants() { $upload_dir = wp_upload_dir( null, false ); $this->define( 'WC_ABSPATH', dirname( WC_PLUGIN_FILE ) . '/' ); $this->define( 'WC_PLUGIN_BASENAME', plugin_basename( WC_PLUGIN_FILE ) ); $this->define( 'WC_VERSION', $this->version ); $this->define( 'WOOCOMMERCE_VERSION', $this->version ); $this->define( 'WC_ROUNDING_PRECISION', 6 ); $this->define( 'WC_DISCOUNT_ROUNDING_MODE', 2 ); $this->define( 'WC_TAX_ROUNDING_MODE', 'yes' === get_option( 'woocommerce_prices_include_tax', 'no' ) ? 2 : 1 ); $this->define( 'WC_DELIMITER', '|' ); $this->define( 'WC_LOG_DIR', $upload_dir['basedir'] . '/wc-logs/' ); $this->define( 'WC_SESSION_CACHE_GROUP', 'wc_session_id' ); $this->define( 'WC_TEMPLATE_DEBUG_MODE', false ); // These three are kept defined for compatibility, but are no longer used. $this->define( 'WC_NOTICE_MIN_PHP_VERSION', '7.2' ); $this->define( 'WC_NOTICE_MIN_WP_VERSION', '5.2' ); $this->define( 'WC_PHP_MIN_REQUIREMENTS_NOTICE', 'wp_php_min_requirements_' . WC_NOTICE_MIN_PHP_VERSION . '_' . WC_NOTICE_MIN_WP_VERSION ); /** Define if we're checking against major, minor or no versions in the following places: * - plugin screen in WP Admin (displaying extra warning when updating to new major versions) * - System Status Report ('Installed version not tested with active version of WooCommerce' warning) * - core update screen in WP Admin (displaying extra warning when updating to new major versions) * - enable/disable automated updates in the plugin screen in WP Admin (if there are any plugins * that don't declare compatibility, the auto-update is disabled) * * We dropped SemVer before WC 5.0, so all versions are backwards compatible now, thus no more check needed. * The SSR in the name is preserved for bw compatibility, as this was initially used in System Status Report. */ $this->define( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE', 'none' ); } /** * Register custom tables within $wpdb object. */ private function define_tables() { global $wpdb; // List of tables without prefixes. $tables = array( 'payment_tokenmeta' => 'woocommerce_payment_tokenmeta', 'order_itemmeta' => 'woocommerce_order_itemmeta', 'wc_product_meta_lookup' => 'wc_product_meta_lookup', 'wc_tax_rate_classes' => 'wc_tax_rate_classes', 'wc_reserved_stock' => 'wc_reserved_stock', ); foreach ( $tables as $name => $table ) { $wpdb->$name = $wpdb->prefix . $table; $wpdb->tables[] = $table; } } /** * Define constant if not already set. * * @param string $name Constant name. * @param string|bool $value Constant value. */ private function define( $name, $value ) { if ( ! defined( $name ) ) { define( $name, $value ); } } /** * Returns true if the request is a non-legacy REST API request. * * Legacy REST requests should still run some extra code for backwards compatibility. * * @todo: replace this function once core WP function is available: https://core.trac.wordpress.org/ticket/42061. * * @return bool */ public function is_rest_api_request() { if ( empty( $_SERVER['REQUEST_URI'] ) ) { return false; } $rest_prefix = trailingslashit( rest_get_url_prefix() ); $is_rest_api_request = ( false !== strpos( $_SERVER['REQUEST_URI'], $rest_prefix ) ); // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized /** * Whether this is a REST API request. * * @since 3.6.0 */ return apply_filters( 'woocommerce_is_rest_api_request', $is_rest_api_request ); } /** * Load REST API. */ public function load_rest_api() { \Automattic\WooCommerce\RestApi\Server::instance()->init(); } /** * What type of request is this? * * @param string $type admin, ajax, cron or frontend. * @return bool */ private function is_request( $type ) { switch ( $type ) { case 'admin': return is_admin(); case 'ajax': return defined( 'DOING_AJAX' ); case 'cron': return defined( 'DOING_CRON' ); case 'frontend': return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' ) && ! $this->is_rest_api_request(); } } /** * Include required core files used in admin and on the frontend. */ public function includes() { /** * Class autoloader. */ include_once WC_ABSPATH . 'includes/class-wc-autoloader.php'; /** * Interfaces. */ include_once WC_ABSPATH . 'includes/interfaces/class-wc-abstract-order-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-coupon-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-download-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-download-log-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-object-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-product-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-type-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-refund-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-payment-token-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-product-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-product-variable-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-shipping-zone-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-logger-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-log-handler-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-webhooks-data-store-interface.php'; include_once WC_ABSPATH . 'includes/interfaces/class-wc-queue-interface.php'; /** * Core traits. */ include_once WC_ABSPATH . 'includes/traits/trait-wc-item-totals.php'; /** * Abstract classes. */ include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-data.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-object-query.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-payment-token.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-product.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-order.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-settings-api.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-shipping-method.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-payment-gateway.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-integration.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-log-handler.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-deprecated-hooks.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-session.php'; include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-privacy.php'; /** * Core classes. */ include_once WC_ABSPATH . 'includes/wc-core-functions.php'; include_once WC_ABSPATH . 'includes/class-wc-datetime.php'; include_once WC_ABSPATH . 'includes/class-wc-post-types.php'; include_once WC_ABSPATH . 'includes/class-wc-install.php'; include_once WC_ABSPATH . 'includes/class-wc-geolocation.php'; include_once WC_ABSPATH . 'includes/class-wc-download-handler.php'; include_once WC_ABSPATH . 'includes/class-wc-comments.php'; include_once WC_ABSPATH . 'includes/class-wc-post-data.php'; include_once WC_ABSPATH . 'includes/class-wc-ajax.php'; include_once WC_ABSPATH . 'includes/class-wc-emails.php'; include_once WC_ABSPATH . 'includes/class-wc-data-exception.php'; include_once WC_ABSPATH . 'includes/class-wc-query.php'; include_once WC_ABSPATH . 'includes/class-wc-meta-data.php'; include_once WC_ABSPATH . 'includes/class-wc-order-factory.php'; include_once WC_ABSPATH . 'includes/class-wc-order-query.php'; include_once WC_ABSPATH . 'includes/class-wc-product-factory.php'; include_once WC_ABSPATH . 'includes/class-wc-product-query.php'; include_once WC_ABSPATH . 'includes/class-wc-payment-tokens.php'; include_once WC_ABSPATH . 'includes/class-wc-shipping-zone.php'; include_once WC_ABSPATH . 'includes/gateways/class-wc-payment-gateway-cc.php'; include_once WC_ABSPATH . 'includes/gateways/class-wc-payment-gateway-echeck.php'; include_once WC_ABSPATH . 'includes/class-wc-countries.php'; include_once WC_ABSPATH . 'includes/class-wc-integrations.php'; include_once WC_ABSPATH . 'includes/class-wc-cache-helper.php'; include_once WC_ABSPATH . 'includes/class-wc-https.php'; include_once WC_ABSPATH . 'includes/class-wc-deprecated-action-hooks.php'; include_once WC_ABSPATH . 'includes/class-wc-deprecated-filter-hooks.php'; include_once WC_ABSPATH . 'includes/class-wc-background-emailer.php'; include_once WC_ABSPATH . 'includes/class-wc-discounts.php'; include_once WC_ABSPATH . 'includes/class-wc-cart-totals.php'; include_once WC_ABSPATH . 'includes/customizer/class-wc-shop-customizer.php'; include_once WC_ABSPATH . 'includes/class-wc-regenerate-images.php'; include_once WC_ABSPATH . 'includes/class-wc-privacy.php'; include_once WC_ABSPATH . 'includes/class-wc-structured-data.php'; include_once WC_ABSPATH . 'includes/class-wc-shortcodes.php'; include_once WC_ABSPATH . 'includes/class-wc-logger.php'; include_once WC_ABSPATH . 'includes/queue/class-wc-action-queue.php'; include_once WC_ABSPATH . 'includes/queue/class-wc-queue.php'; include_once WC_ABSPATH . 'includes/admin/marketplace-suggestions/class-wc-marketplace-updater.php'; include_once WC_ABSPATH . 'includes/blocks/class-wc-blocks-utils.php'; /** * Data stores - used to store and retrieve CRUD object data from the database. */ include_once WC_ABSPATH . 'includes/class-wc-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-data-store-wp.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-coupon-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-grouped-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-variable-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-variation-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/abstract-wc-order-item-type-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-coupon-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-fee-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-product-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-shipping-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-tax-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-payment-token-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-data-store-session.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-download-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-download-log-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-shipping-zone-data-store.php'; include_once WC_ABSPATH . 'includes/data-stores/abstract-wc-order-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-refund-data-store-cpt.php'; include_once WC_ABSPATH . 'includes/data-stores/class-wc-webhook-data-store.php'; /** * REST API. */ include_once WC_ABSPATH . 'includes/legacy/class-wc-legacy-api.php'; include_once WC_ABSPATH . 'includes/class-wc-api.php'; include_once WC_ABSPATH . 'includes/class-wc-rest-authentication.php'; include_once WC_ABSPATH . 'includes/class-wc-rest-exception.php'; include_once WC_ABSPATH . 'includes/class-wc-auth.php'; include_once WC_ABSPATH . 'includes/class-wc-register-wp-admin-settings.php'; /** * Tracks. */ include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks.php'; include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-event.php'; include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-client.php'; include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-footer-pixel.php'; include_once WC_ABSPATH . 'includes/tracks/class-wc-site-tracking.php'; /** * WCCOM Site. */ include_once WC_ABSPATH . 'includes/wccom-site/class-wc-wccom-site.php'; /** * Libraries and packages. */ include_once WC_ABSPATH . 'packages/action-scheduler/action-scheduler.php'; if ( defined( 'WP_CLI' ) && WP_CLI ) { include_once WC_ABSPATH . 'includes/class-wc-cli.php'; } if ( $this->is_request( 'admin' ) ) { include_once WC_ABSPATH . 'includes/admin/class-wc-admin.php'; } // We load frontend includes in the post editor, because they may be invoked via pre-loading of blocks. $in_post_editor = doing_action( 'load-post.php' ) || doing_action( 'load-post-new.php' ); if ( $this->is_request( 'frontend' ) || $this->is_rest_api_request() || $in_post_editor ) { $this->frontend_includes(); } if ( $this->is_request( 'cron' ) && 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) ) { include_once WC_ABSPATH . 'includes/class-wc-tracker.php'; } $this->theme_support_includes(); $this->query = new WC_Query(); $this->api = new WC_API(); $this->api->init(); } /** * Include classes for theme support. * * @since 3.3.0 */ private function theme_support_includes() { if ( wc_is_wp_default_theme_active() ) { switch ( get_template() ) { case 'twentyten': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-ten.php'; break; case 'twentyeleven': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-eleven.php'; break; case 'twentytwelve': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twelve.php'; break; case 'twentythirteen': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-thirteen.php'; break; case 'twentyfourteen': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-fourteen.php'; break; case 'twentyfifteen': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-fifteen.php'; break; case 'twentysixteen': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-sixteen.php'; break; case 'twentyseventeen': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-seventeen.php'; break; case 'twentynineteen': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-nineteen.php'; break; case 'twentytwenty': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty.php'; break; case 'twentytwentyone': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty-one.php'; break; case 'twentytwentytwo': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty-two.php'; break; case 'twentytwentythree': include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty-three.php'; break; } } } /** * Include required frontend files. */ public function frontend_includes() { include_once WC_ABSPATH . 'includes/wc-cart-functions.php'; include_once WC_ABSPATH . 'includes/wc-notice-functions.php'; include_once WC_ABSPATH . 'includes/wc-template-hooks.php'; include_once WC_ABSPATH . 'includes/class-wc-template-loader.php'; include_once WC_ABSPATH . 'includes/class-wc-frontend-scripts.php'; include_once WC_ABSPATH . 'includes/class-wc-form-handler.php'; include_once WC_ABSPATH . 'includes/class-wc-cart.php'; include_once WC_ABSPATH . 'includes/class-wc-tax.php'; include_once WC_ABSPATH . 'includes/class-wc-shipping-zones.php'; include_once WC_ABSPATH . 'includes/class-wc-customer.php'; include_once WC_ABSPATH . 'includes/class-wc-embed.php'; include_once WC_ABSPATH . 'includes/class-wc-session-handler.php'; } /** * Function used to Init WooCommerce Template Functions - This makes them pluggable by plugins and themes. */ public function include_template_functions() { include_once WC_ABSPATH . 'includes/wc-template-functions.php'; } /** * Init WooCommerce when WordPress Initialises. */ public function init() { /** * Action triggered before WooCommerce initialization begins. */ do_action( 'before_woocommerce_init' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment // Set up localisation. $this->load_plugin_textdomain(); // Load class instances. $this->product_factory = new WC_Product_Factory(); $this->order_factory = new WC_Order_Factory(); $this->countries = new WC_Countries(); $this->integrations = new WC_Integrations(); $this->structured_data = new WC_Structured_Data(); $this->deprecated_hook_handlers['actions'] = new WC_Deprecated_Action_Hooks(); $this->deprecated_hook_handlers['filters'] = new WC_Deprecated_Filter_Hooks(); // Classes/actions loaded for the frontend and for ajax requests. if ( $this->is_request( 'frontend' ) ) { wc_load_cart(); } $this->load_webhooks(); /** * Action triggered after WooCommerce initialization finishes. */ do_action( 'woocommerce_init' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment } /** * Load Localisation files. * * Note: the first-loaded translation file overrides any following ones if the same translation is present. * * Locales found in: * - WP_LANG_DIR/woocommerce/woocommerce-LOCALE.mo * - WP_LANG_DIR/plugins/woocommerce-LOCALE.mo */ public function load_plugin_textdomain() { $locale = determine_locale(); /** * Filter to adjust the WooCommerce locale to use for translations. */ $locale = apply_filters( 'plugin_locale', $locale, 'woocommerce' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment unload_textdomain( 'woocommerce' ); load_textdomain( 'woocommerce', WP_LANG_DIR . '/woocommerce/woocommerce-' . $locale . '.mo' ); load_plugin_textdomain( 'woocommerce', false, plugin_basename( dirname( WC_PLUGIN_FILE ) ) . '/i18n/languages' ); } /** * Ensure theme and server variable compatibility and setup image sizes. */ public function setup_environment() { /** * WC_TEMPLATE_PATH constant. * * @deprecated 2.2 Use WC()->template_path() instead. */ $this->define( 'WC_TEMPLATE_PATH', $this->template_path() ); $this->add_thumbnail_support(); } /** * Ensure post thumbnail support is turned on. */ private function add_thumbnail_support() { if ( ! current_theme_supports( 'post-thumbnails' ) ) { add_theme_support( 'post-thumbnails' ); } add_post_type_support( 'product', 'thumbnail' ); } /** * Add WC Image sizes to WP. * * As of 3.3, image sizes can be registered via themes using add_theme_support for woocommerce * and defining an array of args. If these are not defined, we will use defaults. This is * handled in wc_get_image_size function. * * 3.3 sizes: * * woocommerce_thumbnail - Used in product listings. We assume these work for a 3 column grid layout. * woocommerce_single - Used on single product pages for the main image. * * @since 2.3 */ public function add_image_sizes() { $thumbnail = wc_get_image_size( 'thumbnail' ); $single = wc_get_image_size( 'single' ); $gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' ); add_image_size( 'woocommerce_thumbnail', $thumbnail['width'], $thumbnail['height'], $thumbnail['crop'] ); add_image_size( 'woocommerce_single', $single['width'], $single['height'], $single['crop'] ); add_image_size( 'woocommerce_gallery_thumbnail', $gallery_thumbnail['width'], $gallery_thumbnail['height'], $gallery_thumbnail['crop'] ); } /** * Get the plugin url. * * @return string */ public function plugin_url() { return untrailingslashit( plugins_url( '/', WC_PLUGIN_FILE ) ); } /** * Get the plugin path. * * @return string */ public function plugin_path() { return untrailingslashit( plugin_dir_path( WC_PLUGIN_FILE ) ); } /** * Get the template path. * * @return string */ public function template_path() { /** * Filter to adjust the base templates path. */ return apply_filters( 'woocommerce_template_path', 'woocommerce/' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment } /** * Get Ajax URL. * * @return string */ public function ajax_url() { return admin_url( 'admin-ajax.php', 'relative' ); } /** * Return the WC API URL for a given request. * * @param string $request Requested endpoint. * @param bool|null $ssl If should use SSL, null if should auto detect. Default: null. * @return string */ public function api_request_url( $request, $ssl = null ) { if ( is_null( $ssl ) ) { $scheme = wp_parse_url( home_url(), PHP_URL_SCHEME ); } elseif ( $ssl ) { $scheme = 'https'; } else { $scheme = 'http'; } if ( strstr( get_option( 'permalink_structure' ), '/index.php/' ) ) { $api_request_url = trailingslashit( home_url( '/index.php/wc-api/' . $request, $scheme ) ); } elseif ( get_option( 'permalink_structure' ) ) { $api_request_url = trailingslashit( home_url( '/wc-api/' . $request, $scheme ) ); } else { $api_request_url = add_query_arg( 'wc-api', $request, trailingslashit( home_url( '', $scheme ) ) ); } /** * Filter to adjust the url of an incoming API request. */ return esc_url_raw( apply_filters( 'woocommerce_api_request_url', $api_request_url, $request, $ssl ) ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment } /** * Load & enqueue active webhooks. * * @since 2.2 */ private function load_webhooks() { if ( ! is_blog_installed() ) { return; } /** * Hook: woocommerce_load_webhooks_limit. * * @since 3.6.0 * @param int $limit Used to limit how many webhooks are loaded. Default: no limit. */ $limit = apply_filters( 'woocommerce_load_webhooks_limit', null ); wc_load_webhooks( 'active', $limit ); } /** * Initialize the customer and cart objects and setup customer saving on shutdown. * * @since 3.6.4 * @return void */ public function initialize_cart() { // Cart needs customer info. if ( is_null( $this->customer ) || ! $this->customer instanceof WC_Customer ) { $this->customer = new WC_Customer( get_current_user_id(), true ); // Customer should be saved during shutdown. add_action( 'shutdown', array( $this->customer, 'save' ), 10 ); } if ( is_null( $this->cart ) || ! $this->cart instanceof WC_Cart ) { $this->cart = new WC_Cart(); } } /** * Initialize the session class. * * @since 3.6.4 * @return void */ public function initialize_session() { /** * Filter to overwrite the session class that handles session data for users. */ $session_class = apply_filters( 'woocommerce_session_handler', 'WC_Session_Handler' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment if ( is_null( $this->session ) || ! $this->session instanceof $session_class ) { $this->session = new $session_class(); $this->session->init(); } } /** * Set tablenames inside WPDB object. */ public function wpdb_table_fix() { $this->define_tables(); } /** * Ran when any plugin is activated. * * @since 3.6.0 * @param string $filename The filename of the activated plugin. */ public function activated_plugin( $filename ) { include_once dirname( __FILE__ ) . '/admin/helper/class-wc-helper.php'; if ( '/woocommerce.php' === substr( $filename, -16 ) ) { set_transient( 'woocommerce_activated_plugin', $filename ); } WC_Helper::activated_plugin( $filename ); } /** * Ran when any plugin is deactivated. * * @since 3.6.0 * @param string $filename The filename of the deactivated plugin. */ public function deactivated_plugin( $filename ) { include_once dirname( __FILE__ ) . '/admin/helper/class-wc-helper.php'; WC_Helper::deactivated_plugin( $filename ); } /** * Get queue instance. * * @return WC_Queue_Interface */ public function queue() { return WC_Queue::instance(); } /** * Get Checkout Class. * * @return WC_Checkout */ public function checkout() { return WC_Checkout::instance(); } /** * Get gateways class. * * @return WC_Payment_Gateways */ public function payment_gateways() { return WC_Payment_Gateways::instance(); } /** * Get shipping class. * * @return WC_Shipping */ public function shipping() { return WC_Shipping::instance(); } /** * Email Class. * * @return WC_Emails */ public function mailer() { return WC_Emails::instance(); } /** * Check if plugin assets are built and minified * * @return bool */ public function build_dependencies_satisfied() { // Check if we have compiled CSS. if ( ! file_exists( WC()->plugin_path() . '/assets/css/admin.css' ) ) { return false; } // Check if we have minified JS. if ( ! file_exists( WC()->plugin_path() . '/assets/js/admin/woocommerce_admin.min.js' ) ) { return false; } return true; } /** * Output a admin notice when build dependencies not met. * * @return void */ public function build_dependencies_notice() { if ( $this->build_dependencies_satisfied() ) { return; } $message_one = __( 'You have installed a development version of WooCommerce which requires files to be built and minified. From the plugin directory, run pnpm install and then pnpm run build --filter=woocommerce to build and minify assets.', 'woocommerce' ); $message_two = sprintf( /* translators: 1: URL of WordPress.org Repository 2: URL of the GitHub Repository release page */ __( 'Or you can download a pre-built version of the plugin from the WordPress.org repository or by visiting the releases page in the GitHub repository.', 'woocommerce' ), 'https://wordpress.org/plugins/woocommerce/', 'https://github.com/woocommerce/woocommerce/releases' ); printf( '

%s %s

', $message_one, $message_two ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Is the WooCommerce Admin actively included in the WooCommerce core? * Based on presence of a basic WC Admin function. * * @return boolean */ public function is_wc_admin_active() { return function_exists( 'wc_admin_url' ); } /** * Call a user function. This should be used to execute any non-idempotent function, especially * those in the `includes` directory or provided by WordPress. * * This method can be useful for unit tests, since functions called using this method * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_function_mocks. * * @param string $function_name The function to execute. * @param mixed ...$parameters The parameters to pass to the function. * * @return mixed The result from the function. * * @since 4.4 */ public function call_function( $function_name, ...$parameters ) { return wc_get_container()->get( LegacyProxy::class )->call_function( $function_name, ...$parameters ); } /** * Call a static method in a class. This should be used to execute any non-idempotent method in classes * from the `includes` directory. * * This method can be useful for unit tests, since methods called using this method * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_static_mocks. * * @param string $class_name The name of the class containing the method. * @param string $method_name The name of the method. * @param mixed ...$parameters The parameters to pass to the method. * * @return mixed The result from the method. * * @since 4.4 */ public function call_static( $class_name, $method_name, ...$parameters ) { return wc_get_container()->get( LegacyProxy::class )->call_static( $class_name, $method_name, ...$parameters ); } /** * Gets an instance of a given legacy class. * This must not be used to get instances of classes in the `src` directory. * * This method can be useful for unit tests, since objects obtained using this method * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_class_mocks. * * @param string $class_name The name of the class to get an instance for. * @param mixed ...$args Parameters to be passed to the class constructor or to the appropriate internal 'get_instance_of_' method. * * @return object The instance of the class. * @throws \Exception The requested class belongs to the `src` directory, or there was an error creating an instance of the class. * * @since 4.4 */ public function get_instance_of( string $class_name, ...$args ) { return wc_get_container()->get( LegacyProxy::class )->get_instance_of( $class_name, ...$args ); } /** * Gets the value of a global. * * @param string $global_name The name of the global to get the value for. * @return mixed The value of the global. */ public function get_global( string $global_name ) { return wc_get_container()->get( LegacyProxy::class )->get_global( $global_name ); } } PKK[î¢?ï±±wc-formatting-functions.phpnu„[µü¤strip_invalid_text_for_column( $wpdb->options, 'option_value', $value ?? '' ); if ( is_wp_error( $value ) ) { $value = ''; } $value = esc_url_raw( trim( $value ) ); $value = str_replace( 'http://', '', $value ); return untrailingslashit( $value ); } /** * Gets the filename part of a download URL. * * @param string $file_url File URL. * @return string */ function wc_get_filename_from_url( $file_url ) { $parts = wp_parse_url( $file_url ); if ( isset( $parts['path'] ) ) { return basename( $parts['path'] ); } } /** * Normalise dimensions, unify to cm then convert to wanted unit value. * * Usage: * wc_get_dimension( 55, 'in' ); * wc_get_dimension( 55, 'in', 'm' ); * * @param int|float $dimension Dimension. * @param string $to_unit Unit to convert to. * Options: 'in', 'mm', 'cm', 'm'. * @param string $from_unit Unit to convert from. * Defaults to ''. * Options: 'in', 'mm', 'cm', 'm'. * @return float */ function wc_get_dimension( $dimension, $to_unit, $from_unit = '' ) { $to_unit = strtolower( $to_unit ); if ( empty( $from_unit ) ) { $from_unit = strtolower( get_option( 'woocommerce_dimension_unit' ) ); } // Unify all units to cm first. if ( $from_unit !== $to_unit ) { switch ( $from_unit ) { case 'in': $dimension *= 2.54; break; case 'm': $dimension *= 100; break; case 'mm': $dimension *= 0.1; break; case 'yd': $dimension *= 91.44; break; } // Output desired unit. switch ( $to_unit ) { case 'in': $dimension *= 0.3937; break; case 'm': $dimension *= 0.01; break; case 'mm': $dimension *= 10; break; case 'yd': $dimension *= 0.010936133; break; } } return ( $dimension < 0 ) ? 0 : $dimension; } /** * Normalise weights, unify to kg then convert to wanted unit value. * * Usage: * wc_get_weight(55, 'kg'); * wc_get_weight(55, 'kg', 'lbs'); * * @param int|float $weight Weight. * @param string $to_unit Unit to convert to. * Options: 'g', 'kg', 'lbs', 'oz'. * @param string $from_unit Unit to convert from. * Defaults to ''. * Options: 'g', 'kg', 'lbs', 'oz'. * @return float */ function wc_get_weight( $weight, $to_unit, $from_unit = '' ) { $weight = (float) $weight; $to_unit = strtolower( $to_unit ); if ( empty( $from_unit ) ) { $from_unit = strtolower( get_option( 'woocommerce_weight_unit' ) ); } // Unify all units to kg first. if ( $from_unit !== $to_unit ) { switch ( $from_unit ) { case 'g': $weight *= 0.001; break; case 'lbs': $weight *= 0.453592; break; case 'oz': $weight *= 0.0283495; break; } // Output desired unit. switch ( $to_unit ) { case 'g': $weight *= 1000; break; case 'lbs': $weight *= 2.20462; break; case 'oz': $weight *= 35.274; break; } } return ( $weight < 0 ) ? 0 : $weight; } /** * Trim trailing zeros off prices. * * @param string|float|int $price Price. * @return string */ function wc_trim_zeros( $price ) { return preg_replace( '/' . preg_quote( wc_get_price_decimal_separator(), '/' ) . '0++$/', '', $price ?? '' ); } /** * Round a tax amount. * * @param double $value Amount to round. * @param int $precision DP to round. Defaults to wc_get_price_decimals. * @return float */ function wc_round_tax_total( $value, $precision = null ) { $precision = is_null( $precision ) ? wc_get_price_decimals() : intval( $precision ); $rounded_tax = NumberUtil::round( $value, $precision, wc_get_tax_rounding_mode() ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.round_modeFound return apply_filters( 'wc_round_tax_total', $rounded_tax, $value, $precision, WC_TAX_ROUNDING_MODE ); } /** * Round half down in PHP 5.2. * * @since 3.2.6 * @param float $value Value to round. * @param int $precision Precision to round down to. * @return float */ function wc_legacy_round_half_down( $value, $precision ) { $value = wc_float_to_string( $value ) ?? ''; if ( false !== strstr( $value, '.' ) ) { $value = explode( '.', $value ); if ( strlen( $value[1] ) > $precision && substr( $value[1], -1 ) === '5' ) { $value[1] = substr( $value[1], 0, -1 ) . '4'; } $value = implode( '.', $value ); } return NumberUtil::round( floatval( $value ), $precision ); } /** * Make a refund total negative. * * @param float $amount Refunded amount. * * @return float */ function wc_format_refund_total( $amount ) { return $amount * -1; } /** * Format decimal numbers ready for DB storage. * * Sanitize, optionally remove decimals, and optionally round + trim off zeros. * * This function does not remove thousands - this should be done before passing a value to the function. * * @param float|string $number Expects either a float or a string with a decimal separator only (no thousands). * @param mixed $dp number Number of decimal points to use, blank to use woocommerce_price_num_decimals, or false to avoid all rounding. * @param bool $trim_zeros From end of string. * @return string */ function wc_format_decimal( $number, $dp = false, $trim_zeros = false ) { $number = $number ?? ''; $locale = localeconv(); $decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'] ); // Remove locale from string. if ( ! is_float( $number ) ) { $number = str_replace( $decimals, '.', $number ); // Convert multiple dots to just one. $number = preg_replace( '/\.(?![^.]+$)|[^0-9.-]/', '', wc_clean( $number ) ); } if ( false !== $dp ) { $dp = intval( '' === $dp ? wc_get_price_decimals() : $dp ); $number = number_format( floatval( $number ), $dp, '.', '' ); } elseif ( is_float( $number ) ) { // DP is false - don't use number format, just return a string using whatever is given. Remove scientific notation using sprintf. $number = str_replace( $decimals, '.', sprintf( '%.' . wc_get_rounding_precision() . 'f', $number ) ); // We already had a float, so trailing zeros are not needed. $trim_zeros = true; } if ( $trim_zeros && strstr( $number, '.' ) ) { $number = rtrim( rtrim( $number, '0' ), '.' ); } return $number; } /** * Convert a float to a string without locale formatting which PHP adds when changing floats to strings. * * @param float $float Float value to format. * @return string */ function wc_float_to_string( $float ) { if ( ! is_float( $float ) ) { return $float; } $locale = localeconv(); $string = strval( $float ); $string = str_replace( $locale['decimal_point'], '.', $string ); return $string; } /** * Format a price with WC Currency Locale settings. * * @param string $value Price to localize. * @return string */ function wc_format_localized_price( $value ) { return apply_filters( 'woocommerce_format_localized_price', str_replace( '.', wc_get_price_decimal_separator(), strval( $value ) ), $value ); } /** * Format a decimal with the decimal separator for prices or PHP Locale settings. * * @param string $value Decimal to localize. * @return string */ function wc_format_localized_decimal( $value ) { $locale = localeconv(); $decimal_point = isset( $locale['decimal_point'] ) ? $locale['decimal_point'] : '.'; $decimal = ( ! empty( wc_get_price_decimal_separator() ) ) ? wc_get_price_decimal_separator() : $decimal_point; return apply_filters( 'woocommerce_format_localized_decimal', str_replace( '.', $decimal, strval( $value ) ), $value ); } /** * Format a coupon code. * * @since 3.0.0 * @param string $value Coupon code to format. * @return string */ function wc_format_coupon_code( $value ) { return apply_filters( 'woocommerce_coupon_code', $value ); } /** * Sanitize a coupon code. * * Uses sanitize_post_field since coupon codes are stored as * post_titles - the sanitization and escaping must match. * * @since 3.6.0 * @param string $value Coupon code to format. * @return string */ function wc_sanitize_coupon_code( $value ) { return wp_filter_kses( sanitize_post_field( 'post_title', $value ?? '', 0, 'db' ) ); } /** * Clean variables using sanitize_text_field. Arrays are cleaned recursively. * Non-scalar values are ignored. * * @param string|array $var Data to sanitize. * @return string|array */ function wc_clean( $var ) { if ( is_array( $var ) ) { return array_map( 'wc_clean', $var ); } else { return is_scalar( $var ) ? sanitize_text_field( $var ) : $var; } } /** * Function wp_check_invalid_utf8 with recursive array support. * * @param string|array $var Data to sanitize. * @return string|array */ function wc_check_invalid_utf8( $var ) { if ( is_array( $var ) ) { return array_map( 'wc_check_invalid_utf8', $var ); } else { return wp_check_invalid_utf8( $var ); } } /** * Run wc_clean over posted textarea but maintain line breaks. * * @since 3.0.0 * @param string $var Data to sanitize. * @return string */ function wc_sanitize_textarea( $var ) { return implode( "\n", array_map( 'wc_clean', explode( "\n", $var ?? '' ) ) ); } /** * Sanitize a string destined to be a tooltip. * * @since 2.3.10 Tooltips are encoded with htmlspecialchars to prevent XSS. Should not be used in conjunction with esc_attr() * @param string $var Data to sanitize. * @return string */ function wc_sanitize_tooltip( $var ) { return htmlspecialchars( wp_kses( html_entity_decode( $var ?? '' ), array( 'br' => array(), 'em' => array(), 'strong' => array(), 'small' => array(), 'span' => array(), 'ul' => array(), 'li' => array(), 'ol' => array(), 'p' => array(), ) ) ); } /** * Merge two arrays. * * @param array $a1 First array to merge. * @param array $a2 Second array to merge. * @return array */ function wc_array_overlay( $a1, $a2 ) { foreach ( $a1 as $k => $v ) { if ( ! array_key_exists( $k, $a2 ) ) { continue; } if ( is_array( $v ) && is_array( $a2[ $k ] ) ) { $a1[ $k ] = wc_array_overlay( $v, $a2[ $k ] ); } else { $a1[ $k ] = $a2[ $k ]; } } return $a1; } /** * Formats a stock amount by running it through a filter. * * @param int|float $amount Stock amount. * @return int|float */ function wc_stock_amount( $amount ) { return apply_filters( 'woocommerce_stock_amount', $amount ); } /** * Get the price format depending on the currency position. * * @return string */ function get_woocommerce_price_format() { $currency_pos = get_option( 'woocommerce_currency_pos' ); $format = '%1$s%2$s'; switch ( $currency_pos ) { case 'left': $format = '%1$s%2$s'; break; case 'right': $format = '%2$s%1$s'; break; case 'left_space': $format = '%1$s %2$s'; break; case 'right_space': $format = '%2$s %1$s'; break; } return apply_filters( 'woocommerce_price_format', $format, $currency_pos ); } /** * Return the thousand separator for prices. * * @since 2.3 * @return string */ function wc_get_price_thousand_separator() { return stripslashes( apply_filters( 'wc_get_price_thousand_separator', get_option( 'woocommerce_price_thousand_sep' ) ) ); } /** * Return the decimal separator for prices. * * @since 2.3 * @return string */ function wc_get_price_decimal_separator() { $separator = apply_filters( 'wc_get_price_decimal_separator', get_option( 'woocommerce_price_decimal_sep' ) ); return $separator ? stripslashes( $separator ) : '.'; } /** * Return the number of decimals after the decimal point. * * @since 2.3 * @return int */ function wc_get_price_decimals() { return absint( apply_filters( 'wc_get_price_decimals', get_option( 'woocommerce_price_num_decimals', 2 ) ) ); } /** * Format the price with a currency symbol. * * @param float $price Raw price. * @param array $args Arguments to format a price { * Array of arguments. * Defaults to empty array. * * @type bool $ex_tax_label Adds exclude tax label. * Defaults to false. * @type string $currency Currency code. * Defaults to empty string (Use the result from get_woocommerce_currency()). * @type string $decimal_separator Decimal separator. * Defaults the result of wc_get_price_decimal_separator(). * @type string $thousand_separator Thousand separator. * Defaults the result of wc_get_price_thousand_separator(). * @type string $decimals Number of decimals. * Defaults the result of wc_get_price_decimals(). * @type string $price_format Price format depending on the currency position. * Defaults the result of get_woocommerce_price_format(). * } * @return string */ function wc_price( $price, $args = array() ) { $args = apply_filters( 'wc_price_args', wp_parse_args( $args, array( 'ex_tax_label' => false, 'currency' => '', 'decimal_separator' => wc_get_price_decimal_separator(), 'thousand_separator' => wc_get_price_thousand_separator(), 'decimals' => wc_get_price_decimals(), 'price_format' => get_woocommerce_price_format(), ) ) ); $original_price = $price; // Convert to float to avoid issues on PHP 8. $price = (float) $price; $unformatted_price = $price; $negative = $price < 0; /** * Filter raw price. * * @param float $raw_price Raw price. * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. */ $price = apply_filters( 'raw_woocommerce_price', $negative ? $price * -1 : $price, $original_price ); /** * Filter formatted price. * * @param float $formatted_price Formatted price. * @param float $price Unformatted price. * @param int $decimals Number of decimals. * @param string $decimal_separator Decimal separator. * @param string $thousand_separator Thousand separator. * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. */ $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 ); if ( apply_filters( 'woocommerce_price_trim_zeros', false ) && $args['decimals'] > 0 ) { $price = wc_trim_zeros( $price ); } $formatted_price = ( $negative ? '-' : '' ) . sprintf( $args['price_format'], '' . get_woocommerce_currency_symbol( $args['currency'] ) . '', $price ); $return = '' . $formatted_price . ''; if ( $args['ex_tax_label'] && wc_tax_enabled() ) { $return .= ' ' . WC()->countries->ex_tax_or_vat() . ''; } /** * Filters the string of price markup. * * @param string $return Price HTML markup. * @param string $price Formatted price. * @param array $args Pass on the args. * @param float $unformatted_price Price as float to allow plugins custom formatting. Since 3.2.0. * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. */ return apply_filters( 'wc_price', $return, $price, $args, $unformatted_price, $original_price ); } /** * Notation to numbers. * * This function transforms the php.ini notation for numbers (like '2M') to an integer. * * @param string $size Size value. * @return int */ function wc_let_to_num( $size ) { $size = $size ?? ''; $l = substr( $size, -1 ); $ret = (int) substr( $size, 0, -1 ); switch ( strtoupper( $l ) ) { case 'P': $ret *= 1024; // No break. case 'T': $ret *= 1024; // No break. case 'G': $ret *= 1024; // No break. case 'M': $ret *= 1024; // No break. case 'K': $ret *= 1024; // No break. } return $ret; } /** * WooCommerce Date Format - Allows to change date format for everything WooCommerce. * * @return string */ function wc_date_format() { $date_format = get_option( 'date_format' ); if ( empty( $date_format ) ) { // Return default date format if the option is empty. $date_format = 'F j, Y'; } return apply_filters( 'woocommerce_date_format', $date_format ); } /** * WooCommerce Time Format - Allows to change time format for everything WooCommerce. * * @return string */ function wc_time_format() { $time_format = get_option( 'time_format' ); if ( empty( $time_format ) ) { // Return default time format if the option is empty. $time_format = 'g:i a'; } return apply_filters( 'woocommerce_time_format', $time_format ); } /** * Convert mysql datetime to PHP timestamp, forcing UTC. Wrapper for strtotime. * * Based on wcs_strtotime_dark_knight() from WC Subscriptions by Prospress. * * @since 3.0.0 * @param string $time_string Time string. * @param int|null $from_timestamp Timestamp to convert from. * @return int */ function wc_string_to_timestamp( $time_string, $from_timestamp = null ) { $time_string = $time_string ?? ''; $original_timezone = date_default_timezone_get(); // @codingStandardsIgnoreStart date_default_timezone_set( 'UTC' ); if ( null === $from_timestamp ) { $next_timestamp = strtotime( $time_string ); } else { $next_timestamp = strtotime( $time_string, $from_timestamp ); } date_default_timezone_set( $original_timezone ); // @codingStandardsIgnoreEnd return $next_timestamp; } /** * Convert a date string to a WC_DateTime. * * @since 3.1.0 * @param string $time_string Time string. * @return WC_DateTime */ function wc_string_to_datetime( $time_string ) { $time_string = $time_string ?? ''; // Strings are defined in local WP timezone. Convert to UTC. 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 ) ) { $offset = ! empty( $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 ?? '000' ); // 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 ) { $phone = $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 ) { $string = $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 ) { $string = $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 = '...' ) { $string = $string ?? ''; 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 ) { $raw_string = $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( $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_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit' ) ); $weight_string = sprintf( // translators: 1. A formatted number; 2. A label for a weight unit of measure. E.g. 2.72 kg. _x( '%1$s %2$s', 'formatted weight', 'woocommerce' ), $weight_string, $weight_label ); } 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_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit' ) ); $dimension_string = sprintf( // translators: 1. A formatted number; 2. A label for a dimensions unit of measure. E.g. 3.14 cm. _x( '%1$s %2$s', 'formatted dimensions', 'woocommerce' ), $dimension_string, $dimension_label ); } 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: `&` -> `&amp;`. ); } /** * 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 ); PKK[ↄµq?q?class-wc-comments.phpnu„[µü¤comment_post_ID ) should keep them safe since only admin and. * shop managers can view orders anyway. * * The frontend view order pages get around this filter by using remove_filter('comments_clauses', array( 'WC_Comments' ,'exclude_order_comments'), 10, 1 ); * * @param array $clauses A compacted array of comment query clauses. * @return array */ public static function exclude_order_comments( $clauses ) { $clauses['where'] .= ( $clauses['where'] ? ' AND ' : '' ) . " comment_type != 'order_note' "; return $clauses; } /** * Exclude order comments from feed. * * @deprecated 3.1 * @param mixed $join Deprecated. */ public static function exclude_order_comments_from_feed_join( $join ) { wc_deprecated_function( 'WC_Comments::exclude_order_comments_from_feed_join', '3.1' ); } /** * Exclude order comments from queries and RSS. * * @param string $where The WHERE clause of the query. * @return string */ public static function exclude_order_comments_from_feed_where( $where ) { return $where . ( $where ? ' AND ' : '' ) . " comment_type != 'order_note' "; } /** * Exclude webhook comments from queries and RSS. * * @since 2.2 * @param array $clauses A compacted array of comment query clauses. * @return array */ public static function exclude_webhook_comments( $clauses ) { $clauses['where'] .= ( $clauses['where'] ? ' AND ' : '' ) . " comment_type != 'webhook_delivery' "; return $clauses; } /** * Exclude webhooks comments from feed. * * @deprecated 3.1 * @param mixed $join Deprecated. */ public static function exclude_webhook_comments_from_feed_join( $join ) { wc_deprecated_function( 'WC_Comments::exclude_webhook_comments_from_feed_join', '3.1' ); } /** * Exclude webhook comments from queries and RSS. * * @since 2.1 * @param string $where The WHERE clause of the query. * @return string */ public static function exclude_webhook_comments_from_feed_where( $where ) { return $where . ( $where ? ' AND ' : '' ) . " comment_type != 'webhook_delivery' "; } /** * Validate the comment ratings. * * @param array $comment_data Comment data. * @return array */ public static function check_comment_rating( $comment_data ) { // If posting a comment (not trackback etc) and not logged in. if ( ! is_admin() && isset( $_POST['comment_post_ID'], $_POST['rating'], $comment_data['comment_type'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) && empty( $_POST['rating'] ) && self::is_default_comment_type( $comment_data['comment_type'] ) && wc_review_ratings_enabled() && wc_review_ratings_required() ) { // WPCS: input var ok, CSRF ok. wp_die( esc_html__( 'Please rate the product.', 'woocommerce' ) ); exit; } return $comment_data; } /** * Rating field for comments. * * @param int $comment_id Comment ID. */ public static function add_comment_rating( $comment_id ) { if ( isset( $_POST['rating'], $_POST['comment_post_ID'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) ) { // WPCS: input var ok, CSRF ok. if ( ! $_POST['rating'] || $_POST['rating'] > 5 || $_POST['rating'] < 0 ) { // WPCS: input var ok, CSRF ok, sanitization ok. return; } add_comment_meta( $comment_id, 'rating', intval( $_POST['rating'] ), true ); // WPCS: input var ok, CSRF ok. $post_id = isset( $_POST['comment_post_ID'] ) ? absint( $_POST['comment_post_ID'] ) : 0; // WPCS: input var ok, CSRF ok. if ( $post_id ) { self::clear_transients( $post_id ); } } } /** * Modify recipient of review email. * * @param array $emails Emails. * @param int $comment_id Comment ID. * @return array */ public static function comment_moderation_recipients( $emails, $comment_id ) { $comment = get_comment( $comment_id ); if ( $comment && 'product' === get_post_type( $comment->comment_post_ID ) ) { $emails = array( get_option( 'admin_email' ) ); } return $emails; } /** * Ensure product average rating and review count is kept up to date. * * @param int $post_id Post ID. */ public static function clear_transients( $post_id ) { if ( 'product' === get_post_type( $post_id ) ) { $product = wc_get_product( $post_id ); $product->set_rating_counts( self::get_rating_counts_for_product( $product ) ); $product->set_average_rating( self::get_average_rating_for_product( $product ) ); $product->set_review_count( self::get_review_count_for_product( $product ) ); $product->save(); } } /** * Delete comments count cache whenever there is * new comment or the status of a comment changes. Cache * will be regenerated next time WC_Comments::wp_count_comments() * is called. */ public static function delete_comments_count_cache() { delete_transient( 'wc_count_comments' ); } /** * Remove order notes, webhook delivery logs, and product reviews from wp_count_comments(). * * @since 2.2 * @param object $stats Comment stats. * @param int $post_id Post ID. * @return object */ public static function wp_count_comments( $stats, $post_id ) { global $wpdb; if ( 0 === $post_id ) { $stats = get_transient( 'wc_count_comments' ); if ( ! $stats ) { $stats = array( 'total_comments' => 0, 'all' => 0, ); $count = $wpdb->get_results( " SELECT comment_approved, COUNT(*) AS num_comments FROM {$wpdb->comments} LEFT JOIN {$wpdb->posts} ON comment_post_ID = {$wpdb->posts}.ID WHERE comment_type NOT IN ('action_log', 'order_note', 'webhook_delivery') AND {$wpdb->posts}.post_type NOT IN ('product') GROUP BY comment_approved ", ARRAY_A ); $approved = array( '0' => 'moderated', '1' => 'approved', 'spam' => 'spam', 'trash' => 'trash', 'post-trashed' => 'post-trashed', ); foreach ( (array) $count as $row ) { // Don't count post-trashed toward totals. if ( ! in_array( $row['comment_approved'], array( 'post-trashed', 'trash', 'spam' ), true ) ) { $stats['all'] += $row['num_comments']; $stats['total_comments'] += $row['num_comments']; } elseif ( ! in_array( $row['comment_approved'], array( 'post-trashed', 'trash' ), true ) ) { $stats['total_comments'] += $row['num_comments']; } if ( isset( $approved[ $row['comment_approved'] ] ) ) { $stats[ $approved[ $row['comment_approved'] ] ] = $row['num_comments']; } } foreach ( $approved as $key ) { if ( empty( $stats[ $key ] ) ) { $stats[ $key ] = 0; } } $stats = (object) $stats; set_transient( 'wc_count_comments', $stats ); } } return $stats; } /** * Make sure WP displays avatars for comments with the `review` type. * * @since 2.3 * @param array $comment_types Comment types. * @return array */ public static function add_avatar_for_review_comment_type( $comment_types ) { return array_merge( $comment_types, array( 'review' ) ); } /** * Add Product Reviews filter for `review` comment type. * * @since 6.0.0 * * @param array $comment_types Array of comment type labels keyed by their name. * * @return array */ public static function add_review_comment_filter( array $comment_types ): array { $comment_types['review'] = __( 'Product Reviews', 'woocommerce' ); return $comment_types; } /** * Determine if a review is from a verified owner at submission. * * @param int $comment_id Comment ID. * @return bool */ public static function add_comment_purchase_verification( $comment_id ) { $comment = get_comment( $comment_id ); $verified = false; if ( 'product' === get_post_type( $comment->comment_post_ID ) ) { $verified = wc_customer_bought_product( $comment->comment_author_email, $comment->user_id, $comment->comment_post_ID ); add_comment_meta( $comment_id, 'verified', (int) $verified, true ); } return $verified; } /** * Get product rating for a product. Please note this is not cached. * * @since 3.0.0 * @param WC_Product $product Product instance. * @return float */ public static function get_average_rating_for_product( &$product ) { global $wpdb; $count = $product->get_rating_count(); if ( $count ) { $ratings = $wpdb->get_var( $wpdb->prepare( " SELECT SUM(meta_value) FROM $wpdb->commentmeta LEFT JOIN $wpdb->comments ON $wpdb->commentmeta.comment_id = $wpdb->comments.comment_ID WHERE meta_key = 'rating' AND comment_post_ID = %d AND comment_approved = '1' AND meta_value > 0 ", $product->get_id() ) ); $average = number_format( $ratings / $count, 2, '.', '' ); } else { $average = 0; } return $average; } /** * Utility function for getting review counts for multiple products in one query. This is not cached. * * @since 5.0.0 * * @param array $product_ids Array of product IDs. * * @return array */ public static function get_review_counts_for_product_ids( $product_ids ) { global $wpdb; if ( empty( $product_ids ) ) { return array(); } $product_id_string_placeholder = substr( str_repeat( ',%s', count( $product_ids ) ), 1 ); $review_counts = $wpdb->get_results( // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Ignored for allowing interpolation in IN query. $wpdb->prepare( " SELECT comment_post_ID as product_id, COUNT( comment_post_ID ) as review_count FROM $wpdb->comments WHERE comment_parent = 0 AND comment_post_ID IN ( $product_id_string_placeholder ) AND comment_approved = '1' AND comment_type in ( 'review', '', 'comment' ) GROUP BY product_id ", $product_ids ), // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared. ARRAY_A ); // Convert to key value pairs. $counts = array_replace( array_fill_keys( $product_ids, 0 ), array_column( $review_counts, 'review_count', 'product_id' ) ); return $counts; } /** * Get product review count for a product (not replies). Please note this is not cached. * * @since 3.0.0 * @param WC_Product $product Product instance. * @return int */ public static function get_review_count_for_product( &$product ) { $counts = self::get_review_counts_for_product_ids( array( $product->get_id() ) ); return $counts[ $product->get_id() ]; } /** * Get product rating count for a product. Please note this is not cached. * * @since 3.0.0 * @param WC_Product $product Product instance. * @return int[] */ public static function get_rating_counts_for_product( &$product ) { global $wpdb; $counts = array(); $raw_counts = $wpdb->get_results( $wpdb->prepare( " SELECT meta_value, COUNT( * ) as meta_value_count FROM $wpdb->commentmeta LEFT JOIN $wpdb->comments ON $wpdb->commentmeta.comment_id = $wpdb->comments.comment_ID WHERE meta_key = 'rating' AND comment_post_ID = %d AND comment_approved = '1' AND meta_value > 0 GROUP BY meta_value ", $product->get_id() ) ); foreach ( $raw_counts as $count ) { $counts[ $count->meta_value ] = absint( $count->meta_value_count ); // WPCS: slow query ok. } return $counts; } /** * Update comment type of product reviews. * * @since 3.5.0 * @param array $comment_data Comment data. * @return array */ public static function update_comment_type( $comment_data ) { if ( ! is_admin() && isset( $_POST['comment_post_ID'], $comment_data['comment_type'] ) && self::is_default_comment_type( $comment_data['comment_type'] ) && 'product' === get_post_type( absint( $_POST['comment_post_ID'] ) ) ) { // WPCS: input var ok, CSRF ok. $comment_data['comment_type'] = 'review'; } return $comment_data; } /** * Validate product reviews if requires a verified owner. * * @param int $comment_post_id Post ID. */ public static function validate_product_review_verified_owners( $comment_post_id ) { // Only validate if option is enabled. if ( 'yes' !== get_option( 'woocommerce_review_rating_verification_required' ) ) { return; } // Validate only products. if ( 'product' !== get_post_type( $comment_post_id ) ) { return; } // Skip if is a verified owner. if ( wc_customer_bought_product( '', get_current_user_id(), $comment_post_id ) ) { return; } wp_die( esc_html__( 'Only logged in customers who have purchased this product may leave a review.', 'woocommerce' ), esc_html__( 'Reviews can only be left by "verified owners"', 'woocommerce' ), array( 'code' => 403, ) ); } /** * Determines if a comment is of the default type. * * Prior to WordPress 5.5, '' was the default comment type. * As of 5.5, the default type is 'comment'. * * @since 4.3.0 * @param string $comment_type Comment type. * @return bool */ private static function is_default_comment_type( $comment_type ) { return ( '' === $comment_type || 'comment' === $comment_type ); } } WC_Comments::init(); PKK[qZíg¦ƒ¦ƒclass-wc-discounts.phpnu„[µü¤ Item Key => Value */ protected $discounts = array(); /** * WC_Discounts Constructor. * * @param WC_Cart|WC_Order $object Cart or order object. */ public function __construct( $object = null ) { if ( is_a( $object, 'WC_Cart' ) ) { $this->set_items_from_cart( $object ); } elseif ( is_a( $object, 'WC_Order' ) ) { $this->set_items_from_order( $object ); } } /** * Set items directly. Used by WC_Cart_Totals. * * @since 3.2.3 * @param array $items Items to set. */ public function set_items( $items ) { $this->items = $items; $this->discounts = array(); uasort( $this->items, array( $this, 'sort_by_price' ) ); } /** * Normalise cart items which will be discounted. * * @since 3.2.0 * @param WC_Cart $cart Cart object. */ public function set_items_from_cart( $cart ) { $this->items = array(); $this->discounts = array(); if ( ! is_a( $cart, 'WC_Cart' ) ) { return; } $this->object = $cart; foreach ( $cart->get_cart() as $key => $cart_item ) { $item = new stdClass(); $item->key = $key; $item->object = $cart_item; $item->product = $cart_item['data']; $item->quantity = $cart_item['quantity']; $item->price = wc_add_number_precision_deep( (float) $item->product->get_price() * (float) $item->quantity ); $this->items[ $key ] = $item; } uasort( $this->items, array( $this, 'sort_by_price' ) ); } /** * Normalise order items which will be discounted. * * @since 3.2.0 * @param WC_Order $order Order object. */ public function set_items_from_order( $order ) { $this->items = array(); $this->discounts = array(); if ( ! is_a( $order, 'WC_Order' ) ) { return; } $this->object = $order; foreach ( $order->get_items() as $order_item ) { $item = new stdClass(); $item->key = $order_item->get_id(); $item->object = $order_item; $item->product = $order_item->get_product(); $item->quantity = $order_item->get_quantity(); $item->price = wc_add_number_precision_deep( $order_item->get_subtotal() ); if ( $order->get_prices_include_tax() ) { $item->price += wc_add_number_precision_deep( $order_item->get_subtotal_tax() ); } $this->items[ $order_item->get_id() ] = $item; } uasort( $this->items, array( $this, 'sort_by_price' ) ); } /** * Get the object concerned. * * @since 3.3.2 * @return object */ public function get_object() { return $this->object; } /** * Get items. * * @since 3.2.0 * @return object[] */ public function get_items() { return $this->items; } /** * Get items to validate. * * @since 3.3.2 * @return object[] */ public function get_items_to_validate() { return apply_filters( 'woocommerce_coupon_get_items_to_validate', $this->get_items(), $this ); } /** * Get discount by key with or without precision. * * @since 3.2.0 * @param string $key name of discount row to return. * @param bool $in_cents Should the totals be returned in cents, or without precision. * @return float */ public function get_discount( $key, $in_cents = false ) { $item_discount_totals = $this->get_discounts_by_item( $in_cents ); return isset( $item_discount_totals[ $key ] ) ? $item_discount_totals[ $key ] : 0; } /** * Get all discount totals. * * @since 3.2.0 * @param bool $in_cents Should the totals be returned in cents, or without precision. * @return array */ public function get_discounts( $in_cents = false ) { $discounts = $this->discounts; return $in_cents ? $discounts : wc_remove_number_precision_deep( $discounts ); } /** * Get all discount totals per item. * * @since 3.2.0 * @param bool $in_cents Should the totals be returned in cents, or without precision. * @return array */ public function get_discounts_by_item( $in_cents = false ) { $discounts = $this->discounts; $item_discount_totals = (array) array_shift( $discounts ); foreach ( $discounts as $item_discounts ) { foreach ( $item_discounts as $item_key => $item_discount ) { $item_discount_totals[ $item_key ] += $item_discount; } } return $in_cents ? $item_discount_totals : wc_remove_number_precision_deep( $item_discount_totals ); } /** * Get all discount totals per coupon. * * @since 3.2.0 * @param bool $in_cents Should the totals be returned in cents, or without precision. * @return array */ public function get_discounts_by_coupon( $in_cents = false ) { $coupon_discount_totals = array_map( 'array_sum', $this->discounts ); return $in_cents ? $coupon_discount_totals : wc_remove_number_precision_deep( $coupon_discount_totals ); } /** * Get discounted price of an item without precision. * * @since 3.2.0 * @param object $item Get data for this item. * @return float */ public function get_discounted_price( $item ) { return wc_remove_number_precision_deep( $this->get_discounted_price_in_cents( $item ) ); } /** * Get discounted price of an item to precision (in cents). * * @since 3.2.0 * @param object $item Get data for this item. * @return int */ public function get_discounted_price_in_cents( $item ) { return absint( NumberUtil::round( $item->price - $this->get_discount( $item->key, true ) ) ); } /** * Apply a discount to all items using a coupon. * * @since 3.2.0 * @param WC_Coupon $coupon Coupon object being applied to the items. * @param bool $validate Set to false to skip coupon validation. * @throws Exception Error message when coupon isn't valid. * @return bool|WP_Error True if applied or WP_Error instance in failure. */ public function apply_coupon( $coupon, $validate = true ) { if ( ! is_a( $coupon, 'WC_Coupon' ) ) { return new WP_Error( 'invalid_coupon', __( 'Invalid coupon', 'woocommerce' ) ); } $is_coupon_valid = $validate ? $this->is_coupon_valid( $coupon ) : true; if ( is_wp_error( $is_coupon_valid ) ) { return $is_coupon_valid; } $coupon_code = $coupon->get_code(); if ( ! isset( $this->discounts[ $coupon_code ] ) || ! is_array( $this->discounts[ $coupon_code ] ) ) { $this->discounts[ $coupon_code ] = array_fill_keys( array_keys( $this->items ), 0 ); } $items_to_apply = $this->get_items_to_apply_coupon( $coupon ); // Core discounts are handled here as of 3.2. switch ( $coupon->get_discount_type() ) { case 'percent': $this->apply_coupon_percent( $coupon, $items_to_apply ); break; case 'fixed_product': $this->apply_coupon_fixed_product( $coupon, $items_to_apply ); break; case 'fixed_cart': $this->apply_coupon_fixed_cart( $coupon, $items_to_apply ); break; default: $this->apply_coupon_custom( $coupon, $items_to_apply ); break; } return true; } /** * Sort by price. * * @since 3.2.0 * @param array $a First element. * @param array $b Second element. * @return int */ protected function sort_by_price( $a, $b ) { $price_1 = $a->price * $a->quantity; $price_2 = $b->price * $b->quantity; if ( $price_1 === $price_2 ) { return 0; } return ( $price_1 < $price_2 ) ? 1 : -1; } /** * Filter out all products which have been fully discounted to 0. * Used as array_filter callback. * * @since 3.2.0 * @param object $item Get data for this item. * @return bool */ protected function filter_products_with_price( $item ) { return $this->get_discounted_price_in_cents( $item ) > 0; } /** * Get items which the coupon should be applied to. * * @since 3.2.0 * @param object $coupon Coupon object. * @return array */ protected function get_items_to_apply_coupon( $coupon ) { $items_to_apply = array(); foreach ( $this->get_items_to_validate() as $item ) { $item_to_apply = clone $item; // Clone the item so changes to this item do not affect the originals. if ( 0 === $this->get_discounted_price_in_cents( $item_to_apply ) || 0 >= $item_to_apply->quantity ) { continue; } if ( ! $coupon->is_valid_for_product( $item_to_apply->product, $item_to_apply->object ) && ! $coupon->is_valid_for_cart() ) { continue; } $items_to_apply[] = $item_to_apply; } return $items_to_apply; } /** * Apply percent discount to items and return an array of discounts granted. * * @since 3.2.0 * @param WC_Coupon $coupon Coupon object. Passed through filters. * @param array $items_to_apply Array of items to apply the coupon to. * @return int Total discounted. */ protected function apply_coupon_percent( $coupon, $items_to_apply ) { $total_discount = 0; $cart_total = 0; $limit_usage_qty = 0; $applied_count = 0; $adjust_final_discount = true; if ( null !== $coupon->get_limit_usage_to_x_items() ) { $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); } $coupon_amount = $coupon->get_amount(); foreach ( $items_to_apply as $item ) { // Find out how much price is available to discount for the item. $discounted_price = $this->get_discounted_price_in_cents( $item ); // Get the price we actually want to discount, based on settings. $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : NumberUtil::round( $item->price ); // See how many and what price to apply to. $apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); $price_to_discount = ( $price_to_discount / $item->quantity ) * $apply_quantity; // Run coupon calculations. $discount = floor( $price_to_discount * ( $coupon_amount / 100 ) ); if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) { // Send through the legacy filter, but not as cents. $filtered_discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) ); if ( $filtered_discount !== $discount ) { $discount = $filtered_discount; $adjust_final_discount = false; } } $discount = wc_round_discount( min( $discounted_price, $discount ), 0 ); $cart_total = $cart_total + $price_to_discount; $total_discount = $total_discount + $discount; $applied_count = $applied_count + $apply_quantity; // Store code and discount amount per item. $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; } // Work out how much discount would have been given to the cart as a whole and compare to what was discounted on all line items. $cart_total_discount = wc_round_discount( $cart_total * ( $coupon_amount / 100 ), 0 ); if ( $total_discount < $cart_total_discount && $adjust_final_discount ) { $total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $cart_total_discount - $total_discount ); } return $total_discount; } /** * Apply fixed product discount to items. * * @since 3.2.0 * @param WC_Coupon $coupon Coupon object. Passed through filters. * @param array $items_to_apply Array of items to apply the coupon to. * @param int $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon. * @return int Total discounted. */ protected function apply_coupon_fixed_product( $coupon, $items_to_apply, $amount = null ) { $total_discount = 0; $amount = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() ); $limit_usage_qty = 0; $applied_count = 0; if ( null !== $coupon->get_limit_usage_to_x_items() ) { $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); } foreach ( $items_to_apply as $item ) { // Find out how much price is available to discount for the item. $discounted_price = $this->get_discounted_price_in_cents( $item ); // Get the price we actually want to discount, based on settings. $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price; // Run coupon calculations. if ( $limit_usage_qty ) { $apply_quantity = $limit_usage_qty - $applied_count < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); $discount = min( $amount, $item->price / $item->quantity ) * $apply_quantity; } else { $apply_quantity = apply_filters( 'woocommerce_coupon_get_apply_quantity', $item->quantity, $item, $coupon, $this ); $discount = $amount * $apply_quantity; } if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) { // Send through the legacy filter, but not as cents. $discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) ); } $discount = min( $discounted_price, $discount ); $total_discount = $total_discount + $discount; $applied_count = $applied_count + $apply_quantity; // Store code and discount amount per item. $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; } return $total_discount; } /** * Apply fixed cart discount to items. * * @since 3.2.0 * @param WC_Coupon $coupon Coupon object. Passed through filters. * @param array $items_to_apply Array of items to apply the coupon to. * @param int $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon. * @return int Total discounted. */ protected function apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount = null ) { $total_discount = 0; $amount = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() ); $items_to_apply = array_filter( $items_to_apply, array( $this, 'filter_products_with_price' ) ); $item_count = array_sum( wp_list_pluck( $items_to_apply, 'quantity' ) ); if ( ! $item_count ) { return $total_discount; } if ( ! $amount ) { // If there is no amount we still send it through so filters are fired. $total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, 0 ); } else { $per_item_discount = absint( $amount / $item_count ); // round it down to the nearest cent. if ( $per_item_discount > 0 ) { $total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, $per_item_discount ); /** * If there is still discount remaining, repeat the process. */ if ( $total_discount > 0 && $total_discount < $amount ) { $total_discount += $this->apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount - $total_discount ); } } elseif ( $amount > 0 ) { $total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $amount ); } } return $total_discount; } /** * Apply custom coupon discount to items. * * @since 3.3 * @param WC_Coupon $coupon Coupon object. Passed through filters. * @param array $items_to_apply Array of items to apply the coupon to. * @return int Total discounted. */ protected function apply_coupon_custom( $coupon, $items_to_apply ) { $limit_usage_qty = 0; $applied_count = 0; if ( null !== $coupon->get_limit_usage_to_x_items() ) { $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); } // Apply the coupon to each item. foreach ( $items_to_apply as $item ) { // Find out how much price is available to discount for the item. $discounted_price = $this->get_discounted_price_in_cents( $item ); // Get the price we actually want to discount, based on settings. $price_to_discount = wc_remove_number_precision( ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price ); // See how many and what price to apply to. $apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); // Run coupon calculations. $discount = wc_add_number_precision( $coupon->get_discount_amount( $price_to_discount / $item->quantity, $item->object, true ) ) * $apply_quantity; $discount = wc_round_discount( min( $discounted_price, $discount ), 0 ); $applied_count = $applied_count + $apply_quantity; // Store code and discount amount per item. $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; } // Allow post-processing for custom coupon types (e.g. calculating discrepancy, etc). $this->discounts[ $coupon->get_code() ] = apply_filters( 'woocommerce_coupon_custom_discounts_array', $this->discounts[ $coupon->get_code() ], $coupon ); return array_sum( $this->discounts[ $coupon->get_code() ] ); } /** * Deal with remaining fractional discounts by splitting it over items * until the amount is expired, discounting 1 cent at a time. * * @since 3.2.0 * @param WC_Coupon $coupon Coupon object if applicable. Passed through filters. * @param array $items_to_apply Array of items to apply the coupon to. * @param int $amount Fixed discount amount to apply. * @return int Total discounted. */ protected function apply_coupon_remainder( $coupon, $items_to_apply, $amount ) { $total_discount = 0; foreach ( $items_to_apply as $item ) { for ( $i = 0; $i < $item->quantity; $i ++ ) { // Find out how much price is available to discount for the item. $price_to_discount = $this->get_discounted_price_in_cents( $item ); // Run coupon calculations. $discount = min( $price_to_discount, 1 ); // Store totals. $total_discount += $discount; // Store code and discount amount per item. $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; if ( $total_discount >= $amount ) { break 2; } } if ( $total_discount >= $amount ) { break; } } return $total_discount; } /** * Ensure coupon exists or throw exception. * * A coupon is also considered to no longer exist if it has been placed in the trash, even if the trash has not yet * been emptied. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_exists( $coupon ) { if ( ( ! $coupon->get_id() && ! $coupon->get_virtual() ) || 'trash' === $coupon->get_status() ) { /* translators: %s: coupon code */ throw new Exception( sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), esc_html( $coupon->get_code() ) ), 105 ); } return true; } /** * Ensure coupon usage limit is valid or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_usage_limit( $coupon ) { if ( ! $coupon->get_usage_limit() ) { return true; } $usage_count = $coupon->get_usage_count(); $data_store = $coupon->get_data_store(); $tentative_usage_count = is_callable( array( $data_store, 'get_tentative_usage_count' ) ) ? $data_store->get_tentative_usage_count( $coupon->get_id() ) : 0; if ( $usage_count + $tentative_usage_count < $coupon->get_usage_limit() ) { // All good. return true; } // Coupon usage limit is reached. Let's show as informative error message as we can. if ( 0 === $tentative_usage_count ) { // No held coupon, usage limit is indeed reached. $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; } elseif ( is_user_logged_in() ) { $recent_pending_orders = wc_get_orders( array( 'limit' => 1, 'post_status' => array( 'wc-failed', 'wc-pending' ), 'customer' => get_current_user_id(), 'return' => 'ids', ) ); if ( count( $recent_pending_orders ) > 0 ) { // User logged in and have a pending order, maybe they are trying to use the coupon. $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK; } else { $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; } } else { // Maybe this user was trying to use the coupon but got stuck. We can't know for sure (performantly). Show a slightly better error message. $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST; } throw new Exception( $coupon->get_coupon_error( $error_code ), $error_code ); } /** * Ensure coupon user usage limit is valid or throw exception. * * Per user usage limit - check here if user is logged in (against user IDs). * Checked again for emails later on in WC_Cart::check_customer_coupons(). * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @param int $user_id User ID. * @return bool */ protected function validate_coupon_user_usage_limit( $coupon, $user_id = 0 ) { if ( empty( $user_id ) ) { if ( $this->object instanceof WC_Order ) { $user_id = $this->object->get_customer_id(); } else { $user_id = get_current_user_id(); } } if ( $coupon && $user_id && apply_filters( 'woocommerce_coupon_validate_user_usage_limit', $coupon->get_usage_limit_per_user() > 0, $user_id, $coupon, $this ) && $coupon->get_id() && $coupon->get_data_store() ) { $data_store = $coupon->get_data_store(); $usage_count = $data_store->get_usage_by_user_id( $coupon, $user_id ); if ( $usage_count >= $coupon->get_usage_limit_per_user() ) { if ( $data_store->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) ) > 0 ) { $error_message = $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK ); $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK; } else { $error_message = $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED ); $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; } throw new Exception( $error_message, $error_code ); } } return true; } /** * Ensure coupon date is valid or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_expiry_date( $coupon ) { if ( $coupon->get_date_expires() && apply_filters( 'woocommerce_coupon_validate_expiry_date', time() > $coupon->get_date_expires()->getTimestamp(), $coupon, $this ) ) { throw new Exception( __( 'This coupon has expired.', 'woocommerce' ), 107 ); } return true; } /** * Ensure coupon amount is valid or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_minimum_amount( $coupon ) { $subtotal = wc_remove_number_precision( $this->get_object_subtotal() ); if ( $coupon->get_minimum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_minimum_amount', $coupon->get_minimum_amount() > $subtotal, $coupon, $subtotal ) ) { /* translators: %s: coupon minimum amount */ throw new Exception( sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_minimum_amount() ) ), 108 ); } return true; } /** * Ensure coupon amount is valid or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_maximum_amount( $coupon ) { $subtotal = wc_remove_number_precision( $this->get_object_subtotal() ); if ( $coupon->get_maximum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_maximum_amount', $coupon->get_maximum_amount() < $subtotal, $coupon ) ) { /* translators: %s: coupon maximum amount */ throw new Exception( sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_maximum_amount() ) ), 112 ); } return true; } /** * Ensure coupon is valid for products in the list is valid or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_product_ids( $coupon ) { if ( count( $coupon->get_product_ids() ) > 0 ) { $valid = false; foreach ( $this->get_items_to_validate() as $item ) { if ( $item->product && ( in_array( $item->product->get_id(), $coupon->get_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_product_ids(), true ) ) ) { $valid = true; break; } } if ( ! $valid ) { throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); } } return true; } /** * Ensure coupon is valid for product categories in the list is valid or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_product_categories( $coupon ) { if ( count( $coupon->get_product_categories() ) > 0 ) { $valid = false; foreach ( $this->get_items_to_validate() as $item ) { if ( $coupon->get_exclude_sale_items() && $item->product && $item->product->is_on_sale() ) { continue; } $product_cats = wc_get_product_cat_ids( $item->product->get_id() ); if ( $item->product->get_parent_id() ) { $product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) ); } // If we find an item with a cat in our allowed cat list, the coupon is valid. if ( count( array_intersect( $product_cats, $coupon->get_product_categories() ) ) > 0 ) { $valid = true; break; } } if ( ! $valid ) { throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); } } return true; } /** * Ensure coupon is valid for sale items in the list is valid or throw exception. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_sale_items( $coupon ) { if ( $coupon->get_exclude_sale_items() ) { $valid = true; foreach ( $this->get_items_to_validate() as $item ) { if ( $item->product && $item->product->is_on_sale() ) { $valid = false; break; } } if ( ! $valid ) { throw new Exception( __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' ), 110 ); } } return true; } /** * All exclusion rules must pass at the same time for a product coupon to be valid. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_excluded_items( $coupon ) { $items = $this->get_items_to_validate(); if ( ! empty( $items ) && $coupon->is_type( wc_get_product_coupon_types() ) ) { $valid = false; foreach ( $items as $item ) { if ( $item->product && $coupon->is_valid_for_product( $item->product, $item->object ) ) { $valid = true; break; } } if ( ! $valid ) { throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); } } return true; } /** * Cart discounts cannot be added if non-eligible product is found. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_eligible_items( $coupon ) { if ( ! $coupon->is_type( wc_get_product_coupon_types() ) ) { $this->validate_coupon_sale_items( $coupon ); $this->validate_coupon_excluded_product_ids( $coupon ); $this->validate_coupon_excluded_product_categories( $coupon ); } return true; } /** * Exclude products. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_excluded_product_ids( $coupon ) { // Exclude Products. if ( count( $coupon->get_excluded_product_ids() ) > 0 ) { $products = array(); foreach ( $this->get_items_to_validate() as $item ) { if ( $item->product && in_array( $item->product->get_id(), $coupon->get_excluded_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_excluded_product_ids(), true ) ) { $products[] = $item->product->get_name(); } } if ( ! empty( $products ) ) { /* translators: %s: products list */ throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ), 113 ); } } return true; } /** * Exclude categories from product list. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool */ protected function validate_coupon_excluded_product_categories( $coupon ) { if ( count( $coupon->get_excluded_product_categories() ) > 0 ) { $categories = array(); foreach ( $this->get_items_to_validate() as $item ) { if ( ! $item->product ) { continue; } $product_cats = wc_get_product_cat_ids( $item->product->get_id() ); if ( $item->product->get_parent_id() ) { $product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) ); } $cat_id_list = array_intersect( $product_cats, $coupon->get_excluded_product_categories() ); if ( count( $cat_id_list ) > 0 ) { foreach ( $cat_id_list as $cat_id ) { $cat = get_term( $cat_id, 'product_cat' ); $categories[] = $cat->name; } } } if ( ! empty( $categories ) ) { /* translators: %s: categories list */ throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) ), 114 ); } } return true; } /** * Get the object subtotal * * @return int */ protected function get_object_subtotal() { if ( is_a( $this->object, 'WC_Cart' ) ) { return wc_add_number_precision( $this->object->get_displayed_subtotal() ); } elseif ( is_a( $this->object, 'WC_Order' ) ) { $subtotal = wc_add_number_precision( $this->object->get_subtotal() ); if ( $this->object->get_prices_include_tax() ) { // Add tax to tax-exclusive subtotal. $subtotal = $subtotal + wc_add_number_precision( NumberUtil::round( $this->object->get_total_tax(), wc_get_price_decimals() ) ); } return $subtotal; } else { return array_sum( wp_list_pluck( $this->items, 'price' ) ); } } /** * Check if a coupon is valid. * * Error Codes: * - 100: Invalid filtered. * - 101: Invalid removed. * - 102: Not yours removed. * - 103: Already applied. * - 104: Individual use only. * - 105: Not exists. * - 106: Usage limit reached. * - 107: Expired. * - 108: Minimum spend limit not met. * - 109: Not applicable. * - 110: Not valid for sale items. * - 111: Missing coupon code. * - 112: Maximum spend limit met. * - 113: Excluded products. * - 114: Excluded categories. * * @since 3.2.0 * @throws Exception Error message. * @param WC_Coupon $coupon Coupon data. * @return bool|WP_Error */ public function is_coupon_valid( $coupon ) { try { $this->validate_coupon_exists( $coupon ); $this->validate_coupon_usage_limit( $coupon ); $this->validate_coupon_user_usage_limit( $coupon ); $this->validate_coupon_expiry_date( $coupon ); $this->validate_coupon_minimum_amount( $coupon ); $this->validate_coupon_maximum_amount( $coupon ); $this->validate_coupon_product_ids( $coupon ); $this->validate_coupon_product_categories( $coupon ); $this->validate_coupon_excluded_items( $coupon ); $this->validate_coupon_eligible_items( $coupon ); if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $coupon, $this ) ) { throw new Exception( __( 'Coupon is not valid.', 'woocommerce' ), 100 ); } } catch ( Exception $e ) { /** * Filter the coupon error message. * * @param string $error_message Error message. * @param int $error_code Error code. * @param WC_Coupon $coupon Coupon data. */ $message = apply_filters( 'woocommerce_coupon_error', is_numeric( $e->getMessage() ) ? $coupon->get_coupon_error( $e->getMessage() ) : $e->getMessage(), $e->getCode(), $coupon ); return new WP_Error( 'invalid_coupon', $message, array( 'status' => 400, ) ); } return true; } } PKK[¤â¬y  wc-coupon-functions.phpnu„[µü¤ __( 'Percentage discount', 'woocommerce' ), 'fixed_cart' => __( 'Fixed cart discount', 'woocommerce' ), 'fixed_product' => __( 'Fixed product discount', 'woocommerce' ), ) ); } /** * Get a coupon type's name. * * @param string $type Coupon type. * @return string */ function wc_get_coupon_type( $type = '' ) { $types = wc_get_coupon_types(); return isset( $types[ $type ] ) ? $types[ $type ] : ''; } /** * Coupon types that apply to individual products. Controls which validation rules will apply. * * @since 2.5.0 * @return array */ function wc_get_product_coupon_types() { return (array) apply_filters( 'woocommerce_product_coupon_types', array( 'fixed_product', 'percent' ) ); } /** * Coupon types that apply to the cart as a whole. Controls which validation rules will apply. * * @since 2.5.0 * @return array */ function wc_get_cart_coupon_types() { return (array) apply_filters( 'woocommerce_cart_coupon_types', array( 'fixed_cart' ) ); } /** * Check if coupons are enabled. * Filterable. * * @since 2.5.0 * * @return bool */ function wc_coupons_enabled() { return apply_filters( 'woocommerce_coupons_enabled', 'yes' === get_option( 'woocommerce_enable_coupons' ) ); } /** * Get coupon code by ID. * * @since 3.0.0 * @param int $id Coupon ID. * @return string */ function wc_get_coupon_code_by_id( $id ) { $data_store = WC_Data_Store::load( 'coupon' ); return empty( $id ) ? '' : (string) $data_store->get_code_by_id( $id ); } /** * Get coupon ID by code. * * @since 3.0.0 * @param string $code Coupon code. * @param int $exclude Used to exclude an ID from the check if you're checking existence. * @return int */ function wc_get_coupon_id_by_code( $code, $exclude = 0 ) { if ( StringUtil::is_null_or_whitespace( $code ) ) { return 0; } $data_store = WC_Data_Store::load( 'coupon' ); $ids = wp_cache_get( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $code, 'coupons' ); if ( false === $ids ) { $ids = $data_store->get_ids_by_code( $code ); if ( $ids ) { wp_cache_set( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $code, $ids, 'coupons' ); } } $ids = array_diff( array_filter( array_map( 'absint', (array) $ids ) ), array( $exclude ) ); return apply_filters( 'woocommerce_get_coupon_id_from_code', absint( current( $ids ) ), $code, $exclude ); } PKK[­ª:Z..*log-handlers/class-wc-log-handler-file.phpnu„[µü¤log_size_limit = apply_filters( 'woocommerce_log_file_size_limit', $log_size_limit ); add_action( 'plugins_loaded', array( $this, 'write_cached_logs' ) ); } /** * Destructor. * * Cleans up open file handles. */ public function __destruct() { foreach ( $this->handles as $handle ) { if ( is_resource( $handle ) ) { fclose( $handle ); // @codingStandardsIgnoreLine. } } } /** * Handle a log entry. * * @param int $timestamp Log timestamp. * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @param string $message Log message. * @param array $context { * Additional information for log handlers. * * @type string $source Optional. Determines log file to write to. Default 'log'. * @type bool $_legacy Optional. Default false. True to use outdated log format * originally used in deprecated WC_Logger::add calls. * } * * @return bool False if value was not handled and true if value was handled. */ public function handle( $timestamp, $level, $message, $context ) { if ( isset( $context['source'] ) && $context['source'] ) { $handle = $context['source']; } else { $handle = 'log'; } $entry = self::format_entry( $timestamp, $level, $message, $context ); return $this->add( $entry, $handle ); } /** * Builds a log entry text from timestamp, level and message. * * @param int $timestamp Log timestamp. * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @param string $message Log message. * @param array $context Additional information for log handlers. * * @return string Formatted log entry. */ protected static function format_entry( $timestamp, $level, $message, $context ) { if ( isset( $context['_legacy'] ) && true === $context['_legacy'] ) { if ( isset( $context['source'] ) && $context['source'] ) { $handle = $context['source']; } else { $handle = 'log'; } $message = apply_filters( 'woocommerce_logger_add_message', $message, $handle ); $time = date_i18n( 'm-d-Y @ H:i:s' ); $entry = "{$time} - {$message}"; } else { $entry = parent::format_entry( $timestamp, $level, $message, $context ); } return $entry; } /** * Open log file for writing. * * @param string $handle Log handle. * @param string $mode Optional. File mode. Default 'a'. * @return bool Success. */ protected function open( $handle, $mode = 'a' ) { if ( $this->is_open( $handle ) ) { return true; } $file = self::get_log_file_path( $handle ); if ( $file ) { if ( ! file_exists( $file ) ) { $temphandle = @fopen( $file, 'w+' ); // @codingStandardsIgnoreLine. if ( is_resource( $temphandle ) ) { @fclose( $temphandle ); // @codingStandardsIgnoreLine. if ( Constants::is_defined( 'FS_CHMOD_FILE' ) ) { @chmod( $file, FS_CHMOD_FILE ); // @codingStandardsIgnoreLine. } } } $resource = @fopen( $file, $mode ); // @codingStandardsIgnoreLine. if ( $resource ) { $this->handles[ $handle ] = $resource; return true; } } return false; } /** * Check if a handle is open. * * @param string $handle Log handle. * @return bool True if $handle is open. */ protected function is_open( $handle ) { return array_key_exists( $handle, $this->handles ) && is_resource( $this->handles[ $handle ] ); } /** * Close a handle. * * @param string $handle Log handle. * @return bool success */ protected function close( $handle ) { $result = false; if ( $this->is_open( $handle ) ) { $result = fclose( $this->handles[ $handle ] ); // @codingStandardsIgnoreLine. unset( $this->handles[ $handle ] ); } return $result; } /** * Add a log entry to chosen file. * * @param string $entry Log entry text. * @param string $handle Log entry handle. * * @return bool True if write was successful. */ protected function add( $entry, $handle ) { $result = false; if ( $this->should_rotate( $handle ) ) { $this->log_rotate( $handle ); } if ( $this->open( $handle ) && is_resource( $this->handles[ $handle ] ) ) { $result = fwrite( $this->handles[ $handle ], $entry . PHP_EOL ); // @codingStandardsIgnoreLine. } else { $this->cache_log( $entry, $handle ); } return false !== $result; } /** * Clear entries from chosen file. * * @param string $handle Log handle. * * @return bool */ public function clear( $handle ) { $result = false; // Close the file if it's already open. $this->close( $handle ); /** * $this->open( $handle, 'w' ) == Open the file for writing only. Place the file pointer at * the beginning of the file, and truncate the file to zero length. */ if ( $this->open( $handle, 'w' ) && is_resource( $this->handles[ $handle ] ) ) { $result = true; } do_action( 'woocommerce_log_clear', $handle ); return $result; } /** * Remove/delete the chosen file. * * @param string $handle Log handle. * * @return bool */ public function remove( $handle ) { $removed = false; $logs = $this->get_log_files(); $handle = sanitize_title( $handle ); if ( isset( $logs[ $handle ] ) && $logs[ $handle ] ) { $file = realpath( trailingslashit( WC_LOG_DIR ) . $logs[ $handle ] ); if ( 0 === stripos( $file, realpath( trailingslashit( WC_LOG_DIR ) ) ) && is_file( $file ) && is_writable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable $this->close( $file ); // Close first to be certain no processes keep it alive after it is unlinked. $removed = unlink( $file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink } do_action( 'woocommerce_log_remove', $handle, $removed ); } return $removed; } /** * Check if log file should be rotated. * * Compares the size of the log file to determine whether it is over the size limit. * * @param string $handle Log handle. * @return bool True if if should be rotated. */ protected function should_rotate( $handle ) { $file = self::get_log_file_path( $handle ); if ( $file ) { if ( $this->is_open( $handle ) ) { $file_stat = fstat( $this->handles[ $handle ] ); return $file_stat['size'] > $this->log_size_limit; } elseif ( file_exists( $file ) ) { return filesize( $file ) > $this->log_size_limit; } else { return false; } } else { return false; } } /** * Rotate log files. * * Logs are rotated by prepending '.x' to the '.log' suffix. * The current log plus 10 historical logs are maintained. * For example: * base.9.log -> [ REMOVED ] * base.8.log -> base.9.log * ... * base.0.log -> base.1.log * base.log -> base.0.log * * @param string $handle Log handle. */ protected function log_rotate( $handle ) { for ( $i = 8; $i >= 0; $i-- ) { $this->increment_log_infix( $handle, $i ); } $this->increment_log_infix( $handle ); } /** * Increment a log file suffix. * * @param string $handle Log handle. * @param null|int $number Optional. Default null. Log suffix number to be incremented. * @return bool True if increment was successful, otherwise false. */ protected function increment_log_infix( $handle, $number = null ) { if ( null === $number ) { $suffix = ''; $next_suffix = '.0'; } else { $suffix = '.' . $number; $next_suffix = '.' . ( $number + 1 ); } $rename_from = self::get_log_file_path( "{$handle}{$suffix}" ); $rename_to = self::get_log_file_path( "{$handle}{$next_suffix}" ); if ( $this->is_open( $rename_from ) ) { $this->close( $rename_from ); } if ( is_writable( $rename_from ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable return rename( $rename_from, $rename_to ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_rename } else { return false; } } /** * Get a log file path. * * @param string $handle Log name. * @return bool|string The log file path or false if path cannot be determined. */ public static function get_log_file_path( $handle ) { if ( function_exists( 'wp_hash' ) ) { return trailingslashit( WC_LOG_DIR ) . self::get_log_file_name( $handle ); } else { wc_doing_it_wrong( __METHOD__, __( 'This method should not be called before plugins_loaded.', 'woocommerce' ), '3.0' ); return false; } } /** * Get a log file name. * * File names consist of the handle, followed by the date, followed by a hash, .log. * * @since 3.3 * @param string $handle Log name. * @return bool|string The log file name or false if cannot be determined. */ public static function get_log_file_name( $handle ) { if ( function_exists( 'wp_hash' ) ) { $date_suffix = date( 'Y-m-d', time() ); $hash_suffix = wp_hash( $handle ); return sanitize_file_name( implode( '-', array( $handle, $date_suffix, $hash_suffix ) ) . '.log' ); } else { wc_doing_it_wrong( __METHOD__, __( 'This method should not be called before plugins_loaded.', 'woocommerce' ), '3.3' ); return false; } } /** * Cache log to write later. * * @param string $entry Log entry text. * @param string $handle Log entry handle. */ protected function cache_log( $entry, $handle ) { $this->cached_logs[] = array( 'entry' => $entry, 'handle' => $handle, ); } /** * Write cached logs. */ public function write_cached_logs() { foreach ( $this->cached_logs as $log ) { $this->add( $log['entry'], $log['handle'] ); } } /** * Delete all logs older than a defined timestamp. * * @since 3.4.0 * @param integer $timestamp Timestamp to delete logs before. */ public static function delete_logs_before_timestamp( $timestamp = 0 ) { if ( ! $timestamp ) { return; } $log_files = self::get_log_files(); foreach ( $log_files as $log_file ) { $last_modified = filemtime( trailingslashit( WC_LOG_DIR ) . $log_file ); if ( $last_modified < $timestamp ) { @unlink( trailingslashit( WC_LOG_DIR ) . $log_file ); // @codingStandardsIgnoreLine. } } } /** * Get all log files in the log directory. * * @since 3.4.0 * @return array */ public static function get_log_files() { $files = @scandir( WC_LOG_DIR ); // @codingStandardsIgnoreLine. $result = array(); if ( ! empty( $files ) ) { foreach ( $files as $key => $value ) { if ( ! in_array( $value, array( '.', '..' ), true ) ) { if ( ! is_dir( $value ) && strstr( $value, '.log' ) ) { $result[ sanitize_title( $value ) ] = $value; } } } } return $result; } } PKK[/Súßß(log-handlers/class-wc-log-handler-db.phpnu„[µü¤get_log_source(); } return $this->add( $timestamp, $level, $message, $source, $context ); } /** * Add a log entry to chosen file. * * @param int $timestamp Log timestamp. * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @param string $message Log message. * @param string $source Log source. Useful for filtering and sorting. * @param array $context Context will be serialized and stored in database. * * @return bool True if write was successful. */ protected static function add( $timestamp, $level, $message, $source, $context ) { global $wpdb; $insert = array( 'timestamp' => date( 'Y-m-d H:i:s', $timestamp ), 'level' => WC_Log_Levels::get_level_severity( $level ), 'message' => $message, 'source' => $source, ); $format = array( '%s', '%d', '%s', '%s', '%s', // possible serialized context. ); if ( ! empty( $context ) ) { try { $insert['context'] = serialize( $context ); // @codingStandardsIgnoreLine. } catch ( Exception $e ) { $insert['context'] = serialize( 'There was an error while serializing the context: ' . $e->getMessage() ); } } return false !== $wpdb->insert( "{$wpdb->prefix}woocommerce_log", $insert, $format ); } /** * Clear all logs from the DB. * * @return bool True if flush was successful. */ public static function flush() { global $wpdb; return $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_log" ); } /** * Clear entries for a chosen handle/source. * * @param string $source Log source. * @return bool */ public function clear( $source ) { global $wpdb; return $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_log WHERE source = %s", $source ) ); } /** * Delete selected logs from DB. * * @param int|string|array $log_ids Log ID or array of Log IDs to be deleted. * * @return bool */ public static function delete( $log_ids ) { global $wpdb; if ( ! is_array( $log_ids ) ) { $log_ids = array( $log_ids ); } $format = array_fill( 0, count( $log_ids ), '%d' ); $query_in = '(' . implode( ',', $format ) . ')'; return $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_log WHERE log_id IN {$query_in}", $log_ids ) ); // @codingStandardsIgnoreLine. } /** * Delete all logs older than a defined timestamp. * * @since 3.4.0 * @param integer $timestamp Timestamp to delete logs before. */ public static function delete_logs_before_timestamp( $timestamp = 0 ) { if ( ! $timestamp ) { return; } global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_log WHERE timestamp < %s", date( 'Y-m-d H:i:s', $timestamp ) ) ); } /** * Get appropriate source based on file name. * * Try to provide an appropriate source in case none is provided. * * @return string Text to use as log source. "" (empty string) if none is found. */ protected static function get_log_source() { static $ignore_files = array( 'class-wc-log-handler-db', 'class-wc-logger' ); /** * PHP < 5.3.6 correct behavior * * @see http://php.net/manual/en/function.debug-backtrace.php#refsect1-function.debug-backtrace-parameters */ if ( Constants::is_defined( 'DEBUG_BACKTRACE_IGNORE_ARGS' ) ) { $debug_backtrace_arg = DEBUG_BACKTRACE_IGNORE_ARGS; // phpcs:ignore PHPCompatibility.Constants.NewConstants.debug_backtrace_ignore_argsFound } else { $debug_backtrace_arg = false; } $trace = debug_backtrace( $debug_backtrace_arg ); // @codingStandardsIgnoreLine. foreach ( $trace as $t ) { if ( isset( $t['file'] ) ) { $filename = pathinfo( $t['file'], PATHINFO_FILENAME ); if ( ! in_array( $filename, $ignore_files, true ) ) { return $filename; } } } return ''; } } PKK[êP?+log-handlers/class-wc-log-handler-email.phpnu„[µü¤add_email( $recipient ); } } else { $this->add_email( $recipients ); } $this->set_threshold( $threshold ); add_action( 'shutdown', array( $this, 'send_log_email' ) ); } /** * Set handler severity threshold. * * @param string $level emergency|alert|critical|error|warning|notice|info|debug. */ public function set_threshold( $level ) { $this->threshold = WC_Log_Levels::get_level_severity( $level ); } /** * Determine whether handler should handle log. * * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @return bool True if the log should be handled. */ protected function should_handle( $level ) { return $this->threshold <= WC_Log_Levels::get_level_severity( $level ); } /** * Handle a log entry. * * @param int $timestamp Log timestamp. * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @param string $message Log message. * @param array $context Optional. Additional information for log handlers. * * @return bool False if value was not handled and true if value was handled. */ public function handle( $timestamp, $level, $message, $context ) { if ( $this->should_handle( $level ) ) { $this->add_log( $timestamp, $level, $message, $context ); return true; } return false; } /** * Send log email. * * @return bool True if email is successfully sent otherwise false. */ public function send_log_email() { $result = false; if ( ! empty( $this->logs ) ) { $subject = $this->get_subject(); $body = $this->get_body(); $result = wp_mail( $this->recipients, $subject, $body ); $this->clear_logs(); } return $result; } /** * Build subject for log email. * * @return string subject */ protected function get_subject() { $site_name = get_bloginfo( 'name' ); $max_level = strtoupper( WC_Log_Levels::get_severity_level( $this->max_severity ) ); $log_count = count( $this->logs ); return sprintf( /* translators: 1: Site name 2: Maximum level 3: Log count */ _n( '[%1$s] %2$s: %3$s WooCommerce log message', '[%1$s] %2$s: %3$s WooCommerce log messages', $log_count, 'woocommerce' ), $site_name, $max_level, $log_count ); } /** * Build body for log email. * * @return string body */ protected function get_body() { $site_name = get_bloginfo( 'name' ); $entries = implode( PHP_EOL, $this->logs ); $log_count = count( $this->logs ); return _n( 'You have received the following WooCommerce log message:', 'You have received the following WooCommerce log messages:', $log_count, 'woocommerce' ) . PHP_EOL . PHP_EOL . $entries . PHP_EOL . PHP_EOL /* translators: %s: Site name */ . sprintf( __( 'Visit %s admin area:', 'woocommerce' ), $site_name ) . PHP_EOL . admin_url(); } /** * Adds an email to the list of recipients. * * @param string $email Email address to add. */ public function add_email( $email ) { array_push( $this->recipients, $email ); } /** * Add log message. * * @param int $timestamp Log timestamp. * @param string $level emergency|alert|critical|error|warning|notice|info|debug. * @param string $message Log message. * @param array $context Additional information for log handlers. */ protected function add_log( $timestamp, $level, $message, $context ) { $this->logs[] = $this->format_entry( $timestamp, $level, $message, $context ); $log_severity = WC_Log_Levels::get_level_severity( $level ); if ( $this->max_severity < $log_severity ) { $this->max_severity = $log_severity; } } /** * Clear log messages. */ protected function clear_logs() { $this->logs = array(); } } PKK[>Ô ³(³(#libraries/wp-background-process.phpnu„[µü¤cron_hook_identifier = $this->identifier . '_cron'; $this->cron_interval_identifier = $this->identifier . '_cron_interval'; add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); } /** * Dispatch * * @access public * @return void */ public function dispatch() { // Schedule the cron healthcheck. $this->schedule_event(); // Perform remote post. return parent::dispatch(); } /** * Push to queue * * @param mixed $data Data. * * @return $this */ public function push_to_queue( $data ) { $this->data[] = $data; return $this; } /** * Save queue * * @return $this */ public function save() { $key = $this->generate_key(); if ( ! empty( $this->data ) ) { update_site_option( $key, $this->data ); } return $this; } /** * Update queue * * @param string $key Key. * @param array $data Data. * * @return $this */ public function update( $key, $data ) { if ( ! empty( $data ) ) { update_site_option( $key, $data ); } return $this; } /** * Delete queue * * @param string $key Key. * * @return $this */ public function delete( $key ) { delete_site_option( $key ); return $this; } /** * Generate key * * Generates a unique key based on microtime. Queue items are * given a unique key so that they can be merged upon save. * * @param int $length Length. * * @return string */ protected function generate_key( $length = 64 ) { $unique = md5( microtime() . rand() ); $prepend = $this->identifier . '_batch_'; return substr( $prepend . $unique, 0, $length ); } /** * Maybe process queue * * Checks whether data exists within the queue and that * the process is not already running. */ public function maybe_handle() { // Don't lock up other requests while processing session_write_close(); if ( $this->is_process_running() ) { // Background process already running. wp_die(); } if ( $this->is_queue_empty() ) { // No data to process. wp_die(); } check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); wp_die(); } /** * Is queue empty * * @return bool */ protected function is_queue_empty() { global $wpdb; $table = $wpdb->options; $column = 'option_name'; if ( is_multisite() ) { $table = $wpdb->sitemeta; $column = 'meta_key'; } $key = $this->identifier . '_batch_%'; $count = $wpdb->get_var( $wpdb->prepare( " SELECT COUNT(*) FROM {$table} WHERE {$column} LIKE %s ", $key ) ); return ! ( $count > 0 ); } /** * Is process running * * Check whether the current process is already running * in a background process. */ protected function is_process_running() { if ( get_site_transient( $this->identifier . '_process_lock' ) ) { // Process already running. return true; } return false; } /** * Lock process * * Lock the process so that multiple instances can't run simultaneously. * Override if applicable, but the duration should be greater than that * defined in the time_exceeded() method. */ protected function lock_process() { $this->start_time = time(); // Set start time of current process. $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration ); set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration ); } /** * Unlock process * * Unlock the process so that other instances can spawn. * * @return $this */ protected function unlock_process() { delete_site_transient( $this->identifier . '_process_lock' ); return $this; } /** * Get batch * * @return stdClass Return the first batch from the queue */ protected function get_batch() { global $wpdb; $table = $wpdb->options; $column = 'option_name'; $key_column = 'option_id'; $value_column = 'option_value'; if ( is_multisite() ) { $table = $wpdb->sitemeta; $column = 'meta_key'; $key_column = 'meta_id'; $value_column = 'meta_value'; } $key = $this->identifier . '_batch_%'; $query = $wpdb->get_row( $wpdb->prepare( " SELECT * FROM {$table} WHERE {$column} LIKE %s ORDER BY {$key_column} ASC LIMIT 1 ", $key ) ); $batch = new stdClass(); $batch->key = $query->$column; $batch->data = maybe_unserialize( $query->$value_column ); return $batch; } /** * Handle * * Pass each queue item to the task handler, while remaining * within server memory and time limit constraints. */ protected function handle() { $this->lock_process(); do { $batch = $this->get_batch(); foreach ( $batch->data as $key => $value ) { $task = $this->task( $value ); if ( false !== $task ) { $batch->data[ $key ] = $task; } else { unset( $batch->data[ $key ] ); } if ( $this->time_exceeded() || $this->memory_exceeded() ) { // Batch limits reached. break; } } // Update or delete current batch. if ( ! empty( $batch->data ) ) { $this->update( $batch->key, $batch->data ); } else { $this->delete( $batch->key ); } } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); $this->unlock_process(); // Start next batch or complete process. if ( ! $this->is_queue_empty() ) { $this->dispatch(); } else { $this->complete(); } wp_die(); } /** * Memory exceeded * * Ensures the batch process never exceeds 90% * of the maximum WordPress memory. * * @return bool */ protected function memory_exceeded() { $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory $current_memory = memory_get_usage( true ); $return = false; if ( $current_memory >= $memory_limit ) { $return = true; } return apply_filters( $this->identifier . '_memory_exceeded', $return ); } /** * Get memory limit * * @return int */ protected function get_memory_limit() { if ( function_exists( 'ini_get' ) ) { $memory_limit = ini_get( 'memory_limit' ); } else { // Sensible default. $memory_limit = '128M'; } if ( ! $memory_limit || -1 === $memory_limit ) { // Unlimited, set to 32GB. $memory_limit = '32000M'; } return wp_convert_hr_to_bytes( $memory_limit ); } /** * Time exceeded. * * Ensures the batch never exceeds a sensible time limit. * A timeout limit of 30s is common on shared hosting. * * @return bool */ protected function time_exceeded() { $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds $return = false; if ( time() >= $finish ) { $return = true; } return apply_filters( $this->identifier . '_time_exceeded', $return ); } /** * Complete. * * Override if applicable, but ensure that the below actions are * performed, or, call parent::complete(). */ protected function complete() { // Unschedule the cron healthcheck. $this->clear_scheduled_event(); } /** * Schedule cron healthcheck * * @access public * @param mixed $schedules Schedules. * @return mixed */ public function schedule_cron_healthcheck( $schedules ) { $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); if ( property_exists( $this, 'cron_interval' ) ) { $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval ); } // Adds every 5 minutes to the existing schedules. $schedules[ $this->identifier . '_cron_interval' ] = array( 'interval' => MINUTE_IN_SECONDS * $interval, 'display' => sprintf( __( 'Every %d minutes', 'woocommerce' ), $interval ), ); return $schedules; } /** * Handle cron healthcheck * * Restart the background process if not already running * and data exists in the queue. */ public function handle_cron_healthcheck() { if ( $this->is_process_running() ) { // Background process already running. exit; } if ( $this->is_queue_empty() ) { // No data to process. $this->clear_scheduled_event(); exit; } $this->handle(); exit; } /** * Schedule event */ protected function schedule_event() { if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier ); } } /** * Clear scheduled event */ protected function clear_scheduled_event() { $timestamp = wp_next_scheduled( $this->cron_hook_identifier ); if ( $timestamp ) { wp_unschedule_event( $timestamp, $this->cron_hook_identifier ); } } /** * Cancel Process * * Stop processing queue items, clear cronjob and delete batch. * */ public function cancel_process() { if ( ! $this->is_queue_empty() ) { $batch = $this->get_batch(); $this->delete( $batch->key ); wp_clear_scheduled_hook( $this->cron_hook_identifier ); } } /** * Task * * Override this method to perform any actions required on each * queue item. Return the modified item for further processing * in the next pass through. Or, return false to remove the * item from the queue. * * @param mixed $item Queue item to iterate over. * * @return mixed */ abstract protected function task( $item ); } PKK[DöŸ×¤ ¤ libraries/wp-async-request.phpnu„[µü¤identifier = $this->prefix . '_' . $this->action; add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); } /** * Set data used during the request * * @param array $data Data. * * @return $this */ public function data( $data ) { $this->data = $data; return $this; } /** * Dispatch the async request * * @return array|WP_Error */ public function dispatch() { $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); $args = $this->get_post_args(); return wp_remote_post( esc_url_raw( $url ), $args ); } /** * Get query args * * @return array */ protected function get_query_args() { if ( property_exists( $this, 'query_args' ) ) { return $this->query_args; } return array( 'action' => $this->identifier, 'nonce' => wp_create_nonce( $this->identifier ), ); } /** * Get query URL * * @return string */ protected function get_query_url() { if ( property_exists( $this, 'query_url' ) ) { return $this->query_url; } return admin_url( 'admin-ajax.php' ); } /** * Get post args * * @return array */ protected function get_post_args() { if ( property_exists( $this, 'post_args' ) ) { return $this->post_args; } return array( 'timeout' => 0.01, 'blocking' => false, 'body' => $this->data, 'cookies' => $_COOKIE, 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), ); } /** * Maybe handle * * Check for correct nonce and pass to handler. */ public function maybe_handle() { // Don't lock up other requests while processing session_write_close(); check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); wp_die(); } /** * Handle * * Override this method to perform any actions required * during the async request. */ abstract protected function handle(); } PKK[0ú`W‘4‘4 libraries/class-wc-eval-math.phpnu„[µü¤ 2.71, 'pi' => 3.14 ); /** * User-defined functions. * * @var array */ public static $f = array(); /** * Constants. * * @var array */ public static $vb = array( 'e', 'pi' ); /** * Built-in functions. * * @var array */ public static $fb = array(); /** * Evaluate maths string. * * @param string $expr * @return mixed */ public static function evaluate( $expr ) { self::$last_error = null; $expr = trim( $expr ); if ( substr( $expr, -1, 1 ) == ';' ) { $expr = substr( $expr, 0, strlen( $expr ) -1 ); // strip semicolons at the end } // =============== // is it a variable assignment? if ( preg_match( '/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches ) ) { if ( in_array( $matches[1], self::$vb ) ) { // make sure we're not assigning to a constant return self::trigger( "cannot assign to constant '$matches[1]'" ); } if ( ( $tmp = self::pfx( self::nfx( $matches[2] ) ) ) === false ) { return false; // get the result and make sure it's good } self::$v[ $matches[1] ] = $tmp; // if so, stick it in the variable array return self::$v[ $matches[1] ]; // and return the resulting value // =============== // is it a function assignment? } elseif ( preg_match( '/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches ) ) { $fnn = $matches[1]; // get the function name if ( in_array( $matches[1], self::$fb ) ) { // make sure it isn't built in return self::trigger( "cannot redefine built-in function '$matches[1]()'" ); } $args = explode( ",", preg_replace( "/\s+/", "", $matches[2] ) ); // get the arguments if ( ( $stack = self::nfx( $matches[3] ) ) === false ) { return false; // see if it can be converted to postfix } $stack_size = count( $stack ); for ( $i = 0; $i < $stack_size; $i++ ) { // freeze the state of the non-argument variables $token = $stack[ $i ]; if ( preg_match( '/^[a-z]\w*$/', $token ) and ! in_array( $token, $args ) ) { if ( array_key_exists( $token, self::$v ) ) { $stack[ $i ] = self::$v[ $token ]; } else { return self::trigger( "undefined variable '$token' in function definition" ); } } } self::$f[ $fnn ] = array( 'args' => $args, 'func' => $stack ); return true; // =============== } else { return self::pfx( self::nfx( $expr ) ); // straight up evaluation, woo } } /** * Convert infix to postfix notation. * * @param string $expr * * @return array|string */ private static function nfx( $expr ) { $index = 0; $stack = new WC_Eval_Math_Stack; $output = array(); // postfix form of expression, to be passed to pfx() $expr = trim( $expr ); $ops = array( '+', '-', '*', '/', '^', '_' ); $ops_r = array( '+' => 0, '-' => 0, '*' => 0, '/' => 0, '^' => 1 ); // right-associative operator? $ops_p = array( '+' => 0, '-' => 0, '*' => 1, '/' => 1, '_' => 1, '^' => 2 ); // operator precedence $expecting_op = false; // we use this in syntax-checking the expression // and determining when a - is a negation if ( preg_match( "/[^\w\s+*^\/()\.,-]/", $expr, $matches ) ) { // make sure the characters are all good return self::trigger( "illegal character '{$matches[0]}'" ); } while ( 1 ) { // 1 Infinite Loop ;) $op = substr( $expr, $index, 1 ); // get the first character at the current index // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand $ex = preg_match( '/^([A-Za-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr( $expr, $index ), $match ); // =============== if ( '-' === $op and ! $expecting_op ) { // is it a negation instead of a minus? $stack->push( '_' ); // put a negation on the stack $index++; } elseif ( '_' === $op ) { // we have to explicitly deny this, because it's legal on the stack return self::trigger( "illegal character '_'" ); // but not in the input expression // =============== } elseif ( ( in_array( $op, $ops ) or $ex ) and $expecting_op ) { // are we putting an operator on the stack? if ( $ex ) { // are we expecting an operator but have a number/variable/function/opening parenthesis? $op = '*'; $index--; // it's an implicit multiplication } // heart of the algorithm: while ( $stack->count > 0 and ( $o2 = $stack->last() ) and in_array( $o2, $ops ) and ( $ops_r[ $op ] ? $ops_p[ $op ] < $ops_p[ $o2 ] : $ops_p[ $op ] <= $ops_p[ $o2 ] ) ) { $output[] = $stack->pop(); // pop stuff off the stack into the output } // many thanks: https://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail $stack->push( $op ); // finally put OUR operator onto the stack $index++; $expecting_op = false; // =============== } elseif ( ')' === $op && $expecting_op ) { // ready to close a parenthesis? while ( ( $o2 = $stack->pop() ) != '(' ) { // pop off the stack back to the last ( if ( is_null( $o2 ) ) { return self::trigger( "unexpected ')'" ); } else { $output[] = $o2; } } if ( preg_match( "/^([A-Za-z]\w*)\($/", $stack->last( 2 ), $matches ) ) { // did we just close a function? $fnn = $matches[1]; // get the function name $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you) $output[] = $stack->pop(); // pop the function and push onto the output if ( in_array( $fnn, self::$fb ) ) { // check the argument count if ( $arg_count > 1 ) { return self::trigger( "too many arguments ($arg_count given, 1 expected)" ); } } elseif ( array_key_exists( $fnn, self::$f ) ) { if ( count( self::$f[ $fnn ]['args'] ) != $arg_count ) { return self::trigger( "wrong number of arguments ($arg_count given, " . count( self::$f[ $fnn ]['args'] ) . " expected)" ); } } else { // did we somehow push a non-function on the stack? this should never happen return self::trigger( "internal error" ); } } $index++; // =============== } elseif ( ',' === $op and $expecting_op ) { // did we just finish a function argument? while ( ( $o2 = $stack->pop() ) != '(' ) { if ( is_null( $o2 ) ) { return self::trigger( "unexpected ','" ); // oops, never had a ( } else { $output[] = $o2; // pop the argument expression stuff and push onto the output } } // make sure there was a function if ( ! preg_match( "/^([A-Za-z]\w*)\($/", $stack->last( 2 ), $matches ) ) { return self::trigger( "unexpected ','" ); } $stack->push( $stack->pop() + 1 ); // increment the argument count $stack->push( '(' ); // put the ( back on, we'll need to pop back to it again $index++; $expecting_op = false; // =============== } elseif ( '(' === $op and ! $expecting_op ) { $stack->push( '(' ); // that was easy $index++; // =============== } elseif ( $ex and ! $expecting_op ) { // do we now have a function/variable/number? $expecting_op = true; $val = $match[1]; if ( preg_match( "/^([A-Za-z]\w*)\($/", $val, $matches ) ) { // may be func, or variable w/ implicit multiplication against parentheses... if ( in_array( $matches[1], self::$fb ) or array_key_exists( $matches[1], self::$f ) ) { // it's a func $stack->push( $val ); $stack->push( 1 ); $stack->push( '(' ); $expecting_op = false; } else { // it's a var w/ implicit multiplication $val = $matches[1]; $output[] = $val; } } else { // it's a plain old var or num $output[] = $val; } $index += strlen( $val ); // =============== } elseif ( ')' === $op ) { // miscellaneous error checking return self::trigger( "unexpected ')'" ); } elseif ( in_array( $op, $ops ) and ! $expecting_op ) { return self::trigger( "unexpected operator '$op'" ); } else { // I don't even want to know what you did to get here return self::trigger( "an unexpected error occurred" ); } if ( strlen( $expr ) == $index ) { if ( in_array( $op, $ops ) ) { // did we end with an operator? bad. return self::trigger( "operator '$op' lacks operand" ); } else { break; } } while ( substr( $expr, $index, 1 ) == ' ' ) { // step the index past whitespace (pretty much turns whitespace $index++; // into implicit multiplication if no operator is there) } } while ( ! is_null( $op = $stack->pop() ) ) { // pop everything off the stack and push onto output if ( '(' === $op ) { return self::trigger( "expecting ')'" ); // if there are (s on the stack, ()s were unbalanced } $output[] = $op; } return $output; } /** * Evaluate postfix notation. * * @param mixed $tokens * @param array $vars * * @return mixed */ private static function pfx( $tokens, $vars = array() ) { if ( false == $tokens ) { return false; } $stack = new WC_Eval_Math_Stack; foreach ( $tokens as $token ) { // nice and easy // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on if ( in_array( $token, array( '+', '-', '*', '/', '^' ) ) ) { if ( is_null( $op2 = $stack->pop() ) ) { return self::trigger( "internal error" ); } if ( is_null( $op1 = $stack->pop() ) ) { return self::trigger( "internal error" ); } switch ( $token ) { case '+': $stack->push( $op1 + $op2 ); break; case '-': $stack->push( $op1 - $op2 ); break; case '*': $stack->push( $op1 * $op2 ); break; case '/': if ( 0 == $op2 ) { return self::trigger( 'division by zero' ); } $stack->push( $op1 / $op2 ); break; case '^': $stack->push( pow( $op1, $op2 ) ); break; } // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on } elseif ( '_' === $token ) { $stack->push( -1 * $stack->pop() ); // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on } elseif ( ! preg_match( "/^([a-z]\w*)\($/", $token, $matches ) ) { if ( is_numeric( $token ) ) { $stack->push( $token ); } elseif ( array_key_exists( $token, self::$v ) ) { $stack->push( self::$v[ $token ] ); } elseif ( array_key_exists( $token, $vars ) ) { $stack->push( $vars[ $token ] ); } else { return self::trigger( "undefined variable '$token'" ); } } } // when we're out of tokens, the stack should have a single element, the final result if ( 1 != $stack->count ) { return self::trigger( "internal error" ); } return $stack->pop(); } /** * Trigger an error, but nicely, if need be. * * @param string $msg * * @return bool */ private static function trigger( $msg ) { self::$last_error = $msg; if ( ! Constants::is_true( 'DOING_AJAX' ) && Constants::is_true( 'WP_DEBUG' ) ) { echo "\nError found in:"; self::debugPrintCallingFunction(); trigger_error( $msg, E_USER_WARNING ); } return false; } /** * Prints the file name, function name, and * line number which called your function * (not this function, then one that called * it to begin with) */ private static function debugPrintCallingFunction() { $file = 'n/a'; $func = 'n/a'; $line = 'n/a'; $debugTrace = debug_backtrace(); if ( isset( $debugTrace[1] ) ) { $file = $debugTrace[1]['file'] ? $debugTrace[1]['file'] : 'n/a'; $line = $debugTrace[1]['line'] ? $debugTrace[1]['line'] : 'n/a'; } if ( isset( $debugTrace[2] ) ) { $func = $debugTrace[2]['function'] ? $debugTrace[2]['function'] : 'n/a'; } echo "\n$file, $func, $line\n"; } } /** * Class WC_Eval_Math_Stack. */ class WC_Eval_Math_Stack { /** * Stack array. * * @var array */ public $stack = array(); /** * Stack counter. * * @var integer */ public $count = 0; /** * Push value into stack. * * @param mixed $val */ public function push( $val ) { $this->stack[ $this->count ] = $val; $this->count++; } /** * Pop value from stack. * * @return mixed */ public function pop() { if ( $this->count > 0 ) { $this->count--; return $this->stack[ $this->count ]; } return null; } /** * Get last value from stack. * * @param int $n * * @return mixed */ public function last( $n=1 ) { $key = $this->count - $n; return array_key_exists( $key, $this->stack ) ? $this->stack[ $key ] : null; } } } PKK[Zɱ_7_7admin/class-wc-admin-status.phpnu„[µü¤execute_tool( $action ); $tool = $tools[ $action ]; $tool_requires_refresh = $tool['requires_refresh'] ?? false; $tool = array( 'id' => $action, 'name' => $tool['name'], 'action' => $tool['button'], 'description' => $tool['desc'], 'disabled' => $tool['disabled'] ?? false, ); $tool = array_merge( $tool, $response ); /** * Fires after a WooCommerce system status tool has been executed. * * @param array $tool Details about the tool that has been executed. */ do_action( 'woocommerce_system_status_tool_executed', $tool ); } else { $response = array( 'success' => false, 'message' => __( 'Tool does not exist.', 'woocommerce' ), ); } if ( $response['success'] ) { echo '

' . esc_html( $response['message'] ) . '

'; } else { echo '

' . esc_html( $response['message'] ) . '

'; } } // Display message if settings settings have been saved. if ( isset( $_REQUEST['settings-updated'] ) ) { // WPCS: input var ok. echo '

' . esc_html__( 'Your changes have been saved.', 'woocommerce' ) . '

'; } if ( $tool_requires_refresh ) { $tools = self::get_tools(); } include_once __DIR__ . '/views/html-admin-page-status-tools.php'; } /** * Get tools. * * @return array of tools */ public static function get_tools() { $tools_controller = new WC_REST_System_Status_Tools_Controller(); return $tools_controller->get_tools(); } /** * Show the logs page. */ public static function status_logs() { wc_get_container()->get( LoggingPageController::class )->render(); } /** * Show the log page contents for file log handler. */ public static function status_logs_file() { $logs = self::scan_log_files(); if ( ! empty( $_REQUEST['log_file'] ) && isset( $logs[ sanitize_title( wp_unslash( $_REQUEST['log_file'] ) ) ] ) ) { // WPCS: input var ok, CSRF ok. $viewed_log = $logs[ sanitize_title( wp_unslash( $_REQUEST['log_file'] ) ) ]; // WPCS: input var ok, CSRF ok. } elseif ( ! empty( $logs ) ) { $viewed_log = current( $logs ); } $handle = ! empty( $viewed_log ) ? self::get_log_file_handle( $viewed_log ) : ''; if ( ! empty( $_REQUEST['handle'] ) ) { // WPCS: input var ok, CSRF ok. self::remove_log(); } include_once __DIR__ . '/views/html-admin-page-status-logs.php'; } /** * Show the log page contents for db log handler. */ public static function status_logs_db() { if ( ! empty( $_REQUEST['flush-logs'] ) ) { // WPCS: input var ok, CSRF ok. self::flush_db_logs(); } if ( isset( $_REQUEST['action'] ) && isset( $_REQUEST['log'] ) ) { // WPCS: input var ok, CSRF ok. self::log_table_bulk_actions(); } $log_table_list = new WC_Admin_Log_Table_List(); $log_table_list->prepare_items(); include_once __DIR__ . '/views/html-admin-page-status-logs-db.php'; } /** * Retrieve metadata from a file. Based on WP Core's get_file_data function. * * @since 2.1.1 * @param string $file Path to the file. * @return string */ public static function get_file_version( $file ) { // Avoid notices if file does not exist. if ( ! file_exists( $file ) ) { return ''; } // We don't need to write to the file, so just open for reading. $fp = fopen( $file, 'r' ); // @codingStandardsIgnoreLine. // Pull only the first 8kiB of the file in. $file_data = fread( $fp, 8192 ); // @codingStandardsIgnoreLine. // PHP will close file handle, but we are good citizens. fclose( $fp ); // @codingStandardsIgnoreLine. // Make sure we catch CR-only line endings. $file_data = str_replace( "\r", "\n", $file_data ); $version = ''; if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( '@version', '/' ) . '(.*)$/mi', $file_data, $match ) && $match[1] ) { $version = _cleanup_header_comment( $match[1] ); } return $version; } /** * Return the log file handle. * * @param string $filename Filename to get the handle for. * @return string */ public static function get_log_file_handle( $filename ) { return substr( $filename, 0, strlen( $filename ) > 48 ? strlen( $filename ) - 48 : strlen( $filename ) - 4 ); } /** * Scan the template files. * * @param string $template_path Path to the template directory. * @return array */ public static function scan_template_files( $template_path ) { $files = @scandir( $template_path ); // @codingStandardsIgnoreLine. $result = array(); if ( ! empty( $files ) ) { foreach ( $files as $key => $value ) { if ( ! in_array( $value, array( '.', '..' ), true ) ) { if ( is_dir( $template_path . DIRECTORY_SEPARATOR . $value ) ) { $sub_files = self::scan_template_files( $template_path . DIRECTORY_SEPARATOR . $value ); foreach ( $sub_files as $sub_file ) { $result[] = $value . DIRECTORY_SEPARATOR . $sub_file; } } else { $result[] = $value; } } } } return $result; } /** * Scan the log files. * * @return array */ public static function scan_log_files() { return WC_Log_Handler_File::get_log_files(); } /** * Get latest version of a theme by slug. * * @param object $theme WP_Theme object. * @return string Version number if found. */ public static function get_latest_theme_version( $theme ) { include_once ABSPATH . 'wp-admin/includes/theme.php'; $api = themes_api( 'theme_information', array( 'slug' => $theme->get_stylesheet(), 'fields' => array( 'sections' => false, 'tags' => false, ), ) ); $update_theme_version = 0; // Check .org for updates. if ( is_object( $api ) && ! is_wp_error( $api ) && isset( $api->version ) ) { $update_theme_version = $api->version; } elseif ( strstr( $theme->{'Author URI'}, 'woothemes' ) ) { // Check WooThemes Theme Version. $theme_dir = substr( strtolower( str_replace( ' ', '', $theme->Name ) ), 0, 45 ); // @codingStandardsIgnoreLine. $theme_version_data = get_transient( $theme_dir . '_version_data' ); if ( false === $theme_version_data ) { $theme_changelog = wp_safe_remote_get( 'http://dzv365zjfbd8v.cloudfront.net/changelogs/' . $theme_dir . '/changelog.txt' ); $cl_lines = explode( "\n", wp_remote_retrieve_body( $theme_changelog ) ); if ( ! empty( $cl_lines ) ) { foreach ( $cl_lines as $line_num => $cl_line ) { if ( preg_match( '/^[0-9]/', $cl_line ) ) { $theme_date = str_replace( '.', '-', trim( substr( $cl_line, 0, strpos( $cl_line, '-' ) ) ) ); $theme_version = preg_replace( '~[^0-9,.]~', '', stristr( $cl_line, 'version' ) ); $theme_update = trim( str_replace( '*', '', $cl_lines[ $line_num + 1 ] ) ); $theme_version_data = array( 'date' => $theme_date, 'version' => $theme_version, 'update' => $theme_update, 'changelog' => $theme_changelog, ); set_transient( $theme_dir . '_version_data', $theme_version_data, DAY_IN_SECONDS ); break; } } } } if ( ! empty( $theme_version_data['version'] ) ) { $update_theme_version = $theme_version_data['version']; } } return $update_theme_version; } /** * Remove/delete the chosen file. */ public static function remove_log() { if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'remove_log' ) ) { // WPCS: input var ok, sanitization ok. wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); } if ( ! empty( $_REQUEST['handle'] ) ) { // WPCS: input var ok. $log_handler = new WC_Log_Handler_File(); $log_handler->remove( wp_unslash( $_REQUEST['handle'] ) ); // WPCS: input var ok, sanitization ok. } wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); exit(); } /** * Clear DB log table. * * @since 3.0.0 */ private static function flush_db_logs() { if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'woocommerce-status-logs' ) ) { // WPCS: input var ok, sanitization ok. wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); } WC_Log_Handler_DB::flush(); wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); exit(); } /** * Bulk DB log table actions. * * @since 3.0.0 */ private static function log_table_bulk_actions() { if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'woocommerce-status-logs' ) ) { // WPCS: input var ok, sanitization ok. wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); } $log_ids = array_map( 'absint', (array) isset( $_REQUEST['log'] ) ? wp_unslash( $_REQUEST['log'] ) : array() ); // WPCS: input var ok, sanitization ok. if ( ( isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action'] ) || ( isset( $_REQUEST['action2'] ) && 'delete' === $_REQUEST['action2'] ) ) { // WPCS: input var ok, sanitization ok. WC_Log_Handler_DB::delete( $log_ids ); wp_safe_redirect( esc_url_raw( admin_url( 'admin.php?page=wc-status&tab=logs' ) ) ); exit(); } } /** * Prints table info if a base table is not present. */ private static function output_tables_info() { $missing_tables = WC_Install::verify_base_tables( false ); if ( 0 === count( $missing_tables ) ) { return; } ?>
' . $plugin_name . ''; } $has_newer_version = false; $version_string = $plugin['version']; $network_string = ''; if ( strstr( $plugin['url'], 'woothemes.com' ) || strstr( $plugin['url'], 'woocommerce.com' ) || strstr( $plugin['url'], 'woo.com' ) ) { if ( ! empty( $plugin['version_latest'] ) && version_compare( $plugin['version_latest'], $plugin['version'], '>' ) ) { /* translators: 1: current version. 2: latest version */ $version_string = sprintf( __( '%1$s (update to version %2$s is available)', 'woocommerce' ), $plugin['version'], $plugin['version_latest'] ); } if ( false !== $plugin['network_activated'] ) { $network_string = ' – ' . esc_html__( 'Network enabled', 'woocommerce' ) . ''; } } $untested_string = ''; if ( array_key_exists( $plugin['plugin'], $untested_plugins ) ) { $untested_string = ' – '; /* translators: %s: version */ $untested_string .= esc_html( sprintf( __( 'Installed version not tested with active version of WooCommerce %s', 'woocommerce' ), $wc_version ) ); $untested_string .= ''; } ?>   'key', 'plural' => 'keys', 'ajax' => false, ) ); } /** * No items found text. */ public function no_items() { esc_html_e( 'No keys found.', 'woocommerce' ); } /** * Get list columns. * * @return array */ public function get_columns() { return array( 'cb' => '', 'title' => __( 'Description', 'woocommerce' ), 'truncated_key' => __( 'Consumer key ending in', 'woocommerce' ), 'user' => __( 'User', 'woocommerce' ), 'permissions' => __( 'Permissions', 'woocommerce' ), 'last_access' => __( 'Last access', 'woocommerce' ), ); } /** * Column cb. * * @param array $key Key data. * @return string */ public function column_cb( $key ) { return sprintf( '', $key['key_id'] ); } /** * Return title column. * * @param array $key Key data. * @return string */ public function column_title( $key ) { $url = admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=keys&edit-key=' . $key['key_id'] ); $user_id = intval( $key['user_id'] ); // Check if current user can edit other users or if it's the same user. $can_edit = current_user_can( 'edit_user', $user_id ) || get_current_user_id() === $user_id; $output = ''; if ( $can_edit ) { $output .= ''; } if ( empty( $key['description'] ) ) { $output .= esc_html__( 'API key', 'woocommerce' ); } else { $output .= esc_html( $key['description'] ); } if ( $can_edit ) { $output .= ''; } $output .= ''; // Get actions. $actions = array( /* translators: %s: API key ID. */ 'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $key['key_id'] ), ); if ( $can_edit ) { $actions['edit'] = '' . __( 'View/Edit', 'woocommerce' ) . ''; $actions['trash'] = '' . esc_html__( 'Revoke', 'woocommerce' ) . ''; } $row_actions = array(); foreach ( $actions as $action => $link ) { $row_actions[] = '' . $link . ''; } $output .= '
' . implode( ' | ', $row_actions ) . '
'; return $output; } /** * Return truncated consumer key column. * * @param array $key Key data. * @return string */ public function column_truncated_key( $key ) { return '***' . esc_html( $key['truncated_key'] ) . ''; } /** * Return user column. * * @param array $key Key data. * @return string */ public function column_user( $key ) { $user = get_user_by( 'id', $key['user_id'] ); if ( ! $user ) { return ''; } if ( current_user_can( 'edit_user', $user->ID ) ) { return '' . esc_html( $user->display_name ) . ''; } return esc_html( $user->display_name ); } /** * Return permissions column. * * @param array $key Key data. * @return string */ public function column_permissions( $key ) { $permission_key = $key['permissions']; $permissions = array( 'read' => __( 'Read', 'woocommerce' ), 'write' => __( 'Write', 'woocommerce' ), 'read_write' => __( 'Read/Write', 'woocommerce' ), ); if ( isset( $permissions[ $permission_key ] ) ) { return esc_html( $permissions[ $permission_key ] ); } else { return ''; } } /** * Return last access column. * * @param array $key Key data. * @return string */ public function column_last_access( $key ) { if ( ! empty( $key['last_access'] ) ) { /* translators: 1: last access date 2: last access time */ $date = sprintf( __( '%1$s at %2$s', 'woocommerce' ), date_i18n( wc_date_format(), strtotime( $key['last_access'] ) ), date_i18n( wc_time_format(), strtotime( $key['last_access'] ) ) ); return apply_filters( 'woocommerce_api_key_last_access_datetime', $date, $key['last_access'] ); } return __( 'Unknown', 'woocommerce' ); } /** * Get bulk actions. * * @return array */ protected function get_bulk_actions() { if ( ! current_user_can( 'remove_users' ) ) { return array(); } return array( 'revoke' => __( 'Revoke', 'woocommerce' ), ); } /** * Search box. * * @param string $text Button text. * @param string $input_id Input ID. */ public function search_box( $text, $input_id ) { if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) { // WPCS: input var okay, CSRF ok. return; } $input_id = $input_id . '-search-input'; $search_query = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; // WPCS: input var okay, CSRF ok. echo ''; } /** * Prepare table list items. */ public function prepare_items() { global $wpdb; $per_page = $this->get_items_per_page( 'woocommerce_keys_per_page' ); $current_page = $this->get_pagenum(); if ( 1 < $current_page ) { $offset = $per_page * ( $current_page - 1 ); } else { $offset = 0; } $search = ''; if ( ! empty( $_REQUEST['s'] ) ) { // WPCS: input var okay, CSRF ok. $search = "AND description LIKE '%" . esc_sql( $wpdb->esc_like( wc_clean( wp_unslash( $_REQUEST['s'] ) ) ) ) . "%' "; // WPCS: input var okay, CSRF ok. } // Get the API keys. $keys = $wpdb->get_results( "SELECT key_id, user_id, description, permissions, truncated_key, last_access FROM {$wpdb->prefix}woocommerce_api_keys WHERE 1 = 1 {$search}" . $wpdb->prepare( 'ORDER BY key_id DESC LIMIT %d OFFSET %d;', $per_page, $offset ), ARRAY_A ); // WPCS: unprepared SQL ok. $count = $wpdb->get_var( "SELECT COUNT(key_id) FROM {$wpdb->prefix}woocommerce_api_keys WHERE 1 = 1 {$search};" ); // WPCS: unprepared SQL ok. $this->items = $keys; // Set the pagination. $this->set_pagination_args( array( 'total_items' => $count, 'per_page' => $per_page, 'total_pages' => ceil( $count / $per_page ), ) ); } } PKK[;=ÌRèDèD#admin/class-wc-admin-taxonomies.phpnu„[µü¤default_cat_id = get_option( 'default_product_cat', 0 ); // Category/term ordering. add_action( 'create_term', array( $this, 'create_term' ), 5, 3 ); add_action( 'delete_product_cat', function() { wc_get_container()->get( AssignDefaultCategory::class )->schedule_action(); } ); // Add form. add_action( 'product_cat_add_form_fields', array( $this, 'add_category_fields' ) ); add_action( 'product_cat_edit_form_fields', array( $this, 'edit_category_fields' ), 10 ); add_action( 'created_term', array( $this, 'save_category_fields' ), 10, 3 ); add_action( 'edit_term', array( $this, 'save_category_fields' ), 10, 3 ); // Add columns. add_filter( 'manage_edit-product_cat_columns', array( $this, 'product_cat_columns' ) ); add_filter( 'manage_product_cat_custom_column', array( $this, 'product_cat_column' ), 10, 3 ); // Add row actions. add_filter( 'product_cat_row_actions', array( $this, 'product_cat_row_actions' ), 10, 2 ); add_filter( 'admin_init', array( $this, 'handle_product_cat_row_actions' ) ); // Taxonomy page descriptions. add_action( 'product_cat_pre_add_form', array( $this, 'product_cat_description' ) ); add_action( 'after-product_cat-table', array( $this, 'product_cat_notes' ) ); $attribute_taxonomies = wc_get_attribute_taxonomies(); if ( ! empty( $attribute_taxonomies ) ) { foreach ( $attribute_taxonomies as $attribute ) { add_action( 'pa_' . $attribute->attribute_name . '_pre_add_form', array( $this, 'product_attribute_description' ) ); } } // Maintain hierarchy of terms. add_filter( 'wp_terms_checklist_args', array( $this, 'disable_checked_ontop' ) ); // Admin footer scripts for this product categories admin screen. add_action( 'admin_footer', array( $this, 'scripts_at_product_cat_screen_footer' ) ); } /** * Order term when created (put in position 0). * * @param mixed $term_id Term ID. * @param mixed $tt_id Term taxonomy ID. * @param string $taxonomy Taxonomy slug. */ public function create_term( $term_id, $tt_id = '', $taxonomy = '' ) { if ( 'product_cat' !== $taxonomy && ! taxonomy_is_product_attribute( $taxonomy ) ) { return; } update_term_meta( $term_id, 'order', 0 ); } /** * When a term is deleted, delete its meta. * * @deprecated 3.6.0 No longer needed. * @param mixed $term_id Term ID. */ public function delete_term( $term_id ) { wc_deprecated_function( 'delete_term', '3.6' ); } /** * Category thumbnail fields. */ public function add_category_fields() { ?>
term_id, 'display_type', true ); $thumbnail_id = absint( get_term_meta( $term->term_id, 'thumbnail_id', true ) ); if ( $thumbnail_id ) { $image = wp_get_attachment_thumb_url( $thumbnail_id ); } else { $image = wc_placeholder_img_src(); } ?>
array() ) ); } /** * Add some notes to describe the behavior of the default category. */ public function product_cat_notes() { $category_id = get_option( 'default_product_cat', 0 ); $category = get_term( $category_id, 'product_cat' ); $category_name = ( ! $category || is_wp_error( $category ) ) ? _x( 'Uncategorized', 'Default category slug', 'woocommerce' ) : $category->name; ?>


' . esc_html( $category_name ) . '' ); ?>


Note: Deleting a term will remove it from all products and variations to which it has been assigned. Recreating a term will not automatically assign it back to products.', 'woocommerce' ) ), array( 'p' => array() ) ); } /** * Thumbnail column added to category admin. * * @param mixed $columns Columns array. * @return array */ public function product_cat_columns( $columns ) { $new_columns = array(); if ( isset( $columns['cb'] ) ) { $new_columns['cb'] = $columns['cb']; unset( $columns['cb'] ); } $new_columns['thumb'] = __( 'Image', 'woocommerce' ); $columns = array_merge( $new_columns, $columns ); $columns['handle'] = ''; return $columns; } /** * Adjust row actions. * * @param array $actions Array of actions. * @param object $term Term object. * @return array */ public function product_cat_row_actions( $actions, $term ) { $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); if ( $default_category_id !== $term->term_id && current_user_can( 'edit_term', $term->term_id ) ) { $actions['make_default'] = sprintf( '%s', wp_nonce_url( 'edit-tags.php?action=make_default&taxonomy=product_cat&post_type=product&tag_ID=' . absint( $term->term_id ), 'make_default_' . absint( $term->term_id ) ), /* translators: %s: taxonomy term name */ esc_attr( sprintf( __( 'Make “%s” the default category', 'woocommerce' ), $term->name ) ), __( 'Make default', 'woocommerce' ) ); } return $actions; } /** * Handle custom row actions. */ public function handle_product_cat_row_actions() { if ( isset( $_GET['action'], $_GET['tag_ID'], $_GET['_wpnonce'] ) && 'make_default' === $_GET['action'] ) { // WPCS: CSRF ok, input var ok. $make_default_id = absint( $_GET['tag_ID'] ); // WPCS: Input var ok. if ( wp_verify_nonce( $_GET['_wpnonce'], 'make_default_' . $make_default_id ) && current_user_can( 'edit_term', $make_default_id ) ) { // WPCS: Sanitization ok, input var ok, CSRF ok. update_option( 'default_product_cat', $make_default_id ); } } } /** * Thumbnail column value added to category admin. * * @param string $columns Column HTML output. * @param string $column Column name. * @param int $id Product ID. * * @return string */ public function product_cat_column( $columns, $column, $id ) { if ( 'thumb' === $column ) { // Prepend tooltip for default category. $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); if ( $default_category_id === $id ) { $columns .= wc_help_tip( __( 'This is the default category and it cannot be deleted. It will be automatically assigned to products with no category.', 'woocommerce' ) ); } $thumbnail_id = get_term_meta( $id, 'thumbnail_id', true ); if ( $thumbnail_id ) { $image = wp_get_attachment_thumb_url( $thumbnail_id ); } else { $image = wc_placeholder_img_src(); } // Prevent esc_url from breaking spaces in urls for image embeds. Ref: https://core.trac.wordpress.org/ticket/23605 . $image = str_replace( ' ', '%20', $image ); $columns .= '' . esc_attr__( 'Thumbnail', 'woocommerce' ) . ''; } if ( 'handle' === $column ) { $columns .= ''; } return $columns; } /** * Maintain term hierarchy when editing a product. * * @param array $args Term checklist args. * @return array */ public function disable_checked_ontop( $args ) { if ( ! empty( $args['taxonomy'] ) && 'product_cat' === $args['taxonomy'] ) { $args['checked_ontop'] = false; } return $args; } /** * Admin footer scripts for the product categories admin screen * * @return void */ public function scripts_at_product_cat_screen_footer() { if ( ! isset( $_GET['taxonomy'] ) || 'product_cat' !== $_GET['taxonomy'] ) { // WPCS: CSRF ok, input var ok. return; } // Ensure the tooltip is displayed when the image column is disabled on product categories. wc_enqueue_js( "(function( $ ) { 'use strict'; var product_cat = $( 'tr#tag-" . absint( $this->default_cat_id ) . "' ); product_cat.find( 'th' ).empty(); product_cat.find( 'td.thumb span' ).detach( 'span' ).appendTo( product_cat.find( 'th' ) ); })( jQuery );" ); } } $wc_admin_taxonomies = WC_Admin_Taxonomies::get_instance(); PKK[[Õéèè admin/class-wc-admin-reports.phpnu„[µü¤ array( 'title' => __( 'Orders', 'woocommerce' ), 'reports' => array( 'sales_by_date' => array( 'title' => __( 'Sales by date', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'sales_by_product' => array( 'title' => __( 'Sales by product', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'sales_by_category' => array( 'title' => __( 'Sales by category', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'coupon_usage' => array( 'title' => __( 'Coupons by date', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'downloads' => array( 'title' => __( 'Customer downloads', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), ), ), 'customers' => array( 'title' => __( 'Customers', 'woocommerce' ), 'reports' => array( 'customers' => array( 'title' => __( 'Customers vs. guests', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'customer_list' => array( 'title' => __( 'Customer list', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), ), ), 'stock' => array( 'title' => __( 'Stock', 'woocommerce' ), 'reports' => array( 'low_in_stock' => array( 'title' => __( 'Low in stock', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'out_of_stock' => array( 'title' => __( 'Out of stock', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'most_stocked' => array( 'title' => __( 'Most stocked', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), ), ), ); if ( wc_tax_enabled() ) { $reports['taxes'] = array( 'title' => __( 'Taxes', 'woocommerce' ), 'reports' => array( 'taxes_by_code' => array( 'title' => __( 'Taxes by code', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), 'taxes_by_date' => array( 'title' => __( 'Taxes by date', 'woocommerce' ), 'description' => '', 'hide_title' => true, 'callback' => array( __CLASS__, 'get_report' ), ), ), ); } $reports = apply_filters( 'woocommerce_admin_reports', $reports ); $reports = apply_filters( 'woocommerce_reports_charts', $reports ); // Backwards compatibility. foreach ( $reports as $key => $report_group ) { if ( isset( $reports[ $key ]['charts'] ) ) { $reports[ $key ]['reports'] = $reports[ $key ]['charts']; } foreach ( $reports[ $key ]['reports'] as $report_key => $report ) { if ( isset( $reports[ $key ]['reports'][ $report_key ]['function'] ) ) { $reports[ $key ]['reports'][ $report_key ]['callback'] = $reports[ $key ]['reports'][ $report_key ]['function']; } } } return $reports; } /** * Get a report from our reports subfolder. * * @param string $name */ public static function get_report( $name ) { $name = sanitize_title( str_replace( '_', '-', $name ) ); $class = 'WC_Report_' . str_replace( '-', '_', $name ); include_once apply_filters( 'wc_admin_reports_path', 'reports/class-wc-report-' . $name . '.php', $name, $class ); if ( ! class_exists( $class ) ) { return; } $report = new $class(); $report->output_report(); } } PKK[ ªˆ p'p'admin/class-wc-admin.phpnu„[µü¤id ) { case 'dashboard': case 'dashboard-network': include __DIR__ . '/class-wc-admin-dashboard-setup.php'; include __DIR__ . '/class-wc-admin-dashboard.php'; break; case 'options-permalink': include __DIR__ . '/class-wc-admin-permalink-settings.php'; break; case 'plugins': include __DIR__ . '/plugin-updates/class-wc-plugins-screen-updates.php'; break; case 'update-core': include __DIR__ . '/plugin-updates/class-wc-updates-screen-updates.php'; break; case 'users': case 'user': case 'profile': case 'user-edit': include __DIR__ . '/class-wc-admin-profile.php'; break; } } /** * Handle redirects to setup/welcome page after install and updates. * * The user must have access rights, and we must ignore the network/bulk plugin updaters. */ public function admin_redirects() { // Don't run this fn from Action Scheduler requests, as it would clear _wc_activation_redirect transient. // That means OBW would never be shown. if ( wc_is_running_from_async_action_scheduler() ) { return; } // phpcs:disable WordPress.Security.NonceVerification.Recommended // Nonced plugin install redirects. if ( ! empty( $_GET['wc-install-plugin-redirect'] ) ) { $plugin_slug = wc_clean( wp_unslash( $_GET['wc-install-plugin-redirect'] ) ); if ( current_user_can( 'install_plugins' ) && in_array( $plugin_slug, array( 'woocommerce-gateway-stripe' ), true ) ) { $nonce = wp_create_nonce( 'install-plugin_' . $plugin_slug ); $url = self_admin_url( 'update.php?action=install-plugin&plugin=' . $plugin_slug . '&_wpnonce=' . $nonce ); } else { $url = admin_url( 'plugin-install.php?tab=search&type=term&s=' . $plugin_slug ); } wp_safe_redirect( $url ); exit; } // phpcs:enable WordPress.Security.NonceVerification.Recommended } /** * Prevent any user who cannot 'edit_posts' (subscribers, customers etc) from accessing admin. */ public function prevent_admin_access() { $prevent_access = false; // Do not interfere with admin-post or admin-ajax requests. $exempted_paths = array( 'admin-post.php', 'admin-ajax.php' ); if ( /** * This filter is documented in ../wc-user-functions.php * * @since 3.6.0 */ apply_filters( 'woocommerce_disable_admin_bar', true ) && isset( $_SERVER['SCRIPT_FILENAME'] ) && ! in_array( basename( sanitize_text_field( wp_unslash( $_SERVER['SCRIPT_FILENAME'] ) ) ), $exempted_paths, true ) ) { $has_cap = false; $access_caps = array( 'edit_posts', 'manage_woocommerce', 'view_admin_dashboard' ); foreach ( $access_caps as $access_cap ) { if ( current_user_can( $access_cap ) ) { $has_cap = true; break; } } if ( ! $has_cap ) { $prevent_access = true; } } if ( apply_filters( 'woocommerce_prevent_admin_access', $prevent_access ) ) { wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); exit; } } /** * Preview email template. */ public function preview_emails() { if ( isset( $_GET['preview_woocommerce_mail'] ) ) { if ( ! ( isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'preview-mail' ) ) ) { die( 'Security check' ); } // load the mailer class. $mailer = WC()->mailer(); // get the preview email subject. $email_heading = __( 'HTML email template', 'woocommerce' ); // get the preview email content. ob_start(); include __DIR__ . '/views/html-email-template-preview.php'; $message = ob_get_clean(); // create a new email. $email = new WC_Email(); // wrap the content with the email template and then add styles. $message = apply_filters( 'woocommerce_mail_content', $email->style_inline( $mailer->wrap_message( $email_heading, $message ) ) ); // print the preview email. // phpcs:ignore WordPress.Security.EscapeOutput echo $message; // phpcs:enable exit; } } /** * Change the admin footer text on WooCommerce admin pages. * * @since 2.3 * @param string $footer_text text to be rendered in the footer. * @return string */ public function admin_footer_text( $footer_text ) { if ( ! current_user_can( 'manage_woocommerce' ) || ! function_exists( 'wc_get_screen_ids' ) ) { return $footer_text; } $current_screen = get_current_screen(); $wc_pages = wc_get_screen_ids(); // Set only WC pages. $wc_pages = array_diff( $wc_pages, array( 'profile', 'user-edit' ) ); // Check to make sure we're on a WooCommerce admin page. if ( isset( $current_screen->id ) && apply_filters( 'woocommerce_display_admin_footer_text', in_array( $current_screen->id, $wc_pages, true ) ) ) { // Change the footer text. if ( ! get_option( 'woocommerce_admin_footer_text_rated' ) ) { $footer_text = sprintf( /* translators: 1: WooCommerce 2:: five stars */ __( 'If you like %1$s please leave us a %2$s rating. A huge thanks in advance!', 'woocommerce' ), sprintf( '%s', esc_html__( 'WooCommerce', 'woocommerce' ) ), '★★★★★' ); wc_enqueue_js( "jQuery( 'a.wc-rating-link' ).on( 'click', function() { jQuery.post( '" . WC()->ajax_url() . "', { action: 'woocommerce_rated' } ); jQuery( this ).parent().text( jQuery( this ).data( 'rated' ) ); });" ); } else { $footer_text = __( 'Thank you for selling with WooCommerce.', 'woocommerce' ); } } return $footer_text; } /** * Check on a Jetpack install queued by the Setup Wizard. * * See: WC_Admin_Setup_Wizard::install_jetpack() */ public function setup_wizard_check_jetpack() { $jetpack_active = class_exists( 'Jetpack' ); wp_send_json_success( array( 'is_active' => $jetpack_active ? 'yes' : 'no', ) ); } /** * Disable WXR export of scheduled action posts. * * @since 3.6.2 * * @param array $args Scheduled action post type registration args. * * @return array */ public function disable_webhook_post_export( $args ) { $args['can_export'] = false; return $args; } /** * Include admin classes. * * @since 4.2.0 * @param string $classes Body classes string. * @return string */ public function include_admin_body_class( $classes ) { if ( in_array( array( 'wc-wp-version-gte-53', 'wc-wp-version-gte-55' ), explode( ' ', $classes ), true ) ) { return $classes; } $raw_version = get_bloginfo( 'version' ); $version_parts = explode( '-', $raw_version ); $version = count( $version_parts ) > 1 ? $version_parts[0] : $raw_version; // Add WP 5.3+ compatibility class. if ( $raw_version && version_compare( $version, '5.3', '>=' ) ) { $classes .= ' wc-wp-version-gte-53'; } // Add WP 5.5+ compatibility class. if ( $raw_version && version_compare( $version, '5.5', '>=' ) ) { $classes .= ' wc-wp-version-gte-55'; } return $classes; } } return new WC_Admin(); PKK[Ûì*Ä2"2"/admin/reports/class-wc-report-customer-list.phpnu„[µü¤ 'customer', 'plural' => 'customers', 'ajax' => false, ) ); } /** * No items found text. */ public function no_items() { esc_html_e( 'No customers found.', 'woocommerce' ); } /** * Output the report. */ public function output_report() { $this->prepare_items(); echo '
'; if ( ! empty( $_GET['link_orders'] ) && wp_verify_nonce( $_REQUEST['_wpnonce'], 'link_orders' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $linked = wc_update_new_customer_past_orders( absint( $_GET['link_orders'] ) ); /* translators: single or plural number of orders */ echo '

' . sprintf( esc_html( _n( '%s previous order linked', '%s previous orders linked', $linked, 'woocommerce' ), $linked ) ) . '

'; } if ( ! empty( $_GET['refresh'] ) && wp_verify_nonce( $_REQUEST['_wpnonce'], 'refresh' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $user_id = absint( $_GET['refresh'] ); $user = get_user_by( 'id', $user_id ); delete_user_meta( $user_id, '_money_spent' ); delete_user_meta( $user_id, '_order_count' ); delete_user_meta( $user_id, '_last_order' ); /* translators: User display name */ echo '

' . sprintf( esc_html__( 'Refreshed stats for %s', 'woocommerce' ), esc_html( $user->display_name ) ) . '

'; } echo '
'; $this->search_box( __( 'Search customers', 'woocommerce' ), 'customer_search' ); $this->display(); echo '
'; echo '
'; } /** * Get column value. * * @param WP_User $user WP User object. * @param string $column_name Column name. * @return string */ public function column_default( $user, $column_name ) { switch ( $column_name ) { case 'customer_name': if ( $user->last_name && $user->first_name ) { return $user->last_name . ', ' . $user->first_name; } else { return '-'; } case 'username': return $user->user_login; case 'location': $state_code = get_user_meta( $user->ID, 'billing_state', true ); $country_code = get_user_meta( $user->ID, 'billing_country', true ); $state = isset( WC()->countries->states[ $country_code ][ $state_code ] ) ? WC()->countries->states[ $country_code ][ $state_code ] : $state_code; $country = isset( WC()->countries->countries[ $country_code ] ) ? WC()->countries->countries[ $country_code ] : $country_code; $value = ''; if ( $state ) { $value .= $state . ', '; } $value .= $country; if ( $value ) { return $value; } else { return '-'; } case 'email': return '' . $user->user_email . ''; case 'spent': return wc_price( wc_get_customer_total_spent( $user->ID ) ); case 'orders': return wc_get_customer_order_count( $user->ID ); case 'last_order': $orders = wc_get_orders( array( 'limit' => 1, 'status' => array_map( 'wc_get_order_status_name', wc_get_is_paid_statuses() ), 'customer' => $user->ID, ) ); if ( ! empty( $orders ) ) { $order = $orders[0]; return '' . _x( '#', 'hash before order number', 'woocommerce' ) . $order->get_order_number() . ' – ' . wc_format_datetime( $order->get_date_created() ); } else { return '-'; } break; case 'wc_actions': ob_start(); ?>

wp_nonce_url( add_query_arg( 'refresh', $user->ID ), 'refresh' ), 'name' => __( 'Refresh stats', 'woocommerce' ), 'action' => 'refresh', ); $actions['edit'] = array( 'url' => admin_url( 'user-edit.php?user_id=' . $user->ID ), 'name' => __( 'Edit', 'woocommerce' ), 'action' => 'edit', ); $actions['view'] = array( 'url' => admin_url( 'edit.php?post_type=shop_order&_customer_user=' . $user->ID ), 'name' => __( 'View orders', 'woocommerce' ), 'action' => 'view', ); $orders = wc_get_orders( array( 'limit' => 1, 'status' => array_map( 'wc_get_order_status_name', wc_get_is_paid_statuses() ), 'customer' => array( array( 0, $user->user_email ) ), ) ); if ( $orders ) { $actions['link'] = array( 'url' => wp_nonce_url( add_query_arg( 'link_orders', $user->ID ), 'link_orders' ), 'name' => __( 'Link previous orders', 'woocommerce' ), 'action' => 'link', ); } $actions = apply_filters( 'woocommerce_admin_user_actions', $actions, $user ); foreach ( $actions as $action ) { printf( '%s', esc_attr( $action['action'] ), esc_url( $action['url'] ), esc_attr( $action['name'] ), esc_attr( $action['name'] ) ); } do_action( 'woocommerce_admin_user_actions_end', $user ); ?>

__( 'Name (Last, First)', 'woocommerce' ), 'username' => __( 'Username', 'woocommerce' ), 'email' => __( 'Email', 'woocommerce' ), 'location' => __( 'Location', 'woocommerce' ), 'orders' => __( 'Orders', 'woocommerce' ), 'spent' => __( 'Money spent', 'woocommerce' ), 'last_order' => __( 'Last order', 'woocommerce' ), 'wc_actions' => __( 'Actions', 'woocommerce' ), ); return $columns; } /** * Order users by name. * * @param WP_User_Query $query Query that gets passed through. * @return WP_User_Query */ public function order_by_last_name( $query ) { global $wpdb; $s = ! empty( $_REQUEST['s'] ) ? wp_unslash( $_REQUEST['s'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $query->query_from .= " LEFT JOIN {$wpdb->usermeta} as meta2 ON ({$wpdb->users}.ID = meta2.user_id) "; $query->query_where .= " AND meta2.meta_key = 'last_name' "; $query->query_orderby = ' ORDER BY meta2.meta_value, user_login ASC '; if ( $s ) { $query->query_from .= " LEFT JOIN {$wpdb->usermeta} as meta3 ON ({$wpdb->users}.ID = meta3.user_id)"; $query->query_where .= " AND ( user_login LIKE '%" . esc_sql( str_replace( '*', '', $s ) ) . "%' OR user_nicename LIKE '%" . esc_sql( str_replace( '*', '', $s ) ) . "%' OR meta3.meta_value LIKE '%" . esc_sql( str_replace( '*', '', $s ) ) . "%' ) "; $query->query_orderby = ' GROUP BY ID ' . $query->query_orderby; } return $query; } /** * Prepare customer list items. */ public function prepare_items() { $current_page = absint( $this->get_pagenum() ); $per_page = 20; /** * Init column headers. */ $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() ); add_action( 'pre_user_query', array( $this, 'order_by_last_name' ) ); /** * Get users. */ $admin_users = new WP_User_Query( array( 'role' => 'administrator', 'fields' => 'ID', ) ); $manager_users = new WP_User_Query( array( 'role' => 'shop_manager', 'fields' => 'ID', ) ); $query = new WP_User_Query( apply_filters( 'woocommerce_admin_report_customer_list_user_query_args', array( 'exclude' => array_merge( $admin_users->get_results(), $manager_users->get_results() ), 'number' => $per_page, 'offset' => ( $current_page - 1 ) * $per_page, ) ) ); $this->items = $query->get_results(); remove_action( 'pre_user_query', array( $this, 'order_by_last_name' ) ); /** * Pagination. */ $this->set_pagination_args( array( 'total_items' => $query->total_users, 'per_page' => $per_page, 'total_pages' => ceil( $query->total_users / $per_page ), ) ); } } PKK[3'·Ï$Ï$/admin/reports/class-wc-report-taxes-by-date.phpnu„[µü¤ __( 'Year', 'woocommerce' ), 'last_month' => __( 'Last month', 'woocommerce' ), 'month' => __( 'This month', 'woocommerce' ), ); $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( $_GET['range'] ) : 'last_month'; if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) { $current_range = 'last_month'; } $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); $hide_sidebar = true; include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; } /** * Get the main chart. */ public function get_main_chart() { $query_data = array( '_order_tax' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'tax_amount', ), '_order_shipping_tax' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'shipping_tax_amount', ), '_order_total' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_sales', ), '_order_shipping' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_shipping', ), 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', 'name' => 'total_orders', 'distinct' => true, ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ); // We exclude on-hold orders are they are still pending payment. $tax_rows_orders = $this->get_order_report_data( array( 'data' => $query_data, 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'sales-reports' ), 'order_status' => array( 'completed', 'processing', 'refunded' ), ) ); $tax_rows_full_refunds = $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'distinct' => true, 'function' => '', 'name' => 'ID', ), 'post_parent' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_parent', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => array( 'shop_order_refund' ), 'parent_order_status' => array( 'refunded' ), ) ); $tax_rows_partial_refunds = $this->get_order_report_data( array( 'data' => $query_data, 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => array( 'shop_order_refund' ), 'parent_order_status' => array( 'completed', 'processing' ), // Partial refunds inside refunded orders should be ignored. ) ); $tax_rows = array(); foreach ( $tax_rows_orders + $tax_rows_partial_refunds as $tax_row ) { $key = date( ( 'month' === $this->chart_groupby ) ? 'Ym' : 'Ymd', strtotime( $tax_row->post_date ) ); $tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array( 'tax_amount' => 0, 'shipping_tax_amount' => 0, 'total_sales' => 0, 'total_shipping' => 0, 'total_orders' => 0, ); } foreach ( $tax_rows_orders as $tax_row ) { $key = date( ( 'month' === $this->chart_groupby ) ? 'Ym' : 'Ymd', strtotime( $tax_row->post_date ) ); $tax_rows[ $key ]->total_orders += $tax_row->total_orders; $tax_rows[ $key ]->tax_amount += $tax_row->tax_amount; $tax_rows[ $key ]->shipping_tax_amount += $tax_row->shipping_tax_amount; $tax_rows[ $key ]->total_sales += $tax_row->total_sales; $tax_rows[ $key ]->total_shipping += $tax_row->total_shipping; } foreach ( $tax_rows_partial_refunds as $tax_row ) { $key = date( ( 'month' === $this->chart_groupby ) ? 'Ym' : 'Ymd', strtotime( $tax_row->post_date ) ); $tax_rows[ $key ]->tax_amount += $tax_row->tax_amount; $tax_rows[ $key ]->shipping_tax_amount += $tax_row->shipping_tax_amount; $tax_rows[ $key ]->total_sales += $tax_row->total_sales; $tax_rows[ $key ]->total_shipping += $tax_row->total_shipping; } foreach ( $tax_rows_full_refunds as $tax_row ) { $key = date( ( 'month' === $this->chart_groupby ) ? 'Ym' : 'Ymd', strtotime( $tax_row->post_date ) ); $tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array( 'tax_amount' => 0, 'shipping_tax_amount' => 0, 'total_sales' => 0, 'total_shipping' => 0, 'total_orders' => 0, ); $parent_order = wc_get_order( $tax_row->post_parent ); if ( $parent_order ) { $tax_rows[ $key ]->tax_amount += $parent_order->get_cart_tax() * -1; $tax_rows[ $key ]->shipping_tax_amount += $parent_order->get_shipping_tax() * -1; $tax_rows[ $key ]->total_sales += $parent_order->get_total() * -1; $tax_rows[ $key ]->total_shipping += $parent_order->get_shipping_total() * -1; } } ?> $tax_row ) { $gross = $tax_row->total_sales - $tax_row->total_shipping; $total_tax = $tax_row->tax_amount + $tax_row->shipping_tax_amount; ?>
chart_groupby ) ? date_i18n( 'F', strtotime( $date . '01' ) ) : date_i18n( get_option( 'date_format' ), strtotime( $date ) ); ?> total_orders; ?> total_shipping ); ?>
sprintf( __( '%s signups in this period', 'woocommerce' ), '' . count( $this->customers ) . '' ), 'color' => $this->chart_colours['signups'], 'highlight_series' => 2, ); return $legend; } /** * Get chart widgets. * * @return array */ public function get_chart_widgets() { $widgets = array(); $widgets[] = array( 'title' => '', 'callback' => array( $this, 'customers_vs_guests' ), ); return $widgets; } /** * Output customers vs guests chart. */ public function customers_vs_guests() { $customer_order_totals = $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', 'name' => 'total_orders', ), ), 'where_meta' => array( array( 'meta_key' => '_customer_user', 'meta_value' => '0', 'operator' => '>', ), ), 'filter_range' => true, ) ); $guest_order_totals = $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', 'name' => 'total_orders', ), ), 'where_meta' => array( array( 'meta_key' => '_customer_user', 'meta_value' => '0', 'operator' => '=', ), ), 'filter_range' => true, ) ); ?>
__( 'Year', 'woocommerce' ), 'last_month' => __( 'Last month', 'woocommerce' ), 'month' => __( 'This month', 'woocommerce' ), '7day' => __( 'Last 7 days', 'woocommerce' ), ); $this->chart_colours = array( 'signups' => '#3498db', 'customers' => '#1abc9c', 'guests' => '#8fdece', ); $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ), true ) ) { $current_range = '7day'; } $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); $admin_users = new WP_User_Query( array( 'role' => 'administrator', 'fields' => 'ID', ) ); $manager_users = new WP_User_Query( array( 'role' => 'shop_manager', 'fields' => 'ID', ) ); $users_query = new WP_User_Query( apply_filters( 'woocommerce_admin_report_customers_user_query_args', array( 'fields' => array( 'user_registered' ), 'exclude' => array_merge( $admin_users->get_results(), $manager_users->get_results() ), ) ) ); $this->customers = $users_query->get_results(); foreach ( $this->customers as $key => $customer ) { if ( strtotime( $customer->user_registered ) < $this->start_date || strtotime( $customer->user_registered ) > $this->end_date ) { unset( $this->customers[ $key ] ); } } include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; } /** * Output an export link. */ public function get_export_button() { $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; ?> get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', 'name' => 'total_orders', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where_meta' => array( array( 'meta_key' => '_customer_user', 'meta_value' => '0', 'operator' => '>', ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, ) ); $guest_orders = $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', 'name' => 'total_orders', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where_meta' => array( array( 'meta_key' => '_customer_user', 'meta_value' => '0', 'operator' => '=', ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, ) ); $signups = $this->prepare_chart_data( $this->customers, 'user_registered', '', $this->chart_interval, $this->start_date, $this->chart_groupby ); $customer_orders = $this->prepare_chart_data( $customer_orders, 'post_date', 'total_orders', $this->chart_interval, $this->start_date, $this->chart_groupby ); $guest_orders = $this->prepare_chart_data( $guest_orders, 'post_date', 'total_orders', $this->chart_interval, $this->start_date, $this->chart_groupby ); $chart_data = wp_json_encode( array( 'signups' => array_values( $signups ), 'customer_orders' => array_values( $customer_orders ), 'guest_orders' => array_values( $guest_orders ), ) ); ?>
max_items = 0; $this->items = array(); $stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) ); $nostock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) ); $query_from = apply_filters( 'woocommerce_report_low_in_stock_query_from', $wpdb->prepare( " FROM {$wpdb->posts} as posts INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id WHERE 1=1 AND posts.post_type IN ( 'product', 'product_variation' ) AND posts.post_status = 'publish' AND lookup.stock_quantity <= %d AND lookup.stock_quantity > %d ", $stock, $nostock ) ); $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT SQL_CALC_FOUND_ROWS posts.ID as id, posts.post_parent as parent {$query_from} ORDER BY posts.post_title DESC LIMIT %d, %d;", ( $current_page - 1 ) * $per_page, $per_page ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $this->max_items = $wpdb->get_var( 'SELECT FOUND_ROWS();' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } } PKK[[¸zÃ!Ã!/admin/reports/class-wc-report-taxes-by-code.phpnu„[µü¤ __( 'Year', 'woocommerce' ), 'last_month' => __( 'Last month', 'woocommerce' ), 'month' => __( 'This month', 'woocommerce' ), ); $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : 'last_month'; if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) { $current_range = 'last_month'; } $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); $hide_sidebar = true; include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; } /** * Get the main chart. */ public function get_main_chart() { global $wpdb; $query_data = array( 'order_item_name' => array( 'type' => 'order_item', 'function' => '', 'name' => 'tax_rate', ), 'tax_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'tax', 'function' => '', 'name' => 'tax_amount', ), 'shipping_tax_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'tax', 'function' => '', 'name' => 'shipping_tax_amount', ), 'rate_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'tax', 'function' => '', 'name' => 'rate_id', ), 'ID' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_id', ), ); $query_where = array( array( 'key' => 'order_item_type', 'value' => 'tax', 'operator' => '=', ), array( 'key' => 'order_item_name', 'value' => '', 'operator' => '!=', ), ); // We exclude on-hold orders as they are still pending payment. $tax_rows_orders = $this->get_order_report_data( array( 'data' => $query_data, 'where' => $query_where, 'order_by' => 'posts.post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'sales-reports' ), 'order_status' => array( 'completed', 'processing', 'refunded' ), ) ); $tax_rows_partial_refunds = $this->get_order_report_data( array( 'data' => $query_data, 'where' => $query_where, 'order_by' => 'posts.post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => array( 'shop_order_refund' ), 'parent_order_status' => array( 'completed', 'processing' ), // Partial refunds inside refunded orders should be ignored. ) ); $tax_rows_full_refunds = $this->get_order_report_data( array( 'data' => $query_data, 'where' => $query_where, 'order_by' => 'posts.post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => array( 'shop_order_refund' ), 'parent_order_status' => array( 'refunded' ), ) ); // Merge. $tax_rows = array(); // Initialize an associative array to store unique post_ids. $unique_post_ids = array(); foreach ( $tax_rows_orders + $tax_rows_partial_refunds as $tax_row ) { $key = $tax_row->tax_rate; $tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array( 'tax_amount' => 0, 'shipping_tax_amount' => 0, 'total_orders' => 0, ); $tax_rows[ $key ]->tax_rate = $tax_row->tax_rate; $tax_rows[ $key ]->tax_amount += wc_round_tax_total( $tax_row->tax_amount ); $tax_rows[ $key ]->shipping_tax_amount += wc_round_tax_total( $tax_row->shipping_tax_amount ); if ( ! isset( $unique_post_ids[ $key ] ) || ! in_array( $tax_row->post_id, $unique_post_ids[ $key ], true ) ) { $unique_post_ids[ $key ] = isset( $unique_post_ids[ $key ] ) ? $unique_post_ids[ $key ] : array(); $unique_post_ids[ $key ][] = $tax_row->post_id; $tax_rows[ $key ]->total_orders += 1; } } foreach ( $tax_rows_full_refunds as $tax_row ) { $key = $tax_row->tax_rate; $tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array( 'tax_amount' => 0, 'shipping_tax_amount' => 0, 'total_orders' => 0, ); $tax_rows[ $key ]->tax_rate = $tax_row->tax_rate; $tax_rows[ $key ]->tax_amount += wc_round_tax_total( $tax_row->tax_amount ); $tax_rows[ $key ]->shipping_tax_amount += wc_round_tax_total( $tax_row->shipping_tax_amount ); } ?> $tax_row ) { $rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $rate_id ) ); ?>
tax_rate, $rate_id, $tax_row ) ); ?> % total_orders ); ?> tax_amount ); // phpcs:ignore ?> shipping_tax_amount ); // phpcs:ignore ?> tax_amount + $tax_row->shipping_tax_amount ); // phpcs:ignore ?>
'stock', 'plural' => 'stock', 'ajax' => false, ) ); } /** * No items found text. */ public function no_items() { _e( 'No products found.', 'woocommerce' ); } /** * Don't need this. * * @param string $position */ public function display_tablenav( $position ) { if ( 'top' !== $position ) { parent::display_tablenav( $position ); } } /** * Output the report. */ public function output_report() { $this->prepare_items(); echo '
'; $this->display(); echo '
'; } /** * Get column value. * * @param mixed $item * @param string $column_name */ public function column_default( $item, $column_name ) { global $product; if ( ! $product || $product->get_id() !== $item->id ) { $product = wc_get_product( $item->id ); } if ( ! $product ) { return; } switch ( $column_name ) { case 'product': if ( $sku = $product->get_sku() ) { echo esc_html( $sku ) . ' - '; } echo esc_html( $product->get_name() ); // Get variation data. if ( $product->is_type( 'variation' ) ) { echo '
' . wp_kses_post( wc_get_formatted_variation( $product, true ) ) . '
'; } break; case 'parent': if ( $item->parent ) { echo esc_html( get_the_title( $item->parent ) ); } else { echo '-'; } break; case 'stock_status': if ( $product->is_on_backorder() ) { $stock_html = '' . __( 'On backorder', 'woocommerce' ) . ''; } elseif ( $product->is_in_stock() ) { $stock_html = '' . __( 'In stock', 'woocommerce' ) . ''; } else { $stock_html = '' . __( 'Out of stock', 'woocommerce' ) . ''; } echo apply_filters( 'woocommerce_admin_stock_html', $stock_html, $product ); break; case 'stock_level': echo esc_html( $product->get_stock_quantity() ); break; case 'wc_actions': ?>

is_type( 'variation' ) ? $item->parent : $item->id; $actions['edit'] = array( 'url' => admin_url( 'post.php?post=' . $action_id . '&action=edit' ), 'name' => __( 'Edit', 'woocommerce' ), 'action' => 'edit', ); if ( $product->is_visible() ) { $actions['view'] = array( 'url' => get_permalink( $action_id ), 'name' => __( 'View', 'woocommerce' ), 'action' => 'view', ); } $actions = apply_filters( 'woocommerce_admin_stock_report_product_actions', $actions, $product ); foreach ( $actions as $action ) { printf( '%4$s', esc_attr( $action['action'] ), esc_url( $action['url'] ), sprintf( esc_attr__( '%s product', 'woocommerce' ), $action['name'] ), esc_html( $action['name'] ) ); } ?>

__( 'Product', 'woocommerce' ), 'parent' => __( 'Parent', 'woocommerce' ), 'stock_level' => __( 'Units in stock', 'woocommerce' ), 'stock_status' => __( 'Stock status', 'woocommerce' ), 'wc_actions' => __( 'Actions', 'woocommerce' ), ); return $columns; } /** * Prepare customer list items. */ public function prepare_items() { $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() ); $current_page = absint( $this->get_pagenum() ); $per_page = apply_filters( 'woocommerce_admin_stock_report_products_per_page', 20 ); $this->get_items( $current_page, $per_page ); /** * Pagination. */ $this->set_pagination_args( array( 'total_items' => $this->max_items, 'per_page' => $per_page, 'total_pages' => ceil( $this->max_items / $per_page ), ) ); } } PKK[¼põ>l}l}/admin/reports/class-wc-report-sales-by-date.phpnu„[µü¤report_data ) ) { $this->query_report_data(); } return $this->report_data; } /** * Get all data needed for this report and store in the class. */ private function query_report_data() { $this->report_data = new stdClass(); $this->report_data->order_counts = (array) $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'function' => 'COUNT', 'name' => 'count', 'distinct' => true, ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); $this->report_data->coupons = (array) $this->get_order_report_data( array( 'data' => array( 'order_item_name' => array( 'type' => 'order_item', 'function' => '', 'name' => 'order_item_name', ), 'discount_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'coupon', 'function' => 'SUM', 'name' => 'discount_amount', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'order_items.order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'group_by' => $this->group_by_query . ', order_item_name', 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); // All items from orders - even those refunded. $this->report_data->order_items = (array) $this->get_order_report_data( array( 'data' => array( '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_count', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'order_items.order_item_type', 'value' => 'line_item', 'operator' => '=', ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); /** * Get total of fully refunded items. */ $this->report_data->refunded_order_items = absint( $this->get_order_report_data( array( 'data' => array( '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_count', ), ), 'where' => array( array( 'key' => 'order_items.order_item_type', 'value' => 'line_item', 'operator' => '=', ), ), 'query_type' => 'get_var', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), 'order_status' => array( 'refunded' ), ) ) ); /** * Order totals by date. Charts should show GROSS amounts to avoid going -ve. */ $this->report_data->orders = (array) $this->get_order_report_data( array( 'data' => array( '_order_total' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_sales', ), '_order_shipping' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_shipping', ), '_order_tax' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_tax', ), '_order_shipping_tax' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'total_shipping_tax', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'sales-reports' ), 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); /** * If an order is 100% refunded we should look at the parent's totals, but the refunds dates. * We also need to ensure each parent order's values are only counted/summed once. */ $this->report_data->full_refunds = (array) $this->get_order_report_data( array( 'data' => array( '_order_total' => array( 'type' => 'parent_meta', 'function' => '', 'name' => 'total_refund', ), '_order_shipping' => array( 'type' => 'parent_meta', 'function' => '', 'name' => 'total_shipping', ), '_order_tax' => array( 'type' => 'parent_meta', 'function' => '', 'name' => 'total_tax', ), '_order_shipping_tax' => array( 'type' => 'parent_meta', 'function' => '', 'name' => 'total_shipping_tax', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'group_by' => 'posts.post_parent', 'query_type' => 'get_results', 'filter_range' => true, 'order_status' => false, 'parent_order_status' => array( 'refunded' ), ) ); foreach ( $this->report_data->full_refunds as $key => $order ) { $total_refund = is_numeric( $order->total_refund ) ? $order->total_refund : 0; $total_shipping = is_numeric( $order->total_shipping ) ? $order->total_shipping : 0; $total_tax = is_numeric( $order->total_tax ) ? $order->total_tax : 0; $total_shipping_tax = is_numeric( $order->total_shipping_tax ) ? $order->total_shipping_tax : 0; $this->report_data->full_refunds[ $key ]->net_refund = $total_refund - ( $total_shipping + $total_tax + $total_shipping_tax ); } /** * Partial refunds. This includes line items, shipping and taxes. Not grouped by date. */ $this->report_data->partial_refunds = (array) $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'function' => '', 'name' => 'refund_id', ), '_refund_amount' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_refund', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), 'order_item_type' => array( 'type' => 'order_item', 'function' => '', 'name' => 'item_type', 'join_type' => 'LEFT', ), '_order_total' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_sales', ), '_order_shipping' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_shipping', 'join_type' => 'LEFT', ), '_order_tax' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_tax', 'join_type' => 'LEFT', ), '_order_shipping_tax' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_shipping_tax', 'join_type' => 'LEFT', ), '_qty' => array( 'type' => 'order_item_meta', 'function' => 'SUM', 'name' => 'order_item_count', 'join_type' => 'LEFT', ), ), 'group_by' => 'refund_id', 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_status' => false, 'parent_order_status' => array( 'completed', 'processing', 'on-hold' ), ) ); foreach ( $this->report_data->partial_refunds as $key => $order ) { $this->report_data->partial_refunds[ $key ]->net_refund = (float) $order->total_refund - ( (float) $order->total_shipping + (float) $order->total_tax + (float) $order->total_shipping_tax ); } /** * Refund lines - all partial refunds on all order types so we can plot full AND partial refunds on the chart. */ $this->report_data->refund_lines = (array) $this->get_order_report_data( array( 'data' => array( 'ID' => array( 'type' => 'post_data', 'function' => '', 'name' => 'refund_id', ), '_refund_amount' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_refund', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), 'order_item_type' => array( 'type' => 'order_item', 'function' => '', 'name' => 'item_type', 'join_type' => 'LEFT', ), '_order_total' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_sales', ), '_order_shipping' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_shipping', 'join_type' => 'LEFT', ), '_order_tax' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_tax', 'join_type' => 'LEFT', ), '_order_shipping_tax' => array( 'type' => 'meta', 'function' => '', 'name' => 'total_shipping_tax', 'join_type' => 'LEFT', ), '_qty' => array( 'type' => 'order_item_meta', 'function' => 'SUM', 'name' => 'order_item_count', 'join_type' => 'LEFT', ), ), 'group_by' => 'refund_id', 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_status' => false, 'parent_order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); /** * Total up refunds. Note: when an order is fully refunded, a refund line will be added. */ $this->report_data->total_tax_refunded = 0; $this->report_data->total_shipping_refunded = 0; $this->report_data->total_shipping_tax_refunded = 0; $this->report_data->total_refunds = 0; $this->report_data->refunded_orders = array_merge( $this->report_data->partial_refunds, $this->report_data->full_refunds ); foreach ( $this->report_data->refunded_orders as $key => $value ) { $this->report_data->total_tax_refunded += floatval( $value->total_tax < 0 ? $value->total_tax * -1 : $value->total_tax ); $this->report_data->total_refunds += floatval( $value->total_refund ); $this->report_data->total_shipping_tax_refunded += floatval( $value->total_shipping_tax < 0 ? $value->total_shipping_tax * -1 : $value->total_shipping_tax ); $this->report_data->total_shipping_refunded += floatval( $value->total_shipping < 0 ? $value->total_shipping * -1 : $value->total_shipping ); // Only applies to partial. if ( isset( $value->order_item_count ) ) { $this->report_data->refunded_order_items += floatval( $value->order_item_count < 0 ? $value->order_item_count * -1 : $value->order_item_count ); } } // Totals from all orders - including those refunded. Subtract refunded amounts. $this->report_data->total_tax = wc_format_decimal( array_sum( wp_list_pluck( $this->report_data->orders, 'total_tax' ) ) - $this->report_data->total_tax_refunded, 2 ); $this->report_data->total_shipping = wc_format_decimal( array_sum( wp_list_pluck( $this->report_data->orders, 'total_shipping' ) ) - $this->report_data->total_shipping_refunded, 2 ); $this->report_data->total_shipping_tax = wc_format_decimal( array_sum( wp_list_pluck( $this->report_data->orders, 'total_shipping_tax' ) ) - $this->report_data->total_shipping_tax_refunded, 2 ); // Total the refunds and sales amounts. Sales subtract refunds. Note - total_sales also includes shipping costs. $this->report_data->total_sales = wc_format_decimal( array_sum( wp_list_pluck( $this->report_data->orders, 'total_sales' ) ) - $this->report_data->total_refunds, 2 ); $this->report_data->net_sales = wc_format_decimal( $this->report_data->total_sales - $this->report_data->total_shipping - max( 0, $this->report_data->total_tax ) - max( 0, $this->report_data->total_shipping_tax ), 2 ); // Calculate average based on net. $this->report_data->average_sales = wc_format_decimal( $this->report_data->net_sales / ( $this->chart_interval + 1 ), 2 ); $this->report_data->average_total_sales = wc_format_decimal( $this->report_data->total_sales / ( $this->chart_interval + 1 ), 2 ); // Total orders and discounts also includes those which have been refunded at some point. $this->report_data->total_coupons = number_format( array_sum( wp_list_pluck( $this->report_data->coupons, 'discount_amount' ) ), 2, '.', '' ); $this->report_data->total_refunded_orders = absint( count( $this->report_data->full_refunds ) ); // Total orders in this period, even if refunded. $this->report_data->total_orders = absint( array_sum( wp_list_pluck( $this->report_data->order_counts, 'count' ) ) ); // Item items ordered in this period, even if refunded. $this->report_data->total_items = absint( array_sum( wp_list_pluck( $this->report_data->order_items, 'order_item_count' ) ) ); // 3rd party filtering of report data $this->report_data = apply_filters( 'woocommerce_admin_report_data', $this->report_data ); } /** * Get the legend for the main chart sidebar. * * @return array */ public function get_chart_legend() { $legend = array(); $data = $this->get_report_data(); switch ( $this->chart_groupby ) { case 'day': $average_total_sales_title = sprintf( /* translators: %s: average total sales */ __( '%s average gross daily sales', 'woocommerce' ), '' . wc_price( $data->average_total_sales ) . '' ); $average_sales_title = sprintf( /* translators: %s: average sales */ __( '%s average net daily sales', 'woocommerce' ), '' . wc_price( $data->average_sales ) . '' ); break; case 'month': default: $average_total_sales_title = sprintf( /* translators: %s: average total sales */ __( '%s average gross monthly sales', 'woocommerce' ), '' . wc_price( $data->average_total_sales ) . '' ); $average_sales_title = sprintf( /* translators: %s: average sales */ __( '%s average net monthly sales', 'woocommerce' ), '' . wc_price( $data->average_sales ) . '' ); break; } $legend[] = array( 'title' => sprintf( /* translators: %s: total sales */ __( '%s gross sales in this period', 'woocommerce' ), '' . wc_price( $data->total_sales ) . '' ), 'placeholder' => __( 'This is the sum of the order totals after any refunds and including shipping and taxes.', 'woocommerce' ), 'color' => $this->chart_colours['sales_amount'], 'highlight_series' => 6, ); if ( $data->average_total_sales > 0 ) { $legend[] = array( 'title' => $average_total_sales_title, 'color' => $this->chart_colours['average'], 'highlight_series' => 2, ); } $legend[] = array( 'title' => sprintf( /* translators: %s: net sales */ __( '%s net sales in this period', 'woocommerce' ), '' . wc_price( $data->net_sales ) . '' ), 'placeholder' => __( 'This is the sum of the order totals after any refunds and excluding shipping and taxes.', 'woocommerce' ), 'color' => $this->chart_colours['net_sales_amount'], 'highlight_series' => 7, ); if ( $data->average_sales > 0 ) { $legend[] = array( 'title' => $average_sales_title, 'color' => $this->chart_colours['net_average'], 'highlight_series' => 3, ); } $legend[] = array( 'title' => sprintf( /* translators: %s: total orders */ __( '%s orders placed', 'woocommerce' ), '' . $data->total_orders . '' ), 'color' => $this->chart_colours['order_count'], 'highlight_series' => 1, ); $legend[] = array( 'title' => sprintf( /* translators: %s: total items */ __( '%s items purchased', 'woocommerce' ), '' . $data->total_items . '' ), 'color' => $this->chart_colours['item_count'], 'highlight_series' => 0, ); $legend[] = array( 'title' => sprintf( /* translators: 1: total refunds 2: total refunded orders 3: refunded items */ _n( '%1$s refunded %2$d order (%3$d item)', '%1$s refunded %2$d orders (%3$d items)', $this->report_data->total_refunded_orders, 'woocommerce' ), '' . wc_price( $data->total_refunds ) . '', $this->report_data->total_refunded_orders, $this->report_data->refunded_order_items ), 'color' => $this->chart_colours['refund_amount'], 'highlight_series' => 8, ); $legend[] = array( 'title' => sprintf( /* translators: %s: total shipping */ __( '%s charged for shipping', 'woocommerce' ), '' . wc_price( $data->total_shipping ) . '' ), 'color' => $this->chart_colours['shipping_amount'], 'highlight_series' => 5, ); $legend[] = array( 'title' => sprintf( /* translators: %s: total coupons */ __( '%s worth of coupons used', 'woocommerce' ), '' . wc_price( $data->total_coupons ) . '' ), 'color' => $this->chart_colours['coupon_amount'], 'highlight_series' => 4, ); return $legend; } /** * Output the report. */ public function output_report() { $ranges = array( 'year' => __( 'Year', 'woocommerce' ), 'last_month' => __( 'Last month', 'woocommerce' ), 'month' => __( 'This month', 'woocommerce' ), '7day' => __( 'Last 7 days', 'woocommerce' ), ); $this->chart_colours = array( 'sales_amount' => '#b1d4ea', 'net_sales_amount' => '#3498db', 'average' => '#b1d4ea', 'net_average' => '#3498db', 'order_count' => '#dbe1e3', 'item_count' => '#ecf0f1', 'shipping_amount' => '#5cc488', 'coupon_amount' => '#f1c40f', 'refund_amount' => '#e74c3c', ); $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ), true ) ) { $current_range = '7day'; } $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; } /** * Output an export link. */ public function get_export_button() { $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?> $this->prepare_chart_data( $this->report_data->order_counts, 'post_date', 'count', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'order_item_counts' => $this->prepare_chart_data( $this->report_data->order_items, 'post_date', 'order_item_count', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'order_amounts' => $this->prepare_chart_data( $this->report_data->orders, 'post_date', 'total_sales', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'coupon_amounts' => $this->prepare_chart_data( $this->report_data->coupons, 'post_date', 'discount_amount', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'shipping_amounts' => $this->prepare_chart_data( $this->report_data->orders, 'post_date', 'total_shipping', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'refund_amounts' => $this->prepare_chart_data( $this->report_data->refund_lines, 'post_date', 'total_refund', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'net_refund_amounts' => $this->prepare_chart_data( $this->report_data->refunded_orders, 'post_date', 'net_refund', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'shipping_tax_amounts' => $this->prepare_chart_data( $this->report_data->orders, 'post_date', 'total_shipping_tax', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'tax_amounts' => $this->prepare_chart_data( $this->report_data->orders, 'post_date', 'total_tax', $this->chart_interval, $this->start_date, $this->chart_groupby ), 'net_order_amounts' => array(), 'gross_order_amounts' => array(), ); foreach ( $data['order_amounts'] as $order_amount_key => $order_amount_value ) { $data['gross_order_amounts'][ $order_amount_key ] = $order_amount_value; $data['gross_order_amounts'][ $order_amount_key ][1] -= $data['refund_amounts'][ $order_amount_key ][1]; $data['net_order_amounts'][ $order_amount_key ] = $order_amount_value; // Subtract the sum of the values from net order amounts. $data['net_order_amounts'][ $order_amount_key ][1] -= $data['net_refund_amounts'][ $order_amount_key ][1] + $data['shipping_amounts'][ $order_amount_key ][1] + $data['shipping_tax_amounts'][ $order_amount_key ][1] + $data['tax_amounts'][ $order_amount_key ][1]; } // 3rd party filtering of report data. $data = apply_filters( 'woocommerce_admin_report_chart_data', $data ); // Encode in json format. $chart_data = wp_json_encode( array( 'order_counts' => array_values( $data['order_counts'] ), 'order_item_counts' => array_values( $data['order_item_counts'] ), 'order_amounts' => array_map( array( $this, 'round_chart_totals' ), array_values( $data['order_amounts'] ) ), 'gross_order_amounts' => array_map( array( $this, 'round_chart_totals' ), array_values( $data['gross_order_amounts'] ) ), 'net_order_amounts' => array_map( array( $this, 'round_chart_totals' ), array_values( $data['net_order_amounts'] ) ), 'shipping_amounts' => array_map( array( $this, 'round_chart_totals' ), array_values( $data['shipping_amounts'] ) ), 'coupon_amounts' => array_map( array( $this, 'round_chart_totals' ), array_values( $data['coupon_amounts'] ) ), 'refund_amounts' => array_map( array( $this, 'round_chart_totals' ), array_values( $data['refund_amounts'] ) ), ) ); ?>
product_ids = array_filter( array_map( 'absint', $_GET['product_ids'] ) ); } elseif ( isset( $_GET['product_ids'] ) ) { $this->product_ids = array_filter( array( absint( $_GET['product_ids'] ) ) ); } // @codingStandardsIgnoreEnd } /** * Get the legend for the main chart sidebar. * * @return array */ public function get_chart_legend() { if ( empty( $this->product_ids ) ) { return array(); } $legend = array(); $total_sales = $this->get_order_report_data( array( 'data' => array( '_line_total' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_amount', ), ), 'where_meta' => array( 'relation' => 'OR', array( 'type' => 'order_item_meta', 'meta_key' => array( '_product_id', '_variation_id' ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $this->product_ids, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'operator' => 'IN', ), ), 'query_type' => 'get_var', 'filter_range' => true, 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); $total_items = absint( $this->get_order_report_data( array( 'data' => array( '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_count', ), ), 'where_meta' => array( 'relation' => 'OR', array( 'type' => 'order_item_meta', 'meta_key' => array( '_product_id', '_variation_id' ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $this->product_ids, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'operator' => 'IN', ), ), 'query_type' => 'get_var', 'filter_range' => true, 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ) ); $legend[] = array( /* translators: %s: total items sold */ 'title' => sprintf( __( '%s sales for the selected items', 'woocommerce' ), '' . wc_price( $total_sales ) . '' ), 'color' => $this->chart_colours['sales_amount'], 'highlight_series' => 1, ); $legend[] = array( /* translators: %s: total items purchased */ 'title' => sprintf( __( '%s purchases for the selected items', 'woocommerce' ), '' . ( $total_items ) . '' ), 'color' => $this->chart_colours['item_count'], 'highlight_series' => 0, ); return $legend; } /** * Output the report. */ public function output_report() { $ranges = array( 'year' => __( 'Year', 'woocommerce' ), 'last_month' => __( 'Last month', 'woocommerce' ), 'month' => __( 'This month', 'woocommerce' ), '7day' => __( 'Last 7 days', 'woocommerce' ), ); $this->chart_colours = array( 'sales_amount' => '#3498db', 'item_count' => '#d4d9dc', ); $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; //phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ), true ) ) { $current_range = '7day'; } $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; } /** * Get chart widgets. * * @return array */ public function get_chart_widgets() { $widgets = array(); if ( ! empty( $this->product_ids ) ) { $widgets[] = array( 'title' => __( 'Showing reports for:', 'woocommerce' ), 'callback' => array( $this, 'current_filters' ), ); } $widgets[] = array( 'title' => '', 'callback' => array( $this, 'products_widget' ), ); return $widgets; } /** * Output current filters. */ public function current_filters() { $this->product_ids_titles = array(); foreach ( $this->product_ids as $product_id ) { $product = wc_get_product( $product_id ); if ( $product ) { $this->product_ids_titles[] = $product->get_formatted_name(); } else { $this->product_ids_titles[] = '#' . $product_id; } } echo '

' . wp_kses_post( implode( ', ', $this->product_ids_titles ) ) . '

'; echo '

' . esc_html__( 'Reset', 'woocommerce' ) . '

'; } /** * Output products widget. */ public function products_widget() { ?>

get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_qty', ), ), 'order_by' => 'order_item_qty DESC', 'group_by' => 'product_id', 'limit' => 12, 'query_type' => 'get_results', 'filter_range' => true, 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); if ( $top_sellers ) { // @codingStandardsIgnoreStart foreach ( $top_sellers as $product ) { echo ''; } // @codingStandardsIgnoreEnd } else { echo ''; } ?>
' . esc_html( $product->order_item_qty ) . ' ' . esc_html( get_the_title( $product->product_id ) ) . ' ' . $this->sales_sparkline( $product->product_id, 7, 'count' ) . '
' . esc_html__( 'No products found in range', 'woocommerce' ) . '

get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_qty', ), ), 'where_meta' => array( array( 'type' => 'order_item_meta', 'meta_key' => '_line_subtotal', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => '0', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'operator' => '=', ), ), 'order_by' => 'order_item_qty DESC', 'group_by' => 'product_id', 'limit' => 12, 'query_type' => 'get_results', 'filter_range' => true, ) ); if ( $top_freebies ) { // @codingStandardsIgnoreStart foreach ( $top_freebies as $product ) { echo ''; } // @codingStandardsIgnoreEnd } else { echo ''; } ?>
' . esc_html( $product->order_item_qty ) . ' ' . esc_html( get_the_title( $product->product_id ) ) . ' ' . $this->sales_sparkline( $product->product_id, 7, 'count' ) . '
' . esc_html__( 'No products found in range', 'woocommerce' ) . '

get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), '_line_total' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_total', ), ), 'order_by' => 'order_item_total DESC', 'group_by' => 'product_id', 'limit' => 12, 'query_type' => 'get_results', 'filter_range' => true, 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); if ( $top_earners ) { // @codingStandardsIgnoreStart foreach ( $top_earners as $product ) { echo ''; } // @codingStandardsIgnoreEnd } else { echo ''; } ?>
' . wc_price( $product->order_item_total ) . ' ' . esc_html( get_the_title( $product->product_id ) ) . ' ' . $this->sales_sparkline( $product->product_id, 7, 'sales' ) . '
' . esc_html__( 'No products found in range', 'woocommerce' ) . '
> product_ids ) ) { ?>

get_order_report_data( array( 'data' => array( '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_count', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), ), 'where_meta' => array( 'relation' => 'OR', array( 'type' => 'order_item_meta', 'meta_key' => array( '_product_id', '_variation_id' ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $this->product_ids, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'operator' => 'IN', ), ), 'group_by' => 'product_id,' . $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); $order_item_amounts = $this->get_order_report_data( array( 'data' => array( '_line_total' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_amount', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), ), 'where_meta' => array( 'relation' => 'OR', array( 'type' => 'order_item_meta', 'meta_key' => array( '_product_id', '_variation_id' ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $this->product_ids, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'operator' => 'IN', ), ), 'group_by' => 'product_id, ' . $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_status' => array( 'completed', 'processing', 'on-hold', 'refunded' ), ) ); // Prepare data for report. $order_item_counts = $this->prepare_chart_data( $order_item_counts, 'post_date', 'order_item_count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $order_item_amounts = $this->prepare_chart_data( $order_item_amounts, 'post_date', 'order_item_amount', $this->chart_interval, $this->start_date, $this->chart_groupby ); // Encode in json format. $chart_data = wp_json_encode( array( 'order_item_counts' => array_values( $order_item_counts ), 'order_item_amounts' => array_values( $order_item_amounts ), ) ); ?>
coupon_codes = array_filter( array_map( 'sanitize_text_field', wp_unslash( $_GET['coupon_codes'] ) ) ); } elseif ( isset( $_GET['coupon_codes'] ) ) { $this->coupon_codes = array_filter( array( sanitize_text_field( wp_unslash( $_GET['coupon_codes'] ) ) ) ); } } /** * Get the legend for the main chart sidebar. * * @return array */ public function get_chart_legend() { $legend = array(); $total_discount_query = array( 'data' => array( 'discount_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'coupon', 'function' => 'SUM', 'name' => 'discount_amount', ), ), 'where' => array( array( 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'query_type' => 'get_var', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), ); $total_coupons_query = array( 'data' => array( 'order_item_id' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => 'COUNT', 'name' => 'order_coupon_count', ), ), 'where' => array( array( 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'query_type' => 'get_var', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), ); if ( ! empty( $this->coupon_codes ) ) { $coupon_code_query = array( 'type' => 'order_item', 'key' => 'order_item_name', 'value' => $this->coupon_codes, 'operator' => 'IN', ); $total_discount_query['where'][] = $coupon_code_query; $total_coupons_query['where'][] = $coupon_code_query; } $total_discount = $this->get_order_report_data( $total_discount_query ); $total_coupons = absint( $this->get_order_report_data( $total_coupons_query ) ); $legend[] = array( /* translators: %s: discount amount */ 'title' => sprintf( __( '%s discounts in total', 'woocommerce' ), '' . wc_price( $total_discount ) . '' ), 'color' => $this->chart_colours['discount_amount'], 'highlight_series' => 1, ); $legend[] = array( /* translators: %s: coupons amount */ 'title' => sprintf( __( '%s coupons used in total', 'woocommerce' ), '' . $total_coupons . '' ), 'color' => $this->chart_colours['coupon_count'], 'highlight_series' => 0, ); return $legend; } /** * Output the report. */ public function output_report() { $ranges = array( 'year' => __( 'Year', 'woocommerce' ), 'last_month' => __( 'Last month', 'woocommerce' ), 'month' => __( 'This month', 'woocommerce' ), '7day' => __( 'Last 7 days', 'woocommerce' ), ); $this->chart_colours = array( 'discount_amount' => '#3498db', 'coupon_count' => '#d4d9dc', ); $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) { $current_range = '7day'; } $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; } /** * Get chart widgets. * * @return array */ public function get_chart_widgets() { $widgets = array(); $widgets[] = array( 'title' => '', 'callback' => array( $this, 'coupons_widget' ), ); return $widgets; } /** * Output coupons widget. */ public function coupons_widget() { ?>

get_order_report_data( array( 'data' => array( 'order_item_name' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => '', 'distinct' => true, 'name' => 'order_item_name', ), ), 'where' => array( array( 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'query_type' => 'get_col', 'filter_range' => false, ) ); if ( ! empty( $used_coupons ) && is_array( $used_coupons ) ) : ?>

get_order_report_data( array( 'data' => array( 'order_item_name' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => '', 'name' => 'coupon_code', ), 'order_item_id' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => 'COUNT', 'name' => 'coupon_count', ), ), 'where' => array( array( 'type' => 'order_item', 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'order_by' => 'coupon_count DESC', 'group_by' => 'order_item_name', 'limit' => 12, 'query_type' => 'get_results', 'filter_range' => true, ) ); if ( ! empty( $most_popular ) && is_array( $most_popular ) ) { foreach ( $most_popular as $coupon ) { echo ''; } } else { echo ''; } ?>
' . esc_html( $coupon->coupon_count ) . ' ' . esc_html( $coupon->coupon_code ) . '
' . esc_html__( 'No coupons found in range', 'woocommerce' ) . '

get_order_report_data( array( 'data' => array( 'order_item_name' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => '', 'name' => 'coupon_code', ), 'discount_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'coupon', 'function' => 'SUM', 'name' => 'discount_amount', ), ), 'where' => array( array( 'type' => 'order_item', 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'order_by' => 'discount_amount DESC', 'group_by' => 'order_item_name', 'limit' => 12, 'query_type' => 'get_results', 'filter_range' => true, ) ); if ( ! empty( $most_discount ) && is_array( $most_discount ) ) { foreach ( $most_discount as $coupon ) { // @codingStandardsIgnoreStart echo ''; // @codingStandardsIgnoreEnd } } else { echo ''; } ?>
' . wc_price( $coupon->discount_amount ) . ' ' . esc_html( $coupon->coupon_code ) . '
' . esc_html__( 'No coupons found in range', 'woocommerce' ) . '
array( 'order_item_name' => array( 'type' => 'order_item', 'order_item_type' => 'coupon', 'function' => 'COUNT', 'name' => 'order_coupon_count', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'group_by' => $this->group_by_query, 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), ); $order_discount_amounts_query = array( 'data' => array( 'discount_amount' => array( 'type' => 'order_item_meta', 'order_item_type' => 'coupon', 'function' => 'SUM', 'name' => 'discount_amount', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'order_item_type', 'value' => 'coupon', 'operator' => '=', ), ), 'group_by' => $this->group_by_query . ', order_item_name', 'order_by' => 'post_date ASC', 'query_type' => 'get_results', 'filter_range' => true, 'order_types' => wc_get_order_types( 'order-count' ), ); if ( ! empty( $this->coupon_codes ) ) { $coupon_code_query = array( 'type' => 'order_item', 'key' => 'order_item_name', 'value' => $this->coupon_codes, 'operator' => 'IN', ); $order_coupon_counts_query['where'][] = $coupon_code_query; $order_discount_amounts_query['where'][] = $coupon_code_query; } $order_coupon_counts = $this->get_order_report_data( $order_coupon_counts_query ); $order_discount_amounts = $this->get_order_report_data( $order_discount_amounts_query ); // Prepare data for report. $order_coupon_counts = $this->prepare_chart_data( $order_coupon_counts, 'post_date', 'order_coupon_count', $this->chart_interval, $this->start_date, $this->chart_groupby ); $order_discount_amounts = $this->prepare_chart_data( $order_discount_amounts, 'post_date', 'discount_amount', $this->chart_interval, $this->start_date, $this->chart_groupby ); // Encode in json format. $chart_data = wp_json_encode( array( 'order_coupon_counts' => array_values( $order_coupon_counts ), 'order_discount_amounts' => array_values( $order_discount_amounts ), ) ); ?>
array( * 'type' => 'meta', * 'function' => 'SUM', * 'name' => 'total_sales' * ) * * @param array $args arguments for the report. * @return mixed depending on query_type */ public function get_order_report_data( $args = array() ) { global $wpdb; $default_args = array( 'data' => array(), 'where' => array(), 'where_meta' => array(), 'query_type' => 'get_row', 'group_by' => '', 'order_by' => '', 'limit' => '', 'filter_range' => false, 'nocache' => false, 'debug' => false, 'order_types' => wc_get_order_types( 'reports' ), 'order_status' => array( 'completed', 'processing', 'on-hold' ), 'parent_order_status' => false, ); $args = apply_filters( 'woocommerce_reports_get_order_report_data_args', $args ); $args = wp_parse_args( $args, $default_args ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract extract( $args ); if ( empty( $data ) ) { return ''; } $order_status = apply_filters( 'woocommerce_reports_order_statuses', $order_status ); $query = array(); $select = array(); foreach ( $data as $raw_key => $value ) { $key = sanitize_key( $raw_key ); $distinct = ''; if ( isset( $value['distinct'] ) ) { $distinct = 'DISTINCT'; } switch ( $value['type'] ) { case 'meta': $get_key = "meta_{$key}.meta_value"; break; case 'parent_meta': $get_key = "parent_meta_{$key}.meta_value"; break; case 'post_data': $get_key = "posts.{$key}"; break; case 'order_item_meta': $get_key = "order_item_meta_{$key}.meta_value"; break; case 'order_item': $get_key = "order_items.{$key}"; break; } if ( empty( $get_key ) ) { // Skip to the next foreach iteration else the query will be invalid. continue; } if ( $value['function'] ) { $get = "{$value['function']}({$distinct} {$get_key})"; } else { $get = "{$distinct} {$get_key}"; } $select[] = "{$get} as {$value['name']}"; } $query['select'] = 'SELECT ' . implode( ',', $select ); $query['from'] = "FROM {$wpdb->posts} AS posts"; // Joins. $joins = array(); foreach ( ( $data + $where ) as $raw_key => $value ) { $join_type = isset( $value['join_type'] ) ? $value['join_type'] : 'INNER'; $type = isset( $value['type'] ) ? $value['type'] : false; $key = sanitize_key( $raw_key ); switch ( $type ) { case 'meta': $joins[ "meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS meta_{$key} ON ( posts.ID = meta_{$key}.post_id AND meta_{$key}.meta_key = '{$raw_key}' )"; break; case 'parent_meta': $joins[ "parent_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS parent_meta_{$key} ON (posts.post_parent = parent_meta_{$key}.post_id) AND (parent_meta_{$key}.meta_key = '{$raw_key}')"; break; case 'order_item_meta': $joins['order_items'] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON (posts.ID = order_items.order_id)"; if ( ! empty( $value['order_item_type'] ) ) { $joins['order_items'] .= " AND (order_items.order_item_type = '{$value['order_item_type']}')"; } $joins[ "order_item_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_{$key} ON " . "(order_items.order_item_id = order_item_meta_{$key}.order_item_id) " . " AND (order_item_meta_{$key}.meta_key = '{$raw_key}')"; break; case 'order_item': $joins['order_items'] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id"; break; } } if ( ! empty( $where_meta ) ) { foreach ( $where_meta as $value ) { if ( ! is_array( $value ) ) { continue; } $join_type = isset( $value['join_type'] ) ? $value['join_type'] : 'INNER'; $type = isset( $value['type'] ) ? $value['type'] : false; $key = sanitize_key( is_array( $value['meta_key'] ) ? $value['meta_key'][0] . '_array' : $value['meta_key'] ); if ( 'order_item_meta' === $type ) { $joins['order_items'] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_items.order_id"; $joins[ "order_item_meta_{$key}" ] = "{$join_type} JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_{$key} ON order_items.order_item_id = order_item_meta_{$key}.order_item_id"; } else { // If we have a where clause for meta, join the postmeta table. $joins[ "meta_{$key}" ] = "{$join_type} JOIN {$wpdb->postmeta} AS meta_{$key} ON posts.ID = meta_{$key}.post_id"; } } } if ( ! empty( $parent_order_status ) ) { $joins['parent'] = "LEFT JOIN {$wpdb->posts} AS parent ON posts.post_parent = parent.ID"; } $query['join'] = implode( ' ', $joins ); $query['where'] = " WHERE posts.post_type IN ( '" . implode( "','", $order_types ) . "' ) "; if ( ! empty( $order_status ) ) { $query['where'] .= " AND posts.post_status IN ( 'wc-" . implode( "','wc-", $order_status ) . "') "; } if ( ! empty( $parent_order_status ) ) { if ( ! empty( $order_status ) ) { $query['where'] .= " AND ( parent.post_status IN ( 'wc-" . implode( "','wc-", $parent_order_status ) . "') OR parent.ID IS NULL ) "; } else { $query['where'] .= " AND parent.post_status IN ( 'wc-" . implode( "','wc-", $parent_order_status ) . "') "; } } // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date if ( $filter_range ) { $query['where'] .= " AND posts.post_date >= '" . date( 'Y-m-d H:i:s', $this->start_date ) . "' AND posts.post_date < '" . date( 'Y-m-d H:i:s', strtotime( '+1 DAY', $this->end_date ) ) . "' "; } // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date if ( ! empty( $where_meta ) ) { $relation = isset( $where_meta['relation'] ) ? $where_meta['relation'] : 'AND'; $query['where'] .= ' AND ('; foreach ( $where_meta as $index => $value ) { if ( ! is_array( $value ) ) { continue; } $key = sanitize_key( is_array( $value['meta_key'] ) ? $value['meta_key'][0] . '_array' : $value['meta_key'] ); if ( strtolower( $value['operator'] ) === 'in' || strtolower( $value['operator'] ) === 'not in' ) { if ( is_array( $value['meta_value'] ) ) { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value $value['meta_value'] = implode( "','", $value['meta_value'] ); } if ( ! empty( $value['meta_value'] ) ) { $where_value = "{$value['operator']} ('{$value['meta_value']}')"; } } else { $where_value = "{$value['operator']} '{$value['meta_value']}'"; } if ( ! empty( $where_value ) ) { if ( $index > 0 ) { $query['where'] .= ' ' . $relation; } if ( isset( $value['type'] ) && 'order_item_meta' === $value['type'] ) { if ( is_array( $value['meta_key'] ) ) { $query['where'] .= " ( order_item_meta_{$key}.meta_key IN ('" . implode( "','", $value['meta_key'] ) . "')"; } else { $query['where'] .= " ( order_item_meta_{$key}.meta_key = '{$value['meta_key']}'"; } $query['where'] .= " AND order_item_meta_{$key}.meta_value {$where_value} )"; } else { if ( is_array( $value['meta_key'] ) ) { $query['where'] .= " ( meta_{$key}.meta_key IN ('" . implode( "','", $value['meta_key'] ) . "')"; } else { $query['where'] .= " ( meta_{$key}.meta_key = '{$value['meta_key']}'"; } $query['where'] .= " AND meta_{$key}.meta_value {$where_value} )"; } } } $query['where'] .= ')'; } if ( ! empty( $where ) ) { foreach ( $where as $value ) { if ( strtolower( $value['operator'] ) === 'in' || strtolower( $value['operator'] ) === 'not in' ) { if ( is_array( $value['value'] ) ) { $value['value'] = implode( "','", $value['value'] ); } if ( ! empty( $value['value'] ) ) { $where_value = "{$value['operator']} ('{$value['value']}')"; } } else { $where_value = "{$value['operator']} '{$value['value']}'"; } if ( ! empty( $where_value ) ) { $query['where'] .= " AND {$value['key']} {$where_value}"; } } } if ( $group_by ) { $query['group_by'] = "GROUP BY {$group_by}"; } if ( $order_by ) { $query['order_by'] = "ORDER BY {$order_by}"; } if ( $limit ) { $query['limit'] = "LIMIT {$limit}"; } $query = apply_filters( 'woocommerce_reports_get_order_report_query', $query ); $query = implode( ' ', $query ); if ( $debug ) { echo '
';
			wc_print_r( $query );
			echo '
'; } if ( $debug || $nocache ) { self::enable_big_selects(); $result = apply_filters( 'woocommerce_reports_get_order_report_data', $wpdb->$query_type( $query ), $data ); } else { $query_hash = md5( $query_type . $query ); $result = $this->get_cached_query( $query_hash ); if ( null === $result ) { self::enable_big_selects(); $result = apply_filters( 'woocommerce_reports_get_order_report_data', $wpdb->$query_type( $query ), $data ); } $this->set_cached_query( $query_hash, $result ); } return $result; } /** * Init the static hooks of the class. */ protected static function add_update_transients_hook() { if ( ! has_action( 'shutdown', array( 'WC_Admin_Report', 'maybe_update_transients' ) ) ) { add_action( 'shutdown', array( 'WC_Admin_Report', 'maybe_update_transients' ) ); } } /** * Enables big mysql selects for reports, just once for this session. */ protected static function enable_big_selects() { static $big_selects = false; global $wpdb; if ( ! $big_selects ) { $wpdb->query( 'SET SESSION SQL_BIG_SELECTS=1' ); $big_selects = true; } } /** * Get the cached query result or null if it's not in the cache. * * @param string $query_hash The query hash. * * @return mixed */ protected function get_cached_query( $query_hash ) { $class = strtolower( get_class( $this ) ); if ( ! isset( self::$cached_results[ $class ] ) ) { self::$cached_results[ $class ] = get_transient( strtolower( get_class( $this ) ) ); } if ( isset( self::$cached_results[ $class ][ $query_hash ] ) ) { return self::$cached_results[ $class ][ $query_hash ]; } return null; } /** * Set the cached query result. * * @param string $query_hash The query hash. * @param mixed $data The data to cache. */ protected function set_cached_query( $query_hash, $data ) { $class = strtolower( get_class( $this ) ); if ( ! isset( self::$cached_results[ $class ] ) ) { self::$cached_results[ $class ] = get_transient( $class ); } if ( false === self::$cached_results[ $class ] ) { self::$cached_results[ $class ] = array(); } self::add_update_transients_hook(); self::$transients_to_update[ $class ] = $class; self::$cached_results[ $class ][ $query_hash ] = $data; } /** * Function to update the modified transients at the end of the request. */ public static function maybe_update_transients() { foreach ( self::$transients_to_update as $key => $transient_name ) { set_transient( $transient_name, self::$cached_results[ $transient_name ], DAY_IN_SECONDS ); } // Transients have been updated reset the list. self::$transients_to_update = array(); } /** * Put data with post_date's into an array of times. * * @param array $data array of your data. * @param string $date_key key for the 'date' field. e.g. 'post_date'. * @param string $data_key key for the data you are charting. * @param int $interval interval to use. * @param string $start_date start date. * @param string $group_by group by. * @return array */ public function prepare_chart_data( $data, $date_key, $data_key, $interval, $start_date, $group_by ) { // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date $prepared_data = array(); // Ensure all days (or months) have values in this range. if ( 'day' === $group_by ) { for ( $i = 0; $i <= $interval; $i ++ ) { $time = strtotime( date( 'Ymd', strtotime( "+{$i} DAY", $start_date ) ) ) . '000'; if ( ! isset( $prepared_data[ $time ] ) ) { $prepared_data[ $time ] = array( esc_js( $time ), 0 ); } } } else { $current_yearnum = date( 'Y', $start_date ); $current_monthnum = date( 'm', $start_date ); for ( $i = 0; $i <= $interval; $i ++ ) { $time = strtotime( $current_yearnum . str_pad( $current_monthnum, 2, '0', STR_PAD_LEFT ) . '01' ) . '000'; if ( ! isset( $prepared_data[ $time ] ) ) { $prepared_data[ $time ] = array( esc_js( $time ), 0 ); } $current_monthnum ++; if ( $current_monthnum > 12 ) { $current_monthnum = 1; $current_yearnum ++; } } } foreach ( $data as $d ) { switch ( $group_by ) { case 'day': $time = strtotime( date( 'Ymd', strtotime( $d->$date_key ) ) ) . '000'; break; case 'month': default: $time = strtotime( date( 'Ym', strtotime( $d->$date_key ) ) . '01' ) . '000'; break; } if ( ! isset( $prepared_data[ $time ] ) ) { continue; } if ( $data_key ) { $prepared_data[ $time ][1] += is_numeric( $d->$data_key ) ? $d->$data_key : 0; } else { $prepared_data[ $time ][1] ++; } } return $prepared_data; // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date } /** * Prepares a sparkline to show sales in the last X days. * * @param int $id ID of the product to show. Blank to get all orders. * @param int $days Days of stats to get. * @param string $type Type of sparkline to get. Ignored if ID is not set. * @return string */ public function sales_sparkline( $id = '', $days = 7, $type = 'sales' ) { // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested if ( $id ) { $meta_key = ( 'sales' === $type ) ? '_line_total' : '_qty'; $data = $this->get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), $meta_key => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'sparkline_value', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'post_date', 'value' => date( 'Y-m-d', strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ) ), 'operator' => '>', ), array( 'key' => 'order_item_meta__product_id.meta_value', 'value' => $id, 'operator' => '=', ), ), 'group_by' => 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)', 'query_type' => 'get_results', 'filter_range' => false, ) ); } else { $data = $this->get_order_report_data( array( 'data' => array( '_order_total' => array( 'type' => 'meta', 'function' => 'SUM', 'name' => 'sparkline_value', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'where' => array( array( 'key' => 'post_date', 'value' => date( 'Y-m-d', strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ) ), 'operator' => '>', ), ), 'group_by' => 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)', 'query_type' => 'get_results', 'filter_range' => false, ) ); } $total = 0; foreach ( $data as $d ) { $total += $d->sparkline_value; } if ( 'sales' === $type ) { /* translators: 1: total income 2: days */ $tooltip = sprintf( __( 'Sold %1$s worth in the last %2$d days', 'woocommerce' ), wp_strip_all_tags( wc_price( $total ) ), $days ); } else { /* translators: 1: total items sold 2: days */ $tooltip = sprintf( _n( 'Sold %1$d item in the last %2$d days', 'Sold %1$d items in the last %2$d days', $total, 'woocommerce' ), $total, $days ); } $sparkline_data = array_values( $this->prepare_chart_data( $data, 'post_date', 'sparkline_value', $days - 1, strtotime( 'midnight -' . ( $days - 1 ) . ' days', current_time( 'timestamp' ) ), 'day' ) ); return ''; // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested } /** * Get the current range and calculate the start and end dates. * * @param string $current_range Type of range. */ public function calculate_current_range( $current_range ) { // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested // phpcs:disable WordPress.Security.NonceVerification.Recommended switch ( $current_range ) { case 'custom': // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated $this->start_date = max( strtotime( '-20 years' ), strtotime( sanitize_text_field( wp_unslash( $_GET['start_date'] ) ) ) ); if ( empty( $_GET['end_date'] ) ) { $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); } else { $this->end_date = strtotime( 'midnight', strtotime( sanitize_text_field( wp_unslash( $_GET['end_date'] ) ) ) ); } $interval = 0; $min_date = $this->start_date; // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition while ( ( $min_date = strtotime( '+1 MONTH', $min_date ) ) <= $this->end_date ) { $interval ++; } // 3 months max for day view if ( $interval > 3 ) { $this->chart_groupby = 'month'; } else { $this->chart_groupby = 'day'; } break; case 'year': $this->start_date = strtotime( date( 'Y-01-01', current_time( 'timestamp' ) ) ); $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); $this->chart_groupby = 'month'; break; case 'last_month': $first_day_current_month = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) ); $this->start_date = strtotime( date( 'Y-m-01', strtotime( '-1 DAY', $first_day_current_month ) ) ); $this->end_date = strtotime( date( 'Y-m-t', strtotime( '-1 DAY', $first_day_current_month ) ) ); $this->chart_groupby = 'day'; break; case 'month': $this->start_date = strtotime( date( 'Y-m-01', current_time( 'timestamp' ) ) ); $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); $this->chart_groupby = 'day'; break; case '7day': $this->start_date = strtotime( '-6 days', strtotime( 'midnight', current_time( 'timestamp' ) ) ); $this->end_date = strtotime( 'midnight', current_time( 'timestamp' ) ); $this->chart_groupby = 'day'; break; } // Group by. switch ( $this->chart_groupby ) { case 'day': $this->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)'; $this->chart_interval = absint( ceil( max( 0, ( $this->end_date - $this->start_date ) / ( 60 * 60 * 24 ) ) ) ); $this->barwidth = 60 * 60 * 24 * 1000; break; case 'month': $this->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date)'; $this->chart_interval = 0; $min_date = strtotime( date( 'Y-m-01', $this->start_date ) ); // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition while ( ( $min_date = strtotime( '+1 MONTH', $min_date ) ) <= $this->end_date ) { $this->chart_interval ++; } $this->barwidth = 60 * 60 * 24 * 7 * 4 * 1000; break; } // phpcs:enable WordPress.Security.NonceVerification.Recommended // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date, WordPress.DateTime.CurrentTimeTimestamp.Requested } /** * Return currency tooltip JS based on WooCommerce currency position settings. * * @return string */ public function get_currency_tooltip() { switch ( get_option( 'woocommerce_currency_pos' ) ) { case 'right': $currency_tooltip = 'append_tooltip: "' . get_woocommerce_currency_symbol() . '"'; break; case 'right_space': $currency_tooltip = 'append_tooltip: " ' . get_woocommerce_currency_symbol() . '"'; break; case 'left': $currency_tooltip = 'prepend_tooltip: "' . get_woocommerce_currency_symbol() . '"'; break; case 'left_space': default: $currency_tooltip = 'prepend_tooltip: "' . get_woocommerce_currency_symbol() . ' "'; break; } return $currency_tooltip; } /** * Get the main chart. */ public function get_main_chart() {} /** * Get the legend for the main chart sidebar. * * @return array */ public function get_chart_legend() { return array(); } /** * Get chart widgets. * * @return array */ public function get_chart_widgets() { return array(); } /** * Get an export link if needed. */ public function get_export_button() {} /** * Output the report. */ public function output_report() {} /** * Check nonce for current range. * * @since 3.0.4 * @param string $current_range Current range. */ public function check_current_range_nonce( $current_range ) { if ( 'custom' !== $current_range ) { return; } if ( ! isset( $_GET['wc_reports_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['wc_reports_nonce'] ), 'custom_range' ) ) { // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated wp_die( /* translators: %1$s: open link, %2$s: close link */ sprintf( esc_html__( 'This report link has expired. %1$sClick here to view the filtered report%2$s.', 'woocommerce' ), '', '' ), esc_attr__( 'Confirm navigation', 'woocommerce' ) ); // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotValidated exit; } } } PKK[ò,ŠZûû.admin/reports/class-wc-report-most-stocked.phpnu„[µü¤max_items = 0; $this->items = array(); $stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 0 ) ); $query_from = apply_filters( 'woocommerce_report_most_stocked_query_from', $wpdb->prepare( " FROM {$wpdb->posts} as posts INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id WHERE 1=1 AND posts.post_type IN ( 'product', 'product_variation' ) AND posts.post_status = 'publish' AND lookup.stock_quantity > %d ", $stock ) ); $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT SQL_CALC_FOUND_ROWS posts.ID as id, posts.post_parent as parent {$query_from} ORDER BY lookup.stock_quantity DESC, id ASC LIMIT %d, %d;", ( $current_page - 1 ) * $per_page, $per_page ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $this->max_items = $wpdb->get_var( 'SELECT FOUND_ROWS();' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } } PKK[O*â553admin/reports/class-wc-report-sales-by-category.phpnu„[µü¤show_categories = is_array( $_GET['show_categories'] ) ? array_map( 'absint', $_GET['show_categories'] ) : array( absint( $_GET['show_categories'] ) ); } } /** * Get all product ids in a category (and its children). * * @param int $category_id Category ID. * @return array */ public function get_products_in_category( $category_id ) { $term_ids = get_term_children( $category_id, 'product_cat' ); $term_ids[] = $category_id; $product_ids = get_objects_in_term( $term_ids, 'product_cat' ); return array_unique( apply_filters( 'woocommerce_report_sales_by_category_get_products_in_category', $product_ids, $category_id ) ); } /** * Get the legend for the main chart sidebar. * * @return array */ public function get_chart_legend() { if ( empty( $this->show_categories ) ) { return array(); } $legend = array(); $index = 0; foreach ( $this->show_categories as $category ) { $category = get_term( $category, 'product_cat' ); $total = 0; $product_ids = $this->get_products_in_category( $category->term_id ); foreach ( $product_ids as $id ) { if ( isset( $this->item_sales[ $id ] ) ) { $total += $this->item_sales[ $id ]; } } $legend[] = array( /* translators: 1: total items sold 2: category name */ 'title' => sprintf( __( '%1$s sales in %2$s', 'woocommerce' ), '' . wc_price( $total ) . '', $category->name ), 'color' => isset( $this->chart_colours[ $index ] ) ? $this->chart_colours[ $index ] : $this->chart_colours[0], 'highlight_series' => $index, ); $index++; } return $legend; } /** * Output the report. */ public function output_report() { $ranges = array( 'year' => __( 'Year', 'woocommerce' ), 'last_month' => __( 'Last month', 'woocommerce' ), 'month' => __( 'This month', 'woocommerce' ), '7day' => __( 'Last 7 days', 'woocommerce' ), ); $this->chart_colours = array( '#3498db', '#34495e', '#1abc9c', '#2ecc71', '#f1c40f', '#e67e22', '#e74c3c', '#2980b9', '#8e44ad', '#2c3e50', '#16a085', '#27ae60', '#f39c12', '#d35400', '#c0392b' ); $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : '7day'; if ( ! in_array( $current_range, array( 'custom', 'year', 'last_month', 'month', '7day' ) ) ) { $current_range = '7day'; } $this->check_current_range_nonce( $current_range ); $this->calculate_current_range( $current_range ); // Get item sales data. if ( ! empty( $this->show_categories ) ) { $order_items = $this->get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id', ), '_line_total' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_amount', ), 'post_date' => array( 'type' => 'post_data', 'function' => '', 'name' => 'post_date', ), ), 'group_by' => 'ID, product_id, post_date', 'query_type' => 'get_results', 'filter_range' => true, ) ); $this->item_sales = array(); $this->item_sales_and_times = array(); if ( is_array( $order_items ) ) { foreach ( $order_items as $order_item ) { switch ( $this->chart_groupby ) { case 'day': $time = strtotime( gmdate( 'Ymd', strtotime( $order_item->post_date ) ) ) * 1000; break; case 'month': default: $time = strtotime( gmdate( 'Ym', strtotime( $order_item->post_date ) ) . '01' ) * 1000; break; } $this->item_sales_and_times[ $time ][ $order_item->product_id ] = isset( $this->item_sales_and_times[ $time ][ $order_item->product_id ] ) ? $this->item_sales_and_times[ $time ][ $order_item->product_id ] + $order_item->order_item_amount : $order_item->order_item_amount; $this->item_sales[ $order_item->product_id ] = isset( $this->item_sales[ $order_item->product_id ] ) ? $this->item_sales[ $order_item->product_id ] + $order_item->order_item_amount : $order_item->order_item_amount; } } } include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; } /** * Get chart widgets. * * @return array */ public function get_chart_widgets() { return array( array( 'title' => __( 'Categories', 'woocommerce' ), 'callback' => array( $this, 'category_widget' ), ), ); } /** * Output category widget. */ public function category_widget() { $categories = get_terms( 'product_cat', array( 'orderby' => 'name' ) ); ?>
show_categories ) ) { ?>

show_categories as $category ) { $category = get_term( $category, 'product_cat' ); $product_ids = $this->get_products_in_category( $category->term_id ); $category_chart_data = array(); for ( $i = 0; $i <= $this->chart_interval; $i ++ ) { $interval_total = 0; switch ( $this->chart_groupby ) { case 'day': $time = strtotime( gmdate( 'Ymd', strtotime( "+{$i} DAY", $this->start_date ) ) ) * 1000; break; case 'month': default: $time = strtotime( gmdate( 'Ym', strtotime( "+{$i} MONTH", $this->start_date ) ) . '01' ) * 1000; break; } foreach ( $product_ids as $id ) { if ( isset( $this->item_sales_and_times[ $time ][ $id ] ) ) { $interval_total += $this->item_sales_and_times[ $time ][ $id ]; } } $category_chart_data[] = array( $time, (float) wc_format_decimal( $interval_total, wc_get_price_decimals() ) ); } $chart_data[ $category->term_id ]['category'] = $category->name; $chart_data[ $category->term_id ]['data'] = $category_chart_data; $index++; } ?>
max_items = 0; $this->items = array(); $stock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) ); $query_from = apply_filters( 'woocommerce_report_out_of_stock_query_from', $wpdb->prepare( " FROM {$wpdb->posts} as posts INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id WHERE 1=1 AND posts.post_type IN ( 'product', 'product_variation' ) AND posts.post_status = 'publish' AND lookup.stock_quantity <= %d ", $stock ) ); $this->items = $wpdb->get_results( $wpdb->prepare( "SELECT SQL_CALC_FOUND_ROWS posts.ID as id, posts.post_parent as parent {$query_from} ORDER BY posts.post_title DESC LIMIT %d, %d;", ( $current_page - 1 ) * $per_page, $per_page ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $this->max_items = $wpdb->get_var( 'SELECT FOUND_ROWS();' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } } PKK[wèòÐ)Ð)+admin/reports/class-wc-report-downloads.phpnu„[µü¤ 'download', 'plural' => 'downloads', 'ajax' => false, ) ); } /** * Don't need this. * * @param string $position Top or bottom. */ public function display_tablenav( $position ) { if ( 'top' !== $position ) { parent::display_tablenav( $position ); } } /** * Output the report. */ public function output_report() { $this->prepare_items(); // Subtitle for permission if set. if ( ! empty( $_GET['permission_id'] ) ) { // WPCS: input var ok. $permission_id = absint( $_GET['permission_id'] ); // WPCS: input var ok. // Load the permission, order, etc. so we can render more information. $permission = null; $product = null; try { $permission = new WC_Customer_Download( $permission_id ); $product = wc_get_product( $permission->product_id ); } catch ( Exception $e ) { wp_die( sprintf( esc_html__( 'Permission #%d not found.', 'woocommerce' ), esc_html( $permission_id ) ) ); } } echo '

' . esc_html__( 'Customer downloads', 'woocommerce' ); $filters = $this->get_filter_vars(); $filter_list = array(); $filter_names = array( 'product_id' => __( 'Product', 'woocommerce' ), 'download_id' => __( 'File ID', 'woocommerce' ), 'permission_id' => __( 'Permission ID', 'woocommerce' ), 'order_id' => __( 'Order', 'woocommerce' ), 'user_id' => __( 'User', 'woocommerce' ), 'user_ip_address' => __( 'IP address', 'woocommerce' ), ); foreach ( $filters as $key => $value ) { if ( is_null( $value ) ) { continue; } switch ( $key ) { case 'order_id': $order = wc_get_order( $value ); if ( $order ) { $display_value = _x( '#', 'hash before order number', 'woocommerce' ) . $order->get_order_number(); } else { break 2; } break; case 'product_id': $product = wc_get_product( $value ); if ( $product ) { $display_value = $product->get_formatted_name(); } else { break 2; } break; default: $display_value = $value; break; } $filter_list[] = $filter_names[ $key ] . ' ' . $display_value . ' ×'; } echo '

'; echo '

'; echo esc_html__( 'Active filters', 'woocommerce' ) . ': '; echo $filter_list ? wp_kses_post( implode( ', ', $filter_list ) ) : ''; echo '

'; echo '
'; $this->display(); echo '
'; } /** * Get column value. * * @param mixed $item Item being displayed. * @param string $column_name Column name. */ public function column_default( $item, $column_name ) { $permission = null; $product = null; try { $permission = new WC_Customer_Download( $item->permission_id ); $product = wc_get_product( $permission->product_id ); } catch ( Exception $e ) { // Ok to continue rendering other information even if permission and/or product is not found. return; } switch ( $column_name ) { case 'timestamp': echo esc_html( $item->timestamp ); break; case 'product': if ( ! empty( $product ) ) { edit_post_link( esc_html( $product->get_formatted_name() ), '', '', $product->get_id(), 'view-link' ); echo '
'; echo '' . esc_html__( 'Filter by product', 'woocommerce' ) . ''; echo '
'; } break; case 'file': if ( ! empty( $permission ) && ! empty( $product ) ) { // File information. $file = $product->get_file( $permission->get_download_id() ); if ( false === $file ) { echo esc_html__( 'File does not exist', 'woocommerce' ); } else { echo esc_html( $file->get_name() . ' - ' . basename( $file->get_file() ) ); echo '
'; echo '' . esc_html__( 'Filter by file', 'woocommerce' ) . ''; echo '
'; } } break; case 'order': if ( ! empty( $permission ) && ( $order = wc_get_order( $permission->order_id ) ) ) { edit_post_link( esc_html( _x( '#', 'hash before order number', 'woocommerce' ) . $order->get_order_number() ), '', '', $permission->order_id, 'view-link' ); echo '
'; echo '' . esc_html__( 'Filter by order', 'woocommerce' ) . ''; echo '
'; } break; case 'user': if ( $item->user_id > 0 ) { $user = get_user_by( 'id', $item->user_id ); if ( ! empty( $user ) ) { echo '' . esc_html( $user->display_name ) . ''; echo '
'; echo '' . esc_html__( 'Filter by user', 'woocommerce' ) . ''; echo '
'; } } else { esc_html_e( 'Guest', 'woocommerce' ); } break; case 'user_ip_address': echo esc_html( $item->user_ip_address ); echo '
'; echo '' . esc_html__( 'Filter by IP address', 'woocommerce' ) . ''; echo '
'; break; } } /** * Get columns. * * @return array */ public function get_columns() { $columns = array( 'timestamp' => __( 'Timestamp', 'woocommerce' ), 'product' => __( 'Product', 'woocommerce' ), 'file' => __( 'File', 'woocommerce' ), 'order' => __( 'Order', 'woocommerce' ), 'user' => __( 'User', 'woocommerce' ), 'user_ip_address' => __( 'IP address', 'woocommerce' ), ); return $columns; } /** * Prepare download list items. */ public function prepare_items() { $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() ); $current_page = absint( $this->get_pagenum() ); // Allow filtering per_page value, but ensure it's at least 1. $per_page = max( 1, apply_filters( 'woocommerce_admin_downloads_report_downloads_per_page', 20 ) ); $this->get_items( $current_page, $per_page ); /** * Pagination. */ $this->set_pagination_args( array( 'total_items' => $this->max_items, 'per_page' => $per_page, 'total_pages' => ceil( $this->max_items / $per_page ), ) ); } /** * No items found text. */ public function no_items() { esc_html_e( 'No customer downloads found.', 'woocommerce' ); } /** * Get filters from querystring. * * @return object */ protected function get_filter_vars() { $product_id = ! empty( $_GET['product_id'] ) ? absint( wp_unslash( $_GET['product_id'] ) ) : null; // WPCS: input var ok. $download_id = ! empty( $_GET['download_id'] ) ? wc_clean( wp_unslash( $_GET['download_id'] ) ) : null; // WPCS: input var ok. $permission_id = ! empty( $_GET['permission_id'] ) ? absint( wp_unslash( $_GET['permission_id'] ) ) : null; // WPCS: input var ok. $order_id = ! empty( $_GET['order_id'] ) ? absint( wp_unslash( $_GET['order_id'] ) ) : null; // WPCS: input var ok. $user_id = ! empty( $_GET['user_id'] ) ? absint( wp_unslash( $_GET['user_id'] ) ) : null; // WPCS: input var ok. $user_ip_address = ! empty( $_GET['user_ip_address'] ) ? wc_clean( wp_unslash( $_GET['user_ip_address'] ) ) : null; // WPCS: input var ok. return (object) array( 'product_id' => $product_id, 'download_id' => $download_id, 'permission_id' => $permission_id, 'order_id' => $order_id, 'user_id' => $user_id, 'user_ip_address' => $user_ip_address, ); } /** * Get downloads matching criteria. * * @param int $current_page Current viewed page. * @param int $per_page How many results to show per page. */ public function get_items( $current_page, $per_page ) { global $wpdb; $this->max_items = 0; $this->items = array(); $filters = $this->get_filter_vars(); // Get downloads from database. $table = $wpdb->prefix . WC_Customer_Download_Log_Data_Store::get_table_name(); $query_from = " FROM {$table} as downloads "; if ( ! is_null( $filters->product_id ) || ! is_null( $filters->download_id ) || ! is_null( $filters->order_id ) ) { $query_from .= " LEFT JOIN {$wpdb->prefix}woocommerce_downloadable_product_permissions as permissions on downloads.permission_id = permissions.permission_id "; } $query_from .= ' WHERE 1=1 '; if ( ! is_null( $filters->product_id ) ) { $query_from .= $wpdb->prepare( ' AND product_id = %d ', $filters->product_id ); } if ( ! is_null( $filters->download_id ) ) { $query_from .= $wpdb->prepare( ' AND download_id = %s ', $filters->download_id ); } if ( ! is_null( $filters->order_id ) ) { $query_from .= $wpdb->prepare( ' AND order_id = %d ', $filters->order_id ); } if ( ! is_null( $filters->permission_id ) ) { $query_from .= $wpdb->prepare( ' AND downloads.permission_id = %d ', $filters->permission_id ); } if ( ! is_null( $filters->user_id ) ) { $query_from .= $wpdb->prepare( ' AND downloads.user_id = %d ', $filters->user_id ); } if ( ! is_null( $filters->user_ip_address ) ) { $query_from .= $wpdb->prepare( ' AND user_ip_address = %s ', $filters->user_ip_address ); } $query_from = apply_filters( 'woocommerce_report_downloads_query_from', $query_from ); $query_order = $wpdb->prepare( 'ORDER BY timestamp DESC LIMIT %d, %d;', ( $current_page - 1 ) * $per_page, $per_page ); $this->items = $wpdb->get_results( "SELECT * {$query_from} {$query_order}" ); // WPCS: cache ok, db call ok, unprepared SQL ok. $this->max_items = $wpdb->get_var( "SELECT COUNT( DISTINCT download_log_id ) {$query_from};" ); // WPCS: cache ok, db call ok, unprepared SQL ok. } } PKK[òÔ²(€€(admin/helper/class-wc-helper-options.phpnu„[µü¤slug ) ) { return $response; } // Only for slugs that start with woo- if ( 0 !== strpos( $args->slug, 'woocommerce-com-' ) ) { return $response; } $clean_slug = str_replace( 'woocommerce-com-', '', $args->slug ); // Look through update data by slug. $update_data = WC_Helper_Updater::get_update_data(); $products = wp_list_filter( $update_data, array( 'slug' => $clean_slug ) ); if ( empty( $products ) ) { return $response; } $product_id = array_keys( $products ); $product_id = array_shift( $product_id ); // Fetch the product information from the Helper API. $request = WC_Helper_API::get( add_query_arg( array( 'product_id' => absint( $product_id ), ), 'info' ), array( 'authenticated' => true ) ); $results = json_decode( wp_remote_retrieve_body( $request ), true ); if ( ! empty( $results ) ) { $response = (object) $results; $product = array_shift( $products ); if ( isset( $product['package'] ) ) { $response->download_link = $product['package']; } } return $response; } } WC_Helper_Plugin_Info::load(); PKK[1Í¥!!2admin/helper/class-wc-helper-subscriptions-api.phpnu„[µü¤ 'POST', 'callback' => array( __CLASS__, 'refresh' ), 'permission_callback' => array( __CLASS__, 'get_permission' ), ) ); register_rest_route( 'wc/v3', '/marketplace/subscriptions', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_subscriptions' ), 'permission_callback' => array( __CLASS__, 'get_permission' ), ) ); register_rest_route( 'wc/v3', '/marketplace/subscriptions/connect', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'connect' ), 'permission_callback' => array( __CLASS__, 'get_permission' ), 'args' => array( 'product_key' => array( 'required' => true, 'type' => 'string', ), ), ) ); register_rest_route( 'wc/v3', '/marketplace/subscriptions/disconnect', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'disconnect' ), 'permission_callback' => array( __CLASS__, 'get_permission' ), 'args' => array( 'product_key' => array( 'required' => true, 'type' => 'string', ), ), ) ); register_rest_route( 'wc/v3', '/marketplace/subscriptions/activate', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'activate' ), 'permission_callback' => array( __CLASS__, 'get_permission' ), 'args' => array( 'product_key' => array( 'required' => true, 'type' => 'string', ), ), ) ); } /** * The Extensions page can only be accessed by users with the manage_woocommerce * capability. So the API mimics that behavior. */ public static function get_permission() { return current_user_can( 'manage_woocommerce' ); } /** * Fetch subscriptions from WooCommerce.com and serve them * as JSON. */ public static function get_subscriptions() { $subscriptions = WC_Helper::get_subscription_list_data(); wp_send_json( array_values( $subscriptions ) ); } /** * Refresh account and subscriptions from WooCommerce.com and serve subscriptions * as JSON. */ public static function refresh() { WC_Helper::refresh_helper_subscriptions(); self::get_subscriptions(); } /** * Connect a WooCommerce.com subscription. * * @param WP_REST_Request $request Request object. */ public static function connect( $request ) { $product_key = $request->get_param( 'product_key' ); try { $success = WC_Helper::activate_helper_subscription( $product_key ); } catch ( Exception $e ) { wp_send_json_error( array( 'message' => $e->getMessage(), ), 400 ); } if ( $success ) { wp_send_json_success( array( 'message' => __( 'Your subscription has been connected.', 'woocommerce' ), ) ); } else { wp_send_json_error( array( 'message' => __( 'There was an error connecting your subscription. Please try again.', 'woocommerce' ), ), 400 ); } } /** * Disconnect a WooCommerce.com subscription. * * @param WP_REST_Request $request Request object. */ public static function disconnect( $request ) { $product_key = $request->get_param( 'product_key' ); try { $success = WC_Helper::deactivate_helper_subscription( $product_key ); } catch ( Exception $e ) { wp_send_json_error( array( 'message' => $e->getMessage(), ), 400 ); } if ( $success ) { wp_send_json_success( array( 'message' => __( 'Your subscription has been disconnected.', 'woocommerce' ), ) ); } else { wp_send_json_error( array( 'message' => __( 'There was an error disconnecting your subscription. Please try again.', 'woocommerce' ), ), 400 ); } } /** * Activate a WooCommerce.com product. * This activates the plugin/theme on the site. * * @param WP_REST_Request $request Request object. */ public static function activate( $request ) { $product_key = $request->get_param( 'product_key' ); $subscription = WC_Helper::get_subscription( $product_key ); if ( ! $subscription ) { wp_send_json_error( array( 'message' => __( 'We couldn\'t find a subscription for this product.', 'woocommerce' ), ), 400 ); } if ( true !== $subscription['local']['installed'] || ! isset( $subscription['local']['active'] ) ) { wp_send_json_error( array( 'message' => __( 'This product is not installed.', 'woocommerce' ), ), 400 ); } if ( true === $subscription['local']['active'] ) { wp_send_json_success( array( 'message' => __( 'This product is already active.', 'woocommerce' ), ), ); } if ( 'plugin' === $subscription['product_type'] ) { $success = activate_plugin( $subscription['local']['path'] ); if ( is_wp_error( $success ) ) { wp_send_json_error( array( 'message' => __( 'There was an error activating this plugin.', 'woocommerce' ), ), 400 ); } } elseif ( 'theme' === $subscription['product_type'] ) { switch_theme( $subscription['local']['slug'] ); $theme = wp_get_theme(); if ( $subscription['local']['slug'] !== $theme->get_stylesheet() ) { wp_send_json_error( array( 'message' => __( 'There was an error activating this theme.', 'woocommerce' ), ), 400 ); } } wp_send_json_success( array( 'message' => __( 'This product has been activated.', 'woocommerce' ), ), ); } } WC_Helper_Subscriptions_API::load(); PKK[÷{|¿¿'admin/helper/class-wc-helper-compat.phpnu„[µü¤admin, 'maybe_display_activation_notice' ) ); remove_action( 'admin_notices', array( $GLOBALS['woothemes_updater']->admin, 'maybe_display_activation_notice' ) ); remove_action( 'network_admin_menu', array( $GLOBALS['woothemes_updater']->admin, 'register_settings_screen' ) ); remove_action( 'admin_menu', array( $GLOBALS['woothemes_updater']->admin, 'register_settings_screen' ) ); } /** * Attempt to migrate a legacy connection to a new one. */ public static function migrate_connection() { // Don't attempt to migrate if attempted before. if ( WC_Helper_Options::get( 'did-migrate' ) ) { return; } $auth = WC_Helper_Options::get( 'auth' ); if ( ! empty( $auth ) ) { return; } WC_Helper::log( 'Attempting oauth/migrate' ); WC_Helper_Options::update( 'did-migrate', true ); $master_key = get_option( 'woothemes_helper_master_key' ); if ( empty( $master_key ) ) { WC_Helper::log( 'Master key not found, aborting' ); return; } $request = WC_Helper_API::post( 'oauth/migrate', array( 'body' => array( 'home_url' => home_url(), 'master_key' => $master_key, ), ) ); if ( is_wp_error( $request ) || wp_remote_retrieve_response_code( $request ) !== 200 ) { WC_Helper::log( 'Call to oauth/migrate returned a non-200 response code' ); return; } $request_token = json_decode( wp_remote_retrieve_body( $request ) ); if ( empty( $request_token ) ) { WC_Helper::log( 'Call to oauth/migrate returned an empty token' ); return; } // Obtain an access token. $request = WC_Helper_API::post( 'oauth/access_token', array( 'body' => array( 'request_token' => $request_token, 'home_url' => home_url(), 'migrate' => true, ), ) ); if ( is_wp_error( $request ) || wp_remote_retrieve_response_code( $request ) !== 200 ) { WC_Helper::log( 'Call to oauth/access_token returned a non-200 response code' ); return; } $access_token = json_decode( wp_remote_retrieve_body( $request ), true ); if ( empty( $access_token ) ) { WC_Helper::log( 'Call to oauth/access_token returned an invalid token' ); return; } WC_Helper_Options::update( 'auth', array( 'access_token' => $access_token['access_token'], 'access_token_secret' => $access_token['access_token_secret'], 'site_id' => $access_token['site_id'], 'user_id' => null, // Set this later 'updated' => time(), ) ); // Obtain the connected user info. if ( ! WC_Helper::_flush_authentication_cache() ) { WC_Helper::log( 'Could not obtain connected user info in migrate_connection' ); WC_Helper_Options::update( 'auth', array() ); return; } } /** * Attempt to deactivate the legacy helper plugin. */ public static function deactivate_plugin() { include_once ABSPATH . 'wp-admin/includes/plugin.php'; if ( ! function_exists( 'deactivate_plugins' ) ) { return; } if ( is_plugin_active( 'woothemes-updater/woothemes-updater.php' ) ) { deactivate_plugins( 'woothemes-updater/woothemes-updater.php' ); // Notify the user when the plugin is deactivated. add_action( 'pre_current_active_plugins', array( __CLASS__, 'plugin_deactivation_notice' ) ); } } /** * Display admin notice directing the user where to go. */ public static function plugin_deactivation_notice() { ?>

Manage subscriptions from the extensions tab instead.', 'woocommerce' ), esc_url( admin_url( 'admin.php?page=wc-addons§ion=helper' ) ) ); ?>

'wc-addons', 'section' => 'helper', ), admin_url( 'admin.php' ) ); include WC_Helper::get_view_filename( 'html-helper-compat.php' ); } } WC_Helper_Compat::load(); PKK[2H˜~µµ$admin/helper/class-wc-helper-api.phpnu„[µü¤version . '; ' . get_bloginfo( 'url' ); } /** * Allow developers to filter the request args passed to wp_safe_remote_request(). * Useful to remove sslverify when working on a local api dev environment. */ $args = apply_filters( 'woocommerce_helper_api_request_args', $args, $endpoint ); // TODO: Check response signatures on certain endpoints. return wp_safe_remote_request( $url, $args ); } /** * Adds authentication headers to an HTTP request. * * @param string $url The request URI. * @param array $args By-ref, the args that will be passed to wp_remote_request(). * @return bool Were the headers added? */ private static function _authenticate( &$url, &$args ) { $auth = WC_Helper_Options::get( 'auth' ); if ( empty( $auth['access_token'] ) || empty( $auth['access_token_secret'] ) ) { return false; } $request_uri = parse_url( $url, PHP_URL_PATH ); $query_string = parse_url( $url, PHP_URL_QUERY ); if ( is_string( $query_string ) ) { $request_uri .= '?' . $query_string; } $data = array( 'host' => parse_url( $url, PHP_URL_HOST ), 'request_uri' => $request_uri, 'method' => ! empty( $args['method'] ) ? $args['method'] : 'GET', ); if ( ! empty( $args['body'] ) ) { $data['body'] = $args['body']; } $signature = hash_hmac( 'sha256', json_encode( $data ), $auth['access_token_secret'] ); if ( empty( $args['headers'] ) ) { $args['headers'] = array(); } $headers = array( 'Authorization' => 'Bearer ' . $auth['access_token'], 'X-Woo-Signature' => $signature, ); $args['headers'] = wp_parse_args( $headers, $args['headers'] ); $url = add_query_arg( array( 'token' => $auth['access_token'], 'signature' => $signature, ), $url ); return true; } /** * Wrapper for self::request(). * * @param string $endpoint The helper API endpoint to request. * @param array $args Arguments passed to wp_remote_request(). * * @return array The response object from wp_safe_remote_request(). */ public static function get( $endpoint, $args = array() ) { $args['method'] = 'GET'; return self::request( $endpoint, $args ); } /** * Wrapper for self::request(). * * @param string $endpoint The helper API endpoint to request. * @param array $args Arguments passed to wp_remote_request(). * * @return array The response object from wp_safe_remote_request(). */ public static function post( $endpoint, $args = array() ) { $args['method'] = 'POST'; return self::request( $endpoint, $args ); } /** * Wrapper for self::request(). * * @param string $endpoint The helper API endpoint to request. * @param array $args Arguments passed to wp_remote_request(). * * @return array The response object from wp_safe_remote_request(). */ public static function put( $endpoint, $args = array() ) { $args['method'] = 'PUT'; return self::request( $endpoint, $args ); } /** * Using the API base, form a request URL from a given endpoint. * * @param string $endpoint The endpoint to request. * @param string $query_string Optional query string to append to the URL. * * @return string The absolute endpoint URL. */ public static function url( $endpoint, $query_string = '' ) { $endpoint = ltrim( $endpoint, '/' ); $endpoint = sprintf( '%s/%s/%s', self::$api_base, $endpoint, $query_string ); $endpoint = esc_url_raw( $endpoint ); $endpoint = rtrim( $endpoint, '/' ); return $endpoint; } } WC_Helper_API::load(); PKK[1î+;I>I>(admin/helper/class-wc-helper-updater.phpnu„[µü¤ 'woocommerce-com-' . $plugin['_product_id'], 'slug' => 'woocommerce-com-' . $data['slug'], 'plugin' => $filename, 'new_version' => $data['version'], 'url' => $data['url'], 'package' => $data['package'], 'upgrade_notice' => $data['upgrade_notice'], ); if ( isset( $data['requires_php'] ) ) { $item['requires_php'] = $data['requires_php']; } // We don't want to deliver a valid upgrade package when their subscription has expired. // To avoid the generic "no_package" error that empty strings give, we will store an // indication of expiration for the `upgrader_pre_download` filter to error on. if ( ! self::_has_active_subscription( $plugin['_product_id'] ) ) { $item['package'] = 'woocommerce-com-expired-' . $plugin['_product_id']; } if ( $transient instanceof stdClass ) { if ( version_compare( $plugin['Version'], $data['version'], '<' ) ) { $transient->response[ $filename ] = (object) $item; unset( $transient->no_update[ $filename ] ); } else { $transient->no_update[ $filename ] = (object) $item; unset( $transient->response[ $filename ] ); } } } if ( $transient instanceof stdClass ) { $translations = self::get_translations_update_data(); $transient->translations = array_merge( isset( $transient->translations ) ? $transient->translations : array(), $translations ); } return $transient; } /** * Runs on pre_set_site_transient_update_themes, provides custom * packages for Woo.com-hosted extensions. * * @param object $transient The update_themes transient object. * * @return object The same or a modified version of the transient. */ public static function transient_update_themes( $transient ) { $update_data = self::get_update_data(); foreach ( WC_Helper::get_local_woo_themes() as $theme ) { if ( empty( $update_data[ $theme['_product_id'] ] ) ) { continue; } $data = $update_data[ $theme['_product_id'] ]; $slug = $theme['_stylesheet']; $item = array( 'theme' => $slug, 'new_version' => $data['version'], 'url' => $data['url'], 'package' => '', ); if ( self::_has_active_subscription( $theme['_product_id'] ) ) { $item['package'] = $data['package']; } if ( version_compare( $theme['Version'], $data['version'], '<' ) ) { $transient->response[ $slug ] = $item; } else { unset( $transient->response[ $slug ] ); $transient->checked[ $slug ] = $data['version']; } } return $transient; } /** * Get update data for all plugins. * * @return array Update data {product_id => data} * @see get_update_data */ public static function get_available_extensions_downloads_data() { $payload = array(); // Scan subscriptions. foreach ( WC_Helper::get_subscriptions() as $subscription ) { $payload[ $subscription['product_id'] ] = array( 'product_id' => $subscription['product_id'], 'file_id' => '', ); } // Scan local plugins which may or may not have a subscription. foreach ( WC_Helper::get_local_woo_plugins() as $data ) { if ( ! isset( $payload[ $data['_product_id'] ] ) ) { $payload[ $data['_product_id'] ] = array( 'product_id' => $data['_product_id'], ); } $payload[ $data['_product_id'] ]['file_id'] = $data['_file_id']; } return self::_update_check( $payload ); } /** * Get update data for all extensions. * * Scans through all subscriptions for the connected user, as well * as all Woo extensions without a subscription, and obtains update * data for each product. * * @return array Update data {product_id => data} */ public static function get_update_data() { $payload = array(); // Scan subscriptions. foreach ( WC_Helper::get_subscriptions() as $subscription ) { $payload[ $subscription['product_id'] ] = array( 'product_id' => $subscription['product_id'], 'file_id' => '', ); } // Scan local plugins which may or may not have a subscription. foreach ( WC_Helper::get_local_woo_plugins() as $data ) { if ( ! isset( $payload[ $data['_product_id'] ] ) ) { $payload[ $data['_product_id'] ] = array( 'product_id' => $data['_product_id'], ); } $payload[ $data['_product_id'] ]['file_id'] = $data['_file_id']; } // Scan local themes. foreach ( WC_Helper::get_local_woo_themes() as $data ) { if ( ! isset( $payload[ $data['_product_id'] ] ) ) { $payload[ $data['_product_id'] ] = array( 'product_id' => $data['_product_id'], ); } $payload[ $data['_product_id'] ]['file_id'] = $data['_file_id']; } return self::_update_check( $payload ); } /** * Get translations updates information. * * Scans through all subscriptions for the connected user, as well * as all Woo extensions without a subscription, and obtains update * data for each product. * * @return array Update data {product_id => data} */ public static function get_translations_update_data() { $payload = array(); $installed_translations = wp_get_installed_translations( 'plugins' ); $locales = array_values( get_available_languages() ); /** * Filters the locales requested for plugin translations. * * @since 3.7.0 * @since 4.5.0 The default value of the `$locales` parameter changed to include all locales. * * @param array $locales Plugin locales. Default is all available locales of the site. */ $locales = apply_filters( 'plugins_update_check_locales', $locales ); $locales = array_unique( $locales ); // No locales, the response will be empty, we can return now. if ( empty( $locales ) ) { return array(); } // Scan local plugins which may or may not have a subscription. $plugins = WC_Helper::get_local_woo_plugins(); $active_woo_plugins = array_intersect( array_keys( $plugins ), get_option( 'active_plugins', array() ) ); /* * Use only plugins that are subscribed to the automatic translations updates. */ $active_for_translations = array_filter( $active_woo_plugins, function( $plugin ) use ( $plugins ) { return apply_filters( 'woocommerce_translations_updates_for_' . $plugins[ $plugin ]['slug'], false ); } ); // Nothing to check for, exit. if ( empty( $active_for_translations ) ) { return array(); } if ( wp_doing_cron() ) { $timeout = 30; } else { // Three seconds, plus one extra second for every 10 plugins. $timeout = 3 + (int) ( count( $active_for_translations ) / 10 ); } $request_body = array( 'locales' => $locales, 'plugins' => array(), ); foreach ( $active_for_translations as $active_plugin ) { $plugin = $plugins[ $active_plugin ]; $request_body['plugins'][ $plugin['slug'] ] = array( 'version' => $plugin['Version'] ); } $raw_response = wp_remote_post( 'https://translate.wordpress.com/api/translations-updates/woocommerce', array( 'body' => json_encode( $request_body ), 'headers' => array( 'Content-Type: application/json' ), 'timeout' => $timeout, ) ); // Something wrong happened on the translate server side. $response_code = wp_remote_retrieve_response_code( $raw_response ); if ( 200 !== $response_code ) { return array(); } $response = json_decode( wp_remote_retrieve_body( $raw_response ), true ); // API error, api returned but something was wrong. if ( array_key_exists( 'success', $response ) && false === $response['success'] ) { return array(); } $translations = array(); foreach ( $response['data'] as $plugin_name => $language_packs ) { foreach ( $language_packs as $language_pack ) { // Maybe we have this language pack already installed so lets check revision date. if ( array_key_exists( $plugin_name, $installed_translations ) && array_key_exists( $language_pack['wp_locale'], $installed_translations[ $plugin_name ] ) ) { $installed_translation_revision_time = new DateTime( $installed_translations[ $plugin_name ][ $language_pack['wp_locale'] ]['PO-Revision-Date'] ); $new_translation_revision_time = new DateTime( $language_pack['last_modified'] ); // Skip if translation language pack is not newer than what is installed already. if ( $new_translation_revision_time <= $installed_translation_revision_time ) { continue; } } $translations[] = array( 'type' => 'plugin', 'slug' => $plugin_name, 'language' => $language_pack['wp_locale'], 'version' => $language_pack['version'], 'updated' => $language_pack['last_modified'], 'package' => $language_pack['package'], 'autoupdate' => true, ); } } return $translations; } /** * Run an update check API call. * * The call is cached based on the payload (product ids, file ids). If * the payload changes, the cache is going to miss. * * @param array $payload Information about the plugin to update. * @return array Update data for each requested product. */ private static function _update_check( $payload ) { ksort( $payload ); $hash = md5( wp_json_encode( $payload ) ); $cache_key = '_woocommerce_helper_updates'; $data = get_transient( $cache_key ); if ( false !== $data ) { if ( hash_equals( $hash, $data['hash'] ) ) { return $data['products']; } } $data = array( 'hash' => $hash, 'updated' => time(), 'products' => array(), 'errors' => array(), ); $request = WC_Helper_API::post( 'update-check', array( 'body' => wp_json_encode( array( 'products' => $payload ) ), 'authenticated' => true, ) ); if ( wp_remote_retrieve_response_code( $request ) !== 200 ) { $data['errors'][] = 'http-error'; } else { $data['products'] = json_decode( wp_remote_retrieve_body( $request ), true ); } set_transient( $cache_key, $data, 12 * HOUR_IN_SECONDS ); return $data['products']; } /** * Check for an active subscription. * * Checks a given product id against all subscriptions on * the current site. Returns true if at least one active * subscription is found. * * @param int $product_id The product id to look for. * * @return bool True if active subscription found. */ private static function _has_active_subscription( $product_id ) { if ( ! isset( $auth ) ) { $auth = WC_Helper_Options::get( 'auth' ); } if ( ! isset( $subscriptions ) ) { $subscriptions = WC_Helper::get_subscriptions(); } if ( empty( $auth['site_id'] ) || empty( $subscriptions ) ) { return false; } // Check for an active subscription. foreach ( $subscriptions as $subscription ) { if ( $subscription['product_id'] != $product_id ) { continue; } if ( in_array( absint( $auth['site_id'] ), $subscription['connections'] ) ) { return true; } } return false; } /** * Get the number of products that have updates. * * @return int The number of products with updates. */ public static function get_updates_count() { $cache_key = '_woocommerce_helper_updates_count'; $count = get_transient( $cache_key ); if ( false !== $count ) { return $count; } // Don't fetch any new data since this function in high-frequency. if ( ! get_transient( '_woocommerce_helper_subscriptions' ) ) { return 0; } if ( ! get_transient( '_woocommerce_helper_updates' ) ) { return 0; } $count = 0; $update_data = self::get_update_data(); if ( empty( $update_data ) ) { set_transient( $cache_key, $count, 12 * HOUR_IN_SECONDS ); return $count; } // Scan local plugins. foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) { if ( empty( $update_data[ $plugin['_product_id'] ] ) ) { continue; } if ( version_compare( $plugin['Version'], $update_data[ $plugin['_product_id'] ]['version'], '<' ) ) { $count++; } } // Scan local themes. foreach ( WC_Helper::get_local_woo_themes() as $theme ) { if ( empty( $update_data[ $theme['_product_id'] ] ) ) { continue; } if ( version_compare( $theme['Version'], $update_data[ $theme['_product_id'] ]['version'], '<' ) ) { $count++; } } set_transient( $cache_key, $count, 12 * HOUR_IN_SECONDS ); return $count; } /** * Return the updates count markup. * * @return string Updates count markup, empty string if no updates avairable. */ public static function get_updates_count_html() { $count = self::get_updates_count(); if ( ! $count ) { return ''; } $count_html = sprintf( '%d', $count, number_format_i18n( $count ) ); return $count_html; } /** * Flushes cached update data. */ public static function flush_updates_cache() { delete_transient( '_woocommerce_helper_updates' ); delete_transient( '_woocommerce_helper_updates_count' ); delete_site_transient( 'update_plugins' ); delete_site_transient( 'update_themes' ); } /** * Fires when a user successfully updated a theme or a plugin. */ public static function upgrader_process_complete() { delete_transient( '_woocommerce_helper_updates_count' ); } /** * Hooked into the upgrader_pre_download filter in order to better handle error messaging around expired * plugin updates. Initially we were using an empty string, but the error message that no_package * results in does not fit the cause. * * @since 4.1.0 * @param bool $reply Holds the current filtered response. * @param string $package The path to the package file for the update. * @return false|WP_Error False to proceed with the update as normal, anything else to be returned instead of updating. */ public static function block_expired_updates( $reply, $package ) { // Don't override a reply that was set already. if ( false !== $reply ) { return $reply; } // Only for packages with expired subscriptions. if ( 0 !== strpos( $package, 'woocommerce-com-expired-' ) ) { return false; } return new WP_Error( 'woocommerce_subscription_expired', sprintf( // translators: %s: URL of Woo.com subscriptions tab. __( 'Please visit the subscriptions page and renew to continue receiving updates.', 'woocommerce' ), esc_url( admin_url( 'admin.php?page=wc-addons§ion=helper' ) ) ) ); } } WC_Helper_Updater::load(); PKK[A¯Ù Ù  admin/helper/class-wc-helper.phpnu„[µü¤ 'wc-addons', 'section' => 'helper', 'wc-helper-connect' => 1, 'wc-helper-nonce' => wp_create_nonce( 'connect' ), ), admin_url( 'admin.php' ) ); include self::get_view_filename( 'html-oauth-start.php' ); return; } $disconnect_url = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'wc-helper-disconnect' => 1, 'wc-helper-nonce' => wp_create_nonce( 'disconnect' ), ), admin_url( 'admin.php' ) ); $current_filter = self::get_current_filter(); $refresh_url = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => $current_filter, 'wc-helper-refresh' => 1, 'wc-helper-nonce' => wp_create_nonce( 'refresh' ), ), admin_url( 'admin.php' ) ); // Installed plugins and themes, with or without an active subscription. $woo_plugins = self::get_local_woo_plugins(); $woo_themes = self::get_local_woo_themes(); $subscriptions_list_data = self::get_subscription_list_data(); $subscriptions = array_filter( $subscriptions_list_data, function( $subscription ) { return ! empty( $subscription['product_key'] ); } ); $updates = WC_Helper_Updater::get_update_data(); $subscriptions_product_ids = wp_list_pluck( $subscriptions, 'product_id' ); foreach ( $subscriptions as &$subscription ) { $subscription['activate_url'] = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => $current_filter, 'wc-helper-activate' => 1, 'wc-helper-product-key' => $subscription['product_key'], 'wc-helper-product-id' => $subscription['product_id'], 'wc-helper-nonce' => wp_create_nonce( 'activate:' . $subscription['product_key'] ), ), admin_url( 'admin.php' ) ); $subscription['deactivate_url'] = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => $current_filter, 'wc-helper-deactivate' => 1, 'wc-helper-product-key' => $subscription['product_key'], 'wc-helper-product-id' => $subscription['product_id'], 'wc-helper-nonce' => wp_create_nonce( 'deactivate:' . $subscription['product_key'] ), ), admin_url( 'admin.php' ) ); $subscription['update_url'] = admin_url( 'update-core.php' ); $local = wp_list_filter( array_merge( $woo_plugins, $woo_themes ), array( '_product_id' => $subscription['product_id'] ) ); if ( ! empty( $local ) ) { $local = array_shift( $local ); if ( 'plugin' === $local['_type'] ) { // A magic update_url. $subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $local['_filename'], 'upgrade-plugin_' . $local['_filename'] ); } elseif ( 'theme' === $local['_type'] ) { // Another magic update_url. $subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-theme&theme=' . $local['_stylesheet'] ), 'upgrade-theme_' . $local['_stylesheet'] ); } } $subscription['download_primary'] = true; $subscription['download_url'] = 'https://woo.com/my-account/downloads/'; if ( ! $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) { $subscription['download_url'] = $updates[ $subscription['product_id'] ]['package']; } $subscription['actions'] = array(); if ( $subscription['has_update'] && ! $subscription['expired'] ) { $action = array( /* translators: %s: version number */ 'message' => sprintf( __( 'Version %s is available.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ), 'button_label' => __( 'Update', 'woocommerce' ), 'button_url' => $subscription['update_url'], 'status' => 'update-available', 'icon' => 'dashicons-update', ); // Subscription is not active on this site. if ( ! $subscription['active'] ) { $action['message'] .= ' ' . __( 'To enable this update you need to activate this subscription.', 'woocommerce' ); $action['button_label'] = null; $action['button_url'] = null; } $subscription['actions'][] = $action; } if ( $subscription['has_update'] && $subscription['expired'] ) { $action = array( /* translators: %s: version number */ 'message' => sprintf( __( 'Version %s is available.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ), 'status' => 'expired', 'icon' => 'dashicons-info', ); $action['message'] .= ' ' . __( 'To enable this update you need to purchase a new subscription.', 'woocommerce' ); $action['button_label'] = __( 'Purchase', 'woocommerce' ); $action['button_url'] = self::add_utm_params_to_url_for_subscription_link( $subscription['product_url'], 'purchase' ); $subscription['actions'][] = $action; } elseif ( $subscription['expired'] && ! empty( $subscription['master_user_email'] ) ) { $action = array( 'message' => sprintf( __( 'This subscription has expired. Contact the owner to renew the subscription to receive updates and support.', 'woocommerce' ) ), 'status' => 'expired', 'icon' => 'dashicons-info', ); $subscription['actions'][] = $action; } elseif ( $subscription['expired'] ) { $action = array( 'message' => sprintf( __( 'This subscription has expired. Please renew to receive updates and support.', 'woocommerce' ) ), 'button_label' => __( 'Renew', 'woocommerce' ), 'button_url' => self::add_utm_params_to_url_for_subscription_link( 'https://woo.com/my-account/my-subscriptions/', 'renew' ), 'status' => 'expired', 'icon' => 'dashicons-info', ); $subscription['actions'][] = $action; } if ( $subscription['expiring'] && ! $subscription['autorenew'] ) { $action = array( 'message' => __( 'Subscription is expiring soon.', 'woocommerce' ), 'button_label' => __( 'Enable auto-renew', 'woocommerce' ), 'button_url' => self::add_utm_params_to_url_for_subscription_link( 'https://woo.com/my-account/my-subscriptions/', 'auto-renew' ), 'status' => 'expired', 'icon' => 'dashicons-info', ); $subscription['download_primary'] = false; $subscription['actions'][] = $action; } elseif ( $subscription['expiring'] ) { $action = array( 'message' => sprintf( __( 'This subscription is expiring soon. Please renew to continue receiving updates and support.', 'woocommerce' ) ), 'button_label' => __( 'Renew', 'woocommerce' ), 'button_url' => self::add_utm_params_to_url_for_subscription_link( 'https://woo.com/my-account/my-subscriptions/', 'renew' ), 'status' => 'expired', 'icon' => 'dashicons-info', ); $subscription['download_primary'] = false; $subscription['actions'][] = $action; } // Mark the first action primary. foreach ( $subscription['actions'] as $key => $action ) { if ( ! empty( $action['button_label'] ) ) { $subscription['actions'][ $key ]['primary'] = true; break; } } } // Break the by-ref. unset( $subscription ); // Installed products without a subscription. $no_subscriptions = array(); foreach ( array_merge( $woo_plugins, $woo_themes ) as $filename => $data ) { if ( in_array( $data['_product_id'], $subscriptions_product_ids ) ) { continue; } $data['_product_url'] = '#'; $data['_has_update'] = false; if ( ! empty( $updates[ $data['_product_id'] ] ) ) { $data['_has_update'] = version_compare( $updates[ $data['_product_id'] ]['version'], $data['Version'], '>' ); if ( ! empty( $updates[ $data['_product_id'] ]['url'] ) ) { $data['_product_url'] = $updates[ $data['_product_id'] ]['url']; } elseif ( ! empty( $data['PluginURI'] ) ) { $data['_product_url'] = $data['PluginURI']; } } $data['_actions'] = array(); if ( $data['_has_update'] ) { $action = array( /* translators: %s: version number */ 'message' => sprintf( __( 'Version %s is available. To enable this update you need to purchase a new subscription.', 'woocommerce' ), esc_html( $updates[ $data['_product_id'] ]['version'] ) ), 'button_label' => __( 'Purchase', 'woocommerce' ), 'button_url' => self::add_utm_params_to_url_for_subscription_link( $data['_product_url'], 'purchase' ), 'status' => 'expired', 'icon' => 'dashicons-info', ); $data['_actions'][] = $action; } else { $action = array( /* translators: 1: subscriptions docs 2: subscriptions docs */ 'message' => sprintf( __( 'To receive updates and support for this extension, you need to purchase a new subscription or consolidate your extensions to one connected account by sharing or transferring this extension to this connected account.', 'woocommerce' ), 'https://woo.com/document/managing-woocommerce-com-subscriptions/#section-10', 'https://woo.com/document/managing-woocommerce-com-subscriptions/#section-5' ), 'button_label' => __( 'Purchase', 'woocommerce' ), 'button_url' => self::add_utm_params_to_url_for_subscription_link( $data['_product_url'], 'purchase' ), 'status' => 'expired', 'icon' => 'dashicons-info', ); $data['_actions'][] = $action; } $no_subscriptions[ $filename ] = $data; } // Update the user id if it came from a migrated connection. if ( empty( $auth['user_id'] ) ) { $auth['user_id'] = get_current_user_id(); WC_Helper_Options::update( 'auth', $auth ); } // Sort alphabetically. uasort( $subscriptions, array( __CLASS__, '_sort_by_product_name' ) ); uasort( $no_subscriptions, array( __CLASS__, '_sort_by_name' ) ); // Filters. self::get_filters_counts( $subscriptions ); // Warm it up. self::_filter( $subscriptions, self::get_current_filter() ); // We have an active connection. include self::get_view_filename( 'html-main.php' ); return; } /** * Add tracking parameters to buttons (Renew, Purchase, etc.) on subscriptions page * * @param string $url URL to product page or to https://woo.com/my-account/my-subscriptions/. * @param string $utm_content value of utm_content query parameter used for tracking * * @return string URL including utm parameters for tracking */ public static function add_utm_params_to_url_for_subscription_link( $url, $utm_content ) { $utm_params = 'utm_source=subscriptionsscreen&' . 'utm_medium=product&' . 'utm_campaign=wcaddons&' . 'utm_content=' . $utm_content; // there are already some URL parameters if ( strpos( $url, '?' ) ) { return $url . '&' . $utm_params; } return $url . '?' . $utm_params; } /** * Get available subscriptions filters. * * @return array An array of filter keys and labels. */ public static function get_filters() { $filters = array( 'all' => __( 'All', 'woocommerce' ), 'active' => __( 'Active', 'woocommerce' ), 'inactive' => __( 'Inactive', 'woocommerce' ), 'installed' => __( 'Installed', 'woocommerce' ), 'update-available' => __( 'Update Available', 'woocommerce' ), 'expiring' => __( 'Expiring Soon', 'woocommerce' ), 'expired' => __( 'Expired', 'woocommerce' ), 'download' => __( 'Download', 'woocommerce' ), ); return $filters; } /** * Get counts data for the filters array. * * @param array $subscriptions The array of all available subscriptions. * * @return array Filter counts (filter => count). */ public static function get_filters_counts( $subscriptions = null ) { static $filters; if ( isset( $filters ) ) { return $filters; } $filters = array_fill_keys( array_keys( self::get_filters() ), 0 ); if ( empty( $subscriptions ) ) { return array(); } foreach ( $filters as $key => $count ) { $_subs = $subscriptions; self::_filter( $_subs, $key ); $filters[ $key ] = count( $_subs ); } return $filters; } /** * Get current filter. * * @return string The current filter. */ public static function get_current_filter() { $current_filter = 'all'; $valid_filters = array_keys( self::get_filters() ); if ( ! empty( $_GET['filter'] ) && in_array( wp_unslash( $_GET['filter'] ), $valid_filters ) ) { $current_filter = wc_clean( wp_unslash( $_GET['filter'] ) ); } return $current_filter; } /** * Filter an array of subscriptions by $filter. * * @param array $subscriptions The subscriptions array, passed by ref. * @param string $filter The filter. */ private static function _filter( &$subscriptions, $filter ) { switch ( $filter ) { case 'active': $subscriptions = wp_list_filter( $subscriptions, array( 'active' => true ) ); break; case 'inactive': $subscriptions = wp_list_filter( $subscriptions, array( 'active' => false ) ); break; case 'installed': foreach ( $subscriptions as $key => $subscription ) { if ( empty( $subscription['local']['installed'] ) ) { unset( $subscriptions[ $key ] ); } } break; case 'update-available': $subscriptions = wp_list_filter( $subscriptions, array( 'has_update' => true ) ); break; case 'expiring': $subscriptions = wp_list_filter( $subscriptions, array( 'expiring' => true ) ); break; case 'expired': $subscriptions = wp_list_filter( $subscriptions, array( 'expired' => true ) ); break; case 'download': foreach ( $subscriptions as $key => $subscription ) { if ( $subscription['local']['installed'] || $subscription['expired'] ) { unset( $subscriptions[ $key ] ); } } break; } } /** * Enqueue admin scripts and styles. */ public static function admin_enqueue_scripts() { $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; $wc_screen_id = 'woocommerce'; if ( $wc_screen_id . '_page_wc-addons' === $screen_id && isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { wp_enqueue_style( 'woocommerce-helper', WC()->plugin_url() . '/assets/css/helper.css', array(), Constants::get_constant( 'WC_VERSION' ) ); wp_style_add_data( 'woocommerce-helper', 'rtl', 'replace' ); } } /** * Various success/error notices. * * Runs during admin page render, so no headers/redirects here. * * @return array Array pairs of message/type strings with notices. */ private static function _get_return_notices() { $return_status = isset( $_GET['wc-helper-status'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-status'] ) ) : null; $notices = array(); switch ( $return_status ) { case 'activate-success': $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; $subscription = self::_get_subscriptions_from_product_id( $product_id ); $notices[] = array( 'type' => 'updated', 'message' => sprintf( /* translators: %s: product name */ __( '%s activated successfully. You will now receive updates for this product.', 'woocommerce' ), '' . esc_html( $subscription['product_name'] ) . '' ), ); break; case 'activate-error': $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; $subscription = self::_get_subscriptions_from_product_id( $product_id ); $notices[] = array( 'type' => 'error', 'message' => sprintf( /* translators: %s: product name */ __( 'An error has occurred when activating %s. Please try again later.', 'woocommerce' ), '' . esc_html( $subscription['product_name'] ) . '' ), ); break; case 'deactivate-success': $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; $subscription = self::_get_subscriptions_from_product_id( $product_id ); $local = self::_get_local_from_product_id( $product_id ); $message = sprintf( /* translators: %s: product name */ __( 'Subscription for %s deactivated successfully. You will no longer receive updates for this product.', 'woocommerce' ), '' . esc_html( $subscription['product_name'] ) . '' ); if ( $local && is_plugin_active( $local['_filename'] ) && current_user_can( 'activate_plugins' ) ) { $deactivate_plugin_url = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => self::get_current_filter(), 'wc-helper-deactivate-plugin' => 1, 'wc-helper-product-id' => $subscription['product_id'], 'wc-helper-nonce' => wp_create_nonce( 'deactivate-plugin:' . $subscription['product_id'] ), ), admin_url( 'admin.php' ) ); $message = sprintf( /* translators: %1$s: product name, %2$s: deactivate url */ __( 'Subscription for %1$s deactivated successfully. You will no longer receive updates for this product. Click here if you wish to deactivate the plugin as well.', 'woocommerce' ), '' . esc_html( $subscription['product_name'] ) . '', esc_url( $deactivate_plugin_url ) ); } $notices[] = array( 'message' => $message, 'type' => 'updated', ); break; case 'deactivate-error': $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; $subscription = self::_get_subscriptions_from_product_id( $product_id ); $notices[] = array( 'type' => 'error', 'message' => sprintf( /* translators: %s: product name */ __( 'An error has occurred when deactivating the subscription for %s. Please try again later.', 'woocommerce' ), '' . esc_html( $subscription['product_name'] ) . '' ), ); break; case 'deactivate-plugin-success': $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; $subscription = self::_get_subscriptions_from_product_id( $product_id ); $notices[] = array( 'type' => 'updated', 'message' => sprintf( /* translators: %s: product name */ __( 'The extension %s has been deactivated successfully.', 'woocommerce' ), '' . esc_html( $subscription['product_name'] ) . '' ), ); break; case 'deactivate-plugin-error': $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; $subscription = self::_get_subscriptions_from_product_id( $product_id ); $notices[] = array( 'type' => 'error', 'message' => sprintf( /* translators: %1$s: product name, %2$s: plugins screen url */ __( 'An error has occurred when deactivating the extension %1$s. Please proceed to the Plugins screen to deactivate it manually.', 'woocommerce' ), '' . esc_html( $subscription['product_name'] ) . '', admin_url( 'plugins.php' ) ), ); break; case 'helper-connected': $notices[] = array( 'message' => __( 'You have successfully connected your store to Woo.com', 'woocommerce' ), 'type' => 'updated', ); break; case 'helper-disconnected': $notices[] = array( 'message' => __( 'You have successfully disconnected your store from Woo.com', 'woocommerce' ), 'type' => 'updated', ); break; case 'helper-refreshed': $notices[] = array( 'message' => __( 'Authentication and subscription caches refreshed successfully.', 'woocommerce' ), 'type' => 'updated', ); break; } return $notices; } /** * Various early-phase actions with possible redirects. * * @param object $screen WP screen object. */ public static function current_screen( $screen ) { $wc_screen_id = 'woocommerce'; if ( $wc_screen_id . '_page_wc-addons' !== $screen->id ) { return; } if ( empty( $_GET['section'] ) || 'helper' !== $_GET['section'] ) { return; } if ( ! empty( $_GET['wc-helper-connect'] ) ) { return self::_helper_auth_connect(); } if ( ! empty( $_GET['wc-helper-return'] ) ) { return self::_helper_auth_return(); } if ( ! empty( $_GET['wc-helper-disconnect'] ) ) { return self::_helper_auth_disconnect(); } if ( ! empty( $_GET['wc-helper-refresh'] ) ) { return self::_helper_auth_refresh(); } if ( ! empty( $_GET['wc-helper-activate'] ) ) { return self::_helper_subscription_activate(); } if ( ! empty( $_GET['wc-helper-deactivate'] ) ) { return self::helper_subscription_deactivate(); } if ( ! empty( $_GET['wc-helper-deactivate-plugin'] ) ) { return self::_helper_plugin_deactivate(); } } /** * Get helper redirect URL. * * @param array $args Query args. * @param bool $redirect_to_wc_admin Whether to redirect to WC Admin. * @return string */ private static function get_helper_redirect_url( $args = array(), $redirect_to_wc_admin = false ) { global $current_screen; if ( true === $redirect_to_wc_admin && 'woocommerce_page_wc-addons' === $current_screen->id ) { return add_query_arg( array( 'page' => 'wc-admin', 'tab' => 'my-subscriptions', 'path' => rawurlencode( '/extensions' ), ), admin_url( 'admin.php' ) ); } return add_query_arg( $args, admin_url( 'admin.php' ) ); } /** * Initiate a new OAuth connection. */ private static function _helper_auth_connect() { if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'connect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized self::log( 'Could not verify nonce in _helper_auth_connect' ); wp_die( 'Could not verify nonce' ); } $redirect_url_args = array( 'page' => 'wc-addons', 'section' => 'helper', 'wc-helper-return' => 1, 'wc-helper-nonce' => wp_create_nonce( 'connect' ), ); if ( isset( $_GET['redirect-to-wc-admin'] ) ) { $redirect_url_args['redirect-to-wc-admin'] = 1; } $redirect_uri = add_query_arg( $redirect_url_args, admin_url( 'admin.php' ) ); $request = WC_Helper_API::post( 'oauth/request_token', array( 'body' => array( 'home_url' => home_url(), 'redirect_uri' => $redirect_uri, ), ) ); $code = wp_remote_retrieve_response_code( $request ); if ( 200 !== $code ) { self::log( sprintf( 'Call to oauth/request_token returned a non-200 response code (%d)', $code ) ); wp_die( 'Something went wrong' ); } $secret = json_decode( wp_remote_retrieve_body( $request ) ); if ( empty( $secret ) ) { self::log( sprintf( 'Call to oauth/request_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) ); wp_die( 'Something went wrong' ); } /** * Fires when the Helper connection process is initiated. */ do_action( 'woocommerce_helper_connect_start' ); $connect_url = add_query_arg( array( 'home_url' => rawurlencode( home_url() ), 'redirect_uri' => rawurlencode( $redirect_uri ), 'secret' => rawurlencode( $secret ), ), WC_Helper_API::url( 'oauth/authorize' ) ); wp_redirect( esc_url_raw( $connect_url ) ); die(); } /** * Return from Woo.com OAuth flow. */ private static function _helper_auth_return() { if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'connect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized self::log( 'Could not verify nonce in _helper_auth_return' ); wp_die( 'Something went wrong' ); } // Bail if the user clicked deny. if ( ! empty( $_GET['deny'] ) ) { /** * Fires when the Helper connection process is denied/cancelled. */ do_action( 'woocommerce_helper_denied' ); wp_safe_redirect( self::get_helper_redirect_url( array( 'page' => 'wc-addons', 'section' => 'helper', ), isset( $_GET['redirect-to-wc-admin'] ) ) ); die(); } // We do need a request token... if ( empty( $_GET['request_token'] ) ) { self::log( 'Request token not found in _helper_auth_return' ); wp_die( 'Something went wrong' ); } // Obtain an access token. $request = WC_Helper_API::post( 'oauth/access_token', array( 'body' => array( 'request_token' => wp_unslash( $_GET['request_token'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 'home_url' => home_url(), ), ) ); $code = wp_remote_retrieve_response_code( $request ); if ( 200 !== $code ) { self::log( sprintf( 'Call to oauth/access_token returned a non-200 response code (%d)', $code ) ); wp_die( 'Something went wrong' ); } $access_token = json_decode( wp_remote_retrieve_body( $request ), true ); if ( ! $access_token ) { self::log( sprintf( 'Call to oauth/access_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) ); wp_die( 'Something went wrong' ); } self::update_auth_option( $access_token['access_token'], $access_token['access_token_secret'], $access_token['site_id'] ); /** * Fires when the Helper connection process has completed successfully. */ do_action( 'woocommerce_helper_connected' ); // Enable tracking when connected. if ( class_exists( 'WC_Tracker' ) ) { update_option( 'woocommerce_allow_tracking', 'yes' ); WC_Tracker::send_tracking_data( true ); } // If connecting through in-app purchase, redirects back to Woo.com // for product installation. if ( ! empty( $_GET['wccom-install-url'] ) ) { wp_redirect( wp_unslash( $_GET['wccom-install-url'] ) ); exit; } wp_safe_redirect( self::get_helper_redirect_url( array( 'page' => 'wc-addons', 'section' => 'helper', 'wc-helper-status' => 'helper-connected', ), isset( $_GET['redirect-to-wc-admin'] ) ) ); die(); } /** * Disconnect from Woo.com, clear OAuth tokens. */ private static function _helper_auth_disconnect() { if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'disconnect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized self::log( 'Could not verify nonce in _helper_auth_disconnect' ); wp_die( 'Could not verify nonce' ); } /** * Fires when the Helper has been disconnected. */ do_action( 'woocommerce_helper_disconnected' ); $redirect_uri = self::get_helper_redirect_url( array( 'page' => 'wc-addons', 'section' => 'helper', 'wc-helper-status' => 'helper-disconnected', ), isset( $_GET['redirect-to-wc-admin'] ) ); self::disconnect(); wp_safe_redirect( $redirect_uri ); die(); } /** * User hit the Refresh button, clear all caches. */ private static function _helper_auth_refresh() { if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'refresh' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized self::log( 'Could not verify nonce in _helper_auth_refresh' ); wp_die( 'Could not verify nonce' ); } self::refresh_helper_subscriptions(); $redirect_uri = self::get_helper_redirect_url( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => self::get_current_filter(), 'wc-helper-status' => 'helper-refreshed', ), isset( $_GET['redirect-to-wc-admin'] ) ); wp_safe_redirect( $redirect_uri ); die(); } /** * Flush helper authentication cache. */ public static function refresh_helper_subscriptions() { /** * Fires when Helper subscriptions are refreshed. * * @since 8.3.0 */ do_action( 'woocommerce_helper_subscriptions_refresh' ); self::_flush_authentication_cache(); self::_flush_subscriptions_cache(); self::_flush_updates_cache(); } /** * Active a product subscription. */ private static function _helper_subscription_activate() { $product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : ''; $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'activate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized self::log( 'Could not verify nonce in _helper_subscription_activate' ); wp_die( 'Could not verify nonce' ); } try { $activated = self::activate_helper_subscription( $product_key, $product_id ); } catch ( Exception $e ) { $activated = false; } $redirect_uri = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => self::get_current_filter(), 'wc-helper-status' => $activated ? 'activate-success' : 'activate-error', 'wc-helper-product-id' => $product_id, ), admin_url( 'admin.php' ) ); wp_safe_redirect( $redirect_uri ); die(); } /** * Activate helper subscription. * * @throws Exception If the subscription could not be activated or found. * @param string $product_key Subscription product key. * @return bool True if activated, false otherwise. */ public static function activate_helper_subscription( $product_key ) { $subscription = self::get_subscription( $product_key ); if ( ! $subscription ) { throw new Exception( __( 'Subscription not found', 'woocommerce' ) ); } $product_id = $subscription['product_id']; // Activate subscription. $activation_response = WC_Helper_API::post( 'activate', array( 'authenticated' => true, 'body' => wp_json_encode( array( 'product_key' => $product_key, ) ), ) ); $activated = wp_remote_retrieve_response_code( $activation_response ) === 200; $body = json_decode( wp_remote_retrieve_body( $activation_response ), true ); if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' === $body['code'] ) { $activated = true; } if ( $activated ) { /** * Fires when the Helper activates a product successfully. * * @param int $product_id Product ID being activated. * @param string $product_key Subscription product key. * @param array $activation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response ); } else { /** * Fires when the Helper fails to activate a product. * * @param int $product_id Product ID being activated. * @param string $product_key Subscription product key. * @param array $activation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response ); throw new Exception( $body['message'] ?? __( 'Unknown error', 'woocommerce' ) ); } // Attempt to activate this plugin. $local = self::_get_local_from_product_id( $product_id ); if ( $local && 'plugin' == $local['_type'] && current_user_can( 'activate_plugins' ) && ! is_plugin_active( $local['_filename'] ) ) { activate_plugin( $local['_filename'] ); } self::_flush_subscriptions_cache(); self::_flush_updates_cache(); return $activated; } /** * Deactivate a product subscription. */ private static function helper_subscription_deactivate() { $product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : ''; $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized self::log( 'Could not verify nonce in helper_subscription_deactivate' ); wp_die( 'Could not verify nonce' ); } try { $deactivated = self::deactivate_helper_subscription( $product_key ); } catch ( Exception $e ) { $deactivated = false; } $redirect_uri = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => self::get_current_filter(), 'wc-helper-status' => $deactivated ? 'deactivate-success' : 'deactivate-error', 'wc-helper-product-id' => $product_id, ), admin_url( 'admin.php' ) ); wp_safe_redirect( $redirect_uri ); die(); } /** * Deactivate a product subscription. * * @throws Exception If the subscription could not be deactivated or found. * @param string $product_key Subscription product key. * @return bool True if deactivated, false otherwise. */ public static function deactivate_helper_subscription( $product_key ) { $subscription = self::get_subscription( $product_key ); if ( ! $subscription ) { throw new Exception( __( 'Subscription not found', 'woocommerce' ) ); } $product_id = $subscription['product_id']; $deactivation_response = WC_Helper_API::post( 'deactivate', array( 'authenticated' => true, 'body' => wp_json_encode( array( 'product_key' => $product_key, ) ), ) ); $code = wp_remote_retrieve_response_code( $deactivation_response ); $deactivated = 200 === $code; if ( $deactivated ) { /** * Fires when the Helper activates a product successfully. * * @param int $product_id Product ID being deactivated. * @param string $product_key Subscription product key. * @param array $deactivation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_deactivate_success', $product_id, $product_key, $deactivation_response ); } else { self::log( sprintf( 'Deactivate API call returned a non-200 response code (%d)', $code ) ); /** * Fires when the Helper fails to activate a product. * * @param int $product_id Product ID being deactivated. * @param string $product_key Subscription product key. * @param array $deactivation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response ); $body = json_decode( wp_remote_retrieve_body( $deactivation_response ), true ); throw new Exception( $body['message'] ?? __( 'Unknown error', 'woocommerce' ) ); } self::_flush_subscriptions_cache(); return $deactivated; } /** * Deactivate a plugin. */ private static function _helper_plugin_deactivate() { $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; $deactivated = false; if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate-plugin:' . $product_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized self::log( 'Could not verify nonce in _helper_plugin_deactivate' ); wp_die( 'Could not verify nonce' ); } if ( ! current_user_can( 'activate_plugins' ) ) { wp_die( 'You are not allowed to manage plugins on this site.' ); } $local = wp_list_filter( array_merge( self::get_local_woo_plugins(), self::get_local_woo_themes() ), array( '_product_id' => $product_id ) ); // Attempt to deactivate this plugin or theme. if ( ! empty( $local ) ) { $local = array_shift( $local ); if ( is_plugin_active( $local['_filename'] ) ) { deactivate_plugins( $local['_filename'] ); } $deactivated = ! is_plugin_active( $local['_filename'] ); } $redirect_uri = add_query_arg( array( 'page' => 'wc-addons', 'section' => 'helper', 'filter' => self::get_current_filter(), 'wc-helper-status' => $deactivated ? 'deactivate-plugin-success' : 'deactivate-plugin-error', 'wc-helper-product-id' => $product_id, ), admin_url( 'admin.php' ) ); wp_safe_redirect( $redirect_uri ); die(); } /** * Get a local plugin/theme entry from product_id. * * @param int $product_id The product id. * * @return array|bool The array containing the local plugin/theme data or false. */ private static function _get_local_from_product_id( $product_id ) { $local = wp_list_filter( array_merge( self::get_local_woo_plugins(), self::get_local_woo_themes() ), array( '_product_id' => $product_id ) ); if ( ! empty( $local ) ) { return array_shift( $local ); } return false; } /** * Checks whether current site has product subscription of a given ID. * * @since 3.7.0 * * @param int $product_id The product id. * * @return bool Returns true if product subscription exists, false otherwise. */ public static function has_product_subscription( $product_id ) { $subscription = self::_get_subscriptions_from_product_id( $product_id, true ); return ! empty( $subscription ); } /** * Get a subscription entry from product_id. If multiple subscriptions are * found with the same product id and $single is set to true, will return the * first one in the list, so you can use this method to get things like extension * name, version, etc. * * @param int $product_id The product id. * @param bool $single Whether to return a single subscription or all matching a product id. * * @return array|bool The array containing sub data or false. */ private static function _get_subscriptions_from_product_id( $product_id, $single = true ) { $subscriptions = wp_list_filter( self::get_subscriptions(), array( 'product_id' => $product_id ) ); if ( ! empty( $subscriptions ) ) { return $single ? array_shift( $subscriptions ) : $subscriptions; } return false; } /** * Get locally installed plugins * * @return array */ public static function get_local_plugins() { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugins = get_plugins(); $output_plugins = array(); foreach ( $plugins as $filename => $data ) { array_push( $output_plugins, array( '_filename' => $filename, '_type' => 'plugin', 'slug' => dirname( $filename ), 'Version' => $data['Version'], ) ); } return $output_plugins; } /** * Get locally installed themes. * * @return array */ public static function get_local_themes() { if ( ! function_exists( 'wp_get_themes' ) ) { require_once ABSPATH . 'wp-admin/includes/theme.php'; } $themes = wp_get_themes(); $output_themes = array(); foreach ( $themes as $theme ) { array_push( $output_themes, array( '_filename' => $theme->get_stylesheet() . '/style.css', '_stylesheet' => $theme->get_stylesheet(), '_type' => 'theme', 'slug' => $theme->get_stylesheet(), 'Version' => $theme->get( 'Version' ), ) ); } return $output_themes; } /** * Obtain a list of data about locally installed Woo extensions. */ public static function get_local_woo_plugins() { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugins = get_plugins(); /** * Check if plugins have WC headers, if not then clear cache and fetch again. * WC Headers will not be present if `wc_enable_wc_plugin_headers` hook was added after a `get_plugins` call -- for example when WC is activated/updated. * Also, get_plugins call is expensive, so we should clear this cache very conservatively. */ if ( ! empty( $plugins ) && ! array_key_exists( 'Woo', current( $plugins ) ) ) { wp_clean_plugins_cache( false ); $plugins = get_plugins(); } $woo_plugins = array(); // Backwards compatibility for woothemes_queue_update(). $_compat = array(); if ( ! empty( $GLOBALS['woothemes_queued_updates'] ) ) { foreach ( $GLOBALS['woothemes_queued_updates'] as $_compat_plugin ) { $_compat[ $_compat_plugin->file ] = array( 'product_id' => $_compat_plugin->product_id, 'file_id' => $_compat_plugin->file_id, ); } } foreach ( $plugins as $filename => $data ) { if ( empty( $data['Woo'] ) && ! empty( $_compat[ $filename ] ) ) { $data['Woo'] = sprintf( '%d:%s', $_compat[ $filename ]['product_id'], $_compat[ $filename ]['file_id'] ); } if ( empty( $data['Woo'] ) ) { continue; } list( $product_id, $file_id ) = explode( ':', $data['Woo'] ); if ( empty( $product_id ) || empty( $file_id ) ) { continue; } // Omit the WooCommerce plugin used on Woo Express sites. if ( 'WooCommerce' === $data['Name'] ) { continue; } $data['_filename'] = $filename; $data['_product_id'] = absint( $product_id ); $data['_file_id'] = $file_id; $data['_type'] = 'plugin'; $data['slug'] = dirname( $filename ); $woo_plugins[ $filename ] = $data; } return $woo_plugins; } /** * Get locally installed Woo themes. */ public static function get_local_woo_themes() { $themes = wp_get_themes(); $woo_themes = array(); foreach ( $themes as $theme ) { $header = $theme->get( 'Woo' ); // Backwards compatibility for theme_info.txt. if ( ! $header ) { $txt = $theme->get_stylesheet_directory() . '/theme_info.txt'; if ( is_readable( $txt ) ) { $txt = file_get_contents( $txt ); $txt = preg_split( '#\s#', $txt ); if ( count( $txt ) >= 2 ) { $header = sprintf( '%d:%s', $txt[0], $txt[1] ); } } } if ( empty( $header ) ) { continue; } list( $product_id, $file_id ) = explode( ':', $header ); if ( empty( $product_id ) || empty( $file_id ) ) { continue; } $data = array( 'Name' => $theme->get( 'Name' ), 'Version' => $theme->get( 'Version' ), 'Woo' => $header, '_filename' => $theme->get_stylesheet() . '/style.css', '_stylesheet' => $theme->get_stylesheet(), '_product_id' => absint( $product_id ), '_file_id' => $file_id, '_type' => 'theme', 'slug' => dirname( $theme->get_stylesheet() ), ); $woo_themes[ $data['_filename'] ] = $data; } return $woo_themes; } /** * Get the connected user's subscriptions. * * @return array */ public static function get_subscriptions() { $cache_key = '_woocommerce_helper_subscriptions'; $data = get_transient( $cache_key ); if ( false !== $data ) { return $data; } $request_uri = wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $source = ''; if ( stripos( $request_uri, 'wc-addons' ) ) : $source = 'my-subscriptions'; elseif ( stripos( $request_uri, 'plugins.php' ) ) : $source = 'plugins'; elseif ( stripos( $request_uri, 'wc-admin' ) ) : $source = 'inbox-notes'; elseif ( stripos( $request_uri, 'admin-ajax.php' ) ) : $source = 'heartbeat-api'; elseif ( defined( 'WP_CLI' ) && WP_CLI ) : $source = 'wc-cli'; endif; // Obtain the connected user info. $request = WC_Helper_API::get( 'subscriptions', array( 'authenticated' => true, 'query_string' => '' !== $source ? esc_url( '?source=' . $source ) : '', ) ); if ( wp_remote_retrieve_response_code( $request ) !== 200 ) { set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS ); return array(); } $data = json_decode( wp_remote_retrieve_body( $request ), true ); if ( empty( $data ) || ! is_array( $data ) ) { $data = array(); } set_transient( $cache_key, $data, 1 * HOUR_IN_SECONDS ); return $data; } /** * Get subscription data for a given product key. * * @param string $product_key Subscription product key. * @return array|bool The array containing sub data or false. */ public static function get_subscription( $product_key ) { $subscriptions = wp_list_filter( self::get_subscriptions(), array( 'product_key' => $product_key ) ); if ( empty( $subscriptions ) ) { return false; } $subscription = array_shift( $subscriptions ); $subscription['local'] = self::get_subscription_local_data( $subscription ); return $subscription; } /** * Get the connected user's subscription list data. * This is used by the My Subscriptions page. * * @return array */ public static function get_subscription_list_data() { $subscriptions = self::get_subscriptions(); // Installed plugins and themes, with or without an active subscription. $woo_plugins = self::get_local_woo_plugins(); $woo_themes = self::get_local_woo_themes(); $subscriptions_product_ids = wp_list_pluck( $subscriptions, 'product_id' ); $auth = WC_Helper_Options::get( 'auth' ); $site_id = isset( $auth['site_id'] ) ? absint( $auth['site_id'] ) : 0; // Installed products without a subscription. foreach ( array_merge( $woo_plugins, $woo_themes ) as $filename => $data ) { if ( in_array( $data['_product_id'], $subscriptions_product_ids, true ) ) { continue; } $subscriptions[] = array( 'product_key' => '', 'product_id' => $data['_product_id'], 'product_name' => $data['Name'], 'product_url' => $data['PluginURI'] ?? '', 'zip_slug' => $data['slug'], 'documentation_url' => '', 'key_type' => '', 'key_type_label' => '', 'lifetime' => false, 'product_status' => 'publish', 'connections' => array(), 'expires' => 0, 'expired' => true, 'expiring' => false, 'sites_max' => 0, 'sites_active' => 0, 'autorenew' => false, 'maxed' => false, ); } foreach ( $subscriptions as &$subscription ) { $subscription['active'] = in_array( $site_id, $subscription['connections'], true ); $updates = WC_Helper_Updater::get_update_data(); $subscription['local'] = self::get_subscription_local_data( $subscription ); $subscription['has_update'] = false; if ( $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) { $subscription['has_update'] = version_compare( $updates[ $subscription['product_id'] ]['version'], $subscription['local']['version'], '>' ); } if ( ! empty( $updates[ $subscription['product_id'] ] ) ) { $subscription['version'] = $updates[ $subscription['product_id'] ]['version']; } } // Sort subscriptions by name and expiration date. usort( $subscriptions, function( $a, $b ) { $compare_value = strcasecmp( $a['product_name'], $b['product_name'] ); if ( 0 === $compare_value ) { return strcasecmp( $a['expires'], $b['expires'] ); } return $compare_value; } ); // Add subscription install flags after the active and local data is set. foreach ( $subscriptions as &$subscription ) { $subscription['subscription_available'] = self::is_subscription_available( $subscription, $subscriptions ); $subscription['subscription_installed'] = self::is_subscription_installed( $subscription, $subscriptions ); } // Break the by-ref. unset( $subscription ); return $subscriptions; } /** * Check if a subscription is available to use. * That is, is not already active and hasn't expired, and there are no other subscriptions * for this product already active on this site. * * @param array $subscription The subscription we're checking. * @param array $subscriptions The list of all the user's subscriptions. * @return bool True if multiple licenses exist, false otherwise. */ public static function is_subscription_available( $subscription, $subscriptions ) { if ( true === $subscription['active'] ) { return false; } if ( true === $subscription['expired'] ) { return false; } $product_subscriptions = wp_list_filter( $subscriptions, array( 'product_id' => $subscription['product_id'], 'active' => true, ) ); // If there are no subscriptions for this product already active on this site, then it's available. if ( empty( $product_subscriptions ) ) { return true; } return false; } /** * Check if product relating to a subscription is installed. * This method will return true if the product is installed, but will exclude subscriptions for the same product that are not in use. * If a product is installed and inactive, this will ensure that one subscription is marked as installed. * * @param array $subscription The subscription we're checking. * @param array $subscriptions The list of all the user's subscriptions. * @return bool True if installed, false otherwise. */ public static function is_subscription_installed( $subscription, $subscriptions ) { if ( false === $subscription['local']['installed'] ) { return false; } // If the subscription is active, then it's installed. if ( true === $subscription['active'] ) { return true; } $product_subscriptions = wp_list_filter( $subscriptions, array( 'product_id' => $subscription['product_id'], ) ); if ( empty( $product_subscriptions ) ) { return false; } // If there are no other subscriptions for this product, then it's installed. if ( 1 === count( $product_subscriptions ) ) { return true; } $active_subscription = wp_list_filter( $product_subscriptions, array( 'active' => true, ) ); // If there is another active subscription, this subscription is not installed. // If the current subscription is active, it would already return true above. if ( ! empty( $active_subscription ) ) { return false; } // If there are multiple subscriptions, but no active subscriptions, then mark the first one as installed. $product_subscription = array_shift( $product_subscriptions ); if ( $product_subscription['product_key'] === $subscription['product_key'] ) { return true; } return false; } /** * Add local data to a subscription. * * @param array $subscription The subscription data. * @return array The subscription data with local data added. */ public static function get_subscription_local_data( array $subscription ) { $local_plugins = self::get_local_plugins(); $local_themes = self::get_local_themes(); $installed_product = wp_list_filter( array_merge( $local_plugins, $local_themes ), array( 'slug' => $subscription['zip_slug'] ) ); $installed_product = array_shift( $installed_product ); if ( empty( $installed_product ) ) { return array( 'installed' => false, 'active' => false, 'version' => null, 'type' => null, 'slug' => null, 'path' => null, ); } $local_data = array( 'installed' => true, 'active' => false, 'version' => $installed_product['Version'], 'type' => $installed_product['_type'], 'slug' => null, 'path' => $installed_product['_filename'], ); if ( 'plugin' === $installed_product['_type'] ) { $local_data['slug'] = $installed_product['slug']; if ( is_plugin_active( $installed_product['_filename'] ) ) { $local_data['active'] = true; } elseif ( is_multisite() && is_plugin_active_for_network( $installed_product['_filename'] ) ) { $local_data['active'] = true; } } elseif ( 'theme' === $installed_product['_type'] ) { $local_data['slug'] = $installed_product['_stylesheet']; if ( in_array( $installed_product['_stylesheet'], array( get_stylesheet(), get_template() ), true ) ) { $local_data['active'] = true; } } return $local_data; } /** * Runs when any plugin is activated. * * Depending on the activated plugin attempts to look through available * subscriptions and auto-activate one if possible, so the user does not * need to visit the Helper UI at all after installing a new extension. * * @param string $filename The filename of the activated plugin. */ public static function activated_plugin( $filename ) { $plugins = self::get_local_woo_plugins(); // Not a local woo plugin. if ( empty( $plugins[ $filename ] ) ) { return; } // Make sure we have a connection. $auth = WC_Helper_Options::get( 'auth' ); if ( empty( $auth ) ) { return; } $plugin = $plugins[ $filename ]; $product_id = $plugin['_product_id']; $subscriptions = self::_get_subscriptions_from_product_id( $product_id, false ); // No valid subscriptions for this product. if ( empty( $subscriptions ) ) { return; } $subscription = null; foreach ( $subscriptions as $_sub ) { // Don't attempt to activate expired subscriptions. if ( $_sub['expired'] ) { continue; } // No more sites available in this subscription. if ( $_sub['sites_max'] && $_sub['sites_active'] >= $_sub['sites_max'] ) { continue; } // Looks good. $subscription = $_sub; break; } // No valid subscription found. if ( ! $subscription ) { return; } $product_key = $subscription['product_key']; $activation_response = WC_Helper_API::post( 'activate', array( 'authenticated' => true, 'body' => wp_json_encode( array( 'product_key' => $product_key, ) ), ) ); $activated = wp_remote_retrieve_response_code( $activation_response ) === 200; $body = json_decode( wp_remote_retrieve_body( $activation_response ), true ); if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' === $body['code'] ) { $activated = true; } if ( $activated ) { self::log( 'Auto-activated a subscription for ' . $filename ); /** * Fires when the Helper activates a product successfully. * * @param int $product_id Product ID being activated. * @param string $product_key Subscription product key. * @param array $activation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response ); } else { self::log( 'Could not activate a subscription upon plugin activation: ' . $filename ); /** * Fires when the Helper fails to activate a product. * * @param int $product_id Product ID being activated. * @param string $product_key Subscription product key. * @param array $activation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response ); } self::_flush_subscriptions_cache(); self::_flush_updates_cache(); } /** * Runs when any plugin is deactivated. * * When a user deactivates a plugin, attempt to deactivate any subscriptions * associated with the extension. * * @param string $filename The filename of the deactivated plugin. */ public static function deactivated_plugin( $filename ) { $plugins = self::get_local_woo_plugins(); // Not a local woo plugin. if ( empty( $plugins[ $filename ] ) ) { return; } // Make sure we have a connection. $auth = WC_Helper_Options::get( 'auth' ); if ( empty( $auth ) ) { return; } $plugin = $plugins[ $filename ]; $product_id = $plugin['_product_id']; $subscriptions = self::_get_subscriptions_from_product_id( $product_id, false ); $site_id = absint( $auth['site_id'] ); // No valid subscriptions for this product. if ( empty( $subscriptions ) ) { return; } $deactivated = 0; foreach ( $subscriptions as $subscription ) { // Don't touch subscriptions that aren't activated on this site. if ( ! in_array( $site_id, $subscription['connections'], true ) ) { continue; } $product_key = $subscription['product_key']; $deactivation_response = WC_Helper_API::post( 'deactivate', array( 'authenticated' => true, 'body' => wp_json_encode( array( 'product_key' => $product_key, ) ), ) ); if ( wp_remote_retrieve_response_code( $deactivation_response ) === 200 ) { $deactivated++; /** * Fires when the Helper activates a product successfully. * * @param int $product_id Product ID being deactivated. * @param string $product_key Subscription product key. * @param array $deactivation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_deactivate_success', $product_id, $product_key, $deactivation_response ); } else { /** * Fires when the Helper fails to activate a product. * * @param int $product_id Product ID being deactivated. * @param string $product_key Subscription product key. * @param array $deactivation_response The response object from wp_safe_remote_request(). */ do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response ); } } if ( $deactivated ) { self::log( sprintf( 'Auto-deactivated %d subscription(s) for %s', $deactivated, $filename ) ); self::_flush_subscriptions_cache(); self::_flush_updates_cache(); } } /** * Various Helper-related admin notices. */ public static function admin_notices() { if ( apply_filters( 'woocommerce_helper_suppress_admin_notices', false ) ) { return; } $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; if ( 'update-core' !== $screen_id ) { return; } // Don't nag if Woo doesn't have an update available. if ( ! self::_woo_core_update_available() ) { return; } // Add a note about available extension updates if Woo core has an update available. $notice = self::_get_extensions_update_notice(); if ( ! empty( $notice ) ) { echo '

' . $notice . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } } /** * Get an update notice if one or more Woo extensions has an update available. * * @return string|null The update notice or null if everything is up to date. */ private static function _get_extensions_update_notice() { $plugins = self::get_local_woo_plugins(); $updates = WC_Helper_Updater::get_update_data(); $available = 0; foreach ( $plugins as $data ) { if ( empty( $updates[ $data['_product_id'] ] ) ) { continue; } $product_id = $data['_product_id']; if ( version_compare( $updates[ $product_id ]['version'], $data['Version'], '>' ) ) { $available++; } } if ( ! $available ) { return; } return sprintf( /* translators: %1$s: helper url, %2$d: number of extensions */ _n( 'Note: You currently have %2$d paid extension which should be updated first before updating WooCommerce.', 'Note: You currently have %2$d paid extensions which should be updated first before updating WooCommerce.', $available, 'woocommerce' ), admin_url( 'admin.php?page=wc-addons§ion=helper' ), $available ); } /** * Whether WooCommerce has an update available. * * @return bool True if a Woo core update is available. */ private static function _woo_core_update_available() { $updates = get_site_transient( 'update_plugins' ); if ( empty( $updates->response ) ) { return false; } if ( empty( $updates->response['woocommerce/woocommerce.php'] ) ) { return false; } $data = $updates->response['woocommerce/woocommerce.php']; if ( version_compare( Constants::get_constant( 'WC_VERSION' ), $data->new_version, '>=' ) ) { return false; } return true; } /** * Flush subscriptions cache. */ public static function _flush_subscriptions_cache() { delete_transient( '_woocommerce_helper_subscriptions' ); } /** * Flush auth cache. */ public static function _flush_authentication_cache() { $request = WC_Helper_API::get( 'oauth/me', array( 'authenticated' => true, ) ); if ( wp_remote_retrieve_response_code( $request ) !== 200 ) { return false; } $user_data = json_decode( wp_remote_retrieve_body( $request ), true ); if ( ! $user_data ) { return false; } WC_Helper_Options::update( 'auth_user_data', array( 'name' => $user_data['name'], 'email' => $user_data['email'], ) ); return true; } /** * Flush updates cache. */ private static function _flush_updates_cache() { WC_Helper_Updater::flush_updates_cache(); } /** * Sort subscriptions by the product_name. * * @param array $a Subscription array. * @param array $b Subscription array. * * @return int */ public static function _sort_by_product_name( $a, $b ) { return strcmp( $a['product_name'], $b['product_name'] ); } /** * Sort subscriptions by the Name. * * @param array $a Product array. * @param array $b Product array. * * @return int */ public static function _sort_by_name( $a, $b ) { return strcmp( $a['Name'], $b['Name'] ); } /** * Log a helper event. * * @param string $message Log message. * @param string $level Optional, defaults to info, valid levels: emergency|alert|critical|error|warning|notice|info|debug. */ public static function log( $message, $level = 'info' ) { if ( ! Constants::is_true( 'WP_DEBUG' ) ) { return; } if ( ! isset( self::$log ) ) { self::$log = wc_get_logger(); } self::$log->log( $level, $message, array( 'source' => 'helper' ) ); } /** * Handles WC Helper disconnect tasks. * * @return void */ public static function disconnect() { WC_Helper_API::post( 'oauth/invalidate_token', array( 'authenticated' => true, ) ); WC_Helper_Options::update( 'auth', array() ); WC_Helper_Options::update( 'auth_user_data', array() ); self::_flush_subscriptions_cache(); self::_flush_updates_cache(); } /** * Checks if `access_token` exists in `auth` option. * * @return bool */ public static function is_site_connected(): bool { $auth = WC_Helper_Options::get( 'auth' ); // If `access_token` is empty, there's no active connection. return ! empty( $auth['access_token'] ); } /** * Allows to connect with WCCOM using application password. used it to connect via CLI * * @param string $password The application password. * * @return void|WP_Error */ public static function connect_with_password( string $password ) { $request = WC_Helper_API::post( 'connect', array( 'headers' => array( 'X-API-Key' => $password, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( array( 'home_url' => home_url() ) ), 'authenticated' => false, ) ); $code = wp_remote_retrieve_response_code( $request ); if ( $code === 403 ) { $message = 'Invalid password'; self::log( $message ); return new WP_Error( 'connect-with-password-invalid-password', $message ); } elseif ( $code !== 200 ) { $message = sprintf( 'Call to /connect returned a non-200 response code (%d)', $code ); self::log( $message ); return new WP_Error( 'connect-with-password-' . $code, $message ); } $access_data = json_decode( wp_remote_retrieve_body( $request ), true ); if ( empty( $access_data['access_token'] ) || empty( $access_data['access_token_secret'] ) ) { $message = sprintf( 'Call to /connect returned an invalid body: %s', wp_remote_retrieve_body( $request ) ); self::log( $message ); return new WP_Error( 'connect-with-password-invalid-response', $message ); } self::update_auth_option( $access_data['access_token'], $access_data['access_token_secret'], $access_data['site_id'] ); } /** * Updates auth options and flushes cache * * @param string $access_token The access token. * @param string $access_token_secret The secret access token. * @param int $site_id The site id returned by the API. * * @return void */ public static function update_auth_option( string $access_token, string $access_token_secret, int $site_id ): void { WC_Helper_Options::update( 'auth', array( 'access_token' => $access_token, 'access_token_secret' => $access_token_secret, 'site_id' => $site_id, 'user_id' => get_current_user_id(), 'updated' => time(), ) ); // Obtain the connected user info. if ( ! self::_flush_authentication_cache() ) { self::log( 'Could not obtain connected user info in _helper_auth_return.' ); WC_Helper_Options::update( 'auth', array() ); wp_die( 'Something went wrong. Could not obtain connected user info in _helper_auth_return.' ); } self::_flush_subscriptions_cache(); self::_flush_updates_cache(); } } WC_Helper::load(); PKK[‹ÚŽ(j j &admin/helper/class-wc-helper-admin.phpnu„[µü¤ WC_Helper::is_site_connected(), 'connectURL' => self::get_connection_url(), 'userEmail' => $auth_user_email, 'userAvatar' => get_avatar_url( $auth_user_email, array( 'size' => '48' ) ), 'storeCountry' => wc_get_base_location()['country'], 'inAppPurchaseURLParams' => WC_Admin_Addons::get_in_app_purchase_url_params(), ); return $settings; } /** * Generates the URL for connecting or disconnecting the store to/from Woo.com. * Approach taken from existing helper code that isn't exposed. * * @return string */ public static function get_connection_url() { global $current_screen; $connect_url_args = array( 'page' => 'wc-addons', 'section' => 'helper', ); // No active connection. if ( WC_Helper::is_site_connected() ) { $connect_url_args['wc-helper-disconnect'] = 1; $connect_url_args['wc-helper-nonce'] = wp_create_nonce( 'disconnect' ); } else { $connect_url_args['wc-helper-connect'] = 1; $connect_url_args['wc-helper-nonce'] = wp_create_nonce( 'connect' ); } if ( isset( $current_screen->id ) && 'woocommerce_page_wc-admin' === $current_screen->id ) { $connect_url_args['redirect-to-wc-admin'] = 1; } return add_query_arg( $connect_url_args, admin_url( 'admin.php' ) ); } /** * Registers the REST routes for the featured products endpoint. * This endpoint is used by the WooCommerce > Extensions > Discover * page. */ public static function register_rest_routes() { register_rest_route( 'wc/v3', '/marketplace/featured', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_featured' ), 'permission_callback' => array( __CLASS__, 'get_permission' ), ) ); } /** * The Extensions page can only be accessed by users with the manage_woocommerce * capability. So the API mimics that behavior. */ public static function get_permission() { return current_user_can( 'manage_woocommerce' ); } /** * Fetch featured products from Woo.com and serve them * as JSON. */ public static function get_featured() { $featured = WC_Admin_Addons::fetch_featured(); if ( is_wp_error( $featured ) ) { wp_send_json_error( array( 'message' => $featured->get_error_message() ) ); } wp_send_json( $featured ); } } WC_Helper_Admin::load(); PKK[ÛAæÃíí+admin/helper/views/html-section-notices.phpnu„[µü¤
PKK[—ÕMþ½½+admin/helper/views/html-section-account.phpnu„[µü¤

PKK[¹¼>ÈÈ'admin/helper/views/html-section-nav.phpnu„[µü¤ PKK[ne÷êê'admin/helper/views/html-oauth-start.phpnu„[µü¤ WooCommerce -> Extensions -> Woo.com Subscriptions main page. * * @package WooCommerce\Views */ defined( 'ABSPATH' ) || exit(); ?>

<?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?>

PKK[Qîà,à, admin/helper/views/html-main.phpnu„[µü¤

Plugins screen.', 'woocommerce' ), array( 'a' => array( 'href' => array(), ), ) ), esc_url( admin_url( 'plugins.php' ) ) ); ?>


0 ) { /* translators: %1$d: sites active, %2$d max sites active */ printf( esc_html__( 'Subscription: Using %1$d of %2$d sites available', 'woocommerce' ), absint( $subscription['sites_active'] ), absint( $subscription['sites_max'] ) ); } else { esc_html_e( 'Subscription: Unlimited', 'woocommerce' ); } // Check shared. if ( ! empty( $subscription['is_shared'] ) && ! empty( $subscription['owner_email'] ) ) { /* translators: Email address of person who shared the subscription. */ printf( '
' . esc_html__( 'Shared by %s', 'woocommerce' ), esc_html( $subscription['owner_email'] ) ); } elseif ( isset( $subscription['master_user_email'] ) ) { /* translators: Email address of person who shared the subscription. */ printf( '
' . esc_html__( 'Shared by %s', 'woocommerce' ), esc_html( $subscription['master_user_email'] ) ); } ?>

Below is a list of Woo.com products available on your site - but are either out-dated or do not have a valid subscription.

$data ) : ?>

array( 'href' => array(), 'title' => array(), ), 'br' => array(), 'em' => array(), 'strong' => array(), ) ); ?>

PKK[$·JÖÖ)admin/helper/views/html-helper-compat.phpnu„[µü¤

View and manage your extensions now.', 'woocommerce' ), esc_url( $helper_url ) ); ?>

PKK[mM×ñw[w[ admin/class-wc-admin-notices.phpnu„[µü¤ callback. * * @var array */ private static $core_notices = array( 'update' => 'update_notice', 'template_files' => 'template_file_check_notice', 'legacy_shipping' => 'legacy_shipping_notice', 'no_shipping_methods' => 'no_shipping_methods_notice', 'regenerating_thumbnails' => 'regenerating_thumbnails_notice', 'regenerating_lookup_table' => 'regenerating_lookup_table_notice', 'no_secure_connection' => 'secure_connection_notice', 'maxmind_license_key' => 'maxmind_missing_license_key_notice', 'redirect_download_method' => 'redirect_download_method_notice', 'uploads_directory_is_unprotected' => 'uploads_directory_is_unprotected_notice', 'base_tables_missing' => 'base_tables_missing_notice', 'download_directories_sync_complete' => 'download_directories_sync_complete', ); /** * Constructor. */ public static function init() { self::$notices = get_option( 'woocommerce_admin_notices', array() ); add_action( 'switch_theme', array( __CLASS__, 'reset_admin_notices' ) ); add_action( 'woocommerce_installed', array( __CLASS__, 'reset_admin_notices' ) ); add_action( 'wp_loaded', array( __CLASS__, 'add_redirect_download_method_notice' ) ); add_action( 'admin_init', array( __CLASS__, 'hide_notices' ), 20 ); self::add_action( 'admin_init', array( __CLASS__, 'maybe_remove_legacy_api_removal_notice' ), 20 ); // @TODO: This prevents Action Scheduler async jobs from storing empty list of notices during WC installation. // That could lead to OBW not starting and 'Run setup wizard' notice not appearing in WP admin, which we want // to avoid. if ( ! WC_Install::is_new_install() || ! wc_is_running_from_async_action_scheduler() ) { add_action( 'shutdown', array( __CLASS__, 'store_notices' ) ); } if ( current_user_can( 'manage_woocommerce' ) ) { add_action( 'admin_print_styles', array( __CLASS__, 'add_notices' ) ); } } /** * Parses query to create nonces when available. * * @deprecated 5.4.0 * @param object $response The WP_REST_Response we're working with. * @return object $response The prepared WP_REST_Response object. */ public static function prepare_note_with_nonce( $response ) { wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '5.4.0' ); return $response; } /** * Store notices to DB */ public static function store_notices() { update_option( 'woocommerce_admin_notices', self::get_notices() ); } /** * Get notices * * @return array */ public static function get_notices() { return self::$notices; } /** * Remove all notices. */ public static function remove_all_notices() { self::$notices = array(); } /** * Reset notices for themes when switched or a new version of WC is installed. */ public static function reset_admin_notices() { if ( ! self::is_ssl() ) { self::add_notice( 'no_secure_connection' ); } if ( ! self::is_uploads_directory_protected() ) { self::add_notice( 'uploads_directory_is_unprotected' ); } self::add_notice( 'template_files' ); self::add_min_version_notice(); self::add_maxmind_missing_license_key_notice(); self::maybe_add_legacy_api_removal_notice(); } // phpcs:disable Generic.Commenting.Todo.TaskFound /** * Add an admin notice about the removal of the Legacy REST API if the said API is enabled, * and a notice about soon to be unsupported webhooks with Legacy API payload if at least one of these exist. * * TODO: Change this method in WooCommerce 9.0 so that it checks if the Legacy REST API extension is installed, and if not, it points to the extension URL in the WordPress plugins directory. */ private static function maybe_add_legacy_api_removal_notice() { if ( is_plugin_active( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' ) ) { return; } if ( 'yes' === get_option( 'woocommerce_api_enabled' ) ) { self::add_custom_notice( 'legacy_api_removed_in_woo_90', sprintf( '%s%s', sprintf( '

%s

', esc_html__( 'The WooCommerce Legacy REST API will be removed soon', 'woocommerce' ) ), sprintf( // translators: Placeholders are URLs. wpautop( wp_kses_data( __( 'The WooCommerce Legacy REST API, currently enabled in this site, will be removed in WooCommerce 9.0. A separate WooCommerce extension will be available to keep it enabled. Learn more about this change.', 'woocommerce' ) ) ), admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=legacy_api' ), 'https://developer.woo.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/' ) ) ); } if ( wc_get_container()->get( WebhookUtil::class )->get_legacy_webhooks_count() > 0 ) { self::add_custom_notice( 'legacy_webhooks_unsupported_in_woo_90', sprintf( '%s%s', sprintf( '

%s

', esc_html__( 'WooCommerce webhooks that use the Legacy REST API will be unsupported soon', 'woocommerce' ) ), sprintf( // translators: Placeholders are URLs. wpautop( wp_kses_data( __( 'The WooCommerce Legacy REST API will be removed in WooCommerce 9.0, and this will cause webhooks on this site that are configured to use the Legacy REST API to stop working. A separate WooCommerce extension will be available to allow these webhooks to keep using the Legacy REST API without interruption. You can also edit these webhooks to use the current REST API version to generate the payload instead. Learn more about this change.', 'woocommerce' ) ) ), admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks&legacy=true' ), 'https://developer.woo.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/' ) ) ); } } /** * Remove the admin notice about the removal of the Legacy REST API if the said API is disabled * or if the Legacy REST API extension is installed, and remove the notice about Legacy webhooks * if no such webhooks exist anymore or if the Legacy REST API extension is installed. * * TODO: Change this method in WooCommerce 9.0 so that the notice gets removed if the Legacy REST API extension is installed and active. */ private static function maybe_remove_legacy_api_removal_notice() { $plugin_is_active = is_plugin_active( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' ); if ( self::has_notice( 'legacy_api_removed_in_woo_90' ) && ( $plugin_is_active || 'yes' !== get_option( 'woocommerce_api_enabled' ) ) ) { self::remove_notice( 'legacy_api_removed_in_woo_90' ); } if ( self::has_notice( 'legacy_webhooks_unsupported_in_woo_90' ) && ( $plugin_is_active || 0 === wc_get_container()->get( WebhookUtil::class )->get_legacy_webhooks_count() ) ) { self::remove_notice( 'legacy_webhooks_unsupported_in_woo_90' ); } } // phpcs:enable Generic.Commenting.Todo.TaskFound /** * Show a notice. * * @param string $name Notice name. * @param bool $force_save Force saving inside this method instead of at the 'shutdown'. */ public static function add_notice( $name, $force_save = false ) { self::$notices = array_unique( array_merge( self::get_notices(), array( $name ) ) ); if ( $force_save ) { // Adding early save to prevent more race conditions with notices. self::store_notices(); } } /** * Remove a notice from being displayed. * * @param string $name Notice name. * @param bool $force_save Force saving inside this method instead of at the 'shutdown'. */ public static function remove_notice( $name, $force_save = false ) { self::$notices = array_diff( self::get_notices(), array( $name ) ); delete_option( 'woocommerce_admin_notice_' . $name ); if ( $force_save ) { // Adding early save to prevent more race conditions with notices. self::store_notices(); } } /** * See if a notice is being shown. * * @param string $name Notice name. * * @return boolean */ public static function has_notice( $name ) { return in_array( $name, self::get_notices(), true ); } /** * Hide a notice if the GET variable is set. */ public static function hide_notices() { if ( isset( $_GET['wc-hide-notice'] ) && isset( $_GET['_wc_notice_nonce'] ) ) { // WPCS: input var ok, CSRF ok. if ( ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_wc_notice_nonce'] ) ), 'woocommerce_hide_notices_nonce' ) ) { // WPCS: input var ok, CSRF ok. wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) ); } $notice_name = sanitize_text_field( wp_unslash( $_GET['wc-hide-notice'] ) ); // WPCS: input var ok, CSRF ok. /** * Filter the capability required to dismiss a given notice. * * @since 6.7.0 * * @param string $default_capability The default required capability. * @param string $notice_name The notice name. */ $required_capability = apply_filters( 'woocommerce_dismiss_admin_notice_capability', 'manage_woocommerce', $notice_name ); if ( ! current_user_can( $required_capability ) ) { wp_die( esc_html__( 'You don’t have permission to do this.', 'woocommerce' ) ); } self::hide_notice( $notice_name ); } } /** * Hide a single notice. * * @param string $name Notice name. */ private static function hide_notice( $name ) { self::remove_notice( $name ); update_user_meta( get_current_user_id(), 'dismissed_' . $name . '_notice', true ); do_action( 'woocommerce_hide_' . $name . '_notice' ); } /** * Add notices + styles if needed. */ public static function add_notices() { $notices = self::get_notices(); if ( empty( $notices ) ) { return; } $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; $show_on_screens = array( 'dashboard', 'plugins', ); // Notices should only show on WooCommerce screens, the main dashboard, and on the plugins screen. if ( ! in_array( $screen_id, wc_get_screen_ids(), true ) && ! in_array( $screen_id, $show_on_screens, true ) ) { return; } wp_enqueue_style( 'woocommerce-activation', plugins_url( '/assets/css/activation.css', WC_PLUGIN_FILE ), array(), Constants::get_constant( 'WC_VERSION' ) ); // Add RTL support. wp_style_add_data( 'woocommerce-activation', 'rtl', 'replace' ); foreach ( $notices as $notice ) { if ( ! empty( self::$core_notices[ $notice ] ) && apply_filters( 'woocommerce_show_admin_notice', true, $notice ) ) { add_action( 'admin_notices', array( __CLASS__, self::$core_notices[ $notice ] ) ); } else { add_action( 'admin_notices', array( __CLASS__, 'output_custom_notices' ) ); } } } /** * Add a custom notice. * * @param string $name Notice name. * @param string $notice_html Notice HTML. */ public static function add_custom_notice( $name, $notice_html ) { self::add_notice( $name ); update_option( 'woocommerce_admin_notice_' . $name, wp_kses_post( $notice_html ) ); } /** * Output any stored custom notices. */ public static function output_custom_notices() { $notices = self::get_notices(); if ( ! empty( $notices ) ) { foreach ( $notices as $notice ) { if ( empty( self::$core_notices[ $notice ] ) ) { $notice_html = get_option( 'woocommerce_admin_notice_' . $notice ); if ( $notice_html ) { include dirname( __FILE__ ) . '/views/html-notice-custom.php'; } } } } } /** * If we need to update the database, include a message with the DB update button. */ public static function update_notice() { $screen = get_current_screen(); $screen_id = $screen ? $screen->id : ''; if ( WC()->is_wc_admin_active() && in_array( $screen_id, wc_get_screen_ids(), true ) ) { return; } if ( WC_Install::needs_db_update() ) { $next_scheduled_date = WC()->queue()->get_next( 'woocommerce_run_update_callback', null, 'woocommerce-db-updates' ); if ( $next_scheduled_date || ! empty( $_GET['do_update_woocommerce'] ) ) { // WPCS: input var ok, CSRF ok. include dirname( __FILE__ ) . '/views/html-notice-updating.php'; } else { include dirname( __FILE__ ) . '/views/html-notice-update.php'; } } else { include dirname( __FILE__ ) . '/views/html-notice-updated.php'; } } /** * If we have just installed, show a message with the install pages button. * * @deprecated 4.6.0 */ public static function install_notice() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', esc_html__( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); } /** * Show a notice highlighting bad template files. */ public static function template_file_check_notice() { $core_templates = WC_Admin_Status::scan_template_files( WC()->plugin_path() . '/templates' ); $outdated = false; foreach ( $core_templates as $file ) { $theme_file = false; if ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { $theme_file = get_stylesheet_directory() . '/' . $file; } elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) { $theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file; } elseif ( file_exists( get_template_directory() . '/' . $file ) ) { $theme_file = get_template_directory() . '/' . $file; } elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) { $theme_file = get_template_directory() . '/' . WC()->template_path() . $file; } if ( false !== $theme_file ) { $core_version = WC_Admin_Status::get_file_version( WC()->plugin_path() . '/templates/' . $file ); $theme_version = WC_Admin_Status::get_file_version( $theme_file ); if ( $core_version && $theme_version && version_compare( $theme_version, $core_version, '<' ) ) { $outdated = true; break; } } } if ( $outdated ) { include dirname( __FILE__ ) . '/views/html-notice-template-check.php'; } else { self::remove_notice( 'template_files' ); } } /** * Show a notice asking users to convert to shipping zones. * * @todo remove in 4.0.0 */ public static function legacy_shipping_notice() { $maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' ); $enabled = false; foreach ( $maybe_load_legacy_methods as $method ) { $options = get_option( 'woocommerce_' . $method . '_settings' ); if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) { $enabled = true; } } if ( $enabled ) { include dirname( __FILE__ ) . '/views/html-notice-legacy-shipping.php'; } else { self::remove_notice( 'template_files' ); } } /** * No shipping methods. */ public static function no_shipping_methods_notice() { if ( wc_shipping_enabled() && ( empty( $_GET['page'] ) || empty( $_GET['tab'] ) || 'wc-settings' !== $_GET['page'] || 'shipping' !== $_GET['tab'] ) ) { // WPCS: input var ok, CSRF ok. $product_count = wp_count_posts( 'product' ); $method_count = wc_get_shipping_method_count(); if ( $product_count->publish > 0 && 0 === $method_count ) { include dirname( __FILE__ ) . '/views/html-notice-no-shipping-methods.php'; } if ( $method_count > 0 ) { self::remove_notice( 'no_shipping_methods' ); } } } /** * Notice shown when regenerating thumbnails background process is running. */ public static function regenerating_thumbnails_notice() { include dirname( __FILE__ ) . '/views/html-notice-regenerating-thumbnails.php'; } /** * Notice about secure connection. */ public static function secure_connection_notice() { if ( self::is_ssl() || get_user_meta( get_current_user_id(), 'dismissed_no_secure_connection_notice', true ) ) { return; } include dirname( __FILE__ ) . '/views/html-notice-secure-connection.php'; } /** * Notice shown when regenerating thumbnails background process is running. * * @since 3.6.0 */ public static function regenerating_lookup_table_notice() { // See if this is still relevant. if ( ! wc_update_product_lookup_tables_is_running() ) { self::remove_notice( 'regenerating_lookup_table' ); return; } include dirname( __FILE__ ) . '/views/html-notice-regenerating-lookup-table.php'; } /** * Add notice about minimum PHP and WordPress requirement. * * @since 3.6.5 */ public static function add_min_version_notice() { if ( version_compare( phpversion(), WC_NOTICE_MIN_PHP_VERSION, '<' ) || version_compare( get_bloginfo( 'version' ), WC_NOTICE_MIN_WP_VERSION, '<' ) ) { self::add_notice( WC_PHP_MIN_REQUIREMENTS_NOTICE ); } } /** * Notice about WordPress and PHP minimum requirements. * * @deprecated 8.2.0 WordPress and PHP minimum requirements notices are no longer shown. * * @since 3.6.5 * @return void */ public static function wp_php_min_requirements_notice() { } /** * Add MaxMind missing license key notice. * * @since 3.9.0 */ public static function add_maxmind_missing_license_key_notice() { $default_address = get_option( 'woocommerce_default_customer_address' ); if ( ! in_array( $default_address, array( 'geolocation', 'geolocation_ajax' ), true ) ) { return; } $integration_options = get_option( 'woocommerce_maxmind_geolocation_settings' ); if ( empty( $integration_options['license_key'] ) ) { self::add_notice( 'maxmind_license_key' ); } } /** * Add notice about Redirect-only download method, nudging user to switch to a different method instead. */ public static function add_redirect_download_method_notice() { if ( 'redirect' === get_option( 'woocommerce_file_download_method' ) ) { self::add_notice( 'redirect_download_method' ); } else { self::remove_notice( 'redirect_download_method' ); } } /** * Notice about the completion of the product downloads sync, with further advice for the site operator. */ public static function download_directories_sync_complete() { $notice_dismissed = apply_filters( 'woocommerce_hide_download_directories_sync_complete', get_user_meta( get_current_user_id(), 'download_directories_sync_complete', true ) ); if ( $notice_dismissed ) { self::remove_notice( 'download_directories_sync_complete' ); } if ( Users::is_site_administrator() ) { include __DIR__ . '/views/html-notice-download-dir-sync-complete.php'; } } /** * Display MaxMind missing license key notice. * * @since 3.9.0 */ public static function maxmind_missing_license_key_notice() { $user_dismissed_notice = get_user_meta( get_current_user_id(), 'dismissed_maxmind_license_key_notice', true ); $filter_dismissed_notice = ! apply_filters( 'woocommerce_maxmind_geolocation_display_notices', true ); if ( $user_dismissed_notice || $filter_dismissed_notice ) { self::remove_notice( 'maxmind_license_key' ); return; } include dirname( __FILE__ ) . '/views/html-notice-maxmind-license-key.php'; } /** * Notice about Redirect-Only download method. * * @since 4.0 */ public static function redirect_download_method_notice() { if ( apply_filters( 'woocommerce_hide_redirect_method_nag', get_user_meta( get_current_user_id(), 'dismissed_redirect_download_method_notice', true ) ) ) { self::remove_notice( 'redirect_download_method' ); return; } include dirname( __FILE__ ) . '/views/html-notice-redirect-only-download.php'; } /** * Notice about uploads directory begin unprotected. * * @since 4.2.0 */ public static function uploads_directory_is_unprotected_notice() { if ( get_user_meta( get_current_user_id(), 'dismissed_uploads_directory_is_unprotected_notice', true ) || self::is_uploads_directory_protected() ) { self::remove_notice( 'uploads_directory_is_unprotected' ); return; } include dirname( __FILE__ ) . '/views/html-notice-uploads-directory-is-unprotected.php'; } /** * Notice about base tables missing. */ public static function base_tables_missing_notice() { $notice_dismissed = apply_filters( 'woocommerce_hide_base_tables_missing_nag', get_user_meta( get_current_user_id(), 'dismissed_base_tables_missing_notice', true ) ); if ( $notice_dismissed ) { self::remove_notice( 'base_tables_missing' ); } include dirname( __FILE__ ) . '/views/html-notice-base-table-missing.php'; } /** * Determine if the store is running SSL. * * @return bool Flag SSL enabled. * @since 3.5.1 */ protected static function is_ssl() { $shop_page = wc_get_page_permalink( 'shop' ); return ( is_ssl() && 'https' === substr( $shop_page, 0, 5 ) ); } /** * Wrapper for is_plugin_active. * * @param string $plugin Plugin to check. * @return boolean */ protected static function is_plugin_active( $plugin ) { if ( ! function_exists( 'is_plugin_active' ) ) { include_once ABSPATH . 'wp-admin/includes/plugin.php'; } return is_plugin_active( $plugin ); } /** * Simplify Commerce is no longer in core. * * @deprecated 3.6.0 No longer shown. */ public static function simplify_commerce_notice() { wc_deprecated_function( 'WC_Admin_Notices::simplify_commerce_notice', '3.6.0' ); } /** * Show the Theme Check notice. * * @deprecated 3.3.0 No longer shown. */ public static function theme_check_notice() { wc_deprecated_function( 'WC_Admin_Notices::theme_check_notice', '3.3.0' ); } /** * Check if uploads directory is protected. * * @since 4.2.0 * @return bool */ protected static function is_uploads_directory_protected() { $cache_key = '_woocommerce_upload_directory_status'; $status = get_transient( $cache_key ); // Check for cache. if ( false !== $status ) { return 'protected' === $status; } // Get only data from the uploads directory. $uploads = wp_get_upload_dir(); // Check for the "uploads/woocommerce_uploads" directory. $response = wp_safe_remote_get( esc_url_raw( $uploads['baseurl'] . '/woocommerce_uploads/' ), array( 'redirection' => 0, ) ); $response_code = intval( wp_remote_retrieve_response_code( $response ) ); $response_content = wp_remote_retrieve_body( $response ); // Check if returns 200 with empty content in case can open an index.html file, // and check for non-200 codes in case the directory is protected. $is_protected = ( 200 === $response_code && empty( $response_content ) ) || ( 200 !== $response_code ); set_transient( $cache_key, $is_protected ? 'protected' : 'unprotected', 1 * DAY_IN_SECONDS ); return $is_protected; } } WC_Admin_Notices::init(); PKK[aüƒ“ƒ“!admin/class-wc-admin-settings.phpnu„[µü¤query->init_query_vars(); WC()->query->add_endpoints(); do_action( 'woocommerce_settings_saved' ); } /** * Add a message. * * @param string $text Message. */ public static function add_message( $text ) { self::$messages[] = $text; } /** * Add an error. * * @param string $text Message. */ public static function add_error( $text ) { self::$errors[] = $text; } /** * Output messages + errors. */ public static function show_messages() { if ( count( self::$errors ) > 0 ) { foreach ( self::$errors as $error ) { echo '

' . esc_html( $error ) . '

'; } } elseif ( count( self::$messages ) > 0 ) { foreach ( self::$messages as $message ) { echo '

' . esc_html( $message ) . '

'; } } } /** * Settings page. * * Handles the display of the main woocommerce settings page in admin. */ public static function output() { global $current_section, $current_tab; $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; do_action( 'woocommerce_settings_start' ); wp_enqueue_script( 'woocommerce_settings', WC()->plugin_url() . '/assets/js/admin/settings' . $suffix . '.js', array( 'jquery', 'wp-util', 'jquery-ui-datepicker', 'jquery-ui-sortable', 'iris', 'selectWoo' ), WC()->version, true ); wp_localize_script( 'woocommerce_settings', 'woocommerce_settings_params', array( 'i18n_nav_warning' => __( 'The changes you made will be lost if you navigate away from this page.', 'woocommerce' ), 'i18n_moved_up' => __( 'Item moved up', 'woocommerce' ), 'i18n_moved_down' => __( 'Item moved down', 'woocommerce' ), 'i18n_no_specific_countries_selected' => __( 'Selecting no country / region to sell to prevents from completing the checkout. Continue anyway?', 'woocommerce' ), ) ); // Get tabs for the settings page. $tabs = apply_filters( 'woocommerce_settings_tabs_array', array() ); include dirname( __FILE__ ) . '/views/html-admin-settings.php'; } /** * Get a setting from the settings API. * * @param string $option_name Option name. * @param mixed $default Default value. * @return mixed */ public static function get_option( $option_name, $default = '' ) { if ( ! $option_name ) { return $default; } // Array value. if ( strstr( $option_name, '[' ) ) { parse_str( $option_name, $option_array ); // Option name is first key. $option_name = current( array_keys( $option_array ) ); // Get value. $option_values = get_option( $option_name, '' ); $key = key( $option_array[ $option_name ] ); if ( isset( $option_values[ $key ] ) ) { $option_value = $option_values[ $key ]; } else { $option_value = null; } } else { // Single value. $option_value = get_option( $option_name, null ); } if ( is_array( $option_value ) ) { $option_value = wp_unslash( $option_value ); } elseif ( ! is_null( $option_value ) ) { $option_value = stripslashes( $option_value ); } return ( null === $option_value ) ? $default : $option_value; } /** * Output admin fields. * * Loops through the woocommerce options array and outputs each field. * * @param array[] $options Opens array to output. */ public static function output_fields( $options ) { foreach ( $options as $value ) { if ( ! isset( $value['type'] ) ) { continue; } if ( ! isset( $value['id'] ) ) { $value['id'] = ''; } // The 'field_name' key can be used when it is useful to specify an input field name that is different // from the input field ID. We use the key 'field_name' because 'name' is already in use for a different // purpose. if ( ! isset( $value['field_name'] ) ) { $value['field_name'] = $value['id']; } if ( ! isset( $value['title'] ) ) { $value['title'] = isset( $value['name'] ) ? $value['name'] : ''; } if ( ! isset( $value['class'] ) ) { $value['class'] = ''; } if ( ! isset( $value['css'] ) ) { $value['css'] = ''; } if ( ! isset( $value['default'] ) ) { $value['default'] = ''; } if ( ! isset( $value['desc'] ) ) { $value['desc'] = ''; } if ( ! isset( $value['desc_tip'] ) ) { $value['desc_tip'] = false; } if ( ! isset( $value['placeholder'] ) ) { $value['placeholder'] = ''; } if ( ! isset( $value['row_class'] ) ) { $value['row_class'] = ''; } if ( ! empty( $value['row_class'] ) && substr( $value['row_class'], 0, 16 ) !== 'wc-settings-row-' ) { $value['row_class'] = 'wc-settings-row-' . $value['row_class']; } if ( ! isset( $value['suffix'] ) ) { $value['suffix'] = ''; } if ( ! isset( $value['value'] ) ) { $value['value'] = self::get_option( $value['id'], $value['default'] ); } // Custom attribute handling. $custom_attributes = array(); if ( ! empty( $value['custom_attributes'] ) && is_array( $value['custom_attributes'] ) ) { foreach ( $value['custom_attributes'] as $attribute => $attribute_value ) { $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"'; } } // Description handling. $field_description = self::get_field_description( $value ); $description = $field_description['description']; $tooltip_html = $field_description['tooltip_html']; // Switch based on type. switch ( $value['type'] ) { // Section Titles. case 'title': if ( ! empty( $value['title'] ) ) { echo '

' . esc_html( $value['title'] ) . '

'; } if ( ! empty( $value['desc'] ) ) { echo '
'; echo wp_kses_post( wpautop( wptexturize( $value['desc'] ) ) ); echo '
'; } echo '' . "\n\n"; if ( ! empty( $value['id'] ) ) { do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) ); } break; case 'info': ?>"> '; break; // Section Ends. case 'sectionend': if ( ! empty( $value['id'] ) ) { do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) . '_end' ); } echo '
'; if ( ! empty( $value['id'] ) ) { do_action( 'woocommerce_settings_' . sanitize_title( $value['id'] ) . '_after' ); } break; // Standard text inputs and subtypes like 'number'. case 'text': case 'password': case 'datetime': case 'datetime-local': case 'date': case 'month': case 'time': case 'week': case 'number': case 'email': case 'url': case 'tel': $option_value = $value['value']; ?>"> /> ">   />‎ "> "> ">
' . esc_html__( 'The settings of this image size have been disabled because its values are being overwritten by a filter.', 'woocommerce' ) . '

'; } ?> "> id="-width" type="text" size="3" value="" /> × id="-height" type="text" size="3" value="" />px $value['field_name'], 'id' => $value['id'], 'sort_column' => 'menu_order', 'sort_order' => 'ASC', 'show_option_none' => ' ', 'class' => $value['class'], 'echo' => false, 'selected' => absint( $value['value'] ), 'post_status' => 'publish,private,draft', ); if ( isset( $value['args'] ) ) { $args = wp_parse_args( $value['args'], $args ); } ?> "> post_title, $option_value ); } ?> "> "> countries->countries; } asort( $countries ); ?> ">
__( 'Day(s)', 'woocommerce' ), 'weeks' => __( 'Week(s)', 'woocommerce' ), 'months' => __( 'Month(s)', 'woocommerce' ), 'years' => __( 'Year(s)', 'woocommerce' ), ); $option_value = wc_parse_relative_date_option( $value['value'] ); ?> "> />  ' . wp_kses_post( $description ) . '

'; } elseif ( $description && in_array( $value['type'], array( 'checkbox' ), true ) ) { $description = wp_kses_post( $description ); } elseif ( $description ) { $description = '

' . wp_kses_post( $description ) . '

'; } if ( $tooltip_html && in_array( $value['type'], array( 'checkbox' ), true ) ) { $tooltip_html = '

' . $tooltip_html . '

'; } elseif ( $tooltip_html ) { $tooltip_html = wc_help_tip( $tooltip_html ); } return array( 'description' => $description, 'tooltip_html' => $tooltip_html, ); } /** * Save admin fields. * * Loops through the woocommerce options array and outputs each field. * * @param array $options Options array to output. * @param array $data Optional. Data to use for saving. Defaults to $_POST. * @return bool */ public static function save_fields( $options, $data = null ) { if ( is_null( $data ) ) { $data = $_POST; // WPCS: input var okay, CSRF ok. } if ( empty( $data ) ) { return false; } // Options to update will be stored here and saved later. $update_options = array(); $autoload_options = array(); // Loop options and get values to save. foreach ( $options as $option ) { if ( ! isset( $option['id'] ) || ! isset( $option['type'] ) || ( isset( $option['is_option'] ) && false === $option['is_option'] ) ) { continue; } $option_name = $option['field_name'] ?? $option['id']; // Get posted value. if ( strstr( $option_name, '[' ) ) { parse_str( $option_name, $option_name_array ); $option_name = current( array_keys( $option_name_array ) ); $setting_name = key( $option_name_array[ $option_name ] ); $raw_value = isset( $data[ $option_name ][ $setting_name ] ) ? wp_unslash( $data[ $option_name ][ $setting_name ] ) : null; } else { $setting_name = ''; $raw_value = isset( $data[ $option_name ] ) ? wp_unslash( $data[ $option_name ] ) : null; } // Format the value based on option type. switch ( $option['type'] ) { case 'checkbox': $value = '1' === $raw_value || 'yes' === $raw_value ? 'yes' : 'no'; break; case 'textarea': $value = wp_kses_post( trim( $raw_value ) ); break; case 'multiselect': case 'multi_select_countries': $value = array_filter( array_map( 'wc_clean', (array) $raw_value ) ); break; case 'image_width': $value = array(); if ( isset( $raw_value['width'] ) ) { $value['width'] = wc_clean( $raw_value['width'] ); $value['height'] = wc_clean( $raw_value['height'] ); $value['crop'] = isset( $raw_value['crop'] ) ? 1 : 0; } else { $value['width'] = $option['default']['width']; $value['height'] = $option['default']['height']; $value['crop'] = $option['default']['crop']; } break; case 'select': $allowed_values = empty( $option['options'] ) ? array() : array_map( 'strval', array_keys( $option['options'] ) ); if ( empty( $option['default'] ) && empty( $allowed_values ) ) { $value = null; break; } $default = ( empty( $option['default'] ) ? $allowed_values[0] : $option['default'] ); $value = in_array( $raw_value, $allowed_values, true ) ? $raw_value : $default; break; case 'relative_date_selector': $value = wc_parse_relative_date_option( $raw_value ); break; default: $value = wc_clean( $raw_value ); break; } /** * Fire an action when a certain 'type' of field is being saved. * * @deprecated 2.4.0 - doesn't allow manipulation of values! */ if ( has_action( 'woocommerce_update_option_' . sanitize_title( $option['type'] ) ) ) { wc_deprecated_function( 'The woocommerce_update_option_X action', '2.4.0', 'woocommerce_admin_settings_sanitize_option filter' ); do_action( 'woocommerce_update_option_' . sanitize_title( $option['type'] ), $option ); continue; } /** * Sanitize the value of an option. * * @since 2.4.0 */ $value = apply_filters( 'woocommerce_admin_settings_sanitize_option', $value, $option, $raw_value ); /** * Sanitize the value of an option by option name. * * @since 2.4.0 */ $value = apply_filters( "woocommerce_admin_settings_sanitize_option_$option_name", $value, $option, $raw_value ); if ( is_null( $value ) ) { continue; } // Check if option is an array and handle that differently to single values. if ( $option_name && $setting_name ) { if ( ! isset( $update_options[ $option_name ] ) ) { $update_options[ $option_name ] = get_option( $option_name, array() ); } if ( ! is_array( $update_options[ $option_name ] ) ) { $update_options[ $option_name ] = array(); } $update_options[ $option_name ][ $setting_name ] = $value; } else { $update_options[ $option_name ] = $value; } $autoload_options[ $option_name ] = isset( $option['autoload'] ) ? (bool) $option['autoload'] : true; /** * Fire an action before saved. * * @deprecated 2.4.0 - doesn't allow manipulation of values! */ do_action( 'woocommerce_update_option', $option ); } // Save all options in our array. foreach ( $update_options as $name => $value ) { update_option( $name, $value, $autoload_options[ $name ] ? 'yes' : 'no' ); } return true; } /** * Checks which method we're using to serve downloads. * * If using force or x-sendfile, this ensures the .htaccess is in place. */ public static function check_download_folder_protection() { $upload_dir = wp_get_upload_dir(); $downloads_path = $upload_dir['basedir'] . '/woocommerce_uploads'; $download_method = get_option( 'woocommerce_file_download_method' ); $file_path = $downloads_path . '/.htaccess'; $file_content = 'redirect' === $download_method ? 'Options -Indexes' : 'deny from all'; $create = false; if ( wp_mkdir_p( $downloads_path ) && ! file_exists( $file_path ) ) { $create = true; } else { $current_content = @file_get_contents( $file_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents if ( $current_content !== $file_content ) { unlink( $file_path ); $create = true; } } if ( $create ) { $file_handle = @fopen( $file_path, 'wb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen if ( $file_handle ) { fwrite( $file_handle, $file_content ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite fclose( $file_handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose } } } } endif; PKK[alÝ;admin/class-wc-admin-help.phpnu„[µü¤id, wc_get_screen_ids() ) ) { return; } $screen->add_help_tab( array( 'id' => 'woocommerce_support_tab', 'title' => __( 'Help & Support', 'woocommerce' ), 'content' => '

' . __( 'Help & Support', 'woocommerce' ) . '

' . '

' . sprintf( /* translators: %s: Documentation URL */ __( 'Should you need help understanding, using, or extending WooCommerce, please read our documentation. You will find all kinds of resources including snippets, tutorials and much more.', 'woocommerce' ), 'https://woo.com/documentation/plugins/woocommerce/?utm_source=helptab&utm_medium=product&utm_content=docs&utm_campaign=woocommerceplugin' ) . '

' . '

' . sprintf( /* translators: %s: Forum URL */ __( 'For further assistance with WooCommerce core, use the community forum. For help with premium extensions sold on Woo.com, open a support request at Woo.com.', 'woocommerce' ), 'https://wordpress.org/support/plugin/woocommerce', 'https://woo.com/my-account/create-a-ticket/?utm_source=helptab&utm_medium=product&utm_content=tickets&utm_campaign=woocommerceplugin' ) . '

' . '

' . __( 'Before asking for help, we recommend checking the system status page to identify any problems with your configuration.', 'woocommerce' ) . '

' . '

' . __( 'System status', 'woocommerce' ) . ' ' . __( 'Community forum', 'woocommerce' ) . ' ' . __( 'Woo.com support', 'woocommerce' ) . '

', ) ); $screen->add_help_tab( array( 'id' => 'woocommerce_bugs_tab', 'title' => __( 'Found a bug?', 'woocommerce' ), 'content' => '

' . __( 'Found a bug?', 'woocommerce' ) . '

' . /* translators: 1: GitHub issues URL 2: GitHub contribution guide URL 3: System status report URL */ '

' . sprintf( __( 'If you find a bug within WooCommerce core you can create a ticket via GitHub issues. Ensure you read the contribution guide prior to submitting your report. To help us solve your issue, please be as descriptive as possible and include your system status report.', 'woocommerce' ), 'https://github.com/woocommerce/woocommerce/issues?state=open', 'https://github.com/woocommerce/woocommerce/blob/trunk/.github/CONTRIBUTING.md', admin_url( 'admin.php?page=wc-status' ) ) . '

' . '

' . __( 'Report a bug', 'woocommerce' ) . ' ' . __( 'System status', 'woocommerce' ) . '

', ) ); $screen->set_help_sidebar( '

' . __( 'For more information:', 'woocommerce' ) . '

' . '

' . __( 'About WooCommerce', 'woocommerce' ) . '

' . '

' . __( 'WordPress.org project', 'woocommerce' ) . '

' . '

' . __( 'GitHub project', 'woocommerce' ) . '

' . '

' . __( 'Official themes', 'woocommerce' ) . '

' . '

' . __( 'Official extensions', 'woocommerce' ) . '

' ); } } return new WC_Admin_Help(); PKK[ùìiu´V´V%admin/class-wc-admin-setup-wizard.phpnu„[µü¤countries->get_base_country(); // https://developers.taxjar.com/api/reference/#countries . $tax_supported_countries = array_merge( array( 'US', 'CA', 'AU', 'GB' ), WC()->countries->get_european_union_countries() ); return in_array( $country_code, $tax_supported_countries, true ); } /** * Should we show the MailChimp install option? * True only if the user can install plugins. * * @deprecated 4.6.0 * @return boolean */ protected function should_show_mailchimp() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); return current_user_can( 'install_plugins' ); } /** * Should we show the Facebook install option? * True only if the user can install plugins, * and up until the end date of the recommendation. * * @deprecated 4.6.0 * @return boolean */ protected function should_show_facebook() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); return current_user_can( 'install_plugins' ); } /** * Is the WooCommerce Admin actively included in the WooCommerce core? * Based on presence of a basic WC Admin function. * * @deprecated 4.6.0 * @return boolean */ protected function is_wc_admin_active() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); return function_exists( 'wc_admin_url' ); } /** * Should we show the WooCommerce Admin install option? * True only if the user can install plugins, * and is running the correct version of WordPress. * * @see WC_Admin_Setup_Wizard::$wc_admin_plugin_minimum_wordpress_version * * @deprecated 4.6.0 * @return boolean */ protected function should_show_wc_admin() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $wordpress_minimum_met = version_compare( get_bloginfo( 'version' ), $this->wc_admin_plugin_minimum_wordpress_version, '>=' ); return current_user_can( 'install_plugins' ) && $wordpress_minimum_met && ! $this->is_wc_admin_active(); } /** * Should we show the new WooCommerce Admin onboarding experience? * * @deprecated 4.6.0 * @return boolean */ protected function should_show_wc_admin_onboarding() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); // As of WooCommerce 4.1, all new sites should use the latest OBW from wc-admin package. // This filter will allow for forcing the old wizard while we migrate e2e tests. return ! apply_filters( 'woocommerce_setup_wizard_force_legacy', false ); } /** * Should we display the 'Recommended' step? * True if at least one of the recommendations will be displayed. * * @deprecated 4.6.0 * @return boolean */ protected function should_show_recommended_step() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); return $this->should_show_theme() || $this->should_show_automated_tax() || $this->should_show_mailchimp() || $this->should_show_facebook() || $this->should_show_wc_admin(); } /** * Register/enqueue scripts and styles for the Setup Wizard. * * Hooked onto 'admin_enqueue_scripts'. * * @deprecated 4.6.0 */ public function enqueue_scripts() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); } /** * Show the setup wizard. * * @deprecated 4.6.0 */ public function setup_wizard() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); if ( empty( $_GET['page'] ) || 'wc-setup' !== $_GET['page'] ) { // WPCS: CSRF ok, input var ok. return; } $default_steps = array( 'new_onboarding' => array( 'name' => '', 'view' => array( $this, 'wc_setup_new_onboarding' ), 'handler' => array( $this, 'wc_setup_new_onboarding_save' ), ), 'store_setup' => array( 'name' => __( 'Store setup', 'woocommerce' ), 'view' => array( $this, 'wc_setup_store_setup' ), 'handler' => array( $this, 'wc_setup_store_setup_save' ), ), 'payment' => array( 'name' => __( 'Payment', 'woocommerce' ), 'view' => array( $this, 'wc_setup_payment' ), 'handler' => array( $this, 'wc_setup_payment_save' ), ), 'shipping' => array( 'name' => __( 'Shipping', 'woocommerce' ), 'view' => array( $this, 'wc_setup_shipping' ), 'handler' => array( $this, 'wc_setup_shipping_save' ), ), 'recommended' => array( 'name' => __( 'Recommended', 'woocommerce' ), 'view' => array( $this, 'wc_setup_recommended' ), 'handler' => array( $this, 'wc_setup_recommended_save' ), ), 'activate' => array( 'name' => __( 'Activate', 'woocommerce' ), 'view' => array( $this, 'wc_setup_activate' ), 'handler' => array( $this, 'wc_setup_activate_save' ), ), 'next_steps' => array( 'name' => __( 'Ready!', 'woocommerce' ), 'view' => array( $this, 'wc_setup_ready' ), 'handler' => '', ), ); // Hide the new/improved onboarding experience screen if the user is not part of the a/b test. if ( ! $this->should_show_wc_admin_onboarding() ) { unset( $default_steps['new_onboarding'] ); } // Hide recommended step if nothing is going to be shown there. if ( ! $this->should_show_recommended_step() ) { unset( $default_steps['recommended'] ); } // Hide shipping step if the store is selling digital products only. if ( 'virtual' === get_option( 'woocommerce_product_type' ) ) { unset( $default_steps['shipping'] ); } // Hide activate section when the user does not have capabilities to install plugins, think multiside admins not being a super admin. if ( ! current_user_can( 'install_plugins' ) ) { unset( $default_steps['activate'] ); } $this->steps = apply_filters( 'woocommerce_setup_wizard_steps', $default_steps ); $this->step = isset( $_GET['step'] ) ? sanitize_key( $_GET['step'] ) : current( array_keys( $this->steps ) ); // WPCS: CSRF ok, input var ok. // @codingStandardsIgnoreStart if ( ! empty( $_POST['save_step'] ) && isset( $this->steps[ $this->step ]['handler'] ) ) { call_user_func( $this->steps[ $this->step ]['handler'], $this ); } // @codingStandardsIgnoreEnd ob_start(); $this->setup_wizard_header(); $this->setup_wizard_steps(); $this->setup_wizard_content(); $this->setup_wizard_footer(); exit; } /** * Get the URL for the next step's screen. * * @param string $step slug (default: current step). * @return string URL for next step if a next step exists. * Admin URL if it's the last step. * Empty string on failure. * * @deprecated 4.6.0 * @since 3.0.0 */ public function get_next_step_link( $step = '' ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); if ( ! $step ) { $step = $this->step; } $keys = array_keys( $this->steps ); if ( end( $keys ) === $step ) { return admin_url(); } $step_index = array_search( $step, $keys, true ); if ( false === $step_index ) { return ''; } return add_query_arg( 'step', $keys[ $step_index + 1 ], remove_query_arg( 'activate_error' ) ); } /** * Setup Wizard Header. * * @deprecated 4.6.0 */ public function setup_wizard_header() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); // same as default WP from wp-admin/admin-header.php. $wp_version_class = 'branch-' . str_replace( array( '.', ',' ), '-', floatval( get_bloginfo( 'version' ) ) ); set_current_screen(); ?> > <?php esc_html_e( 'WooCommerce › Setup Wizard', 'woocommerce' ); ?>

<?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?>

step; ?> steps; $selected_features = array_filter( $this->wc_setup_activate_get_feature_list() ); // Hide the activate step if Jetpack is already active, unless WooCommerce Services // features are selected, or unless the Activate step was already taken. if ( class_exists( 'Jetpack' ) && Jetpack::is_active() && empty( $selected_features ) && 'yes' !== get_transient( 'wc_setup_activated' ) ) { unset( $output_steps['activate'] ); } unset( $output_steps['new_onboarding'] ); ?>
    $step ) { $is_completed = array_search( $this->step, array_keys( $this->steps ), true ) > array_search( $step_key, array_keys( $this->steps ), true ); if ( $step_key === $this->step ) { ?>
'; if ( ! empty( $this->steps[ $this->step ]['view'] ) ) { call_user_func( $this->steps[ $this->step ]['view'], $this ); } echo ''; } /** * Display's a prompt for users to try out the new improved WooCommerce onboarding experience in WooCommerce Admin. * * @deprecated 4.6.0 */ public function wc_setup_new_onboarding() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); ?>

<?php esc_attr_e( 'WooCommerce', 'woocommerce' ); ?>

is_wc_admin_active() ) : ?>

countries->get_base_address(); $address_2 = WC()->countries->get_base_address_2(); $city = WC()->countries->get_base_city(); $state = WC()->countries->get_base_state(); $country = WC()->countries->get_base_country(); $postcode = WC()->countries->get_base_postcode(); $currency = get_option( 'woocommerce_currency', 'USD' ); $product_type = get_option( 'woocommerce_product_type', 'both' ); $sell_in_person = get_option( 'woocommerce_sell_in_person', 'none_selected' ); if ( empty( $country ) ) { $user_location = WC_Geolocation::geolocate_ip(); $country = $user_location['country']; $state = $user_location['state']; } $locale_info = include WC()->plugin_path() . '/i18n/locale-info.php'; $currency_by_country = wp_list_pluck( $locale_info, 'currency_code' ); ?>

/>
/> tracking_modal(); ?>

close_http_connection(); foreach ( $this->deferred_actions as $action ) { $action['func']( ...$action['args'] ); // Clear the background installation flag if this is a plugin. if ( isset( $action['func'][1] ) && 'background_installer' === $action['func'][1] && isset( $action['args'][0] ) ) { delete_option( 'woocommerce_setup_background_installing_' . $action['args'][0] ); } } } /** * Helper method to queue the background install of a plugin. * * @param string $plugin_id Plugin id used for background install. * @param array $plugin_info Plugin info array containing name and repo-slug, and optionally file if different from [repo-slug].php. * * @deprecated 4.6.0 */ protected function install_plugin( $plugin_id, $plugin_info ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); // Make sure we don't trigger multiple simultaneous installs. if ( get_option( 'woocommerce_setup_background_installing_' . $plugin_id ) ) { return; } $plugin_file = isset( $plugin_info['file'] ) ? $plugin_info['file'] : $plugin_info['repo-slug'] . '.php'; if ( is_plugin_active( $plugin_info['repo-slug'] . '/' . $plugin_file ) ) { return; } if ( empty( $this->deferred_actions ) ) { add_action( 'shutdown', array( $this, 'run_deferred_actions' ) ); } array_push( $this->deferred_actions, array( 'func' => array( 'WC_Install', 'background_installer' ), 'args' => array( $plugin_id, $plugin_info ), ) ); // Set the background installation flag for this plugin. update_option( 'woocommerce_setup_background_installing_' . $plugin_id, true ); } /** * Helper method to queue the background install of a theme. * * @param string $theme_id Theme id used for background install. * * @deprecated 4.6.0 */ protected function install_theme( $theme_id ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); if ( empty( $this->deferred_actions ) ) { add_action( 'shutdown', array( $this, 'run_deferred_actions' ) ); } array_push( $this->deferred_actions, array( 'func' => array( 'WC_Install', 'theme_background_installer' ), 'args' => array( $theme_id ), ) ); } /** * Helper method to install Jetpack. * * @deprecated 4.6.0 */ protected function install_jetpack() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $this->install_plugin( 'jetpack', array( 'name' => __( 'Jetpack', 'woocommerce' ), 'repo-slug' => 'jetpack', ) ); } /** * Helper method to install WooCommerce Services and its Jetpack dependency. * * @deprecated 4.6.0 */ protected function install_woocommerce_services() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $this->install_jetpack(); $this->install_plugin( 'woocommerce-services', array( 'name' => __( 'WooCommerce Services', 'woocommerce' ), 'repo-slug' => 'woocommerce-services', ) ); } /** * Retrieve info for missing WooCommerce Services and/or Jetpack plugin. * * @deprecated 4.6.0 * @return array */ protected function get_wcs_requisite_plugins() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $plugins = array(); if ( ! is_plugin_active( 'woocommerce-services/woocommerce-services.php' ) && ! get_option( 'woocommerce_setup_background_installing_woocommerce-services' ) ) { $plugins[] = array( 'name' => __( 'WooCommerce Services', 'woocommerce' ), 'slug' => 'woocommerce-services', ); } if ( ! is_plugin_active( 'jetpack/jetpack.php' ) && ! get_option( 'woocommerce_setup_background_installing_jetpack' ) ) { $plugins[] = array( 'name' => __( 'Jetpack', 'woocommerce' ), 'slug' => 'jetpack', ); } return $plugins; } /** * Plugin install info message markup with heading. * * @deprecated 4.6.0 */ public function plugin_install_info() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); ?> array( 'name' => __( 'Flat Rate', 'woocommerce' ), 'description' => __( 'Set a fixed price to cover shipping costs.', 'woocommerce' ), 'settings' => array( 'cost' => array( 'type' => 'text', 'default_value' => __( 'Cost', 'woocommerce' ), 'description' => __( 'What would you like to charge for flat rate shipping?', 'woocommerce' ), 'required' => true, ), ), ), 'free_shipping' => array( 'name' => __( 'Free Shipping', 'woocommerce' ), 'description' => __( "Don't charge for shipping.", 'woocommerce' ), ), ); return $shipping_methods; } /** * Render the available shipping methods for a given country code. * * @param string $country_code Country code. * @param string $currency_code Currency code. * @param string $input_prefix Input prefix. * * @deprecated 4.6.0 */ protected function shipping_method_selection_form( $country_code, $currency_code, $input_prefix ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $selected = 'flat_rate'; $shipping_methods = $this->get_wizard_shipping_methods( $country_code, $currency_code ); ?>
$method ) : ?>

$method ) : ?>
$setting ) : ?> />

countries->get_base_country(); $country_name = WC()->countries->countries[ $country_code ]; $prefixed_country_name = WC()->countries->estimated_for_prefix( $country_code ) . $country_name; $currency_code = get_woocommerce_currency(); $existing_zones = WC_Shipping_Zones::get_zones(); $intro_text = ''; if ( empty( $existing_zones ) ) { $intro_text = sprintf( /* translators: %s: country name including the 'the' prefix if needed */ __( "We've created two Shipping Zones - for %s and for the rest of the world. Below you can set Flat Rate shipping costs for these Zones or offer Free Shipping.", 'woocommerce' ), $prefixed_country_name ); } $is_wcs_labels_supported = $this->is_wcs_shipping_labels_supported_country( $country_code ); $is_shipstation_supported = $this->is_shipstation_supported_country( $country_code ); ?>

get_product_weight_selection(), $this->get_product_dimension_selection() ), array( 'span' => array( 'class' => array(), ), 'select' => array( 'id' => array(), 'name' => array(), 'class' => array(), ), 'option' => array( 'value' => array(), 'selected' => array(), ), ) ); ?>

plugin_install_info(); ?>

user_email; return $user_email; } /** * Array of all possible "in cart" gateways that can be offered. * * @deprecated 4.6.0 * @return array */ protected function get_wizard_available_in_cart_payment_gateways() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $user_email = $this->get_current_user_email(); $stripe_description = '

' . sprintf( /* translators: %s: URL */ __( 'Accept debit and credit cards in 135+ currencies, methods such as Alipay, and one-touch checkout with Apple Pay. Learn more.', 'woocommerce' ), 'https://woo.com/products/stripe/' ) . '

'; $paypal_checkout_description = '

' . sprintf( /* translators: %s: URL */ __( 'Safe and secure payments using credit cards or your customer\'s PayPal account. Learn more.', 'woocommerce' ), 'https://woo.com/products/woocommerce-gateway-paypal-checkout/' ) . '

'; $klarna_checkout_description = '

' . sprintf( /* translators: %s: URL */ __( 'Full checkout experience with pay now, pay later and slice it. No credit card numbers, no passwords, no worries. Learn more about Klarna.', 'woocommerce' ), 'https://woo.com/products/klarna-checkout/' ) . '

'; $klarna_payments_description = '

' . sprintf( /* translators: %s: URL */ __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries. Learn more about Klarna.', 'woocommerce' ), 'https://woo.com/products/klarna-payments/ ' ) . '

'; $square_description = '

' . sprintf( /* translators: %s: URL */ __( 'Securely accept credit and debit cards with one low rate, no surprise fees (custom rates available). Sell online and in store and track sales and inventory in one place. Learn more about Square.', 'woocommerce' ), 'https://woo.com/products/square/' ) . '

'; return array( 'stripe' => array( 'name' => __( 'WooCommerce Stripe Gateway', 'woocommerce' ), 'image' => WC()->plugin_url() . '/assets/images/stripe.png', 'description' => $stripe_description, 'class' => 'checked stripe-logo', 'repo-slug' => 'woocommerce-gateway-stripe', 'settings' => array( 'create_account' => array( 'label' => __( 'Set up Stripe for me using this email:', 'woocommerce' ), 'type' => 'checkbox', 'value' => 'yes', 'default' => 'yes', 'placeholder' => '', 'required' => false, 'plugins' => $this->get_wcs_requisite_plugins(), ), 'email' => array( 'label' => __( 'Stripe email address:', 'woocommerce' ), 'type' => 'email', 'value' => $user_email, 'placeholder' => __( 'Stripe email address', 'woocommerce' ), 'required' => true, ), ), ), 'ppec_paypal' => array( 'name' => __( 'WooCommerce PayPal Checkout Gateway', 'woocommerce' ), 'image' => WC()->plugin_url() . '/assets/images/paypal.png', 'description' => $paypal_checkout_description, 'enabled' => false, 'class' => 'checked paypal-logo', 'repo-slug' => 'woocommerce-gateway-paypal-express-checkout', 'settings' => array( 'reroute_requests' => array( 'label' => __( 'Set up PayPal for me using this email:', 'woocommerce' ), 'type' => 'checkbox', 'value' => 'yes', 'default' => 'yes', 'placeholder' => '', 'required' => false, 'plugins' => $this->get_wcs_requisite_plugins(), ), 'email' => array( 'label' => __( 'Direct payments to email address:', 'woocommerce' ), 'type' => 'email', 'value' => $user_email, 'placeholder' => __( 'Email address to receive payments', 'woocommerce' ), 'required' => true, ), ), ), 'paypal' => array( 'name' => __( 'PayPal Standard', 'woocommerce' ), 'description' => __( 'Accept payments via PayPal using account balance or credit card.', 'woocommerce' ), 'image' => '', 'settings' => array( 'email' => array( 'label' => __( 'PayPal email address:', 'woocommerce' ), 'type' => 'email', 'value' => $user_email, 'placeholder' => __( 'PayPal email address', 'woocommerce' ), 'required' => true, ), ), ), 'klarna_checkout' => array( 'name' => __( 'Klarna Checkout for WooCommerce', 'woocommerce' ), 'description' => $klarna_checkout_description, 'image' => WC()->plugin_url() . '/assets/images/klarna-black.png', 'enabled' => true, 'class' => 'klarna-logo', 'repo-slug' => 'klarna-checkout-for-woocommerce', ), 'klarna_payments' => array( 'name' => __( 'Klarna Payments for WooCommerce', 'woocommerce' ), 'description' => $klarna_payments_description, 'image' => WC()->plugin_url() . '/assets/images/klarna-black.png', 'enabled' => true, 'class' => 'klarna-logo', 'repo-slug' => 'klarna-payments-for-woocommerce', ), 'square' => array( 'name' => __( 'WooCommerce Square', 'woocommerce' ), 'description' => $square_description, 'image' => WC()->plugin_url() . '/assets/images/square-black.png', 'class' => 'square-logo', 'enabled' => false, 'repo-slug' => 'woocommerce-square', ), 'eway' => array( 'name' => __( 'WooCommerce eWAY Gateway', 'woocommerce' ), 'description' => __( 'The eWAY extension for WooCommerce allows you to take credit card payments directly on your store without redirecting your customers to a third party site to make payment.', 'woocommerce' ), 'image' => WC()->plugin_url() . '/assets/images/eway-logo.jpg', 'enabled' => false, 'class' => 'eway-logo', 'repo-slug' => 'woocommerce-gateway-eway', ), 'payfast' => array( 'name' => __( 'WooCommerce PayFast Gateway', 'woocommerce' ), 'description' => __( 'The PayFast extension for WooCommerce enables you to accept payments by Credit Card and EFT via one of South Africa’s most popular payment gateways. No setup fees or monthly subscription costs.', 'woocommerce' ), 'image' => WC()->plugin_url() . '/assets/images/payfast.png', 'class' => 'payfast-logo', 'enabled' => false, 'repo-slug' => 'woocommerce-payfast-gateway', 'file' => 'gateway-payfast.php', ), ); } /** * Simple array of "in cart" gateways to show in wizard. * * @deprecated 4.6.0 * @return array */ public function get_wizard_in_cart_payment_gateways() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $gateways = $this->get_wizard_available_in_cart_payment_gateways(); $country = WC()->countries->get_base_country(); $currency = get_woocommerce_currency(); $can_stripe = $this->is_stripe_supported_country( $country ); $can_eway = $this->is_eway_payments_supported_country( $country ); $can_payfast = ( 'ZA' === $country ); // South Africa. $can_paypal = $this->is_paypal_supported_currency( $currency ); if ( ! current_user_can( 'install_plugins' ) ) { return $can_paypal ? array( 'paypal' => $gateways['paypal'] ) : array(); } $klarna_or_square = false; if ( $this->is_klarna_checkout_supported_country( $country ) ) { $klarna_or_square = 'klarna_checkout'; } elseif ( $this->is_klarna_payments_supported_country( $country ) ) { $klarna_or_square = 'klarna_payments'; } elseif ( $this->is_square_supported_country( $country ) && get_option( 'woocommerce_sell_in_person' ) ) { $klarna_or_square = 'square'; } $offered_gateways = array(); if ( $can_stripe ) { $gateways['stripe']['enabled'] = true; $gateways['stripe']['featured'] = true; $offered_gateways += array( 'stripe' => $gateways['stripe'] ); } elseif ( $can_paypal ) { $gateways['ppec_paypal']['enabled'] = true; } if ( $klarna_or_square ) { if ( in_array( $klarna_or_square, array( 'klarna_checkout', 'klarna_payments' ), true ) ) { $gateways[ $klarna_or_square ]['enabled'] = true; $gateways[ $klarna_or_square ]['featured'] = false; $offered_gateways += array( $klarna_or_square => $gateways[ $klarna_or_square ], ); } else { $offered_gateways += array( $klarna_or_square => $gateways[ $klarna_or_square ], ); } } if ( $can_paypal ) { $offered_gateways += array( 'ppec_paypal' => $gateways['ppec_paypal'] ); } if ( $can_eway ) { $offered_gateways += array( 'eway' => $gateways['eway'] ); } if ( $can_payfast ) { $offered_gateways += array( 'payfast' => $gateways['payfast'] ); } return $offered_gateways; } /** * Simple array of "manual" gateways to show in wizard. * * @deprecated 4.6.0 * @return array */ public function get_wizard_manual_payment_gateways() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $gateways = array( 'cheque' => array( 'name' => _x( 'Check payments', 'Check payment method', 'woocommerce' ), 'description' => __( 'A simple offline gateway that lets you accept a check as method of payment.', 'woocommerce' ), 'image' => '', 'class' => '', ), 'bacs' => array( 'name' => __( 'Bank transfer (BACS) payments', 'woocommerce' ), 'description' => __( 'A simple offline gateway that lets you accept BACS payment.', 'woocommerce' ), 'image' => '', 'class' => '', ), 'cod' => array( 'name' => __( 'Cash on delivery', 'woocommerce' ), 'description' => __( 'A simple offline gateway that lets you accept cash on delivery.', 'woocommerce' ), 'image' => '', 'class' => '', ), ); return $gateways; } /** * Display service item in list. * * @param int $item_id Item ID. * @param array $item_info Item info array. * * @deprecated 4.6.0 */ public function display_service_item( $item_id, $item_info ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $item_class = 'wc-wizard-service-item'; if ( isset( $item_info['class'] ) ) { $item_class .= ' ' . $item_info['class']; } $previously_saved_settings = get_option( 'woocommerce_' . $item_id . '_settings' ); // Show the user-saved state if it was previously saved. // Otherwise, rely on the item info. if ( is_array( $previously_saved_settings ) ) { $should_enable_toggle = ( isset( $previously_saved_settings['enabled'] ) && 'yes' === $previously_saved_settings['enabled'] ) ? true : ( isset( $item_info['enabled'] ) && $item_info['enabled'] ); } else { $should_enable_toggle = isset( $item_info['enabled'] ) && $item_info['enabled']; } $plugins = null; if ( isset( $item_info['repo-slug'] ) ) { $plugin = array( 'slug' => $item_info['repo-slug'], 'name' => $item_info['name'], ); $plugins = array( $plugin ); } ?>
  • <?php echo esc_attr( $item_info['name'] ); ?>

    data-plugins="" />
    $setting ) : ?>
    data-plugins="" />
  • is_featured_service( $service ); } /** * Payment Step. * * @deprecated 4.6.0 */ public function wc_setup_payment() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $featured_gateways = array_filter( $this->get_wizard_in_cart_payment_gateways(), array( $this, 'is_featured_service' ) ); $in_cart_gateways = array_filter( $this->get_wizard_in_cart_payment_gateways(), array( $this, 'is_not_featured_service' ) ); $manual_gateways = $this->get_wizard_manual_payment_gateways(); ?>

    Additional payment methods can be installed later.', 'woocommerce' ), array( 'a' => array( 'href' => array(), 'target' => array(), ), ) ), esc_url( admin_url( 'admin.php?page=wc-addons§ion=payment-gateways' ) ) ); ?>

    plugin_install_info(); ?>

    plugin_install_info(); ?>

    get_next_step_link() ) ) ); exit; } } /** * * @deprecated 4.6.0 */ protected function wc_setup_activate_get_feature_list() { $features = array(); $stripe_settings = get_option( 'woocommerce_stripe_settings', false ); $stripe_enabled = is_array( $stripe_settings ) && isset( $stripe_settings['create_account'] ) && 'yes' === $stripe_settings['create_account'] && isset( $stripe_settings['enabled'] ) && 'yes' === $stripe_settings['enabled']; $ppec_settings = get_option( 'woocommerce_ppec_paypal_settings', false ); $ppec_enabled = is_array( $ppec_settings ) && isset( $ppec_settings['reroute_requests'] ) && 'yes' === $ppec_settings['reroute_requests'] && isset( $ppec_settings['enabled'] ) && 'yes' === $ppec_settings['enabled']; $features['payment'] = $stripe_enabled || $ppec_enabled; $features['taxes'] = (bool) get_option( 'woocommerce_setup_automated_taxes', false ); $features['labels'] = (bool) get_option( 'woocommerce_setup_shipping_labels', false ); return $features; } /** * * @deprecated 4.6.0 */ protected function wc_setup_activate_get_feature_list_str() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $features = $this->wc_setup_activate_get_feature_list(); if ( $features['payment'] && $features['taxes'] && $features['labels'] ) { return __( 'payment setup, automated taxes and discounted shipping labels', 'woocommerce' ); } else if ( $features['payment'] && $features['taxes'] ) { return __( 'payment setup and automated taxes', 'woocommerce' ); } else if ( $features['payment'] && $features['labels'] ) { return __( 'payment setup and discounted shipping labels', 'woocommerce' ); } else if ( $features['payment'] ) { return __( 'payment setup', 'woocommerce' ); } else if ( $features['taxes'] && $features['labels'] ) { return __( 'automated taxes and discounted shipping labels', 'woocommerce' ); } else if ( $features['taxes'] ) { return __( 'automated taxes', 'woocommerce' ); } else if ( $features['labels'] ) { return __( 'discounted shipping labels', 'woocommerce' ); } return false; } /** * Activate step. * * @deprecated 4.6.0 */ public function wc_setup_activate() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $this->wc_setup_activate_actions(); $jetpack_connected = class_exists( 'Jetpack' ) && Jetpack::is_active(); $has_jetpack_error = false; if ( isset( $_GET['activate_error'] ) ) { $has_jetpack_error = true; $title = __( "Sorry, we couldn't connect your store to Jetpack", 'woocommerce' ); $error_message = $this->get_activate_error_message( sanitize_text_field( wp_unslash( $_GET['activate_error'] ) ) ); $description = $error_message; } else { $feature_list = $this->wc_setup_activate_get_feature_list_str(); $description = false; if ( $feature_list ) { if ( ! $jetpack_connected ) { /* translators: %s: list of features, potentially comma separated */ $description_base = __( 'Your store is almost ready! To activate services like %s, just connect with Jetpack.', 'woocommerce' ); } else { $description_base = __( 'Thanks for using Jetpack! Your store is almost ready: to activate services like %s, just connect your store.', 'woocommerce' ); } $description = sprintf( $description_base, $feature_list ); } if ( ! $jetpack_connected ) { $title = $feature_list ? __( 'Connect your store to Jetpack', 'woocommerce' ) : __( 'Connect your store to Jetpack to enable extra features', 'woocommerce' ); $button_text = __( 'Continue with Jetpack', 'woocommerce' ); } elseif ( $feature_list ) { $title = __( 'Connect your store to activate WooCommerce Services', 'woocommerce' ); $button_text = __( 'Continue with WooCommerce Services', 'woocommerce' ); } else { wp_redirect( esc_url_raw( $this->get_next_step_link() ) ); exit; } } ?>

    Terms of Service and to share details with WordPress.com', 'woocommerce' ) ), 'https://wordpress.com/tos', 'https://jetpack.com/support/what-data-does-jetpack-sync' ); ?>

    __( "Sorry! We tried, but we couldn't connect Jetpack just now 😭. Please go to the Plugins tab to connect Jetpack, so that you can finish setting up your store.", 'woocommerce' ), 'jetpack_cant_be_installed' => __( "Sorry! We tried, but we couldn't install Jetpack for you 😭. Please go to the Plugins tab to install it, and finish setting up your store.", 'woocommerce' ), 'register_http_request_failed' => __( "Sorry! We couldn't contact Jetpack just now 😭. Please make sure that your site is visible over the internet, and that it accepts incoming and outgoing requests via curl. You can also try to connect to Jetpack again, and if you run into any more issues, please contact support.", 'woocommerce' ), 'siteurl_private_ip_dev' => __( "Your site might be on a private network. Jetpack can only connect to public sites. Please make sure your site is visible over the internet, and then try connecting again ðŸ™." , 'woocommerce' ), ); } /** * * @deprecated 4.6.0 */ protected function get_activate_error_message( $code = '' ) { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); $errors = $this->get_all_activate_errors(); return array_key_exists( $code, $errors ) ? $errors[ $code ] : $errors['default']; } /** * Activate step save. * * Install, activate, and launch connection flow for Jetpack. * * @deprecated 4.6.0 */ public function wc_setup_activate_save() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); } /** * Final step. * * @deprecated 4.6.0 */ public function wc_setup_ready() { _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', 'Onboarding is maintained in WooCommerce Admin.' ); // We've made it! Don't prompt the user to run the wizard again. WC_Admin_Notices::remove_notice( 'install', true ); $user_email = $this->get_current_user_email(); $docs_url = 'https://woo.com/documentation/plugins/woocommerce/getting-started/?utm_source=setupwizard&utm_medium=product&utm_content=docs&utm_campaign=woocommerceplugin'; $help_text = sprintf( /* translators: %1$s: link to docs */ __( 'Visit Woo.com to learn more about getting started.', 'woocommerce' ), $docs_url ); ?>

    get_notes_with_name( self::NOTE_NAME ); if ( empty( $note_ids ) ) { return; } if ( count( $note_ids ) > 1 ) { // Remove weird duplicates. Leave the first one. $current_notice = array_shift( $note_ids ); foreach ( $note_ids as $note_id ) { $note = new Note( $note_id ); $data_store->delete( $note ); } return $current_notice; } return current( $note_ids ); } /** * Set this notice to an actioned one, so that it's no longer displayed. */ public static function set_notice_actioned() { $note_id = self::get_current_notice(); if ( ! $note_id ) { return; } $note = new Note( $note_id ); $note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED ); $note->save(); } /** * Check whether the note is up to date for a fresh display. * * The check tests if * - actions are set up for the first 'Update database' notice, and * - URL for note's action is equal to the given URL (to check for potential nonce update). * * @param Note $note Note to check. * @param string $update_url URL to check the note against. * @param array $current_actions List of actions to check for. * @return bool */ private static function note_up_to_date( $note, $update_url, $current_actions ) { $actions = $note->get_actions(); return count( $current_actions ) === count( array_intersect( wp_list_pluck( $actions, 'name' ), $current_actions ) ) && in_array( $update_url, wp_list_pluck( $actions, 'query' ), true ); } /** * Create and set up the first (out of 3) 'Database update needed' notice and store it in the database. * * If a $note_id is given, the method updates the note instead of creating a new one. * * @param integer $note_id Note db record to update. * @return int Created/Updated note id */ private static function update_needed_notice( $note_id = null ) { $update_url = add_query_arg( array( 'do_update_woocommerce' => 'true', ), wc_get_current_admin_url() ? wc_get_current_admin_url() : admin_url( 'admin.php?page=wc-settings' ) ); $note_actions = array( array( 'name' => 'update-db_run', 'label' => __( 'Update WooCommerce Database', 'woocommerce' ), 'url' => $update_url, 'status' => 'unactioned', 'primary' => true, 'nonce_action' => 'wc_db_update', 'nonce_name' => 'wc_db_update_nonce', ), array( 'name' => 'update-db_learn-more', 'label' => __( 'Learn more about updates', 'woocommerce' ), 'url' => 'https://woo.com/document/how-to-update-woocommerce/', 'status' => 'unactioned', 'primary' => false, ), ); if ( $note_id ) { $note = new Note( $note_id ); } else { $note = new Note(); } // Check if the note needs to be updated (e.g. expired nonce or different note type stored in the previous run). if ( self::note_up_to_date( $note, $update_url, wp_list_pluck( $note_actions, 'name' ) ) ) { return $note_id; } $note->set_title( __( 'WooCommerce database update required', 'woocommerce' ) ); $note->set_content( __( 'WooCommerce has been updated! To keep things running smoothly, we have to update your database to the newest version.', 'woocommerce' ) /* translators: %1$s: opening tag %2$s: closing tag*/ . sprintf( ' ' . esc_html__( 'The database update process runs in the background and may take a little while, so please be patient. Advanced users can alternatively update via %1$sWP CLI%2$s.', 'woocommerce' ), '', '' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_UPDATE ); $note->set_name( self::NOTE_NAME ); $note->set_content_data( (object) array() ); $note->set_source( 'woocommerce-core' ); // In case db version is out of sync with WC version or during the next update, the notice needs to show up again, // so set it to unactioned. $note->set_status( Note::E_WC_ADMIN_NOTE_UNACTIONED ); // Set new actions. $note->clear_actions(); foreach ( $note_actions as $note_action ) { $note->add_action( ...array_values( $note_action ) ); if ( isset( $note_action['nonce_action'] ) ) { $note->add_nonce_to_action( $note_action['name'], $note_action['nonce_action'], $note_action['nonce_name'] ); } } return $note->save(); } /** * Update the existing note with $note_id with information about the db upgrade being in progress. * * This is the second out of 3 notices displayed to the user. * * @param int $note_id Note id to update. */ private static function update_in_progress_notice( $note_id ) { // Same actions as in includes/admin/views/html-notice-updating.php. This just redirects, performs no action, so without nonce. $pending_actions_url = admin_url( 'admin.php?page=wc-status&tab=action-scheduler&s=woocommerce_run_update&status=pending' ); $cron_disabled = Constants::is_true( 'DISABLE_WP_CRON' ); $cron_cta = $cron_disabled ? __( 'You can manually run queued updates here.', 'woocommerce' ) : __( 'View progress →', 'woocommerce' ); $note = new Note( $note_id ); $note->set_title( __( 'WooCommerce database update in progress', 'woocommerce' ) ); $note->set_content( __( 'WooCommerce is updating the database in the background. The database update process may take a little while, so please be patient.', 'woocommerce' ) ); $note->clear_actions(); $note->add_action( 'update-db_see-progress', $cron_cta, $pending_actions_url, 'unactioned', false ); $note->save(); } /** * Update the existing note with $note_id with information that db upgrade is done. * * This is the last notice (3 out of 3 notices) displayed to the user. * * @param int $note_id Note id to update. */ private static function update_done_notice( $note_id ) { $hide_notices_url = html_entity_decode( // to convert &s to normal &, otherwise produces invalid link. add_query_arg( array( 'wc-hide-notice' => 'update', ), wc_get_current_admin_url() ? remove_query_arg( 'do_update_woocommerce', wc_get_current_admin_url() ) : admin_url( 'admin.php?page=wc-settings' ) ) ); $note_actions = array( array( 'name' => 'update-db_done', 'label' => __( 'Thanks!', 'woocommerce' ), 'url' => $hide_notices_url, 'status' => 'actioned', 'primary' => true, 'nonce_action' => 'woocommerce_hide_notices_nonce', 'nonce_name' => '_wc_notice_nonce', ), ); $note = new Note( $note_id ); // Check if the note needs to be updated (e.g. expired nonce or different note type stored in the previous run). if ( self::note_up_to_date( $note, $hide_notices_url, wp_list_pluck( $note_actions, 'name' ) ) ) { return $note_id; } $note->set_title( __( 'WooCommerce database update done', 'woocommerce' ) ); $note->set_content( __( 'WooCommerce database update complete. Thank you for updating to the latest version!', 'woocommerce' ) ); $note->clear_actions(); foreach ( $note_actions as $note_action ) { $note->add_action( ...array_values( $note_action ) ); if ( isset( $note_action['nonce_action'] ) ) { $note->add_nonce_to_action( $note_action['name'], $note_action['nonce_action'], $note_action['nonce_name'] ); } } $note->save(); } /** * Prepare the correct content of the db update note to be displayed by WC Admin. * * This one gets called on each page load, so try to bail quickly. * * If the db needs an update, the notice should be always shown. * If the db does not need an update, but the notice has *not* been actioned (i.e. after the db update, when * store owner hasn't acknowledged the successful db update), still show the Thanks notice. * If the db does not need an update, and the notice has been actioned, then notice should *not* be shown. * The notice should also be hidden if the db does not need an update and the notice does not exist. */ public static function show_reminder() { $needs_db_update = \WC_Install::needs_db_update(); $note_id = self::get_current_notice(); if ( ! $needs_db_update ) { // Db update not needed && note does not exist -> don't show it. if ( ! $note_id ) { return; } $note = new Note( $note_id ); if ( $note::E_WC_ADMIN_NOTE_ACTIONED === $note->get_status() ) { // Db update not needed && note actioned -> don't show it. return; } else { // Db update not needed && notice is unactioned -> Thank you note. self::update_done_notice( $note_id ); return; } } else { // Db needs update &&. if ( ! $note_id ) { // Db needs update && no notice exists -> create one that shows Nudge to update. $note_id = self::update_needed_notice(); } $next_scheduled_date = WC()->queue()->get_next( 'woocommerce_run_update_callback', null, 'woocommerce-db-updates' ); if ( $next_scheduled_date || ! empty( $_GET['do_update_woocommerce'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended // Db needs update && db update is scheduled -> update note to In progress. self::update_in_progress_notice( $note_id ); } else { // Db needs update && db update is not scheduled -> Nudge to run the db update. self::update_needed_notice( $note_id ); } } } } PKK[krÒ’ ’ -admin/notes/class-wc-notes-refund-returns.phpnu„[µü¤get_notes_with_name( self::NOTE_NAME ); if ( ! empty( $note_id ) ) { $note = new Note( $note_id ); if ( false !== $note || $note::E_WC_ADMIN_NOTE_ACTIONED === $note->get_status() ) { // note actioned -> don't show it. return; } } // Add note. $note = self::get_note( $page_id ); $note->save(); delete_option( 'woocommerce_refund_returns_page_created' ); } /** * Get the note. * * @param int $page_id The ID of the page. * @return object $note The note object. */ public static function get_note( $page_id ) { $note = new Note(); $note->set_title( __( 'Setup a Refund and Returns Policy page to boost your store\'s credibility.', 'woocommerce' ) ); $note->set_content( __( 'We have created a sample draft Refund and Returns Policy page for you. Please have a look and update it to fit your store.', 'woocommerce' ) ); $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); $note->set_name( self::NOTE_NAME ); $note->set_content_data( (object) array() ); $note->set_source( 'woocommerce-core' ); $note->add_action( 'notify-refund-returns-page', __( 'Edit page', 'woocommerce' ), admin_url( sprintf( 'post.php?post=%d&action=edit', (int) $page_id ) ) ); return $note; } /** * Get the note. * * @param Note $note_from_db The note object from the database. * @return Note $note The note object. */ public static function get_note_from_db( $note_from_db ) { if ( ! $note_from_db instanceof Note || get_user_locale() === $note_from_db->get_locale() ) { return $note_from_db; } if ( self::NOTE_NAME === $note_from_db->get_name() ) { $note = self::get_note( 0 ); $note_from_db->set_title( $note->get_title() ); $note_from_db->set_content( $note->get_content() ); $action_from_db = $note_from_db->get_action( 'notify-refund-returns-page' ); $action_from_class = $note->get_action( 'notify-refund-returns-page' ); if ( $action_from_db && $action_from_class ) { $action_from_db->label = $action_from_class->label; $note_from_db->set_actions( array( $action_from_db ) ); } } return $note_from_db; } } PKK[ø˜ýZ¨“¨“#admin/class-wc-admin-post-types.phpnu„[µü¤request_data(); $screen_id = false; if ( function_exists( 'get_current_screen' ) ) { $screen = get_current_screen(); $screen_id = isset( $screen, $screen->id ) ? $screen->id : ''; } if ( ! empty( $request_data['screen'] ) ) { $screen_id = wc_clean( wp_unslash( $request_data['screen'] ) ); } switch ( $screen_id ) { case 'edit-shop_order': include_once __DIR__ . '/list-tables/class-wc-admin-list-table-orders.php'; $wc_list_table = new WC_Admin_List_Table_Orders(); break; case 'edit-shop_coupon': include_once __DIR__ . '/list-tables/class-wc-admin-list-table-coupons.php'; $wc_list_table = new WC_Admin_List_Table_Coupons(); break; case 'edit-product': include_once __DIR__ . '/list-tables/class-wc-admin-list-table-products.php'; $wc_list_table = new WC_Admin_List_Table_Products(); break; } // Ensure the table handler is only loaded once. Prevents multiple loads if a plugin calls check_ajax_referer many times. remove_action( 'current_screen', array( $this, 'setup_screen' ) ); remove_action( 'check_ajax_referer', array( $this, 'setup_screen' ) ); } /** * Change messages when a post type is updated. * * @param array $messages Array of messages. * @return array */ public function post_updated_messages( $messages ) { global $post; $messages['product'] = array( 0 => '', // Unused. Messages start at index 1. /* translators: %1$s: Product link opening tag. %2$s: Product link closing tag.*/ 1 => sprintf( __( 'Product updated. %1$sView Product%2$s', 'woocommerce' ), '', '' ), 2 => __( 'Custom field updated.', 'woocommerce' ), 3 => __( 'Custom field deleted.', 'woocommerce' ), 4 => __( 'Product updated.', 'woocommerce' ), 5 => __( 'Revision restored.', 'woocommerce' ), /* translators: %1$s: Product link opening tag. %2$s: Product link closing tag.*/ 6 => sprintf( __( 'Product published. %1$sView Product%2$s', 'woocommerce' ), '', '' ), 7 => __( 'Product saved.', 'woocommerce' ), /* translators: %s: product url */ 8 => sprintf( __( 'Product submitted. Preview product', 'woocommerce' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ), 9 => sprintf( /* translators: 1: date 2: product url */ __( 'Product scheduled for: %1$s. Preview product', 'woocommerce' ), '' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) . '', esc_url( get_permalink( $post->ID ) ) ), /* translators: %s: product url */ 10 => sprintf( __( 'Product draft updated. Preview product', 'woocommerce' ), esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) ) ), ); $messages = $this->order_updated_messages( $messages ); $messages['shop_coupon'] = array( 0 => '', // Unused. Messages start at index 1. 1 => __( 'Coupon updated.', 'woocommerce' ), 2 => __( 'Custom field updated.', 'woocommerce' ), 3 => __( 'Custom field deleted.', 'woocommerce' ), 4 => __( 'Coupon updated.', 'woocommerce' ), 5 => __( 'Revision restored.', 'woocommerce' ), 6 => __( 'Coupon updated.', 'woocommerce' ), 7 => __( 'Coupon saved.', 'woocommerce' ), 8 => __( 'Coupon submitted.', 'woocommerce' ), 9 => sprintf( /* translators: %s: date */ __( 'Coupon scheduled for: %s.', 'woocommerce' ), '' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $post->post_date ) ) . '' ), 10 => __( 'Coupon draft updated.', 'woocommerce' ), ); return $messages; } /** * Add messages when an order is updated. * * @param array $messages Array of messages. * * @return array */ public function order_updated_messages( array $messages ) { global $post, $theorder; if ( ! isset( $theorder ) || ! $theorder instanceof WC_Abstract_Order ) { if ( ! isset( $post ) || 'shop_order' !== $post->post_type ) { return $messages; } else { \Automattic\WooCommerce\Utilities\OrderUtil::init_theorder_object( $post ); } } $messages['shop_order'] = array( 0 => '', // Unused. Messages start at index 1. 1 => __( 'Order updated.', 'woocommerce' ), 2 => __( 'Custom field updated.', 'woocommerce' ), 3 => __( 'Custom field deleted.', 'woocommerce' ), 4 => __( 'Order updated.', 'woocommerce' ), 5 => __( 'Revision restored.', 'woocommerce' ), 6 => __( 'Order updated.', 'woocommerce' ), 7 => __( 'Order saved.', 'woocommerce' ), 8 => __( 'Order submitted.', 'woocommerce' ), 9 => sprintf( /* translators: %s: date */ __( 'Order scheduled for: %s.', 'woocommerce' ), '' . date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( $theorder->get_date_created() ?? $post->post_date ) ) . '' ), 10 => __( 'Order draft updated.', 'woocommerce' ), 11 => __( 'Order updated and sent.', 'woocommerce' ), ); return $messages; } /** * Specify custom bulk actions messages for different post types. * * @param array $bulk_messages Array of messages. * @param array $bulk_counts Array of how many objects were updated. * @return array */ public function bulk_post_updated_messages( $bulk_messages, $bulk_counts ) { $bulk_messages['product'] = array( /* translators: %s: product count */ 'updated' => _n( '%s product updated.', '%s products updated.', $bulk_counts['updated'], 'woocommerce' ), /* translators: %s: product count */ 'locked' => _n( '%s product not updated, somebody is editing it.', '%s products not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), /* translators: %s: product count */ 'deleted' => _n( '%s product permanently deleted.', '%s products permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), /* translators: %s: product count */ 'trashed' => _n( '%s product moved to the Trash.', '%s products moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), /* translators: %s: product count */ 'untrashed' => _n( '%s product restored from the Trash.', '%s products restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), ); $bulk_messages['shop_order'] = array( /* translators: %s: order count */ 'updated' => _n( '%s order updated.', '%s orders updated.', $bulk_counts['updated'], 'woocommerce' ), /* translators: %s: order count */ 'locked' => _n( '%s order not updated, somebody is editing it.', '%s orders not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), /* translators: %s: order count */ 'deleted' => _n( '%s order permanently deleted.', '%s orders permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), /* translators: %s: order count */ 'trashed' => _n( '%s order moved to the Trash.', '%s orders moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), /* translators: %s: order count */ 'untrashed' => _n( '%s order restored from the Trash.', '%s orders restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), ); $bulk_messages['shop_coupon'] = array( /* translators: %s: coupon count */ 'updated' => _n( '%s coupon updated.', '%s coupons updated.', $bulk_counts['updated'], 'woocommerce' ), /* translators: %s: coupon count */ 'locked' => _n( '%s coupon not updated, somebody is editing it.', '%s coupons not updated, somebody is editing them.', $bulk_counts['locked'], 'woocommerce' ), /* translators: %s: coupon count */ 'deleted' => _n( '%s coupon permanently deleted.', '%s coupons permanently deleted.', $bulk_counts['deleted'], 'woocommerce' ), /* translators: %s: coupon count */ 'trashed' => _n( '%s coupon moved to the Trash.', '%s coupons moved to the Trash.', $bulk_counts['trashed'], 'woocommerce' ), /* translators: %s: coupon count */ 'untrashed' => _n( '%s coupon restored from the Trash.', '%s coupons restored from the Trash.', $bulk_counts['untrashed'], 'woocommerce' ), ); return $bulk_messages; } /** * Custom bulk edit - form. * * @param string $column_name Column being shown. * @param string $post_type Post type being shown. */ public function bulk_edit( $column_name, $post_type ) { if ( 'price' !== $column_name || 'product' !== $post_type ) { return; } $shipping_class = get_terms( 'product_shipping_class', array( 'hide_empty' => false, ) ); include WC()->plugin_path() . '/includes/admin/views/html-bulk-edit-product.php'; } /** * Custom quick edit - form. * * @param string $column_name Column being shown. * @param string $post_type Post type being shown. */ public function quick_edit( $column_name, $post_type ) { if ( 'price' !== $column_name || 'product' !== $post_type ) { return; } $shipping_class = get_terms( 'product_shipping_class', array( 'hide_empty' => false, ) ); include WC()->plugin_path() . '/includes/admin/views/html-quick-edit-product.php'; } /** * Offers a way to hook into save post without causing an infinite loop * when quick/bulk saving product info. * * @since 3.0.0 * @param int $post_id Post ID being saved. * @param object $post Post object being saved. */ public function bulk_and_quick_edit_hook( $post_id, $post ) { remove_action( 'save_post', array( $this, 'bulk_and_quick_edit_hook' ) ); do_action( 'woocommerce_product_bulk_and_quick_edit', $post_id, $post ); add_action( 'save_post', array( $this, 'bulk_and_quick_edit_hook' ), 10, 2 ); } /** * Quick and bulk edit saving. * * @param int $post_id Post ID being saved. * @param object $post Post object being saved. * @return int */ public function bulk_and_quick_edit_save_post( $post_id, $post ) { $request_data = $this->request_data(); // If this is an autosave, our form has not been submitted, so we don't want to do anything. if ( Constants::is_true( 'DOING_AUTOSAVE' ) ) { return $post_id; } // Don't save revisions and autosaves. if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) || 'product' !== $post->post_type || ! current_user_can( 'edit_post', $post_id ) ) { return $post_id; } // Check nonce. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash if ( ! isset( $request_data['woocommerce_quick_edit_nonce'] ) || ! wp_verify_nonce( $request_data['woocommerce_quick_edit_nonce'], 'woocommerce_quick_edit_nonce' ) ) { return $post_id; } // Get the product and save. $product = wc_get_product( $post ); if ( ! empty( $request_data['woocommerce_quick_edit'] ) ) { // WPCS: input var ok. $this->quick_edit_save( $post_id, $product ); } else { $this->bulk_edit_save( $post_id, $product ); } return $post_id; } /** * Quick edit. * * @param int $post_id Post ID being saved. * @param WC_Product $product Product object. */ private function quick_edit_save( $post_id, $product ) { $request_data = $this->request_data(); $data_store = $product->get_data_store(); $old_regular_price = $product->get_regular_price(); $old_sale_price = $product->get_sale_price(); $input_to_props = array( '_weight' => 'weight', '_length' => 'length', '_width' => 'width', '_height' => 'height', '_visibility' => 'catalog_visibility', '_tax_class' => 'tax_class', '_tax_status' => 'tax_status', ); foreach ( $input_to_props as $input_var => $prop ) { if ( isset( $request_data[ $input_var ] ) ) { $product->{"set_{$prop}"}( wc_clean( wp_unslash( $request_data[ $input_var ] ) ) ); } } if ( isset( $request_data['_sku'] ) ) { $sku = $product->get_sku(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash $new_sku = (string) wc_clean( $request_data['_sku'] ); if ( $new_sku !== $sku ) { if ( ! empty( $new_sku ) ) { $unique_sku = wc_product_has_unique_sku( $post_id, $new_sku ); if ( $unique_sku ) { $product->set_sku( wc_clean( wp_unslash( $new_sku ) ) ); } } else { $product->set_sku( '' ); } } } if ( ! empty( $request_data['_shipping_class'] ) ) { if ( '_no_shipping_class' === $request_data['_shipping_class'] ) { $product->set_shipping_class_id( 0 ); } else { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $request_data['_shipping_class'] ) ); $product->set_shipping_class_id( $shipping_class_id ); } } if ( ! empty( $request_data['_tax_class'] ) ) { $tax_class = sanitize_title( wp_unslash( $request_data['_tax_class'] ) ); if ( 'standard' === $tax_class ) { $tax_class = ''; } $product->set_tax_class( $tax_class ); } $product->set_featured( isset( $request_data['_featured'] ) ); if ( $product->is_type( 'simple' ) || $product->is_type( 'external' ) ) { if ( isset( $request_data['_regular_price'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash $new_regular_price = ( '' === $request_data['_regular_price'] ) ? '' : wc_format_decimal( $request_data['_regular_price'] ); $product->set_regular_price( $new_regular_price ); } else { $new_regular_price = null; } if ( isset( $request_data['_sale_price'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash $new_sale_price = ( '' === $request_data['_sale_price'] ) ? '' : wc_format_decimal( $request_data['_sale_price'] ); $product->set_sale_price( $new_sale_price ); } else { $new_sale_price = null; } // Handle price - remove dates and set to lowest. $price_changed = false; if ( ! is_null( $new_regular_price ) && $new_regular_price !== $old_regular_price ) { $price_changed = true; } elseif ( ! is_null( $new_sale_price ) && $new_sale_price !== $old_sale_price ) { $price_changed = true; } if ( $price_changed ) { $product->set_date_on_sale_to( '' ); $product->set_date_on_sale_from( '' ); } } // Handle Stock Data. // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $manage_stock = ! empty( $request_data['_manage_stock'] ) && 'grouped' !== $product->get_type() ? 'yes' : 'no'; $backorders = ! empty( $request_data['_backorders'] ) ? wc_clean( $request_data['_backorders'] ) : 'no'; if ( ! empty( $request_data['_stock_status'] ) ) { $stock_status = wc_clean( $request_data['_stock_status'] ); } else { $stock_status = $product->is_type( 'variable' ) ? null : 'instock'; } // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $product->set_manage_stock( $manage_stock ); if ( 'external' !== $product->get_type() ) { $product->set_backorders( $backorders ); } if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { $stock_amount = 'yes' === $manage_stock && isset( $request_data['_stock'] ) && is_numeric( wp_unslash( $request_data['_stock'] ) ) ? wc_stock_amount( wp_unslash( $request_data['_stock'] ) ) : ''; $product->set_stock_quantity( $stock_amount ); } $product = $this->maybe_update_stock_status( $product, $stock_status ); $product->save(); do_action( 'woocommerce_product_quick_edit_save', $product ); } /** * Bulk edit. * * @param int $post_id Post ID being saved. * @param WC_Product $product Product object. */ public function bulk_edit_save( $post_id, $product ) { // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash $request_data = $this->request_data(); $data_store = $product->get_data_store(); if ( ! empty( $request_data['change_weight'] ) && isset( $request_data['_weight'] ) ) { $product->set_weight( wc_clean( wp_unslash( $request_data['_weight'] ) ) ); } if ( ! empty( $request_data['change_dimensions'] ) ) { if ( isset( $request_data['_length'] ) ) { $product->set_length( wc_clean( wp_unslash( $request_data['_length'] ) ) ); } if ( isset( $request_data['_width'] ) ) { $product->set_width( wc_clean( wp_unslash( $request_data['_width'] ) ) ); } if ( isset( $request_data['_height'] ) ) { $product->set_height( wc_clean( wp_unslash( $request_data['_height'] ) ) ); } } if ( ! empty( $request_data['_tax_status'] ) ) { $product->set_tax_status( wc_clean( $request_data['_tax_status'] ) ); } if ( ! empty( $request_data['_tax_class'] ) ) { $tax_class = sanitize_title( wp_unslash( $request_data['_tax_class'] ) ); if ( 'standard' === $tax_class ) { $tax_class = ''; } $product->set_tax_class( $tax_class ); } if ( ! empty( $request_data['_shipping_class'] ) ) { if ( '_no_shipping_class' === $request_data['_shipping_class'] ) { $product->set_shipping_class_id( 0 ); } else { $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $request_data['_shipping_class'] ) ); $product->set_shipping_class_id( $shipping_class_id ); } } if ( ! empty( $request_data['_visibility'] ) ) { $product->set_catalog_visibility( wc_clean( $request_data['_visibility'] ) ); } if ( ! empty( $request_data['_featured'] ) ) { // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $product->set_featured( wp_unslash( $request_data['_featured'] ) ); // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized } if ( ! empty( $request_data['_sold_individually'] ) ) { if ( 'yes' === $request_data['_sold_individually'] ) { $product->set_sold_individually( 'yes' ); } else { $product->set_sold_individually( '' ); } } // Handle price - remove dates and set to lowest. $change_price_product_types = apply_filters( 'woocommerce_bulk_edit_save_price_product_types', array( 'simple', 'external' ) ); $can_product_type_change_price = false; foreach ( $change_price_product_types as $product_type ) { if ( $product->is_type( $product_type ) ) { $can_product_type_change_price = true; break; } } if ( $can_product_type_change_price ) { $regular_price_changed = $this->set_new_price( $product, 'regular' ); $sale_price_changed = $this->set_new_price( $product, 'sale' ); if ( $regular_price_changed || $sale_price_changed ) { $product->set_date_on_sale_to( '' ); $product->set_date_on_sale_from( '' ); if ( $product->get_regular_price() < $product->get_sale_price() ) { $product->set_sale_price( '' ); } } } // Handle Stock Data. $was_managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; $backorders = $product->get_backorders(); $backorders = ! empty( $request_data['_backorders'] ) ? wc_clean( $request_data['_backorders'] ) : $backorders; if ( ! empty( $request_data['_manage_stock'] ) ) { $manage_stock = 'yes' === wc_clean( $request_data['_manage_stock'] ) && 'grouped' !== $product->get_type() ? 'yes' : 'no'; } else { $manage_stock = $was_managing_stock; } $stock_amount = 'yes' === $manage_stock && ! empty( $request_data['change_stock'] ) && isset( $request_data['_stock'] ) ? wc_stock_amount( $request_data['_stock'] ) : $product->get_stock_quantity(); $product->set_manage_stock( $manage_stock ); if ( 'external' !== $product->get_type() ) { $product->set_backorders( $backorders ); } if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { $change_stock = absint( $request_data['change_stock'] ); switch ( $change_stock ) { case 2: wc_update_product_stock( $product, $stock_amount, 'increase', true ); break; case 3: wc_update_product_stock( $product, $stock_amount, 'decrease', true ); break; default: wc_update_product_stock( $product, $stock_amount, 'set', true ); break; } } else { // Reset values if WooCommerce Setting - Manage Stock status is disabled. $product->set_stock_quantity( '' ); $product->set_manage_stock( 'no' ); } $stock_status = empty( $request_data['_stock_status'] ) ? null : wc_clean( $request_data['_stock_status'] ); $product = $this->maybe_update_stock_status( $product, $stock_status ); $product->save(); do_action( 'woocommerce_product_bulk_edit_save', $product ); // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash } /** * Disable the auto-save functionality for Orders. */ public function disable_autosave() { global $post; if ( $post instanceof WP_Post && in_array( get_post_type( $post->ID ), wc_get_order_types( 'order-meta-boxes' ), true ) ) { wp_dequeue_script( 'autosave' ); } } /** * Output extra data on post forms. * * @param WP_Post $post Current post object. */ public function edit_form_top( $post ) { echo ''; } /** * Change title boxes in admin. * * @param string $text Text to shown. * @param WP_Post $post Current post object. * @return string */ public function enter_title_here( $text, $post ) { switch ( $post->post_type ) { case 'product': $text = esc_html__( 'Product name', 'woocommerce' ); break; case 'shop_coupon': $text = esc_html__( 'Coupon code', 'woocommerce' ); break; } return $text; } /** * Print coupon description textarea field. * * @param WP_Post $post Current post object. */ public function edit_form_after_title( $post ) { // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped if ( 'shop_coupon' === $post->post_type ) { ?> post_type && 'post' === $screen->base ) { $hidden = array_merge( $hidden, array( 'postcustom' ) ); } return $hidden; } /** * Output product visibility options. */ public function product_data_visibility() { global $post, $thepostid, $product_object; if ( 'product' !== $post->post_type ) { return; } $thepostid = $post->ID; $product_object = $thepostid ? wc_get_product( $thepostid ) : new WC_Product(); $current_visibility = $product_object->get_catalog_visibility(); $current_featured = wc_bool_to_string( $product_object->get_featured() ); $visibility_options = wc_get_product_visibility_options(); ?>
    ' . esc_html__( 'This setting determines which shop pages products will be listed on.', 'woocommerce' ) . '

    '; foreach ( $visibility_options as $name => $label ) { echo '
    '; } echo '

    '; ?>

    unique_filename( $full_filename, $ext ); // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Change filename to append random text. * * @param string $full_filename Original filename with extension. * @param string $ext Extension. * * @return string Modified filename. */ public function unique_filename( $full_filename, $ext ) { $ideal_random_char_length = 6; // Not going with a larger length because then downloaded filename will not be pretty. $max_filename_length = 255; // Max file name length for most file systems. $length_to_prepend = min( $ideal_random_char_length, $max_filename_length - strlen( $full_filename ) - 1 ); if ( 1 > $length_to_prepend ) { return $full_filename; } $suffix = strtolower( wp_generate_password( $length_to_prepend, false, false ) ); $filename = $full_filename; if ( strlen( $ext ) > 0 ) { $filename = substr( $filename, 0, strlen( $filename ) - strlen( $ext ) ); } $full_filename = str_replace( $filename, "$filename-$suffix", $full_filename ); return $full_filename; } /** * Run a filter when uploading a downloadable product. */ public function woocommerce_media_upload_downloadable_product() { do_action( 'media_upload_file' ); } /** * Grant downloadable file access to any newly added files on any existing. * orders for this product that have previously been granted downloadable file access. * * @param int $product_id product identifier. * @param int $variation_id optional product variation identifier. * @param array $downloadable_files newly set files. * @deprecated 3.3.0 and moved to post-data class. */ public function process_product_file_download_paths( $product_id, $variation_id, $downloadable_files ) { wc_deprecated_function( 'WC_Admin_Post_Types::process_product_file_download_paths', '3.3', '' ); WC_Post_Data::process_product_file_download_paths( $product_id, $variation_id, $downloadable_files ); } /** * When editing the shop page, we should hide templates. * * @param array $page_templates Templates array. * @param string $theme Classname. * @param WP_Post $post The current post object. * @return array */ public function hide_cpt_archive_templates( $page_templates, $theme, $post ) { $shop_page_id = wc_get_page_id( 'shop' ); if ( $post && absint( $post->ID ) === $shop_page_id ) { $page_templates = array(); } return $page_templates; } /** * Show a notice above the CPT archive. * * @param WP_Post $post The current post object. */ public function show_cpt_archive_notice( $post ) { $shop_page_id = wc_get_page_id( 'shop' ); if ( $post && absint( $post->ID ) === $shop_page_id ) { echo '
    '; /* translators: %s: URL to read more about the shop page. */ echo '

    ' . sprintf( wp_kses_post( __( 'This is the WooCommerce shop page. The shop page is a special archive that lists your products. You can read more about this here.', 'woocommerce' ) ), 'https://woo.com/document/woocommerce-pages/#section-4' ) . '

    '; echo '
    '; } } /** * Add a post display state for special WC pages in the page list table. * * @param array $post_states An array of post display states. * @param WP_Post $post The current post object. */ public function add_display_post_states( $post_states, $post ) { if ( wc_get_page_id( 'shop' ) === $post->ID ) { $post_states['wc_page_for_shop'] = __( 'Shop Page', 'woocommerce' ); } if ( wc_get_page_id( 'cart' ) === $post->ID ) { $post_states['wc_page_for_cart'] = __( 'Cart Page', 'woocommerce' ); } if ( wc_get_page_id( 'checkout' ) === $post->ID ) { $post_states['wc_page_for_checkout'] = __( 'Checkout Page', 'woocommerce' ); } if ( wc_get_page_id( 'myaccount' ) === $post->ID ) { $post_states['wc_page_for_myaccount'] = __( 'My Account Page', 'woocommerce' ); } if ( wc_get_page_id( 'terms' ) === $post->ID ) { $post_states['wc_page_for_terms'] = __( 'Terms and Conditions Page', 'woocommerce' ); } return $post_states; } /** * Apply product type constraints to stock status. * * @param WC_Product $product The product whose stock status will be adjusted. * @param string|null $stock_status The stock status to use for adjustment, or null if no new stock status has been supplied in the request. * @return WC_Product The supplied product, or the synced product if it was a variable product. */ private function maybe_update_stock_status( $product, $stock_status ) { if ( $product->is_type( 'external' ) ) { // External products are always in stock. $product->set_stock_status( 'instock' ); } elseif ( isset( $stock_status ) ) { if ( $product->is_type( 'variable' ) && ! $product->get_manage_stock() ) { // Stock status is determined by children. foreach ( $product->get_children() as $child_id ) { $child = wc_get_product( $child_id ); if ( ! $product->get_manage_stock() ) { $child->set_stock_status( $stock_status ); $child->save(); } } $product = WC_Product_Variable::sync( $product, false ); } else { $product->set_stock_status( $stock_status ); } } return $product; } /** * Set the new regular or sale price if requested. * * @param WC_Product $product The product to set the new price for. * @param string $price_type 'regular' or 'sale'. * @return bool true if a new price has been set, false otherwise. */ private function set_new_price( $product, $price_type ) { // phpcs:disable WordPress.Security.NonceVerification.Recommended $request_data = $this->request_data(); if ( empty( $request_data[ "change_{$price_type}_price" ] ) || ! isset( $request_data[ "_{$price_type}_price" ] ) ) { return false; } $old_price = (float) $product->{"get_{$price_type}_price"}(); $price_changed = false; $change_price = absint( $request_data[ "change_{$price_type}_price" ] ); $raw_price = wc_clean( wp_unslash( $request_data[ "_{$price_type}_price" ] ) ); $is_percentage = (bool) strstr( $raw_price, '%' ); $price = wc_format_decimal( $raw_price ); switch ( $change_price ) { case 1: $new_price = $price; break; case 2: if ( $is_percentage ) { $percent = $price / 100; $new_price = $old_price + ( $old_price * $percent ); } else { $new_price = $old_price + $price; } break; case 3: if ( $is_percentage ) { $percent = $price / 100; $new_price = max( 0, $old_price - ( $old_price * $percent ) ); } else { $new_price = max( 0, $old_price - $price ); } break; case 4: if ( 'sale' !== $price_type ) { break; } $regular_price = $product->get_regular_price(); if ( $is_percentage && is_numeric( $regular_price ) ) { $percent = $price / 100; $new_price = max( 0, $regular_price - ( NumberUtil::round( $regular_price * $percent, wc_get_price_decimals() ) ) ); } else { $new_price = max( 0, (float) $regular_price - (float) $price ); } break; default: break; } if ( isset( $new_price ) && $new_price !== $old_price ) { $price_changed = true; $new_price = NumberUtil::round( $new_price, wc_get_price_decimals() ); $product->{"set_{$price_type}_price"}( $new_price ); } return $price_changed; // phpcs:disable WordPress.Security.NonceVerification.Recommended } /** * Get the current request data ($_REQUEST superglobal). * This method is added to ease unit testing. * * @return array The $_REQUEST superglobal. */ protected function request_data() { return $_REQUEST; } } new WC_Admin_Post_Types(); PKK[|•ݽ((,admin/class-wc-admin-webhooks-table-list.phpnu„[µü¤ 'webhook', 'plural' => 'webhooks', 'ajax' => false, ) ); } /** * No items found text. */ public function no_items() { esc_html_e( 'No webhooks found.', 'woocommerce' ); } /** * Get list columns. * * @return array */ public function get_columns() { return array( 'cb' => '', 'title' => __( 'Name', 'woocommerce' ), 'status' => __( 'Status', 'woocommerce' ), 'topic' => __( 'Topic', 'woocommerce' ), 'delivery_url' => __( 'Delivery URL', 'woocommerce' ), ); } /** * Column cb. * * @param WC_Webhook $webhook Webhook instance. * @return string */ public function column_cb( $webhook ) { return sprintf( '', $this->_args['singular'], $webhook->get_id() ); } /** * Return title column. * * @param WC_Webhook $webhook Webhook instance. * @return string */ public function column_title( $webhook ) { $edit_link = admin_url( 'admin.php?page=wc-settings&tab=advanced&section=webhooks&edit-webhook=' . $webhook->get_id() ); $output = ''; // Title. $output .= '' . esc_html( $webhook->get_name() ) . ''; // Get actions. $actions = array( /* translators: %s: webhook ID. */ 'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $webhook->get_id() ), 'edit' => '' . esc_html__( 'Edit', 'woocommerce' ) . '', /* translators: %s: webhook name */ 'delete' => 'get_name() ) ) . '" href="' . esc_url( wp_nonce_url( add_query_arg( array( 'delete' => $webhook->get_id(), ), admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks' ) ), 'delete-webhook' ) ) . '">' . esc_html__( 'Delete permanently', 'woocommerce' ) . '', ); $actions = apply_filters( 'webhook_row_actions', $actions, $webhook ); $row_actions = array(); foreach ( $actions as $action => $link ) { $row_actions[] = '' . $link . ''; } $output .= '
    ' . implode( ' | ', $row_actions ) . '
    '; return $output; } /** * Return status column. * * @param WC_Webhook $webhook Webhook instance. * @return string */ public function column_status( $webhook ) { return $webhook->get_i18n_status(); } /** * Return topic column. * * @param WC_Webhook $webhook Webhook instance. * @return string */ public function column_topic( $webhook ) { return $webhook->get_topic(); } /** * Return delivery URL column. * * @param WC_Webhook $webhook Webhook instance. * @return string */ public function column_delivery_url( $webhook ) { return $webhook->get_delivery_url(); } /** * Get the status label for webhooks. * * @param string $status_name Status name. * @param int $amount Amount of webhooks. * @return array */ private function get_status_label( $status_name, $amount ) { $statuses = wc_get_webhook_statuses(); if ( isset( $statuses[ $status_name ] ) ) { return array( 'singular' => sprintf( '%s (%s)', esc_html( $statuses[ $status_name ] ), $amount ), 'plural' => sprintf( '%s (%s)', esc_html( $statuses[ $status_name ] ), $amount ), 'context' => '', 'domain' => 'woocommerce', ); } return array( 'singular' => sprintf( '%s (%s)', esc_html( $status_name ), $amount ), 'plural' => sprintf( '%s (%s)', esc_html( $status_name ), $amount ), 'context' => '', 'domain' => 'woocommerce', ); } /** * Table list views. * * @return array */ protected function get_views() { $status_links = array(); $data_store = WC_Data_Store::load( 'webhook' ); $num_webhooks = $data_store->get_count_webhooks_by_status(); $total_webhooks = array_sum( (array) $num_webhooks ); $statuses = array_keys( wc_get_webhook_statuses() ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $class = empty( $_REQUEST['status'] ) && empty( $_REQUEST['legacy'] ) ? ' class="current"' : ''; /* translators: %s: count */ $status_links['all'] = "" . sprintf( _nx( 'All (%s)', 'All (%s)', $total_webhooks, 'posts', 'woocommerce' ), number_format_i18n( $total_webhooks ) ) . ''; foreach ( $statuses as $status_name ) { $class = ''; if ( empty( $num_webhooks[ $status_name ] ) ) { continue; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_REQUEST['status'] ) && sanitize_key( wp_unslash( $_REQUEST['status'] ) ) === $status_name ) { $class = ' class="current"'; } $label = $this->get_status_label( $status_name, $num_webhooks[ $status_name ] ); $status_links[ $status_name ] = "" . sprintf( translate_nooped_plural( $label, $num_webhooks[ $status_name ] ), number_format_i18n( $num_webhooks[ $status_name ] ) ) . ''; } $legacy_webhooks_count = wc_get_container()->get( WebhookUtil::class )->get_legacy_webhooks_count(); if ( $legacy_webhooks_count > 0 ) { $class = ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( 'true' === sanitize_key( wp_unslash( $_REQUEST['legacy'] ?? '' ) ) ) { $class = ' class="current"'; } $label = $this->get_status_label( __( 'Legacy', 'woocommerce' ), $legacy_webhooks_count ); $status_links['legacy'] = "" . sprintf( translate_nooped_plural( $label, $legacy_webhooks_count ), number_format_i18n( $legacy_webhooks_count ) ) . ''; } return $status_links; } /** * Get bulk actions. * * @return array */ protected function get_bulk_actions() { return array( 'delete' => __( 'Delete permanently', 'woocommerce' ), ); } /** * Process bulk actions. */ public function process_bulk_action() { $action = $this->current_action(); $webhooks = isset( $_REQUEST['webhook'] ) ? array_map( 'absint', (array) $_REQUEST['webhook'] ) : array(); // WPCS: input var okay, CSRF ok. if ( false !== $action ) { check_admin_referer( 'woocommerce-settings' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You do not have permission to edit Webhooks', 'woocommerce' ) ); } if ( 'delete' === $action ) { WC_Admin_Webhooks::bulk_delete( $webhooks ); } } } /** * Generate the table navigation above or below the table. * Included to remove extra nonce input. * * @param string $which The location of the extra table nav markup: 'top' or 'bottom'. */ protected function display_tablenav( $which ) { echo '
    '; if ( $this->has_items() ) { echo '
    '; $this->bulk_actions( $which ); echo '
    '; } $this->extra_tablenav( $which ); $this->pagination( $which ); echo '
    '; echo '
    '; } /** * Search box. * * @param string $text Button text. * @param string $input_id Input ID. */ public function search_box( $text, $input_id ) { if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) { // WPCS: input var okay, CSRF ok. return; } $input_id = $input_id . '-search-input'; $search_query = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; // WPCS: input var okay, CSRF ok. echo ''; } /** * Prepare table list items. */ public function prepare_items() { $per_page = $this->get_items_per_page( 'woocommerce_webhooks_per_page' ); $current_page = $this->get_pagenum(); // Query args. $args = array( 'limit' => $per_page, 'offset' => $per_page * ( $current_page - 1 ), ); // Handle the status query. if ( ! empty( $_REQUEST['status'] ) ) { // WPCS: input var okay, CSRF ok. $args['status'] = sanitize_key( wp_unslash( $_REQUEST['status'] ) ); // WPCS: input var okay, CSRF ok. } if ( ! empty( $_REQUEST['s'] ) ) { // WPCS: input var okay, CSRF ok. $args['search'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ); // WPCS: input var okay, CSRF ok. } $args['paginate'] = true; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( 'true' === sanitize_key( wp_unslash( $_REQUEST['legacy'] ?? null ) ) ) { $args['api_version'] = -1; } // Get the webhooks. $data_store = WC_Data_Store::load( 'webhook' ); $webhooks = $data_store->search_webhooks( $args ); $this->items = array_map( 'wc_get_webhook', $webhooks->webhooks ); // Set the pagination. $this->set_pagination_args( array( 'total_items' => $webhooks->total, 'per_page' => $per_page, 'total_pages' => $webhooks->max_num_pages, ) ); } } PKK[i;;!admin/class-wc-admin-api-keys.phpnu„[µü¤is_api_keys_settings_page() ) { // WPCS: input var okay, CSRF ok. $keys_table_list = new WC_Admin_API_Keys_Table_List(); // Add screen option. add_screen_option( 'per_page', array( 'default' => 10, 'option' => 'woocommerce_keys_per_page', ) ); } } /** * Table list output. */ private static function table_list_output() { global $wpdb, $keys_table_list; echo '

    ' . esc_html__( 'REST API', 'woocommerce' ) . ' ' . esc_html__( 'Add key', 'woocommerce' ) . '

    '; // Get the API keys count. $count = $wpdb->get_var( "SELECT COUNT(key_id) FROM {$wpdb->prefix}woocommerce_api_keys WHERE 1 = 1;" ); if ( absint( $count ) && $count > 0 ) { $keys_table_list->prepare_items(); echo ''; echo ''; echo ''; $keys_table_list->views(); $keys_table_list->search_box( __( 'Search key', 'woocommerce' ), 'key' ); $keys_table_list->display(); } else { echo '
    '; ?>

    0, 'user_id' => '', 'description' => '', 'permissions' => '', 'truncated_key' => '', 'last_access' => '', ); if ( 0 === $key_id ) { return $empty; } $key = $wpdb->get_row( $wpdb->prepare( "SELECT key_id, user_id, description, permissions, truncated_key, last_access FROM {$wpdb->prefix}woocommerce_api_keys WHERE key_id = %d", $key_id ), ARRAY_A ); if ( is_null( $key ) ) { return $empty; } return $key; } /** * API Keys admin actions. */ public function actions() { if ( $this->is_api_keys_settings_page() ) { // Revoke key. if ( isset( $_REQUEST['revoke-key'] ) ) { // WPCS: input var okay, CSRF ok. $this->revoke_key(); } // Bulk actions. if ( isset( $_REQUEST['action'] ) && isset( $_REQUEST['key'] ) ) { // WPCS: input var okay, CSRF ok. $this->bulk_actions(); } } } /** * Notices. */ public static function notices() { if ( isset( $_GET['revoked'] ) ) { // WPCS: input var okay, CSRF ok. $revoked = absint( $_GET['revoked'] ); // WPCS: input var okay, CSRF ok. /* translators: %d: count */ WC_Admin_Settings::add_message( sprintf( _n( '%d API key permanently revoked.', '%d API keys permanently revoked.', $revoked, 'woocommerce' ), $revoked ) ); } } /** * Revoke key. */ private function revoke_key() { global $wpdb; check_admin_referer( 'revoke' ); if ( isset( $_REQUEST['revoke-key'] ) ) { // WPCS: input var okay, CSRF ok. $key_id = absint( $_REQUEST['revoke-key'] ); // WPCS: input var okay, CSRF ok. $user_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT user_id FROM {$wpdb->prefix}woocommerce_api_keys WHERE key_id = %d", $key_id ) ); if ( $key_id && $user_id && ( current_user_can( 'edit_user', $user_id ) || get_current_user_id() === $user_id ) ) { $this->remove_key( $key_id ); } else { wp_die( esc_html__( 'You do not have permission to revoke this API Key', 'woocommerce' ) ); } } wp_safe_redirect( esc_url_raw( add_query_arg( array( 'revoked' => 1 ), admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=keys' ) ) ) ); exit(); } /** * Bulk actions. */ private function bulk_actions() { check_admin_referer( 'woocommerce-settings' ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You do not have permission to edit API Keys', 'woocommerce' ) ); } if ( isset( $_REQUEST['action'] ) ) { // WPCS: input var okay, CSRF ok. $action = sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ); // WPCS: input var okay, CSRF ok. $keys = isset( $_REQUEST['key'] ) ? array_map( 'absint', (array) $_REQUEST['key'] ) : array(); // WPCS: input var okay, CSRF ok. if ( 'revoke' === $action ) { $this->bulk_revoke_key( $keys ); } } } /** * Bulk revoke key. * * @param array $keys API Keys. */ private function bulk_revoke_key( $keys ) { if ( ! current_user_can( 'remove_users' ) ) { wp_die( esc_html__( 'You do not have permission to revoke API Keys', 'woocommerce' ) ); } $qty = 0; foreach ( $keys as $key_id ) { $result = $this->remove_key( $key_id ); if ( $result ) { $qty++; } } // Redirect to webhooks page. wp_safe_redirect( esc_url_raw( add_query_arg( array( 'revoked' => $qty ), admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=keys' ) ) ) ); exit(); } /** * Remove key. * * @param int $key_id API Key ID. * @return bool */ private function remove_key( $key_id ) { global $wpdb; $delete = $wpdb->delete( $wpdb->prefix . 'woocommerce_api_keys', array( 'key_id' => $key_id ), array( '%d' ) ); return $delete; } } new WC_Admin_API_Keys(); PKK[¥«mâ4%4%*admin/class-wc-admin-duplicate-product.phpnu„[µü¤post_type ) { return $actions; } // Add Class to Delete Permanently link in row actions. if ( empty( $the_product ) || $the_product->get_id() !== $post->ID ) { $the_product = wc_get_product( $post ); } if ( 'publish' === $post->post_status && $the_product && 0 < $the_product->get_total_sales() ) { $actions['trash'] = sprintf( '%s', get_delete_post_link( $the_product->get_id(), '', false ), /* translators: %s: post title */ esc_attr( sprintf( __( 'Move “%s” to the Trash', 'woocommerce' ), $the_product->get_name() ) ), esc_html__( 'Trash', 'woocommerce' ) ); } $actions['duplicate'] = '' . esc_html__( 'Duplicate', 'woocommerce' ) . ''; return $actions; } /** * Show the dupe product link in admin. */ public function dupe_button() { global $post; if ( ! current_user_can( apply_filters( 'woocommerce_duplicate_product_capability', 'manage_woocommerce' ) ) ) { return; } if ( ! is_object( $post ) ) { return; } if ( 'product' !== $post->post_type ) { return; } $notify_url = wp_nonce_url( admin_url( 'edit.php?post_type=product&action=duplicate_product&post=' . absint( $post->ID ) ), 'woocommerce-duplicate-product_' . $post->ID ); ?>
    product_duplicate( $product ); // Hook rename to match other woocommerce_product_* hooks, and to move away from depending on a response from the wp_posts table. do_action( 'woocommerce_product_duplicate', $duplicate, $product ); wc_do_deprecated_action( 'woocommerce_duplicate_product', array( $duplicate->get_id(), $this->get_product_to_duplicate( $product_id ) ), '3.0', 'Use woocommerce_product_duplicate action instead.' ); // Redirect to the edit screen for the new draft page. wp_redirect( admin_url( 'post.php?action=edit&post=' . $duplicate->get_id() ) ); exit; } /** * Function to create the duplicate of the product. * * @param WC_Product $product The product to duplicate. * @return WC_Product The duplicate. */ public function product_duplicate( $product ) { /** * Filter to allow us to exclude meta keys from product duplication.. * * @param array $exclude_meta The keys to exclude from the duplicate. * @param array $existing_meta_keys The meta keys that the product already has. * @since 2.6 */ $meta_to_exclude = array_filter( apply_filters( 'woocommerce_duplicate_product_exclude_meta', array(), array_map( function ( $datum ) { return $datum->key; }, $product->get_meta_data() ) ) ); $duplicate = clone $product; $duplicate->set_id( 0 ); /* translators: %s contains the name of the original product. */ $duplicate->set_name( sprintf( esc_html__( '%s (Copy)', 'woocommerce' ), $duplicate->get_name() ) ); $duplicate->set_total_sales( 0 ); if ( '' !== $product->get_sku( 'edit' ) ) { $duplicate->set_sku( wc_product_generate_unique_sku( 0, $product->get_sku( 'edit' ) ) ); } $duplicate->set_status( 'draft' ); $duplicate->set_date_created( null ); $duplicate->set_slug( '' ); $duplicate->set_rating_counts( 0 ); $duplicate->set_average_rating( 0 ); $duplicate->set_review_count( 0 ); foreach ( $meta_to_exclude as $meta_key ) { $duplicate->delete_meta_data( $meta_key ); } /** * This action can be used to modify the object further before it is created - it will be passed by reference. * * @since 3.0 */ do_action( 'woocommerce_product_duplicate_before_save', $duplicate, $product ); // Save parent product. $duplicate->save(); // Duplicate children of a variable product. if ( ! apply_filters( 'woocommerce_duplicate_product_exclude_children', false, $product ) && $product->is_type( 'variable' ) ) { foreach ( $product->get_children() as $child_id ) { $child = wc_get_product( $child_id ); $child_duplicate = clone $child; $child_duplicate->set_parent_id( $duplicate->get_id() ); $child_duplicate->set_id( 0 ); $child_duplicate->set_date_created( null ); // If we wait and let the insertion generate the slug, we will see extreme performance degradation // in the case where a product is used as a template. Every time the template is duplicated, each // variation will query every consecutive slug until it finds an empty one. To avoid this, we can // optimize the generation ourselves, avoiding the issue altogether. $this->generate_unique_slug( $child_duplicate ); if ( '' !== $child->get_sku( 'edit' ) ) { $child_duplicate->set_sku( wc_product_generate_unique_sku( 0, $child->get_sku( 'edit' ) ) ); } foreach ( $meta_to_exclude as $meta_key ) { $child_duplicate->delete_meta_data( $meta_key ); } /** * This action can be used to modify the object further before it is created - it will be passed by reference. * * @since 3.0 */ do_action( 'woocommerce_product_duplicate_before_save', $child_duplicate, $child ); $child_duplicate->save(); } // Get new object to reflect new children. $duplicate = wc_get_product( $duplicate->get_id() ); } return $duplicate; } /** * Get a product from the database to duplicate. * * @deprecated 3.0.0 * @param mixed $id The ID of the product to duplicate. * @return object|bool * @see duplicate_product */ private function get_product_to_duplicate( $id ) { global $wpdb; $id = absint( $id ); if ( ! $id ) { return false; } $post = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID = %d", $id ) ); if ( isset( $post->post_type ) && 'revision' === $post->post_type ) { $id = $post->post_parent; $post = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID = %d", $id ) ); } return $post; } /** * Generates a unique slug for a given product. We do this so that we can override the * behavior of wp_unique_post_slug(). The normal slug generation will run single * select queries on every non-unique slug, resulting in very bad performance. * * @param WC_Product $product The product to generate a slug for. * @since 3.9.0 */ private function generate_unique_slug( $product ) { global $wpdb; // We want to remove the suffix from the slug so that we can find the maximum suffix using this root slug. // This will allow us to find the next-highest suffix that is unique. While this does not support gap // filling, this shouldn't matter for our use-case. $root_slug = preg_replace( '/-[0-9]+$/', '', $product->get_slug() ); $results = $wpdb->get_results( $wpdb->prepare( "SELECT post_name FROM $wpdb->posts WHERE post_name LIKE %s AND post_type IN ( 'product', 'product_variation' )", $root_slug . '%' ) ); // The slug is already unique! if ( empty( $results ) ) { return; } // Find the maximum suffix so we can ensure uniqueness. $max_suffix = 1; foreach ( $results as $result ) { // Pull a numerical suffix off the slug after the last hyphen. $suffix = intval( substr( $result->post_name, strrpos( $result->post_name, '-' ) + 1 ) ); if ( $suffix > $max_suffix ) { $max_suffix = $suffix; } } $product->set_slug( $root_slug . '-' . ( $max_suffix + 1 ) ); } } return new WC_Admin_Duplicate_Product(); PKK[F¨¤A»A»admin/class-wc-admin-addons.phpnu„[µü¤ $headers, 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), ) ); if ( ! is_wp_error( $raw_featured ) ) { $featured = json_decode( wp_remote_retrieve_body( $raw_featured ) ); if ( $featured ) { self::set_locale_data_in_transient( 'wc_addons_featured_2', $featured, $locale, DAY_IN_SECONDS ); } } } if ( is_object( $featured ) ) { self::output_featured_sections( $featured->sections ); return $featured; } } /** * Render featured products and banners using WCCOM's the Featured 2.0 Endpoint * * @return void */ public static function render_featured() { $featured = self::fetch_featured(); if ( is_wp_error( $featured ) ) { self::output_empty( $featured->get_error_message() ); } self::output_featured( $featured ); } /** * Fetch featured products from WCCOM's the Featured 2.0 Endpoint and cache the data for a day. * * @return array|WP_Error */ public static function fetch_featured() { $locale = get_user_locale(); $featured = self::get_locale_data_from_transient( 'wc_addons_featured', $locale ); if ( false === $featured ) { $headers = array(); $auth = WC_Helper_Options::get( 'auth' ); if ( ! empty( $auth['access_token'] ) ) { $headers['Authorization'] = 'Bearer ' . $auth['access_token']; } $parameter_string = '?' . http_build_query( array( 'locale' => get_user_locale() ) ); $country = WC()->countries->get_base_country(); if ( ! empty( $country ) ) { $parameter_string = $parameter_string . '&' . http_build_query( array( 'country' => $country ) ); } // Important: WCCOM Extensions API v2.0 is used. $raw_featured = wp_safe_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/2.0/featured' . $parameter_string, array( 'headers' => $headers, 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), ) ); if ( is_wp_error( $raw_featured ) ) { do_action( 'woocommerce_page_wc-addons_connection_error', $raw_featured->get_error_message() ); $message = self::is_ssl_error( $raw_featured->get_error_message() ) ? __( 'We encountered an SSL error. Please ensure your site supports TLS version 1.2 or above.', 'woocommerce' ) : $raw_featured->get_error_message(); return new WP_Error( 'wc-addons-connection-error', $message ); } $response_code = (int) wp_remote_retrieve_response_code( $raw_featured ); if ( 200 !== $response_code ) { do_action( 'woocommerce_page_wc-addons_connection_error', $response_code ); /* translators: %d: HTTP error code. */ $message = sprintf( esc_html( /* translators: Error code */ __( 'Our request to the featured API got error code %d.', 'woocommerce' ) ), $response_code ); return new WP_Error( 'wc-addons-connection-error', $message ); } $featured = json_decode( wp_remote_retrieve_body( $raw_featured ) ); if ( empty( $featured ) || ! is_array( $featured ) ) { do_action( 'woocommerce_page_wc-addons_connection_error', 'Empty or malformed response' ); $message = __( 'Our request to the featured API got a malformed response.', 'woocommerce' ); return new WP_Error( 'wc-addons-connection-error', $message ); } if ( $featured ) { self::set_locale_data_in_transient( 'wc_addons_featured', $featured, $locale, DAY_IN_SECONDS ); } } return $featured; } /** * Check if the error is due to an SSL error * * @param string $error_message Error message. * * @return bool True if SSL error, false otherwise */ public static function is_ssl_error( $error_message ) { return false !== stripos( $error_message, 'cURL error 35' ); } /** * Build url parameter string * * @param string $category Addon (sub) category. * @param string $term Search terms. * @param string $country Store country. * * @return string url parameter string */ public static function build_parameter_string( $category, $term, $country ) { $parameters = array( 'category' => $category, 'term' => $term, 'country' => $country, 'locale' => get_user_locale(), ); return '?' . http_build_query( $parameters ); } /** * Call API to get extensions * * @param string $category Addon (sub) category. * @param string $term Search terms. * @param string $country Store country. * * @return object|WP_Error Object with products and promotions properties, or WP_Error */ public static function get_extension_data( $category, $term, $country ) { $parameters = self::build_parameter_string( $category, $term, $country ); $headers = array(); $auth = WC_Helper_Options::get( 'auth' ); if ( ! empty( $auth['access_token'] ) ) { $headers['Authorization'] = 'Bearer ' . $auth['access_token']; } $raw_extensions = wp_safe_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/1.0/search' . $parameters, array( 'headers' => $headers, 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), ) ); if ( is_wp_error( $raw_extensions ) ) { do_action( 'woocommerce_page_wc-addons_connection_error', $raw_extensions->get_error_message() ); return $raw_extensions; } $response_code = (int) wp_remote_retrieve_response_code( $raw_extensions ); if ( 200 !== $response_code ) { do_action( 'woocommerce_page_wc-addons_connection_error', $response_code ); return new WP_Error( 'error', sprintf( esc_html( /* translators: Error code */ __( 'Our request to the search API got response code %s.', 'woocommerce' ) ), $response_code ) ); } $addons = json_decode( wp_remote_retrieve_body( $raw_extensions ) ); if ( ! is_object( $addons ) || ! isset( $addons->products ) ) { do_action( 'woocommerce_page_wc-addons_connection_error', 'Empty or malformed response' ); return new WP_Error( 'error', __( 'Our request to the search API got a malformed response.', 'woocommerce' ) ); } return $addons; } /** * Get sections for the addons screen * * @return array of objects */ public static function get_sections() { $locale = get_user_locale(); $addon_sections = self::get_locale_data_from_transient( 'wc_addons_sections', $locale ); if ( false === ( $addon_sections ) ) { $parameter_string = '?' . http_build_query( array( 'locale' => get_user_locale() ) ); $raw_sections = wp_safe_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/1.0/categories' . $parameter_string, array( 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), ) ); if ( ! is_wp_error( $raw_sections ) ) { $addon_sections = json_decode( wp_remote_retrieve_body( $raw_sections ) ); if ( $addon_sections ) { self::set_locale_data_in_transient( 'wc_addons_sections', $addon_sections, $locale, WEEK_IN_SECONDS ); } } } return apply_filters( 'woocommerce_addons_sections', $addon_sections ); } /** * Get section for the addons screen. * * @param string $section_id Required section ID. * * @return object|bool */ public static function get_section( $section_id ) { $sections = self::get_sections(); if ( isset( $sections[ $section_id ] ) ) { return $sections[ $section_id ]; } return false; } /** * Get section content for the addons screen. * * @deprecated 5.9.0 No longer used in In-App Marketplace * * @param string $section_id Required section ID. * * @return array */ public static function get_section_data( $section_id ) { $section = self::get_section( $section_id ); $section_data = ''; if ( ! empty( $section->endpoint ) ) { $section_data = get_transient( 'wc_addons_section_' . $section_id ); if ( false === $section_data ) { $raw_section = wp_safe_remote_get( esc_url_raw( $section->endpoint ), array( 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), ) ); if ( ! is_wp_error( $raw_section ) ) { $section_data = json_decode( wp_remote_retrieve_body( $raw_section ) ); if ( ! empty( $section_data->products ) ) { set_transient( 'wc_addons_section_' . $section_id, $section_data, WEEK_IN_SECONDS ); } } } } return apply_filters( 'woocommerce_addons_section_data', $section_data->products, $section_id ); } /** * Handles the outputting of a contextually aware Storefront link (points to child themes if Storefront is already active). * * @deprecated 5.9.0 No longer used in In-App Marketplace */ public static function output_storefront_button() { $template = get_option( 'template' ); $stylesheet = get_option( 'stylesheet' ); if ( 'storefront' === $template ) { if ( 'storefront' === $stylesheet ) { $url = 'https://woo.com/product-category/themes/storefront-child-theme-themes/'; $text = __( 'Need a fresh look? Try Storefront child themes', 'woocommerce' ); $utm_content = 'nostorefrontchildtheme'; } else { $url = 'https://woo.com/product-category/themes/storefront-child-theme-themes/'; $text = __( 'View more Storefront child themes', 'woocommerce' ); $utm_content = 'hasstorefrontchildtheme'; } } else { $url = 'https://woo.com/storefront/'; $text = __( 'Need a theme? Try Storefront', 'woocommerce' ); $utm_content = 'nostorefront'; } $url = add_query_arg( array( 'utm_source' => 'addons', 'utm_medium' => 'product', 'utm_campaign' => 'woocommerceplugin', 'utm_content' => $utm_content, ), $url ); echo '' . esc_html( $text ) . '' . "\n"; } /** * Handles the outputting of a banner block. * * @deprecated 5.9.0 No longer used in In-App Marketplace * * @param object $block Banner data. */ public static function output_banner_block( $block ) { ?>

    title ); ?>

    description ); ?>

    items as $item ) : ?>

    title ); ?>

    description ); ?>

    href, $item->button, 'addons-button-solid', $item->plugin ); ?>
    container ) && 'column_container_start' === $block->container ) { ?>
    module ) { ?>
    container ) && 'column_container_end' === $block->container ) { ?>

    title ); ?>

    description ); ?>

    items as $item ) : ?>

    title ); ?>

    href, $item->button, 'addons-button-solid', $item->plugin ); ?>

    description ); ?>

    title ); ?>

    description ); ?>

    buttons as $button ) : ?> href, $button->text, 'addons-button-solid' ); ?>

    title ); ?>

    description ); ?>

    items as $item ) : ?>
    image ) ) : ?>
    href, $item->button, 'addons-button-outline-white' ); ?>
    'woocommerce-services', ) ), 'install-addon_woocommerce-services' ); $defaults = array( 'image' => WC()->plugin_url() . '/assets/images/wcs-extensions-banner-3x.jpg', 'image_alt' => __( 'WooCommerce Shipping', 'woocommerce' ), 'title' => __( 'Save time and money with WooCommerce Shipping', 'woocommerce' ), 'description' => __( 'Print discounted USPS and DHL labels straight from your WooCommerce dashboard and save on shipping.', 'woocommerce' ), 'button' => __( 'Free - Install now', 'woocommerce' ), 'href' => $button_url, 'logos' => array(), ); switch ( $location['country'] ) { case 'US': $local_defaults = array( 'logos' => array_merge( $defaults['logos'], array( array( 'link' => WC()->plugin_url() . '/assets/images/wcs-usps-logo.png', 'alt' => 'USPS logo', ), array( 'link' => WC()->plugin_url() . '/assets/images/wcs-dhlexpress-logo.png', 'alt' => 'DHL Express logo', ), ) ), ); break; default: $local_defaults = array(); } $block_data = array_merge( $defaults, $local_defaults, $block ); ?>
    <?php echo esc_attr( $block_data['image_alt'] ); ?>

    'woocommerce-payments', ) ), 'install-addon_woocommerce-payments' ); $defaults = array( 'image' => WC()->plugin_url() . '/assets/images/wcpayments-icon-secure.png', 'image_alt' => __( 'WooPayments', 'woocommerce' ), 'title' => __( 'Payments made simple, with no monthly fees — exclusively for WooCommerce stores.', 'woocommerce' ), 'description' => __( 'Securely accept cards in your store. See payments, track cash flow into your bank account, and stay on top of disputes – right from your dashboard.', 'woocommerce' ), 'button' => __( 'Free - Install now', 'woocommerce' ), 'href' => $button_url, 'logos' => array(), ); $block_data = array_merge( $defaults, $block ); ?>
    <?php echo esc_attr( $block_data['image_alt'] ); ?>

    <?php echo esc_attr( $promotion['image_alt'] ); ?>

    geowhitelist ) ) { $section_object->geowhitelist = explode( ',', $section_object->geowhitelist ); } if ( ! empty( $section_object->geoblacklist ) ) { $section_object->geoblacklist = explode( ',', $section_object->geoblacklist ); } if ( ! self::show_extension( $section_object ) ) { return; } ?>
    <?php echo esc_attr( $section['image_alt'] ); ?>

    module ) { case 'banner_block': self::output_banner_block( $section ); break; case 'column_start': self::output_column( $section ); break; case 'column_end': self::output_column( $section ); break; case 'column_block': self::output_column_block( $section ); break; case 'small_light_block': self::output_small_light_block( $section ); break; case 'small_dark_block': self::output_small_dark_block( $section ); break; case 'wcs_banner_block': self::output_wcs_banner_block( (array) $section ); break; case 'wcpay_banner_block': self::output_wcpay_banner_block( (array) $section ); break; case 'promotion_block': self::output_promotion_block( (array) $section ); break; } } } /** * Handles the outputting of featured page * * @param array $blocks Featured page's blocks. */ private static function output_featured( $blocks ) { foreach ( $blocks as $block ) { $block_type = $block->type ?? null; switch ( $block_type ) { case 'group': self::output_group( $block ); break; case 'banner': self::output_banner( $block ); break; } } } /** * Render a group block including products * * @param mixed $block Block of the page for rendering. * * @return void */ private static function output_group( $block ) { $capacity = $block->capacity ?? 3; $product_list_classes = 3 === $capacity ? 'three-column' : 'two-column'; $product_list_classes = 'products addons-products-' . $product_list_classes; ?>

    title ); ?>

    description ) ) : ?>
    description ); ?>
    url ) : ?>
      items, 0, $capacity ); foreach ( $products as $item ) { self::render_product_card( $item ); } ?>
    buttons ) ) { // Render a product-like banner. ?>
      type ); ?>
    • title ); ?>

      description, array() ); ?>

      buttons as $button ) { $button_classes = array( 'button', 'addons-buttons-banner-button' ); $type = $button->type ?? null; if ( 'primary' === $type ) { $button_classes[] = 'addons-buttons-banner-button-primary'; } ?> title ); ?>
    site_url(), 'wccom-back' => rawurlencode( $back_admin_path ), 'wccom-woo-version' => Constants::get_constant( 'WC_VERSION' ), 'wccom-connect-nonce' => wp_create_nonce( 'connect' ), ); } /** * Add in-app-purchase URL params to link. * * Adds various url parameters to a url to support a streamlined * flow for obtaining and setting up WooCommerce extensons. * * @param string $url Destination URL. */ public static function add_in_app_purchase_url_params( $url ) { return add_query_arg( self::get_in_app_purchase_url_params(), $url ); } /** * Outputs a button. * * @param string $url Destination URL. * @param string $text Button label text. * @param string $style Button style class. * @param string $plugin The plugin the button is promoting. */ public static function output_button( $url, $text, $style, $plugin = '' ) { $style = __( 'Free', 'woocommerce' ) === $text ? 'addons-button-outline-purple' : $style; $style = is_plugin_active( $plugin ) ? 'addons-button-installed' : $style; $text = is_plugin_active( $plugin ) ? __( 'Installed', 'woocommerce' ) : $text; $url = self::add_in_app_purchase_url_params( $url ); ?>

    Woo.com, where you\'ll find the most popular WooCommerce extensions.', 'woocommerce' ) ), 'https://woo.com/products/?utm_source=extensionsscreen&utm_medium=product&utm_campaign=connectionerror' ); ?>

    countries->get_base_country(); $extension_data = self::get_extension_data( $category, $term, $country ); $addons = is_wp_error( $extension_data ) ? $extension_data : $extension_data->products; $promotions = ! empty( $extension_data->promotions ) ? $extension_data->promotions : array(); } // We need Automattic\WooCommerce\Admin\RemoteInboxNotifications for the next part, if not remove all promotions. if ( ! WC()->is_wc_admin_active() ) { $promotions = array(); } // Check for existence of promotions and evaluate out if we should show them. if ( ! empty( $promotions ) ) { foreach ( $promotions as $promo_id => $promotion ) { $evaluator = new PromotionRuleEngine\RuleEvaluator(); $passed = $evaluator->evaluate( $promotion->rules ); if ( ! $passed ) { unset( $promotions[ $promo_id ] ); } } // Transform promotions to the correct format ready for output. $promotions = self::format_promotions( $promotions ); } /** * Addon page view. * * @uses $addons * @uses $search * @uses $sections * @uses $theme * @uses $current_section */ include_once dirname( __FILE__ ) . '/views/html-admin-page-addons.php'; } /** * Install WooCommerce Services from Extensions screens. */ public static function install_woocommerce_services_addon() { check_admin_referer( 'install-addon_woocommerce-services' ); $services_plugin_id = 'woocommerce-services'; $services_plugin = array( 'name' => __( 'WooCommerce Services', 'woocommerce' ), 'repo-slug' => 'woocommerce-services', ); WC_Install::background_installer( $services_plugin_id, $services_plugin ); wp_safe_redirect( remove_query_arg( array( 'install-addon', '_wpnonce' ) ) ); exit; } /** * Install WooCommerce Payments from the Extensions screens. * * @param string $section Optional. Extensions tab. * * @return void