Yii2 framework backup
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

497 lines
17 KiB

12 years ago
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
12 years ago
* @license http://www.yiiframework.com/license/
*/
namespace yii\web;
use Yii;
12 years ago
use yii\base\Object;
use yii\base\InvalidConfigException;
12 years ago
/**
* UrlRule represents a rule used by [[UrlManager]] for parsing and generating URLs.
*
* To define your own URL parsing and creation logic you can extend from this class
* and add it to [[UrlManager::rules]] like this:
*
* ```php
* 'rules' => [
* ['class' => 'MyUrlRule', 'pattern' => '...', 'route' => 'site/index', ...],
* // ...
* ]
* ```
12 years ago
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class UrlRule extends Object implements UrlRuleInterface
12 years ago
{
/**
* Set [[mode]] with this value to mark that this rule is for URL parsing only
*/
const PARSING_ONLY = 1;
/**
* Set [[mode]] with this value to mark that this rule is for URL creation only
*/
const CREATION_ONLY = 2;
12 years ago
/**
* @var string the name of this rule. If not set, it will use [[pattern]] as the name.
*/
public $name;
/**
* On the rule initialization, the [[pattern]] matching parameters names will be replaced with [[placeholders]].
* @var string the pattern used to parse and create the path info part of a URL.
* @see host
* @see placeholders
*/
public $pattern;
/**
* @var string the pattern used to parse and create the host info part of a URL (e.g. `http://example.com`).
* @see pattern
*/
public $host;
/**
* @var string the route to the controller action
*/
public $route;
/**
* @var array the default GET parameters (name => value) that this rule provides.
* When this rule is used to parse the incoming request, the values declared in this property
* will be injected into $_GET.
*/
public $defaults = [];
/**
* @var string the URL suffix used for this rule.
* For example, ".html" can be used so that the URL looks like pointing to a static HTML page.
Refactored UrlManagerTest to cover more use cases UrlManager is a complex class with a large bunch of options that needs to be tested and we had a few regressions in the past even though there are already a lot of tests covering it. Test coverage measured by lines does not help us determine how good it is tested, we need test coverage for every major path through the code. I have refactored the UrlManager tests to reflect the different options and cases to have a better overview of which cases are covered and which are not. UrlManager has two main operation modes: - "default" url format, which is the simple case. These are covered by methods in `UrlManagerTest`. - "pretty" url format. This is the complex case, which involves UrlRules and url parsing. I have created two separate classes for this case: Url creation for "pretty" url format is covered by `UrlManagerCreateUrlTest`. Url parsing for "pretty" url format is covered by `UrlManagerParseUrlTest`. Each of the test classes have a `getUrlManager` method that creates a UrlManager instance with a specific configuration and certain variations in options. It is also tested that options that are not relevant in a certain operation mode have no effect on the result. To make sure to not remove tests that have existed before, here is a map of where code has been moved. The following test methods existed in the [old test class](https://github.com/yiisoft/yii2/blob/4187718c148610b02fc973a19be53600e6230604/tests/framework/web/UrlManagerTest.php): - `testCreateUrl()` split between UrlManagerTest and UrlManagerCreateUrlTest variations should all be covered by `variationsProvider()`. - `testCreateUrlWithNullParams()` covered by UrlManagerCreateUrlTest by `testWithNullParams()` - `testCreateUrlWithEmptyPattern()` - `testCreateAbsoluteUrl()` covered in UrlManagerCreateUrlTest by new tests via `variationsProvider()`. - `testCreateAbsoluteUrlWithSuffix()` covered in UrlManagerCreateUrlTest by `testAbsolutePatterns`. - `testParseRequest()` covered by UrlManagerParseUrlTest, UrlNormalizer related parts moved to UrlNormalizerTest. - `testParseRESTRequest()` moved to UrlManagerParseUrlTest - `testHash()` covered in different tests in UrlManagerCreateUrlTest. - `testMultipleHostsRules($host)` kept as is. Before: $ vendor/bin/phpunit tests/framework/web/UrlManagerTest.php ... OK (12 tests, 89 assertions) After: $ vendor/bin/phpunit tests/framework/web/UrlManager*.php ... OK (72 tests, 648 assertions)
8 years ago
* If not set, the value of [[UrlManager::suffix]] will be used.
*/
public $suffix;
/**
* @var string|array the HTTP verb (e.g. GET, POST, DELETE) that this rule should match.
* Use array to represent multiple verbs that this rule may match.
* If this property is not set, the rule can match any verb.
* Note that this property is only used when parsing a request. It is ignored for URL creation.
*/
public $verb;
/**
* @var int a value indicating if this rule should be used for both request parsing and URL creation,
* parsing only, or creation only.
* If not set or 0, it means the rule is both request parsing and URL creation.
* If it is [[PARSING_ONLY]], the rule is for request parsing only.
* If it is [[CREATION_ONLY]], the rule is for URL creation only.
*/
public $mode;
/**
* @var bool a value indicating if parameters should be url encoded.
*/
public $encodeParams = true;
/**
* @var UrlNormalizer|array|false|null the configuration for [[UrlNormalizer]] used by this rule.
* If `null`, [[UrlManager::normalizer]] will be used, if `false`, normalization will be skipped
* for this rule.
* @since 2.0.10
*/
public $normalizer;
9 years ago
/**
9 years ago
* @var array list of placeholders for matching parameters names. Used in [[parseRequest()]], [[createUrl()]].
* On the rule initialization, the [[pattern]] parameters names will be replaced with placeholders.
* This array contains relations between the original parameters names and their placeholders.
9 years ago
* The array keys are the placeholders and the values are the original names.
*
* @see parseRequest()
* @see createUrl()
* @since 2.0.7
*/
protected $placeholders = [];
9 years ago
/**
* @var string the template for generating a new URL. This is derived from [[pattern]] and is used in generating URL.
*/
private $_template;
/**
* @var string the regex for matching the route part. This is used in generating URL.
*/
private $_routeRule;
/**
* @var array list of regex for matching parameters. This is used in generating URL.
*/
private $_paramRules = [];
/**
* @var array list of parameters used in the route.
*/
private $_routeParams = [];
12 years ago
/**
* @return string
* @since 2.0.11
*/
public function __toString()
{
$str = '';
if ($this->verb !== null) {
$str .= implode(',', $this->verb) . ' ';
}
if ($this->host !== null && strrpos($this->name, $this->host) === false) {
$str .= $this->host . '/';
}
$str .= $this->name;
if ($str === '') {
return '/';
}
return $str;
}
/**
* Initializes this rule.
*/
public function init()
{
if ($this->pattern === null) {
throw new InvalidConfigException('UrlRule::pattern must be set.');
}
if ($this->route === null) {
throw new InvalidConfigException('UrlRule::route must be set.');
}
if (is_array($this->normalizer)) {
$normalizerConfig = array_merge(['class' => UrlNormalizer::className()], $this->normalizer);
$this->normalizer = Yii::createObject($normalizerConfig);
}
if ($this->normalizer !== null && $this->normalizer !== false && !$this->normalizer instanceof UrlNormalizer) {
throw new InvalidConfigException('Invalid config for UrlRule::normalizer.');
}
if ($this->verb !== null) {
if (is_array($this->verb)) {
foreach ($this->verb as $i => $verb) {
$this->verb[$i] = strtoupper($verb);
}
} else {
$this->verb = [strtoupper($this->verb)];
}
}
if ($this->name === null) {
$this->name = $this->pattern;
}
12 years ago
$this->pattern = $this->trimSlashes($this->pattern);
$this->route = trim($this->route, '/');
if ($this->host !== null) {
$this->host = rtrim($this->host, '/');
$this->pattern = rtrim($this->host . '/' . $this->pattern, '/');
} elseif ($this->pattern === '') {
$this->_template = '';
$this->pattern = '#^$#u';
12 years ago
return;
} elseif (($pos = strpos($this->pattern, '://')) !== false) {
if (($pos2 = strpos($this->pattern, '/', $pos + 3)) !== false) {
$this->host = substr($this->pattern, 0, $pos2);
} else {
$this->host = $this->pattern;
}
} elseif (strpos($this->pattern, '//') === 0) {
if (($pos2 = strpos($this->pattern, '/', $pos + 2)) !== false) {
$this->host = substr($this->pattern, 0, $pos2);
} else {
$this->host = $this->pattern;
}
} else {
$this->pattern = '/' . $this->pattern . '/';
}
12 years ago
if (strpos($this->route, '<') !== false && preg_match_all('/<([\w._-]+)>/', $this->route, $matches)) {
foreach ($matches[1] as $name) {
$this->_routeParams[$name] = "<$name>";
}
}
12 years ago
$tr = [
'.' => '\\.',
'*' => '\\*',
'$' => '\\$',
'[' => '\\[',
']' => '\\]',
'(' => '\\(',
')' => '\\)',
];
$tr2 = [];
if (preg_match_all('/<([\w._-]+):?([^>]+)?>/', $this->pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
foreach ($matches as $match) {
$name = $match[1][0];
$pattern = isset($match[2][0]) ? $match[2][0] : '[^\/]+';
$placeholder = 'a' . hash('crc32b', $name); // placeholder must begin with a letter
$this->placeholders[$placeholder] = $name;
if (array_key_exists($name, $this->defaults)) {
$length = strlen($match[0][0]);
$offset = $match[0][1];
if ($offset > 1 && $this->pattern[$offset - 1] === '/' && (!isset($this->pattern[$offset + $length]) || $this->pattern[$offset + $length] === '/')) {
$tr["/<$name>"] = "(/(?P<$placeholder>$pattern))?";
} else {
$tr["<$name>"] = "(?P<$placeholder>$pattern)?";
}
} else {
$tr["<$name>"] = "(?P<$placeholder>$pattern)";
}
if (isset($this->_routeParams[$name])) {
$tr2["<$name>"] = "(?P<$placeholder>$pattern)";
} else {
$this->_paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#u";
}
}
}
12 years ago
$this->_template = preg_replace('/<([\w._-]+):?([^>]+)?>/', '<$1>', $this->pattern);
$this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u';
12 years ago
// if host starts with relative scheme, then insert pattern to match any
if (strpos($this->host, '//') === 0) {
$this->pattern = substr_replace($this->pattern, '[\w]+://', 2, 0);
}
if (!empty($this->_routeParams)) {
$this->_routeRule = '#^' . strtr($this->route, $tr2) . '$#u';
}
}
12 years ago
/**
* @param UrlManager $manager the URL manager
* @return UrlNormalizer|null
* @since 2.0.10
*/
protected function getNormalizer($manager)
{
if ($this->normalizer === null) {
return $manager->normalizer;
} else {
return $this->normalizer;
}
}
/**
* @param UrlManager $manager the URL manager
* @return bool
* @since 2.0.10
*/
protected function hasNormalizer($manager)
{
return $this->getNormalizer($manager) instanceof UrlNormalizer;
}
/**
* Parses the given request and returns the corresponding route and parameters.
* @param UrlManager $manager the URL manager
* @param Request $request the request component
* @return array|bool the parsing result. The route and the parameters are returned as an array.
* If `false`, it means this rule cannot be used to parse this path info.
*/
public function parseRequest($manager, $request)
{
if ($this->mode === self::CREATION_ONLY) {
return false;
}
12 years ago
if (!empty($this->verb) && !in_array($request->getMethod(), $this->verb, true)) {
return false;
}
12 years ago
$suffix = (string)($this->suffix === null ? $manager->suffix : $this->suffix);
$pathInfo = $request->getPathInfo();
$normalized = false;
if ($this->hasNormalizer($manager)) {
$pathInfo = $this->getNormalizer($manager)->normalizePathInfo($pathInfo, $suffix, $normalized);
}
if ($suffix !== '' && $pathInfo !== '') {
$n = strlen($suffix);
if (substr_compare($pathInfo, $suffix, -$n, $n) === 0) {
$pathInfo = substr($pathInfo, 0, -$n);
if ($pathInfo === '') {
// suffix alone is not allowed
return false;
}
} else {
return false;
}
}
if ($this->host !== null) {
$pathInfo = strtolower($request->getHostInfo()) . ($pathInfo === '' ? '' : '/' . $pathInfo);
}
if (!preg_match($this->pattern, $pathInfo, $matches)) {
return false;
}
$matches = $this->substitutePlaceholderNames($matches);
foreach ($this->defaults as $name => $value) {
if (!isset($matches[$name]) || $matches[$name] === '') {
$matches[$name] = $value;
}
}
$params = $this->defaults;
$tr = [];
foreach ($matches as $name => $value) {
if (isset($this->_routeParams[$name])) {
$tr[$this->_routeParams[$name]] = $value;
unset($params[$name]);
} elseif (isset($this->_paramRules[$name])) {
$params[$name] = $value;
}
}
if ($this->_routeRule !== null) {
$route = strtr($this->route, $tr);
} else {
$route = $this->route;
}
Yii::trace("Request parsed with URL rule: {$this->name}", __METHOD__);
12 years ago
if ($normalized) {
// pathInfo was changed by normalizer - we need also normalize route
return $this->getNormalizer($manager)->normalizeRoute([$route, $params]);
} else {
return [$route, $params];
}
}
12 years ago
/**
* Creates a URL according to the given route and parameters.
* @param UrlManager $manager the URL manager
* @param string $route the route. It should not have slashes at the beginning or the end.
* @param array $params the parameters
* @return string|bool the created URL, or `false` if this rule cannot be used for creating this URL.
*/
public function createUrl($manager, $route, $params)
{
if ($this->mode === self::PARSING_ONLY) {
return false;
}
12 years ago
$tr = [];
12 years ago
// match the route part first
if ($route !== $this->route) {
if ($this->_routeRule !== null && preg_match($this->_routeRule, $route, $matches)) {
$matches = $this->substitutePlaceholderNames($matches);
foreach ($this->_routeParams as $name => $token) {
if (isset($this->defaults[$name]) && strcmp($this->defaults[$name], $matches[$name]) === 0) {
$tr[$token] = '';
} else {
$tr[$token] = $matches[$name];
}
}
} else {
return false;
}
}
12 years ago
// match default params
// if a default param is not in the route pattern, its value must also be matched
foreach ($this->defaults as $name => $value) {
if (isset($this->_routeParams[$name])) {
continue;
}
if (!isset($params[$name])) {
// allow omit empty optional params
// @see https://github.com/yiisoft/yii2/issues/10970
if (in_array($name, $this->placeholders) && strcmp($value, '') === 0) {
$params[$name] = '';
} else {
return false;
}
}
if (strcmp($params[$name], $value) === 0) { // strcmp will do string conversion automatically
unset($params[$name]);
if (isset($this->_paramRules[$name])) {
$tr["<$name>"] = '';
}
} elseif (!isset($this->_paramRules[$name])) {
return false;
}
}
12 years ago
// match params in the pattern
foreach ($this->_paramRules as $name => $rule) {
if (isset($params[$name]) && !is_array($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) {
$tr["<$name>"] = $this->encodeParams ? urlencode($params[$name]) : $params[$name];
unset($params[$name]);
} elseif (!isset($this->defaults[$name]) || isset($params[$name])) {
return false;
}
}
12 years ago
$url = $this->trimSlashes(strtr($this->_template, $tr));
if ($this->host !== null) {
$pos = strpos($url, '/', 8);
if ($pos !== false) {
$url = substr($url, 0, $pos) . preg_replace('#/+#', '/', substr($url, $pos));
}
} elseif (strpos($url, '//') !== false) {
$url = preg_replace('#/+#', '/', $url);
}
12 years ago
if ($url !== '') {
$url .= ($this->suffix === null ? $manager->suffix : $this->suffix);
}
if (!empty($params) && ($query = http_build_query($params)) !== '') {
$url .= '?' . $query;
}
return $url;
}
/**
* Returns list of regex for matching parameter.
* @return array parameter keys and regexp rules.
*
* @since 2.0.6
*/
protected function getParamRules()
{
return $this->_paramRules;
}
/**
* Iterates over [[placeholders]] and checks whether each placeholder exists as a key in $matches array.
* When found - replaces this placeholder key with a appropriate name of matching parameter.
* Used in [[parseRequest()]], [[createUrl()]].
*
* @param array $matches result of `preg_match()` call
* @return array input array with replaced placeholder keys
* @see placeholders
* @since 2.0.7
*/
protected function substitutePlaceholderNames(array $matches)
{
foreach ($this->placeholders as $placeholder => $name) {
if (isset($matches[$placeholder])) {
$matches[$name] = $matches[$placeholder];
unset($matches[$placeholder]);
}
}
return $matches;
}
/**
* Trim slashes in passed string. If string begins with '//', two slashes are left as is
* in the beginning of a string.
*
* @param string $string
* @return string
*/
private function trimSlashes($string) {
if (strpos($string, '//') === 0) {
return '//' . trim($string, '/');
}
return trim($string, '/');
}
}