Completed
Push — master ( a9a202...05ac62 )
by
unknown
76:11 queued 59:20
created

generateUrlForPageWithSiteConfiguration()   D

Complexity

Conditions 21
Paths 64

Size

Total Lines 59
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 21
eloc 36
c 1
b 0
f 0
nc 64
nop 5
dl 0
loc 59
rs 4.1666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
declare(strict_types = 1);
3
namespace TYPO3\CMS\Frontend\Typolink;
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
use Psr\Http\Message\ServerRequestInterface;
19
use Psr\Http\Message\UriInterface;
20
use TYPO3\CMS\Core\Cache\CacheManager;
21
use TYPO3\CMS\Core\Context\Context;
22
use TYPO3\CMS\Core\Context\LanguageAspect;
23
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
24
use TYPO3\CMS\Core\Database\ConnectionPool;
25
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
26
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
27
use TYPO3\CMS\Core\Exception\Page\RootLineException;
28
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
29
use TYPO3\CMS\Core\Http\Uri;
30
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
31
use TYPO3\CMS\Core\Routing\RouterInterface;
32
use TYPO3\CMS\Core\Site\Entity\Site;
33
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
34
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
35
use TYPO3\CMS\Core\Site\SiteFinder;
36
use TYPO3\CMS\Core\Utility\GeneralUtility;
37
use TYPO3\CMS\Core\Utility\MathUtility;
38
use TYPO3\CMS\Core\Utility\RootlineUtility;
39
use TYPO3\CMS\Frontend\ContentObject\TypolinkModifyLinkConfigForPageLinksHookInterface;
40
41
/**
42
 * Builds a TypoLink to a certain page
43
 */
