TypoScriptFrontendController   F
last analyzed

Complexity

Total Complexity 449

Size/Duplication

Total Lines 3487
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 449
eloc 1273
dl 0
loc 3487
rs 0.8
c 0
b 0
f 0

98 Methods

Rating   Name   Duplication   Size   Complexity  
A resolveTranslatedPageId() 0 11 3
A isBackendUserLoggedIn() 0 3 1
A isUserOrGroupSet() 0 5 1
B fetch_the_id() 0 68 8
A checkIfLoginAllowedInBranch() 0 22 5
A unsetBackendUser() 0 6 1
A initPageRenderer() 0 11 3
B applyPreviewSettings() 0 23 7
B determineId() 0 49 10
A initCaches() 0 3 1
A checkPagerecordForIncludeSection() 0 3 2
C determineIdIsHiddenPage() 0 59 12
A clear_preview() 0 8 2
A initUserGroups() 0 4 1
A initializeContext() 0 5 2
B checkRootlineForIncludeSection() 0 55 8
B checkEnableFields() 0 17 10
A checkPageGroupAccess() 0 6 2
A setContentType() 0 3 1
A __construct() 0 12 1
C getPageAccessFailureReasons() 0 31 15
A getPageAndRootlineWithDomain() 0 18 6
F getPageAndRootline() 0 150 30
B generatePageTitle() 0 37 8
A updateRootLinesWithTranslations() 0 6 2
B applyHttpHeadersToResponse() 0 32 9
A checkTranslatedShortcut() 0 13 4
A whichWorkspace() 0 3 1
A isINTincScript() 0 3 2
D settingLanguage() 0 121 21
F getConfigArray() 0 115 22
A recursivelyReplaceIntPlaceholdersInContent() 0 10 2
A populatePageDataFromCache() 0 31 4
A isGeneratePage() 0 3 1
B getCacheHeaders() 0 41 11
B setAbsRefPrefix() 0 37 7
B processNonCacheableContentPartsAndSubstituteContentMarkers() 0 42 8
A resolveContentPid() 0 12 3
A setSysLastChanged() 0 14 3
B getFromCache() 0 78 11
A initializeSearchWordData() 0 13 5
A generatePage_preProcessing() 0 8 1
A baseUrlWrap() 0 9 4
A headerNoCache() 0 19 6
A INTincScript_loadJSCode() 0 14 3
A realPageCacheContent() 0 19 4
A addCacheTags() 0 3 1
A setPageCacheContent() 0 27 3
A setRegisterValueForSysLastChanged() 0 5 2
A newCObj() 0 4 1
A getRedirectUriForShortcut() 0 7 3
A getPageCacheTags() 0 3 1
A getUriToCurrentPageForRedirect() 0 12 3
A getWebsiteTitle() 0 14 5
A getLockHash() 0 4 1
A generatePage_postProcessing() 0 28 6
A getFromCache_queryRow() 0 6 1
A printTitle() 0 17 6
A splitLinkVarsString() 0 13 1
D calculateLinkVars() 0 59 18
A INTincScript() 0 38 3
A getPagesTSconfig() 0 18 2
A getHash() 0 3 1
C isAllowedLinkVarValue() 0 33 13
A releaseLocks() 0 4 1
A getRedirectUriForMountPoint() 0 7 3
A getRelevantParametersForCachingFromPageArguments() 0 9 3
A createHashBase() 0 32 3
F preparePageContentGeneration() 0 64 14
A clearPageCacheContent() 0 3 1
A setPageArguments() 0 9 3
A logDeprecatedTyposcript() 0 5 2
A doWorkspacePreview() 0 3 1
A uniqueHash() 0 3 1
A isStaticCacheble() 0 3 3
A getAdditionalHeaders() 0 23 6
A getCurrentPageCacheConfiguration() 0 10 3
A set_cache_timeout_default() 0 5 2
A acquireLock() 0 35 5
A getGlobalInstance() 0 15 4
A getLanguage() 0 3 1
B get_cache_timeout() 0 36 6
A sL() 0 3 1
B set_no_cache() 0 31 7
A convOutputCharset() 0 12 3
A setOutputLanguage() 0 5 1
A logPageAccessFailure() 0 7 2
B getFirstTimeValueForRecord() 0 68 10
A getBackendUser() 0 3 1
A getSite() 0 3 1
A getTimeTracker() 0 3 1
A releaseLock() 0 13 3
A isInPreviewMode() 0 6 4
A disableCache() 0 3 1
A getContext() 0 3 1
A getRequestedId() 0 3 2
A calculatePageCacheTimeout() 0 16 3
A getPageArguments() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like TypoScriptFrontendController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TypoScriptFrontendController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Frontend\Controller;
17
18
use Psr\Http\Message\ResponseInterface;
19
use Psr\Http\Message\ServerRequestInterface;
20
use Psr\Log\LoggerAwareInterface;
21
use Psr\Log\LoggerAwareTrait;
22
use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
23
use TYPO3\CMS\Core\Cache\CacheManager;
24
use TYPO3\CMS\Core\Charset\CharsetConverter;
25
use TYPO3\CMS\Core\Charset\UnknownCharsetException;
26
use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader;
27
use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser;
28
use TYPO3\CMS\Core\Context\Context;
29
use TYPO3\CMS\Core\Context\DateTimeAspect;
30
use TYPO3\CMS\Core\Context\LanguageAspect;
31
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
32
use TYPO3\CMS\Core\Context\UserAspect;
33
use TYPO3\CMS\Core\Context\VisibilityAspect;
34
use TYPO3\CMS\Core\Context\WorkspaceAspect;
35
use TYPO3\CMS\Core\Core\Environment;
36
use TYPO3\CMS\Core\Database\Connection;
37
use TYPO3\CMS\Core\Database\ConnectionPool;
38
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
39
use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
40
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
41
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
42
use TYPO3\CMS\Core\Error\Http\AbstractServerErrorException;
43
use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
44
use TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException;
45
use TYPO3\CMS\Core\Exception\Page\RootLineException;
46
use TYPO3\CMS\Core\Http\ApplicationType;
47
use TYPO3\CMS\Core\Http\NormalizedParams;
48
use TYPO3\CMS\Core\Http\PropagateResponseException;
49
use TYPO3\CMS\Core\Http\ServerRequestFactory;
50
use TYPO3\CMS\Core\Localization\LanguageService;
51
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
52
use TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException;
53
use TYPO3\CMS\Core\Locking\LockFactory;
54
use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
55
use TYPO3\CMS\Core\Page\AssetCollector;
56
use TYPO3\CMS\Core\Page\PageRenderer;
57
use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager;
58
use TYPO3\CMS\Core\Resource\Exception;
59
use TYPO3\CMS\Core\Resource\StorageRepository;
60
use TYPO3\CMS\Core\Routing\PageArguments;
61
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
62
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
63
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
64
use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility;
65
use TYPO3\CMS\Core\Type\Bitmask\Permission;
66
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
67
use TYPO3\CMS\Core\TypoScript\TemplateService;
68
use TYPO3\CMS\Core\Utility\ArrayUtility;
69
use TYPO3\CMS\Core\Utility\GeneralUtility;
70
use TYPO3\CMS\Core\Utility\HttpUtility;
71
use TYPO3\CMS\Core\Utility\MathUtility;
72
use TYPO3\CMS\Core\Utility\PathUtility;
73
use TYPO3\CMS\Core\Utility\RootlineUtility;
74
use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
75
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
76
use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
77
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
78
use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
79
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
80
use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
81
82
/**
83
 * Class for the built TypoScript based frontend. Instantiated in
84
 * \TYPO3\CMS\Frontend\Http\RequestHandler as the global object TSFE.
85
 *
86
 * Main frontend class, instantiated in \TYPO3\CMS\Frontend\Http\RequestHandler
87
 * as the global object TSFE.
88
 *
89
 * This class has a lot of functions and internal variable which are used from
90
 * \TYPO3\CMS\Frontend\Http\RequestHandler
91
 *
92
 * The class is instantiated as $GLOBALS['TSFE'] in \TYPO3\CMS\Frontend\Http\RequestHandler.
93
 *
94
 * The use of this class should be inspired by the order of function calls as
95
 * found in \TYPO3\CMS\Frontend\Http\RequestHandler.
96
 */
97
class TypoScriptFrontendController implements LoggerAwareInterface
98
{
99
    use LoggerAwareTrait;
100
101
    /**
102
     * The page id (int)
103
     * @var string|int
104
     */
105
    public $id = '';
106
107
    /**
108
     * The type (read-only)
109
     * @var int|string
110
     */
111
    public $type = '';
112
113
    /**
114
     * @var SiteInterface
115
     */
116
    protected $site;
117
118
    /**
119
     * @var SiteLanguage
120
     */
121
    protected $language;
122
123
    /**
124
     * @var PageArguments
125
     * @internal
126
     */
127
    protected $pageArguments;
128
129
    /**
130
     * Page will not be cached. Write only TRUE. Never clear value (some other
131
     * code might have reasons to set it TRUE).
132
     * @var bool
133
     */
134
    public $no_cache = false;
135
136
    /**
137
     * The rootLine (all the way to tree root, not only the current site!)
138
     * @var array
139
     */
140
    public $rootLine = [];
141
142
    /**
143
     * The pagerecord
144
     * @var array
145
     */
146
    public $page = [];
147
148
    /**
149
     * This will normally point to the same value as id, but can be changed to
150
     * point to another page from which content will then be displayed instead.
151
     * @var int
152
     */
153
    public $contentPid = 0;
154
155
    /**
156
     * Gets set when we are processing a page of type mounpoint with enabled overlay in getPageAndRootline()
157
     * Used later in checkPageForMountpointRedirect() to determine the final target URL where the user
158
     * should be redirected to.
159
     *
160
     * @var array|null
161
     */
162
    protected $originalMountPointPage;
163
164
    /**
165
     * Gets set when we are processing a page of type shortcut in the early stages
166
     * of the request when we do not know about languages yet, used later in the request
167
     * to determine the correct shortcut in case a translation changes the shortcut
168
     * target
169
     * @var array|null
170
     * @see checkTranslatedShortcut()
171
     */
172
    protected $originalShortcutPage;
173
174
    /**
175
     * sys_page-object, pagefunctions
176
     *
177
     * @var PageRepository|string
178
     */
179
    public $sys_page = '';
180
181
    /**
182
     * Is set to 1 if a pageNotFound handler could have been called.
183
     * @var int
184
     * @internal
185
     */
186
    public $pageNotFound = 0;
187
188
    /**
189
     * Array containing a history of why a requested page was not accessible.
190
     * @var array
191
     */
192
    protected $pageAccessFailureHistory = [];
193
194
    /**
195
     * @var string
196
     * @internal
197
     */
198
    public $MP = '';
199
200
    /**
201
     * The frontend user
202
     *
203
     * @var FrontendUserAuthentication
204
     */
205
    public $fe_user;
206
207
    /**
208
     * Shows whether logins are allowed in branch
209
     * @var bool
210
     */
211
    protected $loginAllowedInBranch = true;
212
213
    /**
214
     * Shows specific mode (all or groups)
215
     * @var string
216
     * @internal
217
     */
218
    protected $loginAllowedInBranch_mode = '';
219
220
    /**
221
     * Value that contains the simulated usergroup if any
222
     * @var int
223
     * @internal only to be used in AdminPanel, and within TYPO3 Core
224
     */
225
    public $simUserGroup = 0;
226
227
    /**
228
     * "CONFIG" object from TypoScript. Array generated based on the TypoScript
229
     * configuration of the current page. Saved with the cached pages.
230
     * @var array
231
     */
232
    public $config = [];
233
234
    /**
235
     * The TypoScript template object. Used to parse the TypoScript template
236
     *
237
     * @var TemplateService
238
     */
239
    public $tmpl;
240
241
    /**
242
     * Is set to the time-to-live time of cached pages. Default is 60*60*24, which is 24 hours.
243
     *
244
     * @var int
245
     * @internal
246
     */
247
    protected $cacheTimeOutDefault = 86400;
248
249
    /**
250
     * Set internally if cached content is fetched from the database.
251
     *
252
     * @var bool
253
     * @internal
254
     */
255
    protected $cacheContentFlag = false;
256
257
    /**
258
     * Set to the expire time of cached content
259
     * @var int
260
     * @internal
261
     */
262
    protected $cacheExpires = 0;
263
264
    /**
265
     * Set if cache headers allowing caching are sent.
266
     * @var bool
267
     * @internal
268
     */
269
    protected $isClientCachable = false;
270
271
    /**
272
     * Used by template fetching system. This array is an identification of
273
     * the template. If $this->all is empty it's because the template-data is not
274
     * cached, which it must be.
275
     * @var array
276
     * @internal
277
     */
278
    public $all = [];
279
280
    /**
281
     * Toplevel - objArrayName, eg 'page'
282
     * @var string
283
     * @internal should only be used by TYPO3 Core
284
     */
285
    public $sPre = '';
286
287
    /**
288
     * TypoScript configuration of the page-object pointed to by sPre.
289
     * $this->tmpl->setup[$this->sPre.'.']
290
     * @var array|string
291
     * @internal should only be used by TYPO3 Core
292
     */
293
    public $pSetup = '';
294
295
    /**
296
     * This hash is unique to the template, the $this->id and $this->type vars and
297
     * the list of groups. Used to get and later store the cached data
298
     * @var string
299
     * @internal
300
     */
301
    public $newHash = '';
302
303
    /**
304
     * This flag is set before the page is generated IF $this->no_cache is set. If this
305
     * flag is set after the page content was generated, $this->no_cache is forced to be set.
306
     * This is done in order to make sure that PHP code from Plugins / USER scripts does not falsely
307
     * clear the no_cache flag.
308
     * @var bool
309
     * @internal
310
     */
311
    protected $no_cacheBeforePageGen = false;
312
313
    /**
314
     * May be set to the pagesTSconfig
315
     * @var array|string
316
     * @internal
317
     */
318
    protected $pagesTSconfig = '';
319
320
    /**
321
     * Eg. insert JS-functions in this array ($additionalHeaderData) to include them
322
     * once. Use associative keys.
323
     *
324
     * Keys in use:
325
     *
326
     * used to accumulate additional HTML-code for the header-section,
327
     * <head>...</head>. Insert either associative keys (like
328
     * additionalHeaderData['myStyleSheet'], see reserved keys above) or num-keys
329
     * (like additionalHeaderData[] = '...')
330
     *
331
     * @var array
332
     */
333
    public $additionalHeaderData = [];
334
335
    /**
336
     * Used to accumulate additional HTML-code for the footer-section of the template
337
     * @var array
338
     */
339
    public $additionalFooterData = [];
340
341
    /**
342
     * Default internal target
343
     * @var string
344
     */
345
    public $intTarget = '';
346
347
    /**
348
     * Default external target
349
     * @var string
350
     */
351
    public $extTarget = '';
352
353
    /**
354
     * Default file link target
355
     * @var string
356
     */
357
    public $fileTarget = '';
358
359
    /**
360
     * If set, typolink() function encrypts email addresses.
361
     * @var string|int
362
     */
363
    public $spamProtectEmailAddresses = 0;
364
365
    /**
366
     * Absolute Reference prefix
367
     * @var string
368
     */
369
    public $absRefPrefix = '';
370
371
    /**
372
     * <A>-tag parameters
373
     * @var string
374
     */
375
    public $ATagParams = '';
376
377
    /**
378
     * Search word regex, calculated if there has been search-words send. This is
379
     * used to mark up the found search words on a page when jumped to from a link
380
     * in a search-result.
381
     * @var string
382
     * @internal
383
     */
384
    public $sWordRegEx = '';
385
386
    /**
387
     * Is set to the incoming array sword_list in case of a page-view jumped to from
388
     * a search-result.
389
     * @var string
390
     * @internal
391
     */
392
    public $sWordList = '';
393
394
    /**
395
     * A string prepared for insertion in all links on the page as url-parameters.
396
     * Based on configuration in TypoScript where you defined which GET_VARS you
397
     * would like to pass on.
398
     * @var string
399
     */
400
    public $linkVars = '';
401
402
    /**
403
     * If set, edit icons are rendered aside content records. Must be set only if
404
     * the ->beUserLogin flag is set and set_no_cache() must be called as well.
405
     * @var string
406
     */
407
    public $displayEditIcons = '';
408
409
    /**
410
     * If set, edit icons are rendered aside individual fields of content. Must be
411
     * set only if the ->beUserLogin flag is set and set_no_cache() must be called as
412
     * well.
413
     * @var string
414
     */
415
    public $displayFieldEditIcons = '';
416
417
    /**
418
     * 'Global' Storage for various applications. Keys should be 'tx_'.extKey for
419
     * extensions.
420
     * @var array
421
     */
422
    public $applicationData = [];
423
424
    /**
425
     * @var array
426
     */
427
    public $register = [];
428
429
    /**
430
     * Stack used for storing array and retrieving register arrays (see
431
     * LOAD_REGISTER and RESTORE_REGISTER)
432
     * @var array
433
     */
434
    public $registerStack = [];
435
436
    /**
437
     * Checking that the function is not called eternally. This is done by
438
     * interrupting at a depth of 50
439
     * @var int
440
     */
441
    public $cObjectDepthCounter = 50;
442
443
    /**
444
     * Used by RecordContentObject and ContentContentObject to ensure the a records is NOT
445
     * rendered twice through it!
446
     * @var array
447
     */
448
    public $recordRegister = [];
449
450
    /**
451
     * This is set to the [table]:[uid] of the latest record rendered. Note that
452
     * class ContentObjectRenderer has an equal value, but that is pointing to the
453
     * record delivered in the $data-array of the ContentObjectRenderer instance, if
454
     * the cObjects CONTENT or RECORD created that instance
455
     * @var string
456
     */
457
    public $currentRecord = '';
458
459
    /**
460
     * Used by class \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject
461
     * to keep track of access-keys.
462
     * @var array
463
     */
464
    public $accessKey = [];
465
466
    /**
467
     * Used to generate page-unique keys. Point is that uniqid() functions is very
468
     * slow, so a unikey key is made based on this, see function uniqueHash()
469
     * @var int
470
     * @internal
471
     */
472
    protected $uniqueCounter = 0;
473
474
    /**
475
     * @var string
476
     * @internal
477
     */
478
    protected $uniqueString = '';
479
480
    /**
481
     * This value will be used as the title for the page in the indexer (if
482
     * indexing happens)
483
     * @var string
484
     * @internal only used by TYPO3 Core, use PageTitle API instead.
485
     */
486
    public $indexedDocTitle = '';
487
488
    /**
489
     * The base URL set for the page header.
490
     * @var string
491
     */
492
    public $baseUrl = '';
493
494
    /**
495
     * Page content render object
496
     *
497
     * @var ContentObjectRenderer
498
     */
499
    public $cObj;
500
501
    /**
502
     * All page content is accumulated in this variable. See RequestHandler
503
     * @var string
504
     */
505
    public $content = '';
506
507
    /**
508
     * Output charset of the websites content. This is the charset found in the
509
     * header, meta tag etc. If different than utf-8 a conversion
510
     * happens before output to browser. Defaults to utf-8.
511
     * @var string
512
     */
513
    public $metaCharset = 'utf-8';
514
515
    /**
516
     * Internal calculations for labels
517
     *
518
     * @var LanguageService
519
     */
520
    protected $languageService;
521
522
    /**
523
     * @var LockingStrategyInterface[][]
524
     */
525
    protected $locks = [];
526
527
    /**
528
     * @var PageRenderer
529
     */
530
    protected $pageRenderer;
531
532
    /**
533
     * The page cache object, use this to save pages to the cache and to
534
     * retrieve them again
535
     *
536
     * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
537
     */
538
    protected $pageCache;
539
540
    /**
541
     * @var array
542
     */
543
    protected $pageCacheTags = [];
544
545
    /**
546
     * Content type HTTP header being sent in the request.
547
     * @todo Ticket: #63642 Should be refactored to a request/response model later
548
     * @internal Should only be used by TYPO3 core for now
549
     *
550
     * @var string
551
     */
552
    protected $contentType = 'text/html';
553
554
    /**
555
     * Doctype to use
556
     *
557
     * @var string
558
     */
559
    public $xhtmlDoctype = '';
560
561
    /**
562
     * @var int
563
     */
564
    public $xhtmlVersion;
565
566
    /**
567
     * Originally requested id from the initial $_GET variable
568
     *
569
     * @var int
570
     */
571
    protected $requestedId;
572
573
    /**
574
     * The context for keeping the current state, mostly related to current page information,
575
     * backend user / frontend user access, workspaceId
576
     *
577
     * @var Context
578
     */
579
    protected $context;
580
581
    /**
582
     * Since TYPO3 v10.0, TSFE is composed out of
583
     *  - Context
584
     *  - Site
585
     *  - SiteLanguage
586
     *  - PageArguments (containing ID, Type, cHash and MP arguments)
587
     *
588
     * Also sets a unique string (->uniqueString) for this script instance; A md5 hash of the microtime()
589
     *
590
     * @param Context $context the Context object to work with
591
     * @param SiteInterface $site The resolved site to work with
592
     * @param SiteLanguage $siteLanguage The resolved language to work with
593
     * @param PageArguments $pageArguments The PageArguments object containing Page ID, type and GET parameters
594
     * @param FrontendUserAuthentication $frontendUser a FrontendUserAuthentication object
595
     */
596
    public function __construct(Context $context, SiteInterface $site, SiteLanguage $siteLanguage, PageArguments $pageArguments, FrontendUserAuthentication $frontendUser)
597
    {
598
        $this->initializeContext($context);
599
        $this->site = $site;
600
        $this->language = $siteLanguage;
601
        $this->setPageArguments($pageArguments);
602
        $this->fe_user = $frontendUser;
603
        $this->uniqueString = md5(microtime());
604
        $this->initPageRenderer();
605
        $this->initCaches();
606
        // Initialize LLL behaviour
607
        $this->setOutputLanguage();
608
    }
609
610
    private function initializeContext(Context $context): void
611
    {
612
        $this->context = $context;
613
        if (!$this->context->hasAspect('frontend.preview')) {
614
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class));
615
        }
