TypoScriptFrontendController::getCacheHeaders()   B
last analyzed

Complexity

Conditions 11
Paths 88

Size

Total Lines 41
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 25
dl 0
loc 41
rs 7.3166
c 0
b 0
f 0
cc 11
nc 88
nop 0

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

1203
                $this->logger->/** @scrutinizer ignore-call */ 
1204
                               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...
1204
                $exceptionClass = get_class($e);
1205
                throw new $exceptionClass($message, 1301648167);
1206
            }
1207
        }
1208
        // Checking for include section regarding the hidden/starttime/endtime/fe_user (that is access control of a whole subbranch!)
1209
        if ($this->checkRootlineForIncludeSection()) {
1210
            if (empty($this->rootLine)) {
1211
                $message = 'The requested page does not exist!';
1212
                try {
1213
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1214
                        $request,
1215
                        $message,
1216
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND)
1217
                    );
1218
                    throw new PropagateResponseException($response, 1533931351);
1219
                } catch (AbstractServerErrorException $e) {
1220
                    $this->logger->warning($message);
1221
                    $exceptionClass = get_class($e);
1222
                    throw new $exceptionClass($message, 1301648234);
1223
                }
1224
            } else {
1225
                $el = reset($this->rootLine);
1226
                $this->id = $el['uid'];
1227
                $this->page = $this->sys_page->getPage($this->id);
1228
                try {
1229
                    $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1230
                } catch (RootLineException $e) {
1231
                    $this->rootLine = [];
1232
                }
1233
            }
1234
        }
1235
    }
1236
1237
    /**
1238
     * If $this->id contains a translated page record, this needs to be resolved to the default language
1239
     * in order for all rootline functionality and access restrictions to be in place further on.
1240
     *
1241
     * Additionally, if a translated page is found, LanguageAspect is set as well.
1242
     */
1243
    protected function resolveTranslatedPageId()
