[FEATURE] Allow extended custom links in FormEngine and TypoLink 64/52264/16
authorBenni Mack <benni@typo3.org>
Thu, 30 Mar 2017 13:47:23 +0000 (15:47 +0200)
committerFrank Naegler <frank.naegler@typo3.org>
Sun, 2 Apr 2017 12:46:44 +0000 (14:46 +0200)
This patch extracts TypoLink generation into separate classes based
on the link type calculated via the LinkService functionality.

Via the configuration $TYPO3_CONF_VARS[FE][typolinkBuilder][$linkType]
new types can be added or existing implementations can be overriden.

Resolves: #80619
Releases: master
Change-Id: Id1dc028cb306ac50352302b8a5c9725ab7f04b31
Reviewed-on: https://review.typo3.org/52264
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Naegler <frank.naegler@typo3.org>
14 files changed:
typo3/sysext/backend/Classes/Form/Element/InputLinkElement.php
typo3/sysext/core/Configuration/DefaultConfiguration.php
typo3/sysext/core/Documentation/Changelog/master/Feature-80619-ExtendLinkGenerationWithinTypolink.rst [new file with mode: 0644]
typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
typo3/sysext/frontend/Classes/Typolink/AbstractTypolinkBuilder.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Typolink/DatabaseRecordLinkBuilder.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Typolink/EmailLinkBuilder.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Typolink/ExternalUrlLinkBuilder.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Typolink/FileOrFolderLinkBuilder.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Typolink/LegacyLinkBuilder.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Typolink/UnableToLinkException.php [new file with mode: 0644]
typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php
typo3/sysext/frontend/Tests/Unit/Typolink/AbstractTypolinkBuilderTest.php [new file with mode: 0644]

index 5594610..050d8e4 100644 (file)
@@ -307,7 +307,6 @@ class InputLinkElement extends AbstractFormElement
             return [];
         }
         $data = ['text' => '', 'icon' => ''];
-        $linkData = [];
         $typolinkService = GeneralUtility::makeInstance(TypoLinkCodecService::class);
         $linkParts = $typolinkService->decode($itemValue);
         $linkService = GeneralUtility::makeInstance(LinkService::class);
@@ -320,6 +319,32 @@ class InputLinkElement extends AbstractFormElement
             return $data;
         }
 
+        // Resolving the TypoLink parts (class, title, params)
+        $additionalAttributes = [];
+        foreach ($linkParts as $key => $value) {
+            if ($key === 'url') {
+                continue;
+            }
+            if ($value) {
+                switch ($key) {
+                    case 'class':
+                        $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_browse_links.xlf:class');
+                        break;
+                    case 'title':
+                        $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_browse_links.xlf:title');
+                        break;
+                    case 'additionalParams':
+                        $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_browse_links.xlf:params');
+                        break;
+                    default:
+                        $label = $key;
+                }
+
+                $additionalAttributes[] = '<span><strong>' . htmlspecialchars($label) . ': </strong> ' . htmlspecialchars($value) . '</span>';
+            }
+        }
+
+        // Resolve the actual link
         switch ($linkData['type']) {
             case LinkService::TYPE_PAGE:
                 $pageRecord = BackendUtility::readPageAccess($linkData['pageuid'], '1=1');
@@ -375,36 +400,20 @@ class InputLinkElement extends AbstractFormElement
                 ];
                 break;
             default:
-                $data = [
-                    'text' => 'not implemented type ' . $linkData['type'],
-                    'icon' => ''
-                ];
-                // @todo this needs a hook for being extensible for other link types. forge #79647
-        }
-
-        $additionalAttributes = [];
-        unset($linkParts['url']);
-        foreach ($linkParts as $key => $value) {
-            if ($value) {
-                switch ($key) {
-                    case 'class':
-                        $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_browse_links.xlf:class');
-                        break;
-                    case 'title':
-                        $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_browse_links.xlf:title');
-                        break;
-                    case 'additionalParams':
-                        $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_browse_links.xlf:params');
-                        break;
-                    default:
-                        $label = $key;
+                // Please note that this hook is preliminary and might change, as this element could become its own
+                // TCA type in the future
+                if (isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['linkHandler'][$linkData['type']])) {
+                    $linkBuilder = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['linkHandler'][$linkData['type']]);
+                    $data = $linkBuilder->getFormData($linkData, $linkParts, $this->data, $this);
+                } else {
+                    $data = [
+                        'text' => 'not implemented type ' . $linkData['type'],
+                        'icon' => ''
+                    ];
                 }
-
-                $additionalAttributes[] = '<span><strong>' . htmlspecialchars($label) . ': </strong> ' . htmlspecialchars($value) . '</span>';
-            }
         }
-        $data['additionalAttributes'] = '<div class="help-block">' . implode(' - ', $additionalAttributes) . '</div>';
 
+        $data['additionalAttributes'] = '<div class="help-block">' . implode(' - ', $additionalAttributes) . '</div>';
         return $data;
     }
 