616
    }
617
618
    /**
619
     * Initializes the page renderer object
620
     */
621
    protected function initPageRenderer()
622
    {
623
        if ($this->pageRenderer !== null) {
624
            return;
625
        }
626
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
627
        $this->pageRenderer->setTemplateFile('EXT:frontend/Resources/Private/Templates/MainPage.html');
628
        // As initPageRenderer could be called in constructor and for USER_INTs, this information is only set
629
        // once - in order to not override any previous settings of PageRenderer.
630
        if ($this->language instanceof SiteLanguage) {
0 ignored issues
show
introduced by
$this->language is always a sub-type of TYPO3\CMS\Core\Site\Entity\SiteLanguage.
Loading history...
631
            $this->pageRenderer->setLanguage($this->language->getTypo3Language());
632
        }
633
    }
634
635
    /**
636
     * @param string $contentType
637
     * @internal Should only be used by TYPO3 core for now
638
     */
639
    public function setContentType($contentType)
640
    {
641
        $this->contentType = $contentType;
642
    }
643
644
    /********************************************
645
     *
646
     * Initializing, resolving page id
647
     *
648
     ********************************************/
649
    /**
650
     * Initializes the caching system.
651
     */
652
    protected function initCaches()
653
    {
654
        $this->pageCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('pages');
655
    }
656
657
    /**
658
     * Initializes the front-end user groups.
659
     * Sets frontend.user aspect based on front-end user status.
660
     */
661
    public function initUserGroups()
662
    {
663
        $userAspect = $this->fe_user->createUserAspect((bool)$this->loginAllowedInBranch);
664
        $this->context->setAspect('frontend.user', $userAspect);
665
    }
666
667
    /**
668
     * Checking if a user is logged in or a group constellation different from "0,-1"
669
     *
670
     * @return bool TRUE if either a login user is found (array fe_user->user) OR if the gr_list is set to something else than '0,-1' (could be done even without a user being logged in!)
671
     */
672
    public function isUserOrGroupSet()
673
    {
674
        /** @var UserAspect $userAspect */
675
        $userAspect = $this->context->getAspect('frontend.user');
676
        return $userAspect->isUserOrGroupSet();
677
    }
678
679
    /**
680
     * Clears the preview-flags, sets sim_exec_time to current time.
681
     * Hidden pages must be hidden as default, $GLOBALS['SIM_EXEC_TIME'] is set to $GLOBALS['EXEC_TIME']
682
     * in bootstrap initializeGlobalTimeVariables(). Alter it by adding or subtracting seconds.
683
     */
684
    public function clear_preview()
685
    {
686
        if ($this->isInPreviewMode()) {
687
            $GLOBALS['SIM_EXEC_TIME'] = $GLOBALS['EXEC_TIME'];
688
            $GLOBALS['SIM_ACCESS_TIME'] = $GLOBALS['ACCESS_TIME'];
689
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class));
690
            $this->context->setAspect('date', GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $GLOBALS['SIM_EXEC_TIME'])));
691
            $this->context->setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class));
692
        }
693
    }
694
695
    /**
696
     * Checks if a backend user is logged in
697
     *
698
     * @return bool whether a backend user is logged in
699
     */
700
    public function isBackendUserLoggedIn()
701
    {
702
        return (bool)$this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false);
703
    }
704
705
    /**
706
     * Determines the id and evaluates any preview settings
707
     * Basically this function is about determining whether a backend user is logged in,
708
     * if he has read access to the page and if he's previewing the page.
709
     * That all determines which id to show and how to initialize the id.
710
     *
711
     * @param ServerRequestInterface|null $request
712
     */
713
    public function determineId(ServerRequestInterface $request = null)
714
    {
715
        $request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals();
716
        // Call pre processing function for id determination
717
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PreProcessing'] ?? [] as $functionReference) {
718
            $parameters = ['parentObject' => $this];
719
            GeneralUtility::callUserFunction($functionReference, $parameters, $this);
720
        }
721
        // If there is a Backend login we are going to check for any preview settings
722
        $originalFrontendUserGroups = $this->applyPreviewSettings($this->getBackendUser());
0 ignored issues
show
Bug introduced by
The method getBackendUser() does not exist on null. ( Ignorable by Annotation )

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

722
        $originalFrontendUserGroups = $this->applyPreviewSettings($this->/** @scrutinizer ignore-call */ getBackendUser());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
723
        // If the front-end is showing a preview, caching MUST be disabled.
724
        $isPreview = $this->isInPreviewMode();
725
        if ($isPreview) {
726
            $this->disableCache();
727
        }
728
        // Now, get the id, validate access etc:
729
        $this->fetch_the_id($request);
730
        // Check if backend user has read access to this page. If not, recalculate the id.
731
        if ($this->isBackendUserLoggedIn() && $isPreview && !$this->getBackendUser()->doesUserHaveAccess($this->page, Permission::PAGE_SHOW)) {
732
            $this->unsetBackendUser();
733
            // Resetting
734
            $this->clear_preview();
735
            $this->fe_user->user[$this->fe_user->usergroup_column] = $originalFrontendUserGroups;
736
            // Fetching the id again, now with the preview settings reset.
737
            $this->fetch_the_id($request);
738
        }
739
        // Checks if user logins are blocked for a certain branch and if so, will unset user login and re-fetch ID.
740
        $this->loginAllowedInBranch = $this->checkIfLoginAllowedInBranch();
741
        // Logins are not allowed, but there is a login, so will we run this.
742
        if (!$this->loginAllowedInBranch && $this->isUserOrGroupSet()) {
743
            // Clear out user, and the group will be re-set in >initUserGroups() due to
744
            // $this->loginAllowedInBranch = false
745
            if ($this->loginAllowedInBranch_mode === 'all') {
746
                $this->fe_user->hideActiveLogin();
747
            }
748
            // Fetching the id again, now with the preview settings reset and respecting $this->loginAllowedInBranch = false
749
            $this->fetch_the_id($request);
750
        }
751
        // Final cleaning.
752
        // Make sure it's an integer
753
        $this->id = ($this->contentPid = (int)$this->id);
754
        // Make sure it's an integer
755
        $this->type = (int)$this->type;
756
        // Setting language and fetch translated page
757
        $this->settingLanguage($request);
758
        // Call post processing function for id determination:
759
        $_params = ['pObj' => &$this];
760
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc'] ?? [] as $_funcRef) {
761
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
762
        }
763
    }
764
765
    protected function unsetBackendUser(): void
766
    {
767
        // Register an empty backend user as aspect
768
        unset($GLOBALS['BE_USER']);
769
        $this->context->setAspect('backend.user', GeneralUtility::makeInstance(UserAspect::class));
770
        $this->context->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class));
771
    }
772
773
    /**
774
     * Evaluates admin panel or workspace settings to see if
775
     * visibility settings like
776
     * - Preview Aspect: isPreview
777
     * - Visibility Aspect: includeHiddenPages
778
     * - Visibility Aspect: includeHiddenContent
779
     * - $simUserGroup
780
     * should be applied to the current object.
781
     *
782
     * @param FrontendBackendUserAuthentication $backendUser
783
     * @return string|null null if no changes to the current frontend usergroups have been made, otherwise the original list of frontend usergroups
784
     * @internal
785
     */
786
    protected function applyPreviewSettings($backendUser = null)
787
    {
788
        if (!$backendUser) {
789
            return null;
790
        }
791
        $originalFrontendUserGroup = null;
792
        if ($this->fe_user->user) {
793
            $originalFrontendUserGroup = $this->context->getPropertyFromAspect('frontend.user', 'groupIds');
794
        }
795
796
        // The preview flag is set if the current page turns out to be hidden
797
        if ($this->id && $this->determineIdIsHiddenPage()) {
798
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, true));
799
            /** @var VisibilityAspect $aspect */
800
            $aspect = $this->context->getAspect('visibility');
801
            $newAspect = GeneralUtility::makeInstance(VisibilityAspect::class, true, $aspect->includeHiddenContent(), $aspect->includeDeletedRecords());
802
            $this->context->setAspect('visibility', $newAspect);
803
        }
804
        // The preview flag will be set if an offline workspace will be previewed
805
        if ($this->whichWorkspace() > 0) {
806
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, true));
807
        }
808
        return $this->context->getPropertyFromAspect('frontend.preview', 'preview', false) ? $originalFrontendUserGroup : null;
809
    }
810
811
    /**
812
     * Checks if the page is hidden in the active workspace.
813
     * If it is hidden, preview flags will be set.
814
     *
815
     * @return bool
816
     */
817
    protected function determineIdIsHiddenPage()
818
    {
819
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
820
            ->getQueryBuilderForTable('pages');
821
        $queryBuilder
822
            ->getRestrictions()
823
            ->removeAll()
824
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
825
826
        $queryBuilder
827
            ->select('uid', 'hidden', 'starttime', 'endtime')
828
            ->from('pages')
829
            ->where(
830
                $queryBuilder->expr()->gte('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
831
            )
832
            ->setMaxResults(1);
833
834
        // $this->id always points to the ID of the default language page, so we check
835
        // the current site language to determine if we need to fetch a translation but consider fallbacks
836
        if ($this->language->getLanguageId() > 0) {
837
            $languagesToCheck = array_merge([$this->language->getLanguageId()], $this->language->getFallbackLanguageIds());
838
            // Check for the language and all its fallbacks
839
            $constraint = $queryBuilder->expr()->andX(
840
                $queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)),
841
                $queryBuilder->expr()->in('sys_language_uid', $queryBuilder->createNamedParameter(array_filter($languagesToCheck), Connection::PARAM_INT_ARRAY))
842
            );
843
            // If the fallback language Ids also contains the default language, this needs to be considered
844
            if (in_array(0, $languagesToCheck, true)) {
845
                $constraint = $queryBuilder->expr()->orX(
846
                    $constraint,
847
                    // Ensure to also fetch the default record
848
                    $queryBuilder->expr()->andX(
849
                        $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)),
850
                        $queryBuilder->expr()->in('sys_language_uid', 0)
851
                    )
852
                );
853
            }
854
            // Ensure that the translated records are shown first (maxResults is set to 1)
855
            $queryBuilder->orderBy('sys_language_uid', 'DESC');
856
        } else {
857
            $constraint = $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT));
858
        }
859
        $queryBuilder->andWhere($constraint);
860
861
        $page = $queryBuilder->execute()->fetch();
862
863
        if ($this->whichWorkspace() > 0) {
864
            // Fetch overlay of page if in workspace and check if it is hidden
865
            $customContext = clone $this->context;
866
            $customContext->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $this->whichWorkspace()));
867
            $customContext->setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class));
868
            $pageSelectObject = GeneralUtility::makeInstance(PageRepository::class, $customContext);
869
            $targetPage = $pageSelectObject->getWorkspaceVersionOfRecord($this->whichWorkspace(), 'pages', $page['uid']);
870
            // Also checks if the workspace version is NOT hidden but the live version is in fact still hidden
871
            $result = $targetPage === -1 || $targetPage === -2 || (is_array($targetPage) && $targetPage['hidden'] == 0 && $page['hidden'] == 1);
872
        } else {
873
            $result = is_array($page) && ($page['hidden'] || $page['starttime'] > $GLOBALS['SIM_EXEC_TIME'] || $page['endtime'] != 0 && $page['endtime'] <= $GLOBALS['SIM_EXEC_TIME']);
874
        }
875
        return $result;
876
    }
877
878
    /**
879
     * Resolves the page id and sets up several related properties.
880
     *
881
     * If $this->id is not set at all or is not a plain integer, the method
882
     * does it's best to set the value to an integer. Resolving is based on
883
     * this options:
884
     *
885
     * - Splitting $this->id if it contains an additional type parameter.
886
     * - Finding the domain record start page
887
     * - First visible page
888
     * - Relocating the id below the domain record if outside
889
     *
890
     * The following properties may be set up or updated:
891
     *
892
     * - id
893
     * - requestedId
894
     * - type
895
     * - sys_page
896
     * - sys_page->where_groupAccess
897
     * - sys_page->where_hid_del
898
     * - Context: FrontendUser Aspect
899
     * - no_cache
900
     * - register['SYS_LASTCHANGED']
901
     * - pageNotFound
902
     *
903
     * Via getPageAndRootlineWithDomain()
904
     *
905
     * - rootLine
906
     * - page
907
     * - MP
908
     * - originalShortcutPage
909
     * - originalMountPointPage
910
     * - pageAccessFailureHistory['direct_access']
911
     * - pageNotFound
912
     *
913
     * @todo:
914
     *
915
     * On the first impression the method does to much. This is increased by
916
     * the fact, that is is called repeated times by the method determineId.
917
     * The reasons are manifold.
918
     *
919
     * 1.) The first part, the creation of sys_page and the type
920
     * resolution don't need to be repeated. They could be separated to be
921
     * called only once.
922
     *
923
     * 2.) The user group setup could be done once on a higher level.
924
     *
925
     * 3.) The workflow of the resolution could be elaborated to be less
926
     * tangled. Maybe the check of the page id to be below the domain via the
927
     * root line doesn't need to be done each time, but for the final result
928
     * only.
929
     *
930
     * 4.) The root line does not need to be directly addressed by this class.
931
     * A root line is always related to one page. The rootline could be handled
932
     * indirectly by page objects. Page objects still don't exist.
933
     *
934
     * @internal
935
     * @param ServerRequestInterface|null $request
936
     */
937
    public function fetch_the_id(ServerRequestInterface $request = null)
938
    {
939
        $request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals();
940
        $timeTracker = $this->getTimeTracker();
941
        $timeTracker->push('fetch_the_id initialize/');
942
        // Set the valid usergroups for FE
943
        $this->initUserGroups();
944
        // Initialize the PageRepository has to be done after the frontend usergroups are initialized / resolved, as
945
        // frontend group aspect is modified before
946
        $this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context);
947
        // The id and type is set to the integer-value - just to be sure...
948
        $this->id = (int)$this->id;
949
        $this->type = (int)$this->type;
950
        $timeTracker->pull();
951
        // We find the first page belonging to the current domain
952
        $timeTracker->push('fetch_the_id domain/');
953
        if (!$this->id) {
954
            // If the id was not previously set, set it to the root page id of the site.
955
            $this->id = $this->site->getRootPageId();
956
        }
957
        $timeTracker->pull();
958
        $timeTracker->push('fetch_the_id rootLine/');
959
        // We store the originally requested id
960
        $this->requestedId = $this->id;
961
        try {
962
            $this->getPageAndRootlineWithDomain($this->site->getRootPageId(), $request);
963
        } catch (ShortcutTargetPageNotFoundException $e) {
964
            $this->pageNotFound = 1;
965
        }
966
        $timeTracker->pull();
967
        if ($this->pageNotFound) {
968
            switch ($this->pageNotFound) {
969
                case 1:
970
                    $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
971
                        $request,
972
                        'ID was not an accessible page',
973
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED)
974
                    );
975
                    break;
976
                case 2:
977
                    $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
978
                        $request,
979
                        'Subsection was found and not accessible',
980
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED)
981
                    );
982
                    break;
983
                case 3:
984
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
985
                        $request,
986
                        'ID was outside the domain',
987
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH)
988
                    );
989
                    break;
990
                default:
991
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
992
                        $request,
993
                        'Unspecified error',
994
                        $this->getPageAccessFailureReasons()
995
                    );
996
            }
997
            throw new PropagateResponseException($response, 1533931329);
998
        }
999
1000
        $this->setRegisterValueForSysLastChanged($this->page);
1001
1002
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['fetchPageId-PostProcessing'] ?? [] as $functionReference) {
1003
            $parameters = ['parentObject' => $this];
1004
            GeneralUtility::callUserFunction($functionReference, $parameters, $this);
1005
        }
1006
    }
