RedirectableUrlMatcherInterface.php000064400000001507150313414260013454 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Routing\Matcher; /** * RedirectableUrlMatcherInterface knows how to redirect the user. * * @author Fabien Potencier */ interface RedirectableUrlMatcherInterface { /** * Redirects the user to another URL. * * @param string $path The path info to redirect to * @param string $route The route name that matched * @param string|null $scheme The URL scheme (null to keep the current one) * * @return array An array of parameters */ public function redirect($path, $route, $scheme = null); } UrlMatcher.php000064400000021746150313414260007334 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Routing\Matcher; use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\NoConfigurationException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; /** * UrlMatcher matches URL based on a set of routes. * * @author Fabien Potencier */ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface { const REQUIREMENT_MATCH = 0; const REQUIREMENT_MISMATCH = 1; const ROUTE_MATCH = 2; protected $context; protected $allow = []; protected $routes; protected $request; protected $expressionLanguage; /** * @var ExpressionFunctionProviderInterface[] */ protected $expressionLanguageProviders = []; public function __construct(RouteCollection $routes, RequestContext $context) { $this->routes = $routes; $this->context = $context; } /** * {@inheritdoc} */ public function setContext(RequestContext $context) { $this->context = $context; } /** * {@inheritdoc} */ public function getContext() { return $this->context; } /** * {@inheritdoc} */ public function match($pathinfo) { $this->allow = []; if ($ret = $this->matchCollection(rawurldecode($pathinfo), $this->routes)) { return $ret; } if ('/' === $pathinfo && !$this->allow) { throw new NoConfigurationException(); } throw 0 < \count($this->allow) ? new MethodNotAllowedException(array_unique($this->allow)) : new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo)); } /** * {@inheritdoc} */ public function matchRequest(Request $request) { $this->request = $request; $ret = $this->match($request->getPathInfo()); $this->request = null; return $ret; } public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) { $this->expressionLanguageProviders[] = $provider; } /** * Tries to match a URL with a set of routes. * * @param string $pathinfo The path info to be parsed * @param RouteCollection $routes The set of routes * * @return array An array of parameters * * @throws NoConfigurationException If no routing configuration could be found * @throws ResourceNotFoundException If the resource could not be found * @throws MethodNotAllowedException If the resource was found but the request method is not allowed */ protected function matchCollection($pathinfo, RouteCollection $routes) { // HEAD and GET are equivalent as per RFC if ('HEAD' === $method = $this->context->getMethod()) { $method = 'GET'; } $supportsTrailingSlash = '/' !== $pathinfo && '' !== $pathinfo && $this instanceof RedirectableUrlMatcherInterface; foreach ($routes as $name => $route) { $compiledRoute = $route->compile(); $staticPrefix = $compiledRoute->getStaticPrefix(); $requiredMethods = $route->getMethods(); // check the static prefix of the URL first. Only use the more expensive preg_match when it matches if ('' === $staticPrefix || 0 === strpos($pathinfo, $staticPrefix)) { // no-op } elseif (!$supportsTrailingSlash || ($requiredMethods && !\in_array('GET', $requiredMethods)) || 'GET' !== $method) { continue; } elseif ('/' === substr($staticPrefix, -1) && substr($staticPrefix, 0, -1) === $pathinfo) { return $this->allow = []; } else { continue; } $regex = $compiledRoute->getRegex(); if ($supportsTrailingSlash && $pos = strpos($regex, '/$')) { $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2); $hasTrailingSlash = true; } else { $hasTrailingSlash = false; } if (!preg_match($regex, $pathinfo, $matches)) { continue; } if ($hasTrailingSlash && '/' !== substr($pathinfo, -1)) { if ((!$requiredMethods || \in_array('GET', $requiredMethods)) && 'GET' === $method) { return $this->allow = []; } continue; } $hostMatches = []; if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) { continue; } $status = $this->handleRouteRequirements($pathinfo, $name, $route); if (self::REQUIREMENT_MISMATCH === $status[0]) { continue; } // check HTTP method requirement if ($requiredMethods) { if (!\in_array($method, $requiredMethods)) { if (self::REQUIREMENT_MATCH === $status[0]) { $this->allow = array_merge($this->allow, $requiredMethods); } continue; } } return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, isset($status[1]) ? $status[1] : [])); } return []; } /** * Returns an array of values to use as request attributes. * * As this method requires the Route object, it is not available * in matchers that do not have access to the matched Route instance * (like the PHP and Apache matcher dumpers). * * @param Route $route The route we are matching against * @param string $name The name of the route * @param array $attributes An array of attributes from the matcher * * @return array An array of parameters */ protected function getAttributes(Route $route, $name, array $attributes) { $attributes['_route'] = $name; return $this->mergeDefaults($attributes, $route->getDefaults()); } /** * Handles specific route requirements. * * @param string $pathinfo The path * @param string $name The route name * @param Route $route The route * * @return array The first element represents the status, the second contains additional information */ protected function handleRouteRequirements($pathinfo, $name, Route $route) { // expression condition if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), ['context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)])) { return [self::REQUIREMENT_MISMATCH, null]; } // check HTTP scheme requirement $scheme = $this->context->getScheme(); $status = $route->getSchemes() && !$route->hasScheme($scheme) ? self::REQUIREMENT_MISMATCH : self::REQUIREMENT_MATCH; return [$status, null]; } /** * Get merged default parameters. * * @param array $params The parameters * @param array $defaults The defaults * * @return array Merged default parameters */ protected function mergeDefaults($params, $defaults) { foreach ($params as $key => $value) { if (!\is_int($key)) { $defaults[$key] = $value; } } return $defaults; } protected function getExpressionLanguage() { if (null === $this->expressionLanguage) { if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); } $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders); } return $this->expressionLanguage; } /** * @internal */ protected function createRequest($pathinfo) { if (!class_exists('Symfony\Component\HttpFoundation\Request')) { return null; } return Request::create($this->context->getScheme().'://'.$this->context->getHost().$this->context->getBaseUrl().$pathinfo, $this->context->getMethod(), $this->context->getParameters(), [], [], [ 'SCRIPT_FILENAME' => $this->context->getBaseUrl(), 'SCRIPT_NAME' => $this->context->getBaseUrl(), ]); } } RequestMatcherInterface.php000064400000002416150313414260012034 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Routing\Matcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\NoConfigurationException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; /** * RequestMatcherInterface is the interface that all request matcher classes must implement. * * @author Fabien Potencier */ interface RequestMatcherInterface { /** * Tries to match a request with a set of routes. * * If the matcher can not find information, it must throw one of the exceptions documented * below. * * @return array An array of parameters * * @throws NoConfigurationException If no routing configuration could be found * @throws ResourceNotFoundException If no matching resource could be found * @throws MethodNotAllowedException If a matching resource was found but the request method is not allowed */ public function matchRequest(Request $request); } UrlMatcherInterface.php000064400000002600150313414260011141 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Routing\Matcher; use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\NoConfigurationException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\RequestContextAwareInterface; /** * UrlMatcherInterface is the interface that all URL matcher classes must implement. * * @author Fabien Potencier */ interface UrlMatcherInterface extends RequestContextAwareInterface { /** * Tries to match a URL path with a set of routes. * * If the matcher can not find information, it must throw one of the exceptions documented * below. * * @param string $pathinfo The path info to be parsed (raw format, i.e. not urldecoded) * * @return array An array of parameters * * @throws NoConfigurationException If no routing configuration could be found * @throws ResourceNotFoundException If the resource could not be found * @throws MethodNotAllowedException If the resource was found but the request method is not allowed */ public function match($pathinfo); } RedirectableUrlMatcher.php000064400000003747150313414260011643 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Routing\Matcher; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Route; /** * @author Fabien Potencier */ abstract class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface { /** * {@inheritdoc} */ public function match($pathinfo) { try { $parameters = parent::match($pathinfo); } catch (ResourceNotFoundException $e) { if ('/' === substr($pathinfo, -1) || !\in_array($this->context->getMethod(), ['HEAD', 'GET'])) { throw $e; } try { $parameters = parent::match($pathinfo.'/'); return array_replace($parameters, $this->redirect($pathinfo.'/', isset($parameters['_route']) ? $parameters['_route'] : null)); } catch (ResourceNotFoundException $e2) { throw $e; } } return $parameters; } /** * {@inheritdoc} */ protected function handleRouteRequirements($pathinfo, $name, Route $route) { // expression condition if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), ['context' => $this->context, 'request' => $this->request ?: $this->createRequest($pathinfo)])) { return [self::REQUIREMENT_MISMATCH, null]; } // check HTTP scheme requirement $scheme = $this->context->getScheme(); $schemes = $route->getSchemes(); if ($schemes && !$route->hasScheme($scheme)) { return [self::ROUTE_MATCH, $this->redirect($pathinfo, $name, current($schemes))]; } return [self::REQUIREMENT_MATCH, null]; } } Dumper/DumperCollection.php000064400000006610150313414260011763 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Routing\Matcher\Dumper; /** * Collection of routes. * * @author Arnaud Le Blanc * * @internal */ class DumperCollection implements \IteratorAggregate { /** * @var DumperCollection|null */ private $parent; /** * @var DumperCollection[]|DumperRoute[] */ private $children = []; /** * @var array */ private $attributes = []; /** * Returns the children routes and collections. * * @return self[]|DumperRoute[] */ public function all() { return $this->children; } /** * Adds a route or collection. * * @param DumperRoute|DumperCollection The route or collection */ public function add($child) { if ($child instanceof self) { $child->setParent($this); } $this->children[] = $child; } /** * Sets children. * * @param array $children The children */ public function setAll(array $children) { foreach ($children as $child) { if ($child instanceof self) { $child->setParent($this); } } $this->children = $children; } /** * Returns an iterator over the children. * * @return \Iterator|DumperCollection[]|DumperRoute[] The iterator */ public function getIterator() { return new \ArrayIterator($this->children); } /** * Returns the root of the collection. * * @return self The root collection */ public function getRoot() { return (null !== $this->parent) ? $this->parent->getRoot() : $this; } /** * Returns the parent collection. * * @return self|null The parent collection or null if the collection has no parent */ protected function getParent() { return $this->parent; } /** * Sets the parent collection. */ protected function setParent(self $parent) { $this->parent = $parent; } /** * Returns true if the attribute is defined. * * @param string $name The attribute name * * @return bool true if the attribute is defined, false otherwise */ public function hasAttribute($name) { return \array_key_exists($name, $this->attributes); } /** * Returns an attribute by name. * * @param string $name The attribute name * @param mixed $default Default value is the attribute doesn't exist * * @return mixed The attribute value */ public function getAttribute($name, $default = null) { return $this->hasAttribute($name) ? $this->attributes[$name] : $default; } /** * Sets an attribute by name. * * @param string $name The attribute name * @param mixed $value The attribute value */ public function setAttribute($name, $value) { $this->attributes[$name] = $value; } /** * Sets multiple attributes. * * @param array $attributes The attributes */ public function setAttributes($attributes) { $this->attributes = $attributes; } } Dumper/StaticPrefixCollection.php000064400000014267150313414260013143 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Routing\Matcher\Dumper; /** * Prefix tree of routes preserving routes order. * * @author Frank de Jonge * * @internal */ class StaticPrefixCollection { /** * @var string */ private $prefix; /** * @var array[]|StaticPrefixCollection[] */ private $items = []; /** * @var int */ private $matchStart = 0; public function __construct($prefix = '') { $this->prefix = $prefix; } public function getPrefix() { return $this->prefix; } /** * @return mixed[]|StaticPrefixCollection[] */ public function getItems() { return $this->items; } /** * Adds a route to a group. * * @param string $prefix * @param mixed $route */ public function addRoute($prefix, $route) { $prefix = '/' === $prefix ? $prefix : rtrim($prefix, '/'); $this->guardAgainstAddingNotAcceptedRoutes($prefix); if ($this->prefix === $prefix) { // When a prefix is exactly the same as the base we move up the match start position. // This is needed because otherwise routes that come afterwards have higher precedence // than a possible regular expression, which goes against the input order sorting. $this->items[] = [$prefix, $route]; $this->matchStart = \count($this->items); return; } foreach ($this->items as $i => $item) { if ($i < $this->matchStart) { continue; } if ($item instanceof self && $item->accepts($prefix)) { $item->addRoute($prefix, $route); return; } $group = $this->groupWithItem($item, $prefix, $route); if ($group instanceof self) { $this->items[$i] = $group; return; } } // No optimised case was found, in this case we simple add the route for possible // grouping when new routes are added. $this->items[] = [$prefix, $route]; } /** * Tries to combine a route with another route or group. * * @param StaticPrefixCollection|array $item * @param string $prefix * @param mixed $route * * @return StaticPrefixCollection|null */ private function groupWithItem($item, $prefix, $route) { $itemPrefix = $item instanceof self ? $item->prefix : $item[0]; $commonPrefix = $this->detectCommonPrefix($prefix, $itemPrefix); if (!$commonPrefix) { return null; } $child = new self($commonPrefix); if ($item instanceof self) { $child->items = [$item]; } else { $child->addRoute($item[0], $item[1]); } $child->addRoute($prefix, $route); return $child; } /** * Checks whether a prefix can be contained within the group. * * @param string $prefix * * @return bool Whether a prefix could belong in a given group */ private function accepts($prefix) { return '' === $this->prefix || 0 === strpos($prefix, $this->prefix); } /** * Detects whether there's a common prefix relative to the group prefix and returns it. * * @param string $prefix * @param string $anotherPrefix * * @return false|string A common prefix, longer than the base/group prefix, or false when none available */ private function detectCommonPrefix($prefix, $anotherPrefix) { $baseLength = \strlen($this->prefix); $commonLength = $baseLength; $end = min(\strlen($prefix), \strlen($anotherPrefix)); for ($i = $baseLength; $i <= $end; ++$i) { if (substr($prefix, 0, $i) !== substr($anotherPrefix, 0, $i)) { break; } $commonLength = $i; } $commonPrefix = rtrim(substr($prefix, 0, $commonLength), '/'); if (\strlen($commonPrefix) > $baseLength) { return $commonPrefix; } return false; } /** * Optimizes the tree by inlining items from groups with less than 3 items. */ public function optimizeGroups() { $index = -1; while (isset($this->items[++$index])) { $item = $this->items[$index]; if ($item instanceof self) { $item->optimizeGroups(); // When a group contains only two items there's no reason to optimize because at minimum // the amount of prefix check is 2. In this case inline the group. if ($item->shouldBeInlined()) { array_splice($this->items, $index, 1, $item->items); // Lower index to pass through the same index again after optimizing. // The first item of the replacements might be a group needing optimization. --$index; } } } } private function shouldBeInlined() { if (\count($this->items) >= 3) { return false; } foreach ($this->items as $item) { if ($item instanceof self) { return true; } } foreach ($this->items as $item) { if (\is_array($item) && $item[0] === $this->prefix) { return false; } } return true; } /** * Guards against adding incompatible prefixes in a group. * * @param string $prefix * * @throws \LogicException when a prefix does not belong in a group */ private function guardAgainstAddingNotAcceptedRoutes($prefix) { if (!$this->accepts($prefix)) { $message = sprintf('Could not add route with prefix %s to collection with prefix %s', $prefix, $this->prefix); throw new \LogicException($message); } } } Dumper/MatcherDumper.php000064400000001407150313414260011252 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Routing\Matcher\Dumper; use Symfony\Component\Routing\RouteCollection; /** * MatcherDumper is the abstract class for all built-in matcher dumpers. * * @author Fabien Potencier */ abstract class MatcherDumper implements MatcherDumperInterface { private $routes; public function __construct(RouteCollection $routes) { $this->routes = $routes; } /** * {@inheritdoc} */ public function getRoutes() { return $this->routes; } } Dumper/PhpMatcherDumper.php000064400000035411150313414260011724 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Routing\Matcher\Dumper; use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; /** * PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes. * * @author Fabien Potencier * @author Tobias Schultze * @author Arnaud Le Blanc */ class PhpMatcherDumper extends MatcherDumper { private $expressionLanguage; /** * @var ExpressionFunctionProviderInterface[] */ private $expressionLanguageProviders = []; /** * Dumps a set of routes to a PHP class. * * Available options: * * * class: The class name * * base_class: The base class name * * @param array $options An array of options * * @return string A PHP class representing the matcher class */ public function dump(array $options = []) { $options = array_replace([ 'class' => 'ProjectUrlMatcher', 'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher', ], $options); // trailing slash support is only enabled if we know how to redirect the user $interfaces = class_implements($options['base_class']); $supportsRedirections = isset($interfaces['Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcherInterface']); return <<context = \$context; } {$this->generateMatchMethod($supportsRedirections)} } EOF; } public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) { $this->expressionLanguageProviders[] = $provider; } /** * Generates the code for the match method implementing UrlMatcherInterface. * * @param bool $supportsRedirections Whether redirections are supported by the base class * * @return string Match method as PHP code */ private function generateMatchMethod($supportsRedirections) { $code = rtrim($this->compileRoutes($this->getRoutes(), $supportsRedirections), "\n"); return <<context; \$request = \$this->request ?: \$this->createRequest(\$pathinfo); \$requestMethod = \$canonicalMethod = \$context->getMethod(); if ('HEAD' === \$requestMethod) { \$canonicalMethod = 'GET'; } $code throw 0 < count(\$allow) ? new MethodNotAllowedException(array_unique(\$allow)) : new ResourceNotFoundException(); } EOF; } /** * Generates PHP code to match a RouteCollection with all its routes. * * @param RouteCollection $routes A RouteCollection instance * @param bool $supportsRedirections Whether redirections are supported by the base class * * @return string PHP code */ private function compileRoutes(RouteCollection $routes, $supportsRedirections) { $fetchedHost = false; $groups = $this->groupRoutesByHostRegex($routes); $code = ''; foreach ($groups as $collection) { if (null !== $regex = $collection->getAttribute('host_regex')) { if (!$fetchedHost) { $code .= " \$host = \$context->getHost();\n\n"; $fetchedHost = true; } $code .= sprintf(" if (preg_match(%s, \$host, \$hostMatches)) {\n", var_export($regex, true)); } $tree = $this->buildStaticPrefixCollection($collection); $groupCode = $this->compileStaticPrefixRoutes($tree, $supportsRedirections); if (null !== $regex) { // apply extra indention at each line (except empty ones) $groupCode = preg_replace('/^.{2,}$/m', ' $0', $groupCode); $code .= $groupCode; $code .= " }\n\n"; } else { $code .= $groupCode; } } // used to display the Welcome Page in apps that don't define a homepage $code .= " if ('/' === \$pathinfo && !\$allow) {\n"; $code .= " throw new Symfony\Component\Routing\Exception\NoConfigurationException();\n"; $code .= " }\n"; return $code; } private function buildStaticPrefixCollection(DumperCollection $collection) { $prefixCollection = new StaticPrefixCollection(); foreach ($collection as $dumperRoute) { $prefix = $dumperRoute->getRoute()->compile()->getStaticPrefix(); $prefixCollection->addRoute($prefix, $dumperRoute); } $prefixCollection->optimizeGroups(); return $prefixCollection; } /** * Generates PHP code to match a tree of routes. * * @param StaticPrefixCollection $collection A StaticPrefixCollection instance * @param bool $supportsRedirections Whether redirections are supported by the base class * @param string $ifOrElseIf either "if" or "elseif" to influence chaining * * @return string PHP code */ private function compileStaticPrefixRoutes(StaticPrefixCollection $collection, $supportsRedirections, $ifOrElseIf = 'if') { $code = ''; $prefix = $collection->getPrefix(); if (!empty($prefix) && '/' !== $prefix) { $code .= sprintf(" %s (0 === strpos(\$pathinfo, %s)) {\n", $ifOrElseIf, var_export($prefix, true)); } $ifOrElseIf = 'if'; foreach ($collection->getItems() as $route) { if ($route instanceof StaticPrefixCollection) { $code .= $this->compileStaticPrefixRoutes($route, $supportsRedirections, $ifOrElseIf); $ifOrElseIf = 'elseif'; } else { $code .= $this->compileRoute($route[1]->getRoute(), $route[1]->getName(), $supportsRedirections, $prefix)."\n"; $ifOrElseIf = 'if'; } } if (!empty($prefix) && '/' !== $prefix) { $code .= " }\n\n"; // apply extra indention at each line (except empty ones) $code = preg_replace('/^.{2,}$/m', ' $0', $code); } return $code; } /** * Compiles a single Route to PHP code used to match it against the path info. * * @param Route $route A Route instance * @param string $name The name of the Route * @param bool $supportsRedirections Whether redirections are supported by the base class * @param string|null $parentPrefix The prefix of the parent collection used to optimize the code * * @return string PHP code * * @throws \LogicException */ private function compileRoute(Route $route, $name, $supportsRedirections, $parentPrefix = null) { $code = ''; $compiledRoute = $route->compile(); $conditions = []; $hasTrailingSlash = false; $matches = false; $hostMatches = false; $methods = $route->getMethods(); $supportsTrailingSlash = $supportsRedirections && (!$methods || \in_array('GET', $methods)); $regex = $compiledRoute->getRegex(); if (!\count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?P.*?)\$\1#'.('u' === substr($regex, -1) ? 'u' : ''), $regex, $m)) { if ($supportsTrailingSlash && '/' === substr($m['url'], -1)) { $conditions[] = sprintf('%s === $trimmedPathinfo', var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true)); $hasTrailingSlash = true; } else { $conditions[] = sprintf('%s === $pathinfo', var_export(str_replace('\\', '', $m['url']), true)); } } else { if ($compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() !== $parentPrefix) { $conditions[] = sprintf('0 === strpos($pathinfo, %s)', var_export($compiledRoute->getStaticPrefix(), true)); } if ($supportsTrailingSlash && $pos = strpos($regex, '/$')) { $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2); $hasTrailingSlash = true; } $conditions[] = sprintf('preg_match(%s, $pathinfo, $matches)', var_export($regex, true)); $matches = true; } if ($compiledRoute->getHostVariables()) { $hostMatches = true; } if ($route->getCondition()) { $conditions[] = $this->getExpressionLanguage()->compile($route->getCondition(), ['context', 'request']); } $conditions = implode(' && ', $conditions); $code .= <<mergeDefaults(array_replace(%s), %s);\n", implode(', ', $vars), str_replace("\n", '', var_export($route->getDefaults(), true)) ); } elseif ($route->getDefaults()) { $code .= sprintf(" \$ret = %s;\n", str_replace("\n", '', var_export(array_replace($route->getDefaults(), ['_route' => $name]), true))); } else { $code .= sprintf(" \$ret = ['_route' => '%s'];\n", $name); } if ($hasTrailingSlash) { $code .= <<redirect(\$rawPathinfo.'/', '$name')); } EOF; } if ($methods) { $methodVariable = \in_array('GET', $methods) ? '$canonicalMethod' : '$requestMethod'; $methods = implode("', '", $methods); } if ($schemes = $route->getSchemes()) { if (!$supportsRedirections) { throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.'); } $schemes = str_replace("\n", '', var_export(array_flip($schemes), true)); if ($methods) { $code .= <<getScheme()]); if (!in_array($methodVariable, ['$methods'])) { if (\$hasRequiredScheme) { \$allow = array_merge(\$allow, ['$methods']); } goto $gotoname; } if (!\$hasRequiredScheme) { if ('GET' !== \$canonicalMethod) { goto $gotoname; } return array_replace(\$ret, \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes))); } EOF; } else { $code .= <<getScheme()])) { if ('GET' !== \$canonicalMethod) { goto $gotoname; } return array_replace(\$ret, \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes))); } EOF; } } elseif ($methods) { $code .= <<setAttribute('host_regex', null); $groups->add($currentGroup); foreach ($routes as $name => $route) { $hostRegex = $route->compile()->getHostRegex(); if ($currentGroup->getAttribute('host_regex') !== $hostRegex) { $currentGroup = new DumperCollection(); $currentGroup->setAttribute('host_regex', $hostRegex); $groups->add($currentGroup); } $currentGroup->add(new DumperRoute($name, $route)); } return $groups; } private function getExpressionLanguage() { if (null === $this->expressionLanguage) { if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); } $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders); } return $this->expressionLanguage; } } Dumper/MatcherDumperInterface.php000064400000001727150313414260013100 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Routing\Matcher\Dumper; use Symfony\Component\Routing\RouteCollection; /** * MatcherDumperInterface is the interface that all matcher dumper classes must implement. * * @author Fabien Potencier */ interface MatcherDumperInterface { /** * Dumps a set of routes to a string representation of executable code * that can then be used to match a request against these routes. * * @param array $options An array of options * * @return string Executable code */ public function dump(array $options = []); /** * Gets the routes to dump. * * @return RouteCollection A RouteCollection instance */ public function getRoutes(); } Dumper/DumperRoute.php000064400000002006150313414260010761 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Routing\Matcher\Dumper; use Symfony\Component\Routing\Route; /** * Container for a Route. * * @author Arnaud Le Blanc * * @internal */ class DumperRoute { private $name; private $route; /** * @param string $name The route name * @param Route $route The route */ public function __construct($name, Route $route) { $this->name = $name; $this->route = $route; } /** * Returns the route name. * * @return string The route name */ public function getName() { return $this->name; } /** * Returns the route. * * @return Route The route */ public function getRoute() { return $this->route; } } TraceableUrlMatcher.php000064400000015523150313414260011133 0ustar00 * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Routing\Matcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Exception\ExceptionInterface; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; /** * TraceableUrlMatcher helps debug path info matching by tracing the match. * * @author Fabien Potencier */ class TraceableUrlMatcher extends UrlMatcher { const ROUTE_DOES_NOT_MATCH = 0; const ROUTE_ALMOST_MATCHES = 1; const ROUTE_MATCHES = 2; protected $traces; public function getTraces($pathinfo) { $this->traces = []; try { $this->match($pathinfo); } catch (ExceptionInterface $e) { } return $this->traces; } public function getTracesForRequest(Request $request) { $this->request = $request; $traces = $this->getTraces($request->getPathInfo()); $this->request = null; return $traces; } protected function matchCollection($pathinfo, RouteCollection $routes) { // HEAD and GET are equivalent as per RFC if ('HEAD' === $method = $this->context->getMethod()) { $method = 'GET'; } $supportsTrailingSlash = '/' !== $pathinfo && '' !== $pathinfo && $this instanceof RedirectableUrlMatcherInterface; foreach ($routes as $name => $route) { $compiledRoute = $route->compile(); $staticPrefix = $compiledRoute->getStaticPrefix(); $requiredMethods = $route->getMethods(); // check the static prefix of the URL first. Only use the more expensive preg_match when it matches if ('' === $staticPrefix || 0 === strpos($pathinfo, $staticPrefix)) { // no-op } elseif (!$supportsTrailingSlash || ($requiredMethods && !\in_array('GET', $requiredMethods)) || 'GET' !== $method) { $this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route); continue; } elseif ('/' === substr($staticPrefix, -1) && substr($staticPrefix, 0, -1) === $pathinfo) { $this->addTrace('Route matches!', self::ROUTE_MATCHES, $name, $route); return $this->allow = []; } else { $this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route); continue; } $regex = $compiledRoute->getRegex(); if ($supportsTrailingSlash && $pos = strpos($regex, '/$')) { $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2); $hasTrailingSlash = true; } else { $hasTrailingSlash = false; } if (!preg_match($regex, $pathinfo, $matches)) { // does it match without any requirements? $r = new Route($route->getPath(), $route->getDefaults(), [], $route->getOptions()); $cr = $r->compile(); if (!preg_match($cr->getRegex(), $pathinfo)) { $this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route); continue; } foreach ($route->getRequirements() as $n => $regex) { $r = new Route($route->getPath(), $route->getDefaults(), [$n => $regex], $route->getOptions()); $cr = $r->compile(); if (\in_array($n, $cr->getVariables()) && !preg_match($cr->getRegex(), $pathinfo)) { $this->addTrace(sprintf('Requirement for "%s" does not match (%s)', $n, $regex), self::ROUTE_ALMOST_MATCHES, $name, $route); continue 2; } } continue; } if ($hasTrailingSlash && '/' !== substr($pathinfo, -1)) { if ((!$requiredMethods || \in_array('GET', $requiredMethods)) && 'GET' === $method) { $this->addTrace('Route matches!', self::ROUTE_MATCHES, $name, $route); return $this->allow = []; } $this->addTrace(sprintf('Method "%s" does not match any of the required methods (%s)', $this->context->getMethod(), implode(', ', $requiredMethods)), self::ROUTE_ALMOST_MATCHES, $name, $route); continue; } $hostMatches = []; if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) { $this->addTrace(sprintf('Host "%s" does not match the requirement ("%s")', $this->context->getHost(), $route->getHost()), self::ROUTE_ALMOST_MATCHES, $name, $route); continue; } $status = $this->handleRouteRequirements($pathinfo, $name, $route); if (self::REQUIREMENT_MISMATCH === $status[0]) { if ($route->getCondition()) { $this->addTrace(sprintf('Condition "%s" does not evaluate to "true"', $route->getCondition()), self::ROUTE_ALMOST_MATCHES, $name, $route); } else { $this->addTrace(sprintf('Scheme "%s" does not match any of the required schemes (%s); the user will be redirected to first required scheme', $this->getContext()->getScheme(), implode(', ', $route->getSchemes())), self::ROUTE_ALMOST_MATCHES, $name, $route); } continue; } // check HTTP method requirement if ($requiredMethods) { if (!\in_array($method, $requiredMethods)) { if (self::REQUIREMENT_MATCH === $status[0]) { $this->allow = array_merge($this->allow, $requiredMethods); } $this->addTrace(sprintf('Method "%s" does not match any of the required methods (%s)', $this->context->getMethod(), implode(', ', $requiredMethods)), self::ROUTE_ALMOST_MATCHES, $name, $route); continue; } } $this->addTrace('Route matches!', self::ROUTE_MATCHES, $name, $route); return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, isset($status[1]) ? $status[1] : [])); } return []; } private function addTrace($log, $level = self::ROUTE_DOES_NOT_MATCH, $name = null, $route = null) { $this->traces[] = [ 'log' => $log, 'name' => $name, 'level' => $level, 'path' => null !== $route ? $route->getPath() : null, ]; } }