‪TYPO3CMS  9.5
PageLinkBuilder.php
Go to the documentation of this file.
1 <?php
2 declare(strict_types = 1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
18 use Psr\Http\Message\ServerRequestInterface;
19 use Psr\Http\Message\UriInterface;
44 
49 {
54  public function ‪build(array &$linkDetails, string $linkText, string $target, array $conf): array
55  {
56  $tsfe = $this->‪getTypoScriptFrontendController();
57  // Checking if the id-parameter is an alias.
58  if (!empty($linkDetails['pagealias'])) {
59  $linkDetails['pageuid'] = $tsfe->sys_page->getPageIdFromAlias($linkDetails['pagealias']);
60  } elseif (empty($linkDetails['pageuid']) || $linkDetails['pageuid'] === 'current') {
61  // If no id or alias is given
62  $linkDetails['pageuid'] = $tsfe->id;
63  }
64 
65  // Link to page even if access is missing?
66  if (isset($conf['linkAccessRestrictedPages'])) {
67  $disableGroupAccessCheck = (bool)$conf['linkAccessRestrictedPages'];
68  } else {
69  $disableGroupAccessCheck = (bool)$tsfe->config['config']['typolinkLinkAccessRestrictedPages'];
70  }
71 
72  // Looking up the page record to verify its existence:
73  $page = $this->‪resolvePage($linkDetails, $conf, $disableGroupAccessCheck);
74 
75  if (empty($page)) {
76  throw new ‪UnableToLinkException('Page id "' . $linkDetails['typoLinkParameter'] . '" was not found, so "' . $linkText . '" was not linked.', 1490987336, null, $linkText);
77  }
78 
79  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typolinkProcessing']['typolinkModifyParameterForPageLinks'] ?? [] as $classData) {
80  $hookObject = GeneralUtility::makeInstance($classData);
81  if (!$hookObject instanceof ‪TypolinkModifyLinkConfigForPageLinksHookInterface) {
82  throw new \UnexpectedValueException('$hookObject must implement interface ' . TypolinkModifyLinkConfigForPageLinksHookInterface::class, 1483114905);
83  }
85  $conf = $hookObject->modifyPageLinkConfiguration($conf, $linkDetails, $page);
86  }
87  $enableLinksAcrossDomains = $tsfe->config['config']['typolinkEnableLinksAcrossDomains'];
88  if ($conf['no_cache.']) {
89  $conf['no_cache'] = (string)$this->contentObjectRenderer->stdWrap($conf['no_cache'], $conf['no_cache.']);
90  }
91 
92  $sectionMark = trim(isset($conf['section.']) ? (string)$this->contentObjectRenderer->stdWrap($conf['section'], $conf['section.']) : (string)$conf['section']);
93  if ($sectionMark === '' && isset($linkDetails['fragment'])) {
94  $sectionMark = $linkDetails['fragment'];
95  }
96  if ($sectionMark !== '') {
97  $sectionMark = '#' . (‪MathUtility::canBeInterpretedAsInteger($sectionMark) ? 'c' : '') . $sectionMark;
98  }
99  // Overruling 'type'
100  $pageType = $linkDetails['pagetype'] ?? '';
101 
102  if (isset($linkDetails['parameters'])) {
103  $conf['additionalParams'] .= '&' . ltrim($linkDetails['parameters'], '&');
104  }
105  // MointPoints, look for closest MPvar:
106  $MPvarAcc = [];
107  if (!$tsfe->config['config']['MP_disableTypolinkClosestMPvalue']) {
108  $temp_MP = $this->‪getClosestMountPointValueForPage($page['uid']);
109  if ($temp_MP) {
110  $MPvarAcc['closest'] = $temp_MP;
111  }
112  }
113  // Look for overlay Mount Point:
114  $mount_info = $tsfe->sys_page->getMountPointInfo($page['uid'], $page);
115  if (is_array($mount_info) && $mount_info['overlay']) {
116  $page = $tsfe->sys_page->getPage($mount_info['mount_pid'], $disableGroupAccessCheck);
117  if (empty($page)) {
118  throw new ‪UnableToLinkException('Mount point "' . $mount_info['mount_pid'] . '" was not available, so "' . $linkText . '" was not linked.', 1490987337, null, $linkText);
119  }
120  $MPvarAcc['re-map'] = $mount_info['MPvar'];
121  }
122  // Query Params:
123  $addQueryParams = $conf['addQueryString'] ? $this->contentObjectRenderer->getQueryArguments($conf['addQueryString.']) : '';
124  $addQueryParams .= isset($conf['additionalParams.']) ? trim((string)$this->contentObjectRenderer->stdWrap($conf['additionalParams'], $conf['additionalParams.'])) : trim((string)$conf['additionalParams']);
125  if ($addQueryParams === '&' || $addQueryParams[0] !== '&') {
126  $addQueryParams = '';
127  }
128  // Mount pages are always local and never link to another domain
129  if (!empty($MPvarAcc)) {
130  // Add "&MP" var:
131  $addQueryParams .= '&MP=' . rawurlencode(implode(',', $MPvarAcc));
132  } elseif (strpos($addQueryParams, '&MP=') === false) {
133  // We do not come here if additionalParams had '&MP='. This happens when typoLink is called from
134  // menu. Mount points always work in the content of the current domain and we must not change
135  // domain if MP variables exist.
136  // If we link across domains and page is free type shortcut, we must resolve the shortcut first!
137  // If we do not do it, TYPO3 will fail to (1) link proper page in RealURL/CoolURI because
138  // they return relative links and (2) show proper page if no RealURL/CoolURI exists when link is clicked
139  if ($enableLinksAcrossDomains
140  && (int)$page['doktype'] === ‪PageRepository::DOKTYPE_SHORTCUT
141  && (int)$page['shortcut_mode'] === ‪PageRepository::SHORTCUT_MODE_NONE
142  ) {
143  // Save in case of broken destination or endless loop
144  $page2 = $page;
145  // Same as in RealURL, seems enough
146  $maxLoopCount = 20;
147  while ($maxLoopCount
148  && is_array($page)
149  && (int)$page['doktype'] === ‪PageRepository::DOKTYPE_SHORTCUT
150  && (int)$page['shortcut_mode'] === ‪PageRepository::SHORTCUT_MODE_NONE
151  ) {
152  $page = $tsfe->sys_page->getPage($page['shortcut'], $disableGroupAccessCheck);
153  $maxLoopCount--;
154  }
155  if (empty($page) || $maxLoopCount === 0) {
156  // We revert if shortcut is broken or maximum number of loops is exceeded (indicates endless loop)
157  $page = $page2;
158  }
159  }
160  }
161  if ($conf['useCacheHash']) {
162  $params = $tsfe->linkVars . $addQueryParams . '&id=' . $page['uid'];
163  if (trim($params, '& ') !== '') {
164  $cHash = GeneralUtility::makeInstance(CacheHashCalculator::class)->generateForParameters($params);
165  $addQueryParams .= $cHash ? '&cHash=' . $cHash : '';
166  }
167  unset($params);
168  }
169 
170  // get config.linkVars and prepend them before the actual GET parameters
171  $queryParameters = [];
172  parse_str($addQueryParams, $queryParameters);
173  if ($tsfe->linkVars) {
174  $globalQueryParameters = [];
175  parse_str($tsfe->linkVars, $globalQueryParameters);
176  $queryParameters = array_replace_recursive($globalQueryParameters, $queryParameters);
177  }
178  // Disable "?id=", for pages with no site configuration, this is added later-on anyway
179  unset($queryParameters['id']);
180 
181  // Override language property if not being set already
182  if (isset($queryParameters['L']) && !isset($conf['language'])) {
183  $conf['language'] = (int)$queryParameters['L'];
184  }
185 
186  // Check if the target page has a site configuration
187  try {
188  $siteOfTargetPage = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId((int)$page['uid'], null, $queryParameters['MP'] ?? '');
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 !== null) {
198  $siteLanguageOfTargetPage = $this->‪getSiteLanguageOfTargetPage($siteOfTargetPage, (string)($conf['language'] ?? 'current'));
199  $languageAspect = ‪LanguageAspectFactory::createFromSiteLanguage($siteLanguageOfTargetPage);
200 
201  // Now overlay the page in the target language, in order to have valid title attributes etc.
202  if ($siteLanguageOfTargetPage->getLanguageId() > 0) {
203  $context = clone GeneralUtility::makeInstance(Context::class);
204  $context->setAspect('language', $languageAspect);
205  $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
206  $page = $pageRepository->getPageOverlay($page);
207  }
208  // Check if the target page can be access depending on l18n_cfg
209  if (!$tsfe->sys_page->isPageSuitableForLanguage($page, $languageAspect)) {
210  if ($siteLanguageOfTargetPage->getLanguageId() === 0 && GeneralUtility::hideIfDefaultLanguage($page['l18n_cfg'])) {
211  throw new ‪UnableToLinkException('Default language of page "' . $linkDetails['typoLinkParameter'] . '" is hidden, so "' . $linkText . '" was not linked.', 1551621985, null, $linkText);
212  }
213  // If the requested language is not the default language and the page has no overlay for this language
214  // generating a link would cause a 404 error when using this like if one of those conditions apply:
215  // - The page is set to be hidden if it is not translated (evaluated in TSFE)
216  // - The site configuration has a "strict" fallback set (evaluated in the Router - very early)
217  if ($siteLanguageOfTargetPage->getLanguageId() > 0 && !isset($page['_PAGES_OVERLAY']) && (GeneralUtility::hideIfNotTranslated($page['l18n_cfg']) || $siteLanguageOfTargetPage->getFallbackType() === 'strict')) {
218  throw new ‪UnableToLinkException('Fallback to default language of page "' . $linkDetails['typoLinkParameter'] . '" is disabled, so "' . $linkText . '" was not linked.', 1551621996, null, $linkText);
219  }
220  }
221 
222  // No need for any L parameter with Site handling
223  unset($queryParameters['L']);
224  if ($pageType) {
225  $queryParameters['type'] = (int)$pageType;
226  }
227  // Generate the URL
228  $url = $this->‪generateUrlForPageWithSiteConfiguration($page, $siteOfTargetPage, $queryParameters, $sectionMark, $conf);
229 
230  $treatAsExternalLink = true;
231  // no scheme => always not external
232  if (!$url->getScheme() || !$url->getHost()) {
233  $treatAsExternalLink = false;
234  } else {
235  // URL has a scheme, possibly because someone requested a full URL. So now lets check if the URL
236  // is on the same site pagetree. If this is the case, we'll treat it as internal
237  // @todo: currently this does not check if the target page is a mounted page in a different site,
238  // so it is treating this as an absolute URL, which is wrong
239  if ($currentSite instanceof ‪Site && $currentSite->‪getRootPageId() === $siteOfTargetPage->getRootPageId()) {
240  $treatAsExternalLink = false;
241  }
242  }
243 
244  $url = (string)$url;
245  if ($treatAsExternalLink) {
246  $target = $target ?: $this->‪resolveTargetAttribute($conf, 'extTarget', false, $tsfe->extTarget);
247  } else {
248  $target = (isset($page['target']) && trim($page['target'])) ? $page['target'] : $target;
249  if (empty($target)) {
250  $target = $this->‪resolveTargetAttribute($conf, 'target', true, $tsfe->intTarget);
251  }
252  }
253  } else {
254  // Now overlay the page in the target language, in order to have valid title attributes etc.
255  if (isset($conf['language']) && $conf['language'] > 0 && $conf['language'] !== 'current') {
256  $page = $tsfe->sys_page->getPageOverlay($page, (int)$conf['language']);
257  }
258  if ($conf['language'] === 0 && GeneralUtility::hideIfDefaultLanguage($page['l18n_cfg'])) {
259  throw new ‪UnableToLinkException('Default language of page "' . $linkDetails['typoLinkParameter'] . '" is hidden, so "' . $linkText . '" was not linked.', 1529527301, null, $linkText);
260  }
261  // If the requested language is not the default language and the page has no overlay for this language
262  // generating a link would cause a 404 error when using this like if the page is set to be hidden
263  // if it is not translated (evaluated in TSFE)
264  if ($conf['language'] > 0 && !isset($page['_PAGES_OVERLAY']) && GeneralUtility::hideIfNotTranslated($page['l18n_cfg'])) {
265  throw new ‪UnableToLinkException('Fallback to default language of page "' . $linkDetails['typoLinkParameter'] . '" is disabled, so "' . $linkText . '" was not linked.', 1529527488, null, $linkText);
266  }
267 
268  // If the typolink.language parameter was set, ensure that this is added to L query parameter
269  if (!isset($queryParameters['L']) && ‪MathUtility::canBeInterpretedAsInteger($conf['language'] ?? false)) {
270  $queryParameters['L'] = $conf['language'];
271  }
272  list($url, $target) = $this->‪generateUrlForPageWithoutSiteConfiguration($page, $queryParameters, $conf, $pageType, $sectionMark, $target, $MPvarAcc);
273  }
274 
275  // If link is to an access restricted page which should be redirected, then find new URL:
276  if (empty($conf['linkAccessRestrictedPages'])
277  && $tsfe->config['config']['typolinkLinkAccessRestrictedPages']
278  && $tsfe->config['config']['typolinkLinkAccessRestrictedPages'] !== 'NONE'
279  && !$tsfe->checkPageGroupAccess($page)
280  ) {
281  $thePage = $tsfe->sys_page->getPage($tsfe->config['config']['typolinkLinkAccessRestrictedPages']);
282  $addParams = str_replace(
283  [
284  '###RETURN_URL###',
285  '###PAGE_ID###'
286  ],
287  [
288  rawurlencode($url),
289  $page['uid']
290  ],
291  $tsfe->config['config']['typolinkLinkAccessRestrictedPages_addParams']
292  );
293  $url = $this->contentObjectRenderer->getTypoLink_URL($thePage['uid'] . ($pageType ? ',' . $pageType : ''), $addParams, $target);
294  $url = $this->‪forceAbsoluteUrl($url, $conf);
295  }
296 
297  // Setting title if blank value to link
298  $linkText = $this->‪parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $page['title']);
299  return [$url, $linkText, $target];
300  }
301 
312  protected function ‪resolvePage(array &$linkDetails, array &$configuration, bool $disableGroupAccessCheck): array
313  {
314  $pageRepository = $this->‪buildPageRepository();
315  // Looking up the page record to verify its existence
316  // This is used when a page to a translated page is executed directly.
317  $page = $pageRepository->getPage($linkDetails['pageuid'], $disableGroupAccessCheck);
318 
319  if (empty($page) || !is_array($page)) {
320  return [];
321  }
322 
323  $languageField = ‪$GLOBALS['TCA']['pages']['ctrl']['languageField'] ?? null;
324  $languageParentField = ‪$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] ?? null;
325  $language = (int)($page[$languageField] ?? 0);
326 
327  // The page that should be linked is actually a default-language page, nothing to do here.
328  if ($language === 0 || empty($page[$languageParentField])) {
329  return $page;
330  }
331 
332  // Let's fetch the default-language page now
333  $languageParentPage = $pageRepository->getPage(
334  $page[$languageParentField],
335  $disableGroupAccessCheck
336  );
337  if (empty($languageParentPage)) {
338  return $page;
339  }
340 
341  // Set the "pageuid" to the default-language page ID.
342  $linkDetails['pageuid'] = (int)$languageParentPage['uid'];
343  $configuration['language'] = $language;
344  $linkDetails['parameters'] .= '&L=' . $language;
345  return $languageParentPage;
346  }
347 
356  protected function ‪getSiteLanguageOfTargetPage(‪Site $siteOfTargetPage, string $targetLanguageId): ‪SiteLanguage
357  {
358  $currentSite = $this->‪getCurrentSite();
359  $currentSiteLanguage = $this->‪getCurrentSiteLanguage();
360  // Happens when currently on a pseudo-site configuration
361  // We assume to use the default language then
362  if ($currentSite && !($currentSiteLanguage instanceof ‪SiteLanguage)) {
363  $currentSiteLanguage = $currentSite->getDefaultLanguage();
364  }
365 
366  if ($targetLanguageId === 'current') {
367  $targetLanguageId = $currentSiteLanguage ? $currentSiteLanguage->getLanguageId() : 0;
368  } else {
369  $targetLanguageId = (int)$targetLanguageId;
370  }
371  try {
372  $siteLanguageOfTargetPage = $siteOfTargetPage->‪getLanguageById($targetLanguageId);
373  } catch (\InvalidArgumentException $e) {
374  throw new ‪UnableToLinkException('The target page does not have a language with ID ' . $targetLanguageId . ' configured in its site configuration.', 1535477406);
375  }
376  return $siteLanguageOfTargetPage;
377  }
378 
390  protected function ‪generateUrlForPageWithSiteConfiguration(array $page, ‪Site $siteOfTargetPage, array $queryParameters, string $fragment, array $conf): UriInterface
391  {
392  $currentSite = $this->‪getCurrentSite();
393  $currentSiteLanguage = $this->‪getCurrentSiteLanguage();
394  // Happens when currently on a pseudo-site configuration
395  // We assume to use the default language then
396  if ($currentSite && !($currentSiteLanguage instanceof ‪SiteLanguage)) {
397  $currentSiteLanguage = $currentSite->getDefaultLanguage();
398  }
399 
400  $siteLanguageOfTargetPage = $this->‪getSiteLanguageOfTargetPage($siteOfTargetPage, (string)($conf['language'] ?? 'current'));
401 
402  // By default, it is assumed to ab an internal link or current domain's linking scheme should be used
403  // Use the config option to override this.
404  $useAbsoluteUrl = $conf['forceAbsoluteUrl'] ?? false;
405  // Check if the current page equal to the site of the target page, now only set the absolute URL
406  // Always generate absolute URLs if no current site is set
407  if (
408  !$currentSite
409  || $currentSite->getRootPageId() !== $siteOfTargetPage->‪getRootPageId()
410  || $siteLanguageOfTargetPage->getBase()->getHost() !== $currentSiteLanguage->getBase()->getHost()) {
411  $useAbsoluteUrl = true;
412  }
413 
414  $targetPageId = (int)($page['l10n_parent'] > 0 ? $page['l10n_parent'] : $page['uid']);
415  $queryParameters['_language'] = $siteLanguageOfTargetPage;
416 
417  if ($conf['no_cache']) {
418  $queryParameters['no_cache'] = 1;
419  }
420 
421  if ($fragment
422  && $useAbsoluteUrl === false
423  && $currentSiteLanguage === $siteLanguageOfTargetPage
424  && $targetPageId === (int)‪$GLOBALS['TSFE']->id
425  && (empty($conf['addQueryString']) || !isset($conf['addQueryString.']))
426  && !‪$GLOBALS['TSFE']->config['config']['baseURL']
427  && count($queryParameters) === 1 // _language is always set
428  ) {
429  $uri = (new ‪Uri())->withFragment($fragment);
430  } else {
431  try {
432  $uri = $siteOfTargetPage->‪getRouter()->‪generateUri(
433  $targetPageId,
434  $queryParameters,
435  $fragment,
437  );
438  } catch (‪InvalidRouteArgumentsException $e) {
439  throw new ‪UnableToLinkException('The target page could not be linked. Error: ' . $e->getMessage(), 1535472406);
440  }
441  // Override scheme, but only if the site does not define a scheme yet AND the site defines a domain/host
442  if ($useAbsoluteUrl && !$uri->getScheme() && $uri->getHost()) {
443  $scheme = $conf['forceAbsoluteUrl.']['scheme'] ?? 'https';
444  $uri = $uri->withScheme($scheme);
445  }
446  }
447 
448  return $uri;
449  }
450 
463  protected function ‪generateUrlForPageWithoutSiteConfiguration(array $page, array $additionalQueryParams, array $conf, string $pageType, string $sectionMark, string $target, array $MPvarAcc): array
464  {
465  // Build a string out of the query parameters
466  $additionalQueryParams = http_build_query($additionalQueryParams, '', '&', PHP_QUERY_RFC3986);
467  if (!empty($additionalQueryParams)) {
468  $additionalQueryParams = '&' . $additionalQueryParams;
469  }
470 
471  $tsfe = $this->‪getTypoScriptFrontendController();
472  $enableLinksAcrossDomains = $tsfe->config['config']['typolinkEnableLinksAcrossDomains'];
473  $targetDomain = '';
474  $currentDomain = (string)GeneralUtility::getIndpEnv('HTTP_HOST');
475  if (!empty($MPvarAcc)) {
476  $domainResolver = GeneralUtility::makeInstance(LegacyDomainResolver::class);
477  $targetDomainRecord = $domainResolver->matchPageId((int)$page['uid'], ‪$GLOBALS['TYPO3_REQUEST']);
478  // Do not prepend the domain if it is the current hostname
479  if (!empty($targetDomainRecord) && !$targetDomainRecord['isCurrentDomain']) {
480  $targetDomain = $targetDomainRecord['domainName'];
481  }
482  }
483  $absoluteUrlScheme = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https' : 'http';
484  // URL shall be absolute:
485  if (isset($conf['forceAbsoluteUrl']) && $conf['forceAbsoluteUrl']) {
486  // Override scheme:
487  if (isset($conf['forceAbsoluteUrl.']['scheme']) && $conf['forceAbsoluteUrl.']['scheme']) {
488  $absoluteUrlScheme = $conf['forceAbsoluteUrl.']['scheme'];
489  }
490  // If no domain records are defined, use current domain
491  $targetDomain = $targetDomain ?: $currentDomain;
492  // If go for an absolute link, add site path if it's not taken care about by absRefPrefix
493  if (!$tsfe->absRefPrefix && $targetDomain === $currentDomain) {
494  $targetDomain = $currentDomain . rtrim(GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'), '/');
495  }
496  }
497  // If target page has a different domain and the current domain's linking scheme (e.g. RealURL/...) should not be used
498  if ($targetDomain !== '' && $targetDomain !== $currentDomain && !$enableLinksAcrossDomains) {
499  $target = $target ?: $this->‪resolveTargetAttribute($conf, 'extTarget', false, $tsfe->extTarget);
500  // Convert IDNA-like domain (if any)
501  if (!preg_match('/^[a-z0-9.\\-]*$/i', $targetDomain)) {
502  $targetDomain = GeneralUtility::idnaEncode($targetDomain);
503  }
504  $url = $absoluteUrlScheme . '://' . $targetDomain . '/index.php?id=' . $page['uid'] . $additionalQueryParams;
505  } else {
506  // Internal link or current domain's linking scheme should be used
507  // Internal target:
508  $target = (isset($page['target']) && trim($page['target'])) ? $page['target'] : $target;
509  if (empty($target)) {
510  $target = $this->‪resolveTargetAttribute($conf, 'target', true, $tsfe->intTarget);
511  }
512  $LD = $this->‪createTotalUrlAndLinkData($page, $target, $conf['no_cache'], $additionalQueryParams, $pageType, $targetDomain);
513  if ($targetDomain !== '') {
514  // We will add domain only if URL does not have it already.
515  if ($enableLinksAcrossDomains && $targetDomain !== $currentDomain && !empty($tsfe->absRefPrefix)) {
516  // Get rid of the absRefPrefix if necessary. absRefPrefix is applicable only
517  // to the current web site. If we have domain here it means we link across
518  // domains. absRefPrefix can contain domain name, which will screw up
519  // the link to the external domain.
520  $prefixLength = strlen($tsfe->absRefPrefix);
521  if (strpos($LD['totalURL'], $tsfe->absRefPrefix) === 0) {
522  $LD['totalURL'] = substr($LD['totalURL'], $prefixLength);
523  }
524  }
525  $urlParts = parse_url($LD['totalURL']);
526  if (empty($urlParts['host'])) {
527  $LD['totalURL'] = $absoluteUrlScheme . '://' . $targetDomain . ($LD['totalURL'][0] === '/' ? '' : '/') . $LD['totalURL'];
528  }
529  }
530  $url = $LD['totalURL'];
531  }
532  $url .= $sectionMark;
533  // If sectionMark is set, there is no baseURL AND the current page is the page the link is to,
534  // check if there are any additional parameters or addQueryString parameters and if not, drop the url.
535  if ($sectionMark
536  && !$tsfe->config['config']['baseURL']
537  && (int)$page['uid'] === (int)$tsfe->id
538  && !trim($additionalQueryParams)
539  && (empty($conf['addQueryString']) || !isset($conf['addQueryString.']))
540  ) {
541  $currentQueryArray = [];
542  parse_str(GeneralUtility::getIndpEnv('QUERY_STRING'), $currentQueryArray);
543 
544  if (empty($currentQueryArray)) {
545  list(, $URLparams) = explode('?', $url);
546  list($URLparams) = explode('#', (string)$URLparams);
547  parse_str($URLparams . $LD['orig_type'], $URLparamsArray);
548  // Type nums must match as well as page ids
549  if ((int)$URLparamsArray['type'] === (int)$tsfe->type) {
550  unset($URLparamsArray['id']);
551  unset($URLparamsArray['type']);
552  // If there are no parameters left.... set the new url.
553  if (empty($URLparamsArray)) {
554  $url = $sectionMark;
555  }
556  }
557  }
558  }
559  return [$url, $target];
560  }
561 
569  protected function ‪getClosestMountPointValueForPage($pageId)
570  {
571  $tsfe = $this->‪getTypoScriptFrontendController();
572  if (empty(‪$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) || !$tsfe->MP) {
573  return '';
574  }
575  // Same page as current.
576  if ((int)$tsfe->id === (int)$pageId) {
577  return $tsfe->MP;
578  }
579 
580  // Find closest mount point
581  // Gets rootline of linked-to page
582  try {
583  $tCR_rootline = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get();
584  } catch (‪RootLineException $e) {
585  $tCR_rootline = [];
586  }
587  $inverseTmplRootline = array_reverse($tsfe->tmpl->rootLine);
588  $rl_mpArray = [];
589  $startMPaccu = false;
590  // Traverse root line of link uid and inside of that the REAL root line of current position.
591  foreach ($tCR_rootline as $tCR_data) {
592  foreach ($inverseTmplRootline as $rlKey => $invTmplRLRec) {
593  // Force accumulating when in overlay mode: Links to this page have to stay within the current branch
594  if ($invTmplRLRec['_MOUNT_OL'] && (int)$tCR_data['uid'] === (int)$invTmplRLRec['uid']) {
595  $startMPaccu = true;
596  }
597  // Accumulate MP data:
598  if ($startMPaccu && $invTmplRLRec['_MP_PARAM']) {
599  $rl_mpArray[] = $invTmplRLRec['_MP_PARAM'];
600  }
601  // If two PIDs matches and this is NOT the site root, start accumulation of MP data (on the next level):
602  // (The check for site root is done so links to branches outsite the site but sharing the site roots PID
603  // is NOT detected as within the branch!)
604  if ((int)$tCR_data['pid'] === (int)$invTmplRLRec['pid'] && count($inverseTmplRootline) !== $rlKey + 1) {
605  $startMPaccu = true;
606  }
607  }
608  if ($startMPaccu) {
609  // Good enough...
610  break;
611  }
612  }
613  return !empty($rl_mpArray) ? implode(',', array_reverse($rl_mpArray)) : '';
614  }
615 
625  public function ‪getMountPointParameterFromRootPointMaps(int $pageId)
626  {
627  // Create map if not found already
628  $config = $this->‪getTypoScriptFrontendController()->config;
629  $mountPointMap = $this->‪initializeMountPointMap(
630  !empty($config['config']['MP_defaults']) ? $config['config']['MP_defaults'] : null,
631  !empty($config['config']['MP_mapRootPoints']) ? $config['config']['MP_mapRootPoints'] : null
632  );
633 
634  // Finding MP var for Page ID:
635  if (!empty($mountPointMap[$pageId])) {
636  return implode(',', $mountPointMap[$pageId]);
637  }
638  return '';
639  }
640 
648  protected function ‪initializeMountPointMap(string $defaultMountPoints = null, string $mapRootPointList = null): array
649  {
650  $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
651  $mountPointMap = $runtimeCache->get('pageLinkBuilderMountPointMap') ?: [];
652  if (!empty($mountPointMap) || (empty($mapRootPointList) && empty($defaultMountPoints))) {
653  return $mountPointMap;
654  }
655  if ($defaultMountPoints) {
656  $defaultMountPoints = GeneralUtility::trimExplode('|', $defaultMountPoints, true);
657  foreach ($defaultMountPoints as $temp_p) {
658  list($temp_idP, $temp_MPp) = explode(':', $temp_p, 2);
659  $temp_ids = GeneralUtility::intExplode(',', $temp_idP);
660  foreach ($temp_ids as $temp_id) {
661  $mountPointMap[$temp_id] = trim($temp_MPp);
662  }
663  }
664  }
665 
666  $rootPoints = GeneralUtility::trimExplode(',', strtolower($mapRootPointList), true);
667  // Traverse rootpoints
668  foreach ($rootPoints as $p) {
669  $initMParray = [];
670  if ($p === 'root') {
671  $rootPage = $this->‪getTypoScriptFrontendController()->tmpl->rootLine[0];
672  $p = $rootPage['uid'];
673  if ($rootPage['_MOUNT_OL'] && $rootPage['_MP_PARAM']) {
674  $initMParray[] = $rootPage['_MP_PARAM'];
675  }
676  }
677  $this->‪populateMountPointMapForPageRecursively($mountPointMap, (int)$p, $initMParray);
678  }
679  $runtimeCache->set('pageLinkBuilderMountPointMap', $mountPointMap);
680  return $mountPointMap;
681  }
682 
693  protected function ‪populateMountPointMapForPageRecursively(array &$mountPointMap, int $id, $MP_array = [], $level = 0)
694  {
695  if ($id <= 0) {
696  return;
697  }
698  // First level, check id
699  if (!$level) {
700  // Find mount point if any:
701  $mount_info = $this->‪getTypoScriptFrontendController()->sys_page->getMountPointInfo($id);
702  // Overlay mode:
703  if (is_array($mount_info) && $mount_info['overlay']) {
704  $MP_array[] = $mount_info['MPvar'];
705  $id = $mount_info['mount_pid'];
706  }
707  // Set mapping information for this level:
708  $mountPointMap[$id] = $MP_array;
709  // Normal mode:
710  if (is_array($mount_info) && !$mount_info['overlay']) {
711  $MP_array[] = $mount_info['MPvar'];
712  $id = $mount_info['mount_pid'];
713  }
714  }
715  if ($id && $level < 20) {
716  $nextLevelAcc = [];
717  // Select and traverse current level pages:
718  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
719  $queryBuilder->getRestrictions()
720  ->removeAll()
721  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
722  $queryResult = $queryBuilder
723  ->select('uid', 'pid', 'doktype', 'mount_pid', 'mount_pid_ol', 't3ver_state')
724  ->from('pages')
725  ->where(
726  $queryBuilder->expr()->eq(
727  'pid',
728  $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
729  ),
730  $queryBuilder->expr()->neq(
731  'doktype',
732  $queryBuilder->createNamedParameter(‪PageRepository::DOKTYPE_RECYCLER, \PDO::PARAM_INT)
733  ),
734  $queryBuilder->expr()->neq(
735  'doktype',
736  $queryBuilder->createNamedParameter(‪PageRepository::DOKTYPE_BE_USER_SECTION, \PDO::PARAM_INT)
737  )
738  )->execute();
739  while ($row = $queryResult->fetch()) {
740  // Find mount point if any:
741  $next_id = (int)$row['uid'];
742  $next_MP_array = $MP_array;
743  $mount_info = $this->‪getTypoScriptFrontendController()->sys_page->getMountPointInfo($next_id, $row);
744  // Overlay mode:
745  if (is_array($mount_info) && $mount_info['overlay']) {
746  $next_MP_array[] = $mount_info['MPvar'];
747  $next_id = (int)$mount_info['mount_pid'];
748  }
749  if (!isset($mountPointMap[$next_id])) {
750  // Set mapping information for this level:
751  $mountPointMap[$next_id] = $next_MP_array;
752  // Normal mode:
753  if (is_array($mount_info) && !$mount_info['overlay']) {
754  $next_MP_array[] = $mount_info['MPvar'];
755  $next_id = (int)$mount_info['mount_pid'];
756  }
757  // Register recursive call
758  // (have to do it this way since ALL of the current level should be registered BEFORE the sublevel at any time)
759  $nextLevelAcc[] = [$next_id, $next_MP_array];
760  }
761  }
762  // Call recursively, if any:
763  foreach ($nextLevelAcc as $pSet) {
764  $this->‪populateMountPointMapForPageRecursively($mountPointMap, $pSet[0], $pSet[1], $level + 1);
765  }
766  }
767  }
768 
783  protected function ‪createTotalUrlAndLinkData($page, $target, $no_cache, $addParams = '', $typeOverride = '', $targetDomain = '')
784  {
785  $LD = [];
786  // Adding Mount Points, "&MP=", parameter for the current page if any is set
787  // but non other set explicitly
788  if (strpos($addParams, '&MP=') === false) {
789  $mountPointParameter = $this->‪getMountPointParameterFromRootPointMaps((int)$page['uid']);
790  if ($mountPointParameter) {
791  $addParams .= '&MP=' . rawurlencode($mountPointParameter);
792  }
793  }
794  // Setting ID/alias:
795  $script = 'index.php';
796  if ($page['alias']) {
797  $LD['url'] = $script . '?id=' . rawurlencode($page['alias']);
798  } else {
799  $LD['url'] = $script . '?id=' . $page['uid'];
800  }
801  // typeNum
802  $typeNum = $this->‪getTypoScriptFrontendController()->tmpl->setup[$target . '.']['typeNum'];
803  if (!‪MathUtility::canBeInterpretedAsInteger($typeOverride) && (int)$this->‪getTypoScriptFrontendController()->config['config']['forceTypeValue']) {
804  $typeOverride = (int)$this->‪getTypoScriptFrontendController()->config['config']['forceTypeValue'];
805  }
806  if ((string)$typeOverride !== '') {
807  $typeNum = $typeOverride;
808  }
809  // Override...
810  if ($typeNum) {
811  $LD['type'] = '&type=' . (int)$typeNum;
812  } else {
813  $LD['type'] = '';
814  }
815  // Preserving the type number.
816  $LD['orig_type'] = $LD['type'];
817  // noCache
818  $LD['no_cache'] = $no_cache ? '&no_cache=1' : '';
819  // linkVars
820  if ($addParams) {
821  $LD['linkVars'] = ‪HttpUtility::buildQueryString(GeneralUtility::explodeUrl2Array($this->‪getTypoScriptFrontendController()->linkVars . $addParams), '&');
822  } else {
823  $LD['linkVars'] = $this->‪getTypoScriptFrontendController()->linkVars;
824  }
825  // Add absRefPrefix if exists.
826  $LD['url'] = $this->‪getTypoScriptFrontendController()->absRefPrefix . $LD['url'];
827  // 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.
828  $LD['sectionIndex'] = $page['sectionIndex_uid'] ? '#c' . $page['sectionIndex_uid'] : '';
829  // Compile the normal total url
830  $LD['totalURL'] = rtrim($LD['url'] . $LD['type'] . $LD['no_cache'] . $LD['linkVars'] . $this->‪getTypoScriptFrontendController()->getMethodUrlIdToken, '?') . $LD['sectionIndex'];
831  // Call post processing function for link rendering:
832  $_params = [
833  'LD' => &$LD,
834  'args' => ['page' => $page, 'oTarget' => $target, 'no_cache' => $no_cache, 'script' => $script, 'addParams' => $addParams, 'typeOverride' => $typeOverride, 'targetDomain' => $targetDomain],
835  'typeNum' => $typeNum
836  ];
837  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tstemplate.php']['linkData-PostProc'] ?? [] as $_funcRef) {
838  GeneralUtility::callUserFunction($_funcRef, $_params, $this->‪getTypoScriptFrontendController()->tmpl);
839  }
840  return $LD;
841  }
842 
849  protected function ‪getCurrentSite(): ?‪SiteInterface
850  {
851  if (‪$GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
852  return ‪$GLOBALS['TYPO3_REQUEST']->getAttribute('site', null);
853  }
854  if (‪MathUtility::canBeInterpretedAsInteger(‪$GLOBALS['TSFE']->id) && ‪$GLOBALS['TSFE']->id > 0) {
855  $matcher = GeneralUtility::makeInstance(SiteMatcher::class);
856  try {
857  $site = $matcher->matchByPageId((int)‪$GLOBALS['TSFE']->id);
858  } catch (‪SiteNotFoundException $e) {
859  $site = null;
860  }
861  return $site;
862  }
863  return null;
864  }
865 
873  {
874  if (‪$GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
875  return ‪$GLOBALS['TYPO3_REQUEST']->getAttribute('language', null);
876  }
877  return null;
878  }
879 
887  {
888  // clone global context object (singleton)
889  $context = clone GeneralUtility::makeInstance(Context::class);
890  $context->setAspect(
891  'language',
892  GeneralUtility::makeInstance(LanguageAspect::class)
893  );
894  $pageRepository = GeneralUtility::makeInstance(
895  PageRepository::class,
896  $context
897  );
898  return $pageRepository;
899  }
900 }
‪TYPO3\CMS\Core\Site\Entity\SiteInterface
Definition: SiteInterface.php:25
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger($var)
Definition: MathUtility.php:73
‪TYPO3\CMS\Core\Context\LanguageAspectFactory
Definition: LanguageAspectFactory.php:25
‪TYPO3\CMS\Core\Routing\RouterInterface
Definition: RouterInterface.php:27
‪TYPO3\CMS\Core\Site\Entity\Site\getLanguageById
‪SiteLanguage getLanguageById(int $languageId)
Definition: Site.php:234
‪TYPO3\CMS\Frontend\Page\PageRepository\DOKTYPE_SHORTCUT
‪const DOKTYPE_SHORTCUT
Definition: PageRepository.php:170
‪TYPO3\CMS\Core\Site\Entity\Site\getRouter
‪RouterInterface getRouter(Context $context=null)
Definition: Site.php:364
‪TYPO3\CMS\Core\Utility\RootlineUtility
Definition: RootlineUtility.php:36
‪TYPO3\CMS\Core\Exception\SiteNotFoundException
Definition: SiteNotFoundException.php:25
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Core\Context\LanguageAspectFactory\createFromSiteLanguage
‪static LanguageAspect createFromSiteLanguage(SiteLanguage $language)
Definition: LanguageAspectFactory.php:102
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:49
‪TYPO3\CMS\Core\Routing\RouterInterface\generateUri
‪UriInterface generateUri($route, array $parameters=[], string $fragment='', string $type=self::ABSOLUTE_URL)
‪TYPO3\CMS\Core\Http\Uri
Definition: Uri.php:27
‪TYPO3\CMS\Core\Site\Entity\Site
Definition: Site.php:39
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage
Definition: SiteLanguage.php:25
‪TYPO3\CMS\Frontend\Compatibility\LegacyDomainResolver
Definition: LegacyDomainResolver.php:37
‪TYPO3\CMS\Frontend\Page\PageRepository
Definition: PageRepository.php:53
‪TYPO3\CMS\Core\Utility\HttpUtility\buildQueryString
‪static string buildQueryString(array $parameters, string $prependCharacter='', bool $skipEmptyParameters=false)
Definition: HttpUtility.php:160
‪TYPO3\CMS\Core\Routing\RouterInterface\ABSOLUTE_PATH
‪const ABSOLUTE_PATH
Definition: RouterInterface.php:36
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:34
‪TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException
Definition: InvalidRouteArgumentsException.php:23
‪TYPO3\CMS\Core\Context\LanguageAspect
Definition: LanguageAspect.php:55
‪TYPO3\CMS\Core\Site\Entity\Site\getRootPageId
‪int getRootPageId()
Definition: Site.php:196
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:26
‪TYPO3\CMS\Frontend\Page\CacheHashCalculator
Definition: CacheHashCalculator.php:24
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:21
‪TYPO3\CMS\Frontend\Page\PageRepository\DOKTYPE_RECYCLER
‪const DOKTYPE_RECYCLER
Definition: PageRepository.php:175
‪TYPO3\CMS\Core\Exception\Page\RootLineException
Definition: RootLineException.php:24
‪TYPO3\CMS\Core\Utility\HttpUtility
Definition: HttpUtility.php:21
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:44
‪TYPO3\CMS\Core\Routing\RouterInterface\ABSOLUTE_URL
‪const ABSOLUTE_URL
Definition: RouterInterface.php:31
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:45
‪TYPO3\CMS\Frontend\Page\PageRepository\SHORTCUT_MODE_NONE
‪const SHORTCUT_MODE_NONE
Definition: PageRepository.php:180
‪TYPO3\CMS\Core\Routing\SiteMatcher
Definition: SiteMatcher.php:53
‪TYPO3\CMS\Frontend\Page\PageRepository\DOKTYPE_BE_USER_SECTION
‪const DOKTYPE_BE_USER_SECTION
Definition: PageRepository.php:171