1007
1008
    /**
1009
     * Loads the page and root line records based on $this->id
1010
     *
1011
     * A final page and the matching root line are determined and loaded by
1012
     * the algorithm defined by this method.
1013
     *
1014
     * First it loads the initial page from the page repository for $this->id.
1015
     * If that can't be loaded directly, it gets the root line for $this->id.
1016
     * It walks up the root line towards the root page until the page
1017
     * repository can deliver a page record. (The loading restrictions of
1018
     * the root line records are more liberal than that of the page record.)
1019
     *
1020
     * Now the page type is evaluated and handled if necessary. If the page is
1021
     * a short cut, it is replaced by the target page. If the page is a mount
1022
     * point in overlay mode, the page is replaced by the mounted page.
1023
     *
1024
     * After this potential replacements are done, the root line is loaded
1025
     * (again) for this page record. It walks up the root line up to
1026
     * the first viewable record.
1027
     *
1028
     * (While upon the first accessibility check of the root line it was done
1029
     * by loading page by page from the page repository, this time the method
1030
     * checkRootlineForIncludeSection() is used to find the most distant
1031
     * accessible page within the root line.)
1032
     *
1033
     * Having found the final page id, the page record and the root line are
1034
     * loaded for last time by this method.
1035
     *
1036
     * Exceptions may be thrown for DOKTYPE_SPACER and not loadable page records
1037
     * or root lines.
1038
     *
1039
     * May set or update this properties:
1040
     *
1041
     * @see TypoScriptFrontendController::$id
1042
     * @see TypoScriptFrontendController::$MP
1043
     * @see TypoScriptFrontendController::$page
1044
     * @see TypoScriptFrontendController::$pageNotFound
1045
     * @see TypoScriptFrontendController::$pageAccessFailureHistory
1046
     * @see TypoScriptFrontendController::$originalMountPointPage
1047
     * @see TypoScriptFrontendController::$originalShortcutPage
1048
     *
1049
     * @throws \TYPO3\CMS\Core\Error\Http\ServiceUnavailableException
1050
     * @throws PageNotFoundException
1051
     */
1052
    protected function getPageAndRootline(ServerRequestInterface $request)
1053
    {
1054
        $requestedPageRowWithoutGroupCheck = [];
1055
        $this->resolveTranslatedPageId();
1056
        if (empty($this->page)) {
1057
            // If no page, we try to find the page before in the rootLine.
1058
            // Page is 'not found' in case the id itself was not an accessible page. code 1
1059
            $this->pageNotFound = 1;
1060
            $requestedPageIsHidden = false;
1061
            try {
1062
                $hiddenField = $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['disabled'] ?? '';
1063
                $includeHiddenPages = $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages') || $this->isBackendUserLoggedIn();
1064
                if (!empty($hiddenField) && !$includeHiddenPages) {
1065
                    // Page is "hidden" => 404 (deliberately done in default language, as this cascades to language overlays)
1066
                    $rawPageRecord = $this->sys_page->getPage_noCheck($this->id);
1067
                    $requestedPageIsHidden = (bool)$rawPageRecord[$hiddenField];
1068
                }
1069
1070
                $requestedPageRowWithoutGroupCheck = $this->sys_page->getPage($this->id, true);
1071
                if (!empty($requestedPageRowWithoutGroupCheck)) {
1072
                    $this->pageAccessFailureHistory['direct_access'][] = $requestedPageRowWithoutGroupCheck;
1073
                }
1074
                $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1075
                if (!empty($this->rootLine)) {
1076
                    $c = count($this->rootLine) - 1;
1077
                    while ($c > 0) {
1078
                        // Add to page access failure history:
1079
                        $this->pageAccessFailureHistory['direct_access'][] = $this->rootLine[$c];
1080
                        // Decrease to next page in rootline and check the access to that, if OK, set as page record and ID value.
1081
                        $c--;
1082
                        $this->id = $this->rootLine[$c]['uid'];
1083
                        $this->page = $this->sys_page->getPage($this->id);
1084
                        if (!empty($this->page)) {
1085
                            break;
1086
                        }
1087
                    }
1088
                }
1089
            } catch (RootLineException $e) {
1090
                $this->rootLine = [];
1091
            }
1092
            // If still no page...
1093
            if ($requestedPageIsHidden || (empty($requestedPageRowWithoutGroupCheck) && empty($this->page))) {
1094
                $message = 'The requested page does not exist!';
1095
                $this->logPageAccessFailure($message, $request);
1096
                try {
1097
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1098
                        $request,
1099
                        $message,
1100
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND)
1101
                    );
1102
                    throw new PropagateResponseException($response, 1533931330);
1103
                } catch (PageNotFoundException $e) {
1104
                    throw new PageNotFoundException($message, 1301648780);
1105
                }
1106
            }
1107
        }
1108
        // Spacer and sysfolders is not accessible in frontend
1109
        if ($this->page['doktype'] == PageRepository::DOKTYPE_SPACER || $this->page['doktype'] == PageRepository::DOKTYPE_SYSFOLDER) {
1110
            $message = 'The requested page does not exist!';
1111
            $this->logPageAccessFailure($message, $request);
1112
            try {
1113
                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1114
                    $request,
1115
                    $message,
1116
                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_INVALID_PAGETYPE)
1117
                );
1118
                throw new PropagateResponseException($response, 1533931343);
1119
            } catch (PageNotFoundException $e) {
1120
                throw new PageNotFoundException($message, 1301648781);
1121
            }
1122
        }
1123
        // Is the ID a link to another page??
1124
        if ($this->page['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
1125
            // We need to clear MP if the page is a shortcut. Reason is if the short cut goes to another page, then we LEAVE the rootline which the MP expects.
1126
            $this->MP = '';
1127
            // saving the page so that we can check later - when we know
1128
            // about languages - whether we took the correct shortcut or
1129
            // whether a translation of the page overwrites the shortcut
1130
            // target and we need to follow the new target
1131
            $this->originalShortcutPage = $this->page;
1132
            $this->page = $this->sys_page->getPageShortcut($this->page['shortcut'], $this->page['shortcut_mode'], $this->page['uid']);
1133
            $this->id = $this->page['uid'];
1134
        }
1135
        // If the page is a mountpoint which should be overlaid with the contents of the mounted page,
1136
        // it must never be accessible directly, but only in the mountpoint context. Therefore we change
1137
        // the current ID and the user is redirected by checkPageForMountpointRedirect().
1138
        if ($this->page['doktype'] == PageRepository::DOKTYPE_MOUNTPOINT && $this->page['mount_pid_ol']) {
1139
            $this->originalMountPointPage = $this->page;
1140
            $this->page = $this->sys_page->getPage($this->page['mount_pid']);
1141
            if (empty($this->page)) {
1142
                $message = 'This page (ID ' . $this->originalMountPointPage['uid'] . ') is of type "Mount point" and '
1143
                    . 'mounts a page which is not accessible (ID ' . $this->originalMountPointPage['mount_pid'] . ').';
1144
                throw new PageNotFoundException($message, 1402043263);
1145
            }
1146
            // If the current page is a shortcut, the MP parameter will be replaced
1147
            if ($this->MP === '' || !empty($this->originalShortcutPage)) {
1148
                $this->MP = $this->page['uid'] . '-' . $this->originalMountPointPage['uid'];
1149
            } else {
1150
                $this->MP .= ',' . $this->page['uid'] . '-' . $this->originalMountPointPage['uid'];
1151
            }
1152
            $this->id = $this->page['uid'];
1153
        }
1154
        // Gets the rootLine
1155
        try {
1156
            $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1157
        } catch (RootLineException $e) {
1158
            $this->rootLine = [];
1159
        }
1160
        // If not rootline we're off...
1161
        if (empty($this->rootLine)) {
1162
            $message = 'The requested page didn\'t have a proper connection to the tree-root!';
1163
            $this->logPageAccessFailure($message, $request);
1164
            try {
1165
                $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction(
1166
                    $request,
1167
                    $message,
1168
                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ROOTLINE_BROKEN)
1169
                );
1170
                throw new PropagateResponseException($response, 1533931350);
1171
            } catch (AbstractServerErrorException $e) {
1172
                $this->logger->error($message, ['exception' => $e]);
0 ignored issues
show
Bug introduced by
The method error() does not exist on null. ( Ignorable by Annotation )

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

1172
                $this->logger->/** @scrutinizer ignore-call */ 
1173
                               error($message, ['exception' => $e]);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1173
                $exceptionClass = get_class($e);
1174
                throw new $exceptionClass($message, 1301648167);
1175
            }
1176
        }
1177
        // Checking for include section regarding the hidden/starttime/endtime/fe_user (that is access control of a whole subbranch!)
1178
        if ($this->checkRootlineForIncludeSection()) {
1179
            if (empty($this->rootLine)) {
1180
                $message = 'The requested page was not accessible!';
1181
                $this->logPageAccessFailure($message, $request);
1182
                try {
1183
                    $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction(
1184
                        $request,
1185
                        $message,
1186
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_GENERAL)
1187
                    );
1188
                    throw new PropagateResponseException($response, 1533931351);
1189
                } catch (AbstractServerErrorException $e) {
1190
                    $this->logger->warning($message);
1191
                    $exceptionClass = get_class($e);
1192
                    throw new $exceptionClass($message, 1301648234);
1193
                }
1194
            } else {
1195
                $el = reset($this->rootLine);
1196
                $this->id = $el['uid'];
1197
                $this->page = $this->sys_page->getPage($this->id);
1198
                try {
1199
                    $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1200
                } catch (RootLineException $e) {
1201
                    $this->rootLine = [];
1202
                }
1203
            }
1204
        }
1205
    }
1206
1207
    /**
1208
     * If $this->id contains a translated page record, this needs to be resolved to the default language
1209
     * in order for all rootline functionality and access restrictions to be in place further on.
1210
     *
1211
     * Additionally, if a translated page is found, LanguageAspect is set as well.
1212
     */
1213
    protected function resolveTranslatedPageId()
