<?php
declare(strict_types=1);
/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Enterprise License (PEL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PEL
*/
namespace Pimcore\Routing\Dynamic;
use Pimcore\Config;
use Pimcore\Controller\Config\ConfigNormalizer;
use Pimcore\Http\Request\Resolver\SiteResolver;
use Pimcore\Http\RequestHelper;
use Pimcore\Model\Document;
use Pimcore\Routing\DocumentRoute;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\RouteCollection;
class DocumentRouteHandler implements DynamicRouteHandlerInterface
{
/**
* @var Document\Service
*/
private $documentService;
/**
* @var SiteResolver
*/
private $siteResolver;
/**
* @var RequestHelper
*/
private $requestHelper;
/**
* @var ConfigNormalizer
*/
private $configNormalizer;
/**
* Determines if unpublished documents should be matched, even when not in admin mode. This
* is mainly needed for maintencance jobs/scripts.
*
* @var bool
*/
private $forceHandleUnpublishedDocuments = false;
/**
* @var array
*/
private $directRouteDocumentTypes = ['page', 'snippet', 'email', 'newsletter', 'printpage', 'printcontainer'];
/**
* @param Document\Service $documentService
* @param SiteResolver $siteResolver
* @param RequestHelper $requestHelper
* @param ConfigNormalizer $configNormalizer
*/
public function __construct(
Document\Service $documentService,
SiteResolver $siteResolver,
RequestHelper $requestHelper,
ConfigNormalizer $configNormalizer
) {
$this->documentService = $documentService;
$this->siteResolver = $siteResolver;
$this->requestHelper = $requestHelper;
$this->configNormalizer = $configNormalizer;
}
public function setForceHandleUnpublishedDocuments(bool $handle)
{
$this->forceHandleUnpublishedDocuments = $handle;
}
/**
* @return array
*/
public function getDirectRouteDocumentTypes()
{
return $this->directRouteDocumentTypes;
}
/**
* @param string $type
*/
public function addDirectRouteDocumentType($type)
{
if (!in_array($type, $this->directRouteDocumentTypes)) {
$this->directRouteDocumentTypes[] = $type;
}
}
/**
* @inheritDoc
*/
public function getRouteByName(string $name)
{
if (preg_match('/^document_(\d+)$/', $name, $match)) {
$document = Document::getById($match[1]);
if ($this->isDirectRouteDocument($document) && $this->isDocumentSupported($document)) {
return $this->buildRouteForDocument($document);
}
}
throw new RouteNotFoundException(sprintf("Route for name '%s' was not found", $name));
}
/**
* @inheritDoc
*/
public function matchRequest(RouteCollection $collection, DynamicRequestContext $context)
{
$document = Document::getByPath($context->getPath());
// check for a pretty url inside a site
if (!$document && $this->siteResolver->isSiteRequest($context->getRequest())) {
$site = $this->siteResolver->getSite($context->getRequest());
$sitePrettyDocId = $this->documentService->getDao()->getDocumentIdByPrettyUrlInSite($site, $context->getOriginalPath());
if ($sitePrettyDocId) {
if ($sitePrettyDoc = Document::getById($sitePrettyDocId)) {
$document = $sitePrettyDoc;
// TODO set pretty path via siteResolver?
// undo the modification of the path by the site detection (prefixing with site root path)
// this is not necessary when using pretty-urls and will cause problems when validating the
// prettyUrl later (redirecting to the prettyUrl in the case the page was called by the real path)
$context->setPath($context->getOriginalPath());
}
}
}
// check for a parent hardlink with childs
if (!$document instanceof Document) {
$hardlinkedParentDocument = $this->documentService->getNearestDocumentByPath($context->getPath(), true);
if ($hardlinkedParentDocument instanceof Document\Hardlink) {
if ($hardLinkedDocument = Document\Hardlink\Service::getChildByPath($hardlinkedParentDocument, $context->getPath())) {
$document = $hardLinkedDocument;
}
}
}
if ($document && $document instanceof Document) {
if ($route = $this->buildRouteForDocument($document, $context)) {
$collection->add($route->getRouteKey(), $route);
}
}
}
/**
* Build a route for a document. Context is only set from match mode, not when generating URLs.
*
* @param Document $document
* @param DynamicRequestContext|null $context
*
* @return DocumentRoute|null
*/
public function buildRouteForDocument(Document $document, DynamicRequestContext $context = null)
{
// check for direct hardlink
if ($document instanceof Document\Hardlink) {
$document = Document\Hardlink\Service::wrap($document);
if (!$document) {
return null;
}
}
// check if document should be handled (not legacy)
if (!$this->isDocumentSupported($document)) {
return null;
}
$route = new DocumentRoute($document->getFullPath());
// coming from matching -> set route path the currently matched one
if (null !== $context) {
$route->setPath($context->getOriginalPath());
}
$route->setDefault('_locale', $document->getProperty('language'));
$route->setDocument($document);
if ($this->isDirectRouteDocument($document)) {
/** @var Document\PageSnippet $document */
$route = $this->handleDirectRouteDocument($document, $route, $context);
} elseif ($document->getType() === 'link') {
/** @var Document\Link $document */
$route = $this->handleLinkDocument($document, $route);
}
return $route;
}
/**
* Handle route params for link document
*
* @param Document\Link $document
* @param DocumentRoute $route
*
* @return DocumentRoute
*/
private function handleLinkDocument(Document\Link $document, DocumentRoute $route)
{
$route->setDefault('_controller', 'FrameworkBundle:Redirect:urlRedirect');
$route->setDefault('path', $document->getHref());
$route->setDefault('permanent', true);
return $route;
}
/**
* Handle direct route documents (not link)
*
* @param Document\PageSnippet $document
* @param DocumentRoute $route
* @param DynamicRequestContext|null $context
*
* @return DocumentRoute|null
*/
private function handleDirectRouteDocument(
Document\PageSnippet $document,
DocumentRoute $route,
DynamicRequestContext $context = null
) {
// if we have a request we're currently in match mode (not generating URLs) -> only match when frontend request by admin
try {
$request = null;
if ($context) {
$request = $context->getRequest();
}
$isAdminRequest = $this->requestHelper->isFrontendRequestByAdmin($request);
} catch (\LogicException $e) {
// catch logic exception here - when the exception fires, it is no admin request
$isAdminRequest = false;
}
// abort if document is not published and the request is no admin request
// and matching unpublished documents was not forced
if (!$document->isPublished()) {
if (!($isAdminRequest || $this->forceHandleUnpublishedDocuments)) {
return null;
}
}
if (!$isAdminRequest && null !== $context) {
// check for redirects (pretty URL, SEO) when not in admin mode and while matching (not generating route)
if ($redirectRoute = $this->handleDirectRouteRedirect($document, $route, $context)) {
return $redirectRoute;
}
}
return $this->buildRouteForPageSnippetDocument($document, $route);
}
/**
* Handle document redirects (pretty url, SEO without trailing slash)
*
* @param Document\PageSnippet $document
* @param DocumentRoute $route
* @param DynamicRequestContext|null $context
*
* @return DocumentRoute|null
*/
private function handleDirectRouteRedirect(
Document\PageSnippet $document,
DocumentRoute $route,
DynamicRequestContext $context = null
) {
$redirectTargetUrl = $context->getOriginalPath();
// check for a pretty url, and if the document is called by that, otherwise redirect to pretty url
if ($document instanceof Document\Page && !$document instanceof Document\Hardlink\Wrapper\WrapperInterface) {
if ($prettyUrl = $document->getPrettyUrl()) {
if (rtrim(strtolower($prettyUrl), ' /') !== rtrim(strtolower($context->getOriginalPath()), '/')) {
$redirectTargetUrl = $prettyUrl;
}
}
}
// check for a trailing slash in path, if exists, redirect to this page without the slash
// the only reason for this is: SEO, Analytics, ... there is no system specific reason, pimcore would work also with a trailing slash without problems
// use $originalPath because of the sites
// only do redirecting with GET requests
if ($context->getRequest()->getMethod() === 'GET') {
$config = Config::getSystemConfig();
if ($config->documents->allowtrailingslash) {
if ($config->documents->allowtrailingslash === 'no') {
if ($redirectTargetUrl !== '/' && substr($redirectTargetUrl, -1) === '/') {
$redirectTargetUrl = rtrim($redirectTargetUrl, '/');
}
}
}
// only allow the original key of a document to be the URL (lowercase/uppercase)
if ($redirectTargetUrl !== '/' && rtrim($redirectTargetUrl, '/') !== rawurldecode($document->getFullPath())) {
$redirectTargetUrl = $document->getFullPath();
}
}
if (null !== $redirectTargetUrl && $redirectTargetUrl !== $context->getOriginalPath()) {
$route->setDefault('_controller', 'FrameworkBundle:Redirect:urlRedirect');
$route->setDefault('path', $redirectTargetUrl);
$route->setDefault('permanent', true);
return $route;
}
}
/**
* Handle page snippet route (controller, action, view)
*
* @param Document\PageSnippet $document
* @param DocumentRoute $route
*
* @return DocumentRoute
*/
private function buildRouteForPageSnippetDocument(Document\PageSnippet $document, DocumentRoute $route)
{
$controller = $this->configNormalizer->formatControllerReference(
$document->getModule(),
$document->getController(),
$document->getAction()
);
$route->setDefault('_controller', $controller);
if ($document->getTemplate()) {
$template = $this->configNormalizer->normalizeTemplateName($document->getTemplate());
$route->setDefault('_template', $template);
}
return $route;
}
/**
* Check if document is can be used to generate a route
*
* @param $document
*
* @return bool
*/
private function isDirectRouteDocument($document)
{
if ($document instanceof Document\PageSnippet) {
if (in_array($document->getType(), $this->getDirectRouteDocumentTypes())) {
return true;
}
}
return false;
}
/**
* @param Document $document
*
* @return bool
*/
private function isDocumentSupported(Document $document)
{
return !$document->doRenderWithLegacyStack();
}
}