Browse Source

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
tags/2.0.0-beta
Carsten Brandt 12 years ago
parent
commit
a2c6d22124
  1. 86
      tests/unit/framework/web/ResponseTest.php
  2. 32
      yii/web/Response.php

86
tests/unit/framework/web/ResponseTest.php

@ -0,0 +1,86 @@
<?php
namespace yii\web;
use yiiunit\framework\web\ResponseTest;
/**
* Mock PHP header function to check for sent headers
* @param string $string
* @param bool $replace
* @param int $httpResponseCode
*/
function header($string, $replace = true, $httpResponseCode = null) {
ResponseTest::$headers[] = $string;
// TODO implement replace
if ($httpResponseCode !== null) {
ResponseTest::$httpResponseCode = $httpResponseCode;
}
}
namespace yiiunit\framework\web;
use yii\helpers\StringHelper;
use yii\web\Response;
class ResponseTest extends \yiiunit\TestCase
{
public static $headers = array();
public static $httpResponseCode = 200;
protected function setUp()
{
parent::setUp();
$this->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;
}
}

32
yii/web/Response.php

@ -8,6 +8,7 @@
namespace yii\web; namespace yii\web;
use Yii; use Yii;
use yii\base\HttpException;
use yii\helpers\FileHelper; use yii\helpers\FileHelper;
use yii\helpers\Html; use yii\helpers\Html;
use yii\helpers\StringHelper; use yii\helpers\StringHelper;
@ -32,31 +33,26 @@ class Response extends \yii\base\Response
* @param string $content content to be set. * @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 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 * @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) public function sendFile($fileName, $content, $mimeType = null, $terminate = true)
{ {
if ($mimeType === null) { if ($mimeType === null && (($mimeType = FileHelper::getMimeTypeByExtension($fileName)) === null)) {
if (($mimeType = FileHelper::getMimeTypeByExtension($fileName)) === null) {
$mimeType = 'application/octet-stream'; $mimeType = 'application/octet-stream';
} }
}
$fileSize = StringHelper::strlen($content); $fileSize = StringHelper::strlen($content);
$contentStart = 0; $contentStart = 0;
$contentEnd = $fileSize - 1; $contentEnd = $fileSize - 1;
if (isset($_SERVER['HTTP_RANGE'])) { // tell the client that we accept range requests
header('Accept-Ranges: bytes'); header('Accept-Ranges: bytes');
if (isset($_SERVER['HTTP_RANGE'])) {
// client sent us a multibyte range, can not hold this one for now // 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"); header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
ob_start(); throw new HttpException(416, 'Requested Range Not Satisfiable');
Yii::app()->end(0,false);
ob_end_clean();
exit(0);
} }
$range = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']); $range = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']);
@ -67,7 +63,7 @@ class Response extends \yii\base\Response
} else { } else {
$range = explode('-', $range); $range = explode('-', $range);
$contentStart = $range[0]; $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. /* 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); $wrongContentStart = ($contentStart > $contentEnd || $contentStart > $fileSize - 1 || $contentStart < 0);
if ($wrongContentStart) { if ($wrongContentStart) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes $contentStart-$contentEnd/$fileSize"); header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
ob_start(); throw new HttpException(416, 'Requested Range Not Satisfiable');
Yii::app()->end(0,false);
ob_end_clean();
exit(0);
} }
header('HTTP/1.1 206 Partial Content'); header('HTTP/1.1 206 Partial Content');
@ -99,17 +91,17 @@ class Response extends \yii\base\Response
header('Pragma: public'); header('Pragma: public');
header('Expires: 0'); header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header("Content-type: $mimeType"); header('Content-Type: ' . $mimeType);
header('Content-Length: ' . $length); header('Content-Length: ' . $length);
header("Content-Disposition: attachment; filename=\"$fileName\""); header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Content-Transfer-Encoding: binary'); header('Content-Transfer-Encoding: binary');
$content = StringHelper::strlen($content); $content = StringHelper::substr($content, $contentStart, $length);
if ($terminate) { if ($terminate) {
// clean up the application first because the file downloading could take long time // clean up the application first because the file downloading could take long time
// which may cause timeout of some resources (such as DB connection) // which may cause timeout of some resources (such as DB connection)
ob_start(); ob_start();
Yii::app()->end(0,false); Yii::$app->end(0, false);
ob_end_clean(); ob_end_clean();
echo $content; echo $content;
exit(0); exit(0);

Loading…
Cancel
Save