vendor/contao/core-bundle/src/Routing/Route404Provider.php line 125

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\ContaoCoreBundle;
  12. use Contao\CoreBundle\Exception\NoRootPageFoundException;
  13. use Contao\CoreBundle\Framework\ContaoFramework;
  14. use Contao\CoreBundle\Routing\Page\PageRegistry;
  15. use Contao\CoreBundle\Routing\Page\PageRoute;
  16. use Contao\CoreBundle\Util\LocaleUtil;
  17. use Contao\PageModel;
  18. use Symfony\Bundle\FrameworkBundle\Controller\RedirectController;
  19. use Symfony\Cmf\Component\Routing\Candidates\CandidatesInterface;
  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 Route404Provider extends AbstractPageRouteProvider
  25. {
  26.     /**
  27.      * @internal Do not inherit from this class; decorate the "contao.routing.route_404_provider" service instead
  28.      */
  29.     public function __construct(ContaoFramework $frameworkCandidatesInterface $candidatesPageRegistry $pageRegistry)
  30.     {
  31.         parent::__construct($framework$candidates$pageRegistry);
  32.     }
  33.     public function getRouteCollectionForRequest(Request $request): RouteCollection
  34.     {
  35.         $this->framework->initialize(true);
  36.         $collection = new RouteCollection();
  37.         $routes array_merge($this->getNotFoundRoutes(), $this->getLocaleFallbackRoutes($request));
  38.         $this->sortRoutes($routes$request->getLanguages());
  39.         foreach ($routes as $name => $route) {
  40.             $collection->add($name$route);
  41.         }
  42.         return $collection;
  43.     }
  44.     public function getRouteByName($name): Route
  45.     {
  46.         $this->framework->initialize(true);
  47.         $ids $this->getPageIdsFromNames([$name]);
  48.         if (empty($ids)) {
  49.             throw new RouteNotFoundException('Route name does not match a page ID');
  50.         }
  51.         $pageAdapter $this->framework->getAdapter(PageModel::class);
  52.         $page $pageAdapter->findByPk($ids[0]);
  53.         if (null === $page) {
  54.             throw new RouteNotFoundException(sprintf('Page ID "%s" not found'$ids[0]));
  55.         }
  56.         $routes = [];
  57.         $this->addNotFoundRoutesForPage($page$routes);
  58.         if ($this->pageRegistry->isRoutable($page)) {
  59.             $this->addLocaleRedirectRoute($this->pageRegistry->getRoute($page), null$routes);
  60.         }
  61.         if (!\array_key_exists($name$routes)) {
  62.             throw new RouteNotFoundException('Route "'.$name.'" not found');
  63.         }
  64.         return $routes[$name];
  65.     }
  66.     public function getRoutesByNames($names): array
  67.     {
  68.         $this->framework->initialize(true);
  69.         $pageAdapter $this->framework->getAdapter(PageModel::class);
  70.         if (null === $names) {
  71.             $pages $pageAdapter->findAll();
  72.         } else {
  73.             $ids $this->getPageIdsFromNames($names);
  74.             if (empty($ids)) {
  75.                 return [];
  76.             }
  77.             $pages $pageAdapter->findBy('tl_page.id IN ('.implode(','$ids).')', []);
  78.         }
  79.         $routes = [];
  80.         foreach ($pages as $page) {
  81.             $this->addNotFoundRoutesForPage($page$routes);
  82.             if ($this->pageRegistry->isRoutable($page)) {
  83.                 $this->addLocaleRedirectRoute($this->pageRegistry->getRoute($page), null$routes);
  84.             }
  85.         }
  86.         $this->sortRoutes($routes);
  87.         return $routes;
  88.     }
  89.     private function getNotFoundRoutes(): array
  90.     {
  91.         $this->framework->initialize(true);
  92.         $pageModel $this->framework->getAdapter(PageModel::class);
  93.         $pages $pageModel->findByType('error_404');
  94.         if (null === $pages) {
  95.             return [];
  96.         }
  97.         $routes = [];
  98.         foreach ($pages as $page) {
  99.             $this->addNotFoundRoutesForPage($page$routes);
  100.         }
  101.         return $routes;
  102.     }
  103.     private function addNotFoundRoutesForPage(PageModel $page, array &$routes): void
  104.     {
  105.         if ('error_404' !== $page->type) {
  106.             return;
  107.         }
  108.         try {
  109.             $page->loadDetails();
  110.             if (!$page->rootId) {
  111.                 return;
  112.             }
  113.         } catch (NoRootPageFoundException $e) {
  114.             return;
  115.         }
  116.         $defaults = [
  117.             '_token_check' => true,
  118.             '_controller' => 'Contao\FrontendIndex::renderPage',
  119.             '_scope' => ContaoCoreBundle::SCOPE_FRONTEND,
  120.             '_locale' => LocaleUtil::formatAsLocale((string) $page->rootLanguage),
  121.             '_format' => 'html',
  122.             '_canonical_route' => 'tl_page.'.$page->id,
  123.             'pageModel' => $page,
  124.         ];
  125.         $requirements = ['_url_fragment' => '.*'];
  126.         $path '/{_url_fragment}';
  127.         $routes['tl_page.'.$page->id.'.error_404'] = new Route(
  128.             $path,
  129.             $defaults,
  130.             $requirements,
  131.             ['utf8' => true],
  132.             $page->domain,
  133.             $page->rootUseSSL 'https' 'http'
  134.         );
  135.         if (!$page->urlPrefix) {
  136.             return;
  137.         }
  138.         $path '/'.$page->urlPrefix.$path;
  139.         $routes['tl_page.'.$page->id.'.error_404.locale'] = new Route(
  140.             $path,
  141.             $defaults,
  142.             $requirements,
  143.             ['utf8' => true],
  144.             $page->domain,
  145.             $page->rootUseSSL 'https' 'http'
  146.         );
  147.     }
  148.     private function getLocaleFallbackRoutes(Request $request): array
  149.     {
  150.         if ('/' === $request->getPathInfo()) {
  151.             return [];
  152.         }
  153.         $routes = [];
  154.         foreach ($this->findCandidatePages($request) as $page) {
  155.             $this->addLocaleRedirectRoute($this->pageRegistry->getRoute($page), $request$routes);
  156.         }
  157.         return $routes;
  158.     }
  159.     private function addLocaleRedirectRoute(PageRoute $route, ?Request $request, array &$routes): void
  160.     {
  161.         $length = \strlen($route->getUrlPrefix());
  162.         if (=== $length) {
  163.             return;
  164.         }
  165.         $redirect = new Route(
  166.             substr($route->getPath(), $length 1),
  167.             $route->getDefaults(),
  168.             $route->getRequirements(),
  169.             $route->getOptions(),
  170.             $route->getHost(),
  171.             $route->getSchemes(),
  172.             $route->getMethods()
  173.         );
  174.         $path $route->getPath();
  175.         if (null !== $request) {
  176.             $path '/'.$route->getUrlPrefix().$request->getPathInfo();
  177.         }
  178.         $redirect->addDefaults([
  179.             '_controller' => RedirectController::class,
  180.             'path' => $path,
  181.             'permanent' => false,
  182.         ]);
  183.         $routes['tl_page.'.$route->getPageModel()->id.'.locale'] = $redirect;
  184.     }
  185.     /**
  186.      * Sorts routes so that the FinalMatcher will correctly resolve them.
  187.      *
  188.      * 1. Sort locale-aware routes first, so e.g. /de/not-found.html renders the german error page
  189.      * 2. Then sort by hostname, so the ones with empty host are only taken if no hostname matches
  190.      * 3. Lastly pages must be sorted by accept language and fallback, so the best language matches first
  191.      */
  192.     private function sortRoutes(array &$routes, array $languages null): void
  193.     {
  194.         // Convert languages array so key is language and value is priority
  195.         if (null !== $languages) {
  196.             $languages $this->convertLanguagesForSorting($languages);
  197.         }
  198.         uasort(
  199.             $routes,
  200.             function (Route $aRoute $b) use ($languages$routes) {
  201.                 $nameA array_search($a$routestrue);
  202.                 $nameB array_search($b$routestrue);
  203.                 $errorA false !== strpos('.error_404'$nameA, -10);
  204.                 $errorB false !== strpos('.error_404'$nameB, -10);
  205.                 if ($errorA && !$errorB) {
  206.                     return 1;
  207.                 }
  208.                 if ($errorB && !$errorA) {
  209.                     return -1;
  210.                 }
  211.                 $localeA '.locale' === substr($nameA, -7);
  212.                 $localeB '.locale' === substr($nameB, -7);
  213.                 if ($localeA && !$localeB) {
  214.                     return -1;
  215.                 }
  216.                 if ($localeB && !$localeA) {
  217.                     return 1;
  218.                 }
  219.                 return $this->compareRoutes($a$b$languages);
  220.             }
  221.         );
  222.     }
  223. }