From 874fcaaecbcbc77edfb63c5e73cd3e5b4dc8035d Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Sat, 30 Dec 2017 15:11:06 +0200 Subject: [PATCH] `Psr\Http\Message\ServerRequestInterface` applied for `yii\web\Request` (#15416) `Psr\Http\Message\ServerRequestInterface` applied for `yii\web\Request` --- framework/UPGRADE.md | 3 +- framework/data/DataFilter.php | 2 +- framework/rest/CreateAction.php | 2 +- framework/rest/IndexAction.php | 2 +- framework/rest/UpdateAction.php | 2 +- framework/validators/FileValidator.php | 2 +- framework/web/MultipartFormDataParser.php | 2 +- framework/web/Request.php | 361 ++++++++++++++++----- tests/framework/filters/AccessRuleTest.php | 53 ++- tests/framework/helpers/HtmlTest.php | 2 +- tests/framework/validators/FileValidatorTest.php | 6 +- .../framework/web/MultipartFormDataParserTest.php | 4 +- tests/framework/web/RequestTest.php | 69 +++- 13 files changed, 396 insertions(+), 114 deletions(-) diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index 8af546a..12db619 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -82,11 +82,12 @@ Upgrade from Yii 2.0.x * Profiling related functionality has been extracted into a separated component under `yii\profile\ProfilerInterface`. Profiling messages should be collection using `yii\base\Application::$profiler`. In case you wish to continue storing profiling messages along with the log ones, you may use `yii\profile\LogTarget` profiling target. -* Classes `yii\web\Request` and `yii\web\Response` have been updated to match interfaces `Psr\Http\Message\RequestInterface` +* Classes `yii\web\Request` and `yii\web\Response` have been updated to match interfaces `Psr\Http\Message\ServerRequestInterface` and `Psr\Http\Message\ResponseInterface` accordingly. Make sure you use their methods and properties correctly. In particular: method `getHeaders()` and corresponding virtual property `$headers` are no longer return `HeaderCollection` instance, you can use `getHeaderCollection()` in order to use old headers setup syntax; `Request|Response::$version` renamed to `Request|Response::$protocolVersion`; `Response::$statusText` renamed `Response::$reasonPhrase`; + `Request::$bodyParams` renamed to `Request::$parsedBody`; `Request::getBodyParam()` renamed to `Request::getParsedBodyParam()`; * `yii\web\Response::$stream` is no longer available, use `yii\web\Response::withBody()` to setup stream response. You can use `Response::$bodyRange` to setup stream content range. * Classes `yii\web\CookieCollection`, `yii\web\HeaderCollection` and `yii\web\UploadedFile` have been moved under diff --git a/framework/data/DataFilter.php b/framework/data/DataFilter.php index b172af7..4df32ab 100644 --- a/framework/data/DataFilter.php +++ b/framework/data/DataFilter.php @@ -64,7 +64,7 @@ use yii\validators\StringValidator; * use yii\data\DataFilter; * * $dataFilter = new DataFilter(); - * $dataFilter->load(Yii::$app->request->getBodyParams()); + * $dataFilter->load(Yii::$app->request->getParsedBody()); * ``` * * In order to function this class requires a search model specified via [[searchModel]]. This search model should declare diff --git a/framework/rest/CreateAction.php b/framework/rest/CreateAction.php index ed1325c..887bf7f 100644 --- a/framework/rest/CreateAction.php +++ b/framework/rest/CreateAction.php @@ -48,7 +48,7 @@ class CreateAction extends Action 'scenario' => $this->scenario, ]); - $model->load(Yii::$app->getRequest()->getBodyParams(), ''); + $model->load(Yii::$app->getRequest()->getParsedBody(), ''); if ($model->save()) { $response = Yii::$app->getResponse(); $response->setStatusCode(201); diff --git a/framework/rest/IndexAction.php b/framework/rest/IndexAction.php index 03d6200..fb2938a 100644 --- a/framework/rest/IndexAction.php +++ b/framework/rest/IndexAction.php @@ -88,7 +88,7 @@ class IndexAction extends Action */ protected function prepareDataProvider() { - $requestParams = Yii::$app->getRequest()->getBodyParams(); + $requestParams = Yii::$app->getRequest()->getParsedBody(); if (empty($requestParams)) { $requestParams = Yii::$app->getRequest()->getQueryParams(); } diff --git a/framework/rest/UpdateAction.php b/framework/rest/UpdateAction.php index a299679..7ecacbf 100644 --- a/framework/rest/UpdateAction.php +++ b/framework/rest/UpdateAction.php @@ -44,7 +44,7 @@ class UpdateAction extends Action } $model->scenario = $this->scenario; - $model->load(Yii::$app->getRequest()->getBodyParams(), ''); + $model->load(Yii::$app->getRequest()->getParsedBody(), ''); if ($model->save() === false && !$model->hasErrors()) { throw new ServerErrorHttpException('Failed to update the object for unknown reason.'); } diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index a037223..7d2dd3f 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -307,7 +307,7 @@ class FileValidator extends Validator } if (($request = Yii::$app->getRequest()) instanceof \yii\web\Request) { - $maxFileSize = Yii::$app->getRequest()->getBodyParam('MAX_FILE_SIZE', 0); + $maxFileSize = Yii::$app->getRequest()->getParsedBodyParam('MAX_FILE_SIZE', 0); if ($maxFileSize > 0 && $maxFileSize < $limit) { $limit = (int)$maxFileSize; } diff --git a/framework/web/MultipartFormDataParser.php b/framework/web/MultipartFormDataParser.php index ae85063..d06f38c 100644 --- a/framework/web/MultipartFormDataParser.php +++ b/framework/web/MultipartFormDataParser.php @@ -41,7 +41,7 @@ use yii\http\ResourceStream; * ```php * use yii\http\UploadedFile; * - * $restRequestData = Yii::$app->request->getBodyParams(); + * $restRequestData = Yii::$app->request->getParsedBody(); * $uploadedFile = Yii::$app->request->getUploadedFileByName('photo'); * * $model = new Item(); diff --git a/framework/web/Request.php b/framework/web/Request.php index 4608370..89b4d0c 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -7,7 +7,7 @@ namespace yii\web; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UploadedFileInterface; use Psr\Http\Message\UriInterface; @@ -15,7 +15,6 @@ use Yii; use yii\base\InvalidConfigException; use yii\di\Instance; use yii\helpers\ArrayHelper; -use yii\helpers\Html; use yii\http\Cookie; use yii\http\CookieCollection; use yii\http\FileStream; @@ -48,7 +47,7 @@ use yii\validators\IpValidator; * @property string|null $authUser The username sent via HTTP authentication, null if the username is not * given. This property is read-only. * @property string $baseUrl The relative URL for the application. - * @property array $bodyParams The request parameters given in the request body. + * @property array $parsedBody The request parameters given in the request body. * @property string $contentType Request content-type. Null is returned if this information is not available. * This property is read-only. * @property CookieCollection $cookies The cookie collection. This property is read-only. @@ -102,7 +101,7 @@ use yii\validators\IpValidator; * @since 2.0 * @SuppressWarnings(PHPMD.SuperGlobals) */ -class Request extends \yii\base\Request implements RequestInterface +class Request extends \yii\base\Request implements ServerRequestInterface { use MessageTrait; @@ -161,7 +160,7 @@ class Request extends \yii\base\Request implements RequestInterface * @var string the name of the POST parameter that is used to indicate if a request is a PUT, PATCH or DELETE * request tunneled through POST. Defaults to '_method'. * @see getMethod() - * @see getBodyParams() + * @see getParsedBody() */ public $methodParam = '_method'; /** @@ -181,7 +180,7 @@ class Request extends \yii\base\Request implements RequestInterface * To register a parser for parsing all request types you can use `'*'` as the array key. * This one will be used as a fallback in case no other types match. * - * @see getBodyParams() + * @see getParsedBody() */ public $parsers = []; /** @@ -268,6 +267,21 @@ class Request extends \yii\base\Request implements RequestInterface ]; /** + * @var array attributes derived from the request. + * @since 2.1.0 + */ + private $_attributes; + /** + * @var array server parameters. + * @since 2.1.0 + */ + private $_serverParams; + /** + * @var array the cookies sent by the client to the server. + * @since 2.1.0 + */ + private $_cookieParams; + /** * @var CookieCollection Collection of request cookies. */ private $_cookies; @@ -434,10 +448,8 @@ class Request extends \yii\base\Request implements RequestInterface $this->_method = $_POST[$this->methodParam]; } elseif ($this->hasHeader('x-http-method-override')) { $this->_method = $this->getHeaderLine('x-http-method-override'); - } elseif (isset($_SERVER['REQUEST_METHOD'])) { - $this->_method = $_SERVER['REQUEST_METHOD']; } else { - $this->_method = 'GET'; + $this->_method = $this->getServerParam('REQUEST_METHOD', 'GET'); } } return $this->_method; @@ -647,7 +659,7 @@ class Request extends \yii\base\Request implements RequestInterface $this->setBody($body); } - private $_bodyParams; + private $_parsedBody; /** * Returns the request parameters given in the request body. @@ -662,16 +674,16 @@ class Request extends \yii\base\Request implements RequestInterface * @throws InvalidConfigException if a registered parser does not implement the [[RequestParserInterface]]. * @throws UnsupportedMediaTypeHttpException if unable to parse raw body. * @see getMethod() - * @see getBodyParam() - * @see setBodyParams() + * @see getParsedBodyParam() + * @see setParsedBody() */ - public function getBodyParams() + public function getParsedBody() { - if ($this->_bodyParams === null) { + if ($this->_parsedBody === null) { if (isset($_POST[$this->methodParam])) { - $this->_bodyParams = $_POST; - unset($this->_bodyParams[$this->methodParam]); - return $this->_bodyParams; + $this->_parsedBody = $_POST; + unset($this->_parsedBody[$this->methodParam]); + return $this->_parsedBody; } $contentType = $this->getContentType(); @@ -685,44 +697,55 @@ class Request extends \yii\base\Request implements RequestInterface if (!($parser instanceof RequestParserInterface)) { throw new InvalidConfigException("The '$contentType' request parser is invalid. It must implement the yii\\web\\RequestParserInterface."); } - $this->_bodyParams = $parser->parse($this); + $this->_parsedBody = $parser->parse($this); } elseif (isset($this->parsers['*'])) { $parser = Yii::createObject($this->parsers['*']); if (!($parser instanceof RequestParserInterface)) { throw new InvalidConfigException('The fallback request parser is invalid. It must implement the yii\\web\\RequestParserInterface.'); } - $this->_bodyParams = $parser->parse($this); + $this->_parsedBody = $parser->parse($this); } elseif ($this->getMethod() === 'POST') { if ($contentType !== 'application/x-www-form-urlencoded' && $contentType !== 'multipart/form-data') { throw new UnsupportedMediaTypeHttpException(); } // PHP has already parsed the body so we have all params in $_POST - $this->_bodyParams = $_POST; + $this->_parsedBody = $_POST; if ($contentType === 'multipart/form-data') { - $this->_bodyParams = ArrayHelper::merge($this->_bodyParams, $this->getUploadedFiles()); + $this->_parsedBody = ArrayHelper::merge($this->_parsedBody, $this->getUploadedFiles()); } } else { if ($contentType !== 'application/x-www-form-urlencoded') { throw new UnsupportedMediaTypeHttpException(); } - $this->_bodyParams = []; - mb_parse_str($this->getBody()->__toString(), $this->_bodyParams); + $this->_parsedBody = []; + mb_parse_str($this->getBody()->__toString(), $this->_parsedBody); } } - return $this->_bodyParams; + return $this->_parsedBody; } /** * Sets the request body parameters. * @param array $values the request body parameters (name-value pairs) - * @see getBodyParam() - * @see getBodyParams() + * @see getParsedBodyParam() + * @see getParsedBody() */ - public function setBodyParams($values) + public function setParsedBody($values) { - $this->_bodyParams = $values; + $this->_parsedBody = $values; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function withParsedBody($data) + { + $newInstance = clone $this; + $newInstance->setParsedBody($data); + return $newInstance; } /** @@ -731,12 +754,12 @@ class Request extends \yii\base\Request implements RequestInterface * @param string $name the parameter name * @param mixed $defaultValue the default parameter value if the parameter does not exist. * @return mixed the parameter value - * @see getBodyParams() - * @see setBodyParams() + * @see getParsedBody() + * @see setParsedBody() */ - public function getBodyParam($name, $defaultValue = null) + public function getParsedBodyParam($name, $defaultValue = null) { - $params = $this->getBodyParams(); + $params = $this->getParsedBody(); return isset($params[$name]) ? $params[$name] : $defaultValue; } @@ -751,10 +774,10 @@ class Request extends \yii\base\Request implements RequestInterface public function post($name = null, $defaultValue = null) { if ($name === null) { - return $this->getBodyParams(); + return $this->getParsedBody(); } - return $this->getBodyParam($name, $defaultValue); + return $this->getParsedBodyParam($name, $defaultValue); } private $_queryParams; @@ -787,6 +810,20 @@ class Request extends \yii\base\Request implements RequestInterface } /** + * {@inheritdoc} + */ + public function withQueryParams(array $query) + { + if ($this->getQueryParams() === $query) { + return $this; + } + + $newInstance = clone $this; + $newInstance->setQueryParams($query); + return $newInstance; + } + + /** * Returns GET parameter with a given name. If name isn't specified, returns an array of all GET parameters. * * @param string $name the parameter name @@ -808,7 +845,7 @@ class Request extends \yii\base\Request implements RequestInterface * @param string $name the GET parameter name. * @param mixed $defaultValue the default parameter value if the GET parameter does not exist. * @return mixed the GET parameter value - * @see getBodyParam() + * @see getParsedBodyParam() */ public function getQueryParam($name, $defaultValue = null) { @@ -817,6 +854,82 @@ class Request extends \yii\base\Request implements RequestInterface return isset($params[$name]) ? $params[$name] : $defaultValue; } + /** + * Sets the data related to the incoming request environment. + * @param array $serverParams server parameters. + * @since 2.1.0 + */ + public function setServerParams(array $serverParams) + { + $this->_serverParams = $serverParams; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function getServerParams() + { + if ($this->_serverParams === null) { + $this->_serverParams = $_SERVER; + } + return $this->_serverParams; + } + + /** + * Return the server environment parameter by name. + * @param string $name parameter name. + * @param mixed $default default value to return if the parameter does not exist. + * @return mixed parameter value. + * @since 2.1.0 + */ + public function getServerParam($name, $default = null) + { + $params = $this->getServerParams(); + if (!isset($params[$name])) { + return $default; + } + return $params[$name]; + } + + /** + * Specifies cookies. + * @param array $cookies array of key/value pairs representing cookies. + * @since 2.1.0 + */ + public function setCookieParams(array $cookies) + { + $this->_cookieParams = $cookies; + $this->_cookies = null; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function getCookieParams() + { + if ($this->_cookieParams === null) { + $this->_cookieParams = $_COOKIE; + } + return $this->_cookieParams; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function withCookieParams(array $cookies) + { + if ($this->getCookieParams() === $cookies) { + return $this; + } + + $newInstance = clone $this; + $newInstance->setCookieParams($cookies); + return $newInstance; + } + private $_hostInfo; private $_hostName; @@ -854,8 +967,8 @@ class Request extends \yii\base\Request implements RequestInterface $http = $secure ? 'https' : 'http'; if ($this->hasHeader('Host')) { $this->_hostInfo = $http . '://' . $this->getHeaderLine('Host'); - } elseif (isset($_SERVER['SERVER_NAME'])) { - $this->_hostInfo = $http . '://' . $_SERVER['SERVER_NAME']; + } elseif (($serverName = $this->getServerParam('SERVER_NAME')) !== null) { + $this->_hostInfo = $http . '://' . $serverName; $port = $secure ? $this->getSecurePort() : $this->getPort(); if (($port !== 80 && !$secure) || ($port !== 443 && $secure)) { $this->_hostInfo .= ':' . $port; @@ -941,16 +1054,17 @@ class Request extends \yii\base\Request implements RequestInterface if ($this->_scriptUrl === null) { $scriptFile = $this->getScriptFile(); $scriptName = basename($scriptFile); - if (isset($_SERVER['SCRIPT_NAME']) && basename($_SERVER['SCRIPT_NAME']) === $scriptName) { - $this->_scriptUrl = $_SERVER['SCRIPT_NAME']; - } elseif (isset($_SERVER['PHP_SELF']) && basename($_SERVER['PHP_SELF']) === $scriptName) { - $this->_scriptUrl = $_SERVER['PHP_SELF']; - } elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $scriptName) { - $this->_scriptUrl = $_SERVER['ORIG_SCRIPT_NAME']; - } elseif (isset($_SERVER['PHP_SELF']) && ($pos = strpos($_SERVER['PHP_SELF'], '/' . $scriptName)) !== false) { - $this->_scriptUrl = substr($_SERVER['SCRIPT_NAME'], 0, $pos) . '/' . $scriptName; - } elseif (!empty($_SERVER['DOCUMENT_ROOT']) && strpos($scriptFile, $_SERVER['DOCUMENT_ROOT']) === 0) { - $this->_scriptUrl = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $scriptFile)); + $serverParams = $this->getServerParams(); + if (isset($serverParams['SCRIPT_NAME']) && basename($serverParams['SCRIPT_NAME']) === $scriptName) { + $this->_scriptUrl = $serverParams['SCRIPT_NAME']; + } elseif (isset($serverParams['PHP_SELF']) && basename($serverParams['PHP_SELF']) === $scriptName) { + $this->_scriptUrl = $serverParams['PHP_SELF']; + } elseif (isset($serverParams['ORIG_SCRIPT_NAME']) && basename($serverParams['ORIG_SCRIPT_NAME']) === $scriptName) { + $this->_scriptUrl = $serverParams['ORIG_SCRIPT_NAME']; + } elseif (isset($serverParams['PHP_SELF']) && ($pos = strpos($serverParams['PHP_SELF'], '/' . $scriptName)) !== false) { + $this->_scriptUrl = substr($serverParams['SCRIPT_NAME'], 0, $pos) . '/' . $scriptName; + } elseif (!empty($serverParams['DOCUMENT_ROOT']) && strpos($scriptFile, $serverParams['DOCUMENT_ROOT']) === 0) { + $this->_scriptUrl = str_replace('\\', '/', str_replace($serverParams['DOCUMENT_ROOT'], '', $scriptFile)); } else { throw new InvalidConfigException('Unable to determine the entry script URL.'); } @@ -984,8 +1098,8 @@ class Request extends \yii\base\Request implements RequestInterface return $this->_scriptFile; } - if (isset($_SERVER['SCRIPT_FILENAME'])) { - return $_SERVER['SCRIPT_FILENAME']; + if (($scriptFilename = $this->getServerParam('SCRIPT_FILENAME')) !== null) { + return $scriptFilename; } throw new InvalidConfigException('Unable to determine the entry script file path.'); @@ -1072,8 +1186,8 @@ class Request extends \yii\base\Request implements RequestInterface $pathInfo = substr($pathInfo, strlen($scriptUrl)); } elseif ($baseUrl === '' || strpos($pathInfo, $baseUrl) === 0) { $pathInfo = substr($pathInfo, strlen($baseUrl)); - } elseif (isset($_SERVER['PHP_SELF']) && strpos($_SERVER['PHP_SELF'], $scriptUrl) === 0) { - $pathInfo = substr($_SERVER['PHP_SELF'], strlen($scriptUrl)); + } elseif (($phpSelf = $this->getServerParam('PHP_SELF')) !== null && strpos($phpSelf, $scriptUrl) === 0) { + $pathInfo = substr($phpSelf, strlen($scriptUrl)); } else { throw new InvalidConfigException('Unable to determine the path info of the current request.'); } @@ -1134,17 +1248,19 @@ class Request extends \yii\base\Request implements RequestInterface */ protected function resolveRequestUri() { + $serverParams = $this->getServerParams(); + if ($this->hasHeader('x-rewrite-url')) { // IIS $requestUri = $this->getHeaderLine('x-rewrite-url'); - } elseif (isset($_SERVER['REQUEST_URI'])) { - $requestUri = $_SERVER['REQUEST_URI']; + } elseif (isset($serverParams['REQUEST_URI'])) { + $requestUri = $serverParams['REQUEST_URI']; if ($requestUri !== '' && $requestUri[0] !== '/') { $requestUri = preg_replace('/^(http|https):\/\/[^\/]+/i', '', $requestUri); } - } elseif (isset($_SERVER['ORIG_PATH_INFO'])) { // IIS 5.0 CGI - $requestUri = $_SERVER['ORIG_PATH_INFO']; - if (!empty($_SERVER['QUERY_STRING'])) { - $requestUri .= '?' . $_SERVER['QUERY_STRING']; + } elseif (isset($serverParams['ORIG_PATH_INFO'])) { // IIS 5.0 CGI + $requestUri = $serverParams['ORIG_PATH_INFO']; + if (!empty($serverParams['QUERY_STRING'])) { + $requestUri .= '?' . $serverParams['QUERY_STRING']; } } else { throw new InvalidConfigException('Unable to determine the request URI.'); @@ -1159,7 +1275,7 @@ class Request extends \yii\base\Request implements RequestInterface */ public function getQueryString() { - return isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : ''; + return $this->getServerParam('QUERY_STRING', ''); } /** @@ -1168,7 +1284,8 @@ class Request extends \yii\base\Request implements RequestInterface */ public function getIsSecureConnection() { - if (isset($_SERVER['HTTPS']) && (strcasecmp($_SERVER['HTTPS'], 'on') === 0 || $_SERVER['HTTPS'] == 1)) { + $https = $this->getServerParam('HTTPS'); + if ($https !== null && (strcasecmp($https, 'on') === 0 || $https == 1)) { return true; } foreach ($this->secureProtocolHeaders as $header => $values) { @@ -1190,7 +1307,7 @@ class Request extends \yii\base\Request implements RequestInterface */ public function getServerName() { - return isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null; + return $this->getServerParam('SERVER_NAME'); } /** @@ -1199,7 +1316,8 @@ class Request extends \yii\base\Request implements RequestInterface */ public function getServerPort() { - return isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : null; + $port = $this->getServerParam('SERVER_PORT'); + return $port === null ? null : (int) $port; } /** @@ -1286,7 +1404,7 @@ class Request extends \yii\base\Request implements RequestInterface */ public function getRemoteIP() { - return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null; + return $this->getServerParam('REMOTE_ADDR'); } /** @@ -1299,7 +1417,7 @@ class Request extends \yii\base\Request implements RequestInterface */ public function getRemoteHost() { - return isset($_SERVER['REMOTE_HOST']) ? $_SERVER['REMOTE_HOST'] : null; + return $this->getServerParam('REMOTE_HOST'); } /** @@ -1330,8 +1448,8 @@ class Request extends \yii\base\Request implements RequestInterface */ public function getAuthCredentials() { - $username = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null; - $password = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : null; + $username = $this->getServerParam('PHP_AUTH_USER'); + $password = $this->getServerParam('PHP_AUTH_PW'); if ($username !== null || $password !== null) { return [$username, $password]; } @@ -1370,7 +1488,8 @@ class Request extends \yii\base\Request implements RequestInterface public function getPort() { if ($this->_port === null) { - $this->_port = !$this->getIsSecureConnection() && isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : 80; + $serverPort = $this->getServerParam('SERVER_PORT'); + $this->_port = !$this->getIsSecureConnection() && $serverPort === null ? (int) $serverPort : 80; } return $this->_port; @@ -1402,7 +1521,8 @@ class Request extends \yii\base\Request implements RequestInterface public function getSecurePort() { if ($this->_securePort === null) { - $this->_securePort = $this->getIsSecureConnection() && isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : 443; + $serverPort = $this->getServerParam('SERVER_PORT'); + $this->_securePort = $this->getIsSecureConnection() && $serverPort === null ? (int) $serverPort : 443; } return $this->_securePort; @@ -1682,7 +1802,7 @@ class Request extends \yii\base\Request implements RequestInterface } /** - * Converts `$_COOKIE` into an array of [[Cookie]]. + * Converts [[cookieParams]] into an array of [[Cookie]]. * @return array the cookies obtained from request * @throws InvalidConfigException if [[cookieValidationKey]] is not set when [[enableCookieValidation]] is true */ @@ -1691,9 +1811,9 @@ class Request extends \yii\base\Request implements RequestInterface $cookies = []; if ($this->enableCookieValidation) { if ($this->cookieValidationKey == '') { - throw new InvalidConfigException(get_class($this) . '::cookieValidationKey must be configured with a secret key.'); + throw new InvalidConfigException(get_class($this) . '::$cookieValidationKey must be configured with a secret key.'); } - foreach ($_COOKIE as $name => $value) { + foreach ($this->getCookieParams() as $name => $value) { if (!is_string($value)) { continue; } @@ -1712,7 +1832,7 @@ class Request extends \yii\base\Request implements RequestInterface } } } else { - foreach ($_COOKIE as $name => $value) { + foreach ($this->getCookieParams() as $name => $value) { $cookies[$name] = Yii::createObject([ 'class' => \yii\http\Cookie::class, 'name' => $name, @@ -1726,15 +1846,13 @@ class Request extends \yii\base\Request implements RequestInterface } /** - * Returns uploaded files for this request. - * Uploaded files are returned in format according to [PSR-7 Uploaded Files specs](http://www.php-fig.org/psr/psr-7/#16-uploaded-files). - * @return array uploaded files. + * {@inheritdoc} * @since 2.1.0 */ public function getUploadedFiles() { if ($this->_uploadedFiles === null) { - $this->getBodyParams(); // uploaded files are the part of the body and may be set while its parsing + $this->getParsedBody(); // uploaded files are the part of the body and may be set while its parsing if ($this->_uploadedFiles === null) { $this->_uploadedFiles = $this->defaultUploadedFiles(); } @@ -1754,6 +1872,17 @@ class Request extends \yii\base\Request implements RequestInterface } /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function withUploadedFiles(array $uploadedFiles) + { + $newInstance = clone $this; + $newInstance->setUploadedFiles($uploadedFiles); + return $newInstance; + } + + /** * Initializes default uploaded files data structure parsing super-global $_FILES. * @see http://www.php-fig.org/psr/psr-7/#16-uploaded-files * @return array uploaded files. @@ -1977,7 +2106,7 @@ class Request extends \yii\base\Request implements RequestInterface return $this->validateCsrfTokenInternal($clientSuppliedToken, $trueToken); } - return $this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken) + return $this->validateCsrfTokenInternal($this->getParsedBodyParam($this->csrfParam), $trueToken) || $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken); } @@ -2001,6 +2130,86 @@ class Request extends \yii\base\Request implements RequestInterface /** * {@inheritdoc} + * @since 2.1.0 + */ + public function getAttributes() + { + if ($this->_attributes === null) { + $this->_attributes = $this->defaultAttributes(); + } + return $this->_attributes; + } + + /** + * @param array $attributes attributes derived from the request. + */ + public function setAttributes(array $attributes) + { + $this->_attributes = $attributes; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function getAttribute($name, $default = null) + { + $attributes = $this->getAttributes(); + if (!array_key_exists($name, $attributes)) { + return $default; + } + + return $attributes[$name]; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function withAttribute($name, $value) + { + $attributes = $this->getAttributes(); + if (array_key_exists($name, $attributes) && $attributes[$name] === $value) { + return $this; + } + + $attributes[$name] = $value; + + $newInstance = clone $this; + $newInstance->setAttributes($attributes); + return $newInstance; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function withoutAttribute($name) + { + $attributes = $this->getAttributes(); + if (!array_key_exists($name, $attributes)) { + return $this; + } + + unset($attributes[$name]); + + $newInstance = clone $this; + $newInstance->setAttributes($attributes); + return $newInstance; + } + + /** + * Returns default server request attributes to be used in case they are not explicitly set. + * @return array attributes derived from the request. + * @since 2.1.0 + */ + protected function defaultAttributes() + { + return []; + } + + /** + * {@inheritdoc} */ public function __clone() { diff --git a/tests/framework/filters/AccessRuleTest.php b/tests/framework/filters/AccessRuleTest.php index 48701dc..2acef76 100644 --- a/tests/framework/filters/AccessRuleTest.php +++ b/tests/framework/filters/AccessRuleTest.php @@ -416,7 +416,9 @@ class AccessRuleTest extends \yiiunit\TestCase $this->assertFalse($rule->allows($action, $user, $request)); // match, one IP - $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $request->setServerParams([ + 'REMOTE_ADDR' => '127.0.0.1' + ]); $rule->ips = ['127.0.0.1']; $rule->allow = true; $this->assertTrue($rule->allows($action, $user, $request)); @@ -424,7 +426,9 @@ class AccessRuleTest extends \yiiunit\TestCase $this->assertFalse($rule->allows($action, $user, $request)); // no match, one IP - $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $request->setServerParams([ + 'REMOTE_ADDR' => '127.0.0.1' + ]); $rule->ips = ['192.168.0.1']; $rule->allow = true; $this->assertNull($rule->allows($action, $user, $request)); @@ -432,13 +436,17 @@ class AccessRuleTest extends \yiiunit\TestCase $this->assertNull($rule->allows($action, $user, $request)); // no partial match, one IP - $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $request->setServerParams([ + 'REMOTE_ADDR' => '127.0.0.1' + ]); $rule->ips = ['127.0.0.10']; $rule->allow = true; $this->assertNull($rule->allows($action, $user, $request)); $rule->allow = false; $this->assertNull($rule->allows($action, $user, $request)); - $_SERVER['REMOTE_ADDR'] = '127.0.0.10'; + $request->setServerParams([ + 'REMOTE_ADDR' => '127.0.0.10' + ]); $rule->ips = ['127.0.0.1']; $rule->allow = true; $this->assertNull($rule->allows($action, $user, $request)); @@ -446,7 +454,9 @@ class AccessRuleTest extends \yiiunit\TestCase $this->assertNull($rule->allows($action, $user, $request)); // match, one IP IPv6 - $_SERVER['REMOTE_ADDR'] = '::1'; + $request->setServerParams([ + 'REMOTE_ADDR' => '::1' + ]); $rule->ips = ['::1']; $rule->allow = true; $this->assertTrue($rule->allows($action, $user, $request)); @@ -454,7 +464,9 @@ class AccessRuleTest extends \yiiunit\TestCase $this->assertFalse($rule->allows($action, $user, $request)); // no match, one IP IPv6 - $_SERVER['REMOTE_ADDR'] = '::1'; + $request->setServerParams([ + 'REMOTE_ADDR' => '::1' + ]); $rule->ips = ['dead::beaf::1']; $rule->allow = true; $this->assertNull($rule->allows($action, $user, $request)); @@ -462,13 +474,18 @@ class AccessRuleTest extends \yiiunit\TestCase $this->assertNull($rule->allows($action, $user, $request)); // no partial match, one IP IPv6 - $_SERVER['REMOTE_ADDR'] = '::1'; + $request->setServerParams([ + 'REMOTE_ADDR' => '::1' + ]); $rule->ips = ['::123']; $rule->allow = true; $this->assertNull($rule->allows($action, $user, $request)); $rule->allow = false; $this->assertNull($rule->allows($action, $user, $request)); - $_SERVER['REMOTE_ADDR'] = '::123'; + + $request->setServerParams([ + 'REMOTE_ADDR' => '::123' + ]); $rule->ips = ['::1']; $rule->allow = true; $this->assertNull($rule->allows($action, $user, $request)); @@ -476,7 +493,9 @@ class AccessRuleTest extends \yiiunit\TestCase $this->assertNull($rule->allows($action, $user, $request)); // undefined IP - $_SERVER['REMOTE_ADDR'] = null; + $request->setServerParams([ + 'REMOTE_ADDR' => null + ]); $rule->ips = ['192.168.*']; $rule->allow = true; $this->assertNull($rule->allows($action, $user, $request)); @@ -493,7 +512,9 @@ class AccessRuleTest extends \yiiunit\TestCase $rule = new AccessRule(); // no match - $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $request->setServerParams([ + 'REMOTE_ADDR' => '127.0.0.1' + ]); $rule->ips = ['192.168.*']; $rule->allow = true; $this->assertNull($rule->allows($action, $user, $request)); @@ -501,7 +522,9 @@ class AccessRuleTest extends \yiiunit\TestCase $this->assertNull($rule->allows($action, $user, $request)); // match - $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $request->setServerParams([ + 'REMOTE_ADDR' => '127.0.0.1' + ]); $rule->ips = ['127.0.*']; $rule->allow = true; $this->assertTrue($rule->allows($action, $user, $request)); @@ -509,7 +532,9 @@ class AccessRuleTest extends \yiiunit\TestCase $this->assertFalse($rule->allows($action, $user, $request)); // match, IPv6 - $_SERVER['REMOTE_ADDR'] = '2a01:4f8:120:7202::2'; + $request->setServerParams([ + 'REMOTE_ADDR' => '2a01:4f8:120:7202::2' + ]); $rule->ips = ['2a01:4f8:120:*']; $rule->allow = true; $this->assertTrue($rule->allows($action, $user, $request)); @@ -517,7 +542,9 @@ class AccessRuleTest extends \yiiunit\TestCase $this->assertFalse($rule->allows($action, $user, $request)); // no match, IPv6 - $_SERVER['REMOTE_ADDR'] = '::1'; + $request->setServerParams([ + 'REMOTE_ADDR' => '::1' + ]); $rule->ips = ['2a01:4f8:120:*']; $rule->allow = true; $this->assertNull($rule->allows($action, $user, $request)); diff --git a/tests/framework/helpers/HtmlTest.php b/tests/framework/helpers/HtmlTest.php index 3cf052c..3f6a919 100644 --- a/tests/framework/helpers/HtmlTest.php +++ b/tests/framework/helpers/HtmlTest.php @@ -149,7 +149,7 @@ class HtmlTest extends TestCase ], ]); $this->expectException(\yii\base\InvalidConfigException::class); - $this->expectExceptionMessage('yii\web\Request::cookieValidationKey must be configured with a secret key.'); + $this->expectExceptionMessage('yii\web\Request::$cookieValidationKey must be configured with a secret key.'); Html::csrfMetaTags(); } diff --git a/tests/framework/validators/FileValidatorTest.php b/tests/framework/validators/FileValidatorTest.php index 7fa8e5e..aaa872d 100644 --- a/tests/framework/validators/FileValidatorTest.php +++ b/tests/framework/validators/FileValidatorTest.php @@ -81,15 +81,15 @@ class FileValidatorTest extends TestCase $size = min($this->sizeToBytes(ini_get('upload_max_filesize')), $this->sizeToBytes(ini_get('post_max_size'))); $val = new FileValidator(); - Yii::$app->request->setBodyParams([]); + Yii::$app->request->setParsedBody([]); $this->assertEquals($size, $val->getSizeLimit()); $val->maxSize = $size + 1; // set and test if value is overridden $this->assertEquals($size, $val->getSizeLimit()); $val->maxSize = abs($size - 1); $this->assertEquals($size - 1, $val->getSizeLimit()); - Yii::$app->request->setBodyParams(['MAX_FILE_SIZE' => $size + 1]); + Yii::$app->request->setParsedBody(['MAX_FILE_SIZE' => $size + 1]); $this->assertEquals($size - 1, $val->getSizeLimit()); - Yii::$app->request->setBodyParams(['MAX_FILE_SIZE' => abs($size - 2)]); + Yii::$app->request->setParsedBody(['MAX_FILE_SIZE' => abs($size - 2)]); $this->assertSame(abs($size - 2), $val->getSizeLimit()); } diff --git a/tests/framework/web/MultipartFormDataParserTest.php b/tests/framework/web/MultipartFormDataParserTest.php index 1b277dc..67ce574 100644 --- a/tests/framework/web/MultipartFormDataParserTest.php +++ b/tests/framework/web/MultipartFormDataParserTest.php @@ -84,7 +84,7 @@ class MultipartFormDataParserTest extends TestCase 'multipart/form-data' => MultipartFormDataParser::class ] ]); - $bodyParams = $request->getBodyParams(); + $bodyParams = $request->getParsedBody(); $this->assertEquals($_POST, $bodyParams); $this->assertEquals([], $request->getUploadedFiles()); } @@ -114,7 +114,7 @@ class MultipartFormDataParserTest extends TestCase 'multipart/form-data' => MultipartFormDataParser::class ] ]); - $bodyParams = $request->getBodyParams(); + $bodyParams = $request->getParsedBody(); $this->assertEquals([], $bodyParams); } diff --git a/tests/framework/web/RequestTest.php b/tests/framework/web/RequestTest.php index 48fa5db..0018efa 100644 --- a/tests/framework/web/RequestTest.php +++ b/tests/framework/web/RequestTest.php @@ -162,9 +162,9 @@ class RequestTest extends TestCase // only accept valid token on POST foreach (['POST', 'PUT', 'DELETE'] as $method) { $request->setMethod($method); - $request->setBodyParams([]); + $request->setParsedBody([]); $this->assertFalse($request->validateCsrfToken()); - $request->setBodyParams([$request->csrfParam => $token]); + $request->setParsedBody([$request->csrfParam => $token]); $this->assertTrue($request->validateCsrfToken()); } } @@ -190,7 +190,7 @@ class RequestTest extends TestCase // only accept valid token on POST foreach (['POST', 'PUT', 'DELETE'] as $method) { $request->setMethod($method); - $request->setBodyParams([]); + $request->setParsedBody([]); $this->assertFalse($request->withoutHeader(Request::CSRF_HEADER)->validateCsrfToken()); $this->assertTrue($request->withAddedHeader(Request::CSRF_HEADER, $token)->validateCsrfToken()); @@ -292,10 +292,12 @@ class RequestTest extends TestCase { $request = new Request(); - $_SERVER['SERVER_NAME'] = 'servername'; + $request->setServerParams([ + 'SERVER_NAME' => 'servername' + ]); $this->assertEquals('servername', $request->getServerName()); - unset($_SERVER['SERVER_NAME']); + $request->setServerParams([]); $this->assertNull($request->getServerName()); } @@ -303,10 +305,12 @@ class RequestTest extends TestCase { $request = new Request(); - $_SERVER['SERVER_PORT'] = 33; + $request->setServerParams([ + 'SERVER_PORT' => 33 + ]); $this->assertEquals(33, $request->getServerPort()); - unset($_SERVER['SERVER_PORT']); + $request->setServerParams([]); $this->assertNull($request->getServerPort()); } @@ -607,17 +611,17 @@ class RequestTest extends TestCase $request->setBody($body); $_POST = ['name' => 'post']; - $this->assertSame(['name' => 'value'], $request->withHeader('Content-Type', 'application/x-www-form-urlencoded')->getBodyParams()); - $this->assertSame(['name' => 'post'], $request->withHeader('Content-Type', 'application/x-www-form-urlencoded')->withMethod('POST')->getBodyParams()); - $this->assertSame(['name' => 'post'], $request->withHeader('Content-Type', 'multipart/form-data')->withMethod('POST')->getBodyParams()); + $this->assertSame(['name' => 'value'], $request->withHeader('Content-Type', 'application/x-www-form-urlencoded')->getParsedBody()); + $this->assertSame(['name' => 'post'], $request->withHeader('Content-Type', 'application/x-www-form-urlencoded')->withMethod('POST')->getParsedBody()); + $this->assertSame(['name' => 'post'], $request->withHeader('Content-Type', 'multipart/form-data')->withMethod('POST')->getParsedBody()); try { - $request->getBodyParams(); + $request->getParsedBody(); } catch (UnsupportedMediaTypeHttpException $noContentTypeException) {} $this->assertTrue(isset($noContentTypeException)); try { - $request->withMethod('POST')->getBodyParams(); + $request->withMethod('POST')->getParsedBody(); } catch (UnsupportedMediaTypeHttpException $postWithoutContentTypeException) {} $this->assertTrue(isset($postWithoutContentTypeException)); } @@ -838,4 +842,45 @@ class RequestTest extends TestCase $this->assertCount(1, $uploadedFiles); $this->assertTrue($uploadedFiles[0] instanceof UploadedFile); } + + public function testSetupAttributes() + { + $request = new Request(); + + $request->setAttributes(['some' => 'foo']); + $this->assertSame(['some' => 'foo'], $request->getAttributes()); + } + + /** + * @depends testSetupAttributes + */ + public function testGetAttribute() + { + $request = new Request(); + + $request->setAttributes(['some' => 'foo']); + + $this->assertSame('foo', $request->getAttribute('some')); + $this->assertSame(null, $request->getAttribute('un-existing')); + $this->assertSame('default', $request->getAttribute('un-existing', 'default')); + } + + /** + * @depends testSetupAttributes + */ + public function testModifyAttributes() + { + $request = new Request(); + + $request->setAttributes(['attr1' => '1']); + + $newStorage = $request->withAttribute('attr2', '2'); + $this->assertNotSame($newStorage, $request); + $this->assertSame(['attr1' => '1', 'attr2' => '2'], $newStorage->getAttributes()); + + $request = $newStorage; + $newStorage = $request->withoutAttribute('attr1'); + $this->assertNotSame($newStorage, $request); + $this->assertSame(['attr2' => '2'], $newStorage->getAttributes()); + } }