Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[HttpFoundation] Added BinaryFileResponse.
- Loading branch information
Showing
2 changed files
with
383 additions
and
0 deletions.
There are no files selected for viewing
259 changes: 259 additions & 0 deletions
259
src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,259 @@ | |||
<?php | |||
|
|||
/** | |||
* This file is part of the Symfony package. | |||
* | |||
* (c) Fabien Potencier <fabien@symfony.com> | |||
* | |||
* For the full copyright and license information, please view the LICENSE | |||
* file that was distributed with this source code. | |||
*/ | |||
|
|||
namespace Symfony\Component\HttpFoundation; | |||
|
|||
use Symfony\Component\HttpFoundation\File\File; | |||
use Symfony\Component\HttpFoundation\File\Exception\FileException; | |||
|
|||
/** | |||
* BinaryFileResponse represents an HTTP response delivering a file. | |||
* | |||
* @author Niklas Fiekas <niklas.fiekas@tu-clausthal.de> | |||
* @author stealth35 <stealth35-php@live.fr> | |||
* @author Igor Wiedler <igor@wiedler.ch> | |||
* @author Jordan Alliot <jordan.alliot@gmail.com> | |||
* @author Sergey Linnik <linniksa@gmail.com> | |||
*/ | |||
class BinaryFileResponse extends Response | |||
{ | |||
protected static $trustXSendfileTypeHeader = false; | |||
|
|||
protected $file; | |||
protected $offset; | |||
protected $maxlen; | |||
|
|||
/** | |||
* Constructor. | |||
* | |||
* @param SplFileInfo|string $file The file to stream | |||
* @param integer $status The response status code | |||
* @param array $headers An array of response headers | |||
* @param boolean $public Files are public by default | |||
* @param null|string $contentDisposition The type of Content-Disposition to set automatically with the filename | |||
* @param boolean $autoEtag Whether the ETag header should be automatically set | |||
* @param boolean $autoLastModified Whether the Last-Modified header should be automatically set | |||
*/ | |||
public function __construct($file, $status = 200, $headers = array(), $public = true, $contentDisposition = null, $autoEtag = false, $autoLastModified = true) | |||
{ | |||
parent::__construct(null, $status, $headers); | |||
|
|||
$this->setFile($file, $contentDisposition, $autoEtag, $autoLastModified); | |||
|
|||
if ($public) { | |||
$this->setPublic(); | |||
} | |||
} | |||
|
|||
/** | |||
* {@inheritdoc} | |||
*/ | |||
public static function create($file = null, $status = 200, $headers = array(), $public = true, $contentDisposition = null, $autoEtag = false, $autoLastModified = true) | |||
{ | |||
return new static($file, $status, $headers, $public, $contentDisposition, $autoEtag, $autoLastModified); | |||
} | |||
|
|||
/** | |||
* Sets the file to stream. | |||
* | |||
* @param SplFileInfo|string $file The file to stream | |||
*/ | |||
public function setFile($file, $contentDisposition = null, $autoEtag = false, $autoLastModified = true) | |||
{ | |||
$file = new File((string) $file); | |||
|
|||
if (!$file->isReadable()) { | |||
throw new FileException('File must be readable.'); | |||
} | |||
|
|||
$this->file = $file; | |||
|
|||
if ($autoEtag) { | |||
$this->setAutoEtag(); | |||
} | |||
|
|||
if ($autoLastModified) { | |||
$this->setAutoLastModified(); | |||
} | |||
|
|||
if ($contentDisposition) { | |||
$this->setContentDisposition($contentDisposition); | |||
} | |||
|
|||
return $this; | |||
} | |||
|
|||
/** | |||
* Gets the file. | |||
* | |||
* @return File The file to stream | |||
*/ | |||
public function getFile() | |||
{ | |||
return $this->file; | |||
} | |||
|
|||
/** | |||
* Automatically sets the Last-Modified header according the file modification date. | |||
*/ | |||
public function setAutoLastModified() | |||
{ | |||
$this->setLastModified(\DateTime::createFromFormat('U', $this->file->getMTime())); | |||
|
|||
return $this; | |||
} | |||
|
|||
/** | |||
* Automatically sets the ETag header according to the checksum of the file. | |||
*/ | |||
public function setAutoEtag() | |||
{ | |||
$this->setEtag(sha1_file($this->file->getPathname())); | |||
|
|||
return $this; | |||
} | |||
|
|||
/** | |||
* Sets the Content-Disposition header with the given filename. | |||
* | |||
* @param string $disposition ResponseHeaderBag::DISPOSITION_INLINE or ResponseHeaderBag::DISPOSITION_ATTACHMENT | |||
* @param string $filename Optionally use this filename instead of the real name of the file | |||
* @param string $filenameFallback A fallback filename, containing only ASCII characters. Defaults to an automatically encoded filename | |||
*/ | |||
public function setContentDisposition($disposition, $filename = '', $filenameFallback = '') | |||
{ | |||
if ($filename === '') { | |||
$filename = $this->file->getFilename(); | |||
} | |||
|
|||
$dispositionHeader = $this->headers->makeDisposition($disposition, $filename, $filenameFallback); | |||
$this->headers->set('Content-Disposition', $dispositionHeader); | |||
|
|||
return $this; | |||
} | |||
|
|||
/** | |||
* {@inheritdoc} | |||
*/ | |||
public function prepare(Request $request) | |||
{ | |||
$this->headers->set('Content-Length', $this->file->getSize()); | |||
$this->headers->set('Accept-Ranges', 'bytes'); | |||
$this->headers->set('Content-Transfer-Encoding', 'binary'); | |||
|
|||
if (!$this->headers->has('Content-Type')) { | |||
$this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream'); | |||
} | |||
|
|||
if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) { | |||
$this->setProtocolVersion('1.1'); | |||
} | |||
|
|||
$this->offset = 0; | |||
$this->maxlen = -1; | |||
|
|||
if (self::$trustXSendfileTypeHeader && $request->headers->has('X-Sendfile-Type')) { | |||
// Use X-Sendfile, do not send any content. | |||
$type = $request->headers->get('X-Sendfile-Type'); | |||
$path = $this->file->getRealPath(); | |||
if (strtolower($type) == 'x-accel-redirect') { | |||
// Do X-Accel-Mapping substitutions. | |||
foreach (explode(',', $request->headers->get('X-Accel-Mapping', '')) as $mapping) { | |||
$mapping = explode('=', $mapping, 2); | |||
|
|||
if (2 == count($mapping)) { | |||
$location = trim($mapping[0]); | |||
$pathPrefix = trim($mapping[1]); | |||
|
|||
if (substr($path, 0, strlen($pathPrefix)) == $pathPrefix) { | |||
$path = $location . substr($path, strlen($pathPrefix)); | |||
break; | |||
} | |||
} | |||
} | |||
} | |||
$this->headers->set($type, $path); | |||
$this->maxlen = 0; | |||
} elseif ($request->headers->has('Range')) { | |||
// Process the range headers. | |||
if (!$request->headers->has('If-Range') || $this->getEtag() == $request->headers->get('If-Range')) { | |||
$range = $request->headers->get('Range'); | |||
|
|||
list($start, $end) = array_map('intval', explode('-', substr($range, 6), 2)) + array(0); | |||
|
|||
if ('' !== $end) { | |||
$this->maxlen = $end - $start; | |||
} else { | |||
$end = $this->file->getSize() - 1; | |||
} | |||
|
|||
$this->offset = $start; | |||
|
|||
$this->setStatusCode(206); | |||
$this->headers->set('Content-Range', sprintf('bytes %s-%s/%s', $start, $end, $this->file->getSize())); | |||
} | |||
} | |||
} | |||
|
|||
/** | |||
* Sends the file. | |||
*/ | |||
public function sendContent() | |||
{ | |||
if (!$this->isSuccessful()) { | |||
parent::sendContent(); | |||
|
|||
return; | |||
} | |||
|
|||
if (0 === $this->maxlen) { | |||
return; | |||
} | |||
|
|||
$out = fopen('php://output', 'wb'); | |||
$file = fopen($this->file->getPathname(), 'rb'); | |||
|
|||
stream_copy_to_stream($file, $out, $this->maxlen, $this->offset); | |||
|
|||
fclose($out); | |||
fclose($file); | |||
} | |||
|
|||
/** | |||
* {@inheritdoc} | |||
* | |||
* @throws \LogicException when the content is not null | |||
*/ | |||
public function setContent($content) | |||
{ | |||
if (null !== $content) { | |||
throw new \LogicException('The content cannot be set on a BinaryFileResponse instance.'); | |||
} | |||
} | |||
|
|||
/** | |||
* {@inheritdoc} | |||
* | |||
* @return false | |||
*/ | |||
public function getContent() | |||
{ | |||
return false; | |||
} | |||
|
|||
/** | |||
* Trust X-Sendfile-Type header. | |||
*/ | |||
public static function trustXSendfileTypeHeader() | |||
{ | |||
self::$trustXSendfileTypeHeader = true; | |||
} | |||
} |
124 changes: 124 additions & 0 deletions
124
src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,124 @@ | |||
<?php | |||
|
|||
/* | |||
* This file is part of the Symfony package. | |||
* | |||
* (c) Fabien Potencier <fabien@symfony.com> | |||
* | |||
* For the full copyright and license information, please view the LICENSE | |||
* file that was distributed with this source code. | |||
*/ | |||
|
|||
namespace Symfony\Component\HttpFoundation\Tests; | |||
|
|||
use Symfony\Component\HttpFoundation\BinaryFileResponse; | |||
use Symfony\Component\HttpFoundation\Request; | |||
use Symfony\Component\HttpFoundation\ResponseHeaderBag; | |||
|
|||
class BinaryFileResponseTest extends \PHPUnit_Framework_TestCase | |||
{ | |||
public function testConstruction() | |||
{ | |||
$response = new BinaryFileResponse('README.md', 404, array('X-Header' => 'Foo'), true, null, true, true); | |||
$this->assertEquals(404, $response->getStatusCode()); | |||
$this->assertEquals('Foo', $response->headers->get('X-Header')); | |||
$this->assertTrue($response->headers->has('ETag')); | |||
$this->assertTrue($response->headers->has('Last-Modified')); | |||
$this->assertFalse($response->headers->has('Content-Disposition')); | |||
|
|||
$response = BinaryFileResponse::create('README.md', 404, array(), true, ResponseHeaderBag::DISPOSITION_INLINE); | |||
$this->assertEquals(404, $response->getStatusCode()); | |||
$this->assertFalse($response->headers->has('ETag')); | |||
$this->assertEquals('inline; filename="README.md"', $response->headers->get('Content-Disposition')); | |||
} | |||
|
|||
/** | |||
* @expectedException \LogicException | |||
*/ | |||
public function testSetContent() | |||
{ | |||
$response = new BinaryFileResponse('README.md'); | |||
$response->setContent('foo'); | |||
} | |||
|
|||
public function testGetContent() | |||
{ | |||
$response = new BinaryFileResponse('README.md'); | |||
$this->assertFalse($response->getContent()); | |||
} | |||
|
|||
public function testRequests() | |||
{ | |||
$response = BinaryFileResponse::create('src/Symfony/Component/HttpFoundation/Tests/File/Fixtures/test.gif')->setAutoEtag(); | |||
|
|||
// do a request to get the ETag | |||
$request = Request::create('/'); | |||
$response->prepare($request); | |||
$etag = $response->headers->get('ETag'); | |||
|
|||
// prepare a request for a range of the testing file | |||
$request = Request::create('/'); | |||
$request->headers->set('If-Range', $etag); | |||
$request->headers->set('Range', 'bytes=1-4'); | |||
|
|||
$this->expectOutputString('IF8'); | |||
$response = clone $response; | |||
$response->prepare($request); | |||
$response->sendContent(); | |||
|
|||
$this->assertEquals('binary', $response->headers->get('Content-Transfer-Encoding')); | |||
} | |||
|
|||
public function testXSendfile() | |||
{ | |||
$request = Request::create('/'); | |||
$request->headers->set('X-Sendfile-Type', 'X-Sendfile'); | |||
|
|||
BinaryFileResponse::trustXSendfileTypeHeader(); | |||
$response = BinaryFileResponse::create('README.md'); | |||
$response->prepare($request); | |||
|
|||
$this->expectOutputString(''); | |||
$response->sendContent(); | |||
|
|||
$this->assertContains('README.md', $response->headers->get('X-Sendfile')); | |||
} | |||
|
|||
/** | |||
* @dataProvider getSampleXAccelMappings | |||
*/ | |||
public function testXAccelMapping($realpath, $mapping, $virtual) | |||
{ | |||
$request = Request::create('/'); | |||
$request->headers->set('X-Sendfile-Type', 'X-Accel-Redirect'); | |||
$request->headers->set('X-Accel-Mapping', $mapping); | |||
|
|||
$file = $this->getMockBuilder('Symfony\Component\HttpFoundation\File\File') | |||
->disableOriginalConstructor() | |||
->getMock(); | |||
$file->expects($this->any()) | |||
->method('getRealPath') | |||
->will($this->returnValue($realpath)); | |||
$file->expects($this->any()) | |||
->method('isReadable') | |||
->will($this->returnValue(true)); | |||
|
|||
BinaryFileResponse::trustXSendFileTypeHeader(); | |||
$response = new BinaryFileResponse('README.md'); | |||
$reflection = new \ReflectionObject($response); | |||
$property = $reflection->getProperty('file'); | |||
$property->setAccessible(true); | |||
$property->setValue($response, $file); | |||
|
|||
$response->prepare($request); | |||
$this->assertEquals($virtual, $response->headers->get('X-Accel-Redirect')); | |||
} | |||
|
|||
public function getSampleXAccelMappings() | |||
{ | |||
return array( | |||
array('/var/www/var/www/files/foo.txt', '/files/=/var/www/', '/files/var/www/files/foo.txt'), | |||
array('/home/foo/bar.txt', '/files/=/var/www/,/baz/=/home/foo/', '/baz/bar.txt'), | |||
); | |||
} | |||
} |