diff --git a/src/Symfony/Component/Routing/RouteCompiler.php b/src/Symfony/Component/Routing/RouteCompiler.php index 03d3e38644ef0..c65f975426812 100644 --- a/src/Symfony/Component/Routing/RouteCompiler.php +++ b/src/Symfony/Component/Routing/RouteCompiler.php @@ -54,6 +54,13 @@ public function compile(Route $route) $precedingChar = strlen($precedingText) > 0 ? substr($precedingText, -1) : ''; $isSeparator = '' !== $precedingChar && false !== strpos(static::SEPARATORS, $precedingChar); + if ('\\' === $precedingChar) { + // When the variable placeholder is excaped with "\", e.g. "static\{static}", treat it as static text (without the escape char). + // This allows "{static}" to be used in the route without being considered as variable. + $this->addTextToken($tokens, substr($precedingText, 0, -1) . $match[0][0]); + continue; + } + if (is_numeric($varName)) { throw new \DomainException(sprintf('Variable name "%s" cannot be numeric in route pattern "%s". Please use a different name.', $varName, $pattern)); } @@ -62,9 +69,9 @@ public function compile(Route $route) } if ($isSeparator && strlen($precedingText) > 1) { - $tokens[] = array('text', substr($precedingText, 0, -1)); + $this->addTextToken($tokens, substr($precedingText, 0, -1)); } elseif (!$isSeparator && strlen($precedingText) > 0) { - $tokens[] = array('text', $precedingText); + $this->addTextToken($tokens, $precedingText); } $regexp = $route->getRequirement($varName); @@ -94,7 +101,7 @@ public function compile(Route $route) } if ($pos < strlen($pattern)) { - $tokens[] = array('text', substr($pattern, $pos)); + $this->addTextToken($tokens, substr($pattern, $pos)); } // find the first optional token @@ -122,6 +129,22 @@ public function compile(Route $route) ); } + /** + * Adds a text token to the tokens array. + * + * @param array $tokens The route tokens + * @param string $text The static text + */ + private function addTextToken(array &$tokens, $text) + { + // when the last token is a text token, we can simply add the new text to it + if (false !== end($tokens) && 'text' === $tokens[key($tokens)][0]) { + $tokens[key($tokens)][1] .= $text; + } else { + $tokens[] = array('text', $text); + } + } + /** * Returns the next static character in the Route pattern that will serve as a separator. * @@ -135,8 +158,13 @@ private function findNextSeparator($pattern) // return empty string if pattern is empty or false (false which can be returned by substr) return ''; } - // first remove all placeholders from the pattern so we can find the next real static character - $pattern = preg_replace('#\{\w+\}#', '', $pattern); + + // first remove all (non-escaped) placeholders from the pattern so we can find the next real static character + $pattern = preg_replace('#(?getGenerator($routes)->generate('test', array('page' => 'do.t', '_format' => 'html')); } + public function testEscapedVariable() + { + $routes = $this->getRoutes('test', new Route('/{foo}\{static}{bar}')); + $generator = $this->getGenerator($routes); + + $this->assertSame('/app.php/foo%7Bstatic%7Dbar', $generator->generate('test', array('foo' => 'foo', 'bar' => 'bar'))); + } + protected function getGenerator(RouteCollection $routes, array $parameters = array(), $logger = null) { $context = new RequestContext('/app.php'); diff --git a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php index 0927c750443a3..93b3cea79734e 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php @@ -309,6 +309,15 @@ public function testDefaultRequirementOfVariableDisallowsNextSeparator() $matcher->match('/do.t.html'); } + public function testEscapedVariable() + { + $coll = new RouteCollection(); + $coll->add('test', new Route('/{foo}\{static}{bar}')); + $matcher = new UrlMatcher($coll, new RequestContext()); + + $this->assertEquals(array('foo' => 'foo', 'bar' => 'bar', '_route' => 'test'), $matcher->match('/foo{static}bar')); + } + /** * @expectedException Symfony\Component\Routing\Exception\ResourceNotFoundException */ diff --git a/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php b/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php index 1c87ca3257073..35d24474d1b32 100644 --- a/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php +++ b/src/Symfony/Component/Routing/Tests/RouteCompilerTest.php @@ -129,6 +129,20 @@ public function provideCompileData() array('text', '/{static'), )), + array( + 'Route with escaped variable as static text', + array('/\{static}'), + '/{static}', '#^/\{static\}$#s', array(), array( + array('text', '/{static}'), + )), + + array( + 'Route with escaped variable and a "\" preceding it', + array('/\\\\{static}'), // i.e. two backslashes + '/\{static}', '#^/\\\\\\{static\}$#s', array(), array( + array('text', '/\{static}'), + )), + array( 'Route without separator between variables', array('/{w}{x}{y}{z}.{_format}', array('z' => 'default-z', '_format' => 'html'), array('y' => '(y|Y)')), @@ -140,6 +154,17 @@ public function provideCompileData() array('variable', '/', '[^/\.]+', 'w'), )), + array( + 'Route without separator between variables + escaped variables', + array('/\{w}{x}\{z}{y}\\\\{z}.'), + '/{w}', '#^/\{w\}(?[^/]+)\{z\}(?[^/]+)\\\\\\{z\}\.$#s', array('x', 'y'), array( + array('text', '\{z}.'), + array('variable', '', '[^/]+', 'y'), + array('text', '{z}'), + array('variable', '', '[^/]+', 'x'), + array('text', '/{w}'), + )), + array( 'Route with a format', array('/foo/{bar}.{_format}'),