[BUGFIX] Ensure most site related exceptions are handled
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Typolink / PageLinkBuilder.php
index 8fdfe05..a8d7caf 100644 (file)
@@ -16,15 +16,24 @@ namespace TYPO3\CMS\Frontend\Typolink;
  */
 
 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\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\Exception\Page\RootLineException;
-use TYPO3\CMS\Core\Routing\PageUriBuilder;
+use TYPO3\CMS\Core\Exception\SiteNotFoundException;
+use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
+use TYPO3\CMS\Core\Routing\RouterInterface;
+use TYPO3\CMS\Core\Routing\SiteMatcher;
+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\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\RootlineUtility;
+use TYPO3\CMS\Frontend\Compatibility\LegacyDomainResolver;
 use TYPO3\CMS\Frontend\ContentObject\TypolinkModifyLinkConfigForPageLinksHookInterface;
 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
 use TYPO3\CMS\Frontend\Page\PageRepository;
@@ -36,6 +45,7 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
 {
     /**
      * @inheritdoc
+     * @throws UnableToLinkException
      */
     public function build(array &$linkDetails, string $linkText, string $target, array $conf): array
     {
@@ -56,25 +66,18 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         }
 
         // Looking up the page record to verify its existence:
-        $page = $tsfe->sys_page->getPage($linkDetails['pageuid'], $disableGroupAccessCheck);
+        $page = $this->resolvePage($linkDetails, $conf, $disableGroupAccessCheck);
 
         if (empty($page)) {
             throw new UnableToLinkException('Page id "' . $linkDetails['typoLinkParameter'] . '" was not found, so "' . $linkText . '" was not linked.', 1490987336, null, $linkText);
         }
-        $language = $page[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
-        if ($language === 0 && GeneralUtility::hideIfDefaultLanguage($page['l18n_cfg'])) {
-            throw new UnableToLinkException('Default language of page  "' . $linkDetails['typoLinkParameter'] . '" is hidden, so "' . $linkText . '" was not linked.', 1529527301, null, $linkText);
-        }
-        if ($language > 0 && !isset($page['_PAGES_OVERLAY']) && GeneralUtility::hideIfNotTranslated($page['l18n_cfg'])) {
-            throw new UnableToLinkException('Fallback to default language of page "' . $linkDetails['typoLinkParameter'] . '" is disabled, so "' . $linkText . '" was not linked.', 1529527488, null, $linkText);
-        }
 
         foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typolinkProcessing']['typolinkModifyParameterForPageLinks'] ?? [] as $classData) {
             $hookObject = GeneralUtility::makeInstance($classData);
             if (!$hookObject instanceof TypolinkModifyLinkConfigForPageLinksHookInterface) {
                 throw new \UnexpectedValueException('$hookObject must implement interface ' . TypolinkModifyLinkConfigForPageLinksHookInterface::class, 1483114905);
             }
-            /** @var $hookObject TypolinkModifyLinkConfigForPageLinksHookInterface */
+            /** @var TypolinkModifyLinkConfigForPageLinksHookInterface $hookObject */
             $conf = $hookObject->modifyPageLinkConfiguration($conf, $linkDetails, $page);
         }
         $enableLinksAcrossDomains = $tsfe->config['config']['typolinkEnableLinksAcrossDomains'];
@@ -112,16 +115,12 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
             }
             $MPvarAcc['re-map'] = $mount_info['MPvar'];
         }
-        // Setting title if blank value to link
-        $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $page['title']);
         // Query Params:
         $addQueryParams = $conf['addQueryString'] ? $this->contentObjectRenderer->getQueryArguments($conf['addQueryString.']) : '';
         $addQueryParams .= isset($conf['additionalParams.']) ? trim((string)$this->contentObjectRenderer->stdWrap($conf['additionalParams'], $conf['additionalParams.'])) : trim((string)$conf['additionalParams']);
         if ($addQueryParams === '&' || $addQueryParams[0] !== '&') {
             $addQueryParams = '';
         }
-        $targetDomain = '';
-        $currentDomain = (string)GeneralUtility::getIndpEnv('HTTP_HOST');
         // Mount pages are always local and never link to another domain
         if (!empty($MPvarAcc)) {
             // Add "&MP" var:
@@ -154,23 +153,267 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
                     $page = $page2;
                 }
             }