44
class PageLinkBuilder extends AbstractTypolinkBuilder
45
{
46
    /**
47
     * @inheritdoc
48
     * @throws UnableToLinkException
49
     */
50
    public function build(array &$linkDetails, string $linkText, string $target, array $conf): array
51
    {
52
        $tsfe = $this->getTypoScriptFrontendController();
53
        if (empty($linkDetails['pageuid']) || $linkDetails['pageuid'] === 'current') {
54
            // If no id is given
55
            $linkDetails['pageuid'] = $tsfe->id;
56
        }
57
58
        // Link to page even if access is missing?
59
        if (isset($conf['linkAccessRestrictedPages'])) {
60
            $disableGroupAccessCheck = (bool)$conf['linkAccessRestrictedPages'];
61
        } else {
62
            $disableGroupAccessCheck = (bool)$tsfe->config['config']['typolinkLinkAccessRestrictedPages'];
63
        }
64
65
        // Looking up the page record to verify its existence:
66
        $page = $this->resolvePage($linkDetails, $conf, $disableGroupAccessCheck);
67
68
        if (empty($page)) {
69
            throw new UnableToLinkException('Page id "' . $linkDetails['typoLinkParameter'] . '" was not found, so "' . $linkText . '" was not linked.', 1490987336, null, $linkText);
70
        }
71
72
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typolinkProcessing']['typolinkModifyParameterForPageLinks'] ?? [] as $classData) {
73
            $hookObject = GeneralUtility::makeInstance($classData);
74
            if (!$hookObject instanceof TypolinkModifyLinkConfigForPageLinksHookInterface) {
75
                throw new \UnexpectedValueException('$hookObject must implement interface ' . TypolinkModifyLinkConfigForPageLinksHookInterface::class, 1483114905);
76
            }
77
            /** @var TypolinkModifyLinkConfigForPageLinksHookInterface $hookObject */
78
            $conf = $hookObject->modifyPageLinkConfiguration($conf, $linkDetails, $page);
79
        }
80
        if ($conf['no_cache.']) {
81
            $conf['no_cache'] = (string)$this->contentObjectRenderer->stdWrap($conf['no_cache'], $conf['no_cache.']);
82
        }
83
84
        $sectionMark = trim(isset($conf['section.']) ? (string)$this->contentObjectRenderer->stdWrap($conf['section'], $conf['section.']) : (string)$conf['section']);
85
        if ($sectionMark === '' && isset($linkDetails['fragment'])) {
86
            $sectionMark = $linkDetails['fragment'];
87
        }
88
        if ($sectionMark !== '') {
89
            $sectionMark = '#' . (MathUtility::canBeInterpretedAsInteger($sectionMark) ? 'c' : '') . $sectionMark;
90
        }
91
        // Overruling 'type'
92
        $pageType = $linkDetails['pagetype'] ?? '';
93
94
        if (isset($linkDetails['parameters'])) {
95
            $conf['additionalParams'] .= '&' . ltrim($linkDetails['parameters'], '&');
96
        }
97
        // MountPoints, look for closest MPvar:
98
        $MPvarAcc = [];
99
        if (!$tsfe->config['config']['MP_disableTypolinkClosestMPvalue']) {
100
            $temp_MP = $this->getClosestMountPointValueForPage($page['uid']);
101
            if ($temp_MP) {
102
                $MPvarAcc['closest'] = $temp_MP;
103
            }
104
        }
105
        // Look for overlay Mount Point:
106
        $mount_info = $tsfe->sys_page->getMountPointInfo($page['uid'], $page);
107
        if (is_array($mount_info) && $mount_info['overlay']) {
108
            $page = $tsfe->sys_page->getPage($mount_info['mount_pid'], $disableGroupAccessCheck);
109
            if (empty($page)) {
110
                throw new UnableToLinkException('Mount point "' . $mount_info['mount_pid'] . '" was not available, so "' . $linkText . '" was not linked.', 1490987337, null, $linkText);
111
            }
112
            $MPvarAcc['re-map'] = $mount_info['MPvar'];
113
        }
114
        // Query Params:
115
        $addQueryParams = $conf['addQueryString'] ? $this->contentObjectRenderer->getQueryArguments($conf['addQueryString.']) : '';
116
        $addQueryParams .= isset($conf['additionalParams.']) ? trim((string)$this->contentObjectRenderer->stdWrap($conf['additionalParams'], $conf['additionalParams.'])) : trim((string)$conf['additionalParams']);
117
        if ($addQueryParams === '&' || $addQueryParams[0] !== '&') {
118
            $addQueryParams = '';
119
        }
120
        // Mount pages are always local and never link to another domain
121
        if (!empty($MPvarAcc)) {
122
            // Add "&MP" var:
123
            $addQueryParams .= '&MP=' . rawurlencode(implode(',', $MPvarAcc));
124
        } elseif (strpos($addQueryParams, '&MP=') === false) {
125
            // We do not come here if additionalParams had '&MP='. This happens when typoLink is called from
126
            // menu. Mount points always work in the content of the current domain and we must not change
127
            // domain if MP variables exist.
128
            // If we link across domains and page is free type shortcut, we must resolve the shortcut first!
129
            if ((int)$page['doktype'] === PageRepository::DOKTYPE_SHORTCUT
130
                && (int)$page['shortcut_mode'] === PageRepository::SHORTCUT_MODE_NONE
131
            ) {
132
                // Save in case of broken destination or endless loop
133
                $page2 = $page;
134
                // Same as in RealURL, seems enough
135
                $maxLoopCount = 20;
136
                while ($maxLoopCount
137
                    && is_array($page)
138
                    && (int)$page['doktype'] === PageRepository::DOKTYPE_SHORTCUT
139
                    && (int)$page['shortcut_mode'] === PageRepository::SHORTCUT_MODE_NONE
140
                ) {
141
                    $page = $tsfe->sys_page->getPage($page['shortcut'], $disableGroupAccessCheck);
142
                    $maxLoopCount--;
143
                }
144
                if (empty($page) || $maxLoopCount === 0) {
145
                    // We revert if shortcut is broken or maximum number of loops is exceeded (indicates endless loop)
146
                    $page = $page2;
147
                }
148
            }
149
        }
150
151
        if (isset($conf['useCacheHash'])) {
152
            // This option will be removed in TYPO3 v11.0.
153
            trigger_error('Setting typolink.useCacheHash has no effect anymore. Remove the option in all your TypoScript code and Fluid templates.', E_USER_DEPRECATED);
154
        }
155
156
        // get config.linkVars and prepend them before the actual GET parameters
157
        $queryParameters = [];
158
        parse_str($addQueryParams, $queryParameters);
159
        if ($tsfe->linkVars) {
160
            $globalQueryParameters = [];
161
            parse_str($tsfe->linkVars, $globalQueryParameters);
162
            $queryParameters = array_replace_recursive($globalQueryParameters, $queryParameters);
163
        }
164
        // Disable "?id=", for pages with no site configuration, this is added later-on anyway
165
        unset($queryParameters['id']);
166
167
        // Override language property if not being set already
168
        if (isset($queryParameters['L']) && !isset($conf['language'])) {
169
            $conf['language'] = (int)$queryParameters['L'];
170
            unset($queryParameters['L']);
171
        }
172
173
        // Check if the target page has a site configuration
174
        try {
175
            $siteOfTargetPage = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId((int)$page['uid'], null, $queryParameters['MP'] ?? '');
176
            $currentSite = $this->getCurrentSite();
177
        } catch (SiteNotFoundException $e) {
178
            // Usually happens in tests, as sites with configuration should be available everywhere.
179
            $siteOfTargetPage = null;
180
            $currentSite = null;
181
        }
182
183
        // Link to a page that has a site configuration
184
        if ($siteOfTargetPage !== null) {
185
            $siteLanguageOfTargetPage = $this->getSiteLanguageOfTargetPage($siteOfTargetPage, (string)($conf['language'] ?? 'current'));
186
            $languageAspect = LanguageAspectFactory::createFromSiteLanguage($siteLanguageOfTargetPage);
187
188
            // Now overlay the page in the target language, in order to have valid title attributes etc.
189
            if ($siteLanguageOfTargetPage->getLanguageId() > 0) {
190
                $context = clone GeneralUtility::makeInstance(Context::class);
191
                $context->setAspect('language', $languageAspect);
192
                $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
193
                $page = $pageRepository->getPageOverlay($page);
194
            }
195
            // Check if the target page can be access depending on l18n_cfg
196
            if (!$tsfe->sys_page->isPageSuitableForLanguage($page, $languageAspect)) {
197
                $languageField = $GLOBALS['TCA']['pages']['ctrl']['languageField'] ?? null;
198
                $languageOfPageRecord = (int)($page[$languageField] ?? 0);
199
                if ($languageOfPageRecord === 0 && GeneralUtility::hideIfDefaultLanguage($page['l18n_cfg'])) {
200
                    throw new UnableToLinkException('Default language of page  "' . $linkDetails['typoLinkParameter'] . '" is hidden, so "' . $linkText . '" was not linked.', 1551621985, null, $linkText);
201
                }
202
                if ($languageOfPageRecord > 0 && !isset($page['_PAGES_OVERLAY']) && GeneralUtility::hideIfNotTranslated($page['l18n_cfg'])) {
203
                    throw new UnableToLinkException('Fallback to default language of page "' . $linkDetails['typoLinkParameter'] . '" is disabled, so "' . $linkText . '" was not linked.', 1551621996, null, $linkText);
204
                }
205
            }
206
207
            if ($pageType) {
208
                $queryParameters['type'] = (int)$pageType;
209
            }
210
211
            $treatAsExternalLink = true;
212
            // External links are resolved via calling Typolink again (could be anything, really)
213
            if ((int)$page['doktype'] === PageRepository::DOKTYPE_LINK) {
214
                $conf['parameter'] = $page['url'];
215
                unset($conf['parameter.']);
216
                $this->contentObjectRenderer->typoLink($linkText, $conf);
217
                $target = $this->contentObjectRenderer->lastTypoLinkTarget;
218
                $url = $this->contentObjectRenderer->lastTypoLinkUrl;
219
                if (empty($url)) {
220
                    throw new UnableToLinkException('Link to external page "' . $page['uid'] . '" does not have a proper target URL, so "' . $linkText . '" was not linked.', 1551621999, null, $linkText);
221
                }
222
            } else {
223
                // Generate the URL
224
                $url = $this->generateUrlForPageWithSiteConfiguration($page, $siteOfTargetPage, $queryParameters, $sectionMark, $conf);
225
                // no scheme => always not external
226
                if (!$url->getScheme() || !$url->getHost()) {
227
                    $treatAsExternalLink = false;
228
                } else {
229
                    // URL has a scheme, possibly because someone requested a full URL. So now lets check if the URL
230
                    // is on the same site pagetree. If this is the case, we'll treat it as internal
231
                    // @todo: currently this does not check if the target page is a mounted page in a different site,
232
                    // so it is treating this as an absolute URL, which is wrong
233
                    if ($currentSite instanceof Site && $currentSite->getRootPageId() === $siteOfTargetPage->getRootPageId()) {
234
                        $treatAsExternalLink = false;
235
                    }
236
                }
237
                $url = (string)$url;
238
            }
239
            if ($treatAsExternalLink) {
240
                $target = $target ?: $this->resolveTargetAttribute($conf, 'extTarget', false, $tsfe->extTarget);
241
            } else {
242
                $target = (isset($page['target']) && trim($page['target'])) ? $page['target'] : $target;
243
                if (empty($target)) {
244
                    $target = $this->resolveTargetAttribute($conf, 'target', true, $tsfe->intTarget);
245
                }
246
            }
247
        } else {
248
            throw new UnableToLinkException('Could not link to page with ID: ' . $page['uid'], 1546887172, null, $linkText);
249
        }
250
251
        // If link is to an access restricted page which should be redirected, then find new URL:
252
        if (empty($conf['linkAccessRestrictedPages'])
253
            && $tsfe->config['config']['typolinkLinkAccessRestrictedPages']
254
            && $tsfe->config['config']['typolinkLinkAccessRestrictedPages'] !== 'NONE'
255
            && !$tsfe->checkPageGroupAccess($page)
256
        ) {
257
            $thePage = $tsfe->sys_page->getPage($tsfe->config['config']['typolinkLinkAccessRestrictedPages']);
258
            $addParams = str_replace(
259
                [
260
                    '###RETURN_URL###',
261
                    '###PAGE_ID###'
262
                ],
263
                [
264
                    rawurlencode($url),
265
                    $page['uid']
266
                ],
267
                $tsfe->config['config']['typolinkLinkAccessRestrictedPages_addParams']
268
            );
269
            $url = $this->contentObjectRenderer->getTypoLink_URL($thePage['uid'] . ($pageType ? ',' . $pageType : ''), $addParams, $target);
270
            $url = $this->forceAbsoluteUrl($url, $conf);
271
        }
272
273
        // Setting title if blank value to link
274
        $linkText = $this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $page['title']);
