Commit f2d477dc by Qiang Xue

Response WIP

parent cf1e12ad
......@@ -20,6 +20,7 @@ class SiteController extends Controller
public function actionIndex()
{
Yii::$app->end(0, false);
echo $this->render('index');
}
......
......@@ -128,6 +128,11 @@ class Application extends Module
ini_set('display_errors', 0);
set_exception_handler(array($this, 'handleException'));
set_error_handler(array($this, 'handleError'), error_reporting());
// Allocating twice more than required to display memory exhausted error
// in case of trying to allocate last 1 byte while all memory is taken.
$this->_memoryReserve = str_repeat('x', 1024 * 256);
register_shutdown_function(array($this, 'end'), 0, false);
register_shutdown_function(array($this, 'handleFatalError'));
}
}
......@@ -142,11 +147,10 @@ class Application extends Module
{
if (!$this->_ended) {
$this->_ended = true;
$this->getResponse()->end();
$this->afterRequest();
}
$this->handleFatalError();
if ($exit) {
exit($status);
}
......@@ -160,11 +164,10 @@ class Application extends Module
public function run()
{
$this->beforeRequest();
// Allocating twice more than required to display memory exhausted error
// in case of trying to allocate last 1 byte while all memory is taken.
$this->_memoryReserve = str_repeat('x', 1024 * 256);
register_shutdown_function(array($this, 'end'), 0, false);
$response = $this->getResponse();
$response->begin();
$status = $this->processRequest();
$response->end();
$this->afterRequest();
return $status;
}
......@@ -315,6 +318,15 @@ class Application extends Module
}
/**
* Returns the response component.
* @return \yii\web\Response|\yii\console\Response the response component
*/
public function getResponse()
{
return $this->getComponent('response');
}
/**
* Returns the view object.
* @return View the view object that is used to render various view files.
*/
......
......@@ -82,11 +82,12 @@ class ErrorHandler extends Component
} elseif (!(Yii::$app instanceof \yii\web\Application)) {
Yii::$app->renderException($exception);
} else {
$response = Yii::$app->getResponse();
if (!headers_sent()) {
if ($exception instanceof HttpException) {
header('HTTP/1.0 ' . $exception->statusCode . ' ' . $exception->getName());
$response->setStatusCode($exception->statusCode);
} else {
header('HTTP/1.0 500 ' . get_class($exception));
$response->setStatusCode(500);
}
}
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
......@@ -100,13 +101,13 @@ class ErrorHandler extends Component
$view = new View();
$request = '';
foreach (array('GET', 'POST', 'SERVER', 'FILES', 'COOKIE', 'SESSION', 'ENV') as $name) {
if (!empty($GLOBALS['_' . $name])) {
$request .= '$_' . $name . ' = ' . var_export($GLOBALS['_' . $name], true) . ";\n\n";
foreach (array('_GET', '_POST', '_SERVER', '_FILES', '_COOKIE', '_SESSION', '_ENV') as $name) {
if (!empty($GLOBALS[$name])) {
$request .= '$' . $name . ' = ' . var_export($GLOBALS[$name], true) . ";\n\n";
}
}
$request = rtrim($request, "\n\n");
echo $view->renderFile($this->mainView, array(
$response->content = $view->renderFile($this->mainView, array(
'exception' => $exception,
'request' => $request,
), $this);
......
......@@ -13,6 +13,9 @@ namespace yii\base;
*/
class Response extends Component
{
const EVENT_BEGIN_RESPONSE = 'beginResponse';
const EVENT_END_RESPONSE = 'endResponse';
/**
* Starts output buffering
*/
......@@ -56,4 +59,14 @@ class Response extends Component
ob_end_clean();
}
}
public function begin()
{
$this->trigger(self::EVENT_BEGIN_RESPONSE);
}
public function end()
{
$this->trigger(self::EVENT_END_RESPONSE);
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console;
/**
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Response extends \yii\base\Response
{
}
......@@ -277,11 +277,8 @@ class CaptchaAction extends Action
imagecolordeallocate($image, $foreColor);
header('Pragma: public');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Content-Transfer-Encoding: binary');
header("Content-type: image/png");
$this->sendHttpHeaders();
imagepng($image);
imagedestroy($image);
}
......@@ -319,12 +316,21 @@ class CaptchaAction extends Action
$x += (int)($fontMetrics['textWidth']) + $this->offset;
}
header('Pragma: public');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Content-Transfer-Encoding: binary');
header("Content-type: image/png");
$image->setImageFormat('png');
echo $image;
Yii::$app->getResponse()->content = (string)$image;
$this->sendHttpHeaders();
}
/**
* Sends the HTTP headers needed by image response.
*/
protected function sendHttpHeaders()
{
Yii::$app->getResponse()->getHeaders()
->set('Pragma', 'public')
->set('Expires', '0')
->set('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->set('Content-Transfer-Encoding', 'binary')
->set('Content-type', 'image/png');
}
}
......@@ -45,7 +45,7 @@ class Cookie extends \yii\base\Object
* By setting this property to true, the cookie will not be accessible by scripting languages,
* such as JavaScript, which can effectively help to reduce identity theft through XSS attacks.
*/
public $httponly = false;
public $httpOnly = false;
/**
* Magic method to turn a cookie object into a string without having to explicitly access [[value]].
......
......@@ -9,7 +9,8 @@ namespace yii\web;
use Yii;
use ArrayIterator;
use yii\helpers\SecurityHelper;
use yii\base\InvalidCallException;
use yii\base\Object;
/**
* CookieCollection maintains the cookies available in the current request.
......@@ -19,17 +20,12 @@ use yii\helpers\SecurityHelper;
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ArrayAccess, \Countable
class CookieCollection extends Object implements \IteratorAggregate, \ArrayAccess, \Countable
{
/**
* @var boolean whether to enable cookie validation. By setting this property to true,
* if a cookie is tampered on the client side, it will be ignored when received on the server side.
* @var boolean whether this collection is read only.
*/
public $enableValidation = true;
/**
* @var string the secret key used for cookie validation. If not set, a random key will be generated and used.
*/
public $validationKey;
public $readOnly = false;
/**
* @var Cookie[] the cookies in this collection (indexed by the cookie names)
......@@ -38,12 +34,14 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \
/**
* Constructor.
* @param array $cookies the cookies that this collection initially contains. This should be
* an array of name-value pairs.s
* @param array $config name-value pairs that will be used to initialize the object properties
*/
public function __construct($config = array())
public function __construct($cookies = array(), $config = array())
{
$this->_cookies = $cookies;
parent::__construct($config);
$this->_cookies = $this->loadCookies();
}
/**
......@@ -114,50 +112,53 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \
* Adds a cookie to the collection.
* If there is already a cookie with the same name in the collection, it will be removed first.
* @param Cookie $cookie the cookie to be added
* @throws InvalidCallException if the cookie collection is read only
*/
public function add($cookie)
{
if (isset($this->_cookies[$cookie->name])) {
$c = $this->_cookies[$cookie->name];
setcookie($c->name, '', 0, $c->path, $c->domain, $c->secure, $c->httponly);
if ($this->readOnly) {
throw new InvalidCallException('The cookie collection is read only.');
}
$value = $cookie->value;
if ($this->enableValidation) {
if ($this->validationKey === null) {
$key = SecurityHelper::getSecretKey(__CLASS__ . '/' . Yii::$app->id);
} else {
$key = $this->validationKey;
}
$value = SecurityHelper::hashData(serialize($value), $key);
}
setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly);
$this->_cookies[$cookie->name] = $cookie;
}
/**
* Removes a cookie from the collection.
* Removes a cookie.
* If `$removeFromBrowser` is true, the cookie will be removed from the browser.
* In this case, a cookie with outdated expiry will be added to the collection.
* @param Cookie|string $cookie the cookie object or the name of the cookie to be removed.
* @param boolean $removeFromBrowser whether to remove the cookie from browser
* @throws InvalidCallException if the cookie collection is read only
*/
public function remove($cookie)
public function remove($cookie, $removeFromBrowser = true)
{
if (is_string($cookie) && isset($this->_cookies[$cookie])) {
$cookie = $this->_cookies[$cookie];
if ($this->readOnly) {
throw new InvalidCallException('The cookie collection is read only.');
}
if ($cookie instanceof Cookie) {
setcookie($cookie->name, '', 0, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly);
$cookie->expire = 1;
$cookie->value = '';
} else {
$cookie = new Cookie(array(
'name' => $cookie,
'expire' => 1,
));
}
if ($removeFromBrowser) {
$this->_cookies[$cookie->name] = $cookie;
} else {
unset($this->_cookies[$cookie->name]);
}
}
/**
* Removes all cookies.
* @throws InvalidCallException if the cookie collection is read only
*/
public function removeAll()
{
foreach ($this->_cookies as $cookie) {
setcookie($cookie->name, '', 0, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly);
if ($this->readOnly) {
throw new InvalidCallException('The cookie collection is read only.');
}
$this->_cookies = array();
}
......@@ -222,36 +223,4 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \
{
$this->remove($name);
}
/**
* Returns the current cookies in terms of [[Cookie]] objects.
* @return Cookie[] list of current cookies
*/
protected function loadCookies()
{
$cookies = array();
if ($this->enableValidation) {
if ($this->validationKey === null) {
$key = SecurityHelper::getSecretKey(__CLASS__ . '/' . Yii::$app->id);
} else {
$key = $this->validationKey;
}
foreach ($_COOKIE as $name => $value) {
if (is_string($value) && ($value = SecurityHelper::validateData($value, $key)) !== false) {
$cookies[$name] = new Cookie(array(
'name' => $name,
'value' => @unserialize($value),
));
}
}
} else {
foreach ($_COOKIE as $name => $value) {
$cookies[$name] = new Cookie(array(
'name' => $name,
'value' => $value,
));
}
}
return $cookies;
}
}
......@@ -79,11 +79,13 @@ class HeaderCollection extends Object implements \IteratorAggregate, \ArrayAcces
* If there is already a header with the same name, it will be replaced.
* @param string $name the name of the header
* @param string $value the value of the header
* @return HeaderCollection the collection object itself
*/
public function set($name, $value)
public function set($name, $value = '')
{
$name = strtolower($name);
$this->_headers[$name] = (array)$value;
return $this;
}
/**
......@@ -92,11 +94,13 @@ class HeaderCollection extends Object implements \IteratorAggregate, \ArrayAcces
* be appended to it instead of replacing it.
* @param string $name the name of the header
* @param string $value the value of the header
* @return HeaderCollection the collection object itself
*/
public function add($name, $value)
{
$name = strtolower($name);
$this->_headers[$name][] = $value;
return $this;
}
/**
......
......@@ -50,7 +50,7 @@ class HttpCache extends ActionFilter
/**
* @var string HTTP cache control header. If null, the header will not be sent.
*/
public $cacheControlHeader = 'Cache-Control: max-age=3600, public';
public $cacheControlHeader = 'max-age=3600, public';
/**
* This method is invoked right before an action is to be executed (after all possible filters.)
......@@ -60,7 +60,7 @@ class HttpCache extends ActionFilter
*/
public function beforeAction($action)
{
$verb = Yii::$app->request->getMethod();
$verb = Yii::$app->getRequest()->getMethod();
if ($verb !== 'GET' && $verb !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) {
return true;
}
......@@ -75,17 +75,18 @@ class HttpCache extends ActionFilter
}
$this->sendCacheControlHeader();
$response = Yii::$app->getResponse();
if ($etag !== null) {
header("ETag: $etag");
$response->getHeaders()->set('Etag', $etag);
}
if ($this->validateCache($lastModified, $etag)) {
header('HTTP/1.1 304 Not Modified');
$response->setStatusCode(304);
return false;
}
if ($lastModified !== null) {
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
$response->getHeaders()->set('Last-Modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
}
return true;
}
......@@ -113,9 +114,10 @@ class HttpCache extends ActionFilter
protected function sendCacheControlHeader()
{
session_cache_limiter('public');
header('Pragma:', true);
$headers = Yii::$app->getResponse()->getHeaders();
$headers->set('Pragma');
if ($this->cacheControlHeader !== null) {
header($this->cacheControlHeader, true);
$headers->set('Cache-Control', $this->cacheControlHeader);
}
}
......
......@@ -10,6 +10,7 @@ namespace yii\web;
use Yii;
use yii\base\HttpException;
use yii\base\InvalidConfigException;
use yii\helpers\SecurityHelper;
/**
* @author Qiang Xue <qiang.xue@gmail.com>
......@@ -37,16 +38,12 @@ class Request extends \yii\base\Request
* @var array the configuration of the CSRF cookie. This property is used only when [[enableCsrfValidation]] is true.
* @see Cookie
*/
public $csrfCookie = array('httponly' => true);
public $csrfCookie = array('httpOnly' => true);
/**
* @var boolean whether cookies should be validated to ensure they are not tampered. Defaults to true.
*/
public $enableCookieValidation = true;
/**
* @var string the secret key used for cookie validation. If not set, a random key will be generated and used.
*/
public $cookieValidationKey;
/**
* @var string|boolean the name of the POST parameter that is used to indicate if a request is a PUT or DELETE
* request tunneled through POST. Default to '_method'.
* @see getMethod
......@@ -717,14 +714,64 @@ class Request extends \yii\base\Request
public function getCookies()
{
if ($this->_cookies === null) {
$this->_cookies = new CookieCollection(array(
'enableValidation' => $this->enableCookieValidation,
'validationKey' => $this->cookieValidationKey,
$this->_cookies = new CookieCollection($this->loadCookies(), array(
'readOnly' => true,
));
}
return $this->_cookies;
}
/**
* Converts `$_COOKIE` into an array of [[Cookie]].
* @return array the cookies obtained from request
*/
protected function loadCookies()
{
$cookies = array();
if ($this->enableCookieValidation) {
$key = $this->getCookieValidationKey();
foreach ($_COOKIE as $name => $value) {
if (is_string($value) && ($value = SecurityHelper::validateData($value, $key)) !== false) {
$cookies[$name] = new Cookie(array(
'name' => $name,
'value' => @unserialize($value),
));
}
}
} else {
foreach ($_COOKIE as $name => $value) {
$cookies[$name] = new Cookie(array(
'name' => $name,
'value' => $value,
));
}
}
return $cookies;
}
private $_cookieValidationKey;
/**
* @return string the secret key used for cookie validation. If it was set previously,
* a random key will be generated and used.
*/
public function getCookieValidationKey()
{
if ($this->_cookieValidationKey === null) {
$this->_cookieValidationKey = SecurityHelper::getSecretKey(__CLASS__ . '/' . Yii::$app->id);
}
return $this->_cookieValidationKey;
}
/**
* Sets the secret key used for cookie validation.
* @param string $value the secret key used for cookie validation.
*/
public function setCookieValidationKey($value)
{
$this->_cookieValidationKey = $value;
}
private $_csrfToken;
/**
......
......@@ -13,6 +13,7 @@ use yii\base\InvalidParamException;
use yii\helpers\FileHelper;
use yii\helpers\Html;
use yii\helpers\Json;
use yii\helpers\SecurityHelper;
use yii\helpers\StringHelper;
/**
......@@ -131,18 +132,35 @@ class Response extends \yii\base\Response
}
}
public function begin()
{
parent::begin();
$this->beginOutput();
}
public function end()
{
$this->content .= $this->endOutput();
$this->send();
parent::end();
}
public function getStatusCode()
{
return $this->_statusCode;
}
public function setStatusCode($value)
public function setStatusCode($value, $text = null)
{
$this->_statusCode = (int)$value;
if ($this->isInvalid()) {
throw new InvalidParamException("The HTTP status code is invalid: $value");
}
if ($text === null) {
$this->statusText = isset(self::$statusTexts[$this->_statusCode]) ? self::$statusTexts[$this->_statusCode] : '';
} else {
$this->statusText = $text;
}
}
/**
......@@ -186,13 +204,42 @@ class Response extends \yii\base\Response
*/
protected function sendHeaders()
{
if (headers_sent()) {
return;
}
header("HTTP/{$this->version} " . $this->getStatusCode() . " {$this->statusText}");
foreach ($this->_headers as $name => $values) {
if ($this->_headers) {
$headers = $this->getHeaders();
foreach ($headers as $name => $values) {
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();
}
/**
......@@ -222,13 +269,15 @@ class Response extends \yii\base\Response
$contentStart = 0;
$contentEnd = $fileSize - 1;
$headers = $this->getHeaders();
// tell the client that we accept range requests
header('Accept-Ranges: bytes');
$headers->set('Accept-Ranges', 'bytes');
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("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
$headers->set('Content-Range', "bytes $contentStart-$contentEnd/$fileSize");
throw new HttpException(416, 'Requested Range Not Satisfiable');
}
......@@ -257,25 +306,26 @@ class Response extends \yii\base\Response
$wrongContentStart = ($contentStart > $contentEnd || $contentStart > $fileSize - 1 || $contentStart < 0);
if ($wrongContentStart) {
header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
$headers->set('Content-Range', "bytes $contentStart-$contentEnd/$fileSize");
throw new HttpException(416, 'Requested Range Not Satisfiable');
}
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
$this->setStatusCode(206);
$headers->set('Content-Range', "bytes $contentStart-$contentEnd/$fileSize");
} else {
header('HTTP/1.1 200 OK');
$this->setStatusCode(200);
}
$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);
header('Content-Length: ' . $length);
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Content-Transfer-Encoding: binary');
$headers->set('Pragma', 'public')
->set('Expires', '0')
->set('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->set('Content-Type', $mimeType)
->set('Content-Length', $length)
->set('Content-Disposition', "attachment; filename=\"$fileName\"")
->set('Content-Transfer-Encoding', 'binary');
$content = StringHelper::substr($content, $contentStart, $length);
if ($terminate) {
......@@ -371,16 +421,18 @@ class Response extends \yii\base\Response
$options['xHeader'] = 'X-Sendfile';
}
$headers = $this->getHeaders();
if ($options['mimeType'] !== null) {
header('Content-type: ' . $options['mimeType']);
$headers->set('Content-Type', $options['mimeType']);
}
header('Content-Disposition: ' . $disposition . '; filename="' . $options['saveName'] . '"');
$headers->set('Content-Disposition', "$disposition; filename=\"{$options['saveName']}\"");
if (isset($options['addHeaders'])) {
foreach ($options['addHeaders'] as $header => $value) {
header($header . ': ' . $value);
$headers->set($header, $value);
}
}
header(trim($options['xHeader']) . ': ' . $filePath);
$headers->set(trim($options['xHeader']), $filePath);
if (!isset($options['terminate']) || $options['terminate']) {
Yii::$app->end();
......@@ -422,7 +474,8 @@ class Response extends \yii\base\Response
if (Yii::$app->getRequest()->getIsAjax()) {
$statusCode = $this->ajaxRedirectCode;
}
header('Location: ' . $url, true, $statusCode);
$this->getHeaders()->set('Location', $url);
$this->setStatusCode($statusCode);
if ($terminate) {
Yii::$app->end();
}
......@@ -441,6 +494,8 @@ class Response extends \yii\base\Response
$this->redirect(Yii::$app->getRequest()->getUrl() . $anchor, $terminate);
}
private $_cookies;
/**
* Returns the cookie collection.
* Through the returned cookie collection, you add or remove cookies as follows,
......@@ -462,7 +517,10 @@ class Response extends \yii\base\Response
*/
public function getCookies()
{
return Yii::$app->getRequest()->getCookies();
if ($this->_cookies === null) {
$this->_cookies = new CookieCollection;
}
return $this->_cookies;
}
/**
......
......@@ -63,7 +63,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co
* @var array parameter-value pairs to override default session cookie parameters
*/
public $cookieParams = array(
'httponly' => true
'httpOnly' => true
);
/**
......@@ -241,26 +241,31 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co
*/
public function getCookieParams()
{
return session_get_cookie_params();
$params = session_get_cookie_params();
if (isset($params['httponly'])) {
$params['httpOnly'] = $params['httponly'];
unset($params['httponly']);
}
return $params;
}
/**
* Sets the session cookie parameters.
* The effect of this method only lasts for the duration of the script.
* Call this method before the session starts.
* @param array $value cookie parameters, valid keys include: lifetime, path, domain, secure and httponly.
* @param array $value cookie parameters, valid keys include: `lifetime`, `path`, `domain`, `secure` and `httpOnly`.
* @throws InvalidParamException if the parameters are incomplete.
* @see http://us2.php.net/manual/en/function.session-set-cookie-params.php
*/
public function setCookieParams($value)
{
$data = session_get_cookie_params();
$data = $this->getCookieParams();
extract($data);
extract($value);
if (isset($lifetime, $path, $domain, $secure, $httponly)) {
session_set_cookie_params($lifetime, $path, $domain, $secure, $httponly);
if (isset($lifetime, $path, $domain, $secure, $httpOnly)) {
session_set_cookie_params($lifetime, $path, $domain, $secure, $httpOnly);
} else {
throw new InvalidParamException('Please make sure these parameters are provided: lifetime, path, domain, secure and httponly.');
throw new InvalidParamException('Please make sure these parameters are provided: lifetime, path, domain, secure and httpOnly.');
}
}
......
......@@ -56,7 +56,7 @@ class User extends Component
* @var array the configuration of the identity cookie. This property is used only when [[enableAutoLogin]] is true.
* @see Cookie
*/
public $identityCookie = array('name' => '_identity', 'httponly' => true);
public $identityCookie = array('name' => '_identity', 'httpOnly' => true);
/**
* @var integer the number of seconds in which the user will be logged out automatically if he
* remains inactive. If this property is not set, the user will be logged out after
......
......@@ -81,7 +81,7 @@ class VerbFilter extends Behavior
if (!in_array($verb, $allowed)) {
$event->isValid = false;
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7
header('Allow: ' . implode(', ', $allowed));
Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $allowed));
throw new HttpException(405, 'Method Not Allowed. This url can only handle the following request methods: ' . implode(', ', $allowed));
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment