Your IP : 216.73.216.43


Current Path : /var/www/surf/TYPO3/vendor/typo3/cms-frontend/Classes/Typolink/
Upload File :
Current File : //var/www/surf/TYPO3/vendor/typo3/cms-frontend/Classes/Typolink/PageLinkBuilder.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 TYPO3\CMS\Core\Cache\CacheManager;
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\Domain\Access\RecordAccessVoter;
use TYPO3\CMS\Core\Domain\Page;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
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\Routing\InvalidRouteArgumentsException;
use TYPO3\CMS\Core\Routing\PageArguments;
use TYPO3\CMS\Core\Routing\RouterInterface;
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\Controller\TypoScriptFrontendController;
use TYPO3\CMS\Frontend\Event\ModifyPageLinkConfigurationEvent;

/**
 * Builds a TypoLink to a certain page
 */
class PageLinkBuilder extends AbstractTypolinkBuilder
{
    public function build(array &$linkDetails, string $linkText, string $target, array $conf): LinkResultInterface
    {
        $linkResultType = LinkService::TYPE_PAGE;
        $tsfe = $this->getTypoScriptFrontendController();
        $conf['additionalParams'] = $conf['additionalParams'] ?? '';
        if (empty($linkDetails['pageuid']) || $linkDetails['pageuid'] === 'current') {
            // If no id is given
            $linkDetails['pageuid'] = $tsfe->id;
        }

        // Link to page even if access is missing?
        if (isset($conf['linkAccessRestrictedPages'])) {
            $disableGroupAccessCheck = (bool)($conf['linkAccessRestrictedPages'] ?? false);
        } else {
            $disableGroupAccessCheck = (bool)($tsfe->config['config']['typolinkLinkAccessRestrictedPages'] ?? false);
        }

        // Looking up the page record to verify its existence:
        $page = $this->resolvePage($linkDetails, $conf, $disableGroupAccessCheck);

        if (empty($page)) {
            throw new UnableToLinkException('Page id "' . $linkDetails['pageuid'] . '" was not found, so "' . $linkText . '" was not linked.', 1490987336, null, $linkText);
        }

        $fragment = $this->calculateUrlFragment($conf, $linkDetails);
        $queryParameters = $this->calculateQueryParameters($conf, $linkDetails);
        // Add MP parameter
        $mountPointParameter = $this->calculateMountPointParameters($page, $disableGroupAccessCheck, $linkText);
        if ($mountPointParameter !== null) {
            $queryParameters['MP'] = $mountPointParameter;
        }

        $event = new ModifyPageLinkConfigurationEvent($conf, $linkDetails, $page, $queryParameters, $fragment);
        $event = GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch($event);
        $conf = $event->getConfiguration();
        $page = $event->getPage();
        $queryParameters = $event->getQueryParameters();
        $fragment = $event->getFragment();

        // Check if the target page has a site configuration
        try {
            $siteOfTargetPage = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId((int)$page['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: ' . $page['uid'], 1546887172, null, $linkText);
        }

        try {
            $siteLanguageOfTargetPage = $this->getSiteLanguageOfTargetPage($siteOfTargetPage, (string)($conf['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 = $conf['page'] ?? null;
            if ($pageObject instanceof Page
                && $pageObject->getPageId() == $page['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
                )
            ) {
                $page = $pageObject->toArray(true);
            } else {
                $page = $pageRepository->getLanguageOverlay('pages', $page);
            }

            // 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($page, $pageRepository, $disableGroupAccessCheck);
            if (!empty($shortcutPage)) {
                $page = $shortcutPage;
            }
        }
        // Check if the target page can be access depending on l18n_cfg
        if (!$pageRepository->isPageSuitableForLanguage($page, $languageAspect)) {
            $pageTranslationVisibility = new PageTranslationVisibility((int)($page['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 (evaluated in TSFE)
            //  - The site configuration has a "strict" fallback set (evaluated in the Router - very early)
            if ($siteLanguageOfTargetPage->getLanguageId() > 0 && !isset($page['_PAGES_OVERLAY']) && ($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);
            }
        }

        $treatAsExternalLink = true;
        // External links are resolved via calling Typolink again (could be anything, really)
        if ((int)$page['doktype'] === PageRepository::DOKTYPE_LINK) {
            $conf['parameter'] = $page['url'];
            unset($conf['parameter.']);
            // Use "pages.target" as this is the requested field for external links as well
            if (!isset($conf['extTarget'])) {
                $conf['extTarget'] = (isset($page['target']) && trim($page['target'])) ? $page['target'] : $target;
            }
            $linkResultFromExternalUrl = $this->contentObjectRenderer->createLink($linkText, $conf);
            $target = $linkResultFromExternalUrl->getTarget();
            $url = $linkResultFromExternalUrl->getUrl();
            // 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 = $linkResultFromExternalUrl->getType();
            if (empty($url)) {
                throw new UnableToLinkException('Link to external page "' . $page['uid'] . '" does not have a proper target URL, so "' . $linkText . '" was not linked.', 1551621999, null, $linkText);
            }
        } else {
            // Generate the URL
            $url = $this->generateUrlForPageWithSiteConfiguration($page, $siteOfTargetPage, $queryParameters, $fragment, $conf);
            // 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($page, $conf, $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($conf, $page)) {
            $url = $this->modifyUrlForAccessRestrictedPage($url, $page, $linkDetails['pagetype'] ?? '');
            $result = new LinkResult($linkResultType, $url);
            $additionalAttributes = (string)($tsfe->config['config']['typolinkLinkAccessRestrictedPages.']['ATagParams'] ?? '');
            if ($additionalAttributes !== '') {
                $additionalAttributes = GeneralUtility::get_tag_attributes($additionalAttributes);
                $result = $result->withAttributes($additionalAttributes);
            }
        }

        // Setting title if blank value to link
        $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $page['title'] ?? '');
        return $result
            ->withLinkConfiguration($conf)
            ->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);
        // get config.linkVars and prepend them before the actual GET parameters
        $tsfe = $this->getTypoScriptFrontendController();
        if ($tsfe->linkVars) {
            $globalQueryParameters = [];
            parse_str($tsfe->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'] ?? false) {
            $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;
    }

    /**
     * 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 = [];
        $tsfe = $this->getTypoScriptFrontendController();
        if (!($tsfe->config['config']['MP_disableTypolinkClosestMPvalue'] ?? false)) {
            $temp_MP = $this->getClosestMountPointValueForPage((int)$page['uid']);
            if ($temp_MP) {
                $mountPointPairs['closest'] = $temp_MP;
            }
        }
        // Look for overlay Mount Point:
        $mount_info = $tsfe->sys_page->getMountPointInfo($page['uid'], $page);
        if (is_array($mount_info) && $mount_info['overlay']) {
            $page = $tsfe->sys_page->getPage($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 = GeneralUtility::makeInstance(SiteFinder::class)->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): bool
    {
        return empty($conf['linkAccessRestrictedPages'])
            && (($tsfe = $this->getTypoScriptFrontendController())->config['config']['typolinkLinkAccessRestrictedPages'] ?? false)
            && $tsfe->config['config']['typolinkLinkAccessRestrictedPages'] !== 'NONE'
            && !GeneralUtility::makeInstance(RecordAccessVoter::class)->groupAccessGranted('pages', $page, $tsfe->getContext());
    }

    /**
     * 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): string
    {
        $tsfe = $this->getTypoScriptFrontendController();
        $thePage = $tsfe->sys_page->getPage($tsfe->config['config']['typolinkLinkAccessRestrictedPages']);
        $addParams = str_replace(
            [
                '###RETURN_URL###',
                '###PAGE_ID###',
            ],
            [
                rawurlencode($url),
                $page['uid'],
            ],
            $tsfe->config['config']['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): 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() ?? $configuration['page']->toArray();
        }
        // 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 need to be fetched again.
        if (($page['uid'] ?? false) !== $linkDetails['pageuid']) {
            $page = $pageRepository->getPage($linkDetails['pageuid'], $disableGroupAccessCheck);
        }

        if (empty($page) || !is_array($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']));
        }

        $page = $this->resolveShortcutPage($page, $pageRepository, $disableGroupAccessCheck);

        $languageField = $GLOBALS['TCA']['pages']['ctrl']['languageField'] ?? null;
        $languageParentField = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] ?? null;
        $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(
            $page[$languageParentField],
            $disableGroupAccessCheck
        );
        if (empty($languageParentPage)) {
            return $page;
        }
        // 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, false, $disableGroupAccessCheck);
        } catch (\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): UriInterface
    {
        $tsfe = $this->getTypoScriptFrontendController();
        $currentSite = $this->getCurrentSite();
        $currentSiteLanguage = $this->getCurrentSiteLanguage() ?? $currentSite?->getDefaultLanguage();

        $siteLanguageOfTargetPage = $this->getSiteLanguageOfTargetPage($siteOfTargetPage, (string)($conf['language'] ?? 'current'));

        // 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 ($tsfe->config['config']['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 = new Page($page);

        if ($fragment
            && $useAbsoluteUrl === false
            && $currentSiteLanguage === $siteLanguageOfTargetPage
            && $targetPageId === $tsfe->id
            && (empty($conf['addQueryString']) || !isset($conf['addQueryString.']))
            && !($tsfe->config['config']['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
    {
        $tsfe = $this->getTypoScriptFrontendController();
        if (empty($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) || !$tsfe->MP) {
            return '';
        }
        // Same page as current.
        if ($tsfe->id === $pageId) {
            return $tsfe->MP;
        }

        // Find closest mount point
        // Gets rootline of linked-to page
        try {
            $tCR_rootline = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get();
        } catch (RootLineException $e) {
            $tCR_rootline = [];
        }
        $inverseLocalRootLine = array_reverse($tsfe->config['rootLine'] ?? []);
        $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): string
    {
        // Create map if not found already
        $config = $this->getTypoScriptFrontendController()->config;
        $mountPointMap = $this->initializeMountPointMap(
            !empty($config['config']['MP_defaults']) ? $config['config']['MP_defaults'] : '',
            !empty($config['config']['MP_mapRootPoints']) ? $config['config']['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
    {
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
        $mountPointMap = $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);
        // Traverse rootpoints
        foreach ($rootPoints as $p) {
            $initMParray = [];
            if ($p === 'root') {
                $rootPage = $this->getTypoScriptFrontendController()->config['rootLine'][0];
                $p = $rootPage['uid'];
                if (($rootPage['_MOUNT_OL'] ?? false) && ($rootPage['_MP_PARAM'] ?? false)) {
                    $initMParray[] = $rootPage['_MP_PARAM'];
                }
            }
            $this->populateMountPointMapForPageRecursively($mountPointMap, (int)$p, $initMParray);
        }
        $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->getTypoScriptFrontendController()->sys_page->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_RECYCLER, 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->getTypoScriptFrontendController()->sys_page->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) {
            $excludeString = str_replace(',', '&', $configuration['exclude']);
            $excludedQueryParts = [];
            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.
     */
    protected function getCurrentSite(): ?SiteInterface
    {
        if ($this->typoScriptFrontendController instanceof TypoScriptFrontendController) {
            return $this->typoScriptFrontendController->getSite();
        }
        if ($GLOBALS['TSFE'] instanceof TypoScriptFrontendController) {
            return $GLOBALS['TSFE']->getSite();
        }
        if (isset($GLOBALS['TYPO3_REQUEST']) && $GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
            return $GLOBALS['TYPO3_REQUEST']->getAttribute('site', null);
        }
        return null;
    }

    /**
     * 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
    {
        if ($this->typoScriptFrontendController instanceof TypoScriptFrontendController) {
            return $this->typoScriptFrontendController->getLanguage();
        }
        if (isset($GLOBALS['TYPO3_REQUEST']) && $GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
            return $GLOBALS['TYPO3_REQUEST']->getAttribute('language', null);
        }
        return null;
    }

    /**
     * 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 ?? GeneralUtility::makeInstance(LanguageAspect::class)
        );
        return GeneralUtility::makeInstance(
            PageRepository::class,
            $context
        );
    }
}