1244
    {
1245
        $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

1245
        $this->page = $this->sys_page->getPage(/** @scrutinizer ignore-type */ $this->id);
Loading history...
1246
        // Accessed a default language page record, nothing to resolve
1247
        if (empty($this->page) || (int)$this->page[$GLOBALS['TCA']['pages']['ctrl']['languageField']] === 0) {
1248
            return;
1249
        }
1250
        $languageId = (int)$this->page[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
1251
        $this->page = $this->sys_page->getPage($this->page[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']]);
1252
        $this->context->setAspect('language', GeneralUtility::makeInstance(LanguageAspect::class, $languageId));
1253
        $this->id = $this->page['uid'];
1254
    }
1255
1256
    /**
1257
     * Checks if visibility of the page is blocked upwards in the root line.
1258
     *
1259
     * If any page in the root line is blocking visibility, true is returned.
1260
     *
1261
     * All pages from the blocking page downwards are removed from the root
1262
     * line, so that the remaining pages can be used to relocate the page up
1263
     * to lowest visible page.
1264
     *
1265
     * The blocking feature of a page must be turned on by setting the page
1266
     * record field 'extendToSubpages' to 1 in case of hidden, starttime,
1267
     * endtime or fe_group restrictions.
1268
     *
1269
     * Additionally this method checks for backend user sections in root line
1270
     * and if found evaluates if a backend user is logged in and has access.
1271
     *
1272
     * Recyclers are also checked and trigger page not found if found in root
1273
     * line.
1274
     *
1275
     * @todo Find a better name, i.e. checkVisibilityByRootLine
1276
     * @todo Invert boolean return value. Return true if visible.
1277
     *
1278
     * @return bool
1279
     */
1280
    protected function checkRootlineForIncludeSection(): bool
1281
    {
1282
        $c = count($this->rootLine);
1283
        $removeTheRestFlag = false;
1284
        for ($a = 0; $a < $c; $a++) {
1285
            if (!$this->checkPagerecordForIncludeSection($this->rootLine[$a])) {
1286
                // Add to page access failure history and mark the page as not found
1287
                // Keep the rootline however to trigger an access denied error instead of a service unavailable error
1288
                $this->pageAccessFailureHistory['sub_section'][] = $this->rootLine[$a];
1289
                $this->pageNotFound = 2;
1290
            }
1291
1292
            if ((int)$this->rootLine[$a]['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION) {
1293
                // If there is a backend user logged in, check if they have read access to the page:
1294
                if ($this->isBackendUserLoggedIn()) {
1295
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1296
                        ->getQueryBuilderForTable('pages');
1297
1298
                    $queryBuilder
1299
                        ->getRestrictions()
1300
                        ->removeAll();
1301
1302
                    $row = $queryBuilder
1303
                        ->select('uid')
1304
                        ->from('pages')
1305
                        ->where(
1306
                            $queryBuilder->expr()->eq(
1307
                                'uid',
1308
                                $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
1309
                            ),
1310
                            $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)
1311
                        )
1312
                        ->execute()
1313
                        ->fetchAssociative();
1314
1315
                    // versionOL()?
1316
                    if (!$row) {
1317
                        // 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...
1318
                        $removeTheRestFlag = true;
1319
                    }
1320
                } else {
1321
                    // Don't go here, if there is no backend user logged in.
1322
                    $removeTheRestFlag = true;
1323
                }
1324
            } elseif ((int)$this->rootLine[$a]['doktype'] === PageRepository::DOKTYPE_RECYCLER) {
1325
                // page is in a recycler
1326
                $removeTheRestFlag = true;
1327
            }
1328
            if ($removeTheRestFlag) {
1329
                // Page is 'not found' in case a subsection was found and not accessible, code 2
1330
                $this->pageNotFound = 2;
1331
                unset($this->rootLine[$a]);
1332
            }
1333
        }
1334
        return $removeTheRestFlag;
1335
    }
1336
1337
    /**
1338
     * Checks page record for enableFields
1339
     * Returns TRUE if enableFields does not disable the page record.
1340
     * Takes notice of the includeHiddenPages visibility aspect flag and uses SIM_ACCESS_TIME for start/endtime evaluation
1341
     *
1342
     * @param array $row The page record to evaluate (needs fields: hidden, starttime, endtime, fe_group)
1343
     * @param bool $bypassGroupCheck Bypass group-check
1344
     * @return bool TRUE, if record is viewable.
1345
     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getTreeList()
1346
     * @see checkPagerecordForIncludeSection()
1347
     */
1348
    public function checkEnableFields($row, $bypassGroupCheck = false)
1349
    {
1350
        $_params = ['pObj' => $this, 'row' => &$row, 'bypassGroupCheck' => &$bypassGroupCheck];
1351
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['hook_checkEnableFields'] ?? [] as $_funcRef) {
1352
            // Call hooks: If one returns FALSE, method execution is aborted with result "This record is not available"
1353
            $return = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1354
            if ($return === false) {
1355
                return false;
1356
            }
1357
        }
1358
        if ((!$row['hidden'] || $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages', false))
1359
            && $row['starttime'] <= $GLOBALS['SIM_ACCESS_TIME']
1360
            && ($row['endtime'] == 0 || $row['endtime'] > $GLOBALS['SIM_ACCESS_TIME'])
1361
            && ($bypassGroupCheck || $this->checkPageGroupAccess($row))) {
1362
            return true;
1363
        }
1364
        return false;
1365
    }
1366
1367
    /**
1368
     * Check group access against a page record
1369
     *
1370
     * @param array $row The page record to evaluate (needs field: fe_group)
1371
     * @return bool TRUE, if group access is granted.
1372
     * @internal
1373
     */
1374
    public function checkPageGroupAccess($row)
1375
    {
1376
        /** @var UserAspect $userAspect */
1377
        $userAspect = $this->context->getAspect('frontend.user');
1378
        $pageGroupList = explode(',', $row['fe_group'] ?: 0);
1379
        return count(array_intersect($userAspect->getGroupIds(), $pageGroupList)) > 0;
1380
    }
1381
1382
    /**
1383
     * Checks if the current page of the root line is visible.
1384
     *
1385
     * If the field extendToSubpages is 0, access is granted,
1386
     * else the fields hidden, starttime, endtime, fe_group are evaluated.
1387
     *
1388
     * @todo Find a better name, i.e. isVisibleRecord()
1389
     *
1390
     * @param array $row The page record
1391
     * @return bool true if visible
1392
     * @internal
1393
     * @see checkEnableFields()
1394
     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getTreeList()
1395
     * @see checkRootlineForIncludeSection()
1396
     */
1397
    public function checkPagerecordForIncludeSection(array $row): bool
1398
    {
1399
        return !$row['extendToSubpages'] || $this->checkEnableFields($row);
1400
    }
1401
1402
    /**
1403
     * 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!)
1404
     *
1405
     * @return bool returns TRUE if logins are OK, otherwise FALSE (and then the login user must be unset!)
1406
     */
1407
    public function checkIfLoginAllowedInBranch()
1408
    {
1409
        // Initialize:
1410
        $c = count($this->rootLine);
1411
        $loginAllowed = true;
1412
        // Traverse root line from root and outwards:
1413
        for ($a = 0; $a < $c; $a++) {
1414
            // If a value is set for login state:
1415
            if ($this->rootLine[$a]['fe_login_mode'] > 0) {
1416
                // Determine state from value:
1417
                if ((int)$this->rootLine[$a]['fe_login_mode'] === 1) {
1418
                    $loginAllowed = false;
1419
                    $this->loginAllowedInBranch_mode = 'all';
1420
                } elseif ((int)$this->rootLine[$a]['fe_login_mode'] === 3) {
1421
                    $loginAllowed = false;
1422
                    $this->loginAllowedInBranch_mode = 'groups';
1423
                } else {
1424
                    $loginAllowed = true;
1425
                }
1426
            }
1427
        }
1428
        return $loginAllowed;
1429
    }
1430
1431
    /**
1432
     * 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
1433
     *
1434
     * @param string $failureReasonCode the error code to be attached (optional), see PageAccessFailureReasons list for details
1435
     * @return array Summary of why page access was not allowed.
1436
     */
1437
    public function getPageAccessFailureReasons(string $failureReasonCode = null)
1438
    {
1439
        $output = [];
1440
        if ($failureReasonCode) {
1441
            $output['code'] = $failureReasonCode;
1442
        }
1443
        $combinedRecords = array_merge(
1444
            is_array($this->pageAccessFailureHistory['direct_access'] ?? false) ? $this->pageAccessFailureHistory['direct_access'] : [['fe_group' => 0]],
1445
            is_array($this->pageAccessFailureHistory['sub_section'] ?? false) ? $this->pageAccessFailureHistory['sub_section'] : []
1446
        );
1447
        if (!empty($combinedRecords)) {
1448
            foreach ($combinedRecords as $k => $pagerec) {
1449
                // 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
1450
                // 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!
1451
                if (!$k || $pagerec['extendToSubpages']) {
1452
                    if ($pagerec['hidden'] ?? false) {
1453
                        $output['hidden'][$pagerec['uid']] = true;
1454
                    }
1455
                    if (isset($pagerec['starttime']) && $pagerec['starttime'] > $GLOBALS['SIM_ACCESS_TIME']) {
1456
                        $output['starttime'][$pagerec['uid']] = $pagerec['starttime'];
1457
                    }
1458
                    if (isset($pagerec['endtime']) && $pagerec['endtime'] != 0 && $pagerec['endtime'] <= $GLOBALS['SIM_ACCESS_TIME']) {
1459
                        $output['endtime'][$pagerec['uid']] = $pagerec['endtime'];
1460
                    }
1461
                    if (!$this->checkPageGroupAccess($pagerec)) {
1462
                        $output['fe_group'][$pagerec['uid']] = $pagerec['fe_group'];
1463
                    }
1464
                }
1465
            }
1466
        }
1467
        return $output;
1468
    }
1469
1470
    /**
1471
     * Gets ->page and ->rootline information based on ->id. ->id may change during this operation.
1472
     * If not inside a site, then default to first page in site.
1473
     *
1474
     * @param int $rootPageId Page uid of the page where the found site is located
1475
     * @internal
1476
     */
1477
    protected function getPageAndRootlineWithDomain($rootPageId, ServerRequestInterface $request)
1478
    {
1479
        $this->getPageAndRootline($request);
1480
        // 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.
1481
        if (is_array($this->rootLine) && $this->rootLine !== []) {
1482
            $idFound = false;
1483
            foreach ($this->rootLine as $key => $val) {
1484
                if ($val['uid'] == $rootPageId) {
1485
                    $idFound = true;
1486
                    break;
1487
                }
1488
            }
1489
            if (!$idFound) {
1490
                // Page is 'not found' in case the id was outside the domain, code 3
1491
                $this->pageNotFound = 3;
1492
                $this->id = $rootPageId;
1493
                // re-get the page and rootline if the id was not found.
1494
                $this->getPageAndRootline($request);
1495
            }
1496
        }
1497
    }
1498
1499
    /********************************************
1500
     *
1501
     * Template and caching related functions.
1502
     *
1503
     *******************************************/
1504
1505
    protected function setPageArguments(PageArguments $pageArguments): void
1506
    {
1507
        $this->pageArguments = $pageArguments;
1508
        $this->id = $pageArguments->getPageId();
1509
        $this->type = $pageArguments->getPageType() ?: 0;
1510
        if ($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
1511
            $this->MP = (string)($pageArguments->getArguments()['MP'] ?? '');
1512
            // Ensure no additional arguments are given via the &MP=123-345,908-172 (e.g. "/")
1513
            $this->MP = preg_replace('/[^0-9,-]/', '', $this->MP);
1514
        }
1515
    }
1516
1517
    /**
1518
     * Fetches the arguments that are relevant for creating the hash base from the given PageArguments object.
1519
     * Excluded parameters are not taken into account when calculating the hash base.
1520
     *
1521
     * @param PageArguments $pageArguments
1522
     * @return array
1523
     */
1524
    protected function getRelevantParametersForCachingFromPageArguments(PageArguments $pageArguments): array
1525
    {
1526
        $queryParams = $pageArguments->getDynamicArguments();
1527
        if (!empty($queryParams) && ($pageArguments->getArguments()['cHash'] ?? false)) {
1528
            $queryParams['id'] = $pageArguments->getPageId();
1529
            return GeneralUtility::makeInstance(CacheHashCalculator::class)
1530
                ->getRelevantParameters(HttpUtility::buildQueryString($queryParams));
1531
        }
1532
        return [];
1533
    }
1534
1535
    /**
1536
     * See if page is in cache and get it if so
1537
     * Stores the page content in $this->content if something is found.
1538
     *
1539
     * @param ServerRequestInterface|null $request if given this is used to determine values in headerNoCache() instead of the superglobal $_SERVER
1540
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
1541
     */
1542
    public function getFromCache(ServerRequestInterface $request = null)
1543
    {
1544
        // clearing the content-variable, which will hold the pagecontent
1545
        $this->content = '';
1546
        // Unsetting the lowlevel config
1547
        $this->config = [];
1548
        $this->cacheContentFlag = false;
1549
1550
        if ($this->no_cache) {
1551
            return;
1552
        }
1553
1554
        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...
1555
            $this->tmpl = GeneralUtility::makeInstance(TemplateService::class, $this->context, null, $this);
1556
        }
1557
1558
        $pageSectionCacheContent = $this->tmpl->getCurrentPageData((int)$this->id, (string)$this->MP);
1559
        if (!is_array($pageSectionCacheContent)) {
0 ignored issues
show
introduced by
The condition is_array($pageSectionCacheContent) is always true.
Loading history...
1560
            // Nothing in the cache, we acquire an "exclusive lock" for the key now.
1561
            // We use the Registry to store this lock centrally,
1562
            // but we protect the access again with a global exclusive lock to avoid race conditions
1563
1564
            $this->acquireLock('pagesection', $this->id . '::' . $this->MP);
1565
            //
1566
            // from this point on we're the only one working on that page ($key)
1567
            //
1568
1569
            // query the cache again to see if the page data are there meanwhile
1570
            $pageSectionCacheContent = $this->tmpl->getCurrentPageData((int)$this->id, (string)$this->MP);
1571
            if (is_array($pageSectionCacheContent)) {
1572
                // we have the content, nice that some other process did the work for us already
1573
                $this->releaseLock('pagesection');
1574
            }
1575
            // We keep the lock set, because we are the ones generating the page now and filling the cache.
1576
            // This indicates that we have to release the lock later in releaseLocks()
1577
        }
1578
1579
        if (is_array($pageSectionCacheContent)) {
0 ignored issues
show
introduced by
The condition is_array($pageSectionCacheContent) is always true.
Loading history...
1580
            // 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.
1581
            // If this hash is not the same in here in this section and after page-generation, then the page will not be properly cached!
1582
            // 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.
1583
            $pageSectionCacheContent = $this->tmpl->matching($pageSectionCacheContent);
1584
            ksort($pageSectionCacheContent);
1585
            $this->all = $pageSectionCacheContent;
1586
        }
1587
1588
        // Look for page in cache only if a shift-reload is not sent to the server.
1589
        $lockHash = $this->getLockHash();
1590
        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...
1591
            // we got page section information (TypoScript), so lets see if there is also a cached version
1592
            // of this page in the pages cache.
1593
            $this->newHash = $this->getHash();
1594
            $this->getTimeTracker()->push('Cache Row');
1595
            $row = $this->getFromCache_queryRow();
1596
            if (!is_array($row)) {
0 ignored issues
show
introduced by
The condition is_array($row) is always true.
Loading history...
1597
                // nothing in the cache, we acquire an exclusive lock now
1598
                $this->acquireLock('pages', $lockHash);
1599
                //
1600
                // from this point on we're the only one working on that page ($lockHash)
1601
                //
1602
1603
                // query the cache again to see if the data are there meanwhile
1604
                $row = $this->getFromCache_queryRow();
1605
                if (is_array($row)) {
1606
                    // we have the content, nice that some other process did the work for us
1607
                    $this->releaseLock('pages');
1608
                }
1609
                // We keep the lock set, because we are the ones generating the page now and filling the cache.
1610
                // This indicates that we have to release the lock later in releaseLocks()
1611
            }
1612
            if (is_array($row)) {
0 ignored issues
show
introduced by
The condition is_array($row) is always true.
Loading history...
1613
                $this->populatePageDataFromCache($row);
1614
            }
1615
            $this->getTimeTracker()->pull();
1616
        } else {
1617
            // the user forced rebuilding the page cache or there was no pagesection information
1618
            // get a lock for the page content so other processes will not interrupt the regeneration
1619
            $this->acquireLock('pages', $lockHash);
1620
        }
1621
    }
1622
1623
    /**
1624
     * Returning the cached version of page with hash = newHash
1625
     *
1626
     * @return array Cached row, if any. Otherwise void.
1627
     */
1628
    public function getFromCache_queryRow()
1629
    {
1630
        $this->getTimeTracker()->push('Cache Query');
1631
        $row = $this->pageCache->get($this->newHash);
1632
        $this->getTimeTracker()->pull();
1633
        return $row;
1634
    }
1635
1636
    /**
1637
     * This method properly sets the values given from the pages cache into the corresponding
1638
     * TSFE variables. The counterpart is setPageCacheContent() where all relevant information is fetched.
1639
     * This also contains all data that could be cached, even for pages that are partially cached, as they
1640
     * have non-cacheable content still to be rendered.
1641
     *
1642
     * @see getFromCache()
1643
     * @see setPageCacheContent()
1644
     * @param array $cachedData
1645
     */
1646
    protected function populatePageDataFromCache(array $cachedData): void
1647
    {
1648
        // Call hook when a page is retrieved from cache
1649
        $_params = ['pObj' => &$this, 'cache_pages_row' => &$cachedData];
1650
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['pageLoadedFromCache'] ?? [] as $_funcRef) {
1651
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1652
        }
1653
        // Fetches the lowlevel config stored with the cached data
1654
        $this->config = $cachedData['cache_data'];
1655
        // Getting the content
1656
        $this->content = $cachedData['content'];
1657
        // Setting flag, so we know, that some cached content has been loaded
1658
        $this->cacheContentFlag = true;
1659
        $this->cacheExpires = $cachedData['expires'];
1660
        // Restore the current tags as they can be retrieved by getPageCacheTags()
1661
        $this->pageCacheTags = $cachedData['cacheTags'] ?? [];
1662
1663
        // Restore page title information, this is needed to generate the page title for
1664
        // partially cached pages.
1665
        $this->page['title'] = $cachedData['pageTitleInfo']['title'];
1666
        $this->indexedDocTitle = $cachedData['pageTitleInfo']['indexedDocTitle'];
1667
1668
        if (isset($this->config['config']['debug'])) {
1669
            $debugCacheTime = (bool)$this->config['config']['debug'];
1670
        } else {
1671
            $debugCacheTime = !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']);
1672
        }
1673
        if ($debugCacheTime) {
1674
            $this->prepareDebugInformationForCachedPage($cachedData);
1675
        }
1676
    }