275
        return [$url, $linkText, $target];
276
    }
277
278
    /**
279
     * Resolves page and if a translated page was found, resolves that to it
280
     * language parent, adjusts `$linkDetails['pageuid']` (for hook processing)
281
     * and modifies `$configuration['language']` (for language URL generation).
282
     *
283
     * @param array $linkDetails
284
     * @param array $configuration
285
     * @param bool $disableGroupAccessCheck
286
     * @return array
287
     */
288
    protected function resolvePage(array &$linkDetails, array &$configuration, bool $disableGroupAccessCheck): array
289
    {
290
        $pageRepository = $this->buildPageRepository();
291
        // Looking up the page record to verify its existence
292
        // This is used when a page to a translated page is executed directly.
293
        $page = $pageRepository->getPage($linkDetails['pageuid'], $disableGroupAccessCheck);
294
295
        if (empty($page) || !is_array($page)) {
296
            return [];
297
        }
298
299
        $languageField = $GLOBALS['TCA']['pages']['ctrl']['languageField'] ?? null;
300
        $languageParentField = $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] ?? null;
301
        $language = (int)($page[$languageField] ?? 0);
302
303
        // The page that should be linked is actually a default-language page, nothing to do here.
304
        if ($language === 0 || empty($page[$languageParentField])) {
305
            return $page;
306
        }
