/includes/class-wc-download-handler.php

  1. <?php 
  2.  
  3. if ( ! defined( 'ABSPATH' ) ) { 
  4. exit; // Exit if accessed directly 
  5.  
  6. /** 
  7. * Download handler. 
  8. * 
  9. * Handle digital downloads. 
  10. * 
  11. * @class WC_Download_Handler 
  12. * @version 2.2.0 
  13. * @package WooCommerce/Classes 
  14. * @category Class 
  15. * @author WooThemes 
  16. */ 
  17. class WC_Download_Handler { 
  18.  
  19. /** 
  20. * Hook in methods. 
  21. */ 
  22. public static function init() { 
  23. if ( isset( $_GET['download_file'], $_GET['order'], $_GET['email'] ) ) { 
  24. add_action( 'init', array( __CLASS__, 'download_product' ) ); 
  25. add_action( 'woocommerce_download_file_redirect', array( __CLASS__, 'download_file_redirect' ), 10, 2 ); 
  26. add_action( 'woocommerce_download_file_xsendfile', array( __CLASS__, 'download_file_xsendfile' ), 10, 2 ); 
  27. add_action( 'woocommerce_download_file_force', array( __CLASS__, 'download_file_force' ), 10, 2 ); 
  28.  
  29. /** 
  30. * Check if we need to download a file and check validity. 
  31. */ 
  32. public static function download_product() { 
  33. $product_id = absint( $_GET['download_file'] ); 
  34. $product = wc_get_product( $product_id ); 
  35. $data_store = WC_Data_Store::load( 'customer-download' ); 
  36.  
  37. if ( ! $product || ! isset( $_GET['key'], $_GET['order'] ) ) { 
  38. self::download_error( __( 'Invalid download link.', 'woocommerce' ) ); 
  39.  
  40. $download_ids = $data_store->get_downloads( array( 
  41. 'user_email' => sanitize_email( str_replace( ' ', '+', $_GET['email'] ) ),  
  42. 'order_key' => wc_clean( $_GET['order'] ),  
  43. 'product_id' => $product_id,  
  44. 'download_id' => wc_clean( preg_replace( '/\s+/', ' ', $_GET['key'] ) ),  
  45. 'orderby' => 'downloads_remaining',  
  46. 'order' => 'DESC',  
  47. 'limit' => 1,  
  48. 'return' => 'ids',  
  49. ) ); 
  50.  
  51. if ( empty( $download_ids ) ) { 
  52. self::download_error( __( 'Invalid download link.', 'woocommerce' ) ); 
  53.  
  54. $download = new WC_Customer_Download( current( $download_ids ) ); 
  55.  
  56. self::check_order_is_valid( $download ); 
  57. self::check_downloads_remaining( $download ); 
  58. self::check_download_expiry( $download ); 
  59. self::check_download_login_required( $download ); 
  60.  
  61. do_action( 
  62. 'woocommerce_download_product',  
  63. $download->get_user_email(),  
  64. $download->get_order_key(),  
  65. $download->get_product_id(),  
  66. $download->get_user_id(),  
  67. $download->get_download_id(),  
  68. $download->get_order_id() 
  69. ); 
  70. $count = $download->get_download_count(); 
  71. $remaining = $download->get_downloads_remaining(); 
  72. $download->set_download_count( $count + 1 ); 
  73. if ( '' !== $remaining ) { 
  74. $download->set_downloads_remaining( $remaining - 1 ); 
  75. $download->save(); 
  76.  
  77. self::download( $product->get_file_download_path( $download->get_download_id() ), $download->get_product_id() ); 
  78.  
  79. /** 
  80. * Check if an order is valid for downloading from. 
  81. * @param WC_Customer_Download $download 
  82. * @access private 
  83. */ 
  84. private static function check_order_is_valid( $download ) { 
  85. if ( $download->get_order_id() && ( $order = wc_get_order( $download->get_order_id() ) ) && ! $order->is_download_permitted() ) { 
  86. self::download_error( __( 'Invalid order.', 'woocommerce' ), '', 403 ); 
  87.  
  88. /** 
  89. * Check if there are downloads remaining. 
  90. * @param WC_Customer_Download $download 
  91. * @access private 
  92. */ 
  93. private static function check_downloads_remaining( $download ) { 
  94. if ( '' !== $download->get_downloads_remaining() && 0 >= $download->get_downloads_remaining() ) { 
  95. self::download_error( __( 'Sorry, you have reached your download limit for this file', 'woocommerce' ), '', 403 ); 
  96.  
  97. /** 
  98. * Check if the download has expired. 
  99. * @param WC_Customer_Download $download 
  100. * @access private 
  101. */ 
  102. private static function check_download_expiry( $download ) { 
  103. if ( ! is_null( $download->get_access_expires() ) && $download->get_access_expires()->getTimestamp() < strtotime( 'midnight', current_time( 'timestamp', true ) ) ) { 
  104. self::download_error( __( 'Sorry, this download has expired', 'woocommerce' ), '', 403 ); 
  105.  
  106. /** 
  107. * Check if a download requires the user to login first. 
  108. * @param WC_Customer_Download $download 
  109. * @access private 
  110. */ 
  111. private static function check_download_login_required( $download ) { 
  112. if ( $download->get_user_id() && 'yes' === get_option( 'woocommerce_downloads_require_login' ) ) { 
  113. if ( ! is_user_logged_in() ) { 
  114. if ( wc_get_page_id( 'myaccount' ) ) { 
  115. wp_safe_redirect( add_query_arg( 'wc_error', urlencode( __( 'You must be logged in to download files.', 'woocommerce' ) ), wc_get_page_permalink( 'myaccount' ) ) ); 
  116. exit; 
  117. } else { 
  118. 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 ); 
  119. } elseif ( ! current_user_can( 'download_file', $download ) ) { 
  120. self::download_error( __( 'This is not your download link.', 'woocommerce' ), '', 403 ); 
  121.  
  122. /** 
  123. * @deprecated 
  124. */ 
  125. public static function count_download( $download_data ) {} 
  126.  
  127. /** 
  128. * Download a file - hook into init function. 
  129. * @param string $file_path URL to file 
  130. * @param integer $product_id of the product being downloaded 
  131. */ 
  132. public static function download( $file_path, $product_id ) { 
  133. if ( ! $file_path ) { 
  134. self::download_error( __( 'No file defined', 'woocommerce' ) ); 
  135.  
  136. $filename = basename( $file_path ); 
  137.  
  138. if ( strstr( $filename, '?' ) ) { 
  139. $filename = current( explode( '?', $filename ) ); 
  140.  
  141. $filename = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id ); 
  142. $file_download_method = apply_filters( 'woocommerce_file_download_method', get_option( 'woocommerce_file_download_method', 'force' ), $product_id ); 
  143.  
  144. // Add action to prevent issues in IE 
  145. add_action( 'nocache_headers', array( __CLASS__, 'ie_nocache_headers_fix' ) ); 
  146.  
  147. // Trigger download via one of the methods 
  148. do_action( 'woocommerce_download_file_' . $file_download_method, $file_path, $filename ); 
  149.  
  150. /** 
  151. * Redirect to a file to start the download. 
  152. * @param string $file_path 
  153. * @param string $filename 
  154. */ 
  155. public static function download_file_redirect( $file_path, $filename = '' ) { 
  156. header( 'Location: ' . $file_path ); 
  157. exit; 
  158.  
  159. /** 
  160. * Parse file path and see if its remote or local. 
  161. * @param string $file_path 
  162. * @return array 
  163. */ 
  164. public static function parse_file_path( $file_path ) { 
  165. $wp_uploads = wp_upload_dir(); 
  166. $wp_uploads_dir = $wp_uploads['basedir']; 
  167. $wp_uploads_url = $wp_uploads['baseurl']; 
  168.  
  169. // Replace uploads dir, site url etc with absolute counterparts if we can 
  170. $replacements = array( 
  171. $wp_uploads_url => $wp_uploads_dir,  
  172. network_site_url( '/', 'https' ) => ABSPATH,  
  173. network_site_url( '/', 'http' ) => ABSPATH,  
  174. site_url( '/', 'https' ) => ABSPATH,  
  175. site_url( '/', 'http' ) => ABSPATH,  
  176. ); 
  177.  
  178. $file_path = str_replace( array_keys( $replacements ), array_values( $replacements ), $file_path ); 
  179. $parsed_file_path = parse_url( $file_path ); 
  180. $remote_file = true; 
  181.  
  182. // See if path needs an abspath prepended to work 
  183. if ( file_exists( ABSPATH . $file_path ) ) { 
  184. $remote_file = false; 
  185. $file_path = ABSPATH . $file_path; 
  186.  
  187. } elseif ( '/wp-content' === substr( $file_path, 0, 11 ) ) { 
  188. $remote_file = false; 
  189. $file_path = realpath( WP_CONTENT_DIR . substr( $file_path, 11 ) ); 
  190.  
  191. // Check if we have an absolute path 
  192. } elseif ( ( ! isset( $parsed_file_path['scheme'] ) || ! in_array( $parsed_file_path['scheme'], array( 'http', 'https', 'ftp' ) ) ) && isset( $parsed_file_path['path'] ) && file_exists( $parsed_file_path['path'] ) ) { 
  193. $remote_file = false; 
  194. $file_path = $parsed_file_path['path']; 
  195.  
  196. return array( 
  197. 'remote_file' => $remote_file,  
  198. 'file_path' => $file_path,  
  199. ); 
  200.  
  201. /** 
  202. * Download a file using X-Sendfile, X-Lighttpd-Sendfile, or X-Accel-Redirect if available. 
  203. * @param string $file_path 
  204. * @param string $filename 
  205. */ 
  206. public static function download_file_xsendfile( $file_path, $filename ) { 
  207. $parsed_file_path = self::parse_file_path( $file_path ); 
  208.  
  209. if ( function_exists( 'apache_get_modules' ) && in_array( 'mod_xsendfile', apache_get_modules() ) ) { 
  210. self::download_headers( $parsed_file_path['file_path'], $filename ); 
  211. header( "X-Sendfile: " . $parsed_file_path['file_path'] ); 
  212. exit; 
  213. } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'lighttpd' ) ) { 
  214. self::download_headers( $parsed_file_path['file_path'], $filename ); 
  215. header( "X-Lighttpd-Sendfile: " . $parsed_file_path['file_path'] ); 
  216. exit; 
  217. } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'nginx' ) || stristr( getenv( 'SERVER_SOFTWARE' ), 'cherokee' ) ) { 
  218. self::download_headers( $parsed_file_path['file_path'], $filename ); 
  219. $xsendfile_path = trim( preg_replace( '`^' . str_replace( '\\', '/', getcwd() ) . '`', '', $parsed_file_path['file_path'] ), '/' ); 
  220. header( "X-Accel-Redirect: /$xsendfile_path" ); 
  221. exit; 
  222.  
  223. // Fallback 
  224. self::download_file_force( $file_path, $filename ); 
  225.  
  226. /** 
  227. * Force download - this is the default method. 
  228. * @param string $file_path 
  229. * @param string $filename 
  230. */ 
  231. public static function download_file_force( $file_path, $filename ) { 
  232. $parsed_file_path = self::parse_file_path( $file_path ); 
  233.  
  234. self::download_headers( $parsed_file_path['file_path'], $filename ); 
  235.  
  236. if ( ! self::readfile_chunked( $parsed_file_path['file_path'] ) ) { 
  237. if ( $parsed_file_path['remote_file'] ) { 
  238. self::download_file_redirect( $file_path ); 
  239. } else { 
  240. self::download_error( __( 'File not found', 'woocommerce' ) ); 
  241.  
  242. exit; 
  243.  
  244. /** 
  245. * Get content type of a download. 
  246. * @param string $file_path 
  247. * @return string 
  248. * @access private 
  249. */ 
  250. private static function get_download_content_type( $file_path ) { 
  251. $file_extension = strtolower( substr( strrchr( $file_path, "." ), 1 ) ); 
  252. $ctype = "application/force-download"; 
  253.  
  254. foreach ( get_allowed_mime_types() as $mime => $type ) { 
  255. $mimes = explode( '|', $mime ); 
  256. if ( in_array( $file_extension, $mimes ) ) { 
  257. $ctype = $type; 
  258. break; 
  259.  
  260. return $ctype; 
  261.  
  262. /** 
  263. * Set headers for the download. 
  264. * @param string $file_path 
  265. * @param string $filename 
  266. * @access private 
  267. */ 
  268. private static function download_headers( $file_path, $filename ) { 
  269. self::check_server_config(); 
  270. self::clean_buffers(); 
  271. nocache_headers(); 
  272.  
  273. header( "X-Robots-Tag: noindex, nofollow", true ); 
  274. header( "Content-Type: " . self::get_download_content_type( $file_path ) ); 
  275. header( "Content-Description: File Transfer" ); 
  276. header( "Content-Disposition: attachment; filename=\"" . $filename . "\";" ); 
  277. header( "Content-Transfer-Encoding: binary" ); 
  278.  
  279. if ( $size = @filesize( $file_path ) ) { 
  280. header( "Content-Length: " . $size ); 
  281.  
  282. /** 
  283. * Check and set certain server config variables to ensure downloads work as intended. 
  284. */ 
  285. private static function check_server_config() { 
  286. wc_set_time_limit( 0 ); 
  287. if ( function_exists( 'get_magic_quotes_runtime' ) && get_magic_quotes_runtime() && version_compare( phpversion(), '5.4', '<' ) ) { 
  288. set_magic_quotes_runtime( 0 ); 
  289. if ( function_exists( 'apache_setenv' ) ) { 
  290. @apache_setenv( 'no-gzip', 1 ); 
  291. @ini_set( 'zlib.output_compression', 'Off' ); 
  292. @session_write_close(); 
  293.  
  294. /** 
  295. * Clean all output buffers. 
  296. * 
  297. * Can prevent errors, for example: transfer closed with 3 bytes remaining to read. 
  298. * 
  299. * @access private 
  300. */ 
  301. private static function clean_buffers() { 
  302. if ( ob_get_level() ) { 
  303. $levels = ob_get_level(); 
  304. for ( $i = 0; $i < $levels; $i++ ) { 
  305. @ob_end_clean(); 
  306. } else { 
  307. @ob_end_clean(); 
  308.  
  309. /** 
  310. * readfile_chunked. 
  311. * 
  312. * Reads file in chunks so big downloads are possible without changing PHP.INI - http://codeigniter.com/wiki/Download_helper_for_large_files/. 
  313. * 
  314. * @param string $file 
  315. * @return bool Success or fail 
  316. */ 
  317. public static function readfile_chunked( $file ) { 
  318. $chunksize = 1024 * 1024; 
  319. $handle = @fopen( $file, 'r' ); 
  320.  
  321. if ( false === $handle ) { 
  322. return false; 
  323.  
  324. while ( ! @feof( $handle ) ) { 
  325. echo @fread( $handle, $chunksize ); 
  326.  
  327. if ( ob_get_length() ) { 
  328. ob_flush(); 
  329. flush(); 
  330.  
  331. return @fclose( $handle ); 
  332.  
  333. /** 
  334. * Filter headers for IE to fix issues over SSL. 
  335. * 
  336. * IE bug prevents download via SSL when Cache Control and Pragma no-cache headers set. 
  337. * 
  338. * @param array $headers 
  339. * @return array 
  340. */ 
  341. public static function ie_nocache_headers_fix( $headers ) { 
  342. if ( is_ssl() && ! empty( $GLOBALS['is_IE'] ) ) { 
  343. $headers['Cache-Control'] = 'private'; 
  344. unset( $headers['Pragma'] ); 
  345. return $headers; 
  346.  
  347. /** 
  348. * Die with an error message if the download fails. 
  349. * @param string $message 
  350. * @param string $title 
  351. * @param integer $status 
  352. * @access private 
  353. */ 
  354. private static function download_error( $message, $title = '', $status = 404 ) { 
  355. if ( ! strstr( $message, '<a ' ) ) { 
  356. $message .= ' <a href="' . esc_url( wc_get_page_permalink( 'shop' ) ) . '" class="wc-forward">' . esc_html__( 'Go to shop', 'woocommerce' ) . '</a>'; 
  357. wp_die( $message, $title, array( 'response' => $status ) ); 
  358.  
  359. WC_Download_Handler::init(); 
.