1677
1678
    protected function prepareDebugInformationForCachedPage(array $cachedData): void
1679
    {
1680
        $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'];
1681
        $timeFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
1682
        $this->debugInformationHeader = 'Cached page generated ' . date($dateFormat . ' ' . $timeFormat, $cachedData['tstamp']) . '. Expires ' . date($dateFormat . ' ' . $timeFormat, $cachedData['expires']);
1683
    }
1684
1685
    /**
1686
     * Detecting if shift-reload has been clicked
1687
     * Will not be called if re-generation of page happens by other reasons (for instance that the page is not in cache yet!)
1688
     * Also, a backend user MUST be logged in for the shift-reload to be detected due to DoS-attack-security reasons.
1689
     *
1690
     * @param ServerRequestInterface|null $request
1691
     * @return bool If shift-reload in client browser has been clicked, disable getting cached page (and regenerate it).
1692
     */
1693
    public function headerNoCache(ServerRequestInterface $request = null)
1694
    {
1695
        if ($request instanceof ServerRequestInterface) {
1696
            $serverParams = $request->getServerParams();
1697
        } else {
1698
            $serverParams = $_SERVER;
1699
        }
1700
        $disableAcquireCacheData = false;
1701
        if ($this->isBackendUserLoggedIn()) {
1702
            if (strtolower($serverParams['HTTP_CACHE_CONTROL'] ?? '') === 'no-cache' || strtolower($serverParams['HTTP_PRAGMA'] ?? '') === 'no-cache') {
1703
                $disableAcquireCacheData = true;
1704
            }
1705
        }
1706
        // Call hook for possible by-pass of requiring of page cache (for recaching purpose)
1707
        $_params = ['pObj' => &$this, 'disableAcquireCacheData' => &$disableAcquireCacheData];
1708
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['headerNoCache'] ?? [] as $_funcRef) {
1709
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1710
        }
1711
        return $disableAcquireCacheData;
1712
    }
1713
1714
    /**
1715
     * Calculates the cache-hash
1716
     * This hash is unique to the template, the variables ->id, ->type, list of fe user groups, ->MP (Mount Points) and cHash array
1717
     * Used to get and later store the cached data.
1718
     *
1719
     * @return string MD5 hash of serialized hash base from createHashBase(), prefixed with page id
1720
     * @see getFromCache()
1721
     * @see getLockHash()
1722
     */
1723
    protected function getHash()
1724
    {
1725
        return $this->id . '_' . md5($this->createHashBase(false));
1726
    }
1727
1728
    /**
1729
     * Calculates the lock-hash
1730
     * This hash is unique to the above hash, except that it doesn't contain the template information in $this->all.
1731
     *
1732
     * @return string MD5 hash prefixed with page id
1733
     * @see getFromCache()
1734
     * @see getHash()
1735
     */
1736
    protected function getLockHash()
1737
    {
1738
        $lockHash = $this->createHashBase(true);
1739
        return $this->id . '_' . md5($lockHash);
1740
    }
1741
1742
    /**
1743
     * Calculates the cache-hash (or the lock-hash)
1744
     * This hash is unique to the template,
1745
     * the variables ->id, ->type, list of frontend user groups,
1746
     * ->MP (Mount Points) and cHash array
1747
     * Used to get and later store the cached data.
1748
     *
1749
     * @param bool $createLockHashBase Whether to create the lock hash, which doesn't contain the "this->all" (the template information)
1750
     * @return string the serialized hash base
1751
     */
