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

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\Config;
  12. use Contao\CoreBundle\ContaoCoreBundle;
  13. use Contao\CoreBundle\Exception\NoRootPageFoundException;
  14. use Contao\CoreBundle\Framework\ContaoFramework;
  15. use Contao\Model;
  16. use Contao\Model\Collection;
  17. use Contao\PageModel;
  18. use Contao\System;
  19. use Symfony\Cmf\Component\Routing\RouteProviderInterface;
  20. use Symfony\Component\HttpFoundation\Request;
  21. use Symfony\Component\Routing\Exception\RouteNotFoundException;
  22. use Symfony\Component\Routing\Route;
  23. use Symfony\Component\Routing\RouteCollection;
  24. class RouteProvider implements RouteProviderInterface
  25. {
  26.     /**
  27.      * @var ContaoFramework
  28.      */
  29.     private $framework;
  30.     /**
  31.      * @var string
  32.      */
  33.     private $urlSuffix;
  34.     /**
  35.      * @var bool
  36.      */
  37.     private $prependLocale;
  38.     /**
  39.      * @internal Do not inherit from this class; decorate the "contao.routing.route_provider" service instead
  40.      */
  41.     public function __construct(ContaoFramework $frameworkstring $urlSuffixbool $prependLocale)
  42.     {
  43.         $this->framework $framework;
  44.         $this->urlSuffix $urlSuffix;
  45.         $this->prependLocale $prependLocale;
  46.     }
  47.     public function getRouteCollectionForRequest(Request $request): RouteCollection
  48.     {
  49.         $this->framework->initialize(true);
  50.         $pathInfo rawurldecode($request->getPathInfo());
  51.         // The request string must not contain "auto_item" (see #4012)
  52.         if (false !== strpos($pathInfo'/auto_item/')) {
  53.             return new RouteCollection();
  54.         }
  55.         $routes = [];
  56.         if ('/' === $pathInfo || ($this->prependLocale && preg_match('@^/([a-z]{2}(-[A-Z]{2})?)/$@'$pathInfo))) {
  57.             $this->addRoutesForRootPages($this->findRootPages($request->getHttpHost()), $routes);
  58.             return $this->createCollectionForRoutes($routes$request->getLanguages());
  59.         }
  60.         $pathInfo $this->removeSuffixAndLanguage($pathInfo);
  61.         if (null === $pathInfo) {
  62.             return new RouteCollection();
  63.         }
  64.         $candidates $this->getAliasCandidates($pathInfo);
  65.         $pages $this->findPages($candidates);
  66.         $this->addRoutesForPages($pages$routes);
  67.         return $this->createCollectionForRoutes($routes$request->getLanguages());
  68.     }
  69.     public function getRouteByName($name): Route
  70.     {
  71.         $this->framework->initialize(true);
  72.         $ids $this->getPageIdsFromNames([$name]);
  73.         if (empty($ids)) {
  74.             throw new RouteNotFoundException('Route name "'.$name.'" is not supported by '.__METHOD__);
  75.         }
  76.         /** @var PageModel $pageModel */
  77.         $pageModel $this->framework->getAdapter(PageModel::class);
  78.         $page $pageModel->findByPk($ids[0]);
  79.         if (null === $page) {
  80.             throw new RouteNotFoundException(sprintf('Page ID "%s" not found'$ids[0]));
  81.         }
  82.         $routes = [];
  83.         $this->addRoutesForPage($page$routes);
  84.         return $routes[$name];
  85.     }
  86.     public function getRoutesByNames($names): array
  87.     {
  88.         $this->framework->initialize(true);
  89.         /** @var PageModel $pageModel */
  90.         $pageModel $this->framework->getAdapter(PageModel::class);
  91.         if (null === $names) {
  92.             $pages $pageModel->findAll();
  93.         } else {
  94.             $ids $this->getPageIdsFromNames($names);
  95.             if (empty($ids)) {
  96.                 return [];
  97.             }
  98.             $pages $pageModel->findBy('tl_page.id IN ('.implode(','$ids).')', []);
  99.         }
  100.         if (!$pages instanceof Collection) {
  101.             return [];
  102.         }
  103.         $routes = [];
  104.         $this->addRoutesForPages($pages$routes);
  105.         $this->sortRoutes($routes);
  106.         return $routes;
  107.     }
  108.     private function removeSuffixAndLanguage(string $pathInfo): ?string
  109.     {
  110.         $suffixLength = \strlen($this->urlSuffix);
  111.         if (!== $suffixLength) {
  112.             if (substr($pathInfo, -$suffixLength) !== $this->urlSuffix) {
  113.                 return null;
  114.             }
  115.             $pathInfo substr($pathInfo0, -$suffixLength);
  116.         }
  117.         if (=== strncmp($pathInfo'/'1)) {
  118.             $pathInfo substr($pathInfo1);
  119.         }
  120.         if ($this->prependLocale) {
  121.             $matches = [];
  122.             if (!preg_match('@^([a-z]{2}(-[A-Z]{2})?)/(.+)$@'$pathInfo$matches)) {
  123.                 return null;
  124.             }
  125.             $pathInfo $matches[3];
  126.         }
  127.         return $pathInfo;
  128.     }
  129.     /**
  130.      * Compiles all possible aliases by applying dirname() to the request (e.g. news/archive/item, news/archive, news).
  131.      *
  132.      * @return array<string>
  133.      */
  134.     private function getAliasCandidates(string $pathInfo): array
  135.     {
  136.         $pos strpos($pathInfo'/');
  137.         if (false === $pos) {
  138.             return [$pathInfo];
  139.         }
  140.         /** @var Config $config */
  141.         $config $this->framework->getAdapter(Config::class);
  142.         if (!$config->get('folderUrl')) {
  143.             return [substr($pathInfo0$pos)];
  144.         }
  145.         $candidates = [$pathInfo];
  146.         while ('/' !== $pathInfo && false !== strpos($pathInfo'/')) {
  147.             $pathInfo = \dirname($pathInfo);
  148.             $candidates[] = $pathInfo;
  149.         }
  150.         return $candidates;
  151.     }
  152.     /**
  153.      * @param iterable<PageModel> $pages
  154.      */
  155.     private function addRoutesForPages(iterable $pages, array &$routes): void
  156.     {
  157.         foreach ($pages as $page) {
  158.             $this->addRoutesForPage($page$routes);
  159.         }
  160.     }
  161.     /**
  162.      * @param array<PageModel> $pages
  163.      */
  164.     private function addRoutesForRootPages(array $pages, array &$routes): void
  165.     {
  166.         foreach ($pages as $page) {
  167.             $this->addRoutesForRootPage($page$routes);
  168.         }
  169.     }
  170.     private function createCollectionForRoutes(array $routes, array $languages): RouteCollection
  171.     {
  172.         $this->sortRoutes($routes$languages);
  173.         $collection = new RouteCollection();
  174.         foreach ($routes as $name => $route) {
  175.             $collection->add($name$route);
  176.         }
  177.         return $collection;
  178.     }
  179.     private function addRoutesForPage(PageModel $page, array &$routes): void
  180.     {
  181.         try {
  182.             $page->loadDetails();
  183.             if (!$page->rootId) {
  184.                 return;
  185.             }
  186.         } catch (NoRootPageFoundException $e) {
  187.             return;
  188.         }
  189.         $defaults $this->getRouteDefaults($page);
  190.         $defaults['parameters'] = '';
  191.         $requirements = ['parameters' => '(/.+?)?'];
  192.         $path sprintf('/%s{parameters}%s'$page->alias ?: $page->id$this->urlSuffix);
  193.         if ($this->prependLocale) {
  194.             $path '/{_locale}'.$path;
  195.             $requirements['_locale'] = $page->rootLanguage;
  196.         }
  197.         $routes['tl_page.'.$page->id] = new Route(
  198.             $path,
  199.             $defaults,
  200.             $requirements,
  201.             ['utf8' => true],
  202.             $page->domain,
  203.             $page->rootUseSSL 'https' null
  204.         );
  205.         $this->addRoutesForRootPage($page$routes);
  206.     }
  207.     private function addRoutesForRootPage(PageModel $page, array &$routes): void
  208.     {
  209.         if ('root' !== $page->type && 'index' !== $page->alias && '/' !== $page->alias) {
  210.             return;
  211.         }
  212.         $page->loadDetails();
  213.         $path '/';
  214.         $requirements = [];
  215.         $defaults $this->getRouteDefaults($page);
  216.         if ($this->prependLocale) {
  217.             $path '/{_locale}'.$path;
  218.             $requirements['_locale'] = $page->rootLanguage;
  219.         }
  220.         $routes['tl_page.'.$page->id.'.root'] = new Route(
  221.             $path,
  222.             $defaults,
  223.             $requirements,
  224.             [],
  225.             $page->domain,
  226.             $page->rootUseSSL 'https' null,
  227.             []
  228.         );
  229.         /** @var Config $config */
  230.         $config $this->framework->getAdapter(Config::class);
  231.         if (!$config->get('doNotRedirectEmpty')) {
  232.             $defaults['_controller'] = 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction';
  233.             $defaults['path'] = '/'.$page->language.'/';
  234.             $defaults['permanent'] = false;
  235.         }
  236.         $routes['tl_page.'.$page->id.'.fallback'] = new Route(
  237.             '/',
  238.             $defaults,
  239.             [],
  240.             [],
  241.             $page->domain,
  242.             $page->rootUseSSL 'https' null,
  243.             []
  244.         );
  245.     }
  246.     /**
  247.      * @return array<string,PageModel|bool|string>
  248.      */
  249.     private function getRouteDefaults(PageModel $page): array
  250.     {
  251.         return [
  252.             '_token_check' => true,
  253.             '_controller' => 'Contao\FrontendIndex::renderPage',
  254.             '_scope' => ContaoCoreBundle::SCOPE_FRONTEND,
  255.             '_locale' => $page->rootLanguage,
  256.             '_canonical_route' => 'tl_page.'.$page->id,
  257.             'pageModel' => $page,
  258.         ];
  259.     }
  260.     /**
  261.      * @return array<int>
  262.      */
  263.     private function getPageIdsFromNames(array $names): array
  264.     {
  265.         $ids = [];
  266.         foreach ($names as $name) {
  267.             $parts explode('.'$name);
  268.             if ('tl_page' !== $parts[0] || 'error_404' === ($parts[2] ?? null)) {
  269.                 continue;
  270.             }
  271.             [, $id] = explode('.'$name);
  272.             if (!preg_match('/^[1-9]\d*$/'$id)) {
  273.                 continue;
  274.             }
  275.             $ids[] = (int) $id;
  276.         }
  277.         return array_unique($ids);
  278.     }
  279.     /**
  280.      * Sorts routes so that the FinalMatcher will correctly resolve them.
  281.      *
  282.      * 1. The ones with hostname should come first, so the ones with empty host are only taken if no hostname matches
  283.      * 2. Root pages come last, so non-root pages with index alias (= identical path) match first
  284.      * 3. Root/Index pages must be sorted by accept language and fallback, so the best language matches first
  285.      * 4. Pages with longer alias (folder page) must come first to match if applicable
  286.      */
  287.     private function sortRoutes(array &$routes, array $languages null): void
  288.     {
  289.         // Convert languages array so key is language and value is priority
  290.         if (null !== $languages) {
  291.             foreach ($languages as &$language) {
  292.                 $language str_replace('_''-'$language);
  293.                 if (=== \strlen($language)) {
  294.                     $lng substr($language02);
  295.                     // Append the language if only language plus dialect is given (see #430)
  296.                     if (!\in_array($lng$languagestrue)) {
  297.                         $languages[] = $lng;
  298.                     }
  299.                 }
  300.             }
  301.             unset($language);
  302.             $languages array_flip(array_values($languages));
  303.         }
  304.         uasort(
  305.             $routes,
  306.             static function (Route $aRoute $b) use ($languages$routes) {
  307.                 $fallbackA '.fallback' === substr(array_search($a$routestrue), -9);
  308.                 $fallbackB '.fallback' === substr(array_search($b$routestrue), -9);
  309.                 if ($fallbackA && !$fallbackB) {
  310.                     return 1;
  311.                 }
  312.                 if ($fallbackB && !$fallbackA) {
  313.                     return -1;
  314.                 }
  315.                 if ('' !== $a->getHost() && '' === $b->getHost()) {
  316.                     return -1;
  317.                 }
  318.                 if ('' === $a->getHost() && '' !== $b->getHost()) {
  319.                     return 1;
  320.                 }
  321.                 /** @var PageModel $pageA */
  322.                 $pageA $a->getDefault('pageModel');
  323.                 /** @var PageModel $pageB */
  324.                 $pageB $b->getDefault('pageModel');
  325.                 // Check if the page models are valid (should always be the case, as routes are generated from pages)
  326.                 if (!$pageA instanceof PageModel || !$pageB instanceof PageModel) {
  327.                     return 0;
  328.                 }
  329.                 $langA null;
  330.                 $langB null;
  331.                 if (null !== $languages && $pageA->rootLanguage !== $pageB->rootLanguage) {
  332.                     $langA $languages[$pageA->rootLanguage] ?? null;
  333.                     $langB $languages[$pageB->rootLanguage] ?? null;
  334.                 }
  335.                 if (null === $langA && null === $langB) {
  336.                     if ($pageA->rootIsFallback && !$pageB->rootIsFallback) {
  337.                         return -1;
  338.                     }
  339.                     if ($pageB->rootIsFallback && !$pageA->rootIsFallback) {
  340.                         return 1;
  341.                     }
  342.                 } else {
  343.                     if (null === $langA && null !== $langB) {
  344.                         return 1;
  345.                     }
  346.                     if (null !== $langA && null === $langB) {
  347.                         return -1;
  348.                     }
  349.                     if ($langA $langB) {
  350.                         return -1;
  351.                     }
  352.                     if ($langA $langB) {
  353.                         return 1;
  354.                     }
  355.                 }
  356.                 if ('root' !== $pageA->type && 'root' === $pageB->type) {
  357.                     return -1;
  358.                 }
  359.                 if ('root' === $pageA->type && 'root' !== $pageB->type) {
  360.                     return 1;
  361.                 }
  362.                 return strnatcasecmp((string) $pageB->alias, (string) $pageA->alias);
  363.             }
  364.         );
  365.     }
  366.     /**
  367.      * @return array<Model>
  368.      */
  369.     private function findPages(array $candidates): array
  370.     {
  371.         $ids = [];
  372.         $aliases = [];
  373.         foreach ($candidates as $candidate) {
  374.             if (preg_match('/^[1-9]\d*$/'$candidate)) {
  375.                 $ids[] = (int) $candidate;
  376.             } else {
  377.                 $aliases[] = $candidate;
  378.             }
  379.         }
  380.         $conditions = [];
  381.         if (!empty($ids)) {
  382.             $conditions[] = 'tl_page.id IN ('.implode(','$ids).')';
  383.         }
  384.         if (!empty($aliases)) {
  385.             $conditions[] = 'tl_page.alias IN ('.implode(','array_fill(0, \count($aliases), '?')).')';
  386.         }
  387.         /** @var PageModel $pageModel */
  388.         $pageModel $this->framework->getAdapter(PageModel::class);
  389.         $pages $pageModel->findBy([implode(' OR '$conditions)], $aliases);
  390.         if (!$pages instanceof Collection) {
  391.             return [];
  392.         }
  393.         return $pages->getModels();
  394.     }
  395.     /**
  396.      * @return array<Model>
  397.      */
  398.     private function findRootPages(string $httpHost): array
  399.     {
  400.         if (
  401.             !empty($GLOBALS['TL_HOOKS']['getRootPageFromUrl'])
  402.             && \is_array($GLOBALS['TL_HOOKS']['getRootPageFromUrl'])
  403.         ) {
  404.             /** @var System $system */
  405.             $system $this->framework->getAdapter(System::class);
  406.             foreach ($GLOBALS['TL_HOOKS']['getRootPageFromUrl'] as $callback) {
  407.                 $page $system->importStatic($callback[0])->{$callback[1]}();
  408.                 if ($page instanceof PageModel) {
  409.                     return [$page];
  410.                 }
  411.             }
  412.         }
  413.         $rootPages = [];
  414.         $indexPages = [];
  415.         /** @var PageModel $pageModel */
  416.         $pageModel $this->framework->getAdapter(PageModel::class);
  417.         $pages $pageModel->findBy(["(tl_page.type='root' AND (tl_page.dns=? OR tl_page.dns=''))"], $httpHost);
  418.         if ($pages instanceof Collection) {
  419.             $rootPages $pages->getModels();
  420.         }
  421.         $pages $pageModel->findBy(["tl_page.alias='index' OR tl_page.alias='/'"], null);
  422.         if ($pages instanceof Collection) {
  423.             $indexPages $pages->getModels();
  424.         }
  425.         return array_merge($rootPages$indexPages);
  426.     }
  427. }