From ba31ee618f219a046677322df13baab6e78840a0 Mon Sep 17 00:00:00 2001 From: Ragazzo Date: Wed, 15 May 2013 14:55:02 +0400 Subject: [PATCH 1/5] partial response added, new code-style applied --- yii/web/Response.php | 74 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/yii/web/Response.php b/yii/web/Response.php index 54b7f6e..2d2d230 100644 --- a/yii/web/Response.php +++ b/yii/web/Response.php @@ -35,28 +35,86 @@ class Response extends \yii\base\Response */ public function sendFile($fileName, $content, $mimeType = null, $terminate = true) { - if ($mimeType === null && ($mimeType = FileHelper::getMimeType($fileName)) === null) { - $mimeType = 'application/octet-stream'; + if ($mimeType === null) { + if (($mimeType = CFileHelper::getMimeTypeByExtension($fileName)) === null) { + $mimeType='text/plain'; + } } + + $fileSize = (function_exists('mb_strlen') ? mb_strlen($content,'8bit') : strlen($content)); + $contentStart = 0; + $contentEnd = $fileSize - 1; + + if (isset($_SERVER['HTTP_RANGE'])) { + header('Accept-Ranges: bytes'); + + //client sent us a multibyte range, can not hold this one for now + if (strpos(',',$_SERVER['HTTP_RANGE']) !== false) { + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header("Content-Range: bytes $contentStart-$contentEnd/$fileSize"); + ob_start(); + Yii::app()->end(0,false); + ob_end_clean(); + exit(0); + } + + $range = str_replace('bytes=','',$_SERVER['HTTP_RANGE']); + + //range requests starts from "-", so it means that data must be dumped the end point. + if ($range[0] === '-') { + $contentStart = $fileSize - substr($range,1); + } else { + $range = explode('-',$range); + $contentStart = $range[0]; + $contentEnd = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $fileSize; + } + + /* Check the range and make sure it's treated according to the specs. + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + */ + // End bytes can not be larger than $end. + $contentEnd = ($contentEnd > $fileSize) ? $fileSize : $contentEnd; + + // Validate the requested range and return an error if it's not correct. + $wrongContentStart = ($contentStart > $contentEnd || $contentStart > $fileSize - 1 || $contentStart < 0); + + if ($wrongContentStart) { + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header("Content-Range: bytes $contentStart-$contentEnd/$fileSize"); + ob_start(); + Yii::app()->end(0,false); + ob_end_clean(); + exit(0); + } + + header('HTTP/1.1 206 Partial Content'); + header("Content-Range: bytes $contentStart-$contentEnd/$fileSize"); + } else { + header('HTTP/1.1 200 OK'); + } + + $length = $contentEnd - $contentStart + 1; // Calculate new content length + header('Pragma: public'); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header("Content-type: $mimeType"); - if (ob_get_length() === false) { - header('Content-Length: ' . (function_exists('mb_strlen') ? mb_strlen($content, '8bit') : strlen($content))); - } + header('Content-Length: '.$length); header("Content-Disposition: attachment; filename=\"$fileName\""); header('Content-Transfer-Encoding: binary'); + $content = function_exists('mb_substr') ? mb_substr($content,$contentStart,$length) : substr($content,$contentStart,$length); if ($terminate) { // clean up the application first because the file downloading could take long time // which may cause timeout of some resources (such as DB connection) - Yii::app()->end(0, false); + ob_start(); + Yii::app()->end(0,false); + ob_end_clean(); echo $content; exit(0); - } else { - echo $content; } + else + echo $content; } /** From 04563cb76a5b34e6676d8ea9a1257e93ea08b2ed Mon Sep 17 00:00:00 2001 From: Ragazzo Date: Wed, 15 May 2013 14:57:12 +0400 Subject: [PATCH 2/5] code style fix --- yii/web/Response.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yii/web/Response.php b/yii/web/Response.php index 2d2d230..1bcb54e 100644 --- a/yii/web/Response.php +++ b/yii/web/Response.php @@ -112,9 +112,9 @@ class Response extends \yii\base\Response ob_end_clean(); echo $content; exit(0); - } - else + } else { echo $content; + } } /** From 959cff3e9b872b7a8b23a94a9e41959a932088a4 Mon Sep 17 00:00:00 2001 From: Ragazzo Date: Wed, 15 May 2013 15:22:01 +0400 Subject: [PATCH 3/5] string helper fixed, mime-type reverted --- yii/web/Response.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/yii/web/Response.php b/yii/web/Response.php index 1bcb54e..0f166f4 100644 --- a/yii/web/Response.php +++ b/yii/web/Response.php @@ -10,6 +10,7 @@ namespace yii\web; use Yii; use yii\helpers\FileHelper; use yii\helpers\Html; +use yii\helpers\StringHelper; /** * @author Qiang Xue @@ -36,12 +37,12 @@ class Response extends \yii\base\Response public function sendFile($fileName, $content, $mimeType = null, $terminate = true) { if ($mimeType === null) { - if (($mimeType = CFileHelper::getMimeTypeByExtension($fileName)) === null) { - $mimeType='text/plain'; + if (($mimeType = FileHelper::getMimeTypeByExtension($fileName)) === null) { + $mimeType='application/octet-stream'; } } - $fileSize = (function_exists('mb_strlen') ? mb_strlen($content,'8bit') : strlen($content)); + $fileSize = StringHelper::strlen($content); $contentStart = 0; $contentEnd = $fileSize - 1; @@ -49,7 +50,7 @@ class Response extends \yii\base\Response header('Accept-Ranges: bytes'); //client sent us a multibyte range, can not hold this one for now - if (strpos(',',$_SERVER['HTTP_RANGE']) !== false) { + if (strpos(',', $_SERVER['HTTP_RANGE']) !== false) { header('HTTP/1.1 416 Requested Range Not Satisfiable'); header("Content-Range: bytes $contentStart-$contentEnd/$fileSize"); ob_start(); @@ -58,13 +59,13 @@ class Response extends \yii\base\Response exit(0); } - $range = str_replace('bytes=','',$_SERVER['HTTP_RANGE']); + $range = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']); //range requests starts from "-", so it means that data must be dumped the end point. if ($range[0] === '-') { - $contentStart = $fileSize - substr($range,1); + $contentStart = $fileSize - substr($range, 1); } else { - $range = explode('-',$range); + $range = explode('-', $range); $contentStart = $range[0]; $contentEnd = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $fileSize; } @@ -102,7 +103,7 @@ class Response extends \yii\base\Response header('Content-Length: '.$length); header("Content-Disposition: attachment; filename=\"$fileName\""); header('Content-Transfer-Encoding: binary'); - $content = function_exists('mb_substr') ? mb_substr($content,$contentStart,$length) : substr($content,$contentStart,$length); + $content = StringHelper::strlen($content); if ($terminate) { // clean up the application first because the file downloading could take long time From 762ed2e04d35e14d63d62cf12923f623eec27231 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 15 May 2013 20:36:02 +0200 Subject: [PATCH 4/5] Display Name of HttpException instead of classname class name is alwarys HttpException, better display the name of the http error. --- yii/base/ErrorHandler.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/yii/base/ErrorHandler.php b/yii/base/ErrorHandler.php index 44c2ca0..8340723 100644 --- a/yii/base/ErrorHandler.php +++ b/yii/base/ErrorHandler.php @@ -75,8 +75,11 @@ class ErrorHandler extends Component \Yii::$app->runAction($this->errorAction); } elseif (\Yii::$app instanceof \yii\web\Application) { if (!headers_sent()) { - $errorCode = $exception instanceof HttpException ? $exception->statusCode : 500; - header("HTTP/1.0 $errorCode " . get_class($exception)); + if ($exception instanceof HttpException) { + header('HTTP/1.0 ' . $exception->statusCode . ' ' . $exception->getName()); + } else { + header('HTTP/1.0 500 ' . get_class($exception)); + } } if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { \Yii::$app->renderException($exception); From a2c6d221248333d68b056902f20aeade0740b5b9 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 15 May 2013 20:39:23 +0200 Subject: [PATCH 5/5] refactored web/Response::sendFile() - better throw http exception on not satisfiable range request - constitent header names - fixed range end when range request is to the end - added unit test related to #275, fixes #148 --- tests/unit/framework/web/ResponseTest.php | 86 +++++++++++++++++++++++++++++++ yii/web/Response.php | 42 ++++++--------- 2 files changed, 103 insertions(+), 25 deletions(-) create mode 100644 tests/unit/framework/web/ResponseTest.php diff --git a/tests/unit/framework/web/ResponseTest.php b/tests/unit/framework/web/ResponseTest.php new file mode 100644 index 0000000..b3d9080 --- /dev/null +++ b/tests/unit/framework/web/ResponseTest.php @@ -0,0 +1,86 @@ +reset(); + } + + protected function reset() + { + static::$headers = array(); + static::$httpResponseCode = 200; + } + + public function ranges() + { + // TODO test more cases for range requests and check for rfc compatibility + // http://www.w3.org/Protocols/rfc2616/rfc2616.txt + return array( + array('0-5', '0-5', 6, '12ёж'), + array('2-', '2-66', 65, 'ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=?'), + array('-12', '55-66', 12, '(ёжик)=?'), + ); + } + + /** + * @dataProvider ranges + */ + public function testSendFileRanges($rangeHeader, $expectedHeader, $length, $expectedFile) + { + $content = $this->generateTestFileContent(); + + $_SERVER['HTTP_RANGE'] = 'bytes=' . $rangeHeader; + $sent = $this->runSendFile('testFile.txt', $content, null); + $this->assertEquals($expectedFile, $sent); + $this->assertTrue(in_array('HTTP/1.1 206 Partial Content', static::$headers)); + $this->assertTrue(in_array('Accept-Ranges: bytes', static::$headers)); + $this->assertArrayHasKey('Content-Range: bytes ' . $expectedHeader . '/' . StringHelper::strlen($content), array_flip(static::$headers)); + $this->assertTrue(in_array('Content-Type: text/plain', static::$headers)); + $this->assertTrue(in_array('Content-Length: ' . $length, static::$headers)); + } + + protected function generateTestFileContent() + { + return '12ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=?'; + } + + protected function runSendFile($fileName, $content, $mimeType) + { + ob_start(); + ob_implicit_flush(false); + $response = new Response(); + $response->sendFile($fileName, $content, $mimeType, false); + $file = ob_get_clean(); + return $file; + } +} \ No newline at end of file diff --git a/yii/web/Response.php b/yii/web/Response.php index 0f166f4..954c999 100644 --- a/yii/web/Response.php +++ b/yii/web/Response.php @@ -8,6 +8,7 @@ namespace yii\web; use Yii; +use yii\base\HttpException; use yii\helpers\FileHelper; use yii\helpers\Html; use yii\helpers\StringHelper; @@ -32,42 +33,37 @@ class Response extends \yii\base\Response * @param string $content content to be set. * @param string $mimeType mime type of the content. If null, it will be guessed automatically based on the given file name. * @param boolean $terminate whether to terminate the current application after calling this method - * @todo + * @throws \yii\base\HttpException when range request is not satisfiable. */ public function sendFile($fileName, $content, $mimeType = null, $terminate = true) { - if ($mimeType === null) { - if (($mimeType = FileHelper::getMimeTypeByExtension($fileName)) === null) { - $mimeType='application/octet-stream'; - } + if ($mimeType === null && (($mimeType = FileHelper::getMimeTypeByExtension($fileName)) === null)) { + $mimeType = 'application/octet-stream'; } $fileSize = StringHelper::strlen($content); $contentStart = 0; $contentEnd = $fileSize - 1; - if (isset($_SERVER['HTTP_RANGE'])) { - header('Accept-Ranges: bytes'); + // tell the client that we accept range requests + header('Accept-Ranges: bytes'); - //client sent us a multibyte range, can not hold this one for now + if (isset($_SERVER['HTTP_RANGE'])) { + // client sent us a multibyte range, can not hold this one for now if (strpos(',', $_SERVER['HTTP_RANGE']) !== false) { - header('HTTP/1.1 416 Requested Range Not Satisfiable'); header("Content-Range: bytes $contentStart-$contentEnd/$fileSize"); - ob_start(); - Yii::app()->end(0,false); - ob_end_clean(); - exit(0); + throw new HttpException(416, 'Requested Range Not Satisfiable'); } $range = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']); - //range requests starts from "-", so it means that data must be dumped the end point. + // range requests starts from "-", so it means that data must be dumped the end point. if ($range[0] === '-') { $contentStart = $fileSize - substr($range, 1); } else { $range = explode('-', $range); $contentStart = $range[0]; - $contentEnd = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $fileSize; + $contentEnd = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $fileSize - 1; } /* Check the range and make sure it's treated according to the specs. @@ -80,12 +76,8 @@ class Response extends \yii\base\Response $wrongContentStart = ($contentStart > $contentEnd || $contentStart > $fileSize - 1 || $contentStart < 0); if ($wrongContentStart) { - header('HTTP/1.1 416 Requested Range Not Satisfiable'); header("Content-Range: bytes $contentStart-$contentEnd/$fileSize"); - ob_start(); - Yii::app()->end(0,false); - ob_end_clean(); - exit(0); + throw new HttpException(416, 'Requested Range Not Satisfiable'); } header('HTTP/1.1 206 Partial Content'); @@ -99,17 +91,17 @@ class Response extends \yii\base\Response header('Pragma: public'); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - header("Content-type: $mimeType"); - header('Content-Length: '.$length); - header("Content-Disposition: attachment; filename=\"$fileName\""); + header('Content-Type: ' . $mimeType); + header('Content-Length: ' . $length); + header('Content-Disposition: attachment; filename="' . $fileName . '"'); header('Content-Transfer-Encoding: binary'); - $content = StringHelper::strlen($content); + $content = StringHelper::substr($content, $contentStart, $length); if ($terminate) { // clean up the application first because the file downloading could take long time // which may cause timeout of some resources (such as DB connection) ob_start(); - Yii::app()->end(0,false); + Yii::$app->end(0, false); ob_end_clean(); echo $content; exit(0);