1752
    protected function createHashBase($createLockHashBase = false)
1753
    {
1754
        // Fetch the list of user groups
1755
        /** @var UserAspect $userAspect */
1756
        $userAspect = $this->context->getAspect('frontend.user');
1757
        $hashParameters = [
1758
            'id' => (int)$this->id,
1759
            'type' => (int)$this->type,
1760
            'groupIds' => (string)implode(',', $userAspect->getGroupIds()),
1761
            'MP' => (string)$this->MP,
1762
            'site' => $this->site->getIdentifier(),
1763
            // Ensure the language base is used for the hash base calculation as well, otherwise TypoScript and page-related rendering
1764
            // is not cached properly as we don't have any language-specific conditions anymore
1765
            'siteBase' => (string)$this->language->getBase(),
1766
            // additional variation trigger for static routes
1767
            'staticRouteArguments' => $this->pageArguments->getStaticArguments(),
1768
            // dynamic route arguments (if route was resolved)
1769
            'dynamicArguments' => $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments),
1770
        ];
1771
        // Include the template information if we shouldn't create a lock hash
1772
        if (!$createLockHashBase) {
1773
            $hashParameters['all'] = $this->all;
1774
        }
1775
        // Call hook to influence the hash calculation
1776
        $_params = [
1777
            'hashParameters' => &$hashParameters,
1778
            'createLockHashBase' => $createLockHashBase,
1779
        ];
1780
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['createHashBase'] ?? [] as $_funcRef) {
1781
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1782
        }
1783
        return serialize($hashParameters);
1784
    }
1785
1786
    /**
1787
     * Checks if config-array exists already but if not, gets it
1788
     *
1789
     * @param ServerRequestInterface|null $request
1790
     * @throws \TYPO3\CMS\Core\Error\Http\InternalServerErrorException
1791
     * @throws \TYPO3\CMS\Core\Error\Http\ServiceUnavailableException
1792
     */
1793
    public function getConfigArray(ServerRequestInterface $request = null)
1794
    {
1795
        $request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals();
1796
        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...
1797
            $this->tmpl = GeneralUtility::makeInstance(TemplateService::class, $this->context, null, $this);
1798
        }
1799
1800
        // 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
1801
        if (empty($this->config) || $this->isINTincScript() || $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing')) {
1802
            $timeTracker = $this->getTimeTracker();
1803
            $timeTracker->push('Parse template');
1804
            // Start parsing the TS template. Might return cached version.
1805
            $this->tmpl->start($this->rootLine);
1806
            $timeTracker->pull();
1807
            // At this point we have a valid pagesection_cache (generated in $this->tmpl->start()),
1808
            // so let all other processes proceed now. (They are blocked at the pagessection_lock in getFromCache())
1809
            $this->releaseLock('pagesection');
1810
            if ($this->tmpl->loaded) {
1811
                $timeTracker->push('Setting the config-array');
1812
                // toplevel - objArrayName
1813
                $typoScriptPageTypeName = $this->tmpl->setup['types.'][$this->type] ?? '';
1814
                $this->sPre = $typoScriptPageTypeName;
1815
                $this->pSetup = $this->tmpl->setup[$typoScriptPageTypeName . '.'] ?? '';
1816
                if (!is_array($this->pSetup)) {
1817
                    $this->logger->alert('The page is not configured! [type={type}][{type_name}].', ['type' => $this->type, 'type_name' => $typoScriptPageTypeName]);
1818
                    try {
1819
                        $message = 'The page is not configured! [type=' . $this->type . '][' . $typoScriptPageTypeName . '].';
1820
                        $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction(
1821
                            $request,
1822
                            $message,
1823
                            ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED]
1824
                        );
1825
                        throw new PropagateResponseException($response, 1533931374);
1826
                    } catch (AbstractServerErrorException $e) {
1827
                        $explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.';
1828
                        $exceptionClass = get_class($e);
1829
                        throw new $exceptionClass($message . ' ' . $explanation, 1294587217);
1830
                    }
1831
                } else {
1832
                    if (!isset($this->config['config'])) {
1833
                        $this->config['config'] = [];
1834
                    }
1835
                    // Filling the config-array, first with the main "config." part
1836
                    if (is_array($this->tmpl->setup['config.'] ?? null)) {
1837
                        ArrayUtility::mergeRecursiveWithOverrule($this->tmpl->setup['config.'], $this->config['config']);
1838
                        $this->config['config'] = $this->tmpl->setup['config.'];
1839
                    }
1840
                    // override it with the page/type-specific "config."
1841
                    if (is_array($this->pSetup['config.'] ?? null)) {
1842
                        ArrayUtility::mergeRecursiveWithOverrule($this->config['config'], $this->pSetup['config.']);
1843
                    }
1844
                    // Set default values for removeDefaultJS and inlineStyle2TempFile so CSS and JS are externalized if compatversion is higher than 4.0
1845
                    if (!isset($this->config['config']['removeDefaultJS'])) {
1846
                        $this->config['config']['removeDefaultJS'] = 'external';
1847
                    }
1848
                    if (!isset($this->config['config']['inlineStyle2TempFile'])) {
1849
                        $this->config['config']['inlineStyle2TempFile'] = 1;
1850
                    }
1851
1852
                    if (!isset($this->config['config']['compressJs'])) {
1853
                        $this->config['config']['compressJs'] = 0;
1854
                    }
1855
                    // Rendering charset of HTML page.
1856
                    if (isset($this->config['config']['metaCharset']) && $this->config['config']['metaCharset'] !== 'utf-8') {
1857
                        $this->metaCharset = $this->config['config']['metaCharset'];
1858
                    }
1859
                    // Setting default cache_timeout
1860
                    if (isset($this->config['config']['cache_period'])) {
1861
                        $this->set_cache_timeout_default((int)$this->config['config']['cache_period']);
1862
                    }
1863
1864
                    // Processing for the config_array:
1865
                    $this->config['rootLine'] = $this->tmpl->rootLine;
1866
                    // Class for render Header and Footer parts
1867
                    if ($this->pSetup['pageHeaderFooterTemplateFile'] ?? false) {
1868
                        try {
1869
                            $file = GeneralUtility::makeInstance(FilePathSanitizer::class)
1870
                                ->sanitize((string)$this->pSetup['pageHeaderFooterTemplateFile'], true);
1871
                            $this->pageRenderer->setTemplateFile($file);
1872
                        } catch (Exception $e) {
1873
                            // do nothing
1874
                        }
1875
                    }
1876
                }
1877
                $timeTracker->pull();
1878
            } else {
1879
                $message = 'No TypoScript template found!';
1880
                $this->logger->alert($message);
1881
                try {
1882
                    $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction(
1883
                        $request,
1884
                        $message,
1885
                        ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_FOUND]
1886
                    );
1887
                    throw new PropagateResponseException($response, 1533931380);
1888
                } catch (AbstractServerErrorException $e) {
1889
                    $exceptionClass = get_class($e);
1890
                    throw new $exceptionClass($message, 1294587218);
1891
                }
1892
            }
1893
        }
1894
1895
        // No cache
1896
        // Set $this->no_cache TRUE if the config.no_cache value is set!
1897
        if ($this->config['config']['no_cache'] ?? false) {
1898
            $this->set_no_cache('config.no_cache is set', true);
1899
        }
1900
1901
        // Auto-configure settings when a site is configured
1902
        $this->config['config']['absRefPrefix'] = $this->config['config']['absRefPrefix'] ?? 'auto';
1903
1904
        // Hook for postProcessing the configuration array
1905
        $params = ['config' => &$this->config['config']];
1906
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['configArrayPostProc'] ?? [] as $funcRef) {
1907
            GeneralUtility::callUserFunction($funcRef, $params, $this);
1908
        }
1909
    }
1910
1911
    /********************************************
1912
     *
1913
     * Further initialization and data processing
1914
     *
1915
     *******************************************/
1916
    /**
1917
     * Setting the language key that will be used by the current page.
1918
     * 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.
1919
     *
1920
     * @param ServerRequestInterface $request
1921
     * @internal
1922
     */
1923
    protected function settingLanguage(ServerRequestInterface $request)
