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