Cherry pick PR#50805 into trunk (#50814)
This commit is contained in:
parent
d2104e1079
commit
d07cb35247
|
@ -446,6 +446,20 @@ class WC_Settings_Products extends WC_Settings_Page {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
array(
|
||||||
|
'title' => __( 'Count partial downloads', 'woocommerce' ),
|
||||||
|
'desc' => __( 'Count downloads even if only part of a file is fetched.', 'woocommerce' ),
|
||||||
|
'id' => 'woocommerce_downloads_count_partial',
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'default' => 'yes',
|
||||||
|
'desc_tip' => sprintf(
|
||||||
|
/* Translators: 1: opening link tag 2: closing link tag. */
|
||||||
|
__( 'Repeat fetches made within a reasonable window of time (by default, 30 minutes) will not be counted twice. This is a generally reasonably way to enforce download limits in relation to ranged requests. %1$sLearn more.%2$s', 'woocommerce' ),
|
||||||
|
'<a href="https://woocommerce.com/document/digital-downloadable-product-handling/">',
|
||||||
|
'</a>'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
array(
|
array(
|
||||||
'type' => 'sectionend',
|
'type' => 'sectionend',
|
||||||
'id' => 'digital_download_options',
|
'id' => 'digital_download_options',
|
||||||
|
|
|
@ -8,12 +8,19 @@
|
||||||
* @version 2.2.0
|
* @version 2.2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||||
defined( 'ABSPATH' ) || exit;
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download handler class.
|
* Download handler class.
|
||||||
*/
|
*/
|
||||||
class WC_Download_Handler {
|
class WC_Download_Handler {
|
||||||
|
use AccessiblePrivateMethods;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hook used for deferred tracking of partial download attempts.
|
||||||
|
*/
|
||||||
|
public const TRACK_DOWNLOAD_CALLBACK = 'track_partial_download';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook in methods.
|
* Hook in methods.
|
||||||
|
@ -25,6 +32,7 @@ class WC_Download_Handler {
|
||||||
add_action( 'woocommerce_download_file_redirect', array( __CLASS__, 'download_file_redirect' ), 10, 2 );
|
add_action( 'woocommerce_download_file_redirect', array( __CLASS__, 'download_file_redirect' ), 10, 2 );
|
||||||
add_action( 'woocommerce_download_file_xsendfile', array( __CLASS__, 'download_file_xsendfile' ), 10, 2 );
|
add_action( 'woocommerce_download_file_xsendfile', array( __CLASS__, 'download_file_xsendfile' ), 10, 2 );
|
||||||
add_action( 'woocommerce_download_file_force', array( __CLASS__, 'download_file_force' ), 10, 2 );
|
add_action( 'woocommerce_download_file_force', array( __CLASS__, 'download_file_force' ), 10, 2 );
|
||||||
|
self::add_action( self::TRACK_DOWNLOAD_CALLBACK, array( __CLASS__, 'track_download' ), 10, 3 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -135,9 +143,13 @@ class WC_Download_Handler {
|
||||||
// Track the download in logs and change remaining/counts.
|
// Track the download in logs and change remaining/counts.
|
||||||
$current_user_id = get_current_user_id();
|
$current_user_id = get_current_user_id();
|
||||||
$ip_address = WC_Geolocation::get_ip_address();
|
$ip_address = WC_Geolocation::get_ip_address();
|
||||||
if ( ! $download_range['is_range_request'] ) {
|
|
||||||
$download->track_download( $current_user_id > 0 ? $current_user_id : null, ! empty( $ip_address ) ? $ip_address : null );
|
self::track_download(
|
||||||
}
|
$download,
|
||||||
|
$current_user_id > 0 ? $current_user_id : null,
|
||||||
|
! empty( $ip_address ) ? $ip_address : null,
|
||||||
|
$download_range['is_range_request']
|
||||||
|
);
|
||||||
|
|
||||||
self::download( $file_path, $download->get_product_id() );
|
self::download( $file_path, $download->get_product_id() );
|
||||||
}
|
}
|
||||||
|
@ -695,6 +707,76 @@ class WC_Download_Handler {
|
||||||
}
|
}
|
||||||
wp_die( $message, $title, array( 'response' => $status ) ); // WPCS: XSS ok.
|
wp_die( $message, $title, array( 'response' => $status ) ); // WPCS: XSS ok.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes care of tracking download requests, with support for deferring tracking in the case of
|
||||||
|
* partial (ranged request) downloads.
|
||||||
|
*
|
||||||
|
* @param WC_Customer_Download|int $download The download to be tracked.
|
||||||
|
* @param int|null $user_id The user ID, if known.
|
||||||
|
* @param string|null $user_ip_address The download IP address, if known.
|
||||||
|
* @param bool $defer If tracking the download should be deferred.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @throws Exception If the active version of Action Scheduler is less than 3.6.0.
|
||||||
|
*/
|
||||||
|
private static function track_download( $download, $user_id = null, $user_ip_address = null, bool $defer = false ): void {
|
||||||
|
try {
|
||||||
|
// If we were supplied with an integer, convert it to a download object.
|
||||||
|
$download = new WC_Customer_Download( $download );
|
||||||
|
|
||||||
|
// In simple cases, we can track the download immediately.
|
||||||
|
if ( ! $defer ) {
|
||||||
|
$download->track_download( $user_id, $user_ip_address );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Counting of partial downloads may be disabled by the site operator.
|
||||||
|
if ( get_option( 'woocommerce_downloads_count_partial', 'yes' ) !== 'yes' ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines how long the window of time is for tracking unique download attempts, in relation to
|
||||||
|
* partial (ranged) download requests.
|
||||||
|
*
|
||||||
|
* @since 9.2.0
|
||||||
|
*
|
||||||
|
* @param int $window_in_seconds Non-negative number of seconds. Defaults to 1800 (30 minutes).
|
||||||
|
* @param int $download_permission_id References the download permission being tracked.
|
||||||
|
*/
|
||||||
|
$window = absint( apply_filters( 'woocommerce_partial_download_tracking_window', 30 * MINUTE_IN_SECONDS, $download->get_id() ) );
|
||||||
|
|
||||||
|
// If we do not have Action Scheduler 3.6.0+ (this would be an unexpected scenario) then we cannot
|
||||||
|
// track partial downloads, because we require support for unique actions.
|
||||||
|
if ( version_compare( ActionScheduler_Versions::instance()->latest_version(), '3.6.0', '<' ) ) {
|
||||||
|
throw new Exception( 'Support for unique scheduled actions is not currently available.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
as_schedule_single_action(
|
||||||
|
time() + $window,
|
||||||
|
self::TRACK_DOWNLOAD_CALLBACK,
|
||||||
|
array(
|
||||||
|
$download->get_id(),
|
||||||
|
$user_id,
|
||||||
|
$user_ip_address,
|
||||||
|
),
|
||||||
|
'woocommerce',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} catch ( Exception $e ) {
|
||||||
|
wc_get_logger()->error(
|
||||||
|
'There was a problem while tracking a product download.',
|
||||||
|
array(
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'id' => $download->get_id(),
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'ip' => $user_ip_address,
|
||||||
|
'deferred' => $defer ? 'yes' : 'no',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WC_Download_Handler::init();
|
WC_Download_Handler::init();
|
||||||
|
|
|
@ -74,7 +74,7 @@ class WC_Download_Handler_Tests extends \WC_Unit_Test_Case {
|
||||||
$approved_directories->add_approved_directory( 'https://always.trusted' );
|
$approved_directories->add_approved_directory( 'https://always.trusted' );
|
||||||
$approved_directory_rule_id = $approved_directories->add_approved_directory( 'https://new.supplier' );
|
$approved_directory_rule_id = $approved_directories->add_approved_directory( 'https://new.supplier' );
|
||||||
|
|
||||||
$product = WC_Helper_Product::create_downloadable_product(
|
list( $product, $order ) = $this->build_downloadable_product_and_order_one(
|
||||||
array(
|
array(
|
||||||
array(
|
array(
|
||||||
'name' => 'Book 1',
|
'name' => 'Book 1',
|
||||||
|
@ -87,12 +87,7 @@ class WC_Download_Handler_Tests extends \WC_Unit_Test_Case {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
$customer = WC_Helper_Customer::create_customer();
|
|
||||||
$email = 'admin@example.org';
|
$email = 'admin@example.org';
|
||||||
$order = WC_Helper_Order::create_order( $customer->get_id(), $product );
|
|
||||||
$order->set_status( 'completed' );
|
|
||||||
$order->save();
|
|
||||||
|
|
||||||
$product_id = $product->get_id();
|
$product_id = $product->get_id();
|
||||||
$downloads = $product->get_downloads();
|
$downloads = $product->get_downloads();
|
||||||
$download_keys = array_keys( $downloads );
|
$download_keys = array_keys( $downloads );
|
||||||
|
@ -139,6 +134,111 @@ class WC_Download_Handler_Tests extends \WC_Unit_Test_Case {
|
||||||
self::restore_download_handlers();
|
self::restore_download_handlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @testdox The remaining downloads count should iterate accurately.
|
||||||
|
*/
|
||||||
|
public function test_downloads_remaining_count(): void {
|
||||||
|
self::remove_download_handlers();
|
||||||
|
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Ok for unit tests.
|
||||||
|
file_put_contents( WP_CONTENT_DIR . '/uploads/woocommerce_uploads/supersheet-123.ods', str_pad( '', 100 ) );
|
||||||
|
|
||||||
|
list( $product, $order ) = $this->build_downloadable_product_and_order_one(
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'name' => 'Supersheet 123',
|
||||||
|
'file' => content_url( 'uploads/woocommerce_uploads/supersheet-123.ods' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$product_id = $product->get_id();
|
||||||
|
$downloads = $product->get_downloads();
|
||||||
|
$download_keys = array_keys( $downloads );
|
||||||
|
$email = 'admin@example.org';
|
||||||
|
$download = current( WC_Data_Store::load( 'customer-download' )->get_downloads( array( 'product_id' => $product_id ) ) );
|
||||||
|
|
||||||
|
$download->set_downloads_remaining( 10 );
|
||||||
|
$download->save();
|
||||||
|
|
||||||
|
// phpcs:disable WordPress.Security.NonceVerification.Recommended WordPress.Security.ValidatedSanitizedInput.InputNotValidated
|
||||||
|
$_GET = array(
|
||||||
|
'download_file' => $product_id,
|
||||||
|
'order' => $order->get_order_key(),
|
||||||
|
'email' => $email,
|
||||||
|
'uid' => hash( 'sha256', $email ),
|
||||||
|
'key' => $download_keys[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
WC_Download_Handler::download_product();
|
||||||
|
$download = new WC_Customer_Download( $download->get_id() );
|
||||||
|
$this->assertEquals(
|
||||||
|
9,
|
||||||
|
$download->get_downloads_remaining(),
|
||||||
|
'In relation to "normal" download requests, we should see a reduction in the downloads remaining count.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Let's simulate a ranged request (partial download).
|
||||||
|
$_SERVER['HTTP_RANGE'] = 'bytes=10-50';
|
||||||
|
WC_Download_Handler::download_product();
|
||||||
|
$download = new WC_Customer_Download( $download->get_id() );
|
||||||
|
$this->assertEquals(
|
||||||
|
9,
|
||||||
|
$download->get_downloads_remaining(),
|
||||||
|
'In relation to "ranged" (partial) download requests, we should not see an immediate reduction in the downloads remaining count.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Repeat (HTTP_RANGE is still set).
|
||||||
|
WC_Download_Handler::download_product();
|
||||||
|
$download = new WC_Customer_Download( $download->get_id() );
|
||||||
|
$this->assertEquals(
|
||||||
|
9,
|
||||||
|
$download->get_downloads_remaining(),
|
||||||
|
'In relation to "ranged" (partial) download requests, we should not see an immediate reduction in the downloads remaining count.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the deferred download tracking action.
|
||||||
|
$deferred_download_tracker = current(
|
||||||
|
WC_Queue::instance()->search(
|
||||||
|
array( 'hook' => WC_Download_Handler::TRACK_DOWNLOAD_CALLBACK )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Let it do it's thing, and confirm that a further decrement happened (of just 1 unit).
|
||||||
|
do_action_ref_array( $deferred_download_tracker->get_hook(), $deferred_download_tracker->get_args() );
|
||||||
|
$download = new WC_Customer_Download( $download->get_id() );
|
||||||
|
$this->assertEquals(
|
||||||
|
8,
|
||||||
|
$download->get_downloads_remaining(),
|
||||||
|
'In relation to "ranged" (partial) download requests, the deferred update to the downloads remaining count functioned as expected.'
|
||||||
|
);
|
||||||
|
|
||||||
|
self::restore_download_handlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a downloadable product, and then places (and completes) an order for that
|
||||||
|
* object.
|
||||||
|
*
|
||||||
|
* @param array[] $downloadable_files Array of arrays, with each inner array specifying the 'name' and 'file'.
|
||||||
|
*
|
||||||
|
* @return array {
|
||||||
|
* WC_Product,
|
||||||
|
* WC_Order
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private function build_downloadable_product_and_order_one( array $downloadable_files ): array {
|
||||||
|
$product = WC_Helper_Product::create_downloadable_product( $downloadable_files );
|
||||||
|
$customer = WC_Helper_Customer::create_customer();
|
||||||
|
$order = WC_Helper_Order::create_order( $customer->get_id(), $product );
|
||||||
|
$order->set_status( 'completed' );
|
||||||
|
$order->save();
|
||||||
|
|
||||||
|
return array(
|
||||||
|
$product,
|
||||||
|
$order,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister download handlers to prevent unwanted output and side-effects.
|
* Unregister download handlers to prevent unwanted output and side-effects.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -148,6 +148,7 @@ class WC_Settings_Products_Test extends WC_Settings_Unit_Test_Case {
|
||||||
'woocommerce_downloads_grant_access_after_payment' => 'checkbox',
|
'woocommerce_downloads_grant_access_after_payment' => 'checkbox',
|
||||||
'woocommerce_downloads_add_hash_to_filename' => 'checkbox',
|
'woocommerce_downloads_add_hash_to_filename' => 'checkbox',
|
||||||
'woocommerce_downloads_deliver_inline' => 'checkbox',
|
'woocommerce_downloads_deliver_inline' => 'checkbox',
|
||||||
|
'woocommerce_downloads_count_partial' => 'checkbox',
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertEquals( $expected, $settings_ids_and_types );
|
$this->assertEquals( $expected, $settings_ids_and_types );
|
||||||
|
|
Loading…
Reference in New Issue