[BUGFIX] Ensure most site related exceptions are handled
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Typolink / PageLinkBuilder.php
index 001aa65..a8d7caf 100644 (file)
@@ -18,11 +18,14 @@ 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\Exception\SiteNotFoundException;
-use TYPO3\CMS\Core\Routing\PageUriBuilder;
+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;
@@ -42,6 +45,7 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
 {
     /**
      * @inheritdoc
+     * @throws UnableToLinkException
      */
     public function build(array &$linkDetails, string $linkText, string $target, array $conf): array
     {
@@ -62,27 +66,18 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         }
 
         // Looking up the page record to verify its existence:
-        $page = $this->resolvePage($tsfe->sys_page, $linkDetails, $conf, $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);
         }
 
-        $languageField = $GLOBALS['TCA']['pages']['ctrl']['languageField'] ?? null;
-        $language = (int)($page[$languageField] ?? 0);
-        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'];
@@ -168,6 +163,27 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
             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']);
@@ -177,23 +193,14 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
             $siteOfTargetPage = null;
             $currentSite = null;
         }
+
         // Link to a page that has a site configuration
         if ($siteOfTargetPage instanceof Site) {
-            $queryParameters = [];
-            parse_str($addQueryParams, $queryParameters);
-            // get config.linkVars and prepend
-            if ($tsfe->linkVars) {
-                $globalQueryParameters = [];
-                parse_str($tsfe->linkVars, $globalQueryParameters);
-                if (!empty($globalQueryParameters)) {
-                    $queryParameters = array_merge_recursive($globalQueryParameters, $queryParameters);
-                }
-            }
-            unset($queryParameters['id'], $queryParameters['L']);
+            // 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);
 
@@ -219,7 +226,21 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
                 }
             }
         } else {
-            list($url, $target) = $this->generateUrlForPageWithoutSiteConfiguration($page, $addQueryParams, $conf, $pageType, $sectionMark, $target, $MPvarAcc);
+            // 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:
@@ -255,15 +276,16 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
      * language parent, adjusts `$linkDetails['pageuid']` (for hook processing)
      * and modifies `$configuration['language']` (for language URL generation).
      *
-     * @param PageRepository $pageRepository
      * @param array $linkDetails
      * @param array $configuration
      * @param bool $disableGroupAccessCheck
      * @return array
      */
-    protected function resolvePage(PageRepository $pageRepository, array &$linkDetails, array &$configuration, bool $disableGroupAccessCheck): array
+    protected function resolvePage(array &$linkDetails, array &$configuration, bool $disableGroupAccessCheck): array
     {
-        // Looking up the page record to verify its existence:
+        $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)) {
@@ -274,10 +296,12 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         $languageParentField = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] ?? null;
         $language = (int)($page[$languageField] ?? 0);
 
-        if ($language <= 0 || empty($page[$languageParentField])) {
+        // 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
@@ -286,8 +310,10 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
             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;
     }
 
@@ -308,7 +334,7 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         $currentSiteLanguage = $this->getCurrentSiteLanguage();
         // Happens when currently on a pseudo-site configuration
         // We assume to use the default language then
-        if (!($currentSiteLanguage instanceof SiteLanguage)) {
+        if ($currentSite && !($currentSiteLanguage instanceof SiteLanguage)) {
             $currentSiteLanguage = $currentSite->getDefaultLanguage();
         }
 
@@ -328,22 +354,26 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         // 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()) {
+        // 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']);
-
-        $uri = GeneralUtility::makeInstance(PageUriBuilder::class)->buildUri(
-            $targetPageId,
-            $queryParameters,
-            $fragment,
-            ['site' => $siteOfTargetPage, 'language' => $siteLanguageOfTargetPage],
-            $useAbsoluteUrl ? PageUriBuilder::ABSOLUTE_URL : PageUriBuilder::ABSOLUTE_PATH
-        );
+        $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';
@@ -356,7 +386,7 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
      * Generate a URL for a page without site configuration
      *
      * @param array $page
-     * @param string $additionalQueryParams
+     * @param array $additionalQueryParams
      * @param array $conf
      * @param string $pageType
      * @param string $sectionMark
@@ -364,20 +394,12 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
      * @param array $MPvarAcc
      * @return array
      */
-    protected function generateUrlForPageWithoutSiteConfiguration(array $page, string $additionalQueryParams, array $conf, string $pageType, string $sectionMark, string $target, array $MPvarAcc): array
+    protected function generateUrlForPageWithoutSiteConfiguration(array $page, array $additionalQueryParams, array $conf, string $pageType, string $sectionMark, string $target, array $MPvarAcc): array
     {
-        // new 'language' property takes precedence over '&L=1' if numeric
-        // here 'additionalParams=&L={language-value}' will be overridden
-        if (MathUtility::canBeInterpretedAsInteger($conf['language'] ?? '')) {
-            $queryParameters = [];
-            parse_str($additionalQueryParams, $queryParameters);
-            $queryParameters['L'] = $conf['language'];
-            $additionalQueryParams = http_build_query(
-                $queryParameters,
-                '',
-                '&',
-                PHP_QUERY_RFC3986
-            );
+        // Build a string out of the query parameters
+        $additionalQueryParams = http_build_query($additionalQueryParams, '', '&', PHP_QUERY_RFC3986);
+        if (!empty($additionalQueryParams)) {
+            $additionalQueryParams = '&' . $additionalQueryParams;
         }
 
         $tsfe = $this->getTypoScriptFrontendController();
@@ -766,7 +788,12 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         }
         if (MathUtility::canBeInterpretedAsInteger($GLOBALS['TSFE']->id) && $GLOBALS['TSFE']->id > 0) {
             $matcher = GeneralUtility::makeInstance(SiteMatcher::class);
-            return $matcher->matchByPageId((int)$GLOBALS['TSFE']->id);
+            try {
+                $site = $matcher->matchByPageId((int)$GLOBALS['TSFE']->id);
+            } catch (SiteNotFoundException $e) {
+                $site = null;
+            }
+            return $site;
         }
         return null;
     }
@@ -784,4 +811,25 @@ class PageLinkBuilder extends AbstractTypolinkBuilder
         }
         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;
+    }
 }