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