1214
    {
1215
        $this->page = $this->sys_page->getPage($this->id);
0 ignored issues
show
Bug introduced by
It seems like $this->id can also be of type string; however, parameter $uid of TYPO3\CMS\Core\Domain\Re...geRepository::getPage() 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

1215
        $this->page = $this->sys_page->getPage(/** @scrutinizer ignore-type */ $this->id);
Loading history...
1216
        // Accessed a default language page record, nothing to resolve
1217
        if (empty($this->page) || (int)$this->page[$GLOBALS['TCA']['pages']['ctrl']['languageField']] === 0) {
1218
            return;
1219
        }
1220
        $languageId = (int)$this->page[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
1221
        $this->page = $this->sys_page->getPage($this->page[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']]);
1222
        $this->context->setAspect('language', GeneralUtility::makeInstance(LanguageAspect::class, $languageId));
1223
        $this->id = $this->page['uid'];
1224
    }
1225
1226
    /**
1227
     * Checks if visibility of the page is blocked upwards in the root line.
1228
     *
1229
     * If any page in the root line is blocking visibility, true is returned.
1230
     *
1231
     * All pages from the blocking page downwards are removed from the root
1232
     * line, so that the remaining pages can be used to relocate the page up
1233
     * to lowest visible page.
1234
     *
1235
     * The blocking feature of a page must be turned on by setting the page
1236
     * record field 'extendToSubpages' to 1 in case of hidden, starttime,
1237
     * endtime or fe_group restrictions.
1238
     *
1239
     * Additionally this method checks for backend user sections in root line
1240
     * and if found evaluates if a backend user is logged in and has access.
1241
     *
1242
     * Recyclers are also checked and trigger page not found if found in root
1243
     * line.
1244
     *
1245
     * @todo Find a better name, i.e. checkVisibilityByRootLine
1246
     * @todo Invert boolean return value. Return true if visible.
1247
     *
1248
     * @return bool
1249
     */
1250
    protected function checkRootlineForIncludeSection(): bool
1251
    {
1252
        $c = count($this->rootLine);
1253
        $removeTheRestFlag = false;
1254
        for ($a = 0; $a < $c; $a++) {
1255
            if (!$this->checkPagerecordForIncludeSection($this->rootLine[$a])) {
1256
                // Add to page access failure history and mark the page as not found
1257
                // Keep the rootline however to trigger an access denied error instead of a service unavailable error
1258
                $this->pageAccessFailureHistory['sub_section'][] = $this->rootLine[$a];
1259
                $this->pageNotFound = 2;
1260
            }
1261
1262
            if ((int)$this->rootLine[$a]['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION) {
1263
                // If there is a backend user logged in, check if they have read access to the page:
1264
                if ($this->isBackendUserLoggedIn()) {
1265
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1266
                        ->getQueryBuilderForTable('pages');
1267
1268
                    $queryBuilder
1269
                        ->getRestrictions()
1270
                        ->removeAll();
1271
1272
                    $row = $queryBuilder
1273
                        ->select('uid')
1274
                        ->from('pages')
1275
                        ->where(
1276
                            $queryBuilder->expr()->eq(
1277
                                'uid',
1278
                                $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
1279
                            ),
1280
                            $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)
1281
                        )
1282
                        ->execute()
1283
                        ->fetch();
1284
1285
                    // versionOL()?
1286
                    if (!$row) {
1287
                        // If there was no page selected, the user apparently did not have read access to the current PAGE (not position in rootline) and we set the remove-flag...
1288
                        $removeTheRestFlag = true;
1289
                    }
1290
                } else {
1291
                    // Don't go here, if there is no backend user logged in.
1292
                    $removeTheRestFlag = true;
1293
                }
1294
            } elseif ((int)$this->rootLine[$a]['doktype'] === PageRepository::DOKTYPE_RECYCLER) {
1295
                // page is in a recycler
1296
                $removeTheRestFlag = true;
1297
            }
1298
            if ($removeTheRestFlag) {
1299
                // Page is 'not found' in case a subsection was found and not accessible, code 2
1300
                $this->pageNotFound = 2;
1301
                unset($this->rootLine[$a]);
1302
            }
1303
        }
1304
        return $removeTheRestFlag;
1305
    }
1306
1307
    /**
1308
     * Checks page record for enableFields
1309
     * Returns TRUE if enableFields does not disable the page record.
1310
     * Takes notice of the includeHiddenPages visibility aspect flag and uses SIM_ACCESS_TIME for start/endtime evaluation
1311
     *
1312
     * @param array $row The page record to evaluate (needs fields: hidden, starttime, endtime, fe_group)
1313
     * @param bool $bypassGroupCheck Bypass group-check
1314
     * @return bool TRUE, if record is viewable.
1315
     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getTreeList()
1316
     * @see checkPagerecordForIncludeSection()
1317
     */
1318
    public function checkEnableFields($row, $bypassGroupCheck = false)
1319
    {
1320
        $_params = ['pObj' => $this, 'row' => &$row, 'bypassGroupCheck' => &$bypassGroupCheck];
1321
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['hook_checkEnableFields'] ?? [] as $_funcRef) {
1322
            // Call hooks: If one returns FALSE, method execution is aborted with result "This record is not available"
1323
            $return = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1324
            if ($return === false) {
1325
                return false;
1326
            }
1327
        }
1328
        if ((!$row['hidden'] || $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages', false))
1329
            && $row['starttime'] <= $GLOBALS['SIM_ACCESS_TIME']
1330
            && ($row['endtime'] == 0 || $row['endtime'] > $GLOBALS['SIM_ACCESS_TIME'])
1331
            && ($bypassGroupCheck || $this->checkPageGroupAccess($row))) {
1332
            return true;
1333
        }
1334
        return false;
1335
    }
1336
1337
    /**
1338
     * Check group access against a page record
1339
     *
1340
     * @param array $row The page record to evaluate (needs field: fe_group)
1341
     * @return bool TRUE, if group access is granted.
1342
     * @internal
1343
     */
1344
    public function checkPageGroupAccess($row)
1345
    {
1346
        /** @var UserAspect $userAspect */
1347
        $userAspect = $this->context->getAspect('frontend.user');
1348
        $pageGroupList = explode(',', $row['fe_group'] ?: 0);
1349
        return count(array_intersect($userAspect->getGroupIds(), $pageGroupList)) > 0;
1350
    }
1351
1352
    /**
1353
     * Checks if the current page of the root line is visible.
1354
     *
1355
     * If the field extendToSubpages is 0, access is granted,
1356
     * else the fields hidden, starttime, endtime, fe_group are evaluated.
1357
     *
1358
     * @todo Find a better name, i.e. isVisibleRecord()
1359
     *
1360
     * @param array $row The page record
1361
     * @return bool true if visible
1362
     * @internal
1363
     * @see checkEnableFields()
1364
     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getTreeList()
1365
     * @see checkRootlineForIncludeSection()
1366
     */
1367
    public function checkPagerecordForIncludeSection(array $row): bool
1368
    {
1369
        return !$row['extendToSubpages'] || $this->checkEnableFields($row);
1370
    }
1371
1372
    /**
1373
     * Checks if logins are allowed in the current branch of the page tree. Traverses the full root line and returns TRUE if logins are OK, otherwise FALSE (and then the login user must be unset!)
1374
     *
1375
     * @return bool returns TRUE if logins are OK, otherwise FALSE (and then the login user must be unset!)
1376
     */
1377
    public function checkIfLoginAllowedInBranch()
1378
    {
1379
        // Initialize:
1380
        $c = count($this->rootLine);
1381
        $loginAllowed = true;
1382
        // Traverse root line from root and outwards:
1383
        for ($a = 0; $a < $c; $a++) {
1384
            // If a value is set for login state:
1385
            if ($this->rootLine[$a]['fe_login_mode'] > 0) {
1386
                // Determine state from value:
1387
                if ((int)$this->rootLine[$a]['fe_login_mode'] === 1) {
1388
                    $loginAllowed = false;
1389
                    $this->loginAllowedInBranch_mode = 'all';
1390
                } elseif ((int)$this->rootLine[$a]['fe_login_mode'] === 3) {
1391
                    $loginAllowed = false;
1392
                    $this->loginAllowedInBranch_mode = 'groups';
1393
                } else {
1394
                    $loginAllowed = true;
1395
                }
1396
            }
1397
        }
1398
        return $loginAllowed;
1399
    }
1400
1401
    /**
1402
     * Analysing $this->pageAccessFailureHistory into a summary array telling which features disabled display and on which pages and conditions. That data can be used inside a page-not-found handler
1403
     *
1404
     * @param string $failureReasonCode the error code to be attached (optional), see PageAccessFailureReasons list for details
1405
     * @return array Summary of why page access was not allowed.
1406
     */
1407
    public function getPageAccessFailureReasons(string $failureReasonCode = null)
1408
    {
1409
        $output = [];
1410
        if ($failureReasonCode) {
1411
            $output['code'] = $failureReasonCode;
1412
        }
1413
        $combinedRecords = array_merge(
1414
            is_array($this->pageAccessFailureHistory['direct_access'] ?? false) ? $this->pageAccessFailureHistory['direct_access'] : [['fe_group' => 0]],
1415
            is_array($this->pageAccessFailureHistory['sub_section'] ?? false) ? $this->pageAccessFailureHistory['sub_section'] : []
1416
        );
1417
        if (!empty($combinedRecords)) {
1418
            foreach ($combinedRecords as $k => $pagerec) {
1419
                // If $k=0 then it is the very first page the original ID was pointing at and that will get a full check of course
1420
                // If $k>0 it is parent pages being tested. They are only significant for the access to the first page IF they had the extendToSubpages flag set, hence checked only then!
1421
                if (!$k || $pagerec['extendToSubpages']) {
1422
                    if ($pagerec['hidden'] ?? false) {
1423
                        $output['hidden'][$pagerec['uid']] = true;
1424
                    }
1425
                    if (isset($pagerec['starttime']) && $pagerec['starttime'] > $GLOBALS['SIM_ACCESS_TIME']) {
1426
                        $output['starttime'][$pagerec['uid']] = $pagerec['starttime'];
1427
                    }
1428
                    if (isset($pagerec['endtime']) && $pagerec['endtime'] != 0 && $pagerec['endtime'] <= $GLOBALS['SIM_ACCESS_TIME']) {
1429
                        $output['endtime'][$pagerec['uid']] = $pagerec['endtime'];
1430
                    }
1431
                    if (!$this->checkPageGroupAccess($pagerec)) {
1432
                        $output['fe_group'][$pagerec['uid']] = $pagerec['fe_group'];
1433
                    }
1434
                }
1435
            }
1436
        }
1437
        return $output;
1438
    }
1439
1440
    /**
1441
     * Gets ->page and ->rootline information based on ->id. ->id may change during this operation.
1442
     * If not inside a site, then default to first page in site.
1443
     *
1444
     * @param int $rootPageId Page uid of the page where the found site is located
1445
     * @internal
1446
     */
1447
    public function getPageAndRootlineWithDomain($rootPageId, ServerRequestInterface $request)
1448
    {
1449
        $this->getPageAndRootline($request);
1450
        // Checks if the $domain-startpage is in the rootLine. This is necessary so that references to page-id's via ?id=123 from other sites are not possible.
1451
        if (is_array($this->rootLine) && $this->rootLine !== []) {
1452
            $idFound = false;
1453
            foreach ($this->rootLine as $key => $val) {
1454
                if ($val['uid'] == $rootPageId) {
1455
                    $idFound = true;
1456
                    break;
1457
                }
1458
            }
1459
            if (!$idFound) {
1460
                // Page is 'not found' in case the id was outside the domain, code 3
1461
                $this->pageNotFound = 3;
1462
                $this->id = $rootPageId;
1463
                // re-get the page and rootline if the id was not found.
1464
                $this->getPageAndRootline($request);
1465
            }
1466
        }
1467
    }
1468
1469
    /********************************************
1470
     *
1471
     * Template and caching related functions.
1472
     *
1473
     *******************************************/
1474
1475
    protected function setPageArguments(PageArguments $pageArguments): void
1476
    {
1477
        $this->pageArguments = $pageArguments;
1478
        $this->id = $pageArguments->getPageId();
1479
        $this->type = $pageArguments->getPageType() ?: 0;
1480
        if ($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
1481
            $this->MP = (string)($pageArguments->getArguments()['MP'] ?? '');
1482
            // Ensure no additional arguments are given via the &MP=123-345,908-172 (e.g. "/")
1483
            $this->MP = preg_replace('/[^0-9,-]/', '', $this->MP);
1484
        }
1485
    }
1486
1487
    /**
1488
     * Fetches the arguments that are relevant for creating the hash base from the given PageArguments object.
1489
     * Excluded parameters are not taken into account when calculating the hash base.
1490
     *
1491
     * @param PageArguments $pageArguments
1492
     * @return array
1493
     */
1494
    protected function getRelevantParametersForCachingFromPageArguments(PageArguments $pageArguments): array
1495
    {
1496
        $queryParams = $pageArguments->getDynamicArguments();
1497
        if (!empty($queryParams) && ($pageArguments->getArguments()['cHash'] ?? false)) {
1498
            $queryParams['id'] = $pageArguments->getPageId();
1499
            return GeneralUtility::makeInstance(CacheHashCalculator::class)
1500
                ->getRelevantParameters(HttpUtility::buildQueryString($queryParams));
1501
        }
1502
        return [];
1503
    }
1504
1505
    /**
1506
     * See if page is in cache and get it if so
1507
     * Stores the page content in $this->content if something is found.
1508
     *
1509
     * @param ServerRequestInterface|null $request if given this is used to determine values in headerNoCache() instead of the superglobal $_SERVER
1510
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
1511
     */
1512
    public function getFromCache(ServerRequestInterface $request = null)
1513
    {
1514
        // clearing the content-variable, which will hold the pagecontent
1515
        $this->content = '';
1516
        // Unsetting the lowlevel config
1517
        $this->config = [];
1518
        $this->cacheContentFlag = false;
1519
1520
        if ($this->no_cache) {
1521
            return;
1522
        }
1523
1524
        if (!$this->tmpl instanceof TemplateService) {
0 ignored issues
show
introduced by
$this->tmpl is always a sub-type of TYPO3\CMS\Core\TypoScript\TemplateService.
Loading history...
1525
            $this->tmpl = GeneralUtility::makeInstance(TemplateService::class, $this->context, null, $this);
1526
        }
1527
1528
        $pageSectionCacheContent = $this->tmpl->getCurrentPageData((int)$this->id, (string)$this->MP);
1529
        if (!is_array($pageSectionCacheContent)) {
0 ignored issues
show
introduced by
The condition is_array($pageSectionCacheContent) is always true.
Loading history...
1530
            // Nothing in the cache, we acquire an "exclusive lock" for the key now.
1531
            // We use the Registry to store this lock centrally,
1532
            // but we protect the access again with a global exclusive lock to avoid race conditions
1533
1534
            $this->acquireLock('pagesection', $this->id . '::' . $this->MP);
1535
            //
1536
            // from this point on we're the only one working on that page ($key)
1537
            //
1538
1539
            // query the cache again to see if the page data are there meanwhile
1540
            $pageSectionCacheContent = $this->tmpl->getCurrentPageData((int)$this->id, (string)$this->MP);
1541
            if (is_array($pageSectionCacheContent)) {
1542
                // we have the content, nice that some other process did the work for us already
1543
                $this->releaseLock('pagesection');
1544
            }
1545
            // We keep the lock set, because we are the ones generating the page now and filling the cache.
1546
            // This indicates that we have to release the lock later in releaseLocks()
1547
        }
1548
1549
        if (is_array($pageSectionCacheContent)) {
0 ignored issues
show
introduced by
The condition is_array($pageSectionCacheContent) is always true.
Loading history...
1550
            // BE CAREFUL to change the content of the cc-array. This array is serialized and an md5-hash based on this is used for caching the page.
1551
            // If this hash is not the same in here in this section and after page-generation, then the page will not be properly cached!
1552
            // This array is an identification of the template. If $this->all is empty it's because the template-data is not cached, which it must be.
1553
            $pageSectionCacheContent = $this->tmpl->matching($pageSectionCacheContent);
1554
            ksort($pageSectionCacheContent);
1555
            $this->all = $pageSectionCacheContent;
1556
        }
1557
1558
        // Look for page in cache only if a shift-reload is not sent to the server.
1559
        $lockHash = $this->getLockHash();
1560
        if (!$this->headerNoCache($request) && $this->all) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->all of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1561
            // we got page section information (TypoScript), so lets see if there is also a cached version
1562
            // of this page in the pages cache.
1563
            $this->newHash = $this->getHash();
1564
            $this->getTimeTracker()->push('Cache Row');
1565
            $row = $this->getFromCache_queryRow();
1566
            if (!is_array($row)) {
0 ignored issues
show
introduced by
The condition is_array($row) is always true.
Loading history...
1567
                // nothing in the cache, we acquire an exclusive lock now
1568
                $this->acquireLock('pages', $lockHash);
1569
                //
1570
                // from this point on we're the only one working on that page ($lockHash)
1571
                //
1572
1573
                // query the cache again to see if the data are there meanwhile
1574
                $row = $this->getFromCache_queryRow();
1575
                if (is_array($row)) {
1576
                    // we have the content, nice that some other process did the work for us
1577
                    $this->releaseLock('pages');
1578
                }
1579
                // We keep the lock set, because we are the ones generating the page now and filling the cache.
1580
                // This indicates that we have to release the lock later in releaseLocks()
1581
            }
1582
            if (is_array($row)) {
0 ignored issues
show
introduced by
The condition is_array($row) is always true.
Loading history...
1583
                $this->populatePageDataFromCache($row);
1584
            }
1585
            $this->getTimeTracker()->pull();
1586
        } else {
1587
            // the user forced rebuilding the page cache or there was no pagesection information
1588
            // get a lock for the page content so other processes will not interrupt the regeneration
1589
            $this->acquireLock('pages', $lockHash);
1590
        }
1591
    }
1592
1593
    /**
1594
     * Returning the cached version of page with hash = newHash
1595
     *
1596
     * @return array Cached row, if any. Otherwise void.
1597
     */
1598
    public function getFromCache_queryRow()
1599
    {
1600
        $this->getTimeTracker()->push('Cache Query');
1601
        $row = $this->pageCache->get($this->newHash);
1602
        $this->getTimeTracker()->pull();
1603
        return $row;
1604
    }
1605
1606
    /**
1607
     * This method properly sets the values given from the pages cache into the corresponding
1608
     * TSFE variables. The counterpart is setPageCacheContent() where all relevant information is fetched.
1609
     * This also contains all data that could be cached, even for pages that are partially cached, as they
1610
     * have non-cacheable content still to be rendered.
1611
     *
1612
     * @see getFromCache()
1613
     * @see setPageCacheContent()
1614
     * @param array $cachedData
1615
     */
1616
    protected function populatePageDataFromCache(array $cachedData): void
1617
    {
1618
        // Call hook when a page is retrieved from cache
1619
        $_params = ['pObj' => &$this, 'cache_pages_row' => &$cachedData];
1620
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['pageLoadedFromCache'] ?? [] as $_funcRef) {
1621
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1622
        }
1623
        // Fetches the lowlevel config stored with the cached data
1624
        $this->config = $cachedData['cache_data'];
1625
        // Getting the content
1626
        $this->content = $cachedData['content'];
1627
        // Setting flag, so we know, that some cached content has been loaded
1628
        $this->cacheContentFlag = true;
1629
        $this->cacheExpires = $cachedData['expires'];
1630
        // Restore the current tags as they can be retrieved by getPageCacheTags()
1631
        $this->pageCacheTags = $cachedData['cacheTags'] ?? [];
1632
1633
        // Restore page title information, this is needed to generate the page title for
1634
        // partially cached pages.
1635
        $this->page['title'] = $cachedData['pageTitleInfo']['title'];
1636
        $this->indexedDocTitle = $cachedData['pageTitleInfo']['indexedDocTitle'];
1637
1638
        if (isset($this->config['config']['debug'])) {
1639
            $debugCacheTime = (bool)$this->config['config']['debug'];
1640
        } else {
1641
            $debugCacheTime = !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']);
1642
        }
1643
        if ($debugCacheTime) {
1644
            $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'];
1645
            $timeFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
1646
            $this->content .= LF . '<!-- Cached page generated ' . date($dateFormat . ' ' . $timeFormat, $cachedData['tstamp']) . '. Expires ' . date($dateFormat . ' ' . $timeFormat, $cachedData['expires']) . ' -->';
1647
        }
1648
    }
1649
1650
    /**
1651
     * Detecting if shift-reload has been clicked
1652
     * Will not be called if re-generation of page happens by other reasons (for instance that the page is not in cache yet!)
1653
     * Also, a backend user MUST be logged in for the shift-reload to be detected due to DoS-attack-security reasons.
1654
     *
1655
     * @param ServerRequestInterface|null $request
1656
     * @return bool If shift-reload in client browser has been clicked, disable getting cached page (and regenerate it).
1657
     */
1658
    public function headerNoCache(ServerRequestInterface $request = null)
1659
    {
1660
        if ($request instanceof ServerRequestInterface) {
1661
            $serverParams = $request->getServerParams();
1662
        } else {
1663
            $serverParams = $_SERVER;
1664
        }
1665
        $disableAcquireCacheData = false;
1666
        if ($this->isBackendUserLoggedIn()) {
1667
            if (strtolower($serverParams['HTTP_CACHE_CONTROL'] ?? '') === 'no-cache' || strtolower($serverParams['HTTP_PRAGMA'] ?? '') === 'no-cache') {
1668
                $disableAcquireCacheData = true;
1669
            }
1670
        }
1671
        // Call hook for possible by-pass of requiring of page cache (for recaching purpose)
1672
        $_params = ['pObj' => &$this, 'disableAcquireCacheData' => &$disableAcquireCacheData];
1673
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['headerNoCache'] ?? [] as $_funcRef) {
1674
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1675
        }
1676
        return $disableAcquireCacheData;
1677
    }
1678
1679
    /**
1680
     * Calculates the cache-hash
1681
     * This hash is unique to the template, the variables ->id, ->type, list of fe user groups, ->MP (Mount Points) and cHash array
1682
     * Used to get and later store the cached data.
1683
     *
1684
     * @return string MD5 hash of serialized hash base from createHashBase(), prefixed with page id
1685
     * @see getFromCache()
1686
     * @see getLockHash()
1687
     */
1688
    protected function getHash()
1689
    {
1690
        return $this->id . '_' . md5($this->createHashBase(false));
1691
    }
1692
1693
    /**
1694
     * Calculates the lock-hash
1695
     * This hash is unique to the above hash, except that it doesn't contain the template information in $this->all.
1696
     *
1697
     * @return string MD5 hash prefixed with page id
1698
     * @see getFromCache()
1699
     * @see getHash()
1700
     */
1701
    protected function getLockHash()
1702
    {
1703
        $lockHash = $this->createHashBase(true);
1704
        return $this->id . '_' . md5($lockHash);
1705
    }
1706
1707
    /**
1708
     * Calculates the cache-hash (or the lock-hash)
1709
     * This hash is unique to the template,
1710
     * the variables ->id, ->type, list of frontend user groups,
1711
     * ->MP (Mount Points) and cHash array
1712
     * Used to get and later store the cached data.
1713
     *
1714
     * @param bool $createLockHashBase Whether to create the lock hash, which doesn't contain the "this->all" (the template information)
1715
     * @return string the serialized hash base
1716
     */
1717
    protected function createHashBase($createLockHashBase = false)
1718
    {
1719
        // Fetch the list of user groups
1720
        /** @var UserAspect $userAspect */
1721
        $userAspect = $this->context->getAspect('frontend.user');
1722
        $hashParameters = [
1723
            'id' => (int)$this->id,
1724
            'type' => (int)$this->type,
1725
            'groupIds' => (string)implode(',', $userAspect->getGroupIds()),
1726
            'MP' => (string)$this->MP,
1727
            'site' => $this->site->getIdentifier(),
1728
            // Ensure the language base is used for the hash base calculation as well, otherwise TypoScript and page-related rendering
1729
            // is not cached properly as we don't have any language-specific conditions anymore
1730
            'siteBase' => (string)$this->language->getBase(),
1731
            // additional variation trigger for static routes
1732
            'staticRouteArguments' => $this->pageArguments->getStaticArguments(),
1733
            // dynamic route arguments (if route was resolved)
1734
            'dynamicArguments' => $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments),
1735
        ];
1736
        // Include the template information if we shouldn't create a lock hash
1737
        if (!$createLockHashBase) {
1738
            $hashParameters['all'] = $this->all;
1739
        }
1740
        // Call hook to influence the hash calculation
1741
        $_params = [
1742
            'hashParameters' => &$hashParameters,
1743
            'createLockHashBase' => $createLockHashBase
1744
        ];
1745
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['createHashBase'] ?? [] as $_funcRef) {
1746
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1747
        }
1748
        return serialize($hashParameters);
1749
    }
1750
1751
    /**
1752
     * Checks if config-array exists already but if not, gets it
1753
     *
1754
     * @param ServerRequestInterface|null $request
1755
     * @throws \TYPO3\CMS\Core\Error\Http\InternalServerErrorException
1756
     * @throws \TYPO3\CMS\Core\Error\Http\ServiceUnavailableException
1757
     */
1758
    public function getConfigArray(ServerRequestInterface $request = null)
1759
    {
1760
        $request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals();
1761
        if (!$this->tmpl instanceof TemplateService) {
0 ignored issues
show
introduced by
$this->tmpl is always a sub-type of TYPO3\CMS\Core\TypoScript\TemplateService.
Loading history...
1762
            $this->tmpl = GeneralUtility::makeInstance(TemplateService::class, $this->context, null, $this);
1763
        }
1764
1765
        // If config is not set by the cache (which would be a major mistake somewhere) OR if INTincScripts-include-scripts have been registered, then we must parse the template in order to get it
1766
        if (empty($this->config) || $this->isINTincScript() || $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing')) {
1767
            $timeTracker = $this->getTimeTracker();
1768
            $timeTracker->push('Parse template');
1769
            // Start parsing the TS template. Might return cached version.
1770
            $this->tmpl->start($this->rootLine);
1771
            $timeTracker->pull();
1772
            // At this point we have a valid pagesection_cache (generated in $this->tmpl->start()),
1773
            // so let all other processes proceed now. (They are blocked at the pagessection_lock in getFromCache())
1774
            $this->releaseLock('pagesection');
1775
            if ($this->tmpl->loaded) {
1776
                $timeTracker->push('Setting the config-array');
1777
                // toplevel - objArrayName
1778
                $typoScriptPageTypeName = $this->tmpl->setup['types.'][$this->type];
1779
                $this->sPre = $typoScriptPageTypeName;
1780
                $this->pSetup = $this->tmpl->setup[$typoScriptPageTypeName . '.'];
1781
                if (!is_array($this->pSetup)) {
1782
                    $this->logger->alert('The page is not configured! [type={type}][{type_name}].', ['type' => $this->type, 'type_name' => $typoScriptPageTypeName]);
1783
                    try {
1784
                        $message = 'The page is not configured! [type=' . $this->type . '][' . $typoScriptPageTypeName . '].';
1785
                        $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction(
1786
                            $request,
1787
                            $message,
1788
                            ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED]
1789
                        );
1790
                        throw new PropagateResponseException($response, 1533931374);
1791
                    } catch (AbstractServerErrorException $e) {
1792
                        $explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.';
1793
                        $exceptionClass = get_class($e);
1794
                        throw new $exceptionClass($message . ' ' . $explanation, 1294587217);
1795
                    }
1796
                } else {
1797
                    if (!isset($this->config['config'])) {
1798
                        $this->config['config'] = [];
1799
                    }
1800
                    // Filling the config-array, first with the main "config." part
1801
                    if (is_array($this->tmpl->setup['config.'] ?? null)) {
1802
                        ArrayUtility::mergeRecursiveWithOverrule($this->tmpl->setup['config.'], $this->config['config']);
1803
                        $this->config['config'] = $this->tmpl->setup['config.'];
1804
                    }
1805
                    // override it with the page/type-specific "config."
1806
                    if (is_array($this->pSetup['config.'] ?? null)) {
1807
                        ArrayUtility::mergeRecursiveWithOverrule($this->config['config'], $this->pSetup['config.']);
1808
                    }
1809
                    // Set default values for removeDefaultJS and inlineStyle2TempFile so CSS and JS are externalized if compatversion is higher than 4.0
1810
                    if (!isset($this->config['config']['removeDefaultJS'])) {
1811
                        $this->config['config']['removeDefaultJS'] = 'external';
1812
                    }
1813
                    if (!isset($this->config['config']['inlineStyle2TempFile'])) {
1814
                        $this->config['config']['inlineStyle2TempFile'] = 1;
1815
                    }
1816
1817
                    if (!isset($this->config['config']['compressJs'])) {
1818
                        $this->config['config']['compressJs'] = 0;
1819
                    }
1820
                    // Rendering charset of HTML page.
1821
                    if (isset($this->config['config']['metaCharset']) && $this->config['config']['metaCharset'] !== 'utf-8') {
1822
                        $this->metaCharset = $this->config['config']['metaCharset'];
1823
                    }
1824
                    // Setting default cache_timeout
1825
                    if (isset($this->config['config']['cache_period'])) {
1826
                        $this->set_cache_timeout_default((int)$this->config['config']['cache_period']);
1827
                    }
1828
1829
                    // Processing for the config_array:
1830
                    $this->config['rootLine'] = $this->tmpl->rootLine;
1831
                    // Class for render Header and Footer parts
1832
                    if ($this->pSetup['pageHeaderFooterTemplateFile'] ?? false) {
1833
                        try {
1834
                            $file = GeneralUtility::makeInstance(FilePathSanitizer::class)
1835
                                ->sanitize((string)$this->pSetup['pageHeaderFooterTemplateFile']);
1836
                            $this->pageRenderer->setTemplateFile($file);
1837
                        } catch (Exception $e) {
1838
                            // do nothing
1839
                        }
1840
                    }
1841
                }
1842
                $timeTracker->pull();
1843
            } else {
1844
                $message = 'No TypoScript template found!';
1845
                $this->logger->alert($message);
1846
                try {
1847
                    $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction(
1848
                        $request,
1849
                        $message,
1850
                        ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_FOUND]
1851
                    );
1852
                    throw new PropagateResponseException($response, 1533931380);
1853
                } catch (AbstractServerErrorException $e) {
1854
                    $exceptionClass = get_class($e);
1855
                    throw new $exceptionClass($message, 1294587218);
1856
                }
1857
            }
