Failed Conditions
Push — task/2976_TYPO3.11_compatibili... ( 38a128...0b41c4 )
by Rafael
25:41 queued 19:10
created

Tsfe::getTsfeByPageIdAndLanguageId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
nc 1
nop 3
crap 1
1
<?php
2
namespace ApacheSolrForTypo3\Solr\FrontendEnvironment;
3
4
use ApacheSolrForTypo3\Solr\System\Configuration\ConfigurationPageResolver;
5
use Doctrine\DBAL\Driver\Exception as DBALDriverException;
6
use Throwable;
7
use TYPO3\CMS\Backend\Utility\BackendUtility;
8
use TYPO3\CMS\Core\Context\Context;
9
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
10
use TYPO3\CMS\Core\Context\TypoScriptAspect;
11
use TYPO3\CMS\Core\Context\VisibilityAspect;
12
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
13
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
14
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
15
use TYPO3\CMS\Core\Localization\Locales;
16
use TYPO3\CMS\Core\Routing\PageArguments;
17
use TYPO3\CMS\Core\SingletonInterface;
18
use TYPO3\CMS\Core\Site\SiteFinder;
19
use TYPO3\CMS\Core\TypoScript\TemplateService;
20
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
21
use TYPO3\CMS\Core\Utility\GeneralUtility;
22
use TYPO3\CMS\Core\Context\UserAspect;
23
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
24
use TYPO3\CMS\Core\Http\ServerRequest;
25
use function Webmozart\Assert\Tests\StaticAnalysis\null;
0 ignored issues
show
introduced by
The function Webmozart\Assert\Tests\StaticAnalysis\null was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
26
27
class Tsfe implements SingletonInterface
28
{
29
30
    /**
31
     * @var TypoScriptFrontendController[]
32
     */
33
    protected array $tsfeCache = [];
34
35
    /**
36
     * @var ServerRequest[]
37
     */
38
    protected array $serverRequestCache = [];
39
40
    /**
41
     * @var SiteFinder
42
     */
43
    protected SiteFinder $siteFinder;
44
45
    /**
46
     * Initializes isolated TypoScriptFrontendController for Indexing and backend actions.
47
     *
48
     * @param SiteFinder|null $siteFinder
49
     */
50 144
    public function __construct(?SiteFinder $siteFinder = null)
51
    {
52 144
        $this->siteFinder = $siteFinder ?? GeneralUtility::makeInstance(SiteFinder::class);
53 144
    }
54
55
    /**
56
     * Initializes the TSFE for a given page ID and language.
57
     *
58
     * @param int $pageId
59
     * @param int $language
60
     *
61
     * @param int|null $rootPageId
62
     *
63
     * @throws DBALDriverException
64
     * @throws Exception\Exception
65
     * @throws SiteNotFoundException
66
     *
67
     * @todo: Move whole caching stuff from this method and let return TSFE.
68
     */
69 144
    protected function initializeTsfe(int $pageId, int $language = 0, ?int $rootPageId = null)
70
    {
71 144
        $cacheIdentifier = $this->getCacheIdentifier($pageId, $language, $rootPageId);
72
73
        // Handle spacer and sys-folders, since they are not accessible in frontend, and TSFE can not be fully initialized on them.
74
        // Apart from this, the plugin.tx_solr.index.queue.[indexConfig].additionalPageIds is handled as well.
75 144
        $pidToUse = $this->getPidToUseForTsfeInitialization($pageId, $rootPageId);
76 144
        if ($pidToUse !== $pageId) {
77 4
            $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

77
            $this->initializeTsfe(/** @scrutinizer ignore-type */ $pidToUse, $language, $rootPageId);
Loading history...
78 4
            $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

78
            $reusedCacheIdentifier = $this->getCacheIdentifier(/** @scrutinizer ignore-type */ $pidToUse, $language, $rootPageId);
Loading history...
79 4
            $this->serverRequestCache[$cacheIdentifier] = $this->serverRequestCache[$reusedCacheIdentifier];
80 4
            $this->tsfeCache[$cacheIdentifier] = $this->tsfeCache[$reusedCacheIdentifier];
81
//            if ($rootPageId === null) {
82
//                // @Todo: Resolve and set TSFE object for $rootPageId.
83
//            }
84 4
            return;
85
        }
86
87
        /* @var Context $context */
88 144
        $context = clone (GeneralUtility::makeInstance(Context::class));
89 144
        $site = $this->siteFinder->getSiteByPageId($pageId);
90
        // $siteLanguage and $languageAspect takes the language id into account.
91
        //   See: $site->getLanguageById($language);
92
        //   Therefore the whole TSFE stack is initialized and must be used as is.
93
        //   Note: ServerRequest, Context, Language, cObj of TSFE MUST NOT be changed or touched in any way,
94
        //         Otherwise the caching of TSFEs makes no sense anymore.
95
        //         If you want something to change in TSFE object, please use cloned one!
96 143
        $siteLanguage = $site->getLanguageById($language);
97 143
        $languageAspect = LanguageAspectFactory::createFromSiteLanguage($siteLanguage);
98 143
        $context->setAspect('language', $languageAspect);
99
100 143
        $serverRequest = $this->serverRequestCache[$cacheIdentifier] ?? null;
101 143
        if (!isset($this->serverRequestCache[$cacheIdentifier])) {
102 143
            $serverRequest = GeneralUtility::makeInstance(ServerRequest::class);
103 143
            $this->serverRequestCache[$cacheIdentifier] = $serverRequest =
104 143
                $serverRequest->withAttribute('site', $site)
105 143
                ->withAttribute('language', $siteLanguage)
106 143
                ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
107 143
                ->withUri($site->getBase());
108
        }
109
110 143
        if (!isset($this->tsfeCache[$cacheIdentifier])) {
111
            // TYPO3 by default enables a preview mode if a backend user is logged in,
112
            // the VisibilityAspect is configured to show hidden elements.
113
            // Due to this setting hidden relations/translations might be indexed
114
            // when running the Solr indexer via the TYPO3 backend.
115
            // To avoid this, the VisibilityAspect is adapted for indexing.
116 143
            $context->setAspect(
117 143
                'visibility',
118 143
                GeneralUtility::makeInstance(
119 143
                    VisibilityAspect::class,
120 143
                    false,
121 143
                    false
122
                )
123
            );
124
125 143
            $feUser = GeneralUtility::makeInstance(FrontendUserAuthentication::class);
126
            // for certain situations we need to trick TSFE into granting us
127
            // access to the page in any case to make getPageAndRootline() work
128
            // see http://forge.typo3.org/issues/42122
129 143
            $pageRecord = BackendUtility::getRecord('pages', $pageId, 'fe_group');
130 143
            $userGroups = [0, -1];
131 143
            if (!empty($pageRecord['fe_group'])) {
132
                $userGroups = array_unique(array_merge($userGroups, explode(',', $pageRecord['fe_group'])));
133
            }
134 143
            $context->setAspect('frontend.user', GeneralUtility::makeInstance(UserAspect::class, $feUser, $userGroups));
135
136
            /* @var PageArguments $pageArguments */
137 143
            $pageArguments = GeneralUtility::makeInstance(PageArguments::class, $pageId, 0, []);
138
139
            /* @var TypoScriptFrontendController $tsfe */
140 143
            $tsfe = GeneralUtility::makeInstance(TypoScriptFrontendController::class, $context, $site, $siteLanguage, $pageArguments, $feUser);
141
142
            // @extensionScannerIgnoreLine
143
            /** Done in {@link \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::settingLanguage} */
144
            //$tsfe->sys_page = GeneralUtility::makeInstance(PageRepository::class);
145
146 143
            $template = GeneralUtility::makeInstance(TemplateService::class, $context, null, $tsfe);
147 143
            $template->tt_track = false;
148 143
            $tsfe->tmpl = $template;
149 143
            $context->setAspect('typoscript', GeneralUtility::makeInstance(TypoScriptAspect::class, true));
150 143
            $tsfe->no_cache = true;
151
152
            try {
153 143
                $tsfe->determineId($serverRequest);
154 143
                $serverRequest->withAttribute('frontend.controller', $tsfe);
155 143
                $tsfe->no_cache = false;
156 143
                $tsfe->getConfigArray($serverRequest);
157
158 143
                $tsfe->newCObj($serverRequest);
159 143
                $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

159
                /** @scrutinizer ignore-call */ 
160
                $tsfe->absRefPrefix = self::getAbsRefPrefixFromTSFE($tsfe);
Loading history...
160 143
                $tsfe->calculateLinkVars([]);
161 5
            } catch (Throwable $exception) {
162
                // @todo: logging
163 5
                $this->serverRequestCache[$cacheIdentifier] = null;
164 5
                $this->tsfeCache[$cacheIdentifier] = null;
165 5
                return;
166
            }
167
168 143
            $this->tsfeCache[$cacheIdentifier] = $tsfe;
169
        }
170
171
        // @todo: Not right place for that action, move on more convenient place: indexing a single item+id+lang.
172 143
        Locales::setSystemLocaleFromSiteLanguage($siteLanguage);
173 143
    }
174
175
    /**
176
     * Returns TypoScriptFrontendController with sand cast context.
177
     *
178
     * @param int $pageId
179
     * @param int $language
180
     *
181
     * @param int|null $rootPageId
182
     *
183
     * @return TypoScriptFrontendController
184
     *
185
     * @throws SiteNotFoundException
186
     * @throws DBALDriverException
187
     * @throws Exception\Exception
188
     */
189 144
    public function getTsfeByPageIdAndLanguageId(int $pageId, int $language = 0, ?int $rootPageId = null): ?TypoScriptFrontendController
190
    {
191 144
        $this->assureIsInitialized($pageId, $language, $rootPageId);
192 143
        return $this->tsfeCache[$this->getCacheIdentifier($pageId, $language, $rootPageId)];
193
    }
194
195
    /**
196
     * Returns TypoScriptFrontendController for first available language id in fallback chain.
197
     *
198
     * Is usable for BE-Modules/CLI-Commands stack only, where the rendered TypoScript configuration
199
     * of EXT:solr* stack is wanted and the language id does not matter.
200
     *
201
     * NOTE: This method MUST NOT be used on indexing context.
202
     *
203
     * @param int $pageId
204
     * @param int ...$languageFallbackChain
205
     * @return TypoScriptFrontendController|null
206
     */
207 141
    public function getTsfeByPageIdAndLanguageFallbackChain(int $pageId, int ...$languageFallbackChain): ?TypoScriptFrontendController
208
    {
209 141
        foreach ($languageFallbackChain as $languageId) {
210
            try {
211 141
                $tsfe = $this->getTsfeByPageIdAndLanguageId($pageId, $languageId);
212 141
                if ($tsfe instanceof TypoScriptFrontendController) {
213 141
                    return $tsfe;
214
                }
215
            } catch (Throwable $e) {
216
                // no needs to log or do anything, the method MUST not return anything if it can't.
217
                continue;
218
            }
219
        }
220 1
        return null;
221
    }
222
223
    /**
224
     * Returns TSFE for first initializable site language.
225
     *
226
     * Is usable for BE-Modules/CLI-Commands stack only, where the rendered TypoScript configuration
227
     * of EXT:solr* stack is wanted and the language id does not matter.
228
     *
229
     * @param int $pageId
230
     * @return TypoScriptFrontendController|null
231
     */
232 33
    public function getTsfeByPageIdIgnoringLanguage(int $pageId): ?TypoScriptFrontendController
233
    {
234
        try {
235 33
            $typo3Site = $this->siteFinder->getSiteByPageId($pageId);
236
        } catch (Throwable $e)
237
        {
238
            return null;
239
        }
240 33
        $availableLanguageIds = array_map(function($siteLanguage) {
241 33
            return $siteLanguage->getLanguageId();
242 33
        }, $typo3Site->getLanguages());
243
244 33
        if (empty($availableLanguageIds)) {
245
            return null;
246
        }
247 33
        return $this->getTsfeByPageIdAndLanguageFallbackChain($pageId, ...$availableLanguageIds);
248
    }
249
250
    /**
251
     * Returns TypoScriptFrontendController with sand cast context.
252
     *
253
     * @param int $pageId
254
     * @param int $language
255
     *
256
     * @param int|null $rootPageId
257
     *
258
     * @return ServerRequest
259
     *
260
     * @throws SiteNotFoundException
261
     * @throws DBALDriverException
262
     * @throws Exception\Exception
263
     * @noinspection PhpUnused
264
     */
265
    public function getServerRequestForTsfeByPageIdAndLanguageId(int $pageId, int $language = 0, ?int $rootPageId = null): ?ServerRequest
266
    {
267
        $this->assureIsInitialized($pageId, $language, $rootPageId);
268
        return $this->serverRequestCache[$this->getCacheIdentifier($pageId, $language, $rootPageId)];
269
    }
270
271
    /**
272
     * Initializes the TSFE, ServerRequest, Context if not already done.
273
     *
274
     * @param int $pageId
275
     * @param int $language
276
     *
277
     * @param int|null $rootPageId
278
     *
279
     * @throws DBALDriverException
280
     * @throws SiteNotFoundException
281
     * @throws Exception\Exception
282
     */
283 144
    protected function assureIsInitialized(int $pageId, int $language, ?int $rootPageId = null): void
284
    {
285 144
        $cacheIdentifier = $this->getCacheIdentifier($pageId, $language, $rootPageId);
286 144
        if(!array_key_exists($cacheIdentifier, $this->tsfeCache)) {
287 144
            $this->initializeTsfe($pageId, $language, $rootPageId);
288 143
            return;
289
        }
290 140
        if ($this->tsfeCache[$cacheIdentifier] instanceof TypoScriptFrontendController) {
291 140
            $this->tsfeCache[$cacheIdentifier]->newCObj($this->serverRequestCache[$cacheIdentifier]);
292
        }
293 140
    }
294
295
    /**
296
     * Returns the cache identifier for cached TSFE and ServerRequest objects.
297
     *
298
     * @param int $pageId
299
     * @param int $language
300
     *
301
     * @param int|null $rootPageId
302
     *
303
     * @return string
304
     */
305 144
    protected function getCacheIdentifier(int $pageId, int $language, ?int $rootPageId = null): string
306
    {
307 144
        return 'root:' . ($rootPageId ?? 'null') . '|page:' . $pageId . '|lang:' . $language;
308
    }
309
310
    /**
311
     * The TSFE can not be initialized for Spacer and sys-folders.
312
     * See: "Spacer and sys folders is not accessible in frontend" on {@link \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::getPageAndRootline()}
313
     *
314
     * Note: The requested $pidToUse can be one of configured plugin.tx_solr.index.queue.[indexConfig].additionalPageIds.
315
     *
316
     * @param int $pidToUse
317
     *
318
     * @param int|null $rootPageId
319
     *
320
     * @return int
321
     * @throws DBALDriverException
322
     * @throws Exception\Exception
323
     */
324 144
    protected function getPidToUseForTsfeInitialization(int $pidToUse, ?int $rootPageId = null): ?int
325
    {
326
        // handle plugin.tx_solr.index.queue.[indexConfig].additionalPageIds
327 144
        if (isset($rootPageId) && !$this->isRequestedPageAPartOfRequestedSite($pidToUse)) {
328 19
            return $rootPageId;
329
        }
330 144
        $pageRecord = BackendUtility::getRecord('pages', $pidToUse);
331 144
        $isSpacerOrSysfolder = ($pageRecord['doktype'] ?? null) == PageRepository::DOKTYPE_SPACER || ($pageRecord['doktype'] ?? null) == PageRepository::DOKTYPE_SYSFOLDER;
332 144
        if ($isSpacerOrSysfolder === false) {
333 144
            return $pidToUse;
334
        }
335
        /* @var ConfigurationPageResolver $configurationPageResolve */
336 4
        $configurationPageResolver = GeneralUtility::makeInstance(ConfigurationPageResolver::class);
337 4
        $askedPid = $pidToUse;
338 4
        $pidToUse = $configurationPageResolver->getClosestPageIdWithActiveTemplate($pidToUse);
339 4
        if (!isset($pidToUse) && !isset($rootPageId)) {
340 2
            throw new Exception\Exception(
341 2
                "The closest page with active template to page \"$askedPid\" could not be resolved and alternative rootPageId is not provided.",
342 2
                1637339439
343
            );
344 2
        } else if (isset($rootPageId)) {
345
            return $rootPageId;
346
        }
347 2
        return $this->getPidToUseForTsfeInitialization($pidToUse, $rootPageId);
348
    }
349
350
    /**
351
     * Checks if the requested page belongs to site of given root page.
352
     *
353
     * @param int $pageId
354
     * @param int|null $rootPageId
355
     *
356
     * @return bool
357
     */
358 19
    protected function isRequestedPageAPartOfRequestedSite(int $pageId, ?int $rootPageId = null): bool
359
    {
360 19
        if (!isset($rootPageId)) {
361 19
            return false;
362
        }
363
        try {
364
            $site = $this->siteFinder->getSiteByPageId($pageId);
365
        } catch (SiteNotFoundException $e) {
366
            return false;
367
        }
368
        return $rootPageId === $site->getRootPageId();
369
    }
370
371
    /**
372
     * Resolves the configured absRefPrefix to a valid value and resolved if absRefPrefix
373
     * is set to "auto".
374
     */
375 143
    private function getAbsRefPrefixFromTSFE(TypoScriptFrontendController $TSFE): string
376
    {
377 143
        $absRefPrefix = '';
378 143
        if (empty($TSFE->config['config']['absRefPrefix'])) {
379
            return $absRefPrefix;
380
        }
381
382 143
        $absRefPrefix = trim($TSFE->config['config']['absRefPrefix']);
383 143
        if ($absRefPrefix === 'auto') {
384 143
            $absRefPrefix = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
385
        }
386
387 143
        return $absRefPrefix;
388
    }
389
}
390