Compare commits

...

2 Commits
v8.2.0 ... main

Author SHA1 Message Date
WooCommerce 56e03c0544 Updates to 8.3.0 2026-01-08 10:21:29 +00:00
WooCommerce bfe7fb609d Updates to 8.2.1 2025-12-24 10:19:37 +00:00
42 changed files with 10134 additions and 318 deletions

View File

@ -77,6 +77,14 @@ a.close-subscriptions-search {
display: block;
width: 50%;
}
.woocommerce_options_panel ._subscription_downloads_field .description,
.woocommerce_options_panel .subscription_linked_downloadable_products .description {
clear: both;
display: block;
margin: 0;
}
/* Variation Subscription Product Sync Settings */
.variable_subscription_sync .subscription_sync_annual > .select2-container {
max-width: 48%;
@ -385,6 +393,12 @@ p._subscription_gifting_field_description.form-field .description {
opacity: 0.5;
}
#woocommerce-product-data
.form-field._subscription_downloads_field
.select2-selection__choice {
font-size: 14px;
}
/* Variation Pricing Fields with WooCommerce 3.0+ */
.wc-metaboxes-wrapper .variable_subscription_trial label,
.wc-metaboxes-wrapper .variable_subscription_pricing label,

View File

@ -639,6 +639,53 @@ jQuery( function ( $ ) {
},
} );
/**
* Relocates the linked downloadable product fields in the context of a variable subscription product, and
* selectively shows/hides it, depending on whether the variation is downloadable or not.
*/
function initLinkedDownloadableProductFieldsForVariationProduct() {
$( '#variable_product_options .variable_subscription_linked_downloadable_products' )
.not( '.wcs_moved' )
.each( function () {
const $this = $( this );
const $variablePricing = $( this ).siblings( '.variable_pricing' );
const $downloadableFiles = $( this ).siblings( '.form-row.downloadable_files' ).parent( 'div' );
$this.insertAfter( $downloadableFiles );
$this.addClass( 'wcs_gifting_moved' );
} );
}
/**
* Move the linked downloadable product fields to the correct location (which is underneath the downloadable files
* section).
*/
function relocateLinkedDownloadableProductFields() {
const $linkedDownloadableProducts = $( '.subscription_linked_downloadable_products_section' );
const $productTypeSelector = $('select#product-type');
const showHide = () => {
$productTypeSelector.val() === WCSubscriptions.productType
? $linkedDownloadableProducts.show()
: $linkedDownloadableProducts.hide();
};
$linkedDownloadableProducts
.not( '.wcs_moved' )
.each( function () {
const $downloadableFilesSection = $( '.form-field.downloadable_files' ).parent( 'div' );
if ( $downloadableFilesSection.length === 1 ) {
$( this ).insertAfter( $downloadableFilesSection );
$( this ).addClass( 'wcs_moved' );
}
} );
showHide();
$productTypeSelector.on( 'change', showHide );
}
$( '.options_group.pricing ._sale_price_field .description' ).prepend(
'<span id="sale-price-period" style="display: none;"></span>'
);
@ -653,9 +700,9 @@ jQuery( function ( $ ) {
$( '.options_group.subscription_pricing' )
);
// Move the subscription variation pricing and gifting sections to a better location in the DOM on load
// We do this because these sections are initially loaded at the end of the variable product options section,
// which is not the best location for them, so we move to before the "Sale price" section.
// Move the subscription variation pricing, gifting and linked downloadable product sections to a better location in
// the DOM on load. We do this because these sections are initially loaded at the end of the variable product
// options section, which is not the best location for them, so we move to before the "Sale price" section.
if (
$( '#variable_product_options .variable_subscription_pricing' ).length >
0
@ -668,6 +715,7 @@ jQuery( function ( $ ) {
) {
$.moveSubscriptionGiftingFields();
}
// When a variation is added
$( '#woocommerce-product-data' ).on(
'woocommerce_variations_added woocommerce_variations_loaded',
@ -677,6 +725,7 @@ jQuery( function ( $ ) {
$.showHideVariableSubscriptionMeta();
$.showHideSyncOptions();
$.setSubscriptionLengths();
initLinkedDownloadableProductFieldsForVariationProduct();
}
);
@ -690,6 +739,7 @@ jQuery( function ( $ ) {
$.showHideSyncOptions();
$.disableEnableOneTimeShipping();
$.showHideSubscriptionsPanels();
relocateLinkedDownloadableProductFields();
}
// Update subscription ranges when subscription period or interval is changed

View File

@ -1,31 +0,0 @@
/* global wc_subscription_downloads_product */
jQuery( document ).ready( function ( $ ) {
function subscriptionDownloadsChosen() {
$( 'select.subscription-downloads-ids' ).ajaxChosen({
method: 'GET',
url: wc_subscription_downloads_product.ajax_url,
dataType: 'json',
afterTypeDelay: 100,
minTermLength: 1,
data: {
action: 'wc_subscription_downloads_search',
security: wc_subscription_downloads_product.security
}
}, function ( data ) {
var orders = {};
$.each( data, function ( i, val ) {
orders[i] = val;
});
return orders;
});
}
subscriptionDownloadsChosen();
$( 'body' ).on( 'woocommerce_variations_added', function () {
subscriptionDownloadsChosen();
});
});

View File

@ -18,7 +18,8 @@ jQuery( document ).ready( function ( $ ) {
.find( '.recipient_email' )
.trigger( 'focus' );
}
} );
} )
.removeClass( 'hidden' );
const shipToDifferentAddressCheckbox = $( document ).find(
'#ship-to-different-address-checkbox'
@ -31,12 +32,14 @@ jQuery( document ).ready( function ( $ ) {
$( this )
.closest( '.wcsg_add_recipient_fields_container' )
.find( '.wcsg_add_recipient_fields' )
.slideUp( 250 );
.slideUp( 250 )
.addClass( 'hidden' );
const recipientEmailElement = $( this )
.closest( '.wcsg_add_recipient_fields_container' )
.find( '.recipient_email' );
recipientEmailElement.val( '' );
hideValidationErrorForEmailField( recipientEmailElement );
setShippingAddressNoticeVisibility( true );
if ( $( 'form.checkout' ).length !== 0 ) {

1
build/admin.asset.php Normal file
View File

@ -0,0 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-i18n', 'wp-primitives'), 'version' => '8bad0ce18409676b15d1');

1
build/admin.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => 'b52cf873a54e347c8f9e');

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wc-blocks-checkout', 'wc-blocks-data-store', 'wc-price-format', 'wc-settings', 'wp-data', 'wp-element', 'wp-i18n', 'wp-plugins'), 'version' => '841d6ab2fe0080f15a21');
<?php return array('dependencies' => array('react', 'wc-blocks-checkout', 'wc-blocks-data-store', 'wc-price-format', 'wc-settings', 'wp-data', 'wp-element', 'wp-i18n', 'wp-plugins'), 'version' => '32bfa486d902830f29c7');

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'wc-blocks-checkout', 'wp-components', 'wp-data', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => 'e7869a0a766c0f65ff8a');
<?php return array('dependencies' => array('react', 'react-dom', 'wc-blocks-checkout', 'wp-components', 'wp-data', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => 'cf4dd71c0c7418b591fd');

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,18 @@
*** WooCommerce Subscriptions Changelog ***
2026-01-06 - version 8.3.0
* Add: Support for ajax add-to-cart flows from the single product page, when upgrading or downgrading subscriptions.
* Add: Enhanced functionality from the WooCommerce Subscription Downloads plugin has now been integrated directly into WooCommerce Subscriptions.
* Fix: Ensured the subject line for the "Customer Renewal Invoice" email correctly updates.
2025-12-22 - version 8.2.1
* Fix: Subscription pricing strings now correctly display singular and plural billing intervals like "$10 / month" and "$10 every 3 months" in cart and checkout totals.
* Fix: Display all recurring coupon names in the totals section of classic cart and checkout when multiple recurring coupons are applied.
* Fix: Payment method selector options on Subscriptions list page are now properly displaying the payment method name for gateways providing multiple payment methods.
* Fix: PayPal Standard plugin IPN renewal orders not being created for failed payment retries.
* Fix: Prevent fatal errors on shop pages when special characters like % are used in a product name.
* Fix: Prevent an email validation error from briefly and inadvertently flashing on the screen when the "this is a gift" checkbox is used.
2025-12-10 - version 8.2.0
* Fix: Various accessibility issues.
* Added button roles.

View File

@ -0,0 +1,59 @@
<?php
/**
* WCS_Admin_Assets Class
*
* Handles admin assets (scripts and styles) for WooCommerce Subscriptions.
*
* @package WooCommerce Subscriptions/Admin
*/
class WCS_Admin_Assets {
/**
* Initialize the tour handler
*/
public static function init() {
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
}
/**
* Enqueue required scripts and styles
*/
public static function enqueue_scripts() {
$script_asset_path = \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/admin.asset.php' );
$script_asset = file_exists( $script_asset_path )
? require $script_asset_path
: array(
'dependencies' => array(
'react',
'wc-blocks-checkout',
'wc-price-format',
'wc-settings',
'wp-element',
'wp-i18n',
'wp-plugins',
),
'version' => WC_Subscriptions::$version,
);
wp_enqueue_script(
'wcs-admin',
plugins_url( '/build/admin.js', WC_Subscriptions::$plugin_file ),
$script_asset['dependencies'],
$script_asset['version'],
true
);
wp_enqueue_style(
'wcs-admin',
plugins_url( '/build/style-admin.css', WC_Subscriptions::$plugin_file ),
array( 'wp-components' ),
$script_asset['version']
);
wp_set_script_translations(
'wcs-admin',
'woocommerce-subscriptions',
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'languages'
);
}
}

View File

@ -24,6 +24,7 @@ class WC_REST_Subscriptions_Settings_Option_Controller extends WP_REST_Controlle
*/
private const ALLOWED_OPTIONS = [
'woocommerce_subscriptions_gifting_is_welcome_announcement_dismissed',
'woocommerce_subscriptions_downloads_is_welcome_announcement_dismissed',
];
/**

View File

@ -34,6 +34,7 @@ class WC_Subscriptions_Plugin extends WC_Subscriptions_Core_Plugin {
WCS_Call_To_Action_Button_Text_Manager::init();
WCS_Subscriber_Role_Manager::init();
WCS_Upgrade_Notice_Manager::init();
WCS_Admin_Assets::init();
$tracks_events = new WC_Tracks_Events();
$tracks_events->setup();
@ -300,10 +301,16 @@ class WC_Subscriptions_Plugin extends WC_Subscriptions_Core_Plugin {
*/
public function init_downloads() {
if (
! defined( 'WCS_ALLOW_SUBSCRIPTION_DOWNLOADS' )
|| $this->is_plugin_being_activated( 'woocommerce-subscription-downloads' )
$this->is_plugin_being_activated( 'woocommerce-subscription-downloads' )
|| class_exists( WC_Subscription_Downloads::class, false )
) {
if ( class_exists( WC_Subscription_Downloads::class, false ) ) {
// Will show the welcome announcement if the standalone plugin is active and the welcome announcement has not been dismissed.
if ( ! WC_Subscription_Downloads_Admin_Welcome_Announcement::is_welcome_announcement_dismissed() ) {
WC_Subscription_Downloads_Admin_Welcome_Announcement::init();
}
}
return;
}

View File

@ -45,6 +45,7 @@ class WCS_Autoloader extends WCS_Core_Autoloader {
'wcs_webhooks' => true,
'wcs_auth' => true,
'wcs_upgrade_notice_manager' => true,
'wcs_admin_assets' => true,
'wc_subscriptions_cli' => true,
);

View File

@ -1171,7 +1171,7 @@ class WCS_Admin_Post_Types {
// @phpstan-ignore property.notFound
foreach ( WC()->payment_gateways->get_available_payment_gateways() as $gateway_id => $gateway ) {
echo '<option value="' . esc_attr( $gateway_id ) . '"' . ( $selected_gateway_id === $gateway_id ? 'selected' : '' ) . '>' . esc_html( $gateway->title ) . '</option>';
echo '<option value="' . esc_attr( $gateway_id ) . '"' . ( $selected_gateway_id === $gateway_id ? 'selected' : '' ) . '>' . esc_html( method_exists( $gateway, 'get_title' ) ? $gateway->get_title() : $gateway->title ) . '</option>';
}
echo '<option value="_manual_renewal">' . esc_html__( 'Manual Renewal', 'woocommerce-subscriptions' ) . '</option>';
?>

View File

@ -99,7 +99,7 @@ class WC_Product_Subscription extends WC_Product_Simple {
);
}
return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( $text, $this->get_name() ), $this );
return apply_filters( 'woocommerce_product_add_to_cart_description', $text, $this );
}
/**

View File

@ -383,17 +383,10 @@ class WC_Subscriptions_Product {
}
break;
}
} elseif ( 1 === $billing_interval ) {
$subscription_string = sprintf(
// translators: 1$: recurring amount, 2$: subscription period (e.g. "month") (e.g. "$15 / month").
__( '%1$s / %2$s', 'woocommerce-subscriptions' ),
$price,
wcs_get_subscription_period_strings( $billing_interval, $billing_period )
);
} else {
$subscription_string = sprintf(
// translators: 1$: recurring amount, 2$: subscription period (e.g. "3 months") (e.g. "$15 every 2nd month").
__( '%1$s every %2$s', 'woocommerce-subscriptions' ),
// translators: 1$: recurring amount, 2$: subscription period (e.g. "month" or "3 months") (e.g. "$15 / month" or "$15 every 2nd month").
_n( '%1$s / %2$s', '%1$s every %2$s', $billing_interval, 'woocommerce-subscriptions' ),
$price,
wcs_get_subscription_period_strings( $billing_interval, $billing_period )
);

View File

@ -57,6 +57,43 @@ class WCS_Limiter {
do_action( 'woocommerce_subscriptions_product_options_advanced' );
}
/**
* Checks if the session contains a renewal for a given product.
* Used for the pay for order flow.
*
* @param WC_Product $product The product to check.
* @return bool
*/
private static function session_contains_renewal( $product ) {
if ( ! empty( WC()->session->cart ) ) {
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
if ( (int) $product->get_id() === (int) $cart_item['product_id'] && isset( $cart_item['subscription_renewal'] ) ) {
return true;
}
}
}
return false;
}
/**
* Checks if the session contains a resubscribe for a given product.
* Used for the pay for order flow with limited subscriptions products.
*
* @param WC_Product $product The product to check.
* @return bool
*/
private static function session_contains_resubscribe( $product ) {
if ( ! empty( WC()->session->cart ) ) {
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
if ( (int) $product->get_id() === (int) $cart_item['product_id'] && isset( $cart_item['subscription_resubscribe'] ) ) {
return true;
}
}
}
return false;
}
/**
* Canonical is_purchasable method to be called by product classes.
*
@ -68,73 +105,62 @@ class WCS_Limiter {
*/
public static function is_purchasable( $purchasable, $product ) {
// Prevents making a non purchasable product purchasable again.
// This can happen if the product is disabled and limited and the customer is trying to renew the subscription for example.
if ( ! $purchasable ) {
return $purchasable;
// Check if product is private (for variations, also check parent product)
$is_private_product = 'private' === $product->get_status();
if ( $product->get_parent_id() > 0 ) {
$parent_product = wc_get_product( $product->get_parent_id() );
if ( $parent_product ) {
$is_private_product = 'private' === $parent_product->get_status();
}
}
switch ( $product->get_type() ) {
case 'subscription':
case 'variable-subscription':
// Checks if the product is limited.
if ( false === self::is_product_limited( $purchasable, $product ) ) {
// Product is limited, so it is not purchasable.
// Checks limits for variable subscription products.
if ( $product->get_type() === 'subscription_variation' && isset( $parent_product ) ) {
if ( $is_private_product ) {
$purchasable = false;
// Forces product to be available when processing a renewal order. Allowing people to renew private products.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce validation is not required for this context.
if ( self::is_paying_for_failed_renewal_order( $parent_product ) || isset( $_GET['subscription_renewal'] ) || wcs_cart_contains_renewal() || self::session_contains_renewal( $parent_product ) ) {
$purchasable = true;
}
}
if ( 'no' !== wcs_get_product_limitation( $parent_product ) && ( ! empty( WC()->cart->cart_contents ) || self::session_contains_resubscribe( $parent_product ) ) && ! wcs_is_order_received_page() && ! wcs_is_paypal_api_page() ) {
// When mixed checkout is disabled, the variation is replaceable.
if ( 'yes' === get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase', 'no' ) ) {
foreach ( WC()->cart->cart_contents as $cart_item ) {
// If the variable product is limited, it can't be purchased if it is the same variation
if ( $product->get_parent_id() === $cart_item['data']->get_parent_id() && $product->get_id() !== $cart_item['data']->get_id() ) {
$purchasable = false;
break;
}
}
}
}
} else { // Checks limits for simple subscription products.
if ( $is_private_product ) {
$purchasable = false;
// Forces product to be available when processing a renewal order. Allowing people to renew private products.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce validation is not required for this context.
if ( self::is_paying_for_failed_renewal_order( $product ) || isset( $_GET['subscription_renewal'] ) || wcs_cart_contains_renewal() || self::session_contains_renewal( $product ) ) {
$purchasable = true;
}
}
// This actually means the product is limited when returning false.
if ( false === self::is_product_limited( $purchasable, $product ) ) {
$resubscribe_cart_item = wcs_cart_contains_resubscribe();
// Allows the product to be resubscribed but not purchased again.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce validation is not required for this context.
if ( empty( $_GET['resubscribe'] ) && false === $resubscribe_cart_item && false === self::session_contains_resubscribe( $product ) ) {
$purchasable = false;
// Unless it's resubscribing, renewing or restoring cart from session.
$resubscribe_cart_item = wcs_cart_contains_resubscribe();
// Resubscribe logic
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_GET['resubscribe'] ) || false !== $resubscribe_cart_item ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$subscription_id = ( isset( $_GET['resubscribe'] ) ) ? absint( $_GET['resubscribe'] ) : $resubscribe_cart_item['subscription_resubscribe']['subscription_id'];
$subscription = wcs_get_subscription( $subscription_id );
if ( $subscription && $subscription->has_product( $product->get_id() ) && wcs_can_user_resubscribe_to( $subscription ) ) {
$purchasable = true;
}
// Renewal logic
} elseif (
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
isset( $_GET['subscription_renewal'] ) ||
wcs_cart_contains_renewal()
) {
$purchasable = true;
// Restoring cart from session, so need to check the cart in the session (wcs_cart_contains_renewal() only checks the cart).
} elseif ( ! empty( WC()->session->cart ) ) {
foreach ( WC()->session->cart as $cart_item_key => $cart_item ) {
if ( (int) $product->get_id() === (int) $cart_item['product_id'] && ( isset( $cart_item['subscription_renewal'] ) || isset( $cart_item['subscription_resubscribe'] ) ) ) {
$purchasable = true;
break;
}
}
}
}
break;
case 'subscription_variation':
$variable_product = wc_get_product( $product->get_parent_id() );
if ( 'no' != wcs_get_product_limitation( $variable_product ) && ! empty( WC()->cart->cart_contents ) && ! wcs_is_order_received_page() && ! wcs_is_paypal_api_page() ) {
// When mixed checkout is disabled, the variation is replaceable
if ( 'no' === get_option( WC_Subscriptions_Admin::$option_prefix . '_multiple_purchase', 'no' ) ) {
$purchasable = true;
} else { // When mixed checkout is enabled
foreach ( WC()->cart->cart_contents as $cart_item ) {
// If the variable product is limited, it can't be purchased if it is the same variation
if ( $product->get_parent_id() === $cart_item['data']->get_parent_id() && $product->get_id() !== $cart_item['data']->get_id() ) {
$purchasable = false;
break;
}
}
}
}
break;
}
}
return $purchasable;
}
@ -302,12 +328,73 @@ class WCS_Limiter {
return self::$order_awaiting_payment_for_product[ $product_id ];
}
/**
* Check if we're currently paying for a failed renewal order containing the product.
*
* @since 8.3.0 - Migrated from WooCommerce Subscriptions v2.1.0
* @param WC_Product $product The product to check.
* @return bool
*/
protected static function is_paying_for_failed_renewal_order( $product ) {
global $wp;
// Check if we're on the pay for order page
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET['pay_for_order'] ) || ! isset( $_GET['key'] ) || ! isset( $wp->query_vars['order-pay'] ) ) {
// Also check if cart contains a failed renewal order payment
$failed_renewal_cart_item = wcs_cart_contains_failed_renewal_order_payment();
if ( false !== $failed_renewal_cart_item ) {
$cart_item_product_id = isset( $failed_renewal_cart_item['variation_id'] ) && $failed_renewal_cart_item['variation_id'] > 0
? $failed_renewal_cart_item['variation_id']
: $failed_renewal_cart_item['product_id'];
// Check both the product ID and parent product ID (for variations)
if ( (int) $product->get_id() === (int) $cart_item_product_id || (int) $product->get_id() === (int) $failed_renewal_cart_item['product_id'] ) {
return true;
}
// Also check if product is a variation and matches the parent
if ( $product->get_parent_id() > 0 && (int) $product->get_parent_id() === (int) $failed_renewal_cart_item['product_id'] ) {
return true;
}
}
return false;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : '';
$order_id = isset( $wp->query_vars['order-pay'] ) ? $wp->query_vars['order-pay'] : 0;
$order = wc_get_order( absint( $order_id ) );
if ( ! $order || $order->get_order_key() !== $order_key ) {
return false;
}
// Check if order is a failed renewal order
if ( ! $order->has_status( 'failed' ) && ! wcs_order_contains_renewal( $order ) ) {
return false;
}
// Check if the order contains the product
foreach ( $order->get_items() as $item ) {
$item_product_id = isset( $item['variation_id'] ) && $item['variation_id'] > 0 ? $item['variation_id'] : $item['product_id'];
// Check both the product ID and parent product ID (for variations)
if ( (int) $product->get_id() === (int) $item_product_id || (int) $product->get_id() === (int) $item['product_id'] ) {
return true;
}
// Also check if product is a variation and matches the parent
if ( $product->get_parent_id() > 0 && (int) $product->get_parent_id() === (int) $item['product_id'] ) {
return true;
}
}
return false;
}
/**
* Filters the order statuses that enable the order again button and functionality.
*
* This function will return no statuses if the order contains non purchasable or limited products.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v3.0.2
* @since 8.3.0 - Migrated from WooCommerce Subscriptions v3.0.2
*
* @param array $statuses The order statuses that enable the order again button.
* @return array $statuses An empty array if the order contains limited products, otherwise the default statuses are returned.

View File

@ -124,7 +124,7 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
* @return string
*/
function get_subject() {
return apply_filters( 'woocommerce_subscriptions_email_subject_new_renewal_order', parent::get_subject(), $this->object );
return apply_filters( 'woocommerce_subscriptions_email_subject_new_renewal_order', $this->format_string( $this->get_option( 'subject', $this->get_default_subject() ) ), $this->object );
}
/**
@ -134,7 +134,7 @@ class WCS_Email_Customer_Renewal_Invoice extends WC_Email_Customer_Invoice {
* @return string
*/
function get_heading() {
return apply_filters( 'woocommerce_email_heading_customer_renewal_order', parent::get_heading(), $this->object );
return apply_filters( 'woocommerce_email_heading_customer_renewal_order', $this->format_string( $this->get_option( 'heading', $this->get_default_heading() ) ), $this->object );
}
/**

View File

@ -190,7 +190,7 @@ class WCS_PayPal_Standard_IPN_Handler extends WC_Gateway_Paypal_IPN_Handler {
$transaction_order = wc_get_order( substr( $transaction_details['invoice'], strrpos( $transaction_details['invoice'], '-' ) + 1 ) );
// check if the failed signup has been previously recorded
if ( wcs_get_objects_property( $transaction_order, 'id' ) !== $subscription->get_meta( '_paypal_failed_sign_up_recorded', true ) ) {
if ( wcs_get_objects_property( $transaction_order, 'id' ) !== (int) $subscription->get_meta( '_paypal_failed_sign_up_recorded', true ) ) {
$is_renewal_sign_up_after_failure = true;
}
}

View File

@ -227,9 +227,8 @@ class WC_Subscriptions_Upgrader {
WCS_Plugin_Upgrade_7_8_0::check_gifting_plugin_is_enabled();
}
if ( false && version_compare( self::$stored_plugin_version, '8.2.0', '<' ) ) {
// TODO: remove false from the above conditional, once we are ready to make subscription downloads functionality available.
WCS_Plugin_Upgrade_8_1_0::check_downloads_plugin_is_enabled();
if ( version_compare( self::$stored_plugin_version, '8.3.0', '<' ) ) {
WCS_Plugin_Upgrade_8_3_0::check_downloads_plugin_is_enabled();
}
}

View File

@ -7,7 +7,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WCS_Plugin_Upgrade_8_1_0 {
class WCS_Plugin_Upgrade_8_3_0 {
/**
* Check if the Gifting plugin is enabled and update the settings.

View File

@ -190,22 +190,12 @@ function wcs_price_string( $subscription_details ) {
break;
}
} elseif ( ! empty( $subscription_details['initial_amount'] ) ) {
if ( 1 === $subscription_details['subscription_interval'] ) {
// translators: 1$: initial amount, 2$: initial description (e.g. "up front"), 3$: recurring amount, 4$: subscription period (e.g. "month")
$subscription_string = sprintf( __( '%1$s %2$s then %3$s / %4$s', 'woocommerce-subscriptions' ), $initial_amount_string, $subscription_details['initial_description'], $recurring_amount_string, $subscription_period_string );
} else {
// translators: 1$: initial amount, 2$: initial description (e.g. "up front"), 3$: recurring amount, 4$: subscription period (e.g. "3 months")
$subscription_string = sprintf( __( '%1$s %2$s then %3$s every %4$s', 'woocommerce-subscriptions' ), $initial_amount_string, $subscription_details['initial_description'], $recurring_amount_string, $subscription_period_string );
}
// translators: 1$: initial amount, 2$: initial description (e.g. "up front"), 3$: recurring amount, 4$: subscription period (e.g. "month" or "3 months")
$subscription_string = sprintf( _n( '%1$s %2$s then %3$s / %4$s', '%1$s %2$s then %3$s every %4$s', $subscription_details['subscription_interval'], 'woocommerce-subscriptions' ), $initial_amount_string, $subscription_details['initial_description'], $recurring_amount_string, $subscription_period_string );
} elseif ( ! empty( $subscription_details['recurring_amount'] ) || intval( $subscription_details['recurring_amount'] ) === 0 ) {
if ( true === $subscription_details['use_per_slash'] ) {
if ( 1 === $subscription_details['subscription_interval'] ) {
// translators: 1$: recurring amount, 2$: subscription period (e.g. "month") (e.g. "$15 / month")
$subscription_string = sprintf( __( '%1$s / %2$s', 'woocommerce-subscriptions' ), $recurring_amount_string, $subscription_period_string );
} else {
// translators: 1$: recurring amount, 2$: subscription period (e.g. "3 months") (e.g. "$15 every 2nd month")
$subscription_string = sprintf( __( '%1$s every %2$s', 'woocommerce-subscriptions' ), $recurring_amount_string, $subscription_period_string );
}
// translators: 1$: recurring amount, 2$: subscription period (e.g. "month" or "3 months") (e.g. "$15 / month" or "$15 every 2nd month")
$subscription_string = sprintf( _n( '%1$s / %2$s', '%1$s every %2$s', $subscription_details['subscription_interval'], 'woocommerce-subscriptions' ), $recurring_amount_string, $subscription_period_string );
} else {
// translators: %1$: recurring amount (e.g. "$15"), %2$: subscription period (e.g. "month") (e.g. "$15 every 2nd month")
$subscription_string = sprintf( __( '%1$s every %2$s', 'woocommerce-subscriptions' ), $recurring_amount_string, $subscription_period_string );

View File

@ -0,0 +1,93 @@
<?php
/**
* Downloads Admin Announcement Handler Class
*
* @package WooCommerce Subscriptions
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class WC_Subscription_Downloads_Admin_Welcome_Announcement {
/**
* Initialize the tour handler
*/
public static function init() {
// Register scripts and styles
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
// Add the tour HTML to the admin footer
add_action( 'admin_footer', array( __CLASS__, 'output_tour' ) );
}
/**
* Enqueue required scripts and styles
*/
public static function enqueue_scripts() {
if ( ! self::is_woocommerce_admin_or_subscriptions_listing() ) {
return;
}
$screen = get_current_screen();
wp_localize_script(
'wcs-admin',
'wcsDownloadsSettings',
array(
'imagesPath' => plugins_url( '/assets/images', WC_Subscriptions::$plugin_file ),
'pluginsUrl' => admin_url( 'plugins.php' ),
'subscriptionsUrl' => WC_Subscriptions_Admin::settings_tab_url() . '#woocommerce_subscriptions_downloads_enable',
'isStandaloneDownloadsEnabled' => is_plugin_active( 'woocommerce-subscription-downloads/woocommerce-subscription-downloads.php' ),
'isSubscriptionsListing' => 'woocommerce_page_wc-orders--shop_subscription' === $screen->id,
)
);
}
/**
* Output the tour HTML in the admin footer
*/
public static function output_tour() {
if ( ! self::is_woocommerce_admin_or_subscriptions_listing() ) {
return;
}
// Add a div for the tour to be rendered into
echo '<div id="wcs-downloads-welcome-announcement-root" class="woocommerce-tour-kit"></div>';
}
/**
* Checks if the welcome tour has been dismissed.
*
* @return bool
*/
public static function is_welcome_announcement_dismissed() {
return '1' === get_option(
'woocommerce_subscriptions_downloads_is_welcome_announcement_dismissed',
''
);
}
/**
* Checks if the current screen is WooCommerce Admin or subscriptions listing.
*
* @return bool
*/
private static function is_woocommerce_admin_or_subscriptions_listing() {
$screen = get_current_screen();
if ( ! $screen ) {
return false;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$action_param = isset( $_GET['action'] ) ? wc_clean( wp_unslash( $_GET['action'] ) ) : '';
$is_woocommerce_admin = 'woocommerce_page_wc-admin' === $screen->id;
$is_subscriptions_listing = 'woocommerce_page_wc-orders--shop_subscription' === $screen->id && empty( $action_param );
return $is_woocommerce_admin || $is_subscriptions_listing;
}
}

View File

@ -17,6 +17,7 @@ class WC_Subscription_Downloads_Ajax {
*/
public function __construct() {
add_action( 'wp_ajax_wc_subscription_downloads_search', array( $this, 'search_subscriptions' ) );
add_action( 'wp_ajax_wc_subscription_linked_downloadable_products_search', array( $this, 'search_downloadable_products' ) );
}
/**
@ -72,4 +73,39 @@ class WC_Subscription_Downloads_Ajax {
wp_send_json( $found_subscriptions );
}
/**
* Searches for downloadable products that are simple or variants.
*
* @return void
*/
public function search_downloadable_products(): void {
$results = array();
// Prevent error noise from leaking.
ob_start();
if ( isset( $_GET['term'] ) && check_ajax_referer( 'search-products', 'security' ) ) {
$term = wc_clean( wp_unslash( $_GET['term'] ) );
}
if ( ! empty( $term ) ) {
$products = wc_get_products(
array(
'downloadable' => true,
'limit' => 100,
's' => $term,
'type' => array( 'simple', 'variation' ),
'status' => 'any',
)
);
foreach ( $products as $product ) {
$results[ $product->get_id() ] = sanitize_text_field( $product->get_formatted_name() );
}
}
ob_clean();
wp_send_json( $results );
}
}

View File

@ -7,52 +7,202 @@ if ( ! defined( 'ABSPATH' ) ) {
* WooCommerce Subscription Downloads Products.
*
* @package WC_Subscription_Downloads_Products
* @category Products
* @author WooThemes
*/
class WC_Subscription_Downloads_Products {
public const EDITOR_UPDATE = 'wcsubs_subscription_download_relationships';
public const RELATIONSHIP_DOWNLOAD_TO_SUB = 'download-to-sub';
public const RELATIONSHIP_VAR_DOWNLOAD_TO_SUB = 'var-download-to-sub';
public const RELATIONSHIP_SUB_TO_DOWNLOAD = 'sub-to-download';
public const RELATIONSHIP_VAR_SUB_TO_DOWNLOAD = 'var-sub-to-download';
/**
* Products actions.
*/
public function __construct() {
add_action( 'woocommerce_product_options_downloads', array( $this, 'simple_write_panel_options' ), 10 );
add_action( 'woocommerce_product_options_downloads', array( $this, 'simple_write_panel_options' ) );
add_action( 'woocommerce_variation_options_download', array( $this, 'variable_write_panel_options' ), 10, 3 );
add_action( 'admin_enqueue_scripts', array( $this, 'scripts' ) );
add_action( 'woocommerce_process_product_meta_simple', array( $this, 'save_simple_product_data' ), 10 );
add_action( 'woocommerce_save_product_variation', array( $this, 'save_variation_product_data' ), 10, 2 );
add_action( 'woocommerce_product_options_pricing', array( $this, 'subscription_product_editor_ui' ) );
add_action( 'woocommerce_variable_subscription_pricing', array( $this, 'variable_subscription_product_editor_ui' ), 10, 3 );
add_action( 'save_post_product', array( $this, 'handle_product_save' ) );
add_action( 'save_post_product_variation', array( $this, 'handle_product_variation_save' ) );
add_action( 'woocommerce_save_product_variation', array( $this, 'handle_product_variation_save' ) );
add_action( 'woocommerce_update_product', array( $this, 'handle_product_save' ) );
add_action( 'woocommerce_update_product_variation', array( $this, 'handle_product_variation_save' ) );
add_action( 'init', array( $this, 'init' ) );
}
public function init() {
if ( version_compare( WC_VERSION, '3.0', '<' ) ) {
add_action( 'woocommerce_duplicate_product', array( $this, 'save_subscriptions_when_duplicating_product' ), 10, 2 );
} else {
add_action( 'woocommerce_product_duplicate', array( $this, 'save_subscriptions_when_duplicating_product' ), 10, 2 );
add_action( 'woocommerce_product_duplicate', array( $this, 'save_subscriptions_when_duplicating_product' ), 10, 2 );
}
/**
* Handle product save - generic handler for all product updates.
*
* @param int $post_id Post ID.
*
* @return void
*/
public function handle_product_save( $post_id ) {
// Bail if this is an autosave or revision.
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( wp_is_post_revision( $post_id ) ) {
return;
}
$product = wc_get_product( $post_id );
if ( ! $product ) {
return;
}
$product_is_downloadable = $product->is_downloadable();
$product_is_subscription = $product->is_type( array( 'subscription', 'variable-subscription' ) );
// We do not allow downloadable subscription products to be linked with other subscription products; this is
// principally to avoid confusion (though it would be technically feasible).
if ( $product_is_subscription && ! $product_is_downloadable ) {
$this->handle_subscription_product_save( $post_id );
} elseif ( ! $product_is_subscription && $product_is_downloadable ) {
$this->handle_downloadable_product_save( $post_id );
}
}
/**
* Product screen scripts.
* Handle product variation save - generic handler for all variation updates.
*
* @param int $post_id Post ID.
*
* @return void
*/
public function scripts() {
$screen = get_current_screen();
public function handle_product_variation_save( $post_id ) {
// Bail if this is an autosave or revision.
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( 'product' == $screen->id && version_compare( WC_VERSION, '2.3.0', '<' ) ) {
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
if ( wp_is_post_revision( $post_id ) ) {
return;
}
wp_enqueue_script( 'wc_subscription_downloads_writepanel', plugins_url( 'assets/js/admin/writepanel' . $suffix . '.js', plugin_dir_path( __FILE__ ) ), array( 'ajax-chosen', 'chosen' ), WC_Subscriptions::$version, true );
$variation = wc_get_product( $post_id );
if ( ! $variation || ! $variation->is_type( 'variation' ) ) {
return;
}
wp_localize_script(
'wc_subscription_downloads_writepanel',
'wc_subscription_downloads_product',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'security' => wp_create_nonce( 'search-products' ),
)
);
// Handle downloadable variations (they link TO subscriptions).
if ( $variation->is_downloadable() ) {
$this->handle_downloadable_product_save( $post_id );
}
// Handle subscription variations (they link TO downloadable products).
$parent = wc_get_product( $variation->get_parent_id() );
if ( $parent && $parent->is_type( 'variable-subscription' ) ) {
$this->handle_subscription_product_save( $post_id );
}
}
/**
* Handle save for downloadable products (simple or variation).
* These products link TO subscription products.
*
* @param int $product_id Product or variation ID.
*
* @return void
*/
private function handle_downloadable_product_save( $product_id ) {
$product = wc_get_product( $product_id );
if ( ! $product ) {
return;
}
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if (
isset( $_POST[ self::RELATIONSHIP_VAR_DOWNLOAD_TO_SUB . $product_id ] )
&& wp_verify_nonce( $_POST[ self::RELATIONSHIP_VAR_DOWNLOAD_TO_SUB . $product_id ], self::EDITOR_UPDATE )
) {
$subscription_ids = wc_clean( wp_unslash( $_POST['_variable_subscription_downloads_ids'][ $product_id ] ?? array() ) );
$subscription_ids = array_filter( (array) $subscription_ids );
$this->update_subscription_downloads( $product_id, $subscription_ids );
}
if (
isset( $_POST[ self::RELATIONSHIP_DOWNLOAD_TO_SUB ] )
&& wp_verify_nonce( $_POST[ self::RELATIONSHIP_DOWNLOAD_TO_SUB ], self::EDITOR_UPDATE )
) {
$subscription_ids = wc_clean( wp_unslash( $_POST['_subscription_downloads_ids'] ?? array() ) );
$subscription_ids = array_filter( (array) $subscription_ids );
$this->update_subscription_downloads( $product_id, $subscription_ids );
}
// Observe and act on product status changes (regardless of whether they were made from within the product
// editor, therefore we don't care about nonce checks here).
$this->assess_downloadable_product_status( $product_id );
// phpcs:enable
}
/**
* Handle save for subscription products (simple subscription or variation).
* These products link TO downloadable products.
*
* @param int $product_id Subscription product or variation ID.
*
* @return void
*/
private function handle_subscription_product_save( $product_id ) {
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if (
isset( $_POST[ self::RELATIONSHIP_VAR_SUB_TO_DOWNLOAD . $product_id ] )
&& wp_verify_nonce( $_POST[ self::RELATIONSHIP_VAR_SUB_TO_DOWNLOAD . $product_id ], self::EDITOR_UPDATE )
) {
$product_ids = wc_clean( wp_unslash( $_POST[ '_subscription_linked_downloadable_products_' . $product_id ] ?? array() ) );
$product_ids = array_filter( (array) $product_ids );
$this->update_subscription_products( $product_id, $product_ids );
return;
}
if (
isset( $_POST[ self::RELATIONSHIP_SUB_TO_DOWNLOAD ] )
&& wp_verify_nonce( $_POST[ self::RELATIONSHIP_SUB_TO_DOWNLOAD ], self::EDITOR_UPDATE )
) {
$product_ids = wc_clean( wp_unslash( (array) $_POST['_subscription_linked_downloadable_products'] ?? array() ) );
$product_ids = array_filter( (array) $product_ids );
$this->update_subscription_products( $product_id, $product_ids );
}
// phpcs:enable
}
/**
* Assess downloadable product status and adjust permissions accordingly.
* Called when no form data is available (e.g., status change, REST API update, file changes).
*
* @param int $product_id Product ID.
*
* @return void
*/
private function assess_downloadable_product_status( $product_id ) {
$product = wc_get_product( $product_id );
if ( ! $product || ! $product->is_downloadable() ) {
return;
}
$status_object = get_post_status_object( $product->get_status() );
$is_public = $status_object && $status_object->public;
// Always revoke existing permissions first to ensure clean state.
// This handles file changes and status transitions.
$this->revoke_permissions_for_product( $product_id );
// Grant fresh permissions only if product is public.
if ( $is_public ) {
$this->grant_permissions_for_product( $product_id );
}
}
@ -62,8 +212,8 @@ class WC_Subscription_Downloads_Products {
public function simple_write_panel_options() {
global $post;
?>
<p class="form-field _subscription_downloads_field">
<label for="subscription-downloads-ids"><?php esc_html_e( 'Included subscription products', 'woocommerce-subscriptions' ); ?></label>
<p class="form-field _subscription_downloads_field hide_if_subscription">
<label for="subscription-downloads-ids"><?php esc_html_e( 'Linked subscription products', 'woocommerce-subscriptions' ); ?></label>
<select id="subscription-downloads-ids" multiple="multiple" data-action="wc_subscription_downloads_search" data-placeholder="<?php esc_attr_e( 'Select subscriptions', 'woocommerce-subscriptions' ); ?>" class="subscription-downloads-ids wc-product-search" name="_subscription_downloads_ids[]" style="width: 50%;">
<?php
@ -71,17 +221,18 @@ class WC_Subscription_Downloads_Products {
if ( $subscriptions_ids ) {
foreach ( $subscriptions_ids as $subscription_id ) {
$_subscription = wc_get_product( $subscription_id );
$subscription = wc_get_product( $subscription_id );
if ( $_subscription ) {
echo '<option value="' . esc_attr( $subscription_id ) . '" selected="selected">' . esc_html( wp_strip_all_tags( $_subscription->get_formatted_name() ) ) . '</option>';
if ( $subscription ) {
echo '<option value="' . esc_attr( $subscription_id ) . '" selected="selected">' . esc_html( wp_strip_all_tags( $subscription->get_formatted_name() ) ) . '</option>';
}
}
}
?>
</select>
<?php echo wc_help_tip( wc_sanitize_tooltip( __( 'Select subscription products that will include this downloadable product.', 'woocommerce-subscriptions' ) ) ); ?>
<span class="description"><?php esc_html_e( 'Select subscription products that will include this downloadable product.', 'woocommerce-subscriptions' ); ?></span>
<?php wp_nonce_field( self::EDITOR_UPDATE, self::RELATIONSHIP_DOWNLOAD_TO_SUB, false ); ?>
</p>
<?php
@ -94,30 +245,145 @@ class WC_Subscription_Downloads_Products {
?>
<tr class="show_if_variation_downloadable">
<td colspan="2">
<p class="form-field form-row form-row-full">
<label><?php esc_html_e( 'Included subscription products', 'woocommerce-subscriptions' ); ?>:</label>
<p class="form-field _subscription_downloads_field form-row form-row-full hide_if_variable-subscription">
<label><?php esc_html_e( 'Linked subscription products', 'woocommerce-subscriptions' ); ?>:</label>
<?php echo wc_help_tip( wc_sanitize_tooltip( __( 'Select subscription products that will include this downloadable product.', 'woocommerce-subscriptions' ) ) ); ?>
<select multiple="multiple" data-placeholder="<?php esc_html_e( 'Select subscriptions', 'woocommerce-subscriptions' ); ?>" class="subscription-downloads-ids wc-product-search" name="_variable_subscription_downloads_ids[<?php echo esc_attr( $loop ); ?>][]" style="width: 100%">
<select multiple="multiple" data-placeholder="<?php esc_html_e( 'Select subscriptions', 'woocommerce-subscriptions' ); ?>" class="subscription-downloads-ids wc-product-search" name="_variable_subscription_downloads_ids[<?php echo esc_attr( $variation->ID ); ?>][]" style="width: 100%">
<?php
$subscriptions_ids = WC_Subscription_Downloads::get_subscriptions( $variation->ID );
if ( $subscriptions_ids ) {
foreach ( $subscriptions_ids as $subscription_id ) {
$_subscription = wc_get_product( $subscription_id );
$subscription = wc_get_product( $subscription_id );
if ( $_subscription ) {
echo '<option value="' . esc_attr( $subscription_id ) . '" selected="selected">' . esc_html( wp_strip_all_tags( $_subscription->get_formatted_name() ) ) . '</option>';
if ( $subscription ) {
echo '<option value="' . esc_attr( $subscription_id ) . '" selected="selected">' . esc_html( wp_strip_all_tags( $subscription->get_formatted_name() ) ) . '</option>';
}
}
}
?>
</select>
<?php wp_nonce_field( self::EDITOR_UPDATE, self::RELATIONSHIP_VAR_DOWNLOAD_TO_SUB . $variation->ID, false ); ?>
</p>
</td>
</tr>
<?php
}
/**
* Adds a field with which to link the subscription product (the product being edited) with zero-or-many
* downloadable products.
*
* @return void
*/
public function subscription_product_editor_ui(): void {
global $post;
if ( ! $post instanceof WP_Post ) {
wc_get_logger()->warning(
'Unable to add the downloadable products selector to the product editor (global post object is unavailable).',
array( 'backtrace' => true )
);
return;
}
$description = esc_html__( 'Select simple and variable downloadable products that will be included with this subscription product.', 'woocommerce-subscriptions' );
$label = esc_html__( 'Linked downloadable products', 'woocommerce-subscriptions' );
$linked_products = '';
$nonce_field = wp_nonce_field( self::EDITOR_UPDATE, self::RELATIONSHIP_SUB_TO_DOWNLOAD, false, false );
$placeholder = esc_attr__( 'Select products', 'woocommerce-subscriptions' );
foreach ( WC_Subscription_Downloads::get_downloadable_products( $post->ID ) as $product_id ) {
$product_id = absint( $product_id );
$product = wc_get_product( $product_id );
if ( $product ) {
$product_name = esc_html( wp_strip_all_tags( $product->get_formatted_name() ) );
$linked_products .= "<option value='$product_id' selected='selected'>$product_name</option>";
}
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- variables are escaped above.
echo "
<div class='options_group subscription_linked_downloadable_products_section'>
<p class='form-field subscription_linked_downloadable_products'>
<label for='subscription-linked-downloadable-products'>$label</label>
<select
class='wc-product-search subscription-downloads-ids'
data-action='wc_subscription_linked_downloadable_products_search'
data-placeholder='$placeholder'
id='subscription-linked-downloadable-products'
multiple='multiple'
name='_subscription_linked_downloadable_products[]'
style='width: 50%;'
>
$linked_products
</select>
<span class='description'>$description</span>
$nonce_field
</p>
</div>
";
}
/**
* @param int $loop
* @param array $variation_data
* @param WP_Post $variation
*
* @return void
*/
public function variable_subscription_product_editor_ui( $loop, $variation_data, $variation ): void {
if ( ! $variation instanceof WP_Post ) {
wc_get_logger()->warning(
'Unable to add the downloadable products selector to the variation section of the product editor (we do not have a valid post object).',
array( 'backtrace' => true )
);
return;
}
$variation_id = (int) $variation->ID;
$label = esc_html__( 'Linked downloadable products', 'woocommerce-subscriptions' );
$linked_products = '';
$nonce_field = wp_nonce_field( self::EDITOR_UPDATE, self::RELATIONSHIP_VAR_SUB_TO_DOWNLOAD . $variation_id, false, false );
$placeholder = esc_attr__( 'Select products', 'woocommerce-subscriptions' );
$tooltip = wc_help_tip( wc_sanitize_tooltip( __( 'Select simple and variable downloadable products that will be included with this subscription variation.', 'woocommerce-subscriptions' ) ) );
foreach ( WC_Subscription_Downloads::get_downloadable_products( $variation->ID ) as $product_id ) {
$product_id = absint( $product_id );
$product = wc_get_product( $product_id );
if ( $product ) {
$product_name = esc_html( wp_strip_all_tags( $product->get_formatted_name() ) );
$linked_products .= "<option value='$product_id' selected='selected'>$product_name</option>";
}
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- variables are escaped above.
echo "
<div class='variable_subscription_linked_downloadable_products show_if_variable-subscription' style='display: none'>
<p class='form-row form-field subscription_linked_downloadable_products'>
<label for='subscription-linked-downloadable-products'>$label</label>
$tooltip
<select
class='wc-product-search subscription-downloads-ids'
data-action='wc_subscription_linked_downloadable_products_search'
data-placeholder='$placeholder'
id='subscription-linked-downloadable-products'
multiple='multiple'
name='_subscription_linked_downloadable_products_{$variation_id}[]'
style='width: 100%;'
>
$linked_products
</select>
$nonce_field
</p>
</div>
";
}
/**
* Search orders from subscription product ID.
*
@ -164,9 +430,7 @@ class WC_Subscription_Downloads_Products {
$orders[] = $order->id;
}
$orders = apply_filters( 'woocommerce_subscription_downloads_get_orders', $orders, $subscription_product_id );
return $orders;
return apply_filters( 'woocommerce_subscription_downloads_get_orders', $orders, $subscription_product_id );
}
/**
@ -181,37 +445,93 @@ class WC_Subscription_Downloads_Products {
protected function revoke_access_to_download( $download_id, $product_id, $order_id ) {
global $wpdb;
$wpdb->query( $wpdb->prepare( "
DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE order_id = %d AND product_id = %d AND download_id = %s;
", $order_id, $product_id, $download_id ) );
$wpdb->query(
$wpdb->prepare(
"
DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE order_id = %d AND product_id = %d AND download_id = %s;
",
$order_id,
$product_id,
$download_id
)
);
do_action( 'woocommerce_ajax_revoke_access_to_product_download', $download_id, $product_id, $order_id );
}
/**
* Update subscription downloads table and orders.
* Update subscription downloads table and orders according in respect to the described relationship between a
* regular product and zero-to-many regular subscription products.
*
* @param int $product_id
* @param array $subscriptions
* @param int $product_id The downloadable product ID.
* @param array $subscriptions Subscription product IDs.
*
* @return void
*/
protected function update_subscription_downloads( $product_id, $subscriptions ) {
$current = array_map( 'intval', WC_Subscription_Downloads::get_subscriptions( $product_id ) );
$subscriptions = array_map( 'intval', (array) $subscriptions );
sort( $current );
sort( $subscriptions );
$to_delete = array_diff( $current, $subscriptions );
$to_create = array_diff( $subscriptions, $current );
$this->delete_relationships( $to_delete, array( $product_id ) );
$this->create_relationships( $to_create, array( $product_id ) );
}
/**
* Update subscription downloads table and orders according in respect to the described relationship between a
* subscription product and zero-to-many regular products.
*
* @param int $subscription_product_id Subscription product ID.
* @param int[] $new_ids IDs for downloadable products that should be associated with the subscription product.
*
* @return void
*/
private function update_subscription_products( int $subscription_product_id, array $new_ids ): void {
$existing_ids = array_map( 'intval', WC_Subscription_Downloads::get_downloadable_products( $subscription_product_id ) );
$new_ids = array_map( 'intval', $new_ids );
sort( $existing_ids );
sort( $new_ids );
$to_delete = array_diff( $existing_ids, $new_ids );
$to_create = array_diff( $new_ids, $existing_ids );
$this->delete_relationships( array( $subscription_product_id ), $to_delete );
$this->create_relationships( array( $subscription_product_id ), $to_create );
}
/**
* Deletes relationships that exist between any of the supplied subscription IDs and any of the supplied product
* IDs.
*
* The most common use case will be to supply a single subscription ID and one-or-more product IDs, or else the
* inverse.
*
* @param int[] $subscription_ids
* @param int[] $product_ids
*
* @return void
*/
private function delete_relationships( array $subscription_ids, array $product_ids ): void {
global $wpdb;
$subscriptions = (array) $subscriptions;
$current = WC_Subscription_Downloads::get_subscriptions( $product_id );
foreach ( $product_ids as $product_id ) {
$product_id = (int) $product_id;
foreach ( $subscription_ids as $subscription_id ) {
$subscription_id = (int) $subscription_id;
// Delete items.
$delete_ids = array_diff( $current, $subscriptions );
if ( $delete_ids ) {
foreach ( $delete_ids as $delete ) {
$wpdb->delete(
$wpdb->prefix . 'woocommerce_subscription_downloads',
array(
'product_id' => $product_id,
'subscription_id' => $delete,
'subscription_id' => $subscription_id,
),
array(
'%d',
@ -219,10 +539,10 @@ class WC_Subscription_Downloads_Products {
)
);
$_orders = $this->get_orders( $delete );
foreach ( $_orders as $order_id ) {
$_product = wc_get_product( $product_id );
$downloads = $_product->get_downloads();
$orders = $this->get_orders( $subscription_id );
foreach ( $orders as $order_id ) {
$product = wc_get_product( $product_id );
$downloads = $product->get_downloads();
// Adds the downloadable files to the order/subscription.
foreach ( array_keys( $downloads ) as $download_id ) {
@ -231,16 +551,35 @@ class WC_Subscription_Downloads_Products {
}
}
}
}
// Add items.
$add_ids = array_diff( $subscriptions, $current );
if ( $add_ids ) {
foreach ( $add_ids as $add ) {
$wpdb->insert(
$wpdb->prefix . 'woocommerce_subscription_downloads',
/**
* Revoke download permissions for a product across all related subscriptions.
*
* @param int $product_id Product ID.
*
* @return void
*/
private function revoke_permissions_for_product( $product_id ) {
global $wpdb;
$subscription_ids = WC_Subscription_Downloads::get_subscriptions( $product_id );
if ( empty( $subscription_ids ) ) {
return;
}
foreach ( $subscription_ids as $subscription_id ) {
$orders = $this->get_orders( $subscription_id );
foreach ( $orders as $order_id ) {
// Delete ALL permissions for this product+order combination.
// This ensures that when files change, old permissions with different download_ids are removed.
$wpdb->delete(
$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
array(
'product_id' => $product_id,
'subscription_id' => $add,
'order_id' => $order_id,
'product_id' => $product_id,
),
array(
'%d',
@ -248,23 +587,105 @@ class WC_Subscription_Downloads_Products {
)
);
$_orders = $this->get_orders( $add );
foreach ( $_orders as $order_id ) {
$order = wc_get_order( $order_id );
do_action( 'woocommerce_revoke_access_to_product_download', $product_id, $order_id );
}
}
}
if ( ! is_a( $order, 'WC_Subscription' ) ) {
// avoid adding permissions to orders and it's
// subscription for the same user, causing duplicates
// to show up
continue;
}
/**
* Grant download permissions for a product across all related subscriptions.
*
* @param int $product_id Product ID.
*
* @return void
*/
private function grant_permissions_for_product( $product_id ) {
$subscription_product_ids = WC_Subscription_Downloads::get_subscriptions( $product_id );
$product = wc_get_product( $product_id );
$_product = wc_get_product( $product_id );
$downloads = $_product->get_downloads();
if ( empty( $subscription_product_ids ) || ! $product ) {
return;
}
// Adds the downloadable files to the order/subscription.
foreach ( array_keys( $downloads ) as $download_id ) {
wc_downloadable_file_permission( $download_id, $product_id, $order );
$downloads = $product->get_downloads();
foreach ( $subscription_product_ids as $subscription_id ) {
$orders = $this->get_orders( $subscription_id );
foreach ( $orders as $order_id ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, 'WC_Subscription' ) ) {
continue;
}
foreach ( array_keys( $downloads ) as $download_id ) {
wc_downloadable_file_permission( $download_id, $product_id, $order );
}
}
}
}
/**
* Adds relationships between the specified subscription and product IDs.
*
* The most common use case will be to supply a single subscription ID and one-or-more product IDs, or else the
* inverse.
*
* @param int[] $subscription_ids
* @param int[] $product_ids
*
* @return void
*/
private function create_relationships( array $subscription_ids, array $product_ids ): void {
global $wpdb;
foreach ( $product_ids as $product_id ) {
$product_id = (int) $product_id;
$product = wc_get_product( $product_id );
// Check if product has public status.
$has_public_status = false;
if ( $product ) {
$status_object = get_post_status_object( $product->get_status() );
$has_public_status = $status_object && $status_object->public;
}
foreach ( $subscription_ids as $subscription_id ) {
$subscription_id = (int) $subscription_id;
$wpdb->insert(
$wpdb->prefix . 'woocommerce_subscription_downloads',
array(
'product_id' => $product_id,
'subscription_id' => $subscription_id,
),
array(
'%d',
'%d',
)
);
// Only grant download permissions if product has public status.
if ( $has_public_status ) {
$orders = $this->get_orders( $subscription_id );
foreach ( $orders as $order_id ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, 'WC_Subscription' ) ) {
// avoid adding permissions to orders and it's
// subscription for the same user, causing duplicates
// to show up
continue;
}
$product = wc_get_product( $product_id );
$downloads = $product->get_downloads();
// Adds the downloadable files to the order/subscription.
foreach ( array_keys( $downloads ) as $download_id ) {
wc_downloadable_file_permission( $download_id, $product_id, $order );
}
}
}
}
@ -289,26 +710,6 @@ class WC_Subscription_Downloads_Products {
$this->update_subscription_downloads( $product_id, $subscription_downloads_ids );
}
/**
* Save variable product data.
*
* @param int $variation_id
* @param int $index
*
* @return void
*/
public function save_variation_product_data( $variation_id, $index ) {
if ( ! isset( $_POST['variable_is_downloadable'][ $index ] ) ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$subscription_download_ids = isset( $_POST['_variable_subscription_downloads_ids'][ $index ] ) ? wc_clean( wp_unslash( $_POST['_variable_subscription_downloads_ids'][ $index ] ) ) : array();
$subscription_download_ids = array_filter( $subscription_download_ids ); // nosemgrep: audit.php.lang.misc.array-filter-no-callback -- $subscription_download_ids are already passed through wc_clean() and wp_unslash().
$this->update_subscription_downloads( $variation_id, $subscription_download_ids );
}
/**
* Save subscriptions information when duplicating a product.
*
@ -370,4 +771,45 @@ class WC_Subscription_Downloads_Products {
return (string) wc_get_formatted_variation( $product_variation, true );
}
/**
* Deprecated, do not use. Previously took care of saving product data for variations.
*
* @deprecated 8.3.0
*
* @param int $variation_id
* @param int $index
*
* @return void
*/
// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
public function save_variation_product_data( $variation_id, $index ) {
wc_deprecated_function( __METHOD__, '8.3.0', __CLASS__ . '::handle_product_variation_save' );
}
/**
* Deprecated, do not use. Previously took care of saving product data.
*
* @deprecated 8.3.0
*
* @param int $subscription_product_id
* @param int|null $index
*
* @return void
*/
// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
public function save_subscription_product_data( int $subscription_product_id, ?int $index = null ) {
wc_deprecated_function( __METHOD__, '8.3.0', __CLASS__ . '::handle_product_save' );
}
/**
* Deprecated, do not use. Previously set up assets for the Subscription Downloads extension.
*
* @deprecated 8.3.0
*
* @return void
*/
public function scripts() {
wc_deprecated_function( __METHOD__, '8.3.0' );
}
}

View File

@ -60,8 +60,8 @@ class WC_Subscription_Downloads_Settings {
'id' => WC_Subscriptions_Admin::$option_prefix . '_downloads_settings',
),
array(
'name' => __( 'Enable product linking to subscriptions', 'woocommerce-subscriptions' ),
'desc' => __( 'Allow simple and variable downloadable products to be included with subscription products.', 'woocommerce-subscriptions' ),
'name' => __( 'Enable downloadable file sharing', 'woocommerce-subscriptions' ),
'desc' => __( 'Allow downloadable files from simple and variable products to be shared with subscription products so they are available to active subscribers.', 'woocommerce-subscriptions' ),
'id' => WC_Subscriptions_Admin::$option_prefix . '_enable_downloadable_file_linking',
'default' => 'no',
'type' => 'checkbox',

View File

@ -33,32 +33,8 @@ class WCSG_Admin_Welcome_Announcement {
$screen = get_current_screen();
$script_asset_path = \WC_Subscriptions_Plugin::instance()->get_plugin_directory( 'build/gifting-welcome-announcement.asset.php' );
$script_asset = file_exists( $script_asset_path )
? require $script_asset_path
: array(
'dependencies' => array(
'react',
'wc-blocks-checkout',
'wc-price-format',
'wc-settings',
'wp-element',
'wp-i18n',
'wp-plugins',
),
'version' => WC_Subscriptions::$version,
);
wp_enqueue_script(
'wcs-gifting-welcome-announcement',
plugins_url( '/build/gifting-welcome-announcement.js', WC_Subscriptions::$plugin_file ),
$script_asset['dependencies'],
$script_asset['version'],
true
);
wp_localize_script(
'wcs-gifting-welcome-announcement',
'wcs-admin',
'wcsGiftingSettings',
array(
'imagesPath' => plugins_url( '/assets/images', WC_Subscriptions::$plugin_file ),
@ -68,19 +44,6 @@ class WCSG_Admin_Welcome_Announcement {
'isSubscriptionsListing' => 'woocommerce_page_wc-orders--shop_subscription' === $screen->id,
)
);
wp_enqueue_style(
'wcs-gifting-welcome-announcement',
plugins_url( '/build/style-gifting-welcome-announcement.css', WC_Subscriptions::$plugin_file ),
array( 'wp-components' ),
$script_asset['version']
);
wp_set_script_translations(
'wcs-gifting-welcome-announcement',
'woocommerce-subscriptions',
plugin_dir_path( WC_Subscriptions::$plugin_file ) . 'languages'
);
}
/**

View File

@ -1,4 +1,7 @@
<?php
use Automattic\WooCommerce_Subscriptions\Internal\Utilities\Request;
/**
* A class to make it possible to switch between different subscriptions (i.e. upgrade/downgrade a subscription)
*
@ -44,6 +47,9 @@ class WC_Subscriptions_Switcher {
// Add the "Switch" button to the View Subscription table
add_action( 'woocommerce_order_item_meta_end', array( __CLASS__, 'print_switch_link' ), 10, 3 );
// Add hidden form inputs for AJAX add-to-cart compatibility during switches
add_action( 'woocommerce_before_add_to_cart_button', array( __CLASS__, 'add_switch_hidden_inputs' ) );
// We need to create subscriptions on checkout and want to do it after almost all other extensions have added their products/items/fees
add_action( 'woocommerce_checkout_order_processed', array( __CLASS__, 'process_checkout' ), 50, 2 );
@ -179,22 +185,26 @@ class WC_Subscriptions_Switcher {
public static function subscription_switch_handler() {
global $post;
// If the current user doesn't own the subscription, remove the query arg from the URL
if ( isset( $_GET['switch-subscription'] ) && isset( $_GET['item'] ) ) {
$switch_subscription_id = Request::get_var( 'switch-subscription' );
$item_id = Request::get_var( 'item' );
$subscription = wcs_get_subscription( absint( $_GET['switch-subscription'] ) );
$line_item = $subscription ? wcs_get_order_item( absint( $_GET['item'] ), $subscription ) : false;
$nonce = ! empty( $_GET['_wcsnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wcsnonce'] ) ) : false;
// If the current user doesn't own the subscription, remove the query arg from the URL
if ( $switch_subscription_id && $item_id ) {
$subscription = wcs_get_subscription( absint( $switch_subscription_id ) );
$line_item = $subscription ? wcs_get_order_item( absint( $item_id ), $subscription ) : false;
$nonce = Request::get_var( '_wcsnonce' );
$nonce = $nonce ? sanitize_text_field( wp_unslash( $nonce ) ) : false;
// Visiting a switch link for someone elses subscription or if the switch link doesn't contain a valid nonce
if ( ! is_object( $subscription ) || empty( $nonce ) || ! wp_verify_nonce( $nonce, 'wcs_switch_request' ) || empty( $line_item ) || ! self::can_item_be_switched_by_user( $line_item, $subscription ) ) {
wp_safe_redirect( remove_query_arg( array( 'switch-subscription', 'auto-switch', 'item', '_wcsnonce' ) ) );
exit();
Request::redirect( remove_query_arg( array( 'switch-subscription', 'auto-switch', 'item', '_wcsnonce' ) ) );
return;
} else {
if ( isset( $_GET['auto-switch'] ) ) {
if ( Request::get_var( 'auto-switch' ) ) {
$switch_message = __( 'You have a subscription to this product. Choosing a new subscription will replace your existing subscription.', 'woocommerce-subscriptions' );
} else {
$switch_message = __( 'Choose a new subscription.', 'woocommerce-subscriptions' );
@ -239,8 +249,8 @@ class WC_Subscriptions_Switcher {
if ( $removed_item_count > 0 ) {
wc_add_notice( _n( 'Your cart contained an invalid subscription switch request. It has been removed.', 'Your cart contained invalid subscription switch requests. They have been removed.', $removed_item_count, 'woocommerce-subscriptions' ), 'error' );
wp_safe_redirect( wc_get_cart_url() );
exit();
Request::redirect( wc_get_cart_url() );
return;
}
} elseif ( is_product() && $product = wc_get_product( $post ) ) { // Automatically initiate the switch process for limited variable subscriptions
@ -308,8 +318,8 @@ class WC_Subscriptions_Switcher {
}
if ( apply_filters( 'wcs_initiate_auto_switch', self::can_item_be_switched_by_user( $item, $subscription ), $item, $subscription ) ) {
wp_safe_redirect( add_query_arg( 'auto-switch', 'true', self::get_switch_url( $item_id, $item, $subscription ) ) );
exit;
Request::redirect( add_query_arg( 'auto-switch', 'true', self::get_switch_url( $item_id, $item, $subscription ) ) );
return;
}
}
}
@ -560,6 +570,30 @@ class WC_Subscriptions_Switcher {
echo wp_kses( apply_filters( 'woocommerce_subscriptions_switch_link', $switch_link, $item_id, $item, $subscription ), array( 'a' => array( 'href' => array(), 'title' => array(), 'class' => array() ) ) ); // phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
}
/**
* Add hidden form inputs for subscription switch parameters.
*
* When a customer is switching subscriptions, the switch parameters are passed via URL query arguments.
* This method outputs them as hidden form inputs so they're included when AJAX add-to-cart plugins
* serialize and submit the form via POST.
*
* @since 8.3.0
*/
public static function add_switch_hidden_inputs() {
$switch_subscription_id = Request::get_var( 'switch-subscription' );
$item_id = Request::get_var( 'item' );
$nonce = Request::get_var( '_wcsnonce' );
// Only output if we're in a switch context with a valid nonce
if ( ! $switch_subscription_id || ! $item_id || ! $nonce ) {
return;
}
echo '<input type="hidden" name="switch-subscription" value="' . esc_attr( $switch_subscription_id ) . '" />';
echo '<input type="hidden" name="item" value="' . esc_attr( $item_id ) . '" />';
echo '<input type="hidden" name="_wcsnonce" value="' . esc_attr( $nonce ) . '" />';
}
/**
* The link for switching a subscription - the product page for variable subscriptions, or grouped product page for grouped subscriptions.
*
@ -1382,21 +1416,25 @@ class WC_Subscriptions_Switcher {
try {
if ( ! isset( $_GET['switch-subscription'] ) ) {
$switch_subscription_id = Request::get_var( 'switch-subscription' );
if ( ! $switch_subscription_id ) {
return $is_valid;
}
if ( empty( $_GET['_wcsnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wcsnonce'] ) ), 'wcs_switch_request' ) ) {
$nonce = Request::get_var( '_wcsnonce' );
if ( empty( $nonce ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $nonce ) ), 'wcs_switch_request' ) ) {
return false;
}
$subscription = wcs_get_subscription( absint( $_GET['switch-subscription'] ) );
$subscription = wcs_get_subscription( absint( $switch_subscription_id ) );
if ( ! $subscription ) {
throw new Exception( __( 'The subscription may have been deleted.', 'woocommerce-subscriptions' ) );
}
$item_id = absint( $_GET['item'] );
$item_id = absint( Request::get_var( 'item' ) );
$item = wcs_get_order_item( $item_id, $subscription );
// Prevent switching to non-subscription product
@ -1468,11 +1506,13 @@ class WC_Subscriptions_Switcher {
public static function set_switch_details_in_cart( $cart_item_data, $product_id, $variation_id ) {
try {
if ( ! isset( $_GET['switch-subscription'] ) ) {
$switch_subscription_id = Request::get_var( 'switch-subscription' );
if ( ! $switch_subscription_id ) {
return $cart_item_data;
}
$subscription = wcs_get_subscription( absint( $_GET['switch-subscription'] ) );
$subscription = wcs_get_subscription( absint( $switch_subscription_id ) );
if ( ! $subscription ) {
throw new Exception( __( 'The subscription may have been deleted.', 'woocommerce-subscriptions' ) );
@ -1482,11 +1522,11 @@ class WC_Subscriptions_Switcher {
if ( ! current_user_can( 'switch_shop_subscription', $subscription->get_id() ) ) {
wc_add_notice( __( 'You can not switch this subscription. It appears you do not own the subscription.', 'woocommerce-subscriptions' ), 'error' );
WC()->cart->empty_cart( true );
wp_safe_redirect( get_permalink( $product_id ) );
exit();
Request::redirect( get_permalink( $product_id ) );
return;
}
$item = wcs_get_order_item( absint( $_GET['item'] ), $subscription );
$item = wcs_get_order_item( absint( Request::get_var( 'item' ) ), $subscription );
// Else it's a valid switch
$product = wc_get_product( $item['product_id'] );
@ -1538,7 +1578,7 @@ class WC_Subscriptions_Switcher {
$cart_item_data['subscription_switch'] = array(
'subscription_id' => $subscription->get_id(),
'item_id' => absint( $_GET['item'] ),
'item_id' => absint( Request::get_var( 'item' ) ),
'next_payment_timestamp' => $next_payment_timestamp,
'upgraded_or_downgraded' => '',
);
@ -1549,8 +1589,8 @@ class WC_Subscriptions_Switcher {
wc_add_notice( __( 'There was an error locating the switch details.', 'woocommerce-subscriptions' ), 'error' );
WC()->cart->empty_cart( true );
wp_safe_redirect( get_permalink( wc_get_page_id( 'cart' ) ) );
exit();
Request::redirect( get_permalink( wc_get_page_id( 'cart' ) ) );
return;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,102 @@
<?php
namespace Automattic\WooCommerce_Subscriptions\Internal\Utilities;
/**
* Utilities for handling request-related operations, including test-safe redirects and exits.
*
* @internal This class may be modified, moved or removed in future releases.
*/
class Request {
/**
* Cached POST input variables.
*
* @var array|null
*/
private static ?array $post_vars = null;
/**
* Cached GET input variables.
*
* @var array|null
*/
private static ?array $url_vars = null;
/**
* Exit unless running in test environment.
*
* This method allows tests to run without terminating the PHP process,
* enabling proper code coverage collection and test assertions.
* In production, this behaves exactly like exit().
*
* @since 8.3.0
* @return void
*/
public static function exit() {
if ( defined( 'WCS_ENVIRONMENT_TYPE' ) && 'tests' === WCS_ENVIRONMENT_TYPE ) {
return;
}
exit();
}
/**
* Safe redirect that works in test environment.
*
* In production, this performs a normal wp_safe_redirect() and exits by default.
* In tests, it stores the redirect information in a global variable for assertions
* instead of attempting to send headers (which would fail).
*
* @since 8.3.0
* @param string $location The URL to redirect to.
* @param int $status HTTP status code (default 302).
* @param bool $should_exit Whether to exit after setting redirect headers (default true).
* @return void
*/
public static function redirect( $location, $status = 302, $should_exit = true ) {
if ( defined( 'WCS_ENVIRONMENT_TYPE' ) && 'tests' === WCS_ENVIRONMENT_TYPE ) {
// Store redirect info for test assertions but don't actually redirect
$GLOBALS['wcs_test_redirect'] = array(
'location' => $location,
'status' => $status,
);
return;
}
wp_safe_redirect( $location, $status );
if ( $should_exit ) {
self::exit();
}
}
/**
* Supplies the value of the POST or URL query parameter matching $key, or else returns $default.
*
* Essentially, this is an alternative to inspecting the $_REQUEST super-global and is intended for cases where we
* are interested in a key:value pair, regardless of whether it was sent as a post var or URL query var.
*
* Its advantages are that it only ever examines the POST and GET inputs (POST taking priority, if both contain the
* same key): cookies are always ignored. It also looks directly at the inputs, instead of using the $_POST or $_GET
* superglobals (which can be manipulated).
*
* @since 8.3.0
*
* @param string $key The key to look up.
* @param mixed $default_value The value to return if the key is not found. Defaults to null.
*
* @return mixed
*/
public static function get_var( string $key, $default_value = null ) {
if ( null === self::$post_vars ) {
self::$post_vars = filter_input_array( INPUT_POST ) ?? array();
self::$url_vars = filter_input_array( INPUT_GET ) ?? array();
}
if ( isset( self::$post_vars[ $key ] ) ) {
return self::$post_vars[ $key ];
} elseif ( isset( self::$url_vars[ $key ] ) ) {
return self::$url_vars[ $key ];
}
return $default_value;
}
}

View File

@ -2,6 +2,8 @@
/**
* Outputs a subscription variation's pricing fields for WooCommerce 2.3+
*
* You may also be interested in assets/js/admin/admin.js, which dynamically relocates some of these fields.
*
* @version 1.0.0 - Migrated from WooCommerce Subscriptions v2.2.12
*
* @var int $loop

View File

@ -8,9 +8,9 @@
*/
defined( 'ABSPATH' ) || exit;
$display_heading = true;
foreach ( WC()->cart->get_coupons() as $code => $coupon ) {
$display_heading = true;
foreach ( $recurring_carts as $recurring_cart_key => $recurring_cart ) {
foreach ( $recurring_cart->get_coupons() as $recurring_code => $recurring_coupon ) {
if ( $recurring_code !== $code ) {

View File

@ -12,6 +12,7 @@ return array(
'Automattic\\WooCommerce_Subscriptions\\Internal\\Telemetry\\Orders' => $baseDir . '/src/Internal/Telemetry/Orders.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Telemetry\\Products' => $baseDir . '/src/Internal/Telemetry/Products.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Telemetry\\Subscriptions' => $baseDir . '/src/Internal/Telemetry/Subscriptions.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Utilities\\Request' => $baseDir . '/src/Internal/Utilities/Request.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Utilities\\Scheduled_Actions' => $baseDir . '/src/Internal/Utilities/Scheduled_Actions.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Composer\\Installers\\AglInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AglInstaller.php',
@ -138,6 +139,7 @@ return array(
'WCSG_Template_Loader' => $baseDir . '/includes/gifting/class-wcsg-template-loader.php',
'WCS_Gifting' => $baseDir . '/includes/gifting/class-wcs-gifting.php',
'WC_Subscription_Downloads' => $baseDir . '/includes/downloads/class-wc-subscription-downloads.php',
'WC_Subscription_Downloads_Admin_Welcome_Announcement' => $baseDir . '/includes/downloads/class-wc-subscription-downloads-admin-welcome-announcement.php',
'WC_Subscription_Downloads_Ajax' => $baseDir . '/includes/downloads/class-wc-subscription-downloads-ajax.php',
'WC_Subscription_Downloads_Install' => $baseDir . '/includes/downloads/class-wc-subscription-downloads-install.php',
'WC_Subscription_Downloads_Order' => $baseDir . '/includes/downloads/class-wc-subscription-downloads-order.php',

View File

@ -35,6 +35,7 @@ class ComposerStaticInitcb45de1ca955f89ec737c442c7cf101c
'Automattic\\WooCommerce_Subscriptions\\Internal\\Telemetry\\Orders' => __DIR__ . '/../..' . '/src/Internal/Telemetry/Orders.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Telemetry\\Products' => __DIR__ . '/../..' . '/src/Internal/Telemetry/Products.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Telemetry\\Subscriptions' => __DIR__ . '/../..' . '/src/Internal/Telemetry/Subscriptions.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Utilities\\Request' => __DIR__ . '/../..' . '/src/Internal/Utilities/Request.php',
'Automattic\\WooCommerce_Subscriptions\\Internal\\Utilities\\Scheduled_Actions' => __DIR__ . '/../..' . '/src/Internal/Utilities/Scheduled_Actions.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Composer\\Installers\\AglInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AglInstaller.php',
@ -161,6 +162,7 @@ class ComposerStaticInitcb45de1ca955f89ec737c442c7cf101c
'WCSG_Template_Loader' => __DIR__ . '/../..' . '/includes/gifting/class-wcsg-template-loader.php',
'WCS_Gifting' => __DIR__ . '/../..' . '/includes/gifting/class-wcs-gifting.php',
'WC_Subscription_Downloads' => __DIR__ . '/../..' . '/includes/downloads/class-wc-subscription-downloads.php',
'WC_Subscription_Downloads_Admin_Welcome_Announcement' => __DIR__ . '/../..' . '/includes/downloads/class-wc-subscription-downloads-admin-welcome-announcement.php',
'WC_Subscription_Downloads_Ajax' => __DIR__ . '/../..' . '/includes/downloads/class-wc-subscription-downloads-ajax.php',
'WC_Subscription_Downloads_Install' => __DIR__ . '/../..' . '/includes/downloads/class-wc-subscription-downloads-install.php',
'WC_Subscription_Downloads_Order' => __DIR__ . '/../..' . '/includes/downloads/class-wc-subscription-downloads-order.php',

View File

@ -1,9 +1,9 @@
<?php return array(
'root' => array(
'name' => 'woocommerce/woocommerce-subscriptions',
'pretty_version' => 'dev-release/8.2.0',
'version' => 'dev-release/8.2.0',
'reference' => '649a58503cd3715ecc5d37281a2cbb62fe5ad320',
'pretty_version' => 'dev-release/8.3.0',
'version' => 'dev-release/8.3.0',
'reference' => '38b39c285d0bae952d5746ba460ae914e4ac0f60',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -29,9 +29,9 @@
'dev_requirement' => false,
),
'woocommerce/woocommerce-subscriptions' => array(
'pretty_version' => 'dev-release/8.2.0',
'version' => 'dev-release/8.2.0',
'reference' => '649a58503cd3715ecc5d37281a2cbb62fe5ad320',
'pretty_version' => 'dev-release/8.3.0',
'version' => 'dev-release/8.3.0',
'reference' => '38b39c285d0bae952d5746ba460ae914e4ac0f60',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),

View File

@ -5,11 +5,11 @@
* Description: Sell products and services with recurring payments in your WooCommerce Store.
* Author: WooCommerce
* Author URI: https://woocommerce.com/
* Version: 8.2.0
* Version: 8.3.0
* Requires Plugins: woocommerce
*
* WC requires at least: 10.3.0
* WC tested up to: 10.4.0
* WC tested up to: 10.4.3
* Requires PHP: 7.4
*
* License: GNU General Public License v3.0
@ -84,7 +84,7 @@ class WC_Subscriptions {
public static $plugin_file = __FILE__;
/** @var string */
public static $version = '8.2.0'; // WRCS: DEFINED_VERSION.
public static $version = '8.3.0'; // WRCS: DEFINED_VERSION.
/** @var string */
public static $wc_minimum_supported_version = '7.7';