1009 lines
50 KiB
PHP
1009 lines
50 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Frontend\Typolink;
|
|
|
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use Psr\Http\Message\UriInterface;
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
use TYPO3\CMS\Core\Cache\CacheTag;
|
|
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
|
use TYPO3\CMS\Core\Configuration\Features;
|
|
use TYPO3\CMS\Core\Context\Context;
|
|
use TYPO3\CMS\Core\Context\LanguageAspect;
|
|
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
|
|
use TYPO3\CMS\Core\Database\Connection;
|
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
|
use TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry;
|
|
use TYPO3\CMS\Core\Domain\Access\RecordAccessVoter;
|
|
use TYPO3\CMS\Core\Domain\Page;
|
|
use TYPO3\CMS\Core\Domain\RecordFactory;
|
|
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
|
|
use TYPO3\CMS\Core\Error\Http\LinkedPageNotResolvableException;
|
|
use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
|
|
use TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException;
|
|
use TYPO3\CMS\Core\Exception\Page\CircularPageReferenceChainException;
|
|
use TYPO3\CMS\Core\Exception\Page\PageReferenceResolvingReachedIterationLimitException;
|
|
use TYPO3\CMS\Core\Exception\Page\RootLineException;
|
|
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
|
|
use TYPO3\CMS\Core\Http\Uri;
|
|
use TYPO3\CMS\Core\LinkHandling\LinkService;
|
|
use TYPO3\CMS\Core\LinkHandling\PageTypeLinkResolver;
|
|
use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService;
|
|
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
|
|
use TYPO3\CMS\Core\Routing\PageArguments;
|
|
use TYPO3\CMS\Core\Routing\RouterInterface;
|
|
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
|
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
|
use TYPO3\CMS\Core\Site\Entity\Site;
|
|
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
|
|
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
|
|
use TYPO3\CMS\Core\Site\SiteFinder;
|
|
use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility;
|
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Core\Utility\HttpUtility;
|
|
use TYPO3\CMS\Core\Utility\MathUtility;
|
|
use TYPO3\CMS\Core\Utility\RootlineUtility;
|
|
use TYPO3\CMS\Frontend\Cache\CacheLifetimeCalculator;
|
|
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
|
use TYPO3\CMS\Frontend\Event\ModifyPageLinkConfigurationEvent;
|
|
use TYPO3\CMS\Frontend\Page\PageInformation;
|
|
|
|
/**
|
|
* Builds a TypoLink to a certain page
|
|
*/
|
|
class PageLinkBuilder extends AbstractTypolinkBuilder implements TypolinkBuilderInterface
|
|
{
|
|
protected ContentObjectRenderer $contentObjectRenderer;
|
|
|
|
public function __construct(
|
|
protected readonly SiteFinder $siteFinder,
|
|
protected readonly EventDispatcherInterface $eventDispatcher,
|
|
protected readonly CacheLifetimeCalculator $cacheLifetimeCalculator,
|
|
protected readonly Features $features,
|
|
protected readonly TcaSchemaFactory $tcaSchemaFactory,
|
|
protected readonly RecordAccessVoter $recordAccessVoter,
|
|
#[Autowire(service: 'cache.runtime')]
|
|
protected readonly FrontendInterface $runtimeCache,
|
|
protected readonly LinkVarsCalculator $linkVarsCalculator,
|
|
protected readonly LinkService $linkService,
|
|
protected readonly TypoLinkCodecService $linkCodecService,
|
|
protected readonly PageTypeLinkResolver $pageTypeLinkResolver,
|
|
protected readonly LoggerInterface $logger,
|
|
protected readonly PageDoktypeRegistry $pageDoktypeRegistry,
|
|
protected readonly RecordFactory $recordFactory,
|
|
protected readonly PageRepository $pageRepository,
|
|
) {}
|
|
|
|
public function buildLink(array $linkDetails, array $configuration, ServerRequestInterface $request, string $linkText = ''): LinkResultInterface
|
|
{
|
|
$contentObjectRenderer = $request->getAttribute('currentContentObject');
|
|
if ($contentObjectRenderer === null) {
|
|
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
|
|
$contentObjectRenderer->setRequest($request);
|
|
}
|
|
$this->contentObjectRenderer = $contentObjectRenderer;
|
|
$target = $linkDetails['target'] ?? '';
|
|
$configuration['additionalParams'] = $configuration['additionalParams'] ?? '';
|
|
$configuration['queryParameters'] = (array)($configuration['queryParameters'] ?? []);
|
|
$treatAsExternalLink = false;
|
|
$url = null;
|
|
$linkResultType = LinkService::TYPE_PAGE;
|
|
|
|
if (empty($linkDetails['pageuid']) || $linkDetails['pageuid'] === 'current') {
|
|
// If no id is given try to fetch it from PageInformation attribute, else fetch it from site.
|
|
$pageId = $request->getAttribute('frontend.page.information')?->getId();
|
|
if ($pageId === null) {
|
|
$site = $request->getAttribute('site');
|
|
if ($site !== null) {
|
|
$pageId = $site->getRootPageId();
|
|
} else {
|
|
// @todo: We can usually expect a site to be always set. This fallback here may only be
|
|
// required due to incomplete setup in transform.html VH functional test?!
|
|
$allSites = $this->siteFinder->getAllSites();
|
|
$firstSite = reset($allSites);
|
|
$pageId = $firstSite->getRootPageId();
|
|
}
|
|
}
|
|
$linkDetails['pageuid'] = $pageId;
|
|
}
|
|
|
|
// Link to page even if access is missing?
|
|
$frontendTypoScriptConfigArray = $request->getAttribute('frontend.typoscript')?->getConfigArray();
|
|
if (isset($configuration['linkAccessRestrictedPages'])) {
|
|
$disableGroupAccessCheck = (bool)$configuration['linkAccessRestrictedPages'];
|
|
} else {
|
|
$disableGroupAccessCheck = (bool)($frontendTypoScriptConfigArray['typolinkLinkAccessRestrictedPages'] ?? false);
|
|
}
|
|
|
|
// Looking up the page record to verify its existence, resolving shortcuts and links to pages
|
|
$originalPage = $this->resolvePage($linkDetails, $configuration, $disableGroupAccessCheck, false);
|
|
$resolvedPage = $this->resolvePage($linkDetails, $configuration, $disableGroupAccessCheck);
|
|
|
|
if (empty($resolvedPage)) {
|
|
throw new UnableToLinkException('Page id "' . $linkDetails['pageuid'] . '" was not found, so "' . $linkText . '" was not linked.', 1490987336, null, $linkText);
|
|
}
|
|
|
|
$fragment = $this->calculateUrlFragment($configuration, $linkDetails);
|
|
$queryParameters = $this->calculateQueryParameters($configuration, $linkDetails);
|
|
// Add MP parameter
|
|
$mountPointParameter = $this->calculateMountPointParameters($resolvedPage, $disableGroupAccessCheck, $linkText);
|
|
if ($mountPointParameter !== null) {
|
|
$queryParameters['MP'] = $mountPointParameter;
|
|
}
|
|
|
|
$event = new ModifyPageLinkConfigurationEvent($configuration, $linkDetails, $resolvedPage, $queryParameters, $fragment, $request);
|
|
$event = $this->eventDispatcher->dispatch($event);
|
|
$configuration = $event->getConfiguration();
|
|
$resolvedPage = $event->getPage();
|
|
$queryParameters = $event->getQueryParameters();
|
|
$fragment = $event->getFragment();
|
|
|
|
// Check if the target page has a site configuration
|
|
try {
|
|
$siteOfTargetPage = $this->siteFinder->getSiteByPageId((int)$resolvedPage['uid'], null, $queryParameters['MP'] ?? '');
|
|
$currentSite = $this->getCurrentSite();
|
|
} catch (SiteNotFoundException $e) {
|
|
// Usually happens in tests, as sites with configuration should be available everywhere.
|
|
$siteOfTargetPage = null;
|
|
$currentSite = null;
|
|
}
|
|
if ($siteOfTargetPage === null) {
|
|
throw new UnableToLinkException('Could not link to page with ID: ' . $resolvedPage['uid'], 1546887172, null, $linkText);
|
|
}
|
|
|
|
try {
|
|
$siteLanguageOfTargetPage = $this->getSiteLanguageOfTargetPage($siteOfTargetPage, (string)($configuration['language'] ?? 'current'));
|
|
} catch (UnableToLinkException $e) {
|
|
throw new UnableToLinkException($e->getMessage(), $e->getCode(), $e, $linkText);
|
|
}
|
|
$languageAspect = LanguageAspectFactory::createFromSiteLanguage($siteLanguageOfTargetPage);
|
|
$pageRepository = $this->buildPageRepository($languageAspect);
|
|
|
|
// Now overlay the page in the target language, in order to have valid title attributes etc.
|
|
if ($siteLanguageOfTargetPage->getLanguageId() > 0) {
|
|
$pageObject = $configuration['page'] ?? null;
|
|
if ($pageObject instanceof Page
|
|
&& $pageObject->getPageId() === (int)$resolvedPage['uid'] // No MP/Shortcut changes
|
|
&& !$event->pageWasModified()
|
|
&& (
|
|
$pageObject->getLanguageId() === $languageAspect->getId()
|
|
|| $pageObject->getRequestedLanguage() === $languageAspect->getId() // Page is suitable for that language
|
|
|| $pageObject->getLanguageId() === 0 // No translation found
|
|
)
|
|
) {
|
|
$resolvedPage = $pageObject->toArray(true);
|
|
} else {
|
|
$resolvedPage = $pageRepository->getLanguageOverlay('pages', $resolvedPage);
|
|
}
|
|
|
|
// Check if the translated page is a shortcut, but the default page wasn't a shortcut, so this is
|
|
// resolved as well, see ScenarioDTest in functional tests.
|
|
// Currently not supported: When this is the case (only a translated page is a shortcut),
|
|
// but the page links to a different site.
|
|
$shortcutPage = $this->resolveShortcutPage($resolvedPage, $pageRepository, $disableGroupAccessCheck);
|
|
if (!empty($shortcutPage)) {
|
|
$resolvedPage = $shortcutPage;
|
|
}
|
|
|
|
if ((int)($originalPage['doktype'] ?? 0) === PageRepository::DOKTYPE_LINK) {
|
|
$originalPage = $pageRepository->getLanguageOverlay('pages', $originalPage);
|
|
}
|
|
}
|
|
// Check if the target page can be access depending on l18n_cfg
|
|
if (!$pageRepository->isPageSuitableForLanguage($resolvedPage, $languageAspect)) {
|
|
$pageTranslationVisibility = new PageTranslationVisibility((int)($resolvedPage['l18n_cfg'] ?? 0));
|
|
if ($siteLanguageOfTargetPage->getLanguageId() === 0 && $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage()) {
|
|
throw new UnableToLinkException('Default language of page "' . ($linkDetails['typoLinkParameter'] ?? 'unknown') . '" is hidden, so "' . $linkText . '" was not linked.', 1551621985, null, $linkText);
|
|
}
|
|
// If the requested language is not the default language and the page has no overlay for this language
|
|
// generating a link would cause a 404 error when using this like if one of those conditions apply:
|
|
// - The page is set to be hidden if it is not translated
|
|
// - The site configuration has a "strict" fallback set (evaluated in the Router - very early)
|
|
if ($siteLanguageOfTargetPage->getLanguageId() > 0 && !isset($resolvedPage['_LOCALIZED_UID']) && ($pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists() || $siteLanguageOfTargetPage->getFallbackType() === 'strict')) {
|
|
throw new UnableToLinkException('Fallback to default language of page "' . ($linkDetails['typoLinkParameter'] ?? 'unknown') . '" is disabled, so "' . $linkText . '" was not linked.', 1551621996, null, $linkText);
|
|
}
|
|
}
|
|
|
|
// Links are resolved via calling Typolink again (could be anything, really)
|
|
if ((int)$originalPage['doktype'] === PageRepository::DOKTYPE_LINK) {
|
|
$typolinkParts = $this->linkCodecService->decode($originalPage['link'] ?? '');
|
|
$typolinkTargetLinkParts = $this->linkService->resolve($typolinkParts['url']);
|
|
if ($typolinkTargetLinkParts['type'] === 'page') {
|
|
$treatAsExternalLink = false;
|
|
$configuration['additionalParams'] = $typolinkParts['additionalParams'];
|
|
$queryParameters = $this->calculateQueryParameters($configuration, $typolinkParts);
|
|
$target = $typolinkParts['target'];
|
|
if (MathUtility::canBeInterpretedAsInteger($typolinkTargetLinkParts['fragment'] ?? false)) {
|
|
$fragment = 'c' . $typolinkTargetLinkParts['fragment'];
|
|
} else {
|
|
$fragment = $typolinkTargetLinkParts['fragment'] ?? '';
|
|
}
|
|
if ($typolinkTargetLinkParts['pageuid'] === 'current') {
|
|
// "current" is always resolved from the immutable frontend.page.information
|
|
// request attribute (see the top of this method), so if the page whose link we
|
|
// are processing IS the page currently being rendered, resolving "current" is
|
|
// guaranteed to reproduce this exact same state forever - fail immediately
|
|
// instead of recursing at all.
|
|
$currentlyRenderedPageId = $request->getAttribute('frontend.page.information')?->getId();
|
|
if ((int)$originalPage['uid'] === $currentlyRenderedPageId) {
|
|
throw new UnableToLinkException(
|
|
'Page "' . $originalPage['uid'] . '" of type "Link" links to itself ("current page"), so "' . $linkText . '" was not linked.',
|
|
1784639469,
|
|
null,
|
|
$linkText
|
|
);
|
|
}
|
|
$configuration['parameter'] = $originalPage['link'] ?? '';
|
|
unset($configuration['parameter.']);
|
|
$linkResult = $this->contentObjectRenderer->createLink($linkText, $configuration);
|
|
$target = $linkResult->getTarget();
|
|
$url = $linkResult->getUrl();
|
|
}
|
|
// Valid references have been resolved by th
|
|
} else {
|
|
$configuration['parameter'] = $originalPage['link'] ?? '';
|
|
unset($configuration['parameter.']);
|
|
if (!isset($configuration['extTarget'])) {
|
|
$configuration['extTarget'] = (isset($originalPage['target']) && trim($originalPage['target'])) ? $originalPage['target'] : $target;
|
|
}
|
|
$linkResult = $this->contentObjectRenderer->createLink($linkText, $configuration);
|
|
$target = $linkResult->getTarget();
|
|
$url = $linkResult->getUrl();
|
|
if (empty($url)) {
|
|
throw new UnableToLinkException('Link on page "' . $originalPage['uid'] . '" does not have a proper target link, so "' . $linkText . '" was not linked.', 1551621999, null, $linkText);
|
|
}
|
|
// If the page external URL is resolved into a URL or email, this should be taken into account when compiling the final link result object
|
|
$linkResultType = $linkResult->getType();
|
|
$treatAsExternalLink = true;
|
|
}
|
|
} elseif (!$this->pageDoktypeRegistry->isPageTypeViewable((int)$resolvedPage['doktype'])) {
|
|
throw new UnableToLinkException('Link to page of type ' . $resolvedPage['doktype'] . ' is not possible.', 1742757285, null, $linkText);
|
|
}
|
|
if ($url === null) {
|
|
// Generate the URL
|
|
$url = $this->generateUrlForPageWithSiteConfiguration($resolvedPage, $siteOfTargetPage, $queryParameters, $fragment, $configuration, $request);
|
|
// no scheme => always not external
|
|
if (!$url->getScheme() || !$url->getHost()) {
|
|
$treatAsExternalLink = false;
|
|
} else {
|
|
// URL has a scheme, possibly because someone requested a full URL. So now lets check if the URL
|
|
// is on the same site pagetree. If this is the case, we'll treat it as internal
|
|
// @todo: currently this does not check if the target page is a mounted page in a different site,
|
|
// so it is treating this as an absolute URL, which is wrong
|
|
if ($currentSite && $currentSite->getRootPageId() === $siteOfTargetPage->getRootPageId()) {
|
|
$treatAsExternalLink = false;
|
|
}
|
|
}
|
|
$url = (string)$url;
|
|
}
|
|
|
|
$target = $this->calculateTargetAttribute($resolvedPage, $configuration, $treatAsExternalLink, $target);
|
|
|
|
// If link is to an access-restricted page which should be redirected, then find new URL
|
|
$result = new LinkResult($linkResultType, $url);
|
|
if ($this->shouldModifyUrlForAccessRestrictedPage($configuration, $resolvedPage, $request)) {
|
|
$url = $this->modifyUrlForAccessRestrictedPage($url, $resolvedPage, $linkDetails['pagetype'] ?? '', $request);
|
|
$result = new LinkResult($linkResultType, $url);
|
|
$additionalAttributes = (string)($frontendTypoScriptConfigArray['typolinkLinkAccessRestrictedPages.']['ATagParams'] ?? '');
|
|
if ($additionalAttributes !== '') {
|
|
$additionalAttributes = GeneralUtility::get_tag_attributes($additionalAttributes);
|
|
$result = $result->withAttributes($additionalAttributes);
|
|
}
|
|
}
|
|
|
|
$this->addPageCacheTag($request, $resolvedPage);
|
|
|
|
// Setting title if blank value to link
|
|
$linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $resolvedPage['title'] ?? '');
|
|
return $result
|
|
->withLinkConfiguration($configuration)
|
|
->withTarget($target)
|
|
->withLinkText($linkText);
|
|
}
|
|
|
|
/**
|
|
* Checks for the stdWrap "section" which returns the fragment for the generated URL.
|
|
*
|
|
* Detail: When using a section as an integer, a "c" is added, which referes to a tt_content element.
|
|
* see https://forge.typo3.org/issues/19832 for further work on this limitation.
|
|
*/
|
|
protected function calculateUrlFragment(array $conf, array $linkDetails): string
|
|
{
|
|
$fragment = trim((string)$this->contentObjectRenderer->stdWrapValue('section', $conf, $linkDetails['fragment'] ?? ''));
|
|
return ($fragment && MathUtility::canBeInterpretedAsInteger($fragment) ? 'c' : '') . $fragment;
|
|
}
|
|
|
|
/**
|
|
* Takes all given options into account to calculate the additional GET parameters for the link,
|
|
* and returns them as a clean array without duplicates.
|
|
*
|
|
* - addQueryString
|
|
* - additionalParams (+ the ones added from "parameter")
|
|
* - config.linkVars
|
|
* - type (from "parameter")
|
|
* - no_cache
|
|
*
|
|
* This also does a transformation to remove "L" and "_language" arguments and put this into the $conf array.
|
|
*
|
|
* Mount Points are added later-on.
|
|
*/
|
|
protected function calculateQueryParameters(array &$conf, array $linkDetails): array
|
|
{
|
|
if (isset($linkDetails['parameters'])) {
|
|
$conf['additionalParams'] .= '&' . ltrim($linkDetails['parameters'], '&');
|
|
}
|
|
|
|
$queryParameters = [];
|
|
$addQueryParams = ($conf['addQueryString'] ?? false) ? $this->getQueryArguments($conf['addQueryString'], $conf['addQueryString.'] ?? []) : '';
|
|
$addQueryParams .= trim((string)$this->contentObjectRenderer->stdWrapValue('additionalParams', $conf));
|
|
if ($addQueryParams === '&' || ($addQueryParams[0] ?? '') !== '&') {
|
|
$addQueryParams = '';
|
|
}
|
|
parse_str($addQueryParams, $queryParameters);
|
|
if (!empty($conf['queryParameters']) && is_array($conf['queryParameters'])) {
|
|
$queryParameters = array_replace_recursive($queryParameters, $conf['queryParameters']);
|
|
}
|
|
$linkVars = $this->calculateGlobalQueryParameters();
|
|
if ($linkVars !== '') {
|
|
$globalQueryParameters = [];
|
|
parse_str($linkVars, $globalQueryParameters);
|
|
$queryParameters = array_replace_recursive($globalQueryParameters, $queryParameters);
|
|
}
|
|
// Disable "?id=", for pages with no site configuration, this is added later-on anyway
|
|
unset($queryParameters['id']);
|
|
if ($linkDetails['pagetype'] ?? '') {
|
|
$queryParameters['type'] = $linkDetails['pagetype'];
|
|
}
|
|
$conf['no_cache'] = (string)$this->contentObjectRenderer->stdWrapValue('no_cache', $conf);
|
|
if ($conf['no_cache']) {
|
|
$queryParameters['no_cache'] = 1;
|
|
}
|
|
// Override language property if not being set already, supporting historically 'L' and
|
|
// modern '_language' arguments, giving '_language' the precedence.
|
|
if (isset($queryParameters['_language'])) {
|
|
if (!isset($conf['language'])) {
|
|
$conf['language'] = $queryParameters['_language'];
|
|
}
|
|
unset($queryParameters['_language']);
|
|
}
|
|
if (isset($queryParameters['L'])) {
|
|
if (!isset($conf['language'])) {
|
|
$conf['language'] = $queryParameters['L'];
|
|
}
|
|
unset($queryParameters['L']);
|
|
}
|
|
return $queryParameters;
|
|
}
|
|
|
|
/**
|
|
* Get "global" query parameters that should be added.
|
|
* Depends on given request query parameters plus config.linkVars TypoScript setting
|
|
* plus eventually special arguments for logged in Backend users.
|
|
*/
|
|
protected function calculateGlobalQueryParameters(): string
|
|
{
|
|
// @todo: The link builders should get the request hand over explicitly.
|
|
$request = $this->contentObjectRenderer->getRequest();
|
|
$requestQueryParams = $request->getQueryParams();
|
|
$frontendTypoScriptConfigArray = $request->getAttribute('frontend.typoscript')?->getConfigArray();
|
|
$typoScriptConfigLinkVars = (string)($frontendTypoScriptConfigArray['linkVars'] ?? '');
|
|
return $this->linkVarsCalculator->getAllowedLinkVarsFromRequest(
|
|
$typoScriptConfigLinkVars,
|
|
$requestQueryParams,
|
|
GeneralUtility::makeInstance(Context::class)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Calculates a possible "&MP=" GET parameter for this link when using mount points.
|
|
*/
|
|
protected function calculateMountPointParameters(array &$page, bool $disableGroupAccessCheck, string $linkText): ?string
|
|
{
|
|
// MountPoints, look for closest MPvar:
|
|
$mountPointPairs = [];
|
|
$frontendTypoScriptConfigArray = $this->contentObjectRenderer->getRequest()->getAttribute('frontend.typoscript')?->getConfigArray();
|
|
if (!($frontendTypoScriptConfigArray['MP_disableTypolinkClosestMPvalue'] ?? false)) {
|
|
$temp_MP = $this->getClosestMountPointValueForPage((int)$page['uid']);
|
|
if ($temp_MP) {
|
|
$mountPointPairs['closest'] = $temp_MP;
|
|
}
|
|
}
|
|
// Look for overlay Mount Point:
|
|
$mount_info = $this->pageRepository->getMountPointInfo($page['uid'], $page);
|
|
if (is_array($mount_info) && $mount_info['overlay']) {
|
|
$page = $this->pageRepository->getPage((int)$mount_info['mount_pid'], $disableGroupAccessCheck);
|
|
if (empty($page)) {
|
|
throw new UnableToLinkException('Mount point "' . $mount_info['mount_pid'] . '" was not available, so "' . $linkText . '" was not linked.', 1490987337, null, $linkText);
|
|
}
|
|
$mountPointPairs['re-map'] = $mount_info['MPvar'];
|
|
}
|
|
// Mount pages are always local and never link to another domain,
|
|
$addMountPointParameters = !empty($mountPointPairs);
|
|
// Add "&MP" var, only if the original page was NOT a shortcut to another domain
|
|
if ($addMountPointParameters && !empty($page['_SHORTCUT_ORIGINAL_PAGE_UID'])) {
|
|
$siteOfTargetPage = $this->siteFinder->getSiteByPageId((int)$page['_SHORTCUT_ORIGINAL_PAGE_UID']);
|
|
$currentSite = $this->getCurrentSite();
|
|
if ($siteOfTargetPage !== $currentSite) {
|
|
$addMountPointParameters = false;
|
|
}
|
|
}
|
|
if ($addMountPointParameters) {
|
|
return rawurlencode(implode(',', $mountPointPairs));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the final "target" attribute for a link.
|
|
*/
|
|
protected function calculateTargetAttribute(array $page, array $conf, bool $treatAsExternalLink, string $target): string
|
|
{
|
|
if ($treatAsExternalLink) {
|
|
$target = $target ?: $this->resolveTargetAttribute($conf, 'extTarget');
|
|
} else {
|
|
$target = (isset($page['target']) && trim($page['target'])) ? $page['target'] : $target;
|
|
if (empty($target)) {
|
|
$target = $this->resolveTargetAttribute($conf, 'target');
|
|
}
|
|
}
|
|
return $target;
|
|
}
|
|
|
|
/**
|
|
* Checks if config.typolinkLinkAccessRestrictedPages is set to a specific page target,
|
|
* and the current link configuration typolink.linkAccessRestrictedPages is not set
|
|
* (= which would directly link to page that is access restricted).
|
|
*
|
|
* Only happens if the target is access restricted.
|
|
* @see modifyUrlForAccessRestrictedPage
|
|
*/
|
|
protected function shouldModifyUrlForAccessRestrictedPage(array $conf, array $page, ServerRequestInterface $request): bool
|
|
{
|
|
$frontendTypoScriptConfigArray = $request->getAttribute('frontend.typoscript')?->getConfigArray();
|
|
$typolinkLinkAccessRestrictedPages = $frontendTypoScriptConfigArray['typolinkLinkAccessRestrictedPages'] ?? false;
|
|
return empty($conf['linkAccessRestrictedPages'])
|
|
&& $typolinkLinkAccessRestrictedPages
|
|
&& $typolinkLinkAccessRestrictedPages !== 'NONE'
|
|
&& !$this->recordAccessVoter->groupAccessGranted('pages', $page, GeneralUtility::makeInstance(Context::class));
|
|
}
|
|
|
|
/**
|
|
* If the target page is access restricted, and globally configured to be linked to a different page (e.g. login page)
|
|
* via config.typolinkLinkAccessRestrictedPages = 123 then the URL is modified.
|
|
* @see shouldModifyUrlForAccessRestrictedPage
|
|
*/
|
|
protected function modifyUrlForAccessRestrictedPage(string $url, array $page, string $overridePageType, ServerRequestInterface $request): string
|
|
{
|
|
$frontendTypoScriptConfigArray = $request->getAttribute('frontend.typoscript')?->getConfigArray();
|
|
$thePage = $this->pageRepository->getPage((int)($frontendTypoScriptConfigArray ['typolinkLinkAccessRestrictedPages'] ?? 0));
|
|
$addParams = str_replace(
|
|
[
|
|
'###RETURN_URL###',
|
|
'###PAGE_ID###',
|
|
],
|
|
[
|
|
rawurlencode($url),
|
|
$page['uid'],
|
|
],
|
|
$frontendTypoScriptConfigArray['typolinkLinkAccessRestrictedPages_addParams'] ?? ''
|
|
);
|
|
return $this->contentObjectRenderer->createUrl(
|
|
[
|
|
'parameter' => $thePage['uid'] . ($overridePageType ? ',' . $overridePageType : ''),
|
|
'additionalParams' => $addParams,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolves page and if a translated page was found, resolves that to its
|
|
* language parent, adjusts `$linkDetails['pageuid']` (for hook processing)
|
|
* and modifies `$configuration['language']` (for language URL generation).
|
|
*/
|
|
protected function resolvePage(array &$linkDetails, array &$configuration, bool $disableGroupAccessCheck, bool $followLinks = true): array
|
|
{
|
|
$pageRepository = $this->buildPageRepository();
|
|
// Looking up the page record to verify its existence
|
|
// This is used when a page to a translated page is executed directly.
|
|
|
|
if (isset($configuration['page']) && $configuration['page'] instanceof Page) {
|
|
$page = $configuration['page']->getTranslationSource()?->toArray(true) ?? $configuration['page']->toArray(true);
|
|
}
|
|
// A page with doktype external and ?showModal=1 in url field leads to recursion in HMENU/Sitemap.
|
|
// In the second call of this function $linkDetails['pageuid'] is different (=current page) to uid of Page
|
|
// object, and it needs to be fetched again.
|
|
if (($page['uid'] ?? false) !== $linkDetails['pageuid']) {
|
|
$page = $pageRepository->getPage((int)$linkDetails['pageuid'], $disableGroupAccessCheck);
|
|
}
|
|
|
|
if (!isset($page)) {
|
|
return [];
|
|
}
|
|
|
|
// If the page repository (= current page) does actually link to a different page
|
|
// It is needed to also resolve the page translation now, as it might have a different shortcut
|
|
// page
|
|
if (isset($configuration['language']) && $configuration['language'] !== 'current') {
|
|
$page = $pageRepository->getLanguageOverlay('pages', $page, new LanguageAspect((int)$configuration['language'], (int)$configuration['language']));
|
|
}
|
|
|
|
if ($followLinks) {
|
|
$page = $this->resolveShortcutPage($page, $pageRepository, $disableGroupAccessCheck);
|
|
}
|
|
|
|
$languageCapability = $this->tcaSchemaFactory->get('pages')->getCapability(TcaSchemaCapability::Language);
|
|
$languageField = $languageCapability->getLanguageField()->getName();
|
|
$languageParentField = $languageCapability->getTranslationOriginPointerField()->getName();
|
|
$language = (int)($page[$languageField] ?? 0);
|
|
|
|
// The page that should be linked is actually a default-language page, nothing to do here.
|
|
if ($language === 0 || empty($page[$languageParentField])) {
|
|
return $page;
|
|
}
|
|
|
|
// Let's fetch the default-language page now
|
|
$languageParentPage = $pageRepository->getPage(
|
|
(int)$page[$languageParentField],
|
|
$disableGroupAccessCheck
|
|
);
|
|
if (empty($languageParentPage)) {
|
|
return $page;
|
|
}
|
|
if ($followLinks) {
|
|
// Check for the shortcut of the default-language page
|
|
$languageParentPage = $this->resolveShortcutPage($languageParentPage, $pageRepository, $disableGroupAccessCheck);
|
|
}
|
|
|
|
// Set the "pageuid" to the default-language page ID.
|
|
$linkDetails['pageuid'] = (int)$languageParentPage['uid'];
|
|
$configuration['language'] = $language;
|
|
return $languageParentPage;
|
|
}
|
|
|
|
/**
|
|
* Checks if page is a shortcut, then resolves the target page directly
|
|
*/
|
|
protected function resolveShortcutPage(array $page, PageRepository $pageRepository, bool $disableGroupAccessCheck): array
|
|
{
|
|
try {
|
|
$page = $pageRepository->resolveShortcutPage($page, $disableGroupAccessCheck);
|
|
$page = $pageRepository->resolveLinkPage($page, $disableGroupAccessCheck);
|
|
} catch (PageNotFoundException|LinkedPageNotResolvableException|CircularPageReferenceChainException|PageReferenceResolvingReachedIterationLimitException|ShortcutTargetPageNotFoundException $e) {
|
|
$this->logger->warning($e->getMessage(), ['Exception' => $e]);
|
|
// Keep the existing page record if shortcut could not be resolved
|
|
}
|
|
return $page;
|
|
}
|
|
|
|
/**
|
|
* Fetches the requested language of a site that the link should be built for
|
|
*
|
|
* @param string $targetLanguageId "current" or the languageId
|
|
* @throws UnableToLinkException
|
|
*/
|
|
protected function getSiteLanguageOfTargetPage(Site $siteOfTargetPage, string $targetLanguageId): SiteLanguage
|
|
{
|
|
$currentSite = $this->getCurrentSite();
|
|
$currentSiteLanguage = $this->getCurrentSiteLanguage() ?? $currentSite?->getDefaultLanguage();
|
|
|
|
if ($targetLanguageId === 'current') {
|
|
$targetLanguageId = $currentSiteLanguage?->getLanguageId() ?? 0;
|
|
} else {
|
|
$targetLanguageId = (int)$targetLanguageId;
|
|
}
|
|
try {
|
|
$siteLanguageOfTargetPage = $siteOfTargetPage->getLanguageById($targetLanguageId);
|
|
} catch (\InvalidArgumentException $e) {
|
|
throw new UnableToLinkException('The target page does not have a language with ID ' . $targetLanguageId . ' configured in its site configuration.', 1535477406);
|
|
}
|
|
return $siteLanguageOfTargetPage;
|
|
}
|
|
|
|
/**
|
|
* Create a UriInterface object when linking to a page with a site configuration
|
|
*
|
|
* @throws UnableToLinkException
|
|
*/
|
|
protected function generateUrlForPageWithSiteConfiguration(array $page, Site $siteOfTargetPage, array $queryParameters, string $fragment, array $conf, ServerRequestInterface $request): UriInterface
|
|
{
|
|
$currentSite = $this->getCurrentSite();
|
|
$currentSiteLanguage = $this->getCurrentSiteLanguage() ?? $currentSite?->getDefaultLanguage();
|
|
$siteLanguageOfTargetPage = $this->getSiteLanguageOfTargetPage($siteOfTargetPage, (string)($conf['language'] ?? 'current'));
|
|
$frontendTypoScriptConfigArray = $request->getAttribute('frontend.typoscript')?->getConfigArray();
|
|
|
|
// By default, it is assumed to ab an internal link or current domain's linking scheme should be used
|
|
// Use the config option to override this.
|
|
// Global option config.forceAbsoluteUrls = 1 overrides any setting for this specific link
|
|
if ($frontendTypoScriptConfigArray['forceAbsoluteUrls'] ?? false) {
|
|
$useAbsoluteUrl = true;
|
|
} else {
|
|
$useAbsoluteUrl = $conf['forceAbsoluteUrl'] ?? false;
|
|
}
|
|
// Check if the current page equal to the site of the target page, now only set the absolute URL
|
|
// Always generate absolute URLs if no current site is set
|
|
if (
|
|
!$currentSite
|
|
|| $currentSite->getRootPageId() !== $siteOfTargetPage->getRootPageId()
|
|
|| $siteLanguageOfTargetPage->getBase()->getHost() !== $currentSiteLanguage?->getBase()?->getHost()) {
|
|
$useAbsoluteUrl = true;
|
|
}
|
|
|
|
$targetPageId = (int)($page['l10n_parent'] > 0 ? $page['l10n_parent'] : $page['uid']);
|
|
$queryParameters['_language'] = $siteLanguageOfTargetPage;
|
|
$pageObject = $this->recordFactory->createFromDatabaseRow('pages', $page);
|
|
|
|
if ($fragment
|
|
&& $useAbsoluteUrl === false
|
|
&& $currentSiteLanguage === $siteLanguageOfTargetPage
|
|
&& $targetPageId === ($request->getAttribute('frontend.page.information')?->getId() ?? 0)
|
|
&& (empty($conf['addQueryString']) || !isset($conf['addQueryString.']))
|
|
&& !($frontendTypoScriptConfigArray['baseURL'] ?? false)
|
|
&& count($queryParameters) === 1 // _language is always set
|
|
) {
|
|
$uri = (new Uri())->withFragment($fragment);
|
|
} else {
|
|
try {
|
|
$uri = $siteOfTargetPage->getRouter()->generateUri(
|
|
$pageObject,
|
|
$queryParameters,
|
|
$fragment,
|
|
$useAbsoluteUrl ? RouterInterface::ABSOLUTE_URL : RouterInterface::ABSOLUTE_PATH
|
|
);
|
|
} catch (InvalidRouteArgumentsException $e) {
|
|
throw new UnableToLinkException('The target page could not be linked. Error: ' . $e->getMessage(), 1535472406);
|
|
}
|
|
// Override scheme if absoluteUrl is set, but only if the site defines a domain/host. Fall back to site scheme and else https.
|
|
if ($useAbsoluteUrl && $uri->getHost()) {
|
|
$scheme = $conf['forceAbsoluteUrl.']['scheme'] ?? false;
|
|
if (!$scheme) {
|
|
$scheme = $uri->getScheme() ?: 'https';
|
|
}
|
|
$uri = $uri->withScheme($scheme);
|
|
}
|
|
}
|
|
|
|
return $uri;
|
|
}
|
|
|
|
/**
|
|
* The function will do its best to find a MP value that will keep the page id inside the current Mount Point rootline if any.
|
|
*
|
|
* @param int $pageId page id
|
|
* @return string MP value, prefixed with &MP= (depending on $raw)
|
|
*/
|
|
protected function getClosestMountPointValueForPage(int $pageId): string
|
|
{
|
|
$request = $this->contentObjectRenderer->getRequest();
|
|
$pageInformation = $request->getAttribute('frontend.page.information');
|
|
if (!$pageInformation instanceof PageInformation) {
|
|
return '';
|
|
}
|
|
$mountPoint = $pageInformation->getMountPoint();
|
|
if (empty($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) || $mountPoint === '') {
|
|
return '';
|
|
}
|
|
// Same page as current.
|
|
if ($pageInformation->getId() === $pageId) {
|
|
return $mountPoint;
|
|
}
|
|
$localRootLine = $pageInformation->getLocalRootLine();
|
|
if ($localRootLine === []) {
|
|
return '';
|
|
}
|
|
|
|
// Find the closest mount point
|
|
// Gets rootline of linked-to page
|
|
try {
|
|
$tCR_rootline = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get();
|
|
} catch (RootLineException) {
|
|
return '';
|
|
}
|
|
|
|
$inverseLocalRootLine = array_reverse($localRootLine);
|
|
$rl_mpArray = [];
|
|
$startMPaccu = false;
|
|
// Traverse root line of link uid and inside of that the REAL root line of current position.
|
|
foreach ($tCR_rootline as $tCR_data) {
|
|
foreach ($inverseLocalRootLine as $rlKey => $invTmplRLRec) {
|
|
// Force accumulating when in overlay mode: Links to this page have to stay within the current branch
|
|
if (($invTmplRLRec['_MOUNT_OL'] ?? false) && (int)$tCR_data['uid'] === (int)$invTmplRLRec['uid']) {
|
|
$startMPaccu = true;
|
|
}
|
|
// Accumulate MP data:
|
|
if ($startMPaccu && ($invTmplRLRec['_MP_PARAM'] ?? false)) {
|
|
$rl_mpArray[] = $invTmplRLRec['_MP_PARAM'];
|
|
}
|
|
// If two PIDs matches and this is NOT the site root, start accumulation of MP data (on the next level):
|
|
// (The check for site root is done so links to branches outside the site but sharing the site roots PID
|
|
// is NOT detected as within the branch!)
|
|
if ((int)$tCR_data['pid'] === (int)$invTmplRLRec['pid'] && count($inverseLocalRootLine) !== $rlKey + 1) {
|
|
$startMPaccu = true;
|
|
}
|
|
}
|
|
if ($startMPaccu) {
|
|
// Good enough...
|
|
break;
|
|
}
|
|
}
|
|
return !empty($rl_mpArray) ? implode(',', array_reverse($rl_mpArray)) : '';
|
|
}
|
|
|
|
/**
|
|
* Initializes the automatically created mountPointMap coming from the "config.MP_mapRootPoints" setting
|
|
* Can be called many times with overhead only the first time since then the map is generated and cached in memory.
|
|
*
|
|
* @param int $pageId Page id to return MPvar value for.
|
|
*/
|
|
public function getMountPointParameterFromRootPointMaps(int $pageId, ServerRequestInterface $request): string
|
|
{
|
|
// Create map if not found already
|
|
$frontendTypoScriptConfigArray = $request->getAttribute('frontend.typoscript')?->getConfigArray();
|
|
$mountPointMap = $this->initializeMountPointMap(
|
|
!empty($frontendTypoScriptConfigArray['MP_defaults']) ? $frontendTypoScriptConfigArray['MP_defaults'] : '',
|
|
!empty($frontendTypoScriptConfigArray['MP_mapRootPoints']) ? $frontendTypoScriptConfigArray['MP_mapRootPoints'] : ''
|
|
);
|
|
// Finding MP var for Page ID:
|
|
if (!empty($mountPointMap[$pageId])) {
|
|
return implode(',', $mountPointMap[$pageId]);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Create mount point map, based on TypoScript config.MP_mapRootPoints and config.MP_defaults.
|
|
*
|
|
* @param string $defaultMountPoints a string as defined in config.MP_defaults
|
|
* @param string $mapRootPointList a string as defined in config.MP_mapRootPoints
|
|
*/
|
|
protected function initializeMountPointMap(string $defaultMountPoints = '', string $mapRootPointList = ''): array
|
|
{
|
|
$mountPointMap = $this->runtimeCache->get('pageLinkBuilderMountPointMap') ?: [];
|
|
if (!empty($mountPointMap) || (empty($mapRootPointList) && empty($defaultMountPoints))) {
|
|
return $mountPointMap;
|
|
}
|
|
if ($defaultMountPoints) {
|
|
$defaultMountPoints = GeneralUtility::trimExplode('|', $defaultMountPoints, true);
|
|
foreach ($defaultMountPoints as $temp_p) {
|
|
[$temp_idP, $temp_MPp] = explode(':', $temp_p, 2);
|
|
$temp_ids = GeneralUtility::intExplode(',', $temp_idP);
|
|
foreach ($temp_ids as $temp_id) {
|
|
$mountPointMap[$temp_id] = trim($temp_MPp);
|
|
}
|
|
}
|
|
}
|
|
|
|
$rootPoints = GeneralUtility::trimExplode(',', strtolower($mapRootPointList), true);
|
|
$pageInformation = $this->contentObjectRenderer->getRequest()->getAttribute('frontend.page.information');
|
|
// Traverse rootpoints
|
|
foreach ($rootPoints as $p) {
|
|
$initMParray = [];
|
|
if ($p === 'root') {
|
|
$rootPage = $pageInformation->getLocalRootLine()[0];
|
|
$p = $rootPage['uid'];
|
|
if (($rootPage['_MOUNT_OL'] ?? false) && ($rootPage['_MP_PARAM'] ?? false)) {
|
|
$initMParray[] = $rootPage['_MP_PARAM'];
|
|
}
|
|
}
|
|
$this->populateMountPointMapForPageRecursively($mountPointMap, (int)$p, $initMParray);
|
|
}
|
|
$this->runtimeCache->set('pageLinkBuilderMountPointMap', $mountPointMap);
|
|
return $mountPointMap;
|
|
}
|
|
|
|
/**
|
|
* Creating mountPointMap for a certain ID root point.
|
|
*
|
|
* @param array $mountPointMap the exiting mount point map
|
|
* @param int $id Root id from which to start map creation.
|
|
* @param array $MP_array MP_array passed from root page.
|
|
* @param int $level Recursion brake. Incremented for each recursive call. 20 is the limit.
|
|
* @see getMountPointParameterFromRootPointMaps()
|
|
*/
|
|
protected function populateMountPointMapForPageRecursively(array &$mountPointMap, int $id, array $MP_array = [], int $level = 0): void
|
|
{
|
|
if ($id <= 0) {
|
|
return;
|
|
}
|
|
// First level, check id
|
|
if (!$level) {
|
|
// Find mount point if any:
|
|
$mount_info = $this->pageRepository->getMountPointInfo($id);
|
|
// Overlay mode:
|
|
if (is_array($mount_info) && $mount_info['overlay']) {
|
|
$MP_array[] = $mount_info['MPvar'];
|
|
$id = $mount_info['mount_pid'];
|
|
}
|
|
// Set mapping information for this level:
|
|
$mountPointMap[$id] = $MP_array;
|
|
// Normal mode:
|
|
if (is_array($mount_info) && !$mount_info['overlay']) {
|
|
$MP_array[] = $mount_info['MPvar'];
|
|
$id = $mount_info['mount_pid'];
|
|
}
|
|
}
|
|
if ($id && $level < 20) {
|
|
$nextLevelAcc = [];
|
|
// Select and traverse current level pages:
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
|
|
$queryBuilder->getRestrictions()
|
|
->removeAll()
|
|
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
|
$queryResult = $queryBuilder
|
|
->select('uid', 'pid', 'doktype', 'mount_pid', 'mount_pid_ol', 't3ver_state', 'l10n_parent')
|
|
->from('pages')
|
|
->where(
|
|
$queryBuilder->expr()->eq(
|
|
'pid',
|
|
$queryBuilder->createNamedParameter($id, Connection::PARAM_INT)
|
|
),
|
|
$queryBuilder->expr()->neq(
|
|
'doktype',
|
|
$queryBuilder->createNamedParameter(PageRepository::DOKTYPE_BE_USER_SECTION, Connection::PARAM_INT)
|
|
)
|
|
)->executeQuery();
|
|
while ($row = $queryResult->fetchAssociative()) {
|
|
// Find mount point if any:
|
|
$next_id = (int)$row['uid'];
|
|
$next_MP_array = $MP_array;
|
|
$mount_info = $this->pageRepository->getMountPointInfo($next_id, $row);
|
|
// Overlay mode:
|
|
if (is_array($mount_info) && $mount_info['overlay']) {
|
|
$next_MP_array[] = $mount_info['MPvar'];
|
|
$next_id = (int)$mount_info['mount_pid'];
|
|
}
|
|
if (!isset($mountPointMap[$next_id])) {
|
|
// Set mapping information for this level:
|
|
$mountPointMap[$next_id] = $next_MP_array;
|
|
// Normal mode:
|
|
if (is_array($mount_info) && !$mount_info['overlay']) {
|
|
$next_MP_array[] = $mount_info['MPvar'];
|
|
$next_id = (int)$mount_info['mount_pid'];
|
|
}
|
|
// Register recursive call
|
|
// (have to do it this way since ALL of the current level should be registered BEFORE the sublevel at any time)
|
|
$nextLevelAcc[] = [$next_id, $next_MP_array];
|
|
}
|
|
}
|
|
// Call recursively, if any:
|
|
foreach ($nextLevelAcc as $pSet) {
|
|
$this->populateMountPointMapForPageRecursively($mountPointMap, $pSet[0], $pSet[1], $level + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the query arguments and assembles them for URLs.
|
|
* By default, only the resolved query arguments from the route are used, using "untrusted" as $queryInformation
|
|
* allows to also include ANY query parameter - use with care.
|
|
*
|
|
* Arguments may be removed or set, depending on configuration.
|
|
*
|
|
* @param bool|string|int $queryInformation is set to "1", "true", "0", "false" or "untrusted"
|
|
* @param array $configuration Configuration
|
|
* @return string The URL query part (starting with a &) or empty
|
|
*/
|
|
protected function getQueryArguments(bool|string|int $queryInformation, array $configuration): string
|
|
{
|
|
if (!$queryInformation || $queryInformation === 'false') {
|
|
return '';
|
|
}
|
|
$request = $this->contentObjectRenderer->getRequest();
|
|
$pageArguments = $request->getAttribute('routing');
|
|
if (!$pageArguments instanceof PageArguments) {
|
|
return '';
|
|
}
|
|
$currentQueryArray = $pageArguments->getRouteArguments();
|
|
if ($queryInformation === 'untrusted') {
|
|
$currentQueryArray = array_replace_recursive($pageArguments->getQueryArguments(), $currentQueryArray);
|
|
}
|
|
if ($configuration['exclude'] ?? false) {
|
|
$excludeItems = array_map(urlencode(...), GeneralUtility::trimExplode(',', $configuration['exclude']));
|
|
$excludeString = implode('&', $excludeItems);
|
|
parse_str($excludeString, $excludedQueryParts);
|
|
$newQueryArray = ArrayUtility::arrayDiffKeyRecursive($currentQueryArray, $excludedQueryParts);
|
|
} else {
|
|
$newQueryArray = $currentQueryArray;
|
|
}
|
|
return HttpUtility::buildQueryString($newQueryArray, '&');
|
|
}
|
|
|
|
/**
|
|
* Check if we have a site object in the current request.
|
|
* If null, this usually means that this class was called from CLI context.
|
|
*
|
|
* @todo: We may want to see if we can't *require* a site object to be
|
|
* always set and fail here otherwise!
|
|
*/
|
|
protected function getCurrentSite(): ?SiteInterface
|
|
{
|
|
return $this->contentObjectRenderer->getRequest()->getAttribute('site');
|
|
}
|
|
|
|
/**
|
|
* If the current request has a site language, this means that the SiteResolver has detected a
|
|
* page with a site configuration and a selected language, so let's choose that one.
|
|
*/
|
|
protected function getCurrentSiteLanguage(): ?SiteLanguage
|
|
{
|
|
$request = $this->contentObjectRenderer->getRequest();
|
|
return $request->getAttribute('language') ?? $this->getCurrentSite()?->getDefaultLanguage();
|
|
}
|
|
|
|
/**
|
|
* Builds PageRepository instance without depending on global context, e.g.
|
|
* not automatically overlaying records based on current request language.
|
|
*/
|
|
protected function buildPageRepository(?LanguageAspect $languageAspect = null): PageRepository
|
|
{
|
|
// clone global context object (singleton)
|
|
$context = clone GeneralUtility::makeInstance(Context::class);
|
|
$context->setAspect('language', $languageAspect ?? new LanguageAspect());
|
|
return GeneralUtility::makeInstance(PageRepository::class, $context);
|
|
}
|
|
|
|
protected function addPageCacheTag(ServerRequestInterface $request, array $pageRecord): void
|
|
{
|
|
if ($this->features->isFeatureEnabled('frontend.cache.autoTagging')) {
|
|
$lifetime = $this->cacheLifetimeCalculator->calculateLifetimeForPage(
|
|
$pageRecord['uid'],
|
|
$pageRecord,
|
|
$request->getAttribute('frontend.typoscript')?->getConfigArray() ?? [],
|
|
GeneralUtility::makeInstance(Context::class)
|
|
);
|
|
$request->getAttribute('frontend.cache.collector')?->addCacheTags(
|
|
new CacheTag(sprintf('pages_%s', $pageRecord['uid']), $lifetime)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines whether lib.parseFunc is defined.
|
|
*/
|
|
protected function isLibParseFuncDefined(): bool
|
|
{
|
|
$configuration = $this->contentObjectRenderer->mergeTSRef(
|
|
['parseFunc' => '< lib.parseFunc'],
|
|
'parseFunc'
|
|
);
|
|
return !empty($configuration['parseFunc.']) && is_array($configuration['parseFunc.']);
|
|
}
|
|
|
|
/**
|
|
* Helper method to a fallback method parsing HTML out of it
|
|
*
|
|
* @param string $originalLinkText the original string, if empty, the fallback link text
|
|
* @param string $fallbackLinkText the string to be used.
|
|
* @return string the final text
|
|
*/
|
|
protected function parseFallbackLinkTextIfLinkTextIsEmpty(string $originalLinkText, string $fallbackLinkText): string
|
|
{
|
|
if ($originalLinkText !== '') {
|
|
return $originalLinkText;
|
|
}
|
|
if ($this->isLibParseFuncDefined()) {
|
|
return $this->contentObjectRenderer->parseFunc($fallbackLinkText, ['makelinks' => 0], '< lib.parseFunc');
|
|
}
|
|
// encode in case `lib.parseFunc` is not configured
|
|
return $this->encodeFallbackLinkTextIfLinkTextIsEmpty($originalLinkText, $fallbackLinkText);
|
|
}
|
|
}
|