@ -8,11 +8,12 @@
namespace yii\web;
namespace yii\web;
use Yii;
use Yii;
use yii\base \HttpException;
use yii\we b\HttpException;
use yii\base\InvalidParamException;
use yii\base\InvalidParamException;
use yii\helpers\FileHelper;
use yii\helpers\FileHelper;
use yii\helpers\Html;
use yii\helpers\Html;
use yii\helpers\Json;
use yii\helpers\Json;
use yii\helpers\SecurityHelper;
use yii\helpers\StringHelper;
use yii\helpers\StringHelper;
/**
/**
@ -45,11 +46,10 @@ class Response extends \yii\base\Response
* @var string the version of the HTTP protocol to use
* @var string the version of the HTTP protocol to use
*/
*/
public $version = '1.0';
public $version = '1.0';
/**
/**
* @var array list of HTTP status codes and the corresponding texts
* @var array list of HTTP status codes and the corresponding texts
*/
*/
public static $statusText s = array(
public static $httpStatuse s = array(
100 => 'Continue',
100 => 'Continue',
101 => 'Switching Protocols',
101 => 'Switching Protocols',
102 => 'Processing',
102 => 'Processing',
@ -93,7 +93,7 @@ class Response extends \yii\base\Response
415 => 'Unsupported Media Type',
415 => 'Unsupported Media Type',
416 => 'Requested range unsatisfiable',
416 => 'Requested range unsatisfiable',
417 => 'Expectation failed',
417 => 'Expectation failed',
418 => 'I’ m a teapot',
418 => 'I\' m a teapot',
422 => 'Unprocessable entity',
422 => 'Unprocessable entity',
423 => 'Locked',
423 => 'Locked',
424 => 'Method failure',
424 => 'Method failure',
@ -117,7 +117,10 @@ class Response extends \yii\base\Response
511 => 'Network Authentication Required',
511 => 'Network Authentication Required',
);
);
private $_statusCode = 200;
/**
* @var integer the HTTP status code to send with the response.
*/
private $_statusCode;
/**
/**
* @var HeaderCollection
* @var HeaderCollection
*/
*/
@ -131,18 +134,38 @@ class Response extends \yii\base\Response
}
}
}
}
public function begin()
{
parent::begin();
$this->beginBuffer();
}
public function end()
{
$this->content .= $this->endBuffer();
$this->send();
parent::end();
}
/**
* @return integer the HTTP status code to send with the response.
*/
public function getStatusCode()
public function getStatusCode()
{
{
return $this->_statusCode;
return $this->_statusCode;
}
}
public function setStatusCode($value)
public function setStatusCode($value, $text = null )
{
{
$this->_statusCode = (int)$value;
$this->_statusCode = (int)$value;
if ($this->isInvalid()) {
if ($this->getI sInvalid()) {
throw new InvalidParamException("The HTTP status code is invalid: $value");
throw new InvalidParamException("The HTTP status code is invalid: $value");
}
}
$this->statusText = isset(self::$statusTexts[$this->_statusCode]) ? self::$statusTexts[$this->_statusCode] : '';
if ($text === null) {
$this->statusText = isset(self::$httpStatuses[$this->_statusCode]) ? self::$httpStatuses[$this->_statusCode] : '';
} else {
$this->statusText = $text;
}
}
}
/**
/**
@ -160,15 +183,17 @@ class Response extends \yii\base\Response
public function renderJson($data)
public function renderJson($data)
{
{
$this->getHeaders()->set('content-t ype', 'application/json');
$this->getHeaders()->set('Content-T ype', 'application/json');
$this->content = Json::encode($data);
$this->content = Json::encode($data);
$this->send();
}
}
public function renderJsonp($data, $callbackName)
public function renderJsonp($data, $callbackName)
{
{
$this->getHeaders()->set('content-t ype', 'text/javascript');
$this->getHeaders()->set('Content-T ype', 'text/javascript');
$data = Json::encode($data);
$data = Json::encode($data);
$this->content = "$callbackName($data);";
$this->content = "$callbackName($data);";
$this->send();
}
}
/**
/**
@ -179,6 +204,25 @@ class Response extends \yii\base\Response
{
{
$this->sendHeaders();
$this->sendHeaders();
$this->sendContent();
$this->sendContent();
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} else {
for ($level = ob_get_level(); $level > 0; --$level) {
if (!@ob_end_flush()) {
ob_clean();
}
}
flush();
}
}
public function reset()
{
$this->_headers = null;
$this->_statusCode = null;
$this->statusText = null;
$this->content = null;
}
}
/**
/**
@ -186,13 +230,45 @@ class Response extends \yii\base\Response
*/
*/
protected function sendHeaders()
protected function sendHeaders()
{
{
header("HTTP/{$this->version} " . $this->getStatusCode() . " {$this->statusText}");
if (headers_sent()) {
foreach ($this->_headers as $name => $values) {
return;
}
$statusCode = $this->getStatusCode();
if ($statusCode !== null) {
header("HTTP/{$this->version} $statusCode {$this->statusText}");
}
if ($this->_headers) {
$headers = $this->getHeaders();
foreach ($headers as $name => $values) {
foreach ($values as $value) {
foreach ($values as $value) {
header("$name: $value");
header("$name: $value", false);
}
}
$headers->removeAll();
}
$this->sendCookies();
}
/**
* Sends the cookies to the client.
*/
protected function sendCookies()
{
if ($this->_cookies === null) {
return;
}
$request = Yii::$app->getRequest();
if ($request->enableCookieValidation) {
$validationKey = $request->getCookieValidationKey();
}
}
foreach ($this->getCookies() as $cookie) {
$value = $cookie->value;
if ($cookie->expire != 1 & & isset($validationKey)) {
$value = SecurityHelper::hashData(serialize($value), $validationKey);
}
}
$this->_headers->removeAll();
setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly);
}
$this->getCookies()->removeAll();
}
}
/**
/**
@ -205,89 +281,132 @@ class Response extends \yii\base\Response
}
}
/**
/**
* Sends a file to user.
* Sends a file to the browser.
* @param string $fileName file name
* @param string $filePath the path of the file to be sent.
* @param string $content content to be set.
* @param string $attachmentName the file name shown to the user. If null, it will be determined from `$filePath`.
* @param string $mimeType mime type of the content. If null, it will be guessed automatically based on the given file name.
* @param string $mimeType the MIME type of the content. If null, it will be guessed based on `$filePath`
* @param boolean $terminate whether to terminate the current application after calling this method
* @throws \yii\base\HttpException when range request is not satisfiable.
*/
*/
public function sendFile($fileName, $content, $mimeType = null, $terminate = true )
public function sendFile($filePath, $attachmentName = null, $mimeType = null)
{
{
if ($mimeType === null & & (( $mimeType = FileHelper::getMimeTypeByExtension($fileName)) === null) ) {
if ($mimeType === null & & ($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null ) {
$mimeType = 'application/octet-stream';
$mimeType = 'application/octet-stream';
}
}
if ($attachmentName === null) {
$attachmentName = basename($filePath);
}
$handle = fopen($filePath, 'rb');
$this->sendStreamAsFile($handle, $attachmentName, $mimeType);
}
$fileSize = StringHelper::strlen($content);
/**
$contentStart = 0;
* Sends the specified content as a file to the browser.
$contentEnd = $fileSize - 1;
* @param string $content the content to be sent. The existing [[content]] will be discarded.
* @param string $attachmentName the file name shown to the user.
// tell the client that we accept range requests
* @param string $mimeType the MIME type of the content.
header('Accept-Ranges: bytes');
*/
public function sendContentAsFile($content, $attachmentName, $mimeType = 'application/octet-stream')
{
$this->getHeaders()
->addDefault('Pragma', 'public')
->addDefault('Accept-Ranges', 'bytes')
->addDefault('Expires', '0')
->addDefault('Content-Type', $mimeType)
->addDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->addDefault('Content-Transfer-Encoding', 'binary')
->addDefault('Content-Length', StringHelper::strlen($content))
->addDefault('Content-Disposition', "attachment; filename=\"$attachmentName\"");
if (isset($_SERVER['HTTP_RANGE'])) {
$this->content = $content;
// client sent us a multibyte range, can not hold this one for now
$this->send();
if (strpos($_SERVER['HTTP_RANGE'], ',') !== false) {
header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
throw new HttpException(416, 'Requested Range Not Satisfiable');
}
}
$range = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']);
/**
* Sends the specified stream as a file to the browser.
* @param resource $handle the handle of the stream to be sent.
* @param string $attachmentName the file name shown to the user.
* @param string $mimeType the MIME type of the stream content.
* @throws HttpException if the requested range cannot be satisfied.
*/
public function sendStreamAsFile($handle, $attachmentName, $mimeType = 'application/octet-stream')
{
$headers = $this->getHeaders();
fseek($handle, 0, SEEK_END);
$fileSize = ftell($handle);
$range = $this->getHttpRange($fileSize);
if ($range === false) {
$headers->set('Content-Range', "bytes */$fileSize");
throw new HttpException(416, Yii::t('yii', 'Requested range not satisfiable'));
}
// range requests starts from "-", so it means that data must be dumped the end point.
list($begin, $end) = $range;
if ($range[0] === '-') {
if ($begin !=0 || $end != $fileSize - 1) {
$contentStart = $fileSize - substr($range, 1);
$this->setStatusCode(206);
$headers->set('Content-Range', "bytes $begin-$end/$fileSize");
} else {
} else {
$range = explode('-', $range);
$this->setStatusCode(200);
$contentStart = $range[0];
// check if the last-byte-pos presents in header
if ((isset($range[1]) & & is_numeric($range[1]))) {
$contentEnd = $range[1];
}
}
if (isset($options['mimeType'])) {
$headers->set('Content-Type', $options['mimeType']);
}
}
/* Check the range and make sure it's treated according to the specs.
$length = $end - $begin + 1;
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
*/
$headers->addDefault('Pragma', 'public')
// End bytes can not be larger than $end.
->addDefault('Accept-Ranges', 'bytes')
$contentEnd = ($contentEnd > $fileSize) ? $fileSize - 1 : $contentEnd;
->addDefault('Expires', '0')
->addDefault('Content-Type', $mimeType)
->addDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->addDefault('Content-Transfer-Encoding', 'binary')
->addDefault('Content-Length', $length)
->addDefault('Content-Disposition', "attachment; filename=\"$attachmentName\"");
// Validate the requested range and return an error if it's not correct.
$this->send();
$wrongContentStart = ($contentStart > $contentEnd || $contentStart > $fileSize - 1 || $contentStart < 0 ) ;
if ($wrongContentStart) {
fseek($handle, $begin);
header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
set_time_limit(0); // Reset time limit for big files
throw new HttpException(416, 'Requested Range Not Satisfiable');
$chunkSize = 8 * 1024 * 1024; // 8MB per chunk
while (!feof($handle) & & ($pos = ftell($handle)) < = $end) {
if ($pos + $chunkSize > $end) {
$chunkSize = $end - $pos + 1;
}
echo fread($handle, $chunkSize);
flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
}
fclose($handle);
}
}
header('HTTP/1.1 206 Partial Content');
/**
header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
* Determines the HTTP range given in the request.
* @param integer $fileSize the size of the file that will be used to validate the requested HTTP range.
* @return array|boolean the range (begin, end), or false if the range request is invalid.
*/
protected function getHttpRange($fileSize)
{
if (!isset($_SERVER['HTTP_RANGE']) || $_SERVER['HTTP_RANGE'] === '-') {
return array(0, $fileSize - 1);
}
if (!preg_match('/^bytes=(\d*)-(\d*)$/', $_SERVER['HTTP_RANGE'], $matches)) {
return false;
}
if ($matches[1] === '') {
$start = $fileSize - $matches[2];
$end = $fileSize - 1;
} elseif ($matches[2] !== '') {
$start = $matches[1];
$end = $matches[2];
if ($end >= $fileSize) {
$end = $fileSize - 1;
}
} else {
} else {
header('HTTP/1.1 200 OK');
$start = $matches[1];
$end = $fileSize - 1;
}
}
if ($start < 0 | | $ start > $end) {
$length = $contentEnd - $contentStart + 1; // Calculate new content length
return false;
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-Transfer-Encoding: binary');
$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);
ob_end_clean();
echo $content;
exit(0);
} else {
} else {
echo $content ;
return array($start, $end);
}
}
}
}
@ -305,86 +424,58 @@ class Response extends \yii\base\Response
* specified by that header using web server internals including all optimizations like caching-headers.
* specified by that header using web server internals including all optimizations like caching-headers.
*
*
* As this header directive is non-standard different directives exists for different web servers applications:
* As this header directive is non-standard different directives exists for different web servers applications:
* < ul >
*
* < li > Apache: {@link http://tn123.org/mod_xsendfile X-Sendfile}< / li >
* - Apache: [X-Sendfile](http://tn123.org/mod_xsendfile)
* < li > Lighttpd v1.4: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-LIGHTTPD-send-file}< / li >
* - Lighttpd v1.4: [X-LIGHTTPD-send-file](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
* < li > Lighttpd v1.5: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-Sendfile}< / li >
* - Lighttpd v1.5: [X-Sendfile](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
* < li > Nginx: {@link http://wiki.nginx.org/XSendfile X-Accel-Redirect}< / li >
* - Nginx: [X-Accel-Redirect](http://wiki.nginx.org/XSendfile)
* < li > Cherokee: {@link http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile X-Sendfile and X-Accel-Redirect}< / li >
* - Cherokee: [X-Sendfile and X-Accel-Redirect](http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile)
* < / ul >
*
* So for this method to work the X-SENDFILE option/module should be enabled by the web server and
* So for this method to work the X-SENDFILE option/module should be enabled by the web server and
* a proper xHeader should be sent.
* a proper xHeader should be sent.
*
*
* < b > Note:< / b >
* **Note**
* This option allows to download files that are not under web folders, and even files that are otherwise protected (deny from all) like .htaccess
*
* This option allows to download files that are not under web folders, and even files that are otherwise protected
* (deny from all) like `.htaccess`.
*
* **Side effects**
*
*
* < b > Side effects< / b > :
* If this option is disabled by the web server, when this method is called a download configuration dialog
* If this option is disabled by the web server, when this method is called a download configuration dialog
* will open but the downloaded file will have 0 bytes.
* will open but the downloaded file will have 0 bytes.
*
*
* < b > Known issues< / b > :
* **Known issues**
*
* There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show
* There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show
* an error message like this: "Internet Explorer was not able to open this Internet site. The requested site is either unavailable or cannot be found.".
* an error message like this: "Internet Explorer was not able to open this Internet site. The requested site
* You can work around this problem by removing the < code > Pragma< / code > -header.
* is either unavailable or cannot be found.". You can work around this problem by removing the `Pragma`-header.
*
* **Example**
*
* ~~~
* Yii::app()->request->xSendFile('/home/user/Pictures/picture1.jpg');
* ~~~
*
*
* < b > Example< / b > :
* < pre >
* <?php
* Yii::app()->request->xSendFile('/home/user/Pictures/picture1.jpg', array(
* 'saveName' => 'image1.jpg',
* 'mimeType' => 'image/jpeg',
* 'terminate' => false,
* ));
* ?>
* < / pre >
* @param string $filePath file name with full path
* @param string $filePath file name with full path
* @param array $options additional options:
* @param string $mimeType the MIME type of the file. If null, it will be determined based on `$filePath`.
* < ul >
* @param string $attachmentName file name shown to the user. If null, it will be determined from `$filePath`.
* < li > saveName: file name shown to the user, if not set real file name will be used< / li >
* @param string $xHeader the name of the x-sendfile header.
* < li > mimeType: mime type of the file, if not set it will be guessed automatically based on the file name, if set to null no content-type header will be sent.< / li >
*/
* < li > xHeader: appropriate x-sendfile header, defaults to "X-Sendfile"< / li >
public function xSendFile($filePath, $attachmentName = null, $mimeType = null, $xHeader = 'X-Sendfile')
* < li > terminate: whether to terminate the current application after calling this method, defaults to true< / li >
* < li > forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true< / li >
* < li > addHeaders: an array of additional http headers in header-value pairs< / li >
* < / ul >
* @todo
*/
public function xSendFile($filePath, $options = array())
{
{
if (!isset($options['forceDownload']) || $options['forceDownload']) {
if ($mimeType === null & & ($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) {
$disposition = 'attachment';
$mimeType = 'application/octet-stream';
} else {
$disposition = 'inline';
}
if (!isset($options['saveName'])) {
$options['saveName'] = basename($filePath);
}
if (!isset($options['mimeType'])) {
if (($options['mimeType'] = FileHelper::getMimeTypeByExtension($filePath)) === null) {
$options['mimeType'] = 'text/plain';
}
}
if ($attachmentName === null) {
$attachmentName = basename($filePath);
}
}
if (!isset($options['xHeader'])) {
$this->getHeaders()
$options['xHeader'] = 'X-Sendfile';
->addDefault($xHeader, $filePath)
}
->addDefault('Content-Type', $mimeType)
->addDefault('Content-Disposition', "attachment; filename=\"$attachmentName\"");
if ($options['mimeType'] !== null) {
$this->send();
header('Content-type: ' . $options['mimeType']);
}
header('Content-Disposition: ' . $disposition . '; filename="' . $options['saveName'] . '"');
if (isset($options['addHeaders'])) {
foreach ($options['addHeaders'] as $header => $value) {
header($header . ': ' . $value);
}
}
header(trim($options['xHeader']) . ': ' . $filePath);
if (!isset($options['terminate']) || $options['terminate']) {
Yii::$app->end();
}
}
}
/**
/**
@ -422,7 +513,8 @@ class Response extends \yii\base\Response
if (Yii::$app->getRequest()->getIsAjax()) {
if (Yii::$app->getRequest()->getIsAjax()) {
$statusCode = $this->ajaxRedirectCode;
$statusCode = $this->ajaxRedirectCode;
}
}
header('Location: ' . $url, true, $statusCode);
$this->getHeaders()->set('Location', $url);
$this->setStatusCode($statusCode);
if ($terminate) {
if ($terminate) {
Yii::$app->end();
Yii::$app->end();
}
}
@ -441,6 +533,8 @@ class Response extends \yii\base\Response
$this->redirect(Yii::$app->getRequest()->getUrl() . $anchor, $terminate);
$this->redirect(Yii::$app->getRequest()->getUrl() . $anchor, $terminate);
}
}
private $_cookies;
/**
/**
* Returns the cookie collection.
* Returns the cookie collection.
* Through the returned cookie collection, you add or remove cookies as follows,
* Through the returned cookie collection, you add or remove cookies as follows,
@ -462,13 +556,16 @@ class Response extends \yii\base\Response
*/
*/
public function getCookies()
public function getCookies()
{
{
return Yii::$app->getRequest()->getCookies();
if ($this->_cookies === null) {
$this->_cookies = new CookieCollection;
}
return $this->_cookies;
}
}
/**
/**
* @return boolean whether this response has a valid [[statusCode]].
* @return boolean whether this response has a valid [[statusCode]].
*/
*/
public function i sInvalid()
public function getI sInvalid()
{
{
return $this->getStatusCode() < 100 | | $ this- > getStatusCode() >= 600;
return $this->getStatusCode() < 100 | | $ this- > getStatusCode() >= 600;
}
}
@ -476,15 +573,15 @@ class Response extends \yii\base\Response
/**
/**
* @return boolean whether this response is informational
* @return boolean whether this response is informational
*/
*/
public function i sInformational()
public function getI sInformational()
{
{
return $this->getStatusCode() >= 100 & & $this->getStatusCode() < 200 ;
return $this->getStatusCode() >= 100 & & $this->getStatusCode() < 200 ;
}
}
/**
/**
* @return boolean whether this response is successfully
* @return boolean whether this response is successful
*/
*/
public function i sSuccessful()
public function getI sSuccessful()
{
{
return $this->getStatusCode() >= 200 & & $this->getStatusCode() < 300 ;
return $this->getStatusCode() >= 200 & & $this->getStatusCode() < 300 ;
}
}
@ -492,7 +589,7 @@ class Response extends \yii\base\Response
/**
/**
* @return boolean whether this response is a redirection
* @return boolean whether this response is a redirection
*/
*/
public function i sRedirection()
public function getI sRedirection()
{
{
return $this->getStatusCode() >= 300 & & $this->getStatusCode() < 400 ;
return $this->getStatusCode() >= 300 & & $this->getStatusCode() < 400 ;
}
}
@ -500,7 +597,7 @@ class Response extends \yii\base\Response
/**
/**
* @return boolean whether this response indicates a client error
* @return boolean whether this response indicates a client error
*/
*/
public function i sClientError()
public function getI sClientError()
{
{
return $this->getStatusCode() >= 400 & & $this->getStatusCode() < 500 ;
return $this->getStatusCode() >= 400 & & $this->getStatusCode() < 500 ;
}
}
@ -508,7 +605,7 @@ class Response extends \yii\base\Response
/**
/**
* @return boolean whether this response indicates a server error
* @return boolean whether this response indicates a server error
*/
*/
public function i sServerError()
public function getI sServerError()
{
{
return $this->getStatusCode() >= 500 & & $this->getStatusCode() < 600 ;
return $this->getStatusCode() >= 500 & & $this->getStatusCode() < 600 ;
}
}
@ -516,7 +613,7 @@ class Response extends \yii\base\Response
/**
/**
* @return boolean whether this response is OK
* @return boolean whether this response is OK
*/
*/
public function i sOk()
public function getI sOk()
{
{
return 200 === $this->getStatusCode();
return 200 === $this->getStatusCode();
}
}
@ -524,7 +621,7 @@ class Response extends \yii\base\Response
/**
/**
* @return boolean whether this response indicates the current request is forbidden
* @return boolean whether this response indicates the current request is forbidden
*/
*/
public function i sForbidden()
public function getI sForbidden()
{
{
return 403 === $this->getStatusCode();
return 403 === $this->getStatusCode();
}
}
@ -532,7 +629,7 @@ class Response extends \yii\base\Response
/**
/**
* @return boolean whether this response indicates the currently requested resource is not found
* @return boolean whether this response indicates the currently requested resource is not found
*/
*/
public function i sNotFound()
public function getI sNotFound()
{
{
return 404 === $this->getStatusCode();
return 404 === $this->getStatusCode();
}
}
@ -540,7 +637,7 @@ class Response extends \yii\base\Response
/**
/**
* @return boolean whether this response is empty
* @return boolean whether this response is empty
*/
*/
public function i sEmpty()
public function getI sEmpty()
{
{
return in_array($this->getStatusCode(), array(201, 204, 304));
return in_array($this->getStatusCode(), array(201, 204, 304));
}
}