[TASK] Create URLs with typolink for pages with sites 63/58063/17
authorBenni Mack <benni@typo3.org>
Thu, 30 Aug 2018 17:33:44 +0000 (19:33 +0200)
committerBenni Mack <benni@typo3.org>
Thu, 30 Aug 2018 22:37:57 +0000 (00:37 +0200)
The PageLinkBuilder now separates out into generating links
for pages with site configuration, and without site
configuration (= as before).

The new link mechanism does not rely on previous hooks
and options like config.absRefPrefix which are not necessary
for URLs as they are always absolute.

A new TypoScript option "typolink.language" is introduced.
If not set, it is set to "current", and allows to link
across pagetrees to a given site, or render a language menu
without having to use "&L=" as additional query parameter.
 ("current" is the current language and default) is
used to also allow to explicitly link to a different language
if this language was configured for the site configuration
for the site.

As for the linking process:
- typolink.forceAbsoluteUrl is used to generate the full
  URL to a target page
- typolink.forceAbsoluteUrl.scheme is only applicable
  if the site base of the target does not explicitly
  define a scheme
- If you link to a different page tree or to a different
host/scheme, it is always treated as absolute.

On top: If you link across page trees, the link is
treated as external (extTarget is used).

The next steps are to create tests and incorporate
config.linkVars into this process.

Resolves: #86048
Releases: master
Change-Id: I01b2b43efafa23ce0d256bdcd0feb35756fbe1d5
Reviewed-on: https://review.typo3.org/58063
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: TYPO3com <no-reply@typo3.com>
typo3/sysext/core/Classes/Routing/PageUriBuilder.php
typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php
typo3/sysext/frontend/Tests/Functional/SiteHandling/LinkGeneratorTest.php

index 077b9c6..4d39382 100644 (file)
@@ -21,6 +21,8 @@ use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Http\Uri;
 use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
