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