TYPO3 CMS  TYPO3_8-7
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 
23 
28 {
32  public function build(array &$linkDetails, string $linkText, string $target, array $conf): array
33  {
34  $tsfe = $this->getTypoScriptFrontendController();
35  // Checking if the id-parameter is an alias.
36  if (!empty($linkDetails['pagealias'])) {
37  $linkDetails['pageuid'] = $tsfe->sys_page->getPageIdFromAlias($linkDetails['pagealias']);
38  } elseif (empty($linkDetails['pageuid']) || $linkDetails['pageuid'] === 'current') {
39  // If no id or alias is given
40  $linkDetails['pageuid'] = $tsfe->id;
41  }
42 
43  // Link to page even if access is missing?
44  if (isset($conf['linkAccessRestrictedPages'])) {
45  $disableGroupAccessCheck = (bool)$conf['linkAccessRestrictedPages'];
46  } else {
47  $disableGroupAccessCheck = (bool)$tsfe->config['config']['typolinkLinkAccessRestrictedPages'];
48  }
49 
50  // Looking up the page record to verify its existence:
51  $page = $tsfe->sys_page->getPage($linkDetails['pageuid'], $disableGroupAccessCheck);
52 
53  if (empty($page)) {
54  throw new UnableToLinkException('Page id "' . $linkDetails['typoLinkParameter'] . '" was not found, so "' . $linkText . '" was not linked.', 1490987336, null, $linkText);
55  }
56  $language = empty($page['_PAGES_OVERLAY']) ? 0 : $page['_PAGES_OVERLAY_LANGUAGE'];
57  if ($language === 0 && GeneralUtility::hideIfDefaultLanguage($page['l18n_cfg'])) {
58  throw new UnableToLinkException('Default language of page "' . $linkDetails['typoLinkParameter'] . '" is hidden, so "' . $linkText . '" was not linked.', 1529527301, null, $linkText);
59  }
60  if ($language > 0 && !isset($page['_PAGES_OVERLAY']) && GeneralUtility::hideIfNotTranslated($page['l18n_cfg'])) {
61  throw new UnableToLinkException('Fallback to default language of page "' . $linkDetails['typoLinkParameter'] . '" is disabled, so "' . $linkText . '" was not linked.', 1529527488, null, $linkText);
62  }
63 
64  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typolinkProcessing']['typolinkModifyParameterForPageLinks'])) {
65  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typolinkProcessing']['typolinkModifyParameterForPageLinks'] as $classData) {
66  $hookObject = GeneralUtility::makeInstance($classData);
67  if (!$hookObject instanceof TypolinkModifyLinkConfigForPageLinksHookInterface) {
68  throw new \UnexpectedValueException('$hookObject must implement interface ' . TypolinkModifyLinkConfigForPageLinksHookInterface::class, 1483114905);
69  }
71  $conf = $hookObject->modifyPageLinkConfiguration($conf, $linkDetails, $page);
72  }
73  }
74  $enableLinksAcrossDomains = $tsfe->config['config']['typolinkEnableLinksAcrossDomains'];
75  if ($conf['no_cache.']) {
76  $conf['no_cache'] = (string)$this->contentObjectRenderer->stdWrap($conf['no_cache'], $conf['no_cache.']);
77  }
78 
79  $sectionMark = trim(isset($conf['section.']) ? (string)$this->contentObjectRenderer->stdWrap($conf['section'], $conf['section.']) : (string)$conf['section']);
80  if ($sectionMark === '' && isset($linkDetails['fragment'])) {
81  $sectionMark = $linkDetails['fragment'];
82  }
83  if ($sectionMark !== '') {
84  $sectionMark = '#' . (MathUtility::canBeInterpretedAsInteger($sectionMark) ? 'c' : '') . $sectionMark;
85  }
86  // Overruling 'type'
87  $pageType = $linkDetails['pagetype'] ?? '';
88 
89  if (isset($linkDetails['parameters'])) {
90  $conf['additionalParams'] .= '&' . ltrim($linkDetails['parameters'], '&');
91  }
92  // MointPoints, look for closest MPvar:
93  $MPvarAcc = [];
94  if (!$tsfe->config['config']['MP_disableTypolinkClosestMPvalue']) {
95  $temp_MP = $this->getClosestMountPointValueForPage($page['uid']);
96  if ($temp_MP) {
97  $MPvarAcc['closest'] = $temp_MP;
98  }
99  }
100  // Look for overlay Mount Point:
101  $mount_info = $tsfe->sys_page->getMountPointInfo($page['uid'], $page);
102  if (is_array($mount_info) && $mount_info['overlay']) {
103  $page = $tsfe->sys_page->getPage($mount_info['mount_pid'], $disableGroupAccessCheck);
104  if (empty($page)) {
105  throw new UnableToLinkException('Mount point "' . $mount_info['mount_pid'] . '" was not available, so "' . $linkText . '" was not linked.', 1490987337, null, $linkText);
106  }
107  $MPvarAcc['re-map'] = $mount_info['MPvar'];
108  }
109  // Setting title if blank value to link
110  $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $page['title']);
111  // Query Params:
112  $addQueryParams = $conf['addQueryString'] ? $this->contentObjectRenderer->getQueryArguments($conf['addQueryString.']) : '';
113  $addQueryParams .= isset($conf['additionalParams.']) ? trim((string)$this->contentObjectRenderer->stdWrap($conf['additionalParams'], $conf['additionalParams.'])) : trim((string)$conf['additionalParams']);
114  if ($addQueryParams === '&' || $addQueryParams[0] !== '&') {
115  $addQueryParams = '';
116  }
117  $targetDomain = '';
118  $currentDomain = (string)GeneralUtility::getIndpEnv('HTTP_HOST');
119  // Mount pages are always local and never link to another domain
120  if (!empty($MPvarAcc)) {
121  // Add "&MP" var:
122  $addQueryParams .= '&MP=' . rawurlencode(implode(',', $MPvarAcc));
123  } elseif (strpos($addQueryParams, '&MP=') === false && $tsfe->config['config']['typolinkCheckRootline']) {
124  // We do not come here if additionalParams had '&MP='. This happens when typoLink is called from
125  // menu. Mount points always work in the content of the current domain and we must not change
126  // domain if MP variables exist.
127  // If we link across domains and page is free type shortcut, we must resolve the shortcut first!
128  // If we do not do it, TYPO3 will fail to (1) link proper page in RealURL/CoolURI because
129  // they return relative links and (2) show proper page if no RealURL/CoolURI exists when link is clicked
130  if ($enableLinksAcrossDomains
131  && (int)$page['doktype'] === PageRepository::DOKTYPE_SHORTCUT
132  && (int)$page['shortcut_mode'] === PageRepository::SHORTCUT_MODE_NONE
133  ) {
134  // Save in case of broken destination or endless loop
135  $page2 = $page;
136  // Same as in RealURL, seems enough
137  $maxLoopCount = 20;
138  while ($maxLoopCount
139  && is_array($page)
140  && (int)$page['doktype'] === PageRepository::DOKTYPE_SHORTCUT
141  && (int)$page['shortcut_mode'] === PageRepository::SHORTCUT_MODE_NONE
142  ) {
143  $page = $tsfe->sys_page->getPage($page['shortcut'], $disableGroupAccessCheck);
144  $maxLoopCount--;
145  }
146  if (empty($page) || $maxLoopCount === 0) {
147  // We revert if shortcut is broken or maximum number of loops is exceeded (indicates endless loop)
148  $page = $page2;
149  }
150  }
151 
152  $targetDomain = $tsfe->getDomainNameForPid($page['uid']);
153  // Do not prepend the domain if it is the current hostname
154  if (!$targetDomain || $tsfe->domainNameMatchesCurrentRequest($targetDomain)) {
155  $targetDomain = '';
156  }
157  }
158  if ($conf['useCacheHash']) {
159  $params = $tsfe->linkVars . $addQueryParams . '&id=' . $page['uid'];
160  if (trim($params, '& ') !== '') {
161  $cacheHash = GeneralUtility::makeInstance(CacheHashCalculator::class);
162  $cHash = $cacheHash->generateForParameters($params);
163  $addQueryParams .= $cHash ? '&cHash=' . $cHash : '';
164  }
165  unset($params);
166  }
167  $absoluteUrlScheme = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https' : 'http';
168  // URL shall be absolute:
169  if (isset($conf['forceAbsoluteUrl']) && $conf['forceAbsoluteUrl']) {
170  // Override scheme:
171  if (isset($conf['forceAbsoluteUrl.']['scheme']) && $conf['forceAbsoluteUrl.']['scheme']) {
172  $absoluteUrlScheme = $conf['forceAbsoluteUrl.']['scheme'];
173  }
174  // If no domain records are defined, use current domain:
175  $currentUrlScheme = parse_url(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'), PHP_URL_SCHEME);
176  if ($targetDomain === '' && ($conf['forceAbsoluteUrl'] || $absoluteUrlScheme !== $currentUrlScheme)) {
177  $targetDomain = $currentDomain;
178  }
179  // If go for an absolute link, add site path if it's not taken care about by absRefPrefix
180  if (!$tsfe->config['config']['absRefPrefix'] && $targetDomain === $currentDomain) {
181  $targetDomain = $currentDomain . rtrim(GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'), '/');
182  }
183  }
184  // If target page has a different domain and the current domain's linking scheme (e.g. RealURL/...) should not be used
185  if ($targetDomain !== '' && $targetDomain !== $currentDomain && !$enableLinksAcrossDomains) {
186  $target = $target ?: $this->resolveTargetAttribute($conf, 'extTarget', false, $tsfe->extTarget);
187  $LD['target'] = $target;
188  // Convert IDNA-like domain (if any)
189  if (!preg_match('/^[a-z0-9.\\-]*$/i', $targetDomain)) {
190  $targetDomain = GeneralUtility::idnaEncode($targetDomain);
191  }
192  $url = $absoluteUrlScheme . '://' . $targetDomain . '/index.php?id=' . $page['uid'] . $addQueryParams . $sectionMark;
193  } else {
194  // Internal link or current domain's linking scheme should be used
195  // Internal target:
196  if (empty($target)) {
197  $target = $this->resolveTargetAttribute($conf, 'target', true, $tsfe->intTarget);
198  }
199  $LD = $tsfe->tmpl->linkData($page, $target, $conf['no_cache'], '', '', $addQueryParams, $pageType, $targetDomain);
200  if ($targetDomain !== '') {
201  // We will add domain only if URL does not have it already.
202  if ($enableLinksAcrossDomains && $targetDomain !== $currentDomain && isset($tsfe->config['config']['absRefPrefix'])) {
203  // Get rid of the absRefPrefix if necessary. absRefPrefix is applicable only
204  // to the current web site. If we have domain here it means we link across
205  // domains. absRefPrefix can contain domain name, which will screw up
206  // the link to the external domain.
207  $prefixLength = strlen($tsfe->config['config']['absRefPrefix']);
208  if (substr($LD['totalURL'], 0, $prefixLength) === $tsfe->config['config']['absRefPrefix']) {
209  $LD['totalURL'] = substr($LD['totalURL'], $prefixLength);
210  }
211  }
212  $urlParts = parse_url($LD['totalURL']);
213  if (empty($urlParts['host'])) {
214  $LD['totalURL'] = $absoluteUrlScheme . '://' . $targetDomain . ($LD['totalURL'][0] === '/' ? '' : '/') . $LD['totalURL'];
215  }
216  }
217  $url = $LD['totalURL'] . $sectionMark;
218  }
219  $target = $LD['target'];
220  // If sectionMark is set, there is no baseURL AND the current page is the page the link is to, check if there are any additional parameters or addQueryString parameters and if not, drop the url.
221  if ($sectionMark
222  && !$tsfe->config['config']['baseURL']
223  && (int)$page['uid'] === (int)$tsfe->id
224  && !trim($addQueryParams)
225  && (empty($conf['addQueryString']) || !isset($conf['addQueryString.']))
226  ) {
227  $currentQueryArray = GeneralUtility::explodeUrl2Array(GeneralUtility::getIndpEnv('QUERY_STRING'), true);
228  $currentQueryParams = GeneralUtility::implodeArrayForUrl('', $currentQueryArray, '', false, true);
229 
230  if (!trim($currentQueryParams)) {
231  list(, $URLparams) = explode('?', $url);
232  list($URLparams) = explode('#', (string)$URLparams);
233  parse_str($URLparams . $LD['orig_type'], $URLparamsArray);
234  // Type nums must match as well as page ids
235  if ((int)$URLparamsArray['type'] === (int)$tsfe->type) {
236  unset($URLparamsArray['id']);
237  unset($URLparamsArray['type']);
238  // If there are no parameters left.... set the new url.
239  if (empty($URLparamsArray)) {
240  $url = $sectionMark;
241  }
242  }
243  }
244  }
245 
246  // If link is to an access restricted page which should be redirected, then find new URL:
247  if (empty($conf['linkAccessRestrictedPages'])
248  && $tsfe->config['config']['typolinkLinkAccessRestrictedPages']
249  && $tsfe->config['config']['typolinkLinkAccessRestrictedPages'] !== 'NONE'
250  && !$tsfe->checkPageGroupAccess($page)
251  ) {
252  $thePage = $tsfe->sys_page->getPage($tsfe->config['config']['typolinkLinkAccessRestrictedPages']);
253  $addParams = str_replace(
254  [
255  '###RETURN_URL###',
256  '###PAGE_ID###'
257  ],
258  [
259  rawurlencode($url),
260  $page['uid']
261  ],
262  $tsfe->config['config']['typolinkLinkAccessRestrictedPages_addParams']
263  );
264  $url = $this->contentObjectRenderer->getTypoLink_URL($thePage['uid'] . ($pageType ? ',' . $pageType : ''), $addParams, $target);
265  $url = $this->forceAbsoluteUrl($url, $conf);
266  $this->contentObjectRenderer->lastTypoLinkLD['totalUrl'] = $url;
267  }
268 
269  return [$url, $linkText, $target];
270  }
271 
279  protected function getClosestMountPointValueForPage($pageId)
280  {
281  $tsfe = $this->getTypoScriptFrontendController();
282  if (empty($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) || !$tsfe->MP) {
283  return '';
284  }
285  // Same page as current.
286  if ((int)$tsfe->id === (int)$pageId) {
287  return $tsfe->MP;
288  }
289 
290  // Find closest mount point
291  // Gets rootline of linked-to page
292  $tCR_rootline = $tsfe->sys_page->getRootLine($pageId, '', true);
293  $inverseTmplRootline = array_reverse($tsfe->tmpl->rootLine);
294  $rl_mpArray = [];
295  $startMPaccu = false;
296  // Traverse root line of link uid and inside of that the REAL root line of current position.
297  foreach ($tCR_rootline as $tCR_data) {
298  foreach ($inverseTmplRootline as $rlKey => $invTmplRLRec) {
299  // Force accumulating when in overlay mode: Links to this page have to stay within the current branch
300  if ($invTmplRLRec['_MOUNT_OL'] && (int)$tCR_data['uid'] === (int)$invTmplRLRec['uid']) {
301  $startMPaccu = true;
302  }
303  // Accumulate MP data:
304  if ($startMPaccu && $invTmplRLRec['_MP_PARAM']) {
305  $rl_mpArray[] = $invTmplRLRec['_MP_PARAM'];
306  }
307  // If two PIDs matches and this is NOT the site root, start accumulation of MP data (on the next level):
308  // (The check for site root is done so links to branches outsite the site but sharing the site roots PID
309  // is NOT detected as within the branch!)
310  if ((int)$tCR_data['pid'] === (int)$invTmplRLRec['pid'] && count($inverseTmplRootline) !== $rlKey + 1) {
311  $startMPaccu = true;
312  }
313  }
314  if ($startMPaccu) {
315  // Good enough...
316  break;
317  }
318  }
319  return !empty($rl_mpArray) ? implode(',', array_reverse($rl_mpArray)) : '';
320  }
321 }
static hideIfDefaultLanguage($localizationConfiguration)
static makeInstance($className,... $constructorArguments)
static hideIfNotTranslated($l18n_cfg_fieldValue)
static implodeArrayForUrl($name, array $theArray, $str='', $skipBlank=false, $rawurlencodeParamName=false)
static explodeUrl2Array($string, $multidim=false)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']