307
308
        // Let's fetch the default-language page now
309
        $languageParentPage = $pageRepository->getPage(
310
            $page[$languageParentField],
311
            $disableGroupAccessCheck
312
        );
313
        if (empty($languageParentPage)) {
314
            return $page;
315
        }
316
317
        // Set the "pageuid" to the default-language page ID.
318
        $linkDetails['pageuid'] = (int)$languageParentPage['uid'];
319
        $configuration['language'] = $language;
320
        return $languageParentPage;
321
    }
322
323
    /**
324
     * Fetches the requested language of a site that the link should be built for
325
     *
326
     * @param Site $siteOfTargetPage
327
     * @param string $targetLanguageId "current" or the languageId
328
     * @return SiteLanguage
329
     * @throws UnableToLinkException
330
     */
331
    protected function getSiteLanguageOfTargetPage(Site $siteOfTargetPage, string $targetLanguageId): SiteLanguage
332
    {
333
        $currentSite = $this->getCurrentSite();
334
        $currentSiteLanguage = $this->getCurrentSiteLanguage();
335
        // Happens when currently on a pseudo-site configuration
336
        // We assume to use the default language then
337
        if ($currentSite && !($currentSiteLanguage instanceof SiteLanguage)) {
338
            $currentSiteLanguage = $currentSite->getDefaultLanguage();
339
        }
340
341
        if ($targetLanguageId === 'current') {
342
            $targetLanguageId = $currentSiteLanguage ? $currentSiteLanguage->getLanguageId() : 0;
343
        } else {
344
            $targetLanguageId = (int)$targetLanguageId;
345
        }
346
        try {
347
            $siteLanguageOfTargetPage = $siteOfTargetPage->getLanguageById($targetLanguageId);
348
        } catch (\InvalidArgumentException $e) {
349
            throw new UnableToLinkException('The target page does not have a language with ID ' . $targetLanguageId . ' configured in its site configuration.', 1535477406);
350
        }
351
        return $siteLanguageOfTargetPage;
352
    }
