| | |
| | /** |
| | + * Retrieve the file name being downloaded from the HTTP GET request. |
| | + * |
| | + * @return string The sanitized file name (without path information). |
| | + */ |
| | + function get_sanitized_filename() { |
| | + $filepath = isset( $_GET[ 'filename' ] ) ? $_GET[ 'filename' ] : ''; |
| | + $fileinfo = pathinfo( $filepath ); |
| | + |
| | + // Remove path information (no /etc/passwd or ../../etc/passwd for you). |
| | + $basename = $fileinfo[ 'basename' ]; |
| | + |
| | + if( isset( $_SERVER[ 'HTTP_USER_AGENT' ] ) ) { |
| | + $periods = substr_count( $basename, '.' ); |
| | + |
| | + // Address IE bug regarding multiple periods in filename. |
| | + $basename = strstr( $_SERVER[ 'HTTP_USER_AGENT' ], 'MSIE' ) |
| | + ? mb_ereg_replace( '/\./', '%2e', $basename, $periods - 1 ) |
| | + : $basename; |
| | + } |
| | + |
| | + // Trim all spaces, even internal ones. |
| | + $basename = mb_ereg_replace( '/\s+/', '', $basename ); |
| | + |
| | + // Sanitize. |
| | + $basename = mb_ereg_replace( '([^\w\d\-_~,;\[\]\(\).])', '', $basename ); |
| | + |
| | + return $basename; |
| | + } |
| | + |
| | + /** |
| | * Answers whether the user's download token has expired. |
| | * |
 |
| | |
| | return $expired; |
| | - } |
| | - |
| | - function create_lock_filename( $filename ) { |
| | - return $filename .'.lock'; |
| | } |
| | |
| | /** |
| | - * Acquires a lock for a particular file. Callers would be prudent to |
| | - * call this function from within a try/finally block and close the lock |
| | - * in the finally section. The amount of time between opening and closing |
| | - * the lock must be minimal because parallel processes will be waiting on |
| | - * the lock's release. |
| | + * Downloads a file, allowing for resuming partial downloads. |
| | * |
| | - * @param string $filename The name of file to lock. |
| | + * @param string $filename File to download, must be in script directory. |
| | * |
| | - * @return bool True if the lock was obtained, false upon excessive attempts. |
| | + * @return bool True if the file was transferred. |
| | */ |
| | - function lock_open( $filename ) { |
| | - $lockdir = create_lock_filename( $filename ); |
| | - |
| | - // Track the number of times a lock attempt is made. |
| | - $iterations = 0; |
| | + function download( $filename ) { |
| | + // Don't cache the file stats result (e.g., file size). |
| | + clearstatcache(); |
| | |
| | - do { |
| | - // Creates and tests lock file existence atomically. |
| | - if( @mkdir( $lockdir, 0777 ) ) { |
| | - // Exit the loop. |
| | - $iterations = 0; |
| | - } |
| | - else { |
| | - $iterations++; |
| | - $lifetime = time() - filemtime( $lockdir ); |
| | + $size = @filesize( $filename ); |
| | + $size = $size === false || empty( $size ) ? 0 : $size; |
| | + $content_type = mime_content_type( $filename ); |
| | + $content_length = $size; |
| | + $seek_start = 0; |
| | |
| | - if( $lifetime > 10 ) { |
| | - // If the lock has gone stale, delete it. |
| | - @rmdir( $lockdir ); |
| | - } |
| | - else { |
| | - // Wait a random duration to avoid concurrency conflicts. |
| | - usleep( rand( 1000, 10000 ) ); |
| | - } |
| | - } |
| | - } |
| | - while( $iterations > 0 && $iterations < 10 ); |
| | + // Added by PHP, removed by us. |
| | + header_remove( 'x-powered-by' ); |
| | |
| | - // Indicate whether the maximum number of lock attempts were exceeded. |
| | - return $iterations == 0; |
| | - } |
| | + // Check if a range is sent by browser or download manager. |
| | + if( isset( $_SERVER[ 'HTTP_RANGE' ] ) ) { |
| | + $range_format = '/^bytes=\d*-\d*(,\d*-\d*)*$/'; |
| | + $request_range = $_SERVER[ 'HTTP_RANGE' ]; |
| | |
| | - /** |
| | - * Releases the lock on a particular file. |
| | - * |
| | - * @param string $filename The name of file that was locked. |
| | - */ |
| | - function lock_close( $filename ) { |
| | - @rmdir( create_lock_filename( $filename ) ); |
| | - } |
| | + // Ensure the content request range is in a valid format. |
| | + if( !preg_match( $range_format, $request_range, $matches ) ) { |
| | + header( 'HTTP/1.1 416 Requested Range Not Satisfiable' ); |
| | + header( "Content-Range: bytes */$size" ); |
| | |
| | - /** |
| | - * Increments the number in a file using an exclusive lock. If the file |
| | - * doesn't exist, it will be created and the initial value set to 0. |
| | - * |
| | - * @param string $filename The file containing a number to increment. |
| | - */ |
| | - function increment_count( $filename ) { |
| | - try { |
| | - lock_open( $filename ); |
| | + // Return early because the range is invalid. |
| | + return false; |
| | + } |
| | |
| | - // Coerce value to largest natural numeric data type. |
| | - $count = @file_get_contents( $filename ) + 0; |
| | + // Multiple ranges could be specified, but only serve the first range. |
| | + $seek_start = $matches[ 1 ] + 0; |
| | |
| | - // Write the new counter value. |
| | - file_put_contents( $filename, $count + 1 ); |
| | - } |
| | - finally { |
| | - lock_close( $filename ); |
| | - } |
| | - } |
| | + if( isset( $matches[ 2 ] ) ) { |
| | + $seek_end = $matches[ 2 ] + 0; |
| | + } |
| | + else { |
| | + $seek_end = $size - 1; |
| | + } |
| | |
| | - /** |
| | - * Retrieve the file name being downloaded from the HTTP GET request. |
| | - * |
| | - * @return string The sanitized file name (without path information). |
| | - */ |
| | - function get_sanitized_filename() { |
| | - $filepath = isset( $_GET[ 'filename' ] ) ? $_GET[ 'filename' ] : ''; |
| | - $fileinfo = pathinfo( $filepath ); |
| | + $range_bytes = $seek_start . '-' . $seek_end . '/' . $size; |
| | + $content_length = $seek_end - $seek_start + 1; |
| | |
| | - // Remove path information (no /etc/passwd or ../../etc/passwd for you). |
| | - $basename = $fileinfo[ 'basename' ]; |
| | + header( 'HTTP/1.1 206 Partial Content' ); |
| | + header( "Content-Range: bytes $range_bytes" ); |
| | + } |
| | |
| | - if( isset( $_SERVER[ 'HTTP_USER_AGENT' ] ) ) { |
| | - $periods = substr_count( $basename, '.' ); |
| | + // HTTP/1.1 clients must treat invalid date formats, especially 0, as past. |
| | + header( 'Expires: 0' ); |
| | |
| | - // Address IE bug regarding multiple periods in filename. |
| | - $basename = strstr( $_SERVER[ 'HTTP_USER_AGENT' ], 'MSIE' ) |
| | - ? mb_ereg_replace( '/\./', '%2e', $basename, $periods - 1 ) |
| | - : $basename; |
| | - } |
| | + // Prevent local caching. |
| | + header( 'Cache-Control: public, must-revalidate, post-check=0, pre-check=0' ); |
| | |
| | - // Trim all spaces, even internal ones. |
| | - $basename = mb_ereg_replace( '/\s+/', '', $basename ); |
| | + // No response message portion may be cached (e.g., by a proxy server). |
| | + header( 'Cache-Control: private', false ); |
| | |
| | - // Sanitize. |
| | - $basename = mb_ereg_replace( '([^\w\d\-_~,;\[\]\(\).])', '', $basename ); |
| | + // Force the browser to download, rather than displaying the file inline. |
| | + header( "Content-Disposition: attachment; filename=\"$filename\"" ); |
| | + header( 'Accept-Ranges: bytes' ); |
| | + header( "Content-Length: $content_length" ); |
| | + header( "Content-Type: $content_type" ); |
| | |
| | - return $basename; |
| | + // Honour HTTP HEAD requests. |
| | + return $_SERVER['REQUEST_METHOD'] === 'HEAD' |
| | + ? false |
| | + : transmit( $filename, $seek_start, $size ); |
| | } |
| | - |
| | /** |
| | * Transmits a file from the server to the client. |
 |
| | |
| | /** |
| | - * Downloads a file, allowing for resuming partial downloads. |
| | - * |
| | - * @param string $filename File to download, must be in script directory. |
| | + * Increments the number in a file using an exclusive lock. If the file |
| | + * doesn't exist, it will be created and the initial value set to 0. |
| | * |
| | - * @return bool True if the file was transferred. |
| | + * @param string $filename The file containing a number to increment. |
| | */ |
| | - function download( $filename ) { |
| | - // Don't cache the file stats result (e.g., file size). |
| | - clearstatcache(); |
| | - |
| | - $size = @filesize( $filename ); |
| | - $size = $size === false || empty( $size ) ? 0 : $size; |
| | - $content_type = mime_content_type( $filename ); |
| | - $content_length = $size; |
| | - $seek_start = 0; |
| | - |
| | - // Added by PHP, removed by us. |
| | - header_remove( 'x-powered-by' ); |
| | + function increment_count( $filename ) { |
| | + try { |
| | + lock_open( $filename ); |
| | |
| | - // Check if a range is sent by browser or download manager. |
| | - if( isset( $_SERVER[ 'HTTP_RANGE' ] ) ) { |
| | - $range_format = '/^bytes=\d*-\d*(,\d*-\d*)*$/'; |
| | - $request_range = $_SERVER[ 'HTTP_RANGE' ]; |
| | + // Coerce value to largest natural numeric data type. |
| | + $count = @file_get_contents( $filename ) + 0; |
| | |
| | - // Ensure the content request range is in a valid format. |
| | - if( !preg_match( $range_format, $request_range, $matches ) ) { |
| | - header( 'HTTP/1.1 416 Requested Range Not Satisfiable' ); |
| | - header( "Content-Range: bytes */$size" ); |
| | + // Write the new counter value. |
| | + file_put_contents( $filename, $count + 1 ); |
| | + } |
| | + finally { |
| | + lock_close( $filename ); |
| | + } |
| | + } |
| | |
| | - // Return early because the range is invalid. |
| | - return false; |
| | - } |
| | + /** |
| | + * Acquires a lock for a particular file. Callers would be prudent to |
| | + * call this function from within a try/finally block and close the lock |
| | + * in the finally section. The amount of time between opening and closing |
| | + * the lock must be minimal because parallel processes will be waiting on |
| | + * the lock's release. |
| | + * |
| | + * @param string $filename The name of file to lock. |
| | + * |
| | + * @return bool True if the lock was obtained, false upon excessive attempts. |
| | + */ |
| | + function lock_open( $filename ) { |
| | + $lockdir = create_lock_filename( $filename ); |
| | |
| | - // Multiple ranges could be specified, but only serve the first range. |
| | - $seek_start = $matches[ 1 ] + 0; |
| | + // Track the number of times a lock attempt is made. |
| | + $iterations = 0; |
| | |
| | - if( isset( $matches[ 2 ] ) ) { |
| | - $seek_end = $matches[ 2 ] + 0; |
| | + do { |
| | + // Creates and tests lock file existence atomically. |
| | + if( @mkdir( $lockdir, 0777 ) ) { |
| | + // Exit the loop. |
| | + $iterations = 0; |
| | } |
| | else { |
| | - $seek_end = $size - 1; |
| | - } |
| | - |
| | - $range_bytes = $seek_start . '-' . $seek_end . '/' . $size; |
| | - $content_length = $seek_end - $seek_start + 1; |
| | + $iterations++; |
| | + $lifetime = time() - filemtime( $lockdir ); |
| | |
| | - header( 'HTTP/1.1 206 Partial Content' ); |
| | - header( "Content-Range: bytes $range_bytes" ); |
| | + if( $lifetime > 10 ) { |
| | + // If the lock has gone stale, delete it. |
| | + @rmdir( $lockdir ); |
| | + } |
| | + else { |
| | + // Wait a random duration to avoid concurrency conflicts. |
| | + usleep( rand( 1000, 10000 ) ); |
| | + } |
| | + } |
| | } |
| | - |
| | - // HTTP/1.1 clients must treat invalid date formats, especially 0, as past. |
| | - header( 'Expires: 0' ); |
| | - |
| | - // Prevent local caching. |
| | - header( 'Cache-Control: public, must-revalidate, post-check=0, pre-check=0' ); |
| | + while( $iterations > 0 && $iterations < 10 ); |
| | |
| | - // No response message portion may be cached (e.g., by a proxy server). |
| | - header( 'Cache-Control: private', false ); |
| | + // Indicate whether the maximum number of lock attempts were exceeded. |
| | + return $iterations == 0; |
| | + } |
| | |
| | - // Force the browser to download, rather than displaying the file inline. |
| | - header( "Content-Disposition: attachment; filename=\"$filename\"" ); |
| | - header( 'Accept-Ranges: bytes' ); |
| | - header( "Content-Length: $content_length" ); |
| | - header( "Content-Type: $content_type" ); |
| | + /** |
| | + * Releases the lock on a particular file. |
| | + * |
| | + * @param string $filename The name of file that was locked. |
| | + */ |
| | + function lock_close( $filename ) { |
| | + @rmdir( create_lock_filename( $filename ) ); |
| | + } |
| | |
| | - // Honour HTTP HEAD requests. |
| | - return $_SERVER['REQUEST_METHOD'] === 'HEAD' |
| | - ? false |
| | - : transmit( $filename, $seek_start, $size ); |
| | + /** |
| | + * Creates a uniquely named lock directory name. |
| | + * |
| | + * @param string $filename The name of the file under contention. |
| | + * |
| | + * @return string A unique lock file reference for the given filename. |
| | + */ |
| | + function create_lock_filename( $filename ) { |
| | + return $filename .'.lock'; |
| | } |
| | ?> |