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