353
354
    /**
355
     * Create a UriInterface object when linking to a page with a site configuration
356
     *
357
     * @param array $page
358
     * @param Site $siteOfTargetPage
359
     * @param array $queryParameters
360
     * @param string $fragment
361
     * @param array $conf
362
     * @return UriInterface
363
     * @throws UnableToLinkException
364
     */
365
    protected function generateUrlForPageWithSiteConfiguration(array $page, Site $siteOfTargetPage, array $queryParameters, string $fragment, array $conf): UriInterface
366
    {
367
        $currentSite = $this->getCurrentSite();
368
        $currentSiteLanguage = $this->getCurrentSiteLanguage();
369
        // Happens when currently on a pseudo-site configuration
370
        // We assume to use the default language then
371
        if ($currentSite && !($currentSiteLanguage instanceof SiteLanguage)) {
372
            $currentSiteLanguage = $currentSite->getDefaultLanguage();
373
        }
374
375
        $siteLanguageOfTargetPage = $this->getSiteLanguageOfTargetPage($siteOfTargetPage, (string)($conf['language'] ?? 'current'));
376
377
        // By default, it is assumed to ab an internal link or current domain's linking scheme should be used
378
        // Use the config option to override this.
379
        $useAbsoluteUrl = $conf['forceAbsoluteUrl'] ?? false;
380
        // Check if the current page equal to the site of the target page, now only set the absolute URL
381
        // Always generate absolute URLs if no current site is set
382
        if (
383
            !$currentSite
384
            || $currentSite->getRootPageId() !== $siteOfTargetPage->getRootPageId()
385
            || $siteLanguageOfTargetPage->getBase()->getHost() !== $currentSiteLanguage->getBase()->getHost()) {
386
            $useAbsoluteUrl = true;
387
        }
388
389
        $targetPageId = (int)($page['l10n_parent'] > 0 ? $page['l10n_parent'] : $page['uid']);
390
        $queryParameters['_language'] = $siteLanguageOfTargetPage;
391
392
        if ($conf['no_cache']) {
393
            $queryParameters['no_cache'] = 1;
394
        }
395
396
        if ($fragment
397
            && $useAbsoluteUrl === false
398
            && $currentSiteLanguage === $siteLanguageOfTargetPage
399
            && $targetPageId === (int)$GLOBALS['TSFE']->id
400
            && (empty($conf['addQueryString']) || !isset($conf['addQueryString.']))
401
            && !$GLOBALS['TSFE']->config['config']['baseURL']
402
            && count($queryParameters) === 1 // _language is always set
403
            ) {
404
            $uri = (new Uri())->withFragment($fragment);
405
        } else {
406
            try {
407
                $uri = $siteOfTargetPage->getRouter()->generateUri(
408
                    $targetPageId,
409
                    $queryParameters,
410
                    $fragment,
411
                    $useAbsoluteUrl ? RouterInterface::ABSOLUTE_URL : RouterInterface::ABSOLUTE_PATH
412
                );
413
            } catch (InvalidRouteArgumentsException $e) {
414
                throw new UnableToLinkException('The target page could not be linked. Error: ' . $e->getMessage(), 1535472406);
415
            }
416
            // Override scheme, but only if the site does not define a scheme yet AND the site defines a domain/host
417
            if ($useAbsoluteUrl && !$uri->getScheme() && $uri->getHost()) {
418
                $scheme = $conf['forceAbsoluteUrl.']['scheme'] ?? 'https';
419
                $uri = $uri->withScheme($scheme);
420
            }
421
        }
422
423
        return $uri;
424
    }
