/** * Helper functions for interacting with the Store API. * * This file is autoloaded via composer.json. */ use Automattic\WooCommerce\StoreApi\StoreApi; use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema; if ( ! function_exists( 'woocommerce_store_api_register_endpoint_data' ) ) { /** * Register endpoint data under a specified namespace. * * @see Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema::register_endpoint_data() * * @param array $args Args to pass to register_endpoint_data. * @returns boolean|\WP_Error True on success, WP_Error on fail. */ function woocommerce_store_api_register_endpoint_data( $args ) { try { $extend = StoreApi::container()->get( ExtendSchema::class ); $extend->register_endpoint_data( $args ); } catch ( \Exception $error ) { return new \WP_Error( 'error', $error->getMessage() ); } return true; } } if ( ! function_exists( 'woocommerce_store_api_register_update_callback' ) ) { /** * Add callback functions that can be executed by the cart/extensions endpoint. * * @see Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema::register_update_callback() * * @param array $args Args to pass to register_update_callback. * @returns boolean|\WP_Error True on success, WP_Error on fail. */ function woocommerce_store_api_register_update_callback( $args ) { try { $extend = StoreApi::container()->get( ExtendSchema::class ); $extend->register_update_callback( $args ); } catch ( \Exception $error ) { return new \WP_Error( 'error', $error->getMessage() ); } return true; } } if ( ! function_exists( 'woocommerce_store_api_register_payment_requirements' ) ) { /** * Registers and validates payment requirements callbacks. * * @see Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema::register_payment_requirements() * * @param array $args Args to pass to register_payment_requirements. * @returns boolean|\WP_Error True on success, WP_Error on fail. */ function woocommerce_store_api_register_payment_requirements( $args ) { try { $extend = StoreApi::container()->get( ExtendSchema::class ); $extend->register_payment_requirements( $args ); } catch ( \Exception $error ) { return new \WP_Error( 'error', $error->getMessage() ); } return true; } } if ( ! function_exists( 'woocommerce_store_api_get_formatter' ) ) { /** * Returns a formatter instance. * * @see Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema::get_formatter() * * @param string $name Formatter name. * @return Automattic\WooCommerce\StoreApi\Formatters\FormatterInterface */ function woocommerce_store_api_get_formatter( $name ) { return StoreApi::container()->get( ExtendSchema::class )->get_formatter( $name ); } }/** * WooCommerce Coupons Functions * * Functions for coupon specific things. * * @package WooCommerce\Functions * @version 3.0.0 */ defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Utilities\StringUtil; use Automattic\WooCommerce\Admin\API\Reports\Coupons\DataStore as CouponsDataStore; /** * Get coupon types. * * @return array */ function wc_get_coupon_types() { return (array) apply_filters( 'woocommerce_coupon_discount_types', array( 'percent' => __( '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' ) ); } /** * Check if two coupon codes are the same. * Lowercasing to ensure case-insensitive comparison. * * @since 9.9.0 * * @param string $coupon_1 Coupon code 1. * @param string $coupon_2 Coupon code 2. * @return bool */ function wc_is_same_coupon( $coupon_1, $coupon_2 ) { return wc_strtolower( $coupon_1 ) === wc_strtolower( $coupon_2 ); } /** * 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' ); // Coupon code allows spaces, which doesn't work well with some cache engines (e.g. memcached). $hashed_code = md5( $code ); $cache_key = WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $hashed_code; $ids = wp_cache_get( $cache_key, 'coupons' ); if ( false === $ids ) { $ids = $data_store->get_ids_by_code( $code ); if ( $ids ) { wp_cache_set( $cache_key, $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 ); } /** * Repair coupon lookup entries with zero discount_amount. A bug in WC 9.9 (fixed in 10.0) * caused discount_amount to be set to zero when a coupon code was used with * different case (e.g. "10-off" vs "10-OFF"). * * @since 10.1.0 * @return array Array with 'success' boolean and 'message' string. */ function wc_repair_zero_discount_coupons_lookup_table() { global $wpdb; $table_name = $wpdb->prefix . 'wc_order_coupon_lookup'; // Check if table exists. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared if ( $wpdb->get_var( "SHOW TABLES LIKE '$table_name'" ) !== $table_name ) { return array( 'success' => false, 'message' => __( 'Coupons lookup table does not exist.', 'woocommerce' ), ); } // Get entries with zero discount_amount. $zero_discount_entries = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT order_id, coupon_id FROM $table_name WHERE discount_amount = %f", 0.0 ), ARRAY_A ); if ( empty( $zero_discount_entries ) ) { return array( 'success' => true, 'message' => __( 'No entries with zero discount amount found. Coupons lookup table is up to date.', 'woocommerce' ), ); } $processed_count = 0; $error_count = 0; foreach ( $zero_discount_entries as $entry ) { try { $result = CouponsDataStore::sync_order_coupons( $entry['order_id'] ); if ( false !== $result ) { ++$processed_count; } else { ++$error_count; } } catch ( Exception $e ) { ++$error_count; $logger = wc_get_logger(); $logger->error( sprintf( 'Error fixing coupon lookup entry for order %d: %s', $entry['order_id'], $e->getMessage() ), array( 'source' => 'coupons-lookup-fix', 'order_id' => $entry['order_id'], 'error' => $e, ) ); } } // Clear any related caches. wp_cache_flush_group( 'coupons' ); WC_Cache_Helper::get_transient_version( 'woocommerce_reports', true ); $message = sprintf( /* translators: %1$d: number of entries processed, %2$d: number of errors */ __( 'Coupons lookup table entries with zero discount amount repaired successfully. Processed %1$d entries with %2$d errors.', 'woocommerce' ), $processed_count, $error_count ); return array( 'success' => true, 'message' => $message, ); }/** * WooCommerce Customer Functions * * Functions for customers. * * @package WooCommerce\Functions * @version 2.2.0 */ use Automattic\WooCommerce\Enums\OrderInternalStatus; use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; use Automattic\WooCommerce\Internal\Utilities\Users; use Automattic\WooCommerce\Utilities\OrderUtil; defined( 'ABSPATH' ) || exit; /** * Prevent any user who cannot 'edit_posts' (subscribers, customers etc) from seeing the admin bar. * * Note: get_option( 'woocommerce_lock_down_admin', true ) is a deprecated option here for backwards compatibility. Defaults to true. * * @param bool $show_admin_bar If should display admin bar. * @return bool */ function wc_disable_admin_bar( $show_admin_bar ) { /** * Controls whether the WooCommerce admin bar should be disabled. * * @since 3.0.0 * * @param bool $enabled */ if ( apply_filters( 'woocommerce_disable_admin_bar', true ) && ! ( current_user_can( 'edit_posts' ) || current_user_can( 'manage_woocommerce' ) ) ) { $show_admin_bar = false; } return $show_admin_bar; } add_filter( 'show_admin_bar', 'wc_disable_admin_bar', 10, 1 ); // phpcs:ignore WordPress.VIP.AdminBarRemoval.RemovalDetected if ( ! function_exists( 'wc_create_new_customer' ) ) { /** * Create a new customer. * * @since 9.4.0 Moved woocommerce_registration_error_email_exists filter to the shortcode checkout class. * @since 9.4.0 Removed handling for generating username/password based on settings--this is consumed at form level. Here, if data is missing it will be generated. * * @param string $email Customer email. * @param string $username Customer username. * @param string $password Customer password. * @param array $args List of arguments to pass to `wp_insert_user()`. * @return int|WP_Error Returns WP_Error on failure, Int (user ID) on success. */ function wc_create_new_customer( $email, $username = '', $password = '', $args = array() ) { if ( empty( $email ) || ! is_email( $email ) ) { return new WP_Error( 'registration-error-invalid-email', __( 'Please provide a valid email address.', 'woocommerce' ) ); } if ( email_exists( $email ) ) { return new WP_Error( 'registration-error-email-exists', sprintf( // Translators: %s Email address. esc_html__( 'An account is already registered with %s. Please log in or use a different email address.', 'woocommerce' ), esc_html( $email ) ) ); } if ( empty( $username ) ) { $username = wc_create_new_customer_username( $email, $args ); } $username = sanitize_user( $username ); if ( empty( $username ) || ! validate_username( $username ) ) { return new WP_Error( 'registration-error-invalid-username', __( 'Please provide a valid account username.', 'woocommerce' ) ); } if ( username_exists( $username ) ) { return new WP_Error( 'registration-error-username-exists', __( 'An account is already registered with that username. Please choose another.', 'woocommerce' ) ); } // Handle password creation. $password_generated = false; if ( empty( $password ) ) { $password = wp_generate_password(); $password_generated = true; } if ( empty( $password ) ) { return new WP_Error( 'registration-error-missing-password', __( 'Please create a password for your account.', 'woocommerce' ) ); } // Use WP_Error to handle registration errors. $errors = new WP_Error(); /** * Fires before a customer account is registered. * * This hook fires before customer accounts are created and passes the form data (username, email) and an array * of errors. * * This could be used to add extra validation logic and append errors to the array. * * @since 7.2.0 * * @internal Matches filter name in WooCommerce core. * * @param string $username Customer username. * @param string $user_email Customer email address. * @param \WP_Error $errors Error object. */ do_action( 'woocommerce_register_post', $username, $email, $errors ); /** * Filters registration errors before a customer account is registered. * * This hook filters registration errors. This can be used to manipulate the array of errors before * they are displayed. * * @since 7.2.0 * * @internal Matches filter name in WooCommerce core. * * @param \WP_Error $errors Error object. * @param string $username Customer username. * @param string $user_email Customer email address. * @return \WP_Error */ $errors = apply_filters( 'woocommerce_registration_errors', $errors, $username, $email ); if ( is_wp_error( $errors ) && $errors->get_error_code() ) { return $errors; } // Merged passed args with sanitized username, email, and password. $customer_data = array_merge( $args, array( 'user_login' => $username, 'user_pass' => $password, 'user_email' => $email, 'role' => 'customer', ) ); /** * Filters customer data before a customer account is registered. * * This hook filters customer data. It allows user data to be changed, for example, username, password, email, * first name, last name, and role. * * @since 7.2.0 * * @param array $customer_data An array of customer (user) data. * @return array */ $new_customer_data = apply_filters( 'woocommerce_new_customer_data', wp_parse_args( $customer_data, array( 'first_name' => '', 'last_name' => '', 'source' => 'unknown', ) ) ); $customer_id = wp_insert_user( $new_customer_data ); if ( is_wp_error( $customer_id ) ) { return $customer_id; } // Set account flag to remind customer to update generated password. if ( $password_generated ) { update_user_option( $customer_id, 'default_password_nag', true, true ); } /** * Fires after a customer account has been registered. * * This hook fires after customer accounts are created and passes the customer data. * * @since 7.2.0 * * @internal Matches filter name in WooCommerce core. * * @param integer $customer_id New customer (user) ID. * @param array $new_customer_data Array of customer (user) data. * @param string $password_generated The generated password for the account. */ do_action( 'woocommerce_created_customer', $customer_id, $new_customer_data, $password_generated ); return $customer_id; } } /** * Create a unique username for a new customer. * * @since 3.6.0 * @param string $email New customer email address. * @param array $new_user_args Array of new user args, maybe including first and last names. * @param string $suffix Append string to username to make it unique. * @return string Generated username. */ function wc_create_new_customer_username( $email, $new_user_args = array(), $suffix = '' ) { $username_parts = array(); if ( isset( $new_user_args['first_name'] ) ) { $username_parts[] = sanitize_user( $new_user_args['first_name'], true ); } if ( isset( $new_user_args['last_name'] ) ) { $username_parts[] = sanitize_user( $new_user_args['last_name'], true ); } // Remove empty parts. $username_parts = array_filter( $username_parts ); // If there are no parts, e.g. name had unicode chars, or was not provided, fallback to email. if ( empty( $username_parts ) ) { $email_parts = explode( '@', $email ); $email_username = $email_parts[0]; // Exclude common prefixes. if ( in_array( $email_username, array( 'sales', 'hello', 'mail', 'contact', 'info', ), true ) ) { // Get the domain part. $email_username = $email_parts[1]; } $username_parts[] = sanitize_user( $email_username, true ); } $username = wc_strtolower( implode( '.', $username_parts ) ); if ( $suffix ) { $username .= $suffix; } /** * WordPress 4.4 - filters the list of blocked usernames. * * @since 3.7.0 * @param array $usernames Array of blocked usernames. */ $illegal_logins = (array) apply_filters( 'illegal_user_logins', array() ); // Stop illegal logins and generate a new random username. if ( in_array( strtolower( $username ), array_map( 'strtolower', $illegal_logins ), true ) ) { $new_args = array(); /** * Filter generated customer username. * * @since 3.7.0 * @param string $username Generated username. * @param string $email New customer email address. * @param array $new_user_args Array of new user args, maybe including first and last names. * @param string $suffix Append string to username to make it unique. */ $new_args['first_name'] = apply_filters( 'woocommerce_generated_customer_username', 'woo_user_' . zeroise( wp_rand( 0, 9999 ), 4 ), $email, $new_user_args, $suffix ); return wc_create_new_customer_username( $email, $new_args, $suffix ); } if ( username_exists( $username ) ) { // Generate something unique to append to the username in case of a conflict with another user. $suffix = '-' . zeroise( wp_rand( 0, 9999 ), 4 ); return wc_create_new_customer_username( $email, $new_user_args, $suffix ); } /** * Filter new customer username. * * @since 3.7.0 * @param string $username Customer username. * @param string $email New customer email address. * @param array $new_user_args Array of new user args, maybe including first and last names. * @param string $suffix Append string to username to make it unique. */ return apply_filters( 'woocommerce_new_customer_username', $username, $email, $new_user_args, $suffix ); } /** * Login a customer (set auth cookie and set global user object). * * @param int $customer_id Customer ID. */ function wc_set_customer_auth_cookie( $customer_id ) { wp_set_current_user( $customer_id ); wp_set_auth_cookie( $customer_id, true ); // Update session. if ( is_callable( array( WC()->session, 'init_session_cookie' ) ) ) { WC()->session->init_session_cookie(); } } /** * Get past orders (by email) and update them. * * @param int $customer_id Customer ID. * @return int */ function wc_update_new_customer_past_orders( $customer_id ) { $linked = 0; $complete = 0; $customer = get_user_by( 'id', absint( $customer_id ) ); $customer_orders = wc_get_orders( array( 'limit' => -1, 'customer' => array( array( 0, $customer->user_email ) ), 'return' => 'ids', ) ); if ( ! empty( $customer_orders ) ) { foreach ( $customer_orders as $order_id ) { $order = wc_get_order( $order_id ); if ( ! $order ) { continue; } $order->set_customer_id( $customer->ID ); $order->save(); if ( $order->has_downloadable_item() ) { $data_store = WC_Data_Store::load( 'customer-download' ); $data_store->delete_by_order_id( $order->get_id() ); wc_downloadable_product_permissions( $order->get_id(), true ); } do_action( 'woocommerce_update_new_customer_past_order', $order_id, $customer ); if ( $order->get_status() === OrderInternalStatus::COMPLETED ) { ++$complete; } ++$linked; } } if ( $complete ) { update_user_meta( $customer_id, 'paying_customer', 1 ); Users::update_site_user_meta( $customer_id, 'wc_order_count', '' ); Users::update_site_user_meta( $customer_id, 'wc_money_spent', '' ); Users::delete_site_user_meta( $customer_id, 'wc_last_order' ); } return $linked; } /** * Order payment completed - This is a paying customer. * * @param int $order_id Order ID. */ function wc_paying_customer( $order_id ) { $order = wc_get_order( $order_id ); $customer_id = $order->get_customer_id(); if ( $customer_id > 0 && 'shop_order_refund' !== $order->get_type() ) { $customer = new WC_Customer( $customer_id ); if ( ! $customer->get_is_paying_customer() ) { $customer->set_is_paying_customer( true ); $customer->save(); } } } add_action( 'woocommerce_payment_complete', 'wc_paying_customer' ); add_action( 'woocommerce_order_status_completed', 'wc_paying_customer' ); /** * Checks if a user (by email or ID or both) has bought an item. * * @param string $customer_email Customer email to check. * @param int $user_id User ID to check. * @param int $product_id Product ID to check. * @return bool */ function wc_customer_bought_product( $customer_email, $user_id, $product_id ) { global $wpdb; $result = apply_filters( 'woocommerce_pre_customer_bought_product', null, $customer_email, $user_id, $product_id ); if ( null !== $result ) { return $result; } /** * Whether to use lookup tables - it can optimize performance, but correctness depends on the frequency of the AS job. * * @since 9.7.0 * * @param bool $enabled * @param string $customer_email Customer email to check. * @param int $user_id User ID to check. * @param int $product_id Product ID to check. * @return bool */ $use_lookup_tables = apply_filters( 'woocommerce_customer_bought_product_use_lookup_tables', false, $customer_email, $user_id, $product_id ); if ( $use_lookup_tables ) { // Lookup tables get refreshed along with the `woocommerce_reports` transient version (due to async processing). // With high orders placement rate, this caching here will be short-lived (suboptimal for BFCM/Christmas and busy stores in general). $cache_version = WC_Cache_Helper::get_transient_version( 'woocommerce_reports' ); } elseif ( '' === $customer_email && $user_id ) { // Optimized: for specific customers version with orders count (it's a user meta from in-memory populated datasets). // Best-case scenario for caching here, as it only depends on the customer orders placement rate. $cache_version = wc_get_customer_order_count( $user_id ); } else { // Fallback: create, update, and delete operations on orders clears caches and refreshes `orders` transient version. // With high orders placement rate, this caching here will be short-lived (suboptimal for BFCM/Christmas and busy stores in general). // For the core, no use-cases for this branch. Themes/extensions are still valid use-cases. $cache_version = WC_Cache_Helper::get_transient_version( 'orders' ); } $cache_group = 'orders'; $cache_key = 'wc_customer_bought_product_' . md5( $customer_email . '-' . $user_id . '-' . $use_lookup_tables ); $cache_value = wp_cache_get( $cache_key, $cache_group ); if ( isset( $cache_value['value'], $cache_value['version'] ) && $cache_value['version'] === $cache_version ) { $result = $cache_value['value']; } else { $customer_data = array( $user_id ); if ( $user_id ) { $user = get_user_by( 'id', $user_id ); if ( isset( $user->user_email ) ) { $customer_data[] = $user->user_email; } } if ( is_email( $customer_email ) ) { $customer_data[] = $customer_email; } $customer_data = array_map( 'esc_sql', array_filter( array_unique( $customer_data ) ) ); $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); if ( count( $customer_data ) === 0 ) { return false; } if ( OrderUtil::custom_orders_table_usage_is_enabled() ) { $statuses = array_map( function ( $status ) { return "wc-$status"; }, $statuses ); $order_table = OrdersTableDataStore::get_orders_table_name(); $user_id_clause = ''; if ( $user_id ) { $user_id_clause = 'OR o.customer_id = ' . absint( $user_id ); } if ( $use_lookup_tables ) { // HPOS: yes, Lookup table: yes. $sql = " SELECT DISTINCT product_or_variation_id FROM ( SELECT CASE WHEN product_id != 0 THEN product_id ELSE variation_id END AS product_or_variation_id FROM {$wpdb->prefix}wc_order_product_lookup lookup INNER JOIN $order_table AS o ON lookup.order_id = o.ID WHERE o.status IN ('" . implode( "','", $statuses ) . "') AND ( o.billing_email IN ('" . implode( "','", $customer_data ) . "') $user_id_clause ) ) AS subquery WHERE product_or_variation_id != 0 "; } else { // HPOS: yes, Lookup table: no. $sql = " SELECT DISTINCT im.meta_value FROM $order_table AS o INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON o.id = i.order_id INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id WHERE o.status IN ('" . implode( "','", $statuses ) . "') AND im.meta_key IN ('_product_id', '_variation_id' ) AND im.meta_value != 0 AND ( o.billing_email IN ('" . implode( "','", $customer_data ) . "') $user_id_clause ) "; } $result = $wpdb->get_col( $sql ); } elseif ( $use_lookup_tables ) { // HPOS: no, Lookup table: yes. $result = $wpdb->get_col( " SELECT DISTINCT product_or_variation_id FROM ( SELECT CASE WHEN lookup.product_id != 0 THEN lookup.product_id ELSE lookup.variation_id END AS product_or_variation_id FROM {$wpdb->prefix}wc_order_product_lookup AS lookup INNER JOIN {$wpdb->posts} AS p ON p.ID = lookup.order_id INNER JOIN {$wpdb->postmeta} AS pm ON p.ID = pm.post_id WHERE p.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) AND pm.meta_key IN ( '_billing_email', '_customer_user' ) AND pm.meta_value IN ( '" . implode( "','", $customer_data ) . "' ) ) AS subquery WHERE product_or_variation_id != 0 " ); // WPCS: unprepared SQL ok. } else { // HPOS: no, Lookup table: no. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared $result = $wpdb->get_col( " SELECT DISTINCT im.meta_value FROM {$wpdb->posts} AS p INNER JOIN {$wpdb->postmeta} AS pm ON p.ID = pm.post_id INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON p.ID = i.order_id INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id WHERE p.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) AND p.post_type = 'shop_order' AND pm.meta_key IN ( '_billing_email', '_customer_user' ) AND im.meta_key IN ( '_product_id', '_variation_id' ) AND im.meta_value != 0 AND pm.meta_value IN ( '" . implode( "','", $customer_data ) . "' ) " ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared } $result = array_map( 'absint', $result ); wp_cache_set( $cache_key, array( 'version' => $cache_version, 'value' => $result, ), $cache_group, MONTH_IN_SECONDS ); } return in_array( absint( $product_id ), $result, true ); } /** * Checks if the current user has a role. * * @param string $role The role. * @return bool */ function wc_current_user_has_role( $role ) { return wc_user_has_role( wp_get_current_user(), $role ); } /** * Checks if a user has a role. * * @param int|\WP_User $user The user. * @param string $role The role. * @return bool */ function wc_user_has_role( $user, $role ) { if ( ! is_object( $user ) ) { $user = get_userdata( $user ); } if ( ! $user || ! $user->exists() ) { return false; } return in_array( $role, $user->roles, true ); } /** * Checks if a user has a certain capability. * * @param array $allcaps All capabilities. * @param array $caps Capabilities. * @param array $args Arguments. * * @return array The filtered array of all capabilities. */ function wc_customer_has_capability( $allcaps, $caps, $args ) { if ( isset( $caps[0] ) ) { switch ( $caps[0] ) { case 'view_order': $user_id = intval( $args[1] ); $order = wc_get_order( $args[2] ); if ( $order && $user_id === $order->get_user_id() ) { $allcaps['view_order'] = true; } break; case 'pay_for_order': $user_id = intval( $args[1] ); $order_id = isset( $args[2] ) ? $args[2] : null; // When no order ID, we assume it's a new order // and thus, customer can pay for it. if ( ! $order_id ) { $allcaps['pay_for_order'] = true; break; } $order = wc_get_order( $order_id ); if ( $order && ( $user_id === $order->get_user_id() || ! $order->get_user_id() ) ) { $allcaps['pay_for_order'] = true; } break; case 'order_again': $user_id = intval( $args[1] ); $order = wc_get_order( $args[2] ); if ( $order && $user_id === $order->get_user_id() ) { $allcaps['order_again'] = true; } break; case 'cancel_order': $user_id = intval( $args[1] ); $order = wc_get_order( $args[2] ); if ( $order && $user_id === $order->get_user_id() ) { $allcaps['cancel_order'] = true; } break; case 'download_file': $user_id = intval( $args[1] ); $download = $args[2]; if ( $download && $user_id === $download->get_user_id() ) { $allcaps['download_file'] = true; } break; } } return $allcaps; } add_filter( 'user_has_cap', 'wc_customer_has_capability', 10, 3 ); /** * Safe way of allowing shop managers restricted capabilities that will remove * access to the capabilities if WooCommerce is deactivated. * * @since 3.5.4 * @param bool[] $allcaps Array of key/value pairs where keys represent a capability name and boolean values * represent whether the user has that capability. * @param string[] $caps Required primitive capabilities for the requested capability. * @param array $args Arguments that accompany the requested capability check. * @param WP_User $user The user object. * @return bool[] */ function wc_shop_manager_has_capability( $allcaps, $caps, $args, $user ) { if ( wc_user_has_role( $user, 'shop_manager' ) ) { // @see wc_modify_map_meta_cap, which limits editing to customers. $allcaps['edit_users'] = true; } return $allcaps; } add_filter( 'user_has_cap', 'wc_shop_manager_has_capability', 10, 4 ); /** * Modify the list of editable roles to prevent non-admin adding admin users. * * @param array $roles Roles. * @return array */ function wc_modify_editable_roles( $roles ) { if ( is_multisite() && is_super_admin() ) { return $roles; } if ( ! wc_current_user_has_role( 'administrator' ) ) { unset( $roles['administrator'] ); if ( wc_current_user_has_role( 'shop_manager' ) ) { $shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); return array_intersect_key( $roles, array_flip( $shop_manager_editable_roles ) ); } } return $roles; } add_filter( 'editable_roles', 'wc_modify_editable_roles' ); /** * Modify capabilities to prevent non-admin users editing admin users. * * $args[0] will be the user being edited in this case. * * @param array $caps Array of caps. * @param string $cap Name of the cap we are checking. * @param int $user_id ID of the user being checked against. * @param array $args Arguments. * @return array */ function wc_modify_map_meta_cap( $caps, $cap, $user_id, $args ) { if ( is_multisite() && is_super_admin() ) { return $caps; } switch ( $cap ) { case 'edit_user': case 'remove_user': case 'promote_user': case 'delete_user': if ( ! isset( $args[0] ) || $args[0] === $user_id ) { break; } elseif ( ! wc_current_user_has_role( 'administrator' ) ) { if ( wc_user_has_role( $args[0], 'administrator' ) ) { $caps[] = 'do_not_allow'; } elseif ( wc_current_user_has_role( 'shop_manager' ) ) { // Shop managers can only edit customer info. $userdata = get_userdata( $args[0] ); $shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment if ( property_exists( $userdata, 'roles' ) && ! empty( $userdata->roles ) && ! array_intersect( $userdata->roles, $shop_manager_editable_roles ) ) { $caps[] = 'do_not_allow'; } } } break; } return $caps; } add_filter( 'map_meta_cap', 'wc_modify_map_meta_cap', 10, 4 ); /** * Get customer download permissions from the database. * * @param int $customer_id Customer/User ID. * @return array */ function wc_get_customer_download_permissions( $customer_id ) { $data_store = WC_Data_Store::load( 'customer-download' ); return apply_filters( 'woocommerce_permission_list', $data_store->get_downloads_for_customer( $customer_id ), $customer_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment } /** * Get customer available downloads. * * @param int $customer_id Customer/User ID. * @return array */ function wc_get_customer_available_downloads( $customer_id ) { $downloads = array(); $_product = null; $order = null; $file_number = 0; // Get results from valid orders only. $results = wc_get_customer_download_permissions( $customer_id ); if ( $results ) { foreach ( $results as $result ) { $order_id = intval( $result->order_id ); if ( ! $order || $order->get_id() !== $order_id ) { // New order. $order = wc_get_order( $order_id ); $_product = null; } // Make sure the order exists for this download. if ( ! $order ) { continue; } // Check if downloads are permitted. if ( ! $order->is_download_permitted() ) { continue; } $product_id = intval( $result->product_id ); if ( ! $_product || $_product->get_id() !== $product_id ) { // New product. $file_number = 0; $_product = wc_get_product( $product_id ); } // Check product exists and has the file. if ( ! $_product || ! $_product->exists() || ! $_product->has_file( $result->download_id ) ) { continue; } $download_file = $_product->get_file( $result->download_id ); // If the downloadable file has been disabled (it may be located in an untrusted location) then do not return it. if ( ! $download_file->get_enabled() ) { continue; } // Download name will be 'Product Name' for products with a single downloadable file, and 'Product Name - File X' for products with multiple files. // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment $download_name = apply_filters( 'woocommerce_downloadable_product_name', $download_file['name'], $_product, $result->download_id, $file_number ); $downloads[] = array( 'download_url' => add_query_arg( array( 'download_file' => $product_id, 'order' => $result->order_key, 'email' => rawurlencode( $result->user_email ), 'key' => $result->download_id, ), home_url( '/' ) ), 'download_id' => $result->download_id, 'product_id' => $_product->get_id(), 'product_name' => $_product->get_name(), 'product_url' => $_product->is_visible() ? $_product->get_permalink() : '', // Since 3.3.0. 'download_name' => $download_name, 'order_id' => $order->get_id(), 'order_key' => $order->get_order_key(), 'downloads_remaining' => $result->downloads_remaining, 'access_expires' => $result->access_expires, 'file' => array( 'name' => $download_file->get_name(), 'file' => $download_file->get_file(), ), ); ++$file_number; } } // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment return apply_filters( 'woocommerce_customer_available_downloads', $downloads, $customer_id ); } /** * Get total spent by customer. * * @param int $user_id User ID. * @return string */ function wc_get_customer_total_spent( $user_id ) { $customer = new WC_Customer( $user_id ); return $customer->get_total_spent(); } /** * Get total orders by customer. * * @param int $user_id User ID. * @return int */ function wc_get_customer_order_count( $user_id ) { $customer = new WC_Customer( $user_id ); return $customer->get_order_count(); } /** * Reset _customer_user on orders when a user is deleted. * * @param int $user_id User ID. */ function wc_reset_order_customer_id_on_deleted_user( $user_id ) { global $wpdb; if ( OrderUtil::custom_orders_table_usage_is_enabled() ) { $order_table_ds = wc_get_container()->get( OrdersTableDataStore::class ); $order_table = $order_table_ds::get_orders_table_name(); $wpdb->update( $order_table, array( 'customer_id' => 0, 'date_updated_gmt' => current_time( 'mysql', true ), ), array( 'customer_id' => $user_id, ), array( '%d', '%s', ), array( '%d', ) ); } if ( ! OrderUtil::custom_orders_table_usage_is_enabled() || OrderUtil::is_custom_order_tables_in_sync() ) { $wpdb->update( $wpdb->postmeta, array( 'meta_value' => 0, //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value ), array( 'meta_key' => '_customer_user', //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $user_id, //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value ) ); } } add_action( 'deleted_user', 'wc_reset_order_customer_id_on_deleted_user' ); /** * Get review verification status. * * @param int $comment_id Comment ID. * @return bool */ function wc_review_is_from_verified_owner( $comment_id ) { $verified = get_comment_meta( $comment_id, 'verified', true ); return '' === $verified ? WC_Comments::add_comment_purchase_verification( $comment_id ) : (bool) $verified; } /** * Disable author archives for customers. * * @since 2.5.0 */ function wc_disable_author_archives_for_customers() { global $author; if ( is_author() ) { $user = get_user_by( 'id', $author ); if ( user_can( $user, 'customer' ) && ! user_can( $user, 'edit_posts' ) ) { wp_safe_redirect( wc_get_page_permalink( 'shop' ) ); exit; } } } add_action( 'template_redirect', 'wc_disable_author_archives_for_customers' ); /** * Hooks into the `profile_update` hook to set the user last updated timestamp. * * @since 2.6.0 * @param int $user_id The user that was updated. * @param array $old The profile fields pre-change. */ function wc_update_profile_last_update_time( $user_id, $old ) { wc_set_user_last_update_time( $user_id ); } add_action( 'profile_update', 'wc_update_profile_last_update_time', 10, 2 ); /** * Hooks into the update user meta function to set the user last updated timestamp. * * @since 2.6.0 * @param int $meta_id ID of the meta object that was changed. * @param int $user_id The user that was updated. * @param string $meta_key Name of the meta key that was changed. * @param mixed $_meta_value Value of the meta that was changed. */ function wc_meta_update_last_update_time( $meta_id, $user_id, $meta_key, $_meta_value ) { $keys_to_track = apply_filters( 'woocommerce_user_last_update_fields', array( 'first_name', 'last_name' ) ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment $update_time = in_array( $meta_key, $keys_to_track, true ) ? true : false; $update_time = 'billing_' === substr( $meta_key, 0, 8 ) ? true : $update_time; $update_time = 'shipping_' === substr( $meta_key, 0, 9 ) ? true : $update_time; if ( $update_time ) { wc_set_user_last_update_time( $user_id ); } } add_action( 'update_user_meta', 'wc_meta_update_last_update_time', 10, 4 ); /** * Sets a user's "last update" time to the current timestamp. * * @since 2.6.0 * @param int $user_id The user to set a timestamp for. */ function wc_set_user_last_update_time( $user_id ) { update_user_meta( $user_id, 'last_update', gmdate( 'U' ) ); } /** * Get customer saved payment methods list. * * @since 2.6.0 * @param int $customer_id Customer ID. * @return array */ function wc_get_customer_saved_methods_list( $customer_id ) { return apply_filters( 'woocommerce_saved_payment_methods_list', array(), $customer_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment } /** * Get info about customer's last order. * * @since 2.6.0 * @param int $customer_id Customer ID. * @return WC_Order|bool Order object if successful or false. */ function wc_get_customer_last_order( $customer_id ) { $customer = new WC_Customer( $customer_id ); return $customer->get_last_order(); } /** * When a user is deleted in WordPress, delete corresponding WooCommerce data. * * @param int $user_id User ID being deleted. */ function wc_delete_user_data( $user_id ) { global $wpdb; // Clean up sessions. $wpdb->delete( $wpdb->prefix . 'woocommerce_sessions', array( 'session_key' => $user_id, ) ); // Revoke API keys. $wpdb->delete( $wpdb->prefix . 'woocommerce_api_keys', array( 'user_id' => $user_id, ) ); // Clean up payment tokens. $payment_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id ); foreach ( $payment_tokens as $payment_token ) { $payment_token->delete(); } } add_action( 'delete_user', 'wc_delete_user_data' ); /** * Store user agents. Used for tracker. * * @since 3.0.0 * @param string $user_login User login. * @param int|object $user User. */ function wc_maybe_store_user_agent( $user_login, $user ) { if ( 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) && user_can( $user, 'manage_woocommerce' ) ) { $admin_user_agents = array_filter( (array) get_option( 'woocommerce_tracker_ua', array() ) ); $admin_user_agents[] = wc_get_user_agent(); update_option( 'woocommerce_tracker_ua', array_unique( $admin_user_agents ), false ); } } add_action( 'wp_login', 'wc_maybe_store_user_agent', 10, 2 ); /** * Update logic triggered on login. * * @since 3.4.0 * @param string $user_login User login. * @param object $user User. */ function wc_user_logged_in( $user_login, $user ) { wc_update_user_last_active( $user->ID ); update_user_meta( $user->ID, '_woocommerce_load_saved_cart_after_login', 1 ); } add_action( 'wp_login', 'wc_user_logged_in', 10, 2 ); /** * Update when the user was last active. * * @since 3.4.0 */ function wc_current_user_is_active() { if ( ! is_user_logged_in() ) { return; } wc_update_user_last_active( get_current_user_id() ); } add_action( 'wp', 'wc_current_user_is_active', 10 ); /** * Set the user last active timestamp to now. * * @since 3.4.0 * @param int $user_id User ID to mark active. */ function wc_update_user_last_active( $user_id ) { if ( ! $user_id ) { return; } update_user_meta( $user_id, 'wc_last_active', (string) strtotime( gmdate( 'Y-m-d', time() ) ) ); } /** * Translate WC roles using the woocommerce textdomain. * * @since 3.7.0 * @param string $translation Translated text. * @param string $text Text to translate. * @param string $context Context information for the translators. * @param string $domain Text domain. Unique identifier for retrieving translated strings. * @return string */ function wc_translate_user_roles( $translation, $text, $context, $domain ) { // translate_user_role() only accepts a second parameter starting in WP 5.2. if ( version_compare( get_bloginfo( 'version' ), '5.2', '<' ) ) { return $translation; } if ( 'User role' === $context && 'default' === $domain && in_array( $text, array( 'Shop manager', 'Customer' ), true ) ) { return translate_user_role( $text, 'woocommerce' ); } return $translation; } add_filter( 'gettext_with_context', 'wc_translate_user_roles', 10, 4 );/** * WooCommerce Order Functions * * Functions for order specific things. * * @package WooCommerce\Functions * @version 3.4.0 */ use Automattic\WooCommerce\Caches\OrderCountCache; use Automattic\WooCommerce\Enums\OrderStatus; use Automattic\WooCommerce\Enums\OrderInternalStatus; use Automattic\WooCommerce\Enums\PaymentGatewayFeature; use Automattic\WooCommerce\Internal\CostOfGoodsSold\CostOfGoodsSoldController; use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer; use Automattic\WooCommerce\Internal\Utilities\Users; use Automattic\WooCommerce\Utilities\OrderUtil; use Automattic\WooCommerce\Utilities\StringUtil; defined( 'ABSPATH' ) || exit; /** * Standard way of retrieving orders based on certain parameters. * * This function should be used for order retrieval so that when we move to * custom tables, functions still work. * * Args and usage: https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query * * @since 2.6.0 * @param array $args Array of args (above). * @return WC_Order[]|stdClass Number of pages and an array of order objects if * paginate is true, or just an array of values. */ function wc_get_orders( $args ) { $map_legacy = array( 'numberposts' => '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( OrderInternalStatus::PENDING => _x( 'Pending payment', 'Order status', 'woocommerce' ), OrderInternalStatus::PROCESSING => _x( 'Processing', 'Order status', 'woocommerce' ), OrderInternalStatus::ON_HOLD => _x( 'On hold', 'Order status', 'woocommerce' ), OrderInternalStatus::COMPLETED => _x( 'Completed', 'Order status', 'woocommerce' ), OrderInternalStatus::CANCELLED => _x( 'Cancelled', 'Order status', 'woocommerce' ), OrderInternalStatus::REFUNDED => _x( 'Refunded', 'Order status', 'woocommerce' ), OrderInternalStatus::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() { /** * Filter the list of statuses which are considered 'paid'. * * @since 3.0.0 * * @param array $statuses List of statuses. */ return apply_filters( 'woocommerce_order_is_paid_statuses', array( OrderStatus::PROCESSING, OrderStatus::COMPLETED ) ); } /** * Get list of statuses which are consider 'pending payment'. * * @since 3.6.0 * @return array */ function wc_get_is_pending_statuses() { /** * Filter the list of statuses which are considered 'pending payment'. * * @since 3.6.0 * * @param array $statuses List of statuses. */ return apply_filters( 'woocommerce_order_is_pending_statuses', array( OrderStatus::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 ) { // "Special statuses": these are in common usage across WooCommerce, but are not normally returned by // wc_get_order_statuses(). $special_statuses = array( 'wc-' . OrderStatus::AUTO_DRAFT => OrderStatus::AUTO_DRAFT, 'wc-' . OrderStatus::TRASH => OrderStatus::TRASH, ); // Merge order is important. If the special statuses are ever returned by wc_get_order_statuses(), those definitions // should take priority. $statuses = array_merge( $special_statuses, wc_get_order_statuses() ); $unprefixed = OrderUtil::remove_status_prefix( (string) $status ); if ( ! is_string( $status ) ) { wc_doing_it_wrong( __FUNCTION__, __( 'An invalid order status slug was supplied.', 'woocommerce' ), '9.6' ); } return $statuses[ 'wc-' . $unprefixed ] ?? $unprefixed; } /** * 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: * - 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( '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( OrderStatus::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( OrderStatus::DRAFT, OrderStatus::TRASH, ); $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 ); try { $types_for_count = $type ? array( $type ) : $valid_types; $order_count_cache = new OrderCountCache(); foreach ( $types_for_count as $type ) { $cache = $order_count_cache->get( $type, array( $status ) ); if ( false !== $cache && isset( $cache[ $status ] ) ) { $count += $cache[ $status ]; } else { $count_for_type = OrderUtil::get_count_for_type( $type ); $count += $count_for_type[ $status ]; } } return $count; } catch ( Exception $e ) { return 0; } } /** * 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( OrderStatus::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(); Users::delete_site_user_meta( $order->get_customer_id(), 'wc_money_spent' ); Users::delete_site_user_meta( $order->get_customer_id(), 'wc_order_count' ); Users::delete_site_user_meta( $order->get_customer_id(), 'wc_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'] ); $refunded_order_and_products = array(); 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 ( is_array( $args['line_items'] ) && 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; } // array of order id and product id which were refunded. // later to be used for revoking download permission. // checking if the item is a product, as we only need to revoke download permission for products. if ( $item->is_type( 'line_item' ) ) { $refunded_order_and_products[ $item_id ] = array( 'order_id' => $order->get_id(), 'product_id' => $item->get_product_id(), ); } $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'] ); } // delete downloads that were refunded using order and product id, if present. if ( ! empty( $refunded_order_and_products ) ) { foreach ( $refunded_order_and_products as $refunded_order_and_product ) { $download_data_store = WC_Data_Store::load( 'customer-download' ); $downloads = $download_data_store->get_downloads( $refunded_order_and_product ); if ( ! empty( $downloads ) ) { foreach ( $downloads as $download ) { $download_data_store->delete_by_id( $download->get_id() ); } } } } /** * 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 || ( ! empty( $args['line_items'] ) && $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() ); /** * Filter the status to set the order to when fully refunded. * * @since 2.7.0 * * @param string $parent_status The status to set the order to when fully refunded. * @param int $order_id The order ID. * @param int $refund_id The refund ID. */ $parent_status = apply_filters( 'woocommerce_order_fully_refunded_status', OrderStatus::REFUNDED, $order->get_id(), $refund->get_id() ); if ( $parent_status ) { $order->update_status( $parent_status ); } } } $order->set_date_modified( time() ); if ( wc_get_container()->get( CostOfGoodsSoldController::class )->feature_is_enabled() && $order->has_cogs() ) { $order->calculate_cogs_total_value(); } $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( PaymentGatewayFeature::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( OrderStatus::CANCELLED, OrderStatus::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 ); $invalid_statuses = array( OrderStatus::CANCELLED, OrderStatus::FAILED, OrderStatus::TRASH ); /** * Allow invalid order status filtering for updating coupon usage. * * @since 9.0.0 * * @param array $invalid_statuses Array of statuses to consider invalid. */ $invalid_statuses = apply_filters( 'woocommerce_update_coupon_usage_invalid_statuses', $invalid_statuses ); if ( $order->has_status( $invalid_statuses ) && $has_recorded ) { $action = 'reduce'; $order->get_data_store()->set_recorded_coupon_usage_counts( $order, false ); } elseif ( ! $order->has_status( $invalid_statuses ) && ! $has_recorded ) { $action = 'increase'; $order->get_data_store()->set_recorded_coupon_usage_counts( $order, true ); } elseif ( $order->has_status( $invalid_statuses ) ) { wc_release_coupons_for_order( $order ); 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; } } wc_release_coupons_for_order( $order ); } } 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' ); add_action( 'woocommerce_order_status_failed', 'wc_update_coupon_usage_counts' ); add_action( 'woocommerce_trash_order', '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', '60' ); // Clear existing scheduled events (both Action Scheduler and WP-Cron). if ( function_exists( 'as_unschedule_all_actions' ) ) { as_unschedule_all_actions( 'woocommerce_cancel_unpaid_orders' ); } // Always clear WP-Cron events as well in case they exist. wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); // Check if we should reschedule. Don't reschedule if stock management is disabled or hold duration is not set. if ( $held_duration < 1 || 'yes' !== get_option( 'woocommerce_manage_stock' ) ) { return; } /** * Filters the interval at which to cancel unpaid orders in minutes. * * @since 5.1.0 * * @param int $cancel_unpaid_interval The interval at which to cancel unpaid orders in minutes. */ $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) ); // Don't reschedule if the interval is 0 to prevent endless loops. if ( $cancel_unpaid_interval < 1 ) { return; } // Schedule the next event using Action Scheduler if available, otherwise fall back to WordPress cron. if ( function_exists( 'as_schedule_single_action' ) ) { as_schedule_single_action( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders', array(), 'woocommerce', false ); } else { wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); } $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( OrderStatus::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, 'order_id' => absint( $data->comment_post_ID ), ), $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 ) { $note = wc_get_order_note( $note_id ); if ( $note && wp_delete_comment( $note_id, true ) ) { /** * Action hook fired after an order note is deleted. * * @param int $note_id Order note ID. * @param stdClass $note Object with the deleted order note details. * * @since 9.1.0 */ do_action( 'woocommerce_order_note_deleted', $note_id, $note ); return true; } return false; } /** * Apply wptexturize while preserving URLs to prevent their content from being altered. * * @since 10.1.0 * @param string $content The order note content. * @return string The processed content. */ function wc_wptexturize_order_note( $content ) { // Pattern to match URLs (http, https protocols). $url_pattern = '/\b(?:https?):\/\/[^\s<>"{}|\\^`\[\]]+/i'; // Find all URLs in the content. preg_match_all( $url_pattern, $content, $urls ); if ( empty( $urls[0] ) ) { // No URLs found, safe to apply wptexturize. return wptexturize( $content ); } // Get unique URLs to avoid issues with duplicate URLs. $unique_urls = array_unique( $urls[0] ); // Replace URLs with placeholders. $placeholders = array(); $placeholder_content = $content; foreach ( $unique_urls as $index => $url ) { $placeholder = sprintf( '___WC_URL_PLACEHOLDER_%d___', $index ); $placeholders[ $placeholder ] = $url; $placeholder_content = str_replace( $url, $placeholder, $placeholder_content ); } // Apply wptexturize to content with placeholders. $texturized_content = wptexturize( $placeholder_content ); // Restore original URLs. return str_replace( array_keys( $placeholders ), array_values( $placeholders ), $texturized_content ); }
Sorry, but nothing matched your search terms. Please try again with some different keywords.
/** * WooCommerce Account Functions * * Functions for account specific things. * * @package WooCommerce\Functions * @version 2.6.0 */ use Automattic\WooCommerce\Enums\OrderStatus; use Automattic\WooCommerce\Enums\PaymentGatewayFeature; defined( 'ABSPATH' ) || exit; /** * Returns the url to the lost password endpoint url. * * @param string $default_url Default lost password URL. * @return string */ function wc_lostpassword_url( $default_url = '' ) { // Avoid loading too early. if ( ! did_action( 'init' ) ) { return $default_url; } // Don't change the admin form. if ( did_action( 'login_form_login' ) ) { return $default_url; } // Don't redirect to the woocommerce endpoint on global network admin lost passwords. if ( is_multisite() && isset( $_GET['redirect_to'] ) && false !== strpos( wp_unslash( $_GET['redirect_to'] ), network_admin_url() ) ) { // WPCS: input var ok, sanitization ok, CSRF ok. return $default_url; } $wc_account_page_url = wc_get_page_permalink( 'myaccount' ); $wc_account_page_exists = wc_get_page_id( 'myaccount' ) > 0; $lost_password_endpoint = get_option( 'woocommerce_myaccount_lost_password_endpoint' ); if ( $wc_account_page_exists && ! empty( $lost_password_endpoint ) ) { return wc_get_endpoint_url( $lost_password_endpoint, '', $wc_account_page_url ); } else { return $default_url; } } add_filter( 'lostpassword_url', 'wc_lostpassword_url', 10, 1 ); /** * Get the link to the edit account details page. * * @return string */ function wc_customer_edit_account_url() { $edit_account_url = wc_get_endpoint_url( 'edit-account', '', wc_get_page_permalink( 'myaccount' ) ); return apply_filters( 'woocommerce_customer_edit_account_url', $edit_account_url ); } /** * Get the edit address slug translation. * * @param string $id Address ID. * @param bool $flip Flip the array to make it possible to retrieve the values from both sides. * * @return string Address slug i18n. */ function wc_edit_address_i18n( $id, $flip = false ) { $slugs = apply_filters( 'woocommerce_edit_address_slugs', array( 'billing' => sanitize_title( _x( 'billing', 'edit-address-slug', 'woocommerce' ) ), 'shipping' => sanitize_title( _x( 'shipping', 'edit-address-slug', 'woocommerce' ) ), ) ); if ( $flip ) { $slugs = array_flip( $slugs ); } if ( ! isset( $slugs[ $id ] ) ) { return $id; } return $slugs[ $id ]; } /** * Get My Account menu items. * * @since 2.6.0 * @return array */ function wc_get_account_menu_items() { $endpoints = array( 'orders' => get_option( 'woocommerce_myaccount_orders_endpoint', 'orders' ), 'downloads' => get_option( 'woocommerce_myaccount_downloads_endpoint', 'downloads' ), 'edit-address' => get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ), 'payment-methods' => get_option( 'woocommerce_myaccount_payment_methods_endpoint', 'payment-methods' ), 'edit-account' => get_option( 'woocommerce_myaccount_edit_account_endpoint', 'edit-account' ), 'customer-logout' => get_option( 'woocommerce_logout_endpoint', 'customer-logout' ), ); $items = array( 'dashboard' => __( 'Dashboard', 'woocommerce' ), 'orders' => __( 'Orders', 'woocommerce' ), 'downloads' => __( 'Downloads', 'woocommerce' ), 'edit-address' => _n( 'Address', 'Addresses', ( 1 + (int) wc_shipping_enabled() ), 'woocommerce' ), 'payment-methods' => __( 'Payment methods', 'woocommerce' ), 'edit-account' => __( 'Account details', 'woocommerce' ), 'customer-logout' => __( 'Log out', 'woocommerce' ), ); // Remove missing endpoints. foreach ( $endpoints as $endpoint_id => $endpoint ) { if ( empty( $endpoint ) ) { unset( $items[ $endpoint_id ] ); } } // Check if payment gateways support add new payment methods. if ( isset( $items['payment-methods'] ) ) { $support_payment_methods = false; foreach ( WC()->payment_gateways->get_available_payment_gateways() as $gateway ) { if ( $gateway->supports( PaymentGatewayFeature::ADD_PAYMENT_METHOD ) || $gateway->supports( PaymentGatewayFeature::TOKENIZATION ) ) { $support_payment_methods = true; break; } } if ( ! $support_payment_methods ) { unset( $items['payment-methods'] ); } } return apply_filters( 'woocommerce_account_menu_items', $items, $endpoints ); } /** * Find current item in account menu. * * @since 9.3.0 * @param string $endpoint Endpoint. * @return bool */ function wc_is_current_account_menu_item( $endpoint ) { global $wp; $current = isset( $wp->query_vars[ $endpoint ] ); if ( 'dashboard' === $endpoint && ( isset( $wp->query_vars['page'] ) || empty( $wp->query_vars ) ) ) { $current = true; // Dashboard is not an endpoint, so needs a custom check. } elseif ( 'orders' === $endpoint && isset( $wp->query_vars['view-order'] ) ) { $current = true; // When looking at individual order, highlight Orders list item (to signify where in the menu the user currently is). } elseif ( 'payment-methods' === $endpoint && isset( $wp->query_vars['add-payment-method'] ) ) { $current = true; } return $current; } /** * Get account menu item classes. * * @since 2.6.0 * @param string $endpoint Endpoint. * @return string */ function wc_get_account_menu_item_classes( $endpoint ) { $classes = array( 'woocommerce-MyAccount-navigation-link', 'woocommerce-MyAccount-navigation-link--' . $endpoint, ); if ( wc_is_current_account_menu_item( $endpoint ) ) { $classes[] = 'is-active'; } $classes = apply_filters( 'woocommerce_account_menu_item_classes', $classes, $endpoint ); return implode( ' ', array_map( 'sanitize_html_class', $classes ) ); } /** * Get account endpoint URL. * * @since 2.6.0 * @param string $endpoint Endpoint. * @return string */ function wc_get_account_endpoint_url( $endpoint ) { if ( 'dashboard' === $endpoint ) { return wc_get_page_permalink( 'myaccount' ); } $url = wc_get_endpoint_url( $endpoint, '', wc_get_page_permalink( 'myaccount' ) ); if ( 'customer-logout' === $endpoint ) { return wp_nonce_url( $url, 'customer-logout' ); } return $url; } /** * Get My Account > Orders columns. * * @since 2.6.0 * @return array */ function wc_get_account_orders_columns() { /** * Filters the array of My Account > Orders columns. * * @since 2.6.0 * @param array $columns Array of column labels keyed by column IDs. */ return apply_filters( 'woocommerce_account_orders_columns', array( 'order-number' => __( 'Order', 'woocommerce' ), 'order-date' => __( 'Date', 'woocommerce' ), 'order-status' => __( 'Status', 'woocommerce' ), 'order-total' => __( 'Total', 'woocommerce' ), 'order-actions' => __( 'Actions', 'woocommerce' ), ) ); } /** * Get My Account > Downloads columns. * * @since 2.6.0 * @return array */ function wc_get_account_downloads_columns() { $columns = apply_filters( 'woocommerce_account_downloads_columns', array( 'download-product' => __( 'Product', 'woocommerce' ), 'download-remaining' => __( 'Downloads remaining', 'woocommerce' ), 'download-expires' => __( 'Expires', 'woocommerce' ), 'download-file' => __( 'Download', 'woocommerce' ), 'download-actions' => ' ', ) ); if ( ! has_filter( 'woocommerce_account_download_actions' ) ) { unset( $columns['download-actions'] ); } return $columns; } /** * Get My Account > Payment methods columns. * * @since 2.6.0 * @return array */ function wc_get_account_payment_methods_columns() { return apply_filters( 'woocommerce_account_payment_methods_columns', array( 'method' => __( 'Method', 'woocommerce' ), 'expires' => __( 'Expires', 'woocommerce' ), 'actions' => ' ', ) ); } /** * Get My Account > Payment methods types * * @since 2.6.0 * @return array */ function wc_get_account_payment_methods_types() { return apply_filters( 'woocommerce_payment_methods_types', array( 'cc' => __( 'Credit card', 'woocommerce' ), 'echeck' => __( 'eCheck', 'woocommerce' ), ) ); } /** * Get account orders actions. * * @since 3.2.0 * @param int|WC_Order $order Order instance or ID. * @return array */ function wc_get_account_orders_actions( $order ) { if ( ! is_object( $order ) ) { $order_id = absint( $order ); $order = wc_get_order( $order_id ); } $actions = array( 'pay' => array( 'url' => $order->get_checkout_payment_url(), 'name' => __( 'Pay', 'woocommerce' ), /* translators: %s: order number */ 'aria-label' => sprintf( __( 'Pay for order %s', 'woocommerce' ), $order->get_order_number() ), ), 'view' => array( 'url' => $order->get_view_order_url(), 'name' => __( 'View', 'woocommerce' ), /* translators: %s: order number */ 'aria-label' => sprintf( __( 'View order %s', 'woocommerce' ), $order->get_order_number() ), ), 'cancel' => array( 'url' => $order->get_cancel_order_url( wc_get_page_permalink( 'myaccount' ) ), 'name' => __( 'Cancel', 'woocommerce' ), /* translators: %s: order number */ 'aria-label' => sprintf( __( 'Cancel order %s', 'woocommerce' ), $order->get_order_number() ), ), ); if ( ! $order->needs_payment() ) { unset( $actions['pay'] ); } /** * Filters the valid order statuses for cancel action. * * @since 3.2.0 * * @param array $statuses_for_cancel Array of valid order statuses for cancel action. * @param WC_Order $order Order instance. */ $statuses_for_cancel = apply_filters( 'woocommerce_valid_order_statuses_for_cancel', array( OrderStatus::PENDING, OrderStatus::FAILED ), $order ); if ( ! in_array( $order->get_status(), $statuses_for_cancel, true ) ) { unset( $actions['cancel'] ); } return apply_filters( 'woocommerce_my_account_my_orders_actions', $actions, $order ); } /** * Get account formatted address. * * @since 3.2.0 * @param string $address_type Type of address; 'billing' or 'shipping'. * @param int $customer_id Customer ID. * Defaults to 0. * @return string */ function wc_get_account_formatted_address( $address_type = 'billing', $customer_id = 0 ) { $getter = "get_{$address_type}"; $address = array(); if ( 0 === $customer_id ) { $customer_id = get_current_user_id(); } $customer = new WC_Customer( $customer_id ); if ( is_callable( array( $customer, $getter ) ) ) { $address = $customer->$getter(); unset( $address['email'], $address['tel'] ); } return WC()->countries->get_formatted_address( apply_filters( 'woocommerce_my_account_my_address_formatted_address', $address, $customer->get_id(), $address_type ) ); } /** * Returns an array of a user's saved payments list for output on the account tab. * * @since 2.6 * @param array $list List of payment methods passed from wc_get_customer_saved_methods_list(). * @param int $customer_id The customer to fetch payment methods for. * @return array Filtered list of customers payment methods. */ function wc_get_account_saved_payment_methods_list( $list, $customer_id ) { $payment_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id ); foreach ( $payment_tokens as $payment_token ) { $delete_url = wc_get_endpoint_url( 'delete-payment-method', $payment_token->get_id() ); $delete_url = wp_nonce_url( $delete_url, 'delete-payment-method-' . $payment_token->get_id() ); $set_default_url = wc_get_endpoint_url( 'set-default-payment-method', $payment_token->get_id() ); $set_default_url = wp_nonce_url( $set_default_url, 'set-default-payment-method-' . $payment_token->get_id() ); $type = strtolower( $payment_token->get_type() ); $list[ $type ][] = array( 'method' => array( 'gateway' => $payment_token->get_gateway_id(), ), 'expires' => esc_html__( 'N/A', 'woocommerce' ), 'is_default' => $payment_token->is_default(), 'actions' => array( 'delete' => array( 'url' => $delete_url, 'name' => esc_html__( 'Delete', 'woocommerce' ), ), ), ); $key = key( array_slice( $list[ $type ], -1, 1, true ) ); if ( ! $payment_token->is_default() ) { $list[ $type ][ $key ]['actions']['default'] = array( 'url' => $set_default_url, 'name' => esc_html__( 'Make default', 'woocommerce' ), ); } $list[ $type ][ $key ] = apply_filters( 'woocommerce_payment_methods_list_item', $list[ $type ][ $key ], $payment_token ); } return $list; } add_filter( 'woocommerce_saved_payment_methods_list', 'wc_get_account_saved_payment_methods_list', 10, 2 ); /** * Controls the output for credit cards on the my account page. * * @since 2.6 * @param array $item Individual list item from woocommerce_saved_payment_methods_list. * @param WC_Payment_Token $payment_token The payment token associated with this method entry. * @return array Filtered item. */ function wc_get_account_saved_payment_methods_list_item_cc( $item, $payment_token ) { if ( 'cc' !== strtolower( $payment_token->get_type() ) ) { return $item; } $card_type = $payment_token->get_card_type(); $item['method']['last4'] = $payment_token->get_last4(); $item['method']['brand'] = ( ! empty( $card_type ) ? ucwords( str_replace( '_', ' ', $card_type ) ) : esc_html__( 'Credit card', 'woocommerce' ) ); $item['expires'] = $payment_token->get_expiry_month() . '/' . substr( $payment_token->get_expiry_year(), -2 ); return $item; } add_filter( 'woocommerce_payment_methods_list_item', 'wc_get_account_saved_payment_methods_list_item_cc', 10, 2 ); /** * Controls the output for eChecks on the my account page. * * @since 2.6 * @param array $item Individual list item from woocommerce_saved_payment_methods_list. * @param WC_Payment_Token $payment_token The payment token associated with this method entry. * @return array Filtered item. */ function wc_get_account_saved_payment_methods_list_item_echeck( $item, $payment_token ) { if ( 'echeck' !== strtolower( $payment_token->get_type() ) ) { return $item; } $item['method']['last4'] = $payment_token->get_last4(); $item['method']['brand'] = esc_html__( 'eCheck', 'woocommerce' ); return $item; } add_filter( 'woocommerce_payment_methods_list_item', 'wc_get_account_saved_payment_methods_list_item_echeck', 10, 2 );/** * WooCommerce Terms * * Functions for handling terms/term meta. * * @package WooCommerce\Functions * @version 2.1.0 */ defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Enums\ProductStockStatus; /** * Change get terms defaults for attributes to order by the sorting setting, or default to menu_order for sortable taxonomies. * * @since 3.6.0 Sorting options are now set as the default automatically, so you no longer have to request to orderby menu_order. * * @param array $defaults An array of default get_terms() arguments. * @param array $taxonomies An array of taxonomies. * @return array */ function wc_change_get_terms_defaults( $defaults, $taxonomies ) { if ( is_array( $taxonomies ) && 1 < count( $taxonomies ) ) { return $defaults; } $taxonomy = is_array( $taxonomies ) ? (string) current( $taxonomies ) : $taxonomies; $orderby = 'name'; if ( taxonomy_is_product_attribute( $taxonomy ) ) { $orderby = wc_attribute_orderby( $taxonomy ); } elseif ( in_array( $taxonomy, apply_filters( 'woocommerce_sortable_taxonomies', array( 'product_cat' ) ), true ) ) { $orderby = 'menu_order'; } // Change defaults. Invalid values will be changed later @see wc_change_pre_get_terms. // These are in place so we know if a specific order was requested. switch ( $orderby ) { case 'menu_order': case 'name_num': case 'parent': $defaults['orderby'] = $orderby; break; } return $defaults; } add_filter( 'get_terms_defaults', 'wc_change_get_terms_defaults', 10, 2 ); /** * Adds support to get_terms for menu_order argument. * * @since 3.6.0 * @param WP_Term_Query $terms_query Instance of WP_Term_Query. */ function wc_change_pre_get_terms( $terms_query ) { $args = &$terms_query->query_vars; // Put back valid orderby values. if ( 'menu_order' === $args['orderby'] ) { $args['orderby'] = 'name'; $args['force_menu_order_sort'] = true; } if ( 'name_num' === $args['orderby'] ) { $args['orderby'] = 'name'; $args['force_numeric_name'] = true; } // When COUNTING, disable custom sorting. if ( 'count' === $args['fields'] ) { return; } // Support menu_order arg used in previous versions. if ( ! empty( $args['menu_order'] ) ) { $args['order'] = 'DESC' === strtoupper( $args['menu_order'] ) ? 'DESC' : 'ASC'; $args['force_menu_order_sort'] = true; } if ( ! empty( $args['force_menu_order_sort'] ) ) { $args['orderby'] = 'meta_value_num'; $args['meta_key'] = 'order'; // phpcs:ignore $terms_query->meta_query->parse_query_vars( $args ); } } add_action( 'pre_get_terms', 'wc_change_pre_get_terms', 10, 1 ); /** * Adjust term query to handle custom sorting parameters. * * @param array $clauses Clauses. * @param array $taxonomies Taxonomies. * @param array $args Arguments. * @return array */ function wc_terms_clauses( $clauses, $taxonomies, $args ) { global $wpdb; // No need to filter when counting. if ( strpos( $clauses['fields'], 'COUNT(*)' ) !== false ) { return $clauses; } // Force numeric sort if using name_num custom sorting param. if ( ! empty( $args['force_numeric_name'] ) ) { $clauses['orderby'] = str_replace( 'ORDER BY t.name', 'ORDER BY t.name+0', $clauses['orderby'] ); } // For sorting, force left join in case order meta is missing. if ( ! empty( $args['force_menu_order_sort'] ) ) { $clauses['join'] = str_replace( "INNER JOIN {$wpdb->termmeta} ON ( t.term_id = {$wpdb->termmeta}.term_id )", "LEFT JOIN {$wpdb->termmeta} ON ( t.term_id = {$wpdb->termmeta}.term_id AND {$wpdb->termmeta}.meta_key='order')", $clauses['join'] ); $clauses['where'] = str_replace( "{$wpdb->termmeta}.meta_key = 'order'", "( {$wpdb->termmeta}.meta_key = 'order' OR {$wpdb->termmeta}.meta_key IS NULL )", $clauses['where'] ); $clauses['orderby'] = 'DESC' === $args['order'] ? str_replace( 'meta_value+0', 'meta_value+0 DESC, t.name', $clauses['orderby'] ) : str_replace( 'meta_value+0', 'meta_value+0 ASC, t.name', $clauses['orderby'] ); } return $clauses; } add_filter( 'terms_clauses', 'wc_terms_clauses', 99, 3 ); /** * Helper to get cached object terms and filter by field using wp_list_pluck(). * Works as a cached alternative for wp_get_post_terms() and wp_get_object_terms(). * * @since 3.0.0 * @param int $object_id Object ID. * @param string $taxonomy Taxonomy slug. * @param string $field Field name. * @param string $index_key Index key name. * @return array */ function wc_get_object_terms( $object_id, $taxonomy, $field = null, $index_key = null ) { // Test if terms exists. get_the_terms() return false when it finds no terms. $terms = get_the_terms( $object_id, $taxonomy ); if ( ! $terms || is_wp_error( $terms ) ) { return array(); } return is_null( $field ) ? $terms : wp_list_pluck( $terms, $field, $index_key ); } /** * Cached version of wp_get_post_terms(). * This is a private function (internal use ONLY). * * @since 3.0.0 * @param int $product_id Product ID. * @param string $taxonomy Taxonomy slug. * @param array $args Query arguments. * @return array */ function _wc_get_cached_product_terms( $product_id, $taxonomy, $args = array() ) { $cache_key = 'wc_' . $taxonomy . md5( wp_json_encode( $args ) ); $cache_group = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . $product_id; $terms = wp_cache_get( $cache_key, $cache_group ); if ( false !== $terms ) { return $terms; } $terms = wp_get_post_terms( $product_id, $taxonomy, $args ); wp_cache_add( $cache_key, $terms, $cache_group ); return $terms; } /** * Wrapper used to get terms for a product. * * @param int $product_id Product ID. * @param string $taxonomy Taxonomy slug. * @param array $args Query arguments. * @return array */ function wc_get_product_terms( $product_id, $taxonomy, $args = array() ) { if ( ! taxonomy_exists( $taxonomy ) ) { return array(); } return apply_filters( 'woocommerce_get_product_terms', _wc_get_cached_product_terms( $product_id, $taxonomy, $args ), $product_id, $taxonomy, $args ); } /** * Sort by name (numeric). * * @param WP_Post $a First item to compare. * @param WP_Post $b Second item to compare. * @return int */ function _wc_get_product_terms_name_num_usort_callback( $a, $b ) { $a_name = (float) $a->name; $b_name = (float) $b->name; if ( abs( $a_name - $b_name ) < 0.001 ) { return 0; } return ( $a_name < $b_name ) ? -1 : 1; } /** * Sort by parent. * * @param WP_Post $a First item to compare. * @param WP_Post $b Second item to compare. * @return int */ function _wc_get_product_terms_parent_usort_callback( $a, $b ) { if ( $a->parent === $b->parent ) { return 0; } return ( $a->parent < $b->parent ) ? 1 : -1; } /** * WooCommerce Dropdown categories. * * @param array $args Args to control display of dropdown. */ function wc_product_dropdown_categories( $args = array() ) { global $wp_query; $args = wp_parse_args( $args, array( 'pad_counts' => 1, 'show_count' => 1, 'hierarchical' => 1, 'hide_empty' => 1, 'show_uncategorized' => 1, 'orderby' => 'name', 'selected' => isset( $wp_query->query_vars['product_cat'] ) ? $wp_query->query_vars['product_cat'] : '', 'show_option_none' => __( 'Select a category', 'woocommerce' ), 'option_none_value' => '', 'value_field' => 'slug', 'taxonomy' => 'product_cat', 'name' => 'product_cat', 'class' => 'dropdown_product_cat', ) ); if ( 'order' === $args['orderby'] ) { $args['orderby'] = 'meta_value_num'; $args['meta_key'] = 'order'; // phpcs:ignore } wp_dropdown_categories( $args ); } /** * Custom walker for Product Categories. * * Previously used by wc_product_dropdown_categories, but wp_dropdown_categories has been fixed in core. * * @param mixed ...$args Variable number of parameters to be passed to the walker. * @return mixed */ function wc_walk_category_dropdown_tree( ...$args ) { if ( ! class_exists( 'WC_Product_Cat_Dropdown_Walker', false ) ) { include_once WC()->plugin_path() . '/includes/walkers/class-wc-product-cat-dropdown-walker.php'; } // The user's options are the third parameter. if ( empty( $args[2]['walker'] ) || ! is_a( $args[2]['walker'], 'Walker' ) ) { $walker = new WC_Product_Cat_Dropdown_Walker(); } else { $walker = $args[2]['walker']; } return $walker->walk( ...$args ); } /** * Migrate data from WC term meta to WP term meta. * * When the database is updated to support term meta, migrate WC term meta data across. * We do this when the new version is >= 34370, and the old version is < 34370 (34370 is when term meta table was added). * * @param string $wp_db_version The new $wp_db_version. * @param string $wp_current_db_version The old (current) $wp_db_version. */ function wc_taxonomy_metadata_migrate_data( $wp_db_version, $wp_current_db_version ) { if ( $wp_db_version >= 34370 && $wp_current_db_version < 34370 ) { global $wpdb; if ( $wpdb->query( "INSERT INTO {$wpdb->termmeta} ( term_id, meta_key, meta_value ) SELECT woocommerce_term_id, meta_key, meta_value FROM {$wpdb->prefix}woocommerce_termmeta;" ) ) { $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_termmeta" ); } } } add_action( 'wp_upgrade', 'wc_taxonomy_metadata_migrate_data', 10, 2 ); /** * Move a term before the a given element of its hierarchy level. * * @param int $the_term Term ID. * @param int $next_id The id of the next sibling element in save hierarchy level. * @param string $taxonomy Taxonomy. * @param int $index Term index (default: 0). * @param mixed $terms List of terms. (default: null). * @return int */ function wc_reorder_terms( $the_term, $next_id, $taxonomy, $index = 0, $terms = null ) { if ( ! $terms ) { $terms = get_terms( $taxonomy, 'hide_empty=0&parent=0&menu_order=ASC' ); } if ( empty( $terms ) ) { return $index; } $id = intval( $the_term->term_id ); $term_in_level = false; // Flag: is our term to order in this level of terms. foreach ( $terms as $term ) { $term_id = intval( $term->term_id ); if ( $term_id === $id ) { // Our term to order, we skip. $term_in_level = true; continue; // Our term to order, we skip. } // the nextid of our term to order, lets move our term here. if ( null !== $next_id && $term_id === $next_id ) { $index++; $index = wc_set_term_order( $id, $index, $taxonomy, true ); } // Set order. $index++; $index = wc_set_term_order( $term_id, $index, $taxonomy ); /** * After a term has had it's order set. */ do_action( 'woocommerce_after_set_term_order', $term, $index, $taxonomy ); // If that term has children we walk through them. $children = get_terms( $taxonomy, "parent={$term_id}&hide_empty=0&menu_order=ASC" ); if ( ! empty( $children ) ) { $index = wc_reorder_terms( $the_term, $next_id, $taxonomy, $index, $children ); } } // No nextid meaning our term is in last position. if ( $term_in_level && null === $next_id ) { $index = wc_set_term_order( $id, $index + 1, $taxonomy, true ); } return $index; } /** * Set the sort order of a term. * * @param int $term_id Term ID. * @param int $index Index. * @param string $taxonomy Taxonomy. * @param bool $recursive Recursive (default: false). * @return int */ function wc_set_term_order( $term_id, $index, $taxonomy, $recursive = false ) { $term_id = (int) $term_id; $index = (int) $index; update_term_meta( $term_id, 'order', $index ); if ( ! $recursive ) { return $index; } $children = get_terms( $taxonomy, "parent=$term_id&hide_empty=0&menu_order=ASC" ); foreach ( $children as $term ) { $index++; $index = wc_set_term_order( $term->term_id, $index, $taxonomy, true ); } clean_term_cache( $term_id, $taxonomy ); return $index; } /** * Function for recounting product terms, ignoring hidden products. * * This is used as the update_count_callback for the Product Category and Product Tag * taxonomies. By default, it actually calculates two (possibly different) counts for each * term, which it stores in two different places. The first count is the one done by WordPress * itself, and is based on the status of the objects that are assigned the terms. In this case, * only products with the publish status are counted. This count is stored in the * `wp_term_taxonomy` table in the `count` field. * * The second count is based on WooCommerce-specific characteristics. In addition to the * publish status requirement, products are only counted if they are considered visible in the * catalog. This count is stored in the `wp_termmeta` table. The wc_change_term_counts function * is used to override the first count with the second count in some circumstances. * * Since the first count only needs to be recalculated when a product status is changed in some * way, it can sometimes be skipped (thus avoiding some potentially expensive queries). Setting * the $callback parameter to false skips the first count. * * @param array $terms List of terms. For legacy reasons, this can * either be a list of taxonomy term IDs or an * associative array in the format of * term ID > parent term ID. * @param WP_Taxonomy $taxonomy The relevant taxonomy. * @param bool $callback Whether to also recalculate the term counts * using the WP Core callback. Default true. * @param bool $terms_are_term_taxonomy_ids Flag to indicate which format the list of * terms is in. Default true, which indicates * that it is a list of taxonomy term IDs. */ function _wc_term_recount( $terms, $taxonomy, $callback = true, $terms_are_term_taxonomy_ids = true ) { global $wpdb; /** * Filter to allow/prevent recounting of terms as it could be expensive. * A likely scenario for this is when bulk importing products. We could * then prevent it from recounting per product but instead recount it once * when import is done. Of course this means the import logic has to support this. * * @since 5.2 * @param bool */ if ( ! apply_filters( 'woocommerce_product_recount_terms', true ) ) { return; } if ( true === $terms_are_term_taxonomy_ids ) { $taxonomy_term_ids = $terms; $term_ids = array_map( function ( $term_taxonomy_id ) use ( $taxonomy ) { $term = get_term_by( 'term_taxonomy_id', $term_taxonomy_id, $taxonomy->name ); return $term instanceof WP_Term ? $term->term_id : null; }, $terms ); } else { $taxonomy_term_ids = array(); // Defer querying these until the callback check. $term_ids = array_keys( $terms ); } $term_ids = array_unique( array_filter( $term_ids ) ); $taxonomy_term_ids = array_unique( array_filter( $taxonomy_term_ids ) ); // Exit if we have no terms to count. if ( empty( $term_ids ) ) { return; } // Standard WP callback for calculating post term counts. if ( $callback ) { if ( count( $taxonomy_term_ids ) < 1 ) { $taxonomy_term_ids = array_map( function ( $term_id ) use ( $taxonomy ) { $term = get_term_by( 'term_id', $term_id, $taxonomy->name ); return $term instanceof WP_Term ? $term->term_taxonomy_id : null; }, $term_ids ); } _update_post_term_count( $taxonomy_term_ids, $taxonomy ); } $exclude_term_ids = array(); $product_visibility_term_ids = wc_get_product_visibility_term_ids(); if ( $product_visibility_term_ids['exclude-from-catalog'] ) { $exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog']; } if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids[ ProductStockStatus::OUT_OF_STOCK ] ) { $exclude_term_ids[] = $product_visibility_term_ids[ ProductStockStatus::OUT_OF_STOCK ]; } $query = array( 'fields' => " SELECT COUNT( DISTINCT ID ) FROM {$wpdb->posts} p ", 'join' => '', 'where' => " WHERE 1=1 AND p.post_status = 'publish' AND p.post_type = 'product' ", ); if ( count( $exclude_term_ids ) ) { $query['join'] .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID'; $query['where'] .= ' AND exclude_join.object_id IS NULL'; } // Ancestors need counting. if ( is_taxonomy_hierarchical( $taxonomy->name ) ) { foreach ( $term_ids as $term_id ) { $term_ids = array_merge( $term_ids, get_ancestors( $term_id, $taxonomy->name ) ); } $term_ids = array_unique( $term_ids ); } // Count the terms. foreach ( $term_ids as $term_id ) { $terms_to_count = array( absint( $term_id ) ); if ( is_taxonomy_hierarchical( $taxonomy->name ) ) { // We need to get the $term's hierarchy so we can count its children too. $children = get_term_children( $term_id, $taxonomy->name ); if ( $children && ! is_wp_error( $children ) ) { $terms_to_count = array_unique( array_map( 'absint', array_merge( $terms_to_count, $children ) ) ); } } // Generate term query. $term_query = $query; $term_query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $terms_to_count ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID'; // Get the count. // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $count = $wpdb->get_var( implode( ' ', $term_query ) ); // Update the count. update_term_meta( $term_id, 'product_count_' . $taxonomy->name, absint( $count ) ); } delete_transient( 'wc_term_counts' ); } /** * Recount terms after the stock amount changes. * * @param int $product_id Product ID. */ function wc_recount_after_stock_change( $product_id ) { if ( 'yes' !== get_option( 'woocommerce_hide_out_of_stock_items' ) ) { return; } _wc_recount_terms_by_product( $product_id ); } add_action( 'woocommerce_product_set_stock_status', 'wc_recount_after_stock_change' ); /** * Overrides the original term count for product categories and tags with the product count. * that takes catalog visibility into account. * * @param array $terms List of terms. * @param string|array $taxonomies Single taxonomy or list of taxonomies. * @return array */ function wc_change_term_counts( $terms, $taxonomies ) { if ( is_admin() || wp_doing_ajax() ) { return $terms; } /** * Filter which product taxonomies should have their term counts overridden to take catalog visibility into account. * * @since 2.1.0 * * @param array $valid_taxonomies List of taxonomy slugs. */ $valid_taxonomies = apply_filters( 'woocommerce_change_term_counts', array( 'product_cat', 'product_tag' ) ); $current_taxonomies = array_intersect( (array) $taxonomies, $valid_taxonomies ); if ( empty( $current_taxonomies ) ) { return $terms; } $o_term_counts = get_transient( 'wc_term_counts' ); $term_counts = false === $o_term_counts ? array() : $o_term_counts; foreach ( $terms as &$term ) { if ( $term instanceof WP_Term && in_array( $term->taxonomy, $current_taxonomies, true ) ) { $key = $term->term_id . '_' . $term->taxonomy; if ( ! isset( $term_counts[ $key ] ) ) { $count = get_term_meta( $term->term_id, 'product_count_' . $term->taxonomy, true ); $count = '' !== $count ? absint( $count ) : 0; $term_counts[ $key ] = $count; } $term->count = $term_counts[ $key ]; } } // Update transient. if ( $term_counts !== $o_term_counts ) { set_transient( 'wc_term_counts', $term_counts, MONTH_IN_SECONDS ); } return $terms; } add_filter( 'get_terms', 'wc_change_term_counts', 10, 2 ); /** * Return products in a given term, and cache value. * * To keep in sync, product_count will be cleared on "set_object_terms". * * @param int $term_id Term ID. * @param string $taxonomy Taxonomy. * @return array */ function wc_get_term_product_ids( $term_id, $taxonomy ) { $product_ids = get_term_meta( $term_id, 'product_ids', true ); if ( false === $product_ids || ! is_array( $product_ids ) ) { $product_ids = get_objects_in_term( $term_id, $taxonomy ); update_term_meta( $term_id, 'product_ids', $product_ids ); } return $product_ids; } /** * When a post is updated and terms recounted (called by _update_post_term_count), clear the ids. * * @param int $object_id Object ID. * @param array $terms An array of object terms. * @param array $tt_ids An array of term taxonomy IDs. * @param string $taxonomy Taxonomy slug. * @param bool $append Whether to append new terms to the old terms. * @param array $old_tt_ids Old array of term taxonomy IDs. */ function wc_clear_term_product_ids( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { foreach ( $old_tt_ids as $term_id ) { delete_term_meta( $term_id, 'product_ids' ); } foreach ( $tt_ids as $term_id ) { delete_term_meta( $term_id, 'product_ids' ); } } add_action( 'set_object_terms', 'wc_clear_term_product_ids', 10, 6 ); /** * Get full list of product visibility term ids. * * @since 3.0.0 * @return int[] */ function wc_get_product_visibility_term_ids() { if ( ! taxonomy_exists( 'product_visibility' ) ) { wc_doing_it_wrong( __FUNCTION__, 'wc_get_product_visibility_term_ids should not be called before taxonomies are registered (woocommerce_after_register_post_type action).', '3.1' ); return array(); } static $term_ids = array(); // The static variable doesn't work well with unit tests. if ( count( $term_ids ) > 0 && ! class_exists( 'WC_Unit_Tests_Bootstrap' ) ) { return $term_ids; } $term_ids = array_map( 'absint', wp_parse_args( wp_list_pluck( get_terms( array( 'taxonomy' => 'product_visibility', 'hide_empty' => false, ) ), 'term_taxonomy_id', 'name' ), array( 'exclude-from-catalog' => 0, 'exclude-from-search' => 0, 'featured' => 0, 'outofstock' => 0, 'rated-1' => 0, 'rated-2' => 0, 'rated-3' => 0, 'rated-4' => 0, 'rated-5' => 0, ) ) ); return $term_ids; } /** * Recounts all terms for product categories and product tags. * * @since 5.2 * * @param bool $include_callback True to update the standard term counts in addition to the product-specific counts, * which will cause a lot more queries to run. * * @return void */ function wc_recount_all_terms( bool $include_callback = true ) { $product_cats = get_terms( array( 'taxonomy' => 'product_cat', 'hide_empty' => false, 'fields' => 'id=>parent', ) ); _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), $include_callback, false ); $product_tags = get_terms( array( 'taxonomy' => 'product_tag', 'hide_empty' => false, 'fields' => 'id=>parent', ) ); _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), $include_callback, false ); } /** * Recounts terms by product. * * @since 5.2 * @param int $product_id The ID of the product. * @return void */ function _wc_recount_terms_by_product( $product_id = '' ) { if ( empty( $product_id ) ) { return; } $product_terms = get_the_terms( $product_id, 'product_cat' ); if ( $product_terms ) { $product_cats = array(); foreach ( $product_terms as $term ) { $product_cats[ $term->term_id ] = $term->parent; } _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), false, false ); } $product_terms = get_the_terms( $product_id, 'product_tag' ); if ( $product_terms ) { $product_tags = array(); foreach ( $product_terms as $term ) { $product_tags[ $term->term_id ] = $term->parent; } _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), false, false ); } }/** * WooCommerce Webhook functions * * @package WooCommerce\Functions * @version 3.3.0 */ defined( 'ABSPATH' ) || exit; /** * Process the web hooks at the end of the request. * * @since 4.4.0 */ function wc_webhook_execute_queue() { global $wc_queued_webhooks; if ( empty( $wc_queued_webhooks ) ) { return; } foreach ( $wc_queued_webhooks as $data ) { // Webhooks are processed in the background by default // so as to avoid delays or failures in delivery from affecting the // user who triggered it. if ( apply_filters( 'woocommerce_webhook_deliver_async', true, $data['webhook'], $data['arg'] ) ) { $queue_args = array( 'webhook_id' => $data['webhook']->get_id(), 'arg' => $data['arg'], ); $next_scheduled_date = WC()->queue()->get_next( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' ); // Make webhooks unique - only schedule one webhook every 10 minutes to maintain backward compatibility with WP Cron behaviour seen in WC < 3.5.0. if ( is_null( $next_scheduled_date ) || $next_scheduled_date->getTimestamp() >= ( 600 + gmdate( 'U' ) ) ) { WC()->queue()->add( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' ); } } else { // Deliver immediately. $data['webhook']->deliver( $data['arg'] ); } } } add_action( 'shutdown', 'wc_webhook_execute_queue' ); /** * Process webhook delivery. * * @since 3.3.0 * @param WC_Webhook $webhook Webhook instance. * @param array $arg Delivery arguments. */ function wc_webhook_process_delivery( $webhook, $arg ) { // We need to queue the webhook so that it can be ran after the request has finished processing. global $wc_queued_webhooks; if ( ! isset( $wc_queued_webhooks ) ) { $wc_queued_webhooks = array(); } $wc_queued_webhooks[] = array( 'webhook' => $webhook, 'arg' => $arg, ); } add_action( 'woocommerce_webhook_process_delivery', 'wc_webhook_process_delivery', 10, 2 ); /** * Wrapper function to execute the `woocommerce_deliver_webhook_async` cron. * hook, see WC_Webhook::process(). * * @since 2.2.0 * @param int $webhook_id Webhook ID to deliver. * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. * @param mixed $arg Hook argument. */ function wc_deliver_webhook_async( $webhook_id, $arg ) { $webhook = new WC_Webhook( $webhook_id ); if ( 0 === $webhook->get_id() ) { return; } $webhook->deliver( $arg ); } add_action( 'woocommerce_deliver_webhook_async', 'wc_deliver_webhook_async', 10, 2 ); /** * Check if the given topic is a valid webhook topic, a topic is valid if: * * + starts with `action.woocommerce_` or `action.wc_`. * + it has a valid resource & event. * * @since 2.2.0 * @param string $topic Webhook topic. * @return bool */ function wc_is_webhook_valid_topic( $topic ) { $invalid_topics = array( 'action.woocommerce_login_credentials', 'action.woocommerce_product_csv_importer_check_import_file_path', 'action.woocommerce_webhook_should_deliver', ); if ( in_array( $topic, $invalid_topics, true ) ) { return false; } // Custom topics are prefixed with woocommerce_ or wc_ are valid. if ( 0 === strpos( $topic, 'action.woocommerce_' ) || 0 === strpos( $topic, 'action.wc_' ) ) { return true; } $data = explode( '.', $topic ); if ( ! isset( $data[0] ) || ! isset( $data[1] ) ) { return false; } $valid_resources = apply_filters( 'woocommerce_valid_webhook_resources', array( 'coupon', 'customer', 'order', 'product' ) ); $valid_events = apply_filters( 'woocommerce_valid_webhook_events', array( 'created', 'updated', 'deleted', 'restored' ) ); if ( in_array( $data[0], $valid_resources, true ) && in_array( $data[1], $valid_events, true ) ) { return true; } return false; } /** * Check if given status is a valid webhook status. * * @since 3.5.3 * @param string $status Status to check. * @return bool */ function wc_is_webhook_valid_status( $status ) { return in_array( $status, array_keys( wc_get_webhook_statuses() ), true ); } /** * Get Webhook statuses. * * @since 2.3.0 * @return array */ function wc_get_webhook_statuses() { return apply_filters( 'woocommerce_webhook_statuses', array( 'active' => __( 'Active', 'woocommerce' ), 'paused' => __( 'Paused', 'woocommerce' ), 'disabled' => __( 'Disabled', 'woocommerce' ), ) ); } /** * Load webhooks. * * @since 3.3.0 * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. * @param string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.5.0. * @param null|int $limit Limit number of webhooks loaded. @since 3.6.0. * @return bool */ function wc_load_webhooks( $status = '', $limit = null ) { // short-circuit if webhooks should not be loaded at all. if ( ! is_null( $limit ) && $limit <= 0 ) { return false; } $data_store = WC_Data_Store::load( 'webhook' ); $webhooks = $data_store->get_webhooks_ids( $status ); $loaded = 0; foreach ( $webhooks as $webhook_id ) { if ( ! is_null( $limit ) && $loaded >= $limit ) { break; } $webhook = new WC_Webhook( $webhook_id ); $webhook->enqueue(); $loaded ++; } return 0 < $loaded; } /** * Get webhook. * * @param int|WC_Webhook $id Webhook ID or object. * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. * @return WC_Webhook|null */ function wc_get_webhook( $id ) { $webhook = new WC_Webhook( $id ); return 0 !== $webhook->get_id() ? $webhook : null; } /** * Get webhoook REST API versions. * * @since 3.5.1 * @return array */ function wc_get_webhook_rest_api_versions() { return array( 'wp_api_v1', 'wp_api_v2', 'wp_api_v3', ); }/** * Deprecated API functions for scheduling actions * * Functions with the wc prefix were deprecated to avoid confusion with * Action Scheduler being included in WooCommerce core, and it providing * a different set of APIs for working with the action queue. * * @package ActionScheduler */ /** * Schedule an action to run one time. * * @param int $timestamp When the job will run. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * * @return string The job ID */ function wc_schedule_single_action( $timestamp, $hook, $args = array(), $group = '' ) { _deprecated_function( __FUNCTION__, '2.1.0', 'as_schedule_single_action()' ); return as_schedule_single_action( $timestamp, $hook, $args, $group ); } /** * Schedule a recurring action. * * @param int $timestamp When the first instance of the job will run. * @param int $interval_in_seconds How long to wait between runs. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * * @deprecated 2.1.0 * * @return string The job ID */ function wc_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) { _deprecated_function( __FUNCTION__, '2.1.0', 'as_schedule_recurring_action()' ); return as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $group ); } /** * Schedule an action that recurs on a cron-like schedule. * * @param int $timestamp The schedule will start on or after this time. * @param string $schedule A cron-link schedule string. * @see http://en.wikipedia.org/wiki/Cron * * * * * * * * ┬ ┬ ┬ ┬ ┬ ┬ * | | | | | | * | | | | | + year [optional] * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) * | | | +---------- month (1 - 12) * | | +--------------- day of month (1 - 31) * | +-------------------- hour (0 - 23) * +------------------------- min (0 - 59) * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * * @deprecated 2.1.0 * * @return string The job ID */ function wc_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '' ) { _deprecated_function( __FUNCTION__, '2.1.0', 'as_schedule_cron_action()' ); return as_schedule_cron_action( $timestamp, $schedule, $hook, $args, $group ); } /** * Cancel the next occurrence of a job. * * @param string $hook The hook that the job will trigger. * @param array $args Args that would have been passed to the job. * @param string $group Action's group. * * @deprecated 2.1.0 */ function wc_unschedule_action( $hook, $args = array(), $group = '' ) { _deprecated_function( __FUNCTION__, '2.1.0', 'as_unschedule_action()' ); as_unschedule_action( $hook, $args, $group ); } /** * Get next scheduled action. * * @param string $hook Action's hook. * @param array $args Action's args. * @param string $group Action's group. * * @deprecated 2.1.0 * * @return int|bool The timestamp for the next occurrence, or false if nothing was found */ function wc_next_scheduled_action( $hook, $args = null, $group = '' ) { _deprecated_function( __FUNCTION__, '2.1.0', 'as_next_scheduled_action()' ); return as_next_scheduled_action( $hook, $args, $group ); } /** * Find scheduled actions * * @param array $args Possible arguments, with their default values: * 'hook' => '' - the name of the action that will be triggered * 'args' => NULL - the args array that will be passed with the action * 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' * 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' * 'group' => '' - the group the action belongs to * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING * 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID * 'per_page' => 5 - Number of results to return * 'offset' => 0 * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date' * 'order' => 'ASC'. * @param string $return_format OBJECT, ARRAY_A, or ids. * * @deprecated 2.1.0 * * @return array */ function wc_get_scheduled_actions( $args = array(), $return_format = OBJECT ) { _deprecated_function( __FUNCTION__, '2.1.0', 'as_get_scheduled_actions()' ); return as_get_scheduled_actions( $args, $return_format ); }