1 <?php
  2 /**
  3  * Download handler
  4  *
  5  * Handle digital downloads.
  6  *
  7  * @class       WC_Download_Handler
  8  * @version     2.1.0
  9  * @package     WooCommerce/Classes
 10  * @category    Class
 11  * @author      WooThemes
 12  */
 13 class WC_Download_Handler {
 14 
 15     /**
 16      * Constructor
 17      */
 18     public function __construct() {
 19         add_action( 'init', array( $this, 'download_product' ) );
 20     }
 21 
 22     /**
 23      * Check if we need to download a file and check validity
 24      */
 25     public function download_product() {
 26         if ( isset( $_GET['download_file'] ) && isset( $_GET['order'] ) && isset( $_GET['email'] ) ) {
 27 
 28             global $wpdb;
 29 
 30             $product_id           = (int) $_GET['download_file'];
 31             $order_key            = $_GET['order'];
 32             $email                = sanitize_email( str_replace( ' ', '+', $_GET['email'] ) );
 33             $download_id          = isset( $_GET['key'] ) ? preg_replace( '/\s+/', ' ', $_GET['key'] ) : '';
 34             $_product             = get_product( $product_id );
 35 
 36             if ( ! is_email( $email) ) {
 37                 wp_die( __( 'Invalid email address.', 'woocommerce' ) . ' <a href="' . esc_url( home_url() ) . '" class="wc-forward">' . __( 'Go to homepage', 'woocommerce' ) . '</a>' );
 38             }
 39 
 40             $query = "
 41                 SELECT order_id,downloads_remaining,user_id,download_count,access_expires,download_id
 42                 FROM " . $wpdb->prefix . "woocommerce_downloadable_product_permissions
 43                 WHERE user_email = %s
 44                 AND order_key = %s
 45                 AND product_id = %s";
 46 
 47             $args = array(
 48                 $email,
 49                 $order_key,
 50                 $product_id
 51             );
 52 
 53             if ( $download_id ) {
 54                 // backwards compatibility for existing download URLs
 55                 $query .= " AND download_id = %s";
 56                 $args[] = $download_id;
 57             }
 58 
 59             $download_result = $wpdb->get_row( $wpdb->prepare( $query, $args ) );
 60 
 61             if ( ! $download_result ) {
 62                 wp_die( __( 'Invalid download.', 'woocommerce' ) . ' <a href="' . esc_url( home_url() ) . '" class="wc-forward">' . __( 'Go to homepage', 'woocommerce' ) . '</a>' );
 63             }
 64 
 65             $download_id            = $download_result->download_id;
 66             $order_id               = $download_result->order_id;
 67             $downloads_remaining    = $download_result->downloads_remaining;
 68             $download_count         = $download_result->download_count;
 69             $user_id                = $download_result->user_id;
 70             $access_expires         = $download_result->access_expires;
 71 
 72             if ( $user_id && get_option( 'woocommerce_downloads_require_login' ) == 'yes' ) {
 73 
 74                 if ( ! is_user_logged_in() ) {
 75                     wp_die( __( 'You must be logged in to download files.', 'woocommerce' ) . ' <a href="' . esc_url( wp_login_url( get_permalink( wc_get_page_id( 'myaccount' ) ) ) ) . '" class="wc-forward">' . __( 'Login', 'woocommerce' ) . '</a>', __( 'Log in to Download Files', 'woocommerce' ) );
 76                 } elseif ( ! current_user_can( 'download_file', $download_result ) ) {
 77                     wp_die( __( 'This is not your download link.', 'woocommerce' ) );
 78                 }
 79 
 80             }
 81 
 82             if ( ! get_post( $product_id ) ) {
 83                 wp_die( __( 'Product no longer exists.', 'woocommerce' ) . ' <a href="' . esc_url( home_url() ) . '" class="wc-forward">' . __( 'Go to homepage', 'woocommerce' ) . '</a>' );
 84             }
 85 
 86             if ( $order_id ) {
 87                 $order = new WC_Order( $order_id );
 88 
 89                 if ( ! $order->is_download_permitted() || $order->post_status != 'publish' ) {
 90                     wp_die( __( 'Invalid order.', 'woocommerce' ) . ' <a href="' . esc_url( home_url() ) . '" class="wc-forward">' . __( 'Go to homepage', 'woocommerce' ) . '</a>' );
 91                 }
 92             }
 93 
 94             if ( $downloads_remaining == '0' ) {
 95                 wp_die( __( 'Sorry, you have reached your download limit for this file', 'woocommerce' ) . ' <a href="' . esc_url( home_url() ) . '" class="wc-forward">' . __( 'Go to homepage', 'woocommerce' ) . '</a>' );
 96             }
 97 
 98             if ( $access_expires > 0 && strtotime( $access_expires) < current_time( 'timestamp' ) ) {
 99                 wp_die( __( 'Sorry, this download has expired', 'woocommerce' ) . ' <a href="' . esc_url( home_url() ) . '" class="wc-forward">' . __( 'Go to homepage', 'woocommerce' ) . '</a>' );
100             }
101 
102             if ( $downloads_remaining > 0 ) {
103                 $wpdb->update( $wpdb->prefix . "woocommerce_downloadable_product_permissions", array(
104                     'downloads_remaining' => $downloads_remaining - 1,
105                 ), array(
106                     'user_email'    => $email,
107                     'order_key'     => $order_key,
108                     'product_id'    => $product_id,
109                     'download_id'   => $download_id
110                 ), array( '%d' ), array( '%s', '%s', '%d', '%s' ) );
111             }
112 
113             // Count the download
114             $wpdb->update( $wpdb->prefix . "woocommerce_downloadable_product_permissions", array(
115                 'download_count' => $download_count + 1,
116             ), array(
117                 'user_email'    => $email,
118                 'order_key'     => $order_key,
119                 'product_id'    => $product_id,
120                 'download_id'   => $download_id
121             ), array( '%d' ), array( '%s', '%s', '%d', '%s' ) );
122 
123             // Trigger action
124             do_action( 'woocommerce_download_product', $email, $order_key, $product_id, $user_id, $download_id, $order_id );
125 
126             // Get the download URL and try to replace the url with a path
127             $file_path = $_product->get_file_download_path( $download_id );
128 
129             // Download it!
130             $this->download( $file_path, $product_id );
131         }
132     }
133 
134     /**
135      * Download a file - hook into init function.
136      */
137     public function download( $file_path, $product_id ) {
138         global $wpdb, $is_IE;
139 
140         $file_download_method = apply_filters( 'woocommerce_file_download_method', get_option( 'woocommerce_file_download_method' ), $product_id );
141 
142         if ( ! $file_path ) {
143             wp_die( __( 'No file defined', 'woocommerce' ) . ' <a href="' . esc_url( home_url() ) . '" class="wc-forward">' . __( 'Go to homepage', 'woocommerce' ) . '</a>' );
144         }
145 
146         // Redirect to the file...
147         if ( $file_download_method == "redirect" ) {
148             header( 'Location: ' . $file_path );
149             exit;
150         }
151 
152         // ...or serve it
153         $remote_file      = true;
154         $parsed_file_path = parse_url( $file_path );
155         
156         $wp_uploads       = wp_upload_dir();
157         $wp_uploads_dir   = $wp_uploads['basedir'];
158         $wp_uploads_url   = $wp_uploads['baseurl'];
159 
160         if ( ( ! 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'] ) ) {
161 
162             /** This is an absolute path */
163             $remote_file  = false;
164 
165         } elseif( strpos( $file_path, $wp_uploads_url ) !== false ) {
166 
167             /** This is a local file given by URL so we need to figure out the path */
168             $remote_file  = false;
169             $file_path    = str_replace( $wp_uploads_url, $wp_uploads_dir, $file_path );
170 
171         } elseif( is_multisite() && ( strpos( $file_path, network_site_url( '/', 'http' ) ) !== false || strpos( $file_path, network_site_url( '/', 'https' ) ) !== false ) ) {
172 
173             /** This is a local file outside of wp-content so figure out the path */
174             $remote_file = false;
175             // Try to replace network url
176             $file_path   = str_replace( network_site_url( '/', 'https' ), ABSPATH, $file_path );
177             $file_path   = str_replace( network_site_url( '/', 'http' ), ABSPATH, $file_path );
178             // Try to replace upload URL
179             $file_path   = str_replace( $wp_uploads_url, $wp_uploads_dir, $file_path );
180 
181         } elseif( strpos( $file_path, site_url( '/', 'http' ) ) !== false || strpos( $file_path, site_url( '/', 'https' ) ) !== false ) {
182 
183             /** This is a local file outside of wp-content so figure out the path */
184             $remote_file = false;
185             $file_path   = str_replace( site_url( '/', 'https' ), ABSPATH, $file_path );
186             $file_path   = str_replace( site_url( '/', 'http' ), ABSPATH, $file_path );
187 
188         } elseif ( file_exists( ABSPATH . $file_path ) ) {
189             
190             /** Path needs an abspath to work */
191             $remote_file = false;
192             $file_path   = ABSPATH . $file_path;
193         }
194 
195         if ( ! $remote_file ) {
196             // Remove Query String
197             if ( strstr( $file_path, '?' ) ) {
198                 $file_path = current( explode( '?', $file_path ) );
199             }
200 
201             // Run realpath
202             $file_path = realpath( $file_path );
203         }
204 
205         // Get extension and type
206         $file_extension  = strtolower( substr( strrchr( $file_path, "." ), 1 ) );
207         $ctype           = "application/force-download";
208 
209         foreach ( get_allowed_mime_types() as $mime => $type ) {
210             $mimes = explode( '|', $mime );
211             if ( in_array( $file_extension, $mimes ) ) {
212                 $ctype = $type;
213                 break;
214             }
215         }
216 
217         // Start setting headers
218         if ( ! ini_get('safe_mode') ) {
219             @set_time_limit(0);
220         }
221 
222         if ( function_exists( 'get_magic_quotes_runtime' ) && get_magic_quotes_runtime() ) {
223             @set_magic_quotes_runtime(0);
224         }
225 
226         if ( function_exists( 'apache_setenv' ) ) {
227             @apache_setenv( 'no-gzip', 1 );
228         }
229 
230         @session_write_close();
231         @ini_set( 'zlib.output_compression', 'Off' );
232 
233         /**
234          * Prevents errors, for example: transfer closed with 3 bytes remaining to read
235          */
236         @ob_end_clean(); // Clear the output buffer
237 
238         if ( ob_get_level() ) {
239 
240             $levels = ob_get_level();
241 
242             for ( $i = 0; $i < $levels; $i++ ) {
243                 @ob_end_clean(); // Zip corruption fix
244             }
245 
246         }
247 
248         if ( $is_IE && is_ssl() ) {
249             // IE bug prevents download via SSL when Cache Control and Pragma no-cache headers set.
250             header( 'Expires: Wed, 11 Jan 1984 05:00:00 GMT' );
251             header( 'Cache-Control: private' );
252         } else {
253             nocache_headers();
254         }
255 
256         $filename = basename( $file_path );
257 
258         if ( strstr( $filename, '?' ) ) {
259             $filename = current( explode( '?', $filename ) );
260         }
261 
262         $filename = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id );
263 
264         header( "X-Robots-Tag: noindex, nofollow", true );
265         header( "Content-Type: " . $ctype );
266         header( "Content-Description: File Transfer" );
267         header( "Content-Disposition: attachment; filename=\"" . $filename . "\";" );
268         header( "Content-Transfer-Encoding: binary" );
269 
270         if ( $size = @filesize( $file_path ) ) {
271             header( "Content-Length: " . $size );
272         }
273 
274         if ( $file_download_method == 'xsendfile' ) {
275 
276             // Path fix - kudos to Jason Judge
277             if ( getcwd() ) {
278                 $file_path = trim( preg_replace( '`^' . str_replace( '\\', '/', getcwd() ) . '`' , '', $file_path ), '/' );
279             }
280 
281             header( "Content-Disposition: attachment; filename=\"" . $filename . "\";" );
282 
283             if ( function_exists( 'apache_get_modules' ) && in_array( 'mod_xsendfile', apache_get_modules() ) ) {
284 
285                 header("X-Sendfile: $file_path");
286                 exit;
287 
288             } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'lighttpd' ) ) {
289 
290                 header( "X-Lighttpd-Sendfile: $file_path" );
291                 exit;
292 
293             } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'nginx' ) || stristr( getenv( 'SERVER_SOFTWARE' ), 'cherokee' ) ) {
294 
295                 header( "X-Accel-Redirect: /$file_path" );
296                 exit;
297 
298             }
299         }
300 
301         if ( $remote_file ) {
302             $this->readfile_chunked( $file_path ) or header( 'Location: ' . $file_path );
303         } else {
304             $this->readfile_chunked( $file_path ) or wp_die( __( 'File not found', 'woocommerce' ) . ' <a href="' . esc_url( home_url() ) . '" class="wc-forward">' . __( 'Go to homepage', 'woocommerce' ) . '</a>' );
305         }
306 
307         exit;
308     }
309 
310     /**
311      * readfile_chunked
312      * Reads file in chunks so big downloads are possible without changing PHP.INI - http://codeigniter.com/wiki/Download_helper_for_large_files/
313      * @param    string $file
314      * @param    bool   $retbytes return bytes of file
315      * @return bool|int
316      * @todo Meaning of the return value? Last return is status of fclose?
317      */
318     public static function readfile_chunked( $file, $retbytes = true ) {
319 
320         $chunksize = 1 * ( 1024 * 1024 );
321         $buffer = '';
322         $cnt = 0;
323 
324         $handle = @fopen( $file, 'r' );
325         if ( $handle === FALSE ) {
326             return FALSE;
327         }
328 
329         while ( ! feof( $handle ) ) {
330             $buffer = fread( $handle, $chunksize );
331             echo $buffer;
332             @ob_flush();
333             @flush();
334 
335             if ( $retbytes ) {
336                 $cnt += strlen( $buffer );
337             }
338         }
339 
340         $status = fclose( $handle );
341 
342         if ( $retbytes && $status ) {
343             return $cnt;
344         }
345 
346         return $status;
347     }
348 }
349 
350 new WC_Download_Handler();
351 
WooCommerce API documentation generated by ApiGen 2.8.0