1858
        }
1859
1860
        // No cache
1861
        // Set $this->no_cache TRUE if the config.no_cache value is set!
1862
        if ($this->config['config']['no_cache'] ?? false) {
1863
            $this->set_no_cache('config.no_cache is set', true);
1864
        }
1865
1866
        // Auto-configure settings when a site is configured
1867
        $this->config['config']['absRefPrefix'] = $this->config['config']['absRefPrefix'] ?? 'auto';
1868
1869
        // Hook for postProcessing the configuration array
1870
        $params = ['config' => &$this->config['config']];
1871
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['configArrayPostProc'] ?? [] as $funcRef) {
1872
            GeneralUtility::callUserFunction($funcRef, $params, $this);
1873
        }
1874
    }
1875
1876
    /********************************************
1877
     *
1878
     * Further initialization and data processing
1879
     *
1880
     *******************************************/
1881
    /**
1882
     * Setting the language key that will be used by the current page.
1883
     * In this function it should be checked, 1) that this language exists, 2) that a page_overlay_record exists, .. and if not the default language, 0 (zero), should be set.
1884
     *
1885
     * @param ServerRequestInterface|null $request
1886
     * @internal
1887
     */
1888
    public function settingLanguage(ServerRequestInterface $request = null)
1889
    {
1890
        $request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals();
1891
        $_params = [];
1892
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_preProcess'] ?? [] as $_funcRef) {
1893
            $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
1894
            GeneralUtility::callUserFunction($_funcRef, $_params, $ref);
1895
        }
1896
1897
        // Get values from site language
1898
        $languageAspect = LanguageAspectFactory::createFromSiteLanguage($this->language);
1899
1900
        $languageId = $languageAspect->getId();
1901
        $languageContentId = $languageAspect->getContentId();
1902
1903
        $pageTranslationVisibility = new PageTranslationVisibility((int)($this->page['l18n_cfg'] ?? 0));
1904
        // If sys_language_uid is set to another language than default:
1905
        if ($languageAspect->getId() > 0) {
1906
            // check whether a shortcut is overwritten by a translated page
1907
            // we can only do this now, as this is the place where we get
1908
            // to know about translations
1909
            $this->checkTranslatedShortcut($languageAspect->getId(), $request);
1910
            // Request the overlay record for the sys_language_uid:
1911
            $olRec = $this->sys_page->getPageOverlay($this->id, $languageAspect->getId());
1912
            if (empty($olRec)) {
1913
                // If requested translation is not available:
1914
                if ($pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists()) {
1915
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1916
                        $request,
1917
                        'Page is not available in the requested language.',
1918
                        ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE]
1919
                    );
1920
                    throw new PropagateResponseException($response, 1533931388);
1921
                }
1922
                switch ((string)$languageAspect->getLegacyLanguageMode()) {
1923
                    case 'strict':
1924
                        $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1925
                            $request,
1926
                            'Page is not available in the requested language (strict).',
1927
                            ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE_STRICT_MODE]
1928
                        );
1929
                        throw new PropagateResponseException($response, 1533931395);
1930
                    case 'fallback':
1931
                    case 'content_fallback':
1932
                        // Setting content uid (but leaving the sys_language_uid) when a content_fallback
1933
                        // value was found.
1934
                        foreach ($languageAspect->getFallbackChain() ?? [] as $orderValue) {
1935
                            if ($orderValue === '0' || $orderValue === 0 || $orderValue === '') {
1936
                                $languageContentId = 0;
1937
                                break;
1938
                            }
1939
                            if (MathUtility::canBeInterpretedAsInteger($orderValue) && !empty($this->sys_page->getPageOverlay($this->id, (int)$orderValue))) {
1940
                                $languageContentId = (int)$orderValue;
1941
                                break;
1942
                            }
1943
                            if ($orderValue === 'pageNotFound') {
1944
                                // The existing fallbacks have not been found, but instead of continuing
1945
                                // page rendering with default language, a "page not found" message should be shown
1946
                                // instead.
1947
                                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1948
                                    $request,
1949
                                    'Page is not available in the requested language (fallbacks did not apply).',
1950
                                    ['code' => PageAccessFailureReasons::LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE]
1951
                                );
1952
                                throw new PropagateResponseException($response, 1533931402);
1953
                            }
1954
                        }
1955
                        break;
1956
                    case 'ignore':
1957
                        $languageContentId = $languageAspect->getId();
1958
                        break;
1959
                    default:
1960
                        // Default is that everything defaults to the default language...
1961
                        $languageId = ($languageContentId = 0);
1962
                }
1963
            }
1964
1965
            // Define the language aspect again now
1966
            $languageAspect = GeneralUtility::makeInstance(
1967
                LanguageAspect::class,
1968
                $languageId,
1969
                $languageContentId,
1970
                $languageAspect->getOverlayType(),
1971
                $languageAspect->getFallbackChain()
1972
            );
1973
1974
            // Setting sys_language if an overlay record was found (which it is only if a language is used)
1975
            // We'll do this every time since the language aspect might have changed now
1976
            // Doing this ensures that page properties like the page title are returned in the correct language
1977
            $this->page = $this->sys_page->getPageOverlay($this->page, $languageAspect->getContentId());
1978
1979
            // Update SYS_LASTCHANGED for localized page record
1980
            $this->setRegisterValueForSysLastChanged($this->page);
1981
        }
1982
1983
        // Set the language aspect
1984
        $this->context->setAspect('language', $languageAspect);
1985
1986
        // Setting sys_language_uid inside sys-page by creating a new page repository
1987
        $this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context);
1988
        // If default language is not available:
1989
        if ((!$languageAspect->getContentId() || !$languageAspect->getId())
1990
            && $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage()
1991
        ) {
1992
            $message = 'Page is not available in default language.';
1993
            $this->logPageAccessFailure($message, $request);
1994
            $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1995
                $request,
1996
                $message,
1997
                ['code' => PageAccessFailureReasons::LANGUAGE_DEFAULT_NOT_AVAILABLE]
1998
            );
1999
            throw new PropagateResponseException($response, 1533931423);
2000
        }
2001
2002
        if ($languageAspect->getId() > 0) {
2003
            $this->updateRootLinesWithTranslations();
2004
        }
2005
2006
        $_params = [];
2007
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_postProcess'] ?? [] as $_funcRef) {
2008
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2009
        }
2010
    }
2011
2012
    /**
2013
     * Updating content of the two rootLines IF the language key is set!
2014
     */
2015
    protected function updateRootLinesWithTranslations()
2016
    {
2017
        try {
2018
            $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
2019
        } catch (RootLineException $e) {
2020
            $this->rootLine = [];
2021
        }
2022
    }
2023
2024
    /**
2025
     * Checks whether a translated shortcut page has a different shortcut
2026
     * target than the original language page.
2027
     * If that is the case, things get corrected to follow that alternative
2028
     * shortcut
2029
     * @param int $languageId
2030
     * @param ServerRequestInterface $request
2031
     */
2032
    protected function checkTranslatedShortcut(int $languageId, ServerRequestInterface $request)
2033
    {
2034
        if (!is_null($this->originalShortcutPage)) {
2035
            $originalShortcutPageOverlay = $this->sys_page->getPageOverlay($this->originalShortcutPage['uid'], $languageId);
2036
            if (!empty($originalShortcutPageOverlay['shortcut']) && $originalShortcutPageOverlay['shortcut'] != $this->id) {
2037
                // the translation of the original shortcut page has a different shortcut target!
2038
                // set the correct page and id
2039
                $shortcut = $this->sys_page->getPageShortcut($originalShortcutPageOverlay['shortcut'], $originalShortcutPageOverlay['shortcut_mode'], $originalShortcutPageOverlay['uid']);
2040
                $this->id = ($this->contentPid = $shortcut['uid']);
2041
                $this->page = $this->sys_page->getPage($this->id);
2042
                // Fix various effects on things like menus f.e.
2043
                $this->fetch_the_id($request);
2044
                $this->tmpl->rootLine = array_reverse($this->rootLine);
2045
            }
2046
        }
2047
    }
2048
2049
    /**
2050
     * Calculates and sets the internal linkVars based upon the current request parameters
2051
     * and the setting "config.linkVars".
2052
     *
2053
     * @param array $queryParams $_GET (usually called with a PSR-7 $request->getQueryParams())
2054
     */
2055
    public function calculateLinkVars(array $queryParams)
2056
    {
2057
        $this->linkVars = '';
2058
        $adminCommand = $queryParams['ADMCMD_prev'] ?? '';
2059
        if (($adminCommand === 'LIVE' || $adminCommand === 'IGNORE') && $this->isBackendUserLoggedIn()) {
2060
            $this->config['config']['linkVars'] = ltrim(($this->config['config']['linkVars'] ?? '') . ',ADMCMD_prev', ',');
2061
        }
2062
        if (empty($this->config['config']['linkVars'])) {
2063
            return;
2064
        }
2065
2066
        $linkVars = $this->splitLinkVarsString((string)$this->config['config']['linkVars']);
2067
2068
        if (empty($linkVars)) {
2069
            return;
2070
        }
2071
        foreach ($linkVars as $linkVar) {
2072
            $test = '';
2073
            if (preg_match('/^(.*)\\((.+)\\)$/', $linkVar, $match)) {
2074
                $linkVar = trim($match[1]);
2075
                $test = trim($match[2]);
2076
            }
2077
2078
            $keys = explode('|', $linkVar);
2079
            $numberOfLevels = count($keys);
2080
            $rootKey = trim($keys[0]);
2081
            if (!isset($queryParams[$rootKey])) {
2082
                continue;
2083
            }
2084
            $value = $queryParams[$rootKey];
2085
            for ($i = 1; $i < $numberOfLevels; $i++) {
2086
                $currentKey = trim($keys[$i]);
2087
                if (isset($value[$currentKey])) {
2088
                    $value = $value[$currentKey];
2089
                } else {
2090
                    $value = false;
2091
                    break;
2092
                }
2093
            }
2094
            if ($value !== false) {
2095
                $parameterName = $keys[0];
2096
                for ($i = 1; $i < $numberOfLevels; $i++) {
2097
                    $parameterName .= '[' . $keys[$i] . ']';
2098
                }
2099
                if (!is_array($value)) {
2100
                    $temp = rawurlencode($value);
2101
                    if ($test !== '' && !$this->isAllowedLinkVarValue($temp, $test)) {
2102
                        // Error: This value was not allowed for this key
2103
                        continue;
2104
                    }
2105
                    $value = '&' . $parameterName . '=' . $temp;
2106
                } else {
2107
                    if ($test !== '' && $test !== 'array') {
2108
                        // Error: This key must not be an array!
2109
                        continue;
2110
                    }
2111
                    $value = HttpUtility::buildQueryString([$parameterName => $value], '&');
2112
                }
2113
                $this->linkVars .= $value;
2114
            }
2115
        }
2116
    }
2117
2118
    /**
2119
     * Split the link vars string by "," but not if the "," is inside of braces
2120
     *
2121
     * @param string $string
2122
     *
2123
     * @return array
2124
     */
2125
    protected function splitLinkVarsString(string $string): array
2126
    {
2127
        $tempCommaReplacementString = '###KASPER###';
2128
2129
        // replace every "," wrapped in "()" by a "unique" string
2130
        $string = preg_replace_callback('/\((?>[^()]|(?R))*\)/', function ($result) use ($tempCommaReplacementString) {
2131
            return str_replace(',', $tempCommaReplacementString, $result[0]);
2132
        }, $string) ?? '';
2133
2134
        $string = GeneralUtility::trimExplode(',', $string);
2135
2136
        // replace all "unique" strings back to ","
2137
        return str_replace($tempCommaReplacementString, ',', $string);
2138
    }
2139
2140
    /**
2141
     * Checks if the value defined in "config.linkVars" contains an allowed value.
2142
     * Otherwise, return FALSE which means the value will not be added to any links.
2143
     *
2144
     * @param string $haystack The string in which to find $needle
2145
     * @param string $needle The string to find in $haystack
2146
     * @return bool Returns TRUE if $needle matches or is found in $haystack
2147
     */
2148
    protected function isAllowedLinkVarValue(string $haystack, string $needle): bool
2149
    {
2150
        $isAllowed = false;
2151
        // Integer
2152
        if ($needle === 'int' || $needle === 'integer') {
2153
            if (MathUtility::canBeInterpretedAsInteger($haystack)) {
2154
                $isAllowed = true;
2155
            }
2156
        } elseif (preg_match('/^\\/.+\\/[imsxeADSUXu]*$/', $needle)) {
2157
            // Regular expression, only "//" is allowed as delimiter
2158
            if (@preg_match($needle, $haystack)) {
2159
                $isAllowed = true;
2160
            }
2161
        } elseif (strpos($needle, '-') !== false) {
2162
            // Range
2163
            if (MathUtility::canBeInterpretedAsInteger($haystack)) {
2164
                $range = explode('-', $needle);
2165
                if ($range[0] <= $haystack && $range[1] >= $haystack) {
2166
                    $isAllowed = true;
2167
                }
2168
            }
2169
        } elseif (strpos($needle, '|') !== false) {
2170
            // List
2171
            // Trim the input
2172
            $haystack = str_replace(' ', '', $haystack);
2173
            if (strpos('|' . $needle . '|', '|' . $haystack . '|') !== false) {
2174
                $isAllowed = true;
2175
            }
2176
        } elseif ((string)$needle === (string)$haystack) {
2177
            // String comparison
2178
            $isAllowed = true;
2179
        }
2180
        return $isAllowed;
2181
    }
2182
2183
    /**
2184
     * Returns URI of target page, if the current page is an overlaid mountpoint.
2185
     *
2186
     * If the current page is of type mountpoint and should be overlaid with the contents of the mountpoint page
2187
     * and is accessed directly, the user will be redirected to the mountpoint context.
2188
     * @internal
2189
     * @param ServerRequestInterface $request
2190
     * @return string|null
2191
     */
2192
    public function getRedirectUriForMountPoint(ServerRequestInterface $request): ?string
2193
    {
2194
        if (!empty($this->originalMountPointPage) && (int)$this->originalMountPointPage['doktype'] === PageRepository::DOKTYPE_MOUNTPOINT) {
2195
            return $this->getUriToCurrentPageForRedirect($request);
2196
        }
2197
2198
        return null;
2199
    }
2200
2201
    /**
2202
     * Returns URI of target page, if the current page is a Shortcut.
2203
     *
2204
     * If the current page is of type shortcut and accessed directly via its URL,
2205
     * the user will be redirected to shortcut target.
2206
     *
2207
     * @internal
2208
     * @param ServerRequestInterface $request
2209
     * @return string|null
2210
     */
2211
    public function getRedirectUriForShortcut(ServerRequestInterface $request): ?string
2212
    {
2213
        if (!empty($this->originalShortcutPage) && $this->originalShortcutPage['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
2214
            return $this->getUriToCurrentPageForRedirect($request);
2215
        }
2216
2217
        return null;
2218
    }
2219
2220
    /**
2221
     * Instantiate \TYPO3\CMS\Frontend\ContentObject to generate the correct target URL
2222
     *
2223
     * @param ServerRequestInterface $request
2224
     * @return string
2225
     */
2226
    protected function getUriToCurrentPageForRedirect(ServerRequestInterface $request): string
2227
    {
2228
        $this->calculateLinkVars($request->getQueryParams());
2229
        $parameter = $this->page['uid'];
2230
        if ($this->type && MathUtility::canBeInterpretedAsInteger($this->type)) {
2231
            $parameter .= ',' . $this->type;
2232
        }
2233
        return GeneralUtility::makeInstance(ContentObjectRenderer::class, $this)->typoLink_URL([
2234
            'parameter' => $parameter,
2235
            'addQueryString' => true,
2236
            'addQueryString.' => ['exclude' => 'id'],
2237
            'forceAbsoluteUrl' => true
2238
        ]);
2239
    }
2240
2241
    /********************************************
2242
     *
2243
     * Page generation; cache handling
2244
     *
2245
     *******************************************/
2246
    /**
2247
     * Returns TRUE if the page should be generated.
2248
     * That is if no URL handler is active and the cacheContentFlag is not set.
2249
     *
2250
     * @return bool
2251
     */
2252
    public function isGeneratePage()
2253
    {
2254
        return !$this->cacheContentFlag;
2255
    }
2256
2257
    /**
2258
     * Set cache content to $this->content
2259
     */
2260
    protected function realPageCacheContent()
2261
    {
2262
        // seconds until a cached page is too old
2263
        $cacheTimeout = $this->get_cache_timeout();
2264
        $timeOutTime = $GLOBALS['EXEC_TIME'] + $cacheTimeout;
2265
        $usePageCache = true;
2266
        // Hook for deciding whether page cache should be written to the cache backend or not
2267
        // NOTE: as hooks are called in a loop, the last hook will have the final word (however each
2268
        // hook receives the current status of the $usePageCache flag)
2269
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['usePageCache'] ?? [] as $className) {
2270
            $usePageCache = GeneralUtility::makeInstance($className)->usePageCache($this, $usePageCache);
2271
        }
2272
        // Write the page to cache, if necessary
2273
        if ($usePageCache) {
2274
            $this->setPageCacheContent($this->content, $this->config, $timeOutTime);
2275
        }
2276
        // Hook for cache post processing (eg. writing static files!)
2277
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['insertPageIncache'] ?? [] as $className) {
2278
            GeneralUtility::makeInstance($className)->insertPageIncache($this, $timeOutTime);
2279
        }
2280
    }
2281
2282
    /**
2283
     * Sets cache content; Inserts the content string into the cache_pages cache.
2284
     *
2285
     * @param string $content The content to store in the HTML field of the cache table
2286
     * @param mixed $data The additional cache_data array, fx. $this->config
2287
     * @param int $expirationTstamp Expiration timestamp
2288
     * @see realPageCacheContent()
2289
     */
2290
    protected function setPageCacheContent($content, $data, $expirationTstamp)
