[BUGFIX] Prematurely end data array processing on invalid item
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Typolink / PageLinkBuilder.php
1 <?php
2
3 declare(strict_types=1);
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 namespace TYPO3\CMS\Frontend\Typolink;
19
20 use Psr\Http\Message\ServerRequestInterface;
21 use Psr\Http\Message\UriInterface;
22 use TYPO3\CMS\Core\Cache\CacheManager;
23 use TYPO3\CMS\Core\Context\Context;
24 use TYPO3\CMS\Core\Context\LanguageAspect;
25 use TYPO3\CMS\Core\Context\LanguageAspectFactory;
26 use TYPO3\CMS\Core\Database\ConnectionPool;
27 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
28 use TYPO3\CMS\Core\Domain\Repository\PageRepository;
29 use TYPO3\CMS\Core\Exception\Page\RootLineException;
30 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
31 use TYPO3\CMS\Core\Http\Uri;
32 use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
33 use TYPO3\CMS\Core\Routing\RouterInterface;
34 use TYPO3\CMS\Core\Site\Entity\Site;
35 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
36 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
37 use TYPO3\CMS\Core\Site\SiteFinder;
38 use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility;
39 use TYPO3\CMS\Core\Utility\GeneralUtility;
40 use TYPO3\CMS\Core\Utility\MathUtility;
41 use TYPO3\CMS\Core\Utility\RootlineUtility;
42 use TYPO3\CMS\Frontend\ContentObject\TypolinkModifyLinkConfigForPageLinksHookInterface;
43
44 /**
45 * Builds a TypoLink to a certain page
46 */
47 class PageLinkBuilder extends AbstractTypolinkBuilder
48 {
49 /**
50 * @inheritdoc
51 * @throws UnableToLinkException
52 */
53 public function build(array &$linkDetails, string $linkText, string $target, array $conf): array
54 {
55 $tsfe = $this->getTypoScriptFrontendController();
56 if (empty($linkDetails['pageuid']) || $linkDetails['pageuid'] === 'current') {
57 // If no id is given
58 $linkDetails['pageuid'] = $tsfe->id;
59 }
60
61 // Link to page even if access is missing?
62 if (isset($conf['linkAccessRestrictedPages'])) {
63 $disableGroupAccessCheck = (bool)$conf['linkAccessRestrictedPages'];
64 } else {
65 $disableGroupAccessCheck = (bool)($tsfe->config['config']['typolinkLinkAccessRestrictedPages'] ?? false);
66 }
67
68 // Looking up the page record to verify its existence:
69 $page = $this->resolvePage($linkDetails, $conf, $disableGroupAccessCheck);
70
71 if (empty($page)) {
72 throw new UnableToLinkException('Page id "' . $linkDetails['typoLinkParameter'] . '" was not found, so "' . $linkText . '" was not linked.', 1490987336, null, $linkText);
73 }
74
75 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typolinkProcessing']['typolinkModifyParameterForPageLinks'] ?? [] as $classData) {
76 $hookObject = GeneralUtility::makeInstance($classData);
77 if (!$hookObject instanceof TypolinkModifyLinkConfigForPageLinksHookInterface) {
78 throw new \UnexpectedValueException('$hookObject must implement interface ' . TypolinkModifyLinkConfigForPageLinksHookInterface::class, 1483114905);
79 }
80 /** @var TypolinkModifyLinkConfigForPageLinksHookInterface $hookObject */
81 $conf = $hookObject->modifyPageLinkConfiguration($conf, $linkDetails, $page);
82 }
83 $conf['no_cache'] = (string)$this->contentObjectRenderer->stdWrapValue('no_cache', $conf ?? []);
84
85 $sectionMark = trim((string)$this->contentObjectRenderer->stdWrapValue('section', $conf ?? []));
86 if ($sectionMark === '' && isset($linkDetails['fragment'])) {
87 $sectionMark = $linkDetails['fragment'];
88 }
89 if ($sectionMark !== '') {
90 $sectionMark = '#' . (MathUtility::canBeInterpretedAsInteger($sectionMark) ? 'c' : '') . $sectionMark;
91 }
92 // Overruling 'type'
93 $pageType = $linkDetails['pagetype'] ?? '';
94
95 if (isset($linkDetails['parameters'])) {
96 $conf['additionalParams'] .= '&' . ltrim($linkDetails['parameters'], '&');
97 }
98 // MountPoints, look for closest MPvar:
99 $MPvarAcc = [];
100 if (!($tsfe->config['config']['MP_disableTypolinkClosestMPvalue'] ?? false)) {
101 $temp_MP = $this->getClosestMountPointValueForPage($page['uid']);
102 if ($temp_MP) {
103 $MPvarAcc['closest'] = $temp_MP;
104 }
105 }
106 // Look for overlay Mount Point:
107 $mount_info = $tsfe->sys_page->getMountPointInfo($page['uid'], $page);
108 if (is_array($mount_info) && $mount_info['overlay']) {
109 $page = $tsfe->sys_page->getPage($mount_info['mount_pid'], $disableGroupAccessCheck);
110 if (empty($page)) {
111 throw new UnableToLinkException('Mount point "' . $mount_info['mount_pid'] . '" was not available, so "' . $linkText . '" was not linked.', 1490987337, null, $linkText);
112 }
113 $MPvarAcc['re-map'] = $mount_info['MPvar'];
114 }
115 // Query Params:
116 $addQueryParams = $conf['addQueryString'] ? $this->contentObjectRenderer->getQueryArguments($conf['addQueryString.'] ?? []) : '';
117 $addQueryParams .= trim((string)$this->contentObjectRenderer->stdWrapValue('additionalParams', $conf ?? []));
118 if ($addQueryParams === '&' || $addQueryParams[0] !== '&') {
119 $addQueryParams = '';
120 }
121 // Mount pages are always local and never link to another domain
122 if (!empty($MPvarAcc)) {
123 // Add "&MP" var:
124 $addQueryParams .= '&MP=' . rawurlencode(implode(',', $MPvarAcc));
125 } elseif (strpos($addQueryParams, '&MP=') === false) {
126 // We do not come here if additionalParams had '&MP='. This happens when typoLink is called from
127 // menu. Mount points always work in the content of the current domain and we must not change
128 // domain if MP variables exist.
129 // If we link across domains and page is free type shortcut, we must resolve the shortcut first!
130 if ((int)$page['doktype'] === PageRepository::DOKTYPE_SHORTCUT
131 && (int)$page['shortcut_mode'] === PageRepository::SHORTCUT_MODE_NONE
132 ) {
133 // Save in case of broken destination or endless loop
134 $page2 = $page;
135 // Same as in RealURL, seems enough
136 $maxLoopCount = 20;
137 while ($maxLoopCount
138 && is_array($page)
139 && (int)$page['doktype'] === PageRepository::DOKTYPE_SHORTCUT
140 && (int)$page['shortcut_mode'] === PageRepository::SHORTCUT_MODE_NONE
141 ) {
142 $page = $tsfe->sys_page->getPage($page['shortcut'], $disableGroupAccessCheck);
143 $maxLoopCount--;
144 }
145 if (empty($page) || $maxLoopCount === 0) {
146 // We revert if shortcut is broken or maximum number of loops is exceeded (indicates endless loop)
147 $page = $page2;
148 }
149 }
150 }
151
152 // get config.linkVars and prepend them before the actual GET parameters
153 $queryParameters = [];
154 parse_str($addQueryParams, $queryParameters);
155 if ($tsfe->linkVars) {
156 $globalQueryParameters = [];
157 parse_str($tsfe->linkVars, $globalQueryParameters);
158 $queryParameters = array_replace_recursive($globalQueryParameters, $queryParameters);
159 }
160 // Disable "?id=", for pages with no site configuration, this is added later-on anyway
161 unset($queryParameters['id']);
162
163 // Override language property if not being set already
164 if (isset($queryParameters['L']) && !isset($conf['language'])) {
165 $conf['language'] = (int)$queryParameters['L'];
166 unset($queryParameters['L']);
167 }
168
169 // Check if the target page has a site configuration
170 try {
171 $siteOfTargetPage = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId((int)$page['uid'], null, $queryParameters['MP'] ?? '');
172 $currentSite = $this->getCurrentSite();
173 } catch (SiteNotFoundException $e) {
174 // Usually happens in tests, as sites with configuration should be available everywhere.
175 $siteOfTargetPage = null;
176 $currentSite = null;
177 }
178
179 // Link to a page that has a site configuration
180 if ($siteOfTargetPage !== null) {
181 try {
182 $siteLanguageOfTargetPage = $this->getSiteLanguageOfTargetPage($siteOfTargetPage, (string)($conf['language'] ?? 'current'));
183 } catch (UnableToLinkException $e) {
184 throw new UnableToLinkException($e->getMessage(), $e->getCode(), $e, $linkText);
185 }
186 $languageAspect = LanguageAspectFactory::createFromSiteLanguage($siteLanguageOfTargetPage);
187
188 // Now overlay the page in the target language, in order to have valid title attributes etc.
189 if ($siteLanguageOfTargetPage->getLanguageId() > 0) {
190 $context = clone GeneralUtility::makeInstance(Context::class);
191 $context->setAspect('language', $languageAspect);
192 $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
193 $page = $pageRepository->getPageOverlay($page);
194 }
195 // Check if the target page can be access depending on l18n_cfg
196 if (!$tsfe->sys_page->isPageSuitableForLanguage($page, $languageAspect)) {
197 $pageTranslationVisibility = new PageTranslationVisibility((int)($page['l18n_cfg'] ?? 0));
198 if ($siteLanguageOfTargetPage->getLanguageId() === 0 && $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage()) {
199 throw new UnableToLinkException('Default language of page "' . $linkDetails['typoLinkParameter'] . '" is hidden, so "' . $linkText . '" was not linked.', 1551621985, null, $linkText);
200 }
201 // If the requested language is not the default language and the page has no overlay for this language
202 // generating a link would cause a 404 error when using this like if one of those conditions apply:
203 // - The page is set to be hidden if it is not translated (evaluated in TSFE)
204 // - The site configuration has a "strict" fallback set (evaluated in the Router - very early)
205 if ($siteLanguageOfTargetPage->getLanguageId() > 0 && !isset($page['_PAGES_OVERLAY']) && ($pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists() || $siteLanguageOfTargetPage->getFallbackType() === 'strict')) {
206 throw new UnableToLinkException('Fallback to default language of page "' . $linkDetails['typoLinkParameter'] . '" is disabled, so "' . $linkText . '" was not linked.', 1551621996, null, $linkText);
207 }
208 }
209
210 if ($pageType) {
211 $queryParameters['type'] = (int)$pageType;
212 }
213
214 $treatAsExternalLink = true;
215 // External links are resolved via calling Typolink again (could be anything, really)
216 if ((int)$page['doktype'] === PageRepository::DOKTYPE_LINK) {
217 $conf['parameter'] = $page['url'];
218 unset($conf['parameter.']);
219 $this->contentObjectRenderer->typoLink($linkText, $conf);
220 $target = $this->contentObjectRenderer->lastTypoLinkTarget;
221 $url = $this->contentObjectRenderer->lastTypoLinkUrl;
222 if (empty($url)) {
223 throw new UnableToLinkException('Link to external page "' . $page['uid'] . '" does not have a proper target URL, so "' . $linkText . '" was not linked.', 1551621999, null, $linkText);
224 }
225 } else {
226 // Generate the URL
227 $url = $this->generateUrlForPageWithSiteConfiguration($page, $siteOfTargetPage, $queryParameters, $sectionMark, $conf);
228 // no scheme => always not external
229 if (!$url->getScheme() || !$url->getHost()) {
230 $treatAsExternalLink = false;
231 } else {
232 // URL has a scheme, possibly because someone requested a full URL. So now lets check if the URL
233 // is on the same site pagetree. If this is the case, we'll treat it as internal
234 // @todo: currently this does not check if the target page is a mounted page in a different site,
235 // so it is treating this as an absolute URL, which is wrong
236 if ($currentSite instanceof Site && $currentSite->getRootPageId() === $siteOfTargetPage->getRootPageId()) {
237 $treatAsExternalLink = false;
238 }
239 }
240 $url = (string)$url;
241 }
242 if ($treatAsExternalLink) {
243 $target = $target ?: $this->resolveTargetAttribute($conf, 'extTarget', false, $tsfe->extTarget);
244 } else {
245 $target = (isset($page['target']) && trim($page['target'])) ? $page['target'] : $target;
246 if (empty($target)) {
247 $target = $this->resolveTargetAttribute($conf, 'target', true, $tsfe->intTarget);
248 }
249 }
250 } else {
251 throw new UnableToLinkException('Could not link to page with ID: ' . $page['uid'], 1546887172, null, $linkText);
252 }
253
254 // If link is to an access restricted page which should be redirected, then find new URL:
255 if (empty($conf['linkAccessRestrictedPages'])
256 && ($tsfe->config['config']['typolinkLinkAccessRestrictedPages'] ?? false)
257 && $tsfe->config['config']['typolinkLinkAccessRestrictedPages'] !== 'NONE'
258 && !$tsfe->checkPageGroupAccess($page)
259 ) {
260 $thePage = $tsfe->sys_page->getPage($tsfe->config['config']['typolinkLinkAccessRestrictedPages']);
261 $addParams = str_replace(
262 [
263 '###RETURN_URL###',
264 '###PAGE_ID###'
265 ],
266 [
267 rawurlencode($url),
268 $page['uid']
269 ],
270 $tsfe->config['config']['typolinkLinkAccessRestrictedPages_addParams']
271 );
272 $url = $this->contentObjectRenderer->getTypoLink_URL($thePage['uid'] . ($pageType ? ',' . $pageType : ''), $addParams, $target);
273 $url = $this->forceAbsoluteUrl($url, $conf);
274 }
275
276 // Setting title if blank value to link
277 $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $page['title']);
278 return [$url, $linkText, $target];
279 }
280
281 /**
282 * Resolves page and if a translated page was found, resolves that to it
283 * language parent, adjusts `$linkDetails['pageuid']` (for hook processing)
284 * and modifies `$configuration['language']` (for language URL generation).
285 *
286 * @param array $linkDetails
287 * @param array $configuration
288 * @param bool $disableGroupAccessCheck
289 * @return array
290 */
291 protected function resolvePage(array &$linkDetails, array &$configuration, bool $disableGroupAccessCheck): array
292 {
293 $pageRepository = $this->buildPageRepository();
294 // Looking up the page record to verify its existence
295 // This is used when a page to a translated page is executed directly.
296 $page = $pageRepository->getPage($linkDetails['pageuid'], $disableGroupAccessCheck);
297
298 if (empty($page) || !is_array($page)) {
299 return [];
300 }
301
302 $languageField = $GLOBALS['TCA']['pages']['ctrl']['languageField'] ?? null;
303 $languageParentField = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] ?? null;
304 $language = (int)($page[$languageField] ?? 0);
305
306 // The page that should be linked is actually a default-language page, nothing to do here.
307 if ($language === 0 || empty($page[$languageParentField])) {
308 return $page;
309 }
310
311 // Let's fetch the default-language page now
312 $languageParentPage = $pageRepository->getPage(
313 $page[$languageParentField],
314 $disableGroupAccessCheck
315 );
316 if (empty($languageParentPage)) {
317 return $page;
318 }
319
320 // Set the "pageuid" to the default-language page ID.
321 $linkDetails['pageuid'] = (int)$languageParentPage['uid'];
322 $configuration['language'] = $language;
323 return $languageParentPage;
324 }
325
326 /**
327 * Fetches the requested language of a site that the link should be built for
328 *
329 * @param Site $siteOfTargetPage
330 * @param string $targetLanguageId "current" or the languageId
331 * @return SiteLanguage
332 * @throws UnableToLinkException
333 */
334 protected function getSiteLanguageOfTargetPage(Site $siteOfTargetPage, string $targetLanguageId): SiteLanguage
335 {
336 $currentSite = $this->getCurrentSite();
337 $currentSiteLanguage = $this->getCurrentSiteLanguage();
338 // Happens when currently on a pseudo-site configuration
339 // We assume to use the default language then
340 if ($currentSite && !($currentSiteLanguage instanceof SiteLanguage)) {
341 $currentSiteLanguage = $currentSite->getDefaultLanguage();
342 }
343
344 if ($targetLanguageId === 'current') {
345 $targetLanguageId = $currentSiteLanguage ? $currentSiteLanguage->getLanguageId() : 0;
346 } else {
347 $targetLanguageId = (int)$targetLanguageId;
348 }
349 try {
350 $siteLanguageOfTargetPage = $siteOfTargetPage->getLanguageById($targetLanguageId);
351 } catch (\InvalidArgumentException $e) {
352 throw new UnableToLinkException('The target page does not have a language with ID ' . $targetLanguageId . ' configured in its site configuration.', 1535477406);
353 }
354 return $siteLanguageOfTargetPage;
355 }
356
357 /**
358 * Create a UriInterface object when linking to a page with a site configuration
359 *
360 * @param array $page
361 * @param Site $siteOfTargetPage
362 * @param array $queryParameters
363 * @param string $fragment
364 * @param array $conf
365 * @return UriInterface
366 * @throws UnableToLinkException
367 */
368 protected function generateUrlForPageWithSiteConfiguration(array $page, Site $siteOfTargetPage, array $queryParameters, string $fragment, array $conf): UriInterface
369 {
370 $currentSite = $this->getCurrentSite();
371 $currentSiteLanguage = $this->getCurrentSiteLanguage();
372 // Happens when currently on a pseudo-site configuration
373 // We assume to use the default language then
374 if ($currentSite && !($currentSiteLanguage instanceof SiteLanguage)) {
375 $currentSiteLanguage = $currentSite->getDefaultLanguage();
376 }
377
378 $siteLanguageOfTargetPage = $this->getSiteLanguageOfTargetPage($siteOfTargetPage, (string)($conf['language'] ?? 'current'));
379
380 // By default, it is assumed to ab an internal link or current domain's linking scheme should be used
381 // Use the config option to override this.
382 $useAbsoluteUrl = $conf['forceAbsoluteUrl'] ?? false;
383 // Check if the current page equal to the site of the target page, now only set the absolute URL
384 // Always generate absolute URLs if no current site is set
385 if (
386 !$currentSite
387 || $currentSite->getRootPageId() !== $siteOfTargetPage->getRootPageId()
388 || $siteLanguageOfTargetPage->getBase()->getHost() !== $currentSiteLanguage->getBase()->getHost()) {
389 $useAbsoluteUrl = true;
390 }
391
392 $targetPageId = (int)($page['l10n_parent'] > 0 ? $page['l10n_parent'] : $page['uid']);
393 $queryParameters['_language'] = $siteLanguageOfTargetPage;
394
395 if ($conf['no_cache'] ?? false) {
396 $queryParameters['no_cache'] = 1;
397 }
398
399 if ($fragment
400 && $useAbsoluteUrl === false
401 && $currentSiteLanguage === $siteLanguageOfTargetPage
402 && $targetPageId === (int)$GLOBALS['TSFE']->id
403 && (empty($conf['addQueryString']) || !isset($conf['addQueryString.']))
404 && !$GLOBALS['TSFE']->config['config']['baseURL']
405 && count($queryParameters) === 1 // _language is always set
406 ) {
407 $uri = (new Uri())->withFragment($fragment);
408 } else {
409 try {
410 $uri = $siteOfTargetPage->getRouter()->generateUri(
411 $targetPageId,
412 $queryParameters,
413 $fragment,
414 $useAbsoluteUrl ? RouterInterface::ABSOLUTE_URL : RouterInterface::ABSOLUTE_PATH
415 );
416 } catch (InvalidRouteArgumentsException $e) {
417 throw new UnableToLinkException('The target page could not be linked. Error: ' . $e->getMessage(), 1535472406);
418 }
419 // Override scheme, but only if the site does not define a scheme yet AND the site defines a domain/host
420 if ($useAbsoluteUrl && !$uri->getScheme() && $uri->getHost()) {
421 $scheme = $conf['forceAbsoluteUrl.']['scheme'] ?? 'https';
422 $uri = $uri->withScheme($scheme);
423 }
424 }
425
426 return $uri;
427 }
428
429 /**
430 * 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.
431 *
432 * @param int $pageId page id
433 * @return string MP value, prefixed with &MP= (depending on $raw)
434 */
435 protected function getClosestMountPointValueForPage($pageId)
436 {
437 $tsfe = $this->getTypoScriptFrontendController();
438 if (empty($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) || !$tsfe->MP) {
439 return '';
440 }
441 // Same page as current.
442 if ((int)$tsfe->id === (int)$pageId) {
443 return $tsfe->MP;
444 }
445
446 // Find closest mount point
447 // Gets rootline of linked-to page
448 try {
449 $tCR_rootline = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get();
450 } catch (RootLineException $e) {
451 $tCR_rootline = [];
452 }
453 $inverseTmplRootline = array_reverse($tsfe->tmpl->rootLine);
454 $rl_mpArray = [];
455 $startMPaccu = false;
456 // Traverse root line of link uid and inside of that the REAL root line of current position.
457 foreach ($tCR_rootline as $tCR_data) {
458 foreach ($inverseTmplRootline as $rlKey => $invTmplRLRec) {
459 // Force accumulating when in overlay mode: Links to this page have to stay within the current branch
460 if ($invTmplRLRec['_MOUNT_OL'] && (int)$tCR_data['uid'] === (int)$invTmplRLRec['uid']) {
461 $startMPaccu = true;
462 }
463 // Accumulate MP data:
464 if ($startMPaccu && $invTmplRLRec['_MP_PARAM']) {
465 $rl_mpArray[] = $invTmplRLRec['_MP_PARAM'];
466 }
467 // If two PIDs matches and this is NOT the site root, start accumulation of MP data (on the next level):
468 // (The check for site root is done so links to branches outside the site but sharing the site roots PID
469 // is NOT detected as within the branch!)
470 if ((int)$tCR_data['pid'] === (int)$invTmplRLRec['pid'] && count($inverseTmplRootline) !== $rlKey + 1) {
471 $startMPaccu = true;
472 }
473 }
474 if ($startMPaccu) {
475 // Good enough...
476 break;
477 }
478 }
479 return !empty($rl_mpArray) ? implode(',', array_reverse($rl_mpArray)) : '';
480 }
481
482 /**
483 * Initializes the automatically created mountPointMap coming from the "config.MP_mapRootPoints" setting
484 * Can be called many times with overhead only the first time since then the map is generated and cached in memory.
485 *
486 * Previously located within TemplateService::getFromMPmap()
487 *
488 * @param int $pageId Page id to return MPvar value for.
489 * @return string
490 */
491 public function getMountPointParameterFromRootPointMaps(int $pageId)
492 {
493 // Create map if not found already
494 $config = $this->getTypoScriptFrontendController()->config;
495 $mountPointMap = $this->initializeMountPointMap(
496 !empty($config['config']['MP_defaults']) ? $config['config']['MP_defaults'] : '',
497 !empty($config['config']['MP_mapRootPoints']) ? $config['config']['MP_mapRootPoints'] : ''
498 );
499
500 // Finding MP var for Page ID:
501 if (!empty($mountPointMap[$pageId])) {
502 return implode(',', $mountPointMap[$pageId]);
503 }
504 return '';
505 }
506
507 /**
508 * Create mount point map, based on TypoScript config.MP_mapRootPoints and config.MP_defaults.
509 *
510 * @param string $defaultMountPoints a string as defined in config.MP_defaults
511 * @param string $mapRootPointList a string as defined in config.MP_mapRootPoints
512 * @return array
513 */
514 protected function initializeMountPointMap(string $defaultMountPoints = '', string $mapRootPointList = ''): array
515 {
516 $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
517 $mountPointMap = $runtimeCache->get('pageLinkBuilderMountPointMap') ?: [];
518 if (!empty($mountPointMap) || (empty($mapRootPointList) && empty($defaultMountPoints))) {
519 return $mountPointMap;
520 }
521 if ($defaultMountPoints) {
522 $defaultMountPoints = GeneralUtility::trimExplode('|', $defaultMountPoints, true);
523 foreach ($defaultMountPoints as $temp_p) {
524 [$temp_idP, $temp_MPp] = explode(':', $temp_p, 2);
525 $temp_ids = GeneralUtility::intExplode(',', $temp_idP);
526 foreach ($temp_ids as $temp_id) {
527 $mountPointMap[$temp_id] = trim($temp_MPp);
528 }
529 }
530 }
531
532 $rootPoints = GeneralUtility::trimExplode(',', strtolower($mapRootPointList), true);
533 // Traverse rootpoints
534 foreach ($rootPoints as $p) {
535 $initMParray = [];
536 if ($p === 'root') {
537 $rootPage = $this->getTypoScriptFrontendController()->tmpl->rootLine[0];
538 $p = $rootPage['uid'];
539 if ($rootPage['_MOUNT_OL'] && $rootPage['_MP_PARAM']) {
540 $initMParray[] = $rootPage['_MP_PARAM'];
541 }
542 }
543 $this->populateMountPointMapForPageRecursively($mountPointMap, (int)$p, $initMParray);
544 }
545 $runtimeCache->set('pageLinkBuilderMountPointMap', $mountPointMap);
546 return $mountPointMap;
547 }
548
549 /**
550 * Creating mountPointMap for a certain ID root point.
551 * Previously called TemplateService->initMPmap_create()
552 *
553 * @param array $mountPointMap the exiting mount point map
554 * @param int $id Root id from which to start map creation.
555 * @param array $MP_array MP_array passed from root page.
556 * @param int $level Recursion brake. Incremented for each recursive call. 20 is the limit.
557 * @see getMountPointParameterFromRootPointMaps()
558 */
559 protected function populateMountPointMapForPageRecursively(array &$mountPointMap, int $id, $MP_array = [], $level = 0)
560 {
561 if ($id <= 0) {
562 return;
563 }
564 // First level, check id
565 if (!$level) {
566 // Find mount point if any:
567 $mount_info = $this->getTypoScriptFrontendController()->sys_page->getMountPointInfo($id);
568 // Overlay mode:
569 if (is_array($mount_info) && $mount_info['overlay']) {
570 $MP_array[] = $mount_info['MPvar'];
571 $id = $mount_info['mount_pid'];
572 }
573 // Set mapping information for this level:
574 $mountPointMap[$id] = $MP_array;
575 // Normal mode:
576 if (is_array($mount_info) && !$mount_info['overlay']) {
577 $MP_array[] = $mount_info['MPvar'];
578 $id = $mount_info['mount_pid'];
579 }
580 }
581 if ($id && $level < 20) {
582 $nextLevelAcc = [];
583 // Select and traverse current level pages:
584 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
585 $queryBuilder->getRestrictions()
586 ->removeAll()
587 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
588 $queryResult = $queryBuilder
589 ->select('uid', 'pid', 'doktype', 'mount_pid', 'mount_pid_ol', 't3ver_state', 'l10n_parent')
590 ->from('pages')
591 ->where(
592 $queryBuilder->expr()->eq(
593 'pid',
594 $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
595 ),
596 $queryBuilder->expr()->neq(
597 'doktype',
598 $queryBuilder->createNamedParameter(PageRepository::DOKTYPE_RECYCLER, \PDO::PARAM_INT)
599 ),
600 $queryBuilder->expr()->neq(
601 'doktype',
602 $queryBuilder->createNamedParameter(PageRepository::DOKTYPE_BE_USER_SECTION, \PDO::PARAM_INT)
603 )
604 )->execute();
605 while ($row = $queryResult->fetch()) {
606 // Find mount point if any:
607 $next_id = (int)$row['uid'];
608 $next_MP_array = $MP_array;
609 $mount_info = $this->getTypoScriptFrontendController()->sys_page->getMountPointInfo($next_id, $row);
610 // Overlay mode:
611 if (is_array($mount_info) && $mount_info['overlay']) {
612 $next_MP_array[] = $mount_info['MPvar'];
613 $next_id = (int)$mount_info['mount_pid'];
614 }
615 if (!isset($mountPointMap[$next_id])) {
616 // Set mapping information for this level:
617 $mountPointMap[$next_id] = $next_MP_array;
618 // Normal mode:
619 if (is_array($mount_info) && !$mount_info['overlay']) {
620 $next_MP_array[] = $mount_info['MPvar'];
621 $next_id = (int)$mount_info['mount_pid'];
622 }
623 // Register recursive call
624 // (have to do it this way since ALL of the current level should be registered BEFORE the sublevel at any time)
625 $nextLevelAcc[] = [$next_id, $next_MP_array];
626 }
627 }
628 // Call recursively, if any:
629 foreach ($nextLevelAcc as $pSet) {
630 $this->populateMountPointMapForPageRecursively($mountPointMap, $pSet[0], $pSet[1], $level + 1);
631 }
632 }
633 }
634
635 /**
636 * Check if we have a site object in the current request. if null, this usually means that
637 * this class was called from CLI context.
638 *
639 * @return SiteInterface|null
640 */
641 protected function getCurrentSite(): ?SiteInterface
642 {
643 if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
644 return $GLOBALS['TYPO3_REQUEST']->getAttribute('site', null);
645 }
646 if (MathUtility::canBeInterpretedAsInteger($GLOBALS['TSFE']->id) && $GLOBALS['TSFE']->id > 0) {
647 $finder = GeneralUtility::makeInstance(SiteFinder::class);
648 try {
649 $site = $finder->getSiteByPageId((int)$GLOBALS['TSFE']->id);
650 } catch (SiteNotFoundException $e) {
651 $site = null;
652 }
653 return $site;
654 }
655 return null;
656 }
657
658 /**
659 * If the current request has a site language, this means that the SiteResolver has detected a
660 * page with a site configuration and a selected language, so let's choose that one.
661 *
662 * @return SiteLanguage|null
663 */
664 protected function getCurrentSiteLanguage(): ?SiteLanguage
665 {
666 if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
667 return $GLOBALS['TYPO3_REQUEST']->getAttribute('language', null);
668 }
669 return null;
670 }
671
672 /**
673 * Builds PageRepository instance without depending on global context, e.g.
674 * not automatically overlaying records based on current request language.
675 *
676 * @return PageRepository
677 */
678 protected function buildPageRepository(): PageRepository
679 {
680 // clone global context object (singleton)
681 $context = clone GeneralUtility::makeInstance(Context::class);
682 $context->setAspect(
683 'language',
684 GeneralUtility::makeInstance(LanguageAspect::class)
685 );
686 $pageRepository = GeneralUtility::makeInstance(
687 PageRepository::class,
688 $context
689 );
690 return $pageRepository;
691 }
692 }