Passed
Push — release-11.5.x ( fafc82...d39173 )
by Rafael
31:01
created

Tsfe::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
// @todo: self::getAbsRefPrefixFromTSFE() returns false instead of string.
4
//        Solve that issue and activate strict types.
5
//declare(strict_types=1);
6
7
/*
8
 * This file is part of the TYPO3 CMS project.
9
 *
10
 * It is free software; you can redistribute it and/or modify it under
11
 * the terms of the GNU General Public License, either version 2
12
 * of the License, or any later version.
13
 *
14
 * For the full copyright and license information, please read the
15
 * LICENSE.txt file that was distributed with this source code.
16
 *
17
 * The TYPO3 project - inspiring people to share!
18
 */
19
20
namespace ApacheSolrForTypo3\Solr\FrontendEnvironment;
21
22
use ApacheSolrForTypo3\Solr\System\Configuration\ConfigurationPageResolver;
23
use Doctrine\DBAL\Driver\Exception as DBALDriverException;
24
use Throwable;
25
use TYPO3\CMS\Backend\Utility\BackendUtility;
26
use TYPO3\CMS\Core\Context\Context;
27
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
28
use TYPO3\CMS\Core\Context\TypoScriptAspect;
29
use TYPO3\CMS\Core\Context\UserAspect;
30
use TYPO3\CMS\Core\Context\VisibilityAspect;
31
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
32
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
33
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
34
use TYPO3\CMS\Core\Http\ServerRequest;
35
use TYPO3\CMS\Core\Localization\Locales;
36
use TYPO3\CMS\Core\Routing\PageArguments;
37
use TYPO3\CMS\Core\SingletonInterface;
38
use TYPO3\CMS\Core\Site\SiteFinder;
39
use TYPO3\CMS\Core\TypoScript\TemplateService;
40
use TYPO3\CMS\Core\Utility\GeneralUtility;
41
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
42
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
43
44
/**
45
 * Class Tsfe is a factory class for TSFE(TypoScriptFrontendController) objects.
46
 */
