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

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