8000 [Routing] Support UTF-8 in paths and parameters · symfony/symfony@4be2ca0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4be2ca0

Browse files
committed
[Routing] Support UTF-8 in paths and parameters
1 parent 64ace10 commit 4be2ca0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+381
-206
lines changed

UPGRADE-4.0.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ HttpKernel
132132
have your own `ControllerResolverInterface` implementation, you should
133133
inject an `ArgumentResolverInterface` instance.
134134

135+
136+
Router
137+
------
138+
* URLs are now assumed to be encoded in UTF-8. Regular expressions for route
139+
requirements now use the PCRE_UTF8 flag to allow matching non-ASCII
140+
characters. To support legacy URLs in other encodings, the default encoding
141+
can be overridden using the `charset` option.
142+
135143
Serializer
136144
----------
137145

src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,12 @@ protected function execute(InputInterface $input, OutputInterface $output)
9090
$context->setHost($host);
9191
}
9292

93-
$matcher = new TraceableUrlMatcher($router->getRouteCollection(), $context);
93+
$pathInfo = $input->getArgument('path_info');
9494

95-
$traces = $matcher->getTraces($input->getArgument('path_info'));
95+
$charset = mb_detect_encoding($pathInfo, null, true);
96+
$matcher = new TraceableUrlMatcher($router->getRouteCollection(), $context, $charset);
97+
98+
$traces = $matcher->getTraces($pathInfo);
9699

97100
$io->newLine();
98101

src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<parameter key="router.options.matcher_dumper_class">Symfony\Component\Routing\Matcher\Dumper\PhpMatcherDumper</parameter>
1414
<parameter key="router.options.matcher.cache_class">%router.cache_class_prefix%UrlMatcher</parameter>
1515
<parameter key="router.options.generator.cache_class">%router.cache_class_prefix%UrlGenerator</parameter>
16+
<parameter key="router.options.charset">%kernel.charset%</parameter>
1617
<parameter key="router.request_context.host">localhost</parameter>
1718
<parameter key="router.request_context.scheme">http</parameter>
1819
<parameter key="router.request_context.base_url"></parameter>
@@ -66,6 +67,7 @@
6667
<argument key="matcher_base_class">%router.options.matcher_base_class%</argument>
6768
<argument key="matcher_dumper_class">%router.options.matcher_dumper_class%</argument>
6869
<argument key="matcher_cache_class">%router.options.matcher.cache_class%</argument>
70+
<argument key="charset">%router.options.charset%</argument>
6971
</argument>
7072
<argument type="service" id="router.request_context" on-invalid="ignore" />
7173
<argument type="service" id="logger" on-invalid="ignore" />

src/Symfony/Bundle/FrameworkBundle/Routing/Router.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ public function __construct(ContainerInterface $container, $resource, array $opt
4242

4343
$this->resource = $resource;
4444
$this->context = $context ?: new RequestContext();
45+
46+
if (array_key_exists('charset', $options)) {
47+
try {
48+
$this->setOption('charset', $options['charset']);
49+
} catch (\InvalidArgumentException $e) {
50+
// symfony/routing version 3.x does not support the charset option.
51+
unset($options['charset']);
52+
}
53+
}
54+
4555
$this->setOptions($options);
4656
}
4757

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,7 @@ protected function createContainer(array $data = array())
639639
'kernel.bundles' => array('FrameworkBundle' => 'Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'),
640640
'kernel.cache_dir' => __DIR__,
641641
'kernel.debug' => false,
642+
'kernel.charset' => 'UTF-8',
642643
'kernel.environment' => 'test',
643644
'kernel.name' => 'kernel',
644645
'kernel.root_dir' => __DIR__,

src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"path": "\/hello\/{name}",
3-
"pathRegex": "#^\/hello(?:\/(?P<name>[a-z]+))?$#s",
3+
"pathRegex": "#^\/hello(?:\/(?P<name>[a-z]+))?$#us",
44
"host": "localhost",
5-
"hostRegex": "#^localhost$#si",
5+
"hostRegex": "#^localhost$#usi",
66
"scheme": "http|https",
77
"method": "GET|HEAD",
88
"class": "Symfony\\Component\\Routing\\Route",

src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
- Path: /hello/{name}
2-
- Path Regex: #^/hello(?:/(?P<name>[a-z]+))?$#s
2+
- Path Regex: #^/hello(?:/(?P<name>[a-z]+))?$#us
33
- Host: localhost
4-
- Host Regex: #^localhost$#si
4+
- Host Regex: #^localhost$#usi
55
- Scheme: http|https
66
- Method: GET|HEAD
77
- Class: Symfony\Component\Routing\Route