425
426
    /**
427
     * 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.
428
     *
429
     * @param int $pageId page id
430
     * @return string MP value, prefixed with &MP= (depending on $raw)
431
     */
432
    protected function getClosestMountPointValueForPage($pageId)
433
    {
434
        $tsfe = $this->getTypoScriptFrontendController();
435
        if (empty($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) || !$tsfe->MP) {
436
            return '';
437
        }
438
        // Same page as current.
439
        if ((int)$tsfe->id === (int)$pageId) {
440
            return $tsfe->MP;
441
        }
442
443
        // Find closest mount point
444
        // Gets rootline of linked-to page
445
        try {
446
            $tCR_rootline = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get();
447
        } catch (RootLineException $e) {
448
            $tCR_rootline = [];
449
        }
450
        $inverseTmplRootline = array_reverse($tsfe->tmpl->rootLine);
451
        $rl_mpArray = [];
452
        $startMPaccu = false;
453
        // Traverse root line of link uid and inside of that the REAL root line of current position.
454
        foreach ($tCR_rootline as $tCR_data) {
455
            foreach ($inverseTmplRootline as $rlKey => $invTmplRLRec) {
456
                // Force accumulating when in overlay mode: Links to this page have to stay within the current branch
457
                if ($invTmplRLRec['_MOUNT_OL'] && (int)$tCR_data['uid'] === (int)$invTmplRLRec['uid']) {
458
                    $startMPaccu = true;
459
                }
460
                // Accumulate MP data:
461
                if ($startMPaccu && $invTmplRLRec['_MP_PARAM']) {
462
                    $rl_mpArray[] = $invTmplRLRec['_MP_PARAM'];
463
                }
464
                // If two PIDs matches and this is NOT the site root, start accumulation of MP data (on the next level):
465
                // (The check for site root is done so links to branches outside the site but sharing the site roots PID
466
                // is NOT detected as within the branch!)
467
                if ((int)$tCR_data['pid'] === (int)$invTmplRLRec['pid'] && count($inverseTmplRootline) !== $rlKey + 1) {
468
                    $startMPaccu = true;
469
                }
470
            }
471
            if ($startMPaccu) {
472
                // Good enough...
473
                break;
474
            }
475
        }
476
        return !empty($rl_mpArray) ? implode(',', array_reverse($rl_mpArray)) : '';
477
    }
478
479
    /**
480
     * Initializes the automatically created mountPointMap coming from the "config.MP_mapRootPoints" setting
481
     * Can be called many times with overhead only the first time since then the map is generated and cached in memory.
482
     *
483
     * Previously located within TemplateService::getFromMPmap()
484
     *
485
     * @param int $pageId Page id to return MPvar value for.
486
     * @return string
487
     */
488
    public function getMountPointParameterFromRootPointMaps(int $pageId)
489
    {
490
        // Create map if not found already
491
        $config = $this->getTypoScriptFrontendController()->config;
492
        $mountPointMap = $this->initializeMountPointMap(
493
            !empty($config['config']['MP_defaults']) ? $config['config']['MP_defaults'] : null,
494
            !empty($config['config']['MP_mapRootPoints']) ? $config['config']['MP_mapRootPoints'] : null
495
        );
496
497
        // Finding MP var for Page ID:
498
        if (!empty($mountPointMap[$pageId])) {
499
            return implode(',', $mountPointMap[$pageId]);
500
        }
501
        return '';
502
    }
