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