plugin updates

This commit is contained in:
Tony Volpe
2024-07-16 13:57:46 +00:00
parent 41f50eacc4
commit 8f93917880
1529 changed files with 259452 additions and 25451 deletions

View File

@@ -231,7 +231,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
'gateway_toggle' => current_user_can( 'manage_woocommerce' ) ? wp_create_nonce( 'woocommerce-toggle-payment-gateway-enabled' ) : null,
),
'urls' => array(
'add_product' => Features::is_enabled( 'new-product-management-experience' ) || \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ? esc_url_raw( admin_url( 'admin.php?page=wc-admin&path=/add-product' ) ) : null,
'add_product' => \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ? esc_url_raw( admin_url( 'admin.php?page=wc-admin&path=/add-product' ) ) : null,
'import_products' => current_user_can( 'import' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_importer' ) ) : null,
'export_products' => current_user_can( 'export' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_exporter' ) ) : null,
),

View File

@@ -485,7 +485,7 @@ class WC_Admin_Menus {
* Maybe add new management product experience.
*/
public function maybe_add_new_product_management_experience() {
if ( Features::is_enabled( 'new-product-management-experience' ) || FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) {
if ( FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) {
global $submenu;
if ( isset( $submenu['edit.php?post_type=product'][10] ) ) {
// Disable phpcs since we need to override submenu classes.

View File

@@ -33,7 +33,7 @@ class WC_Helper {
* @return string The absolute path to the view file.
*/
public static function get_view_filename( $view ) {
return dirname( __FILE__ ) . "/views/$view";
return __DIR__ . "/views/$view";
}
/**
@@ -119,7 +119,7 @@ class WC_Helper {
$subscriptions_list_data = self::get_subscription_list_data();
$subscriptions = array_filter(
$subscriptions_list_data,
function( $subscription ) {
function ( $subscription ) {
return ! empty( $subscription['product_key'] );
}
);
@@ -362,9 +362,9 @@ class WC_Helper {
*/
public static function add_utm_params_to_url_for_subscription_link( $url, $utm_content ) {
$utm_params = 'utm_source=subscriptionsscreen&' .
'utm_medium=product&' .
'utm_campaign=wcaddons&' .
'utm_content=' . $utm_content;
'utm_medium=product&' .
'utm_campaign=wcaddons&' .
'utm_content=' . $utm_content;
// there are already some URL parameters
if ( strpos( $url, '?' ) ) {
@@ -879,7 +879,8 @@ class WC_Helper {
$request = WC_Helper_API::post(
'oauth/access_token',
array(
'body' => array(
'timeout' => 30,
'body' => array(
'request_token' => wp_unslash( $_GET['request_token'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
'home_url' => home_url(),
),
@@ -1554,28 +1555,36 @@ class WC_Helper {
}
/**
* Get the connected user's subscription list data.
* This is used by the My Subscriptions page.
* Get the connected user's subscription list data. Here, we merge connected
* subscriptions with locally installed Woo plugins and themes. We also
* add in information about available updates.
*
* Used by the My Subscriptions page.
*
* @return array
*/
public static function get_subscription_list_data() {
// First, connected subscriptions.
$subscriptions = self::get_subscriptions();
// Installed plugins and themes, with or without an active subscription.
// Then, installed plugins and themes, with or without an active subscription.
$woo_plugins = self::get_local_woo_plugins();
$woo_themes = self::get_local_woo_themes();
// Get the product IDs of the subscriptions.
$subscriptions_product_ids = wp_list_pluck( $subscriptions, 'product_id' );
// Get the site ID.
$auth = WC_Helper_Options::get( 'auth' );
$site_id = isset( $auth['site_id'] ) ? absint( $auth['site_id'] ) : 0;
// Installed products without a subscription.
// Now, merge installed products without a subscription.
foreach ( array_merge( $woo_plugins, $woo_themes ) as $filename => $data ) {
if ( in_array( $data['_product_id'], $subscriptions_product_ids, true ) ) {
continue;
}
// We add these as subscriptions to the previous connected subscriptions list.
$subscriptions[] = array(
'product_key' => '',
'product_id' => $data['_product_id'],
@@ -1587,6 +1596,7 @@ class WC_Helper {
'key_type_label' => '',
'lifetime' => false,
'product_status' => 'publish',
// Connections is empty because this is not a connected subscription.
'connections' => array(),
'expires' => 0,
'expired' => true,
@@ -1598,11 +1608,13 @@ class WC_Helper {
);
}
// Fetch updates so we can refine subscriptions with information about updates.
$updates = WC_Helper_Updater::get_update_data();
// Add local data to merged subscriptions list (both locally installed and purchased).
foreach ( $subscriptions as &$subscription ) {
$subscription['active'] = in_array( $site_id, $subscription['connections'], true );
$updates = WC_Helper_Updater::get_update_data();
$subscription['local'] = self::get_subscription_local_data( $subscription );
$subscription['has_update'] = false;
@@ -1613,12 +1625,17 @@ class WC_Helper {
if ( ! empty( $updates[ $subscription['product_id'] ] ) ) {
$subscription['version'] = $updates[ $subscription['product_id'] ]['version'];
}
// If the update endpoint returns a URL, we prefer it over the default PluginURI.
if ( ! empty( $updates[ $subscription['product_id'] ]['url'] ) ) {
$subscription['product_url'] = $updates[ $subscription['product_id'] ]['url'];
}
}
// Sort subscriptions by name and expiration date.
usort(
$subscriptions,
function( $a, $b ) {
function ( $a, $b ) {
$compare_value = strcasecmp( $a['product_name'], $b['product_name'] );
if ( 0 === $compare_value ) {
return strcasecmp( $a['expires'], $b['expires'] );
@@ -1823,7 +1840,7 @@ class WC_Helper {
}
// No more sites available in this subscription.
if ( $_sub['sites_max'] && $_sub['sites_active'] >= $_sub['sites_max'] ) {
if ( isset( $_sub['maxed'] ) && $_sub['maxed'] ) {
continue;
}
@@ -1938,7 +1955,7 @@ class WC_Helper {
);
if ( wp_remote_retrieve_response_code( $deactivation_response ) === 200 ) {
$deactivated++;
++$deactivated;
/**
* Fires when the Helper activates a product successfully.
@@ -2011,7 +2028,7 @@ class WC_Helper {
$product_id = $data['_product_id'];
if ( version_compare( $updates[ $product_id ]['version'], $data['Version'], '>' ) ) {
$available++;
++$available;
}
}
@@ -2065,6 +2082,7 @@ class WC_Helper {
'oauth/me',
array(
'authenticated' => true,
'timeout' => 30,
)
);
@@ -2261,6 +2279,42 @@ class WC_Helper {
return $woo_com_base_url . 'auto-install-init/';
}
/**
* Retrieve notice for connected store.
*
* @return array An array containing notice data.
*/
public static function get_notices() {
$cache_key = '_woocommerce_helper_notices';
$cached_data = get_transient( $cache_key );
if ( false !== $cached_data ) {
return $cached_data;
}
// Fetch notice data for connected store.
$request = WC_Helper_API::get(
'notices',
array(
'authenticated' => true,
)
);
if ( 200 !== wp_remote_retrieve_response_code( $request ) ) {
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;
}
}
WC_Helper::load();

View File

@@ -562,7 +562,7 @@ class WC_Meta_Box_Order_Data {
}
if ( apply_filters( 'woocommerce_enable_order_notes_field', 'yes' === get_option( 'woocommerce_enable_order_comments', 'yes' ) ) && $order->get_customer_note() ) { // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
echo '<p class="order_note"><strong>' . esc_html( __( 'Customer provided note:', 'woocommerce' ) ) . '</strong> ' . nl2br( esc_html( $order->get_customer_note() ) ) . '</p>';
echo '<p class="order_note"><strong>' . esc_html( __( 'Customer provided note:', 'woocommerce' ) ) . '</strong> ' . wp_kses( nl2br( esc_html( $order->get_customer_note() ) ), array() ) . '</p>';
}
?>
</div>
@@ -615,7 +615,7 @@ class WC_Meta_Box_Order_Data {
?>
<p class="form-field form-field-wide">
<label for="customer_note"><?php esc_html_e( 'Customer provided note', 'woocommerce' ); ?>:</label>
<textarea rows="1" cols="40" name="customer_note" tabindex="6" id="excerpt" placeholder="<?php esc_attr_e( 'Customer notes about the order', 'woocommerce' ); ?>"><?php echo wp_kses_post( $order->get_customer_note() ); ?></textarea>
<textarea rows="1" cols="40" name="customer_note" tabindex="6" id="excerpt" placeholder="<?php esc_attr_e( 'Customer notes about the order', 'woocommerce' ); ?>"><?php echo wp_kses( $order->get_customer_note(), array() ); ?></textarea>
</p>
<?php endif; ?>
</div>

View File

@@ -197,7 +197,7 @@ class WC_Meta_Box_Product_Data {
if ( ! empty( $file_urls ) ) {
$file_url_size = count( $file_urls );
for ( $i = 0; $i < $file_url_size; $i ++ ) {
for ( $i = 0; $i < $file_url_size; $i++ ) {
if ( ! empty( $file_urls[ $i ] ) ) {
$downloads[] = array(
'name' => wc_clean( $file_names[ $i ] ),
@@ -414,6 +414,9 @@ class WC_Meta_Box_Product_Data {
WC_Admin_Meta_Boxes::add_error( $errors->get_error_message() );
}
// Remove _product_template_id for products that were created with the new product editor.
$product->delete_meta_data( '_product_template_id' );
/**
* Set props before save.
*

View File

@@ -71,10 +71,10 @@ if ( ! defined( 'ABSPATH' ) ) {
echo '<input type="hidden" name="_original_stock" value="' . esc_attr( wc_stock_amount( $product_object->get_stock_quantity( 'edit' ) ) ) . '" />';
$backorder_args = array(
'id' => '_backorders',
'value' => $product_object->get_backorders( 'edit' ),
'label' => __( 'Allow backorders?', 'woocommerce' ),
'options' => wc_get_product_backorder_options(),
'id' => '_backorders',
'value' => $product_object->get_backorders( 'edit' ),
'label' => __( 'Allow backorders?', 'woocommerce' ),
'options' => wc_get_product_backorder_options(),
);
/**

View File

@@ -188,19 +188,27 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page {
echo wp_kses_post( $gateway->get_method_description() );
break;
case 'action':
if ( wc_string_to_bool( $gateway->enabled ) ) {
/* Translators: %s Payment gateway name. */
echo '<a class="button alignright" aria-label="' . esc_attr( sprintf( __( 'Manage the "%s" payment method', 'woocommerce' ), $method_title ) ) . '" href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=checkout&section=' . strtolower( $gateway->id ) ) ) . '">' . esc_html__( 'Manage', 'woocommerce' ) . '</a>';
} else {
if (
// Keep old brand name for backwards compatibility.
( 'WooCommerce Payments' === $method_title || 'WooPayments' === $method_title ) &&
class_exists( 'WC_Payments_Account' )
) {
$setup_url = WC_Payments_Account::get_connect_url();
$setup_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=' . strtolower( $gateway->id ) );
// Override the behaviour for 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).
/* 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>';
} else {
$setup_url = admin_url( 'admin.php?page=wc-settings&tab=checkout&section=' . strtolower( $gateway->id ) );
// If the account is fully onboarded, the CTA text and label is "Manage" regardless gateway is enabled or not.
/* Translators: %s Payment gateway name. */
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>';
}
} elseif ( wc_string_to_bool( $gateway->enabled ) ) {
/* Translators: %s Payment gateway name. */
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>';
}

View File

@@ -11,6 +11,13 @@ defined( 'ABSPATH' ) || exit;
<div id="key-fields" class="settings-panel">
<h2><?php esc_html_e( 'Key details', 'woocommerce' ); ?></h2>
<div class="inline notice">
<ul class="advice">
<li><?php esc_html_e( 'API keys open up access to potentially sensitive information. Only share them with organizations you trust.', 'woocommerce' ); ?></li>
<li><?php esc_html_e( 'Stick to one key per client: this makes it easier to revoke access in the future for a single client, without causing disruption for others.', 'woocommerce' ); ?></li>
</ul>
</div>
<input type="hidden" id="key_id" value="<?php echo esc_attr( $key_id ); ?>" />
<table id="api-keys-options" class="form-table">
@@ -24,6 +31,9 @@ defined( 'ABSPATH' ) || exit;
</th>
<td class="forminp">
<input maxlength="200" id="key_description" type="text" class="input-text regular-input" value="<?php echo esc_attr( $key_data['description'] ); ?>" />
<p class="description">
<?php esc_html_e( 'Add a meaningful description, including a note of the person, company or app you are sharing the key with.', 'woocommerce' ); ?>
</p>
</td>
</tr>
<tr valign="top">
@@ -72,6 +82,9 @@ defined( 'ABSPATH' ) || exit;
<option value="<?php echo esc_attr( $permission_id ); ?>" <?php selected( $key_data['permissions'], $permission_id, true ); ?>><?php echo esc_html( $permission_name ); ?></option>
<?php endforeach; ?>
</select>
<p class="conditional description" data-depends-on="#key_permissions" data-show-if-equals="write">
<?php esc_html_e( 'Write-only keys do not prevent clients from seeing information about the entities they are updating.', 'woocommerce' ); ?>
</p>
</td>
</tr>

View File

@@ -6,6 +6,7 @@
*/
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
use Automattic\WooCommerce\Utilities\RestApiUtil;
defined( 'ABSPATH' ) || exit;
@@ -873,10 +874,38 @@ if ( 0 < $mu_plugins_count ) :
echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . ( $_page['block_required'] ? sprintf( esc_html__( 'Page does not contain the %1$s shortcode or the %2$s block.', 'woocommerce' ), esc_html( $_page['shortcode'] ), esc_html( $_page['block'] ) ) : sprintf( esc_html__( 'Page does not contain the %s shortcode.', 'woocommerce' ), esc_html( $_page['shortcode'] ) ) ) . '</mark>'; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */
$found_error = true;
}
// Warn merchants if both the shortcode and block are present, which will be a confusing shopper experience.
if ( $_page['shortcode_present'] && $_page['block_present'] ) {
/* Translators: %1$s: shortcode text, %2$s: block slug. */
echo '<mark class="error"><span class="dashicons dashicons-warning"></span> ' . sprintf( esc_html__( 'Page contains both the %1$s shortcode and the %2$s block.', 'woocommerce' ), esc_html( $_page['shortcode'] ), esc_html( $_page['block'] ) ) . '</mark>'; /* phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped */
$found_error = true;
}
}
if ( ! $found_error ) {
echo '<mark class="yes">#' . absint( $_page['page_id'] ) . ' - ' . esc_html( str_replace( home_url(), '', get_permalink( $_page['page_id'] ) ) ) . '</mark>';
$additional_info = '';
// We only state the used type on the Checkout and the Cart page.
if ( in_array( $_page['block'], array( 'woocommerce/checkout', 'woocommerce/cart' ), true ) ) {
// We check first if, in a blocks theme, the template content does not load the page content.
if ( CartCheckoutUtils::is_overriden_by_custom_template_content( $_page['block'] ) ) {
$additional_info = __( "This page's content is overridden by custom template content", 'woocommerce' );
} elseif ( $_page['shortcode_present'] ) {
/* Translators: %1$s: shortcode text. */
$additional_info = sprintf( __( 'Contains the <strong>%1$s</strong> shortcode', 'woocommerce' ), esc_html( $_page['shortcode'] ) );
} elseif ( $_page['block_present'] ) {
/* Translators: %1$s: block slug. */
$additional_info = sprintf( __( 'Contains the <strong>%1$s</strong> block', 'woocommerce' ), esc_html( $_page['block'] ) );
}
if ( ! empty( $additional_info ) ) {
$additional_info = '<mark class="no"> - <span class="dashicons dashicons-info"></span> ' . $additional_info . '</mark>';
}
}
echo '<mark class="yes">#' . absint( $_page['page_id'] ) . ' - ' . esc_html( str_replace( home_url(), '', get_permalink( $_page['page_id'] ) ) ) . '</mark>' . $additional_info; /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped */
}
echo '</td></tr>';
@@ -969,7 +998,7 @@ if ( 0 < $mu_plugins_count ) :
</tr>
</tbody>
</table>
<table class="wc_status_table widefat" cellspacing="0">
<table class="wc_status_table widefat" id="status-table-templates" cellspacing="0">
<thead>
<tr>
<th colspan="3" data-export-label="Templates"><h2><?php esc_html_e( 'Templates', 'woocommerce' ); ?><?php echo wc_help_tip( esc_html__( 'This section shows any files that are overriding the default WooCommerce template pages.', 'woocommerce' ) ); ?></h2></th>

View File

@@ -24,6 +24,6 @@ $theme = wp_get_theme();
</p>
<p class="submit">
<a class="button-primary" href="https://woocommerce.com/document/template-structure/" target="_blank"><?php esc_html_e( 'Learn more about templates', 'woocommerce' ); ?></a>
<a class="button-primary" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-status' ) ); ?>" target="_blank"><?php esc_html_e( 'View affected templates', 'woocommerce' ); ?></a>
<a class="button-primary" href="<?php echo esc_url( admin_url( 'admin.php?page=wc-status#status-table-templates' ) ); ?>" target="_blank"><?php esc_html_e( 'View affected templates', 'woocommerce' ); ?></a>
</p>
</div>

View File

@@ -69,6 +69,14 @@ class WC_Autoloader {
return;
}
// The Legacy REST API was removed in WooCommerce 9.0, but some servers still have
// the includes/class-wc-api.php file after they upgrade, which causes a fatal error when executing
// "class_exists('WC_API')". This will prevent this error, while still making the class visible
// when it's provided by the WooCommerce Legacy REST API plugin.
if ( 'wc_api' === $class ) {
return;
}
$file = $this->get_file_name_from_class( $class );
$path = '';

View File

@@ -169,8 +169,7 @@ class WC_Cache_Helper {
$redirect_url = add_query_arg( $wp->query_string, '', $redirect_url );
}
$redirect_url = add_query_arg( 'v', $location_hash, remove_query_arg( 'v', $redirect_url ) );
$redirect_url = add_query_arg( 'v', $location_hash, remove_query_arg( array( 'v', 'add-to-cart' ), $redirect_url ) );
wp_safe_redirect( esc_url_raw( $redirect_url ), 307 );
exit;
}

View File

@@ -6,7 +6,8 @@
* @version 3.0.0
*/
use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner;
use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner as CustomOrdersTableCLIRunner;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\CLIRunner as ProductAttributesLookupCLIRunner;
defined( 'ABSPATH' ) || exit;
@@ -26,13 +27,13 @@ class WC_CLI {
* Load command files.
*/
private function includes() {
require_once dirname( __FILE__ ) . '/cli/class-wc-cli-runner.php';
require_once dirname( __FILE__ ) . '/cli/class-wc-cli-rest-command.php';
require_once dirname( __FILE__ ) . '/cli/class-wc-cli-tool-command.php';
require_once dirname( __FILE__ ) . '/cli/class-wc-cli-update-command.php';
require_once dirname( __FILE__ ) . '/cli/class-wc-cli-tracker-command.php';
require_once dirname( __FILE__ ) . '/cli/class-wc-cli-com-command.php';
require_once dirname( __FILE__ ) . '/cli/class-wc-cli-com-extension-command.php';
require_once __DIR__ . '/cli/class-wc-cli-runner.php';
require_once __DIR__ . '/cli/class-wc-cli-rest-command.php';
require_once __DIR__ . '/cli/class-wc-cli-tool-command.php';
require_once __DIR__ . '/cli/class-wc-cli-update-command.php';
require_once __DIR__ . '/cli/class-wc-cli-tracker-command.php';
require_once __DIR__ . '/cli/class-wc-cli-com-command.php';
require_once __DIR__ . '/cli/class-wc-cli-com-extension-command.php';
}
/**
@@ -45,8 +46,10 @@ class WC_CLI {
WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_Tracker_Command::register_commands' );
WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_COM_Command::register_commands' );
WP_CLI::add_hook( 'after_wp_load', 'WC_CLI_COM_Extension_Command::register_commands' );
$cli_runner = wc_get_container()->get( CLIRunner::class );
$cli_runner = wc_get_container()->get( CustomOrdersTableCLIRunner::class );
WP_CLI::add_hook( 'after_wp_load', array( $cli_runner, 'register_commands' ) );
$cli_runner = wc_get_container()->get( ProductAttributesLookupCLIRunner::class );
WP_CLI::add_hook( 'after_wp_load', fn() => \WP_CLI::add_command( 'wc palt', $cli_runner ) );
}
}

View File

@@ -597,7 +597,7 @@ class WC_Emails {
}
/**
* Renders any additional fields captured during block based checkout.
* Renders any additional fields captured during block-based checkout.
*
* @param WC_Order $order Order instance.
* @param bool $sent_to_admin If email is sent to admin.
@@ -634,7 +634,7 @@ class WC_Emails {
}
/**
* Renders any additional address fields captured during block based checkout.
* Renders any additional address fields captured during block-based checkout.
*
* @param string $address_type Address type.
* @param WC_Order $order Order instance.

View File

@@ -298,6 +298,11 @@ class WC_Frontend_Scripts {
'deps' => array( 'jquery', 'woocommerce' ),
'version' => $version,
),
'wc-account-i18n' => array(
'src' => self::get_asset_url( 'assets/js/frontend/account-i18n' . $suffix . '.js' ),
'deps' => array( 'jquery' ),
'version' => $version,
),
'wc-password-strength-meter' => array(
'src' => self::get_asset_url( 'assets/js/frontend/password-strength-meter' . $suffix . '.js' ),
'deps' => array( 'jquery', 'password-strength-meter' ),
@@ -389,6 +394,9 @@ class WC_Frontend_Scripts {
self::enqueue_script( 'wc-password-strength-meter' );
}
}
if ( is_account_page() ) {
self::enqueue_script( 'wc-account-i18n' );
}
if ( is_checkout() ) {
self::enqueue_script( 'wc-checkout' );
}

View File

@@ -255,8 +255,9 @@ class WC_Install {
'8.9.1' => array(
'wc_update_891_create_plugin_autoinstall_history_option',
),
'9.0.0' => array(
'wc_update_900_add_launch_your_store_tour_option',
'9.1.0' => array(
'wc_update_910_add_launch_your_store_tour_option',
'wc_update_910_remove_obsolete_user_meta',
),
);

View File

@@ -183,10 +183,10 @@ class WC_Logger implements WC_Logger_Interface {
* @param array $context Additional information for log handlers.
* @param object $handler The handler object, such as WC_Log_Handler_File. Available since 5.3.
*/
$message = apply_filters( 'woocommerce_logger_log_message', $message, $level, $context, $handler );
$filtered_message = apply_filters( 'woocommerce_logger_log_message', $message, $level, $context, $handler );
if ( null !== $message ) {
$handler->handle( $timestamp, $level, $message, $context );
if ( null !== $filtered_message ) {
$handler->handle( $timestamp, $level, $filtered_message, $context );
}
}
}

View File

@@ -11,6 +11,7 @@
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Admin\Features\Features;
/**
* Post types Class.
@@ -108,6 +109,7 @@ class WC_Post_Types {
'not_found' => __( 'No categories found', 'woocommerce' ),
'item_link' => __( 'Product Category Link', 'woocommerce' ),
'item_link_description' => __( 'A link to a product category.', 'woocommerce' ),
'template_name' => _x( 'Products by Category', 'Template name', 'woocommerce' ),
),
'show_in_rest' => true,
'show_ui' => true,
@@ -153,6 +155,7 @@ class WC_Post_Types {
'not_found' => __( 'No tags found', 'woocommerce' ),
'item_link' => __( 'Product Tag Link', 'woocommerce' ),
'item_link_description' => __( 'A link to a product tag.', 'woocommerce' ),
'template_name' => _x( 'Products by Tag', 'Template name', 'woocommerce' ),
),
'show_in_rest' => true,
'show_ui' => true,
@@ -369,6 +372,73 @@ class WC_Post_Types {
)
);
// Register the product form post type wne the feature is enabled.
if ( Features::is_enabled( 'product-editor-template-system' ) ) {
register_post_type(
'product_form',
/**
* Allow developers to customize the product form post type registration arguments.
*
* @since 9.1.0
* @param array $args The default post type registration arguments.
*/
apply_filters(
'woocommerce_register_post_type_product_form',
array(
'labels'
=> array(
'name' => __( 'Product Forms', 'woocommerce' ),
'singular_name' => __( 'Product Form', 'woocommerce' ),
'all_items' => __( 'All Product Form', 'woocommerce' ),
'menu_name' => _x( 'Product Forms', 'Admin menu name', 'woocommerce' ),
'add_new' => __( 'Add New', 'woocommerce' ),
'add_new_item' => __( 'Add new product form', 'woocommerce' ),
'edit' => __( 'Edit', 'woocommerce' ),
'edit_item' => __( 'Edit product form', 'woocommerce' ),
'new_item' => __( 'New product form', 'woocommerce' ),
'view_item' => __( 'View product form', 'woocommerce' ),
'view_items' => __( 'View product forms', 'woocommerce' ),
'search_items' => __( 'Search product forms', 'woocommerce' ),
'not_found' => __( 'No product forms found', 'woocommerce' ),
'not_found_in_trash' => __( 'No product forms found in trash', 'woocommerce' ),
'parent' => __( 'Parent product form', 'woocommerce' ),
'featured_image' => __( 'Product form image', 'woocommerce' ),
'set_featured_image' => __( 'Set product form image', 'woocommerce' ),
'remove_featured_image' => __( 'Remove product form image', 'woocommerce' ),
'use_featured_image' => __( 'Use as product form image', 'woocommerce' ),
'insert_into_item' => __( 'Insert into product form', 'woocommerce' ),
'uploaded_to_this_item' => __( 'Uploaded to this product form', 'woocommerce' ),
'filter_items_list' => __( 'Filter product forms', 'woocommerce' ),
'items_list_navigation' => __( 'Product forms navigation', 'woocommerce' ),
'items_list' => __( 'Product forms list', 'woocommerce' ),
'item_link' => __( 'Product form Link', 'woocommerce' ),
'item_link_description' => __( 'A link to a product form.', 'woocommerce' ),
),
'description' => __( 'This is where you can set up product forms for various product types in your dashboard.', 'woocommerce' ),
'public' => true,
'menu_icon' => 'dashicons-forms',
'capability_type' => 'product',
'map_meta_cap' => true,
'publicly_queryable' => true,
'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records!
'rewrite' => $permalinks['product_rewrite_slug'] ? array(
'slug' => $permalinks['product_rewrite_slug'],
'with_front' => false,
'feeds' => true,
) : false,
'query_var' => true,
'supports' => $supports,
'has_archive' => $has_archive,
'show_in_rest' => true,
'show_ui' => true,
'show_in_menu' => true,
'exclude_from_search' => true,
'show_in_nav_menus' => false,
)
)
);
}
register_post_type(
'product_variation',
apply_filters(

View File

@@ -601,7 +601,7 @@ class WC_Product_Variable extends WC_Product {
*/
/**
* Sync a variable product with it's children. These sync functions sync
* Sync a variable product with its children. These sync functions sync
* upwards (from child to parent) when the variation is saved.
*
* @param WC_Product|int $product Product object or ID for which you wish to sync.

View File

@@ -272,6 +272,12 @@ class WC_Regenerate_Images {
$imagedata['width'] = $imagedata['sizes']['full']['width'];
}
// The result of the earlier wp_get_attachment_metadata call is filterable, so we may not have height or
// width data at this point.
if ( ! isset( $imagedata['height'] ) || ! isset( $imagedata['width'] ) ) {
return array();
}
return array(
'width' => $imagedata['width'],
'height' => $imagedata['height'],

View File

@@ -34,6 +34,22 @@ class WC_REST_Authentication {
*/
protected $auth_method = '';
/**
* Provides access to the global WC_REST_Authentication instance.
*
* @internal
* @return self
*/
public static function instance(): self {
static $instance;
if ( ! isset( $instance ) ) {
$instance = new self();
}
return $instance;
}
/**
* Initialize authentication actions.
*/
@@ -582,11 +598,39 @@ class WC_REST_Authentication {
}
/**
* Updated API Key last access datetime.
* Updates the `last_access` field for the API key associated with the current request.
*
* This method tries to disambiguate 'primary' API requests from any programmatic REST
* API requests made internally.
*
* @param WP_REST_Request $request The request currently being processed.
*
* @return void
*/
private function update_last_access() {
private function update_last_access( $request ) {
global $wp;
global $wpdb;
// Lots of (programmatically) created REST API requests may be handled within the same process.
// In most cases, however, we do not want to record the last access time for each of these.
$do_not_record = true;
// Try to detect if the REST API request actively being processed matches the current WP request.
if ( is_a( $wp, WP::class ) && is_a( $request, WP_REST_Request::class ) ) {
$actual_http_request = trim( $wp->request, '/' );
$api_request_in_progress = trim( $request->get_route(), '/' );
// Remove the REST API route prefix (normally 'wp-json') for easier comparison.
$rest_prefix = trailingslashit( rest_get_url_prefix() );
if ( str_starts_with( $actual_http_request, $rest_prefix ) ) {
$actual_http_request = substr( $actual_http_request, strlen( $rest_prefix ) );
}
// Recommend recording the last access time only if the actual WP request and the current
// API request being processed are a match.
$do_not_record = $actual_http_request !== $api_request_in_progress;
}
/**
* This filter enables the exclusion of the most recent access time from being logged for REST API calls.
*
@@ -596,7 +640,7 @@ class WC_REST_Authentication {
*
* @since 7.7.0
*/
if ( apply_filters( 'woocommerce_disable_rest_api_access_log', false, $this->user->key_id, $this->user->user_id ) ) {
if ( apply_filters( 'woocommerce_disable_rest_api_access_log', $do_not_record, $this->user->key_id, $this->user->user_id ) ) {
return;
}
@@ -643,11 +687,11 @@ class WC_REST_Authentication {
}
// Register last access.
$this->update_last_access();
$this->update_last_access( $request );
}
return $result;
}
}
new WC_REST_Authentication();
WC_REST_Authentication::instance();

View File

@@ -1162,5 +1162,3 @@ class WC_Tracker {
return get_option( 'woocommerce_mobile_app_usage' );
}
}
WC_Tracker::init();

View File

@@ -533,7 +533,10 @@ class WC_Webhook extends WC_Legacy_Webhook {
// Check for a success, which is a 2xx, 301 or 302 Response Code.
if ( intval( $response_code ) >= 200 && intval( $response_code ) < 303 ) {
$this->set_failure_count( 0 );
$this->save();
if ( 0 !== $this->get_id() ) {
$this->save();
}
} else {
$this->failed_delivery();
}
@@ -557,7 +560,9 @@ class WC_Webhook extends WC_Legacy_Webhook {
$this->set_failure_count( ++$failures );
}
$this->save();
if ( 0 !== $this->get_id() ) {
$this->save();
}
}
/**

View File

@@ -45,7 +45,7 @@ final class WooCommerce {
*
* @var string
*/
public $version = '9.0.0';
public $version = '9.1.2';
/**
* WooCommerce Schema version.
@@ -682,6 +682,7 @@ final class WooCommerce {
if ( $this->is_request( 'cron' ) && 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) ) {
include_once WC_ABSPATH . 'includes/class-wc-tracker.php';
WC_Tracker::init();
}
$this->theme_support_includes();

View File

@@ -118,7 +118,8 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
throw new Exception( __( 'Invalid coupon.', 'woocommerce' ) );
}
$coupon_id = $coupon->get_id();
$coupon_id = $coupon->get_id();
$limit_usage_to_x_items = get_post_meta( $coupon_id, 'limit_usage_to_x_items', true );
$coupon->set_props(
array(
'code' => $post_object->post_title,
@@ -131,11 +132,11 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
'amount' => get_post_meta( $coupon_id, 'coupon_amount', true ),
'usage_count' => get_post_meta( $coupon_id, 'usage_count', true ),
'individual_use' => 'yes' === get_post_meta( $coupon_id, 'individual_use', true ),
'product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'product_ids', true ) ) ),
'excluded_product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'exclude_product_ids', true ) ) ),
'product_ids' => $this->get_coupon_meta_as_array( $coupon_id, 'product_ids' ),
'excluded_product_ids' => $this->get_coupon_meta_as_array( $coupon_id, 'exclude_product_ids' ),
'usage_limit' => get_post_meta( $coupon_id, 'usage_limit', true ),
'usage_limit_per_user' => get_post_meta( $coupon_id, 'usage_limit_per_user', true ),
'limit_usage_to_x_items' => 0 < get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) ? get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) : null,
'limit_usage_to_x_items' => $limit_usage_to_x_items > 0 ? $limit_usage_to_x_items : null,
'free_shipping' => 'yes' === get_post_meta( $coupon_id, 'free_shipping', true ),
'product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'product_categories', true ) ),
'excluded_product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'exclude_product_categories', true ) ),
@@ -151,6 +152,24 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
do_action( 'woocommerce_coupon_loaded', $coupon );
}
/**
* Get a metadata value that is stored as either a string consisting of a comma-separated list of values
* or as a serialized array.
*
* WooCommerce always stores the coupon product ids as a comma-separated string, but it seems that
* some plugins mistakenly change these to an array.
*
* @param int $coupon_id The coupon id.
* @param string $meta_key The meta key to get.
* @return array The metadata value as an array, with empty values removed.
*/
private function get_coupon_meta_as_array( $coupon_id, string $meta_key ) {
$meta_value = get_post_meta( $coupon_id, $meta_key, true );
return array_filter(
is_array( $meta_value ) ? $meta_value : explode( ',', $meta_value )
);
}
/**
* Updates a coupon in the database.
*
@@ -190,6 +209,12 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
$this->update_post_meta( $coupon );
$coupon->apply_changes();
delete_transient( 'rest_api_coupons_type_count' );
// The `coupon_id_from_code` entry in the object cache must not exist when the coupon is not published, otherwise the coupon will remain available for use.
if ( 'publish' !== $coupon->get_status() ) {
wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $coupon->get_code(), 'coupons' );
}
do_action( 'woocommerce_update_coupon', $coupon->get_id(), $coupon );
}
@@ -440,7 +465,7 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
*/
public function get_usage_by_user_id( &$coupon, $user_id ) {
global $wpdb;
$usage_count = $wpdb->get_var(
$usage_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %s;",
$coupon->get_id(),
@@ -461,7 +486,7 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
*/
public function get_usage_by_email( &$coupon, $email ) {
global $wpdb;
$usage_count = $wpdb->get_var(
$usage_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %s;",
$coupon->get_id(),
@@ -485,7 +510,6 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
return $wpdb->get_var(
$this->get_tentative_usage_query_for_user( $coupon_id, $user_aliases )
); // WPCS: unprepared SQL ok.
}
/**
@@ -637,8 +661,8 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
$query_for_tentative_usages = $this->get_tentative_usage_query_for_user( $coupon->get_id(), $user_aliases );
$db_timestamp = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM ' . $wpdb->posts . ' LIMIT 1' );
$coupon_used_by_meta_key = '_maybe_used_by_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false );
$insert_statement = $wpdb->prepare(
$coupon_used_by_meta_key = '_maybe_used_by_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false );
$insert_statement = $wpdb->prepare(
"
INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value )
SELECT %d, %s, %s FROM $wpdb->posts

View File

@@ -7,6 +7,7 @@
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Internal\Utilities\Users;
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -340,17 +341,36 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
* @return WC_Order|false
*/
public function get_last_order( &$customer ) {
//phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
// Try to fetch the last order placed by this customer.
$last_order_id = Users::get_site_user_meta( $customer->get_id(), 'wc_last_order', true );
$last_customer_order = false;
if ( ! empty( $last_order_id ) ) {
$last_customer_order = wc_get_order( $last_order_id );
}
// "Unset" the last order ID if the order is associated with another customer. Unsetting is done by making it an
// empty string, for compatibility with the declared types of the following filter hook.
if (
! $last_customer_order instanceof WC_Order
|| intval( $last_customer_order->get_customer_id() ) !== intval( $customer->get_id() )
) {
$last_order_id = '';
}
/**
* Filters the id of the last order from a given customer.
*
* @param string @last_order_id The last order id as retrieved from the database.
* @param WC_Customer The customer whose last order id is being retrieved.
* @since 4.9.1
*
* @param string $last_order_id The last order id as retrieved from the database.
* @param WC_Customer $customer The customer whose last order id is being retrieved.
*
* @return string The actual last order id to use.
*/
$last_order_id = apply_filters(
'woocommerce_customer_get_last_order',
get_user_meta( $customer->get_id(), '_last_order', true ),
$last_order_id,
$customer
);
//phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment
@@ -385,7 +405,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
);
}
//phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
update_user_meta( $customer->get_id(), '_last_order', $last_order_id );
Users::update_site_user_meta( $customer->get_id(), 'wc_last_order', $last_order_id );
}
if ( ! $last_order_id ) {
@@ -405,7 +425,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
public function get_order_count( &$customer ) {
$count = apply_filters(
'woocommerce_customer_get_order_count',
get_user_meta( $customer->get_id(), '_order_count', true ),
Users::get_site_user_meta( $customer->get_id(), 'wc_order_count', true ),
$customer
);
@@ -436,7 +456,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
}
//phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
update_user_meta( $customer->get_id(), '_order_count', $count );
Users::update_site_user_meta( $customer->get_id(), 'wc_order_count', $count );
}
return absint( $count );
@@ -452,7 +472,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
public function get_total_spent( &$customer ) {
$spent = apply_filters(
'woocommerce_customer_get_total_spent',
get_user_meta( $customer->get_id(), '_money_spent', true ),
Users::get_site_user_meta( $customer->get_id(), 'wc_money_spent', true ),
$customer
);
@@ -499,7 +519,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
if ( ! $spent ) {
$spent = 0;
}
update_user_meta( $customer->get_id(), '_money_spent', $spent );
Users::update_site_user_meta( $customer->get_id(), 'wc_money_spent', $spent );
}
return wc_format_decimal( $spent, 2 );

View File

@@ -138,6 +138,12 @@ 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 );
// get the post object so that we can set the status
// to the correct value; it is possible that the status was
// changed by the woocommerce_new_product_data filter above.
$post_object = get_post( $product->get_id() );
$product->set_status( $post_object->post_status );
$this->update_post_meta( $product, true );
$this->update_terms( $product, true );
$this->update_visibility( $product, true );
@@ -272,7 +278,8 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
$product->apply_changes();
do_action( 'woocommerce_update_product', $product->get_id(), $product );
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
do_action( 'woocommerce_update_product', $product->get_id(), $product, $changes );
}
/**
@@ -1236,7 +1243,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
do_action( 'product_variation_linked', $variation_id );
$count ++;
++$count;
if ( $limit > 0 && $count >= $limit ) {
break;

View File

@@ -166,23 +166,6 @@ if ( ! class_exists( 'WC_Email_New_Order' ) ) :
return __( 'Congratulations on the sale.', 'woocommerce' );
}
/**
* Return content from the additional_content field.
*
* Displayed above the footer.
*
* @since 3.7.0
* @return string
*/
public function get_additional_content() {
/**
* This filter is documented in ./class-wc-email.php
*
* @since 7.8.0
*/
return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $this->get_option( 'additional_content' ) ), $this->object, $this );
}
/**
* Initialise settings form fields.
*/

View File

@@ -416,7 +416,7 @@ class WC_Email extends WC_Settings_API {
* @param object|bool $object The object (ie, product or order) this email relates to, if any.
* @param WC_Email $email WC_Email instance managing the email.
*/
return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $this->get_option( 'additional_content', $this->get_default_additional_content() ) ), $this->object, $this );
return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $this->get_option( 'additional_content' ) ), $this->object, $this );
}
/**
@@ -602,9 +602,23 @@ class WC_Email extends WC_Settings_API {
*/
public function style_inline( $content ) {
if ( in_array( $this->get_content_type(), array( 'text/html', 'multipart/alternative' ), true ) ) {
$css = '';
$css .= $this->get_must_use_css_styles();
$css .= "\n";
ob_start();
wc_get_template( 'emails/email-styles.php' );
$css = apply_filters( 'woocommerce_email_styles', ob_get_clean(), $this );
$css .= ob_get_clean();
/**
* Provides an opportunity to filter the CSS styles included in e-mails.
*
* @since 2.3.0
*
* @param string $css CSS code.
* @param \WC_Email $email E-mail instance.
*/
$css = apply_filters( 'woocommerce_email_styles', $css, $this );
$css_inliner_class = CssInliner::class;
@@ -632,6 +646,29 @@ class WC_Email extends WC_Settings_API {
return $content;
}
/**
* Returns CSS styles that should be included with all HTML e-mails, regardless of theme specific customizations.
*
* @since 9.1.0
*
* @return string
*/
protected function get_must_use_css_styles(): string {
$css = <<<'EOF'
/*
* Temporary measure until e-mail clients more properly support the correct styles.
* See https://github.com/woocommerce/woocommerce/pull/47738.
*/
.screen-reader-text {
display: none;
}
EOF;
return $css;
}
/**
* Return if emogrifier library is supported.
*

View File

@@ -7,6 +7,7 @@ if ( ! function_exists( 'wc_admin_get_feature_config' ) ) {
'activity-panels' => true,
'analytics' => true,
'product-block-editor' => true,
'experimental-blocks' => false,
'coupons' => true,
'core-profiler' => true,
'customize-store' => true,
@@ -20,7 +21,6 @@ if ( ! function_exists( 'wc_admin_get_feature_config' ) ) {
'minified-js' => false,
'mobile-app-banner' => true,
'navigation' => true,
'new-product-management-experience' => false,
'onboarding' => true,
'onboarding-tasks' => true,
'pattern-toolkit-full-composability' => false,
@@ -29,6 +29,7 @@ if ( ! function_exists( 'wc_admin_get_feature_config' ) ) {
'remote-inbox-notifications' => true,
'remote-free-extensions' => true,
'payment-gateway-suggestions' => true,
'printful' => false,
'settings' => false,
'shipping-label-banner' => true,
'subscriptions' => true,
@@ -38,7 +39,7 @@ if ( ! function_exists( 'wc_admin_get_feature_config' ) ) {
'wc-pay-promotion' => true,
'wc-pay-welcome-page' => true,
'async-product-editor-category-field' => false,
'launch-your-store' => false,
'launch-your-store' => true,
'product-editor-template-system' => false,
);
}

View File

@@ -64,13 +64,14 @@ class WC_REST_Customer_Downloads_V1_Controller extends WC_REST_Controller {
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
$customer = get_user_by( 'id', (int) $request['customer_id'] );
$customer = new WC_Customer( (int) $request['customer_id'] );
$customer_id = $customer->get_id();
if ( ! $customer ) {
if ( ! $customer_id ) {
return new WP_Error( 'woocommerce_rest_customer_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
if ( ! wc_rest_check_user_permissions( 'read', $customer->get_id() ) ) {
if ( ! wc_rest_check_user_permissions( 'read', $customer_id ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}

View File

@@ -1951,7 +1951,7 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller {
),
'stock_quantity' => array(
'description' => __( 'Stock quantity.', 'woocommerce' ),
'type' => 'integer',
'type' => has_filter( 'woocommerce_stock_amount', 'intval' ) ? 'integer' : 'number',
'context' => array( 'view', 'edit' ),
),
'in_stock' => array(

View File

@@ -586,7 +586,7 @@ class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {
),
'rate_id' => array(
'description' => __( 'Tax rate ID.', 'woocommerce' ),
'type' => 'string',
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),

View File

@@ -930,6 +930,11 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
$this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted );
$this->maybe_set_item_meta_data( $item, $posted );
if ( 'update' === $action ) {
require_once WC_ABSPATH . 'includes/admin/wc-admin-functions.php';
wc_maybe_adjust_line_item_product_stock( $item );
}
return $item;
}
@@ -1619,7 +1624,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
),
'rate_id' => array(
'description' => __( 'Tax rate ID.', 'woocommerce' ),
'type' => 'string',
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),

View File

@@ -462,8 +462,12 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
protected function get_attribute_taxonomy_name( $slug, $product ) {
// Format slug so it matches attributes of the product.
$slug = wc_attribute_taxonomy_slug( $slug );
$attributes = $product->get_attributes();
$attribute = false;
$attributes = array_combine(
array_map( 'wc_sanitize_taxonomy_name', array_keys( $product->get_attributes() ) ),
array_values( $product->get_attributes() )
);
$attribute = false;
// pa_ attributes.
if ( isset( $attributes[ wc_attribute_taxonomy_name( $slug ) ] ) ) {
@@ -1904,7 +1908,7 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
),
'stock_quantity' => array(
'description' => __( 'Stock quantity.', 'woocommerce' ),
'type' => 'integer',
'type' => has_filter( 'woocommerce_stock_amount', 'intval' ) ? 'integer' : 'number',
'context' => array( 'view', 'edit' ),
),
'in_stock' => array(

View File

@@ -231,13 +231,16 @@ class WC_REST_Shipping_Zone_Methods_V2_Controller extends WC_REST_Shipping_Zones
}
/**
* Fires after a product review is deleted via the REST API.
* Fires after a shipping zone is deleted via the REST API.
*
* @param object $method
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
* @since 9.1.0
*
* @param WC_Shipping_Method $method The shipping zone method being deleted.
* @param WC_Shipping_Zone $zone The shipping zone the method belonged to.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'rest_delete_product_review', $method, $response, $request );
do_action( 'woocommerce_rest_delete_shipping_zone_method', $method, $zone, $response, $request );
return $response;
}

View File

@@ -1407,36 +1407,39 @@ class WC_REST_System_Status_V2_Controller extends WC_REST_Controller {
$shortcode_required = false;
$block_present = false;
$block_required = false;
$page = false;
// Page checks.
if ( $page_id ) {
$page_set = true;
}
if ( get_post( $page_id ) ) {
$page_exists = true;
}
if ( 'publish' === get_post_status( $page_id ) ) {
$page_visible = true;
$page = get_post( $page_id );
if ( $page ) {
$page_exists = true;
if ( 'publish' === $page->post_status ) {
$page_visible = true;
}
}
}
// Shortcode checks.
if ( $values['shortcode'] && get_post( $page_id ) ) {
if ( $values['shortcode'] && $page ) {
$shortcode_required = true;
$page = get_post( $page_id );
if ( strstr( $page->post_content, $values['shortcode'] ) ) {
if ( has_shortcode( $page->post_content, trim( $values['shortcode'], '[]' ) ) ) {
$shortcode_present = true;
}
// Compatibility with the classic shortcode block which can be used instead of shortcodes.
if ( ! $shortcode_present && ( 'woocommerce/checkout' === $values['block'] || 'woocommerce/cart' === $values['block'] ) ) {
$shortcode_present = has_block( 'woocommerce/classic-shortcode', $page->post_content );
}
}
// Block checks.
if ( $values['block'] && get_post( $page_id ) ) {
if ( $values['block'] && $page ) {
$block_required = true;
$block_present = WC_Blocks_Utils::has_block_in_page( $page_id, $values['block'] );
// Compatibility with the classic shortcode block which can be used instead of shortcodes.
if ( ! $block_present && ( 'woocommerce/checkout' === $values['block'] || 'woocommerce/cart' === $values['block'] ) ) {
$block_present = WC_Blocks_Utils::has_block_in_page( $page_id, 'woocommerce/classic-shortcode', true );
}
$block_present = has_block( $values['block'], $page->post_content );
}
// Wrap up our findings into an output array.

View File

@@ -24,4 +24,61 @@ class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Product_Shippi
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Register the routes for product reviews.
*/
public function register_routes() {
parent::register_routes();
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/slug-suggestion',
array(
'args' => array(
'name' => array(
'description' => __( 'Suggest a slug for the term.', 'woocommerce' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'suggest_slug' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Callback fuction for the slug-suggestion endpoint.
*
* @param WP_REST_Request $request Full details about the request.
* @return string The suggested slug.
*/
public function suggest_slug( $request ) {
$name = $request['name'];
$slug = sanitize_title( $name ); // potential slug.
$term = get_term_by( 'slug', $slug, $this->taxonomy );
/*
* If the term exists, creates a unique slug
* based on the name provided.
* Otherwise, returns the sanitized name.
*/
if ( isset( $term->slug ) ) {
/*
* Pass a Term object that has only the taxonomy property,
* to induce the wp_unique_term_slug() function to generate a unique slug.
* Otherwise, the function will return the same slug.
* @see https://core.trac.wordpress.org/browser/tags/6.5/src/wp-includes/taxonomy.php#L3130
* @see https://github.com/WordPress/wordpress-develop/blob/a1b1e0339eb6dfa72a30933cac2a1c6ad2bbfe96/src/wp-includes/taxonomy.php#L3078-L3156
*/
$slug = wp_unique_term_slug( $slug, (object) array( 'taxonomy' => $this->taxonomy ) );
}
return rest_ensure_response( $slug );
}
}

View File

@@ -1131,7 +1131,7 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
),
'stock_quantity' => array(
'description' => __( 'Stock quantity.', 'woocommerce' ),
'type' => 'integer',
'type' => has_filter( 'woocommerce_stock_amount', 'intval' ) ? 'integer' : 'number',
'context' => array( 'view', 'edit' ),
),
'stock_status' => array(

View File

@@ -108,9 +108,6 @@ class WC_Shipping_Flat_Rate extends WC_Shipping_Method {
// Remove whitespace from string.
$sum = preg_replace( '/\s+/', '', $sum );
// Removed thousand separator.
$sum = str_replace( wc_get_price_thousand_separator(), '', $sum );
// Remove locale from string.
$sum = str_replace( $decimals, '.', $sum );

View File

@@ -29,7 +29,8 @@ class WC_Products_Tracking {
add_action( 'load-edit.php', array( $this, 'track_products_view' ), 10 );
add_action( 'load-edit-tags.php', array( $this, 'track_categories_and_tags_view' ), 10, 2 );
add_action( 'edit_post', array( $this, 'track_product_updated' ), 10, 2 );
add_action( 'wp_after_insert_post', array( $this, 'track_product_published' ), 10, 4 );
add_action( 'woocommerce_new_product', array( $this, 'track_product_published' ), 10, 3 );
add_action( 'woocommerce_update_product', array( $this, 'track_product_published' ), 10, 3 );
add_action( 'created_product_cat', array( $this, 'track_product_category_created' ) );
add_action( 'edited_product_cat', array( $this, 'track_product_category_updated' ) );
add_action( 'add_meta_boxes_product', array( $this, 'track_product_updated_client_side' ), 10 );
@@ -125,7 +126,7 @@ class WC_Products_Tracking {
}
/* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */
$source = apply_filters( 'woocommerce_product_source', self::TRACKS_SOURCE );
$source = apply_filters( 'woocommerce_product_source', self::is_importing() ? 'import' : self::TRACKS_SOURCE );
$properties = array(
'product_id' => $product_id,
'source' => $source,
@@ -303,24 +304,21 @@ class WC_Products_Tracking {
/**
* Send a Tracks event when a product is published.
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
* @param bool $update Whether this is an existing post being updated.
* @param null|WP_Post $post_before Null for new posts, the WP_Post object prior
* to the update for updated posts.
* @param int $product_id Product ID.
* @param WC_Product $product Product object.
* @param array $changes Product changes.
*/
public function track_product_published( $post_id, $post, $update, $post_before ) {
public function track_product_published( $product_id, $product, $changes = null ) {
if (
'product' !== $post->post_type ||
'publish' !== $post->post_status ||
( $post_before && 'publish' === $post_before->post_status )
! isset( $product ) ||
'product' !== $product->post_type ||
'publish' !== $product->get_status( 'edit' ) ||
( $changes && ! isset( $changes['status'] ) )
) {
return;
}
$product = wc_get_product( $post_id );
$product_type_options = self::get_product_type_options( $post_id );
$product_type_options = self::get_product_type_options( $product_id );
$product_type_options_string = self::get_product_type_options_string( $product_type_options );
$properties = array(
@@ -334,14 +332,14 @@ class WC_Products_Tracking {
'is_virtual' => $product->is_virtual() ? 'yes' : 'no',
'manage_stock' => $product->get_manage_stock() ? 'yes' : 'no',
'menu_order' => $product->get_menu_order() ? 'yes' : 'no',
'product_id' => $post_id,
'product_id' => $product_id,
'product_gallery' => count( $product->get_gallery_image_ids() ),
'product_image' => $product->get_image_id() ? 'yes' : 'no',
'product_type' => $product->get_type(),
'product_type_options' => $product_type_options_string,
'purchase_note' => $product->get_purchase_note() ? 'yes' : 'no',
'sale_price' => $product->get_sale_price() ? 'yes' : 'no',
'source' => apply_filters( 'woocommerce_product_source', self::TRACKS_SOURCE ),
'source' => apply_filters( 'woocommerce_product_source', self::is_importing() ? 'import' : self::TRACKS_SOURCE ),
'short_description' => $product->get_short_description() ? 'yes' : 'no',
'tags' => count( $product->get_tag_ids() ),
'upsells' => ! empty( $product->get_upsell_ids() ) ? 'yes' : 'no',
@@ -546,4 +544,19 @@ class WC_Products_Tracking {
}
WCAdminAssets::register_script( 'wp-admin-scripts', 'add-term-tracking', false );
}
/**
* Check if the current process is importing products.
*
* @return bool True if importing, false otherwise.
*/
private function is_importing() {
// phpcs:disable WordPress.Security.NonceVerification.Missing
// Check if the current request is a product import.
if ( isset( $_POST['action'] ) && 'woocommerce_do_ajax_product_import' === $_POST['action'] ) {
return true;
}
return false;
// phpcs:enable
}
}

View File

@@ -633,6 +633,16 @@ function wc_update_attribute( $id, $args ) {
$args['id'] = $attribute ? $attribute->id : 0;
// When updating an existing attribute, populate any undefined args with the existing value.
// This prevents those values from being reset to their respective defaults.
if ( $args['id'] ) {
$args['has_archives'] = $args['has_archives'] ?? $attribute->has_archives;
$args['name'] = $args['name'] ?? $attribute->name;
$args['order_by'] = $args['order_by'] ?? $attribute->order_by;
$args['slug'] = $args['slug'] ?? $attribute->slug;
$args['type'] = $args['type'] ?? $attribute->type;
}
if ( $args['id'] && empty( $args['name'] ) ) {
$args['name'] = $attribute->name;
}

View File

@@ -460,7 +460,7 @@ function wc_get_default_shipping_method_for_package( $key, $package, $chosen_met
* If the customer has selected local pickup, keep it selected if it's still in the package. We don't want to auto
* toggle between shipping and pickup even if available shipping methods are changed.
*
* This is important for block based checkout where there is an explicit toggle between shipping and pickup.
* This is important for block-based checkout where there is an explicit toggle between shipping and pickup.
*/
$local_pickup_method_ids = LocalPickupUtils::get_local_pickup_method_ids();
$is_local_pickup_chosen = in_array( $chosen_method_id, $local_pickup_method_ids, true );

View File

@@ -9,6 +9,7 @@
*/
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\Utilities\Users;
use Automattic\WooCommerce\Utilities\StringUtil;
defined( 'ABSPATH' ) || exit;
@@ -484,9 +485,9 @@ function wc_delete_shop_order_transients( $order = 0 ) {
// Clear customer's order related caches.
if ( is_a( $order, 'WC_Order' ) ) {
$order_id = $order->get_id();
delete_user_meta( $order->get_customer_id(), '_money_spent' );
delete_user_meta( $order->get_customer_id(), '_order_count' );
delete_user_meta( $order->get_customer_id(), '_last_order' );
Users::delete_site_user_meta( $order->get_customer_id(), 'wc_money_spent' );
Users::delete_site_user_meta( $order->get_customer_id(), 'wc_order_count' );
Users::delete_site_user_meta( $order->get_customer_id(), 'wc_last_order' );
} else {
$order_id = 0;
}
@@ -1063,6 +1064,7 @@ function wc_get_order_note( $data ) {
'content' => $data->comment_content,
'customer_note' => (bool) get_comment_meta( $data->comment_ID, 'is_customer_note', true ),
'added_by' => __( 'WooCommerce', 'woocommerce' ) === $data->comment_author ? 'system' : $data->comment_author,
'order_id' => absint( $data->comment_post_ID ),
),
$data
);
@@ -1183,5 +1185,20 @@ function wc_create_order_note( $order_id, $note, $is_customer_note = false, $add
* @return bool True on success, false on failure.
*/
function wc_delete_order_note( $note_id ) {
return wp_delete_comment( $note_id, true );
$note = wc_get_order_note( $note_id );
if ( $note && wp_delete_comment( $note_id, true ) ) {
/**
* Action hook fired after an order note is deleted.
*
* @param int $note_id Order note ID.
* @param stdClass $note Object with the deleted order note details.
*
* @since 9.1.0
*/
do_action( 'woocommerce_order_note_deleted', $note_id, $note );
return true;
}
return false;
}

View File

@@ -296,6 +296,7 @@ function wc_increase_stock_levels( $order_id ) {
$item_name = $product->get_formatted_name();
$new_stock = wc_update_product_stock( $product, $item_stock_reduced, 'increase' );
$old_stock = $new_stock - $item_stock_reduced;
if ( is_wp_error( $new_stock ) ) {
/* translators: %s item name. */
@@ -306,7 +307,18 @@ function wc_increase_stock_levels( $order_id ) {
$item->delete_meta_data( '_reduced_stock' );
$item->save();
$changes[] = $item_name . ' ' . ( $new_stock - $item_stock_reduced ) . '&rarr;' . $new_stock;
$changes[] = $item_name . ' ' . $old_stock . '&rarr;' . $new_stock;
/**
* Fires when stock restored to a specific line item
*
* @since 9.1.0
* @param WC_Order_Item_Product $item Order item data.
* @param int $new_stock New stock.
* @param int $old_stock Old stock.
* @param WC_Order $order Order data.
*/
do_action( 'woocommerce_restore_order_item_stock', $item, $new_stock, $old_stock, $order );
}
if ( $changes ) {

View File

@@ -3994,4 +3994,27 @@ function wc_get_pay_buttons() {
echo '</div>';
}
/**
* Update the product archive title to the title of the shop page. Fallback to
* 'Shop' if the shop page doesn't exist.
*
* @param string $post_type_name Post type 'name' label.
* @param string $post_type Post type.
*
* @return string
*/
function wc_update_product_archive_title( $post_type_name, $post_type ) {
if ( is_shop() && 'product' === $post_type ) {
$shop_page_title = get_the_title( wc_get_page_id( 'shop' ) );
if ( $shop_page_title ) {
return $shop_page_title;
}
return __( 'Shop', 'woocommerce' );
}
return $post_type_name;
}
add_filter( 'post_type_archive_title', 'wc_update_product_archive_title', 10, 2 );
// phpcs:enable Generic.Commenting.Todo.TaskFound

View File

@@ -2728,6 +2728,53 @@ function wc_update_891_create_plugin_autoinstall_history_option() {
/**
* Add woocommerce_show_lys_tour.
*/
function wc_update_900_add_launch_your_store_tour_option() {
function wc_update_910_add_launch_your_store_tour_option() {
add_option( 'woocommerce_show_lys_tour', 'yes' );
}
/**
* Remove user meta associated with the keys '_last_order', '_order_count' and '_money_spent'.
*
* New keys are now used for these, to improve compatibility with multisite networks.
*
* @return void
*/
function wc_update_910_remove_obsolete_user_meta() {
global $wpdb;
$deletions = $wpdb->query( "
DELETE FROM $wpdb->usermeta
WHERE meta_key IN (
'_last_order',
'_order_count',
'_money_spent'
)
" );
$logger = wc_get_logger();
if ( null === $logger ) {
return;
}
if ( false === $deletions ) {
$logger->notice(
'During the update to 9.1.0, WooCommerce attempted to remove user meta with the keys "_last_order", "_order_count" and "_money_spent" but was unable to do so.',
array(
'source' => 'wc-updater',
)
);
} else {
$logger->info(
sprintf(
1 === $deletions
? 'During the update to 9.1.0, WooCommerce removed %d user meta row associated with the meta keys "_last_order", "_order_count" or "_money_spent".'
: 'During the update to 9.1.0, WooCommerce removed %d user meta rows associated with the meta keys "_last_order", "_order_count" or "_money_spent".',
number_format_i18n( $deletions )
),
array(
'source' => 'wc-updater',
)
);
}
}

View File

@@ -9,6 +9,7 @@
*/
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Internal\Utilities\Users;
use Automattic\WooCommerce\Utilities\OrderUtil;
defined( 'ABSPATH' ) || exit;
@@ -280,9 +281,9 @@ function wc_update_new_customer_past_orders( $customer_id ) {
if ( $complete ) {
update_user_meta( $customer_id, 'paying_customer', 1 );
update_user_meta( $customer_id, '_order_count', '' );
update_user_meta( $customer_id, '_money_spent', '' );
delete_user_meta( $customer_id, '_last_order' );
Users::update_site_user_meta( $customer_id, 'wc_order_count', '' );
Users::update_site_user_meta( $customer_id, 'wc_money_spent', '' );
Users::delete_site_user_meta( $customer_id, 'wc_last_order' );
}
return $linked;
@@ -367,7 +368,7 @@ function wc_customer_bought_product( $customer_email, $user_id, $product_id ) {
$user_id_clause = 'OR o.customer_id = ' . absint( $user_id );
}
$sql = "
SELECT im.meta_value FROM $order_table AS o
SELECT DISTINCT im.meta_value FROM $order_table AS o
INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON o.id = i.order_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id
WHERE o.status IN ('" . implode( "','", $statuses ) . "')
@@ -379,7 +380,7 @@ AND ( o.billing_email IN ('" . implode( "','", $customer_data ) . "') $user_id_c
} else {
$result = $wpdb->get_col(
"
SELECT im.meta_value FROM {$wpdb->posts} AS p
SELECT DISTINCT im.meta_value FROM {$wpdb->posts} AS p
INNER JOIN {$wpdb->postmeta} AS pm ON p.ID = pm.post_id
INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON p.ID = i.order_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id

View File

@@ -75,6 +75,11 @@ add_action( 'woocommerce_webhook_process_delivery', 'wc_webhook_process_delivery
*/
function wc_deliver_webhook_async( $webhook_id, $arg ) {
$webhook = new WC_Webhook( $webhook_id );
if ( 0 === $webhook->get_id() ) {
return;
}
$webhook->deliver( $arg );
}
add_action( 'woocommerce_deliver_webhook_async', 'wc_deliver_webhook_async', 10, 2 );