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