aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--phpBB/download/file.php4
-rw-r--r--phpBB/includes/functions_download.php168
-rw-r--r--tests/all_tests.php2
-rw-r--r--tests/download/all_tests.php40
-rw-r--r--tests/download/http_byte_range.php62
5 files changed, 274 insertions, 2 deletions
diff --git a/phpBB/download/file.php b/phpBB/download/file.php
index 891352f00c..68a4afe03c 100644
--- a/phpBB/download/file.php
+++ b/phpBB/download/file.php
@@ -246,7 +246,7 @@ if (!download_allowed())
$download_mode = (int) $extensions[$attachment['extension']]['download_mode'];
// Fetching filename here to prevent sniffing of filename
-$sql = 'SELECT attach_id, is_orphan, in_message, post_msg_id, extension, physical_filename, real_filename, mimetype, filetime
+$sql = 'SELECT attach_id, is_orphan, in_message, post_msg_id, extension, physical_filename, real_filename, mimetype, filesize, filetime
FROM ' . ATTACHMENTS_TABLE . "
WHERE attach_id = $download_id";
$result = $db->sql_query_limit($sql, 1);
@@ -275,7 +275,7 @@ if ($thumbnail)
{
$attachment['physical_filename'] = 'thumb_' . $attachment['physical_filename'];
}
-else if (($display_cat == ATTACHMENT_CATEGORY_NONE/* || $display_cat == ATTACHMENT_CATEGORY_IMAGE*/) && !$attachment['is_orphan'])
+else if (($display_cat == ATTACHMENT_CATEGORY_NONE/* || $display_cat == ATTACHMENT_CATEGORY_IMAGE*/) && !$attachment['is_orphan'] && !phpbb_http_byte_range($attachment['filesize']))
{
// Update download count
$sql = 'UPDATE ' . ATTACHMENTS_TABLE . '
diff --git a/phpBB/includes/functions_download.php b/phpBB/includes/functions_download.php
index 87bf7a91a6..94d851e383 100644
--- a/phpBB/includes/functions_download.php
+++ b/phpBB/includes/functions_download.php
@@ -157,6 +157,16 @@ function send_file_to_browser($attachment, $upload_dir, $category)
trigger_error('UNABLE_TO_DELIVER_FILE');
}
+ // Make sure the database record for the filesize is correct
+ if ($size > 0 && $size != $attachment['filesize'])
+ {
+ // Update database record
+ $sql = 'UPDATE ' . ATTACHMENTS_TABLE . '
+ SET filesize = ' . (int) $size . '
+ WHERE attach_id = ' . (int) $attachment['attach_id'];
+ $db->sql_query($sql);
+ }
+
// Now the tricky part... let's dance
header('Pragma: public');
@@ -226,6 +236,16 @@ function send_file_to_browser($attachment, $upload_dir, $category)
if ($fp !== false)
{
+ // Deliver file partially if requested
+ if ($range = phpbb_http_byte_range($size))
+ {
+ fseek($fp, $range['byte_pos_start']);
+
+ send_status_line(206, 'Partial Content');
+ header('Content-Range: bytes ' . $range['byte_pos_start'] . '-' . $range['byte_pos_end'] . '/' . $range['bytes_total']);
+ header('Content-Length: ' . $range['bytes_requested']);
+ }
+
while (!feof($fp))
{
echo fread($fp, 8192);
@@ -407,3 +427,151 @@ function file_gc()
$db->sql_close();
exit;
}
+
+/**
+* HTTP range support (RFC 2616 Section 14.35)
+*
+* Allows browsers to request partial file content
+* in case a download has been interrupted.
+*
+* @param int $filesize the size of the file in bytes we are about to deliver
+*
+* @return mixed false if the whole file has to be delivered
+* associative array on success
+*/
+function phpbb_http_byte_range($filesize)
+{
+ // Only call find_range_request() once.
+ static $request_array;
+
+ if (!$filesize)
+ {
+ return false;
+ }
+
+ if (!isset($request_array))
+ {
+ $request_array = phpbb_find_range_request();
+ }
+
+ return (empty($request_array)) ? false : phpbb_parse_range_request($request_array, $filesize);
+}
+
+/**
+* Searches for HTTP range request in super globals.
+*
+* @return mixed false if no request found
+* array of strings containing the requested ranges otherwise
+* e.g. array(0 => '0-0', 1 => '123-125')
+*/
+function phpbb_find_range_request()
+{
+ $globals = array(
+ array('_SERVER', 'HTTP_RANGE'),
+ array('_ENV', 'HTTP_RANGE'),
+ );
+
+ foreach ($globals as $array)
+ {
+ $global = $array[0];
+ $key = $array[1];
+
+ // Make sure range request starts with "bytes="
+ if (isset($GLOBALS[$global][$key]) && strpos($GLOBALS[$global][$key], 'bytes=') === 0)
+ {
+ // Strip leading 'bytes='
+ // Multiple ranges can be separated by a comma
+ return explode(',', substr($GLOBALS[$global][$key], 6));
+ }
+ }
+
+ return false;
+}
+
+/**
+* Analyses a range request array.
+*
+* A range request can contain multiple ranges,
+* we however only handle the first request and
+* only support requests from a given byte to the end of the file.
+*
+* @param array $request_array array of strings containing the requested ranges
+* @param int $filesize the full size of the file in bytes that has been requested
+*
+* @return mixed false if the whole file has to be delivered
+* associative array on success
+* byte_pos_start the first byte position, can be passed to fseek()
+* byte_pos_end the last byte position
+* bytes_requested the number of bytes requested
+* bytes_total the full size of the file
+*/
+function phpbb_parse_range_request($request_array, $filesize)
+{
+ // Go through all ranges
+ foreach ($request_array as $range_string)
+ {
+ $range = explode('-', trim($range_string));
+
+ // "-" is invalid, "0-0" however is valid and means the very first byte.
+ if (sizeof($range) != 2 || $range[0] === '' && $range[1] === '')
+ {
+ continue;
+ }
+
+ if ($range[0] === '')
+ {
+ // Return last $range[1] bytes.
+
+ if (!$range[1])
+ {
+ continue;
+ }
+
+ if ($range[1] >= $filesize)
+ {
+ return false;
+ }
+
+ $first_byte_pos = $filesize - (int) $range[1];
+ $last_byte_pos = $filesize - 1;
+ }
+ else
+ {
+ // Return bytes from $range[0] to $range[1]
+
+ $first_byte_pos = (int) $range[0];
+ $last_byte_pos = (int) $range[1];
+
+ if ($last_byte_pos && $last_byte_pos < $first_byte_pos)
+ {
+ // The requested range contains 0 bytes.
+ continue;
+ }
+
+ if ($first_byte_pos >= $filesize)
+ {
+ // Requested range not satisfiable
+ return false;
+ }
+
+ // Adjust last-byte-pos if it is absent or greater than the content.
+ if ($range[1] === '' || $last_byte_pos >= $filesize)
+ {
+ $last_byte_pos = $filesize - 1;
+ }
+ }
+
+ // We currently do not support range requests that end before the end of the file
+ if ($last_byte_pos != $filesize - 1)
+ {
+ continue;
+ }
+
+ return array(
+ 'byte_pos_start' => $first_byte_pos,
+ 'byte_pos_end' => $last_byte_pos,
+ 'bytes_requested' => $last_byte_pos - $first_byte_pos + 1,
+ 'bytes_total' => $filesize,
+ );
+ }
+}
diff --git a/tests/all_tests.php b/tests/all_tests.php
index feaaa1f0a1..4c2fa891a2 100644
--- a/tests/all_tests.php
+++ b/tests/all_tests.php
@@ -26,6 +26,7 @@ require_once 'dbal/all_tests.php';
require_once 'regex/all_tests.php';
require_once 'network/all_tests.php';
require_once 'random/all_tests.php';
+require_once 'download/all_tests.php';
// exclude the test directory from code coverage reports
if (version_compare(PHPUnit_Runner_Version::id(), '3.5.0') >= 0)
@@ -59,6 +60,7 @@ class phpbb_all_tests
$suite->addTest(phpbb_regex_all_tests::suite());
$suite->addTest(phpbb_network_all_tests::suite());
$suite->addTest(phpbb_random_all_tests::suite());
+ $suite->addTest(phpbb_download_all_tests::suite());
return $suite;
}
diff --git a/tests/download/all_tests.php b/tests/download/all_tests.php
new file mode 100644
index 0000000000..21305a887c
--- /dev/null
+++ b/tests/download/all_tests.php
@@ -0,0 +1,40 @@
+<?php
+/**
+*
+* @package testing
+* @copyright (c) 2010 phpBB Group
+* @license http://opensource.org/licenses/gpl-license.php GNU Public License
+*
+*/
+
+if (!defined('PHPUnit_MAIN_METHOD'))
+{
+ define('PHPUnit_MAIN_METHOD', 'phpbb_download_all_tests::main');
+}
+
+require_once 'test_framework/framework.php';
+require_once 'PHPUnit/TextUI/TestRunner.php';
+
+require_once 'download/http_byte_range.php';
+
+class phpbb_download_all_tests
+{
+ public static function main()
+ {
+ PHPUnit_TextUI_TestRunner::run(self::suite());
+ }
+
+ public static function suite()
+ {
+ $suite = new PHPUnit_Framework_TestSuite('phpBB Download Tests');
+
+ $suite->addTestSuite('phpbb_download_http_byte_range_test');
+
+ return $suite;
+ }
+}
+
+if (PHPUnit_MAIN_METHOD == 'phpbb_download_all_tests::main')
+{
+ phpbb_regex_all_tests::main();
+}
diff --git a/tests/download/http_byte_range.php b/tests/download/http_byte_range.php
new file mode 100644
index 0000000000..cc42dee353
--- /dev/null
+++ b/tests/download/http_byte_range.php
@@ -0,0 +1,62 @@
+<?php
+/**
+*
+* @package testing
+* @copyright (c) 2010 phpBB Group
+* @license http://opensource.org/licenses/gpl-license.php GNU Public License
+*
+*/
+
+require_once 'test_framework/framework.php';
+require_once '../phpBB/includes/functions_download.php';
+
+class phpbb_download_http_byte_range_test extends phpbb_test_case
+{
+ public function test_find_range_request()
+ {
+ // Missing 'bytes=' prefix
+ $_SERVER['HTTP_RANGE'] = 'bztes=';
+ $this->assertEquals(false, phpbb_find_range_request());
+ unset($_SERVER['HTTP_RANGE']);
+
+ $_ENV['HTTP_RANGE'] = 'bztes=';
+ $this->assertEquals(false, phpbb_find_range_request());
+ unset($_ENV['HTTP_RANGE']);
+
+ $_SERVER['HTTP_RANGE'] = 'bytes=0-0,123-125';
+ $this->assertEquals(array('0-0', '123-125'), phpbb_find_range_request());
+ unset($_SERVER['HTTP_RANGE']);
+ }
+
+ /**
+ * @dataProvider parse_range_request_data()
+ */
+ public function test_parse_range_request($request_array, $filesize, $expected)
+ {
+ $this->assertEquals($expected, phpbb_parse_range_request($request_array, $filesize));
+ }
+
+ public function parse_range_request_data()
+ {
+ return array(
+ // Does not read until the end of file.
+ array(
+ array('3-4'),
+ 10,
+ false,
+ ),
+
+ // Valid request, handle second range.
+ array(
+ array('0-0', '120-125'),
+ 125,
+ array(
+ 'byte_pos_start' => 120,
+ 'byte_pos_end' => 124,
+ 'bytes_requested' => 5,
+ 'bytes_total' => 125,
+ )
+ ),
+ );
+ }
+}