2013-08-09 16:11:15 +00:00
< ? php
/**
2018-03-22 17:12:36 +00:00
* Download handler
2013-08-09 16:11:15 +00:00
*
* Handle digital downloads .
*
2020-08-05 16:36:24 +00:00
* @ package WooCommerce\Classes
2018-03-22 17:12:36 +00:00
* @ version 2.2 . 0
*/
defined ( 'ABSPATH' ) || exit ;
/**
* Download handler class .
2013-08-09 16:11:15 +00:00
*/
class WC_Download_Handler {
/**
2015-11-03 13:31:20 +00:00
* Hook in methods .
2013-08-09 16:11:15 +00:00
*/
2014-05-28 13:52:50 +00:00
public static function init () {
2018-03-22 17:12:36 +00:00
if ( isset ( $_GET [ 'download_file' ], $_GET [ 'order' ] ) && ( isset ( $_GET [ 'email' ] ) || isset ( $_GET [ 'uid' ] ) ) ) { // WPCS: input var ok, CSRF ok.
2014-10-27 12:25:05 +00:00
add_action ( 'init' , array ( __CLASS__ , 'download_product' ) );
}
2018-05-08 09:47:05 +00:00
add_action ( 'woocommerce_download_file_redirect' , array ( __CLASS__ , 'download_file_redirect' ), 10 , 2 );
add_action ( 'woocommerce_download_file_xsendfile' , array ( __CLASS__ , 'download_file_xsendfile' ), 10 , 2 );
add_action ( 'woocommerce_download_file_force' , array ( __CLASS__ , 'download_file_force' ), 10 , 2 );
2013-08-09 16:11:15 +00:00
}
/**
2015-11-03 13:31:20 +00:00
* Check if we need to download a file and check validity .
2013-08-09 16:11:15 +00:00
*/
2014-05-28 13:52:50 +00:00
public static function download_product () {
2020-01-14 11:26:56 +00:00
$product_id = absint ( $_GET [ 'download_file' ] ); // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.VIP.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
2018-03-22 17:12:36 +00:00
$product = wc_get_product ( $product_id );
$data_store = WC_Data_Store :: load ( 'customer-download' );
2013-08-09 16:11:15 +00:00
2018-03-22 17:12:36 +00:00
if ( ! $product || empty ( $_GET [ 'key' ] ) || empty ( $_GET [ 'order' ] ) ) { // WPCS: input var ok, CSRF ok.
2014-10-27 12:25:05 +00:00
self :: download_error ( __ ( 'Invalid download link.' , 'woocommerce' ) );
}
2013-08-09 16:11:15 +00:00
2018-02-13 13:49:47 +00:00
// Fallback, accept email address if it's passed.
2018-03-22 17:12:36 +00:00
if ( empty ( $_GET [ 'email' ] ) && empty ( $_GET [ 'uid' ] ) ) { // WPCS: input var ok, CSRF ok.
2018-02-13 13:49:47 +00:00
self :: download_error ( __ ( 'Invalid download link.' , 'woocommerce' ) );
}
2020-01-24 18:00:55 +00:00
$order_id = wc_get_order_id_by_order_key ( wc_clean ( wp_unslash ( $_GET [ 'order' ] ) ) ); // WPCS: input var ok, CSRF ok.
$order = wc_get_order ( $order_id );
2018-03-22 17:12:36 +00:00
if ( isset ( $_GET [ 'email' ] ) ) { // WPCS: input var ok, CSRF ok.
$email_address = wp_unslash ( $_GET [ 'email' ] ); // WPCS: input var ok, CSRF ok, sanitization ok.
2018-02-13 13:49:47 +00:00
} else {
// Get email address from order to verify hash.
2018-02-21 21:39:43 +00:00
$email_address = is_a ( $order , 'WC_Order' ) ? $order -> get_billing_email () : null ;
2018-02-13 13:49:47 +00:00
2018-03-07 15:13:40 +00:00
// Prepare email address hash.
$email_hash = function_exists ( 'hash' ) ? hash ( 'sha256' , $email_address ) : sha1 ( $email_address );
2018-03-22 17:12:36 +00:00
if ( is_null ( $email_address ) || ! hash_equals ( wp_unslash ( $_GET [ 'uid' ] ), $email_hash ) ) { // WPCS: input var ok, CSRF ok, sanitization ok.
2018-02-13 13:49:47 +00:00
self :: download_error ( __ ( 'Invalid download link.' , 'woocommerce' ) );
}
}
2018-03-22 17:12:36 +00:00
$download_ids = $data_store -> get_downloads (
array (
'user_email' => sanitize_email ( str_replace ( ' ' , '+' , $email_address ) ),
'order_key' => wc_clean ( wp_unslash ( $_GET [ 'order' ] ) ), // WPCS: input var ok, CSRF ok.
'product_id' => $product_id ,
'download_id' => wc_clean ( preg_replace ( '/\s+/' , ' ' , wp_unslash ( $_GET [ 'key' ] ) ) ), // WPCS: input var ok, CSRF ok, sanitization ok.
'orderby' => 'downloads_remaining' ,
'order' => 'DESC' ,
'limit' => 1 ,
'return' => 'ids' ,
)
);
2013-08-09 16:11:15 +00:00
2016-11-18 17:13:02 +00:00
if ( empty ( $download_ids ) ) {
self :: download_error ( __ ( 'Invalid download link.' , 'woocommerce' ) );
2014-10-24 17:21:17 +00:00
}
2013-08-09 16:11:15 +00:00
2016-11-18 17:13:02 +00:00
$download = new WC_Customer_Download ( current ( $download_ids ) );
2020-01-24 18:00:55 +00:00
/**
* Filter download filepath .
*
* @ since 4.0 . 0
* @ param string $file_path File path .
* @ param string $email_address Email address .
* @ param WC_Order | bool $order Order object or false .
* @ param WC_Product $product Product object .
* @ param WC_Customer_Download $download Download data .
*/
$file_path = apply_filters (
'woocommerce_download_product_filepath' ,
$product -> get_file_download_path ( $download -> get_download_id () ),
$email_address ,
$order ,
$product ,
$download
);
2018-05-09 14:34:58 +00:00
$parsed_file_path = self :: parse_file_path ( $file_path );
2018-05-24 15:10:45 +00:00
$download_range = self :: get_download_range ( @ filesize ( $parsed_file_path [ 'file_path' ] ) ); // @codingStandardsIgnoreLine.
2018-05-09 14:34:58 +00:00
2016-11-18 17:13:02 +00:00
self :: check_order_is_valid ( $download );
2018-05-09 14:34:58 +00:00
if ( ! $download_range [ 'is_range_request' ] ) {
// If the remaining download count goes to 0, allow range requests to be able to finish streaming from iOS devices.
self :: check_downloads_remaining ( $download );
}
2016-11-18 17:13:02 +00:00
self :: check_download_expiry ( $download );
self :: check_download_login_required ( $download );
do_action (
'woocommerce_download_product' ,
$download -> get_user_email (),
$download -> get_order_key (),
$download -> get_product_id (),
$download -> get_user_id (),
$download -> get_download_id (),
$download -> get_order_id ()
);
2017-08-23 03:15:23 +00:00
$download -> save ();
2017-07-30 22:38:17 +00:00
2017-08-23 03:15:23 +00:00
// Track the download in logs and change remaining/counts.
2018-05-24 15:10:45 +00:00
$current_user_id = get_current_user_id ();
$ip_address = WC_Geolocation :: get_ip_address ();
2018-05-09 13:52:02 +00:00
if ( ! $download_range [ 'is_range_request' ] ) {
$download -> track_download ( $current_user_id > 0 ? $current_user_id : null , ! empty ( $ip_address ) ? $ip_address : null );
}
2016-05-05 08:28:36 +00:00
2018-05-08 09:47:05 +00:00
self :: download ( $file_path , $download -> get_product_id () );
2014-10-27 10:33:30 +00:00
}
/**
2015-11-03 13:31:20 +00:00
* Check if an order is valid for downloading from .
2018-03-22 17:12:36 +00:00
*
* @ param WC_Customer_Download $download Download instance .
2014-10-27 10:33:30 +00:00
*/
2016-11-18 17:13:02 +00:00
private static function check_order_is_valid ( $download ) {
2018-03-22 17:12:36 +00:00
if ( $download -> get_order_id () ) {
$order = wc_get_order ( $download -> get_order_id () );
if ( $order && ! $order -> is_download_permitted () ) {
self :: download_error ( __ ( 'Invalid order.' , 'woocommerce' ), '' , 403 );
}
2014-10-24 17:21:17 +00:00
}
}
2013-08-09 16:11:15 +00:00
2014-10-27 10:33:30 +00:00
/**
2015-11-03 13:31:20 +00:00
* Check if there are downloads remaining .
2018-03-22 17:12:36 +00:00
*
* @ param WC_Customer_Download $download Download instance .
2014-10-27 10:33:30 +00:00
*/
2016-11-18 17:13:02 +00:00
private static function check_downloads_remaining ( $download ) {
2017-01-03 15:03:55 +00:00
if ( '' !== $download -> get_downloads_remaining () && 0 >= $download -> get_downloads_remaining () ) {
2014-10-27 10:33:30 +00:00
self :: download_error ( __ ( 'Sorry, you have reached your download limit for this file' , 'woocommerce' ), '' , 403 );
}
}
/**
2015-11-03 13:31:20 +00:00
* Check if the download has expired .
2018-03-22 17:12:36 +00:00
*
* @ param WC_Customer_Download $download Download instance .
2014-10-27 10:33:30 +00:00
*/
2016-11-18 17:13:02 +00:00
private static function check_download_expiry ( $download ) {
2020-01-24 18:01:12 +00:00
if ( ! is_null ( $download -> get_access_expires () ) && $download -> get_access_expires () -> getTimestamp () < strtotime ( 'midnight' , time () ) ) {
2014-10-27 10:33:30 +00:00
self :: download_error ( __ ( 'Sorry, this download has expired' , 'woocommerce' ), '' , 403 );
}
}
/**
2015-11-03 13:31:20 +00:00
* Check if a download requires the user to login first .
2018-03-22 17:12:36 +00:00
*
* @ param WC_Customer_Download $download Download instance .
2014-10-27 10:33:30 +00:00
*/
2016-11-18 17:13:02 +00:00
private static function check_download_login_required ( $download ) {
if ( $download -> get_user_id () && 'yes' === get_option ( 'woocommerce_downloads_require_login' ) ) {
2014-10-27 13:38:24 +00:00
if ( ! is_user_logged_in () ) {
if ( wc_get_page_id ( 'myaccount' ) ) {
2018-03-22 17:12:36 +00:00
wp_safe_redirect ( add_query_arg ( 'wc_error' , rawurlencode ( __ ( 'You must be logged in to download files.' , 'woocommerce' ) ), wc_get_page_permalink ( 'myaccount' ) ) );
2014-10-27 13:38:24 +00:00
exit ;
} else {
2015-02-15 19:13:22 +00:00
self :: download_error ( __ ( 'You must be logged in to download files.' , 'woocommerce' ) . ' <a href="' . esc_url ( wp_login_url ( wc_get_page_permalink ( 'myaccount' ) ) ) . '" class="wc-forward">' . __ ( 'Login' , 'woocommerce' ) . '</a>' , __ ( 'Log in to Download Files' , 'woocommerce' ), 403 );
2014-10-27 13:38:24 +00:00
}
2016-11-18 17:13:02 +00:00
} elseif ( ! current_user_can ( 'download_file' , $download ) ) {
2014-10-27 10:33:30 +00:00
self :: download_error ( __ ( 'This is not your download link.' , 'woocommerce' ), '' , 403 );
}
}
}
2014-10-24 17:21:17 +00:00
/**
2018-03-22 17:12:36 +00:00
* Count download .
2017-05-15 11:50:52 +00:00
*
2020-07-16 20:13:08 +00:00
* @ deprecated 4.4 . 0
2018-03-22 17:12:36 +00:00
* @ param array $download_data Download data .
2014-10-24 17:21:17 +00:00
*/
2020-07-16 20:13:08 +00:00
public static function count_download ( $download_data ) {
wc_deprecated_function ( 'WC_Download_Handler::count_download' , '4.4.0' , '' );
}
2013-08-09 16:11:15 +00:00
/**
* Download a file - hook into init function .
2018-03-22 17:12:36 +00:00
*
2018-05-08 09:47:05 +00:00
* @ param string $file_path URL to file .
* @ param integer $product_id Product ID of the product being downloaded .
2013-08-09 16:11:15 +00:00
*/
2018-05-08 09:47:05 +00:00
public static function download ( $file_path , $product_id ) {
2014-02-26 11:54:16 +00:00
if ( ! $file_path ) {
2014-10-24 21:50:19 +00:00
self :: download_error ( __ ( 'No file defined' , 'woocommerce' ) );
2014-02-26 11:54:16 +00:00
}
2013-08-09 16:11:15 +00:00
2014-10-24 17:21:17 +00:00
$filename = basename ( $file_path );
if ( strstr ( $filename , '?' ) ) {
$filename = current ( explode ( '?' , $filename ) );
2013-08-09 16:11:15 +00:00
}
2020-08-17 21:01:41 +00:00
$filename = apply_filters ( 'woocommerce_file_download_filename' , $filename , $product_id );
2020-07-28 18:28:15 +00:00
/**
* Filter download method .
2020-08-17 21:01:41 +00:00
*
* @ since 4.5 . 0
2020-07-28 18:28:15 +00:00
* @ param string $method Download method .
* @ param int $product_id Product ID .
* @ param string $file_path URL to file .
*/
2020-07-28 16:04:39 +00:00
$file_download_method = apply_filters ( 'woocommerce_file_download_method' , get_option ( 'woocommerce_file_download_method' , 'force' ), $product_id , $file_path );
2014-10-24 17:21:17 +00:00
2018-03-22 17:12:36 +00:00
// Add action to prevent issues in IE.
2014-10-27 11:01:16 +00:00
add_action ( 'nocache_headers' , array ( __CLASS__ , 'ie_nocache_headers_fix' ) );
2018-03-22 17:12:36 +00:00
// Trigger download via one of the methods.
2018-05-08 09:47:05 +00:00
do_action ( 'woocommerce_download_file_' . $file_download_method , $file_path , $filename );
2014-10-24 17:21:17 +00:00
}
/**
2015-11-03 13:31:20 +00:00
* Redirect to a file to start the download .
2018-03-22 17:12:36 +00:00
*
2018-05-08 09:47:05 +00:00
* @ param string $file_path File path .
* @ param string $filename File name .
2014-10-24 17:21:17 +00:00
*/
2018-05-08 09:47:05 +00:00
public static function download_file_redirect ( $file_path , $filename = '' ) {
2014-10-24 17:21:17 +00:00
header ( 'Location: ' . $file_path );
exit ;
}
/**
2015-11-03 13:31:20 +00:00
* Parse file path and see if its remote or local .
2018-03-22 17:12:36 +00:00
*
* @ param string $file_path File path .
2014-10-24 17:21:17 +00:00
* @ return array
*/
public static function parse_file_path ( $file_path ) {
2014-10-27 13:38:24 +00:00
$wp_uploads = wp_upload_dir ();
$wp_uploads_dir = $wp_uploads [ 'basedir' ];
$wp_uploads_url = $wp_uploads [ 'baseurl' ];
2017-07-21 10:11:17 +00:00
/**
* Replace uploads dir , site url etc with absolute counterparts if we can .
* Note the str_replace on site_url is on purpose , so if https is forced
* via filters we can still do the string replacement on a HTTP file .
*/
2014-10-27 13:38:24 +00:00
$replacements = array (
2018-03-22 17:12:36 +00:00
$wp_uploads_url => $wp_uploads_dir ,
network_site_url ( '/' , 'https' ) => ABSPATH ,
2017-07-21 10:11:17 +00:00
str_replace ( 'https:' , 'http:' , network_site_url ( '/' , 'http' ) ) => ABSPATH ,
2018-03-22 17:12:36 +00:00
site_url ( '/' , 'https' ) => ABSPATH ,
str_replace ( 'https:' , 'http:' , site_url ( '/' , 'http' ) ) => ABSPATH ,
2014-10-27 13:38:24 +00:00
);
2013-08-09 16:11:15 +00:00
2014-10-27 13:38:24 +00:00
$file_path = str_replace ( array_keys ( $replacements ), array_values ( $replacements ), $file_path );
2018-03-22 17:12:36 +00:00
$parsed_file_path = wp_parse_url ( $file_path );
2014-10-27 13:38:24 +00:00
$remote_file = true ;
2014-04-07 14:09:11 +00:00
2018-08-31 20:26:15 +00:00
// Paths that begin with '//' are always remote URLs.
if ( '//' === substr ( $file_path , 0 , 2 ) ) {
return array (
'remote_file' => true ,
2018-09-27 19:27:15 +00:00
'file_path' => is_ssl () ? 'https:' . $file_path : 'http:' . $file_path ,
2018-08-31 20:26:15 +00:00
);
}
2018-03-22 17:12:36 +00:00
// See if path needs an abspath prepended to work.
2018-08-31 20:26:15 +00:00
if ( file_exists ( ABSPATH . $file_path ) ) {
2014-04-07 14:09:11 +00:00
$remote_file = false ;
2014-10-27 13:38:24 +00:00
$file_path = ABSPATH . $file_path ;
2014-05-25 21:10:23 +00:00
2017-03-24 11:48:32 +00:00
} elseif ( '/wp-content' === substr ( $file_path , 0 , 11 ) ) {
$remote_file = false ;
$file_path = realpath ( WP_CONTENT_DIR . substr ( $file_path , 11 ) );
2018-03-22 17:12:36 +00:00
// Check if we have an absolute path.
} elseif ( ( ! isset ( $parsed_file_path [ 'scheme' ] ) || ! in_array ( $parsed_file_path [ 'scheme' ], array ( 'http' , 'https' , 'ftp' ), true ) ) && isset ( $parsed_file_path [ 'path' ] ) && file_exists ( $parsed_file_path [ 'path' ] ) ) {
2013-08-09 16:11:15 +00:00
$remote_file = false ;
2014-10-27 13:38:24 +00:00
$file_path = $parsed_file_path [ 'path' ];
2014-04-07 14:09:11 +00:00
}
2013-08-09 16:11:15 +00:00
2014-10-24 17:21:17 +00:00
return array (
'remote_file' => $remote_file ,
2016-08-27 01:46:45 +00:00
'file_path' => $file_path ,
2014-10-24 17:21:17 +00:00
);
}
/**
2015-11-03 13:31:20 +00:00
* Download a file using X - Sendfile , X - Lighttpd - Sendfile , or X - Accel - Redirect if available .
2018-03-22 17:12:36 +00:00
*
2018-05-08 09:47:05 +00:00
* @ param string $file_path File path .
* @ param string $filename File name .
2014-10-24 17:21:17 +00:00
*/
2018-05-08 09:47:05 +00:00
public static function download_file_xsendfile ( $file_path , $filename ) {
2014-10-24 21:50:19 +00:00
$parsed_file_path = self :: parse_file_path ( $file_path );
2020-01-14 11:26:56 +00:00
/**
* Fallback on force download method for remote files . This is because :
* 1. xsendfile needs proxy configuration to work for remote files , which cannot be assumed to be available on most hosts .
2021-06-22 06:54:47 +00:00
* 2. Force download method is more secure than redirect method if `allow_url_fopen` is enabled in `php.ini` .
2020-01-14 11:26:56 +00:00
*/
if ( $parsed_file_path [ 'remote_file' ] && ! apply_filters ( 'woocommerce_use_xsendfile_for_remote' , false ) ) {
do_action ( 'woocommerce_download_file_force' , $file_path , $filename );
return ;
}
2018-03-22 17:12:36 +00:00
if ( function_exists ( 'apache_get_modules' ) && in_array ( 'mod_xsendfile' , apache_get_modules (), true ) ) {
2015-07-27 11:44:29 +00:00
self :: download_headers ( $parsed_file_path [ 'file_path' ], $filename );
2019-05-24 23:10:17 +00:00
$filepath = apply_filters ( 'woocommerce_download_file_xsendfile_file_path' , $parsed_file_path [ 'file_path' ], $file_path , $filename , $parsed_file_path );
header ( 'X-Sendfile: ' . $filepath );
2014-10-24 17:21:17 +00:00
exit ;
} elseif ( stristr ( getenv ( 'SERVER_SOFTWARE' ), 'lighttpd' ) ) {
2015-07-27 11:44:29 +00:00
self :: download_headers ( $parsed_file_path [ 'file_path' ], $filename );
2019-05-24 23:10:17 +00:00
$filepath = apply_filters ( 'woocommerce_download_file_xsendfile_lighttpd_file_path' , $parsed_file_path [ 'file_path' ], $file_path , $filename , $parsed_file_path );
2019-05-25 00:15:24 +00:00
header ( 'X-Lighttpd-Sendfile: ' . $filepath );
2014-10-24 17:21:17 +00:00
exit ;
} elseif ( stristr ( getenv ( 'SERVER_SOFTWARE' ), 'nginx' ) || stristr ( getenv ( 'SERVER_SOFTWARE' ), 'cherokee' ) ) {
2015-07-27 11:44:29 +00:00
self :: download_headers ( $parsed_file_path [ 'file_path' ], $filename );
$xsendfile_path = trim ( preg_replace ( '`^' . str_replace ( '\\' , '/' , getcwd () ) . '`' , '' , $parsed_file_path [ 'file_path' ] ), '/' );
2019-05-24 23:10:17 +00:00
$xsendfile_path = apply_filters ( 'woocommerce_download_file_xsendfile_x_accel_redirect_file_path' , $xsendfile_path , $file_path , $filename , $parsed_file_path );
2014-10-24 17:21:17 +00:00
header ( " X-Accel-Redirect: / $xsendfile_path " );
exit ;
}
2018-03-22 17:12:36 +00:00
// Fallback.
2021-07-15 22:40:21 +00:00
wc_get_logger () -> warning (
sprintf (
/* translators: %1$s contains the filepath of the digital asset. */
__ ( '%1$s could not be served using the X-Accel-Redirect/X-Sendfile method. A Force Download will be used instead.' , 'woocommerce' ),
$file_path
)
);
2018-05-08 09:47:05 +00:00
self :: download_file_force ( $file_path , $filename );
2014-10-24 17:21:17 +00:00
}
2018-05-07 08:05:02 +00:00
/**
2018-05-07 21:56:12 +00:00
* Parse the HTTP_RANGE request from iOS devices .
* Does not support multi - range requests .
2018-05-07 08:05:02 +00:00
*
2018-05-09 09:40:02 +00:00
* @ param int $file_size Size of file in bytes .
* @ return array {
* Information about range download request : beginning and length of
* file chunk , whether the range is valid / supported and whether the request is a range request .
*
* @ type int $start Byte offset of the beginning of the range . Default 0.
* @ type int $length Length of the requested file chunk in bytes . Optional .
* @ type bool $is_range_valid Whether the requested range is a valid and supported range .
* @ type bool $is_range_request Whether the request is a range request .
* }
2018-05-07 08:05:02 +00:00
*/
2018-05-07 21:56:12 +00:00
protected static function get_download_range ( $file_size ) {
2018-05-24 15:10:45 +00:00
$start = 0 ;
2018-05-07 21:56:12 +00:00
$download_range = array (
'start' => $start ,
'is_range_valid' => false ,
'is_range_request' => false ,
);
if ( ! $file_size ) {
return $download_range ;
}
2018-05-24 15:10:45 +00:00
$end = $file_size - 1 ;
2018-05-07 21:56:12 +00:00
$download_range [ 'length' ] = $file_size ;
2018-05-07 08:05:02 +00:00
2018-05-24 15:10:45 +00:00
if ( isset ( $_SERVER [ 'HTTP_RANGE' ] ) ) { // @codingStandardsIgnoreLine.
$http_range = sanitize_text_field ( wp_unslash ( $_SERVER [ 'HTTP_RANGE' ] ) ); // WPCS: input var ok.
2018-05-07 21:56:12 +00:00
$download_range [ 'is_range_request' ] = true ;
2018-05-07 08:05:02 +00:00
$c_start = $start ;
$c_end = $end ;
// Extract the range string.
2018-05-09 09:13:36 +00:00
list ( , $range ) = explode ( '=' , $http_range , 2 );
2018-05-07 08:05:02 +00:00
// Make sure the client hasn't sent us a multibyte range.
if ( strpos ( $range , ',' ) !== false ) {
2018-05-07 21:56:12 +00:00
return $download_range ;
2018-05-07 08:05:02 +00:00
}
2018-05-09 09:40:02 +00:00
/*
* If the range starts with an '-' we start from the beginning .
* If not , we forward the file pointer
* and make sure to get the end byte if specified .
*/
2018-05-07 08:05:02 +00:00
if ( '-' === $range [ 0 ] ) {
// The n-number of the last bytes is requested.
2018-05-07 21:56:12 +00:00
$c_start = $file_size - substr ( $range , 1 );
2018-05-07 08:05:02 +00:00
} else {
2018-05-07 21:56:12 +00:00
$range = explode ( '-' , $range );
$c_start = ( isset ( $range [ 0 ] ) && is_numeric ( $range [ 0 ] ) ) ? ( int ) $range [ 0 ] : 0 ;
$c_end = ( isset ( $range [ 1 ] ) && is_numeric ( $range [ 1 ] ) ) ? ( int ) $range [ 1 ] : $file_size ;
2018-05-07 08:05:02 +00:00
}
2018-05-09 09:40:02 +00:00
/*
* Check the range and make sure it ' s treated according to the specs : http :// www . w3 . org / Protocols / rfc2616 / rfc2616 - sec14 . html .
* End bytes can not be larger than $end .
*/
2018-05-07 08:05:02 +00:00
$c_end = ( $c_end > $end ) ? $end : $c_end ;
// Validate the requested range and return an error if it's not correct.
2018-05-07 21:56:12 +00:00
if ( $c_start > $c_end || $c_start > $file_size - 1 || $c_end >= $file_size ) {
return $download_range ;
2018-05-07 08:05:02 +00:00
}
$start = $c_start ;
$end = $c_end ;
$length = $end - $start + 1 ;
2018-05-24 15:10:45 +00:00
$download_range [ 'start' ] = $start ;
$download_range [ 'length' ] = $length ;
2018-05-07 21:56:12 +00:00
$download_range [ 'is_range_valid' ] = true ;
2018-05-07 08:05:02 +00:00
}
2018-05-07 21:56:12 +00:00
return $download_range ;
2018-05-07 08:05:02 +00:00
}
2014-10-24 17:21:17 +00:00
/**
2015-11-03 13:31:20 +00:00
* Force download - this is the default method .
2018-03-22 17:12:36 +00:00
*
2018-05-09 09:40:02 +00:00
* @ param string $file_path File path .
* @ param string $filename File name .
2014-10-24 17:21:17 +00:00
*/
2018-05-08 09:47:05 +00:00
public static function download_file_force ( $file_path , $filename ) {
2014-10-24 21:50:19 +00:00
$parsed_file_path = self :: parse_file_path ( $file_path );
2018-05-24 15:10:45 +00:00
$download_range = self :: get_download_range ( @ filesize ( $parsed_file_path [ 'file_path' ] ) ); // @codingStandardsIgnoreLine.
2014-10-24 21:50:19 +00:00
2018-05-07 21:56:12 +00:00
self :: download_headers ( $parsed_file_path [ 'file_path' ], $filename , $download_range );
2018-05-08 09:47:05 +00:00
2018-05-07 21:56:12 +00:00
$start = isset ( $download_range [ 'start' ] ) ? $download_range [ 'start' ] : 0 ;
$length = isset ( $download_range [ 'length' ] ) ? $download_range [ 'length' ] : 0 ;
2018-05-07 08:05:02 +00:00
if ( ! self :: readfile_chunked ( $parsed_file_path [ 'file_path' ], $start , $length ) ) {
2021-07-15 22:40:21 +00:00
if ( $parsed_file_path [ 'remote_file' ] ) {
wc_get_logger () -> warning (
sprintf (
/* translators: %1$s contains the filepath of the digital asset. */
__ ( '%1$s could not be served using the Force Download method. A redirect will be used instead.' , 'woocommerce' ),
$file_path
)
);
self :: download_file_redirect ( $file_path );
} else {
self :: download_error ( __ ( 'File not found' , 'woocommerce' ) );
}
2013-08-09 16:11:15 +00:00
}
2014-10-24 17:21:17 +00:00
exit ;
}
/**
2015-11-03 13:31:20 +00:00
* Get content type of a download .
2018-03-22 17:12:36 +00:00
*
* @ param string $file_path File path .
2014-10-24 17:21:17 +00:00
* @ return string
*/
2014-10-27 11:01:16 +00:00
private static function get_download_content_type ( $file_path ) {
2018-03-22 17:12:36 +00:00
$file_extension = strtolower ( substr ( strrchr ( $file_path , '.' ), 1 ) );
$ctype = 'application/force-download' ;
2013-08-09 16:11:15 +00:00
foreach ( get_allowed_mime_types () as $mime => $type ) {
$mimes = explode ( '|' , $mime );
2018-03-22 17:12:36 +00:00
if ( in_array ( $file_extension , $mimes , true ) ) {
2013-08-09 16:11:15 +00:00
$ctype = $type ;
break ;
}
}
2014-10-24 17:21:17 +00:00
return $ctype ;
}
/**
2015-11-03 13:31:20 +00:00
* Set headers for the download .
2018-03-22 17:12:36 +00:00
*
2018-05-07 21:56:12 +00:00
* @ param string $file_path File path .
* @ param string $filename File name .
2018-05-09 09:40:02 +00:00
* @ param array $download_range Array containing info about range download request ( see { @ see get_download_range } for structure ) .
2014-10-24 17:21:17 +00:00
*/
2018-05-07 21:56:12 +00:00
private static function download_headers ( $file_path , $filename , $download_range = array () ) {
2014-10-27 11:01:16 +00:00
self :: check_server_config ();
self :: clean_buffers ();
2017-11-08 15:07:00 +00:00
wc_nocache_headers ();
2014-10-27 11:01:16 +00:00
2018-03-22 17:12:36 +00:00
header ( 'X-Robots-Tag: noindex, nofollow' , true );
header ( 'Content-Type: ' . self :: get_download_content_type ( $file_path ) );
header ( 'Content-Description: File Transfer' );
header ( 'Content-Disposition: attachment; filename="' . $filename . '";' );
header ( 'Content-Transfer-Encoding: binary' );
2014-10-27 11:01:16 +00:00
2018-05-09 12:29:52 +00:00
$file_size = @ filesize ( $file_path ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
if ( ! $file_size ) {
return ;
}
2018-05-07 21:56:12 +00:00
if ( isset ( $download_range [ 'is_range_request' ] ) && true === $download_range [ 'is_range_request' ] ) {
if ( false === $download_range [ 'is_range_valid' ] ) {
header ( 'HTTP/1.1 416 Requested Range Not Satisfiable' );
2018-05-09 12:29:52 +00:00
header ( 'Content-Range: bytes 0-' . ( $file_size - 1 ) . '/' . $file_size );
2018-05-07 21:56:12 +00:00
exit ;
}
$start = $download_range [ 'start' ];
$end = $download_range [ 'start' ] + $download_range [ 'length' ] - 1 ;
$length = $download_range [ 'length' ];
header ( 'HTTP/1.1 206 Partial Content' );
2018-05-09 12:29:52 +00:00
header ( " Accept-Ranges: 0- $file_size " );
header ( " Content-Range: bytes $start - $end / $file_size " );
2018-05-07 21:56:12 +00:00
header ( " Content-Length: $length " );
} else {
2018-05-09 12:29:52 +00:00
header ( 'Content-Length: ' . $file_size );
2016-07-11 14:56:35 +00:00
}
2014-10-27 11:01:16 +00:00
}
/**
* Check and set certain server config variables to ensure downloads work as intended .
*/
private static function check_server_config () {
2016-06-06 15:55:27 +00:00
wc_set_time_limit ( 0 );
2014-02-26 11:54:16 +00:00
if ( function_exists ( 'apache_setenv' ) ) {
2018-03-22 17:12:36 +00:00
@ apache_setenv ( 'no-gzip' , 1 ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_apache_setenv
2014-02-26 11:54:16 +00:00
}
2018-03-22 17:12:36 +00:00
@ ini_set ( 'zlib.output_compression' , 'Off' ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_ini_set
@ session_write_close (); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.VIP.SessionFunctionsUsage.session_session_write_close
2014-10-27 11:01:16 +00:00
}
2013-08-09 16:11:15 +00:00
2014-10-27 11:01:16 +00:00
/**
2015-11-03 13:31:20 +00:00
* Clean all output buffers .
2014-10-27 11:01:16 +00:00
*
2015-11-03 13:31:20 +00:00
* Can prevent errors , for example : transfer closed with 3 bytes remaining to read .
2014-10-27 11:01:16 +00:00
*/
private static function clean_buffers () {
2014-11-12 16:15:47 +00:00
if ( ob_get_level () ) {
$levels = ob_get_level ();
for ( $i = 0 ; $i < $levels ; $i ++ ) {
2018-03-22 17:12:36 +00:00
@ ob_end_clean (); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
2014-05-25 21:10:23 +00:00
}
2014-11-12 16:15:47 +00:00
} else {
2018-03-22 17:12:36 +00:00
@ ob_end_clean (); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
2013-12-19 09:14:39 +00:00
}
2013-08-09 16:11:15 +00:00
}
/**
2018-03-22 17:12:36 +00:00
* Read file chunked .
2014-10-24 16:06:30 +00:00
*
2015-11-03 13:31:20 +00:00
* Reads file in chunks so big downloads are possible without changing PHP . INI - http :// codeigniter . com / wiki / Download_helper_for_large_files /.
2014-10-24 16:06:30 +00:00
*
2018-05-07 08:05:02 +00:00
* @ param string $file File .
* @ param int $start Byte offset / position of the beginning from which to read from the file .
* @ param int $length Length of the chunk to be read from the file in bytes , 0 means full file .
2018-03-22 17:12:36 +00:00
* @ return bool Success or fail
2013-08-09 16:11:15 +00:00
*/
2018-05-07 08:05:02 +00:00
public static function readfile_chunked ( $file , $start = 0 , $length = 0 ) {
2017-11-15 16:32:28 +00:00
if ( ! defined ( 'WC_CHUNK_SIZE' ) ) {
define ( 'WC_CHUNK_SIZE' , 1024 * 1024 );
}
2018-03-22 17:12:36 +00:00
$handle = @ fopen ( $file , 'r' ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen
2013-08-09 16:11:15 +00:00
2014-10-24 21:50:19 +00:00
if ( false === $handle ) {
return false ;
}
2013-08-09 16:11:15 +00:00
2018-05-24 15:10:45 +00:00
if ( ! $length ) {
$length = @ filesize ( $file ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
}
2018-05-07 08:05:02 +00:00
2018-05-24 15:10:45 +00:00
$read_length = ( int ) WC_CHUNK_SIZE ;
2018-05-07 08:05:02 +00:00
2018-05-24 15:10:45 +00:00
if ( $length ) {
$end = $start + $length - 1 ;
@ fseek ( $handle , $start ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
2018-05-07 08:05:02 +00:00
$p = @ ftell ( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
2013-08-09 16:11:15 +00:00
2018-05-24 15:10:45 +00:00
while ( ! @ feof ( $handle ) && $p <= $end ) { // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
// Don't run past the end of file.
if ( $p + $read_length > $end ) {
$read_length = $end - $p + 1 ;
}
echo @ fread ( $handle , $read_length ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.WP.AlternativeFunctions.file_system_read_fread
$p = @ ftell ( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
if ( ob_get_length () ) {
ob_flush ();
flush ();
}
}
} else {
while ( ! @ feof ( $handle ) ) { // @codingStandardsIgnoreLine.
echo @ fread ( $handle , $read_length ); // @codingStandardsIgnoreLine.
if ( ob_get_length () ) {
ob_flush ();
flush ();
}
2014-10-24 21:50:19 +00:00
}
2014-02-26 11:54:16 +00:00
}
2013-08-09 16:11:15 +00:00
2018-03-22 17:12:36 +00:00
return @ fclose ( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fclose
2014-10-24 21:50:19 +00:00
}
2014-10-27 11:01:16 +00:00
/**
2015-11-03 13:31:20 +00:00
* Filter headers for IE to fix issues over SSL .
2014-10-27 11:01:16 +00:00
*
* IE bug prevents download via SSL when Cache Control and Pragma no - cache headers set .
*
2018-03-22 17:12:36 +00:00
* @ param array $headers HTTP headers .
2014-10-27 11:01:16 +00:00
* @ return array
*/
2014-10-27 13:38:24 +00:00
public static function ie_nocache_headers_fix ( $headers ) {
2014-10-27 11:01:16 +00:00
if ( is_ssl () && ! empty ( $GLOBALS [ 'is_IE' ] ) ) {
$headers [ 'Cache-Control' ] = 'private' ;
unset ( $headers [ 'Pragma' ] );
}
return $headers ;
}
2014-10-24 21:50:19 +00:00
/**
2015-11-03 13:31:20 +00:00
* Die with an error message if the download fails .
2018-03-22 17:12:36 +00:00
*
* @ param string $message Error message .
* @ param string $title Error title .
* @ param integer $status Error status .
2014-10-24 21:50:19 +00:00
*/
2014-10-27 11:01:16 +00:00
private static function download_error ( $message , $title = '' , $status = 404 ) {
2021-06-22 06:54:47 +00:00
/*
* Since we will now render a message instead of serving a download , we should unwind some of the previously set
* headers .
*/
header ( 'Content-Type: ' . get_option ( 'html_type' ) . '; charset=' . get_option ( 'blog_charset' ) );
header_remove ( 'Content-Description;' );
header_remove ( 'Content-Disposition' );
header_remove ( 'Content-Transfer-Encoding' );
2014-10-24 21:50:19 +00:00
if ( ! strstr ( $message , '<a ' ) ) {
2017-03-13 05:39:46 +00:00
$message .= ' <a href="' . esc_url ( wc_get_page_permalink ( 'shop' ) ) . '" class="wc-forward">' . esc_html__ ( 'Go to shop' , 'woocommerce' ) . '</a>' ;
2014-10-24 21:50:19 +00:00
}
2018-03-22 17:12:36 +00:00
wp_die ( $message , $title , array ( 'response' => $status ) ); // WPCS: XSS ok.
2013-08-09 16:11:15 +00:00
}
}
2014-05-28 13:52:50 +00:00
WC_Download_Handler :: init ();