1924
    {
1925
        $_params = [];
1926
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_preProcess'] ?? [] as $_funcRef) {
1927
            $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
1928
            GeneralUtility::callUserFunction($_funcRef, $_params, $ref);
1929
        }
1930
1931
        // Get values from site language
1932
        $languageAspect = LanguageAspectFactory::createFromSiteLanguage($this->language);
1933
1934
        $languageId = $languageAspect->getId();
1935
        $languageContentId = $languageAspect->getContentId();
1936
1937
        $pageTranslationVisibility = new PageTranslationVisibility((int)($this->page['l18n_cfg'] ?? 0));
1938
        // If sys_language_uid is set to another language than default:
1939
        if ($languageAspect->getId() > 0) {
1940
            // check whether a shortcut is overwritten by a translated page
1941
            // we can only do this now, as this is the place where we get
1942
            // to know about translations
1943
            $this->checkTranslatedShortcut($languageAspect->getId(), $request);
1944
            // Request the overlay record for the sys_language_uid:
1945
            $olRec = $this->sys_page->getPageOverlay($this->id, $languageAspect->getId());
1946
            if (empty($olRec)) {
1947
                // If requested translation is not available:
1948
                if ($pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists()) {
1949
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1950
                        $request,
1951
                        'Page is not available in the requested language.',
1952
                        ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE]
1953
                    );
1954
                    throw new PropagateResponseException($response, 1533931388);
1955
                }
1956
                switch ((string)$languageAspect->getLegacyLanguageMode()) {
1957
                    case 'strict':
1958
                        $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1959
                            $request,
1960
                            'Page is not available in the requested language (strict).',
1961
                            ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE_STRICT_MODE]
1962
                        );
1963
                        throw new PropagateResponseException($response, 1533931395);
1964
                    case 'fallback':
1965
                    case 'content_fallback':
1966
                        // Setting content uid (but leaving the sys_language_uid) when a content_fallback
1967
                        // value was found.
1968
                        foreach ($languageAspect->getFallbackChain() ?? [] as $orderValue) {
1969
                            if ($orderValue === '0' || $orderValue === 0 || $orderValue === '') {
1970
                                $languageContentId = 0;
1971
                                break;
1972
                            }
1973
                            if (MathUtility::canBeInterpretedAsInteger($orderValue) && !empty($this->sys_page->getPageOverlay($this->id, (int)$orderValue))) {
1974
                                $languageContentId = (int)$orderValue;
1975
                                break;
1976
                            }
1977
                            if ($orderValue === 'pageNotFound') {
1978
                                // The existing fallbacks have not been found, but instead of continuing
1979
                                // page rendering with default language, a "page not found" message should be shown
1980
                                // instead.
1981
                                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1982
                                    $request,
1983
                                    'Page is not available in the requested language (fallbacks did not apply).',
1984
                                    ['code' => PageAccessFailureReasons::LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE]
1985
                                );
1986
                                throw new PropagateResponseException($response, 1533931402);
1987
                            }
1988
                        }
1989
                        break;
1990
                    case 'ignore':
1991
                        $languageContentId = $languageAspect->getId();
1992
                        break;
1993
                    default:
1994
                        // Default is that everything defaults to the default language...
1995
                        $languageId = ($languageContentId = 0);
1996
                }
1997
            }
1998
1999
            // Define the language aspect again now
2000
            $languageAspect = GeneralUtility::makeInstance(
2001
                LanguageAspect::class,
2002
                $languageId,
2003
                $languageContentId,
2004
                $languageAspect->getOverlayType(),
2005
                $languageAspect->getFallbackChain()
2006
            );
2007
2008
            // Setting sys_language if an overlay record was found (which it is only if a language is used)
2009
            // We'll do this every time since the language aspect might have changed now
2010
            // Doing this ensures that page properties like the page title are returned in the correct language
2011
            $this->page = $this->sys_page->getPageOverlay($this->page, $languageAspect->getContentId());
2012
2013
            // Update SYS_LASTCHANGED for localized page record
2014
            $this->setRegisterValueForSysLastChanged($this->page);
2015
        }
2016
2017
        // Set the language aspect
2018
        $this->context->setAspect('language', $languageAspect);
2019
2020
        // Setting sys_language_uid inside sys-page by creating a new page repository
2021
        $this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context);
2022
        // If default language is not available:
2023
        if ((!$languageAspect->getContentId() || !$languageAspect->getId())
2024
            && $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage()
2025
        ) {
2026
            $message = 'Page is not available in default language.';
2027
            $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
2028
                $request,
2029
                $message,
2030
                ['code' => PageAccessFailureReasons::LANGUAGE_DEFAULT_NOT_AVAILABLE]
2031
            );
2032
            throw new PropagateResponseException($response, 1533931423);
2033
        }
2034
2035
        if ($languageAspect->getId() > 0) {
2036
            $this->updateRootLinesWithTranslations();
2037
        }
2038
2039
        $_params = [];
2040
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_postProcess'] ?? [] as $_funcRef) {
2041
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2042
        }
2043
    }
2044
2045
    /**
2046
     * Updating content of the two rootLines IF the language key is set!
2047
     */
2048
    protected function updateRootLinesWithTranslations()
2049
    {
2050
        try {
2051
            $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
2052
        } catch (RootLineException $e) {
2053
            $this->rootLine = [];
2054
        }
2055
    }
2056
2057
    /**
2058
     * Checks whether a translated shortcut page has a different shortcut
2059
     * target than the original language page.
2060
     * If that is the case, things get corrected to follow that alternative
2061
     * shortcut
2062
     * @param int $languageId
2063
     * @param ServerRequestInterface $request
2064
     */
2065
    protected function checkTranslatedShortcut(int $languageId, ServerRequestInterface $request)
2066
    {
2067
        if (!is_null($this->originalShortcutPage)) {
2068
            $originalShortcutPageOverlay = $this->sys_page->getPageOverlay($this->originalShortcutPage['uid'], $languageId);
2069
            if (!empty($originalShortcutPageOverlay['shortcut']) && $originalShortcutPageOverlay['shortcut'] != $this->id) {
2070
                // the translation of the original shortcut page has a different shortcut target!
2071
                // set the correct page and id
2072
                $shortcut = $this->sys_page->getPageShortcut($originalShortcutPageOverlay['shortcut'], $originalShortcutPageOverlay['shortcut_mode'], $originalShortcutPageOverlay['uid']);
2073
                $this->id = ($this->contentPid = $shortcut['uid']);
2074
                $this->page = $this->sys_page->getPage($this->id);
2075
                // Fix various effects on things like menus f.e.
2076
                $this->fetch_the_id($request);
2077
                $this->tmpl->rootLine = array_reverse($this->rootLine);
2078
            }
2079
        }
2080
    }
2081
2082
    /**
2083
     * Calculates and sets the internal linkVars based upon the current request parameters
2084
     * and the setting "config.linkVars".
2085
     *
2086
     * @param array $queryParams $_GET (usually called with a PSR-7 $request->getQueryParams())
2087
     */
2088
    public function calculateLinkVars(array $queryParams)