@@ -71,27 +73,42 @@ class PageUriBuilder implements SingletonInterface
     public function buildUri(int $pageId, array $queryParameters = [], string $fragment = null, array $options = [], string $referenceType = self::ABSOLUTE_PATH): UriInterface
     {
         // Resolve site
-        $languageOption = isset($options['language']) ? (int)$options['language'] : null;
+        $site = null;
+        $siteLanguage = null;
+        $languageOption = $options['language'] ?? null;
         $languageQueryParameter = isset($queryParameters['L']) ? (int)$queryParameters['L'] : null;
+
+        if (isset($options['site']) && $options['site'] instanceof Site) {
+            $site = $options['site'];
+        }
+        if (isset($options['language'])) {
+            if ($options['language'] instanceof SiteLanguage) {
+                $siteLanguage = $options['language'];
+                $languageOption = $siteLanguage->getLanguageId();
+            } else {
+                $languageOption = (int)$languageOption;
+            }
+        }
         $languageId = $languageOption ?? $languageQueryParameter ?? null;
 
         // alternative page ID - Used to set as alias as well
         $alternativePageId = $options['alternativePageId'] ?? $pageId;
-        $siteLanguage = null;
-        try {
-            $site = $this->siteFinder->getSiteByPageId($pageId, $options['rootLine'] ?? null);
-            if ($site) {
-                // Resolve language (based on the options / query parameters, and remove it from GET variables,
-                // as the language is determined by the language path
-                unset($queryParameters['L']);
-                $siteLanguage = $site->getLanguageById($languageId ?? 0);
+        if (!($site instanceof Site)) {
+            try {
+                $site = $this->siteFinder->getSiteByPageId($pageId, $options['rootLine'] ?? null);
+                if ($site) {
+                    // Resolve language (based on the options / query parameters, and remove it from GET variables,
+                    // as the language is determined by the language path
+                    unset($queryParameters['L']);
+                    $siteLanguage = $site->getLanguageById($languageId ?? 0);
+                }
+            } catch (SiteNotFoundException | \InvalidArgumentException $e) {
             }
-        } catch (SiteNotFoundException | \InvalidArgumentException $e) {
         }
 
         // If something is found, use /en/?id=123&additionalParams
         // Only if a language is configured for the site, build a URL with a site prefix / base
-        if ($siteLanguage) {
+        if ($site && $siteLanguage) {
             unset($options['legacyUrlPrefix']);
             // Ensure to fetch the path segment / slug if it exists
             if ($siteLanguage->getLanguageId() > 0) {
index e6781dd..1d47edb 100644 (file)
@@ -16,11 +16,16 @@ 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\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\Exception\Page\RootLineException;
+use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Routing\PageUriBuilder;
+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;
@@ -160,10 +165,161 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
             }
             unset($params);
         }
+
+        // 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) {
+            $queryParameters = [];
+            parse_str($addQueryParams, $queryParameters);
+            unset($queryParameters['id'], $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 {
+            list($url, $target) = $this->generateUrlForPageWithoutSiteConfiguration($page, $addQueryParams, $conf, $pageType, $sectionMark, $target, $MPvarAcc);
+        }
+
+        // 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];
+    }
+
+    /**
+     * 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 (!($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
+        if ($currentSite->getRootPageId() !== $siteOfTargetPage->getRootPageId()) {
+            $useAbsoluteUrl = true;
+        // @todo: let's only check for host / scheme once we use the Uri interface
+        } elseif ($siteLanguageOfTargetPage->getBase() !== $currentSiteLanguage->getBase()) {
+            $useAbsoluteUrl = true;
+        }
+
+        $targetPageId = (int)($page['l10n_parent'] > 0 ? $page['l10n_parent'] : $page['uid']);
+
+        $uri = GeneralUtility::makeInstance(PageUriBuilder::class)->buildUri(
+            $targetPageId,
+            $queryParameters,
+            $fragment,
+            ['site' => $siteOfTargetPage, 'language' => $siteLanguageOfTargetPage],
+            $useAbsoluteUrl ? PageUriBuilder::ABSOLUTE_URL : PageUriBuilder::ABSOLUTE_PATH
+        );
+        // 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 string $additionalQueryParams
+     * @param array $conf
+     * @param string $pageType
+     * @param string $sectionMark
+     * @param string $target
+     * @param array $MPvarAcc
+     * @return array
+     */
+    protected function generateUrlForPageWithoutSiteConfiguration(array $page, string $additionalQueryParams, array $conf, string $pageType, string $sectionMark, string $target, array $MPvarAcc): array
+    {
+        $tsfe = $this->getTypoScriptFrontendController();
+        $enableLinksAcrossDomains = $tsfe->config['config']['typolinkEnableLinksAcrossDomains'];
         $targetDomain = '';
         $currentDomain = (string)GeneralUtility::getIndpEnv('HTTP_HOST');
         if (!empty($MPvarAcc)) {
-            // @todo: This obviously needs more detection with Site Handling, to detect the site language ID
             $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
@@ -192,7 +348,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;
+            $url = $absoluteUrlScheme . '://' . $targetDomain . '/index.php?id=' . $page['uid'] . $additionalQueryParams;
         } else {
             // Internal link or current domain's linking scheme should be used
             // Internal target:
@@ -200,7 +356,7 @@ 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 && !empty($tsfe->absRefPrefix)) {
@@ -226,7 +382,7 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         if ($sectionMark
             && !$tsfe->config['config']['baseURL']
             && (int)$page['uid'] === (int)$tsfe->id
-            && !trim($addQueryParams)
+            && !trim($additionalQueryParams)
             && (empty($conf['addQueryString']) || !isset($conf['addQueryString.']))
         ) {
             $currentQueryArray = [];
@@ -248,33 +404,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;
-        }
-
-        // Setting title if blank value to link
-        $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $page['title']);
-        return [$url, $linkText, $target];
+        return [$url, $target];
     }
 
     /**
@@ -500,7 +630,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
@@ -519,9 +648,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;
@@ -529,67 +657,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.
-        $absRefPrefix = $this->getTypoScriptFrontendController()->absRefPrefix;
-        $LD['url'] = $absRefPrefix . $LD['url'];
+        $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();
-            }
-        }
-        $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,
@@ -601,4 +687,36 @@ 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);
+            return $matcher->matchByPageId((int)$GLOBALS['TSFE']->id);
+        }
+        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;
+    }
 }
index 1d43d74..3ae1eb2 100644 (file)
@@ -140,30 +140,28 @@ class LinkGeneratorTest extends AbstractTestCase
     {
         $instructions = [
             // acme.com -> acme.com (same site)
-            ['https://acme.us/', 1100, 1000, '/?id=acme-root'],
-            ['https://acme.us/', 1100, 1100, '/?id=acme-first'],
+            ['https://acme.us/', 1100, 1000, '/?id=1000'],
+            ['https://acme.us/', 1100, 1100, '/?id=1100'],
             ['https://acme.us/', 1100, 1200, '/?id=1200'],
             ['https://acme.us/', 1100, 1210, '/?id=1210'],
             ['https://acme.us/', 1100, 404, '/?id=404'],
             // acme.com -> products.acme.com (nested sub-site)
-            ['https://acme.us/', 1100, 1300, '/?id=1300'],
-            ['https://acme.us/', 1100, 1310, '/?id=1310'],
+            ['https://acme.us/', 1100, 1300, 'https://products.acme.com/?id=1300'],
+            ['https://acme.us/', 1100, 1310, 'https://products.acme.com/?id=1310'],
             // acme.com -> blog.acme.com (different site)
-            // @todo https://blog.acme.com/ not prefixed
-            ['https://acme.us/', 1100, 2000, '/?id=blog-root'],
-            ['https://acme.us/', 1100, 2100, '/?id=2100'],
-            ['https://acme.us/', 1100, 2110, '/john/?id=2110'],
-            ['https://acme.us/', 1100, 2111, '/john/?id=2111'],
+            ['https://acme.us/', 1100, 2000, 'https://blog.acme.com/?id=2000'],
+            ['https://acme.us/', 1100, 2100, 'https://blog.acme.com/?id=2100'],
+            ['https://acme.us/', 1100, 2110, 'https://blog.acme.com/john/?id=2110'],
+            ['https://acme.us/', 1100, 2111, 'https://blog.acme.com/john/?id=2111'],
             // blog.acme.com -> acme.com (different site)
-            // @todo https://acme.com/ not prefixed
-            ['https://blog.acme.com/', 2100, 1000, '/?id=acme-root'],
-            ['https://blog.acme.com/', 2100, 1100, '/?id=acme-first'],
-            ['https://blog.acme.com/', 2100, 1200, '/?id=1200'],
-            ['https://blog.acme.com/', 2100, 1210, '/?id=1210'],
-            ['https://blog.acme.com/', 2100, 404, '/?id=404'],
+            ['https://blog.acme.com/', 2100, 1000, 'https://acme.us/?id=1000'],
+            ['https://blog.acme.com/', 2100, 1100, 'https://acme.us/?id=1100'],
+            ['https://blog.acme.com/', 2100, 1200, 'https://acme.us/?id=1200'],
+            ['https://blog.acme.com/', 2100, 1210, 'https://acme.us/?id=1210'],
+            ['https://blog.acme.com/', 2100, 404, 'https://acme.us/?id=404'],
             // blog.acme.com -> products.acme.com (different sub-site)
-            ['https://blog.acme.com/', 2100, 1300, '/?id=1300'],
-            ['https://blog.acme.com/', 2100, 1310, '/?id=1310'],
+            ['https://blog.acme.com/', 2100, 1300, 'https://products.acme.com/?id=1300'],
+            ['https://blog.acme.com/', 2100, 1310, 'https://products.acme.com/?id=1310'],
         ];
 
         return $this->keysFromTemplate(
@@ -204,30 +202,28 @@ class LinkGeneratorTest extends AbstractTestCase
     {
         $instructions = [
             // acme.com -> acme.com (same site)
-            ['https://acme.us/', [7100, 1700], 7110, 1000, '/?id=acme-root'],
-            ['https://acme.us/', [7100, 1700], 7110, 1100, '/?id=acme-first'],
+            ['https://acme.us/', [7100, 1700], 7110, 1000, '/?id=1000'],
+            ['https://acme.us/', [7100, 1700], 7110, 1100, '/?id=1100'],
             ['https://acme.us/', [7100, 1700], 7110, 1200, '/?id=1200'],
             ['https://acme.us/', [7100, 1700], 7110, 1210, '/?id=1210'],
             ['https://acme.us/', [7100, 1700], 7110, 404, '/?id=404'],
             // acme.com -> products.acme.com (nested sub-site)
-            ['https://acme.us/', [7100, 1700], 7110, 1300, '/?id=1300'],
-            ['https://acme.us/', [7100, 1700], 7110, 1310, '/?id=1310'],
+            ['https://acme.us/', [7100, 1700], 7110, 1300, 'https://products.acme.com/?id=1300'],
+            ['https://acme.us/', [7100, 1700], 7110, 1310, 'https://products.acme.com/?id=1310'],
             // acme.com -> blog.acme.com (different site)
-            // @todo https://blog.acme.com/ not prefixed
-            ['https://acme.us/', [7100, 1700], 7110, 2000, '/?id=blog-root'],
-            ['https://acme.us/', [7100, 1700], 7110, 2100, '/?id=2100'],
-            ['https://acme.us/', [7100, 1700], 7110, 2110, '/john/?id=2110'],
-            ['https://acme.us/', [7100, 1700], 7110, 2111, '/john/?id=2111'],
+            ['https://acme.us/', [7100, 1700], 7110, 2000, 'https://blog.acme.com/?id=2000'],
+            ['https://acme.us/', [7100, 1700], 7110, 2100, 'https://blog.acme.com/?id=2100'],
+            ['https://acme.us/', [7100, 1700], 7110, 2110, 'https://blog.acme.com/john/?id=2110'],
+            ['https://acme.us/', [7100, 1700], 7110, 2111, 'https://blog.acme.com/john/?id=2111'],
             // blog.acme.com -> acme.com (different site)
-            // @todo https://acme.com/ not prefixed
-            ['https://blog.acme.com/', [7100, 2700], 7110, 1000, '/?id=acme-root'],
-            ['https://blog.acme.com/', [7100, 2700], 7110, 1100, '/?id=acme-first'],
-            ['https://blog.acme.com/', [7100, 2700], 7110, 1200, '/?id=1200'],
-            ['https://blog.acme.com/', [7100, 2700], 7110, 1210, '/?id=1210'],
-            ['https://blog.acme.com/', [7100, 2700], 7110, 404, '/?id=404'],
+            ['https://blog.acme.com/', [7100, 2700], 7110, 1000, 'https://acme.us/?id=1000'],
+            ['https://blog.acme.com/', [7100, 2700], 7110, 1100, 'https://acme.us/?id=1100'],
+            ['https://blog.acme.com/', [7100, 2700], 7110, 1200, 'https://acme.us/?id=1200'],
+            ['https://blog.acme.com/', [7100, 2700], 7110, 1210, 'https://acme.us/?id=1210'],
+            ['https://blog.acme.com/', [7100, 2700], 7110, 404, 'https://acme.us/?id=404'],
             // blog.acme.com -> products.acme.com (different sub-site)
-            ['https://blog.acme.com/', [7100, 2700], 7110, 1300, '/?id=1300'],
-            ['https://blog.acme.com/', [7100, 2700], 7110, 1310, '/?id=1310'],
+            ['https://blog.acme.com/', [7100, 2700], 7110, 1300, 'https://products.acme.com/?id=1300'],
+            ['https://blog.acme.com/', [7100, 2700], 7110, 1310, 'https://products.acme.com/?id=1310'],
         ];
 
         return $this->keysFromTemplate(
@@ -277,18 +273,17 @@ class LinkGeneratorTest extends AbstractTestCase
      */
     public function linkIsGeneratedForLanguageDataProvider(): array
     {
-        // @todo L-parameter is not applied
+        // @todo L-parameter is not applied in all cases
         $instructions = [
             // acme.com -> acme.com (same site)
-            ['https://acme.us/', 1100, 1100, 0, '/?id=acme-first'],
-            ['https://acme.us/', 1100, 1100, 1, '/?id=acme-first'],
-            ['https://acme.us/', 1100, 1100, 2, '/?id=acme-first'],
-            // @todo Configuration bug on duplicating alias names and uniqueness
-            ['https://acme.us/', 1100, 1101, 0, '/?id=acme-first0'],
-            ['https://acme.us/', 1100, 1102, 0, '/?id=acme-first1'],
+            ['https://acme.us/', 1100, 1100, 0, '/?id=1100'],
+            ['https://acme.us/', 1100, 1100, 1, '/?id=1100'],
+            ['https://acme.us/', 1100, 1100, 2, '/?id=1100'],
+            ['https://acme.us/', 1100, 1101, 0, '/?id=1100'], // @todo Language missing
+            ['https://acme.us/', 1100, 1102, 0, '/?id=1100'], // @todo Language missing
             // acme.com -> products.acme.com (nested sub-site)
-            ['https://acme.us/', 1100, 1300, 0, '/?id=1300'],
-            ['https://acme.us/', 1100, 1310, 0, '/?id=1310'],
+            ['https://acme.us/', 1100, 1300, 0, 'https://products.acme.com/?id=1300'],
+            ['https://acme.us/', 1100, 1310, 0, 'https://products.acme.com/?id=1310'],
             // acme.com -> archive (outside site)
             ['https://acme.us/', 1100, 3100, 0, '/index.php?id=3100&L=0'],
             ['https://acme.us/', 1100, 3100, 1, '/index.php?id=3100&L=1'],
@@ -296,13 +291,11 @@ class LinkGeneratorTest extends AbstractTestCase
             ['https://acme.us/', 1100, 3101, 0, '/index.php?id=3101&L=0'],
             ['https://acme.us/', 1100, 3102, 0, '/index.php?id=3102&L=0'],
             // blog.acme.com -> acme.com (different site)
-            // @todo https://acme.com/ not prefixed
-            ['https://blog.acme.com/', 2100, 1100, 0, '/?id=acme-first'],
-            ['https://blog.acme.com/', 2100, 1100, 1, '/?id=acme-first'],
-            ['https://blog.acme.com/', 2100, 1100, 2, '/?id=acme-first'],
-            // @todo Configuration bug on duplicating alias names and uniqueness
-            ['https://blog.acme.com/', 2100, 1101, 0, '/?id=acme-first0'],
-            ['https://blog.acme.com/', 2100, 1102, 0, '/?id=acme-first1'],
+            ['https://blog.acme.com/', 2100, 1100, 0, 'https://acme.us/?id=1100'],
+            ['https://blog.acme.com/', 2100, 1100, 1, 'https://acme.us/?id=1100'],
+            ['https://blog.acme.com/', 2100, 1100, 2, 'https://acme.us/?id=1100'],
+            ['https://blog.acme.com/', 2100, 1101, 0, 'https://acme.us/?id=1100'], // @todo Language missing
+            ['https://blog.acme.com/', 2100, 1102, 0, 'https://acme.us/?id=1100'], // @todo Language missing
             // blog.acme.com -> archive (outside site)
             ['https://blog.acme.com/', 2100, 3100, 0, '/index.php?id=3100&L=0'],
             ['https://blog.acme.com/', 2100, 3100, 1, '/index.php?id=3100&L=1'],
@@ -310,8 +303,8 @@ class LinkGeneratorTest extends AbstractTestCase
             ['https://blog.acme.com/', 2100, 3101, 0, '/index.php?id=3101&L=0'],
             ['https://blog.acme.com/', 2100, 3102, 0, '/index.php?id=3102&L=0'],
             // blog.acme.com -> products.acme.com (different sub-site)
-            ['https://blog.acme.com/', 2100, 1300, 0, '/?id=1300'],
-            ['https://blog.acme.com/', 2100, 1310, 0, '/?id=1310'],
+            ['https://blog.acme.com/', 2100, 1300, 0, 'https://products.acme.com/?id=1300'],
+            ['https://blog.acme.com/', 2100, 1310, 0, 'https://products.acme.com/?id=1310'],
         ];
 
         return $this->keysFromTemplate(
@@ -354,30 +347,28 @@ class LinkGeneratorTest extends AbstractTestCase
     {
         $instructions = [
             // acme.com -> acme.com (same site)
-            ['https://acme.us/', 1100, 1000, '/?id=acme-root&testing%5Bvalue%5D=1&cHash=7d1f13fa91159dac7feb3c824936b39d'],
-            ['https://acme.us/', 1100, 1100, '/?id=acme-first&testing%5Bvalue%5D=1&cHash=f42b850e435f0cedd366f5db749fc1af'],
+            ['https://acme.us/', 1100, 1000, '/?id=1000&testing%5Bvalue%5D=1&cHash=7d1f13fa91159dac7feb3c824936b39d'],
+            ['https://acme.us/', 1100, 1100, '/?id=1100&testing%5Bvalue%5D=1&cHash=f42b850e435f0cedd366f5db749fc1af'],
             ['https://acme.us/', 1100, 1200, '/?id=1200&testing%5Bvalue%5D=1&cHash=784e11c50ea1a13fd7d969df4ec53ea3'],
             ['https://acme.us/', 1100, 1210, '/?id=1210&testing%5Bvalue%5D=1&cHash=ccb7067022b9835ebfd8f720722bc708'],
             ['https://acme.us/', 1100, 404, '/?id=404&testing%5Bvalue%5D=1&cHash=864e96f586a78a53452f3bf0f4d24591'],
             // acme.com -> products.acme.com (nested sub-site)
-            ['https://acme.us/', 1100, 1300, '/?id=1300&testing%5Bvalue%5D=1&cHash=dbd6597d72ed5098cce3d03eac1eeefe'],
-            ['https://acme.us/', 1100, 1310, '/?id=1310&testing%5Bvalue%5D=1&cHash=e64bfc7ab7dd6b70d161e4d556be9726'],
+            ['https://acme.us/', 1100, 1300, 'https://products.acme.com/?id=1300&testing%5Bvalue%5D=1&cHash=dbd6597d72ed5098cce3d03eac1eeefe'],
+            ['https://acme.us/', 1100, 1310, 'https://products.acme.com/?id=1310&testing%5Bvalue%5D=1&cHash=e64bfc7ab7dd6b70d161e4d556be9726'],
             // acme.com -> blog.acme.com (different site)
-            // @todo https://blog.acme.com/ not prefixed
-            ['https://acme.us/', 1100, 2000, '/?id=blog-root&testing%5Bvalue%5D=1&cHash=a14da633e46dba71640cb85226cd12c5'],
-            ['https://acme.us/', 1100, 2100, '/?id=2100&testing%5Bvalue%5D=1&cHash=d23d74cb50383f8788a9930ec8ba679f'],
-            ['https://acme.us/', 1100, 2110, '/john/?id=2110&testing%5Bvalue%5D=1&cHash=bf25eea89f44a9a79dabdca98f38a432'],
-            ['https://acme.us/', 1100, 2111, '/john/?id=2111&testing%5Bvalue%5D=1&cHash=42dbaeb9172b6b1ca23b49941e194db2'],
+            ['https://acme.us/', 1100, 2000, 'https://blog.acme.com/?id=2000&testing%5Bvalue%5D=1&cHash=a14da633e46dba71640cb85226cd12c5'],
+            ['https://acme.us/', 1100, 2100, 'https://blog.acme.com/?id=2100&testing%5Bvalue%5D=1&cHash=d23d74cb50383f8788a9930ec8ba679f'],
+            ['https://acme.us/', 1100, 2110, 'https://blog.acme.com/john/?id=2110&testing%5Bvalue%5D=1&cHash=bf25eea89f44a9a79dabdca98f38a432'],
+            ['https://acme.us/', 1100, 2111, 'https://blog.acme.com/john/?id=2111&testing%5Bvalue%5D=1&cHash=42dbaeb9172b6b1ca23b49941e194db2'],
             // blog.acme.com -> acme.com (different site)
-            // @todo https://acme.com/ not prefixed
-            ['https://blog.acme.com/', 2100, 1000, '/?id=acme-root&testing%5Bvalue%5D=1&cHash=7d1f13fa91159dac7feb3c824936b39d'],
-            ['https://blog.acme.com/', 2100, 1100, '/?id=acme-first&testing%5Bvalue%5D=1&cHash=f42b850e435f0cedd366f5db749fc1af'],
-            ['https://blog.acme.com/', 2100, 1200, '/?id=1200&testing%5Bvalue%5D=1&cHash=784e11c50ea1a13fd7d969df4ec53ea3'],
-            ['https://blog.acme.com/', 2100, 1210, '/?id=1210&testing%5Bvalue%5D=1&cHash=ccb7067022b9835ebfd8f720722bc708'],
-            ['https://blog.acme.com/', 2100, 404, '/?id=404&testing%5Bvalue%5D=1&cHash=864e96f586a78a53452f3bf0f4d24591'],
+            ['https://blog.acme.com/', 2100, 1000, 'https://acme.us/?id=1000&testing%5Bvalue%5D=1&cHash=7d1f13fa91159dac7feb3c824936b39d'],
+            ['https://blog.acme.com/', 2100, 1100, 'https://acme.us/?id=1100&testing%5Bvalue%5D=1&cHash=f42b850e435f0cedd366f5db749fc1af'],
+            ['https://blog.acme.com/', 2100, 1200, 'https://acme.us/?id=1200&testing%5Bvalue%5D=1&cHash=784e11c50ea1a13fd7d969df4ec53ea3'],
+            ['https://blog.acme.com/', 2100, 1210, 'https://acme.us/?id=1210&testing%5Bvalue%5D=1&cHash=ccb7067022b9835ebfd8f720722bc708'],
+            ['https://blog.acme.com/', 2100, 404, 'https://acme.us/?id=404&testing%5Bvalue%5D=1&cHash=864e96f586a78a53452f3bf0f4d24591'],
             // blog.acme.com -> products.acme.com (different sub-site)
-            ['https://blog.acme.com/', 2100, 1300, '/?id=1300&testing%5Bvalue%5D=1&cHash=dbd6597d72ed5098cce3d03eac1eeefe'],
-            ['https://blog.acme.com/', 2100, 1310, '/?id=1310&testing%5Bvalue%5D=1&cHash=e64bfc7ab7dd6b70d161e4d556be9726'],
+            ['https://blog.acme.com/', 2100, 1300, 'https://products.acme.com/?id=1300&testing%5Bvalue%5D=1&cHash=dbd6597d72ed5098cce3d03eac1eeefe'],
+            ['https://blog.acme.com/', 2100, 1310, 'https://products.acme.com/?id=1310&testing%5Bvalue%5D=1&cHash=e64bfc7ab7dd6b70d161e4d556be9726'],
         ];
 
         return $this->keysFromTemplate(
@@ -564,16 +555,17 @@ class LinkGeneratorTest extends AbstractTestCase
         // -> most probably since pid=-1 is not correctly resolved
         $instructions = [
             // acme.com -> acme.com (same site)
-            ['https://acme.us/', 1100, 1100, false, '/?id=acme-first'],
-            ['https://acme.us/', 1100, 1100, true, '/index.php?id=acme-first&L=0'],
+            ['https://acme.us/', 1100, 1100, false, '/?id=1100'],
+            ['https://acme.us/', 1100, 1100, true, '/index.php?id=acme-first'], // @todo Alias not removed, yet
             // ['https://acme.us/', 1100, 1950, false, '/?id=1950'], // @todo Not generated for new-placeholder
-            ['https://acme.us/', 1100, 1950, true, '/index.php?id={targetPageId}&L=0'],
+            ['https://acme.us/', 1100, 1950, true, '/index.php?id={targetPageId}'],
             // blog.acme.com -> acme.com (different site)
-            // @todo https://acme.com/ not prefixed
-            ['https://blog.acme.com/', 2100, 1100, false, '/?id=acme-first'],
-            ['https://blog.acme.com/', 2100, 1100, true, '/index.php?id=acme-first&L=0'],
+            ['https://blog.acme.com/', 2100, 1100, false, 'https://acme.us/?id=1100'],
+            // @todo https://acme.us/ not prefixed for resolved version
+            ['https://blog.acme.com/', 2100, 1100, true, '/index.php?id=acme-first'], // @todo Alias not removed, yet
             // ['https://blog.acme.com/', 2100, 1950, false, '/?id=1950'], // @todo Not generated for new-placeholder
-            ['https://blog.acme.com/', 2100, 1950, true, '/index.php?id={targetPageId}&L=0'],
+            // @todo https://acme.us/ not prefixed for resolved version
+            ['https://blog.acme.com/', 2100, 1950, true, '/index.php?id={targetPageId}'],
         ];
 
         return $this->keysFromTemplate(
@@ -632,7 +624,7 @@ class LinkGeneratorTest extends AbstractTestCase
                 'https://acme.us/',
                 1100,
                 [
-                    ['title' => 'EN: Welcome', 'link' => '/?id=acme-first'],
+                    ['title' => 'EN: Welcome', 'link' => '/?id=1100'],
                     [
                         'title' => 'EN: Features',
                         'link' => '/?id=1200',
@@ -645,19 +637,19 @@ class LinkGeneratorTest extends AbstractTestCase
                     ],
                     [
                         'title' => 'EN: Products',
-                        'link' => '/?id=1300',
+                        'link' => 'https://products.acme.com/?id=1300',
                         'children' => [
                             [
                                 'title' => 'EN: Planets',
-                                'link' => '/?id=1310',
+                                'link' => 'https://products.acme.com/?id=1310',
                             ],
                             [
                                 'title' => 'EN: Spaceships',
-                                'link' => '/?id=1320',
+                                'link' => 'https://products.acme.com/?id=1320',
                             ],
                             [
                                 'title' => 'EN: Dark Matter',
-                                'link' => '/?id=1330',
+                                'link' => 'https://products.acme.com/?id=1330',
                             ],
                         ],
                     ],
@@ -669,21 +661,20 @@ class LinkGeneratorTest extends AbstractTestCase
                         'children' => [
                             [
                                 'title' => 'Markets',
-                                'link' => '/index.php?id=7110&MP=7100-1700&L=0',
+                                'link' => '/index.php?id=7110&MP=7100-1700',
                             ],
                             [
                                 'title' => 'Products',
-                                'link' => '/index.php?id=7120&MP=7100-1700&L=0',
+                                'link' => '/index.php?id=7120&MP=7100-1700',
                             ],
                             [
                                 'title' => 'Partners',
-                                'link' => '/index.php?id=7130&MP=7100-1700&L=0',
+                                'link' => '/index.php?id=7130&MP=7100-1700',
                             ],
                         ],
                     ],
                     ['title' => 'Page not found', 'link' => '/?id=404'],
-                    // @todo Link should be prefixed with different site
-                    ['title' => 'Our Blog', 'link' => '/?id=2100'],
+                    ['title' => 'Our Blog', 'link' => 'https://blog.acme.com/?id=2100'],
                 ]
             ],
             'ACME Blog' => [
@@ -696,11 +687,11 @@ class LinkGeneratorTest extends AbstractTestCase
                         'children' => [
                             [
                                 'title' => 'John Doe',
-                                'link' => '/john/?id=2110',
+                                'link' => 'https://blog.acme.com/john/?id=2110',
                             ],
                             [
                                 'title' => 'Jane Doe',
-                                'link' => '/jane/?id=2120',
+                                'link' => 'https://blog.acme.com/jane/?id=2120',
                             ],
                         ],
                     ],
@@ -711,20 +702,19 @@ class LinkGeneratorTest extends AbstractTestCase
                             'children' => [
                                 [
                                     'title' => 'Markets',
-                                    'link' => '/index.php?id=7110&MP=7100-2700&L=0',
+                                    'link' => '/index.php?id=7110&MP=7100-2700',
                                 ],
                                 [
                                     'title' => 'Products',
-                                    'link' => '/index.php?id=7120&MP=7100-2700&L=0',
+                                    'link' => '/index.php?id=7120&MP=7100-2700',
                                 ],
                                 [
                                     'title' => 'Partners',
-                                    'link' => '/index.php?id=7130&MP=7100-2700&L=0',
+                                    'link' => '/index.php?id=7130&MP=7100-2700',
                                 ],
                             ],
                         ],
-                    // @todo Link should be prefixed with different site
-                    ['title' => 'ACME Inc', 'link' => '/?id=acme-first'],
+                    ['title' => 'ACME Inc', 'link' => 'https://acme.us/?id=1100'],
                 ]
             ]
         ];