diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 3c1c369fd2..0e9b088038 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -30,6 +30,7 @@ Yii Framework 2 Change Log - Bug #9915: `yii\helpers\ArrayHelper::getValue()` was erroring instead of returning `null` for non-existing object properties (totaldev, samdark) - Bug #9924: Fixed `yii.js` handleAction corrupted parameter values containing quote (") character (silverfire) - Bug #9984: Fixed wrong captcha color in case Imagick is used (DrDeath72) +- Bug #9999: Fixed `yii\web\UrlRule` to allow route parameter names with `-`, `_`, `.`characters (silverfire) - Bug #10029: Fixed MaskedInput not working with PJAX (martrix78, samdark) - Bug: Fixed generation of canonical URLs for `ViewAction` pages (samdark) - Enh #3506: Added `\yii\validators\IpValidator` to perform validation of IP addresses and subnets (SilverFire, samdark) diff --git a/framework/web/UrlRule.php b/framework/web/UrlRule.php index c0ba9ed295..daabcac144 100644 --- a/framework/web/UrlRule.php +++ b/framework/web/UrlRule.php @@ -104,6 +104,18 @@ class UrlRule extends Object implements UrlRuleInterface * @var array list of parameters used in the route. */ private $_routeParams = []; + /** + * @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. + * key - placeholder + * value - original name + * + * @see parseRequest() + * @see createUrl() + * @since 2.0.7 + */ + private $_placeholders = []; /** @@ -151,7 +163,7 @@ class UrlRule extends Object implements UrlRuleInterface $this->pattern = '/' . $this->pattern . '/'; } - if (strpos($this->route, '<') !== false && preg_match_all('/<(\w+)>/', $this->route, $matches)) { + if (strpos($this->route, '<') !== false && preg_match_all('/<([\w._-]+)>/', $this->route, $matches)) { foreach ($matches[1] as $name) { $this->_routeParams[$name] = "<$name>"; } @@ -166,31 +178,34 @@ class UrlRule extends Object implements UrlRuleInterface '(' => '\\(', ')' => '\\)', ]; + $tr2 = []; - if (preg_match_all('/<(\w+):?([^>]+)?>/', $this->pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { + 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<$name>$pattern))?"; + $tr["/<$name>"] = "(/(?P<$placeholder>$pattern))?"; } else { - $tr["<$name>"] = "(?P<$name>$pattern)?"; + $tr["<$name>"] = "(?P<$placeholder>$pattern)?"; } } else { - $tr["<$name>"] = "(?P<$name>$pattern)"; + $tr["<$name>"] = "(?P<$placeholder>$pattern)"; } if (isset($this->_routeParams[$name])) { - $tr2["<$name>"] = "(?P<$name>$pattern)"; + $tr2["<$name>"] = "(?P<$placeholder>$pattern)"; } else { $this->_paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#u"; } } } - $this->_template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); + $this->_template = preg_replace('/<([\w._-]+):?([^>]+)?>/', '<$1>', $this->pattern); $this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u'; if (!empty($this->_routeParams)) { @@ -216,7 +231,7 @@ class UrlRule extends Object implements UrlRuleInterface } $pathInfo = $request->getPathInfo(); - $suffix = (string) ($this->suffix === null ? $manager->suffix : $this->suffix); + $suffix = (string)($this->suffix === null ? $manager->suffix : $this->suffix); if ($suffix !== '' && $pathInfo !== '') { $n = strlen($suffix); if (substr_compare($pathInfo, $suffix, -$n, $n) === 0) { @@ -237,6 +252,8 @@ class UrlRule extends Object implements UrlRuleInterface 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; @@ -281,6 +298,7 @@ class UrlRule extends Object implements UrlRuleInterface // 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] = ''; @@ -352,4 +370,34 @@ class UrlRule extends Object implements UrlRuleInterface { 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 + */ + private function substitutePlaceholderNames (array $matches) { + foreach ($this->_placeholders as $placeholder => $name) { + if (isset($matches[$placeholder])) { + $matches[$name] = $matches[$placeholder]; + unset($matches[$placeholder]); + } + } + return $matches; + } + + /** + * Returns list of placeholders and original names for matching parameters. + * @return array + * @since 2.0.7 + * @see _placeholders + */ + protected function getPlaceholders() { + return $this->_placeholders; + } +} \ No newline at end of file