plugin updates

This commit is contained in:
Tony Volpe
2024-09-05 11:04:01 -04:00
parent ed6b060261
commit 50cd64dd3d
925 changed files with 16918 additions and 13003 deletions

View File

@@ -626,7 +626,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
*/
public function set_status( $new_status ) {
$old_status = $this->get_status();
$new_status = 'wc-' === substr( $new_status, 0, 3 ) ? substr( $new_status, 3 ) : $new_status;
$new_status = OrderUtil::remove_status_prefix( $new_status );
$status_exceptions = array( 'auto-draft', 'trash' );

View File

@@ -65,6 +65,7 @@ class WC_Product extends WC_Abstract_Legacy_Product {
'description' => '',
'short_description' => '',
'sku' => '',
'global_unique_id' => '',
'price' => '',
'regular_price' => '',
'sale_price' => '',
@@ -251,7 +252,7 @@ class WC_Product extends WC_Abstract_Legacy_Product {
}
/**
* Get SKU (Stock-keeping unit) - product unique ID.
* Get SKU (Stock-keeping unit).
*
* @param string $context What the value is for. Valid values are view and edit.
* @return string
@@ -260,6 +261,17 @@ class WC_Product extends WC_Abstract_Legacy_Product {
return $this->get_prop( 'sku', $context );
}
/**
* Get Unique ID.
*
* @since 9.1.0
* @param string $context What the value is for. Valid values are view and edit.
* @return string
*/
public function get_global_unique_id( $context = 'view' ) {
return $this->get_prop( 'global_unique_id', $context );
}
/**
* Returns the product's active price.
*
@@ -835,6 +847,29 @@ class WC_Product extends WC_Abstract_Legacy_Product {
$this->set_prop( 'sku', $sku );
}
/**
* Set global_unique_id
*
* @since 9.1.0
* @param string $global_unique_id Unique ID.
*/
public function set_global_unique_id( $global_unique_id ) {
$global_unique_id = preg_replace( '/[^0-9\-]/', '', (string) $global_unique_id );
if ( $this->get_object_read() && ! empty( $global_unique_id ) && ! wc_product_has_global_unique_id( $this->get_id(), $global_unique_id ) ) {
$global_unique_id_found = wc_get_product_id_by_global_unique_id( $global_unique_id );
$this->error(
'product_invalid_global_unique_id',
__( 'Invalid or duplicated GTIN, UPC, EAN or ISBN.', 'woocommerce' ),
400,
array(
'resource_id' => $global_unique_id_found,
)
);
}
$this->set_prop( 'global_unique_id', $global_unique_id );
}
/**
* Set the product's active price.
*

View File

@@ -220,6 +220,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
'i18n_delete_product_notice' => __( 'This product has produced sales and may be linked to existing orders. Are you sure you want to delete it?', 'woocommerce' ),
'i18n_remove_personal_data_notice' => __( 'This action cannot be reversed. Are you sure you wish to erase personal data from the selected orders?', 'woocommerce' ),
'i18n_confirm_delete' => __( 'Are you sure you wish to delete this item?', 'woocommerce' ),
'i18n_global_unique_id_error' => __( 'Please enter only numbers and hyphens (-).', 'woocommerce' ),
'decimal_point' => $decimal,
'mon_decimal_point' => wc_get_price_decimal_separator(),
'ajax_url' => admin_url( 'admin-ajax.php' ),
@@ -276,7 +277,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
if ( in_array( $screen_id, array( 'product', 'edit-product' ) ) ) {
wp_enqueue_media();
wp_register_script( 'wc-admin-product-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'media-models' ), $version );
wp_register_script( 'wc-admin-variation-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product-variation' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'serializejson', 'media-models', 'backbone', 'jquery-ui-sortable', 'wc-backbone-modal' ), $version );
wp_register_script( 'wc-admin-variation-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product-variation' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'serializejson', 'media-models', 'backbone', 'jquery-ui-sortable', 'wc-backbone-modal', 'wp-data', 'wp-notices' ), $version );
wp_enqueue_script( 'wc-admin-product-meta-boxes' );
wp_enqueue_script( 'wc-admin-variation-meta-boxes' );

View File

@@ -156,6 +156,9 @@ class WC_Admin_Duplicate_Product {
if ( '' !== $product->get_sku( 'edit' ) ) {
$duplicate->set_sku( wc_product_generate_unique_sku( 0, $product->get_sku( 'edit' ) ) );
}
if ( '' !== $product->get_global_unique_id( 'edit' ) ) {
$duplicate->set_global_unique_id( '' );
}
$duplicate->set_status( 'draft' );
$duplicate->set_date_created( null );
$duplicate->set_slug( '' );
@@ -195,6 +198,9 @@ class WC_Admin_Duplicate_Product {
if ( '' !== $child->get_sku( 'edit' ) ) {
$child_duplicate->set_sku( wc_product_generate_unique_sku( 0, $child->get_sku( 'edit' ) ) );
}
if ( '' !== $child->get_global_unique_id( 'edit' ) ) {
$child_duplicate->set_global_unique_id( '' );
}
foreach ( $meta_to_exclude as $meta_key ) {
$child_duplicate->delete_meta_data( $meta_key );

View File

@@ -164,7 +164,7 @@ class WC_Admin_Notices {
* and the Legacy REST API plugin is not installed.
*/
private static function maybe_add_legacy_api_removal_notice() {
if ( wc_get_container()->get( WebhookUtil::class )->get_legacy_webhooks_count() > 0 && is_null( WC()->api ) ) {
if ( wc_get_container()->get( WebhookUtil::class )->get_legacy_webhooks_count() > 0 && ! WC()->legacy_rest_api_is_available() ) {
self::add_custom_notice(
'legacy_webhooks_unsupported_in_woo_90',
sprintf(
@@ -189,7 +189,7 @@ class WC_Admin_Notices {
* Remove the admin notice about the unsupported webhooks if the Legacy REST API plugin is installed.
*/
private static function maybe_remove_legacy_api_removal_notice() {
if ( self::has_notice( 'legacy_webhooks_unsupported_in_woo_90' ) && ( ! is_null( WC()->api ) || 0 === wc_get_container()->get( WebhookUtil::class )->get_legacy_webhooks_count() ) ) {
if ( self::has_notice( 'legacy_webhooks_unsupported_in_woo_90' ) && ( WC()->legacy_rest_api_is_available() || 0 === wc_get_container()->get( WebhookUtil::class )->get_legacy_webhooks_count() ) ) {
self::remove_notice( 'legacy_webhooks_unsupported_in_woo_90' );
}
}

View File

@@ -43,6 +43,12 @@ class WC_Admin_Post_Types {
add_filter( 'post_updated_messages', array( $this, 'post_updated_messages' ) );
add_filter( 'woocommerce_order_updated_messages', array( $this, 'order_updated_messages' ) );
add_filter( 'bulk_post_updated_messages', array( $this, 'bulk_post_updated_messages' ), 10, 2 );
add_action(
'admin_notices',
function () {
$this->maybe_display_warning_for_password_protected_coupon();
}
);
// Disable Auto Save.
add_action( 'admin_print_scripts', array( $this, 'disable_autosave' ) );
@@ -256,6 +262,33 @@ class WC_Admin_Post_Types {
return $bulk_messages;
}
/**
* Shows a warning when editing a password-protected coupon.
*
* @since 9.2.0
*/
private function maybe_display_warning_for_password_protected_coupon() {
if ( ! function_exists( 'get_current_screen' ) || 'shop_coupon' !== get_current_screen()->id ) {
return;
}
if ( ! isset( $GLOBALS['post'] ) || 'shop_coupon' !== $GLOBALS['post']->post_type ) {
return;
}
wp_admin_notice(
__(
'This coupon is password protected. WooCommerce does not support password protection for coupons. You can temporarily hide a coupon by making it private. Alternatively, usage limits and restrictions can be configured below.',
'woocommerce'
),
array(
'type' => 'warning',
'id' => 'wc-password-protected-coupon-warning',
'additional_classes' => empty( $GLOBALS['post']->post_password ) ? array( 'hidden' ) : array(),
)
);
}
/**
* Custom bulk edit - form.
*

View File

@@ -47,7 +47,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
if ( empty( self::$settings ) ) {
$settings = array();
include_once dirname( __FILE__ ) . '/settings/class-wc-settings-page.php';
include_once __DIR__ . '/settings/class-wc-settings-page.php';
$settings[] = include __DIR__ . '/settings/class-wc-settings-general.php';
$settings[] = include __DIR__ . '/settings/class-wc-settings-products.php';
@@ -153,7 +153,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
// Get tabs for the settings page.
$tabs = apply_filters( 'woocommerce_settings_tabs_array', array() );
include dirname( __FILE__ ) . '/views/html-admin-settings.php';
include __DIR__ . '/views/html-admin-settings.php';
}
/**
@@ -514,11 +514,19 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
$visibility_class[] = $value['row_class'];
}
$must_disable = $value['disabled'] ?? false;
if ( $must_disable ) {
$visibility_class[] = 'disabled';
}
$container_class = implode( ' ', $visibility_class );
$must_disable = $value['disabled'] ?? false;
$has_title = isset( $value['title'] ) && '' !== $value['title'];
$has_legend = isset( $value['legend'] ) && '' !== $value['legend'];
if ( ! isset( $value['checkboxgroup'] ) || 'start' === $value['checkboxgroup'] ) {
$has_tooltip = isset( $value['tooltip'] ) && '' !== $value['tooltip'];
$has_tooltip = isset( $value['tooltip'] ) && '' !== $value['tooltip'];
$tooltip_container_class = $has_tooltip ? 'with-tooltip' : '';
?>
<tr class="<?php echo esc_attr( $container_class ); ?>">
@@ -535,9 +543,9 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
<?php
}
if ( ! empty( $value['title'] ) ) {
if ( $has_title || $has_legend ) {
?>
<legend class="screen-reader-text"><span><?php echo esc_html( $value['title'] ); ?></span></legend>
<legend class="<?php echo $has_legend ? '' : 'screen-reader-text'; ?>"><span><?php echo esc_html( $has_legend ? $value['legend'] : $value['title'] ); ?></span></legend>
<?php
}
@@ -550,7 +558,6 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
type="checkbox"
class="<?php echo esc_attr( isset( $value['class'] ) ? $value['class'] : '' ); ?>"
value="1"
<?php disabled( $value['disabled'] ?? false ); ?>
<?php checked( $option_value, 'yes' ); ?>
<?php echo implode( ' ', $custom_attributes ); // WPCS: XSS ok. ?>
/> <?php echo $description; // WPCS: XSS ok. ?>

View File

@@ -76,7 +76,7 @@ class WC_Admin_Webhooks_Table_List extends WP_List_Table {
// Title.
$warning_prefix =
is_null( wc()->api ) && $this->uses_legacy_rest_api( $webhook ) ?
$this->uses_legacy_rest_api( $webhook ) && ! WC()->legacy_rest_api_is_available() ?
sprintf(
"<span title='%s'>⚠️</span> ",
esc_html__( 'This webhook is configured to be delivered using the Legacy REST API, but the Legacy REST API plugin is not installed on this site.', 'woocommerce' )

View File

@@ -332,7 +332,7 @@ class WC_Admin_Webhooks {
private static function maybe_display_legacy_rest_api_warning() {
global $webhooks_table_list;
if ( ! is_null( wc()->api ) ) {
if ( WC()->legacy_rest_api_is_available() ) {
return;
}

View File

@@ -101,6 +101,14 @@ class WC_Helper_Admin {
$connect_url_args['wc-helper-nonce'] = wp_create_nonce( 'connect' );
}
if ( ! empty( $_GET['utm_source'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$connect_url_args['utm_source'] = wc_clean( wp_unslash( $_GET['utm_source'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
if ( ! empty( $_GET['utm_campaign'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$connect_url_args['utm_campaign'] = wc_clean( wp_unslash( $_GET['utm_campaign'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
return add_query_arg(
$connect_url_args,
admin_url( 'admin.php' )

View File

@@ -6,6 +6,8 @@
* @package WooCommerce\Admin\Helper
*/
use Automattic\WooCommerce\Admin\PluginsHelper;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@@ -26,7 +28,7 @@ class WC_Helper_Updater {
add_action( 'pre_set_site_transient_update_themes', array( __CLASS__, 'transient_update_themes' ), 21, 1 );
add_action( 'upgrader_process_complete', array( __CLASS__, 'upgrader_process_complete' ) );
add_action( 'upgrader_pre_download', array( __CLASS__, 'block_expired_updates' ), 10, 2 );
add_action( 'plugins_loaded', array( __CLASS__, 'add_hook_for_modifying_update_notices' ) );
add_action( 'admin_init', array( __CLASS__, 'add_hook_for_modifying_update_notices' ) );
}
/**
@@ -186,9 +188,11 @@ class WC_Helper_Updater {
public static function add_connect_woocom_plugin_message() {
$connect_page_url = add_query_arg(
array(
'page' => 'wc-admin',
'tab' => 'my-subscriptions',
'path' => rawurlencode( '/extensions' ),
'page' => 'wc-admin',
'tab' => 'my-subscriptions',
'path' => rawurlencode( '/extensions' ),
'utm_source' => 'pu',
'utm_campaign' => 'pu_plugin_screen_connect',
),
admin_url( 'admin.php' )
);
@@ -252,41 +256,68 @@ class WC_Helper_Updater {
* @return void.
*/
public static function display_notice_for_expired_and_expiring_subscriptions( $plugin_data, $response ) {
// Extract product ID from the response.
$product_id = preg_replace( '/[^0-9]/', '', $response->id );
// Get the subscription details based on product ID.
$subscription = current(
wp_list_filter(
WC_Helper::get_subscriptions(),
array( 'product_id' => $product_id )
)
);
// Check if subscription is empty.
if ( empty( $subscription ) ) {
// Product subscriptions.
$subscriptions = wp_list_filter( WC_Helper::get_installed_subscriptions(), array( 'product_id' => $product_id ) );
if ( empty( $subscriptions ) ) {
return;
}
$expired_subscription = current(
array_filter(
$subscriptions,
function ( $subscription ) {
return ! empty( $subscription['expired'] ) && ! $subscription['lifetime'];
}
)
);
$expiring_subscription = current(
array_filter(
$subscriptions,
function ( $subscription ) {
return ! empty( $subscription['expiring'] ) && ! $subscription['autorenew'];
}
)
);
// Prepare the expiry notice based on subscription status.
$expiry_notice = '';
if ( ! empty( $subscription['expired'] ) && ! $subscription['lifetime'] ) {
if ( ! empty( $expired_subscription ) ) {
$renew_link = add_query_arg(
array(
'utm_source' => 'pu',
'utm_campaign' => 'pu_plugin_screen_renew',
),
PluginsHelper::WOO_SUBSCRIPTION_PAGE_URL
);
/* translators: 1: Product regular price */
$product_price = ! empty( $subscription['product_regular_price'] ) ? sprintf( __( 'for %s ', 'woocommerce' ), esc_html( $subscription['product_regular_price'] ) ) : '';
$product_price = ! empty( $expired_subscription['product_regular_price'] ) ? sprintf( __( 'for %s ', 'woocommerce' ), esc_html( $expired_subscription['product_regular_price'] ) ) : '';
$expiry_notice = sprintf(
/* translators: 1: URL to My Subscriptions page 2: Product price */
__( ' Your subscription expired, <a href="%1$s" class="woocommerce-renew-subscription">renew %2$s</a>to update.', 'woocommerce' ),
esc_url( 'https://woocommerce.com/my-account/my-subscriptions/' ),
esc_url( $renew_link ),
$product_price
);
} elseif ( ! empty( $subscription['expiring'] ) && ! $subscription['autorenew'] ) {
} elseif ( ! empty( $expiring_subscription ) ) {
$renew_link = add_query_arg(
array(
'utm_source' => 'pu',
'utm_campaign' => 'pu_plugin_screen_enable_autorenew',
),
PluginsHelper::WOO_SUBSCRIPTION_PAGE_URL
);
$expiry_notice = sprintf(
/* translators: 1: Expiry date 1: URL to My Subscriptions page */
__( ' Your subscription expires on %1$s, <a href="%2$s" class="woocommerce-enable-autorenew">enable auto-renew</a> to continue receiving updates.', 'woocommerce' ),
date_i18n( 'F jS', $subscription['expires'] ),
esc_url( 'https://woocommerce.com/my-account/my-subscriptions/' )
date_i18n( 'F jS', $expiring_subscription['expires'] ),
esc_url( $renew_link )
);
}

View File

@@ -63,6 +63,7 @@ class WC_Helper {
include_once __DIR__ . '/class-wc-helper-admin.php';
include_once __DIR__ . '/class-wc-helper-subscriptions-api.php';
include_once __DIR__ . '/class-wc-helper-orders-api.php';
include_once __DIR__ . '/class-wc-product-usage-notice.php';
}
/**
@@ -785,6 +786,14 @@ class WC_Helper {
$redirect_url_args['install'] = sanitize_text_field( wp_unslash( $_GET['install'] ) );
}
if ( isset( $_GET['utm_source'] ) ) {
$redirect_url_args['utm_source'] = wc_clean( wp_unslash( $_GET['utm_source'] ) );
}
if ( isset( $_GET['utm_campaign'] ) ) {
$redirect_url_args['utm_campaign'] = wc_clean( wp_unslash( $_GET['utm_campaign'] ) );
}
$redirect_uri = add_query_arg(
$redirect_url_args,
admin_url( 'admin.php' )
@@ -1296,6 +1305,60 @@ class WC_Helper {
return ! empty( $subscription );
}
/**
* Get the user's connected subscriptions that are installed on the current
* site.
*
* @return array
*/
public static function get_installed_subscriptions() {
static $installed_subscriptions = null;
// Cache installed_subscriptions in the current request.
if ( is_null( $installed_subscriptions ) ) {
$auth = WC_Helper_Options::get( 'auth' );
$site_id = isset( $auth['site_id'] ) ? absint( $auth['site_id'] ) : 0;
if ( 0 === $site_id ) {
$installed_subscriptions = array();
return $installed_subscriptions;
}
$installed_subscriptions = array_filter(
self::get_subscriptions(),
function ( $subscription ) use ( $site_id ) {
return in_array( $site_id, $subscription['connections'], true );
}
);
}
return $installed_subscriptions;
}
/**
* Get subscription state of a given product ID.
*
* @since TBD
*
* @param int $product_id The product id.
*
* @return array Array of state_name => (bool) state
*/
public static function get_product_subscription_state( $product_id ) {
$product_subscriptions = wp_list_filter( self::get_installed_subscriptions(), array( 'product_id' => $product_id ) );
$subscription = ! empty( $product_subscriptions )
? array_shift( $product_subscriptions )
: array();
return array(
'unregistered' => empty( $subscription ),
'expired' => ( isset( $subscription['expired'] ) && $subscription['expired'] ),
'expiring' => ( isset( $subscription['expiring'] ) && $subscription['expiring'] ),
'key' => $subscription['product_key'] ?? '',
'order_id' => $subscription['order_id'] ?? '',
);
}
/**
* Get a subscription entry from product_id. If multiple subscriptions are
* found with the same product id and $single is set to true, will return the
@@ -1482,6 +1545,41 @@ class WC_Helper {
return $woo_themes;
}
/**
* Get rules for displaying notice regarding marketplace product usage.
*
* @return array
*/
public static function get_product_usage_notice_rules() {
$cache_key = '_woocommerce_helper_product_usage_notice_rules';
$data = get_transient( $cache_key );
if ( false !== $data ) {
return $data;
}
$request = WC_Helper_API::get(
'product-usage-notice-rules',
array(
'authenticated' => false,
)
);
// Retry in 15 minutes for non-200 response.
if ( wp_remote_retrieve_response_code( $request ) !== 200 ) {
set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS );
return array();
}
$data = json_decode( wp_remote_retrieve_body( $request ), true );
if ( empty( $data ) || ! is_array( $data ) ) {
$data = array();
}
set_transient( $cache_key, $data, 1 * HOUR_IN_SECONDS );
return $data;
}
/**
* Get the connected user's subscriptions.
*

View File

@@ -0,0 +1,384 @@
<?php
/**
* WooCommerce Product Usage Notice.
*
* @package WooCommerce\Admin\Helper
*/
declare( strict_types = 1 );
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Product usage notice class.
*/
class WC_Product_Usage_Notice {
/**
* User meta key prefix to store dismiss counts per product. Product ID is
* the suffix part.
*
* @var string
*/
const DISMISSED_COUNT_META_PREFIX = '_woocommerce_product_usage_notice_dismissed_count_';
/**
* User meta key prefix to store timestamp of last dismissed product usage notice.
* Product ID is the suffix part.
*
* @var string
*/
const DISMISSED_TIMESTAMP_META_PREFIX = '_woocommerce_product_usage_notice_dismissed_timestamp_';
/**
* User meta key prefix to store timestamp of last clicked remind later from
* product usage notice. Product ID is the suffix part.
*
* @var string
*/
const REMIND_LATER_TIMESTAMP_META_PREFIX = '_woocommerce_product_usage_notice_remind_later_timestamp_';
/**
* User meta key to store timestamp of last dismissed of any product usage
* notices. There's no product ID in the meta key.
*
* @var string
*/
const LAST_DISMISSED_TIMESTAMP_META = '_woocommerce_product_usage_notice_last_dismissed_timestamp';
/**
* Array of product usage notice rules from helper API.
*
* @var array
*/
private static $product_usage_notice_rules = array();
/**
* Current product usage notice rule applied to the current admin screen.
*
* @var array
*/
private static $current_notice_rule = array();
/**
* Loads the class, runs on init.
*
* @return void
*/
public static function load() {
add_action( 'current_screen', array( __CLASS__, 'maybe_show_product_usage_notice' ) );
add_action( 'wp_ajax_woocommerce_dismiss_product_usage_notice', array( __CLASS__, 'ajax_dismiss' ) );
add_action( 'wp_ajax_woocommerce_remind_later_product_usage_notice', array( __CLASS__, 'ajax_remind_later' ) );
}
/**
* Maybe show product usage notice in a given screen object.
*
* @param \WP_Screen $screen Current \WP_Screen object.
*/
public static function maybe_show_product_usage_notice( $screen ) {
$user_id = get_current_user_id();
if ( ! $user_id ) {
return;
}
if ( ! WC_Helper::is_site_connected() ) {
return;
}
self::$product_usage_notice_rules = WC_Helper::get_product_usage_notice_rules();
if ( empty( self::$product_usage_notice_rules ) ) {
return;
}
self::$current_notice_rule = self::get_current_notice_rule( $screen );
if ( empty( self::$current_notice_rule ) ) {
return;
}
$product_id = self::$current_notice_rule['id'];
if ( self::is_notice_throttled( $user_id, $product_id ) ) {
return;
}
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_product_usage_notice_scripts' ) );
}
/**
* Check whether the user clicked "remind later" recently.
*
* @param int $user_id User ID.
* @param int $product_id Product ID.
*
* @return bool
*/
private static function is_remind_later_clicked_recently( int $user_id, int $product_id ): bool {
$last_remind_later_ts = absint(
get_user_meta(
$user_id,
self::REMIND_LATER_TIMESTAMP_META_PREFIX . $product_id,
true
)
);
if ( 0 === $last_remind_later_ts ) {
return false;
}
$seconds_since_clicked_remind_later = time() - $last_remind_later_ts;
$wait_after_remind_later = self::$current_notice_rule['wait_in_seconds_after_remind_later'];
return $seconds_since_clicked_remind_later < $wait_after_remind_later;
}
/**
* Check whether the user has reached max dismissals of product usage notice.
*
* @param int $user_id User ID.
* @param int $product_id Product ID.
*
* @return bool
*/
private static function has_reached_max_dismissals( int $user_id, int $product_id ): bool {
$dismiss_count = absint(
get_user_meta(
$user_id,
self::DISMISSED_COUNT_META_PREFIX . $product_id,
true
)
);
$max_dismissals = self::$current_notice_rule['max_dismissals'];
return $dismiss_count >= $max_dismissals;
}
/**
* Check whether the user dismissed any product usage notices recently.
*
* @param int $user_id User ID.
*
* @return bool
*/
private static function is_any_notices_dismissed_recently( int $user_id ): bool {
$global_last_dismissed_ts = absint(
get_user_meta(
$user_id,
self::LAST_DISMISSED_TIMESTAMP_META,
true
)
);
if ( 0 === $global_last_dismissed_ts ) {
return false;
}
$seconds_since_dismissed = time() - $global_last_dismissed_ts;
$wait_after_any_dismisses = self::$product_usage_notice_rules['wait_in_seconds_after_any_dismisses'];
return $seconds_since_dismissed < $wait_after_any_dismisses;
}
/**
* Check whether the user dismissed given product usage notice recently.
*
* @param int $user_id User ID.
* @param int $product_id Product ID.
*
* @return bool
*/
private static function is_product_notice_dismissed_recently( int $user_id, int $product_id ): bool {
$last_dismissed_ts = absint(
get_user_meta(
$user_id,
self::DISMISSED_TIMESTAMP_META_PREFIX . $product_id,
true
)
);
if ( 0 === $last_dismissed_ts ) {
return false;
}
$seconds_since_dismissed = time() - $last_dismissed_ts;
$wait_after_dismiss = self::$current_notice_rule['wait_in_seconds_after_dismiss'];
return $seconds_since_dismissed < $wait_after_dismiss;
}
/**
* Check whether current notice is throttled for the user and product.
*
* @param int $user_id User ID.
* @param int $product_id Product ID.
*
* @return bool
*/
private static function is_notice_throttled( int $user_id, int $product_id ): bool {
return self::is_remind_later_clicked_recently( $user_id, $product_id ) ||
self::has_reached_max_dismissals( $user_id, $product_id ) ||
self::is_any_notices_dismissed_recently( $user_id ) ||
self::is_product_notice_dismissed_recently( $user_id, $product_id );
}
/**
* Enqueue scripts needed to display product usage notice (or modal).
*/
public static function enqueue_product_usage_notice_scripts() {
WCAdminAssets::register_style( 'woo-product-usage-notice', 'style', array( 'wp-components' ) );
WCAdminAssets::register_script( 'wp-admin-scripts', 'woo-product-usage-notice', true );
$subscribe_url = add_query_arg(
array(
'add-to-cart' => self::$current_notice_rule['id'],
'utm_source' => 'pu',
'utm_medium' => 'product',
'utm_campaign' => 'pu_modal_subscribe',
),
'https://woocommerce.com/cart/'
);
$renew_url = add_query_arg(
array(
'renew_product' => self::$current_notice_rule['id'],
'product_key' => self::$current_notice_rule['state']['key'],
'order_id' => self::$current_notice_rule['state']['order_id'],
'utm_source' => 'pu',
'utm_medium' => 'product',
'utm_campaign' => 'pu_modal_renew',
),
'https://woocommerce.com/cart/'
);
wp_localize_script(
'wc-admin-woo-product-usage-notice',
'wooProductUsageNotice',
array(
'subscribeUrl' => $subscribe_url,
'renewUrl' => $renew_url,
'dismissAction' => 'woocommerce_dismiss_product_usage_notice',
'remindLaterAction' => 'woocommerce_remind_later_product_usage_notice',
'productId' => self::$current_notice_rule['id'],
'productName' => self::$current_notice_rule['name'],
'productRegularPrice' => self::$current_notice_rule['regular_price'],
'dismissNonce' => wp_create_nonce( 'dismiss_product_usage_notice' ),
'remindLaterNonce' => wp_create_nonce( 'remind_later_product_usage_notice' ),
'showAs' => self::$current_notice_rule['show_as'],
'colorScheme' => self::$current_notice_rule['color_scheme'],
'subscriptionState' => self::$current_notice_rule['state'],
'screenId' => get_current_screen()->id,
)
);
}
/**
* Get product usage notice rule from a given WP_Screen object.
*
* @param \WP_Screen $screen Current \WP_Screen object.
*
* @return array
*/
private static function get_current_notice_rule( $screen ) {
foreach ( self::$product_usage_notice_rules['products'] as $product_id => $rule ) {
if ( ! isset( $rule['screens'][ $screen->id ] ) ) {
continue;
}
// Check query strings.
if ( ! self::query_string_matches( $screen, $rule ) ) {
continue;
}
$product_id = absint( $product_id );
$state = WC_Helper::get_product_subscription_state( $product_id );
if ( $state['expired'] || $state['unregistered'] ) {
$rule['id'] = $product_id;
$rule['state'] = $state;
return $rule;
}
}
return array();
}
/**
* Check whether the screen and GET parameter matches a given rule.
*
* @param \WP_Screen $screen Current \WP_Screen object.
* @param array $rule Product usage notice rule.
*
* @return bool
*/
private static function query_string_matches( $screen, $rule ) {
if ( empty( $rule['screens'][ $screen->id ]['qs'] ) ) {
return true;
}
$qs = $rule['screens'][ $screen->id ]['qs'];
foreach ( $qs as $key => $val ) {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( empty( $_GET[ $key ] ) || $_GET[ $key ] !== $val ) {
return false;
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}
return true;
}
/**
* AJAX handler for dismiss action of product usage notice.
*/
public static function ajax_dismiss() {
if ( ! check_ajax_referer( 'dismiss_product_usage_notice' ) ) {
wp_die( -1 );
}
$user_id = get_current_user_id();
if ( ! $user_id ) {
wp_die( -1 );
}
$product_id = absint( $_GET['product_id'] ?? 0 );
if ( ! $product_id ) {
wp_die( -1 );
}
$dismiss_count = absint( get_user_meta( $user_id, self::DISMISSED_COUNT_META_PREFIX . $product_id, true ) );
update_user_meta( $user_id, self::DISMISSED_COUNT_META_PREFIX . $product_id, $dismiss_count + 1 );
update_user_meta( $user_id, self::DISMISSED_TIMESTAMP_META_PREFIX . $product_id, time() );
update_user_meta( $user_id, self::LAST_DISMISSED_TIMESTAMP_META, time() );
wp_die( 1 );
}
/**
* AJAX handler for "remind later" action of product usage notice.
*/
public static function ajax_remind_later() {
if ( ! check_ajax_referer( 'remind_later_product_usage_notice' ) ) {
wp_die( -1 );
}
$user_id = get_current_user_id();
if ( ! $user_id ) {
wp_die( -1 );
}
$product_id = absint( $_GET['product_id'] ?? 0 );
if ( ! $product_id ) {
wp_die( -1 );
}
update_user_meta( $user_id, self::REMIND_LATER_TIMESTAMP_META_PREFIX . $product_id, time() );
wp_die( 1 );
}
}
WC_Product_Usage_Notice::load();

View File

@@ -369,6 +369,7 @@ class WC_Meta_Box_Product_Data {
$errors = $product->set_props(
array(
'sku' => isset( $_POST['_sku'] ) ? wc_clean( wp_unslash( $_POST['_sku'] ) ) : null,
'global_unique_id' => isset( $_POST['_global_unique_id'] ) ? wc_clean( wp_unslash( $_POST['_global_unique_id'] ) ) : null,
'purchase_note' => isset( $_POST['_purchase_note'] ) ? wp_kses_post( wp_unslash( $_POST['_purchase_note'] ) ) : '',
'downloadable' => isset( $_POST['_downloadable'] ),
'virtual' => isset( $_POST['_virtual'] ),
@@ -544,6 +545,7 @@ class WC_Meta_Box_Product_Data {
'image_id' => isset( $_POST['upload_image_id'][ $i ] ) ? wc_clean( wp_unslash( $_POST['upload_image_id'][ $i ] ) ) : null,
'attributes' => self::prepare_set_attributes( $parent->get_attributes(), 'attribute_', $i ),
'sku' => isset( $_POST['variable_sku'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_sku'][ $i ] ) ) : '',
'global_unique_id' => isset( $_POST['variable_global_unique_id'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_global_unique_id'][ $i ] ) ) : '',
'weight' => isset( $_POST['variable_weight'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_weight'][ $i ] ) ) : '',
'length' => isset( $_POST['variable_length'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_length'][ $i ] ) ) : '',
'width' => isset( $_POST['variable_width'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_width'][ $i ] ) ) : '',

View File

@@ -78,7 +78,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<td class="wc-order-edit-line-item">
<?php if ( $order->is_editable() ) : ?>
<div class="wc-order-edit-line-item-actions">
<a class="edit-order-item" href="#"></a><a class="delete-order-item" href="#"></a>
<a class="edit-order-item tips" href="#" data-tip="<?php esc_attr_e( 'Edit fee', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'Edit fee', 'woocommerce' ); ?>"></a><a class="delete-order-item tips" href="#" data-tip="<?php esc_attr_e( 'Delete fee', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'Delete fee', 'woocommerce' ); ?>"></a>
</div>
<?php endif; ?>
</td>

View File

@@ -181,7 +181,7 @@ $row_class = apply_filters( 'woocommerce_admin_html_order_item_class', ! empt
<td class="wc-order-edit-line-item" width="1%">
<div class="wc-order-edit-line-item-actions">
<?php if ( $order->is_editable() ) : ?>
<a class="edit-order-item tips" href="#" data-tip="<?php esc_attr_e( 'Edit item', 'woocommerce' ); ?>"></a><a class="delete-order-item tips" href="#" data-tip="<?php esc_attr_e( 'Delete item', 'woocommerce' ); ?>"></a>
<a class="edit-order-item tips" href="#" data-tip="<?php esc_attr_e( 'Edit item', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'Edit item', 'woocommerce' ); ?>"></a><a class="delete-order-item tips" href="#" data-tip="<?php esc_attr_e( 'Delete item', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'Delete item', 'woocommerce' ); ?>"></a>
<?php endif; ?>
</div>
</td>

View File

@@ -109,7 +109,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<td class="wc-order-edit-line-item">
<?php if ( $order->is_editable() ) : ?>
<div class="wc-order-edit-line-item-actions">
<a class="edit-order-item" href="#"></a><a class="delete-order-item" href="#"></a>
<a class="edit-order-item tips" href="#" data-tip="<?php esc_attr_e( 'Edit shipping', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'Edit shipping', 'woocommerce' ); ?>"></a><a class="delete-order-item tips" href="#" data-tip="<?php esc_attr_e( 'Delete shipping', 'woocommerce' ); ?>" aria-label="<?php esc_attr_e( 'Delete shipping', 'woocommerce' ); ?>"></a>
</div>
<?php endif; ?>
</td>

View File

@@ -28,6 +28,18 @@ if ( ! defined( 'ABSPATH' ) ) {
do_action( 'woocommerce_product_options_sku' );
woocommerce_wp_text_input(
array(
'id' => '_global_unique_id',
'value' => $product_object->get_global_unique_id( 'edit' ),
'label' => __( 'GTIN, UPC, EAN or ISBN', 'woocommerce' ),
'desc_tip' => true,
'description' => __( 'Enter a barcode or any other identifier unique to this product. It can help you list this product on other channels or marketplaces.', 'woocommerce' ),
)
);
do_action( 'woocommerce_product_options_global_unique_id' );
?>
<div class="inline notice woocommerce-message show_if_variable">
<img class="info-icon" src="<?php echo esc_url( $info_img_url ); ?>" />

View File

@@ -70,27 +70,43 @@ defined( 'ABSPATH' ) || exit;
</h3>
<div class="woocommerce_variable_attributes wc-metabox-content" style="display: none;">
<div class="data">
<p class="form-row form-row-first upload_image">
<a href="#" class="upload_image_button tips <?php echo $variation_object->get_image_id( 'edit' ) ? 'remove' : ''; ?>" data-tip="<?php echo $variation_object->get_image_id( 'edit' ) ? esc_attr__( 'Remove this image', 'woocommerce' ) : esc_attr__( 'Upload an image', 'woocommerce' ); ?>" rel="<?php echo esc_attr( $variation_id ); ?>">
<img src="<?php echo $variation_object->get_image_id( 'edit' ) ? esc_url( wp_get_attachment_thumb_url( $variation_object->get_image_id( 'edit' ) ) ) : esc_url( wc_placeholder_img_src() ); ?>" /><input type="hidden" name="upload_image_id[<?php echo esc_attr( $loop ); ?>]" class="upload_image_id" value="<?php echo esc_attr( $variation_object->get_image_id( 'edit' ) ); ?>" />
</a>
</p>
<?php
if ( wc_product_sku_enabled() ) {
<div class="form-flex-box">
<p class="form-row upload_image">
<a href="#" class="upload_image_button tips <?php echo $variation_object->get_image_id( 'edit' ) ? 'remove' : ''; ?>" data-tip="<?php echo $variation_object->get_image_id( 'edit' ) ? esc_attr__( 'Remove this image', 'woocommerce' ) : esc_attr__( 'Upload an image', 'woocommerce' ); ?>" rel="<?php echo esc_attr( $variation_id ); ?>">
<img src="<?php echo $variation_object->get_image_id( 'edit' ) ? esc_url( wp_get_attachment_thumb_url( $variation_object->get_image_id( 'edit' ) ) ) : esc_url( wc_placeholder_img_src() ); ?>" /><input type="hidden" name="upload_image_id[<?php echo esc_attr( $loop ); ?>]" class="upload_image_id" value="<?php echo esc_attr( $variation_object->get_image_id( 'edit' ) ); ?>" />
</a>
</p>
<div class="form-row form-row-last">
<?php
if ( wc_product_sku_enabled() ) {
woocommerce_wp_text_input(
array(
'id' => "variable_sku{$loop}",
'name' => "variable_sku[{$loop}]",
'value' => $variation_object->get_sku( 'edit' ),
'placeholder' => $variation_object->get_sku(),
'label' => '<abbr title="' . esc_attr__( 'Stock Keeping Unit', 'woocommerce' ) . '">' . esc_html__( 'SKU', 'woocommerce' ) . '</abbr>',
'desc_tip' => true,
'description' => __( 'SKU refers to a Stock-keeping unit, a unique identifier for each distinct product and service that can be purchased.', 'woocommerce' ),
'wrapper_class' => 'form-row',
)
);
}
woocommerce_wp_text_input(
array(
'id' => "variable_sku{$loop}",
'name' => "variable_sku[{$loop}]",
'value' => $variation_object->get_sku( 'edit' ),
'placeholder' => $variation_object->get_sku(),
'label' => '<abbr title="' . esc_attr__( 'Stock Keeping Unit', 'woocommerce' ) . '">' . esc_html__( 'SKU', 'woocommerce' ) . '</abbr>',
'id' => "variable_global_unique_id{$loop}",
'name' => "variable_global_unique_id[{$loop}]",
'value' => $variation_object->get_global_unique_id( 'edit' ),
'placeholder' => $variation_object->get_global_unique_id(),
'label' => __( 'GTIN, UPC, EAN or ISBN', 'woocommerce' ),
'desc_tip' => true,
'description' => __( 'SKU refers to a Stock-keeping unit, a unique identifier for each distinct product and service that can be purchased.', 'woocommerce' ),
'wrapper_class' => 'form-row form-row-last',
'description' => __( 'Enter a barcode or any other identifier unique to this product. It can help you list this product on other channels or marketplaces.', 'woocommerce' ),
'wrapper_class' => 'form-row',
)
);
}
?>
?>
</div>
</div>
<p class="form-row form-row-full options">
<label>
<?php esc_html_e( 'Enabled', 'woocommerce' ); ?>

View File

@@ -53,7 +53,7 @@ class WC_Report_Customer_List extends WP_List_Table {
if ( ! empty( $_GET['link_orders'] ) && wp_verify_nonce( $_REQUEST['_wpnonce'], 'link_orders' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
$linked = wc_update_new_customer_past_orders( absint( $_GET['link_orders'] ) );
/* translators: single or plural number of orders */
echo '<div class="updated"><p>' . sprintf( esc_html( _n( '%s previous order linked', '%s previous orders linked', $linked, 'woocommerce' ), $linked ) ) . '</p></div>';
echo '<div class="updated"><p>' . esc_html( sprintf( _n( '%s previous order linked', '%s previous orders linked', $linked, 'woocommerce' ), $linked ) ) . '</p></div>';
}
if ( ! empty( $_GET['refresh'] ) && wp_verify_nonce( $_REQUEST['_wpnonce'], 'refresh' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput

View File

@@ -11,6 +11,8 @@ if ( class_exists( 'WC_Settings_Accounts', false ) ) {
return new WC_Settings_Accounts();
}
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
/**
* WC_Settings_Accounts.
*/
@@ -50,8 +52,9 @@ class WC_Settings_Accounts extends WC_Settings_Page {
'id' => 'account_registration_options',
),
array(
'title' => __( 'Guest checkout', 'woocommerce' ),
'desc' => __( 'Allow customers to place orders without an account', 'woocommerce' ),
'title' => __( 'Checkout', 'woocommerce' ),
'desc' => __( 'Enable guest checkout (recommended)', 'woocommerce' ),
'desc_tip' => __( 'Allows customers to checkout without an account.', 'woocommerce' ),
'id' => 'woocommerce_enable_guest_checkout',
'default' => 'yes',
'type' => 'checkbox',
@@ -60,7 +63,7 @@ class WC_Settings_Accounts extends WC_Settings_Page {
),
array(
'title' => __( 'Login', 'woocommerce' ),
'desc' => __( 'Allow customers to log into an existing account during checkout', 'woocommerce' ),
'desc' => __( 'Enable log-in during checkout', 'woocommerce' ),
'id' => 'woocommerce_enable_checkout_login_reminder',
'default' => 'no',
'type' => 'checkbox',
@@ -69,31 +72,38 @@ class WC_Settings_Accounts extends WC_Settings_Page {
),
array(
'title' => __( 'Account creation', 'woocommerce' ),
'desc' => __( 'Allow customers to create an account during checkout', 'woocommerce' ),
'desc' => __( 'During checkout', 'woocommerce' ),
'desc_tip' => __( 'Customers can create an account before placing their order.', 'woocommerce' ),
'id' => 'woocommerce_enable_signup_and_login_from_checkout',
'default' => 'no',
'type' => 'checkbox',
'checkboxgroup' => 'start',
'legend' => __( 'Allow customers to create an account:', 'woocommerce' ),
'autoload' => false,
),
array(
'title' => __( 'Account creation', 'woocommerce' ),
'desc' => __( 'On "My account" page', 'woocommerce' ),
'id' => 'woocommerce_enable_myaccount_registration',
'default' => 'no',
'type' => 'checkbox',
'checkboxgroup' => 'end',
'autoload' => false,
),
array(
'title' => __( 'Account creation options', 'woocommerce' ),
'desc' => __( 'Use email address as account login (recommended)', 'woocommerce' ),
'desc_tip' => __( 'If unchecked, customers will need to set a username during account creation.', 'woocommerce' ),
'id' => 'woocommerce_registration_generate_username',
'default' => 'yes',
'type' => 'checkbox',
'checkboxgroup' => 'start',
'autoload' => false,
),
array(
'desc' => __( 'Allow customers to create an account on the "My account" page', 'woocommerce' ),
'id' => 'woocommerce_enable_myaccount_registration',
'default' => 'no',
'type' => 'checkbox',
'checkboxgroup' => '',
'autoload' => false,
),
array(
'desc' => __( 'When creating an account, automatically generate an account username for the customer based on their name, surname or email', 'woocommerce' ),
'id' => 'woocommerce_registration_generate_username',
'default' => 'yes',
'type' => 'checkbox',
'checkboxgroup' => '',
'autoload' => false,
),
array(
'desc' => __( 'When creating an account, send the new user a link to set their password', 'woocommerce' ),
'title' => __( 'Account creation options', 'woocommerce' ),
'desc' => __( 'Send password setup link (recommended)', 'woocommerce' ),
'desc_tip' => __( 'New customers receive an email to set up their password.', 'woocommerce' ),
'id' => 'woocommerce_registration_generate_password',
'default' => 'yes',
'type' => 'checkbox',
@@ -118,7 +128,7 @@ class WC_Settings_Accounts extends WC_Settings_Page {
'id' => 'woocommerce_erasure_request_removes_download_data',
'type' => 'checkbox',
'default' => 'no',
'checkboxgroup' => 'end',
'checkboxgroup' => '',
'autoload' => false,
),
array(
@@ -127,7 +137,7 @@ class WC_Settings_Accounts extends WC_Settings_Page {
'desc_tip' => __( 'Adds an option to the orders screen for removing personal data in bulk. Note that removing personal data cannot be undone.', 'woocommerce' ),
'id' => 'woocommerce_allow_bulk_remove_personal_data',
'type' => 'checkbox',
'checkboxgroup' => 'start',
'checkboxgroup' => 'end',
'default' => 'no',
'autoload' => false,
),
@@ -229,10 +239,71 @@ class WC_Settings_Accounts extends WC_Settings_Page {
),
);
return apply_filters(
'woocommerce_' . $this->id . '_settings',
$account_settings
);
// Change settings when using the block based checkout.
if ( CartCheckoutUtils::is_checkout_block_default() ) {
$account_settings = array_filter(
$account_settings,
function ( $setting ) {
return 'woocommerce_registration_generate_username' !== $setting['id'];
},
);
$account_settings = array_map(
function ( $setting ) {
if ( 'woocommerce_registration_generate_password' === $setting['id'] ) {
unset( $setting['checkboxgroup'] );
}
return $setting;
},
$account_settings
);
}
/**
* Filter account settings.
*
* @hook woocommerce_account_settings
* @since 3.5.0
* @param array $account_settings Account settings.
*/
return apply_filters( 'woocommerce_' . $this->id . '_settings', $account_settings );
}
/**
* Output the HTML for the settings.
*/
public function output() {
parent::output();
// The following code toggles disabled state on the account options based on other values.
?>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
const checkboxes = [
document.getElementById("woocommerce_enable_signup_and_login_from_checkout"),
document.getElementById("woocommerce_enable_myaccount_registration"),
document.getElementById("woocommerce_enable_signup_from_checkout_for_subscriptions")
];
const inputs = [
document.getElementById("woocommerce_registration_generate_username"),
document.getElementById("woocommerce_registration_generate_password")
];
function updateInputs() {
const isChecked = checkboxes.some(cb => cb && cb.checked);
inputs.forEach(input => {
if ( ! input ) {
return;
}
input.disabled = !isChecked;
input.closest('td').classList.toggle("disabled", !isChecked);
});
}
checkboxes.forEach(cb => cb && cb.addEventListener('change', updateInputs));
updateInputs(); // Initial state
});
</script>
<?php
}
}

View File

@@ -391,7 +391,7 @@ class WC_Settings_Advanced extends WC_Settings_Page {
__( 'The legacy REST API is NOT enabled', 'woocommerce' );
$legacy_api_setting_tip =
is_plugin_active( 'woocommerce-legacy-rest-api/woocommerce-legacy-rest-api.php' ) ?
WC()->legacy_rest_api_is_available() ?
__( ' The WooCommerce Legacy REST API extension is installed and active.', 'woocommerce' ) :
sprintf(
/* translators: placeholders are URLs */

View File

@@ -189,17 +189,20 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page {
break;
case 'action':
$setup_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=' . strtolower( $gateway->id ) );
// Override the behaviour for WooPayments plugin.
// Override the behaviour for the WooPayments plugin.
if (
// Keep old brand name for backwards compatibility.
( 'WooCommerce Payments' === $method_title || 'WooPayments' === $method_title ) &&
class_exists( 'WC_Payments_Account' )
) {
if ( ! WooCommercePayments::is_connected() || WooCommercePayments::is_account_partially_onboarded() ) {
// The CTA text and label is "Finish set up" if the account is not connected or not completely onboarded.
$setup_url = WC_Payments_Account::get_connect_url(); // Plugin will handle the redirection to the connect page or directly to the provider (e.g. Stripe).
// The CTA text and label is "Finish setup" if the account is not connected or not completely onboarded.
// Plugin will handle the redirection to the connect page or directly to the provider (e.g. Stripe).
$setup_url = WC_Payments_Account::get_connect_url();
// Add the `from` parameter to the URL, so we know where the user came from.
$setup_url = add_query_arg( 'from', 'WCADMIN_PAYMENT_SETTINGS', $setup_url );
/* Translators: %s Payment gateway name. */
echo '<a class="button alignright" aria-label="' . esc_attr( sprintf( __( 'Set up the "%s" payment method', 'woocommerce' ), $method_title ) ) . '" href="' . esc_url( $setup_url ) . '">' . esc_html__( 'Finish set up', 'woocommerce' ) . '</a>';
echo '<a class="button alignright" aria-label="' . esc_attr( sprintf( __( 'Set up the "%s" payment method', 'woocommerce' ), $method_title ) ) . '" href="' . esc_url( $setup_url ) . '">' . esc_html__( 'Finish setup', 'woocommerce' ) . '</a>';
} else {
// If the account is fully onboarded, the CTA text and label is "Manage" regardless gateway is enabled or not.
/* Translators: %s Payment gateway name. */
@@ -210,7 +213,7 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page {
echo '<a class="button alignright" aria-label="' . esc_attr( sprintf( __( 'Manage the "%s" payment method', 'woocommerce' ), $method_title ) ) . '" href="' . esc_url( $setup_url ) . '">' . esc_html__( 'Manage', 'woocommerce' ) . '</a>';
} else {
/* Translators: %s Payment gateway name. */
echo '<a class="button alignright" aria-label="' . esc_attr( sprintf( __( 'Set up the "%s" payment method', 'woocommerce' ), $method_title ) ) . '" href="' . esc_url( $setup_url ) . '">' . esc_html__( 'Finish set up', 'woocommerce' ) . '</a>';
echo '<a class="button alignright" aria-label="' . esc_attr( sprintf( __( 'Set up the "%s" payment method', 'woocommerce' ), $method_title ) ) . '" href="' . esc_url( $setup_url ) . '">' . esc_html__( 'Finish setup', 'woocommerce' ) . '</a>';
}
break;
case 'status':

View File

@@ -446,6 +446,20 @@ class WC_Settings_Products extends WC_Settings_Page {
),
),
array(
'title' => __( 'Count partial downloads', 'woocommerce' ),
'desc' => __( 'Count downloads even if only part of a file is fetched.', 'woocommerce' ),
'id' => 'woocommerce_downloads_count_partial',
'type' => 'checkbox',
'default' => 'yes',
'desc_tip' => sprintf(
/* Translators: 1: opening link tag 2: closing link tag. */
__( 'Repeat fetches made within a reasonable window of time (by default, 30 minutes) will not be counted twice. This is a generally reasonably way to enforce download limits in relation to ranged requests. %1$sLearn more.%2$s', 'woocommerce' ),
'<a href="https://woocommerce.com/document/digital-downloadable-product-handling/">',
'</a>'
),
),
array(
'type' => 'sectionend',
'id' => 'digital_download_options',

View File

@@ -7,6 +7,7 @@
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
use Automattic\WooCommerce\Blocks\Shipping\ShippingController;
use Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils;
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -242,10 +243,15 @@ do_action( 'woocommerce_shipping_zone_after_methods_table', $zone );
);
} else {
/* translators: %s: Local pickup settings page URL. */
$message = __( 'Local pickup: Set up pickup locations in the <a href="%s">Local pickup settings page</a>.', 'woocommerce' );
if ( LocalPickupUtils::is_local_pickup_enabled() ) {
/* translators: %s: Local pickup settings page URL. */
$message = __( 'Local pickup: Manage existing pickup locations in the <a href="%s">Local pickup settings page</a>.', 'woocommerce' );
}
printf(
wp_kses(
/* translators: %s: Local pickup settings page URL. */
__( 'Local pickup: Set up pickup locations in the <a href="%s">Local pickup settings page</a>.', 'woocommerce' ),
$message,
array( 'a' => array( 'href' => array() ) )
),
esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping&section=pickup_location' ) )

View File

@@ -147,9 +147,9 @@ if ( ! defined( 'ABSPATH' ) ) {
<?php endforeach; ?>
<?php
$legacy_api_option_name =
is_null( wc()->api ) ?
__( 'Legacy API v3 (⚠️ NOT AVAILABLE)', 'woocommerce' ) :
__( 'Legacy API v3 (deprecated)', 'woocommerce' );
WC()->legacy_rest_api_is_available() ?
__( 'Legacy API v3 (deprecated)', 'woocommerce' ) :
__( 'Legacy API v3 (⚠️ NOT AVAILABLE)', 'woocommerce' );
?>
<option value="legacy_v3" <?php selected( 'legacy_v3', $webhook->get_api_version(), true ); ?>><?php echo esc_html( $legacy_api_option_name ); ?></option>
</select>

View File

@@ -102,12 +102,12 @@ if ( file_exists( $plugin_path ) ) {
<td class="help"><?php echo wc_help_tip( esc_html__( 'The WooCommerce Legacy REST API plugin running on this site.', 'woocommerce' ) ); ?></td>
<td>
<?php
if ( is_null( wc()->api ) ) {
echo '<mark class="info-icon"><span class="dashicons dashicons-info"></span> ' . esc_html__( 'The Legacy REST API plugin is not installed on this site.', 'woocommerce' ) . '</mark>';
} else {
if ( WC()->legacy_rest_api_is_available() ) {
$plugin_path = wc_get_container()->get( \Automattic\WooCommerce\Utilities\PluginUtil::class )->get_wp_plugin_id( 'woocommerce-legacy-rest-api' );
$version = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_path )['Version'] ?? '';
echo '<mark class="yes"><span class="dashicons dashicons-yes"></span> ' . esc_html( $version ) . ' <code class="private">' . esc_html( wc()->api->get_rest_api_package_path() ) . '</code></mark> ';
} else {
echo '<mark class="info-icon"><span class="dashicons dashicons-info"></span> ' . esc_html__( 'The Legacy REST API plugin is not installed on this site.', 'woocommerce' ) . '</mark>';
}
?>
</td>
@@ -695,8 +695,8 @@ if ( 0 < $mu_plugins_count ) :
</thead>
<tbody>
<tr>
<td data-export-label="API Enabled"><?php esc_html_e( 'API enabled', 'woocommerce' ); ?>:</td>
<td class="help"><?php echo wc_help_tip( esc_html__( 'Does your site have REST API enabled?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
<td data-export-label="Legacy API Enabled"><?php esc_html_e( 'Legacy API enabled', 'woocommerce' ); ?>:</td>
<td class="help"><?php echo wc_help_tip( esc_html__( 'Does your site have the Legacy REST API enabled?', 'woocommerce' ) ); /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */ ?></td>
<td><?php echo $settings['api_enabled'] ? '<mark class="yes"><span class="dashicons dashicons-yes"></span></mark>' : '<mark class="no">&ndash;</mark>'; ?></td>
</tr>
<tr>

View File

@@ -42,7 +42,7 @@ if ( ! $tab_exists ) {
?>
<p class="submit">
<?php if ( empty( $GLOBALS['hide_save_button'] ) ) : ?>
<button name="save" class="woocommerce-save-button components-button is-primary" type="submit" value="<?php esc_attr_e( 'Save changes', 'woocommerce' ); ?>"><?php esc_html_e( 'Save changes', 'woocommerce' ); ?></button>
<button name="save" disabled class="woocommerce-save-button components-button is-primary" type="submit" value="<?php esc_attr_e( 'Save changes', 'woocommerce' ); ?>"><?php esc_html_e( 'Save changes', 'woocommerce' ); ?></button>
<?php endif; ?>
<?php wp_nonce_field( 'woocommerce-settings' ); ?>
</p>

View File

@@ -333,7 +333,7 @@ class WC_Auth {
*/
// Check if Jetpack is installed and activated.
if ( class_exists( 'Jetpack' ) && Jetpack::connection()->is_active() ) {
if ( class_exists( 'Jetpack' ) && Jetpack::connection()->has_connected_owner() ) {
// Check if the user is using the WordPress.com SSO.
if ( Jetpack::is_module_active( 'sso' ) ) {
@@ -341,7 +341,7 @@ class WC_Auth {
$redirect_url = $this->build_url( $data, 'authorize' );
// Build the SSO URL.
$login_url = Jetpack_SSO::get_instance()->build_sso_button_url(
$login_url = \Automattic\Jetpack\Connection\SSO::get_instance()->build_sso_button_url(
array(
'redirect_to' => rawurlencode( esc_url_raw( $redirect_url ) ),
'action' => 'login',

View File

@@ -106,6 +106,7 @@ class WC_Cart extends WC_Legacy_Cart {
add_action( 'woocommerce_add_to_cart', array( $this, 'calculate_totals' ), 20, 0 );
add_action( 'woocommerce_applied_coupon', array( $this, 'calculate_totals' ), 20, 0 );
add_action( 'woocommerce_removed_coupon', array( $this, 'calculate_totals' ), 20, 0 );
add_action( 'woocommerce_cart_item_removed', array( $this, 'calculate_totals' ), 20, 0 );
add_action( 'woocommerce_cart_item_restored', array( $this, 'calculate_totals' ), 20, 0 );
add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_items' ), 1 );
@@ -715,7 +716,6 @@ class WC_Cart extends WC_Legacy_Cart {
}
return $return;
}
/**

View File

@@ -6,7 +6,7 @@
* @version 3.0.0
*/
use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner as CustomOrdersTableCLIRunner;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner as CustomOrdersTableCLIRunner;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\CLIRunner as ProductAttributesLookupCLIRunner;
defined( 'ABSPATH' ) || exit;

View File

@@ -171,13 +171,8 @@ class WC_Customer_Download extends WC_Data implements ArrayAccess {
*/
public function get_download_count( $context = 'view' ) {
// Check for count of download logs.
$data_store = WC_Data_Store::load( 'customer-download-log' );
$download_log_ids = $data_store->get_download_logs_for_permission( $this->get_id() );
$download_log_count = 0;
if ( ! empty( $download_log_ids ) ) {
$download_log_count = count( $download_log_ids );
}
$data_store = WC_Data_Store::load( 'customer-download-log' );
$download_log_count = $data_store->get_download_logs_count_for_permission( $this->get_id() );
// Check download count in prop.
$download_count_prop = $this->get_prop( 'download_count', $context );

View File

@@ -970,7 +970,7 @@ class WC_Discounts {
}
$user = wp_get_current_user();
$check_emails = array( $user->get_billing_email(), $user->get_email() );
$check_emails = array( $user->user_email );
if ( $this->object instanceof WC_Cart ) {
$check_emails[] = $this->object->get_customer()->get_billing_email();

View File

@@ -8,12 +8,19 @@
* @version 2.2.0
*/
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
defined( 'ABSPATH' ) || exit;
/**
* Download handler class.
*/
class WC_Download_Handler {
use AccessiblePrivateMethods;
/**
* The hook used for deferred tracking of partial download attempts.
*/
public const TRACK_DOWNLOAD_CALLBACK = 'track_partial_download';
/**
* Hook in methods.
@@ -25,6 +32,7 @@ class WC_Download_Handler {
add_action( 'woocommerce_download_file_redirect', array( __CLASS__, 'download_file_redirect' ), 10, 2 );
add_action( 'woocommerce_download_file_xsendfile', array( __CLASS__, 'download_file_xsendfile' ), 10, 2 );
add_action( 'woocommerce_download_file_force', array( __CLASS__, 'download_file_force' ), 10, 2 );
self::add_action( self::TRACK_DOWNLOAD_CALLBACK, array( __CLASS__, 'track_download' ), 10, 3 );
}
/**
@@ -135,9 +143,13 @@ class WC_Download_Handler {
// Track the download in logs and change remaining/counts.
$current_user_id = get_current_user_id();
$ip_address = WC_Geolocation::get_ip_address();
if ( ! $download_range['is_range_request'] ) {
$download->track_download( $current_user_id > 0 ? $current_user_id : null, ! empty( $ip_address ) ? $ip_address : null );
}
self::track_download(
$download,
$current_user_id > 0 ? $current_user_id : null,
! empty( $ip_address ) ? $ip_address : null,
$download_range['is_range_request']
);
self::download( $file_path, $download->get_product_id() );
}
@@ -695,6 +707,76 @@ class WC_Download_Handler {
}
wp_die( $message, $title, array( 'response' => $status ) ); // WPCS: XSS ok.
}
/**
* Takes care of tracking download requests, with support for deferring tracking in the case of
* partial (ranged request) downloads.
*
* @param WC_Customer_Download|int $download The download to be tracked.
* @param int|null $user_id The user ID, if known.
* @param string|null $user_ip_address The download IP address, if known.
* @param bool $defer If tracking the download should be deferred.
*
* @return void
* @throws Exception If the active version of Action Scheduler is less than 3.6.0.
*/
private static function track_download( $download, $user_id = null, $user_ip_address = null, bool $defer = false ): void {
try {
// If we were supplied with an integer, convert it to a download object.
$download = new WC_Customer_Download( $download );
// In simple cases, we can track the download immediately.
if ( ! $defer ) {
$download->track_download( $user_id, $user_ip_address );
return;
}
// Counting of partial downloads may be disabled by the site operator.
if ( get_option( 'woocommerce_downloads_count_partial', 'yes' ) !== 'yes' ) {
return;
}
/**
* Determines how long the window of time is for tracking unique download attempts, in relation to
* partial (ranged) download requests.
*
* @since 9.2.0
*
* @param int $window_in_seconds Non-negative number of seconds. Defaults to 1800 (30 minutes).
* @param int $download_permission_id References the download permission being tracked.
*/
$window = absint( apply_filters( 'woocommerce_partial_download_tracking_window', 30 * MINUTE_IN_SECONDS, $download->get_id() ) );
// If we do not have Action Scheduler 3.6.0+ (this would be an unexpected scenario) then we cannot
// track partial downloads, because we require support for unique actions.
if ( version_compare( ActionScheduler_Versions::instance()->latest_version(), '3.6.0', '<' ) ) {
throw new Exception( 'Support for unique scheduled actions is not currently available.' );
}
as_schedule_single_action(
time() + $window,
self::TRACK_DOWNLOAD_CALLBACK,
array(
$download->get_id(),
$user_id,
$user_ip_address,
),
'woocommerce',
true
);
} catch ( Exception $e ) {
wc_get_logger()->error(
'There was a problem while tracking a product download.',
array(
'error' => $e->getMessage(),
'id' => $download->get_id(),
'user_id' => $user_id,
'ip' => $user_ip_address,
'deferred' => $defer ? 'yes' : 'no',
)
);
}
}
}
WC_Download_Handler::init();

View File

@@ -259,6 +259,9 @@ class WC_Install {
'wc_update_910_add_launch_your_store_tour_option',
'wc_update_910_remove_obsolete_user_meta',
),
'9.2.0' => array(
'wc_update_920_add_wc_hooked_blocks_version_option',
),
);
/**
@@ -268,6 +271,13 @@ class WC_Install {
*/
const NEWLY_INSTALLED_OPTION = 'woocommerce_newly_installed';
/**
* Option name used to track new installation versions of WooCommerce.
*
* @var string
*/
const INITIAL_INSTALLED_VERSION = 'woocommerce_initial_installed_version';
/**
* Option name used to uniquely identify installations of WooCommerce.
*
@@ -312,6 +322,13 @@ class WC_Install {
do_action_deprecated( 'woocommerce_admin_newly_installed', array(), '6.5.0', 'woocommerce_newly_installed' );
update_option( self::NEWLY_INSTALLED_OPTION, 'no' );
/**
* This option is used to track the initial version of WooCommerce that was installed.
*
* @since 9.2.0
*/
add_option( self::INITIAL_INSTALLED_VERSION, WC()->version, '', false );
}
}
@@ -1636,6 +1653,7 @@ CREATE TABLE {$wpdb->prefix}wc_download_log (
CREATE TABLE {$wpdb->prefix}wc_product_meta_lookup (
`product_id` bigint(20) NOT NULL,
`sku` varchar(100) NULL default '',
`global_unique_id` varchar(100) NULL default '',
`virtual` tinyint(1) NULL default 0,
`downloadable` tinyint(1) NULL default 0,
`min_price` decimal(19,4) NULL default NULL,

View File

@@ -405,6 +405,7 @@ class WC_Post_Data {
$data_store->untrash_variations( $id );
wc_product_force_unique_sku( $id );
self::clear_global_unique_id_if_necessary( $id );
wc_get_container()->get( ProductAttributesLookupDataStore::class )->on_product_changed( $id );
} elseif ( 'product_variation' === $post_type ) {
@@ -412,6 +413,19 @@ class WC_Post_Data {
}
}
/**
* Clear global unique id if it's not unique.
*
* @param mixed $id Post ID.
*/
private static function clear_global_unique_id_if_necessary( $id ) {
$product = wc_get_product( $id );
if ( $product && ! wc_product_has_global_unique_id( $id, $product->get_global_unique_id() ) ) {
$product->set_global_unique_id( '' );
$product->save();
}
}
/**
* Get the post type for a given post.
*

View File

@@ -31,7 +31,7 @@ class WC_Privacy extends WC_Abstract_Privacy {
parent::__construct();
// Initialize data exporters and erasers.
add_action( 'plugins_loaded', array( $this, 'register_erasers_exporters' ) );
add_action( 'init', array( $this, 'register_erasers_exporters' ) );
// Cleanup orders daily - this is a callback on a daily cron event.
add_action( 'woocommerce_cleanup_personal_data', array( $this, 'queue_cleanup_personal_data' ) );

View File

@@ -965,6 +965,7 @@ class WC_Tracker {
'hpos_transactions_enabled' => get_option( 'woocommerce_use_db_transactions_for_custom_orders_table_data_sync' ),
'hpos_transactions_level' => get_option( 'woocommerce_db_transactions_isolation_level_for_custom_orders_table_data_sync' ),
'show_marketplace_suggestions' => get_option( 'woocommerce_show_marketplace_suggestions' ),
'admin_install_timestamp' => get_option( 'woocommerce_admin_install_timestamp' ),
);
}

View File

@@ -427,7 +427,7 @@ class WC_Webhook extends WC_Legacy_Webhook {
} elseif ( in_array( $this->get_api_version(), wc_get_webhook_rest_api_versions(), true ) ) {
$payload = $this->get_wp_api_payload( $resource, $resource_id, $event );
} else {
if ( is_null( wc()->api ) ) {
if ( ! WC()->legacy_rest_api_is_available() ) {
throw new \Exception( 'The Legacy REST API plugin is not installed on this site. More information: https://developer.woocommerce.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/ ' );
}
$payload = wc()->api->get_webhook_api_payload( $resource, $resource_id, $event );

View File

@@ -27,9 +27,7 @@ use Automattic\WooCommerce\Internal\Utilities\LegacyRestApiStub;
use Automattic\WooCommerce\Internal\Utilities\WebhookUtil;
use Automattic\WooCommerce\Internal\Admin\Marketplace;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\{ LoggingUtil, TimeUtil };
use Automattic\WooCommerce\Admin\WCAdminHelper;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Utilities\{LoggingUtil, RestApiUtil, TimeUtil};
/**
* Main WooCommerce Class.
@@ -45,7 +43,7 @@ final class WooCommerce {
*
* @var string
*/
public $version = '9.1.4';
public $version = '9.2.3';
/**
* WooCommerce Schema version.
@@ -54,7 +52,7 @@ final class WooCommerce {
*
* @var string
*/
public $db_version = '430';
public $db_version = '920';
/**
* The single instance of the class.
@@ -81,11 +79,11 @@ final class WooCommerce {
/**
* API instance
*
* @deprecated 9.0.0 The Legacy REST API has been removed from WooCommerce core. This property will be null unless the WooCommerce Legacy REST API plugin is installed.
* @deprecated 9.0.0 The Legacy REST API has been removed from WooCommerce core. Now this property points to a RestApiUtil instance, unless the Legacy REST API plugin is installed.
*
* @var WC_API
*/
public $api;
private $api;
/**
* Product factory instance.
@@ -179,17 +177,57 @@ final class WooCommerce {
}
/**
* Auto-load in-accessible properties on demand.
* Autoload inaccessible or non-existing properties on demand.
*
* @param mixed $key Key name.
* @return mixed
*/
public function __get( $key ) {
if ( 'api' === $key ) {
// The Legacy REST API was removed from WooCommerce core as of version 9.0 (moved to a dedicated plugin),
// but some plugins are still using wc()->api->get_endpoint_data. This method now lives in the RestApiUtil class,
// but we expose it through LegacyRestApiStub to limit the scope of what can be done via WC()->api.
//
// On the other hand, if the dedicated plugin is installed it will set the $api property by itself
// to an instance of the old WC_API class, which of course still has the get_endpoint_data method.
if ( is_null( $this->api ) && ! $this->legacy_rest_api_is_available() ) {
$this->api = wc_get_container()->get( LegacyRestApiStub::class );
}
return $this->api;
}
if ( in_array( $key, array( 'payment_gateways', 'shipping', 'mailer', 'checkout' ), true ) ) {
return $this->$key();
}
}
/**
* Set the value of an inaccessible or non-existing property.
*
* @param string $key Property name.
* @param mixed $value Property value.
*/
public function __set( string $key, $value ) {
if ( 'api' === $key ) {
$this->api = $value;
} elseif ( property_exists( $this, $key ) ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
trigger_error( 'Cannot access private property WooCommerce::$' . esc_html( $key ), E_USER_ERROR );
} else {
$this->$key = $value;
}
}
/**
* Check if the Legacy REST API plugin is active (and thus the Legacy REST API is available).
*
* @return bool
*/
public function legacy_rest_api_is_available() {
return class_exists( 'WC_Legacy_REST_API_Plugin', false );
}
/**
* WooCommerce Constructor.
*/
@@ -229,7 +267,9 @@ final class WooCommerce {
'connection',
array(
'slug' => 'woocommerce',
'name' => __( 'WooCommerce', 'woocommerce' ),
// Cannot use __() here because it would cause translations to be loaded too early.
// See https://github.com/woocommerce/woocommerce/pull/47113.
'name' => 'WooCommerce',
)
);
}
@@ -265,7 +305,9 @@ final class WooCommerce {
self::add_action( 'rest_api_init', array( $this, 'register_wp_admin_settings' ) );
add_action( 'woocommerce_installed', array( $this, 'add_woocommerce_remote_variant' ) );
add_action( 'woocommerce_updated', array( $this, 'add_woocommerce_remote_variant' ) );
add_action( 'woocommerce_newly_installed', 'wc_set_hooked_blocks_version', 10 );
self::add_filter( 'robots_txt', array( $this, 'robots_txt' ) );
add_filter( 'wp_plugin_dependencies_slug', array( $this, 'convert_woocommerce_slug' ) );
// These classes set up hooks on instantiation.
@@ -687,8 +729,6 @@ final class WooCommerce {
$this->theme_support_includes();
$this->query = new WC_Query();
LegacyRestApiStub::setup();
}
/**
@@ -998,6 +1038,43 @@ final class WooCommerce {
}
}
/**
* Tell bots not to index some WooCommerce-created directories.
*
* We try to detect the default "User-agent: *" added by WordPress and add our rules to that group, because
* it's possible that some bots will only interpret the first group of rules if there are multiple groups with
* the same user agent.
*
* @param string $output The contents that WordPress will output in a robots.txt file.
*
* @return string
*/
private function robots_txt( $output ) {
$path = ( ! empty( $site_url['path'] ) ) ? $site_url['path'] : '';
$lines = preg_split( '/\r\n|\r|\n/', $output );
$agent_index = array_search( 'User-agent: *', $lines, true );
if ( false !== $agent_index ) {
$above = array_slice( $lines, 0, $agent_index + 1 );
$below = array_slice( $lines, $agent_index + 1 );
} else {
$above = $lines;
$below = array();
$above[] = '';
$above[] = 'User-agent: *';
}
$above[] = "Disallow: $path/wp-content/uploads/wc-logs/";
$above[] = "Disallow: $path/wp-content/uploads/woocommerce_transient_files/";
$above[] = "Disallow: $path/wp-content/uploads/woocommerce_uploads/";
$lines = array_merge( $above, $below );
return implode( PHP_EOL, $lines );
}
/**
* Set tablenames inside WPDB object.
*/

View File

@@ -149,10 +149,10 @@ class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Da
}
/**
* Get array of download log ids by specified args.
* Get array of download logs, or the count of existing logs, by specified args.
*
* @param array $args Arguments to define download logs to retrieve.
* @return array
* @param array $args Arguments to define download logs to retrieve. If $args['return'] is 'count' then the count of existing logs will be returned.
* @return array|int
*/
public function get_download_logs( $args = array() ) {
global $wpdb;
@@ -171,9 +171,11 @@ class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Da
)
);
$is_count = 'count' === $args['return'];
$query = array();
$table = $wpdb->prefix . self::get_table_name();
$query[] = "SELECT * FROM {$table} WHERE 1=1";
$query[] = 'SELECT ' . ( $is_count ? 'COUNT(1)' : '*' ) . " FROM {$table} WHERE 1=1";
if ( $args['permission_id'] ) {
$query[] = $wpdb->prepare( 'AND permission_id = %d', $args['permission_id'] );
@@ -197,7 +199,13 @@ class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Da
$query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) );
}
$raw_download_logs = $wpdb->get_results( implode( ' ', $query ) ); // WPCS: unprepared SQL ok.
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
if ( $is_count ) {
return absint( $wpdb->get_var( implode( ' ', $query ) ) );
}
$raw_download_logs = $wpdb->get_results( implode( ' ', $query ) );
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
switch ( $args['return'] ) {
case 'ids':
@@ -226,6 +234,26 @@ class WC_Customer_Download_Log_Data_Store implements WC_Customer_Download_Log_Da
);
}
/**
* Get the count of download logs for a given download permission.
*
* @param int $permission_id Permission to get logs count for.
* @return int
*/
public function get_download_logs_count_for_permission( $permission_id ) {
// If no permission_id is passed, return an empty array.
if ( empty( $permission_id ) ) {
return 0;
}
return $this->get_download_logs(
array(
'permission_id' => $permission_id,
'return' => 'count',
)
);
}
/**
* Method to delete download logs for a given permission ID.
*

View File

@@ -5,6 +5,8 @@
* @package WooCommerce\Classes
*/
use Automattic\WooCommerce\Utilities\OrderUtil;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@@ -100,6 +102,12 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
$order->set_order_key( wc_generate_order_key() );
}
parent::create( $order );
// Do not fire 'woocommerce_new_order' for draft statuses.
if ( in_array( $order->get_status( 'edit' ), array( 'auto-draft', 'draft', 'checkout-draft' ), true ) ) {
return;
}
do_action( 'woocommerce_new_order', $order->get_id(), $order );
}
@@ -183,22 +191,37 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
// Also grab the current status so we can compare.
$previous_status = get_post_status( $order->get_id() );
// If the order doesn't exist in the DB, we will consider it as new.
if ( ! $previous_status && $order->get_id() === 0 ) {
$previous_status = 'new';
}
// Update the order.
parent::update( $order );
// Fire a hook depending on the status - this should be considered a creation if it was previously draft status.
$new_status = $order->get_status( 'edit' );
$current_status = $order->get_status( 'edit' );
if ( $new_status !== $previous_status && in_array( $previous_status, array( 'new', 'auto-draft', 'draft' ), true ) ) {
do_action( 'woocommerce_new_order', $order->get_id(), $order );
} else {
do_action( 'woocommerce_update_order', $order->get_id(), $order );
// We need to remove the wc- prefix from the status for comparison and proper evaluation of new vs updated orders.
$previous_status = OrderUtil::remove_status_prefix( $previous_status );
$current_status = OrderUtil::remove_status_prefix( $current_status );
$draft_statuses = array( 'new', 'auto-draft', 'draft', 'checkout-draft' );
// This hook should be fired only if the new status is not one of draft statuses and the previous status was one of the draft statuses.
if (
$current_status !== $previous_status
&& ! in_array( $current_status, $draft_statuses, true )
&& in_array( $previous_status, $draft_statuses, true )
) {
do_action( 'woocommerce_new_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
return;
}
do_action( 'woocommerce_update_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
}
/**
* Helper method that updates all the post meta for an order based on it's settings in the WC_Order class.
* Helper method that updates all the post meta for an order based on its settings in the WC_Order class.
*
* @param WC_Order $order Order object.
* @since 3.0.0
@@ -997,6 +1020,40 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
* @return array|object
*/
public function query( $query_vars ) {
/**
* Allows 3rd parties to filter query args that will trigger an unsupported notice.
*
* @since 9.2.0
*
* @param array $unsupported_args Array of query arg names.
*/
$unsupported_args = (array) apply_filters(
'woocommerce_order_data_store_cpt_query_unsupported_args',
array( 'meta_query', 'field_query' )
);
// Trigger doing_it_wrong() for query vars only supported in HPOS.
$unsupported_args_in_query = array_keys( array_filter( array_intersect_key( $query_vars, array_flip( $unsupported_args ) ) ) );
if ( $unsupported_args_in_query && __CLASS__ === get_class( $this ) ) {
wc_doing_it_wrong(
__METHOD__,
esc_html(
sprintf(
// translators: %s is a comma separated list of query arguments.
_n(
'Order query argument (%s) is not supported on the current order datastore.',
'Order query arguments (%s) are not supported on the current order datastore.',
count( $unsupported_args_in_query ),
'woocommerce'
),
implode( ', ', $unsupported_args_in_query )
)
),
'9.2.0'
);
}
$args = $this->get_wp_query_args( $query_vars );
if ( ! empty( $args['errors'] ) ) {

View File

@@ -29,6 +29,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
protected $internal_meta_keys = array(
'_visibility',
'_sku',
'_global_unique_id',
'_price',
'_regular_price',
'_sale_price',
@@ -96,6 +97,46 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
*/
protected $updated_props = array();
/**
* Method to obtain DB lock on SKU to make sure we only
* create product with unique SKU for concurrent requests.
*
* We are doing so by inserting a row in the wc_product_meta_lookup table
* upfront with the SKU of the product we are trying to insert.
*
* If the SKU is already present in the table, it means that another
* request is processing the same SKU and we should not proceed
* with the insert.
*
* Using $wpdb->options as it always has some data, if we select from a table
* that does not have any data, then our query will always return null set
* and the where subquery won't be fired, effectively bypassing any lock.
*
* @param WC_Product $product Product object.
* @return bool True if lock is obtained (unique SKU), false otherwise.
*/
private function obtain_lock_on_sku_for_concurrent_requests( $product ) {
global $wpdb;
$product_id = $product->get_id();
$sku = $product->get_sku();
$query = $wpdb->prepare(
"INSERT INTO $wpdb->wc_product_meta_lookup (product_id, sku)
SELECT %d, %s FROM $wpdb->options
WHERE NOT EXISTS (
SELECT * FROM $wpdb->wc_product_meta_lookup WHERE sku = %s LIMIT 1
) LIMIT 1;",
$product_id,
$sku,
$sku
);
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$result = $wpdb->query( $query );
return (bool) $result;
}
/*
|--------------------------------------------------------------------------
| CRUD Methods
@@ -106,6 +147,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
* Method to create a new product in the database.
*
* @param WC_Product $product Product object.
* @throws Exception If SKU is already under processing.
*/
public function create( &$product ) {
if ( ! $product->get_date_created( 'edit' ) ) {
@@ -137,6 +179,19 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
if ( $id && ! is_wp_error( $id ) ) {
$product->set_id( $id );
$sku = $product->get_sku();
/**
* If SKU is already under processing aka Duplicate SKU
* because of concurrent requests, then we should not proceed
* Delete the product and throw an exception only if the request is
* initiated via REST API
*/
if ( ! empty( $sku ) && WC()->is_rest_api_request() && ! $this->obtain_lock_on_sku_for_concurrent_requests( $product ) ) {
$product->delete( true );
// translators: 1: SKU.
throw new Exception( esc_html( sprintf( __( 'The SKU (%1$s) you are trying to insert is already under processing', 'woocommerce' ), $sku ) ) );
}
// get the post object so that we can set the status
// to the correct value; it is possible that the status was
@@ -332,6 +387,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
$post_meta_values = get_post_meta( $id );
$meta_key_to_props = array(
'_sku' => 'sku',
'_global_unique_id' => 'global_unique_id',
'_regular_price' => 'regular_price',
'_sale_price' => 'sale_price',
'_price' => 'price',
@@ -523,6 +579,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
protected function update_post_meta( &$product, $force = false ) {
$meta_key_to_props = array(
'_sku' => 'sku',
'_global_unique_id' => 'global_unique_id',
'_regular_price' => 'regular_price',
'_sale_price' => 'sale_price',
'_sale_price_dates_from' => 'date_on_sale_from',
@@ -689,7 +746,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
}
}
if ( array_intersect( $this->updated_props, array( 'sku', 'regular_price', 'sale_price', 'date_on_sale_from', 'date_on_sale_to', 'total_sales', 'average_rating', 'stock_quantity', 'stock_status', 'manage_stock', 'downloadable', 'virtual', 'tax_status', 'tax_class' ) ) ) {
if ( array_intersect( $this->updated_props, array( 'sku', 'global_unique_id', 'regular_price', 'sale_price', 'date_on_sale_from', 'date_on_sale_to', 'total_sales', 'average_rating', 'stock_quantity', 'stock_status', 'manage_stock', 'downloadable', 'virtual', 'tax_status', 'tax_class' ) ) ) {
$this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' );
}
@@ -1015,6 +1072,37 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
);
}
/**
* Check if product sku is found for any other product IDs.
*
* @since 9.1.0
* @param int $product_id Product ID.
* @param string $global_unique_id Will be slashed to work around https://core.trac.wordpress.org/ticket/27421.
* @return bool
*/
public function is_existing_global_unique_id( $product_id, $global_unique_id ) {
global $wpdb;
// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
return (bool) $wpdb->get_var(
$wpdb->prepare(
"
SELECT posts.ID
FROM {$wpdb->posts} as posts
INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
WHERE
posts.post_type IN ( 'product', 'product_variation' )
AND posts.post_status != 'trash'
AND lookup.global_unique_id = %s
AND lookup.product_id <> %d
LIMIT 1
",
wp_slash( $global_unique_id ),
$product_id
)
);
}
/**
* Return product ID based on SKU.
*
@@ -1045,6 +1133,42 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
return (int) apply_filters( 'woocommerce_get_product_id_by_sku', $id, $sku );
}
/**
* Return product ID based on Unique ID.
*
* @since 9.1.0
* @param string $global_unique_id Product Unique ID.
* @return int
*/
public function get_product_id_by_global_unique_id( $global_unique_id ) {
global $wpdb;
// phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery
$id = $wpdb->get_var(
$wpdb->prepare(
"
SELECT posts.ID
FROM {$wpdb->posts} as posts
INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
WHERE
posts.post_type IN ( 'product', 'product_variation' )
AND posts.post_status != 'trash'
AND lookup.global_unique_id = %s
LIMIT 1
",
$global_unique_id
)
);
/**
* Hook woocommerce_get_product_id_by_global_unique_id.
*
* @since 9.1.0
* @param mixed $id List of post statuses.
* @param string $global_unique_id Unique ID.
*/
return (int) apply_filters( 'woocommerce_get_product_id_by_global_unique_id', $id, $global_unique_id );
}
/**
* Returns an array of IDs of products that have sales starting soon.
*
@@ -2113,7 +2237,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
$stock = 'yes' === $manage_stock ? wc_stock_amount( get_post_meta( $id, '_stock', true ) ) : null;
$price = wc_format_decimal( get_post_meta( $id, '_price', true ) );
$sale_price = wc_format_decimal( get_post_meta( $id, '_sale_price', true ) );
return array(
$product_data = array(
'product_id' => absint( $id ),
'sku' => get_post_meta( $id, '_sku', true ),
'virtual' => 'yes' === get_post_meta( $id, '_virtual', true ) ? 1 : 0,
@@ -2129,6 +2253,10 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
'tax_status' => get_post_meta( $id, '_tax_status', true ),
'tax_class' => get_post_meta( $id, '_tax_class', true ),
);
if ( get_option( 'woocommerce_schema_version', 0 ) >= 920 ) {
$product_data['global_unique_id'] = get_post_meta( $id, '_global_unique_id', true );
}
return $product_data;
}
return array();
}

View File

@@ -367,6 +367,7 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
'image_id' => get_post_thumbnail_id( $id ),
'backorders' => get_post_meta( $id, '_backorders', true ),
'sku' => get_post_meta( $id, '_sku', true ),
'global_unique_id' => get_post_meta( $id, '_global_unique_id', true ),
'stock_quantity' => get_post_meta( $id, '_stock', true ),
'weight' => get_post_meta( $id, '_weight', true ),
'length' => get_post_meta( $id, '_length', true ),
@@ -403,6 +404,7 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
'title' => $parent_object ? $parent_object->post_title : '',
'status' => $parent_object ? $parent_object->post_status : '',
'sku' => get_post_meta( $product->get_parent_id(), '_sku', true ),
'global_unique_id' => get_post_meta( $product->get_parent_id(), '_global_unique_id', true ),
'manage_stock' => get_post_meta( $product->get_parent_id(), '_manage_stock', true ),
'backorders' => get_post_meta( $product->get_parent_id(), '_backorders', true ),
'stock_quantity' => wc_stock_amount( get_post_meta( $product->get_parent_id(), '_stock', true ) ),

View File

@@ -49,6 +49,7 @@ interface WC_Product_Data_Store_Interface {
*/
public function get_product_id_by_sku( $sku );
/**
* Returns an array of IDs of products that have sales starting soon.
*

View File

@@ -23,13 +23,13 @@ if ( ! function_exists( 'wc_admin_get_feature_config' ) ) {
'navigation' => true,
'onboarding' => true,
'onboarding-tasks' => true,
'pattern-toolkit-full-composability' => false,
'pattern-toolkit-full-composability' => true,
'product-pre-publish-modal' => false,
'product-custom-fields' => true,
'remote-inbox-notifications' => true,
'remote-free-extensions' => true,
'payment-gateway-suggestions' => true,
'printful' => false,
'printful' => true,
'settings' => false,
'shipping-label-banner' => true,
'subscriptions' => true,

View File

@@ -144,6 +144,11 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$review = $this->get_review( (int) $request['id'], (int) $request['product_id'] );
if ( is_wp_error( $review ) ) {
return $review;
}
if ( ! wc_rest_check_product_reviews_permissions( 'read', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -172,6 +177,11 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
$review = $this->get_review( (int) $request['id'], (int) $request['product_id'] );
if ( is_wp_error( $review ) ) {
return $review;
}
if ( ! wc_rest_check_product_reviews_permissions( 'edit', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -186,6 +196,11 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
* @return WP_Error|boolean
*/
public function delete_item_permissions_check( $request ) {
$review = $this->get_review( (int) $request['id'], (int) $request['product_id'] );
if ( is_wp_error( $review ) ) {
return $review;
}
if ( ! wc_rest_check_product_reviews_permissions( 'delete', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -218,6 +233,28 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
return rest_ensure_response( $data );
}
/**
* Fetch a single product review from the database.
*
* @param int $id Review ID.
* @param int $product_id Product ID.
*
* @since 9.2.0
* @return \WP_Comment
*/
protected function get_review( int $id, int $product_id ) {
if ( 0 >= $product_id || 'product' !== get_post_type( $product_id ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$review = 0 <= $id ? get_comment( $id ) : null;
if ( empty( $review ) || empty( $review->comment_ID ) || 'review' !== get_comment_type( $id ) || empty( $review->comment_post_ID ) || (int) $review->comment_post_ID !== $product_id ) {
return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid product review ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
return $review;
}
/**
* Get a single product review.
*
@@ -225,17 +262,9 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$id = (int) $request['id'];
$product_id = (int) $request['product_id'];
if ( 'product' !== get_post_type( $product_id ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$review = get_comment( $id );
if ( empty( $id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) {
return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
$review = $this->get_review( (int) $request['id'], (int) $request['product_id'] );
if ( is_wp_error( $review ) ) {
return $review;
}
$delivery = $this->prepare_item_for_response( $review, $request );
@@ -309,14 +338,9 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
$product_review_id = (int) $request['id'];
$product_id = (int) $request['product_id'];
if ( 'product' !== get_post_type( $product_id ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$review = get_comment( $product_review_id );
if ( empty( $product_review_id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) {
return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
$review = $this->get_review( $product_review_id, $product_id );
if ( is_wp_error( $review ) ) {
return $review;
}
$prepared_review = $this->prepare_item_for_database( $request );
@@ -358,15 +382,11 @@ class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
public function delete_item( $request ) {
$product_id = (int) $request['product_id'];
$product_review_id = (int) $request['id'];
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
$product_review = $this->get_review( $product_review_id, $product_id );
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
if ( 'product' !== get_post_type( $product_id ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$product_review = get_comment( $product_review_id );
if ( empty( $product_review_id ) || empty( $product_review->comment_ID ) || empty( $product_review->comment_post_ID ) ) {
return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid product review ID.', 'woocommerce' ), array( 'status' => 404 ) );
if ( is_wp_error( $product_review ) ) {
return $product_review;
}
/**

View File

@@ -213,7 +213,11 @@ class WC_REST_Tax_Classes_V1_Controller extends WC_REST_Controller {
}
$tax_class = WC_Tax::get_tax_class_by( 'slug', sanitize_title( $request['slug'] ) );
$deleted = WC_Tax::delete_tax_class_by( 'slug', sanitize_title( $request['slug'] ) );
if ( ! $tax_class ) {
return new WP_Error( 'woocommerce_rest_tax_class_invalid_slug', __( 'Invalid slug.', 'woocommerce' ), array( 'status' => 404 ) );
}
$deleted = WC_Tax::delete_tax_class_by( 'slug', sanitize_title( $request['slug'] ) );
if ( ! $deleted ) {
return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) );

View File

@@ -561,7 +561,7 @@ class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller {
$webhook = wc_get_webhook( $id );
if ( empty( $webhook ) || is_null( $webhook ) ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 404 ) );
}
$data = array(

View File

@@ -10,6 +10,8 @@
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Internal\Utilities\Types;
/**
* REST API Order Refunds controller class.
*
@@ -316,11 +318,28 @@ class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {
* The dynamic portion of the hook name, `$this->post_type`,
* refers to the object type slug.
*
* @since 4.5.0
*
* @param WC_Data $coupon Object object.
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating );
$refund = apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating );
// If the filtered result is not a WC_Data instance and is not a WP_Error then something went wrong, but we
// still need to honor the declared return type.
return Types::ensure_instance_of(
$refund,
WC_Data::class,
function ( $thing ) {
return is_wp_error( $thing )
? $thing
: new WP_Error(
'woocommerce_rest_cannot_verify_refund_created',
__( 'An unexpected error occurred while generating the refund.', 'woocommerce' )
);
}
);
}
/**

View File

@@ -468,7 +468,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
}
// Format the order status.
$data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status'];
$data['status'] = OrderUtil::remove_status_prefix( $data['status'] );
// Format line items.
foreach ( $format_line_items as $key ) {

View File

@@ -137,10 +137,48 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr
*
* @since 3.0.0
* @param int $id Object ID.
* @return WC_Data
* @return WC_Data|null
*/
protected function get_object( $id ) {
return wc_get_product( $id );
$object = wc_get_product( $id );
return ( $object && 0 !== $object->get_parent_id() ) ? $object : null;
}
/**
* Checks that a variation belongs to the specified parent product.
*
* @param int $variation_id Variation ID.
* @param int $parent_id Parent product ID to check against.
* @return bool TRUE if variation and parent product exist. FALSE otherwise.
*
* @since 9.2.0
*/
protected function check_variation_parent( int $variation_id, int $parent_id ): bool {
$variation = $this->get_object( $variation_id );
if ( ! $variation || $parent_id !== $variation->get_parent_id() ) {
return false;
}
$parent = wc_get_product( $variation->get_parent_id() );
if ( ! $parent ) {
return false;
}
return true;
}
/**
* Check if a given request has access to read an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
if ( ! $this->check_variation_parent( (int) $request['id'], (int) $request['product_id'] ) ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
return parent::get_item_permissions_check( $request );
}
/**
@@ -150,18 +188,31 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
if ( ! $this->check_variation_parent( (int) $request['id'], (int) $request['product_id'] ) ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$object = $this->get_object( (int) $request['id'] );
if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
// Check if variation belongs to the correct parent product.
if ( $object && 0 !== $object->get_parent_id() && absint( $request['product_id'] ) !== $object->get_parent_id() ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Parent product does not match current variation.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
return true;
}
/**
* Check if a given request has access to delete an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error
*/
public function delete_item_permissions_check( $request ) {
if ( ! $this->check_variation_parent( (int) $request['id'], (int) $request['product_id'] ) ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
return true;
return parent::delete_item_permissions_check( $request );
}
/**

View File

@@ -486,7 +486,7 @@ class WC_REST_System_Status_V2_Controller extends WC_REST_Controller {
'readonly' => true,
'properties' => array(
'api_enabled' => array(
'description' => __( 'REST API enabled?', 'woocommerce' ),
'description' => __( 'Legacy REST API enabled?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,

View File

@@ -97,13 +97,15 @@ class WC_REST_Tax_Classes_V2_Controller extends WC_REST_Tax_Classes_V1_Controlle
$tax_class = WC_Tax::get_tax_class_by( 'slug', sanitize_title( $request['slug'] ) );
}
$data = array();
if ( $tax_class ) {
$class = $this->prepare_item_for_response( $tax_class, $request );
$class = $this->prepare_response_for_collection( $class );
$data[] = $class;
if ( ! $tax_class ) {
return new WP_Error( 'woocommerce_rest_tax_class_invalid_slug', __( 'Invalid slug.', 'woocommerce' ), array( 'status' => 404 ) );
}
$data = array();
$class = $this->prepare_item_for_response( $tax_class, $request );
$class = $this->prepare_response_for_collection( $class );
$data[] = $class;
return rest_ensure_response( $data );
}
}

View File

@@ -36,7 +36,7 @@ class WC_REST_Webhooks_V2_Controller extends WC_REST_Webhooks_V1_Controller {
$webhook = wc_get_webhook( $id );
if ( empty( $webhook ) || is_null( $webhook ) ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 404 ) );
}
$data = array(

View File

@@ -166,7 +166,21 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller {
return $object;
}
$object->save();
try {
$object->save();
} catch ( Exception $e ) {
$error = "woocommerce_rest_{$this->post_type}_not_created";
wc_get_logger()->error(
$e->getMessage(),
array(
'source' => 'woocommerce-rest-api',
'error' => $error,
'code' => 400,
)
);
return new WP_Error( $error, $e->getMessage(), array( 'status' => 400 ) );
}
return $this->get_object( $object->get_id() );
} catch ( WC_Data_Exception $e ) {

View File

@@ -0,0 +1,162 @@
<?php
/**
* REST API CustomFields controller
*
* Handles requests to the /products/custom-fields endpoint.
*
* @package WooCommerce\RestApi
* @since 9.2.0
*/
use Automattic\WooCommerce\Utilities\I18nUtil;
defined( 'ABSPATH' ) || exit;
/**
* REST API Product Custom Fields controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Product_Custom_Fields_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products/custom-fields';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'product';
/**
* Register the routes for products.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/names',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item_names' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Get a collection of custom field names.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_item_names( $request ) {
global $wpdb;
$search = trim( $request['search'] );
$order = strtoupper( $request['order'] ) === 'DESC' ? 'DESC' : 'ASC';
$page = (int) $request['page'];
$limit = (int) $request['per_page'];
$offset = ( $page - 1 ) * $limit;
$base_query = $wpdb->prepare(
"SELECT DISTINCT post_metas.meta_key
FROM {$wpdb->postmeta} post_metas LEFT JOIN {$wpdb->posts} posts ON post_metas.post_id = posts.id
WHERE posts.post_type = %s AND post_metas.meta_key NOT LIKE %s AND post_metas.meta_key LIKE %s",
$this->post_type,
$wpdb->esc_like( '_' ) . '%',
'%' . $wpdb->esc_like( $search ) . '%'
);
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $base_query has been prepared already and $order is a static value.
$query = $wpdb->prepare(
"$base_query ORDER BY post_metas.meta_key $order LIMIT %d, %d",
$offset,
$limit
);
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $base_query has been prepared already.
$total_query = "SELECT COUNT(1) FROM ($base_query) AS total";
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- $query has been prepared already.
$query_result = $wpdb->get_results( $query );
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- $total_query has been prepared already.
$total_items = $wpdb->get_var( $total_query );
$custom_field_names = array();
foreach ( $query_result as $custom_field_name ) {
$custom_field_names[] = $custom_field_name->meta_key;
}
$response = rest_ensure_response( $custom_field_names );
$response->header( 'X-WP-Total', (int) $total_items );
$max_pages = ceil( $total_items / $limit );
$response->header( 'X-WP-TotalPages', (int) $max_pages );
$base = add_query_arg( $request->get_query_params(), rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/names' ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Check if a given request has access to read items.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Add new options for 'order' to the collection params.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['order'] = array(
'description' => __( 'Order sort items ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'asc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}

View File

@@ -149,6 +149,11 @@ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller {
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$review = $this->get_review( (int) $request['id'] );
if ( is_wp_error( $review ) ) {
return $review;
}
if ( ! wc_rest_check_product_reviews_permissions( 'read', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -177,6 +182,11 @@ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller {
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
$review = $this->get_review( (int) $request['id'] );
if ( is_wp_error( $review ) ) {
return $review;
}
if ( ! wc_rest_check_product_reviews_permissions( 'edit', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -191,6 +201,11 @@ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller {
* @return WP_Error|boolean
*/
public function delete_item_permissions_check( $request ) {
$review = $this->get_review( (int) $request['id'] );
if ( is_wp_error( $review ) ) {
return $review;
}
if ( ! wc_rest_check_product_reviews_permissions( 'delete', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
@@ -1057,13 +1072,11 @@ class WC_REST_Product_Reviews_Controller extends WC_REST_Controller {
}
$review = get_comment( $id );
if ( empty( $review ) ) {
if ( empty( $review ) || 'review' !== get_comment_type( $id ) ) {
return $error;
}
if ( ! empty( $review->comment_post_ID ) ) {
$post = get_post( (int) $review->comment_post_ID );
if ( 'product' !== get_post_type( (int) $review->comment_post_ID ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}

View File

@@ -108,6 +108,7 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
'description' => wc_format_content( $object->get_description() ),
'permalink' => $object->get_permalink(),
'sku' => $object->get_sku(),
'global_unique_id' => $object->get_global_unique_id(),
'price' => $object->get_price(),
'regular_price' => $object->get_regular_price(),
'sale_price' => $object->get_sale_price(),
@@ -193,6 +194,11 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
$variation->set_sku( wc_clean( $request['sku'] ) );
}
// Unique ID.
if ( isset( $request['global_unique_id'] ) ) {
$variation->set_global_unique_id( wc_clean( $request['global_unique_id'] ) );
}
// Thumbnail.
if ( isset( $request['image'] ) ) {
if ( is_array( $request['image'] ) ) {
@@ -535,7 +541,12 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
'readonly' => true,
),
'sku' => array(
'description' => __( 'Unique identifier.', 'woocommerce' ),
'description' => __( 'Stock Keeping Unit.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'global_unique_id' => array(
'description' => __( 'GTIN, UPC, EAN or ISBN.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),

View File

@@ -561,6 +561,11 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
$product->set_sku( wc_clean( $request['sku'] ) );
}
// Unique ID.
if ( isset( $request['global_unique_id'] ) ) {
$product->set_global_unique_id( wc_clean( $request['global_unique_id'] ) );
}
// Attributes.
if ( isset( $request['attributes'] ) ) {
$attributes = array();
@@ -987,7 +992,12 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
'context' => array( 'view', 'edit' ),
),
'sku' => array(
'description' => __( 'Unique identifier.', 'woocommerce' ),
'description' => __( 'Stock Keeping Unit.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'global_unique_id' => array(
'description' => __( 'GTIN, UPC, EAN or ISBN.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
@@ -1662,6 +1672,10 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
$data['post_password'] = $product->get_post_password( $context );
}
if ( in_array( 'global_unique_id', $fields, true ) ) {
$data['global_unique_id'] = $product->get_global_unique_id( $context );
}
$post_type_obj = get_post_type_object( $this->post_type );
if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
$permalink_template_requested = in_array( 'permalink_template', $fields, true );

View File

@@ -28,7 +28,7 @@ class Server {
/**
* Hook into WordPress ready to init the REST API as needed.
*/
public function init() {
public function init() { // phpcs:ignore WooCommerce.Functions.InternalInjectionMethod -- Not an injection method.
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ), 10 );
\WC_REST_System_Status_V2_Controller::register_cache_clean();
@@ -57,13 +57,19 @@ class Server {
* @return array List of Namespaces and Main controller classes.
*/
protected function get_rest_namespaces() {
/**
* Filter the list of REST API controllers to load.
*
* @since 4.5.0
* @param array $controllers List of $namespace => $controllers to load.
*/
return apply_filters(
'woocommerce_rest_api_get_rest_namespaces',
array(
'wc/v1' => $this->get_v1_controllers(),
'wc/v2' => $this->get_v2_controllers(),
'wc/v3' => $this->get_v3_controllers(),
'wc-telemetry' => $this->get_telemetry_controllers(),
'wc/v1' => wc_rest_should_load_namespace( 'wc/v1' ) ? $this->get_v1_controllers() : array(),
'wc/v2' => wc_rest_should_load_namespace( 'wc/v2' ) ? $this->get_v2_controllers() : array(),
'wc/v3' => wc_rest_should_load_namespace( 'wc/v3' ) ? $this->get_v3_controllers() : array(),
'wc-telemetry' => wc_rest_should_load_namespace( 'wc-telemetry' ) ? $this->get_telemetry_controllers() : array(),
)
);
}
@@ -157,6 +163,7 @@ class Server {
'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_Controller',
'product-attributes' => 'WC_REST_Product_Attributes_Controller',
'product-categories' => 'WC_REST_Product_Categories_Controller',
'product-custom-fields' => 'WC_REST_Product_Custom_Fields_Controller',
'product-reviews' => 'WC_REST_Product_Reviews_Controller',
'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_Controller',
'product-tags' => 'WC_REST_Product_Tags_Controller',

View File

@@ -39,66 +39,55 @@ class WC_Shortcode_My_Account {
return;
}
if ( ! is_user_logged_in() || isset( $wp->query_vars['lost-password'] ) ) {
self::my_account_add_notices();
// Show the lost password page. This can still be accessed directly by logged in accounts which is important for the initial create password links sent via email.
if ( isset( $wp->query_vars['lost-password'] ) ) {
self::lost_password();
return;
}
// Show login form if not logged in.
if ( ! is_user_logged_in() ) {
wc_get_template( 'myaccount/form-login.php' );
return;
}
// Output the my account page.
self::my_account( $atts );
}
/**
* Add notices to the my account page.
*
* Historically a filter has existed to render a message above the my account page content while the user is
* logged out. See `woocommerce_my_account_message`.
*/
private static function my_account_add_notices() {
global $wp;
if ( ! is_user_logged_in() ) {
/**
* Filters the message shown on the 'my account' page when the user is not logged in.
*
* @since 2.6.0
*/
$message = apply_filters( 'woocommerce_my_account_message', '' );
if ( ! empty( $message ) ) {
wc_add_notice( $message );
}
}
// After password reset, add confirmation message.
if ( ! empty( $_GET['password-reset'] ) ) { // WPCS: input var ok, CSRF ok.
wc_add_notice( __( 'Your password has been reset successfully.', 'woocommerce' ) );
}
// After password reset, add confirmation message.
if ( ! empty( $_GET['password-reset'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
wc_add_notice( __( 'Your password has been reset successfully.', 'woocommerce' ) );
}
if ( isset( $wp->query_vars['lost-password'] ) ) {
self::lost_password();
} else {
wc_get_template( 'myaccount/form-login.php' );
}
} else {
// Start output buffer since the html may need discarding for BW compatibility.
ob_start();
if ( isset( $wp->query_vars['customer-logout'] ) ) {
/* translators: %s: logout url */
wc_add_notice( sprintf( __( 'Are you sure you want to log out? <a href="%s">Confirm and log out</a>', 'woocommerce' ), wc_logout_url() ) );
}
// Collect notices before output.
$notices = wc_get_notices();
// Output the new account page.
self::my_account( $atts );
/**
* Deprecated my-account.php template handling. This code should be
* removed in a future release.
*
* If woocommerce_account_content did not run, this is an old template
* so we need to render the endpoint content again.
*/
if ( ! did_action( 'woocommerce_account_content' ) ) {
if ( ! empty( $wp->query_vars ) ) {
foreach ( $wp->query_vars as $key => $value ) {
if ( 'pagename' === $key ) {
continue;
}
if ( has_action( 'woocommerce_account_' . $key . '_endpoint' ) ) {
ob_clean(); // Clear previous buffer.
wc_set_notices( $notices );
wc_print_notices();
do_action( 'woocommerce_account_' . $key . '_endpoint', $value );
break;
}
}
wc_deprecated_function( 'Your theme version of my-account.php template', '2.6', 'the latest version, which supports multiple account pages and navigation, from WC 2.6.0' );
}
}
// Send output buffer.
ob_end_flush();
// After logging out without a nonce, add confirmation message.
if ( isset( $wp->query_vars['customer-logout'] ) && is_user_logged_in() ) {
/* translators: %s: logout url */
wc_add_notice( sprintf( __( 'Are you sure you want to log out? <a href="%s">Confirm and log out</a>', 'woocommerce' ), wc_logout_url() ) );
}
}
@@ -376,6 +365,7 @@ class WC_Shortcode_My_Account {
do_action( 'password_reset', $user, $new_pass );
wp_set_password( $new_pass, $user->ID );
update_user_meta( $user->ID, 'default_password_nag', false );
self::set_reset_password_cookie();
if ( ! apply_filters( 'woocommerce_disable_password_change_notification', false ) ) {

View File

@@ -209,6 +209,7 @@ class WC_Site_Tracking {
include_once WC_ABSPATH . 'includes/tracks/events/class-wc-order-tracking.php';
include_once WC_ABSPATH . 'includes/tracks/events/class-wc-coupon-tracking.php';
include_once WC_ABSPATH . 'includes/tracks/events/class-wc-theme-tracking.php';
include_once WC_ABSPATH . 'includes/tracks/events/class-wc-product-collection-block-tracking.php';
$tracking_classes = array(
'WC_Extensions_Tracking',
@@ -221,6 +222,7 @@ class WC_Site_Tracking {
'WC_Order_Tracking',
'WC_Coupon_Tracking',
'WC_Theme_Tracking',
'WC_Product_Collection_Block_Tracking',
);
foreach ( $tracking_classes as $tracking_class ) {
@@ -233,5 +235,26 @@ class WC_Site_Tracking {
}
}
/**
* Sets a cookie for tracking purposes, but only if tracking is enabled/allowed.
*
* @internal
* @since 9.2.0
*
* @param string $cookie_key The key of the cookie.
* @param string $cookie_value The value of the cookie.
* @param int $expire Expiry of the cookie.
* @param bool $secure Whether the cookie should be served only over https.
* @param bool $http_only Whether the cookie is only accessible over HTTP.
*
* @return bool If setting the cookie was attempted (will be false if tracking is not allowed).
*/
public static function set_tracking_cookie( string $cookie_key, string $cookie_value, int $expire = 0, bool $secure = false, bool $http_only = false ): bool {
if ( self::is_tracking_enabled() ) {
wc_setcookie( $cookie_key, $cookie_value, $expire, $secure, $http_only );
return true;
}
return false;
}
}

View File

@@ -73,7 +73,7 @@ class WC_Tracks_Client {
// Don't set cookie on API requests.
if ( ! Constants::is_true( 'REST_REQUEST' ) && ! Constants::is_true( 'XMLRPC_REQUEST' ) ) {
wc_setcookie( 'tk_ai', $anon_id );
WC_Site_Tracking::set_tracking_cookie( 'tk_ai', $anon_id );
}
}

View File

@@ -34,7 +34,7 @@ class WC_Extensions_Tracking {
'section' => empty( $_REQUEST['section'] ) ? '_featured' : wc_clean( wp_unslash( $_REQUEST['section'] ) ),
);
$event = 'extensions_view';
$event = 'extensions_view';
if ( 'helper' === $properties['section'] ) {
$event = 'subscriptions_view';
}
@@ -52,7 +52,7 @@ class WC_Extensions_Tracking {
* Send a Tracks event when the Extensions page gets a bad response or no response
* from the WCCOM extensions API.
*
* @param string $error
* @param string $error Error message.
*/
public function track_extensions_page_connection_error( string $error = '' ) {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
@@ -89,7 +89,17 @@ class WC_Extensions_Tracking {
* Send a Tracks even when a Helper connection process completed successfully.
*/
public function track_helper_connection_complete() {
WC_Tracks::record_event( 'extensions_subscriptions_connected' );
$properties = array();
if ( ! empty( $_GET['utm_source'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$properties['utm_source'] = wc_clean( wp_unslash( $_GET['utm_source'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
if ( ! empty( $_GET['utm_campaign'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$properties['utm_campaign'] = wc_clean( wp_unslash( $_GET['utm_campaign'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
WC_Tracks::record_event( 'extensions_subscriptions_connected', $properties );
}
/**

View File

@@ -0,0 +1,320 @@
<?php
declare( strict_types = 1);
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplate;
use Automattic\WooCommerce\Blocks\Templates\CartTemplate;
use Automattic\WooCommerce\Blocks\Templates\MiniCartTemplate;
use Automattic\WooCommerce\Blocks\Templates\CheckoutTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductCatalogTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductAttributeTemplate;
use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate;
/**
* This class adds actions to track usage of the Product Collection Block.
*/
class WC_Product_Collection_Block_Tracking {
/**
* Init Tracking.
*/
public function init() {
add_action( 'save_post', array( $this, 'track_collection_instances' ), 10, 2 );
}
/**
* Track feature usage of the Product Collection block within the site editor.
*
* @param int $post_id The post ID.
* @param \WP_Post $post The post object.
*
* @return void
*/
public function track_collection_instances( $post_id, $post ) {
if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST || ! wc_current_theme_is_fse_theme() ) {
return;
}
if ( ! $post instanceof \WP_Post ) {
return;
}
// Don't track autosaves and drafts.
$post_status = $post->post_status;
if ( 'publish' !== $post_status ) {
return;
}
// Important: Only track instances within specific types.
$post_type = $post->post_type;
if ( ! in_array( $post_type, array( 'post', 'page', 'wp_template', 'wp_template_part', 'wp_block' ), true ) ) {
return;
}
if ( ! has_block( 'woocommerce/product-collection', $post ) && ! has_block( 'core/template-part', $post ) && ! has_block( 'core/block', $post ) ) {
return;
}
$blocks = parse_blocks( $post->post_content );
if ( empty( $blocks ) ) {
return;
}
$instances = $this->parse_blocks_track_data( $blocks );
if ( empty( $instances ) ) {
return;
}
// Count orders.
// Hint: Product count included in Track event. See WC_Tracks::get_blog_details().
$order_count = 0;
foreach ( wc_get_order_statuses() as $status_slug => $status_name ) {
$order_count += wc_orders_count( $status_slug );
}
$additional_data = array(
'editor_context' => $this->parse_editor_location_context( $post ),
'order_count' => $order_count,
);
foreach ( $instances as $instance ) {
$event_properties = array_merge(
$additional_data,
$instance
);
\WC_Tracks::record_event(
'product_collection_instance',
$event_properties
);
}
}
/**
* Track usage of the Product Collection block within the given blocks.
*
* @param array $blocks The parsed blocks to check.
* @param bool $is_in_single_product Whether we are in a single product container (used for keeping state in the recurring process).
* @param bool $is_in_template_part Whether we are in a template part (used for keeping state in the recurring process).
* @param bool $is_in_synced_pattern Whether we are in a synced block (used for keeping state in the recurring process).
*
* @return array Parsed instances of the Product Collection block.
*/
private function parse_blocks_track_data( $blocks, $is_in_single_product = false, $is_in_template_part = false, $is_in_synced_pattern = false ) {
$instances = array();
if ( ! is_array( $blocks ) || empty( $blocks ) ) {
return $instances;
}
foreach ( $blocks as $block ) {
if ( empty( $block['blockName'] ) ) {
continue;
}
if ( 'woocommerce/product-collection' === $block['blockName'] ) {
$instances[] = array(
'collection' => $block['attrs']['collection'] ?? 'product-catalog',
'in_single_product' => $is_in_single_product ? 'yes' : 'no',
'in_template_part' => $is_in_template_part ? 'yes' : 'no',
'in_synced_pattern' => $is_in_synced_pattern ? 'yes' : 'no',
'filters' => wp_json_encode( $this->get_query_filters_usage_data( $block ) ),
);
}
// Track instances within single product container.
$local_is_in_single_product = $is_in_single_product;
if ( 'woocommerce/single-product' === $block['blockName'] ) {
$local_is_in_single_product = true;
}
// Track instances within template part.
// Hint: Supports up to two levels of depth.
if ( ! $is_in_synced_pattern && ! $is_in_template_part && 'core/template-part' === $block['blockName'] ) {
$template_part_theme = $block['attrs']['theme'] ?? '';
$template_part_slug = $block['attrs']['slug'] ?? '';
$template_part = get_block_template( $template_part_theme . '//' . $template_part_slug, 'wp_template_part' );
if ( $template_part instanceof WP_Block_Template && ! empty( $template_part->content ) ) {
// Recursive.
$instances = array_merge( $instances, $this->parse_blocks_track_data( parse_blocks( $template_part->content ), $local_is_in_single_product, true, $is_in_synced_pattern ) );
}
}
// Track instances within synced block.
// Hint: Supports up to two levels of depth.
if ( ! $is_in_synced_pattern && ! $is_in_template_part && 'core/block' === $block['blockName'] ) {
$block_id = $block['attrs']['ref'] ?? 0;
$synced_pattern = get_post( $block_id );
if ( $synced_pattern instanceof WP_Post && ! empty( $synced_pattern->post_content ) ) {
// Recursive.
$instances = array_merge( $instances, $this->parse_blocks_track_data( parse_blocks( $synced_pattern->post_content ), $local_is_in_single_product, $is_in_template_part, true ) );
}
}
// Recursive.
if ( ! empty( $block['innerBlocks'] ) ) {
$instances = array_merge( $instances, $this->parse_blocks_track_data( $block['innerBlocks'], $local_is_in_single_product, $is_in_template_part, $is_in_synced_pattern ) );
}
}
return $instances;
}
/**
* Parse editor's location context from WP Post.
*
* Possible contexts:
* - post
* - page
* - single-product
* - product-archive
* - cart
* - checkout
* - product-catalog
* - order-confirmation
*
* @param WP_Post $post The Post instance.
*
* @return string Returns the context.
*/
private function parse_editor_location_context( $post ) {
$context = 'other';
if ( ! $post instanceof \WP_Post ) {
return $context;
}
$post_type = $post->post_type;
if ( ! in_array( $post_type, array( 'post', 'page', 'wp_template', 'wp_template_part', 'wp_block' ), true ) ) {
return $context;
}
if ( 'wp_template' === $post_type ) {
$name = $post->post_name;
if ( false !== strpos( $name, SingleProductTemplate::SLUG ) ) {
$context = 'single-product';
} elseif ( ProductAttributeTemplate::SLUG === $name ) {
$context = 'product-archive';
} elseif ( false !== strpos( $name, 'taxonomy-' ) ) { // Including the '-' in the check to avoid false positives.
$taxonomy = str_replace( 'taxonomy-', '', $name );
$product_taxonomies = get_object_taxonomies( 'product', 'names' );
if ( in_array( $taxonomy, $product_taxonomies, true ) ) {
$context = 'product-archive';
}
} elseif ( in_array( $name, array( CartTemplate::SLUG, MiniCartTemplate::SLUG ), true ) ) {
$context = 'cart';
} elseif ( CheckoutTemplate::SLUG === $name ) {
$context = 'checkout';
} elseif ( ProductCatalogTemplate::SLUG === $name ) {
$context = 'product-catalog';
} elseif ( OrderConfirmationTemplate::SLUG === $name ) {
$context = 'order-confirmation';
}
}
if ( in_array( $post_type, array( 'wp_block', 'wp_template_part' ), true ) ) {
$context = 'isolated';
}
if ( 'page' === $post_type ) {
$context = 'page';
}
if ( 'post' === $post_type ) {
$context = 'post';
}
return $context;
}
/**
* Parse the collection query filters from the query attributes.
*
* @param array $block The parsed block.
* @return array The filters data for tracking.
*/
private function get_query_filters_usage_data( $block ) {
if ( ! isset( $block['attrs'] ) ) {
return array();
}
$query_attrs = $block['attrs']['query'] ?? array();
$filters = array(
'inherit' => 'no',
'order-by' => 'no',
'on-sale' => 'no',
'stock-status' => 'no',
'handpicked' => 'no',
'keyword' => 'no',
'attributes' => 'no',
'category' => 'no',
'tag' => 'no',
'featured' => 'no',
'created' => 'no',
'price' => 'no',
);
if ( ! empty( $query_attrs['inherit'] ) && true === $query_attrs['inherit'] ) {
$filters['inherit'] = 'yes';
}
if ( ( ! empty( $query_attrs['order'] ) && 'asc' !== $query_attrs['order'] ) || ( ! empty( $query_attrs['orderBy'] ) && 'title' !== $query_attrs['orderBy'] ) ) {
$filters['order-by'] = 'yes';
}
if ( ! empty( $query_attrs['woocommerceOnSale'] ) ) {
$filters['on-sale'] = 'yes';
}
if ( ! empty( $query_attrs['woocommerceStockStatus'] ) ) {
$stock_statuses = wc_get_product_stock_status_options();
$default_values = 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ? array_diff_key( $stock_statuses, array( 'outofstock' => '' ) ) : $stock_statuses;
$default_diff = array_diff( array_keys( $default_values ), $query_attrs['woocommerceStockStatus'] );
if ( ! empty( $default_diff ) ) {
$filters['stock-status'] = 'yes';
}
}
if ( ! empty( $query_attrs['woocommerceAttributes'] ) ) {
$filters['attributes'] = 'yes';
}
if ( ! empty( $query_attrs['timeFrame'] ) ) {
$filters['created'] = 'yes';
}
if ( ! empty( $query_attrs['taxQuery'] ) ) {
if ( ! empty( $query_attrs['taxQuery']['product_cat'] ) ) {
$filters['category'] = 'yes';
}
if ( ! empty( $query_attrs['taxQuery']['product_tag'] ) ) {
$filters['tag'] = 'yes';
}
}
if ( ! empty( $query_attrs['woocommerceHandPickedProducts'] ) ) {
$filters['handpicked'] = 'yes';
}
if ( ! empty( $query_attrs['search'] ) ) {
$filters['keyword'] = 'yes';
}
if ( ! empty( $query_attrs['featured'] ) ) {
$filters['featured'] = 'yes';
}
if ( ! empty( $query_attrs['priceRange'] ) ) {
$filters['price'] = 'yes';
}
return $filters;
}
}

View File

@@ -346,6 +346,7 @@ class WC_Products_Tracking {
'tags' => count( $product->get_tag_ids() ),
'upsells' => ! empty( $product->get_upsell_ids() ) ? 'yes' : 'no',
'weight' => $product->get_weight() ? 'yes' : 'no',
'global_unique_id' => $product->get_global_unique_id() ? 'yes' : 'no',
);
WC_Tracks::record_event( 'product_add_publish', $properties );

View File

@@ -22,6 +22,11 @@ function wc_lostpassword_url( $default_url = '' ) {
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;

View File

@@ -184,7 +184,7 @@ function wc_clear_cart_after_payment() {
}
}
if ( WC()->session->order_awaiting_payment > 0 ) {
if ( is_object( WC()->session ) && WC()->session->order_awaiting_payment > 0 ) {
$order = wc_get_order( WC()->session->order_awaiting_payment );
if ( $order instanceof WC_Order && $order->get_id() > 0 ) {

View File

@@ -10,6 +10,7 @@
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;
@@ -147,9 +148,9 @@ function wc_get_is_pending_statuses() {
*/
function wc_get_order_status_name( $status ) {
$statuses = wc_get_order_statuses();
$status = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status;
$status = isset( $statuses[ 'wc-' . $status ] ) ? $statuses[ 'wc-' . $status ] : $status;
return $status;
$status = OrderUtil::remove_status_prefix( $status );
return $statuses[ 'wc-' . $status ] ?? $status;
}
/**
@@ -539,10 +540,11 @@ function wc_create_refund( $args = array() ) {
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'] );
$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' ) );
@@ -575,6 +577,16 @@ function wc_create_refund( $args = array() ) {
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 );
@@ -634,6 +646,19 @@ function wc_create_refund( $args = array() ) {
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.
*

View File

@@ -168,7 +168,7 @@ function wc_nav_menu_item_classes( $menu_items ) {
$menu_id = (int) $menu_item->object_id;
// Unset active class for blog page.
if ( $page_for_posts === $menu_id ) {
if ( $page_for_posts === $menu_id && isset( $menu_item->object ) && 'page' === $menu_item->object ) {
$menu_items[ $key ]->current = false;
if ( in_array( 'current_page_parent', $classes, true ) ) {

View File

@@ -639,6 +639,52 @@ function wc_product_has_unique_sku( $product_id, $sku ) {
return true;
}
/**
* Check if product unique ID is unique.
*
* @since 9.1.0
* @param int $product_id Product ID.
* @param string $global_unique_id Product Unique ID.
* @return bool
*/
function wc_product_has_global_unique_id( $product_id, $global_unique_id ) {
/**
* Gives plugins an opportunity to verify Unique ID uniqueness themselves.
*
* @since 9.1.0
*
* @param bool|null $has_global_unique_id Set to a boolean value to short-circuit the default Unique ID check.
* @param int $product_id The ID of the current product.
* @param string $sku The Unique ID to check for uniqueness.
*/
$has_global_unique_id = apply_filters( 'wc_product_pre_has_global_unique_id', null, $product_id, $global_unique_id );
if ( ! is_null( $has_global_unique_id ) ) {
return boolval( $has_global_unique_id );
}
$data_store = WC_Data_Store::load( 'product' );
if ( $data_store->has_callable( 'is_existing_global_unique_id' ) ) {
$global_unique_id_found = $data_store->is_existing_global_unique_id( $product_id, $global_unique_id );
} else {
$logger = wc_get_logger();
$logger->error( 'The method is_existing_global_unique_id is not implemented in the data store.', array( 'source' => 'wc_product_has_global_unique_id' ) );
}
/**
* Gives plugins an opportunity to verify Unique ID uniqueness themselves.
*
* @since 9.1.0
*
* @param boolean $global_unique_id_found Whether the Unique ID is found.
* @param int $product_id The ID of the current product.
* @param string $sku The Unique ID to check for uniqueness.
*/
if ( apply_filters( 'wc_product_has_global_unique_id', $global_unique_id_found, $product_id, $global_unique_id ) ) {
return false;
}
return true;
}
/**
* Force a unique SKU.
*
@@ -692,6 +738,24 @@ function wc_get_product_id_by_sku( $sku ) {
return $data_store->get_product_id_by_sku( $sku );
}
/**
* Get product ID by Unique ID.
*
* @since 9.1.0
* @param string $global_unique_id Product Unique ID.
* @return int|null
*/
function wc_get_product_id_by_global_unique_id( $global_unique_id ) {
$data_store = WC_Data_Store::load( 'product' );
if ( $data_store->has_callable( 'get_product_id_by_global_unique_id' ) ) {
return $data_store->get_product_id_by_global_unique_id( $global_unique_id );
} else {
$logger = wc_get_logger();
$logger->error( 'The method get_product_id_by_global_unique_id is not implemented in the data store.', array( 'source' => 'wc_get_product_id_by_global_unique_id' ) );
}
return null;
}
/**
* Get attributes/data for an individual variation from the database and maintain it's integrity.
*
@@ -1421,6 +1485,7 @@ function wc_update_product_lookup_tables() {
'min_max_price',
'stock_quantity',
'sku',
'global_unique_id',
'stock_status',
'average_rating',
'total_sales',
@@ -1517,6 +1582,7 @@ function wc_update_product_lookup_tables_column( $column ) {
);
break;
case 'sku':
case 'global_unique_id':
case 'stock_status':
case 'average_rating':
case 'total_sales':

View File

@@ -373,3 +373,56 @@ function wc_rest_check_product_reviews_permissions( $context = 'read', $object_i
function wc_rest_is_from_product_editor() {
return isset( $_SERVER['HTTP_X_WC_FROM_PRODUCT_EDITOR'] ) && '1' === $_SERVER['HTTP_X_WC_FROM_PRODUCT_EDITOR'];
}
/**
* Check if a REST namespace should be loaded. Useful to maintain site performance even when lots of REST namespaces are registered.
*
* @since 9.2.0.
*
* @param string $ns The namespace to check.
* @param string $rest_route (Optional) The REST route being checked.
*
* @return bool True if the namespace should be loaded, false otherwise.
*/
function wc_rest_should_load_namespace( string $ns, string $rest_route = '' ): bool {
if ( '' === $rest_route ) {
$rest_route = $GLOBALS['wp']->query_vars['rest_route'] ?? '';
}
if ( '' === $rest_route ) {
return true;
}
$rest_route = trailingslashit( ltrim( $rest_route, '/' ) );
$ns = trailingslashit( $ns );
/**
* Known namespaces that we know are safe to not load if the request is not for them. Namespaces not in this namespace should always be loaded, because we don't know if they won't be making another internal REST request to an unloaded namespace.
*/
$known_namespaces = array(
'wc/v1',
'wc/v2',
'wc/v3',
'wc-telemetry',
'wc-admin',
'wc-analytics',
'wc/store',
'wc/private',
);
// We can consider allowing filtering this list in the future.
$known_namespace_request = false;
foreach ( $known_namespaces as $known_namespace ) {
if ( str_starts_with( $rest_route, $known_namespace ) ) {
$known_namespace_request = true;
break;
}
}
if ( ! $known_namespace_request ) {
return true;
}
return str_starts_with( $rest_route, $ns );
}

View File

@@ -10,6 +10,8 @@
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Checkout\Helpers\ReserveStock;
/**
* Update a product's stock amount.
*
@@ -348,7 +350,8 @@ function wc_get_held_stock_quantity( WC_Product $product, $exclude_order_id = 0
return 0;
}
return ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->get_reserved_stock( $product, $exclude_order_id );
$reserve_stock = new ReserveStock();
return $reserve_stock->get_reserved_stock( $product, $exclude_order_id );
}
/**
@@ -374,7 +377,8 @@ function wc_reserve_stock_for_order( $order ) {
$order = $order instanceof WC_Order ? $order : wc_get_order( $order );
if ( $order ) {
( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->reserve_stock_for_order( $order );
$reserve_stock = new ReserveStock();
$reserve_stock->reserve_stock_for_order( $order );
}
}
add_action( 'woocommerce_checkout_order_created', 'wc_reserve_stock_for_order' );
@@ -400,7 +404,8 @@ function wc_release_stock_for_order( $order ) {
$order = $order instanceof WC_Order ? $order : wc_get_order( $order );
if ( $order ) {
( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->release_stock_for_order( $order );
$reserve_stock = new ReserveStock();
$reserve_stock->release_stock_for_order( $order );
}
}
add_action( 'woocommerce_checkout_order_exception', 'wc_release_stock_for_order' );

View File

@@ -1608,6 +1608,7 @@ function wc_get_gallery_image_html( $attachment_id, $main_image = false ) {
$image_size = apply_filters( 'woocommerce_gallery_image_size', $flexslider || $main_image ? 'woocommerce_single' : $thumbnail_size );
$full_size = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) );
$thumbnail_src = wp_get_attachment_image_src( $attachment_id, $thumbnail_size );
$thumbnail_srcset = wp_get_attachment_image_srcset( $attachment_id, $thumbnail_size );
$full_src = wp_get_attachment_image_src( $attachment_id, $full_size );
$alt_text = trim( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) );
$image = wp_get_attachment_image(
@@ -1631,7 +1632,7 @@ function wc_get_gallery_image_html( $attachment_id, $main_image = false ) {
)
);
return '<div data-thumb="' . esc_url( $thumbnail_src[0] ) . '" data-thumb-alt="' . esc_attr( $alt_text ) . '" class="woocommerce-product-gallery__image"><a href="' . esc_url( $full_src[0] ) . '">' . $image . '</a></div>';
return '<div data-thumb="' . esc_url( $thumbnail_src[0] ) . '" data-thumb-alt="' . esc_attr( $alt_text ) . '" data-thumb-srcset="' . esc_attr( $thumbnail_srcset ) . '" class="woocommerce-product-gallery__image"><a href="' . esc_url( $full_src[0] ) . '">' . $image . '</a></div>';
}
if ( ! function_exists( 'woocommerce_output_product_data_tabs' ) ) {
@@ -4018,3 +4019,45 @@ function wc_update_product_archive_title( $post_type_name, $post_type ) {
add_filter( 'post_type_archive_title', 'wc_update_product_archive_title', 10, 2 );
// phpcs:enable Generic.Commenting.Todo.TaskFound
/**
* Set the version of the hooked blocks in the database. Used when WC is installed for the first time.
*
* @since 9.2.0
*
* @return void
*/
function wc_set_hooked_blocks_version() {
// Only set the version if the current theme is a block theme.
if ( ! wc_current_theme_is_fse_theme() && ! current_theme_supports( 'block-template-parts' ) ) {
return;
}
$option_name = 'woocommerce_hooked_blocks_version';
if ( get_option( $option_name ) ) {
return;
}
add_option( $option_name, WC()->version );
}
/**
* If the user switches from a classic to a block theme and they haven't already got a woocommerce_hooked_blocks_version,
* set the version of the hooked blocks in the database, or as "no" to disable all block hooks then set as the latest WC version.
*
* @since 9.2.0
*
* @param string $old_name Old theme name.
* @param \WP_Theme $old_theme Instance of the old theme.
* @return void
*/
function wc_set_hooked_blocks_version_on_theme_switch( $old_name, $old_theme ) {
$option_name = 'woocommerce_hooked_blocks_version';
$option_value = get_option( $option_name, false );
// Sites with the option value set to "no" have already been migrated, and block hooks have been disabled. Checking explicitly for false to avoid setting the option again.
if ( ! $old_theme->is_block_theme() && ( wc_current_theme_is_fse_theme() || current_theme_supports( 'block-template-parts' ) ) && false === $option_value ) {
add_option( $option_name, WC()->version );
}
}

View File

@@ -319,3 +319,8 @@ add_action( 'woocommerce_before_customer_login_form', 'woocommerce_output_all_no
add_action( 'woocommerce_before_lost_password_form', 'woocommerce_output_all_notices', 10 );
add_action( 'before_woocommerce_pay', 'woocommerce_output_all_notices', 10 );
add_action( 'woocommerce_before_reset_password_form', 'woocommerce_output_all_notices', 10 );
/**
* Hooked blocks.
*/
add_action( 'after_switch_theme', 'wc_set_hooked_blocks_version_on_theme_switch', 10, 2 );

View File

@@ -2671,7 +2671,7 @@ function wc_update_870_prevent_listing_of_transient_files_directory() {
}
/**
* If it exists, remove and recreate the inbox note that asks users to connect to `Woo.com` so that the domain name is changed to the updated `WooCommerce.com`.
* If it exists, remove the inbox note that asks users to connect to `Woo.com`.
*/
function wc_update_890_update_connect_to_woocommerce_note() {
$note = Notes::get_note_by_name( WooSubscriptionsNotes::CONNECTION_NOTE_NAME );
@@ -2685,8 +2685,6 @@ function wc_update_890_update_connect_to_woocommerce_note() {
return;
}
Notes::delete_notes_with_name( WooSubscriptionsNotes::CONNECTION_NOTE_NAME );
$new_note = WooSubscriptionsNotes::get_note();
$new_note->save();
}
/**
@@ -2732,6 +2730,45 @@ function wc_update_910_add_launch_your_store_tour_option() {
add_option( 'woocommerce_show_lys_tour', 'yes' );
}
/**
* Add woocommerce_hooked_blocks_version option for existing stores that are using a theme that supports the Block Hooks API
*/
function wc_update_920_add_wc_hooked_blocks_version_option() {
if ( ! wc_current_theme_is_fse_theme() && ! current_theme_supports( 'block-template-parts' ) ) {
return;
}
$option_name = 'woocommerce_hooked_blocks_version';
$option_value = get_option( $option_name );
// If the option already exists, we don't need to do anything.
if ( false !== $option_value ) {
return;
}
/**
* A list of theme slugs to execute this with.
* We are applying this filter to allow for the list to be extended by third-parties who were already using it.
*
* @since 8.4.0
*/
$theme_include_list = apply_filters( 'woocommerce_hooked_blocks_theme_include_list', array( 'Twenty Twenty-Four', 'Twenty Twenty-Three', 'Twenty Twenty-Two', 'Tsubaki', 'Zaino', 'Thriving Artist', 'Amulet', 'Tazza' ) );
$active_theme_name = wp_get_theme()->get( 'Name' );
$should_set_hooked_blocks_version = in_array( $active_theme_name, $theme_include_list, true );
if ( $should_set_hooked_blocks_version ) {
// Set 8.4.0 as the version for existing stores that are using a theme that supports the Block Hooks API.
// This will ensure that the Block Hooks API is enabled for these stores and works as expected.
// Existing stores that aren't running approved block themes will not have the Block Hooks API enabled.
add_option( $option_name, '8.4.0' );
} else {
// For block themes that aren't approved themes set this option to "no" to completely disable hooked blocks.
// This means we can assume the absence of the option is when a site is switching from a classic theme to a block theme for the first time.
// Note: We have to use "no" instead of false since the latter is the default value for the option if it doesn't exist.
add_option( $option_name, 'no' );
}
}
/**
* Remove user meta associated with the keys '_last_order', '_order_count' and '_money_spent'.
*