src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
+--------------+---------------------------------------------------------+
44
| Route Name | |
55
| Path | /hello/{name} |
6-
| Path Regex | #^/hello(?:/(?P<name>[a-z]+))?$#s |
6+
| Path Regex | #^/hello(?:/(?P<name>[a-z]+))?$#us |
77
| Host | localhost |
8-
| Host Regex | #^localhost$#si |
8+
| Host Regex | #^localhost$#usi |
99
| Scheme | http|https |
1010
| Method | GET|HEAD |
1111
| Requirements | name: [a-z]+ |

src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<route class="Symfony\Component\Routing\Route">
3-
<path regex="#^/hello(?:/(?P&lt;name&gt;[a-z]+))?$#s">/hello/{name}</path>
4-
<host regex="#^localhost$#si">localhost</host>
3+
<path regex="#^/hello(?:/(?P&lt;name&gt;[a-z]+))?$#us">/hello/{name}</path>
4+
<host regex="#^localhost$#usi">localhost</host>
55
<scheme>http</scheme>
66
<scheme>https</scheme>
77
<method>GET</method>

src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"path": "\/name\/add",
3-
"pathRegex": "#^\/name\/add$#s",
3+
"pathRegex": "#^\/name\/add$#us",
44
"host": "localhost",
5-
"hostRegex": "#^localhost$#si",
5+
"hostRegex": "#^localhost$#usi",
66
"scheme": "http|https",
77
"method": "PUT|POST",
88
"class": "Symfony\\Component\\Routing\\Route",

src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
- Path: /name/add
2-
- Path Regex: #^/name/add$#s
2+
- Path Regex: #^/name/add$#us
33
- Host: localhost
4-
- Host Regex: #^localhost$#si
4+
- Host Regex: #^localhost$#usi
55
- Scheme: http|https
66
- Method: PUT|POST
77
- Class: Symfony\Component\Routing\Route

src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
+--------------+---------------------------------------------------------+
44
| Route Name | |
55
| Path | /name/add |
6-
| Path Regex | #^/name/add$#s |
6+
| Path Regex | #^/name/add$#us |
77
| Host | localhost |
8-
| Host Regex | #^localhost$#si |
8+
| Host Regex | #^localhost$#usi |
99
| Scheme | http|https |
1010
| Method | PUT|POST |
1111
| Requirements | NO CUSTOM |

src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<route class="Symfony\Component\Routing\Route">
3-
<path regex="#^/name/add$#s">/name/add</path>
4-
<host regex="#^localhost$#si">localhost</host>
3+
<path regex="#^/name/add$#us">/name/add</path>
4+
<host regex="#^localhost$#usi">localhost</host>
55
<scheme>http</scheme>
66
<scheme>https</scheme>
77
<method>PUT</method>

