8000 [Routing] Add seamless support for unicode requirements · symfony/symfony@6385b48 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6385b48

Browse files
[Routing] Add seamless support for unicode requirements
1 parent 904279e commit 6385b48

File tree

6 files changed

+99
-13
lines changed

6 files changed

+99
-13
lines changed

src/Symfony/Component/Routing/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
-----
66

77
* Added support for `bool`, `int`, `float`, `string`, `list` and `map` defaults in XML configurations.
8+
* Added support for unicode requirements
89

910
2.8.0
1011
-----

src/Symfony/Component/Routing/Generator/UrlGenerator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa
158158
if ('variable' === $token[0]) {
159159
if (!$optional || !array_key_exists($token[3], $defaults) || null !== $mergedParams[$token[3]] && (string) $mergedParams[$token[3]] !== (string) $defaults[$token[3]]) {
160160
// check requirement
161-
if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#', $mergedParams[$token[3]])) {
161+
if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#'.(empty($token[4]) ? '' : 'u'), $mergedParams[$token[3]])) {
162162
if ($this->strictRequirements) {
163163
throw new InvalidParameterException(strtr($message, array('{parameter}' => $token[3], '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$token[3]])));
164164
}
@@ -212,7 +212,7 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa
212212
$routeHost = '';
213213
foreach ($hostTokens as $token) {
214214
if ('variable' === $token[0]) {
215-
if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#i', $mergedParams[$token[3]])) {
215+
if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#i'.(empty($token[4]) ? '' : 'u'), $mergedParams[$token[3]])) {
216216
if ($this->strictRequirements) {
217217
throw new InvalidParameterException(strtr($message, array('{parameter}' => $token[3], '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$token[3]])));
218218
}

src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,9 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
223223
}
224224

225225
$supportsTrailingSlash = $supportsRedirections && (!$meth 6D40 ods || in_array('HEAD', $methods));
226+
$regex = $compiledRoute->getRegex();
226227

227-
if (!count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#', $compiledRoute->getRegex(), $m)) {
228+
if (!count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#'.(substr($regex, -1) === 'u' ? 'u' : ''), $regex, $m)) {
228229
if ($supportsTrailingSlash && substr($m['url'], -1) === '/') {
229230
$conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true));
230231
$hasTrailingSlash = true;
@@ -236,7 +237,6 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
236237
$conditions[] = sprintf('0 === strpos($pathinfo, %s)', var_export($compiledRoute->getStaticPrefix(), true));
237238
}
238239

239-
$regex = $compiledRoute->getRegex();
240240
if ($supportsTrailingSlash && $pos = strpos($regex, '/$')) {
241241
$regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2);
242242
$hasTrailingSlash = true;

src/Symfony/Component/Routing/RouteCompiler.php

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ private static function compilePattern(Route $route, $pattern, $isHost)
9191
$matches = array();
9292
$pos = 0;
9393
$defaultSeparator = $isHost ? '.' : '/';
94+
$useUnicode = preg_match('//u', $pattern);
9495

9596
// Match all variables enclosed in "{}" and iterate over them. But we only want to match the innermost variable
9697
// in case of nested "{}", e.g. {foo{bar}}. This in ensured because \w does not match "{" or "}" itself.
@@ -100,7 +101,15 @@ private static function compilePattern(Route $route, $pattern, $isHost)
100101
// get all static text preceding the current variable
101102
$precedingText = substr($pattern, $pos, $match[0][1] - $pos);
102103
$pos = $match[0][1] + strlen($match[0][0]);
103-
$precedingChar = strlen($precedingText) > 0 ? substr($precedingText, -1) : '';
104+
105+
if (!strlen($precedingText)) {
106+
$precedingChar = '';
107+
} elseif ($useUnicode) {
108+
preg_match('/.$/u', $precedingText, $precedingChar);
109+
$precedingChar = $precedingChar[0];
110+
} else {
111+
$precedingChar = substr($precedingText, -1);
112+
}
104113
$isSeparator = '' !== $precedingChar && false !== strpos(static::SEPARATORS, $precedingChar);
105114

106115
if (is_numeric($varName)) {
@@ -110,8 +119,8 @@ private static function compilePattern(Route $route, $pattern, $isHost)
110119
throw new \LogicException(sprintf('Route pattern "%s" cannot reference variable name "%s" more than once.', $pattern, $varName));
111120
}
112121

113-
if ($isSeparator && strlen($precedingText) > 1) {
114-
$tokens[] = array('text', substr($precedingText, 0, -1));
122+
if ($isSeparator && $precedingText !== $precedingChar) {
123+
$tokens[] = array('text', substr($precedingText, 0, -strlen($precedingChar)));
115124
} elseif (!$isSeparator && strlen($precedingText) > 0) {
116125
$tokens[] = array('text', $precedingText);
117126
}
@@ -126,7 +135,7 @@ private static function compilePattern(Route $route, $pattern, $isHost)
126135
// If {page} would also match the separating dot, {_format} would never match as {page} will eagerly consume everything.
127136
// Also even if {_format} was not optional the requirement prevents that {page} matches something that was originally
128137
// part of {_format} when generating the URL, e.g. _format = 'mobile.html'.
129-
$nextSeparator = self::findNextSeparator($followingPattern);
138+
$nextSeparator = self::findNextSeparator($followingPattern, $useUnicode);
130139
$regexp = sprintf(
131140
'[^%s%s]+',
132141
preg_quote($defaultSeparator, self::REGEX_DELIMITER),
@@ -140,6 +149,8 @@ private static function compilePattern(Route $route, $pattern, $isHost)
140149
// directly adjacent, e.g. '/{x}{y}'.
141150
$regexp .= '+';
142151
}
152+
} elseif (!preg_match('//u', $regexp)) {
153+
$useUnicode = false;
143154
}
144155

145156
$tokens[] = array('variable', $isSeparator ? $precedingChar : '', $regexp, $varName);
@@ -168,10 +179,21 @@ private static function compilePattern(Route $route, $pattern, $isHost)
168179
for ($i = 0, $nbToken = count($tokens); $i < $nbToken; ++$i) {
169180
$regexp .= self::computeRegexp($tokens, $i, $firstOptional);
170181
}
182+
$regexp = self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'s'.($isHost ? 'i' : '');
183+
184+
// don't enable Unicode matching if not really required
185+
if ($useUnicode && preg_match('/[\x80-\xFF]|(?:[^\\\\]\\\\(?:\\\\\\\\)*+(?-i:X|[pP][\{A-Za-z]|x[\{A-Fa-f0-9]))/', $regexp)) {
186+
$regexp .= 'u';
187+
for ($i = count($tokens) - 1; $i >= 0; --$i) {
188+
if ('variable' === $tokens[$i][0]) {
189+
$tokens[$i][] = true;
190+
}
191+
}
192+
}
171193

172194
return array(
173195
'staticPrefix' => 'text' === $tokens[0][0] ? $tokens[0][1] : '',
174-
'regex' => self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'s'.($isHost ? 'i' : ''),
196+
'regex' => $regexp,
175197
'tokens' => array_reverse($tokens),
176198
'variables' => $variables,
177199
);
@@ -180,20 +202,26 @@ private static function compilePattern(Route $route, $pattern, $isHost)
180202
/**
181203
* Returns the next static character in the Route pattern that will serve as a separator.
182204
*
183-
* @param string $pattern The route pattern
205+
* @param string $pattern The route pattern
206+
* @param bool $useUnicode Whether the character is encoded in unicode or not
184207
*
185208
* @return string The next static character that functions as separator (or empty string when none available)
186209
*/
187-
private static function findNextSeparator($pattern)
210+
private static function findNextSeparator($pattern, $useUnicode)
188211
{
189212
if ('' == $pattern) {
190213
// return empty string if pattern is empty or false (false which can be returned by substr)
191214
return '';
192215
}
193216
// first remove all placeholders from the pattern so we can find the next real static character
194-
$pattern = preg_replace('#\{\w+\}#', '', $pattern);
217+
if ('' === $pattern = preg_replace('#\{\w+\}#', '', $pattern)) {
218+
return '';
219+
}
220+
if ($useUnicode) {
221+
preg_match('/^./u', $pattern, $pattern);
222+
}
195223

196-
return isset($pattern[0]) && false !== strpos(static::SEPARATORS, $pattern[0]) ? $pattern[0] : '';
224+
return false !== strpos(static::SEPARATORS, $pattern[0]) ? $pattern[0] : '';
197225
}
198226

199227
/**

src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,15 @@ public function testGenerateForRouteWithInvalidMandatoryParameter()
233233
$this->getGenerator($routes)->generate('test', array('foo' => 'bar'), UrlGeneratorInterface::ABSOLUTE_URL);
234234
}
235235

236+
/**
237+
* @expectedException \Symfony\Component\Routing\Exception\InvalidParameterException
238+
*/
239+
public function testGenerateForRouteWithInvalidUnicodeParameter()
240+
{
241+
$routes = $this->getRoutes('test', new Route('/testing/{foo}', array(), array('foo' => '\pL+')));
242+
$this->getGenerator($routes)->generate('test', array('foo' => 'abc123'), UrlGeneratorInterface::ABSOLUTE_URL);
243+
}
244+
236245
/**
237246
* @expectedException \Symfony\Component\Routing\Exception\InvalidParameterException
238247
*/

src/Symfony/Component/Routing/Tests/RouteCompilerTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Routing\Tests;
1313

1414
use Symfony\Component\Routing\Route;
15+
use Symfony\Component\Routing\RouteCompiler;
1516

1617
class RouteCompilerTest extends \PHPUnit_Framework_TestCase
1718
{
@@ -162,6 +163,48 @@ public function provideCompileData()
162163
array('text', '/foo'),
163164
),
164165
),
166+
167+
array(
168+
'Static non unicode route',
169+
array("/fo\xE9"),
170+
"/fo\xE9", "#^/fo\xE9$#s", array(), array(
171+
array('text', "/fo\xE9"),
172+
),
173+
),
174+
175+
array(
176+
'Static unicode route',
177+
array('/foé'),
178+
'/foé', '#^/foé$#su', array(), array(
179+
array('text', '/foé'),
180+
),
181+
),
182+
183+
array(
184+
'Route with a unicode requirement',
185+
array('/{bar}', array('bar' => null), array('bar' => 'é')),
186+
'', '#^/(?P<bar>é)?$#su', array('bar'), array(
187+
array('variable', '/', 'é', 'bar', true),
188+
),
189+
),
190+
191+
array(
192+
'Route with a unicode class requirement',
193+
array('/{bar}', array('bar' => null), array('bar' => '\pM')),
194+
'', '#^/(?P<bar>\pM)?$#su', array('bar'), array(
195+
array('variable', '/', '\pM', 'bar', true),
196+
),
197+
),
198+
199+
array(
200+
'Route with a unicode separator',
201+
array('/foo/{bar}§{_format}', array(), array(), array('compiler_class' => UnicodeRouteCompiler::class)),
202+
'/foo', '#^/foo/(?P<bar>[^/§]++)§(?P<_format>[^/]++)$#su', array('bar', '_format'), array(
203+
array('variable', '§', '[^/]++', '_format', true),
204+
array('variable', '/', '[^/§]++', 'bar', true),
205+
array('text', '/foo'),
206+
),
207+
),
165208
);
166209
}
167210

@@ -275,3 +318,8 @@ public function provideCompileWithHostData()
275318
);
276319
}
277320
}
321+
322+
class UnicodeRouteCompiler extends RouteCompiler
323+
{
324+
const SEPARATORS = '';
325+
}

0 commit comments

Comments
 (0)
0