2089
    {
2090
        $this->linkVars = '';
2091
        $adminCommand = $queryParams['ADMCMD_prev'] ?? '';
2092
        if (($adminCommand === 'LIVE' || $adminCommand === 'IGNORE') && $this->isBackendUserLoggedIn()) {
2093
            $this->config['config']['linkVars'] = ltrim(($this->config['config']['linkVars'] ?? '') . ',ADMCMD_prev', ',');
2094
        }
2095
        if (empty($this->config['config']['linkVars'])) {
2096
            return;
2097
        }
2098
2099
        $linkVars = $this->splitLinkVarsString((string)$this->config['config']['linkVars']);
2100
2101
        if (empty($linkVars)) {
2102
            return;
2103
        }
2104
        foreach ($linkVars as $linkVar) {
2105
            $test = '';
2106
            if (preg_match('/^(.*)\\((.+)\\)$/', $linkVar, $match)) {
2107
                $linkVar = trim($match[1]);
2108
                $test = trim($match[2]);
2109
            }
2110
2111
            $keys = explode('|', $linkVar);
2112
            $numberOfLevels = count($keys);
2113
            $rootKey = trim($keys[0]);
2114
            if (!isset($queryParams[$rootKey])) {
2115
                continue;
2116
            }
2117
            $value = $queryParams[$rootKey];
2118
            for ($i = 1; $i < $numberOfLevels; $i++) {
2119
                $currentKey = trim($keys[$i]);
2120
                if (isset($value[$currentKey])) {
2121
                    $value = $value[$currentKey];
2122
                } else {
2123
                    $value = false;
2124
                    break;
2125
                }
2126
            }
2127
            if ($value !== false) {
2128
                $parameterName = $keys[0];
2129
                for ($i = 1; $i < $numberOfLevels; $i++) {
2130
                    $parameterName .= '[' . $keys[$i] . ']';
2131
                }
2132
                if (!is_array($value)) {
2133
                    $temp = rawurlencode($value);
2134
                    if ($test !== '' && !$this->isAllowedLinkVarValue($temp, $test)) {
2135
                        // Error: This value was not allowed for this key
2136
                        continue;
2137
                    }
2138
                    $value = '&' . $parameterName . '=' . $temp;
2139
                } else {
2140
                    if ($test !== '' && $test !== 'array') {
2141
                        // Error: This key must not be an array!
2142
                        continue;
2143
                    }
2144
                    $value = HttpUtility::buildQueryString([$parameterName => $value], '&');
2145
                }
2146
                $this->linkVars .= $value;
2147
            }
2148
        }
2149
    }
2150
2151
    /**
2152
     * Split the link vars string by "," but not if the "," is inside of braces
2153
     *
2154
     * @param string $string
2155
     *
2156
     * @return array
2157
     */
2158
    protected function splitLinkVarsString(string $string): array
2159
    {
2160
        $tempCommaReplacementString = '###KASPER###';
2161
2162
        // replace every "," wrapped in "()" by a "unique" string
2163
        $string = preg_replace_callback('/\((?>[^()]|(?R))*\)/', static function ($result) use ($tempCommaReplacementString) {
2164
            return str_replace(',', $tempCommaReplacementString, $result[0]);
2165
        }, $string) ?? '';
2166
2167
        $string = GeneralUtility::trimExplode(',', $string);
2168
2169
        // replace all "unique" strings back to ","
2170
        return str_replace($tempCommaReplacementString, ',', $string);
2171
    }
2172
2173
    /**
2174
     * Checks if the value defined in "config.linkVars" contains an allowed value.
2175
     * Otherwise, return FALSE which means the value will not be added to any links.
2176
     *
2177
     * @param string $haystack The string in which to find $needle
2178
     * @param string $needle The string to find in $haystack
2179
     * @return bool Returns TRUE if $needle matches or is found in $haystack
2180
     */
2181
    protected function isAllowedLinkVarValue(string $haystack, string $needle): bool
2182
    {
2183
        $isAllowed = false;
2184
        // Integer
2185
        if ($needle === 'int' || $needle === 'integer') {
2186
            if (MathUtility::canBeInterpretedAsInteger($haystack)) {
2187
                $isAllowed = true;
2188
            }
2189
        } elseif (preg_match('/^\\/.+\\/[imsxeADSUXu]*$/', $needle)) {
2190
            // Regular expression, only "//" is allowed as delimiter
2191
            if (@preg_match($needle, $haystack)) {
2192
                $isAllowed = true;
2193
            }
2194
        } elseif (str_contains($needle, '-')) {
2195
            // Range
2196
            if (MathUtility::canBeInterpretedAsInteger($haystack)) {
2197
                $range = explode('-', $needle);
2198
                if ($range[0] <= $haystack && $range[1] >= $haystack) {
2199
                    $isAllowed = true;
2200
                }
2201
            }
2202
        } elseif (str_contains($needle, '|')) {
2203
            // List
2204
            // Trim the input
2205
            $haystack = str_replace(' ', '', $haystack);
2206
            if (str_contains('|' . $needle . '|', '|' . $haystack . '|')) {
2207
                $isAllowed = true;
2208
            }
2209
        } elseif ((string)$needle === (string)$haystack) {
2210
            // String comparison
2211
            $isAllowed = true;
2212
        }
2213
        return $isAllowed;
2214
    }
2215
2216
    /**
2217
     * Returns URI of target page, if the current page is an overlaid mountpoint.
2218
     *
2219
     * If the current page is of type mountpoint and should be overlaid with the contents of the mountpoint page
2220
     * and is accessed directly, the user will be redirected to the mountpoint context.
2221
     * @internal
2222
     * @param ServerRequestInterface $request
2223
     * @return string|null
2224
     */
2225
    public function getRedirectUriForMountPoint(ServerRequestInterface $request): ?string
2226
    {
2227
        if (!empty($this->originalMountPointPage) && (int)$this->originalMountPointPage['doktype'] === PageRepository::DOKTYPE_MOUNTPOINT) {
2228
            return $this->getUriToCurrentPageForRedirect($request);
2229
        }
2230
2231
        return null;
2232
    }
2233
2234
    /**
2235
     * Returns URI of target page, if the current page is a Shortcut.
2236
     *
2237
     * If the current page is of type shortcut and accessed directly via its URL,
2238
     * the user will be redirected to shortcut target.
2239
     *
2240
     * @internal
2241
     * @param ServerRequestInterface $request
2242
     * @return string|null
2243
     */
2244
    public function getRedirectUriForShortcut(ServerRequestInterface $request): ?string
2245
    {
2246
        if (!empty($this->originalShortcutPage) && $this->originalShortcutPage['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
2247
            return $this->getUriToCurrentPageForRedirect($request);
2248
        }
2249
2250
        return null;
2251
    }
2252
2253
    /**
2254
     * Instantiate \TYPO3\CMS\Frontend\ContentObject to generate the correct target URL
2255
     *
2256
     * @param ServerRequestInterface $request
2257
     * @return string
2258
     */
2259
    protected function getUriToCurrentPageForRedirect(ServerRequestInterface $request): string
2260
    {
2261
        $this->calculateLinkVars($request->getQueryParams());
2262
        $parameter = $this->page['uid'];
2263
        if ($this->type && MathUtility::canBeInterpretedAsInteger($this->type)) {
2264
            $parameter .= ',' . $this->type;
2265
        }
2266
        return GeneralUtility::makeInstance(ContentObjectRenderer::class, $this)->typoLink_URL([
2267
            'parameter' => $parameter,
2268
            'addQueryString' => true,
2269
            'addQueryString.' => ['exclude' => 'id'],
2270
            'forceAbsoluteUrl' => true,
2271
        ]);
2272
    }
2273
2274
    /********************************************
2275
     *
2276
     * Page generation; cache handling
2277
     *
2278
     *******************************************/
2279
    /**
2280
     * Returns TRUE if the page should be generated.
2281
     * That is if no URL handler is active and the cacheContentFlag is not set.
2282
     *
2283
     * @return bool
2284
     */
2285
    public function isGeneratePage()
2286
    {
2287
        return !$this->cacheContentFlag;
2288
    }
2289
2290
    /**
2291
     * Set cache content to $this->content
2292
     */
2293
    protected function realPageCacheContent()
2294
    {
2295
        // seconds until a cached page is too old
2296
        $cacheTimeout = $this->get_cache_timeout();
2297
        $timeOutTime = $GLOBALS['EXEC_TIME'] + $cacheTimeout;
2298
        $usePageCache = true;
2299
        // Hook for deciding whether page cache should be written to the cache backend or not
2300
        // NOTE: as hooks are called in a loop, the last hook will have the final word (however each
2301
        // hook receives the current status of the $usePageCache flag)
2302
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['usePageCache'] ?? [] as $className) {
2303
            $usePageCache = GeneralUtility::makeInstance($className)->usePageCache($this, $usePageCache);
2304
        }
2305
        // Write the page to cache, if necessary
2306
        if ($usePageCache) {
2307
            $this->setPageCacheContent($this->content, $this->config, $timeOutTime);
2308
        }
2309
        // Hook for cache post processing (eg. writing static files!)
2310
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['insertPageIncache'] ?? [] as $className) {
2311
            GeneralUtility::makeInstance($className)->insertPageIncache($this, $timeOutTime);
2312
        }
2313
    }