src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"route_1": {
33
"path": "\/hello\/{name}",
4-
"pathRegex": "#^\/hello(?:\/(?P<name>[a-z]+))?$#s",
4+
"pathRegex": "#^\/hello(?:\/(?P<name>[a-z]+))?$#us",
55
"host": "localhost",
6-
"hostRegex": "#^localhost$#si",
6+
"hostRegex": "#^localhost$#usi",
77
"scheme": "http|https",
88
"method": "GET|HEAD",
99
"class": "Symfony\\Component\\Routing\\Route",
@@ -21,9 +21,9 @@
2121
},
2222
"route_2": {
2323
"path": "\/name\/add",
24-
"pathRegex": "#^\/name\/add$#s",
24+
"pathRegex": "#^\/name\/add$#us",
2525
"host": "localhost",
26-
"hostRegex": "#^localhost$#si",
26+
"hostRegex": "#^localhost$#usi",
2727
"scheme": "http|https",
2828
"method": "PUT|POST",
2929
"class": "Symfony\\Component\\Routing\\Route",

src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ route_1
22
-------
33

44
- Path: /hello/{name}
5-
- Path Regex: #^/hello(?:/(?P<name>[a-z]+))?$#s
5+
- Path Regex: #^/hello(?:/(?P<name>[a-z]+))?$#us
66
- Host: localhost
7-
- Host Regex: #^localhost$#si
7+
- Host Regex: #^localhost$#usi
88
- Scheme: http|https
99
- Method: GET|HEAD
1010
- Class: Symfony\Component\Routing\Route
@@ -22,9 +22,9 @@ route_2
2222
-------
2323

2424
- Path: /name/add
25-
- Path Regex: #^/name/add$#s
25+
- Path Regex: #^/name/add$#us
2626
- Host: localhost
27-
- Host Regex: #^localhost$#si
27+
- Host Regex: #^localhost$#usi
2828
- Scheme: http|https
2929
- Method: PUT|POST
3030
- Class: Symfony\Component\Routing\Route

src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_collection_1.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<routes>
33
<route name="route_1" class="Symfony\Component\Routing\Route">
4-
<path regex="#^/hello(?:/(?P&lt;name&gt;[a-z]+))?$#s">/hello/{name}</path>
5-
<host regex="#^localhost$#si">localhost</host>
4+
<path regex="#^/hello(?:/(?P&lt;name&gt;[a-z]+))?$#us">/hello/{name}</path>
5+
<host regex="#^localhost$#usi">localhost</host>
66
<scheme>http</scheme>
77
<scheme>https</scheme>
88
<method>GET</method>
@@ -20,8 +20,8 @@
2020
</options>
2121
</route>
2222
<route name="route_2" class="Symfony\Component\Routing\Route">
23-
<path regex="#^/name/add$#s">/name/add</path>
24-
<host regex="#^localhost$#si">localhost</host>
23+
<path regex="#^/name/add$#us">/name/add</path>
24+
<host regex="#^localhost$#usi">localhost</host>
2525
<scheme>http</scheme>
2626
<scheme>https</scheme>
2727
<method>PUT</method>

src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public function testRedirectWhenNoSlash()
2323
$coll = new RouteCollection();
2424
$coll->add('foo', new Route('/foo/'));
2525

26-
$matcher = new RedirectableUrlMatcher($coll, $context = new RequestContext());
26+
$matcher = new RedirectableUrlMatcher($coll, $context = new RequestContext(), 'UTF-8');
2727

2828
$this->assertEquals(array(
2929
'_controller' => 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction',
@@ -43,7 +43,7 @@ public function testSchemeRedirect()
4343
$coll = new RouteCollection();
4444
$coll->add('foo', new Route('/foo', array(), array(), array(), '', array('https')));
4545

46-
$matcher = new RedirectableUrlMatcher($coll, $context = new RequestContext());
46+
$matcher = new RedirectableUrlMatcher($coll, $context = new RequestContext(), 'UTF-8');
4747

4848
$this->assertEquals(array(
4949
'_controller' => 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction',

src/Symfony/Component/Routing/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
CHANGELOG
22
=========
33

4+
4.0.0
5+
-----
6+
* URLs are now assumed to be encoded in UTF-8. Regular expressions for route requirements now use
7+
the PCRE_UTF8 flag to allow matching non-ASCII characters. To support legacy URLs in other encodings,
8+
the default encoding can be overridden using the `charset` option.
9+
410
3.2.0
511
-----
612

src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,11 @@ class {$options['class']} extends {$options['base_class']}
5858
/**
5959
* Constructor.
6060
*/
61-
public function __construct(RequestContext \$context, LoggerInterface \$logger = null)
61+
public function __construct(RequestContext \$context, LoggerInterface \$logger = null, \$charset = 'UTF-8')
6262
{
6363
\$this->context = \$context;
6464
\$this->logger = \$logger;
65+
\$this->charset = \$charset;
6566
if (null === self::\$declaredRoutes) {
6667
self::\$declaredRoutes = {$this->generateDeclaredRoutes()};
6768
}

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

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
4747
*/
4848
protected $logger;
4949

50+
/**
51+
* The output encoding used when generating URLs (path, query string and fragment).
52+
*
53+
* @var string
54+
*/
55+
protected $charset;
56+
5057
/**
5158
* This array defines the characters (besides alphanumeric ones) that will not be percent-encoded in the path segment of the generated URL.
5259
*
@@ -82,11 +89,12 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
8289
* @param RequestContext $context The context
8390
* @param LoggerInterface|null $logger A logger instance
8491
*/
85-
public function __construct(RouteCollection $routes, RequestContext $context, LoggerInterface $ 10000 logger = null)
92+
public function __construct(RouteCollection $routes, RequestContext $context, LoggerInterface $logger = null, $charset = 'UTF-8')
8693
{
8794
$this->routes = $routes;
8895
$this->context = $context;
8996
$this->logger = $logger;
97+
$this->charset = $charset;
9098
}
9199

92100
/**
@@ -158,7 +166,7 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa
158166
if ('variable' === $token[0]) {
159167
if (!$optional || !array_key_exists($token[3], $defaults) || null !== $mergedParams[$token[3]] && (string) $mergedParams[$token[3]] !== (string) $defaults[$token[3]]) {
160168
// check requirement
161-
if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#', $mergedParams[$token[3]])) {
169+
if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#u', $mergedParams[$token[3]])) {
162170
if ($this->strictRequirements) {
163171
throw new InvalidParameterException(strtr($message, array('{parameter}' => $token[3], '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$token[3]])));
164172
}
@@ -185,7 +193,7 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa
185193
}
186194

187195
// the contexts base URL is already encoded (see Symfony\Component\HttpFoundation\Request)
188-
$url = strtr(rawurlencode($url), $this->decodedChars);
196+
$url = strtr(rawurlencode($this->fromUtf8($url)), $this->decodedChars);
189197

190198
// the path segments "." and ".." are interpreted as relative reference when resolving a URI; see http://tools.ietf.org/html/rfc3986#section-3.3
191199
// so we need to encode them as they are not used for this purpose here
@@ -266,19 +274,41 @@ protected function doGenerate($variables, $defaults, $requirements, $tokens, $pa
266274
$fragment = isset($extra['_fragment']) ? $extra['_fragment'] : '';
267275
unset($extra['_fragment']);
268276

269-
if ($extra && $query = http_build_query($extra, '', '&')) {
270-
// "/" and "?" can be left decoded for better user experience, see
277+
// ignore null values
278+
$extra = array_filter($extra, function($value) { return isset($value); });
279+
280+
if ($extra) {
281+
if ($this->charset != 'UTF-8') {
282+
$extra = array_combine(
283+
array_map(array($this, 'fromUtf8'), array_keys($extra)),
284+
array_map(array($this, 'fromUtf8'), $extra)
285+
);
286+
}
287+
$query = http_build_query($extra, '', '&');
288+
// "/" and "?" can be left decoded for better user experience, see
271289
// http://tools.ietf.org/html/rfc3986#section-3.4
272290
$url .= '?'.strtr($query, array('%2F' => '/'));
273291
}
274292

275293
if ('' !== $fragment) {
276-
$url .= '#'.strtr(rawurlencode($fragment), array('%2F' => '/', '%3F' => '?'));
294+
$url .= '#'.strtr(rawurlencode($this->fromUtf8($fragment)), array('%2F' => '/', '%3F' => '?'));
277295
}
278296

279297
return $url;
280298
}
281299

300+
/**
301+
* Converts a string from UTF-8 to the encoding used in URLs.
302+
*/
303+
protected function fromUtf8($string)
304+
{
305+
if ($this->charset == 'UTF-8') {
306+
return $string;
307+
} else {
308+
return mb_convert_encoding($string, $this->charset, 'UTF-8');
309+
}
310+
}
311+
282312
/**
283313
* Returns the target path as relative reference from the base path.
284314
*

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ interface UrlGeneratorInterface extends RequestContextAwareInterface
7171
*
7272
* The special parameter _fragment will be used as the document fragment suffixed to the final URL.
7373
*
74+
* All parameters should be encoded in UTF-8 when passed to this function. The generated URL will be
75+
* encoding using the output encoding configured for this generator.
76+
*
7477
* @param string $name The name of the route
7578
* @param mixed $parameters An array of parameters
7679
* @param int $referenceType The type of reference to be generated (one of the constants)

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ class {$options['class']} extends {$options['base_class']}
7373
/**
7474
* Constructor.
7575
*/
76-
public function __construct(RequestContext \$context)
76+
public function __construct(RequestContext \$context, \$charset)
7777
{
7878
\$this->context = \$context;
79+
\$this->charset = \$charset;
7980
}
8081
8182
{$this->generateMatchMethod($supportsRedirections)}
@@ -104,7 +105,7 @@ private function generateMatchMethod($supportsRedirections)
104105
public function match(\$pathinfo)
105106
{
106107
\$allow = array();
107-
\$pathinfo = rawurldecode(\$pathinfo);
108+
\$pathinfo = \$this->toUtf8(rawurldecode(\$pathinfo));
108109
\$context = \$this->context;
109110
\$request = \$this->request;
110111

0 commit comments

Comments
 (0)
0