index 037c6a4..dba8254 100644 (file)
@@ -1000,6 +1000,15 @@ return [
         'versionNumberInFilename' => 'querystring',
         'contentRenderingTemplates' => [], // Array to define the TypoScript parts that define the main content rendering. Extensions like "css_styled_content" provide content rendering templates. Other extensions like "felogin" or "indexed search" extend these templates and their TypoScript parts are added directly after the content templates. See EXT:css_styled_content/ext_localconf.php and EXT:frontend/Classes/TypoScript/TemplateService.php
         'ContentObjects' => [], // Array to register ContentObject (cObjects) like TEXT or HMENU within ext_localconf.php, see EXT:frontend/ext_localconf.php
+        'typolinkBuilder' => [  // Matches the LinkService implementations for generating URL, link text via typolink
+            'page' => \TYPO3\CMS\Frontend\Typolink\PageLinkBuilder::class,
+            'file' => \TYPO3\CMS\Frontend\Typolink\FileOrFolderLinkBuilder::class,
+            'folder' => \TYPO3\CMS\Frontend\Typolink\FileOrFolderLinkBuilder::class,
+            'url' => \TYPO3\CMS\Frontend\Typolink\ExternalUrlLinkBuilder::class,
+            'email' => \TYPO3\CMS\Frontend\Typolink\EmailLinkBuilder::class,
+            'record' => \TYPO3\CMS\Frontend\Typolink\DatabaseRecordLinkBuilder::class,
+            'unknown' => \TYPO3\CMS\Frontend\Typolink\LegacyLinkBuilder::class,
+        ],
     ],
     'MAIL' => [ // Mail configurations to tune how \TYPO3\CMS\Core\Mail\ classes will send their mails.
         'transport' => 'mail',
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-80619-ExtendLinkGenerationWithinTypolink.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-80619-ExtendLinkGenerationWithinTypolink.rst
new file mode 100644 (file)
index 0000000..64fc8cf
--- /dev/null
@@ -0,0 +1,29 @@
+.. include:: ../../Includes.txt
+
+========================================================
+Feature: #80619 - Extend Link Generation within Typolink
+========================================================
+
+See :issue:`80619`
+
+Description
+===========
+
+Generating a link to a page, email, url, email in the TYPO3 Frontend is usually handled via the
+so-called ``typolink`` functionality. Generating links is now flexible, extensions can register
+their own link-building functionality via ``$GLOBALS[TYPO3_CONF_VARS][FE][typolinkBuilder][$linkType]`
+in the extensions' ext_localconf.php.
+
+All existing functionality for Typolink via TypoScript etc. still works as before.
+
+
+Impact
+======
+
+The TYPO3 Core itself handles all native link types (email, url, page, record, file, folder) via this functionality
+already, and it can be overridden.
+
+The functionality goes hand-in-hand with the LinkService registration functionality for setting links of a specific
+type.
+
+.. index:: Frontend, PHP-API
\ No newline at end of file
index f395671..6038dfc 100644 (file)
@@ -58,9 +58,10 @@ use TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 use TYPO3\CMS\Frontend\Http\UrlProcessorInterface;
 use TYPO3\CMS\Frontend\Imaging\GifBuilder;
-use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
 use TYPO3\CMS\Frontend\Page\PageRepository;
 use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
+use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;
+use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
 
 /**
  * This class contains all main TypoScript features.
@@ -5485,7 +5486,8 @@ class ContentObjectRenderer
      * @param array $configuration TypoScript configuration
      * @return array|string
      * @see typoLink()
-     * @todo the whole thing does not work like this anymore. remove the whole function. forge #79647
+     *
+     * @todo the functionality of the "file:" syntax + the hook should be marked as deprecated, an upgrade wizard should handle existing links
      */
     protected function resolveMixedLinkParameter($linkText, $mixedLinkParameter, &$configuration = [])
     {
@@ -5494,7 +5496,7 @@ class ContentObjectRenderer
         // Link parameter value = first part
         $linkParameterParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($mixedLinkParameter);
 
-        // Check for link-handler keyword:
+        // Check for link-handler keyword
         list($linkHandlerKeyword, $linkHandlerValue) = explode(':', $linkParameterParts['url'], 2);
         if ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typolinkLinkHandler'][$linkHandlerKeyword] && (string)$linkHandlerValue !== '') {
             $linkHandlerObj = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typolinkLinkHandler'][$linkHandlerKeyword]);
@@ -5561,9 +5563,6 @@ class ContentObjectRenderer
         $linkText = (string)$linkText;
         $tsfe = $this->getTypoScriptFrontendController();
 
-        $LD = [];
-        $finalTagParts = [];
-        $finalTagParts['aTagParams'] = $this->getATagParams($conf);
         $linkParameter = trim(isset($conf['parameter.']) ? $this->stdWrap($conf['parameter'], $conf['parameter.']) : $conf['parameter']);
         $this->lastTypoLinkUrl = '';
         $this->lastTypoLinkTarget = '';
@@ -5573,7 +5572,6 @@ class ContentObjectRenderer
         if (!is_array($resolvedLinkParameters)) {
             return $resolvedLinkParameters;
         }
-
         $linkParameter = $resolvedLinkParameters['href'];
         $target = $resolvedLinkParameters['target'];
         $title = $resolvedLinkParameters['title'];
@@ -5583,333 +5581,30 @@ class ContentObjectRenderer
         }
 
         // Detecting kind of link and resolve all necessary parameters
-        /** @var LinkService $linkService */
         $linkService = GeneralUtility::makeInstance(LinkService::class);
         $linkDetails = $linkService->resolve($linkParameter);
-        switch ($linkDetails['type']) {
-            // If it's a mail address
-            case LinkService::TYPE_EMAIL:
-                list($this->lastTypoLinkUrl, $linkText) = $this->getMailTo($linkDetails['email'], $linkText);
-            break;
-
-            // URL (external)
-            case LinkService::TYPE_URL:
-                $target = $target ?: $this->resolveTargetAttribute($conf, 'extTarget', true, $tsfe->extTarget);
-                $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $linkDetails['url']);
-                $this->lastTypoLinkUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_EXTERNAL, $linkDetails['url'], $conf);
-            break;
-
-            // File (internal)
-            case LinkService::TYPE_FILE:
-            case LinkService::TYPE_FOLDER:
-                $fileOrFolderObject = $linkDetails['file'] ? $linkDetails['file'] : $linkDetails['folder'];
-                // check if the file exists or if a / is contained (same check as in detectLinkType)
-                if ($fileOrFolderObject instanceof FileInterface || $fileOrFolderObject instanceof Folder) {
-                    $linkLocation = $fileOrFolderObject->getPublicUrl();
-                    // Setting title if blank value to link
-                    $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, rawurldecode($linkLocation));
-                    $linkLocation = (strpos($linkLocation, '/') !== 0 ? $tsfe->absRefPrefix : '') . $linkLocation;
-                    $this->lastTypoLinkUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_FILE, $linkLocation, $conf);
-                    $this->lastTypoLinkUrl = $this->forceAbsoluteUrl($this->lastTypoLinkUrl, $conf);
-
-                    $target = $target ?: $this->resolveTargetAttribute($conf, 'fileTarget', false, $tsfe->fileTarget);
-                } else {
-                    $this->getTimeTracker()->setTSlogMessage('typolink(): File "' . $linkParameter . '" did not exist, so "' . $linkText . '" was not linked.', 1);
-                    return $linkText;
-                }
-            break;
-
-            // Link to a page
-            case LinkService::TYPE_PAGE:
-                // Checking if the id-parameter is an alias.
-                if (!empty($linkDetails['pagealias'])) {
-                    $linkDetails['pageuid'] = $tsfe->sys_page->getPageIdFromAlias($linkDetails['pagealias']);
-                } elseif (empty($linkDetails['pageuid']) || $linkDetails['pageuid'] === 'current') {
-                    // If no id or alias is given
-                    $linkDetails['pageuid'] = $tsfe->id;
-                }
-
-                // Link to page even if access is missing?
-                if (isset($conf['linkAccessRestrictedPages'])) {
-                    $disableGroupAccessCheck = (bool)$conf['linkAccessRestrictedPages'];
-                } else {
-                    $disableGroupAccessCheck = (bool)$tsfe->config['config']['typolinkLinkAccessRestrictedPages'];
-                }
-
-                // Looking up the page record to verify its existence:
-                $page = $tsfe->sys_page->getPage($linkDetails['pageuid'], $disableGroupAccessCheck);
-
-                if (!empty($page)) {
-                    if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typolinkProcessing']['typolinkModifyParameterForPageLinks'])) {
-                        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 */
-                            $conf = $hookObject->modifyPageLinkConfiguration($conf, $linkDetails, $page);
-                        }
-                    }
-                    $enableLinksAcrossDomains = $tsfe->config['config']['typolinkEnableLinksAcrossDomains'];
-                    if ($conf['no_cache.']) {
-                        $conf['no_cache'] = $this->stdWrap($conf['no_cache'], $conf['no_cache.']);
-                    }
-
-                    $sectionMark = trim(isset($conf['section.']) ? $this->stdWrap($conf['section'], $conf['section.']) : $conf['section']);
-                    if ($sectionMark === '' && isset($linkDetails['fragment'])) {
-                        $sectionMark = $linkDetails['fragment'];
-                    }
-                    if ($sectionMark !== '') {
-                        $sectionMark = '#' . (MathUtility::canBeInterpretedAsInteger($sectionMark) ? 'c' : '') . $sectionMark;
-                    }
-                    // Overruling 'type'
-                    $pageType = $linkDetails['pagetype'] ?? 0;
-
-                    if (isset($linkDetails['parameters'])) {
-                        $conf['additionalParams'] .= '&' . ltrim($linkDetails['parameters'], '&');
-                    }
-                    // MointPoints, look for closest MPvar:
-                    $MPvarAcc = [];
-                    if (!$tsfe->config['config']['MP_disableTypolinkClosestMPvalue']) {
-                        $temp_MP = $this->getClosestMPvalueForPage($page['uid'], true);
-                        if ($temp_MP) {
-                            $MPvarAcc['closest'] = $temp_MP;
-                        }
-                    }
-                    // Look for overlay Mount Point:
-                    $mount_info = $tsfe->sys_page->getMountPointInfo($page['uid'], $page);
-                    if (is_array($mount_info) && $mount_info['overlay']) {
-                        $page = $tsfe->sys_page->getPage($mount_info['mount_pid'], $disableGroupAccessCheck);
-                        if (empty($page)) {
-                            $this->getTimeTracker()->setTSlogMessage('typolink(): Mount point "' . $mount_info['mount_pid'] . '" was not available, so "' . $linkText . '" was not linked.', 1);
-                            return $linkText;
-                        }
-                        $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->getQueryArguments($conf['addQueryString.']) : '';
-                    $addQueryParams .= isset($conf['additionalParams.']) ? trim($this->stdWrap($conf['additionalParams'], $conf['additionalParams.'])) : trim($conf['additionalParams']);
-                    if ($addQueryParams === '&' || $addQueryParams[0] !== '&') {
-                        $addQueryParams = '';
-                    }
-                    $targetDomain = '';
-                    $currentDomain = (string)$this->getEnvironmentVariable('HTTP_HOST');
-                    // Mount pages are always local and never link to another domain
-                    if (!empty($MPvarAcc)) {
-                        // Add "&MP" var:
-                        $addQueryParams .= '&MP=' . rawurlencode(implode(',', $MPvarAcc));
-                    } elseif (strpos($addQueryParams, '&MP=') === false && $tsfe->config['config']['typolinkCheckRootline']) {
-                        // We do not come here if additionalParams had '&MP='. This happens when typoLink is called from
-                        // menu. Mount points always work in the content of the current domain and we must not change
-                        // domain if MP variables exist.
-                        // If we link across domains and page is free type shortcut, we must resolve the shortcut first!
-                        // If we do not do it, TYPO3 will fail to (1) link proper page in RealURL/CoolURI because
-                        // they return relative links and (2) show proper page if no RealURL/CoolURI exists when link is clicked
-                        if ($enableLinksAcrossDomains
-                            && (int)$page['doktype'] === PageRepository::DOKTYPE_SHORTCUT
-                            && (int)$page['shortcut_mode'] === PageRepository::SHORTCUT_MODE_NONE
-                        ) {
-                            // Save in case of broken destination or endless loop
-                            $page2 = $page;
-                            // Same as in RealURL, seems enough
-                            $maxLoopCount = 20;
-                            while ($maxLoopCount
-                                && is_array($page)
-                                && (int)$page['doktype'] === PageRepository::DOKTYPE_SHORTCUT
-                                && (int)$page['shortcut_mode'] === PageRepository::SHORTCUT_MODE_NONE
-                            ) {
-                                $page = $tsfe->sys_page->getPage($page['shortcut'], $disableGroupAccessCheck);
-                                $maxLoopCount--;
-                            }
-                            if (empty($page) || $maxLoopCount === 0) {
-                                // We revert if shortcut is broken or maximum number of loops is exceeded (indicates endless loop)
-                                $page = $page2;
-                            }
-                        }
-
-                        $targetDomain = $tsfe->getDomainNameForPid($page['uid']);
-                        // 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, '& ') != '') {
-                            /** @var $cacheHash CacheHashCalculator */
-                            $cacheHash = GeneralUtility::makeInstance(CacheHashCalculator::class);
-                            $cHash = $cacheHash->generateForParameters($params);
-                            $addQueryParams .= $cHash ? '&cHash=' . $cHash : '';
-                        }
-                        unset($params);
-                    }
-                    $absoluteUrlScheme = 'http';
-                    // URL shall be absolute:
-                    if (isset($conf['forceAbsoluteUrl']) && $conf['forceAbsoluteUrl']) {
-                        // Override scheme:
-                        if (isset($conf['forceAbsoluteUrl.']['scheme']) && $conf['forceAbsoluteUrl.']['scheme']) {
-                            $absoluteUrlScheme = $conf['forceAbsoluteUrl.']['scheme'];
-                        } elseif ($this->getEnvironmentVariable('TYPO3_SSL')) {
-                            $absoluteUrlScheme = 'https';
-                        }
-                        // If no domain records are defined, use current domain:
-                        $currentUrlScheme = parse_url($this->getEnvironmentVariable('TYPO3_REQUEST_URL'), PHP_URL_SCHEME);
-                        if ($targetDomain === '' && ($conf['forceAbsoluteUrl'] || $absoluteUrlScheme !== $currentUrlScheme)) {
-                            $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) {
-                            $targetDomain = $currentDomain . rtrim($this->getEnvironmentVariable('TYPO3_SITE_PATH'), '/');
-                        }
-                    }
-                    // If target page has a different domain and the current domain's linking scheme (e.g. RealURL/...) should not be used
-                    if ($targetDomain !== '' && $targetDomain !== $currentDomain && !$enableLinksAcrossDomains) {
-                        $target = $target ?: $this->resolveTargetAttribute($conf, 'extTarget', false, $tsfe->extTarget);
-                        $LD['target'] = $target;
-                        // Convert IDNA-like domain (if any)
-                        if (!preg_match('/^[a-z0-9.\\-]*$/i', $targetDomain)) {
-                            $targetDomain =  GeneralUtility::idnaEncode($targetDomain);
-                        }
-                        $this->lastTypoLinkUrl = $absoluteUrlScheme . '://' . $targetDomain . '/index.php?id=' . $page['uid'] . $addQueryParams . $sectionMark;
-                    } else {
-                        // Internal link or current domain's linking scheme should be used
-                        // Internal target:
-                        if (empty($target)) {
-                            $target = $this->resolveTargetAttribute($conf, 'target', true, $tsfe->intTarget);
-                        }
-                        $LD = $tsfe->tmpl->linkData($page, $target, $conf['no_cache'], '', '', $addQueryParams, $pageType, $targetDomain);
-                        if ($targetDomain !== '') {
-                            // We will add domain only if URL does not have it already.
-                            if ($enableLinksAcrossDomains && $targetDomain !== $currentDomain) {
-                                // 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']) {
-                                    $LD['totalURL'] = substr($LD['totalURL'], $prefixLength);
-                                }
-                            }
-                            $urlParts = parse_url($LD['totalURL']);
-                            if (empty($urlParts['host'])) {
-                                $LD['totalURL'] = $absoluteUrlScheme . '://' . $targetDomain . ($LD['totalURL'][0] === '/' ? '' : '/') . $LD['totalURL'];
-                            }
-                        }
-                        $this->lastTypoLinkUrl = $LD['totalURL'] . $sectionMark;
-                    }
-                    $target = $LD['target'];
-                    // 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)
-                        && (empty($conf['addQueryString']) || !isset($conf['addQueryString.']))
-                    ) {
-                        $currentQueryParams = $this->getQueryArguments([]);
-                        if (!trim($currentQueryParams)) {
-                            list(, $URLparams) = explode('?', $this->lastTypoLinkUrl);
-                            list($URLparams) = explode('#', $URLparams);
-                            parse_str($URLparams . $LD['orig_type'], $URLparamsArray);
-                            // Type nums must match as well as page ids
-                            if ((int)$URLparamsArray['type'] === (int)$tsfe->type) {
-                                unset($URLparamsArray['id']);
-                                unset($URLparamsArray['type']);
-                                // If there are no parameters left.... set the new url.
-                                if (empty($URLparamsArray)) {
-                                    $this->lastTypoLinkUrl = $sectionMark;
-                                }
-                            }
-                        }
-                    }
-                    // 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($this->lastTypoLinkUrl),
-                                $page['uid']
-                            ],
-                            $tsfe->config['config']['typolinkLinkAccessRestrictedPages_addParams']
-                        );
-                        $this->lastTypoLinkUrl = $this->getTypoLink_URL($thePage['uid'] . ($pageType ? ',' . $pageType : ''), $addParams, $target);
-                        $this->lastTypoLinkUrl = $this->forceAbsoluteUrl($this->lastTypoLinkUrl, $conf);
-                        $this->lastTypoLinkLD['totalUrl'] = $this->lastTypoLinkUrl;
-                        $LD = $this->lastTypoLinkLD;
-                    }
-                } else {
-                    $this->getTimeTracker()->setTSlogMessage('typolink(): Page id "' . $linkParameter . '" was not found, so "' . $linkText . '" was not linked.', 1);
-                    return $linkText;
-                }
-            break;
-            case LinkService::TYPE_RECORD:
-                $tsfe = $this->getTypoScriptFrontendController();
-                $configurationKey = $linkDetails['identifier'] . '.';
-                $configuration = $tsfe->tmpl->setup['config.']['recordLinks.'];
-                $linkHandlerConfiguration = $tsfe->pagesTSconfig['TCEMAIN.']['linkHandler.'];
-
-                if (!isset($configuration[$configurationKey]) || !isset($linkHandlerConfiguration[$configurationKey])) {
-                    return $linkText;
-                }
-                $typoScriptConfiguration = $configuration[$configurationKey]['typolink.'];
-                $linkHandlerConfiguration = $linkHandlerConfiguration[$configurationKey]['configuration.'];
-
-                if ($configuration[$configurationKey]['forceLink']) {
-                    $record = $tsfe->sys_page->getRawRecord($linkHandlerConfiguration['table'], $linkDetails['uid']);
-                } else {
-                    $record = $tsfe->sys_page->checkRecord($linkHandlerConfiguration['table'], $linkDetails['uid']);
-                }
-                if ($record === 0) {
-                    return $linkText;
-                }
-
-                // Build the full link to the record
-                $localContentObjectRenderer = GeneralUtility::makeInstance(self::class);
-                $localContentObjectRenderer->start($record, $linkHandlerConfiguration['table']);
-                $localContentObjectRenderer->parameters = $this->parameters;
-                $link = $localContentObjectRenderer->typoLink($linkText, $typoScriptConfiguration);
-
-                $this->lastTypoLinkLD = $localContentObjectRenderer->lastTypoLinkLD;
-                $this->lastTypoLinkUrl = $localContentObjectRenderer->lastTypoLinkUrl;
-                $this->lastTypoLinkTarget = $localContentObjectRenderer->lastTypoLinkTarget;
-
-                return $link;
-                break;
-
-            // Legacy files or something else
-            case LinkService::TYPE_UNKNOWN:
-                if ($linkDetails['file']) {
-                    $linkDetails['type'] = LinkService::TYPE_FILE;
-                    $linkLocation = $linkDetails['file'];
-                    // Setting title if blank value to link
-                    $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, rawurldecode($linkLocation));
-                    $linkLocation = (strpos($linkLocation, '/') !== 0 ? $tsfe->absRefPrefix : '') . $linkLocation;
-                    $this->lastTypoLinkUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_FILE, $linkLocation, $conf);
-                    $this->lastTypoLinkUrl = $this->forceAbsoluteUrl($this->lastTypoLinkUrl, $conf);
-                    $target = $target ?: $this->resolveTargetAttribute($conf, 'fileTarget', false, $tsfe->fileTarget);
-                } elseif ($linkDetails['url']) {
-                    $linkDetails['type'] = LinkService::TYPE_URL;
-                    $target = $target ?: $this->resolveTargetAttribute($conf, 'extTarget', true, $tsfe->extTarget);
-                    $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $linkDetails['url']);
-                    $this->lastTypoLinkUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_EXTERNAL, $linkDetails['url'], $conf);
-                }
-                break;
+        $linkDetails['typoLinkParameter'] = $linkParameter;
+        if (isset($GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']])) {
+            /** @var AbstractTypolinkBuilder $linkBuilder */
+            $linkBuilder = GeneralUtility::makeInstance(
+                $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']],
+                $this
+            );
+            try {
+                list($this->lastTypoLinkUrl, $linkText, $target) = $linkBuilder->build($linkDetails, $linkText, $target, $conf);
+            } catch (UnableToLinkException $e) {
+                // Only return the link text directly
+                return $e->getLinkText();
+            }
+        } else {
+            return $linkText;
         }
 
-        $this->lastTypoLinkTarget = $target;
-        $finalTagParts['url'] = $this->lastTypoLinkUrl;
-        $finalTagParts['TYPE'] = $linkDetails['type'];
-        $finalTagParts['aTagParams'] .= $this->extLinkATagParams($this->lastTypoLinkUrl, $linkDetails['type']);
-        $this->lastTypoLinkLD = $LD;
+        $finalTagParts = [
+            'aTagParams' => $this->getATagParams($conf) . $this->extLinkATagParams($this->lastTypoLinkUrl, $linkDetails['type']),
+            'url'        => $this->lastTypoLinkUrl,
+            'TYPE'       => $linkDetails['type']
+        ];
 
         // Building the final <a href=".."> tag
         $tagAttributes = [];
@@ -5949,8 +5644,8 @@ class ContentObjectRenderer
         }
 
         // Target attribute
-        if (!empty($this->lastTypoLinkTarget)) {
-            $tagAttributes['target'] = htmlspecialchars($this->lastTypoLinkTarget);
+        if (!empty($target)) {
+            $tagAttributes['target'] = htmlspecialchars($target);
         // Create TARGET-attribute only if the right doctype is used
         } elseif ($JSwindowParams && !in_array($tsfe->xhtmlDoctype, ['xhtml_strict', 'xhtml_11'], true)) {
             $tagAttributes['target'] = 'FEopenLink';
@@ -5972,6 +5667,7 @@ class ContentObjectRenderer
         }
         // kept for backwards-compatibility in hooks
         $finalTagParts['targetParams'] = !empty($tagAttributes['target']) ? ' target="' . $tagAttributes['target'] . '"' : '';
+        $this->lastTypoLinkTarget = $target;
 
         // Call user function:
         if ($conf['userFunc']) {
@@ -6015,93 +5711,6 @@ class ContentObjectRenderer
     }
 
     /**
-     * Helper method to a fallback method parsing HTML out of it
-     *
-     * @param string $originalLinkText the original string, if empty, the fallback link text
-     * @param string $fallbackLinkText the string to be used.
-     * @return string the final text
-     */
-    protected function parseFallbackLinkTextIfLinkTextIsEmpty($originalLinkText, $fallbackLinkText)
-    {
-        if ($originalLinkText === '') {
-            return $this->parseFunc($fallbackLinkText, ['makelinks' => 0], '< lib.parseFunc');
-        } else {
-            return $originalLinkText;
-        }
-    }
-
-    /**
-     * Creates the value for target="..." in a typolink configuration
-     *
-     * @param array $conf the typolink configuration
-     * @param string $name the key, usually "target", "extTarget" or "fileTarget"
-     * @param bool $respectFrameSetOption if set, then
-     * @param string $fallbackTarget
-     * @return string the value of the target attribute, if there is one
-     */
-    protected function resolveTargetAttribute(array $conf, string $name, bool $respectFrameSetOption = false, string $fallbackTarget = null): string
-    {
-        $tsfe = $this->getTypoScriptFrontendController();
-        $targetAttributeAllowed = (!$respectFrameSetOption || !$tsfe->config['config']['doctype'] ||
-            in_array((string)$tsfe->config['config']['doctype'], ['xhtml_trans', 'xhtml_frames', 'xhtml_basic', 'html5'], true));
-
-        $target = '';
-        if (isset($conf[$name])) {
-            $target = $conf[$name];
-        } elseif ($targetAttributeAllowed) {
-            $target = $fallbackTarget;
-        }
-        if ($conf[$name . '.']) {
-            $target = $this->stdWrap($target, $conf[$name . '.']);
-        }
-        return $target;
-    }
-
-    /**
-     * Forces a given URL to be absolute.
-     *
-     * @param string $url The URL to be forced to be absolute
-     * @param array $configuration TypoScript configuration of typolink
-     * @return string The absolute URL
-     */
-    protected function forceAbsoluteUrl($url, array $configuration)
-    {
-        if (!empty($url) && !empty($configuration['forceAbsoluteUrl']) &&  preg_match('#^(?:([a-z]+)(://)([^/]*)/?)?(.*)$#', $url, $matches)) {
-            $urlParts = [
-                'scheme' => $matches[1],
-                'delimiter' => '://',
-                'host' => $matches[3],
-                'path' => $matches[4]
-            ];
-            $isUrlModified = false;
-            // Set scheme and host if not yet part of the URL:
-            if (empty($urlParts['host'])) {
-                $urlParts['scheme'] = $this->getEnvironmentVariable('TYPO3_SSL') ? 'https' : 'http';
-                $urlParts['host'] = $this->getEnvironmentVariable('HTTP_HOST');
-                $urlParts['path'] = '/' . ltrim($urlParts['path'], '/');
-                // absRefPrefix has been prepended to $url beforehand
-                // so we only modify the path if no absRefPrefix has been set
-                // otherwise we would destroy the path
-                if ($this->getTypoScriptFrontendController()->absRefPrefix === '') {
-                    $urlParts['path'] = $this->getEnvironmentVariable('TYPO3_SITE_PATH') . ltrim($urlParts['path'], '/');
-                }
-                $isUrlModified = true;
-            }
-            // Override scheme:
-            $forceAbsoluteUrl = &$configuration['forceAbsoluteUrl.']['scheme'];
-            if (!empty($forceAbsoluteUrl) && $urlParts['scheme'] !== $forceAbsoluteUrl) {
-                $urlParts['scheme'] = $forceAbsoluteUrl;
-                $isUrlModified = true;
-            }
-            // Recreate the absolute URL:
-            if ($isUrlModified) {
-                $url = implode('', $urlParts);
-            }
-        }
-        return $url;
-    }
-
-    /**
      * Based on the input "TypoLink" TypoScript configuration this will return the generated URL
      *
      * @param array $conf TypoScript properties for "typolink
diff --git a/typo3/sysext/frontend/Classes/Typolink/AbstractTypolinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/AbstractTypolinkBuilder.php
new file mode 100644 (file)
index 0000000..c82d0dd
--- /dev/null
@@ -0,0 +1,196 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Frontend\Typolink;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Service\DependencyOrderingService;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+use TYPO3\CMS\Frontend\Http\UrlProcessorInterface;
+
+/**
+ * Abstract class to provide proper helper for most types necessary
+ * Hands in the contentobject which is needed here for all the stdWrap magic.
+ */
+abstract class AbstractTypolinkBuilder
+{
+    /**
+     * @var ContentObjectRenderer
+     */
+    protected $contentObjectRenderer;
+
+    /**
+     * AbstractTypolinkBuilder constructor.
+     *
+     * @param $contentObjectRenderer ContentObjectRenderer
+     */
+    public function __construct(ContentObjectRenderer $contentObjectRenderer)
+    {
+        $this->contentObjectRenderer = $contentObjectRenderer;
+    }
+
+    /**
+     * Should be implemented by all subclasses to return an array with three parts:
+     * - URL
+     * - Link Text (can be modified)
+     * - Target (can be modified)
+     *
+     * @param array $linkDetails parsed link details by the LinkService
+     * @param string $linkText the link text
+     * @param string $target the target to point to
+     * @param array $conf the TypoLink configuration array
+     * @return array an array with three parts (URL, Link Text, Target)
+     */
+    abstract public function build(array &$linkDetails, string $linkText, string $target, array $conf): array;
+
+    /**
+     * Forces a given URL to be absolute.
+     *
+     * @param string $url The URL to be forced to be absolute
+     * @param array $configuration TypoScript configuration of typolink
+     * @return string The absolute URL
+     */
+    protected function forceAbsoluteUrl(string $url, array $configuration): string
+    {
+        if (!empty($url) && !empty($configuration['forceAbsoluteUrl']) &&  preg_match('#^(?:([a-z]+)(://)([^/]*)/?)?(.*)$#', $url, $matches)) {
+            $urlParts = [
+                'scheme' => $matches[1],
+                'delimiter' => '://',
+                'host' => $matches[3],
+                'path' => $matches[4]
+            ];
+            $isUrlModified = false;
+            // Set scheme and host if not yet part of the URL:
+            if (empty($urlParts['host'])) {
+                $urlParts['scheme'] = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https' : 'http';
+                $urlParts['host'] = GeneralUtility::getIndpEnv('HTTP_HOST');
+                $urlParts['path'] = '/' . ltrim($urlParts['path'], '/');
+                // absRefPrefix has been prepended to $url beforehand
+                // so we only modify the path if no absRefPrefix has been set
+                // otherwise we would destroy the path
+                if ($this->getTypoScriptFrontendController()->absRefPrefix === '') {
+                    $urlParts['path'] = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH') . ltrim($urlParts['path'], '/');
+                }
+                $isUrlModified = true;
+            }
+            // Override scheme:
+            $forceAbsoluteUrl = &$configuration['forceAbsoluteUrl.']['scheme'];
+            if (!empty($forceAbsoluteUrl) && $urlParts['scheme'] !== $forceAbsoluteUrl) {
+                $urlParts['scheme'] = $forceAbsoluteUrl;
+                $isUrlModified = true;
+            }
+            // Recreate the absolute URL:
+            if ($isUrlModified) {
+                $url = implode('', $urlParts);
+            }
+        }
+        return $url;
+    }
+
+    /**
+     * Helper method to a fallback method parsing HTML out of it
+     *
+     * @param string $originalLinkText the original string, if empty, the fallback link text
+     * @param string $fallbackLinkText the string to be used.
+     * @return string the final text
+     */
+    protected function parseFallbackLinkTextIfLinkTextIsEmpty(string $originalLinkText, string $fallbackLinkText): string
+    {
+        if ($originalLinkText === '') {
+            return $this->contentObjectRenderer->parseFunc($fallbackLinkText, ['makelinks' => 0], '< lib.parseFunc');
+        } else {
+            return $originalLinkText;
+        }
+    }
+
+    /**
+     * Creates the value for target="..." in a typolink configuration
+     *
+     * @param array $conf the typolink configuration
+     * @param string $name the key, usually "target", "extTarget" or "fileTarget"
+     * @param bool $respectFrameSetOption if set, then the fallback is only used as target if the doctype allows it
+     * @param string $fallbackTarget the string to be used when no target is found in the configuration
+     * @return string the value of the target attribute, if there is one
+     */
+    protected function resolveTargetAttribute(array $conf, string $name, bool $respectFrameSetOption = false, string $fallbackTarget = ''): string
+    {
+        $tsfe = $this->getTypoScriptFrontendController();
+        $targetAttributeAllowed = (!$respectFrameSetOption || !$tsfe->config['config']['doctype'] ||
+            in_array((string)$tsfe->config['config']['doctype'], ['xhtml_trans', 'xhtml_frames', 'xhtml_basic', 'html5'], true));
+
+        $target = '';
+        if (isset($conf[$name])) {
+            $target = $conf[$name];
+        } elseif ($targetAttributeAllowed) {
+            $target = $fallbackTarget;
+        }
+        if ($conf[$name . '.']) {
+            $target = (string)$this->contentObjectRenderer->stdWrap($target, $conf[$name . '.']);
+        }
+        return $target;
+    }
+
+    /**
+     * Loops over all configured URL modifier hooks (if available) and returns the generated URL or NULL if no URL was generated.
+     *
+     * @param string $context The context in which the method is called (e.g. typoLink).
+     * @param string $url The URL that should be processed.
+     * @param array $typolinkConfiguration The current link configuration array.
+     * @return string|NULL Returns NULL if URL was not processed or the processed URL as a string.
+     * @throws \RuntimeException if a hook was registered but did not fulfill the correct parameters.
+     */
+    protected function processUrl(string $context, string $url, array $typolinkConfiguration = [])
+    {
+        if (
+            empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'])
+            || !is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'])
+        ) {
+            return $url;
+        }
+
+        $urlProcessors = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'];
+        foreach ($urlProcessors as $identifier => $configuration) {
+            if (empty($configuration) || !is_array($configuration)) {
+                throw new \RuntimeException('Missing configuration for URI processor "' . $identifier . '".', 1491130459);
+            }
+            if (!is_string($configuration['processor']) || empty($configuration['processor']) || !class_exists($configuration['processor']) || !is_subclass_of($configuration['processor'], UrlProcessorInterface::class)) {
+                throw new \RuntimeException('The URI processor "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . UrlProcessorInterface::class . '".', 1491130460);
+            }
+        }
+
+        $orderedProcessors = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($urlProcessors);
+        $keepProcessing = true;
+
+        foreach ($orderedProcessors as $configuration) {
+            /** @var UrlProcessorInterface $urlProcessor */
+            $urlProcessor = GeneralUtility::makeInstance($configuration['processor']);
+            $url = $urlProcessor->process($context, $url, $typolinkConfiguration, $this->contentObjectRenderer, $keepProcessing);
+            if (!$keepProcessing) {
+                break;
+            }
+        }
+
+        return $url;
+    }
+
+    /**
+     * @return TypoScriptFrontendController
+     */
+    public function getTypoScriptFrontendController(): TypoScriptFrontendController
+    {
+        return $GLOBALS['TSFE'];
+    }
+}
diff --git a/typo3/sysext/frontend/Classes/Typolink/DatabaseRecordLinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/DatabaseRecordLinkBuilder.php
new file mode 100644 (file)
index 0000000..ca47842
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Frontend\Typolink;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+
+/**
+ * Builds a TypoLink to a database record
+ */
+class DatabaseRecordLinkBuilder extends AbstractTypolinkBuilder
+{
+    /**
+     * @inheritdoc
+     */
+    public function build(array &$linkDetails, string $linkText, string $target, array $conf): array
+    {
+        $tsfe = $this->getTypoScriptFrontendController();
+        $configurationKey = $linkDetails['identifier'] . '.';
+        $configuration = $tsfe->tmpl->setup['config.']['recordLinks.'];
+        $linkHandlerConfiguration = $tsfe->pagesTSconfig['TCEMAIN.']['linkHandler.'];
+
+        if (!isset($configuration[$configurationKey]) || !isset($linkHandlerConfiguration[$configurationKey])) {
+            throw new UnableToLinkException(
+                'Configuration how to link "' . $linkDetails['typoLinkParameter'] . '" was not found, so "' . $linkText . '" was not linked.',
+                1490989149,
+                null,
+                $linkText
+            );
+        }
+        $typoScriptConfiguration = $configuration[$configurationKey]['typolink.'];
+        $linkHandlerConfiguration = $linkHandlerConfiguration[$configurationKey]['configuration.'];
+
+        if ($configuration[$configurationKey]['forceLink']) {
+            $record = $tsfe->sys_page->getRawRecord($linkHandlerConfiguration['table'], $linkDetails['uid']);
+        } else {
+            $record = $tsfe->sys_page->checkRecord($linkHandlerConfiguration['table'], $linkDetails['uid']);
+        }
+        if ($record === 0) {
+            throw new UnableToLinkException(
+                'Record not found for "' . $linkDetails['typoLinkParameter'] . '" was not found, so "' . $linkText . '" was not linked.',
+                1490989659,
+                null,
+                $linkText
+            );
+        }
+
+        // Build the full link to the record
+        $localContentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
+        $localContentObjectRenderer->start($record, $linkHandlerConfiguration['table']);
+        $localContentObjectRenderer->parameters = $this->contentObjectRenderer->parameters;
+        $link = $localContentObjectRenderer->typoLink($linkText, $typoScriptConfiguration);
+
+        $this->contentObjectRenderer->lastTypoLinkLD = $localContentObjectRenderer->lastTypoLinkLD;
+        $this->contentObjectRenderer->lastTypoLinkUrl = $localContentObjectRenderer->lastTypoLinkUrl;
+        $this->contentObjectRenderer->lastTypoLinkTarget = $localContentObjectRenderer->lastTypoLinkTarget;
+
+        // nasty workaround so typolink stops putting a link together, there is a link already built
+        throw new UnableToLinkException(
+            '', 1491130170, null, $link
+        );
+    }
+}
diff --git a/typo3/sysext/frontend/Classes/Typolink/EmailLinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/EmailLinkBuilder.php
new file mode 100644 (file)
index 0000000..370f0c1
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Frontend\Typolink;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Builds a TypoLink to an email address
+ */
+class EmailLinkBuilder extends AbstractTypolinkBuilder
+{
+    /**
+     * @inheritdoc
+     */
+    public function build(array &$linkDetails, string $linkText, string $target, array $conf): array
+    {
+        list($url, $linkText) = $this->contentObjectRenderer->getMailTo($linkDetails['email'], $linkText);
+        return [$url, $linkText, $target];
+    }
+}
diff --git a/typo3/sysext/frontend/Classes/Typolink/ExternalUrlLinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/ExternalUrlLinkBuilder.php
new file mode 100644 (file)
index 0000000..46a45d8
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Frontend\Typolink;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+use TYPO3\CMS\Frontend\Http\UrlProcessorInterface;
+
+/**
+ * Builds a TypoLink to an external URL
+ */
+class ExternalUrlLinkBuilder extends AbstractTypolinkBuilder
+{
+    /**
+     * @inheritdoc
+     */
+    public function build(array &$linkDetails, string $linkText, string $target, array $conf): array
+    {
+        return [
+            $this->processUrl(UrlProcessorInterface::CONTEXT_EXTERNAL, $linkDetails['url'], $conf),
+            $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $linkDetails['url']),
+            $target ?: $this->resolveTargetAttribute($conf, 'extTarget', true, $this->getTypoScriptFrontendController()->extTarget)
+        ];
+    }
+}
diff --git a/typo3/sysext/frontend/Classes/Typolink/FileOrFolderLinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/FileOrFolderLinkBuilder.php
new file mode 100644 (file)
index 0000000..c8ee336
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Frontend\Typolink;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+use TYPO3\CMS\Core\Resource\FileInterface;
+use TYPO3\CMS\Core\Resource\Folder;
+use TYPO3\CMS\Frontend\Http\UrlProcessorInterface;
+
+/**
+ * Builds a TypoLink to a folder or file
+ */
+class FileOrFolderLinkBuilder extends AbstractTypolinkBuilder
+{
+    /**
+     * @inheritdoc
+     */
+    public function build(array &$linkDetails, string $linkText, string $target, array $conf): array
+    {
+        $fileOrFolderObject = $linkDetails['file'] ? $linkDetails['file'] : $linkDetails['folder'];
+        // check if the file exists or if a / is contained (same check as in detectLinkType)
+        if (!($fileOrFolderObject instanceof FileInterface) && !($fileOrFolderObject instanceof Folder)) {
+            throw new UnableToLinkException(
+                'File "' . $linkDetails['typoLinkParameter'] . '" did not exist, so "' . $linkText . '" was not linked.',
+                1490989449,
+                null,
+                $linkText
+            );
+        }
+
+        $tsfe = $this->getTypoScriptFrontendController();
+        $linkLocation = $fileOrFolderObject->getPublicUrl();
+        // Setting title if blank value to link
+        $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, rawurldecode($linkLocation));
+        $linkLocation = (strpos($linkLocation, '/') !== 0 ? $tsfe->absRefPrefix : '') . $linkLocation;
+        $url = $this->processUrl(UrlProcessorInterface::CONTEXT_FILE, $linkLocation, $conf);
+        return [
+            $this->forceAbsoluteUrl($url, $conf),
+            $linkText,
+            $target ?: $this->resolveTargetAttribute($conf, 'fileTarget', false, $tsfe->fileTarget)
+        ];
+    }
+}
diff --git a/typo3/sysext/frontend/Classes/Typolink/LegacyLinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/LegacyLinkBuilder.php
new file mode 100644 (file)
index 0000000..93ee6c4
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Frontend\Typolink;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+use TYPO3\CMS\Core\LinkHandling\LinkService;
+use TYPO3\CMS\Frontend\Http\UrlProcessorInterface;
+
+/**
+ * Builds a TypoLink to a file (relative to fileadmin/ or something)
+ * or otherwise detects as an external URL
+ */
+class LegacyLinkBuilder extends AbstractTypolinkBuilder
+{
+    /**
+     * @inheritdoc
+     */
+    public function build(array &$linkDetails, string $linkText, string $target, array $conf): array
+    {
+        $tsfe = $this->getTypoScriptFrontendController();
+        if ($linkDetails['file']) {
+            $linkDetails['type'] = LinkService::TYPE_FILE;
+            $linkLocation = $linkDetails['file'];
+            // Setting title if blank value to link
+            $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, rawurldecode($linkLocation));
+            $linkLocation = (strpos($linkLocation, '/') !== 0 ? $tsfe->absRefPrefix : '') . $linkLocation;
+            $url = $this->processUrl(UrlProcessorInterface::CONTEXT_FILE, $linkLocation, $conf);
+            $url = $this->forceAbsoluteUrl($url, $conf);
+            $target = $target ?: $this->resolveTargetAttribute($conf, 'fileTarget', false, $tsfe->fileTarget);
+        } elseif ($linkDetails['url']) {
+            $linkDetails['type'] = LinkService::TYPE_URL;
+            $target = $target ?: $this->resolveTargetAttribute($conf, 'extTarget', true, $tsfe->extTarget);
+            $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $linkDetails['url']);
+            $url = $this->processUrl(UrlProcessorInterface::CONTEXT_EXTERNAL, $linkDetails['url'], $conf);
+        } else {
+            throw new UnableToLinkException('Unknown link detected, so ' . $linkText . ' was not linked.', 1490990031, null, $linkText);
+        }
+        return [$url, $linkText, $target];
+    }
+}
diff --git a/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php b/typo3/sysext/frontend/Classes/Typolink/PageLinkBuilder.php
new file mode 100644 (file)
index 0000000..9dc30fc
--- /dev/null
@@ -0,0 +1,316 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Frontend\Typolink;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\CMS\Frontend\ContentObject\TypolinkModifyLinkConfigForPageLinksHookInterface;
+use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
+use TYPO3\CMS\Frontend\Page\PageRepository;
+
+/**
+ * Builds a TypoLink to a certain page
+ */
+class PageLinkBuilder extends AbstractTypolinkBuilder
+{
+    /**
+     * @inheritdoc
+     */
+    public function build(array &$linkDetails, string $linkText, string $target, array $conf): array
+    {
+        $tsfe = $this->getTypoScriptFrontendController();
+        // Checking if the id-parameter is an alias.
+        if (!empty($linkDetails['pagealias'])) {
+            $linkDetails['pageuid'] = $tsfe->sys_page->getPageIdFromAlias($linkDetails['pagealias']);
+        } elseif (empty($linkDetails['pageuid']) || $linkDetails['pageuid'] === 'current') {
+            // If no id or alias is given
+            $linkDetails['pageuid'] = $tsfe->id;
+        }
+
+        // Link to page even if access is missing?
+        if (isset($conf['linkAccessRestrictedPages'])) {
+            $disableGroupAccessCheck = (bool)$conf['linkAccessRestrictedPages'];
+        } else {
+            $disableGroupAccessCheck = (bool)$tsfe->config['config']['typolinkLinkAccessRestrictedPages'];
+        }
+
+        // Looking up the page record to verify its existence:
+        $page = $tsfe->sys_page->getPage($linkDetails['pageuid'], $disableGroupAccessCheck);
+
+        if (empty($page)) {
+            throw new UnableToLinkException('Page id "' . $linkDetails['typoLinkParameter'] . '" was not found, so "' . $linkText . '" was not linked.', 1490987336, null, $linkText);
+        }
+
+        if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typolinkProcessing']['typolinkModifyParameterForPageLinks'])) {
+            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 */
+                $conf = $hookObject->modifyPageLinkConfiguration($conf, $linkDetails, $page);
+            }
+        }
+        $enableLinksAcrossDomains = $tsfe->config['config']['typolinkEnableLinksAcrossDomains'];
+        if ($conf['no_cache.']) {
+            $conf['no_cache'] = (string)$this->contentObjectRenderer->stdWrap($conf['no_cache'], $conf['no_cache.']);
+        }
+
+        $sectionMark = trim(isset($conf['section.']) ? (string)$this->contentObjectRenderer->stdWrap($conf['section'], $conf['section.']) : (string)$conf['section']);
+        if ($sectionMark === '' && isset($linkDetails['fragment'])) {
+            $sectionMark = $linkDetails['fragment'];
+        }
+        if ($sectionMark !== '') {
+            $sectionMark = '#' . (MathUtility::canBeInterpretedAsInteger($sectionMark) ? 'c' : '') . $sectionMark;
+        }
+        // Overruling 'type'
+        $pageType = $linkDetails['pagetype'] ?? 0;
+
+        if (isset($linkDetails['parameters'])) {
+            $conf['additionalParams'] .= '&' . ltrim($linkDetails['parameters'], '&');
+        }
+        // MointPoints, look for closest MPvar:
+        $MPvarAcc = [];
+        if (!$tsfe->config['config']['MP_disableTypolinkClosestMPvalue']) {
+            $temp_MP = $this->getClosestMountPointValueForPage($page['uid']);
+            if ($temp_MP) {
+                $MPvarAcc['closest'] = $temp_MP;
+            }
+        }
+        // Look for overlay Mount Point:
+        $mount_info = $tsfe->sys_page->getMountPointInfo($page['uid'], $page);
+        if (is_array($mount_info) && $mount_info['overlay']) {
+            $page = $tsfe->sys_page->getPage($mount_info['mount_pid'], $disableGroupAccessCheck);
+            if (empty($page)) {
+                throw new UnableToLinkException('Mount point "' . $mount_info['mount_pid'] . '" was not available, so "' . $linkText . '" was not linked.', 1490987337, null, $linkText);
+            }
+            $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:
+            $addQueryParams .= '&MP=' . rawurlencode(implode(',', $MPvarAcc));
+        } elseif (strpos($addQueryParams, '&MP=') === false && $tsfe->config['config']['typolinkCheckRootline']) {
+            // We do not come here if additionalParams had '&MP='. This happens when typoLink is called from
+            // menu. Mount points always work in the content of the current domain and we must not change
+            // domain if MP variables exist.
+            // If we link across domains and page is free type shortcut, we must resolve the shortcut first!
+            // If we do not do it, TYPO3 will fail to (1) link proper page in RealURL/CoolURI because
+            // they return relative links and (2) show proper page if no RealURL/CoolURI exists when link is clicked
+            if ($enableLinksAcrossDomains
+                && (int)$page['doktype'] === PageRepository::DOKTYPE_SHORTCUT
+                && (int)$page['shortcut_mode'] === PageRepository::SHORTCUT_MODE_NONE
+            ) {
+                // Save in case of broken destination or endless loop
+                $page2 = $page;
+                // Same as in RealURL, seems enough
+                $maxLoopCount = 20;
+                while ($maxLoopCount
+                    && is_array($page)
+                    && (int)$page['doktype'] === PageRepository::DOKTYPE_SHORTCUT
+                    && (int)$page['shortcut_mode'] === PageRepository::SHORTCUT_MODE_NONE
+                ) {
+                    $page = $tsfe->sys_page->getPage($page['shortcut'], $disableGroupAccessCheck);
+                    $maxLoopCount--;
+                }
+                if (empty($page) || $maxLoopCount === 0) {
+                    // We revert if shortcut is broken or maximum number of loops is exceeded (indicates endless loop)
+                    $page = $page2;
+                }
+            }
+
+            $targetDomain = $tsfe->getDomainNameForPid($page['uid']);
+            // 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);
+                $addQueryParams .= $cHash ? '&cHash=' . $cHash : '';
+            }
+            unset($params);
+        }
+        $absoluteUrlScheme = 'http';
+        // URL shall be absolute:
+        if (isset($conf['forceAbsoluteUrl']) && $conf['forceAbsoluteUrl']) {
+            // Override scheme:
+            if (isset($conf['forceAbsoluteUrl.']['scheme']) && $conf['forceAbsoluteUrl.']['scheme']) {
+                $absoluteUrlScheme = $conf['forceAbsoluteUrl.']['scheme'];
+            } elseif (GeneralUtility::getIndpEnv('TYPO3_SSL')) {
+                $absoluteUrlScheme = 'https';
+            }
+            // 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 go for an absolute link, add site path if it's not taken care about by absRefPrefix
+            if (!$tsfe->config['config']['absRefPrefix'] && $targetDomain === $currentDomain) {
+                $targetDomain = $currentDomain . rtrim(GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'), '/');
+            }
+        }
+        // If target page has a different domain and the current domain's linking scheme (e.g. RealURL/...) should not be used
+        if ($targetDomain !== '' && $targetDomain !== $currentDomain && !$enableLinksAcrossDomains) {
+            $target = $target ?: $this->resolveTargetAttribute($conf, 'extTarget', false, $tsfe->extTarget);
+            $LD['target'] = $target;
+            // Convert IDNA-like domain (if any)
+            if (!preg_match('/^[a-z0-9.\\-]*$/i', $targetDomain)) {
+                $targetDomain =  GeneralUtility::idnaEncode($targetDomain);
+            }
+            $url = $absoluteUrlScheme . '://' . $targetDomain . '/index.php?id=' . $page['uid'] . $addQueryParams . $sectionMark;
+        } else {
+            // Internal link or current domain's linking scheme should be used
+            // Internal target:
+            if (empty($target)) {
+                $target = $this->resolveTargetAttribute($conf, 'target', true, $tsfe->intTarget);
+            }
+            $LD = $tsfe->tmpl->linkData($page, $target, $conf['no_cache'], '', '', $addQueryParams, $pageType, $targetDomain);
+            if ($targetDomain !== '') {
+                // We will add domain only if URL does not have it already.
+                if ($enableLinksAcrossDomains && $targetDomain !== $currentDomain) {
+                    // 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']) {
+                        $LD['totalURL'] = substr($LD['totalURL'], $prefixLength);
+                    }
+                }
+                $urlParts = parse_url($LD['totalURL']);
+                if (empty($urlParts['host'])) {
+                    $LD['totalURL'] = $absoluteUrlScheme . '://' . $targetDomain . ($LD['totalURL'][0] === '/' ? '' : '/') . $LD['totalURL'];
+                }
+            }
+            $url = $LD['totalURL'] . $sectionMark;
+        }
+        $target = $LD['target'];
+        // 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)
+            && (empty($conf['addQueryString']) || !isset($conf['addQueryString.']))
+        ) {
+            $currentQueryArray = GeneralUtility::explodeUrl2Array(GeneralUtility::getIndpEnv('QUERY_STRING'), true);
+            $currentQueryParams = GeneralUtility::implodeArrayForUrl('', $currentQueryArray, '', false, true);
+
+            if (!trim($currentQueryParams)) {
+                list(, $URLparams) = explode('?', $url);
+                list($URLparams) = explode('#', $URLparams);
+                parse_str($URLparams . $LD['orig_type'], $URLparamsArray);
+                // Type nums must match as well as page ids
+                if ((int)$URLparamsArray['type'] === (int)$tsfe->type) {
+                    unset($URLparamsArray['id']);
+                    unset($URLparamsArray['type']);
+                    // If there are no parameters left.... set the new url.
+                    if (empty($URLparamsArray)) {
+                        $url = $sectionMark;
+                    }
+                }
+            }
+        }
+
+        // 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];
+    }
+
+    /**
+     * Returns the &MP variable value for a page id.
+     * The function will do its best to find a MP value that will keep the page id inside the current Mount Point rootline if any.
+     *
+     * @param int $pageId page id
+     * @return string MP value, prefixed with &MP= (depending on $raw)
+     */
+    protected function getClosestMountPointValueForPage($pageId)
+    {
+        $tsfe = $this->getTypoScriptFrontendController();
+        if (empty($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) || !$tsfe->MP) {
+            return '';
+        }
+        // Same page as current.
+        if ((int)$tsfe->id === (int)$pageId) {
+            return $tsfe->MP;
+        }
+
+        // Find closest meeting point
+        // Gets rootline of linked-to page
+        $tCR_rootline = $tsfe->sys_page->getRootLine($pageId, '', true);
+        $inverseTmplRootline = array_reverse($tsfe->tmpl->rootLine);
+        $rl_mpArray = [];
+        $startMPaccu = false;
+        // Traverse root line of link uid and inside of that the REAL root line of current position.
+        foreach ($tCR_rootline as $tCR_data) {
+            foreach ($inverseTmplRootline as $rlKey => $invTmplRLRec) {
+                // Force accumulating when in overlay mode: Links to this page have to stay within the current branch
+                if ($invTmplRLRec['_MOUNT_OL'] && (int)$tCR_data['uid'] === (int)$invTmplRLRec['uid']) {
+                    $startMPaccu = true;
+                }
+                // Accumulate MP data:
+                if ($startMPaccu && $invTmplRLRec['_MP_PARAM']) {
+                    $rl_mpArray[] = $invTmplRLRec['_MP_PARAM'];
+                }
+                // If two PIDs matches and this is NOT the site root, start accumulation of MP data (on the next level):
+                // (The check for site root is done so links to branches outsite the site but sharing the site roots PID
+                // is NOT detected as within the branch!)
+                if ((int)$tCR_data['pid'] === (int)$invTmplRLRec['pid'] && count($inverseTmplRootline) !== $rlKey + 1) {
+                    $startMPaccu = true;
+                }
+            }
+            if ($startMPaccu) {
+                // Good enough...
+                break;
+            }
+        }
+        return !empty($rl_mpArray) ? implode(',', array_reverse($rl_mpArray)) : '';
+    }
+}
diff --git a/typo3/sysext/frontend/Classes/Typolink/UnableToLinkException.php b/typo3/sysext/frontend/Classes/Typolink/UnableToLinkException.php
new file mode 100644 (file)
index 0000000..fa76781
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Frontend\Typolink;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Frontend\Exception;
+
+/**
+ * Exception which is thrown when a link could not be set
+ */
+class UnableToLinkException extends Exception
+{
+    /**
+     * @var string the text which should have gone inside the
+     */
+    protected $linkText;
+
+    /**
+     * Constructor the exception. With an additional parameter for the link text
+     *
+     * @param string $message [optional] The Exception message to throw.
+     * @param int $code [optional] The Exception code.
+     * @param \Throwable $previous [optional] The previous throwable used for the exception chaining.
+     * @param string $linkText
+     */
+    public function __construct($message = '', $code = 0, \Throwable $previous = null, $linkText = null)
+    {
+        parent::__construct($message, $code, $previous);
+        $this->linkText = $linkText;
+    }
+
+    /**
+     * Returns the link text when the link could not been set
+     *
+     * @return string
+     */
+    public function getLinkText(): string
+    {
+        return $this->linkText;
+    }
+}
index a488030..98f026a 100644 (file)
@@ -2054,113 +2054,6 @@ class ContentObjectRendererTest extends \TYPO3\TestingFramework\Core\Unit\UnitTe
     }
 
     /**
-     * @param string $expected The expected URL
-     * @param string $url The URL to parse and manipulate
-     * @param array $configuration The configuration array
-     * @test
-     * @dataProvider forceAbsoluteUrlReturnsCorrectAbsoluteUrlDataProvider
-     */
-    public function forceAbsoluteUrlReturnsCorrectAbsoluteUrl($expected, $url, array $configuration)
-    {
-        // Force hostname
-        $this->subject->expects($this->any())->method('getEnvironmentVariable')->will($this->returnValueMap(
-            [
-                ['HTTP_HOST', 'localhost'],
-                ['TYPO3_SITE_PATH', '/'],
-            ]
-        ));
-        $GLOBALS['TSFE']->absRefPrefix = '';
-
-        $this->assertEquals($expected, $this->subject->_call('forceAbsoluteUrl', $url, $configuration));
-    }
-
-    /**
-     * @return array The test data for forceAbsoluteUrlReturnsAbsoluteUrl
-     */
-    public function forceAbsoluteUrlReturnsCorrectAbsoluteUrlDataProvider()
-    {
-        return [
-            'Missing forceAbsoluteUrl leaves URL untouched' => [
-                'foo',
-                'foo',
-                []
-            ],
-            'Absolute URL stays unchanged' => [
-                'http://example.org/',
-                'http://example.org/',
-                [
-                    'forceAbsoluteUrl' => '1'
-                ]
-            ],
-            'Absolute URL stays unchanged 2' => [
-                'http://example.org/resource.html',
-                'http://example.org/resource.html',
-                [
-                    'forceAbsoluteUrl' => '1'
-                ]
-            ],
-            'Scheme and host w/o ending slash stays unchanged' => [
-                'http://example.org',
-                'http://example.org',
-                [
-                    'forceAbsoluteUrl' => '1'
-                ]
-            ],
-            'Scheme can be forced' => [
-                'typo3://example.org',
-                'http://example.org',
-                [
-                    'forceAbsoluteUrl' => '1',
-                    'forceAbsoluteUrl.' => [
-                        'scheme' => 'typo3'
-                    ]
-                ]
-            ],
-            'Relative path old-style' => [
-                'http://localhost/fileadmin/dummy.txt',
-                '/fileadmin/dummy.txt',
-                [
-                    'forceAbsoluteUrl' => '1',
-                ]
-            ],
-            'Relative path' => [
-                'http://localhost/fileadmin/dummy.txt',
-                'fileadmin/dummy.txt',
-                [
-                    'forceAbsoluteUrl' => '1',
-                ]
-            ],
-            'Scheme can be forced with pseudo-relative path' => [
-                'typo3://localhost/fileadmin/dummy.txt',
-                '/fileadmin/dummy.txt',
-                [
-                    'forceAbsoluteUrl' => '1',
-                    'forceAbsoluteUrl.' => [
-                        'scheme' => 'typo3'
-                    ]
-                ]
-            ],
-            'Hostname only is not treated as valid absolute URL' => [
-                'http://localhost/example.org',
-                'example.org',
-                [
-                    'forceAbsoluteUrl' => '1'
-                ]
-            ],
-            'Scheme and host is added to local file path' => [
-                'typo3://localhost/fileadmin/my.pdf',
-                'fileadmin/my.pdf',
-                [
-                    'forceAbsoluteUrl' => '1',
-                    'forceAbsoluteUrl.' => [
-                        'scheme' => 'typo3'
-                    ]
-                ]
-            ]
-        ];
-    }
-
-    /**
      * @test
      */
     public function renderingContentObjectThrowsException()
@@ -2297,28 +2190,6 @@ class ContentObjectRendererTest extends \TYPO3\TestingFramework\Core\Unit\UnitTe
     }
 
     /**
-     * @test
-     */
-    public function forceAbsoluteUrlReturnsCorrectAbsoluteUrlWithSubfolder()
-    {
-        // Force hostname and subfolder
-        $this->subject->expects($this->any())->method('getEnvironmentVariable')->will($this->returnValueMap(
-            [
-                ['HTTP_HOST', 'localhost'],
-                ['TYPO3_SITE_PATH', '/subfolder/'],
-            ]
-        ));
-
-        $expected = 'http://localhost/subfolder/fileadmin/my.pdf';
-        $url = 'fileadmin/my.pdf';
-        $configuration = [
-            'forceAbsoluteUrl' => '1'
-        ];
-
-        $this->assertEquals($expected, $this->subject->_call('forceAbsoluteUrl', $url, $configuration));
-    }
-
-    /**
      * @return array
      */
     protected function getLibParseTarget()
@@ -2917,6 +2788,7 @@ class ContentObjectRendererTest extends \TYPO3\TestingFramework\Core\Unit\UnitTe
         ];
         $typoScriptFrontendControllerMockObject->tmpl = $templateServiceObjectMock;
         $GLOBALS['TSFE'] = $typoScriptFrontendControllerMockObject;
+
         $this->subject->_set('typoScriptFrontendController', $typoScriptFrontendControllerMockObject);
 
         $this->assertEquals($expectedResult, $this->subject->typoLink($linkText, $configuration));
diff --git a/typo3/sysext/frontend/Tests/Unit/Typolink/AbstractTypolinkBuilderTest.php b/typo3/sysext/frontend/Tests/Unit/Typolink/AbstractTypolinkBuilderTest.php
new file mode 100644 (file)
index 0000000..f299086
--- /dev/null
@@ -0,0 +1,237 @@
+<?php
+namespace TYPO3\CMS\Frontend\Tests\Unit\Typolink;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Psr\Log\LoggerInterface;
+use TYPO3\CMS\Core\Log\LogManager;
+use TYPO3\CMS\Core\TypoScript\TemplateService;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+use TYPO3\CMS\Frontend\Page\PageRepository;
+use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class AbstractTypolinkBuilderTest extends UnitTestCase
+{
+    /**
+     * @var array A backup of registered singleton instances
+     */
+    protected $singletonInstances = [];
+
+    /**
+     * @var \PHPUnit_Framework_MockObject_MockObject|TypoScriptFrontendController|\TYPO3\TestingFramework\Core\AccessibleObjectInterface
+     */
+    protected $frontendControllerMock = null;
+
+    /**
+     * @var \PHPUnit_Framework_MockObject_MockObject|TemplateService
+     */
+    protected $templateServiceMock = null;
+
+    /**
+     * Set up
+     */
+    protected function setUp()
+    {
+        GeneralUtility::flushInternalRuntimeCaches();
+
+        $this->singletonInstances = GeneralUtility::getSingletonInstances();
+        $this->createMockedLoggerAndLogManager();
+
+        $this->templateServiceMock =
+            $this->getMockBuilder(TemplateService::class)
+            ->setMethods(['getFileName', 'linkData'])->getMock();
+        $pageRepositoryMock =
+            $this->getAccessibleMock(PageRepository::class, ['getRawRecord', 'getMountPointInfo']);
+        $this->frontendControllerMock =
+            $this->getAccessibleMock(TypoScriptFrontendController::class,
+            ['dummy'], [], '', false);
+        $this->frontendControllerMock->tmpl = $this->templateServiceMock;
+        $this->frontendControllerMock->config = [];
+        $this->frontendControllerMock->page =  [];
+        $this->frontendControllerMock->sys_page = $pageRepositoryMock;
+        $GLOBALS['TSFE'] = $this->frontendControllerMock;
+    }
+
+    protected function tearDown()
+    {
+        GeneralUtility::resetSingletonInstances($this->singletonInstances);
+        parent::tearDown();
+    }
+
+    //////////////////////
+    // Utility functions
+    //////////////////////
+
+    /**
+     * @return TypoScriptFrontendController
+     */
+    protected function getFrontendController()
+    {
+        return $GLOBALS['TSFE'];
+    }
+
+    /**
+     * Avoid logging to the file system (file writer is currently the only configured writer)
+     */
+    protected function createMockedLoggerAndLogManager()
+    {
+        $logManagerMock = $this->getMockBuilder(LogManager::class)->getMock();
+        $loggerMock = $this->getMockBuilder(LoggerInterface::class)->getMock();
+        $logManagerMock->expects($this->any())
+            ->method('getLogger')
+            ->willReturn($loggerMock);
+        GeneralUtility::setSingletonInstance(LogManager::class, $logManagerMock);
+    }
+
+    /**
+     * @return array The test data for forceAbsoluteUrlReturnsAbsoluteUrl
+     */
+    public function forceAbsoluteUrlReturnsCorrectAbsoluteUrlDataProvider()
+    {
+        return [
+            'Missing forceAbsoluteUrl leaves URL untouched' => [
+                'foo',
+                'foo',
+                []
+            ],
+            'Absolute URL stays unchanged' => [
+                'http://example.org/',
+                'http://example.org/',
+                [
+                    'forceAbsoluteUrl' => '1'
+                ]
+            ],
+            'Absolute URL stays unchanged 2' => [
+                'http://example.org/resource.html',
+                'http://example.org/resource.html',
+                [
+                    'forceAbsoluteUrl' => '1'
+                ]
+            ],
+            'Scheme and host w/o ending slash stays unchanged' => [
+                'http://example.org',
+                'http://example.org',
+                [
+                    'forceAbsoluteUrl' => '1'
+                ]
+            ],
+            'Scheme can be forced' => [
+                'typo3://example.org',
+                'http://example.org',
+                [
+                    'forceAbsoluteUrl' => '1',
+                    'forceAbsoluteUrl.' => [
+                        'scheme' => 'typo3'
+                    ]
+                ]
+            ],
+            'Relative path old-style' => [
+                'http://localhost/fileadmin/dummy.txt',
+                '/fileadmin/dummy.txt',
+                [
+                    'forceAbsoluteUrl' => '1',
+                ]
+            ],
+            'Relative path' => [
+                'http://localhost/fileadmin/dummy.txt',
+                'fileadmin/dummy.txt',
+                [
+                    'forceAbsoluteUrl' => '1',
+                ]
+            ],
+            'Scheme can be forced with pseudo-relative path' => [
+                'typo3://localhost/fileadmin/dummy.txt',
+                '/fileadmin/dummy.txt',
+                [
+                    'forceAbsoluteUrl' => '1',
+                    'forceAbsoluteUrl.' => [
+                        'scheme' => 'typo3'
+                    ]
+                ]
+            ],
+            'Hostname only is not treated as valid absolute URL' => [
+                'http://localhost/example.org',
+                'example.org',
+                [
+                    'forceAbsoluteUrl' => '1'
+                ]
+            ],
+            'Scheme and host is added to local file path' => [
+                'typo3://localhost/fileadmin/my.pdf',
+                'fileadmin/my.pdf',
+                [
+                    'forceAbsoluteUrl' => '1',
+                    'forceAbsoluteUrl.' => [
+                        'scheme' => 'typo3'
+                    ]
+                ]
+            ]
+        ];
+    }
+
+    /**
+     * @param string $expected The expected URL
+     * @param string $url The URL to parse and manipulate
+     * @param array $configuration The configuration array
+     * @test
+     * @dataProvider forceAbsoluteUrlReturnsCorrectAbsoluteUrlDataProvider
+     */
+    public function forceAbsoluteUrlReturnsCorrectAbsoluteUrl($expected, $url, array $configuration)
+    {
+        $contentObjectRendererProphecy = $this->prophesize(ContentObjectRenderer::class);
+        $subject = $this->getAccessibleMock(AbstractTypolinkBuilder::class,
+            ['build'],
+            [$contentObjectRendererProphecy->reveal()],
+            '',
+            false
+        );
+        // Force hostname
+        $_SERVER['HTTP_HOST'] = 'localhost';
+        $_SERVER['SCRIPT_NAME'] = '/typo3/index.php';
+        $GLOBALS['TSFE']->absRefPrefix = '';
+
+        $this->assertEquals($expected, $subject->_call('forceAbsoluteUrl', $url, $configuration));
+    }
+
+    /**
+     * @test
+     */
+    public function forceAbsoluteUrlReturnsCorrectAbsoluteUrlWithSubfolder()
+    {
+        $contentObjectRendererProphecy = $this->prophesize(ContentObjectRenderer::class);
+        $subject = $this->getAccessibleMock(AbstractTypolinkBuilder::class,
+            ['build'],
+            [$contentObjectRendererProphecy->reveal()],
+            '',
+            false
+        );
+        // Force hostname
+        $_SERVER['HTTP_HOST'] = 'localhost';
+        $_SERVER['SCRIPT_NAME'] = '/subfolder/typo3/index.php';
+
+        $expected = 'http://localhost/subfolder/fileadmin/my.pdf';
+        $url = 'fileadmin/my.pdf';
+        $configuration = [
+            'forceAbsoluteUrl' => '1'
+        ];
+
+        $this->assertEquals($expected, $subject->_call('forceAbsoluteUrl', $url, $configuration));
+    }
+}