[TASK] Create URLs with typolink for pages with sites
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Typolink / PageLinkBuilder.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Frontend\Typolink;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Psr\Http\Message\ServerRequestInterface;
19 use Psr\Http\Message\UriInterface;
20 use TYPO3\CMS\Core\Cache\CacheManager;
21 use TYPO3\CMS\Core\Database\ConnectionPool;
22 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
23 use TYPO3\CMS\Core\Exception\Page\RootLineException;
24 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
25 use TYPO3\CMS\Core\Routing\PageUriBuilder;
26 use TYPO3\CMS\Core\Routing\SiteMatcher;
27 use TYPO3\CMS\Core\Site\Entity\Site;
28 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
29 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31 use TYPO3\CMS\Core\Utility\MathUtility;
32 use TYPO3\CMS\Core\Utility\RootlineUtility;
33 use TYPO3\CMS\Frontend\Compatibility\LegacyDomainResolver;
34 use TYPO3\CMS\Frontend\ContentObject\TypolinkModifyLinkConfigForPageLinksHookInterface;
35 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
36 use TYPO3\CMS\Frontend\Page\PageRepository;
37
38 /**
39 * Builds a TypoLink to a certain page
40 */
41 class PageLinkBuilder extends AbstractTypolinkBuilder
42 {
43 /**
44 * @inheritdoc
45 */
46 public function build(array &$linkDetails, string $linkText, string $target, array $conf): array
47 {
48 $tsfe = $this->getTypoScriptFrontendController();
49 // Checking if the id-parameter is an alias.
50 if (!empty($linkDetails['pagealias'])) {
51 $linkDetails['pageuid'] = $tsfe->sys_page->getPageIdFromAlias($linkDetails['pagealias']);
52 } elseif (empty($linkDetails['pageuid']) || $linkDetails['pageuid'] === 'current') {
53 // If no id or alias is given
54 $linkDetails['pageuid'] = $tsfe->id;
55 }
56
57 // Link to page even if access is missing?
58 if (isset($conf['linkAccessRestrictedPages'])) {
59 $disableGroupAccessCheck = (bool)$conf['linkAccessRestrictedPages'];
60 } else {
61 $disableGroupAccessCheck = (bool)$tsfe->config['config']['typolinkLinkAccessRestrictedPages'];
62 }
63
64 // Looking up the page record to verify its existence:
65 $page = $tsfe->sys_page->getPage($linkDetails['pageuid'], $disableGroupAccessCheck);
66
67 if (empty($page)) {
68 throw new UnableToLinkException('Page id "' . $linkDetails['typoLinkParameter'] . '" was not found, so "' . $linkText . '" was not linked.', 1490987336, null, $linkText);
69 }
70 $language = (int)$page[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
71 if ($language === 0 && GeneralUtility::hideIfDefaultLanguage($page['l18n_cfg'])) {
72 throw new UnableToLinkException('Default language of page "' . $linkDetails['typoLinkParameter'] . '" is hidden, so "' . $linkText . '" was not linked.', 1529527301, null, $linkText);
73 }
74 if ($language > 0 && !isset($page['_PAGES_OVERLAY']) && GeneralUtility::hideIfNotTranslated($page['l18n_cfg'])) {
75 throw new UnableToLinkException('Fallback to default language of page "' . $linkDetails['typoLinkParameter'] . '" is disabled, so "' . $linkText . '" was not linked.', 1529527488, null, $linkText);
76 }
77
78 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typolinkProcessing']['typolinkModifyParameterForPageLinks'] ?? [] as $classData) {
79 $hookObject = GeneralUtility::makeInstance($classData);
80 if (!$hookObject instanceof TypolinkModifyLinkConfigForPageLinksHookInterface) {
81 throw new \UnexpectedValueException('$hookObject must implement interface ' . TypolinkModifyLinkConfigForPageLinksHookInterface::class, 1483114905);
82 }
83 /** @var $hookObject TypolinkModifyLinkConfigForPageLinksHookInterface */
84 $conf = $hookObject->modifyPageLinkConfiguration($conf, $linkDetails, $page);
85 }
86 $enableLinksAcrossDomains = $tsfe->config['config']['typolinkEnableLinksAcrossDomains'];
87 if ($conf['no_cache.']) {
88 $conf['no_cache'] = (string)$this->contentObjectRenderer->stdWrap($conf['no_cache'], $conf['no_cache.']);
89 }
90
91 $sectionMark = trim(isset($conf['section.']) ? (string)$this->contentObjectRenderer->stdWrap($conf['section'], $conf['section.']) : (string)$conf['section']);
92 if ($sectionMark === '' && isset($linkDetails['fragment'])) {
93 $sectionMark = $linkDetails['fragment'];
94 }
95 if ($sectionMark !== '') {
96 $sectionMark = '#' . (MathUtility::canBeInterpretedAsInteger($sectionMark) ? 'c' : '') . $sectionMark;
97 }
98 // Overruling 'type'
99 $pageType = $linkDetails['pagetype'] ?? '';
100
101 if (isset($linkDetails['parameters'])) {
102 $conf['additionalParams'] .= '&' . ltrim($linkDetails['parameters'], '&');
103 }
104 // MointPoints, look for closest MPvar:
105 $MPvarAcc = [];
106 if (!$tsfe->config['config']['MP_disableTypolinkClosestMPvalue']) {
107 $temp_MP = $this->getClosestMountPointValueForPage($page['uid']);
108 if ($temp_MP) {
109 $MPvarAcc['closest'] = $temp_MP;
110 }
111 }
112 // Look for overlay Mount Point:
113 $mount_info = $tsfe->sys_page->getMountPointInfo($page['uid'], $page);
114 if (is_array($mount_info) && $mount_info['overlay']) {
115 $page = $tsfe->sys_page->getPage($mount_info['mount_pid'], $disableGroupAccessCheck);
116 if (empty($page)) {
117 throw new UnableToLinkException('Mount point "' . $mount_info['mount_pid'] . '" was not available, so "' . $linkText . '" was not linked.', 1490987337, null, $linkText);
118 }
119 $MPvarAcc['re-map'] = $mount_info['MPvar'];
120 }
121 // Query Params:
122 $addQueryParams = $conf['addQueryString'] ? $this->contentObjectRenderer->getQueryArguments($conf['addQueryString.']) : '';
123 $addQueryParams .= isset($conf['additionalParams.']) ? trim((string)$this->contentObjectRenderer->stdWrap($conf['additionalParams'], $conf['additionalParams.'])) : trim((string)$conf['additionalParams']);
124 if ($addQueryParams === '&' || $addQueryParams[0] !== '&') {
125 $addQueryParams = '';
126 }
127 // Mount pages are always local and never link to another domain
128 if (!empty($MPvarAcc)) {
129 // Add "&MP" var:
130 $addQueryParams .= '&MP=' . rawurlencode(implode(',', $MPvarAcc));
131 } elseif (strpos($addQueryParams, '&MP=') === false) {
132 // We do not come here if additionalParams had '&MP='. This happens when typoLink is called from
133 // menu. Mount points always work in the content of the current domain and we must not change
134 // domain if MP variables exist.
135 // If we link across domains and page is free type shortcut, we must resolve the shortcut first!
136 // If we do not do it, TYPO3 will fail to (1) link proper page in RealURL/CoolURI because
137 // they return relative links and (2) show proper page if no RealURL/CoolURI exists when link is clicked
138 if ($enableLinksAcrossDomains
139 && (int)$page['doktype'] === PageRepository::DOKTYPE_SHORTCUT
140 && (int)$page['shortcut_mode'] === PageRepository::SHORTCUT_MODE_NONE
141 ) {
142 // Save in case of broken destination or endless loop
143 $page2 = $page;
144 // Same as in RealURL, seems enough
145 $maxLoopCount = 20;
146 while ($maxLoopCount
147 && is_array($page)
148 && (int)$page['doktype'] === PageRepository::DOKTYPE_SHORTCUT
149 && (int)$page['shortcut_mode'] === PageRepository::SHORTCUT_MODE_NONE
150 ) {
151 $page = $tsfe->sys_page->getPage($page['shortcut'], $disableGroupAccessCheck);
152 $maxLoopCount--;
153 }
154 if (empty($page) || $maxLoopCount === 0) {
155 // We revert if shortcut is broken or maximum number of loops is exceeded (indicates endless loop)
156 $page = $page2;
157 }
158 }
159 }
160 if ($conf['useCacheHash']) {
161 $params = $tsfe->linkVars . $addQueryParams . '&id=' . $page['uid'];
162 if (trim($params, '& ') !== '') {
163 $cHash = GeneralUtility::makeInstance(CacheHashCalculator::class)->generateForParameters($params);
164 $addQueryParams .= $cHash ? '&cHash=' . $cHash : '';
165 }
166 unset($params);
167 }
168
169 // Check if the target page has a site configuration
170 try {
171 $siteOfTargetPage = GeneralUtility::makeInstance(SiteMatcher::class)->matchByPageId((int)$page['uid']);
172 $currentSite = $this->getCurrentSite();
173 } catch (SiteNotFoundException $e) {
174 // Usually happens in tests, as Pseudo Sites should be available everywhere.
175 $siteOfTargetPage = null;
176 $currentSite = null;
177 }
178 // Link to a page that has a site configuration
179 if ($siteOfTargetPage instanceof Site) {
180 $queryParameters = [];
181 parse_str($addQueryParams, $queryParameters);
182 unset($queryParameters['id'], $queryParameters['L']);
183 if ($pageType) {
184 $queryParameters['type'] = (int)$pageType;
185 }
186
187 // Generate the URL
188 $url = $this->generateUrlForPageWithSiteConfiguration($page, $siteOfTargetPage, $queryParameters, $sectionMark, $conf);
189
190 $treatAsExternalLink = true;
191 // no scheme => always not external
192 if (!$url->getScheme() || !$url->getHost()) {
193 $treatAsExternalLink = false;
194 } else {
195 // URL has a scheme, possibly because someone requested a full URL. So now lets check if the URL
196 // is on the same site pagetree. If this is the case, we'll treat it as internal
197 if ($currentSite instanceof Site && $currentSite->getRootPageId() === $siteOfTargetPage->getRootPageId()) {
198 $treatAsExternalLink = false;
199 }
200 }
201
202 $url = (string)$url;
203 if ($treatAsExternalLink) {
204 $target = $target ?: $this->resolveTargetAttribute($conf, 'extTarget', false, $tsfe->extTarget);
205 } else {
206 $target = (isset($page['target']) && trim($page['target'])) ? $page['target'] : $target;
207 if (empty($target)) {
208 $target = $this->resolveTargetAttribute($conf, 'target', true, $tsfe->intTarget);
209 }
210 }
211 } else {
212 list($url, $target) = $this->generateUrlForPageWithoutSiteConfiguration($page, $addQueryParams, $conf, $pageType, $sectionMark, $target, $MPvarAcc);
213 }
214
215 // If link is to an access restricted page which should be redirected, then find new URL:
216 if (empty($conf['linkAccessRestrictedPages'])
217 && $tsfe->config['config']['typolinkLinkAccessRestrictedPages']
218 && $tsfe->config['config']['typolinkLinkAccessRestrictedPages'] !== 'NONE'
219 && !$tsfe->checkPageGroupAccess($page)
220 ) {
221 $thePage = $tsfe->sys_page->getPage($tsfe->config['config']['typolinkLinkAccessRestrictedPages']);
222 $addParams = str_replace(
223 [
224 '###RETURN_URL###',
225 '###PAGE_ID###'
226 ],
227 [
228 rawurlencode($url),
229 $page['uid']
230 ],
231 $tsfe->config['config']['typolinkLinkAccessRestrictedPages_addParams']
232 );
233 $url = $this->contentObjectRenderer->getTypoLink_URL($thePage['uid'] . ($pageType ? ',' . $pageType : ''), $addParams, $target);
234 $url = $this->forceAbsoluteUrl($url, $conf);
235 $this->contentObjectRenderer->lastTypoLinkLD['totalUrl'] = $url;
236 }
237
238 // Setting title if blank value to link
239 $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $page['title']);
240 return [$url, $linkText, $target];
241 }
242
243 /**
244 * Create a UriInterface object when linking to a page with a site configuration
245 *
246 * @param array $page
247 * @param Site $siteOfTargetPage
248 * @param array $queryParameters
249 * @param string $fragment
250 * @param array $conf
251 * @return UriInterface
252 * @throws UnableToLinkException
253 */
254 protected function generateUrlForPageWithSiteConfiguration(array $page, Site $siteOfTargetPage, array $queryParameters, string $fragment, array $conf): UriInterface
255 {
256 $currentSite = $this->getCurrentSite();
257 $currentSiteLanguage = $this->getCurrentSiteLanguage();
258 // Happens when currently on a pseudo-site configuration
259 // We assume to use the default language then
260 if (!($currentSiteLanguage instanceof SiteLanguage)) {
261 $currentSiteLanguage = $currentSite->getDefaultLanguage();
262 }
263
264 $targetLanguageId = $conf['language'] ?? 'current';
265 if ($targetLanguageId === 'current') {
266 $targetLanguageId = $currentSiteLanguage ? $currentSiteLanguage->getLanguageId() : 0;
267 } else {
268 $targetLanguageId = (int)$targetLanguageId;
269 }
270 try {
271 $siteLanguageOfTargetPage = $siteOfTargetPage->getLanguageById($targetLanguageId);
272 } catch (\InvalidArgumentException $e) {
273 throw new UnableToLinkException('The target page does not have a language with ID ' . $targetLanguageId . ' configured in its site configuration.', 1535477406);
274 }
275
276 // By default, it is assumed to ab an internal link or current domain's linking scheme should be used
277 // Use the config option to override this.
278 $useAbsoluteUrl = $conf['forceAbsoluteUrl'] ?? false;
279 // Check if the current page equal to the site of the target page, now only set the absolute URL
280 if ($currentSite->getRootPageId() !== $siteOfTargetPage->getRootPageId()) {
281 $useAbsoluteUrl = true;
282 // @todo: let's only check for host / scheme once we use the Uri interface
283 } elseif ($siteLanguageOfTargetPage->getBase() !== $currentSiteLanguage->getBase()) {
284 $useAbsoluteUrl = true;
285 }
286
287 $targetPageId = (int)($page['l10n_parent'] > 0 ? $page['l10n_parent'] : $page['uid']);
288
289 $uri = GeneralUtility::makeInstance(PageUriBuilder::class)->buildUri(
290 $targetPageId,
291 $queryParameters,
292 $fragment,
293 ['site' => $siteOfTargetPage, 'language' => $siteLanguageOfTargetPage],
294 $useAbsoluteUrl ? PageUriBuilder::ABSOLUTE_URL : PageUriBuilder::ABSOLUTE_PATH
295 );
296 // Override scheme, but only if the site does not define a scheme yet AND the site defines a domain/host
297 if ($useAbsoluteUrl && !$uri->getScheme() && $uri->getHost()) {
298 $scheme = $conf['forceAbsoluteUrl.']['scheme'] ?? 'https';
299 $uri = $uri->withScheme($scheme);
300 }
301 return $uri;
302 }
303
304 /**
305 * Generate a URL for a page without site configuration
306 *
307 * @param array $page
308 * @param string $additionalQueryParams
309 * @param array $conf
310 * @param string $pageType
311 * @param string $sectionMark
312 * @param string $target
313 * @param array $MPvarAcc
314 * @return array
315 */
316 protected function generateUrlForPageWithoutSiteConfiguration(array $page, string $additionalQueryParams, array $conf, string $pageType, string $sectionMark, string $target, array $MPvarAcc): array
317 {
318 $tsfe = $this->getTypoScriptFrontendController();
319 $enableLinksAcrossDomains = $tsfe->config['config']['typolinkEnableLinksAcrossDomains'];
320 $targetDomain = '';
321 $currentDomain = (string)GeneralUtility::getIndpEnv('HTTP_HOST');
322 if (!empty($MPvarAcc)) {
323 $domainResolver = GeneralUtility::makeInstance(LegacyDomainResolver::class);
324 $targetDomainRecord = $domainResolver->matchPageId((int)$page['uid'], $GLOBALS['TYPO3_REQUEST']);
325 // Do not prepend the domain if it is the current hostname
326 if (!empty($targetDomainRecord) && !$targetDomainRecord['isCurrentDomain']) {
327 $targetDomain = $targetDomainRecord['domainName'];
328 }
329 }
330 $absoluteUrlScheme = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https' : 'http';
331 // URL shall be absolute:
332 if (isset($conf['forceAbsoluteUrl']) && $conf['forceAbsoluteUrl']) {
333 // Override scheme:
334 if (isset($conf['forceAbsoluteUrl.']['scheme']) && $conf['forceAbsoluteUrl.']['scheme']) {
335 $absoluteUrlScheme = $conf['forceAbsoluteUrl.']['scheme'];
336 }
337 // If no domain records are defined, use current domain
338 $targetDomain = $targetDomain ?: $currentDomain;
339 // If go for an absolute link, add site path if it's not taken care about by absRefPrefix
340 if (!$tsfe->absRefPrefix && $targetDomain === $currentDomain) {
341 $targetDomain = $currentDomain . rtrim(GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'), '/');
342 }
343 }
344 // If target page has a different domain and the current domain's linking scheme (e.g. RealURL/...) should not be used
345 if ($targetDomain !== '' && $targetDomain !== $currentDomain && !$enableLinksAcrossDomains) {
346 $target = $target ?: $this->resolveTargetAttribute($conf, 'extTarget', false, $tsfe->extTarget);
347 // Convert IDNA-like domain (if any)
348 if (!preg_match('/^[a-z0-9.\\-]*$/i', $targetDomain)) {
349 $targetDomain = GeneralUtility::idnaEncode($targetDomain);
350 }
351 $url = $absoluteUrlScheme . '://' . $targetDomain . '/index.php?id=' . $page['uid'] . $additionalQueryParams;
352 } else {
353 // Internal link or current domain's linking scheme should be used
354 // Internal target:
355 $target = (isset($page['target']) && trim($page['target'])) ? $page['target'] : $target;
356 if (empty($target)) {
357 $target = $this->resolveTargetAttribute($conf, 'target', true, $tsfe->intTarget);
358 }
359 $LD = $this->createTotalUrlAndLinkData($page, $target, $conf['no_cache'], $additionalQueryParams, $pageType, $targetDomain);
360 if ($targetDomain !== '') {
361 // We will add domain only if URL does not have it already.
362 if ($enableLinksAcrossDomains && $targetDomain !== $currentDomain && !empty($tsfe->absRefPrefix)) {
363 // Get rid of the absRefPrefix if necessary. absRefPrefix is applicable only
364 // to the current web site. If we have domain here it means we link across
365 // domains. absRefPrefix can contain domain name, which will screw up
366 // the link to the external domain.
367 $prefixLength = strlen($tsfe->absRefPrefix);
368 if (substr($LD['totalURL'], 0, $prefixLength) === $tsfe->absRefPrefix) {
369 $LD['totalURL'] = substr($LD['totalURL'], $prefixLength);
370 }
371 }
372 $urlParts = parse_url($LD['totalURL']);
373 if (empty($urlParts['host'])) {
374 $LD['totalURL'] = $absoluteUrlScheme . '://' . $targetDomain . ($LD['totalURL'][0] === '/' ? '' : '/') . $LD['totalURL'];
375 }
376 }
377 $url = $LD['totalURL'];
378 }
379 $url .= $sectionMark;
380 // If sectionMark is set, there is no baseURL AND the current page is the page the link is to,
381 // check if there are any additional parameters or addQueryString parameters and if not, drop the url.
382 if ($sectionMark
383 && !$tsfe->config['config']['baseURL']
384 && (int)$page['uid'] === (int)$tsfe->id
385 && !trim($additionalQueryParams)
386 && (empty($conf['addQueryString']) || !isset($conf['addQueryString.']))
387 ) {
388 $currentQueryArray = [];
389 parse_str(GeneralUtility::getIndpEnv('QUERY_STRING'), $currentQueryArray);
390 $currentQueryParams = GeneralUtility::implodeArrayForUrl('', $currentQueryArray, '', false, true);
391
392 if (!trim($currentQueryParams)) {
393 list(, $URLparams) = explode('?', $url);
394 list($URLparams) = explode('#', (string)$URLparams);
395 parse_str($URLparams . $LD['orig_type'], $URLparamsArray);
396 // Type nums must match as well as page ids
397 if ((int)$URLparamsArray['type'] === (int)$tsfe->type) {
398 unset($URLparamsArray['id']);
399 unset($URLparamsArray['type']);
400 // If there are no parameters left.... set the new url.
401 if (empty($URLparamsArray)) {
402 $url = $sectionMark;
403 }
404 }
405 }
406 }
407 return [$url, $target];
408 }
409
410 /**
411 * Returns the &MP variable value for a page id.
412 * 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.
413 *
414 * @param int $pageId page id
415 * @return string MP value, prefixed with &MP= (depending on $raw)
416 */
417 protected function getClosestMountPointValueForPage($pageId)
418 {
419 $tsfe = $this->getTypoScriptFrontendController();
420 if (empty($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) || !$tsfe->MP) {
421 return '';
422 }
423 // Same page as current.
424 if ((int)$tsfe->id === (int)$pageId) {
425 return $tsfe->MP;
426 }
427
428 // Find closest mount point
429 // Gets rootline of linked-to page
430 try {
431 $tCR_rootline = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get();
432 } catch (RootLineException $e) {
433 $tCR_rootline = [];
434 }
435 $inverseTmplRootline = array_reverse($tsfe->tmpl->rootLine);
436 $rl_mpArray = [];
437 $startMPaccu = false;
438 // Traverse root line of link uid and inside of that the REAL root line of current position.
439 foreach ($tCR_rootline as $tCR_data) {
440 foreach ($inverseTmplRootline as $rlKey => $invTmplRLRec) {
441 // Force accumulating when in overlay mode: Links to this page have to stay within the current branch
442 if ($invTmplRLRec['_MOUNT_OL'] && (int)$tCR_data['uid'] === (int)$invTmplRLRec['uid']) {
443 $startMPaccu = true;
444 }
445 // Accumulate MP data:
446 if ($startMPaccu && $invTmplRLRec['_MP_PARAM']) {
447 $rl_mpArray[] = $invTmplRLRec['_MP_PARAM'];
448 }
449 // If two PIDs matches and this is NOT the site root, start accumulation of MP data (on the next level):
450 // (The check for site root is done so links to branches outsite the site but sharing the site roots PID
451 // is NOT detected as within the branch!)
452 if ((int)$tCR_data['pid'] === (int)$invTmplRLRec['pid'] && count($inverseTmplRootline) !== $rlKey + 1) {
453 $startMPaccu = true;
454 }
455 }
456 if ($startMPaccu) {
457 // Good enough...
458 break;
459 }
460 }
461 return !empty($rl_mpArray) ? implode(',', array_reverse($rl_mpArray)) : '';
462 }
463
464 /**
465 * Initializes the automatically created mountPointMap coming from the "config.MP_mapRootPoints" setting
466 * Can be called many times with overhead only the first time since then the map is generated and cached in memory.
467 *
468 * Previously located within TemplateService::getFromMPmap()
469 *
470 * @param int $pageId Page id to return MPvar value for.
471 * @return string
472 */
473 public function getMountPointParameterFromRootPointMaps(int $pageId)
474 {
475 // Create map if not found already
476 $config = $this->getTypoScriptFrontendController()->config;
477 $mountPointMap = $this->initializeMountPointMap(
478 !empty($config['config']['MP_defaults']) ? $config['config']['MP_defaults'] : null,
479 !empty($config['config']['MP_mapRootPoints']) ? $config['config']['MP_mapRootPoints'] : null
480 );
481
482 // Finding MP var for Page ID:
483 if (!empty($mountPointMap[$pageId])) {
484 return implode(',', $mountPointMap[$pageId]);
485 }
486 return '';
487 }
488
489 /**
490 * Create mount point map, based on TypoScript config.MP_mapRootPoints and config.MP_defaults.
491 *
492 * @param string $defaultMountPoints a string as defined in config.MP_defaults
493 * @param string|null $mapRootPointList a string as defined in config.MP_mapRootPoints
494 * @return array
495 */
496 protected function initializeMountPointMap(string $defaultMountPoints = null, string $mapRootPointList = null): array
497 {
498 $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
499 $mountPointMap = $runtimeCache->get('pageLinkBuilderMountPointMap') ?: [];
500 if (!empty($mountPointMap) || (empty($mapRootPointList) && empty($defaultMountPoints))) {
501 return $mountPointMap;
502 }
503 if ($defaultMountPoints) {
504 $defaultMountPoints = GeneralUtility::trimExplode('|', $defaultMountPoints, true);
505 foreach ($defaultMountPoints as $temp_p) {
506 list($temp_idP, $temp_MPp) = explode(':', $temp_p, 2);
507 $temp_ids = GeneralUtility::intExplode(',', $temp_idP);
508 foreach ($temp_ids as $temp_id) {
509 $mountPointMap[$temp_id] = trim($temp_MPp);
510 }
511 }
512 }
513
514 $rootPoints = GeneralUtility::trimExplode(',', strtolower($mapRootPointList), true);
515 // Traverse rootpoints
516 foreach ($rootPoints as $p) {
517 $initMParray = [];
518 if ($p === 'root') {
519 $rootPage = $this->getTypoScriptFrontendController()->tmpl->rootLine[0];
520 $p = $rootPage['uid'];
521 if ($p['_MOUNT_OL'] && $p['_MP_PARAM']) {
522 $initMParray[] = $p['_MP_PARAM'];
523 }
524 }
525 $this->populateMountPointMapForPageRecursively($mountPointMap, $p, $initMParray);
526 }
527 $runtimeCache->set('pageLinkBuilderMountPointMap', $mountPointMap);
528 return $mountPointMap;
529 }
530
531 /**
532 * Creating mountPointMap for a certain ID root point.
533 * Previously called TemplateService->initMPmap_create()
534 *
535 * @param array $mountPointMap the exiting mount point map
536 * @param int $id Root id from which to start map creation.
537 * @param array $MP_array MP_array passed from root page.
538 * @param int $level Recursion brake. Incremented for each recursive call. 20 is the limit.
539 * @see getMountPointParameterFromRootPointMaps()
540 */
541 protected function populateMountPointMapForPageRecursively(array &$mountPointMap, int $id, $MP_array = [], $level = 0)
542 {
543 if ($id <= 0) {
544 return;
545 }
546 // First level, check id
547 if (!$level) {
548 // Find mount point if any:
549 $mount_info = $this->getTypoScriptFrontendController()->sys_page->getMountPointInfo($id);
550 // Overlay mode:
551 if (is_array($mount_info) && $mount_info['overlay']) {
552 $MP_array[] = $mount_info['MPvar'];
553 $id = $mount_info['mount_pid'];
554 }
555 // Set mapping information for this level:
556 $mountPointMap[$id] = $MP_array;
557 // Normal mode:
558 if (is_array($mount_info) && !$mount_info['overlay']) {
559 $MP_array[] = $mount_info['MPvar'];
560 $id = $mount_info['mount_pid'];
561 }
562 }
563 if ($id && $level < 20) {
564 $nextLevelAcc = [];
565 // Select and traverse current level pages:
566 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
567 $queryBuilder->getRestrictions()
568 ->removeAll()
569 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
570 $queryResult = $queryBuilder
571 ->select('uid', 'pid', 'doktype', 'mount_pid', 'mount_pid_ol')
572 ->from('pages')
573 ->where(
574 $queryBuilder->expr()->eq(
575 'pid',
576 $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
577 ),
578 $queryBuilder->expr()->neq(
579 'doktype',
580 $queryBuilder->createNamedParameter(PageRepository::DOKTYPE_RECYCLER, \PDO::PARAM_INT)
581 ),
582 $queryBuilder->expr()->neq(
583 'doktype',
584 $queryBuilder->createNamedParameter(PageRepository::DOKTYPE_BE_USER_SECTION, \PDO::PARAM_INT)
585 )
586 )->execute();
587 while ($row = $queryResult->fetch()) {
588 // Find mount point if any:
589 $next_id = $row['uid'];
590 $next_MP_array = $MP_array;
591 $mount_info = $this->getTypoScriptFrontendController()->sys_page->getMountPointInfo($next_id, $row);
592 // Overlay mode:
593 if (is_array($mount_info) && $mount_info['overlay']) {
594 $next_MP_array[] = $mount_info['MPvar'];
595 $next_id = $mount_info['mount_pid'];
596 }
597 if (!isset($mountPointMap[$next_id])) {
598 // Set mapping information for this level:
599 $mountPointMap[$next_id] = $next_MP_array;
600 // Normal mode:
601 if (is_array($mount_info) && !$mount_info['overlay']) {
602 $next_MP_array[] = $mount_info['MPvar'];
603 $next_id = $mount_info['mount_pid'];
604 }
605 // Register recursive call
606 // (have to do it this way since ALL of the current level should be registered BEFORE the sublevel at any time)
607 $nextLevelAcc[] = [$next_id, $next_MP_array];
608 }
609 }
610 // Call recursively, if any:
611 foreach ($nextLevelAcc as $pSet) {
612 $this->populateMountPointMapForPageRecursively($mountPointMap, $pSet[0], $pSet[1], $level + 1);
613 }
614 }
615 }
616
617 /**
618 * The mother of all functions creating links/URLs etc in a TypoScript environment.
619 * See the references below.
620 * Basically this function takes care of issues such as type,id,alias and Mount Points, URL rewriting (through hooks), M5/B6 encoded parameters etc.
621 * It is important to pass all links created through this function since this is the guarantee that globally configured settings for link creating are observed and that your applications will conform to the various/many configuration options in TypoScript Templates regarding this.
622 *
623 * @param array $page The page record of the page to which we are creating a link. Needed due to fields like uid, alias, target, title and sectionIndex_uid.
624 * @param string $target Target string
625 * @param bool $no_cache If set, then the "&no_cache=1" parameter is included in the URL.
626 * @param string $addParams Additional URL parameters to set in the URL. Syntax is "&foo=bar&foo2=bar2" etc. Also used internally to add parameters if needed.
627 * @param string $typeOverride If you set this value to something else than a blank string, then the typeNumber used in the link will be forced to this value. Normally the typeNum is based on the target set OR on $this->getTypoScriptFrontendController()->config['config']['forceTypeValue'] if found.
628 * @param string $targetDomain The target Doamin, if any was detected in typolink
629 * @return array Contains keys like "totalURL", "url", "sectionIndex", "linkVars", "no_cache", "type" of which "totalURL" is normally the value you would use while the other keys contains various parts that was used to construct "totalURL
630 */
631 protected function createTotalUrlAndLinkData($page, $target, $no_cache, $addParams = '', $typeOverride = '', $targetDomain = '')
632 {
633 $LD = [];
634 // Adding Mount Points, "&MP=", parameter for the current page if any is set
635 // but non other set explicitly
636 if (strpos($addParams, '&MP=') === false) {
637 $mountPointParameter = $this->getMountPointParameterFromRootPointMaps((int)$page['uid']);
638 if ($mountPointParameter) {
639 $addParams .= '&MP=' . rawurlencode($mountPointParameter);
640 }
641 }
642 // Setting ID/alias:
643 $script = 'index.php';
644 if ($page['alias']) {
645 $LD['url'] = $script . '?id=' . rawurlencode($page['alias']);
646 } else {
647 $LD['url'] = $script . '?id=' . $page['uid'];
648 }
649 // typeNum
650 $typeNum = $this->getTypoScriptFrontendController()->tmpl->setup[$target . '.']['typeNum'];
651 if (!MathUtility::canBeInterpretedAsInteger($typeOverride) && (int)$this->getTypoScriptFrontendController()->config['config']['forceTypeValue']) {
652 $typeOverride = (int)$this->getTypoScriptFrontendController()->config['config']['forceTypeValue'];
653 }
654 if ((string)$typeOverride !== '') {
655 $typeNum = $typeOverride;
656 }
657 // Override...
658 if ($typeNum) {
659 $LD['type'] = '&type=' . (int)$typeNum;
660 } else {
661 $LD['type'] = '';
662 }
663 // Preserving the type number.
664 $LD['orig_type'] = $LD['type'];
665 // noCache
666 $LD['no_cache'] = $no_cache ? '&no_cache=1' : '';
667 // linkVars
668 if ($addParams) {
669 $LD['linkVars'] = GeneralUtility::implodeArrayForUrl('', GeneralUtility::explodeUrl2Array($this->getTypoScriptFrontendController()->linkVars . $addParams), '', false, true);
670 } else {
671 $LD['linkVars'] = $this->getTypoScriptFrontendController()->linkVars;
672 }
673 // Add absRefPrefix if exists.
674 $LD['url'] = $this->getTypoScriptFrontendController()->absRefPrefix . $LD['url'];
675 // If the special key 'sectionIndex_uid' (added 'manually' in tslib/menu.php to the page-record) is set, then the link jumps directly to a section on the page.
676 $LD['sectionIndex'] = $page['sectionIndex_uid'] ? '#c' . $page['sectionIndex_uid'] : '';
677 // Compile the normal total url
678 $LD['totalURL'] = rtrim($LD['url'] . $LD['type'] . $LD['no_cache'] . $LD['linkVars'] . $this->getTypoScriptFrontendController()->getMethodUrlIdToken, '?') . $LD['sectionIndex'];
679 // Call post processing function for link rendering:
680 $_params = [
681 'LD' => &$LD,
682 'args' => ['page' => $page, 'oTarget' => $target, 'no_cache' => $no_cache, 'script' => $script, 'addParams' => $addParams, 'typeOverride' => $typeOverride, 'targetDomain' => $targetDomain],
683 'typeNum' => $typeNum
684 ];
685 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tstemplate.php']['linkData-PostProc'] ?? [] as $_funcRef) {
686 GeneralUtility::callUserFunction($_funcRef, $_params, $this->getTypoScriptFrontendController()->tmpl);
687 }
688 return $LD;
689 }
690
691 /**
692 * Check if we have a site object in the current request. if null, this usually means that
693 * this class was called from CLI context.
694 *
695 * @return SiteInterface|null
696 */
697 protected function getCurrentSite(): ?SiteInterface
698 {
699 if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
700 return $GLOBALS['TYPO3_REQUEST']->getAttribute('site', null);
701 }
702 if (MathUtility::canBeInterpretedAsInteger($GLOBALS['TSFE']->id) && $GLOBALS['TSFE']->id > 0) {
703 $matcher = GeneralUtility::makeInstance(SiteMatcher::class);
704 return $matcher->matchByPageId((int)$GLOBALS['TSFE']->id);
705 }
706 return null;
707 }
708
709 /**
710 * If the current request has a site language, this means that the SiteResolver has detected a
711 * page with a site configuration and a selected language, so let's choose that one.
712 *
713 * @return SiteLanguage|null
714 */
715 protected function getCurrentSiteLanguage(): ?SiteLanguage
716 {
717 if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
718 return $GLOBALS['TYPO3_REQUEST']->getAttribute('language', null);
719 }
720 return null;
721 }
722 }