2314
2315
    /**
2316
     * Sets cache content; Inserts the content string into the cache_pages cache.
2317
     *
2318
     * @param string $content The content to store in the HTML field of the cache table
2319
     * @param mixed $data The additional cache_data array, fx. $this->config
2320
     * @param int $expirationTstamp Expiration timestamp
2321
     * @see realPageCacheContent()
2322
     */
2323
    protected function setPageCacheContent($content, $data, $expirationTstamp)
2324
    {
2325
        $cacheData = [
2326
            'identifier' => $this->newHash,
2327
            'page_id' => $this->id,
2328
            'content' => $content,
2329
            'cache_data' => $data,
2330
            'expires' => $expirationTstamp,
2331
            'tstamp' => $GLOBALS['EXEC_TIME'],
2332
            'pageTitleInfo' => [
2333
                'title' => $this->page['title'],
2334
                'indexedDocTitle' => $this->indexedDocTitle,
2335
            ],
2336
        ];
2337
        $this->cacheExpires = $expirationTstamp;
2338
        $this->pageCacheTags[] = 'pageId_' . $cacheData['page_id'];
2339
        // Respect the page cache when content of pid is shown
2340
        if ($this->id !== $this->contentPid) {
2341
            $this->pageCacheTags[] = 'pageId_' . $this->contentPid;
2342
        }
2343
        if (!empty($this->page['cache_tags'])) {
2344
            $tags = GeneralUtility::trimExplode(',', $this->page['cache_tags'], true);
2345
            $this->pageCacheTags = array_merge($this->pageCacheTags, $tags);
2346
        }
2347
        // Add the cache themselves as well, because they are fetched by getPageCacheTags()
2348
        $cacheData['cacheTags'] = $this->pageCacheTags;
2349
        $this->pageCache->set($this->newHash, $cacheData, $this->pageCacheTags, $expirationTstamp - $GLOBALS['EXEC_TIME']);
2350
    }
2351
2352
    /**
2353
     * Clears cache content (for $this->newHash)
2354
     */
2355
    public function clearPageCacheContent()
2356
    {
2357
        $this->pageCache->remove($this->newHash);
2358
    }
2359
2360
    /**
2361
     * Sets sys last changed
2362
     * 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.
2363
     *
2364
     * @see ContentObjectRenderer::lastChanged()
2365
     */
2366
    protected function setSysLastChanged()
2367
    {
2368
        // We only update the info if browsing the live workspace
2369
        if ($this->page['SYS_LASTCHANGED'] < (int)$this->register['SYS_LASTCHANGED'] && !$this->doWorkspacePreview()) {
2370
            $connection = GeneralUtility::makeInstance(ConnectionPool::class)
2371
                ->getConnectionForTable('pages');
2372
            $pageId = $this->page['_PAGES_OVERLAY_UID'] ?? $this->id;
2373
            $connection->update(
2374
                'pages',
2375
                [
2376
                    'SYS_LASTCHANGED' => (int)$this->register['SYS_LASTCHANGED'],
2377
                ],
2378
                [
2379
                    'uid' => (int)$pageId,
2380
                ]
2381
            );
2382
        }
2383
    }
2384
2385
    /**
2386
     * Set the SYS_LASTCHANGED register value, is also called when a translated page is in use,
2387
     * so the register reflects the state of the translated page, not the page in the default language.
2388
     *
2389
     * @param array $page
2390
     * @internal
2391
     */
2392
    protected function setRegisterValueForSysLastChanged(array $page): void
2393
    {
2394
        $this->register['SYS_LASTCHANGED'] = (int)$page['tstamp'];
2395
        if ($this->register['SYS_LASTCHANGED'] < (int)$page['SYS_LASTCHANGED']) {
2396
            $this->register['SYS_LASTCHANGED'] = (int)$page['SYS_LASTCHANGED'];
2397
        }
2398
    }
2399
2400
    /**
2401
     * Release pending locks
2402
     *
2403
     * @internal
2404
     */
2405
    public function releaseLocks()
2406
    {
2407
        $this->releaseLock('pagesection');
2408
        $this->releaseLock('pages');
2409
    }
2410
2411
    /**
2412
     * Adds tags to this page's cache entry, you can then f.e. remove cache
2413
     * entries by tag
2414
     *
2415
     * @param array $tags An array of tag
2416
     */
2417
    public function addCacheTags(array $tags)
2418
    {
2419
        $this->pageCacheTags = array_merge($this->pageCacheTags, $tags);
2420
    }
2421
2422
    /**
2423
     * @return array
2424
     */
2425
    public function getPageCacheTags(): array
2426
    {
2427
        return $this->pageCacheTags;
2428
    }
2429
2430
    /********************************************
2431
     *
2432
     * Page generation; rendering and inclusion
2433
     *
2434
     *******************************************/
2435
    /**
2436
     * Does some processing BEFORE the page content is generated / built.
2437
     */
2438
    public function generatePage_preProcessing()
2439
    {
2440
        // Same codeline as in getFromCache(). But $this->all has been changed by
2441
        // \TYPO3\CMS\Core\TypoScript\TemplateService::start() in the meantime, so this must be called again!
2442
        $this->newHash = $this->getHash();
2443
2444
        // Used as a safety check in case a PHP script is falsely disabling $this->no_cache during page generation.
2445
        $this->no_cacheBeforePageGen = $this->no_cache;
2446
    }
2447
2448
    /**
2449
     * Check the value of "content_from_pid" of the current page record, and see if the current request
2450
     * should actually show content from another page.
2451
     *
2452
     * By using $TSFE->getPageAndRootline() on the cloned object, all rootline restrictions (extendToSubPages)
2453
     * are evaluated as well.
2454
     *
2455
     * @param ServerRequestInterface $request
2456
     * @return int the current page ID or another one if resolved properly - usually set to $this->contentPid
2457
     */
2458
    protected function resolveContentPid(ServerRequestInterface $request): int
2459
    {
2460
        if (!isset($this->page['content_from_pid']) || empty($this->page['content_from_pid'])) {
2461
            return (int)$this->id;
2462
        }
2463
        // make REAL copy of TSFE object - not reference!
2464
        $temp_copy_TSFE = clone $this;
2465
        // 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!
2466
        $temp_copy_TSFE->id = $this->page['content_from_pid'];
2467
        $temp_copy_TSFE->MP = '';
2468
        $temp_copy_TSFE->getPageAndRootline($request);
2469
        return (int)$temp_copy_TSFE->id;
2470
    }
2471
    /**
2472
     * Sets up TypoScript "config." options and set properties in $TSFE.
2473
     *
2474
     * @param ServerRequestInterface $request
2475
     */
2476
    public function preparePageContentGeneration(ServerRequestInterface $request)
