vendor/contao/core-bundle/src/Routing/RouteProvider.php line 44

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of Contao.
  5. *
  6. * (c) Leo Feyer
  7. *
  8. * @license LGPL-3.0-or-later
  9. */
  10. namespace Contao\CoreBundle\Routing;
  11. use Contao\CoreBundle\Exception\NoRootPageFoundException;
  12. use Contao\CoreBundle\Framework\ContaoFramework;
  13. use Contao\CoreBundle\Routing\Page\PageRegistry;
  14. use Contao\CoreBundle\Routing\Page\PageRoute;
  15. use Contao\Model\Collection;
  16. use Contao\PageModel;
  17. use Contao\System;
  18. use Symfony\Cmf\Component\Routing\Candidates\CandidatesInterface;
  19. use Symfony\Component\HttpFoundation\Request;
  20. use Symfony\Component\Routing\Exception\RouteNotFoundException;
  21. use Symfony\Component\Routing\Route;
  22. use Symfony\Component\Routing\RouteCollection;
  23. class RouteProvider extends AbstractPageRouteProvider
  24. {
  25. private bool $legacyRouting;
  26. private bool $prependLocale;
  27. /**
  28. * @internal
  29. */
  30. public function __construct(ContaoFramework $framework, CandidatesInterface $candidates, PageRegistry $pageRegistry, bool $legacyRouting, bool $prependLocale)
  31. {
  32. parent::__construct($framework, $candidates, $pageRegistry);
  33. $this->legacyRouting = $legacyRouting;
  34. $this->prependLocale = $prependLocale;
  35. }
  36. public function getRouteCollectionForRequest(Request $request): RouteCollection
  37. {
  38. $this->framework->initialize(true);
  39. $pathInfo = rawurldecode($request->getPathInfo());
  40. // The request string must start with "/" and must not contain "auto_item" (see #4012)
  41. if (!str_starts_with($pathInfo, '/') || false !== strpos($pathInfo, '/auto_item/')) {
  42. return new RouteCollection();
  43. }
  44. $routes = [];
  45. if ('/' === $pathInfo || ($this->legacyRouting && $this->prependLocale && preg_match('@^/([a-z]{2}(-[A-Z]{2})?)/$@', $pathInfo))) {
  46. $this->addRoutesForRootPages($this->findRootPages($request->getHttpHost()), $routes);
  47. return $this->createCollectionForRoutes($routes, $request->getLanguages());
  48. }
  49. $pages = $this->findCandidatePages($request);
  50. if (empty($pages)) {
  51. return new RouteCollection();
  52. }
  53. $this->addRoutesForPages($pages, $routes);
  54. return $this->createCollectionForRoutes($routes, $request->getLanguages());
  55. }
  56. public function getRouteByName($name): Route
  57. {
  58. $this->framework->initialize(true);
  59. $ids = $this->getPageIdsFromNames([$name]);
  60. if (empty($ids)) {
  61. throw new RouteNotFoundException('Route name does not match a page ID');
  62. }
  63. $pageModel = $this->framework->getAdapter(PageModel::class);
  64. $page = $pageModel->findByPk($ids[0]);
  65. if (null === $page || !$this->pageRegistry->isRoutable($page)) {
  66. throw new RouteNotFoundException(sprintf('Page ID "%s" not found', $ids[0]));
  67. }
  68. $routes = [];
  69. $this->addRoutesForPage($page, $routes);
  70. if (!\array_key_exists($name, $routes)) {
  71. throw new RouteNotFoundException('Route "'.$name.'" not found');
  72. }
  73. return $routes[$name];
  74. }
  75. public function getRoutesByNames($names): array
  76. {
  77. $this->framework->initialize(true);
  78. $pageModel = $this->framework->getAdapter(PageModel::class);
  79. if (null === $names) {
  80. $pages = $pageModel->findAll();
  81. } else {
  82. $ids = $this->getPageIdsFromNames($names);
  83. if (empty($ids)) {
  84. return [];
  85. }
  86. $pages = $pageModel->findBy('tl_page.id IN ('.implode(',', $ids).')', []);
  87. }
  88. if (!$pages instanceof Collection) {
  89. return [];
  90. }
  91. $routes = [];
  92. /** @var array<PageModel> $models */
  93. $models = $pages->getModels();
  94. $models = array_filter($models, fn (PageModel $page): bool => $this->pageRegistry->isRoutable($page));
  95. $this->addRoutesForPages($models, $routes);
  96. $this->sortRoutes($routes);
  97. return $routes;
  98. }
  99. /**
  100. * @param iterable<PageModel> $pages
  101. */
  102. private function addRoutesForPages(iterable $pages, array &$routes): void
  103. {
  104. foreach ($pages as $page) {
  105. $this->addRoutesForPage($page, $routes);
  106. }
  107. }
  108. /**
  109. * @param array<PageModel> $pages
  110. */
  111. private function addRoutesForRootPages(array $pages, array &$routes): void
  112. {
  113. foreach ($pages as $page) {
  114. $route = $this->pageRegistry->getRoute($page);
  115. $this->addRoutesForRootPage($route, $routes);
  116. }
  117. }
  118. private function createCollectionForRoutes(array $routes, array $languages): RouteCollection
  119. {
  120. $this->sortRoutes($routes, $languages);
  121. $collection = new RouteCollection();
  122. foreach ($routes as $name => $route) {
  123. $collection->add($name, $route);
  124. }
  125. return $collection;
  126. }
  127. private function addRoutesForPage(PageModel $page, array &$routes): void
  128. {
  129. try {
  130. $page->loadDetails();
  131. if (!$page->rootId) {
  132. return;
  133. }
  134. } catch (NoRootPageFoundException $e) {
  135. return;
  136. }
  137. $route = $this->pageRegistry->getRoute($page);
  138. $routes['tl_page.'.$page->id] = $route;
  139. $this->addRoutesForRootPage($route, $routes);
  140. }
  141. private function addRoutesForRootPage(PageRoute $route, array &$routes): void
  142. {
  143. $page = $route->getPageModel();
  144. // Only create "root" routes for root pages or pages with root alias
  145. if ('root' !== $page->type && 'index' !== $page->alias && '/' !== $page->alias) {
  146. return;
  147. }
  148. $urlPrefix = $route->getUrlPrefix();
  149. // Do not create a ".root" route for root pages without prefix if `disableLanguageRedirect` is enabled
  150. if ('root' === $page->type && !$urlPrefix && !$this->legacyRouting && $page->disableLanguageRedirect) {
  151. return;
  152. }
  153. $routes['tl_page.'.$page->id.'.root'] = new Route(
  154. $urlPrefix ? '/'.$urlPrefix.'/' : '/',
  155. $route->getDefaults(),
  156. [],
  157. $route->getOptions(),
  158. $route->getHost(),
  159. $route->getSchemes(),
  160. $route->getMethods()
  161. );
  162. // Do not create ".fallback" route if `disableLanguageRedirect` is enabled
  163. if (!$urlPrefix || (!$this->legacyRouting && $page->loadDetails()->disableLanguageRedirect)) {
  164. return;
  165. }
  166. $routes['tl_page.'.$page->id.'.fallback'] = new Route(
  167. '/',
  168. array_merge(
  169. $route->getDefaults(),
  170. [
  171. '_controller' => 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction',
  172. 'path' => '/'.$urlPrefix.'/',
  173. 'permanent' => false,
  174. ]
  175. ),
  176. [],
  177. $route->getOptions(),
  178. $route->getHost(),
  179. $route->getSchemes(),
  180. $route->getMethods()
  181. );
  182. }
  183. /**
  184. * Sorts routes so that the FinalMatcher will correctly resolve them.
  185. *
  186. * 1. The ones with hostname should come first, so the ones with empty host are only taken if no hostname matches
  187. * 2. Root pages come last, so non-root pages with index alias (= identical path) match first
  188. * 3. Root/Index pages must be sorted by accept language and fallback, so the best language matches first
  189. * 4. Pages with longer alias (folder page) must come first to match if applicable
  190. */
  191. private function sortRoutes(array &$routes, ?array $languages = null): void
  192. {
  193. // Convert languages array so key is language and value is priority
  194. if (null !== $languages) {
  195. $languages = $this->convertLanguagesForSorting($languages);
  196. }
  197. uasort(
  198. $routes,
  199. function (Route $a, Route $b) use ($languages, $routes) {
  200. $nameA = array_search($a, $routes, true);
  201. $nameB = array_search($b, $routes, true);
  202. $fallbackA = 0 === substr_compare($nameA, '.fallback', -9);
  203. $fallbackB = 0 === substr_compare($nameB, '.fallback', -9);
  204. if ($fallbackA && !$fallbackB) {
  205. return 1;
  206. }
  207. if ($fallbackB && !$fallbackA) {
  208. return -1;
  209. }
  210. if ('/' === $a->getPath() && '/' !== $b->getPath()) {
  211. return -1;
  212. }
  213. if ('/' === $b->getPath() && '/' !== $a->getPath()) {
  214. return 1;
  215. }
  216. return $this->compareRoutes($a, $b, $languages);
  217. }
  218. );
  219. }
  220. /**
  221. * @return array<PageModel>
  222. */
  223. private function findRootPages(string $httpHost): array
  224. {
  225. if (
  226. $this->legacyRouting
  227. && !empty($GLOBALS['TL_HOOKS']['getRootPageFromUrl'])
  228. && \is_array($GLOBALS['TL_HOOKS']['getRootPageFromUrl'])
  229. ) {
  230. $system = $this->framework->getAdapter(System::class);
  231. foreach ($GLOBALS['TL_HOOKS']['getRootPageFromUrl'] as $callback) {
  232. $page = $system->importStatic($callback[0])->{$callback[1]}();
  233. if ($page instanceof PageModel) {
  234. return [$page];
  235. }
  236. }
  237. }
  238. $models = [];
  239. $pageModel = $this->framework->getAdapter(PageModel::class);
  240. $pages = $pageModel->findBy(["(tl_page.type='root' AND (tl_page.dns=? OR tl_page.dns=''))"], $httpHost);
  241. if ($pages instanceof Collection) {
  242. $models = $pages->getModels();
  243. }
  244. /** @var Collection|array<PageModel> $pages */
  245. $pages = $pageModel->findBy(['tl_page.alias=? OR tl_page.alias=?'], ['index', '/']);
  246. if ($pages instanceof Collection) {
  247. foreach ($pages as $page) {
  248. if ($this->pageRegistry->isRoutable($page)) {
  249. $models[] = $page;
  250. }
  251. }
  252. }
  253. return $models;
  254. }
  255. }