2015-08-17 17:00:26 -07:00
< ? 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\Exception\FileException ;
2018-11-23 12:29:20 +00:00
use Symfony\Component\HttpFoundation\File\File ;
2015-08-17 17:00:26 -07:00
/**
* 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 ;
2016-04-20 09:56:34 -07:00
/**
* @ var File
*/
2015-08-17 17:00:26 -07:00
protected $file ;
2018-11-23 12:29:20 +00:00
protected $offset = 0 ;
protected $maxlen = - 1 ;
2015-08-17 17:00:26 -07:00
protected $deleteFileAfterSend = false ;
/**
* @ param \SplFileInfo | string $file The file to stream
* @ param int $status The response status code
* @ param array $headers An array of response headers
* @ param bool $public Files are public by default
2018-11-23 12:29:20 +00:00
* @ param string | null $contentDisposition The type of Content - Disposition to set automatically with the filename
2015-08-17 17:00:26 -07:00
* @ param bool $autoEtag Whether the ETag header should be automatically set
* @ param bool $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 ();
}
}
/**
* @ param \SplFileInfo | string $file The file to stream
* @ param int $status The response status code
* @ param array $headers An array of response headers
* @ param bool $public Files are public by default
2018-11-23 12:29:20 +00:00
* @ param string | null $contentDisposition The type of Content - Disposition to set automatically with the filename
2015-08-17 17:00:26 -07:00
* @ param bool $autoEtag Whether the ETag header should be automatically set
* @ param bool $autoLastModified Whether the Last - Modified header should be automatically set
*
2017-02-02 16:28:38 -08:00
* @ return static
2015-08-17 17:00:26 -07:00
*/
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
* @ param string $contentDisposition
* @ param bool $autoEtag
* @ param bool $autoLastModified
*
2017-02-02 16:28:38 -08:00
* @ return $this
2015-08-17 17:00:26 -07:00
*
* @ throws FileException
*/
public function setFile ( $file , $contentDisposition = null , $autoEtag = false , $autoLastModified = true )
{
if ( ! $file instanceof File ) {
if ( $file instanceof \SplFileInfo ) {
$file = new File ( $file -> getPathname ());
} else {
$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 ()
{
2018-11-23 12:29:20 +00:00
$this -> setEtag ( base64_encode ( hash_file ( 'sha256' , $this -> file -> getPathname (), true )));
2015-08-17 17:00:26 -07:00
return $this ;
}
/**
* Sets the Content - Disposition header with the given filename .
*
* @ param string $disposition ResponseHeaderBag :: DISPOSITION_INLINE or ResponseHeaderBag :: DISPOSITION_ATTACHMENT
2018-11-23 12:29:20 +00:00
* @ param string $filename Optionally use this UTF - 8 encoded filename instead of the real name of the file
2015-08-17 17:00:26 -07:00
* @ param string $filenameFallback A fallback filename , containing only ASCII characters . Defaults to an automatically encoded filename
*
2017-02-02 16:28:38 -08:00
* @ return $this
2015-08-17 17:00:26 -07:00
*/
public function setContentDisposition ( $disposition , $filename = '' , $filenameFallback = '' )
{
2018-11-23 12:29:20 +00:00
if ( '' === $filename ) {
2015-08-17 17:00:26 -07:00
$filename = $this -> file -> getFilename ();
}
2016-04-20 09:56:34 -07:00
if ( '' === $filenameFallback && ( ! preg_match ( '/^[\x20-\x7e]*$/' , $filename ) || false !== strpos ( $filename , '%' ))) {
2018-11-23 12:29:20 +00:00
$encoding = mb_detect_encoding ( $filename , null , true ) ? : '8bit' ;
2016-04-20 09:56:34 -07:00
2017-04-13 15:53:35 +01:00
for ( $i = 0 , $filenameLength = mb_strlen ( $filename , $encoding ); $i < $filenameLength ; ++ $i ) {
2016-04-20 09:56:34 -07:00
$char = mb_substr ( $filename , $i , 1 , $encoding );
2018-11-23 12:29:20 +00:00
if ( '%' === $char || \ord ( $char ) < 32 || \ord ( $char ) > 126 ) {
2016-04-20 09:56:34 -07:00
$filenameFallback .= '_' ;
} else {
$filenameFallback .= $char ;
}
}
}
2015-08-17 17:00:26 -07:00
$dispositionHeader = $this -> headers -> makeDisposition ( $disposition , $filename , $filenameFallback );
$this -> headers -> set ( 'Content-Disposition' , $dispositionHeader );
return $this ;
}
/**
* { @ inheritdoc }
*/
public function prepare ( Request $request )
{
if ( ! $this -> headers -> has ( 'Content-Type' )) {
$this -> headers -> set ( 'Content-Type' , $this -> file -> getMimeType () ? : 'application/octet-stream' );
}
2016-04-20 09:56:34 -07:00
if ( 'HTTP/1.0' !== $request -> server -> get ( 'SERVER_PROTOCOL' )) {
2015-08-17 17:00:26 -07:00
$this -> setProtocolVersion ( '1.1' );
}
$this -> ensureIEOverSSLCompatibility ( $request );
$this -> offset = 0 ;
$this -> maxlen = - 1 ;
2018-11-23 12:29:20 +00:00
if ( false === $fileSize = $this -> file -> getSize ()) {
return $this ;
}
$this -> headers -> set ( 'Content-Length' , $fileSize );
if ( ! $this -> headers -> has ( 'Accept-Ranges' )) {
// Only accept ranges on safe HTTP methods
$this -> headers -> set ( 'Accept-Ranges' , $request -> isMethodSafe ( false ) ? 'bytes' : 'none' );
}
2015-08-17 17:00:26 -07:00
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 ();
2016-04-20 09:56:34 -07:00
// Fall back to scheme://path for stream wrapped locations.
if ( false === $path ) {
$path = $this -> file -> getPathname ();
}
2018-11-23 12:29:20 +00:00
if ( 'x-accel-redirect' === strtolower ( $type )) {
2015-08-17 17:00:26 -07:00
// Do X-Accel-Mapping substitutions.
// @link http://wiki.nginx.org/X-accel#X-Accel-Redirect
foreach ( explode ( ',' , $request -> headers -> get ( 'X-Accel-Mapping' , '' )) as $mapping ) {
$mapping = explode ( '=' , $mapping , 2 );
2018-11-23 12:29:20 +00:00
if ( 2 === \count ( $mapping )) {
2015-08-17 17:00:26 -07:00
$pathPrefix = trim ( $mapping [ 0 ]);
$location = trim ( $mapping [ 1 ]);
2018-11-23 12:29:20 +00:00
if ( substr ( $path , 0 , \strlen ( $pathPrefix )) === $pathPrefix ) {
$path = $location . substr ( $path , \strlen ( $pathPrefix ));
2015-08-17 17:00:26 -07:00
break ;
}
}
}
}
$this -> headers -> set ( $type , $path );
$this -> maxlen = 0 ;
} elseif ( $request -> headers -> has ( 'Range' )) {
// Process the range headers.
2016-04-20 09:56:34 -07:00
if ( ! $request -> headers -> has ( 'If-Range' ) || $this -> hasValidIfRangeHeader ( $request -> headers -> get ( 'If-Range' ))) {
2015-08-17 17:00:26 -07:00
$range = $request -> headers -> get ( 'Range' );
list ( $start , $end ) = explode ( '-' , substr ( $range , 6 ), 2 ) + array ( 0 );
$end = ( '' === $end ) ? $fileSize - 1 : ( int ) $end ;
if ( '' === $start ) {
$start = $fileSize - $end ;
$end = $fileSize - 1 ;
} else {
$start = ( int ) $start ;
}
if ( $start <= $end ) {
if ( $start < 0 || $end > $fileSize - 1 ) {
$this -> setStatusCode ( 416 );
2016-04-20 09:56:34 -07:00
$this -> headers -> set ( 'Content-Range' , sprintf ( 'bytes */%s' , $fileSize ));
2018-11-23 12:29:20 +00:00
} elseif ( 0 !== $start || $end !== $fileSize - 1 ) {
2015-08-17 17:00:26 -07:00
$this -> maxlen = $end < $fileSize ? $end - $start + 1 : - 1 ;
$this -> offset = $start ;
$this -> setStatusCode ( 206 );
$this -> headers -> set ( 'Content-Range' , sprintf ( 'bytes %s-%s/%s' , $start , $end , $fileSize ));
$this -> headers -> set ( 'Content-Length' , $end - $start + 1 );
}
}
}
}
return $this ;
}
2016-04-20 09:56:34 -07:00
private function hasValidIfRangeHeader ( $header )
{
if ( $this -> getEtag () === $header ) {
return true ;
}
if ( null === $lastModified = $this -> getLastModified ()) {
return false ;
}
return $lastModified -> format ( 'D, d M Y H:i:s' ) . ' GMT' === $header ;
}
2015-08-17 17:00:26 -07:00
/**
* Sends the file .
2016-04-20 09:56:34 -07:00
*
* { @ inheritdoc }
2015-08-17 17:00:26 -07:00
*/
public function sendContent ()
{
if ( ! $this -> isSuccessful ()) {
2016-04-20 09:56:34 -07:00
return parent :: sendContent ();
2015-08-17 17:00:26 -07:00
}
if ( 0 === $this -> maxlen ) {
2016-04-20 09:56:34 -07:00
return $this ;
2015-08-17 17:00:26 -07:00
}
$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 );
if ( $this -> deleteFileAfterSend ) {
unlink ( $this -> file -> getPathname ());
}
2016-04-20 09:56:34 -07:00
return $this ;
2015-08-17 17:00:26 -07:00
}
/**
* { @ 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 ;
}
/**
* If this is set to true , the file will be unlinked after the request is send
* Note : If the X - Sendfile header is used , the deleteFileAfterSend setting will not be used .
2015-10-08 11:40:12 -07:00
*
2015-08-17 17:00:26 -07:00
* @ param bool $shouldDelete
*
2017-02-02 16:28:38 -08:00
* @ return $this
2015-08-17 17:00:26 -07:00
*/
public function deleteFileAfterSend ( $shouldDelete )
{
$this -> deleteFileAfterSend = $shouldDelete ;
return $this ;
}
}