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