503
504
    /**
505
     * Create mount point map, based on TypoScript config.MP_mapRootPoints and config.MP_defaults.
506
     *
507
     * @param string $defaultMountPoints a string as defined in config.MP_defaults
508
     * @param string|null $mapRootPointList a string as defined in config.MP_mapRootPoints
509
     * @return array
510
     */
511
    protected function initializeMountPointMap(string $defaultMountPoints = null, string $mapRootPointList = null): array
512
    {
513
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
514
        $mountPointMap = $runtimeCache->get('pageLinkBuilderMountPointMap') ?: [];
515
        if (!empty($mountPointMap) || (empty($mapRootPointList) && empty($defaultMountPoints))) {
516
            return $mountPointMap;
517
        }
518
        if ($defaultMountPoints) {
519
            $defaultMountPoints = GeneralUtility::trimExplode('|', $defaultMountPoints, true);
520
            foreach ($defaultMountPoints as $temp_p) {
521
                [$temp_idP, $temp_MPp] = explode(':', $temp_p, 2);
522
                $temp_ids = GeneralUtility::intExplode(',', $temp_idP);
523
                foreach ($temp_ids as $temp_id) {
524
                    $mountPointMap[$temp_id] = trim($temp_MPp);
525
                }
526
            }
527
        }
528
529
        $rootPoints = GeneralUtility::trimExplode(',', strtolower($mapRootPointList), true);
530
        // Traverse rootpoints
531
        foreach ($rootPoints as $p) {
532
            $initMParray = [];
533
            if ($p === 'root') {
534
                $rootPage = $this->getTypoScriptFrontendController()->tmpl->rootLine[0];
535
                $p = $rootPage['uid'];
536
                if ($rootPage['_MOUNT_OL'] && $rootPage['_MP_PARAM']) {
537
                    $initMParray[] = $rootPage['_MP_PARAM'];
538
                }
539
            }
540
            $this->populateMountPointMapForPageRecursively($mountPointMap, (int)$p, $initMParray);
541
        }
542
        $runtimeCache->set('pageLinkBuilderMountPointMap', $mountPointMap);
543
        return $mountPointMap;
544
    }
545
546
    /**
547
     * Creating mountPointMap for a certain ID root point.
548
     * Previously called TemplateService->initMPmap_create()
549
     *
550
     * @param array $mountPointMap the exiting mount point map
551
     * @param int $id Root id from which to start map creation.
552
     * @param array $MP_array MP_array passed from root page.
553
     * @param int $level Recursion brake. Incremented for each recursive call. 20 is the limit.
554
     * @see getMountPointParameterFromRootPointMaps()
555
     */
556
    protected function populateMountPointMapForPageRecursively(array &$mountPointMap, int $id, $MP_array = [], $level = 0)