2291
    {
2292
        $cacheData = [
2293
            'identifier' => $this->newHash,
2294
            'page_id' => $this->id,
2295
            'content' => $content,
2296
            'cache_data' => $data,
2297
            'expires' => $expirationTstamp,
2298
            'tstamp' => $GLOBALS['EXEC_TIME'],
2299
            'pageTitleInfo' => [
2300
                'title' => $this->page['title'],
2301
                'indexedDocTitle' => $this->indexedDocTitle
2302
            ]
2303
        ];
2304
        $this->cacheExpires = $expirationTstamp;
2305
        $this->pageCacheTags[] = 'pageId_' . $cacheData['page_id'];
2306
        // Respect the page cache when content of pid is shown
2307
        if ($this->id !== $this->contentPid) {
2308
            $this->pageCacheTags[] = 'pageId_' . $this->contentPid;
2309
        }
2310
        if (!empty($this->page['cache_tags'])) {
2311
            $tags = GeneralUtility::trimExplode(',', $this->page['cache_tags'], true);
2312
            $this->pageCacheTags = array_merge($this->pageCacheTags, $tags);
2313
        }
2314
        // Add the cache themselves as well, because they are fetched by getPageCacheTags()
2315
        $cacheData['cacheTags'] = $this->pageCacheTags;
2316
        $this->pageCache->set($this->newHash, $cacheData, $this->pageCacheTags, $expirationTstamp - $GLOBALS['EXEC_TIME']);
2317
    }
2318
2319
    /**
2320
     * Clears cache content (for $this->newHash)
2321
     */
2322
    public function clearPageCacheContent()
2323
    {
2324
        $this->pageCache->remove($this->newHash);
2325
    }
2326
2327
    /**
2328
     * Sets sys last changed
2329
     * Setting the SYS_LASTCHANGED value in the pagerecord: This value will thus be set to the highest tstamp of records rendered on the page. This includes all records with no regard to hidden records, userprotection and so on.
2330
     *
2331
     * @see ContentObjectRenderer::lastChanged()
2332
     */
2333
    protected function setSysLastChanged()
2334
    {
2335
        // We only update the info if browsing the live workspace
2336
        if ($this->page['SYS_LASTCHANGED'] < (int)$this->register['SYS_LASTCHANGED'] && !$this->doWorkspacePreview()) {
2337
            $connection = GeneralUtility::makeInstance(ConnectionPool::class)
2338
                ->getConnectionForTable('pages');
2339
            $pageId = $this->page['_PAGES_OVERLAY_UID'] ?? $this->id;
2340
            $connection->update(
2341
                'pages',
2342
                [
2343
                    'SYS_LASTCHANGED' => (int)$this->register['SYS_LASTCHANGED']
2344
                ],
2345
                [
2346
                    'uid' => (int)$pageId
2347
                ]
2348
            );
2349
        }
2350
    }
2351
2352
    /**
2353
     * Set the SYS_LASTCHANGED register value, is also called when a translated page is in use,
2354
     * so the register reflects the state of the translated page, not the page in the default language.
2355
     *
2356
     * @param array $page
2357
     * @internal
2358
     */
2359
    protected function setRegisterValueForSysLastChanged(array $page): void
2360
    {
2361
        $this->register['SYS_LASTCHANGED'] = (int)$page['tstamp'];
2362
        if ($this->register['SYS_LASTCHANGED'] < (int)$page['SYS_LASTCHANGED']) {
2363
            $this->register['SYS_LASTCHANGED'] = (int)$page['SYS_LASTCHANGED'];
2364
        }
2365
    }
2366
2367
    /**
2368
     * Release pending locks
2369
     *
2370
     * @internal
2371
     */
2372
    public function releaseLocks()
2373
    {
2374
        $this->releaseLock('pagesection');
2375
        $this->releaseLock('pages');
2376
    }
2377
2378
    /**
2379
     * Adds tags to this page's cache entry, you can then f.e. remove cache
2380
     * entries by tag
2381
     *
2382
     * @param array $tags An array of tag
2383
     */
2384
    public function addCacheTags(array $tags)
2385
    {
2386
        $this->pageCacheTags = array_merge($this->pageCacheTags, $tags);
2387
    }
2388
2389
    /**
2390
     * @return array
2391
     */
2392
    public function getPageCacheTags(): array
2393
    {
2394
        return $this->pageCacheTags;
2395
    }
2396
2397
    /********************************************
2398
     *
2399
     * Page generation; rendering and inclusion
2400
     *
2401
     *******************************************/
2402
    /**
2403
     * Does some processing BEFORE the page content is generated / built.
2404
     */
2405
    public function generatePage_preProcessing()
2406
    {
2407
        // Same codeline as in getFromCache(). But $this->all has been changed by
2408
        // \TYPO3\CMS\Core\TypoScript\TemplateService::start() in the meantime, so this must be called again!
2409
        $this->newHash = $this->getHash();
2410
2411
        // Used as a safety check in case a PHP script is falsely disabling $this->no_cache during page generation.
2412
        $this->no_cacheBeforePageGen = $this->no_cache;
2413
    }
2414
2415
    /**
2416
     * Check the value of "content_from_pid" of the current page record, and see if the current request
2417
     * should actually show content from another page.
2418
     *
2419
     * By using $TSFE->getPageAndRootline() on the cloned object, all rootline restrictions (extendToSubPages)
2420
     * are evaluated as well.
2421
     *
2422
     * @param ServerRequestInterface $request
2423
     * @return int the current page ID or another one if resolved properly - usually set to $this->contentPid
2424
     */
2425
    protected function resolveContentPid(ServerRequestInterface $request): int
2426
    {
2427
        if (!isset($this->page['content_from_pid']) || empty($this->page['content_from_pid'])) {
2428
            return (int)$this->id;
2429
        }
2430
        // make REAL copy of TSFE object - not reference!
2431
        $temp_copy_TSFE = clone $this;
2432
        // Set ->id to the content_from_pid value - we are going to evaluate this pid as was it a given id for a page-display!
2433
        $temp_copy_TSFE->id = $this->page['content_from_pid'];
2434
        $temp_copy_TSFE->MP = '';
2435
        $temp_copy_TSFE->getPageAndRootline($request);
2436
        return (int)$temp_copy_TSFE->id;
2437
    }
2438
    /**
2439
     * Sets up TypoScript "config." options and set properties in $TSFE.
2440
     *
2441
     * @param ServerRequestInterface $request
2442
     */
2443
    public function preparePageContentGeneration(ServerRequestInterface $request)
2444
    {
2445
        $this->getTimeTracker()->push('Prepare page content generation');
2446
        $this->contentPid = $this->resolveContentPid($request);
2447
        // Global vars...
2448
        $this->indexedDocTitle = $this->page['title'] ?? null;
2449
        // Base url:
2450
        if (isset($this->config['config']['baseURL'])) {
2451
            $this->baseUrl = $this->config['config']['baseURL'];
2452
        }
2453
        // Internal and External target defaults
2454
        $this->intTarget = (string)($this->config['config']['intTarget'] ?? '');
2455
        $this->extTarget = (string)($this->config['config']['extTarget'] ?? '');
2456
        $this->fileTarget = (string)($this->config['config']['fileTarget'] ?? '');
2457
        $this->spamProtectEmailAddresses = $this->config['config']['spamProtectEmailAddresses'] ?? 0;
2458
        if ($this->spamProtectEmailAddresses !== 'ascii') {
2459
            $this->spamProtectEmailAddresses = MathUtility::forceIntegerInRange($this->spamProtectEmailAddresses, -10, 10, 0);
2460
        }
2461
        // calculate the absolute path prefix
2462
        if (!empty($this->config['config']['absRefPrefix'])) {
2463
            $absRefPrefix = trim($this->config['config']['absRefPrefix']);
2464
            if ($absRefPrefix === 'auto') {
2465
                $this->absRefPrefix = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
2466
            } else {
2467
                $this->absRefPrefix = $absRefPrefix;
2468
            }
2469
        } else {
2470
            $this->absRefPrefix = '';
2471
        }
2472
        $this->ATagParams = trim($this->config['config']['ATagParams'] ?? '') ? ' ' . trim($this->config['config']['ATagParams']) : '';
2473
        $this->initializeSearchWordData($request->getParsedBody()['sword_list'] ?? $request->getQueryParams()['sword_list'] ?? null);
2474
        // linkVars
2475
        $this->calculateLinkVars($request->getQueryParams());
2476
        // Setting XHTML-doctype from doctype
2477
        if (!isset($this->config['config']['xhtmlDoctype']) || !$this->config['config']['xhtmlDoctype']) {
2478
            $this->config['config']['xhtmlDoctype'] = $this->config['config']['doctype'] ?? '';
2479
        }
2480
        if ($this->config['config']['xhtmlDoctype']) {
2481
            $this->xhtmlDoctype = $this->config['config']['xhtmlDoctype'];
2482
            // Checking XHTML-docytpe
2483
            switch ((string)$this->config['config']['xhtmlDoctype']) {
2484
                case 'xhtml_trans':
2485
                case 'xhtml_strict':
2486
                    $this->xhtmlVersion = 100;
2487
                    break;
2488
                case 'xhtml_basic':
2489
                    $this->xhtmlVersion = 105;
2490
                    break;
2491
                case 'xhtml_11':
2492
                case 'xhtml+rdfa_10':
2493
                    $this->xhtmlVersion = 110;
2494
                    break;
2495
                default:
2496
                    $this->pageRenderer->setRenderXhtml(false);
2497
                    $this->xhtmlDoctype = '';
2498
                    $this->xhtmlVersion = 0;
2499
            }
2500
        } else {
2501
            $this->pageRenderer->setRenderXhtml(false);
2502
        }
2503
2504
        // Global content object
2505
        $this->newCObj($request);
2506
        $this->getTimeTracker()->pull();
2507
    }
2508
2509
    /**
2510
     * Fills the sWordList property and builds the regular expression in TSFE that can be used to split
2511
     * strings by the submitted search words.
2512
     *
2513
     * @param mixed $searchWords - usually an array, but we can't be sure (yet)
2514
     * @see sWordList
2515
     * @see sWordRegEx
2516
     */
2517
    protected function initializeSearchWordData($searchWords)
2518
    {
2519
        $this->sWordRegEx = '';
2520
        $this->sWordList = $searchWords ?? '';
2521
        if (is_array($this->sWordList)) {
2522
            $space = !empty($this->config['config']['sword_standAlone'] ?? null) ? '[[:space:]]' : '';
2523
            $regexpParts = [];
2524
            foreach ($this->sWordList as $val) {
2525
                if (trim($val) !== '') {
2526
                    $regexpParts[] = $space . preg_quote($val, '/') . $space;
2527
                }
2528
            }
2529
            $this->sWordRegEx = implode('|', $regexpParts);
2530
        }
2531
    }
2532
2533
    /**
2534
     * Does processing of the content after the page content was generated.
2535
     *
2536
     * This includes caching the page, indexing the page (if configured) and setting sysLastChanged
2537
     */
2538
    public function generatePage_postProcessing()
2539
    {
2540
        $this->setAbsRefPrefix();
2541
        // This is to ensure, that the page is NOT cached if the no_cache parameter was set before the page was generated. This is a safety precaution, as it could have been unset by some script.
2542
        if ($this->no_cacheBeforePageGen) {
2543
            $this->set_no_cache('no_cache has been set before the page was generated - safety check', true);
2544
        }
2545
        // Hook for post-processing of page content cached/non-cached:
2546
        $_params = ['pObj' => &$this];
2547
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-all'] ?? [] as $_funcRef) {
2548
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2549
        }
2550
        // Processing if caching is enabled:
2551
        if (!$this->no_cache) {
2552
            // Hook for post-processing of page content before being cached:
2553
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-cached'] ?? [] as $_funcRef) {
2554
                GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2555
            }
2556
        }
2557
        // Convert charset for output. Any hooks before (including indexed search) will have to convert from UTF-8 to the target
2558
        // charset as well.
2559
        $this->content = $this->convOutputCharset($this->content);
2560
        // Storing for cache:
2561
        if (!$this->no_cache) {
2562
            $this->realPageCacheContent();
2563
        }
2564
        // Sets sys-last-change:
2565
        $this->setSysLastChanged();
2566
    }
2567
2568
    /**
2569
     * Generate the page title, can be called multiple times,
2570
     * as PageTitleProvider might have been modified by an uncached plugin etc.
2571
     *
2572
     * @return string the generated page title
2573
     */
2574
    public function generatePageTitle(): string
2575
    {
2576
        // Check for a custom pageTitleSeparator, and perform stdWrap on it
2577
        $pageTitleSeparator = (string)$this->cObj->stdWrapValue('pageTitleSeparator', $this->config['config'] ?? []);
2578
        if ($pageTitleSeparator !== '' && $pageTitleSeparator === ($this->config['config']['pageTitleSeparator'] ?? '')) {
2579
            $pageTitleSeparator .= ' ';
2580
        }
2581
2582
        $titleProvider = GeneralUtility::makeInstance(PageTitleProviderManager::class);
2583
        if (!empty($this->config['config']['pageTitleCache'])) {
2584
            $titleProvider->setPageTitleCache($this->config['config']['pageTitleCache']);
2585
        }
2586
        $pageTitle = $titleProvider->getTitle();
2587
        $this->config['config']['pageTitleCache'] = $titleProvider->getPageTitleCache();
2588
2589
        if ($pageTitle !== '') {
2590
            $this->indexedDocTitle = $pageTitle;
2591
        }
2592
2593
        $titleTagContent = $this->printTitle(
2594
            $pageTitle,
2595
            (bool)($this->config['config']['noPageTitle'] ?? false),
2596
            (bool)($this->config['config']['pageTitleFirst'] ?? false),
2597
            $pageTitleSeparator
2598
        );
2599
        $this->config['config']['pageTitle'] = $titleTagContent;
2600
        // stdWrap around the title tag
2601
        $titleTagContent = $this->cObj->stdWrapValue('pageTitle', $this->config['config']);
2602
2603
        // config.noPageTitle = 2 - means do not render the page title
2604
        if (isset($this->config['config']['noPageTitle']) && (int)$this->config['config']['noPageTitle'] === 2) {
2605
            $titleTagContent = '';
2606
        }
2607
        if ($titleTagContent !== '') {
2608
            $this->pageRenderer->setTitle($titleTagContent);
2609
        }
2610
        return (string)$titleTagContent;
2611
    }
2612
2613
    /**
2614
     * Compiles the content for the page <title> tag.
2615
     *
2616
     * @param string $pageTitle The input title string, typically the "title" field of a page's record.
2617
     * @param bool $noTitle If set, then only the site title is outputted
2618
     * @param bool $showTitleFirst If set, then website title and $title is swapped
2619
     * @param string $pageTitleSeparator an alternative to the ": " as the separator between site title and page title
2620
     * @return string The page title on the form "[website title]: [input-title]". Not htmlspecialchar()'ed.
2621
     * @see generatePageTitle()
2622
     */
2623
    protected function printTitle(string $pageTitle, bool $noTitle = false, bool $showTitleFirst = false, string $pageTitleSeparator = ''): string
2624
    {
2625
        $websiteTitle = $this->getWebsiteTitle();
2626
        $pageTitle = $noTitle ? '' : $pageTitle;
2627
        if ($showTitleFirst) {
2628
            $temp = $websiteTitle;
2629
            $websiteTitle = $pageTitle;
2630
            $pageTitle = $temp;
2631
        }
2632
        // only show a separator if there are both site title and page title
2633
        if ($pageTitle === '' || $websiteTitle === '') {
2634
            $pageTitleSeparator = '';
2635
        } elseif (empty($pageTitleSeparator)) {
2636
            // use the default separator if non given
2637
            $pageTitleSeparator = ': ';
2638
        }
2639
        return $websiteTitle . $pageTitleSeparator . $pageTitle;
2640
    }
2641
2642
    /**
2643
     * @return string
2644
     */
2645
    protected function getWebsiteTitle(): string
2646
    {
2647
        if ($this->language instanceof SiteLanguage
2648
            && trim($this->language->getWebsiteTitle()) !== ''
2649
        ) {
2650
            return trim($this->language->getWebsiteTitle());
2651
        }
2652
        if ($this->site instanceof SiteInterface
2653
            && trim($this->site->getConfiguration()['websiteTitle'] ?? '') !== ''
0 ignored issues
show
Bug introduced by
The method getConfiguration() does not exist on TYPO3\CMS\Core\Site\Entity\SiteInterface. It seems like you code against a sub-type of TYPO3\CMS\Core\Site\Entity\SiteInterface such as TYPO3\CMS\Core\Site\Entity\Site. ( Ignorable by Annotation )

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

2653
            && trim($this->site->/** @scrutinizer ignore-call */ getConfiguration()['websiteTitle'] ?? '') !== ''
Loading history...
2654
        ) {
2655
            return trim($this->site->getConfiguration()['websiteTitle']);
2656
        }
2657
2658
        return '';
2659
    }
2660
2661
    /**
2662
     * Processes the INTinclude-scripts
2663
     *
2664
     * @param ServerRequestInterface|null $request
2665
     */
2666
    public function INTincScript(ServerRequestInterface $request = null)
2667
    {
2668
        $request = $request ?? $GLOBALS['TYPO3_REQUEST'];
2669
        $this->additionalHeaderData = $this->config['INTincScript_ext']['additionalHeaderData'] ?? [];
2670
        $this->additionalFooterData = $this->config['INTincScript_ext']['additionalFooterData'] ?? [];
2671
        if (empty($this->config['INTincScript_ext']['pageRenderer'])) {
2672
            $this->initPageRenderer();
2673
        } else {
2674
            /** @var PageRenderer $pageRenderer */
2675
            $pageRenderer = unserialize($this->config['INTincScript_ext']['pageRenderer']);
2676
            $this->pageRenderer->updateState($pageRenderer->getState());
2677
        }
2678
        if (!empty($this->config['INTincScript_ext']['assetCollector'])) {
2679
            /** @var AssetCollector $assetCollector */
2680
            $assetCollector = unserialize($this->config['INTincScript_ext']['assetCollector'], ['allowed_classes' => [AssetCollector::class]]);
2681
            GeneralUtility::makeInstance(AssetCollector::class)->updateState($assetCollector->getState());
2682
        }
2683
2684
        $this->recursivelyReplaceIntPlaceholdersInContent($request);
2685
        $this->getTimeTracker()->push('Substitute header section');
2686
        $this->INTincScript_loadJSCode();
2687
        $this->generatePageTitle();
2688
2689
        $this->content = str_replace(
2690
            [
2691
                '<!--HD_' . $this->config['INTincScript_ext']['divKey'] . '-->',
2692
                '<!--FD_' . $this->config['INTincScript_ext']['divKey'] . '-->',
2693
            ],
2694
            [
2695
                $this->convOutputCharset(implode(LF, $this->additionalHeaderData)),
2696
                $this->convOutputCharset(implode(LF, $this->additionalFooterData)),
2697
            ],
2698
            $this->pageRenderer->renderJavaScriptAndCssForProcessingOfUncachedContentObjects($this->content, $this->config['INTincScript_ext']['divKey'])
2699
        );
2700
        // Replace again, because header and footer data and page renderer replacements may introduce additional placeholders (see #44825)
2701
        $this->recursivelyReplaceIntPlaceholdersInContent($request);
2702
        $this->setAbsRefPrefix();
2703
        $this->getTimeTracker()->pull();
2704
    }
2705
2706
    /**
2707
     * Replaces INT placeholders (COA_INT and USER_INT) in $this->content
2708
     * In case the replacement adds additional placeholders, it loops
2709
     * until no new placeholders are found any more.
2710
     */
2711
    protected function recursivelyReplaceIntPlaceholdersInContent(ServerRequestInterface $request)
