Permalink
Browse files

Fix downloading Ranges in files.

Correctly handle ranges that don't terminate at the end of the file.
Also reject invalid ranges as described in RFC-2616.

Thanks to Kim Biesbjerg for the initial patch.

Fixes #3914
  • Loading branch information...
1 parent b3273e9 commit 494fd05de6a15cadf52fbee3ece789e9a4c841b9 @markstory markstory committed Jul 12, 2013
Showing with 160 additions and 22 deletions.
  1. +64 −18 lib/Cake/Network/CakeResponse.php
  2. +96 −4 lib/Cake/Test/Case/Network/CakeResponseTest.php
View
82 lib/Cake/Network/CakeResponse.php
@@ -346,6 +346,13 @@ class CakeResponse {
protected $_file = null;
/**
+ * File range. Used for requesting ranges of files.
+ *
+ * @var array
+ */
+ protected $_fileRange = null;
+
+/**
* The charset the response body is encoded with
*
* @var string
@@ -413,8 +420,8 @@ public function send() {
$this->_sendHeader($header, $value);
}
if ($this->_file) {
- $this->_sendFile($this->_file);
- $this->_file = null;
+ $this->_sendFile($this->_file, $this->_fileRange);
+ $this->_file = $this->_fileRange = null;
} else {
$this->_sendContent($this->_body);
}
@@ -1268,19 +1275,7 @@ public function file($path, $options = array()) {
$httpRange = env('HTTP_RANGE');
if (isset($httpRange)) {
- list(, $range) = explode('=', $httpRange);
-
- $size = $fileSize - 1;
- $length = $fileSize - $range;
-
- $this->header(array(
- 'Content-Length' => $length,
- 'Content-Range' => 'bytes ' . $range . $size . '/' . $fileSize
- ));
-
- $this->statusCode(206);
- $file->open('rb', true);
- $file->offset($range);
+ $this->_fileRange($file, $httpRange);
} else {
$this->header('Content-Length', $fileSize);
}
@@ -1292,21 +1287,72 @@ public function file($path, $options = array()) {
}
/**
+ * Apply a file range to a file and set the end offset.
+ *
+ * If an invalid range is requested a 416 Status code will be used
+ * in the response.
+ *
+ * @param File $file The file to set a range on.
+ * @param string $httpRange The range to use.
+ * @return void
+ */
+ protected function _fileRange($file, $httpRange) {
+ list(, $range) = explode('=', $httpRange);
+ list($start, $end) = explode('-', $range);
+
+ $fileSize = $file->size();
+ $lastByte = $fileSize - 1;
+ if ($start > $end || $end > $lastByte || $start > $lastByte) {
+ $this->statusCode(416);
+ $this->header(array(
+ 'Content-Range' => 'bytes 0-' . $lastByte . '/' . $fileSize
+ ));
+ return;
+ }
+
+ $this->header(array(
+ 'Content-Length' => $end - $start + 1,
+ 'Content-Range' => 'bytes ' . $start . '-' . $end . '/' . $fileSize
+ ));
+
+ $this->statusCode(206);
+ $this->_fileRange = array($start, $end);
+ }
+
+/**
* Reads out a file, and echos the content to the client.
*
* @param File $file File object
+ * @param array $range The range to read out of the file.
* @return boolean True is whole file is echoed successfully or false if client connection is lost in between
*/
- protected function _sendFile($file) {
+ protected function _sendFile($file, $range) {
$compress = $this->outputCompressed();
$file->open('rb');
+
+ $end = $start = false;
+ if ($range) {
+ list($start, $end) = $range;
+ }
+ if ($start !== false) {
+ $file->offset($start);
+ }
+
+ $bufferSize = 8192;
+ set_time_limit(0);
while (!feof($file->handle)) {
if (!$this->_isActive()) {
$file->close();
return false;
}
- set_time_limit(0);
- echo fread($file->handle, 8192);
+ $offset = $file->offset();
+ if ($end && $offset >= $end) {
+ break;
+ }
+ if ($end && $offset + $bufferSize >= $end) {
+ $bufferSize = $end - $offset;
+ }
+ echo fread($file->handle, $bufferSize);
if (!$compress) {
$this->_flushBuffer();
}
View
100 lib/Cake/Test/Case/Network/CakeResponseTest.php
@@ -1,9 +1,5 @@
<?php
/**
- * CakeResponse Test case file.
- *
- * PHP 5
- *
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
@@ -1389,4 +1385,100 @@ public function testFileExtensionNotSet() {
$response->file(CAKE . 'Test' . DS . 'test_app' . DS . 'Vendor' . DS . 'img' . DS . 'test_2.JPG');
}
+/**
+ * Test fetching ranges from a file.
+ *
+ * @return void
+ */
+ public function testFileRange() {
+ $_SERVER['HTTP_RANGE'] = 'bytes=8-25';
+ $response = $this->getMock('CakeResponse', array(
+ 'header',
+ 'type',
+ '_sendHeader',
+ '_setContentType',
+ '_isActive',
+ '_clearBuffer',
+ '_flushBuffer'
+ ));
+
+ $response->expects($this->exactly(1))
+ ->method('type')
+ ->with('css')
+ ->will($this->returnArgument(0));
+
+ $response->expects($this->at(1))
+ ->method('header')
+ ->with('Content-Disposition', 'attachment; filename="test_asset.css"');
+
+ $response->expects($this->at(2))
+ ->method('header')
+ ->with('Accept-Ranges', 'bytes');
+
+ $response->expects($this->at(3))
+ ->method('header')
+ ->with(array(
+ 'Content-Length' => 18,
+ 'Content-Range' => 'bytes 8-25/38',
+ ));
+
+ $response->expects($this->once())->method('_clearBuffer');
+
+ $response->expects($this->any())
+ ->method('_isActive')
+ ->will($this->returnValue(true));
+
+ $response->file(
+ CAKE . 'Test' . DS . 'test_app' . DS . 'Vendor' . DS . 'css' . DS . 'test_asset.css',
+ array('download' => true)
+ );
+
+ ob_start();
+ $result = $response->send();
+ $output = ob_get_clean();
+ $this->assertEquals(206, $response->statusCode());
+ $this->assertEquals("is the test asset", $output);
+ $this->assertTrue($result !== false);
+ }
+
+/**
+ * Test invalid file ranges.
+ *
+ * @return void
+ */
+ public function testFileRangeInvalid() {
+ $_SERVER['HTTP_RANGE'] = 'bytes=30-2';
+ $response = $this->getMock('CakeResponse', array(
+ 'header',
+ 'type',
+ '_sendHeader',
+ '_setContentType',
+ '_isActive',
+ '_clearBuffer',
+ '_flushBuffer'
+ ));
+
+ $response->expects($this->at(1))
+ ->method('header')
+ ->with('Content-Disposition', 'attachment; filename="test_asset.css"');
+
+ $response->expects($this->at(2))
+ ->method('header')
+ ->with('Accept-Ranges', 'bytes');
+
+ $response->expects($this->at(3))
+ ->method('header')
+ ->with(array(
+ 'Content-Range' => 'bytes 0-37/38',
+ ));
+
+ $response->file(
+ CAKE . 'Test' . DS . 'test_app' . DS . 'Vendor' . DS . 'css' . DS . 'test_asset.css',
+ array('download' => true)
+ );
+
+ $this->assertEquals(416, $response->statusCode());
+ $result = $response->send();
+ }
+
}

0 comments on commit 494fd05

Please sign in to comment.