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

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