2712
    {
2713
        do {
2714
            $nonCacheableData = $this->config['INTincScript'];
2715
            $this->processNonCacheableContentPartsAndSubstituteContentMarkers($nonCacheableData, $request);
2716
            // Check if there were new items added to INTincScript during the previous execution:
2717
            // array_diff_assoc throws notices if values are arrays but not strings. We suppress this here.
2718
            $nonCacheableData = @array_diff_assoc($this->config['INTincScript'], $nonCacheableData);
2719
            $reprocess = count($nonCacheableData) > 0;
2720
        } while ($reprocess);
2721
    }
2722
2723
    /**
2724
     * Processes the INTinclude-scripts and substitute in content.
2725
     *
2726
     * Takes $this->content, and splits the content by <!--INT_SCRIPT.12345 --> and then puts the content
2727
     * back together.
2728
     *
2729
     * @param array $nonCacheableData $GLOBALS['TSFE']->config['INTincScript'] or part of it
2730
     * @see INTincScript()
2731
     */
2732
    protected function processNonCacheableContentPartsAndSubstituteContentMarkers(array $nonCacheableData, ServerRequestInterface $request)
2733
    {
2734
        $timeTracker = $this->getTimeTracker();
2735
        $timeTracker->push('Split content');
2736
        // Splits content with the key.
2737
        $contentSplitByUncacheableMarkers = explode('<!--INT_SCRIPT.', $this->content);
2738
        $this->content = '';
2739
        $timeTracker->setTSlogMessage('Parts: ' . count($contentSplitByUncacheableMarkers));
2740
        $timeTracker->pull();
2741
        foreach ($contentSplitByUncacheableMarkers as $counter => $contentPart) {
2742
            // If the split had a comment-end after 32 characters it's probably a split-string
2743
            if (substr($contentPart, 32, 3) === '-->') {
2744
                $nonCacheableKey = 'INT_SCRIPT.' . substr($contentPart, 0, 32);
2745
                if (is_array($nonCacheableData[$nonCacheableKey])) {
2746
                    $label = 'Include ' . $nonCacheableData[$nonCacheableKey]['type'];
2747
                    $timeTracker->push($label);
2748
                    $nonCacheableContent = '';
2749
                    $contentObjectRendererForNonCacheable = unserialize($nonCacheableData[$nonCacheableKey]['cObj']);
2750
                    /* @var ContentObjectRenderer $contentObjectRendererForNonCacheable */
2751
                    $contentObjectRendererForNonCacheable->setRequest($request);
2752
                    switch ($nonCacheableData[$nonCacheableKey]['type']) {
2753
                        case 'COA':
2754
                            $nonCacheableContent = $contentObjectRendererForNonCacheable->cObjGetSingle('COA', $nonCacheableData[$nonCacheableKey]['conf']);
2755
                            break;
2756
                        case 'FUNC':
2757
                            $nonCacheableContent = $contentObjectRendererForNonCacheable->cObjGetSingle('USER', $nonCacheableData[$nonCacheableKey]['conf']);
2758
                            break;
2759
                        case 'POSTUSERFUNC':
2760
                            $nonCacheableContent = $contentObjectRendererForNonCacheable->callUserFunction($nonCacheableData[$nonCacheableKey]['postUserFunc'], $nonCacheableData[$nonCacheableKey]['conf'], $nonCacheableData[$nonCacheableKey]['content']);
2761
                            break;
2762
                    }
2763
                    $this->content .= $this->convOutputCharset($nonCacheableContent);
2764
                    $this->content .= substr($contentPart, 35);
2765
                    $timeTracker->pull($nonCacheableContent);
2766
                } else {
2767
                    $this->content .= substr($contentPart, 35);
2768
                }
2769
            } elseif ($counter) {
2770
                // If it's not the first entry (which would be "0" of the array keys), then re-add the INT_SCRIPT part
2771
                $this->content .= '<!--INT_SCRIPT.' . $contentPart;
2772
            } else {
2773
                $this->content .= $contentPart;
2774
            }
2775
        }
2776
    }
2777
2778
    /**
2779
     * Loads the JavaScript/CSS code for INTincScript, if there are non-cacheable content objects
2780
     * it prepares the placeholders, otherwise populates options directly.
2781
     *
2782
     * @internal this method should be renamed as it does not only handle JS, but all additional header data
2783
     */
2784
    public function INTincScript_loadJSCode()
2785
    {
2786
        // Prepare code and placeholders for additional header and footer files (and make sure that this isn't called twice)
2787
        if ($this->isINTincScript() && !isset($this->config['INTincScript_ext'])) {
2788
            $substituteHash = $this->uniqueHash();
2789
            $this->config['INTincScript_ext']['divKey'] = $substituteHash;
2790
            // Storing the header-data array
2791
            $this->config['INTincScript_ext']['additionalHeaderData'] = $this->additionalHeaderData;
2792
            // Storing the footer-data array
2793
            $this->config['INTincScript_ext']['additionalFooterData'] = $this->additionalFooterData;
2794
            // Clearing the array
2795
            $this->additionalHeaderData = ['<!--HD_' . $substituteHash . '-->'];
2796
            // Clearing the array
2797
            $this->additionalFooterData = ['<!--FD_' . $substituteHash . '-->'];
2798
        }
2799
    }
2800
2801
    /**
2802
     * Determines if there are any INTincScripts to include = "non-cacheable" parts
2803
     *
2804
     * @return bool Returns TRUE if scripts are found
2805
     */
2806
    public function isINTincScript()
2807
    {
2808
        return !empty($this->config['INTincScript']) && is_array($this->config['INTincScript']);
2809
    }
2810
2811
    /**
2812
     * Add HTTP headers to the response object.
2813
     *
2814
     * @param ResponseInterface $response
2815
     * @return ResponseInterface
2816
     */
2817
    public function applyHttpHeadersToResponse(ResponseInterface $response): ResponseInterface
2818
    {
2819
        // Set header for charset-encoding unless disabled
2820
        if (empty($this->config['config']['disableCharsetHeader'])) {
2821
            $response = $response->withHeader('Content-Type', $this->contentType . '; charset=' . trim($this->metaCharset));
2822
        }
2823
        // Set header for content language unless disabled
2824
        $contentLanguage = $this->language->getTwoLetterIsoCode();
2825
        if (empty($this->config['config']['disableLanguageHeader']) && !empty($contentLanguage)) {
2826
            $response = $response->withHeader('Content-Language', trim($contentLanguage));
2827
        }
2828
        // Set cache related headers to client (used to enable proxy / client caching!)
2829
        if (!empty($this->config['config']['sendCacheHeaders'])) {
2830
            $headers = $this->getCacheHeaders();
2831
            foreach ($headers as $header => $value) {
2832
                $response = $response->withHeader($header, $value);
2833
            }
2834
        }
2835
        // Set additional headers if any have been configured via TypoScript
2836
        $additionalHeaders = $this->getAdditionalHeaders();
2837
        foreach ($additionalHeaders as $headerConfig) {
2838
            [$header, $value] = GeneralUtility::trimExplode(':', $headerConfig['header'], false, 2);
2839
            if ($headerConfig['statusCode']) {
2840
                $response = $response->withStatus((int)$headerConfig['statusCode']);
2841
            }
2842
            if ($headerConfig['replace']) {
2843
                $response = $response->withHeader($header, $value);
2844
            } else {
2845
                $response = $response->withAddedHeader($header, $value);
2846
            }
2847
        }
2848
        return $response;
2849
    }
2850
2851
    /**
2852
     * Get cache headers good for client/reverse proxy caching.
2853
     *
2854
     * @return array
2855
     */
2856
    protected function getCacheHeaders(): array
2857
    {
2858
        // Getting status whether we can send cache control headers for proxy caching:
2859
        $doCache = $this->isStaticCacheble();
2860
        // This variable will be TRUE unless cache headers are configured to be sent ONLY if a branch does not allow logins and logins turns out to be allowed anyway...
2861
        $loginsDeniedCfg = empty($this->config['config']['sendCacheHeaders_onlyWhenLoginDeniedInBranch']) || empty($this->loginAllowedInBranch);
2862
        // Finally, when backend users are logged in, do not send cache headers at all (Admin Panel might be displayed for instance).
2863
        $this->isClientCachable = $doCache && !$this->isBackendUserLoggedIn() && !$this->doWorkspacePreview() && $loginsDeniedCfg;
2864
        if ($this->isClientCachable) {
2865
            $headers = [
2866
                'Expires' => gmdate('D, d M Y H:i:s T', $this->cacheExpires),
2867
                'ETag' => '"' . md5($this->content) . '"',
2868
                'Cache-Control' => 'max-age=' . ($this->cacheExpires - $GLOBALS['EXEC_TIME']),
2869
                // no-cache
2870
                'Pragma' => 'public'
2871
            ];
2872
        } else {
2873
            // "no-store" is used to ensure that the client HAS to ask the server every time, and is not allowed to store anything at all
2874
            $headers = [
2875
                'Cache-Control' => 'private, no-store'
2876
            ];
2877
            // Now, if a backend user is logged in, tell him in the Admin Panel log what the caching status would have been:
2878
            if ($this->isBackendUserLoggedIn()) {
2879
                if ($doCache) {
2880
                    $this->getTimeTracker()->setTSlogMessage('Cache-headers with max-age "' . ($this->cacheExpires - $GLOBALS['EXEC_TIME']) . '" would have been sent');
2881
                } else {
2882
                    $reasonMsg = [];
2883
                    if ($this->no_cache) {
2884
                        $reasonMsg[] = 'Caching disabled (no_cache).';
2885
                    }
2886
                    if ($this->isINTincScript()) {
2887
                        $reasonMsg[] = '*_INT object(s) on page.';
2888
                    }
2889
                    if (is_array($this->fe_user->user)) {
2890
                        $reasonMsg[] = 'Frontend user logged in.';
2891
                    }
2892
                    $this->getTimeTracker()->setTSlogMessage('Cache-headers would disable proxy caching! Reason(s): "' . implode(' ', $reasonMsg) . '"', 1);
2893
                }
2894
            }
2895
        }
2896
        return $headers;
2897
    }
2898
2899
    /**
2900
     * Reporting status whether we can send cache control headers for proxy caching or publishing to static files
2901
     *
2902
     * Rules are:
2903
     * no_cache cannot be set: If it is, the page might contain dynamic content and should never be cached.
2904
     * There can be no USER_INT objects on the page ("isINTincScript()") because they implicitly indicate dynamic content
2905
     * There can be no logged in user because user sessions are based on a cookie and thereby does not offer client caching a chance to know if the user is logged in. Actually, there will be a reverse problem here; If a page will somehow change when a user is logged in he may not see it correctly if the non-login version sent a cache-header! So do NOT use cache headers in page sections where user logins change the page content. (unless using such as realurl to apply a prefix in case of login sections)
2906
     *
2907
     * @return bool
2908
     */
2909
    public function isStaticCacheble()
2910
    {
2911
        return !$this->no_cache && !$this->isINTincScript() && !$this->isUserOrGroupSet();
2912
    }
2913
2914
    /********************************************
2915
     *
2916
     * Various internal API functions
2917
     *
2918
     *******************************************/
2919
    /**
2920
     * Creates an instance of ContentObjectRenderer in $this->cObj
2921
     * This instance is used to start the rendering of the TypoScript template structure
2922
     *
2923
     * @param ServerRequestInterface|null $request
2924
     */
2925
    public function newCObj(ServerRequestInterface $request = null)
2926
    {
2927
        $this->cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class, $this);
2928
        $this->cObj->start($this->page, 'pages', $request);
2929
    }
2930
2931
    /**
2932
     * Converts relative paths in the HTML source to absolute paths for fileadmin/, typo3conf/ext/ and media/ folders.
2933
     *
2934
     * @internal
2935
     * @see \TYPO3\CMS\Frontend\Http\RequestHandler
2936
     * @see INTincScript()
2937
     */
2938
    public function setAbsRefPrefix()
2939
    {
2940
        if (!$this->absRefPrefix) {
2941
            return;
2942
        }
2943
        $search = [
2944
            '"typo3temp/',
2945
            '"' . PathUtility::stripPathSitePrefix(Environment::getExtensionsPath()) . '/',
2946
            '"' . PathUtility::stripPathSitePrefix(Environment::getBackendPath()) . '/ext/',
2947
            '"' . PathUtility::stripPathSitePrefix(Environment::getFrameworkBasePath()) . '/',
2948
        ];
2949
        $replace = [
2950
            '"' . $this->absRefPrefix . 'typo3temp/',
2951
            '"' . $this->absRefPrefix . PathUtility::stripPathSitePrefix(Environment::getExtensionsPath()) . '/',
2952
            '"' . $this->absRefPrefix . PathUtility::stripPathSitePrefix(Environment::getBackendPath()) . '/ext/',
2953
            '"' . $this->absRefPrefix . PathUtility::stripPathSitePrefix(Environment::getFrameworkBasePath()) . '/',
2954
        ];
2955
        /** @var StorageRepository $storageRepository */
2956
        $storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
2957
        $storages = $storageRepository->findAll();
2958
        foreach ($storages as $storage) {
2959
            if ($storage->getDriverType() === 'Local' && $storage->isPublic() && $storage->isOnline()) {
2960
                $folder = $storage->getPublicUrl($storage->getRootLevelFolder());
2961
                $search[] = '"' . $folder;
2962
                $replace[] = '"' . $this->absRefPrefix . $folder;
2963
            }
2964
        }
2965
        // Process additional directories
2966
        $directories = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['FE']['additionalAbsRefPrefixDirectories'], true);
2967
        foreach ($directories as $directory) {
2968
            $search[] = '"' . $directory;
2969
            $replace[] = '"' . $this->absRefPrefix . $directory;
2970
        }
2971
        $this->content = str_replace(
2972
            $search,
2973
            $replace,
2974
            $this->content
2975
        );
2976
    }
2977
2978
    /**
2979
     * Prefixing the input URL with ->baseUrl If ->baseUrl is set and the input url is not absolute in some way.
2980
     * Designed as a wrapper functions for use with all frontend links that are processed by JavaScript (for "realurl" compatibility!). So each time a URL goes into window.open, window.location.href or otherwise, wrap it with this function!
2981
     *
2982
     * @param string $url Input URL, relative or absolute
2983
     * @return string Processed input value.
2984
     */
2985
    public function baseUrlWrap($url)
2986
    {
2987
        if ($this->baseUrl) {
2988
            $urlParts = parse_url($url);
2989
            if (empty($urlParts['scheme']) && $url[0] !== '/') {
2990
                $url = $this->baseUrl . $url;
2991
            }
2992
        }
2993
        return $url;
2994
    }
2995
2996
    /**
2997
     * Logs access to deprecated TypoScript objects and properties.
2998
     *
2999
     * Dumps message to the TypoScript message log (admin panel) and the TYPO3 deprecation log.
3000
     *
3001
     * @param string $typoScriptProperty Deprecated object or property
3002
     * @param string $explanation Message or additional information
3003
     */
3004
    public function logDeprecatedTyposcript($typoScriptProperty, $explanation = '')
3005
    {
3006
        $explanationText = $explanation !== '' ? ' - ' . $explanation : '';
3007
        $this->getTimeTracker()->setTSlogMessage($typoScriptProperty . ' is deprecated.' . $explanationText, 2);
3008
        trigger_error('TypoScript property ' . $typoScriptProperty . ' is deprecated' . $explanationText, E_USER_DEPRECATED);
3009
    }
3010
3011
    /********************************************
3012
     * PUBLIC ACCESSIBLE WORKSPACES FUNCTIONS
3013
     *******************************************/
3014
3015
    /**
3016
     * Returns TRUE if workspace preview is enabled
3017
     *
3018
     * @return bool Returns TRUE if workspace preview is enabled
3019
     */
3020
    public function doWorkspacePreview()
3021
    {
3022
        return $this->context->getPropertyFromAspect('workspace', 'isOffline', false);
3023
    }
3024
3025
    /**
3026
     * Returns the uid of the current workspace
3027
     *
3028
     * @return int returns workspace integer for which workspace is being preview. 0 if none (= live workspace).
3029
     */
3030
    public function whichWorkspace(): int
3031
    {
3032
        return $this->context->getPropertyFromAspect('workspace', 'id', 0);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->context->g...t('workspace', 'id', 0) could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
3033
    }
3034
3035
    /********************************************
3036
     *
3037
     * Various external API functions - for use in plugins etc.
3038
     *
3039
     *******************************************/
3040
3041
    /**
3042
     * Returns the pages TSconfig array based on the current ->rootLine
3043
     *
3044
     * @return array
3045
     */
3046
    public function getPagesTSconfig()
3047
    {
3048
        if (!is_array($this->pagesTSconfig)) {
3049
            $contentHashCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
3050
            $loader = GeneralUtility::makeInstance(PageTsConfigLoader::class);
3051
            $tsConfigString = $loader->load(array_reverse($this->rootLine));
3052
            $parser = GeneralUtility::makeInstance(
3053
                PageTsConfigParser::class,
3054
                GeneralUtility::makeInstance(TypoScriptParser::class),
3055
                $contentHashCache
3056
            );
3057
            $this->pagesTSconfig = $parser->parse(
3058
                $tsConfigString,
3059
                GeneralUtility::makeInstance(ConditionMatcher::class, $this->context, $this->id, $this->rootLine),
3060
                $this->site
3061
            );
3062
        }
3063
        return $this->pagesTSconfig;
3064
    }
3065
3066
    /**
3067
     * Returns a unique md5 hash.
3068
     * There is no special magic in this, the only point is that you don't have to call md5(uniqid()) which is slow and by this you are sure to get a unique string each time in a little faster way.
3069
     *
3070
     * @param string $str Some string to include in what is hashed. Not significant at all.
3071
     * @return string MD5 hash of ->uniqueString, input string and uniqueCounter
3072
     */
3073
    public function uniqueHash($str = '')
3074
    {
3075
        return md5($this->uniqueString . '_' . $str . $this->uniqueCounter++);
3076
    }
3077
3078
    /**
3079
     * Sets the cache-flag to 1. Could be called from user-included php-files in order to ensure that a page is not cached.
3080
     *
3081
     * @param string $reason An optional reason to be written to the log.
3082
     * @param bool $internal Whether the call is done from core itself (should only be used by core).
3083
     */
3084
    public function set_no_cache($reason = '', $internal = false)
3085
    {
3086
        $context = [];
3087
        if ($reason !== '') {
3088
            $warning = '$TSFE->set_no_cache() was triggered. Reason: {reason}.';
3089
            $context['reason'] = $reason;
3090
        } else {
3091
            $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
3092
            // This is a hack to work around ___FILE___ resolving symbolic links
3093
            $realWebPath = PathUtility::dirname((string)realpath(Environment::getBackendPath())) . '/';
3094
            $file = $trace[0]['file'];
3095
            if (strpos($file, $realWebPath) === 0) {
3096
                $file = str_replace($realWebPath, '', $file);
3097
            } else {
3098
                $file = str_replace(Environment::getPublicPath() . '/', '', $file);
3099
            }
3100
            $warning = '$GLOBALS[\'TSFE\']->set_no_cache() was triggered by {file} on line {line}.';
3101
            $context['file'] = $file;
3102
            $context['line'] = $trace[0]['line'];
3103
        }
3104
        if (!$internal && $GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter']) {
3105
            $warning .= ' However, $TYPO3_CONF_VARS[\'FE\'][\'disableNoCacheParameter\'] is set, so it will be ignored!';
3106
            $this->getTimeTracker()->setTSlogMessage($warning, 2);
3107
        } else {
3108
            $warning .= ' Caching is disabled!';
3109
            $this->disableCache();
3110
        }
3111
        if ($internal && $this->isBackendUserLoggedIn()) {
3112
            $this->logger->notice($warning, $context);
3113
        } else {
3114
            $this->logger->warning($warning, $context);
3115
        }
3116
    }