47
class Tsfe implements SingletonInterface
48
{
49
    /**
50
     * @var TypoScriptFrontendController[]
51
     */
52
    protected array $tsfeCache = [];
53
54
    /**
55
     * @var ServerRequest[]
56
     */
57
    protected array $serverRequestCache = [];
58
59
    /**
60
     * @var SiteFinder
61
     */
62
    protected SiteFinder $siteFinder;
63
64
    /**
65
     * Initializes isolated TypoScriptFrontendController for Indexing and backend actions.
66
     *
67
     * @param SiteFinder|null $siteFinder
68
     */
69 207
    public function __construct(?SiteFinder $siteFinder = null)
70
    {
71 207
        $this->siteFinder = $siteFinder ?? GeneralUtility::makeInstance(SiteFinder::class);
72
    }
73
74
    /**
75
     * Initializes the TSFE for a given page ID and language.
76
     *
77
     * @param int $pageId
78
     * @param int $language
79
     *
80
     * @param int|null $rootPageId
81
     *
82
     * @throws DBALDriverException
83
     * @throws Exception\Exception
84
     * @throws SiteNotFoundException
85
     *
86
     *
87
     * @todo: Move whole caching stuff from this method and let return TSFE.
88
     */
89 207
    protected function initializeTsfe(int $pageId, int $language = 0, ?int $rootPageId = null)
90
    {
91 207
        $cacheIdentifier = $this->getCacheIdentifier($pageId, $language, $rootPageId);
92
93
        // Handle spacer and sys-folders, since they are not accessible in frontend, and TSFE can not be fully initialized on them.
94
        // Apart from this, the plugin.tx_solr.index.queue.[indexConfig].additionalPageIds is handled as well.
95 207
        $pidToUse = $this->getPidToUseForTsfeInitialization($pageId, $rootPageId);
96 207
        if ($pidToUse !== $pageId) {
97 7
            $this->initializeTsfe($pidToUse, $language, $rootPageId);
0 ignored issues
show
Bug introduced by
It seems like $pidToUse can also be of type null; however, parameter $pageId of ApacheSolrForTypo3\Solr\...\Tsfe::initializeTsfe() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

97
            $this->initializeTsfe(/** @scrutinizer ignore-type */ $pidToUse, $language, $rootPageId);
Loading history...
98 7
            $reusedCacheIdentifier = $this->getCacheIdentifier($pidToUse, $language, $rootPageId);
0 ignored issues
show
Bug introduced by
It seems like $pidToUse can also be of type null; however, parameter $pageId of ApacheSolrForTypo3\Solr\...e::getCacheIdentifier() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

98
            $reusedCacheIdentifier = $this->getCacheIdentifier(/** @scrutinizer ignore-type */ $pidToUse, $language, $rootPageId);
Loading history...
99 7
            $this->serverRequestCache[$cacheIdentifier] = $this->serverRequestCache[$reusedCacheIdentifier];
100 7
            $this->tsfeCache[$cacheIdentifier] = $this->tsfeCache[$reusedCacheIdentifier];
101
            // if ($rootPageId === null) {
102
            //     @Todo: Resolve and set TSFE object for $rootPageId.
103
            // }
104 7
            return;
105
        }
106
107
        /* @var Context $context */
108 207
        $context = clone GeneralUtility::makeInstance(Context::class);
109 207
        $site = $this->siteFinder->getSiteByPageId($pageId);
110
        // $siteLanguage and $languageAspect takes the language id into account.
111
        //   See: $site->getLanguageById($language);
112
        //   Therefore the whole TSFE stack is initialized and must be used as is.
113
        //   Note: ServerRequest, Context, Language, cObj of TSFE MUST NOT be changed or touched in any way,
114
        //         Otherwise the caching of TSFEs makes no sense anymore.
115
        //         If you want something to change in TSFE object, please use cloned one!
116 207
        $siteLanguage = $site->getLanguageById($language);
117 207
        $languageAspect = LanguageAspectFactory::createFromSiteLanguage($siteLanguage);
118 207
        $context->setAspect('language', $languageAspect);
119
120 207
        $serverRequest = $this->serverRequestCache[$cacheIdentifier] ?? null;
121 207
        if (!isset($this->serverRequestCache[$cacheIdentifier])) {
122 207
            $serverRequest = GeneralUtility::makeInstance(ServerRequest::class);
123 207
            $this->serverRequestCache[$cacheIdentifier] = $serverRequest =
124 207
                $serverRequest->withAttribute('site', $site)
125 207
                ->withAttribute('language', $siteLanguage)
126 207
                ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
127 207
                ->withUri($site->getBase());
128
        }
129
130 207
        if (!isset($this->tsfeCache[$cacheIdentifier])) {
131
            // TYPO3 by default enables a preview mode if a backend user is logged in,
132
            // the VisibilityAspect is configured to show hidden elements.
133
            // Due to this setting hidden relations/translations might be indexed
134
            // when running the Solr indexer via the TYPO3 backend.
135
            // To avoid this, the VisibilityAspect is adapted for indexing.
136 207
            $context->setAspect(
137 207
                'visibility',
138 207
                GeneralUtility::makeInstance(
139 207
                    VisibilityAspect::class,
140 207
                    false,
141 207
                    false
142 207
                )
143 207
            );
144
145
            /** @var FrontendUserAuthentication $feUser */
146 207
            $feUser = GeneralUtility::makeInstance(FrontendUserAuthentication::class);
147
            // for certain situations we need to trick TSFE into granting us
148
            // access to the page in any case to make getPageAndRootline() work
149
            // see http://forge.typo3.org/issues/42122
150 207
            $pageRecord = BackendUtility::getRecord('pages', $pageId, 'fe_group');
151 207
            if (!empty($pageRecord['fe_group'])) {
152 1
                $userGroups = explode(',', $pageRecord['fe_group']);
153
            } else {
154 207
                $userGroups = [0, -1];
155
            }
156 207
            $feUser->user = ['uid' => 0, 'username' => '', 'usergroup' => implode(',', $userGroups) ];
157 207
            $feUser->fetchGroupData();
158 207
            $context->setAspect('frontend.user', GeneralUtility::makeInstance(UserAspect::class, $feUser, $userGroups));
159
160
            /* @var PageArguments $pageArguments */
161 207
            $pageArguments = GeneralUtility::makeInstance(PageArguments::class, $pageId, 0, []);
162
163
            /* @var TypoScriptFrontendController $tsfe */
164 207
            $tsfe = GeneralUtility::makeInstance(TypoScriptFrontendController::class, $context, $site, $siteLanguage, $pageArguments, $feUser);
165
166
            // @extensionScannerIgnoreLine
167
            /** Done in {@link \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::settingLanguage} */
168
            //$tsfe->sys_page = GeneralUtility::makeInstance(PageRepository::class);
169
170 207
            $template = GeneralUtility::makeInstance(TemplateService::class, $context, null, $tsfe);
171 207
            $template->tt_track = false;
172 207
            $tsfe->tmpl = $template;
173 207
            $context->setAspect('typoscript', GeneralUtility::makeInstance(TypoScriptAspect::class, true));
174 207
            $tsfe->no_cache = true;
175
176 207
            $backedUpBackendUser = $GLOBALS['BE_USER'] ?? null;
177
            try {
178 207
                $serverRequest = $serverRequest->withAttribute('frontend.controller', $tsfe);
179 207
                $tsfe->determineId($serverRequest);
180 207
                $tsfe->no_cache = false;
181 207
                $tsfe->getConfigArray($serverRequest);
182
183 207
                $tsfe->newCObj($serverRequest);
184 207
                $tsfe->absRefPrefix = self::getAbsRefPrefixFromTSFE($tsfe);
0 ignored issues
show
Bug Best Practice introduced by
The method ApacheSolrForTypo3\Solr\...tAbsRefPrefixFromTSFE() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

184
                /** @scrutinizer ignore-call */ 
185
                $tsfe->absRefPrefix = self::getAbsRefPrefixFromTSFE($tsfe);
Loading history...
185 207
                $tsfe->calculateLinkVars([]);
186 6
            } catch (Throwable $exception) {
187
                // @todo: logging
188 6
                $this->serverRequestCache[$cacheIdentifier] = null;
189 6
                $this->tsfeCache[$cacheIdentifier] = null;
190
                // Restore backend user, happens when initializeTsfe() is called from Backend context
191 6
                if ($backedUpBackendUser) {
192 3
                    $GLOBALS['BE_USER'] = $backedUpBackendUser;
193
                }
194 6
                return;
195
            }
196
            // Restore backend user, happens when initializeTsfe() is called from Backend context
197 207
            if ($backedUpBackendUser) {
198 40
                $GLOBALS['BE_USER'] = $backedUpBackendUser;
199
            }
200
201 207
            $this->serverRequestCache[$cacheIdentifier] = $serverRequest;
202 207
            $this->tsfeCache[$cacheIdentifier] = $tsfe;
203
        }
204
205
        // @todo: Not right place for that action, move on more convenient place: indexing a single item+id+lang.
206 207
        Locales::setSystemLocaleFromSiteLanguage($siteLanguage);
207
    }
208
209
    /**
210
     * Returns TypoScriptFrontendController with sand cast context.
211
     *
212
     * @param int $pageId
213
     * @param int $language
214
     *
215
     * @param int|null $rootPageId
216
     *
217
     * @return TypoScriptFrontendController
218
     *
219
     * @throws SiteNotFoundException
220
     * @throws DBALDriverException
221
     * @throws Exception\Exception
222
     */
223 207
    public function getTsfeByPageIdAndLanguageId(int $pageId, int $language = 0, ?int $rootPageId = null): ?TypoScriptFrontendController
224
    {
225 207
        $this->assureIsInitialized($pageId, $language, $rootPageId);
226 207
        return $this->tsfeCache[$this->getCacheIdentifier($pageId, $language, $rootPageId)];
227
    }
228
229
    /**
230
     * Returns TypoScriptFrontendController for first available language id in fallback chain.
231
     *
232
     * Is usable for BE-Modules/CLI-Commands stack only, where the rendered TypoScript configuration
233
     * of EXT:solr* stack is wanted and the language id does not matter.
234
     *
235
     * NOTE: This method MUST NOT be used on indexing context.
236
     *
237
     * @param int $pageId
238
     * @param int ...$languageFallbackChain
239
     * @return TypoScriptFrontendController|null
240
     */
241 205
    public function getTsfeByPageIdAndLanguageFallbackChain(int $pageId, int ...$languageFallbackChain): ?TypoScriptFrontendController
242
    {
243 205
        foreach ($languageFallbackChain as $languageId) {
244
            try {
245 205
                $tsfe = $this->getTsfeByPageIdAndLanguageId($pageId, $languageId);
246 205
                if ($tsfe instanceof TypoScriptFrontendController) {
247 205
                    return $tsfe;
248
                }
249
            } catch (Throwable $e) {
250
                // no needs to log or do anything, the method MUST not return anything if it can't.
251
                continue;
252
            }
253
        }
254 1
        return null;
255
    }
256
257
    /**
258
     * Returns TSFE for first initializable site language.
259
     *
260
     * Is usable for BE-Modules/CLI-Commands stack only, where the rendered TypoScript configuration
261
     * of EXT:solr* stack is wanted and the language id does not matter.
262
     *
263
     * @param int $pageId
264
     * @return TypoScriptFrontendController|null
265
     */
266 60
    public function getTsfeByPageIdIgnoringLanguage(int $pageId): ?TypoScriptFrontendController
267
    {
268
        try {
269 60
            $typo3Site = $this->siteFinder->getSiteByPageId($pageId);
270
        } catch (Throwable $e) {
271
            return null;
272
        }
273 60
        $availableLanguageIds = array_map(function ($siteLanguage) {
274 60
            return $siteLanguage->getLanguageId();
275 60
        }, $typo3Site->getLanguages());
276
277 60
        if (empty($availableLanguageIds)) {
278
            return null;
279
        }
280 60
        return $this->getTsfeByPageIdAndLanguageFallbackChain($pageId, ...$availableLanguageIds);
281
    }
282
283
    /**
284
     * Returns TypoScriptFrontendController with sand cast context.
285
     *
286
     * @param int $pageId
287
     * @param int $language
288
     *
289
     * @param int|null $rootPageId
290
     *
291
     * @return ServerRequest
292
     *
293
     * @throws SiteNotFoundException
294
     * @throws DBALDriverException
295
     * @throws Exception\Exception
296
     * @noinspection PhpUnused
297
     */
298
    public function getServerRequestForTsfeByPageIdAndLanguageId(int $pageId, int $language = 0, ?int $rootPageId = null): ?ServerRequest
299
    {
300
        $this->assureIsInitialized($pageId, $language, $rootPageId);
301
        return $this->serverRequestCache[$this->getCacheIdentifier($pageId, $language, $rootPageId)];
302
    }
303
304
    /**
305
     * Initializes the TSFE, ServerRequest, Context if not already done.
306
     *
307
     * @param int $pageId
308
     * @param int $language
309
     *
310
     * @param int|null $rootPageId
311
     *
312
     * @throws DBALDriverException
313
     * @throws SiteNotFoundException
314
     * @throws Exception\Exception
315
     */
316 207
    protected function assureIsInitialized(int $pageId, int $language, ?int $rootPageId = null): void
317
    {
318 207
        $cacheIdentifier = $this->getCacheIdentifier($pageId, $language, $rootPageId);
319 207
        if (!array_key_exists($cacheIdentifier, $this->tsfeCache)) {
320 207
            $this->initializeTsfe($pageId, $language, $rootPageId);
321 207
            return;
322
        }
323 204
        if ($this->tsfeCache[$cacheIdentifier] instanceof TypoScriptFrontendController) {
324 204
            $this->tsfeCache[$cacheIdentifier]->newCObj($this->serverRequestCache[$cacheIdentifier]);
325
        }
326
    }
327
328
    /**
329
     * Returns the cache identifier for cached TSFE and ServerRequest objects.
330
     *
331
     * @param int $pageId
332
     * @param int $language
333
     *
334
     * @param int|null $rootPageId
335
     *
336
     * @return string
337
     */
338 207
    protected function getCacheIdentifier(int $pageId, int $language, ?int $rootPageId = null): string
339
    {
340 207
        return 'root:' . ($rootPageId ?? 'null') . '|page:' . $pageId . '|lang:' . $language;
341
    }
342
343
    /**
344
     * The TSFE can not be initialized for Spacer and sys-folders.
345
     * See: "Spacer and sys folders is not accessible in frontend" on {@link \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::getPageAndRootline()}
346
     *
347
     * Note: The requested $pidToUse can be one of configured plugin.tx_solr.index.queue.[indexConfig].additionalPageIds.
348
     *
349
     * @param int $pidToUse
350
     *
351
     * @param int|null $rootPageId
352
     *
353
     * @return int
354
     * @throws DBALDriverException
355
     * @throws Exception\Exception
356
     */
357 207
    protected function getPidToUseForTsfeInitialization(int $pidToUse, ?int $rootPageId = null): ?int
358
    {
359 207
        $incomingPidToUse = $pidToUse;
360 207
        $incomingRootPageId = $rootPageId;
361
362
        // handle plugin.tx_solr.index.queue.[indexConfig].additionalPageIds
363 207
        if (isset($rootPageId) && !$this->isRequestedPageAPartOfRequestedSite($pidToUse)) {
364 23
            return $rootPageId;
365
        }
366 207
        $pageRecord = BackendUtility::getRecord('pages', $pidToUse);
367 207
        $isSpacerOrSysfolder = ($pageRecord['doktype'] ?? null) == PageRepository::DOKTYPE_SPACER || ($pageRecord['doktype'] ?? null) == PageRepository::DOKTYPE_SYSFOLDER;
368 207
        if ($isSpacerOrSysfolder === false) {
369 207
            return $pidToUse;
370
        }
371
        /* @var ConfigurationPageResolver $configurationPageResolve */
372 3
        $configurationPageResolver = GeneralUtility::makeInstance(ConfigurationPageResolver::class);
373 3
        $askedPid = $pidToUse;
374 3
        $pidToUse = $configurationPageResolver->getClosestPageIdWithActiveTemplate($pidToUse);
375 3
        if (!isset($pidToUse) && !isset($rootPageId)) {
376 1
            throw new Exception\Exception(
377 1
                "The closest page with active template to page \"$askedPid\" could not be resolved and alternative rootPageId is not provided.",
378 1
                1637339439
379 1
            );
380
        }
381 2
        if (isset($rootPageId)) {
382
            return $rootPageId;
383
        }
384
385
        // Check for recursion that can happen if the root page is a sysfolder with a typoscript template
386 2
        if ($pidToUse === $incomingPidToUse && $rootPageId === $incomingRootPageId) {
387
            throw new Exception\Exception(
388
                "Infinite recursion detected while looking for the closest page with active template to page \"$askedPid\" . Please note that the page with active template (usually the root page of the current tree) MUST NOT be a sysfolder.",
389
                1637339476
390
            );
391
        }
392
393 2
        return $this->getPidToUseForTsfeInitialization($pidToUse, $rootPageId);
394
    }
395
396
    /**
397
     * Checks if the requested page belongs to site of given root page.
398
     *
399
     * @param int $pageId
400
     * @param int|null $rootPageId
401
     *
402
     * @return bool
403
     */
404 23
    protected function isRequestedPageAPartOfRequestedSite(int $pageId, ?int $rootPageId = null): bool
405
    {
406 23
        if (!isset($rootPageId)) {
407 23
            return false;
408
        }
409
        try {
410
            $site = $this->siteFinder->getSiteByPageId($pageId);
411
        } catch (SiteNotFoundException $e) {
412
            return false;
413
        }
414
        return $rootPageId === $site->getRootPageId();
415
    }
416
417
    /**
418
     * Resolves the configured absRefPrefix to a valid value and resolved if absRefPrefix
419
     * is set to "auto".
420
     */
421 207
    private function getAbsRefPrefixFromTSFE(TypoScriptFrontendController $TSFE): string
422
    {
423 207
        $absRefPrefix = '';
424 207
        if (empty($TSFE->config['config']['absRefPrefix'])) {
425
            return $absRefPrefix;
426
        }
427
428 207
        $absRefPrefix = trim($TSFE->config['config']['absRefPrefix']);
429 207
        if ($absRefPrefix === 'auto') {
430 207
            $absRefPrefix = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
431
        }
432
433 207
        return $absRefPrefix;
434
    }
435
}
436