2477
    {
2478
        $this->getTimeTracker()->push('Prepare page content generation');
2479
        $this->contentPid = $this->resolveContentPid($request);
2480
        // Global vars...
2481
        $this->indexedDocTitle = $this->page['title'] ?? null;
2482
        // Base url:
2483
        if (isset($this->config['config']['baseURL'])) {
2484
            $this->baseUrl = $this->config['config']['baseURL'];
2485
        }
2486
        // Internal and External target defaults
2487
        $this->intTarget = (string)($this->config['config']['intTarget'] ?? '');
2488
        $this->extTarget = (string)($this->config['config']['extTarget'] ?? '');
2489
        $this->fileTarget = (string)($this->config['config']['fileTarget'] ?? '');
2490
        $this->spamProtectEmailAddresses = $this->config['config']['spamProtectEmailAddresses'] ?? 0;
2491
        if ($this->spamProtectEmailAddresses !== 'ascii') {
2492
            $this->spamProtectEmailAddresses = MathUtility::forceIntegerInRange($this->spamProtectEmailAddresses, -10, 10, 0);
2493
        }
2494
        // calculate the absolute path prefix
2495
        if (!empty($this->absRefPrefix = trim($this->config['config']['absRefPrefix'] ?? ''))) {
2496
            if ($this->absRefPrefix === 'auto') {
2497
                $this->absRefPrefix = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
2498
            }
2499
        }
2500
        // @deprecated. This line can be removed with TYPO3 v12.0.
2501
        $this->ATagParams = trim($this->config['config']['ATagParams'] ?? '') ? ' ' . trim($this->config['config']['ATagParams']) : '';
2502
        $this->initializeSearchWordData($request->getParsedBody()['sword_list'] ?? $request->getQueryParams()['sword_list'] ?? null);
2503
        // linkVars
2504
        $this->calculateLinkVars($request->getQueryParams());
2505
        // Setting XHTML-doctype from doctype
2506
        if (!isset($this->config['config']['xhtmlDoctype']) || !$this->config['config']['xhtmlDoctype']) {
2507
            $this->config['config']['xhtmlDoctype'] = $this->config['config']['doctype'] ?? '';
2508
        }
2509
        if ($this->config['config']['xhtmlDoctype']) {
2510
            $this->xhtmlDoctype = $this->config['config']['xhtmlDoctype'];
2511
            // Checking XHTML-docytpe
2512
            switch ((string)$this->config['config']['xhtmlDoctype']) {
2513
                case 'xhtml_trans':
2514
                case 'xhtml_strict':
2515
                    $this->xhtmlVersion = 100;
2516
                    break;
2517
                case 'xhtml_basic':
2518
                    $this->xhtmlVersion = 105;
2519
                    break;
2520
                case 'xhtml_11':
2521
                case 'xhtml+rdfa_10':
2522
                    $this->xhtmlVersion = 110;
2523
                    break;
2524
                default:
2525
                    $this->pageRenderer->setRenderXhtml(false);
2526
                    $this->xhtmlDoctype = '';
2527
                    $this->xhtmlVersion = 0;
2528
            }
2529
        } else {
2530
            $this->pageRenderer->setRenderXhtml(false);
2531
        }
2532
2533
        // Global content object
2534
        $this->newCObj($request);
2535
        $this->getTimeTracker()->pull();
2536
    }
2537
2538
    /**
2539
     * Fills the sWordList property and builds the regular expression in TSFE that can be used to split
2540
     * strings by the submitted search words.
2541
     *
2542
     * @param mixed $searchWords - usually an array, but we can't be sure (yet)
2543
     * @see sWordList
2544
     * @see sWordRegEx
2545
     */
2546
    protected function initializeSearchWordData($searchWords)
2547
    {
2548
        $this->sWordRegEx = '';
2549
        $this->sWordList = $searchWords ?? '';
2550
        if (is_array($this->sWordList)) {
2551
            $space = !empty($this->config['config']['sword_standAlone'] ?? null) ? '[[:space:]]' : '';
2552
            $regexpParts = [];
2553
            foreach ($this->sWordList as $val) {
2554
                if (trim($val) !== '') {
2555
                    $regexpParts[] = $space . preg_quote($val, '/') . $space;
2556
                }
2557
            }
2558
            $this->sWordRegEx = implode('|', $regexpParts);
2559
        }
2560
    }
2561
2562
    /**
2563
     * Does processing of the content after the page content was generated.
2564
     *
2565
     * This includes caching the page, indexing the page (if configured) and setting sysLastChanged
2566
     */
2567
    public function generatePage_postProcessing()
2568
    {
2569
        $this->setAbsRefPrefix();
2570
        // 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.
2571
        if ($this->no_cacheBeforePageGen) {
2572
            $this->set_no_cache('no_cache has been set before the page was generated - safety check', true);
2573
        }
2574
        // Hook for post-processing of page content cached/non-cached:
2575
        $_params = ['pObj' => &$this];
2576
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-all'] ?? [] as $_funcRef) {
2577
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2578
        }
2579
        // Processing if caching is enabled:
2580
        if (!$this->no_cache) {
2581
            // Hook for post-processing of page content before being cached:
2582
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-cached'] ?? [] as $_funcRef) {
2583
                GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2584
            }
2585
        }
2586
        // Convert charset for output. Any hooks before (including indexed search) will have to convert from UTF-8 to the target
2587
        // charset as well.
2588
        $this->content = $this->convOutputCharset($this->content);
2589
        // Storing for cache:
2590
        if (!$this->no_cache) {
2591
            $this->realPageCacheContent();
2592
        }
2593
        // Sets sys-last-change:
2594
        $this->setSysLastChanged();
2595
    }
2596
2597
    /**
2598
     * Generate the page title, can be called multiple times,
2599
     * as PageTitleProvider might have been modified by an uncached plugin etc.
2600
     *
2601
     * @return string the generated page title
2602
     */
2603
    public function generatePageTitle(): string
2604
    {
2605
        // Check for a custom pageTitleSeparator, and perform stdWrap on it
2606
        $pageTitleSeparator = (string)$this->cObj->stdWrapValue('pageTitleSeparator', $this->config['config'] ?? []);
2607
        if ($pageTitleSeparator !== '' && $pageTitleSeparator === ($this->config['config']['pageTitleSeparator'] ?? '')) {
2608
            $pageTitleSeparator .= ' ';
2609
        }
2610
2611
        $titleProvider = GeneralUtility::makeInstance(PageTitleProviderManager::class);
2612
        if (!empty($this->config['config']['pageTitleCache'])) {
2613
            $titleProvider->setPageTitleCache($this->config['config']['pageTitleCache']);
2614
        }
2615
        $pageTitle = $titleProvider->getTitle();
2616
        $this->config['config']['pageTitleCache'] = $titleProvider->getPageTitleCache();
2617
2618
        if ($pageTitle !== '') {
2619
            $this->indexedDocTitle = $pageTitle;
2620
        }
2621
2622
        $titleTagContent = $this->printTitle(
2623
            $pageTitle,
2624
            (bool)($this->config['config']['noPageTitle'] ?? false),
2625
            (bool)($this->config['config']['pageTitleFirst'] ?? false),
2626
            $pageTitleSeparator
2627
        );
2628
        $this->config['config']['pageTitle'] = $titleTagContent;
2629
        // stdWrap around the title tag
2630
        $titleTagContent = $this->cObj->stdWrapValue('pageTitle', $this->config['config']);
2631
2632
        // config.noPageTitle = 2 - means do not render the page title
2633
        if (isset($this->config['config']['noPageTitle']) && (int)$this->config['config']['noPageTitle'] === 2) {
2634
            $titleTagContent = '';
2635
        }
2636
        if ($titleTagContent !== '') {
2637
            $this->pageRenderer->setTitle($titleTagContent);
2638
        }
2639
        return (string)$titleTagContent;
2640
    }
2641
2642
    /**
2643
     * Compiles the content for the page <title> tag.
2644
     *
2645
     * @param string $pageTitle The input title string, typically the "title" field of a page's record.
2646
     * @param bool $noTitle If set, then only the site title is outputted
2647
     * @param bool $showTitleFirst If set, then website title and $title is swapped
2648
     * @param string $pageTitleSeparator an alternative to the ": " as the separator between site title and page title
2649
     * @return string The page title on the form "[website title]: [input-title]". Not htmlspecialchar()'ed.
2650
     * @see generatePageTitle()
2651
     */
2652
    protected function printTitle(string $pageTitle, bool $noTitle = false, bool $showTitleFirst = false, string $pageTitleSeparator = ''): string
2653
    {
2654
        $websiteTitle = $this->getWebsiteTitle();
2655
        $pageTitle = $noTitle ? '' : $pageTitle;
2656
        if ($showTitleFirst) {
2657
            $temp = $websiteTitle;
2658
            $websiteTitle = $pageTitle;
2659
            $pageTitle = $temp;
2660
        }
2661
        // only show a separator if there are both site title and page title
2662
        if ($pageTitle === '' || $websiteTitle === '') {
2663
            $pageTitleSeparator = '';
2664
        } elseif (empty($pageTitleSeparator)) {
2665
            // use the default separator if non given
2666
            $pageTitleSeparator = ': ';
2667
        }
2668
        return $websiteTitle . $pageTitleSeparator . $pageTitle;
2669
    }
2670
2671
    /**
2672
     * @return string
2673
     */
2674
    protected function getWebsiteTitle(): string
2675
    {
2676
        if ($this->language instanceof SiteLanguage
2677
            && trim($this->language->getWebsiteTitle()) !== ''
2678
        ) {
2679
            return trim($this->language->getWebsiteTitle());
2680
        }
2681
        if ($this->site instanceof SiteInterface
2682
            && 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

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