3117
3118
    /**
3119
     * Disables caching of the current page.
3120
     *
3121
     * @internal
3122
     */
3123
    protected function disableCache()
3124
    {
3125
        $this->no_cache = true;
3126
    }
3127
3128
    /**
3129
     * Sets the cache-timeout in seconds
3130
     *
3131
     * @param int $seconds Cache-timeout in seconds
3132
     */
3133
    public function set_cache_timeout_default($seconds)
3134
    {
3135
        $seconds = (int)$seconds;
3136
        if ($seconds > 0) {
3137
            $this->cacheTimeOutDefault = $seconds;
3138
        }
3139
    }
3140
3141
    /**
3142
     * Get the cache timeout for the current page.
3143
     *
3144
     * @return int The cache timeout for the current page.
3145
     */
3146
    public function get_cache_timeout()
3147
    {
3148
        /** @var \TYPO3\CMS\Core\Cache\Frontend\AbstractFrontend $runtimeCache */
3149
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
3150
        $cachedCacheLifetimeIdentifier = 'core-tslib_fe-get_cache_timeout';
3151
        $cachedCacheLifetime = $runtimeCache->get($cachedCacheLifetimeIdentifier);
3152
        if ($cachedCacheLifetime === false) {
3153
            if ($this->page['cache_timeout']) {
3154
                // Cache period was set for the page:
3155
                $cacheTimeout = $this->page['cache_timeout'];
3156
            } else {
3157
                // Cache period was set via TypoScript "config.cache_period",
3158
                // otherwise it's the default of 24 hours
3159
                $cacheTimeout = $this->cacheTimeOutDefault;
3160
            }
3161
            if (!empty($this->config['config']['cache_clearAtMidnight'])) {
3162
                $timeOutTime = $GLOBALS['EXEC_TIME'] + $cacheTimeout;
3163
                $midnightTime = mktime(0, 0, 0, (int)date('m', $timeOutTime), (int)date('d', $timeOutTime), (int)date('Y', $timeOutTime));
3164
                // If the midnight time of the expire-day is greater than the current time,
3165
                // we may set the timeOutTime to the new midnighttime.
3166
                if ($midnightTime > $GLOBALS['EXEC_TIME']) {
3167
                    $cacheTimeout = $midnightTime - $GLOBALS['EXEC_TIME'];
3168
                }
3169
            }
3170
3171
            // Calculate the timeout time for records on the page and adjust cache timeout if necessary
3172
            $cacheTimeout = min($this->calculatePageCacheTimeout(), $cacheTimeout);
3173
3174
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['get_cache_timeout'] ?? [] as $_funcRef) {
3175
                $params = ['cacheTimeout' => $cacheTimeout];
3176
                $cacheTimeout = GeneralUtility::callUserFunction($_funcRef, $params, $this);
3177
            }
3178
            $runtimeCache->set($cachedCacheLifetimeIdentifier, $cacheTimeout);
3179
            $cachedCacheLifetime = $cacheTimeout;
3180
        }
3181
        return $cachedCacheLifetime;
3182
    }
3183
3184
    /*********************************************
3185
     *
3186
     * Localization and character set conversion
3187
     *
3188
     *********************************************/
3189
    /**
3190
     * Split Label function for front-end applications.
3191
     *
3192
     * @param string $input Key string. Accepts the "LLL:" prefix.
3193
     * @return string Label value, if any.
3194
     */
3195
    public function sL($input)
3196
    {
3197
        return $this->languageService->sL($input);
3198
    }
3199
3200
    /**
3201
     * Sets all internal measures what language the page should be rendered.
3202
     * This is not for records, but rather the HTML / charset and the locallang labels
3203
     */
3204
    protected function setOutputLanguage()
3205
    {
3206
        $this->languageService = GeneralUtility::makeInstance(LanguageServiceFactory::class)->createFromSiteLanguage($this->language);
3207
        // Always disable debugging for TSFE
3208
        $this->languageService->debugKey = false;
3209
    }
3210
3211
    /**
3212
     * Converts input string from utf-8 to metaCharset IF the two charsets are different.
3213
     *
3214
     * @param string $content Content to be converted.
3215
     * @return string Converted content string.
3216
     * @throws \RuntimeException if an invalid charset was configured
3217
     */
3218
    public function convOutputCharset($content)
3219
    {
3220
        if ($this->metaCharset !== 'utf-8') {
3221
            /** @var CharsetConverter $charsetConverter */
3222
            $charsetConverter = GeneralUtility::makeInstance(CharsetConverter::class);
3223
            try {
3224
                $content = $charsetConverter->conv($content, 'utf-8', $this->metaCharset);
3225
            } catch (UnknownCharsetException $e) {
3226
                throw new \RuntimeException('Invalid config.metaCharset: ' . $e->getMessage(), 1508916185);
3227
            }
3228
        }
3229
        return $content;
3230
    }
3231
3232
    /**
3233
     * Calculates page cache timeout according to the records with starttime/endtime on the page.
3234
     *
3235
     * @return int Page cache timeout or PHP_INT_MAX if cannot be determined
3236
     */
3237
    protected function calculatePageCacheTimeout()
3238
    {
3239
        $result = PHP_INT_MAX;
3240
        // Get the configuration
3241
        $tablesToConsider = $this->getCurrentPageCacheConfiguration();
3242
        // Get the time, rounded to the minute (do not pollute MySQL cache!)
3243
        // It is ok that we do not take seconds into account here because this
3244
        // value will be subtracted later. So we never get the time "before"
3245
        // the cache change.
3246
        $now = $GLOBALS['ACCESS_TIME'];
3247
        // Find timeout by checking every table
3248
        foreach ($tablesToConsider as $tableDef) {
3249
            $result = min($result, $this->getFirstTimeValueForRecord($tableDef, $now));
3250
        }
3251
        // We return + 1 second just to ensure that cache is definitely regenerated
3252
        return $result === PHP_INT_MAX ? PHP_INT_MAX : $result - $now + 1;
3253
    }
3254
3255
    /**
3256
     * Obtains a list of table/pid pairs to consider for page caching.
3257
     *
3258
     * TS configuration looks like this:
3259
     *
3260
     * The cache lifetime of all pages takes starttime and endtime of news records of page 14 into account:
3261
     * config.cache.all = tt_news:14
3262
     *
3263
     * The cache.lifetime of the current page allows to take records (e.g. fe_users) into account:
3264
     * config.cache.all = fe_users:current
3265
     *
3266
     * The cache lifetime of page 42 takes starttime and endtime of news records of page 15 and addresses of page 16 into account:
3267
     * config.cache.42 = tt_news:15,tt_address:16
3268
     *
3269
     * @return array Array of 'tablename:pid' pairs. There is at least a current page id in the array
3270
     * @see TypoScriptFrontendController::calculatePageCacheTimeout()
3271
     */
3272
    protected function getCurrentPageCacheConfiguration()
3273
    {
3274
        $result = ['tt_content:' . $this->id];
3275
        if (isset($this->config['config']['cache.'][$this->id])) {
3276
            $result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $this->id, $this->config['config']['cache.'][$this->id])));
3277
        }
3278
        if (isset($this->config['config']['cache.']['all'])) {
3279
            $result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $this->id, $this->config['config']['cache.']['all'])));
3280
        }
3281
        return array_unique($result);
3282
    }
3283
3284
    /**
3285
     * Find the minimum starttime or endtime value in the table and pid that is greater than the current time.
3286
     *
3287
     * @param string $tableDef Table definition (format tablename:pid)
3288
     * @param int $now "Now" time value
3289
     * @throws \InvalidArgumentException
3290
     * @return int Value of the next start/stop time or PHP_INT_MAX if not found
3291
     * @see TypoScriptFrontendController::calculatePageCacheTimeout()
3292
     */
3293
    protected function getFirstTimeValueForRecord($tableDef, $now)
3294
    {
3295
        $now = (int)$now;
3296
        $result = PHP_INT_MAX;
3297
        [$tableName, $pid] = GeneralUtility::trimExplode(':', $tableDef);
3298
        if (empty($tableName) || empty($pid)) {
3299
            throw new \InvalidArgumentException('Unexpected value for parameter $tableDef. Expected <tablename>:<pid>, got \'' . htmlspecialchars($tableDef) . '\'.', 1307190365);
3300
        }
3301
3302
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
3303
            ->getQueryBuilderForTable($tableName);
3304
        $queryBuilder->getRestrictions()
3305
            ->removeByType(StartTimeRestriction::class)
3306
            ->removeByType(EndTimeRestriction::class);
3307
        $timeFields = [];
3308
        $timeConditions = $queryBuilder->expr()->orX();
3309
        foreach (['starttime', 'endtime'] as $field) {
3310
            if (isset($GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field])) {
3311
                $timeFields[$field] = $GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field];
3312
                $queryBuilder->addSelectLiteral(
3313
                    'MIN('
3314
                        . 'CASE WHEN '
3315
                        . $queryBuilder->expr()->lte(
3316
                            $timeFields[$field],
3317
                            $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)
3318
                        )
3319
                        . ' THEN NULL ELSE ' . $queryBuilder->quoteIdentifier($timeFields[$field]) . ' END'
3320
                        . ') AS ' . $queryBuilder->quoteIdentifier($timeFields[$field])
3321
                );
3322
                $timeConditions->add(
3323
                    $queryBuilder->expr()->gt(
3324
                        $timeFields[$field],
3325
                        $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)
3326
                    )
3327
                );
3328
            }
3329
        }
3330
3331
        // if starttime or endtime are defined, evaluate them
3332
        if (!empty($timeFields)) {
3333
            // find the timestamp, when the current page's content changes the next time
3334
            $row = $queryBuilder
3335
                ->from($tableName)
3336
                ->where(
3337
                    $queryBuilder->expr()->eq(
3338
                        'pid',
3339
                        $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
3340
                    ),
3341
                    $timeConditions
3342
                )
3343
                ->execute()
3344
                ->fetch();
3345
3346
            if ($row) {
3347
                foreach ($timeFields as $timeField => $_) {
3348
                    // if a MIN value is found, take it into account for the
3349
                    // cache lifetime we have to filter out start/endtimes < $now,
3350
                    // as the SQL query also returns rows with starttime < $now
3351
                    // and endtime > $now (and using a starttime from the past
3352
                    // would be wrong)
3353
                    if ($row[$timeField] !== null && (int)$row[$timeField] > $now) {
3354
                        $result = min($result, (int)$row[$timeField]);
3355
                    }
3356
                }
3357
            }
3358
        }
3359
3360
        return $result;
3361
    }
3362
3363
    /**
3364
     * Fetches the originally requested id, falls back to $this->id
3365
     *
3366
     * @return int the originally requested page uid
3367
     * @see fetch_the_id()
3368
     */
3369
    public function getRequestedId()
3370
    {
3371
        return $this->requestedId ?: $this->id;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->requestedId ?: $this->id also could return the type string which is incompatible with the documented return type integer.
Loading history...
3372
    }
3373
3374
    /**
3375
     * Acquire a page specific lock
3376
     *
3377
     *
3378
     * The schematics here is:
3379
     * - First acquire an access lock. This is using the type of the requested lock as key.
3380
     *   Since the number of types is rather limited we can use the type as key as it will only
3381
     *   eat up a limited number of lock resources on the system (files, semaphores)
3382
     * - Second, we acquire the actual lock (named page lock). We can be sure we are the only process at this
3383
     *   very moment, hence we either get the lock for the given key or we get an error as we request a non-blocking mode.
3384
     *
3385
     * Interleaving two locks is extremely important, because the actual page lock uses a hash value as key (see callers
3386
     * of this function). If we would simply employ a normal blocking lock, we would get a potentially unlimited
3387
     * (number of pages at least) number of different locks. Depending on the available locking methods on the system
3388
     * we might run out of available resources. (e.g. maximum limit of semaphores is a system setting and applies
3389
     * to the whole system)
3390
     * We therefore must make sure that page locks are destroyed again if they are not used anymore, such that
3391
     * we never use more locking resources than parallel requests to different pages (hashes).
3392
     * In order to ensure this, we need to guarantee that no other process is waiting on a page lock when
3393
     * the process currently having the lock on the page lock is about to release the lock again.
3394
     * This can only be achieved by using a non-blocking mode, such that a process is never put into wait state
3395
     * by the kernel, but only checks the availability of the lock. The access lock is our guard to be sure
3396
     * that no two processes are at the same time releasing/destroying a page lock, whilst the other one tries to
3397
     * get a lock for this page lock.
3398
     * The only drawback of this implementation is that we basically have to poll the availability of the page lock.
3399
     *
3400
     * Note that the access lock resources are NEVER deleted/destroyed, otherwise the whole thing would be broken.
3401
     *
3402
     * @param string $type
3403
     * @param string $key
3404
     * @throws \InvalidArgumentException
3405
     * @throws \RuntimeException
3406
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
3407
     */
3408
    protected function acquireLock($type, $key)
3409
    {
3410
        $lockFactory = GeneralUtility::makeInstance(LockFactory::class);
3411
        $this->locks[$type]['accessLock'] = $lockFactory->createLocker($type);
3412
3413
        $this->locks[$type]['pageLock'] = $lockFactory->createLocker(
3414
            $key,
3415
            LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
3416
        );
3417
3418
        do {
3419
            if (!$this->locks[$type]['accessLock']->acquire()) {
3420
                throw new \RuntimeException('Could not acquire access lock for "' . $type . '"".', 1294586098);
3421
            }
3422
3423
            try {
3424
                $locked = $this->locks[$type]['pageLock']->acquire(
3425
                    LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
3426
                );
3427
            } catch (LockAcquireWouldBlockException $e) {
3428
                // somebody else has the lock, we keep waiting
3429
3430
                // first release the access lock
3431
                $this->locks[$type]['accessLock']->release();
3432
                // now lets make a short break (100ms) until we try again, since
3433
                // the page generation by the lock owner will take a while anyways
3434
                usleep(100000);
3435
                continue;
3436
            }
3437
            $this->locks[$type]['accessLock']->release();
3438
            if ($locked) {
3439
                break;
3440
            }
3441
            throw new \RuntimeException('Could not acquire page lock for ' . $key . '.', 1460975877);
3442
        } while (true);
3443
    }
3444
3445
    /**
3446
     * Release a page specific lock
3447
     *
3448
     * @param string $type
3449
     * @throws \InvalidArgumentException
3450
     * @throws \RuntimeException
3451
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
3452
     */
3453
    protected function releaseLock($type)
3454
    {
3455
        if ($this->locks[$type]['accessLock'] ?? false) {
3456
            if (!$this->locks[$type]['accessLock']->acquire()) {
3457
                throw new \RuntimeException('Could not acquire access lock for "' . $type . '"".', 1460975902);
3458
            }
3459
3460
            $this->locks[$type]['pageLock']->release();
3461
            $this->locks[$type]['pageLock']->destroy();
3462
            $this->locks[$type]['pageLock'] = null;
3463
3464
            $this->locks[$type]['accessLock']->release();
3465
            $this->locks[$type]['accessLock'] = null;
3466
        }
3467
    }
3468
3469
    /**
3470
     * Send additional headers from config.additionalHeaders
3471
     */
3472
    protected function getAdditionalHeaders(): array
3473
    {
3474
        if (!isset($this->config['config']['additionalHeaders.'])) {
3475
            return [];
3476
        }
3477
        $additionalHeaders = [];
3478
        ksort($this->config['config']['additionalHeaders.']);
3479
        foreach ($this->config['config']['additionalHeaders.'] as $options) {
3480
            if (!is_array($options)) {
3481
                continue;
3482
            }
3483
            $header = trim($options['header'] ?? '');
3484
            if ($header === '') {
3485
                continue;
3486
            }
3487
            $additionalHeaders[] = [
3488
                'header' => $header,
3489
                // "replace existing headers" is turned on by default, unless turned off
3490
                'replace' => ($options['replace'] ?? '') !== '0',
3491
                'statusCode' => (int)($options['httpResponseCode'] ?? 0) ?: null
3492
            ];
3493
        }
3494
        return $additionalHeaders;
3495
    }
3496
3497
    protected function isInPreviewMode(): bool
3498
    {
3499
        return $this->context->getPropertyFromAspect('frontend.preview', 'isPreview', false)
3500
            || $GLOBALS['EXEC_TIME'] !== $GLOBALS['SIM_EXEC_TIME']
3501
            || $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages', false)
3502
            || $this->context->getPropertyFromAspect('visibility', 'includeHiddenContent', false);
3503
    }
3504
3505
    /**
3506
     * Log the page access failure with additional request information
3507
     *
3508
     * @param string $message
3509
     * @param ServerRequestInterface $request
3510
     */
3511
    protected function logPageAccessFailure(string $message, ServerRequestInterface $request): void
3512
    {
3513
        $context = ['pageId' => $this->id];
3514
        if (($normalizedParams = $request->getAttribute('normalizedParams')) instanceof NormalizedParams) {
3515
            $context['requestUrl'] = $normalizedParams->getRequestUrl();
3516
        }
3517
        $this->logger->error($message, $context);
3518
    }
3519
3520
    /**
3521
     * Returns the current BE user.
3522
     *
3523
     * @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication
3524
     */
3525
    protected function getBackendUser()
3526
    {
3527
        return $GLOBALS['BE_USER'];
3528
    }
3529
3530
    /**
3531
     * @return TimeTracker
3532
     */
3533
    protected function getTimeTracker()
3534
    {
3535
        return GeneralUtility::makeInstance(TimeTracker::class);
3536
    }
3537
3538
    /**
3539
     * Return the global instance of this class.
3540
     *
3541
     * Intended to be used as prototype factory for this class, see Services.yaml.
3542
     * This is required as long as TypoScriptFrontendController needs request
3543
     * dependent constructor parameters. Once that has been refactored this
3544
     * factory will be removed.
3545
     *
3546
     * @return TypoScriptFrontendController
3547
     * @internal
3548
     */
3549
    public static function getGlobalInstance(): ?self
3550
    {
3551
        if (($GLOBALS['TSFE'] ?? null) instanceof self) {
3552
            return $GLOBALS['TSFE'];
3553
        }
3554
3555
        if (!($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
3556
            || !ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()
3557
        ) {
3558
            // Return null for now (together with shared: false in Services.yaml) as TSFE might not be available in backend context
3559
            // That's not an error then
3560
            return null;
3561
        }
3562
3563
        throw new \LogicException('TypoScriptFrontendController was tried to be injected before initial creation', 1538370377);
3564
    }
3565
3566
    public function getLanguage(): SiteLanguage
3567
    {
3568
        return $this->language;
3569
    }
3570
3571
    public function getSite(): SiteInterface
3572
    {
3573
        return $this->site;
3574
    }
3575
3576
    public function getContext(): Context
3577
    {
3578
        return $this->context;
3579
    }
3580
3581
    public function getPageArguments(): PageArguments
3582
    {
3583
        return $this->pageArguments;
3584
    }
3585
}
3586