Skip to content

Commit

Permalink
merged branch vicb/routing_dumpers (PR #3858)
Browse files Browse the repository at this point in the history
Commits
-------

77185e0 [Routing] Allow spaces in the script name for the apache dumper
6465a69 [Routing] Fixes to handle spaces in route pattern

Discussion
----------

[Routing] Handling of space characters in the dumpers

The compiler was using the 'x' modifier in order to ignore extra spaces and line feeds but the code was flawed:

- it was actually ignoring all the spaces, not only the extra ones added by the compiler,
- all the spaces were stripped in the php and apache matchers.

The proposed fix:

- do not use the 'x' modifier any more (and then do no add extra spaces / line feeds),
- do not strip the spaces in the matchers,
- escapes the spaces (both in regexs and script name) for the apache matcher.

It also include [a small optimization](https://github.com/vicb/symfony/pull/new#L9L89) when the only token of a route is an optional variable token - the idea is to make the regex easier to read.

---------------------------------------------------------------------------

by vicb at 2012-04-10T13:59:45Z

@Baachi fixed now. Thanks.

---------------------------------------------------------------------------

by Tobion at 2012-04-10T16:01:31Z

+1, I saw no reason for pretty printing the regex in the first place (just for debugging I guess).
@vicb since you want to make the regex easier to read, I propose the remove the `P` from the variable regex `?P<bar>`, which is not needed anymore in PHP 5.3 (and we only support PHP 5.3+ anyway).

---------------------------------------------------------------------------

by vicb at 2012-04-10T16:08:36Z

@Tobion could you make a PR to this branch for the named parameters ?

---------------------------------------------------------------------------

by Tobion at 2012-04-10T16:12:34Z

I can include it in #3754 because I'm about the add 2 more fixes to it anyway.
But when I proposed to apply these fixes to 2.0 Fabien rejected it. So not sure what branch you want me to apply this.

---------------------------------------------------------------------------

by vicb at 2012-04-10T16:25:38Z

May be the best is to put it on hold while I am reviewing your PRs. There are already enough changes, we'll make an other PR after all have been sorted out.

What's the difference between 3754 and 3810 ? (3810 + 3763 = 3754 ?)

---------------------------------------------------------------------------

by Tobion at 2012-04-10T16:39:32Z

Lol you forget to link the PR numbers. At first sight I thought it's some sort of mathematical riddle. Haha
#3810 is for 2.0 =  #3763 (already merged) + #3754 for master

---------------------------------------------------------------------------

by vicb at 2012-04-10T16:52:18Z

I didn't link on purpose... the question is if '=' means strictly or loosely equal (any diffs - beside master vs 2.0) ?

---------------------------------------------------------------------------

by Tobion at 2012-04-10T17:06:04Z

It just applies my changes to 2.0. Nothing more. So master still differs from 2.0 by the addional features that were already implemented (e.g. `RouteCollection->addCollection` with optional requirements and options). But since my changes are bug fixes (except the performance improvement in #3763 but that doesn't break anything and makes 2.0 easier to maintain) I thought they should go into 2.0 as well.

---------------------------------------------------------------------------

by vicb at 2012-04-10T17:14:27Z

@Tobion only bug fixes mean "only bug fixes". You should re-open a PR for 2.0 with "only bug fixes", you might want to wait for me to review 3754.

---------------------------------------------------------------------------

by Tobion at 2012-04-10T17:21:00Z

Without #3763 it's much harder to apply the bug fixes. And now that I found 2 more bugs which requiresome rewriting of the PhpMatcherDumper, I don't want to apply all the commits by hand again for 2.0...
  • Loading branch information
fabpot committed Apr 11, 2012
2 parents 57990cc + 77185e0 commit 8835357
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class ApacheMatcherDumper extends MatcherDumper
* @param array $options An array of options
*
* @return string A string to be used as Apache rewrite rules
*
* @throws \LogicException When the route regex is invalid
*/
public function dump(array $options = array())
{
Expand All @@ -39,15 +41,23 @@ public function dump(array $options = array())
'base_uri' => '',
), $options);

$options['script_name'] = self::escape($options['script_name'], ' ', '\\');

$rules = array("# skip \"real\" requests\nRewriteCond %{REQUEST_FILENAME} -f\nRewriteRule .* - [QSA,L]");
$methodVars = array();

foreach ($this->getRoutes()->all() as $name => $route) {
$compiledRoute = $route->compile();

// prepare the apache regex
$regex = preg_replace('/\?P<.+?>/', '', substr(str_replace(array("\n", ' '), '', $compiledRoute->getRegex()), 1, -3));
$regex = '^'.preg_quote($options['base_uri']).substr($regex, 1);
$regex = $compiledRoute->getRegex();
$delimiter = $regex[0];
$regexPatternEnd = strrpos($regex, $delimiter);
if (strlen($regex) < 2 || 0 === $regexPatternEnd) {
throw new \LogicException('The "%s" route regex "%s" is invalid', $name, $regex);
}
$regex = preg_replace('/\?P<.+?>/', '', substr($regex, 1, $regexPatternEnd - 1));
$regex = '^'.self::escape(preg_quote($options['base_uri']).substr($regex, 1), ' ', '\\');

$hasTrailingSlash = '/$' == substr($regex, -2) && '^/$' != $regex;

Expand All @@ -56,7 +66,6 @@ public function dump(array $options = array())
$variables[] = 'E=_ROUTING_'.$variable.':%'.($i + 1);
}
foreach ($route->getDefaults() as $key => $value) {
// todo: a more legit way to escape the value?
$variables[] = 'E=_ROUTING_'.$key.':'.strtr($value, array(
':' => '\\:',
'=' => '\\=',
Expand Down Expand Up @@ -112,4 +121,36 @@ public function dump(array $options = array())

return implode("\n\n", $rules)."\n";
}

/**
* Escapes a string.
*
* @param string $string The string to be escaped
* @param string $char The character to be escaped
* @param string $with The character to be used for escaping
*
* @return string The escaped string
*/
static private function escape($string, $char, $with)
{
$escaped = false;
$output = '';
foreach(str_split($string) as $symbol) {
if ($escaped) {
$output .= $symbol;
$escaped = false;
continue;
}
if ($symbol === $char) {
$output .= $with.$char;
continue;
}
if ($symbol === $with) {
$escaped = true;
}
$output .= $symbol;
}

return $output;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
$conditions = array();
$hasTrailingSlash = false;
$matches = false;
if (!count($compiledRoute->getVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#', str_replace(array("\n", ' '), '', $compiledRoute->getRegex()), $m)) {
if (!count($compiledRoute->getVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#', $compiledRoute->getRegex(), $m)) {
if ($supportsRedirections && substr($m['url'], -1) === '/') {
$conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true));
$hasTrailingSlash = true;
Expand All @@ -160,7 +160,7 @@ private function compileRoute(Route $route, $name, $supportsRedirections, $paren
$conditions[] = sprintf("0 === strpos(\$pathinfo, %s)", var_export($compiledRoute->getStaticPrefix(), true));
}

$regex = str_replace(array("\n", ' '), '', $compiledRoute->getRegex());
$regex = $compiledRoute->getRegex();
if ($supportsRedirections && $pos = strpos($regex, '/$')) {
$regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2);
$hasTrailingSlash = true;
Expand Down
66 changes: 42 additions & 24 deletions src/Symfony/Component/Routing/RouteCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,44 +66,62 @@ public function compile(Route $route)
// find the first optional token
$firstOptional = INF;
for ($i = count($tokens) - 1; $i >= 0; $i--) {
if ('variable' === $tokens[$i][0] && $route->hasDefault($tokens[$i][3])) {
$token = $tokens[$i];
if ('variable' === $token[0] && $route->hasDefault($token[3])) {
$firstOptional = $i;
} else {
break;
}
}

// compute the matching regexp
$regex = '';
$indent = 1;
if (1 === count($tokens) && 0 === $firstOptional) {
$token = $tokens[0];
++$indent;
$regex .= str_repeat(' ', $indent * 4).sprintf("%s(?:\n", preg_quote($token[1], '#'));
$regex .= str_repeat(' ', $indent * 4).sprintf("(?P<%s>%s)\n", $token[3], $token[2]);
} else {
foreach ($tokens as $i => $token) {
if ('text' === $token[0]) {
$regex .= str_repeat(' ', $indent * 4).preg_quote($token[1], '#')."\n";
} else {
if ($i >= $firstOptional) {
$regex .= str_repeat(' ', $indent * 4)."(?:\n";
++$indent;
}
$regex .= str_repeat(' ', $indent * 4).sprintf("%s(?P<%s>%s)\n", preg_quote($token[1], '#'), $token[3], $token[2]);
}
}
}
while (--$indent) {
$regex .= str_repeat(' ', $indent * 4).")?\n";
$regexp = '';
for ($i = 0, $nbToken = count($tokens); $i < $nbToken; $i++) {
$regexp .= $this->computeRegexp($tokens, $i, $firstOptional);
}

return new CompiledRoute(
$route,
'text' === $tokens[0][0] ? $tokens[0][1] : '',
sprintf("#^\n%s$#xs", $regex),
sprintf("#^%s$#s", $regexp),
array_reverse($tokens),
$variables
);
}

/**
* Computes the regexp used to match the token.
*
* @param array $tokens The route tokens
* @param integer $index The index of the current token
* @param integer $firstOptional The index of the first optional token
*
* @return string The regexp
*/
private function computeRegexp(array $tokens, $index, $firstOptional)
{
$token = $tokens[$index];
if('text' === $token[0]) {
// Text tokens
return preg_quote($token[1], '#');
} else {
// Variable tokens
if (0 === $index && 0 === $firstOptional && 1 == count($tokens)) {
// When the only token is an optional variable token, the separator is required
return sprintf('%s(?P<%s>%s)?', preg_quote($token[1], '#'), $token[3], $token[2]);
} else {
$nbTokens = count($tokens);
$regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1], '#'), $token[3], $token[2]);
if ($index >= $firstOptional) {
// Enclose each optional tokens in a subpattern to make it optional
$regexp = "(?:$regexp";
if ($nbTokens - 1 == $index) {
// Close the optional subpatterns
$regexp .= str_repeat(")?", $nbTokens - $firstOptional);
}
}
return $regexp;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz5,E=_ROUTING_foo:%1]
RewriteCond %{REQUEST_URI} ^/test/baz$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz6,E=_ROUTING_foo:bar\ baz]

# baz7
RewriteCond %{REQUEST_URI} ^/te\ st/baz$
RewriteRule .* app.php [QSA,L,E=_ROUTING__route:baz7]

# 405 Method Not Allowed
RewriteCond %{_ROUTING__allow_GET} !-z [OR]
RewriteCond %{_ROUTING__allow_HEAD} !-z [OR]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ public function match($pathinfo)
$pathinfo = urldecode($pathinfo);

// foo
if (0 === strpos($pathinfo, '/foo') && preg_match('#^/foo/(?P<bar>baz|symfony)$#xs', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/foo') && preg_match('#^/foo/(?P<bar>baz|symfony)$#s', $pathinfo, $matches)) {
return array_merge($this->mergeDefaults($matches, array ( 'def' => 'test',)), array('_route' => 'foo'));
}

// bar
if (0 === strpos($pathinfo, '/bar') && preg_match('#^/bar/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/bar') && preg_match('#^/bar/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
$allow = array_merge($allow, array('GET', 'HEAD'));
goto not_bar;
Expand All @@ -42,7 +42,7 @@ public function match($pathinfo)
not_bar:

// barhead
if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
$allow = array_merge($allow, array('GET', 'HEAD'));
goto not_barhead;
Expand All @@ -68,13 +68,13 @@ public function match($pathinfo)
}

// baz4
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/$#xs', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/$#s', $pathinfo, $matches)) {
$matches['_route'] = 'baz4';
return $matches;
}

// baz5
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/$#xs', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/$#s', $pathinfo, $matches)) {
if ($this->context->getMethod() != 'POST') {
$allow[] = 'POST';
goto not_baz5;
Expand All @@ -85,7 +85,7 @@ public function match($pathinfo)
not_baz5:

// baz.baz6
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/$#xs', $pathinfo, $matches)) {
if (0 === strpos($pathinfo, '/test') && preg_match('#^/test/(?P<foo>[^/]+?)/$#s', $pathinfo, $matches)) {
if ($this->context->getMethod() != 'PUT') {
$allow[] = 'PUT';
goto not_bazbaz6;
Expand All @@ -101,33 +101,38 @@ public function match($pathinfo)
}

// quoter
if (preg_match('#^/(?P<quoter>[\']+)$#xs', $pathinfo, $matches)) {
if (preg_match('#^/(?P<quoter>[\']+)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'quoter';
return $matches;
}

// space
if ($pathinfo === '/spa ce') {
return array('_route' => 'space');
}

if (0 === strpos($pathinfo, '/a')) {
if (0 === strpos($pathinfo, '/a/b\'b')) {
// foo1
if (preg_match('#^/a/b\'b/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
if (preg_match('#^/a/b\'b/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'foo1';
return $matches;
}

// bar1
if (preg_match('#^/a/b\'b/(?P<bar>[^/]+?)$#xs', $pathinfo, $matches)) {
if (preg_match('#^/a/b\'b/(?P<bar>[^/]+?)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'bar1';
return $matches;
}

// foo2
if (preg_match('#^/a/b\'b/(?P<foo1>[^/]+?)$#xs', $pathinfo, $matches)) {
if (preg_match('#^/a/b\'b/(?P<foo1>[^/]+?)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'foo2';
return $matches;
}

// bar2
if (preg_match('#^/a/b\'b/(?P<bar1>[^/]+?)$#xs', $pathinfo, $matches)) {
if (preg_match('#^/a/b\'b/(?P<bar1>[^/]+?)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'bar2';
return $matches;
}
Expand All @@ -145,21 +150,21 @@ public function match($pathinfo)
}

// foo4
if (preg_match('#^/aba/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
if (preg_match('#^/aba/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'foo4';
return $matches;
}

}

// foo3
if (preg_match('#^/(?P<_locale>[^/]+?)/b/(?P<foo>[^/]+?)$#xs', $pathinfo, $matches)) {
if (preg_match('#^/(?P<_locale>[^/]+?)/b/(?P<foo>[^/]+?)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'foo3';
return $matches;
}

// bar3
if (preg_match('#^/(?P<_locale>[^/]+?)/b/(?P<bar>[^/]+?)$#xs', $pathinfo, $matches)) {
if (preg_match('#^/(?P<_locale>[^/]+?)/b/(?P<bar>[^/]+?)$#s', $pathinfo, $matches)) {
$matches['_route'] = 'bar3';
return $matches;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# skip "real" requests
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule .* - [QSA,L]

# foo
RewriteCond %{REQUEST_URI} ^/foo$
RewriteRule .* ap\ p_d\ ev.php [QSA,L,E=_ROUTING__route:foo]

0 comments on commit 8835357

Please sign in to comment.