-
-            $targetDomainRecord = $tsfe->getDomainDataForPid($page['uid']);
-            $targetDomain = $targetDomainRecord ? $targetDomainRecord['domainName'] : null;
-            // Do not prepend the domain if it is the current hostname
-            if (!$targetDomain || $tsfe->domainNameMatchesCurrentRequest($targetDomain)) {
-                $targetDomain = '';
-            }
         }
         if ($conf['useCacheHash']) {
             $params = $tsfe->linkVars . $addQueryParams . '&id=' . $page['uid'];
             if (trim($params, '& ') !== '') {
-                $cacheHash = GeneralUtility::makeInstance(CacheHashCalculator::class);
-                $cHash = $cacheHash->generateForParameters($params);
+                $cHash = GeneralUtility::makeInstance(CacheHashCalculator::class)->generateForParameters($params);
                 $addQueryParams .= $cHash ? '&cHash=' . $cHash : '';
             }
             unset($params);
         }
+
+        // get config.linkVars and prepend them before the actual GET parameters
+        $queryParameters = [];
+        parse_str($addQueryParams, $queryParameters);
+        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']);
+
+        // Override language property if not being set already
+        if (isset($queryParameters['L']) && !isset($conf['language'])) {
+            $conf['language'] = (int)$queryParameters['L'];
+        }
+
+        // Now overlay the page in the target language, in order to have valid title attributes etc.
+        if (isset($conf['language']) && $conf['language'] > 0 && $conf['language'] !== 'current') {
+            $page = $tsfe->sys_page->getPageOverlay($page, (int)$conf['language']);
+        }
+
+        // Check if the target page has a site configuration
+        try {
+            $siteOfTargetPage = GeneralUtility::makeInstance(SiteMatcher::class)->matchByPageId((int)$page['uid']);
+            $currentSite = $this->getCurrentSite();
+        } catch (SiteNotFoundException $e) {
+            // Usually happens in tests, as Pseudo Sites should be available everywhere.
+            $siteOfTargetPage = null;
+            $currentSite = null;
+        }
+
+        // Link to a page that has a site configuration
+        if ($siteOfTargetPage instanceof Site) {
+            // No need for any L parameter with Site handling
+            unset($queryParameters['L']);
+            if ($pageType) {
+                $queryParameters['type'] = (int)$pageType;
+            }
+            // Generate the URL
+            $url = $this->generateUrlForPageWithSiteConfiguration($page, $siteOfTargetPage, $queryParameters, $sectionMark, $conf);
+
+            $treatAsExternalLink = true;
+            // 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
+                if ($currentSite instanceof Site && $currentSite->getRootPageId() === $siteOfTargetPage->getRootPageId()) {
+                    $treatAsExternalLink = false;
+                }
+            }
+
+            $url = (string)$url;
+            if ($treatAsExternalLink) {
+                $target = $target ?: $this->resolveTargetAttribute($conf, 'extTarget', false, $tsfe->extTarget);
+            } else {
+                $target = (isset($page['target']) && trim($page['target'])) ? $page['target'] : $target;
+                if (empty($target)) {
+                    $target = $this->resolveTargetAttribute($conf, 'target', true, $tsfe->intTarget);
+                }
+            }
+        } else {
+            // If the typolink.language parameter was set, ensure that this is added to L query parameter
+            if (!isset($queryParameters['L']) && MathUtility::canBeInterpretedAsInteger($conf['language'] ?? false)) {
+                $queryParameters['L'] = $conf['language'];
+            }
+            list($url, $target) = $this->generateUrlForPageWithoutSiteConfiguration($page, $queryParameters, $conf, $pageType, $sectionMark, $target, $MPvarAcc);
+        }
+
+        $languageField = $GLOBALS['TCA']['pages']['ctrl']['languageField'] ?? null;
+        $languageOfPageRecord = (int)($page[$languageField] ?? 0);
+
+        if ($languageOfPageRecord === 0 && GeneralUtility::hideIfDefaultLanguage($page['l18n_cfg'])) {
+            throw new UnableToLinkException('Default language of page  "' . $linkDetails['typoLinkParameter'] . '" is hidden, so "' . $linkText . '" was not linked.', 1529527301, null, $linkText);
+        }
+        if ($languageOfPageRecord > 0 && !isset($page['_PAGES_OVERLAY']) && GeneralUtility::hideIfNotTranslated($page['l18n_cfg'])) {
+            throw new UnableToLinkException('Fallback to default language of page "' . $linkDetails['typoLinkParameter'] . '" is disabled, so "' . $linkText . '" was not linked.', 1529527488, null, $linkText);
+        }
+
+        // If link is to an access restricted page which should be redirected, then find new URL:
+        if (empty($conf['linkAccessRestrictedPages'])
+            && $tsfe->config['config']['typolinkLinkAccessRestrictedPages']
+            && $tsfe->config['config']['typolinkLinkAccessRestrictedPages'] !== 'NONE'
+            && !$tsfe->checkPageGroupAccess($page)
+        ) {
+            $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']
+            );
+            $url = $this->contentObjectRenderer->getTypoLink_URL($thePage['uid'] . ($pageType ? ',' . $pageType : ''), $addParams, $target);
+            $url = $this->forceAbsoluteUrl($url, $conf);
+            $this->contentObjectRenderer->lastTypoLinkLD['totalUrl'] = $url;
+        }
+
+        // Setting title if blank value to link
+        $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $page['title']);
+        return [$url, $linkText, $target];
+    }
+
+    /**
+     * Resolves page and if a translated page was found, resolves that to it
+     * language parent, adjusts `$linkDetails['pageuid']` (for hook processing)
+     * and modifies `$configuration['language']` (for language URL generation).
+     *
+     * @param array $linkDetails
+     * @param array $configuration
+     * @param bool $disableGroupAccessCheck
+     * @return array
+     */
+    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.
+        $page = $pageRepository->getPage($linkDetails['pageuid'], $disableGroupAccessCheck);
+
+        if (empty($page) || !is_array($page)) {
+            return [];
+        }
+
+        $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;
+        }
+
+        // Set the "pageuid" to the default-language page ID.
+        $linkDetails['pageuid'] = (int)$languageParentPage['uid'];
+        $configuration['language'] = $language;
+        $linkDetails['parameters'] .= '&L=' . $language;
+        return $languageParentPage;
+    }
+
+    /**
+     * Create a UriInterface object when linking to a page with a site configuration
+     *
+     * @param array $page
+     * @param Site $siteOfTargetPage
+     * @param array $queryParameters
+     * @param string $fragment
+     * @param array $conf
+     * @return UriInterface
+     * @throws UnableToLinkException
+     */
+    protected function generateUrlForPageWithSiteConfiguration(array $page, Site $siteOfTargetPage, array $queryParameters, string $fragment, array $conf): UriInterface
+    {
+        $currentSite = $this->getCurrentSite();
+        $currentSiteLanguage = $this->getCurrentSiteLanguage();
+        // Happens when currently on a pseudo-site configuration
+        // We assume to use the default language then
+        if ($currentSite && !($currentSiteLanguage instanceof SiteLanguage)) {
+            $currentSiteLanguage = $currentSite->getDefaultLanguage();
+        }
+
+        $targetLanguageId = $conf['language'] ?? 'current';
+        if ($targetLanguageId === 'current') {
+            $targetLanguageId = $currentSiteLanguage ? $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);
+        }
+
+        // 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.
+        $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;
+        try {
+            $uri = $siteOfTargetPage->getRouter()->generateUri(
+                $targetPageId,
+                $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, but only if the site does not define a scheme yet AND the site defines a domain/host
+        if ($useAbsoluteUrl && !$uri->getScheme() && $uri->getHost()) {
+            $scheme = $conf['forceAbsoluteUrl.']['scheme'] ?? 'https';
+            $uri = $uri->withScheme($scheme);
+        }
+        return $uri;
+    }
+
+    /**
+     * Generate a URL for a page without site configuration
+     *
+     * @param array $page
+     * @param array $additionalQueryParams
+     * @param array $conf
+     * @param string $pageType
+     * @param string $sectionMark
+     * @param string $target
+     * @param array $MPvarAcc
+     * @return array
+     */
+    protected function generateUrlForPageWithoutSiteConfiguration(array $page, array $additionalQueryParams, array $conf, string $pageType, string $sectionMark, string $target, array $MPvarAcc): array
+    {
+        // Build a string out of the query parameters
+        $additionalQueryParams = http_build_query($additionalQueryParams, '', '&', PHP_QUERY_RFC3986);
+        if (!empty($additionalQueryParams)) {
+            $additionalQueryParams = '&' . $additionalQueryParams;
+        }
+
+        $tsfe = $this->getTypoScriptFrontendController();
+        $enableLinksAcrossDomains = $tsfe->config['config']['typolinkEnableLinksAcrossDomains'];
+        $targetDomain = '';
+        $currentDomain = (string)GeneralUtility::getIndpEnv('HTTP_HOST');
+        if (!empty($MPvarAcc)) {
+            $domainResolver = GeneralUtility::makeInstance(LegacyDomainResolver::class);
+            $targetDomainRecord = $domainResolver->matchPageId((int)$page['uid'], $GLOBALS['TYPO3_REQUEST']);
+            // Do not prepend the domain if it is the current hostname
+            if (!empty($targetDomainRecord) && !$targetDomainRecord['isCurrentDomain']) {
+                $targetDomain = $targetDomainRecord['domainName'];
+            }
+        }
         $absoluteUrlScheme = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https' : 'http';
         // URL shall be absolute:
         if (isset($conf['forceAbsoluteUrl']) && $conf['forceAbsoluteUrl']) {
@@ -178,13 +421,10 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
             if (isset($conf['forceAbsoluteUrl.']['scheme']) && $conf['forceAbsoluteUrl.']['scheme']) {
                 $absoluteUrlScheme = $conf['forceAbsoluteUrl.']['scheme'];
             }
-            // If no domain records are defined, use current domain:
-            $currentUrlScheme = parse_url(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'), PHP_URL_SCHEME);
-            if ($targetDomain === '' && ($conf['forceAbsoluteUrl'] || $absoluteUrlScheme !== $currentUrlScheme)) {
-                $targetDomain = $currentDomain;
-            }
+            // If no domain records are defined, use current domain
+            $targetDomain = $targetDomain ?: $currentDomain;
             // If go for an absolute link, add site path if it's not taken care about by absRefPrefix
-            if (!$tsfe->config['config']['absRefPrefix'] && $targetDomain === $currentDomain) {
+            if (!$tsfe->absRefPrefix && $targetDomain === $currentDomain) {
                 $targetDomain = $currentDomain . rtrim(GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'), '/');
             }
         }
@@ -195,7 +435,7 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
             if (!preg_match('/^[a-z0-9.\\-]*$/i', $targetDomain)) {
                 $targetDomain = GeneralUtility::idnaEncode($targetDomain);
             }
-            $url = $absoluteUrlScheme . '://' . $targetDomain . '/index.php?id=' . $page['uid'] . $addQueryParams . $sectionMark;
+            $url = $absoluteUrlScheme . '://' . $targetDomain . '/index.php?id=' . $page['uid'] . $additionalQueryParams;
         } else {
             // Internal link or current domain's linking scheme should be used
             // Internal target:
@@ -203,16 +443,16 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
             if (empty($target)) {
                 $target = $this->resolveTargetAttribute($conf, 'target', true, $tsfe->intTarget);
             }
-            $LD = $this->createTotalUrlAndLinkData($page, $target, $conf['no_cache'], $addQueryParams, $pageType, $targetDomain);
+            $LD = $this->createTotalUrlAndLinkData($page, $target, $conf['no_cache'], $additionalQueryParams, $pageType, $targetDomain);
             if ($targetDomain !== '') {
                 // We will add domain only if URL does not have it already.
-                if ($enableLinksAcrossDomains && $targetDomain !== $currentDomain && isset($tsfe->config['config']['absRefPrefix'])) {
+                if ($enableLinksAcrossDomains && $targetDomain !== $currentDomain && !empty($tsfe->absRefPrefix)) {
                     // Get rid of the absRefPrefix if necessary. absRefPrefix is applicable only
                     // to the current web site. If we have domain here it means we link across
                     // domains. absRefPrefix can contain domain name, which will screw up
                     // the link to the external domain.
-                    $prefixLength = strlen($tsfe->config['config']['absRefPrefix']);
-                    if (substr($LD['totalURL'], 0, $prefixLength) === $tsfe->config['config']['absRefPrefix']) {
+                    $prefixLength = strlen($tsfe->absRefPrefix);
+                    if (strpos($LD['totalURL'], $tsfe->absRefPrefix) === 0) {
                         $LD['totalURL'] = substr($LD['totalURL'], $prefixLength);
                     }
                 }
@@ -221,16 +461,19 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
                     $LD['totalURL'] = $absoluteUrlScheme . '://' . $targetDomain . ($LD['totalURL'][0] === '/' ? '' : '/') . $LD['totalURL'];
                 }
             }
-            $url = $LD['totalURL'] . $sectionMark;
+            $url = $LD['totalURL'];
         }
-        // If sectionMark is set, there is no baseURL AND the current page is the page the link is to, check if there are any additional parameters or addQueryString parameters and if not, drop the url.
+        $url .= $sectionMark;
+        // If sectionMark is set, there is no baseURL AND the current page is the page the link is to,
+        // check if there are any additional parameters or addQueryString parameters and if not, drop the url.
         if ($sectionMark
             && !$tsfe->config['config']['baseURL']
             && (int)$page['uid'] === (int)$tsfe->id
-            && !trim($addQueryParams)
+            && !trim($additionalQueryParams)
             && (empty($conf['addQueryString']) || !isset($conf['addQueryString.']))
         ) {
-            $currentQueryArray = GeneralUtility::explodeUrl2Array(GeneralUtility::getIndpEnv('QUERY_STRING'), true);
+            $currentQueryArray = [];
+            parse_str(GeneralUtility::getIndpEnv('QUERY_STRING'), $currentQueryArray);
             $currentQueryParams = GeneralUtility::implodeArrayForUrl('', $currentQueryArray, '', false, true);
 
             if (!trim($currentQueryParams)) {
@@ -248,31 +491,7 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
                 }
             }
         }
-
-        // If link is to an access restricted page which should be redirected, then find new URL:
-        if (empty($conf['linkAccessRestrictedPages'])
-            && $tsfe->config['config']['typolinkLinkAccessRestrictedPages']
-            && $tsfe->config['config']['typolinkLinkAccessRestrictedPages'] !== 'NONE'
-            && !$tsfe->checkPageGroupAccess($page)
-        ) {
-            $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']
-            );
-            $url = $this->contentObjectRenderer->getTypoLink_URL($thePage['uid'] . ($pageType ? ',' . $pageType : ''), $addParams, $target);
-            $url = $this->forceAbsoluteUrl($url, $conf);
-            $this->contentObjectRenderer->lastTypoLinkLD['totalUrl'] = $url;
-        }
-
-        return [$url, $linkText, $target];
+        return [$url, $target];
     }
 
     /**
@@ -498,7 +717,6 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
      */
     protected function createTotalUrlAndLinkData($page, $target, $no_cache, $addParams = '', $typeOverride = '', $targetDomain = '')
     {
-        $allQueryParameters = [];
         $LD = [];
         // Adding Mount Points, "&MP=", parameter for the current page if any is set
         // but non other set explicitly
@@ -517,9 +735,8 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         }
         // typeNum
         $typeNum = $this->getTypoScriptFrontendController()->tmpl->setup[$target . '.']['typeNum'];
-        $config = $this->getTypoScriptFrontendController()->config;
-        if (!MathUtility::canBeInterpretedAsInteger($typeOverride) && !empty($config['config']['forceTypeValue']) && (int)$config['config']['forceTypeValue']) {
-            $typeOverride = (int)$config['config']['forceTypeValue'];
+        if (!MathUtility::canBeInterpretedAsInteger($typeOverride) && (int)$this->getTypoScriptFrontendController()->config['config']['forceTypeValue']) {
+            $typeOverride = (int)$this->getTypoScriptFrontendController()->config['config']['forceTypeValue'];
         }
         if ((string)$typeOverride !== '') {
             $typeNum = $typeOverride;
@@ -527,67 +744,25 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         // Override...
         if ($typeNum) {
             $LD['type'] = '&type=' . (int)$typeNum;
-            $allQueryParameters['type'] = (int)$typeNum;
         } else {
             $LD['type'] = '';
         }
         // Preserving the type number.
         $LD['orig_type'] = $LD['type'];
         // noCache
-        if ($no_cache) {
-            $LD['no_cache'] = '&no_cache=1';
-            $allQueryParameters['no_cache'] = 1;
-        } else {
-            $LD['no_cache'] = '';
-        }
+        $LD['no_cache'] = $no_cache ? '&no_cache=1' : '';
         // linkVars
-        $queryParameters = GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars . $addParams);
-        if (!empty($queryParameters)) {
-            $allQueryParameters = array_replace_recursive($queryParameters, $allQueryParameters);
-            $LD['linkVars'] = GeneralUtility::implodeArrayForUrl('', $queryParameters, '', false, true);
+        if ($addParams) {
+            $LD['linkVars'] = GeneralUtility::implodeArrayForUrl('', GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars . $addParams), '', false, true);
+        } else {
+            $LD['linkVars'] = $this->getTypoScriptFrontendController()->linkVars;
         }
         // Add absRefPrefix if exists.
         $LD['url'] = $this->getTypoScriptFrontendController()->absRefPrefix . $LD['url'];
         // If the special key 'sectionIndex_uid' (added 'manually' in tslib/menu.php to the page-record) is set, then the link jumps directly to a section on the page.
         $LD['sectionIndex'] = $page['sectionIndex_uid'] ? '#c' . $page['sectionIndex_uid'] : '';
-
-        // Compile the total url
-        $urlParts = parse_url($LD['url']);
-
-        // Now see if the URL can be replaced by a URL generated by the Site-based Page Builder,
-        // but first find out if a language has been set explicitly
-        if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
-            $currentSiteLanguage = $GLOBALS['TYPO3_REQUEST']->getAttribute('language');
-            if ($currentSiteLanguage instanceof SiteLanguage) {
-                $languageId = $currentSiteLanguage->getLanguageId();
-            }
-        }
-        $absRefPrefix = $this->getTypoScriptFrontendController()->absRefPrefix;
-        $languageId = $queryParameters['L'] ?? $languageId ?? null;
-        $totalUrl = (string)GeneralUtility::makeInstance(PageUriBuilder::class)->buildUri(
-            (int)$page['uid'],
-            $allQueryParameters,
-            $LD['sectionIndex'],
-            ['language' => $languageId, 'alternativePageId' => $page['alias'] ?: $page['uid'], 'legacyUrlPrefix' => $absRefPrefix],
-            (!$urlParts['scheme'] && !$urlParts['host']) ? PageUriBuilder::ABSOLUTE_PATH : PageUriBuilder::ABSOLUTE_URL
-        );
-
-        // $totalUri contains /index.php for legacy URLs, as previously "it was index.php"
-        // In case an URI has is prefixed with "/" which is not the absRefPrefix, remove it.
-        // this might change in the future
-        if (strpos($totalUrl, '/index.php') === 0 && $absRefPrefix !== '/') {
-            $totalUrl = substr($totalUrl, 1);
-        }
-
-        // Add the method url id token later-on
-        if ($this->getTypoScriptFrontendController()->getMethodUrlIdToken) {
-            if (strpos($totalUrl, '#') !== false) {
-                $totalUrl = str_replace('#', $this->getTypoScriptFrontendController()->getMethodUrlIdToken . '#', $totalUrl);
-            } else {
-                $totalUrl .= $this->getTypoScriptFrontendController()->getMethodUrlIdToken;
-            }
-        }
-        $LD['totalURL'] = $totalUrl;
+        // Compile the normal total url
+        $LD['totalURL'] = rtrim($LD['url'] . $LD['type'] . $LD['no_cache'] . $LD['linkVars'] . $this->getTypoScriptFrontendController()->getMethodUrlIdToken, '?') . $LD['sectionIndex'];
         // Call post processing function for link rendering:
         $_params = [
             'LD' => &$LD,
@@ -599,4 +774,62 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         }
         return $LD;
     }
+
+    /**
+     * Check if we have a site object in the current request. if null, this usually means that
+     * this class was called from CLI context.
+     *
+     * @return SiteInterface|null
+     */
+    protected function getCurrentSite(): ?SiteInterface
+    {
+        if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
+            return $GLOBALS['TYPO3_REQUEST']->getAttribute('site', null);
+        }
+        if (MathUtility::canBeInterpretedAsInteger($GLOBALS['TSFE']->id) && $GLOBALS['TSFE']->id > 0) {
+            $matcher = GeneralUtility::makeInstance(SiteMatcher::class);
+            try {
+                $site = $matcher->matchByPageId((int)$GLOBALS['TSFE']->id);
+            } catch (SiteNotFoundException $e) {
+                $site = null;
+            }
+            return $site;
+        }
+        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.
+     *
+     * @return SiteLanguage|null
+     */
+    protected function getCurrentSiteLanguage(): ?SiteLanguage
+    {
+        if ($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.
+     *
+     * @return PageRepository
+     */
+    protected function buildPageRepository(): PageRepository
+    {
+        // clone global context object (singleton)
+        $context = clone GeneralUtility::makeInstance(Context::class);
+        $context->setAspect(
+            'language',
+            GeneralUtility::makeInstance(LanguageAspect::class)
+        );
+        $pageRepository = GeneralUtility::makeInstance(
+            PageRepository::class,
+            $context
+        );
+        return $pageRepository;
+    }
 }