557
    {
558
        if ($id <= 0) {
559
            return;
560
        }
561
        // First level, check id
562
        if (!$level) {
563
            // Find mount point if any:
564
            $mount_info = $this->getTypoScriptFrontendController()->sys_page->getMountPointInfo($id);
565
            // Overlay mode:
566
            if (is_array($mount_info) && $mount_info['overlay']) {
567
                $MP_array[] = $mount_info['MPvar'];
568
                $id = $mount_info['mount_pid'];
569
            }
570
            // Set mapping information for this level:
571
            $mountPointMap[$id] = $MP_array;
572
            // Normal mode:
573
            if (is_array($mount_info) && !$mount_info['overlay']) {
574
                $MP_array[] = $mount_info['MPvar'];
575
                $id = $mount_info['mount_pid'];
576
            }
577
        }
578
        if ($id && $level < 20) {
579
            $nextLevelAcc = [];
580
            // Select and traverse current level pages:
581
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
582
            $queryBuilder->getRestrictions()
583
                ->removeAll()
584
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
585
            $queryResult = $queryBuilder
586
                ->select('uid', 'pid', 'doktype', 'mount_pid', 'mount_pid_ol', 't3ver_state')
587
                ->from('pages')
588
                ->where(
589
                    $queryBuilder->expr()->eq(
590
                        'pid',
591
                        $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
592
                    ),
593
                    $queryBuilder->expr()->neq(
594
                        'doktype',
595
                        $queryBuilder->createNamedParameter(PageRepository::DOKTYPE_RECYCLER, \PDO::PARAM_INT)
596
                    ),
597
                    $queryBuilder->expr()->neq(
598
                        'doktype',
599
                        $queryBuilder->createNamedParameter(PageRepository::DOKTYPE_BE_USER_SECTION, \PDO::PARAM_INT)
600
                    )
601
                )->execute();
602
            while ($row = $queryResult->fetch()) {
603
                // Find mount point if any:
604
                $next_id = (int)$row['uid'];
605
                $next_MP_array = $MP_array;
606
                $mount_info = $this->getTypoScriptFrontendController()->sys_page->getMountPointInfo($next_id, $row);
607
                // Overlay mode:
608
                if (is_array($mount_info) && $mount_info['overlay']) {
609
                    $next_MP_array[] = $mount_info['MPvar'];
610
                    $next_id = (int)$mount_info['mount_pid'];
611
                }
612
                if (!isset($mountPointMap[$next_id])) {
613
                    // Set mapping information for this level:
614
                    $mountPointMap[$next_id] = $next_MP_array;
615
                    // Normal mode:
616
                    if (is_array($mount_info) && !$mount_info['overlay']) {
617
                        $next_MP_array[] = $mount_info['MPvar'];
618
                        $next_id = (int)$mount_info['mount_pid'];
619
                    }
620
                    // Register recursive call
621
                    // (have to do it this way since ALL of the current level should be registered BEFORE the sublevel at any time)
622
                    $nextLevelAcc[] = [$next_id, $next_MP_array];
623
                }
624
            }
625
            // Call recursively, if any:
626
            foreach ($nextLevelAcc as $pSet) {
627
                $this->populateMountPointMapForPageRecursively($mountPointMap, $pSet[0], $pSet[1], $level + 1);
628
            }
629
        }
630
    }
631
632
    /**
633
     * Check if we have a site object in the current request. if null, this usually means that
634
     * this class was called from CLI context.
635
     *
636
     * @return SiteInterface|null
637
     */
638
    protected function getCurrentSite(): ?SiteInterface
639
    {
640
        if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
641
            return $GLOBALS['TYPO3_REQUEST']->getAttribute('site', null);
642
        }
643
        if (MathUtility::canBeInterpretedAsInteger($GLOBALS['TSFE']->id) && $GLOBALS['TSFE']->id > 0) {
644
            $finder = GeneralUtility::makeInstance(SiteFinder::class);
645
            try {
646
                $site = $finder->getSiteByPageId((int)$GLOBALS['TSFE']->id);
647
            } catch (SiteNotFoundException $e) {
648
                $site = null;
649
            }
650
            return $site;
651
        }
652
        return null;
653
    }
654
655
    /**
656
     * If the current request has a site language, this means that the SiteResolver has detected a
657
     * page with a site configuration and a selected language, so let's choose that one.
658
     *
659
     * @return SiteLanguage|null
660
     */
661
    protected function getCurrentSiteLanguage(): ?SiteLanguage
662
    {
663
        if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
664
            return $GLOBALS['TYPO3_REQUEST']->getAttribute('language', null);
665
        }
666
        return null;
667
    }
668
669
    /**
670
     * Builds PageRepository instance without depending on global context, e.g.
671
     * not automatically overlaying records based on current request language.
672
     *
673
     * @return PageRepository
674
     */
675
    protected function buildPageRepository(): PageRepository
676
    {
677
        // clone global context object (singleton)
678
        $context = clone GeneralUtility::makeInstance(Context::class);
679
        $context->setAspect(
680
            'language',
681
            GeneralUtility::makeInstance(LanguageAspect::class)
682
        );
683
        $pageRepository = GeneralUtility::makeInstance(
684
            PageRepository::class,
685
            $context
686
        );
687
        return $pageRepository;
688
    }
689
}
690