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