WC_Download_Handler

Download handler.

Defined (1)

The class is defined in the following location(s).

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