vendor/pimcore/pimcore/lib/Routing/Dynamic/DocumentRouteHandler.php line 149

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4.  * Pimcore
  5.  *
  6.  * This source file is available under two different licenses:
  7.  * - GNU General Public License version 3 (GPLv3)
  8.  * - Pimcore Commercial License (PCL)
  9.  * Full copyright and license information is available in
  10.  * LICENSE.md which is distributed with this source code.
  11.  *
  12.  *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  13.  *  @license    http://www.pimcore.org/license     GPLv3 and PCL
  14.  */
  15. namespace Pimcore\Routing\Dynamic;
  16. use Pimcore\Config;
  17. use Pimcore\Http\Request\Resolver\SiteResolver;
  18. use Pimcore\Http\Request\Resolver\StaticPageResolver;
  19. use Pimcore\Http\RequestHelper;
  20. use Pimcore\Model\Document;
  21. use Pimcore\Model\Document\Page;
  22. use Pimcore\Routing\DocumentRoute;
  23. use Symfony\Component\Routing\Exception\RouteNotFoundException;
  24. use Symfony\Component\Routing\RouteCollection;
  25. /**
  26.  * @internal
  27.  */
  28. final class DocumentRouteHandler implements DynamicRouteHandlerInterface
  29. {
  30.     /**
  31.      * @var Document\Service
  32.      */
  33.     private $documentService;
  34.     /**
  35.      * @var SiteResolver
  36.      */
  37.     private $siteResolver;
  38.     /**
  39.      * @var RequestHelper
  40.      */
  41.     private $requestHelper;
  42.     /**
  43.      * Determines if unpublished documents should be matched, even when not in admin mode. This
  44.      * is mainly needed for maintencance jobs/scripts.
  45.      *
  46.      * @var bool
  47.      */
  48.     private $forceHandleUnpublishedDocuments false;
  49.     /**
  50.      * @var array
  51.      */
  52.     private $directRouteDocumentTypes = [];
  53.     /**
  54.      * @var Config
  55.      */
  56.     private $config;
  57.     /**
  58.      * @var StaticPageResolver
  59.      */
  60.     private StaticPageResolver $staticPageResolver;
  61.     /**
  62.      * @param Document\Service $documentService
  63.      * @param SiteResolver $siteResolver
  64.      * @param RequestHelper $requestHelper
  65.      * @param Config $config
  66.      * @param StaticPageResolver $staticPageResolver
  67.      */
  68.     public function __construct(
  69.         Document\Service $documentService,
  70.         SiteResolver $siteResolver,
  71.         RequestHelper $requestHelper,
  72.         Config $config,
  73.         StaticPageResolver $staticPageResolver
  74.     ) {
  75.         $this->documentService $documentService;
  76.         $this->siteResolver $siteResolver;
  77.         $this->requestHelper $requestHelper;
  78.         $this->config $config;
  79.         $this->staticPageResolver $staticPageResolver;
  80.     }
  81.     public function setForceHandleUnpublishedDocuments(bool $handle)
  82.     {
  83.         $this->forceHandleUnpublishedDocuments $handle;
  84.     }
  85.     /**
  86.      * @return array
  87.      */
  88.     public function getDirectRouteDocumentTypes()
  89.     {
  90.         if (empty($this->directRouteDocumentTypes)) {
  91.             $routingConfig \Pimcore\Config::getSystemConfiguration('routing');
  92.             $this->directRouteDocumentTypes $routingConfig['direct_route_document_types'];
  93.         }
  94.         return $this->directRouteDocumentTypes;
  95.     }
  96.     /**
  97.      * @deprecated will be removed in Pimcore 11
  98.      *
  99.      * @param string $type
  100.      */
  101.     public function addDirectRouteDocumentType($type)
  102.     {
  103.         trigger_deprecation(
  104.             'pimcore/pimcore',
  105.             '10.1',
  106.             'The DocumentRouteHandler::addDirectRouteDocumentType() method is deprecated, use pimcore.routing.direct_route_document_types config instead.'
  107.         );
  108.         if (!in_array($type$this->getDirectRouteDocumentTypes())) {
  109.             $this->directRouteDocumentTypes[] = $type;
  110.         }
  111.     }
  112.     /**
  113.      * {@inheritdoc}
  114.      */
  115.     public function getRouteByName(string $name)
  116.     {
  117.         if (preg_match('/^document_(\d+)$/'$name$match)) {
  118.             $document Document::getById((int) $match[1]);
  119.             if ($this->isDirectRouteDocument($document)) {
  120.                 return $this->buildRouteForDocument($document);
  121.             }
  122.         }
  123.         throw new RouteNotFoundException(sprintf("Route for name '%s' was not found"$name));
  124.     }
  125.     /**
  126.      * {@inheritdoc}
  127.      */
  128.     public function matchRequest(RouteCollection $collectionDynamicRequestContext $context)
  129.     {
  130.         $document Document::getByPath($context->getPath());
  131.         // check for a pretty url inside a site
  132.         if (!$document && $this->siteResolver->isSiteRequest($context->getRequest())) {
  133.             $site $this->siteResolver->getSite($context->getRequest());
  134.             $sitePrettyDocId $this->documentService->getDao()->getDocumentIdByPrettyUrlInSite($site$context->getOriginalPath());
  135.             if ($sitePrettyDocId) {
  136.                 if ($sitePrettyDoc Document::getById($sitePrettyDocId)) {
  137.                     $document $sitePrettyDoc;
  138.                     // TODO set pretty path via siteResolver?
  139.                     // undo the modification of the path by the site detection (prefixing with site root path)
  140.                     // this is not necessary when using pretty-urls and will cause problems when validating the
  141.                     // prettyUrl later (redirecting to the prettyUrl in the case the page was called by the real path)
  142.                     $context->setPath($context->getOriginalPath());
  143.                 }
  144.             }
  145.         }
  146.         // check for a parent hardlink with children
  147.         if (!$document instanceof Document) {
  148.             $hardlinkedParentDocument $this->documentService->getNearestDocumentByPath($context->getPath(), true);
  149.             if ($hardlinkedParentDocument instanceof Document\Hardlink) {
  150.                 if ($hardLinkedDocument Document\Hardlink\Service::getChildByPath($hardlinkedParentDocument$context->getPath())) {
  151.                     $document $hardLinkedDocument;
  152.                 }
  153.             }
  154.         }
  155.         if ($document && $document instanceof Document) {
  156.             if ($route $this->buildRouteForDocument($document$context)) {
  157.                 $collection->add($route->getRouteKey(), $route);
  158.             }
  159.         }
  160.     }
  161.     /**
  162.      * Build a route for a document. Context is only set from match mode, not when generating URLs.
  163.      *
  164.      * @param Document $document
  165.      * @param DynamicRequestContext|null $context
  166.      *
  167.      * @return DocumentRoute|null
  168.      */
  169.     public function buildRouteForDocument(Document $documentDynamicRequestContext $context null)
  170.     {
  171.         // check for direct hardlink
  172.         if ($document instanceof Document\Hardlink) {
  173.             $document Document\Hardlink\Service::wrap($document);
  174.             if (!$document) {
  175.                 return null;
  176.             }
  177.         }
  178.         $route = new DocumentRoute($document->getFullPath());
  179.         $route->setOption('utf8'true);
  180.         // coming from matching -> set route path the currently matched one
  181.         if (null !== $context) {
  182.             $route->setPath($context->getOriginalPath());
  183.         }
  184.         $route->setDefault('_locale'$document->getProperty('language'));
  185.         $route->setDocument($document);
  186.         if ($this->isDirectRouteDocument($document)) {
  187.             /** @var Document\PageSnippet $document */
  188.             $route $this->handleDirectRouteDocument($document$route$context);
  189.         } elseif ($document->getType() === 'link') {
  190.             /** @var Document\Link $document */
  191.             $route $this->handleLinkDocument($document$route);
  192.         }
  193.         return $route;
  194.     }
  195.     /**
  196.      * Handle route params for link document
  197.      *
  198.      * @param Document\Link $document
  199.      * @param DocumentRoute $route
  200.      *
  201.      * @return DocumentRoute
  202.      */
  203.     private function handleLinkDocument(Document\Link $documentDocumentRoute $route)
  204.     {
  205.         $route->setDefault('_controller''Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction');
  206.         $route->setDefault('path'$document->getHref());
  207.         $route->setDefault('permanent'true);
  208.         return $route;
  209.     }
  210.     /**
  211.      * Handle direct route documents (not link)
  212.      *
  213.      * @param Document\PageSnippet $document
  214.      * @param DocumentRoute $route
  215.      * @param DynamicRequestContext|null $context
  216.      *
  217.      * @return DocumentRoute|null
  218.      */
  219.     private function handleDirectRouteDocument(
  220.         Document\PageSnippet $document,
  221.         DocumentRoute $route,
  222.         DynamicRequestContext $context null
  223.     ) {
  224.         // if we have a request in context, we're currently in match mode (not generating URLs) -> only match when frontend request by admin
  225.         try {
  226.             $request $context $context->getRequest() : $this->requestHelper->getMainRequest();
  227.             $isAdminRequest $this->requestHelper->isFrontendRequestByAdmin($request);
  228.         } catch (\LogicException $e) {
  229.             // catch logic exception here - when the exception fires, it is no admin request
  230.             $isAdminRequest false;
  231.         }
  232.         // abort if document is not published and the request is no admin request
  233.         // and matching unpublished documents was not forced
  234.         if (!$document->isPublished()) {
  235.             if (!($isAdminRequest || $this->forceHandleUnpublishedDocuments)) {
  236.                 return null;
  237.             }
  238.         }
  239.         if (!$isAdminRequest && null !== $context) {
  240.             // check for redirects (pretty URL, SEO) when not in admin mode and while matching (not generating route)
  241.             if ($redirectRoute $this->handleDirectRouteRedirect($document$route$context)) {
  242.                 return $redirectRoute;
  243.             }
  244.             // set static page context
  245.             if ($document instanceof Page && $document->getStaticGeneratorEnabled()) {
  246.                 $this->staticPageResolver->setStaticPageContext($context->getRequest());
  247.             }
  248.         }
  249.         // Use latest version, if available, when the request is admin request
  250.         // so then route should be built based on latest Document settings
  251.         // https://github.com/pimcore/pimcore/issues/9644
  252.         if ($isAdminRequest) {
  253.             $latestVersion $document->getLatestVersion();
  254.             if ($latestVersion) {
  255.                 $latestDoc $latestVersion->loadData();
  256.                 if ($latestDoc instanceof Document\PageSnippet) {
  257.                     $document $latestDoc;
  258.                 }
  259.             }
  260.         }
  261.         return $this->buildRouteForPageSnippetDocument($document$route);
  262.     }
  263.     /**
  264.      * Handle document redirects (pretty url, SEO without trailing slash)
  265.      *
  266.      * @param Document\PageSnippet $document
  267.      * @param DocumentRoute $route
  268.      * @param DynamicRequestContext|null $context
  269.      *
  270.      * @return DocumentRoute|null
  271.      */
  272.     private function handleDirectRouteRedirect(
  273.         Document\PageSnippet $document,
  274.         DocumentRoute $route,
  275.         DynamicRequestContext $context null
  276.     ) {
  277.         $redirectTargetUrl $context->getOriginalPath();
  278.         // check for a pretty url, and if the document is called by that, otherwise redirect to pretty url
  279.         if ($document instanceof Document\Page && !$document instanceof Document\Hardlink\Wrapper\WrapperInterface) {
  280.             if ($prettyUrl $document->getPrettyUrl()) {
  281.                 if (rtrim(strtolower($prettyUrl), ' /') !== rtrim(strtolower($context->getOriginalPath()), '/')) {
  282.                     $redirectTargetUrl $prettyUrl;
  283.                 }
  284.             }
  285.         }
  286.         // check for a trailing slash in path, if exists, redirect to this page without the slash
  287.         // the only reason for this is: SEO, Analytics, ... there is no system specific reason, pimcore would work also with a trailing slash without problems
  288.         // use $originalPath because of the sites
  289.         // only do redirecting with GET requests
  290.         if ($context->getRequest()->getMethod() === 'GET') {
  291.             if (($this->config['documents']['allow_trailing_slash'] ?? null) === 'no') {
  292.                 if ($redirectTargetUrl !== '/' && substr($redirectTargetUrl, -1) === '/') {
  293.                     $redirectTargetUrl rtrim($redirectTargetUrl'/');
  294.                 }
  295.             }
  296.             // only allow the original key of a document to be the URL (lowercase/uppercase)
  297.             if ($redirectTargetUrl !== '/' && rtrim($redirectTargetUrl'/') !== rawurldecode($document->getFullPath())) {
  298.                 $redirectTargetUrl $document->getFullPath();
  299.             }
  300.         }
  301.         if (null !== $redirectTargetUrl && $redirectTargetUrl !== $context->getOriginalPath()) {
  302.             $route->setDefault('_controller''Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction');
  303.             $route->setDefault('path'$redirectTargetUrl);
  304.             $route->setDefault('permanent'true);
  305.             return $route;
  306.         }
  307.         return null;
  308.     }
  309.     /**
  310.      * Handle page snippet route (controller, action, view)
  311.      *
  312.      * @param Document\PageSnippet $document
  313.      * @param DocumentRoute $route
  314.      *
  315.      * @return DocumentRoute
  316.      */
  317.     private function buildRouteForPageSnippetDocument(Document\PageSnippet $documentDocumentRoute $route)
  318.     {
  319.         $route->setDefault('_controller'$document->getController());
  320.         if ($document->getTemplate()) {
  321.             $route->setDefault('_template'$document->getTemplate());
  322.         }
  323.         return $route;
  324.     }
  325.     /**
  326.      * Check if document is can be used to generate a route
  327.      *
  328.      * @param Document|null $document
  329.      *
  330.      * @return bool
  331.      */
  332.     private function isDirectRouteDocument($document)
  333.     {
  334.         if ($document instanceof Document\PageSnippet) {
  335.             if (in_array($document->getType(), $this->getDirectRouteDocumentTypes())) {
  336.                 return true;
  337.             }
  338.         }
  339.         return false;
  340.     }
  341. }