‪TYPO3CMS  ‪main
PageLinkBuilder.php
Go to the documentation of this file.
1 <?php
2 
3 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 
19 
20 use Psr\EventDispatcher\EventDispatcherInterface;
21 use Psr\Http\Message\UriInterface;
50 
55 {
56  public function ‪build(array &$linkDetails, string $linkText, string $target, array $conf): ‪LinkResultInterface
57  {
58  $linkResultType = ‪LinkService::TYPE_PAGE;
59  $conf['additionalParams'] = $conf['additionalParams'] ?? '';
60  $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
61  $request = $this->contentObjectRenderer->getRequest();
62  if (empty($linkDetails['pageuid']) || $linkDetails['pageuid'] === 'current') {
63  // If no id is given try to fetch it from PageInformation attribute, else fetch it from site.
64  $pageId = $request->getAttribute('frontend.page.information')?->getId();
65  if ($pageId === null) {
66  $site = $request->getAttribute('site');
67  if ($site !== null) {
68  $pageId = $site->getRootPageId();
69  } else {
70  // @todo: We can usually expect a site to be always set. This fallback here may only be
71  // required due to incomplete setup in transform.html VH functional test?!
72  $allSites = $siteFinder->getAllSites();
73  $firstSite = reset($allSites);
74  $pageId = $firstSite->getRootPageId();
75  }
76  }
77  $linkDetails['pageuid'] = $pageId;
78  }
79 
80  // Link to page even if access is missing?
81  $frontendTypoScriptConfigArray = $request->getAttribute('frontend.typoscript')?->getConfigArray();
82  if (isset($conf['linkAccessRestrictedPages'])) {
83  $disableGroupAccessCheck = (bool)($conf['linkAccessRestrictedPages'] ?? false);
84  } else {
85  $disableGroupAccessCheck = (bool)($frontendTypoScriptConfigArray['typolinkLinkAccessRestrictedPages'] ?? false);
86  }
87 
88  // Looking up the page record to verify its existence:
89  $page = $this->‪resolvePage($linkDetails, $conf, $disableGroupAccessCheck);
90 
91  if (empty($page)) {
92  throw new ‪UnableToLinkException('Page id "' . $linkDetails['pageuid'] . '" was not found, so "' . $linkText . '" was not linked.', 1490987336, null, $linkText);
93  }
94 
95  $fragment = $this->‪calculateUrlFragment($conf, $linkDetails);
96  $queryParameters = $this->‪calculateQueryParameters($conf, $linkDetails);
97  // Add MP parameter
98  $mountPointParameter = $this->‪calculateMountPointParameters($page, $disableGroupAccessCheck, $linkText);
99  if ($mountPointParameter !== null) {
100  $queryParameters['MP'] = $mountPointParameter;
101  }
102 
103  $event = new ‪ModifyPageLinkConfigurationEvent($conf, $linkDetails, $page, $queryParameters, $fragment);
104  $event = GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch($event);
105  $conf = $event->getConfiguration();
106  $page = $event->getPage();
107  $queryParameters = $event->getQueryParameters();
108  $fragment = $event->getFragment();
109 
110  // Check if the target page has a site configuration
111  try {
112  $siteOfTargetPage = $siteFinder->getSiteByPageId((int)$page['uid'], null, $queryParameters['MP'] ?? '');
113  $currentSite = $this->‪getCurrentSite();
114  } catch (‪SiteNotFoundException $e) {
115  // Usually happens in tests, as sites with configuration should be available everywhere.
116  $siteOfTargetPage = null;
117  $currentSite = null;
118  }
119  if ($siteOfTargetPage === null) {
120  throw new ‪UnableToLinkException('Could not link to page with ID: ' . $page['uid'], 1546887172, null, $linkText);
121  }
122 
123  try {
124  $siteLanguageOfTargetPage = $this->‪getSiteLanguageOfTargetPage($siteOfTargetPage, (string)($conf['language'] ?? 'current'));
125  } catch (‪UnableToLinkException $e) {
126  throw new ‪UnableToLinkException($e->getMessage(), $e->getCode(), $e, $linkText);
127  }
128  $languageAspect = ‪LanguageAspectFactory::createFromSiteLanguage($siteLanguageOfTargetPage);
129  $pageRepository = $this->‪buildPageRepository($languageAspect);
130 
131  // Now overlay the page in the target language, in order to have valid title attributes etc.
132  if ($siteLanguageOfTargetPage->getLanguageId() > 0) {
133  $pageObject = $conf['page'] ?? null;
134  if ($pageObject instanceof ‪Page
135  && $pageObject->‪getPageId() === (int)$page['uid'] // No MP/Shortcut changes
136  && !$event->pageWasModified()
137  && (
138  $pageObject->getLanguageId() === $languageAspect->getId()
139  || $pageObject->getRequestedLanguage() === $languageAspect->getId() // Page is suitable for that language
140  || $pageObject->getLanguageId() === 0 // No translation found
141  )
142  ) {
143  $page = $pageObject->toArray(true);
144  } else {
145  $page = $pageRepository->getLanguageOverlay('pages', $page);
146  }
147 
148  // Check if the translated page is a shortcut, but the default page wasn't a shortcut, so this is
149  // resolved as well, see ScenarioDTest in functional tests.
150  // Currently not supported: When this is the case (only a translated page is a shortcut),
151  // but the page links to a different site.
152  $shortcutPage = $this->‪resolveShortcutPage($page, $pageRepository, $disableGroupAccessCheck);
153  if (!empty($shortcutPage)) {
154  $page = $shortcutPage;
155  }
156  }
157  // Check if the target page can be access depending on l18n_cfg
158  if (!$pageRepository->isPageSuitableForLanguage($page, $languageAspect)) {
159  $pageTranslationVisibility = new ‪PageTranslationVisibility((int)($page['l18n_cfg'] ?? 0));
160  if ($siteLanguageOfTargetPage->getLanguageId() === 0 && $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage()) {
161  throw new ‪UnableToLinkException('Default language of page "' . ($linkDetails['typoLinkParameter'] ?? 'unknown') . '" is hidden, so "' . $linkText . '" was not linked.', 1551621985, null, $linkText);
162  }
163  // If the requested language is not the default language and the page has no overlay for this language
164  // generating a link would cause a 404 error when using this like if one of those conditions apply:
165  // - The page is set to be hidden if it is not translated (evaluated in TSFE)
166  // - The site configuration has a "strict" fallback set (evaluated in the Router - very early)
167  if ($siteLanguageOfTargetPage->getLanguageId() > 0 && !isset($page['_LOCALIZED_UID']) && ($pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists() || $siteLanguageOfTargetPage->getFallbackType() === 'strict')) {
168  throw new ‪UnableToLinkException('Fallback to default language of page "' . ($linkDetails['typoLinkParameter'] ?? 'unknown') . '" is disabled, so "' . $linkText . '" was not linked.', 1551621996, null, $linkText);
169  }
170  }
171 
172  $treatAsExternalLink = true;
173  // External links are resolved via calling Typolink again (could be anything, really)
174  if ((int)$page['doktype'] === ‪PageRepository::DOKTYPE_LINK) {
175  $conf['parameter'] = $page['url'];
176  unset($conf['parameter.']);
177  // Use "pages.target" as this is the requested field for external links as well
178  if (!isset($conf['extTarget'])) {
179  $conf['extTarget'] = (isset($page['target']) && trim($page['target'])) ? $page['target'] : $target;
180  }
181  $linkResultFromExternalUrl = $this->contentObjectRenderer->createLink($linkText, $conf);
182  $target = $linkResultFromExternalUrl->getTarget();
183  ‪$url = $linkResultFromExternalUrl->getUrl();
184  // If the page external URL is resolved into a URL or email, this should be taken into account when compiling the final link result object
185  $linkResultType = $linkResultFromExternalUrl->getType();
186  if (empty(‪$url)) {
187  throw new ‪UnableToLinkException('Link to external page "' . $page['uid'] . '" does not have a proper target URL, so "' . $linkText . '" was not linked.', 1551621999, null, $linkText);
188  }
189  } else {
190  // Generate the URL
191  ‪$url = $this->‪generateUrlForPageWithSiteConfiguration($page, $siteOfTargetPage, $queryParameters, $fragment, $conf);
192  // no scheme => always not external
193  if (!‪$url->getScheme() || !‪$url->getHost()) {
194  $treatAsExternalLink = false;
195  } else {
196  // URL has a scheme, possibly because someone requested a full URL. So now lets check if the URL
197  // is on the same site pagetree. If this is the case, we'll treat it as internal
198  // @todo: currently this does not check if the target page is a mounted page in a different site,
199  // so it is treating this as an absolute URL, which is wrong
200  if ($currentSite && $currentSite->getRootPageId() === $siteOfTargetPage->getRootPageId()) {
201  $treatAsExternalLink = false;
202  }
203  }
204  ‪$url = (string)‪$url;
205  }
206 
207  $target = $this->‪calculateTargetAttribute($page, $conf, $treatAsExternalLink, $target);
208 
209  // If link is to an access-restricted page which should be redirected, then find new URL
210  $result = new LinkResult($linkResultType, ‪$url);
211  if ($this->‪shouldModifyUrlForAccessRestrictedPage($conf, $page)) {
212  ‪$url = $this->‪modifyUrlForAccessRestrictedPage(‪$url, $page, $linkDetails['pagetype'] ?? '');
213  $result = new LinkResult($linkResultType, ‪$url);
214  $additionalAttributes = (string)($frontendTypoScriptConfigArray['typolinkLinkAccessRestrictedPages.']['ATagParams'] ?? '');
215  if ($additionalAttributes !== '') {
216  $additionalAttributes = GeneralUtility::get_tag_attributes($additionalAttributes);
217  $result = $result->withAttributes($additionalAttributes);
218  }
219  }
220 
221  // Setting title if blank value to link
222  $linkText = $this->‪parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $page['title'] ?? '');
223  return $result
224  ->withLinkConfiguration($conf)
225  ->withTarget($target)
226  ->withLinkText($linkText);
227  }
228 
235  protected function ‪calculateUrlFragment(array $conf, array $linkDetails): string
236  {
237  $fragment = trim((string)$this->contentObjectRenderer->stdWrapValue('section', $conf, $linkDetails['fragment'] ?? ''));
238  return ($fragment && ‪MathUtility::canBeInterpretedAsInteger($fragment) ? 'c' : '') . $fragment;
239  }
240 
255  protected function ‪calculateQueryParameters(array &$conf, array $linkDetails): array
256  {
257  if (isset($linkDetails['parameters'])) {
258  $conf['additionalParams'] .= '&' . ltrim($linkDetails['parameters'], '&');
259  }
260 
261  $queryParameters = [];
262  $addQueryParams = ($conf['addQueryString'] ?? false) ? $this->‪getQueryArguments($conf['addQueryString'], $conf['addQueryString.'] ?? []) : '';
263  $addQueryParams .= trim((string)$this->contentObjectRenderer->stdWrapValue('additionalParams', $conf));
264  if ($addQueryParams === '&' || ($addQueryParams[0] ?? '') !== '&') {
265  $addQueryParams = '';
266  }
267  parse_str($addQueryParams, $queryParameters);
268  $linkVars = $this->‪calculateGlobalQueryParameters();
269  if ($linkVars !== '') {
270  $globalQueryParameters = [];
271  parse_str($linkVars, $globalQueryParameters);
272  $queryParameters = array_replace_recursive($globalQueryParameters, $queryParameters);
273  }
274  // Disable "?id=", for pages with no site configuration, this is added later-on anyway
275  unset($queryParameters['id']);
276  if ($linkDetails['pagetype'] ?? '') {
277  $queryParameters['type'] = $linkDetails['pagetype'];
278  }
279  $conf['no_cache'] = (string)$this->contentObjectRenderer->stdWrapValue('no_cache', $conf);
280  if ($conf['no_cache'] ?? false) {
281  $queryParameters['no_cache'] = 1;
282  }
283  // Override language property if not being set already, supporting historically 'L' and
284  // modern '_language' arguments, giving '_language' the precedence.
285  if (isset($queryParameters['_language'])) {
286  if (!isset($conf['language'])) {
287  $conf['language'] = $queryParameters['_language'];
288  }
289  unset($queryParameters['_language']);
290  }
291  if (isset($queryParameters['L'])) {
292  if (!isset($conf['language'])) {
293  $conf['language'] = $queryParameters['L'];
294  }
295  unset($queryParameters['L']);
296  }
297  return $queryParameters;
298  }
299 
305  protected function ‪calculateGlobalQueryParameters(): string
306  {
307  // @todo: The link builders should get the request hand over explicitly.
308  $request = $this->contentObjectRenderer->getRequest();
309  $requestQueryParams = $request->getQueryParams();
310  $frontendTypoScriptConfigArray = $request->getAttribute('frontend.typoscript')?->getConfigArray();
311  $typoScriptConfigLinkVars = (string)($frontendTypoScriptConfigArray['linkVars'] ?? '');
312  return GeneralUtility::makeInstance(LinkVarsCalculator::class)
313  ->getAllowedLinkVarsFromRequest(
314  $typoScriptConfigLinkVars,
315  $requestQueryParams,
316  GeneralUtility::makeInstance(Context::class)
317  );
318  }
319 
323  protected function ‪calculateMountPointParameters(array &$page, bool $disableGroupAccessCheck, string $linkText): ?string
324  {
325  // MountPoints, look for closest MPvar:
326  $mountPointPairs = [];
327  $frontendTypoScriptConfigArray = $this->contentObjectRenderer->getRequest()->getAttribute('frontend.typoscript')?->getConfigArray();
328  if (!($frontendTypoScriptConfigArray['MP_disableTypolinkClosestMPvalue'] ?? false)) {
329  $temp_MP = $this->‪getClosestMountPointValueForPage((int)$page['uid']);
330  if ($temp_MP) {
331  $mountPointPairs['closest'] = $temp_MP;
332  }
333  }
334  $pageRepository = GeneralUtility::makeInstance(PageRepository::class);
335  // Look for overlay Mount Point:
336  $mount_info = $pageRepository->getMountPointInfo($page['uid'], $page);
337  if (is_array($mount_info) && $mount_info['overlay']) {
338  $page = $pageRepository->getPage((int)$mount_info['mount_pid'], $disableGroupAccessCheck);
339  if (empty($page)) {
340  throw new ‪UnableToLinkException('Mount point "' . $mount_info['mount_pid'] . '" was not available, so "' . $linkText . '" was not linked.', 1490987337, null, $linkText);
341  }
342  $mountPointPairs['re-map'] = $mount_info['MPvar'];
343  }
344  // Mount pages are always local and never link to another domain,
345  $addMountPointParameters = !empty($mountPointPairs);
346  // Add "&MP" var, only if the original page was NOT a shortcut to another domain
347  if ($addMountPointParameters && !empty($page['_SHORTCUT_ORIGINAL_PAGE_UID'])) {
348  $siteOfTargetPage = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId((int)$page['_SHORTCUT_ORIGINAL_PAGE_UID']);
349  $currentSite = $this->‪getCurrentSite();
350  if ($siteOfTargetPage !== $currentSite) {
351  $addMountPointParameters = false;
352  }
353  }
354  if ($addMountPointParameters) {
355  return rawurlencode(implode(',', $mountPointPairs));
356  }
357  return null;
358  }
359 
363  protected function ‪calculateTargetAttribute(array $page, array $conf, bool $treatAsExternalLink, string $target): string
364  {
365  if ($treatAsExternalLink) {
366  $target = $target ?: $this->‪resolveTargetAttribute($conf, 'extTarget');
367  } else {
368  $target = (isset($page['target']) && trim($page['target'])) ? $page['target'] : $target;
369  if (empty($target)) {
370  $target = $this->‪resolveTargetAttribute($conf, 'target');
371  }
372  }
373  return $target;
374  }
375 
384  protected function ‪shouldModifyUrlForAccessRestrictedPage(array $conf, array $page): bool
385  {
386  $frontendTypoScriptConfigArray = $this->contentObjectRenderer->getRequest()->getAttribute('frontend.typoscript')?->getConfigArray();
387  $typolinkLinkAccessRestrictedPages = $frontendTypoScriptConfigArray['typolinkLinkAccessRestrictedPages'] ?? false;
388  return empty($conf['linkAccessRestrictedPages'])
389  && $typolinkLinkAccessRestrictedPages
390  && $typolinkLinkAccessRestrictedPages !== 'NONE'
391  && !GeneralUtility::makeInstance(RecordAccessVoter::class)->groupAccessGranted('pages', $page, GeneralUtility::makeInstance(Context::class));
392  }
393 
399  protected function ‪modifyUrlForAccessRestrictedPage(string ‪$url, array $page, string $overridePageType): string
400  {
401  $frontendTypoScriptConfigArray = $this->contentObjectRenderer->getRequest()->getAttribute('frontend.typoscript')?->getConfigArray();
402  $pageRepository = GeneralUtility::makeInstance(PageRepository::class);
403  $thePage = $pageRepository->getPage((int)($frontendTypoScriptConfigArray ['typolinkLinkAccessRestrictedPages'] ?? 0));
404  $addParams = str_replace(
405  [
406  '###RETURN_URL###',
407  '###PAGE_ID###',
408  ],
409  [
410  rawurlencode(‪$url),
411  $page['uid'],
412  ],
413  $frontendTypoScriptConfigArray['typolinkLinkAccessRestrictedPages_addParams'] ?? ''
414  );
415  return $this->contentObjectRenderer->createUrl(
416  [
417  'parameter' => $thePage['uid'] . ($overridePageType ? ',' . $overridePageType : ''),
418  'additionalParams' => $addParams,
419  ]
420  );
421  }
422 
428  protected function ‪resolvePage(array &$linkDetails, array &$configuration, bool $disableGroupAccessCheck): array
429  {
430  $pageRepository = $this->‪buildPageRepository();
431  // Looking up the page record to verify its existence
432  // This is used when a page to a translated page is executed directly.
433 
434  if (isset($configuration['page']) && $configuration['page'] instanceof ‪Page) {
435  $page = $configuration['page']->getTranslationSource()?->toArray() ?? $configuration['page']->toArray();
436  }
437  // A page with doktype external and ?showModal=1 in url field leads to recursion in HMENU/Sitemap.
438  // In the second call of this function $linkDetails['pageuid'] is different (=current page) to uid of Page
439  // object, and it needs to be fetched again.
440  if (($page['uid'] ?? false) !== $linkDetails['pageuid']) {
441  $page = $pageRepository->getPage((int)$linkDetails['pageuid'], $disableGroupAccessCheck);
442  }
443 
444  if (empty($page) || !is_array($page)) {
445  return [];
446  }
447 
448  // If the page repository (= current page) does actually link to a different page
449  // It is needed to also resolve the page translation now, as it might have a different shortcut
450  // page
451  if (isset($configuration['language']) && $configuration['language'] !== 'current') {
452  $page = $pageRepository->getLanguageOverlay('pages', $page, new ‪LanguageAspect((int)$configuration['language'], (int)$configuration['language']));
453  }
454 
455  $page = $this->‪resolveShortcutPage($page, $pageRepository, $disableGroupAccessCheck);
456 
457  $languageField = ‪$GLOBALS['TCA']['pages']['ctrl']['languageField'] ?? null;
458  $languageParentField = ‪$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] ?? null;
459  $language = (int)($page[$languageField] ?? 0);
460 
461  // The page that should be linked is actually a default-language page, nothing to do here.
462  if ($language === 0 || empty($page[$languageParentField])) {
463  return $page;
464  }
465 
466  // Let's fetch the default-language page now
467  $languageParentPage = $pageRepository->getPage(
468  (int)$page[$languageParentField],
469  $disableGroupAccessCheck
470  );
471  if (empty($languageParentPage)) {
472  return $page;
473  }
474  // Check for the shortcut of the default-language page
475  $languageParentPage = $this->‪resolveShortcutPage($languageParentPage, $pageRepository, $disableGroupAccessCheck);
476 
477  // Set the "pageuid" to the default-language page ID.
478  $linkDetails['pageuid'] = (int)$languageParentPage['uid'];
479  $configuration['language'] = $language;
480  return $languageParentPage;
481  }
482 
486  protected function ‪resolveShortcutPage(array $page, ‪PageRepository $pageRepository, bool $disableGroupAccessCheck): array
487  {
488  try {
489  $page = $pageRepository->‪resolveShortcutPage($page, false, $disableGroupAccessCheck);
490  } catch (\‪Exception $e) {
491  // Keep the existing page record if shortcut could not be resolved
492  }
493  return $page;
494  }
495 
502  protected function ‪getSiteLanguageOfTargetPage(‪Site $siteOfTargetPage, string $targetLanguageId): ‪SiteLanguage
503  {
504  $currentSite = $this->‪getCurrentSite();
505  $currentSiteLanguage = $this->‪getCurrentSiteLanguage() ?? $currentSite?->getDefaultLanguage();
506 
507  if ($targetLanguageId === 'current') {
508  $targetLanguageId = $currentSiteLanguage?->getLanguageId() ?? 0;
509  } else {
510  $targetLanguageId = (int)$targetLanguageId;
511  }
512  try {
513  $siteLanguageOfTargetPage = $siteOfTargetPage->‪getLanguageById($targetLanguageId);
514  } catch (\InvalidArgumentException $e) {
515  throw new ‪UnableToLinkException('The target page does not have a language with ID ' . $targetLanguageId . ' configured in its site configuration.', 1535477406);
516  }
517  return $siteLanguageOfTargetPage;
518  }
519 
525  protected function ‪generateUrlForPageWithSiteConfiguration(array $page, ‪Site $siteOfTargetPage, array $queryParameters, string $fragment, array $conf): UriInterface
526  {
527  $currentSite = $this->‪getCurrentSite();
528  $currentSiteLanguage = $this->‪getCurrentSiteLanguage() ?? $currentSite?->getDefaultLanguage();
529  $siteLanguageOfTargetPage = $this->‪getSiteLanguageOfTargetPage($siteOfTargetPage, (string)($conf['language'] ?? 'current'));
530  $frontendTypoScriptConfigArray = $this->contentObjectRenderer->getRequest()->getAttribute('frontend.typoscript')?->getConfigArray();
531 
532  // By default, it is assumed to ab an internal link or current domain's linking scheme should be used
533  // Use the config option to override this.
534  // Global option config.forceAbsoluteUrls = 1 overrides any setting for this specific link
535  if ($frontendTypoScriptConfigArray['forceAbsoluteUrls'] ?? false) {
536  $useAbsoluteUrl = true;
537  } else {
538  $useAbsoluteUrl = $conf['forceAbsoluteUrl'] ?? false;
539  }
540  // Check if the current page equal to the site of the target page, now only set the absolute URL
541  // Always generate absolute URLs if no current site is set
542  if (
543  !$currentSite
544  || $currentSite->getRootPageId() !== $siteOfTargetPage->‪getRootPageId()
545  || $siteLanguageOfTargetPage->getBase()->getHost() !== $currentSiteLanguage?->getBase()?->getHost()) {
546  $useAbsoluteUrl = true;
547  }
548 
549  $targetPageId = (int)($page['l10n_parent'] > 0 ? $page['l10n_parent'] : $page['uid']);
550  $queryParameters['_language'] = $siteLanguageOfTargetPage;
551  $pageObject = new ‪Page($page);
552 
553  if ($fragment
554  && $useAbsoluteUrl === false
555  && $currentSiteLanguage === $siteLanguageOfTargetPage
556  && $targetPageId === ($this->contentObjectRenderer->getRequest()->getAttribute('frontend.page.information')?->getId() ?? 0)
557  && (empty($conf['addQueryString']) || !isset($conf['addQueryString.']))
558  && !($frontendTypoScriptConfigArray['baseURL'] ?? false)
559  && count($queryParameters) === 1 // _language is always set
560  ) {
561  $uri = (new ‪Uri())->withFragment($fragment);
562  } else {
563  try {
564  $uri = $siteOfTargetPage->‪getRouter()->generateUri(
565  $pageObject,
566  $queryParameters,
567  $fragment,
569  );
570  } catch (‪InvalidRouteArgumentsException $e) {
571  throw new ‪UnableToLinkException('The target page could not be linked. Error: ' . $e->getMessage(), 1535472406);
572  }
573  // Override scheme if absoluteUrl is set, but only if the site defines a domain/host. Fall back to site scheme and else https.
574  if ($useAbsoluteUrl && $uri->getHost()) {
575  $scheme = $conf['forceAbsoluteUrl.']['scheme'] ?? false;
576  if (!$scheme) {
577  $scheme = $uri->getScheme() ?: 'https';
578  }
579  $uri = $uri->withScheme($scheme);
580  }
581  }
582 
583  return $uri;
584  }
585 
592  protected function ‪getClosestMountPointValueForPage(int $pageId): string
593  {
594  $request = $this->contentObjectRenderer->getRequest();
595  $mountPoint = $request->getAttribute('frontend.page.information')?->getMountPoint() ?? '';
596  if (empty(‪$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) || !$mountPoint) {
597  return '';
598  }
599  // Same page as current.
600  if (($request->getAttribute('frontend.page.information')?->getId() ?? 0) === $pageId) {
601  return $mountPoint;
602  }
603 
604  // Find the closest mount point
605  // Gets rootline of linked-to page
606  try {
607  $tCR_rootline = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get();
608  } catch (‪RootLineException) {
609  $tCR_rootline = [];
610  }
611  $localRootLine = $request->getAttribute('frontend.page.information')?->getLocalRootLine() ?? [];
612  $inverseLocalRootLine = array_reverse($localRootLine);
613  $rl_mpArray = [];
614  $startMPaccu = false;
615  // Traverse root line of link uid and inside of that the REAL root line of current position.
616  foreach ($tCR_rootline as $tCR_data) {
617  foreach ($inverseLocalRootLine as $rlKey => $invTmplRLRec) {
618  // Force accumulating when in overlay mode: Links to this page have to stay within the current branch
619  if (($invTmplRLRec['_MOUNT_OL'] ?? false) && (int)$tCR_data['uid'] === (int)$invTmplRLRec['uid']) {
620  $startMPaccu = true;
621  }
622  // Accumulate MP data:
623  if ($startMPaccu && ($invTmplRLRec['_MP_PARAM'] ?? false)) {
624  $rl_mpArray[] = $invTmplRLRec['_MP_PARAM'];
625  }
626  // If two PIDs matches and this is NOT the site root, start accumulation of MP data (on the next level):
627  // (The check for site root is done so links to branches outside the site but sharing the site roots PID
628  // is NOT detected as within the branch!)
629  if ((int)$tCR_data['pid'] === (int)$invTmplRLRec['pid'] && count($inverseLocalRootLine) !== $rlKey + 1) {
630  $startMPaccu = true;
631  }
632  }
633  if ($startMPaccu) {
634  // Good enough...
635  break;
636  }
637  }
638  return !empty($rl_mpArray) ? implode(',', array_reverse($rl_mpArray)) : '';
639  }
640 
647  public function ‪getMountPointParameterFromRootPointMaps(int $pageId): string
648  {
649  // Create map if not found already
650  $frontendTypoScriptConfigArray = $this->contentObjectRenderer->getRequest()->getAttribute('frontend.typoscript')?->getConfigArray();
651  $mountPointMap = $this->‪initializeMountPointMap(
652  !empty($frontendTypoScriptConfigArray['MP_defaults']) ? $frontendTypoScriptConfigArray['MP_defaults'] : '',
653  !empty($frontendTypoScriptConfigArray['MP_mapRootPoints']) ? $frontendTypoScriptConfigArray['MP_mapRootPoints'] : ''
654  );
655  // Finding MP var for Page ID:
656  if (!empty($mountPointMap[$pageId])) {
657  return implode(',', $mountPointMap[$pageId]);
658  }
659  return '';
660  }
661 
668  protected function ‪initializeMountPointMap(string $defaultMountPoints = '', string $mapRootPointList = ''): array
669  {
670  $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
671  $mountPointMap = $runtimeCache->get('pageLinkBuilderMountPointMap') ?: [];
672  if (!empty($mountPointMap) || (empty($mapRootPointList) && empty($defaultMountPoints))) {
673  return $mountPointMap;
674  }
675  if ($defaultMountPoints) {
676  $defaultMountPoints = ‪GeneralUtility::trimExplode('|', $defaultMountPoints, true);
677  foreach ($defaultMountPoints as $temp_p) {
678  [$temp_idP, $temp_MPp] = explode(':', $temp_p, 2);
679  $temp_ids = ‪GeneralUtility::intExplode(',', $temp_idP);
680  foreach ($temp_ids as $temp_id) {
681  $mountPointMap[$temp_id] = trim($temp_MPp);
682  }
683  }
684  }
685 
686  $rootPoints = ‪GeneralUtility::trimExplode(',', strtolower($mapRootPointList), true);
687  $pageInformation = $this->contentObjectRenderer->getRequest()->getAttribute('frontend.page.information');
688  // Traverse rootpoints
689  foreach ($rootPoints as $p) {
690  $initMParray = [];
691  if ($p === 'root') {
692  $rootPage = $pageInformation->getLocalRootLine()[0];
693  $p = $rootPage['uid'];
694  if (($rootPage['_MOUNT_OL'] ?? false) && ($rootPage['_MP_PARAM'] ?? false)) {
695  $initMParray[] = $rootPage['_MP_PARAM'];
696  }
697  }
698  $this->‪populateMountPointMapForPageRecursively($mountPointMap, (int)$p, $initMParray);
699  }
700  $runtimeCache->set('pageLinkBuilderMountPointMap', $mountPointMap);
701  return $mountPointMap;
702  }
703 
713  protected function ‪populateMountPointMapForPageRecursively(array &$mountPointMap, int $id, array $MP_array = [], int $level = 0): void
714  {
715  if ($id <= 0) {
716  return;
717  }
718  $pageRepository = GeneralUtility::makeInstance(PageRepository::class);
719  // First level, check id
720  if (!$level) {
721  // Find mount point if any:
722  $mount_info = $pageRepository->getMountPointInfo($id);
723  // Overlay mode:
724  if (is_array($mount_info) && $mount_info['overlay']) {
725  $MP_array[] = $mount_info['MPvar'];
726  $id = $mount_info['mount_pid'];
727  }
728  // Set mapping information for this level:
729  $mountPointMap[$id] = $MP_array;
730  // Normal mode:
731  if (is_array($mount_info) && !$mount_info['overlay']) {
732  $MP_array[] = $mount_info['MPvar'];
733  $id = $mount_info['mount_pid'];
734  }
735  }
736  if ($id && $level < 20) {
737  $nextLevelAcc = [];
738  // Select and traverse current level pages:
739  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
740  $queryBuilder->getRestrictions()
741  ->removeAll()
742  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
743  $queryResult = $queryBuilder
744  ->select('uid', 'pid', 'doktype', 'mount_pid', 'mount_pid_ol', 't3ver_state', 'l10n_parent')
745  ->from('pages')
746  ->where(
747  $queryBuilder->expr()->eq(
748  'pid',
749  $queryBuilder->createNamedParameter($id, ‪Connection::PARAM_INT)
750  ),
751  $queryBuilder->expr()->neq(
752  'doktype',
753  $queryBuilder->createNamedParameter(‪PageRepository::DOKTYPE_BE_USER_SECTION, ‪Connection::PARAM_INT)
754  )
755  )->executeQuery();
756  while ($row = $queryResult->fetchAssociative()) {
757  // Find mount point if any:
758  $next_id = (int)$row['uid'];
759  $next_MP_array = $MP_array;
760  $mount_info = $pageRepository->getMountPointInfo($next_id, $row);
761  // Overlay mode:
762  if (is_array($mount_info) && $mount_info['overlay']) {
763  $next_MP_array[] = $mount_info['MPvar'];
764  $next_id = (int)$mount_info['mount_pid'];
765  }
766  if (!isset($mountPointMap[$next_id])) {
767  // Set mapping information for this level:
768  $mountPointMap[$next_id] = $next_MP_array;
769  // Normal mode:
770  if (is_array($mount_info) && !$mount_info['overlay']) {
771  $next_MP_array[] = $mount_info['MPvar'];
772  $next_id = (int)$mount_info['mount_pid'];
773  }
774  // Register recursive call
775  // (have to do it this way since ALL of the current level should be registered BEFORE the sublevel at any time)
776  $nextLevelAcc[] = [$next_id, $next_MP_array];
777  }
778  }
779  // Call recursively, if any:
780  foreach ($nextLevelAcc as $pSet) {
781  $this->‪populateMountPointMapForPageRecursively($mountPointMap, $pSet[0], $pSet[1], $level + 1);
782  }
783  }
784  }
785 
797  protected function ‪getQueryArguments(bool|string|int $queryInformation, array $configuration): string
798  {
799  if (!$queryInformation || $queryInformation === 'false') {
800  return '';
801  }
802  $request = $this->contentObjectRenderer->getRequest();
803  $pageArguments = $request->getAttribute('routing');
804  if (!$pageArguments instanceof ‪PageArguments) {
805  return '';
806  }
807  $currentQueryArray = $pageArguments->getRouteArguments();
808  if ($queryInformation === 'untrusted') {
809  $currentQueryArray = array_replace_recursive($pageArguments->getQueryArguments(), $currentQueryArray);
810  }
811  if ($configuration['exclude'] ?? false) {
812  $excludeItems = array_map(urlencode(...), ‪GeneralUtility::trimExplode(',', $configuration['exclude']));
813  $excludeString = implode('&', $excludeItems);
814  parse_str($excludeString, $excludedQueryParts);
815  $newQueryArray = ArrayUtility::arrayDiffKeyRecursive($currentQueryArray, $excludedQueryParts);
816  } else {
817  $newQueryArray = $currentQueryArray;
818  }
819  return ‪HttpUtility::buildQueryString($newQueryArray, '&');
820  }
821 
829  protected function ‪getCurrentSite(): ?‪SiteInterface
830  {
831  return $this->contentObjectRenderer->getRequest()->getAttribute('site');
832  }
833 
839  {
840  $request = $this->contentObjectRenderer->getRequest();
841  return $request->getAttribute('language') ?? $this->‪getCurrentSite()?->getDefaultLanguage();
842  }
843 
848  protected function ‪buildPageRepository(‪LanguageAspect $languageAspect = null): ‪PageRepository
849  {
850  // clone global context object (singleton)
851  $context = clone GeneralUtility::makeInstance(Context::class);
852  $context->setAspect(
853  'language',
854  $languageAspect ?? GeneralUtility::makeInstance(LanguageAspect::class)
855  );
856  return GeneralUtility::makeInstance(
857  PageRepository::class,
858  $context
859  );
860  }
861 }
‪TYPO3\CMS\Core\Domain\Page\getPageId
‪getPageId()
Definition: Page.php:60
‪TYPO3\CMS\Core\Site\Entity\SiteInterface
Definition: SiteInterface.php:26
‪TYPO3\CMS\Core\Domain\Page
Definition: Page.php:24
‪TYPO3\CMS\Core\Routing\PageArguments
Definition: PageArguments.php:26
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Core\Context\LanguageAspectFactory
Definition: LanguageAspectFactory.php:27
‪TYPO3\CMS\Core\Routing\RouterInterface
Definition: RouterInterface.php:28
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\resolveShortcutPage
‪resolveShortcutPage(array $page, bool $resolveRandomSubpages=false, bool $disableGroupAccessCheck=false)
Definition: PageRepository.php:1134
‪TYPO3\CMS\Core\Context\LanguageAspectFactory\createFromSiteLanguage
‪static createFromSiteLanguage(SiteLanguage $language)
Definition: LanguageAspectFactory.php:31
‪TYPO3\CMS\Core\Utility\RootlineUtility
Definition: RootlineUtility.php:40
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_LINK
‪const DOKTYPE_LINK
Definition: PageRepository.php:99
‪TYPO3\CMS\Core\Exception\SiteNotFoundException
Definition: SiteNotFoundException.php:25
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility
Definition: PageTranslationVisibility.php:30
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Core\Http\Uri
Definition: Uri.php:30
‪TYPO3\CMS\Core\Site\Entity\Site
Definition: Site.php:42
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage
Definition: SiteLanguage.php:27
‪TYPO3\CMS\Core\Site\Entity\Site\getRootPageId
‪getRootPageId()
Definition: Site.php:201
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Frontend\Exception
Definition: Exception.php:23
‪TYPO3\CMS\Core\Utility\HttpUtility\buildQueryString
‪static string buildQueryString(array $parameters, string $prependCharacter='', bool $skipEmptyParameters=false)
Definition: HttpUtility.php:124
‪TYPO3\CMS\Core\Routing\RouterInterface\ABSOLUTE_PATH
‪const ABSOLUTE_PATH
Definition: RouterInterface.php:37
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_BE_USER_SECTION
‪const DOKTYPE_BE_USER_SECTION
Definition: PageRepository.php:101
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:36
‪TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException
Definition: InvalidRouteArgumentsException.php:25
‪TYPO3\CMS\Core\Context\LanguageAspect
Definition: LanguageAspect.php:57
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪TYPO3\CMS\Webhooks\Message\$url
‪identifier readonly UriInterface $url
Definition: LoginErrorOccurredMessage.php:36
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:26
‪TYPO3\CMS\Core\Site\Entity\Site\getLanguageById
‪getLanguageById(int $languageId)
Definition: Site.php:247
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Core\Exception\Page\RootLineException
Definition: RootLineException.php:24
‪TYPO3\CMS\Core\Utility\HttpUtility
Definition: HttpUtility.php:24
‪TYPO3\CMS\Core\Domain\Repository\PageRepository
Definition: PageRepository.php:69
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Routing\RouterInterface\ABSOLUTE_URL
‪const ABSOLUTE_URL
Definition: RouterInterface.php:32
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Utility\GeneralUtility\intExplode
‪static list< int > intExplode(string $delimiter, string $string, bool $removeEmptyValues=false)
Definition: GeneralUtility.php:756
‪TYPO3\CMS\Core\Domain\Access\RecordAccessVoter
Definition: RecordAccessVoter.php:29
‪TYPO3\CMS\Core\Site\Entity\Site\getRouter
‪getRouter(Context $context=null)
Definition: Site.php:389
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822