1<?php 2 3declare(strict_types=1); 4 5/* 6 * This file is part of the TYPO3 CMS project. 7 * 8 * It is free software; you can redistribute it and/or modify it under 9 * the terms of the GNU General Public License, either version 2 10 * of the License, or any later version. 11 * 12 * For the full copyright and license information, please read the 13 * LICENSE.txt file that was distributed with this source code. 14 * 15 * The TYPO3 project - inspiring people to share! 16 */ 17 18namespace TYPO3\CMS\Core\Routing; 19 20use Symfony\Component\Routing\Exception\ResourceNotFoundException; 21use TYPO3\CMS\Core\Routing\Aspect\MappableProcessor; 22 23/** 24 * Internal class, which is similar to Symfony's Urlmatcher but without validating 25 * - conditions / expression language 26 * - host matches 27 * - method checks 28 * because this method only works in conjunction with PageRouter. 29 * 30 * @internal 31 */ 32class PageUriMatcher 33{ 34 /** 35 * @var RouteCollection<string, Route> 36 */ 37 protected $routes; 38 39 /** 40 * @var MappableProcessor 41 */ 42 protected $mappableProcessor; 43 44 public function __construct(RouteCollection $routes) 45 { 46 $this->routes = $routes; 47 $this->mappableProcessor = new MappableProcessor(); 48 } 49 50 /** 51 * Matches a path segment against the route collection 52 * 53 * @param string $urlPath 54 * @return array 55 * @throws ResourceNotFoundException 56 */ 57 public function match(string $urlPath) 58 { 59 if ($ret = $this->matchCollection(rawurldecode($urlPath), $this->routes)) { 60 return $ret; 61 } 62 throw new ResourceNotFoundException( 63 sprintf('No routes found for "%s".', $urlPath), 64 1538156220 65 ); 66 } 67 68 /** 69 * Tries to match a URL with a set of routes. 70 * 71 * @param string $urlPath The path info to be parsed 72 * @param RouteCollection<string,Route> $routes The set of routes 73 * @return array An array of parameters 74 */ 75 protected function matchCollection(string $urlPath, RouteCollection $routes): ?array 76 { 77 foreach ($routes as $name => $route) { 78 $urlPath = $this->getDecoratedRoutePath($route) ?? $urlPath; 79 $compiledRoute = $route->compile(); 80 81 // check the static prefix of the URL first. Only use the more expensive preg_match when it matches 82 if ('' !== $compiledRoute->getStaticPrefix() && 0 !== strpos($urlPath, $compiledRoute->getStaticPrefix())) { 83 continue; 84 } 85 86 if (!preg_match($compiledRoute->getRegex(), $urlPath, $matches)) { 87 continue; 88 } 89 90 // custom handling of Mappable instances 91 if (!$this->mappableProcessor->resolve($route, $matches)) { 92 continue; 93 } 94 95 return $this->getAttributes($route, $name, $matches); 96 } 97 return null; 98 } 99 100 /** 101 * Resolves an optional route specific decorated route path that has been 102 * assigned by DecoratingEnhancerInterface instances. 103 * 104 * @param Route $route 105 * @return string|null 106 */ 107 protected function getDecoratedRoutePath(Route $route): ?string 108 { 109 if (!$route->hasOption('_decoratedRoutePath')) { 110 return null; 111 } 112 $urlPath = $route->getOption('_decoratedRoutePath'); 113 return rawurldecode($urlPath); 114 } 115 116 /** 117 * Returns an array of values to use as request attributes. 118 * 119 * As this method requires the Route object, it is not available 120 * in matchers that do not have access to the matched Route instance 121 * (like the PHP and Apache matcher dumpers). 122 * 123 * @param Route $route The route we are matching against 124 * @param string $name The name of the route 125 * @param array $attributes An array of attributes from the matcher 126 * @return array An array of parameters 127 */ 128 protected function getAttributes(Route $route, string $name, array $attributes): array 129 { 130 $defaults = $route->getDefaults(); 131 if (isset($defaults['_canonical_route'])) { 132 $name = $defaults['_canonical_route']; 133 unset($defaults['_canonical_route']); 134 } 135 $attributes['_route'] = $name; 136 // store applied default values in route options 137 $relevantDefaults = array_intersect_key($defaults, array_flip($route->compile()->getPathVariables())); 138 // option '_appliedDefaults' contains internal(!) values (default values are not mapped when resolving) 139 // (keys used are deflated and need to be inflated later using VariableProcessor) 140 $route->setOption('_appliedDefaults', array_diff_key($relevantDefaults, $attributes)); 141 // side note: $defaults can contain e.g. '_controller' 142 return $this->mergeDefaults($attributes, $defaults); 143 } 144 145 /** 146 * Get merged default parameters. 147 * 148 * @param array $params The parameters 149 * @param array $defaults The defaults 150 * @return array Merged default parameters 151 */ 152 protected function mergeDefaults(array $params, array $defaults): array 153 { 154 foreach ($params as $key => $value) { 155 if (!is_int($key) && null !== $value) { 156 $defaults[$key] = $value; 157 } 158 } 159 return $defaults; 160 } 161} 162