Passed
Push — master ( 741733...88fc2b )
by
unknown
14:08 queued 26s
created

TypoScriptFrontendController::settingLanguage()   D

Complexity

Conditions 21
Paths 108

Size

Total Lines 120
Code Lines 70

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
eloc 70
nc 108
nop 0
dl 0
loc 120
rs 4.1
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
760
        } else {
761
            trigger_error('TypoScriptFrontendController should evaluate the parameter "id" by the PageArguments object, not by a separate constructor argument. This functionality will be removed in TYPO3 v11.0', E_USER_DEPRECATED);
762
            $this->id = $siteOrId;
763
            if ($request->getAttribute('site') instanceof SiteInterface) {
764
                $this->site = $request->getAttribute('site');
765
            } else {
766
                throw new \InvalidArgumentException('TypoScriptFrontendController must be constructed with a valid Site object or a resolved site in the current request as fallback. None given.', 1561583122);
767
            }
768
        }
769
    }
770
771
    /**
772
     * Until TYPO3 v10.0, the third argument of the constructor was given from GET/POST "type" to define the page type
773
     * Since TYPO3 v10.0, this argument is requested to be of type SiteLanguage, which will be mandatory in TYPO3 v11.0.
774
     * If no SiteLanguage object is given, this is fetched from the given request object.
775
     *
776
     * @param SiteLanguage|int|string $siteLanguageOrType
777
     * @param ServerRequestInterface $request
778
     */
779
    private function initializeSiteLanguageWithCompatibility($siteLanguageOrType, ServerRequestInterface $request): void
780
    {
781
        if ($siteLanguageOrType instanceof SiteLanguage) {
782
            $this->language = $siteLanguageOrType;
783
        } else {
784
            trigger_error('TypoScriptFrontendController should evaluate the parameter "type" by the PageArguments object, not by a separate constructor argument. This functionality will be removed in TYPO3 v11.0', E_USER_DEPRECATED);
785
            $this->type = $siteLanguageOrType;
0 ignored issues
show
Documentation Bug introduced by
It seems like $siteLanguageOrType can also be of type string. However, the property $type is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
786
            if ($request->getAttribute('language') instanceof SiteLanguage) {
787
                $this->language = $request->getAttribute('language');
788
            } else {
789
                throw new \InvalidArgumentException('TypoScriptFrontendController must be constructed with a valid SiteLanguage object or a resolved site in the current request as fallback. None given.', 1561583127);
790
            }
791
        }
792
    }
793
794
    /**
795
     * Since TYPO3 v10.0, the fourth constructor argument should be of type PageArguments. However, until TYPO3 v8,
796
     * this was the GET/POST parameter "no_cache". If no PageArguments object is given, the given request is checked
797
     * for the PageArguments.
798
     *
799
     * @param bool|string|PageArguments|null $pageArguments
800
     * @param ServerRequestInterface $request
801
     * @return PageArguments
802
     */
803
    private function buildPageArgumentsWithFallback($pageArguments, ServerRequestInterface $request): PageArguments
804
    {
805
        if ($pageArguments instanceof PageArguments) {
806
            return $pageArguments;
807
        }
808
        if ($request->getAttribute('routing') instanceof PageArguments) {
809
            return $request->getAttribute('routing');
810
        }
811
        trigger_error('TypoScriptFrontendController must be constructed with a valid PageArguments object or a resolved page argument in the current request as fallback. None given.', E_USER_DEPRECATED);
812
        $queryParams = $request->getQueryParams();
813
        $pageId = $this->id ?: ($queryParams['id'] ?? $request->getParsedBody()['id'] ?? 0);
814
        $pageType = $this->type ?: ($queryParams['type'] ?? $request->getParsedBody()['type'] ?? 0);
815
        return new PageArguments((int)$pageId, (string)$pageType, [], $queryParams);
816
    }
817
818
    /**
819
     * Since TYPO3 v10.0, the fifth constructor argument is expected to to be of Type FrontendUserAuthentication.
820
     * However, up until TYPO3 v9.5 this argument was used to define the "cHash" GET/POST parameter. In order to
821
     * ensure maximum compatibility, a deprecation is triggered if an old argument is still used, and PageArguments
822
     * are updated accordingly, and returned.
823
     *
824
     * @param string|FrontendUserAuthentication|null $cHashOrFrontendUser
825
     * @param PageArguments $pageArguments
826
     * @return PageArguments
827
     */
828
    private function initializeFrontendUserOrUpdateCHashArgument($cHashOrFrontendUser, PageArguments $pageArguments): PageArguments
829
    {
830
        if ($cHashOrFrontendUser === null) {
831
            return $pageArguments;
832
        }
833
        if ($cHashOrFrontendUser instanceof FrontendUserAuthentication) {
834
            $this->fe_user = $cHashOrFrontendUser;
835
            return $pageArguments;
836
        }
837
        trigger_error('TypoScriptFrontendController should evaluate the parameter "cHash" by the PageArguments object, not by a separate constructor argument. This functionality will be removed in TYPO3 v11.0', E_USER_DEPRECATED);
838
        return new PageArguments(
839
            $pageArguments->getPageId(),
840
            $pageArguments->getPageType(),
841
            $pageArguments->getRouteArguments(),
842
            array_replace_recursive($pageArguments->getStaticArguments(), ['cHash' => $cHashOrFrontendUser]),
843
            $pageArguments->getDynamicArguments()
844
        );
845
    }
846
847
    /**
848
     * Since TYPO3 v10.0 the seventh constructor argument is not needed anymore, as all data is already provided by
849
     * the given PageArguments object. However, if a specific MP parameter is given anyways, the PageArguments object
850
     * is updated and returned.
851
     *
852
     * @param string|null $MP
853
     * @param PageArguments $pageArguments
854
     * @return PageArguments
855
     */
856
    private function initializeLegacyMountPointArgument(?string $MP, PageArguments $pageArguments): PageArguments
857
    {
858
        if ($MP === null) {
859
            return $pageArguments;
860
        }
861
        trigger_error('TypoScriptFrontendController should evaluate the MountPoint Parameter "MP" by the PageArguments object, not by a separate constructor argument. This functionality will be removed in TYPO3 v11.0', E_USER_DEPRECATED);
862
        if (!$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
863
            return $pageArguments;
864
        }
865
        return new PageArguments(
866
            $pageArguments->getPageId(),
867
            $pageArguments->getPageType(),
868
            $pageArguments->getRouteArguments(),
869
            array_replace_recursive($pageArguments->getStaticArguments(), ['MP' => $MP]),
870
            $pageArguments->getDynamicArguments()
871
        );
872
    }
873
874
    /**
875
     * Initializes the page renderer object
876
     */
877
    protected function initPageRenderer()
878
    {
879
        if ($this->pageRenderer !== null) {
880
            return;
881
        }
882
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
883
        $this->pageRenderer->setTemplateFile('EXT:frontend/Resources/Private/Templates/MainPage.html');
884
        // As initPageRenderer could be called in constructor and for USER_INTs, this information is only set
885
        // once - in order to not override any previous settings of PageRenderer.
886
        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...
887
            $this->pageRenderer->setLanguage($this->language->getTypo3Language());
888
        }
889
    }
890
891
    /**
892
     * @param string $contentType
893
     * @internal Should only be used by TYPO3 core for now
894
     */
895
    public function setContentType($contentType)
896
    {
897
        $this->contentType = $contentType;
898
    }
899
900
    /********************************************
901
     *
902
     * Initializing, resolving page id
903
     *
904
     ********************************************/
905
    /**
906
     * Initializes the caching system.
907
     */
908
    protected function initCaches()
909
    {
910
        $this->pageCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('pages');
911
    }
912
913
    /**
914
     * Initializes the front-end user groups.
915
     * Sets frontend.user aspect based on front-end user status.
916
     */
917
    public function initUserGroups()
918
    {
919
        $userGroups = [0];
920
        // This affects the hidden-flag selecting the fe_groups for the user!
921
        $this->fe_user->showHiddenRecords = $this->context->getPropertyFromAspect('visibility', 'includeHiddenContent', false);
922
        // no matter if we have an active user we try to fetch matching groups which can be set without an user (simulation for instance!)
923
        $this->fe_user->fetchGroupData();
924
        $isUserAndGroupSet = is_array($this->fe_user->user) && !empty($this->fe_user->groupData['uid']);
925
        if ($isUserAndGroupSet) {
926
            // group -2 is not an existing group, but denotes a 'default' group when a user IS logged in.
927
            // This is used to let elements be shown for all logged in users!
928
            $userGroups[] = -2;
929
            $groupsFromUserRecord = $this->fe_user->groupData['uid'];
930
        } else {
931
            // group -1 is not an existing group, but denotes a 'default' group when not logged in.
932
            // This is used to let elements be hidden, when a user is logged in!
933
            $userGroups[] = -1;
934
            if ($this->loginAllowedInBranch) {
935
                // For cases where logins are not banned from a branch usergroups can be set based on IP masks so we should add the usergroups uids.
936
                $groupsFromUserRecord = $this->fe_user->groupData['uid'];
937
            } else {
938
                // Set to blank since we will NOT risk any groups being set when no logins are allowed!
939
                $groupsFromUserRecord = [];
940
            }
941
        }
942
        // Clean up.
943
        // Make unique and sort the groups
944
        $groupsFromUserRecord = array_unique($groupsFromUserRecord);
945
        if (!empty($groupsFromUserRecord) && !$this->loginAllowedInBranch_mode) {
946
            sort($groupsFromUserRecord);
947
            $userGroups = array_merge($userGroups, array_map('intval', $groupsFromUserRecord));
948
        }
949
950
        $this->context->setAspect('frontend.user', GeneralUtility::makeInstance(UserAspect::class, $this->fe_user ?: null, $userGroups));
951
952
        // For every 60 seconds the is_online timestamp for a logged-in user is updated
953
        if ($isUserAndGroupSet) {
954
            $this->fe_user->updateOnlineTimestamp();
955
        }
956
957
        $this->logger->debug('Valid usergroups for TSFE: ' . implode(',', $userGroups));
958
    }
959
960
    /**
961
     * Checking if a user is logged in or a group constellation different from "0,-1"
962
     *
963
     * @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!)
964
     */
965
    public function isUserOrGroupSet()
966
    {
967
        /** @var UserAspect $userAspect */
968
        $userAspect = $this->context->getAspect('frontend.user');
969
        return $userAspect->isUserOrGroupSet();
970
    }
971
972
    /**
973
     * Clears the preview-flags, sets sim_exec_time to current time.
974
     * Hidden pages must be hidden as default, $GLOBALS['SIM_EXEC_TIME'] is set to $GLOBALS['EXEC_TIME']
975
     * in bootstrap initializeGlobalTimeVariables(). Alter it by adding or subtracting seconds.
976
     */
977
    public function clear_preview()
0 ignored issues
show
Coding Style introduced by
Method name "TypoScriptFrontendController::clear_preview" is not in camel caps format
Loading history...
978
    {
979
        if ($this->context->getPropertyFromAspect('frontend.preview', 'isPreview')
980
            || $GLOBALS['EXEC_TIME'] !== $GLOBALS['SIM_EXEC_TIME']
981
            || $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages', false)
982
            || $this->context->getPropertyFromAspect('visibility', 'includeHiddenContent', false)
983
        ) {
984
            $GLOBALS['SIM_EXEC_TIME'] = $GLOBALS['EXEC_TIME'];
985
            $GLOBALS['SIM_ACCESS_TIME'] = $GLOBALS['ACCESS_TIME'];
986
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class));
987
            $this->context->setAspect('date', GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $GLOBALS['SIM_EXEC_TIME'])));
988
            $this->context->setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class));
989
        }
990
    }
991
992
    /**
993
     * Checks if a backend user is logged in
994
     *
995
     * @return bool whether a backend user is logged in
996
     */
997
    public function isBackendUserLoggedIn()
998
    {
999
        return (bool)$this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false);
1000
    }
1001
1002
    /**
1003
     * Determines the id and evaluates any preview settings
1004
     * Basically this function is about determining whether a backend user is logged in,
1005
     * if he has read access to the page and if he's previewing the page.
1006
     * That all determines which id to show and how to initialize the id.
1007
     */
1008
    public function determineId()
1009
    {
1010
        // Call pre processing function for id determination
1011
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PreProcessing'] ?? [] as $functionReference) {
1012
            $parameters = ['parentObject' => $this];
1013
            GeneralUtility::callUserFunction($functionReference, $parameters, $this);
1014
        }
1015
        // If there is a Backend login we are going to check for any preview settings
1016
        $originalFrontendUserGroups = $this->applyPreviewSettings($this->getBackendUser());
1017
        // If the front-end is showing a preview, caching MUST be disabled.
1018
        $isPreview = $this->context->getPropertyFromAspect('frontend.preview', 'isPreview');
1019
        if ($isPreview) {
1020
            $this->disableCache();
1021
        }
1022
        // Now, get the id, validate access etc:
1023
        $this->fetch_the_id();
1024
        // Check if backend user has read access to this page. If not, recalculate the id.
1025
        if ($this->isBackendUserLoggedIn() && $isPreview && !$this->getBackendUser()->doesUserHaveAccess($this->page, Permission::PAGE_SHOW)) {
1026
            // Resetting
1027
            $this->clear_preview();
1028
            $this->fe_user->user[$this->fe_user->usergroup_column] = $originalFrontendUserGroups;
1029
            // Fetching the id again, now with the preview settings reset.
1030
            $this->fetch_the_id();
1031
        }
1032
        // Checks if user logins are blocked for a certain branch and if so, will unset user login and re-fetch ID.
1033
        $this->loginAllowedInBranch = $this->checkIfLoginAllowedInBranch();
1034
        // Logins are not allowed, but there is a login, so will we run this.
1035
        if (!$this->loginAllowedInBranch && $this->isUserOrGroupSet()) {
1036
            if ($this->loginAllowedInBranch_mode === 'all') {
1037
                // Clear out user and group:
1038
                $this->fe_user->hideActiveLogin();
1039
                $userGroups = [0, -1];
1040
            } else {
1041
                $userGroups = [0, -2];
1042
            }
1043
            $this->context->setAspect('frontend.user', GeneralUtility::makeInstance(UserAspect::class, $this->fe_user ?: null, $userGroups));
1044
            // Fetching the id again, now with the preview settings reset.
1045
            $this->fetch_the_id();
1046
        }
1047
        // Final cleaning.
1048
        // Make sure it's an integer
1049
        $this->id = ($this->contentPid = (int)$this->id);
1050
        // Make sure it's an integer
1051
        $this->type = (int)$this->type;
1052
        // Call post processing function for id determination:
1053
        $_params = ['pObj' => &$this];
1054
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc'] ?? [] as $_funcRef) {
1055
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1056
        }
1057
    }
1058
1059
    /**
1060
     * Evaluates admin panel or workspace settings to see if
1061
     * visibility settings like
1062
     * - Preview Aspect: isPreview
1063
     * - Visibility Aspect: includeHiddenPages
1064
     * - Visibility Aspect: includeHiddenContent
1065
     * - $simUserGroup
1066
     * should be applied to the current object.
1067
     *
1068
     * @param FrontendBackendUserAuthentication $backendUser
1069
     * @return string|null null if no changes to the current frontend usergroups have been made, otherwise the original list of frontend usergroups
1070
     * @internal
1071
     */
1072
    protected function applyPreviewSettings($backendUser = null)
1073
    {
1074
        if (!$backendUser) {
1075
            return null;
1076
        }
1077
        $originalFrontendUserGroup = null;
1078
        if ($this->fe_user->user) {
1079
            $originalFrontendUserGroup = $this->context->getPropertyFromAspect('frontend.user', 'groupIds');
1080
        }
1081
1082
        // The preview flag is set if the current page turns out to be hidden
1083
        if ($this->id && $this->determineIdIsHiddenPage()) {
1084
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, true));
1085
            /** @var VisibilityAspect $aspect */
1086
            $aspect = $this->context->getAspect('visibility');
1087
            $newAspect = GeneralUtility::makeInstance(VisibilityAspect::class, true, $aspect->includeHiddenContent(), $aspect->includeDeletedRecords());
1088
            $this->context->setAspect('visibility', $newAspect);
1089
        }
1090
        // The preview flag will be set if an offline workspace will be previewed
1091
        if ($this->whichWorkspace() > 0) {
1092
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, true));
1093
        }
1094
        return $this->context->getPropertyFromAspect('frontend.preview', 'preview', false) ? $originalFrontendUserGroup : null;
1095
    }
1096
1097
    /**
1098
     * Checks if the page is hidden in the active workspace.
1099
     * If it is hidden, preview flags will be set.
1100
     *
1101
     * @return bool
1102
     */
1103
    protected function determineIdIsHiddenPage()
1104
    {
1105
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1106
            ->getQueryBuilderForTable('pages');
1107
        $queryBuilder
1108
            ->getRestrictions()
1109
            ->removeAll()
1110
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1111
1112
        $queryBuilder
1113
            ->select('uid', 'hidden', 'starttime', 'endtime')
1114
            ->from('pages')
1115
            ->where(
1116
                $queryBuilder->expr()->gte('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
1117
            )
1118
            ->setMaxResults(1);
1119
1120
        // $this->id always points to the ID of the default language page, so we check
1121
        // the current site language to determine if we need to fetch a translation but consider fallbacks
1122
        if ($this->language->getLanguageId() > 0) {
1123
            $languagesToCheck = array_merge([$this->language->getLanguageId()], $this->language->getFallbackLanguageIds());
1124
            // Check for the language and all its fallbacks
1125
            $constraint = $queryBuilder->expr()->andX(
1126
                $queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)),
1127
                $queryBuilder->expr()->in('sys_language_uid', $queryBuilder->createNamedParameter(array_filter($languagesToCheck), Connection::PARAM_INT_ARRAY))
1128
            );
1129
            // If the fallback language Ids also contains the default language, this needs to be considered
1130
            if (in_array(0, $languagesToCheck, true)) {
1131
                $constraint = $queryBuilder->expr()->orX(
1132
                    $constraint,
1133
                    // Ensure to also fetch the default record
1134
                    $queryBuilder->expr()->andX(
1135
                        $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)),
1136
                        $queryBuilder->expr()->in('sys_language_uid', 0)
1137
                    )
1138
                );
1139
            }
1140
            // Ensure that the translated records are shown first (maxResults is set to 1)
1141
            $queryBuilder->orderBy('sys_language_uid', 'DESC');
1142
        } else {
1143
            $constraint = $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT));
1144
        }
1145
        $queryBuilder->andWhere($constraint);
1146
1147
        $page = $queryBuilder->execute()->fetch();
1148
1149
        if ($this->whichWorkspace() > 0) {
1150
            // Fetch overlay of page if in workspace and check if it is hidden
1151
            $customContext = clone $this->context;
1152
            $customContext->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $this->whichWorkspace()));
1153
            $customContext->setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class));
1154
            $pageSelectObject = GeneralUtility::makeInstance(PageRepository::class, $customContext);
1155
            $targetPage = $pageSelectObject->getWorkspaceVersionOfRecord($this->whichWorkspace(), 'pages', $page['uid']);
1156
            $result = $targetPage === -1 || $targetPage === -2;
1157
        } else {
1158
            $result = is_array($page) && ($page['hidden'] || $page['starttime'] > $GLOBALS['SIM_EXEC_TIME'] || $page['endtime'] != 0 && $page['endtime'] <= $GLOBALS['SIM_EXEC_TIME']);
1159
        }
1160
        return $result;
1161
    }
1162
1163
    /**
1164
     * Resolves the page id and sets up several related properties.
1165
     *
1166
     * If $this->id is not set at all or is not a plain integer, the method
1167
     * does it's best to set the value to an integer. Resolving is based on
1168
     * this options:
1169
     *
1170
     * - Splitting $this->id if it contains an additional type parameter.
1171
     * - Finding the domain record start page
1172
     * - First visible page
1173
     * - Relocating the id below the domain record if outside
1174
     *
1175
     * The following properties may be set up or updated:
1176
     *
1177
     * - id
1178
     * - requestedId
1179
     * - type
1180
     * - sys_page
1181
     * - sys_page->where_groupAccess
1182
     * - sys_page->where_hid_del
1183
     * - Context: FrontendUser Aspect
1184
     * - no_cache
1185
     * - register['SYS_LASTCHANGED']
1186
     * - pageNotFound
1187
     *
1188
     * Via getPageAndRootlineWithDomain()
1189
     *
1190
     * - rootLine
1191
     * - page
1192
     * - MP
1193
     * - originalShortcutPage
1194
     * - originalMountPointPage
1195
     * - pageAccessFailureHistory['direct_access']
1196
     * - pageNotFound
1197
     *
1198
     * @todo:
1199
     *
1200
     * On the first impression the method does to much. This is increased by
1201
     * the fact, that is is called repeated times by the method determineId.
1202
     * The reasons are manifold.
1203
     *
1204
     * 1.) The first part, the creation of sys_page and the type
1205
     * resolution don't need to be repeated. They could be separated to be
1206
     * called only once.
1207
     *
1208
     * 2.) The user group setup could be done once on a higher level.
1209
     *
1210
     * 3.) The workflow of the resolution could be elaborated to be less
1211
     * tangled. Maybe the check of the page id to be below the domain via the
1212
     * root line doesn't need to be done each time, but for the final result
1213
     * only.
1214
     *
1215
     * 4.) The root line does not need to be directly addressed by this class.
1216
     * A root line is always related to one page. The rootline could be handled
1217
     * indirectly by page objects. Page objects still don't exist.
1218
     *
1219
     * @throws ServiceUnavailableException
1220
     * @internal
1221
     */
1222
    public function fetch_the_id()
0 ignored issues
show
Coding Style introduced by
Method name "TypoScriptFrontendController::fetch_the_id" is not in camel caps format
Loading history...
1223
    {
1224
        $timeTracker = $this->getTimeTracker();
1225
        $timeTracker->push('fetch_the_id initialize/');
1226
        // Set the valid usergroups for FE
1227
        $this->initUserGroups();
1228
        // Initialize the PageRepository has to be done after the frontend usergroups are initialized / resolved, as
1229
        // frontend group aspect is modified before
1230
        $this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context);
1231
        // The id and type is set to the integer-value - just to be sure...
1232
        $this->id = (int)$this->id;
1233
        $this->type = (int)$this->type;
1234
        $timeTracker->pull();
1235
        // We find the first page belonging to the current domain
1236
        $timeTracker->push('fetch_the_id domain/');
1237
        if (!$this->id) {
1238
            // If the id was not previously set, set it to the root page id of the site.
1239
            $this->id = $this->site->getRootPageId();
1240
        }
1241
        $timeTracker->pull();
1242
        $timeTracker->push('fetch_the_id rootLine/');
1243
        // We store the originally requested id
1244
        $this->requestedId = $this->id;
1245
        try {
1246
            $this->getPageAndRootlineWithDomain($this->site->getRootPageId());
1247
        } catch (ShortcutTargetPageNotFoundException $e) {
1248
            $this->pageNotFound = 1;
1249
        }
1250
        $timeTracker->pull();
1251
        if ($this->pageNotFound) {
1252
            switch ($this->pageNotFound) {
1253
                case 1:
1254
                    $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
1255
                        $GLOBALS['TYPO3_REQUEST'],
1256
                        'ID was not an accessible page',
1257
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED)
1258
                    );
1259
                    break;
1260
                case 2:
1261
                    $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
1262
                        $GLOBALS['TYPO3_REQUEST'],
1263
                        'Subsection was found and not accessible',
1264
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED)
1265
                    );
1266
                    break;
1267
                case 3:
1268
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1269
                        $GLOBALS['TYPO3_REQUEST'],
1270
                        'ID was outside the domain',
1271
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH)
1272
                    );
1273
                    break;
1274
                default:
1275
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1276
                        $GLOBALS['TYPO3_REQUEST'],
1277
                        'Unspecified error',
1278
                        $this->getPageAccessFailureReasons()
1279
                    );
1280
            }
1281
            throw new ImmediateResponseException($response, 1533931329);
1282
        }
1283
1284
        $this->setRegisterValueForSysLastChanged($this->page);
1285
1286
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['fetchPageId-PostProcessing'] ?? [] as $functionReference) {
1287
            $parameters = ['parentObject' => $this];
1288
            GeneralUtility::callUserFunction($functionReference, $parameters, $this);
1289
        }
1290
    }
1291
1292
    /**
1293
     * Loads the page and root line records based on $this->id
1294
     *
1295
     * A final page and the matching root line are determined and loaded by
1296
     * the algorithm defined by this method.
1297
     *
1298
     * First it loads the initial page from the page repository for $this->id.
1299
     * If that can't be loaded directly, it gets the root line for $this->id.
1300
     * It walks up the root line towards the root page until the page
1301
     * repository can deliver a page record. (The loading restrictions of
1302
     * the root line records are more liberal than that of the page record.)
1303
     *
1304
     * Now the page type is evaluated and handled if necessary. If the page is
1305
     * a short cut, it is replaced by the target page. If the page is a mount
1306
     * point in overlay mode, the page is replaced by the mounted page.
1307
     *
1308
     * After this potential replacements are done, the root line is loaded
1309
     * (again) for this page record. It walks up the root line up to
1310
     * the first viewable record.
1311
     *
1312
     * (While upon the first accessibility check of the root line it was done
1313
     * by loading page by page from the page repository, this time the method
1314
     * checkRootlineForIncludeSection() is used to find the most distant
1315
     * accessible page within the root line.)
1316
     *
1317
     * Having found the final page id, the page record and the root line are
1318
     * loaded for last time by this method.
1319
     *
1320
     * Exceptions may be thrown for DOKTYPE_SPACER and not loadable page records
1321
     * or root lines.
1322
     *
1323
     * May set or update this properties:
1324
     *
1325
     * @see TypoScriptFrontendController::$id
1326
     * @see TypoScriptFrontendController::$MP
1327
     * @see TypoScriptFrontendController::$page
1328
     * @see TypoScriptFrontendController::$pageNotFound
1329
     * @see TypoScriptFrontendController::$pageAccessFailureHistory
1330
     * @see TypoScriptFrontendController::$originalMountPointPage
1331
     * @see TypoScriptFrontendController::$originalShortcutPage
1332
     *
1333
     * @throws ServiceUnavailableException
1334
     * @throws PageNotFoundException
1335
     */
1336
    protected function getPageAndRootline()
1337
    {
1338
        $requestedPageRowWithoutGroupCheck = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $requestedPageRowWithoutGroupCheck is dead and can be removed.
Loading history...
1339
        $this->resolveTranslatedPageId();
1340
        if (empty($this->page)) {
1341
            // If no page, we try to find the page before in the rootLine.
1342
            // Page is 'not found' in case the id itself was not an accessible page. code 1
1343
            $this->pageNotFound = 1;
1344
            try {
1345
                $requestedPageRowWithoutGroupCheck = $this->sys_page->getPage($this->id, true);
1346
                if (!empty($requestedPageRowWithoutGroupCheck)) {
1347
                    $this->pageAccessFailureHistory['direct_access'][] = $requestedPageRowWithoutGroupCheck;
1348
                }
1349
                $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1350
                if (!empty($this->rootLine)) {
1351
                    $c = count($this->rootLine) - 1;
1352
                    while ($c > 0) {
1353
                        // Add to page access failure history:
1354
                        $this->pageAccessFailureHistory['direct_access'][] = $this->rootLine[$c];
1355
                        // Decrease to next page in rootline and check the access to that, if OK, set as page record and ID value.
1356
                        $c--;
1357
                        $this->id = $this->rootLine[$c]['uid'];
1358
                        $this->page = $this->sys_page->getPage($this->id);
1359
                        if (!empty($this->page)) {
1360
                            break;
1361
                        }
1362
                    }
1363
                }
1364
            } catch (RootLineException $e) {
1365
                $this->rootLine = [];
1366
            }
1367
            // If still no page...
1368
            if (empty($requestedPageRowWithoutGroupCheck) && empty($this->page)) {
1369
                $message = 'The requested page does not exist!';
1370
                $this->logger->error($message);
1371
                try {
1372
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1373
                        $GLOBALS['TYPO3_REQUEST'],
1374
                        $message,
1375
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND)
1376
                    );
1377
                    throw new ImmediateResponseException($response, 1533931330);
1378
                } catch (PageNotFoundException $e) {
1379
                    throw new PageNotFoundException($message, 1301648780);
1380
                }
1381
            }
1382
        }
1383
        // Spacer is not accessible in frontend
1384
        if ($this->page['doktype'] == PageRepository::DOKTYPE_SPACER) {
1385
            $message = 'The requested page does not exist!';
1386
            $this->logger->error($message);
1387
            try {
1388
                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1389
                    $GLOBALS['TYPO3_REQUEST'],
1390
                    $message,
1391
                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_INVALID_PAGETYPE)
1392
                );
1393
                throw new ImmediateResponseException($response, 1533931343);
1394
            } catch (PageNotFoundException $e) {
1395
                throw new PageNotFoundException($message, 1301648781);
1396
            }
1397
        }
1398
        // Is the ID a link to another page??
1399
        if ($this->page['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
1400
            // 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.
1401
            $this->MP = '';
1402
            // saving the page so that we can check later - when we know
1403
            // about languages - whether we took the correct shortcut or
1404
            // whether a translation of the page overwrites the shortcut
1405
            // target and we need to follow the new target
1406
            $this->originalShortcutPage = $this->page;
1407
            $this->page = $this->sys_page->getPageShortcut($this->page['shortcut'], $this->page['shortcut_mode'], $this->page['uid']);
1408
            $this->id = $this->page['uid'];
1409
        }
1410
        // If the page is a mountpoint which should be overlaid with the contents of the mounted page,
1411
        // it must never be accessible directly, but only in the mountpoint context. Therefore we change
1412
        // the current ID and the user is redirected by checkPageForMountpointRedirect().
1413
        if ($this->page['doktype'] == PageRepository::DOKTYPE_MOUNTPOINT && $this->page['mount_pid_ol']) {
1414
            $this->originalMountPointPage = $this->page;
1415
            $this->page = $this->sys_page->getPage($this->page['mount_pid']);
1416
            if (empty($this->page)) {
1417
                $message = 'This page (ID ' . $this->originalMountPointPage['uid'] . ') is of type "Mount point" and '
1418
                    . 'mounts a page which is not accessible (ID ' . $this->originalMountPointPage['mount_pid'] . ').';
1419
                throw new PageNotFoundException($message, 1402043263);
1420
            }
1421
            // If the current page is a shortcut, the MP parameter will be replaced
1422
            if ($this->MP === '' || !empty($this->originalShortcutPage)) {
1423
                $this->MP = $this->page['uid'] . '-' . $this->originalMountPointPage['uid'];
1424
            } else {
1425
                $this->MP .= ',' . $this->page['uid'] . '-' . $this->originalMountPointPage['uid'];
1426
            }
1427
            $this->id = $this->page['uid'];
1428
        }
1429
        // Gets the rootLine
1430
        try {
1431
            $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1432
        } catch (RootLineException $e) {
1433
            $this->rootLine = [];
1434
        }
1435
        // If not rootline we're off...
1436
        if (empty($this->rootLine)) {
1437
            $message = 'The requested page didn\'t have a proper connection to the tree-root!';
1438
            $this->logger->error($message);
1439
            try {
1440
                $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
1441
                    $GLOBALS['TYPO3_REQUEST'],
1442
                    $message,
1443
                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ROOTLINE_BROKEN)
1444
                );
1445
                throw new ImmediateResponseException($response, 1533931350);
1446
            } catch (ServiceUnavailableException $e) {
1447
                throw new ServiceUnavailableException($message, 1301648167);
1448
            }
1449
        }
1450
        // Checking for include section regarding the hidden/starttime/endtime/fe_user (that is access control of a whole subbranch!)
1451
        if ($this->checkRootlineForIncludeSection()) {
1452
            if (empty($this->rootLine)) {
1453
                $message = 'The requested page was not accessible!';
1454
                try {
1455
                    $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
1456
                        $GLOBALS['TYPO3_REQUEST'],
1457
                        $message,
1458
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_GENERAL)
1459
                    );
1460
                    throw new ImmediateResponseException($response, 1533931351);
1461
                } catch (ServiceUnavailableException $e) {
1462
                    $this->logger->warning($message);
1463
                    throw new ServiceUnavailableException($message, 1301648234);
1464
                }
1465
            } else {
1466
                $el = reset($this->rootLine);
1467
                $this->id = $el['uid'];
1468
                $this->page = $this->sys_page->getPage($this->id);
1469
                try {
1470
                    $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1471
                } catch (RootLineException $e) {
1472
                    $this->rootLine = [];
1473
                }
1474
            }
1475
        }
1476
    }
1477
1478
    /**
1479
     * If $this->id contains a translated page record, this needs to be resolved to the default language
1480
     * in order for all rootline functionality and access restrictions to be in place further on.
1481
     *
1482
     * Additionally, if a translated page is found, LanguageAspect is set as well.
1483
     */
1484
    protected function resolveTranslatedPageId()
1485
    {
1486
        $this->page = $this->sys_page->getPage($this->id);
0 ignored issues
show
Bug introduced by
$this->id of type string is incompatible with the type integer expected by parameter $uid of TYPO3\CMS\Core\Domain\Re...geRepository::getPage(). ( Ignorable by Annotation )

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

1486
        $this->page = $this->sys_page->getPage(/** @scrutinizer ignore-type */ $this->id);
Loading history...
1487
        // Accessed a default language page record, nothing to resolve
1488
        if (empty($this->page) || (int)$this->page[$GLOBALS['TCA']['pages']['ctrl']['languageField']] === 0) {
1489
            return;
1490
        }
1491
        $languageId = (int)$this->page[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
1492
        $this->page = $this->sys_page->getPage($this->page[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']]);
1493
        $this->context->setAspect('language', GeneralUtility::makeInstance(LanguageAspect::class, $languageId));
1494
        $this->id = $this->page['uid'];
1495
    }
1496
1497
    /**
1498
     * Checks if visibility of the page is blocked upwards in the root line.
1499
     *
1500
     * If any page in the root line is blocking visibility, true is returned.
1501
     *
1502
     * All pages from the blocking page downwards are removed from the root
1503
     * line, so that the remaining pages can be used to relocate the page up
1504
     * to lowest visible page.
1505
     *
1506
     * The blocking feature of a page must be turned on by setting the page
1507
     * record field 'extendToSubpages' to 1 in case of hidden, starttime,
1508
     * endtime or fe_group restrictions.
1509
     *
1510
     * Additionally this method checks for backend user sections in root line
1511
     * and if found evaluates if a backend user is logged in and has access.
1512
     *
1513
     * Recyclers are also checked and trigger page not found if found in root
1514
     * line.
1515
     *
1516
     * @todo Find a better name, i.e. checkVisibilityByRootLine
1517
     * @todo Invert boolean return value. Return true if visible.
1518
     *
1519
     * @return bool
1520
     */
1521
    protected function checkRootlineForIncludeSection(): bool
1522
    {
1523
        $c = count($this->rootLine);
1524
        $removeTheRestFlag = false;
1525
        for ($a = 0; $a < $c; $a++) {
1526
            if (!$this->checkPagerecordForIncludeSection($this->rootLine[$a])) {
1527
                // Add to page access failure history and mark the page as not found
1528
                // Keep the rootline however to trigger an access denied error instead of a service unavailable error
1529
                $this->pageAccessFailureHistory['sub_section'][] = $this->rootLine[$a];
1530
                $this->pageNotFound = 2;
1531
            }
1532
1533
            if ((int)$this->rootLine[$a]['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION) {
1534
                // If there is a backend user logged in, check if they have read access to the page:
1535
                if ($this->isBackendUserLoggedIn()) {
1536
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1537
                        ->getQueryBuilderForTable('pages');
1538
1539
                    $queryBuilder
1540
                        ->getRestrictions()
1541
                        ->removeAll();
1542
1543
                    $row = $queryBuilder
1544
                        ->select('uid')
1545
                        ->from('pages')
1546
                        ->where(
1547
                            $queryBuilder->expr()->eq(
1548
                                'uid',
1549
                                $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
1550
                            ),
1551
                            $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)
1552
                        )
1553
                        ->execute()
1554
                        ->fetch();
1555
1556
                    // versionOL()?
1557
                    if (!$row) {
1558
                        // 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...
1559
                        $removeTheRestFlag = true;
1560
                    }
1561
                } else {
1562
                    // Don't go here, if there is no backend user logged in.
1563
                    $removeTheRestFlag = true;
1564
                }
1565
            } elseif ((int)$this->rootLine[$a]['doktype'] === PageRepository::DOKTYPE_RECYCLER) {
1566
                // page is in a recycler
1567
                $removeTheRestFlag = true;
1568
            }
1569
            if ($removeTheRestFlag) {
1570
                // Page is 'not found' in case a subsection was found and not accessible, code 2
1571
                $this->pageNotFound = 2;
1572
                unset($this->rootLine[$a]);
1573
            }
1574
        }
1575
        return $removeTheRestFlag;
1576
    }
1577
1578
    /**
1579
     * Checks page record for enableFields
1580
     * Returns TRUE if enableFields does not disable the page record.
1581
     * Takes notice of the includeHiddenPages visibility aspect flag and uses SIM_ACCESS_TIME for start/endtime evaluation
1582
     *
1583
     * @param array $row The page record to evaluate (needs fields: hidden, starttime, endtime, fe_group)
1584
     * @param bool $bypassGroupCheck Bypass group-check
1585
     * @return bool TRUE, if record is viewable.
1586
     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getTreeList()
1587
     * @see checkPagerecordForIncludeSection()
1588
     */
1589
    public function checkEnableFields($row, $bypassGroupCheck = false)
1590
    {
1591
        $_params = ['pObj' => $this, 'row' => &$row, 'bypassGroupCheck' => &$bypassGroupCheck];
1592
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['hook_checkEnableFields'] ?? [] as $_funcRef) {
1593
            // Call hooks: If one returns FALSE, method execution is aborted with result "This record is not available"
1594
            $return = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1595
            if ($return === false) {
1596
                return false;
1597
            }
1598
        }
1599
        if ((!$row['hidden'] || $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages', false))
1600
            && $row['starttime'] <= $GLOBALS['SIM_ACCESS_TIME']
1601
            && ($row['endtime'] == 0 || $row['endtime'] > $GLOBALS['SIM_ACCESS_TIME'])
1602
            && ($bypassGroupCheck || $this->checkPageGroupAccess($row))) {
1603
            return true;
1604
        }
1605
        return false;
1606
    }
1607
1608
    /**
1609
     * Check group access against a page record
1610
     *
1611
     * @param array $row The page record to evaluate (needs field: fe_group)
1612
     * @return bool TRUE, if group access is granted.
1613
     * @internal
1614
     */
1615
    public function checkPageGroupAccess($row)
1616
    {
1617
        /** @var UserAspect $userAspect */
1618
        $userAspect = $this->context->getAspect('frontend.user');
1619
        $pageGroupList = explode(',', $row['fe_group'] ?: 0);
1620
        return count(array_intersect($userAspect->getGroupIds(), $pageGroupList)) > 0;
1621
    }
1622
1623
    /**
1624
     * Checks if the current page of the root line is visible.
1625
     *
1626
     * If the field extendToSubpages is 0, access is granted,
1627
     * else the fields hidden, starttime, endtime, fe_group are evaluated.
1628
     *
1629
     * @todo Find a better name, i.e. isVisibleRecord()
1630
     *
1631
     * @param array $row The page record
1632
     * @return bool true if visible
1633
     * @internal
1634
     * @see checkEnableFields()
1635
     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getTreeList()
1636
     * @see checkRootlineForIncludeSection()
1637
     */
1638
    public function checkPagerecordForIncludeSection(array $row): bool
1639
    {
1640
        return !$row['extendToSubpages'] || $this->checkEnableFields($row);
1641
    }
1642
1643
    /**
1644
     * 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!)
1645
     *
1646
     * @return bool returns TRUE if logins are OK, otherwise FALSE (and then the login user must be unset!)
1647
     */
1648
    public function checkIfLoginAllowedInBranch()
1649
    {
1650
        // Initialize:
1651
        $c = count($this->rootLine);
1652
        $loginAllowed = true;
1653
        // Traverse root line from root and outwards:
1654
        for ($a = 0; $a < $c; $a++) {
1655
            // If a value is set for login state:
1656
            if ($this->rootLine[$a]['fe_login_mode'] > 0) {
1657
                // Determine state from value:
1658
                if ((int)$this->rootLine[$a]['fe_login_mode'] === 1) {
1659
                    $loginAllowed = false;
1660
                    $this->loginAllowedInBranch_mode = 'all';
1661
                } elseif ((int)$this->rootLine[$a]['fe_login_mode'] === 3) {
1662
                    $loginAllowed = false;
1663
                    $this->loginAllowedInBranch_mode = 'groups';
1664
                } else {
1665
                    $loginAllowed = true;
1666
                }
1667
            }
1668
        }
1669
        return $loginAllowed;
1670
    }
1671
1672
    /**
1673
     * 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
1674
     *
1675
     * @param string $failureReasonCode the error code to be attached (optional), see PageAccessFailureReasons list for details
1676
     * @return array Summary of why page access was not allowed.
1677
     */
1678
    public function getPageAccessFailureReasons(string $failureReasonCode = null)
1679
    {
1680
        $output = [];
1681
        if ($failureReasonCode) {
1682
            $output['code'] = $failureReasonCode;
1683
        }
1684
        $combinedRecords = array_merge(is_array($this->pageAccessFailureHistory['direct_access']) ? $this->pageAccessFailureHistory['direct_access'] : [['fe_group' => 0]], is_array($this->pageAccessFailureHistory['sub_section']) ? $this->pageAccessFailureHistory['sub_section'] : []);
1685
        if (!empty($combinedRecords)) {
1686
            foreach ($combinedRecords as $k => $pagerec) {
1687
                // 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
1688
                // 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!
1689
                if (!$k || $pagerec['extendToSubpages']) {
1690
                    if ($pagerec['hidden']) {
1691
                        $output['hidden'][$pagerec['uid']] = true;
1692
                    }
1693
                    if ($pagerec['starttime'] > $GLOBALS['SIM_ACCESS_TIME']) {
1694
                        $output['starttime'][$pagerec['uid']] = $pagerec['starttime'];
1695
                    }
1696
                    if ($pagerec['endtime'] != 0 && $pagerec['endtime'] <= $GLOBALS['SIM_ACCESS_TIME']) {
1697
                        $output['endtime'][$pagerec['uid']] = $pagerec['endtime'];
1698
                    }
1699
                    if (!$this->checkPageGroupAccess($pagerec)) {
1700
                        $output['fe_group'][$pagerec['uid']] = $pagerec['fe_group'];
1701
                    }
1702
                }
1703
            }
1704
        }
1705
        return $output;
1706
    }
1707
1708
    /**
1709
     * Gets ->page and ->rootline information based on ->id. ->id may change during this operation.
1710
     * If not inside a site, then default to first page in site.
1711
     *
1712
     * @param int $rootPageId Page uid of the page where the found site is located
1713
     * @internal
1714
     */
1715
    public function getPageAndRootlineWithDomain($rootPageId)
1716
    {
1717
        $this->getPageAndRootline();
1718
        // 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.
1719
        if (is_array($this->rootLine) && $this->rootLine !== []) {
1720
            $idFound = false;
1721
            foreach ($this->rootLine as $key => $val) {
1722
                if ($val['uid'] == $rootPageId) {
1723
                    $idFound = true;
1724
                    break;
1725
                }
1726
            }
1727
            if (!$idFound) {
1728
                // Page is 'not found' in case the id was outside the domain, code 3
1729
                $this->pageNotFound = 3;
1730
                $this->id = $rootPageId;
1731
                // re-get the page and rootline if the id was not found.
1732
                $this->getPageAndRootline();
1733
            }
1734
        }
1735
    }
1736
1737
    /********************************************
1738
     *
1739
     * Template and caching related functions.
1740
     *
1741
     *******************************************/
1742
1743
    /**
1744
     * Will disable caching if the cHash value was not set when having dynamic arguments in GET query parameters.
1745
     * This function should be called to check the _existence_ of "&cHash" whenever a plugin generating cacheable output is using extra GET variables. If there _is_ a cHash value the validation of it automatically takes place in makeCacheHash() (see above)
1746
     *
1747
     * @deprecated since TYPO3 v10.2, will be removed in TYPO3 v11. The PSR-15 middleware PageArgumentValidator is already taking care of this.
1748
     */
1749
    public function reqCHash()
1750
    {
1751
        trigger_error('TypoScriptFrontendController->reqCHash() is not needed anymore, as all functionality is handled via the PSR-15 PageArgumentValidator middleware already.', E_USER_DEPRECATED);
1752
        if (!empty($this->pageArguments->getArguments()['cHash']) || empty($this->pageArguments->getDynamicArguments())) {
1753
            return;
1754
        }
1755
        $queryParams = $this->pageArguments->getDynamicArguments();
1756
        $queryParams['id'] = $this->pageArguments->getPageId();
1757
        $argumentsThatWouldRequireCacheHash = GeneralUtility::makeInstance(CacheHashCalculator::class)
1758
                ->getRelevantParameters(HttpUtility::buildQueryString($queryParams));
1759
        if (empty($argumentsThatWouldRequireCacheHash)) {
1760
            return;
1761
        }
1762
        if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError']) {
1763
            $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1764
                $GLOBALS['TYPO3_REQUEST'],
1765
                'Request parameters could not be validated (&cHash empty)',
1766
                ['code' => PageAccessFailureReasons::CACHEHASH_EMPTY]
1767
            );
1768
            throw new ImmediateResponseException($response, 1533931354);
1769
        }
1770
        $this->disableCache();
1771
        $this->getTimeTracker()->setTSlogMessage('TSFE->reqCHash(): No &cHash parameter was sent for GET vars though required so caching is disabled', 2);
1772
    }
1773
1774
    protected function setPageArguments(PageArguments $pageArguments): void
1775
    {
1776
        $this->pageArguments = $pageArguments;
1777
        $this->id = $pageArguments->getPageId();
1778
        $this->type = $pageArguments->getPageType() ?: 0;
0 ignored issues
show
Documentation Bug introduced by
It seems like $pageArguments->getPageType() ?: 0 can also be of type string. However, the property $type is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1779
        if ($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
1780
            $this->MP = (string)($pageArguments->getArguments()['MP'] ?? '');
1781
        }
1782
    }
1783
1784
    /**
1785
     * Fetches the arguments that are relevant for creating the hash base from the given PageArguments object.
1786
     * Excluded parameters are not taken into account when calculating the hash base.
1787
     *
1788
     * @param PageArguments $pageArguments
1789
     * @return array
1790
     */
1791
    protected function getRelevantParametersForCachingFromPageArguments(PageArguments $pageArguments): array
1792
    {
1793
        $queryParams = $pageArguments->getDynamicArguments();
1794
        if (!empty($queryParams) && $pageArguments->getArguments()['cHash'] ?? false) {
1795
            $queryParams['id'] = $pageArguments->getPageId();
1796
            return GeneralUtility::makeInstance(CacheHashCalculator::class)
1797
                ->getRelevantParameters(HttpUtility::buildQueryString($queryParams));
1798
        }
1799
        return [];
1800
    }
1801
1802
    /**
1803
     * See if page is in cache and get it if so
1804
     * Stores the page content in $this->content if something is found.
1805
     *
1806
     * @param ServerRequestInterface|null $request if given this is used to determine values in headerNoCache() instead of the superglobal $_SERVER
1807
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
1808
     */
1809
    public function getFromCache(ServerRequestInterface $request = null)
1810
    {
1811
        // clearing the content-variable, which will hold the pagecontent
1812
        $this->content = '';
1813
        // Unsetting the lowlevel config
1814
        $this->config = [];
1815
        $this->cacheContentFlag = false;
1816
1817
        if ($this->no_cache) {
1818
            return;
1819
        }
1820
1821
        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...
1822
            $this->tmpl = GeneralUtility::makeInstance(TemplateService::class, $this->context, null, $this);
1823
        }
1824
1825
        $pageSectionCacheContent = $this->tmpl->getCurrentPageData((int)$this->id, (string)$this->MP);
1826
        if (!is_array($pageSectionCacheContent)) {
0 ignored issues
show
introduced by
The condition is_array($pageSectionCacheContent) is always true.
Loading history...
1827
            // Nothing in the cache, we acquire an "exclusive lock" for the key now.
1828
            // We use the Registry to store this lock centrally,
1829
            // but we protect the access again with a global exclusive lock to avoid race conditions
1830
1831
            $this->acquireLock('pagesection', $this->id . '::' . $this->MP);
1832
            //
1833
            // from this point on we're the only one working on that page ($key)
1834
            //
1835
1836
            // query the cache again to see if the page data are there meanwhile
1837
            $pageSectionCacheContent = $this->tmpl->getCurrentPageData((int)$this->id, (string)$this->MP);
1838
            if (is_array($pageSectionCacheContent)) {
1839
                // we have the content, nice that some other process did the work for us already
1840
                $this->releaseLock('pagesection');
1841
            }
1842
            // We keep the lock set, because we are the ones generating the page now and filling the cache.
1843
            // This indicates that we have to release the lock later in releaseLocks()
1844
        }
1845
1846
        if (is_array($pageSectionCacheContent)) {
0 ignored issues
show
introduced by
The condition is_array($pageSectionCacheContent) is always true.
Loading history...
1847
            // 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.
1848
            // If this hash is not the same in here in this section and after page-generation, then the page will not be properly cached!
1849
            // 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.
1850
            $pageSectionCacheContent = $this->tmpl->matching($pageSectionCacheContent);
1851
            ksort($pageSectionCacheContent);
1852
            $this->all = $pageSectionCacheContent;
1853
        }
1854
1855
        // Look for page in cache only if a shift-reload is not sent to the server.
1856
        $lockHash = $this->getLockHash();
1857
        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...
1858
            // we got page section information (TypoScript), so lets see if there is also a cached version
1859
            // of this page in the pages cache.
1860
            $this->newHash = $this->getHash();
1861
            $this->getTimeTracker()->push('Cache Row');
1862
            $row = $this->getFromCache_queryRow();
1863
            if (!is_array($row)) {
0 ignored issues
show
introduced by
The condition is_array($row) is always true.
Loading history...
1864
                // nothing in the cache, we acquire an exclusive lock now
1865
                $this->acquireLock('pages', $lockHash);
1866
                //
1867
                // from this point on we're the only one working on that page ($lockHash)
1868
                //
1869
1870
                // query the cache again to see if the data are there meanwhile
1871
                $row = $this->getFromCache_queryRow();
1872
                if (is_array($row)) {
1873
                    // we have the content, nice that some other process did the work for us
1874
                    $this->releaseLock('pages');
1875
                }
1876
                // We keep the lock set, because we are the ones generating the page now and filling the cache.
1877
                // This indicates that we have to release the lock later in releaseLocks()
1878
            }
1879
            if (is_array($row)) {
0 ignored issues
show
introduced by
The condition is_array($row) is always true.
Loading history...
1880
                $this->populatePageDataFromCache($row);
1881
            }
1882
            $this->getTimeTracker()->pull();
1883
        } else {
1884
            // the user forced rebuilding the page cache or there was no pagesection information
1885
            // get a lock for the page content so other processes will not interrupt the regeneration
1886
            $this->acquireLock('pages', $lockHash);
1887
        }
1888
    }
1889
1890
    /**
1891
     * Returning the cached version of page with hash = newHash
1892
     *
1893
     * @return array Cached row, if any. Otherwise void.
1894
     */
1895
    public function getFromCache_queryRow()
0 ignored issues
show
Coding Style introduced by
Method name "TypoScriptFrontendController::getFromCache_queryRow" is not in camel caps format
Loading history...
1896
    {
1897
        $this->getTimeTracker()->push('Cache Query');
1898
        $row = $this->pageCache->get($this->newHash);
1899
        $this->getTimeTracker()->pull();
1900
        return $row;
1901
    }
1902
1903
    /**
1904
     * This method properly sets the values given from the pages cache into the corresponding
1905
     * TSFE variables. The counterpart is setPageCacheContent() where all relevant information is fetched.
1906
     * This also contains all data that could be cached, even for pages that are partially cached, as they
1907
     * have non-cacheable content still to be rendered.
1908
     *
1909
     * @see getFromCache()
1910
     * @see setPageCacheContent()
1911
     * @param array $cachedData
1912
     */
1913
    protected function populatePageDataFromCache(array $cachedData): void
1914
    {
1915
        // Call hook when a page is retrieved from cache
1916
        $_params = ['pObj' => &$this, 'cache_pages_row' => &$cachedData];
1917
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['pageLoadedFromCache'] ?? [] as $_funcRef) {
1918
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1919
        }
1920
        // Fetches the lowlevel config stored with the cached data
1921
        $this->config = $cachedData['cache_data'];
1922
        // Getting the content
1923
        $this->content = $cachedData['content'];
1924
        // Setting flag, so we know, that some cached content has been loaded
1925
        $this->cacheContentFlag = true;
1926
        $this->cacheExpires = $cachedData['expires'];
1927
        // Restore the current tags as they can be retrieved by getPageCacheTags()
1928
        $this->pageCacheTags = $cachedData['cacheTags'] ?? [];
1929
1930
        // Restore page title information, this is needed to generate the page title for
1931
        // partially cached pages.
1932
        $this->page['title'] = $cachedData['pageTitleInfo']['title'];
1933
        $this->indexedDocTitle = $cachedData['pageTitleInfo']['indexedDocTitle'];
1934
1935
        if (isset($this->config['config']['debug'])) {
1936
            $debugCacheTime = (bool)$this->config['config']['debug'];
1937
        } else {
1938
            $debugCacheTime = !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']);
1939
        }
1940
        if ($debugCacheTime) {
1941
            $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'];
1942
            $timeFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
1943
            $this->content .= LF . '<!-- Cached page generated ' . date($dateFormat . ' ' . $timeFormat, $cachedData['tstamp']) . '. Expires ' . date($dateFormat . ' ' . $timeFormat, $cachedData['expires']) . ' -->';
1944
        }
1945
    }
1946
1947
    /**
1948
     * Detecting if shift-reload has been clicked
1949
     * Will not be called if re-generation of page happens by other reasons (for instance that the page is not in cache yet!)
1950
     * Also, a backend user MUST be logged in for the shift-reload to be detected due to DoS-attack-security reasons.
1951
     *
1952
     * @param ServerRequestInterface|null $request
1953
     * @return bool If shift-reload in client browser has been clicked, disable getting cached page (and regenerate it).
1954
     */
1955
    public function headerNoCache(ServerRequestInterface $request = null)
1956
    {
1957
        if ($request instanceof ServerRequestInterface) {
1958
            $serverParams = $request->getServerParams();
1959
        } else {
1960
            $serverParams = $_SERVER;
1961
        }
1962
        $disableAcquireCacheData = false;
1963
        if ($this->isBackendUserLoggedIn()) {
1964
            if (strtolower($serverParams['HTTP_CACHE_CONTROL']) === 'no-cache' || strtolower($serverParams['HTTP_PRAGMA']) === 'no-cache') {
1965
                $disableAcquireCacheData = true;
1966
            }
1967
        }
1968
        // Call hook for possible by-pass of requiring of page cache (for recaching purpose)
1969
        $_params = ['pObj' => &$this, 'disableAcquireCacheData' => &$disableAcquireCacheData];
1970
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['headerNoCache'] ?? [] as $_funcRef) {
1971
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1972
        }
1973
        return $disableAcquireCacheData;
1974
    }
1975
1976
    /**
1977
     * Calculates the cache-hash
1978
     * This hash is unique to the template, the variables ->id, ->type, list of fe user groups, ->MP (Mount Points) and cHash array
1979
     * Used to get and later store the cached data.
1980
     *
1981
     * @return string MD5 hash of serialized hash base from createHashBase()
1982
     * @see getFromCache()
1983
     * @see getLockHash()
1984
     */
1985
    protected function getHash()
1986
    {
1987
        return md5($this->createHashBase(false));
1988
    }
1989
1990
    /**
1991
     * Calculates the lock-hash
1992
     * This hash is unique to the above hash, except that it doesn't contain the template information in $this->all.
1993
     *
1994
     * @return string MD5 hash
1995
     * @see getFromCache()
1996
     * @see getHash()
1997
     */
1998
    protected function getLockHash()
1999
    {
2000
        $lockHash = $this->createHashBase(true);
2001
        return md5($lockHash);
2002
    }
2003
2004
    /**
2005
     * Calculates the cache-hash (or the lock-hash)
2006
     * This hash is unique to the template,
2007
     * the variables ->id, ->type, list of frontend user groups,
2008
     * ->MP (Mount Points) and cHash array
2009
     * Used to get and later store the cached data.
2010
     *
2011
     * @param bool $createLockHashBase Whether to create the lock hash, which doesn't contain the "this->all" (the template information)
2012
     * @return string the serialized hash base
2013
     */
2014
    protected function createHashBase($createLockHashBase = false)
2015
    {
2016
        // Fetch the list of user groups
2017
        /** @var UserAspect $userAspect */
2018
        $userAspect = $this->context->getAspect('frontend.user');
2019
        $hashParameters = [
2020
            'id' => (int)$this->id,
2021
            'type' => (int)$this->type,
2022
            'groupIds' => (string)implode(',', $userAspect->getGroupIds()),
2023
            'MP' => (string)$this->MP,
2024
            'site' => $this->site->getIdentifier(),
2025
            // Ensure the language base is used for the hash base calculation as well, otherwise TypoScript and page-related rendering
2026
            // is not cached properly as we don't have any language-specific conditions anymore
2027
            'siteBase' => (string)$this->language->getBase(),
2028
            // additional variation trigger for static routes
2029
            'staticRouteArguments' => $this->pageArguments->getStaticArguments(),
2030
            // dynamic route arguments (if route was resolved)
2031
            'dynamicArguments' => $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments),
2032
        ];
2033
        // Include the template information if we shouldn't create a lock hash
2034
        if (!$createLockHashBase) {
2035
            $hashParameters['all'] = $this->all;
2036
        }
2037
        // Call hook to influence the hash calculation
2038
        $_params = [
2039
            'hashParameters' => &$hashParameters,
2040
            'createLockHashBase' => $createLockHashBase
2041
        ];
2042
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['createHashBase'] ?? [] as $_funcRef) {
2043
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2044
        }
2045
        return serialize($hashParameters);
2046
    }
2047
2048
    /**
2049
     * Checks if config-array exists already but if not, gets it
2050
     *
2051
     * @throws ServiceUnavailableException
2052
     */
2053
    public function getConfigArray()
2054
    {
2055
        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...
2056
            $this->tmpl = GeneralUtility::makeInstance(TemplateService::class, $this->context, null, $this);
2057
        }
2058
2059
        // 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
2060
        if (empty($this->config) || $this->isINTincScript() || $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing')) {
2061
            $timeTracker = $this->getTimeTracker();
2062
            $timeTracker->push('Parse template');
2063
            // Start parsing the TS template. Might return cached version.
2064
            $this->tmpl->start($this->rootLine);
2065
            $timeTracker->pull();
2066
            // At this point we have a valid pagesection_cache (generated in $this->tmpl->start()),
2067
            // so let all other processes proceed now. (They are blocked at the pagessection_lock in getFromCache())
2068
            $this->releaseLock('pagesection');
2069
            if ($this->tmpl->loaded) {
2070
                $timeTracker->push('Setting the config-array');
2071
                // toplevel - objArrayName
2072
                $typoScriptPageTypeName = $this->tmpl->setup['types.'][$this->type];
2073
                $this->sPre = $typoScriptPageTypeName;
2074
                $this->pSetup = $this->tmpl->setup[$typoScriptPageTypeName . '.'];
2075
                if (!is_array($this->pSetup)) {
2076
                    $message = 'The page is not configured! [type=' . $this->type . '][' . $typoScriptPageTypeName . '].';
2077
                    $this->logger->alert($message);
2078
                    try {
2079
                        $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
2080
                            $GLOBALS['TYPO3_REQUEST'],
2081
                            $message,
2082
                            ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED]
2083
                        );
2084
                        throw new ImmediateResponseException($response, 1533931374);
2085
                    } catch (PageNotFoundException $e) {
2086
                        $explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.';
2087
                        throw new ServiceUnavailableException($message . ' ' . $explanation, 1294587217);
2088
                    }
2089
                } else {
2090
                    if (!isset($this->config['config'])) {
2091
                        $this->config['config'] = [];
2092
                    }
2093
                    // Filling the config-array, first with the main "config." part
2094
                    if (is_array($this->tmpl->setup['config.'])) {
2095
                        ArrayUtility::mergeRecursiveWithOverrule($this->tmpl->setup['config.'], $this->config['config']);
2096
                        $this->config['config'] = $this->tmpl->setup['config.'];
2097
                    }
2098
                    // override it with the page/type-specific "config."
2099
                    if (is_array($this->pSetup['config.'])) {
2100
                        ArrayUtility::mergeRecursiveWithOverrule($this->config['config'], $this->pSetup['config.']);
2101
                    }
2102
                    // Set default values for removeDefaultJS and inlineStyle2TempFile so CSS and JS are externalized if compatversion is higher than 4.0
2103
                    if (!isset($this->config['config']['removeDefaultJS'])) {
2104
                        $this->config['config']['removeDefaultJS'] = 'external';
2105
                    }
2106
                    if (!isset($this->config['config']['inlineStyle2TempFile'])) {
2107
                        $this->config['config']['inlineStyle2TempFile'] = 1;
2108
                    }
2109
2110
                    if (!isset($this->config['config']['compressJs'])) {
2111
                        $this->config['config']['compressJs'] = 0;
2112
                    }
2113
                    // Rendering charset of HTML page.
2114
                    if (isset($this->config['config']['metaCharset']) && $this->config['config']['metaCharset'] !== 'utf-8') {
2115
                        $this->metaCharset = $this->config['config']['metaCharset'];
2116
                    }
2117
                    // Setting default cache_timeout
2118
                    if (isset($this->config['config']['cache_period'])) {
2119
                        $this->set_cache_timeout_default((int)$this->config['config']['cache_period']);
2120
                    }
2121
2122
                    // Processing for the config_array:
2123
                    $this->config['rootLine'] = $this->tmpl->rootLine;
2124
                    // Class for render Header and Footer parts
2125
                    if ($this->pSetup['pageHeaderFooterTemplateFile']) {
2126
                        try {
2127
                            $file = GeneralUtility::makeInstance(FilePathSanitizer::class)
2128
                                ->sanitize((string)$this->pSetup['pageHeaderFooterTemplateFile']);
2129
                            $this->pageRenderer->setTemplateFile($file);
2130
                        } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
2131
                            // do nothing
2132
                        }
2133
                    }
2134
                }
2135
                $timeTracker->pull();
2136
            } else {
2137
                $message = 'No TypoScript template found!';
2138
                $this->logger->alert($message);
2139
                try {
2140
                    $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
2141
                        $GLOBALS['TYPO3_REQUEST'],
2142
                        $message,
2143
                        ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_FOUND]
2144
                    );
2145
                    throw new ImmediateResponseException($response, 1533931380);
2146
                } catch (ServiceUnavailableException $e) {
2147
                    throw new ServiceUnavailableException($message, 1294587218);
2148
                }
2149
            }
2150
        }
2151
2152
        // No cache
2153
        // Set $this->no_cache TRUE if the config.no_cache value is set!
2154
        if ($this->config['config']['no_cache']) {
2155
            $this->set_no_cache('config.no_cache is set', true);
2156
        }
2157
2158
        // Auto-configure settings when a site is configured
2159
        $this->config['config']['absRefPrefix'] = $this->config['config']['absRefPrefix'] ?? 'auto';
2160
2161
        // Hook for postProcessing the configuration array
2162
        $params = ['config' => &$this->config['config']];
2163
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['configArrayPostProc'] ?? [] as $funcRef) {
2164
            GeneralUtility::callUserFunction($funcRef, $params, $this);
2165
        }
2166
    }
2167
2168
    /********************************************
2169
     *
2170
     * Further initialization and data processing
2171
     *
2172
     *******************************************/
2173
2174
    /**
2175
     * Setting the language key that will be used by the current page.
2176
     * 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.
2177
     *
2178
     * @internal
2179
     */
2180
    public function settingLanguage()
2181
    {
2182
        $_params = [];
2183
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_preProcess'] ?? [] as $_funcRef) {
2184
            $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
2185
            GeneralUtility::callUserFunction($_funcRef, $_params, $ref);
2186
        }
2187
2188
        // Get values from site language
2189
        $languageAspect = LanguageAspectFactory::createFromSiteLanguage($this->language);
2190
2191
        $languageId = $languageAspect->getId();
2192
        $languageContentId = $languageAspect->getContentId();
2193
2194
        // If sys_language_uid is set to another language than default:
2195
        if ($languageAspect->getId() > 0) {
2196
            // check whether a shortcut is overwritten by a translated page
2197
            // we can only do this now, as this is the place where we get
2198
            // to know about translations
2199
            $this->checkTranslatedShortcut($languageAspect->getId());
2200
            // Request the overlay record for the sys_language_uid:
2201
            $olRec = $this->sys_page->getPageOverlay($this->id, $languageAspect->getId());
2202
            if (empty($olRec)) {
2203
                // If requested translation is not available:
2204
                if (GeneralUtility::hideIfNotTranslated($this->page['l18n_cfg'])) {
2205
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
2206
                        $GLOBALS['TYPO3_REQUEST'],
2207
                        'Page is not available in the requested language.',
2208
                        ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE]
2209
                    );
2210
                    throw new ImmediateResponseException($response, 1533931388);
2211
                }
2212
                switch ((string)$languageAspect->getLegacyLanguageMode()) {
2213
                    case 'strict':
2214
                        $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
2215
                            $GLOBALS['TYPO3_REQUEST'],
2216
                            'Page is not available in the requested language (strict).',
2217
                            ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE_STRICT_MODE]
2218
                        );
2219
                        throw new ImmediateResponseException($response, 1533931395);
2220
                        break;
2221
                    case 'fallback':
2222
                    case 'content_fallback':
2223
                        // Setting content uid (but leaving the sys_language_uid) when a content_fallback
2224
                        // value was found.
2225
                        foreach ($languageAspect->getFallbackChain() ?? [] as $orderValue) {
2226
                            if ($orderValue === '0' || $orderValue === 0 || $orderValue === '') {
2227
                                $languageContentId = 0;
2228
                                break;
2229
                            }
2230
                            if (MathUtility::canBeInterpretedAsInteger($orderValue) && !empty($this->sys_page->getPageOverlay($this->id, (int)$orderValue))) {
2231
                                $languageContentId = (int)$orderValue;
2232
                                break;
2233
                            }
2234
                            if ($orderValue === 'pageNotFound') {
2235
                                // The existing fallbacks have not been found, but instead of continuing
2236
                                // page rendering with default language, a "page not found" message should be shown
2237
                                // instead.
2238
                                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
2239
                                    $GLOBALS['TYPO3_REQUEST'],
2240
                                    'Page is not available in the requested language (fallbacks did not apply).',
2241
                                    ['code' => PageAccessFailureReasons::LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE]
2242
                                );
2243
                                throw new ImmediateResponseException($response, 1533931402);
2244
                            }
2245
                        }
2246
                        break;
2247
                    case 'ignore':
2248
                        $languageContentId = $languageAspect->getId();
2249
                        break;
2250
                    default:
2251
                        // Default is that everything defaults to the default language...
2252
                        $languageId = ($languageContentId = 0);
2253
                }
2254
            }
2255
2256
            // Define the language aspect again now
2257
            $languageAspect = GeneralUtility::makeInstance(
2258
                LanguageAspect::class,
2259
                $languageId,
2260
                $languageContentId,
2261
                $languageAspect->getOverlayType(),
2262
                $languageAspect->getFallbackChain()
2263
            );
2264
2265
            // Setting sys_language if an overlay record was found (which it is only if a language is used)
2266
            // We'll do this every time since the language aspect might have changed now
2267
            // Doing this ensures that page properties like the page title are returned in the correct language
2268
            $this->page = $this->sys_page->getPageOverlay($this->page, $languageAspect->getContentId());
2269
2270
            // Update SYS_LASTCHANGED for localized page record
2271
            $this->setRegisterValueForSysLastChanged($this->page);
2272
        }
2273
2274
        // Set the language aspect
2275
        $this->context->setAspect('language', $languageAspect);
2276
2277
        // Setting sys_language_uid inside sys-page by creating a new page repository
2278
        $this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context);
2279
        // If default language is not available:
2280
        if ((!$languageAspect->getContentId() || !$languageAspect->getId())
2281
            && GeneralUtility::hideIfDefaultLanguage($this->page['l18n_cfg'] ?? 0)
2282
        ) {
2283
            $message = 'Page is not available in default language.';
2284
            $this->logger->error($message);
2285
            $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
2286
                $GLOBALS['TYPO3_REQUEST'],
2287
                $message,
2288
                ['code' => PageAccessFailureReasons::LANGUAGE_DEFAULT_NOT_AVAILABLE]
2289
            );
2290
            throw new ImmediateResponseException($response, 1533931423);
2291
        }
2292
2293
        if ($languageAspect->getId() > 0) {
2294
            $this->updateRootLinesWithTranslations();
2295
        }
2296
2297
        $_params = [];
2298
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_postProcess'] ?? [] as $_funcRef) {
2299
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2300
        }
2301
    }
2302
2303
    /**
2304
     * Updating content of the two rootLines IF the language key is set!
2305
     */
2306
    protected function updateRootLinesWithTranslations()
2307
    {
2308
        try {
2309
            $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
2310
        } catch (RootLineException $e) {
2311
            $this->rootLine = [];
2312
        }
2313
        $this->tmpl->updateRootlineData($this->rootLine);
2314
    }
2315
2316
    /**
2317
     * Setting locale for frontend rendering
2318
     * @deprecated will be removed in TYPO3 v11.0. Use Locales::setSystemLocaleFromSiteLanguage() instead.
2319
     */
2320
    public function settingLocale()
2321
    {
2322
        trigger_error('TSFE->settingLocale() will be removed in TYPO3 v11.0. Use Locales::setSystemLocaleFromSiteLanguage() instead, as this functionality is independent of TSFE.', E_USER_DEPRECATED);
2323
        if ($this->language->getLocale() && !Locales::setSystemLocaleFromSiteLanguage($this->language)) {
2324
            $this->getTimeTracker()->setTSlogMessage('Locale "' . htmlspecialchars($this->language->getLocale()) . '" not found.', 3);
2325
        }
2326
    }
2327
2328
    /**
2329
     * Checks whether a translated shortcut page has a different shortcut
2330
     * target than the original language page.
2331
     * If that is the case, things get corrected to follow that alternative
2332
     * shortcut
2333
     * @param int $languageId
2334
     */
2335
    protected function checkTranslatedShortcut(int $languageId)
2336
    {
2337
        if (!is_null($this->originalShortcutPage)) {
2338
            $originalShortcutPageOverlay = $this->sys_page->getPageOverlay($this->originalShortcutPage['uid'], $languageId);
2339
            if (!empty($originalShortcutPageOverlay['shortcut']) && $originalShortcutPageOverlay['shortcut'] != $this->id) {
2340
                // the translation of the original shortcut page has a different shortcut target!
2341
                // set the correct page and id
2342
                $shortcut = $this->sys_page->getPageShortcut($originalShortcutPageOverlay['shortcut'], $originalShortcutPageOverlay['shortcut_mode'], $originalShortcutPageOverlay['uid']);
2343
                $this->id = ($this->contentPid = $shortcut['uid']);
2344
                $this->page = $this->sys_page->getPage($this->id);
2345
                // Fix various effects on things like menus f.e.
2346
                $this->fetch_the_id();
2347
                $this->tmpl->rootLine = array_reverse($this->rootLine);
2348
            }
2349
        }
2350
    }
2351
2352
    /**
2353
     * Calculates and sets the internal linkVars based upon the current request parameters
2354
     * and the setting "config.linkVars".
2355
     *
2356
     * @param array $queryParams $_GET (usually called with a PSR-7 $request->getQueryParams())
2357
     */
2358
    public function calculateLinkVars(array $queryParams)
2359
    {
2360
        $this->linkVars = '';
2361
        if (empty($this->config['config']['linkVars'])) {
2362
            return;
2363
        }
2364
2365
        $linkVars = $this->splitLinkVarsString((string)$this->config['config']['linkVars']);
2366
2367
        if (empty($linkVars)) {
2368
            return;
2369
        }
2370
        foreach ($linkVars as $linkVar) {
2371
            $test = $value = '';
2372
            if (preg_match('/^(.*)\\((.+)\\)$/', $linkVar, $match)) {
2373
                $linkVar = trim($match[1]);
2374
                $test = trim($match[2]);
2375
            }
2376
2377
            $keys = explode('|', $linkVar);
2378
            $numberOfLevels = count($keys);
2379
            $rootKey = trim($keys[0]);
2380
            if (!isset($queryParams[$rootKey])) {
2381
                continue;
2382
            }
2383
            $value = $queryParams[$rootKey];
2384
            for ($i = 1; $i < $numberOfLevels; $i++) {
2385
                $currentKey = trim($keys[$i]);
2386
                if (isset($value[$currentKey])) {
2387
                    $value = $value[$currentKey];
2388
                } else {
2389
                    $value = false;
2390
                    break;
2391
                }
2392
            }
2393
            if ($value !== false) {
2394
                $parameterName = $keys[0];
2395
                for ($i = 1; $i < $numberOfLevels; $i++) {
2396
                    $parameterName .= '[' . $keys[$i] . ']';
2397
                }
2398
                if (!is_array($value)) {
2399
                    $temp = rawurlencode($value);
2400
                    if ($test !== '' && !$this->isAllowedLinkVarValue($temp, $test)) {
2401
                        // Error: This value was not allowed for this key
2402
                        continue;
2403
                    }
2404
                    $value = '&' . $parameterName . '=' . $temp;
2405
                } else {
2406
                    if ($test !== '' && $test !== 'array') {
2407
                        // Error: This key must not be an array!
2408
                        continue;
2409
                    }
2410
                    $value = HttpUtility::buildQueryString([$parameterName => $value], '&');
2411
                }
2412
                $this->linkVars .= $value;
2413
            }
2414
        }
2415
    }
2416
2417
    /**
2418
     * Split the link vars string by "," but not if the "," is inside of braces
2419
     *
2420
     * @param string $string
2421
     *
2422
     * @return array
2423
     */
2424
    protected function splitLinkVarsString(string $string): array
2425
    {
2426
        $tempCommaReplacementString = '###KASPER###';
2427
2428
        // replace every "," wrapped in "()" by a "unique" string
2429
        $string = preg_replace_callback('/\((?>[^()]|(?R))*\)/', function ($result) use ($tempCommaReplacementString) {
2430
            return str_replace(',', $tempCommaReplacementString, $result[0]);
2431
        }, $string);
2432
2433
        $string = GeneralUtility::trimExplode(',', $string);
2434
2435
        // replace all "unique" strings back to ","
2436
        return str_replace($tempCommaReplacementString, ',', $string);
2437
    }
2438
2439
    /**
2440
     * Checks if the value defined in "config.linkVars" contains an allowed value.
2441
     * Otherwise, return FALSE which means the value will not be added to any links.
2442
     *
2443
     * @param string $haystack The string in which to find $needle
2444
     * @param string $needle The string to find in $haystack
2445
     * @return bool Returns TRUE if $needle matches or is found in $haystack
2446
     */
2447
    protected function isAllowedLinkVarValue(string $haystack, string $needle): bool
2448
    {
2449
        $isAllowed = false;
2450
        // Integer
2451
        if ($needle === 'int' || $needle === 'integer') {
2452
            if (MathUtility::canBeInterpretedAsInteger($haystack)) {
2453
                $isAllowed = true;
2454
            }
2455
        } elseif (preg_match('/^\\/.+\\/[imsxeADSUXu]*$/', $needle)) {
2456
            // Regular expression, only "//" is allowed as delimiter
2457
            if (@preg_match($needle, $haystack)) {
2458
                $isAllowed = true;
2459
            }
2460
        } elseif (strpos($needle, '-') !== false) {
2461
            // Range
2462
            if (MathUtility::canBeInterpretedAsInteger($haystack)) {
2463
                $range = explode('-', $needle);
2464
                if ($range[0] <= $haystack && $range[1] >= $haystack) {
2465
                    $isAllowed = true;
2466
                }
2467
            }
2468
        } elseif (strpos($needle, '|') !== false) {
2469
            // List
2470
            // Trim the input
2471
            $haystack = str_replace(' ', '', $haystack);
2472
            if (strpos('|' . $needle . '|', '|' . $haystack . '|') !== false) {
2473
                $isAllowed = true;
2474
            }
2475
        } elseif ((string)$needle === (string)$haystack) {
2476
            // String comparison
2477
            $isAllowed = true;
2478
        }
2479
        return $isAllowed;
2480
    }
2481
2482
    /**
2483
     * Returns URI of target page, if the current page is an overlaid mountpoint.
2484
     *
2485
     * If the current page is of type mountpoint and should be overlaid with the contents of the mountpoint page
2486
     * and is accessed directly, the user will be redirected to the mountpoint context.
2487
     * @internal
2488
     * @param ServerRequestInterface $request
2489
     * @return string|null
2490
     */
2491
    public function getRedirectUriForMountPoint(ServerRequestInterface $request): ?string
2492
    {
2493
        if (!empty($this->originalMountPointPage) && (int)$this->originalMountPointPage['doktype'] === PageRepository::DOKTYPE_MOUNTPOINT) {
2494
            return $this->getUriToCurrentPageForRedirect($request);
2495
        }
2496
2497
        return null;
2498
    }
2499
2500
    /**
2501
     * Returns URI of target page, if the current page is a Shortcut.
2502
     *
2503
     * If the current page is of type shortcut and accessed directly via its URL,
2504
     * the user will be redirected to shortcut target.
2505
     *
2506
     * @internal
2507
     * @param ServerRequestInterface $request
2508
     * @return string|null
2509
     */
2510
    public function getRedirectUriForShortcut(ServerRequestInterface $request): ?string
2511
    {
2512
        if (!empty($this->originalShortcutPage) && $this->originalShortcutPage['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
2513
            return $this->getUriToCurrentPageForRedirect($request);
2514
        }
2515
2516
        return null;
2517
    }
2518
2519
    /**
2520
     * Instantiate \TYPO3\CMS\Frontend\ContentObject to generate the correct target URL
2521
     *
2522
     * @param ServerRequestInterface $request
2523
     * @return string
2524
     */
2525
    protected function getUriToCurrentPageForRedirect(ServerRequestInterface $request): string
2526
    {
2527
        $this->calculateLinkVars($request->getQueryParams());
2528
        $parameter = $this->page['uid'];
2529
        if ($this->type && MathUtility::canBeInterpretedAsInteger($this->type)) {
2530
            $parameter .= ',' . $this->type;
2531
        }
2532
        return GeneralUtility::makeInstance(ContentObjectRenderer::class, $this)->typoLink_URL([
2533
            'parameter' => $parameter,
2534
            'addQueryString' => true,
2535
            'addQueryString.' => ['exclude' => 'id'],
2536
            // ensure absolute URL is generated when having a valid Site
2537
            'forceAbsoluteUrl' => $GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface
2538
                && $GLOBALS['TYPO3_REQUEST']->getAttribute('site') instanceof Site
2539
        ]);
2540
    }
2541
2542
    /********************************************
2543
     *
2544
     * Page generation; cache handling
2545
     *
2546
     *******************************************/
2547
    /**
2548
     * Returns TRUE if the page should be generated.
2549
     * That is if no URL handler is active and the cacheContentFlag is not set.
2550
     *
2551
     * @return bool
2552
     */
2553
    public function isGeneratePage()
2554
    {
2555
        return !$this->cacheContentFlag;
2556
    }
2557
2558
    /**
2559
     * Set cache content to $this->content
2560
     */
2561
    protected function realPageCacheContent()
2562
    {
2563
        // seconds until a cached page is too old
2564
        $cacheTimeout = $this->get_cache_timeout();
2565
        $timeOutTime = $GLOBALS['EXEC_TIME'] + $cacheTimeout;
2566
        $usePageCache = true;
2567
        // Hook for deciding whether page cache should be written to the cache backend or not
2568
        // NOTE: as hooks are called in a loop, the last hook will have the final word (however each
2569
        // hook receives the current status of the $usePageCache flag)
2570
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['usePageCache'] ?? [] as $className) {
2571
            $usePageCache = GeneralUtility::makeInstance($className)->usePageCache($this, $usePageCache);
2572
        }
2573
        // Write the page to cache, if necessary
2574
        if ($usePageCache) {
2575
            $this->setPageCacheContent($this->content, $this->config, $timeOutTime);
2576
        }
2577
        // Hook for cache post processing (eg. writing static files!)
2578
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['insertPageIncache'] ?? [] as $className) {
2579
            GeneralUtility::makeInstance($className)->insertPageIncache($this, $timeOutTime);
2580
        }
2581
    }
2582
2583
    /**
2584
     * Sets cache content; Inserts the content string into the cache_pages cache.
2585
     *
2586
     * @param string $content The content to store in the HTML field of the cache table
2587
     * @param mixed $data The additional cache_data array, fx. $this->config
2588
     * @param int $expirationTstamp Expiration timestamp
2589
     * @see realPageCacheContent()
2590
     */
2591
    protected function setPageCacheContent($content, $data, $expirationTstamp)
2592
    {
2593
        $cacheData = [
2594
            'identifier' => $this->newHash,
2595
            'page_id' => $this->id,
2596
            'content' => $content,
2597
            'cache_data' => $data,
2598
            'expires' => $expirationTstamp,
2599
            'tstamp' => $GLOBALS['EXEC_TIME'],
2600
            'pageTitleInfo' => [
2601
                'title' => $this->page['title'],
2602
                'indexedDocTitle' => $this->indexedDocTitle
2603
            ]
2604
        ];
2605
        $this->cacheExpires = $expirationTstamp;
2606
        $this->pageCacheTags[] = 'pageId_' . $cacheData['page_id'];
2607
        // Respect the page cache when content of pid is shown
2608
        if ($this->id !== $this->contentPid) {
0 ignored issues
show
introduced by
The condition $this->id !== $this->contentPid is always true.
Loading history...
2609
            $this->pageCacheTags[] = 'pageId_' . $this->contentPid;
2610
        }
2611
        if (!empty($this->page['cache_tags'])) {
2612
            $tags = GeneralUtility::trimExplode(',', $this->page['cache_tags'], true);
2613
            $this->pageCacheTags = array_merge($this->pageCacheTags, $tags);
2614
        }
2615
        // Add the cache themselves as well, because they are fetched by getPageCacheTags()
2616
        $cacheData['cacheTags'] = $this->pageCacheTags;
2617
        $this->pageCache->set($this->newHash, $cacheData, $this->pageCacheTags, $expirationTstamp - $GLOBALS['EXEC_TIME']);
2618
    }
2619
2620
    /**
2621
     * Clears cache content (for $this->newHash)
2622
     */
2623
    public function clearPageCacheContent()
2624
    {
2625
        $this->pageCache->remove($this->newHash);
2626
    }
2627
2628
    /**
2629
     * Sets sys last changed
2630
     * 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.
2631
     *
2632
     * @see ContentObjectRenderer::lastChanged()
2633
     */
2634
    protected function setSysLastChanged()
2635
    {
2636
        // We only update the info if browsing the live workspace
2637
        if ($this->page['SYS_LASTCHANGED'] < (int)$this->register['SYS_LASTCHANGED'] && !$this->doWorkspacePreview()) {
2638
            $connection = GeneralUtility::makeInstance(ConnectionPool::class)
2639
                ->getConnectionForTable('pages');
2640
            $pageId = $this->page['_PAGES_OVERLAY_UID'] ?? $this->id;
2641
            $connection->update(
2642
                'pages',
2643
                [
2644
                    'SYS_LASTCHANGED' => (int)$this->register['SYS_LASTCHANGED']
2645
                ],
2646
                [
2647
                    'uid' => (int)$pageId
2648
                ]
2649
            );
2650
        }
2651
    }
2652
2653
    /**
2654
     * Set the SYS_LASTCHANGED register value, is also called when a translated page is in use,
2655
     * so the register reflects the state of the translated page, not the page in the default language.
2656
     *
2657
     * @param array $page
2658
     * @internal
2659
     */
2660
    protected function setRegisterValueForSysLastChanged(array $page): void
2661
    {
2662
        $this->register['SYS_LASTCHANGED'] = (int)$page['tstamp'];
2663
        if ($this->register['SYS_LASTCHANGED'] < (int)$page['SYS_LASTCHANGED']) {
2664
            $this->register['SYS_LASTCHANGED'] = (int)$page['SYS_LASTCHANGED'];
2665
        }
2666
    }
2667
2668
    /**
2669
     * Release pending locks
2670
     *
2671
     * @internal
2672
     */
2673
    public function releaseLocks()
2674
    {
2675
        $this->releaseLock('pagesection');
2676
        $this->releaseLock('pages');
2677
    }
2678
2679
    /**
2680
     * Adds tags to this page's cache entry, you can then f.e. remove cache
2681
     * entries by tag
2682
     *
2683
     * @param array $tags An array of tag
2684
     */
2685
    public function addCacheTags(array $tags)
2686
    {
2687
        $this->pageCacheTags = array_merge($this->pageCacheTags, $tags);
2688
    }
2689
2690
    /**
2691
     * @return array
2692
     */
2693
    public function getPageCacheTags(): array
2694
    {
2695
        return $this->pageCacheTags;
2696
    }
2697
2698
    /********************************************
2699
     *
2700
     * Page generation; rendering and inclusion
2701
     *
2702
     *******************************************/
2703
    /**
2704
     * Does some processing BEFORE the page content is generated / built.
2705
     */
2706
    public function generatePage_preProcessing()
0 ignored issues
show
Coding Style introduced by
Method name "TypoScriptFrontendController::generatePage_preProcessing" is not in camel caps format
Loading history...
2707
    {
2708
        // Same codeline as in getFromCache(). But $this->all has been changed by
2709
        // \TYPO3\CMS\Core\TypoScript\TemplateService::start() in the meantime, so this must be called again!
2710
        $this->newHash = $this->getHash();
2711
2712
        // Used as a safety check in case a PHP script is falsely disabling $this->no_cache during page generation.
2713
        $this->no_cacheBeforePageGen = $this->no_cache;
2714
    }
2715
2716
    /**
2717
     * Check the value of "content_from_pid" of the current page record, and see if the current request
2718
     * should actually show content from another page.
2719
     *
2720
     * By using $TSFE->getPageAndRootline() on the cloned object, all rootline restrictions (extendToSubPages)
2721
     * are evaluated as well.
2722
     *
2723
     * @return int the current page ID or another one if resolved properly - usually set to $this->contentPid
2724
     */
2725
    protected function resolveContentPid(): int
2726
    {
2727
        if (!isset($this->page['content_from_pid']) || empty($this->page['content_from_pid'])) {
2728
            return (int)$this->id;
2729
        }
2730
        // make REAL copy of TSFE object - not reference!
2731
        $temp_copy_TSFE = clone $this;
2732
        // 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!
2733
        $temp_copy_TSFE->id = $this->page['content_from_pid'];
2734
        $temp_copy_TSFE->MP = '';
2735
        $temp_copy_TSFE->getPageAndRootline();
2736
        return (int)$temp_copy_TSFE->id;
2737
    }
2738
    /**
2739
     * Sets up TypoScript "config." options and set properties in $TSFE.
2740
     *
2741
     * @param ServerRequestInterface $request
2742
     */
2743
    public function preparePageContentGeneration(ServerRequestInterface $request)
2744
    {
2745
        $this->getTimeTracker()->push('Prepare page content generation');
2746
        $this->contentPid = $this->resolveContentPid();
2747
        // Global vars...
2748
        $this->indexedDocTitle = $this->page['title'] ?? null;
2749
        // Base url:
2750
        if (isset($this->config['config']['baseURL'])) {
2751
            $this->baseUrl = $this->config['config']['baseURL'];
2752
        }
2753
        // Internal and External target defaults
2754
        $this->intTarget = (string)($this->config['config']['intTarget'] ?? '');
2755
        $this->extTarget = (string)($this->config['config']['extTarget'] ?? '');
2756
        $this->fileTarget = (string)($this->config['config']['fileTarget'] ?? '');
2757
        $this->spamProtectEmailAddresses = $this->config['config']['spamProtectEmailAddresses'] ?? 0;
2758
        if ($this->spamProtectEmailAddresses !== 'ascii') {
2759
            $this->spamProtectEmailAddresses = MathUtility::forceIntegerInRange($this->spamProtectEmailAddresses, -10, 10, 0);
2760
        }
2761
        // calculate the absolute path prefix
2762
        if (!empty($this->config['config']['absRefPrefix'])) {
2763
            $absRefPrefix = trim($this->config['config']['absRefPrefix']);
2764
            if ($absRefPrefix === 'auto') {
2765
                $this->absRefPrefix = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
2766
            } else {
2767
                $this->absRefPrefix = $absRefPrefix;
2768
            }
2769
        } else {
2770
            $this->absRefPrefix = '';
2771
        }
2772
        $this->ATagParams = trim($this->config['config']['ATagParams'] ?? '') ? ' ' . trim($this->config['config']['ATagParams']) : '';
2773
        $this->initializeSearchWordData($request->getParsedBody()['sword_list'] ?? $request->getQueryParams()['sword_list'] ?? null);
2774
        // linkVars
2775
        $this->calculateLinkVars($request->getQueryParams());
2776
        // Setting XHTML-doctype from doctype
2777
        if (!isset($this->config['config']['xhtmlDoctype']) || !$this->config['config']['xhtmlDoctype']) {
2778
            $this->config['config']['xhtmlDoctype'] = $this->config['config']['doctype'] ?? '';
2779
        }
2780
        if ($this->config['config']['xhtmlDoctype']) {
2781
            $this->xhtmlDoctype = $this->config['config']['xhtmlDoctype'];
2782
            // Checking XHTML-docytpe
2783
            switch ((string)$this->config['config']['xhtmlDoctype']) {
2784
                case 'xhtml_trans':
2785
                case 'xhtml_strict':
2786
                    $this->xhtmlVersion = 100;
2787
                    break;
2788
                case 'xhtml_basic':
2789
                    $this->xhtmlVersion = 105;
2790
                    break;
2791
                case 'xhtml_11':
2792
                case 'xhtml+rdfa_10':
2793
                    $this->xhtmlVersion = 110;
2794
                    break;
2795
                default:
2796
                    $this->pageRenderer->setRenderXhtml(false);
2797
                    $this->xhtmlDoctype = '';
2798
                    $this->xhtmlVersion = 0;
2799
            }
2800
        } else {
2801
            $this->pageRenderer->setRenderXhtml(false);
2802
        }
2803
2804
        // Global content object
2805
        $this->newCObj();
2806
        $this->getTimeTracker()->pull();
2807
    }
2808
2809
    /**
2810
     * Fills the sWordList property and builds the regular expression in TSFE that can be used to split
2811
     * strings by the submitted search words.
2812
     *
2813
     * @param mixed $searchWords - usually an array, but we can't be sure (yet)
2814
     * @see sWordList
2815
     * @see sWordRegEx
2816
     */
2817
    protected function initializeSearchWordData($searchWords)
2818
    {
2819
        $this->sWordRegEx = '';
2820
        $this->sWordList = $searchWords ?? '';
2821
        if (is_array($this->sWordList)) {
2822
            $space = !empty($this->config['config']['sword_standAlone'] ?? null) ? '[[:space:]]' : '';
2823
            $regexpParts = [];
2824
            foreach ($this->sWordList as $val) {
2825
                if (trim($val) !== '') {
2826
                    $regexpParts[] = $space . preg_quote($val, '/') . $space;
2827
                }
2828
            }
2829
            $this->sWordRegEx = implode('|', $regexpParts);
2830
        }
2831
    }
2832
2833
    /**
2834
     * Does processing of the content after the page content was generated.
2835
     *
2836
     * This includes caching the page, indexing the page (if configured) and setting sysLastChanged
2837
     */
2838
    public function generatePage_postProcessing()
0 ignored issues
show
Coding Style introduced by
Method name "TypoScriptFrontendController::generatePage_postProcessing" is not in camel caps format
Loading history...
2839
    {
2840
        $this->setAbsRefPrefix();
2841
        // 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.
2842
        if ($this->no_cacheBeforePageGen) {
2843
            $this->set_no_cache('no_cache has been set before the page was generated - safety check', true);
2844
        }
2845
        // Hook for post-processing of page content cached/non-cached:
2846
        $_params = ['pObj' => &$this];
2847
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-all'] ?? [] as $_funcRef) {
2848
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2849
        }
2850
        // Processing if caching is enabled:
2851
        if (!$this->no_cache) {
2852
            // Hook for post-processing of page content before being cached:
2853
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-cached'] ?? [] as $_funcRef) {
2854
                GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2855
            }
2856
        }
2857
        // Convert char-set for output: (should be BEFORE indexing of the content (changed 22/4 2005)),
2858
        // because otherwise indexed search might convert from the wrong charset!
2859
        // One thing is that the charset mentioned in the HTML header would be wrong since the output charset (metaCharset)
2860
        // has not been converted to from utf-8. And indexed search will internally convert from metaCharset
2861
        // to utf-8 so the content MUST be in metaCharset already!
2862
        $this->content = $this->convOutputCharset($this->content);
2863
        // Hook for indexing pages
2864
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['pageIndexing'] ?? [] as $className) {
2865
            GeneralUtility::makeInstance($className)->hook_indexContent($this);
2866
        }
2867
        // Storing for cache:
2868
        if (!$this->no_cache) {
2869
            $this->realPageCacheContent();
2870
        }
2871
        // Sets sys-last-change:
2872
        $this->setSysLastChanged();
2873
    }
2874
2875
    /**
2876
     * Generate the page title, can be called multiple times,
2877
     * as PageTitleProvider might have been modified by an uncached plugin etc.
2878
     *
2879
     * @return string the generated page title
2880
     */
2881
    public function generatePageTitle(): string
2882
    {
2883
        $pageTitleSeparator = '';
2884
2885
        // Check for a custom pageTitleSeparator, and perform stdWrap on it
2886
        if (isset($this->config['config']['pageTitleSeparator']) && $this->config['config']['pageTitleSeparator'] !== '') {
2887
            $pageTitleSeparator = $this->config['config']['pageTitleSeparator'];
2888
2889
            if (isset($this->config['config']['pageTitleSeparator.']) && is_array($this->config['config']['pageTitleSeparator.'])) {
2890
                $pageTitleSeparator = $this->cObj->stdWrap($pageTitleSeparator, $this->config['config']['pageTitleSeparator.']);
2891
            } else {
2892
                $pageTitleSeparator .= ' ';
2893
            }
2894
        }
2895
2896
        $titleProvider = GeneralUtility::makeInstance(PageTitleProviderManager::class);
2897
        $pageTitle = $titleProvider->getTitle();
2898
2899
        if ($pageTitle !== '') {
2900
            $this->indexedDocTitle = $pageTitle;
2901
        }
2902
2903
        $titleTagContent = $this->printTitle(
2904
            $pageTitle,
2905
            (bool)($this->config['config']['noPageTitle'] ?? false),
2906
            (bool)($this->config['config']['pageTitleFirst'] ?? false),
2907
            $pageTitleSeparator
2908
        );
2909
        // stdWrap around the title tag
2910
        if (isset($this->config['config']['pageTitle.']) && is_array($this->config['config']['pageTitle.'])) {
2911
            $titleTagContent = $this->cObj->stdWrap($titleTagContent, $this->config['config']['pageTitle.']);
2912
        }
2913
2914
        // config.noPageTitle = 2 - means do not render the page title
2915
        if (isset($this->config['config']['noPageTitle']) && (int)$this->config['config']['noPageTitle'] === 2) {
2916
            $titleTagContent = '';
2917
        }
2918
        if ($titleTagContent !== '') {
2919
            $this->pageRenderer->setTitle($titleTagContent);
2920
        }
2921
        return (string)$titleTagContent;
2922
    }
2923
2924
    /**
2925
     * Compiles the content for the page <title> tag.
2926
     *
2927
     * @param string $pageTitle The input title string, typically the "title" field of a page's record.
2928
     * @param bool $noTitle If set, then only the site title is outputted (from $this->setup['sitetitle'])
2929
     * @param bool $showTitleFirst If set, then "sitetitle" and $title is swapped
2930
     * @param string $pageTitleSeparator an alternative to the ": " as the separator between site title and page title
2931
     * @return string The page title on the form "[sitetitle]: [input-title]". Not htmlspecialchar()'ed.
2932
     * @see generatePageTitle()
2933
     */
2934
    protected function printTitle(string $pageTitle, bool $noTitle = false, bool $showTitleFirst = false, string $pageTitleSeparator = ''): string
2935
    {
2936
        $websiteTitle = $this->getWebsiteTitle();
2937
        $pageTitle = $noTitle ? '' : $pageTitle;
2938
        if ($showTitleFirst) {
2939
            $temp = $websiteTitle;
2940
            $websiteTitle = $pageTitle;
2941
            $pageTitle = $temp;
2942
        }
2943
        // only show a separator if there are both site title and page title
2944
        if ($pageTitle === '' || $websiteTitle === '') {
2945
            $pageTitleSeparator = '';
2946
        } elseif (empty($pageTitleSeparator)) {
2947
            // use the default separator if non given
2948
            $pageTitleSeparator = ': ';
2949
        }
2950
        return $websiteTitle . $pageTitleSeparator . $pageTitle;
2951
    }
2952
2953
    /**
2954
     * @return string
2955
     */
2956
    protected function getWebsiteTitle(): string
2957
    {
2958
        if ($this->language instanceof SiteLanguage
2959
            && trim($this->language->getWebsiteTitle()) !== ''
2960
        ) {
2961
            return trim($this->language->getWebsiteTitle());
2962
        }
2963
        if ($this->site instanceof SiteInterface
2964
            && trim($this->site->getConfiguration()['websiteTitle']) !== ''
2965
        ) {
2966
            return trim($this->site->getConfiguration()['websiteTitle']);
2967
        }
2968
        if (!empty($this->tmpl->setup['sitetitle'])) {
2969
            // @deprecated since TYPO3 v10.2 and will be removed in TYPO3 v11.0
2970
            return trim($this->tmpl->setup['sitetitle']);
2971
        }
2972
2973
        return '';
2974
    }
2975
2976
    /**
2977
     * Processes the INTinclude-scripts
2978
     */
2979
    public function INTincScript()
2980
    {
2981
        $this->additionalHeaderData = is_array($this->config['INTincScript_ext']['additionalHeaderData'] ?? false)
2982
            ? $this->config['INTincScript_ext']['additionalHeaderData']
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
2983
            : [];
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
2984
        $this->additionalFooterData = is_array($this->config['INTincScript_ext']['additionalFooterData'] ?? false)
2985
            ? $this->config['INTincScript_ext']['additionalFooterData']
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
2986
            : [];
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
2987
        $this->additionalJavaScript = $this->config['INTincScript_ext']['additionalJavaScript'] ?? null;
2988
        $this->additionalCSS = $this->config['INTincScript_ext']['additionalCSS'] ?? null;
2989
        if (empty($this->config['INTincScript_ext']['pageRenderer'])) {
2990
            $this->initPageRenderer();
2991
        } else {
2992
            /** @var PageRenderer $pageRenderer */
2993
            $pageRenderer = unserialize($this->config['INTincScript_ext']['pageRenderer']);
2994
            $this->pageRenderer = $pageRenderer;
2995
            GeneralUtility::setSingletonInstance(PageRenderer::class, $pageRenderer);
2996
        }
2997
        if (!empty($this->config['INTincScript_ext']['assetCollector'])) {
2998
            /** @var AssetCollector $assetCollector */
2999
            $assetCollector = unserialize($this->config['INTincScript_ext']['assetCollector'], ['allowed_classes' => [AssetCollector::class]]);
3000
            GeneralUtility::setSingletonInstance(AssetCollector::class, $assetCollector);
3001
        }
3002
3003
        $this->recursivelyReplaceIntPlaceholdersInContent();
3004
        $this->getTimeTracker()->push('Substitute header section');
3005
        $this->INTincScript_loadJSCode();
3006
        $this->generatePageTitle();
3007
3008
        $this->content = str_replace(
3009
            [
3010
                '<!--HD_' . $this->config['INTincScript_ext']['divKey'] . '-->',
3011
                '<!--FD_' . $this->config['INTincScript_ext']['divKey'] . '-->',
3012
            ],
3013
            [
3014
                $this->convOutputCharset(implode(LF, $this->additionalHeaderData)),
3015
                $this->convOutputCharset(implode(LF, $this->additionalFooterData)),
3016
            ],
3017
            $this->pageRenderer->renderJavaScriptAndCssForProcessingOfUncachedContentObjects($this->content, $this->config['INTincScript_ext']['divKey'])
3018
        );
3019
        // Replace again, because header and footer data and page renderer replacements may introduce additional placeholders (see #44825)
3020
        $this->recursivelyReplaceIntPlaceholdersInContent();
3021
        $this->setAbsRefPrefix();
3022
        $this->getTimeTracker()->pull();
3023
    }
3024
3025
    /**
3026
     * Replaces INT placeholders (COA_INT and USER_INT) in $this->content
3027
     * In case the replacement adds additional placeholders, it loops
3028
     * until no new placeholders are found any more.
3029
     */
3030
    protected function recursivelyReplaceIntPlaceholdersInContent()
3031
    {
3032
        do {
3033
            $nonCacheableData = $this->config['INTincScript'];
3034
            $this->processNonCacheableContentPartsAndSubstituteContentMarkers($nonCacheableData);
3035
            // Check if there were new items added to INTincScript during the previous execution:
3036
            // array_diff_assoc throws notices if values are arrays but not strings. We suppress this here.
3037
            $nonCacheableData = @array_diff_assoc($this->config['INTincScript'], $nonCacheableData);
3038
            $reprocess = count($nonCacheableData) > 0;
3039
        } while ($reprocess);
3040
    }
3041
3042
    /**
3043
     * Processes the INTinclude-scripts and substitute in content.
3044
     *
3045
     * Takes $this->content, and splits the content by <!--INT_SCRIPT.12345 --> and then puts the content
3046
     * back together.
3047
     *
3048
     * @param array $nonCacheableData $GLOBALS['TSFE']->config['INTincScript'] or part of it
3049
     * @see INTincScript()
3050
     */
3051
    protected function processNonCacheableContentPartsAndSubstituteContentMarkers(array $nonCacheableData)
3052
    {
3053
        $timeTracker = $this->getTimeTracker();
3054
        $timeTracker->push('Split content');
3055
        // Splits content with the key.
3056
        $contentSplitByUncacheableMarkers = explode('<!--INT_SCRIPT.', $this->content);
3057
        $this->content = '';
3058
        $timeTracker->setTSlogMessage('Parts: ' . count($contentSplitByUncacheableMarkers));
3059
        $timeTracker->pull();
3060
        foreach ($contentSplitByUncacheableMarkers as $counter => $contentPart) {
3061
            // If the split had a comment-end after 32 characters it's probably a split-string
3062
            if (substr($contentPart, 32, 3) === '-->') {
3063
                $nonCacheableKey = 'INT_SCRIPT.' . substr($contentPart, 0, 32);
3064
                if (is_array($nonCacheableData[$nonCacheableKey])) {
3065
                    $label = 'Include ' . $nonCacheableData[$nonCacheableKey]['type'];
3066
                    $timeTracker->push($label);
3067
                    $nonCacheableContent = '';
3068
                    $contentObjectRendererForNonCacheable = unserialize($nonCacheableData[$nonCacheableKey]['cObj']);
3069
                    /* @var ContentObjectRenderer $contentObjectRendererForNonCacheable */
3070
                    switch ($nonCacheableData[$nonCacheableKey]['type']) {
3071
                        case 'COA':
3072
                            $nonCacheableContent = $contentObjectRendererForNonCacheable->cObjGetSingle('COA', $nonCacheableData[$nonCacheableKey]['conf']);
3073
                            break;
3074
                        case 'FUNC':
3075
                            $nonCacheableContent = $contentObjectRendererForNonCacheable->cObjGetSingle('USER', $nonCacheableData[$nonCacheableKey]['conf']);
3076
                            break;
3077
                        case 'POSTUSERFUNC':
3078
                            $nonCacheableContent = $contentObjectRendererForNonCacheable->callUserFunction($nonCacheableData[$nonCacheableKey]['postUserFunc'], $nonCacheableData[$nonCacheableKey]['conf'], $nonCacheableData[$nonCacheableKey]['content']);
3079
                            break;
3080
                    }
3081
                    $this->content .= $this->convOutputCharset($nonCacheableContent);
3082
                    $this->content .= substr($contentPart, 35);
3083
                    $timeTracker->pull($nonCacheableContent);
3084
                } else {
3085
                    $this->content .= substr($contentPart, 35);
3086
                }
3087
            } elseif ($counter) {
3088
                // If it's not the first entry (which would be "0" of the array keys), then re-add the INT_SCRIPT part
3089
                $this->content .= '<!--INT_SCRIPT.' . $contentPart;
3090
            } else {
3091
                $this->content .= $contentPart;
3092
            }
3093
        }
3094
    }
3095
3096
    /**
3097
     * Loads the JavaScript/CSS code for INTincScript, if there are non-cacheable content objects
3098
     * it prepares the placeholders, otherwise populates options directly.
3099
     *
3100
     * @internal this method should be renamed as it does not only handle JS, but all additional header data
3101
     */
3102
    public function INTincScript_loadJSCode()
0 ignored issues
show
Coding Style introduced by
Method name "TypoScriptFrontendController::INTincScript_loadJSCode" is not in camel caps format
Loading history...
3103
    {
3104
        // Prepare code and placeholders for additional header and footer files (and make sure that this isn't called twice)
3105
        if ($this->isINTincScript() && !isset($this->config['INTincScript_ext'])) {
3106
            // Storing the JSCode vars...
3107
            $this->additionalHeaderData['JSCode'] = $this->JSCode;
3108
            $this->config['INTincScript_ext']['divKey'] = $this->uniqueHash();
3109
            // Storing the header-data array
3110
            $this->config['INTincScript_ext']['additionalHeaderData'] = $this->additionalHeaderData;
3111
            // Storing the footer-data array
3112
            $this->config['INTincScript_ext']['additionalFooterData'] = $this->additionalFooterData;
3113
            // Storing the JS-data array
3114
            $this->config['INTincScript_ext']['additionalJavaScript'] = $this->additionalJavaScript;
3115
            // Storing the Style-data array
3116
            $this->config['INTincScript_ext']['additionalCSS'] = $this->additionalCSS;
3117
            // Clearing the array
3118
            $this->additionalHeaderData = ['<!--HD_' . $this->config['INTincScript_ext']['divKey'] . '-->'];
3119
            // Clearing the array
3120
            $this->additionalFooterData = ['<!--FD_' . $this->config['INTincScript_ext']['divKey'] . '-->'];
3121
        } else {
3122
            // Add javascript in a "regular" fashion
3123
            $jsCode = trim($this->JSCode);
3124
            $additionalJavaScript = is_array($this->additionalJavaScript)
0 ignored issues
show
introduced by
The condition is_array($this->additionalJavaScript) is always true.
Loading history...
3125
                ? implode(LF, $this->additionalJavaScript)
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
3126
                : $this->additionalJavaScript;
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
3127
            $additionalJavaScript = trim($additionalJavaScript);
3128
            if ($jsCode !== '' || $additionalJavaScript !== '') {
3129
                $doctype = $this->config['config']['doctype'] ?? 'html5';
3130
                $scriptAttribute = $doctype === 'html5' ? '' : ' type="text/javascript"';
3131
3132
                $this->additionalHeaderData['JSCode'] = '
3133
<script' . $scriptAttribute . '>
3134
	/*<![CDATA[*/
3135
<!--
3136
' . $additionalJavaScript . '
3137
' . $jsCode . '
3138
// -->
3139
	/*]]>*/
3140
</script>';
3141
            }
3142
            // Add CSS
3143
            $additionalCss = is_array($this->additionalCSS) ? implode(LF, $this->additionalCSS) : $this->additionalCSS;
0 ignored issues
show
introduced by
The condition is_array($this->additionalCSS) is always true.
Loading history...
3144
            $additionalCss = trim($additionalCss);
3145
            if ($additionalCss !== '') {
3146
                $this->additionalHeaderData['_CSS'] = '
3147
<style type="text/css">
3148
' . $additionalCss . '
3149
</style>';
3150
            }
3151
        }
3152
    }
3153
3154
    /**
3155
     * Determines if there are any INTincScripts to include = "non-cacheable" parts
3156
     *
3157
     * @return bool Returns TRUE if scripts are found
3158
     */
3159
    public function isINTincScript()
3160
    {
3161
        return !empty($this->config['INTincScript']) && is_array($this->config['INTincScript']);
3162
    }
3163
3164
    /********************************************
3165
     *
3166
     * Finished off; outputting, storing session data, statistics...
3167
     *
3168
     *******************************************/
3169
    /**
3170
     * Determines if content should be outputted.
3171
     * Outputting content is done only if no URL handler is active and no hook disables the output.
3172
     *
3173
     * @return bool Returns TRUE if no redirect URL is set and no hook disables the output.
3174
     */
3175
    public function isOutputting()
3176
    {
3177
        // Initialize by status if there is a Redirect URL
3178
        $enableOutput = true;
3179
        // Call hook for possible disabling of output:
3180
        $_params = ['pObj' => &$this, 'enableOutput' => &$enableOutput];
3181
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['isOutputting'] ?? [] as $_funcRef) {
3182
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
3183
        }
3184
        return $enableOutput;
3185
    }
3186
3187
    /**
3188
     * Process the output before it's actually outputted.
3189
     *
3190
     * This includes substituting the "username" comment.
3191
     * Works on $this->content.
3192
     */
3193
    public function processContentForOutput()
3194
    {
3195
        // Make substitution of eg. username/uid in content only if cache-headers for client/proxy caching is NOT sent!
3196
        if (!$this->isClientCachable) {
3197
            // Substitute various tokens in content. This should happen only if the content is not cached by proxies or client browsers.
3198
            $search = [];
3199
            $replace = [];
3200
            // Hook for supplying custom search/replace data
3201
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['tslib_fe-contentStrReplace'] ?? [] as $_funcRef) {
3202
                $_params = [
3203
                    'search' => &$search,
3204
                    'replace' => &$replace
3205
                ];
3206
                GeneralUtility::callUserFunction($_funcRef, $_params, $this);
3207
            }
3208
            if (!empty($search)) {
3209
                $this->content = str_replace($search, $replace, $this->content);
3210
            }
3211
        }
3212
        // Hook for post-processing of page content before output:
3213
        $_params = ['pObj' => &$this];
3214
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-output'] ?? [] as $_funcRef) {
3215
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
3216
        }
3217
    }
3218
3219
    /**
3220
     * Add HTTP headers to the response object.
3221
     *
3222
     * @param ResponseInterface $response
3223
     * @return ResponseInterface
3224
     */
3225
    public function applyHttpHeadersToResponse(ResponseInterface $response): ResponseInterface
3226
    {
3227
        // Set header for charset-encoding unless disabled
3228
        if (empty($this->config['config']['disableCharsetHeader'])) {
3229
            $response = $response->withHeader('Content-Type', $this->contentType . '; charset=' . trim($this->metaCharset));
3230
        }
3231
        // Set header for content language unless disabled
3232
        $contentLanguage = $this->language->getTwoLetterIsoCode();
3233
        if (empty($this->config['config']['disableLanguageHeader']) && !empty($contentLanguage)) {
3234
            $response = $response->withHeader('Content-Language', trim($contentLanguage));
3235
        }
3236
        // Set cache related headers to client (used to enable proxy / client caching!)
3237
        if (!empty($this->config['config']['sendCacheHeaders'])) {
3238
            $headers = $this->getCacheHeaders();
3239
            foreach ($headers as $header => $value) {
3240
                $response = $response->withHeader($header, $value);
3241
            }
3242
        }
3243
        // Set additional headers if any have been configured via TypoScript
3244
        $additionalHeaders = $this->getAdditionalHeaders();
3245
        foreach ($additionalHeaders as $headerConfig) {
3246
            [$header, $value] = GeneralUtility::trimExplode(':', $headerConfig['header'], false, 2);
3247
            if ($headerConfig['statusCode']) {
3248
                $response = $response->withStatus((int)$headerConfig['statusCode']);
3249
            }
3250
            if ($headerConfig['replace']) {
3251
                $response = $response->withHeader($header, $value);
3252
            } else {
3253
                $response = $response->withAddedHeader($header, $value);
3254
            }
3255
        }
3256
        return $response;
3257
    }
3258
3259
    /**
3260
     * Get cache headers good for client/reverse proxy caching.
3261
     *
3262
     * @return array
3263
     */
3264
    protected function getCacheHeaders(): array
3265
    {
3266
        // Getting status whether we can send cache control headers for proxy caching:
3267
        $doCache = $this->isStaticCacheble();
3268
        // 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...
3269
        $loginsDeniedCfg = empty($this->config['config']['sendCacheHeaders_onlyWhenLoginDeniedInBranch']) || empty($this->loginAllowedInBranch);
3270
        // Finally, when backend users are logged in, do not send cache headers at all (Admin Panel might be displayed for instance).
3271
        $this->isClientCachable = $doCache && !$this->isBackendUserLoggedIn() && !$this->doWorkspacePreview() && $loginsDeniedCfg;
3272
        if ($this->isClientCachable) {
3273
            $headers = [
3274
                'Expires' => gmdate('D, d M Y H:i:s T', $this->cacheExpires),
3275
                'ETag' => '"' . md5($this->content) . '"',
3276
                'Cache-Control' => 'max-age=' . ($this->cacheExpires - $GLOBALS['EXEC_TIME']),
3277
                // no-cache
3278
                'Pragma' => 'public'
3279
            ];
3280
        } else {
3281
            // "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
3282
            $headers = [
3283
                'Cache-Control' => 'private, no-store'
3284
            ];
3285
            // Now, if a backend user is logged in, tell him in the Admin Panel log what the caching status would have been:
3286
            if ($this->isBackendUserLoggedIn()) {
3287
                if ($doCache) {
3288
                    $this->getTimeTracker()->setTSlogMessage('Cache-headers with max-age "' . ($this->cacheExpires - $GLOBALS['EXEC_TIME']) . '" would have been sent');
3289
                } else {
3290
                    $reasonMsg = [];
3291
                    if ($this->no_cache) {
3292
                        $reasonMsg[] = 'Caching disabled (no_cache).';
3293
                    }
3294
                    if ($this->isINTincScript()) {
3295
                        $reasonMsg[] = '*_INT object(s) on page.';
3296
                    }
3297
                    if (is_array($this->fe_user->user)) {
3298
                        $reasonMsg[] = 'Frontend user logged in.';
3299
                    }
3300
                    $this->getTimeTracker()->setTSlogMessage('Cache-headers would disable proxy caching! Reason(s): "' . implode(' ', $reasonMsg) . '"', 1);
3301
                }
3302
            }
3303
        }
3304
        return $headers;
3305
    }
3306
3307
    /**
3308
     * Reporting status whether we can send cache control headers for proxy caching or publishing to static files
3309
     *
3310
     * Rules are:
3311
     * no_cache cannot be set: If it is, the page might contain dynamic content and should never be cached.
3312
     * There can be no USER_INT objects on the page ("isINTincScript()") because they implicitly indicate dynamic content
3313
     * 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)
3314
     *
3315
     * @return bool
3316
     */
3317
    public function isStaticCacheble()
3318
    {
3319
        return !$this->no_cache && !$this->isINTincScript() && !$this->isUserOrGroupSet();
3320
    }
3321
3322
    /********************************************
3323
     *
3324
     * Various internal API functions
3325
     *
3326
     *******************************************/
3327
3328
    /**
3329
     * Creates an instance of ContentObjectRenderer in $this->cObj
3330
     * This instance is used to start the rendering of the TypoScript template structure
3331
     *
3332
     * @see RequestHandler
3333
     */
3334
    public function newCObj()
3335
    {
3336
        $this->cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class, $this);
3337
        $this->cObj->start($this->page, 'pages');
3338
    }
3339
3340
    /**
3341
     * Converts relative paths in the HTML source to absolute paths for fileadmin/, typo3conf/ext/ and media/ folders.
3342
     *
3343
     * @internal
3344
     * @see \TYPO3\CMS\Frontend\Http\RequestHandler
3345
     * @see INTincScript()
3346
     */
3347
    public function setAbsRefPrefix()
3348
    {
3349
        if (!$this->absRefPrefix) {
3350
            return;
3351
        }
3352
        $search = [
3353
            '"typo3temp/',
3354
            '"' . PathUtility::stripPathSitePrefix(Environment::getExtensionsPath()) . '/',
3355
            '"' . PathUtility::stripPathSitePrefix(Environment::getBackendPath()) . '/ext/',
3356
            '"' . PathUtility::stripPathSitePrefix(Environment::getFrameworkBasePath()) . '/',
3357
        ];
3358
        $replace = [
3359
            '"' . $this->absRefPrefix . 'typo3temp/',
3360
            '"' . $this->absRefPrefix . PathUtility::stripPathSitePrefix(Environment::getExtensionsPath()) . '/',
3361
            '"' . $this->absRefPrefix . PathUtility::stripPathSitePrefix(Environment::getBackendPath()) . '/ext/',
3362
            '"' . $this->absRefPrefix . PathUtility::stripPathSitePrefix(Environment::getFrameworkBasePath()) . '/',
3363
        ];
3364
        /** @var StorageRepository $storageRepository */
3365
        $storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
3366
        $storages = $storageRepository->findAll();
3367
        foreach ($storages as $storage) {
3368
            if ($storage->getDriverType() === 'Local' && $storage->isPublic() && $storage->isOnline()) {
3369
                $folder = $storage->getPublicUrl($storage->getRootLevelFolder(), true);
3370
                $search[] = '"' . $folder;
3371
                $replace[] = '"' . $this->absRefPrefix . $folder;
3372
            }
3373
        }
3374
        // Process additional directories
3375
        $directories = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['FE']['additionalAbsRefPrefixDirectories'], true);
3376
        foreach ($directories as $directory) {
3377
            $search[] = '"' . $directory;
3378
            $replace[] = '"' . $this->absRefPrefix . $directory;
3379
        }
3380
        $this->content = str_replace(
3381
            $search,
3382
            $replace,
3383
            $this->content
3384
        );
3385
    }
3386
3387
    /**
3388
     * Prefixing the input URL with ->baseUrl If ->baseUrl is set and the input url is not absolute in some way.
3389
     * 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!
3390
     *
3391
     * @param string $url Input URL, relative or absolute
3392
     * @return string Processed input value.
3393
     */
3394
    public function baseUrlWrap($url)
3395
    {
3396
        if ($this->baseUrl) {
3397
            $urlParts = parse_url($url);
3398
            if (empty($urlParts['scheme']) && $url[0] !== '/') {
3399
                $url = $this->baseUrl . $url;
3400
            }
3401
        }
3402
        return $url;
3403
    }
3404
3405
    /**
3406
     * Logs access to deprecated TypoScript objects and properties.
3407
     *
3408
     * Dumps message to the TypoScript message log (admin panel) and the TYPO3 deprecation log.
3409
     *
3410
     * @param string $typoScriptProperty Deprecated object or property
3411
     * @param string $explanation Message or additional information
3412
     */
3413
    public function logDeprecatedTyposcript($typoScriptProperty, $explanation = '')
3414
    {
3415
        $explanationText = $explanation !== '' ? ' - ' . $explanation : '';
3416
        $this->getTimeTracker()->setTSlogMessage($typoScriptProperty . ' is deprecated.' . $explanationText, 2);
3417
        trigger_error('TypoScript property ' . $typoScriptProperty . ' is deprecated' . $explanationText, E_USER_DEPRECATED);
3418
    }
3419
3420
    /********************************************
3421
     * PUBLIC ACCESSIBLE WORKSPACES FUNCTIONS
3422
     *******************************************/
3423
3424
    /**
3425
     * Returns TRUE if workspace preview is enabled
3426
     *
3427
     * @return bool Returns TRUE if workspace preview is enabled
3428
     */
3429
    public function doWorkspacePreview()
3430
    {
3431
        return $this->context->getPropertyFromAspect('workspace', 'isOffline', false);
3432
    }
3433
3434
    /**
3435
     * Returns the uid of the current workspace
3436
     *
3437
     * @return int returns workspace integer for which workspace is being preview. 0 if none (= live workspace).
3438
     */
3439
    public function whichWorkspace(): int
3440
    {
3441
        return $this->context->getPropertyFromAspect('workspace', 'id', 0);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->context->g...t('workspace', 'id', 0) could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
3442
    }
3443
3444
    /********************************************
3445
     *
3446
     * Various external API functions - for use in plugins etc.
3447
     *
3448
     *******************************************/
3449
3450
    /**
3451
     * Returns the pages TSconfig array based on the current ->rootLine
3452
     *
3453
     * @return array
3454
     */
3455
    public function getPagesTSconfig()
3456
    {
3457
        if (!is_array($this->pagesTSconfig)) {
0 ignored issues
show
introduced by
The condition is_array($this->pagesTSconfig) is always true.
Loading history...
3458
            $contentHashCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
3459
            $loader = GeneralUtility::makeInstance(PageTsConfigLoader::class);
3460
            $tsConfigString = $loader->load(array_reverse($this->rootLine));
3461
            $parser = GeneralUtility::makeInstance(
3462
                PageTsConfigParser::class,
3463
                GeneralUtility::makeInstance(TypoScriptParser::class),
3464
                $contentHashCache
3465
            );
3466
            $this->pagesTSconfig = $parser->parse(
3467
                $tsConfigString,
3468
                GeneralUtility::makeInstance(ConditionMatcher::class, $this->context, $this->id, $this->rootLine)
3469
            );
3470
        }
3471
        return $this->pagesTSconfig;
3472
    }
3473
3474
    /**
3475
     * Sets JavaScript code in the additionalJavaScript array
3476
     *
3477
     * @param string $key is the key in the array, for num-key let the value be empty. Note reserved key: 'openPic'
3478
     * @param string $content is the content if you want any
3479
     * @see ContentObjectRenderer::imageLinkWrap()
3480
     */
3481
    public function setJS($key, $content = '')
3482
    {
3483
        if ($key === 'openPic') {
3484
            $this->additionalJavaScript[$key] = '	function openPic(url, winName, winParams) {
3485
                var theWindow = window.open(url, winName, winParams);
3486
                if (theWindow)	{theWindow.focus();}
3487
            }';
3488
        } elseif ($key) {
3489
            $this->additionalJavaScript[$key] = $content;
3490
        }
3491
    }
3492
3493
    /**
3494
     * Returns a unique md5 hash.
3495
     * 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.
3496
     *
3497
     * @param string $str Some string to include in what is hashed. Not significant at all.
3498
     * @return string MD5 hash of ->uniqueString, input string and uniqueCounter
3499
     */
3500
    public function uniqueHash($str = '')
3501
    {
3502
        return md5($this->uniqueString . '_' . $str . $this->uniqueCounter++);
3503
    }
3504
3505
    /**
3506
     * Sets the cache-flag to 1. Could be called from user-included php-files in order to ensure that a page is not cached.
3507
     *
3508
     * @param string $reason An optional reason to be written to the log.
3509
     * @param bool $internal Whether the call is done from core itself (should only be used by core).
3510
     */
3511
    public function set_no_cache($reason = '', $internal = false)
0 ignored issues
show
Coding Style introduced by
Method name "TypoScriptFrontendController::set_no_cache" is not in camel caps format
Loading history...
3512
    {
3513
        if ($reason !== '') {
3514
            $warning = '$TSFE->set_no_cache() was triggered. Reason: ' . $reason . '.';
3515
        } else {
3516
            $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
3517
            // This is a hack to work around ___FILE___ resolving symbolic links
3518
            $realWebPath = PathUtility::dirname(realpath(Environment::getBackendPath())) . '/';
3519
            $file = $trace[0]['file'];
3520
            if (strpos($file, $realWebPath) === 0) {
3521
                $file = str_replace($realWebPath, '', $file);
3522
            } else {
3523
                $file = str_replace(Environment::getPublicPath() . '/', '', $file);
3524
            }
3525
            $line = $trace[0]['line'];
3526
            $trigger = $file . ' on line ' . $line;
3527
            $warning = '$GLOBALS[\'TSFE\']->set_no_cache() was triggered by ' . $trigger . '.';
3528
        }
3529
        if (!$internal && $GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter']) {
3530
            $warning .= ' However, $TYPO3_CONF_VARS[\'FE\'][\'disableNoCacheParameter\'] is set, so it will be ignored!';
3531
            $this->getTimeTracker()->setTSlogMessage($warning, 2);
3532
        } else {
3533
            $warning .= ' Caching is disabled!';
3534
            $this->disableCache();
3535
        }
3536
        if ($internal && $this->isBackendUserLoggedIn()) {
3537
            $this->logger->notice($warning);
3538
        } else {
3539
            $this->logger->warning($warning);
3540
        }
3541
    }
3542
3543
    /**
3544
     * Disables caching of the current page.
3545
     *
3546
     * @internal
3547
     */
3548
    protected function disableCache()
3549
    {
3550
        $this->no_cache = true;
3551
    }
3552
3553
    /**
3554
     * Sets the cache-timeout in seconds
3555
     *
3556
     * @param int $seconds Cache-timeout in seconds
3557
     */
3558
    public function set_cache_timeout_default($seconds)
0 ignored issues
show
Coding Style introduced by
Method name "TypoScriptFrontendController::set_cache_timeout_default" is not in camel caps format
Loading history...
3559
    {
3560
        $seconds = (int)$seconds;
3561
        if ($seconds > 0) {
3562
            $this->cacheTimeOutDefault = $seconds;
3563
        }
3564
    }
3565
3566
    /**
3567
     * Get the cache timeout for the current page.
3568
     *
3569
     * @return int The cache timeout for the current page.
3570
     */
3571
    public function get_cache_timeout()
0 ignored issues
show
Coding Style introduced by
Method name "TypoScriptFrontendController::get_cache_timeout" is not in camel caps format
Loading history...
3572
    {
3573
        /** @var \TYPO3\CMS\Core\Cache\Frontend\AbstractFrontend $runtimeCache */
3574
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
3575
        $cachedCacheLifetimeIdentifier = 'core-tslib_fe-get_cache_timeout';
3576
        $cachedCacheLifetime = $runtimeCache->get($cachedCacheLifetimeIdentifier);
3577
        if ($cachedCacheLifetime === false) {
3578
            if ($this->page['cache_timeout']) {
3579
                // Cache period was set for the page:
3580
                $cacheTimeout = $this->page['cache_timeout'];
3581
            } else {
3582
                // Cache period was set via TypoScript "config.cache_period",
3583
                // otherwise it's the default of 24 hours
3584
                $cacheTimeout = $this->cacheTimeOutDefault;
3585
            }
3586
            if (!empty($this->config['config']['cache_clearAtMidnight'])) {
3587
                $timeOutTime = $GLOBALS['EXEC_TIME'] + $cacheTimeout;
3588
                $midnightTime = mktime(0, 0, 0, date('m', $timeOutTime), date('d', $timeOutTime), date('Y', $timeOutTime));
0 ignored issues
show
Bug introduced by
date('m', $timeOutTime) of type string is incompatible with the type integer expected by parameter $month of mktime(). ( Ignorable by Annotation )

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

3588
                $midnightTime = mktime(0, 0, 0, /** @scrutinizer ignore-type */ date('m', $timeOutTime), date('d', $timeOutTime), date('Y', $timeOutTime));
Loading history...
Bug introduced by
date('d', $timeOutTime) of type string is incompatible with the type integer expected by parameter $day of mktime(). ( Ignorable by Annotation )

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

3588
                $midnightTime = mktime(0, 0, 0, date('m', $timeOutTime), /** @scrutinizer ignore-type */ date('d', $timeOutTime), date('Y', $timeOutTime));
Loading history...
Bug introduced by
date('Y', $timeOutTime) of type string is incompatible with the type integer expected by parameter $year of mktime(). ( Ignorable by Annotation )

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

3588
                $midnightTime = mktime(0, 0, 0, date('m', $timeOutTime), date('d', $timeOutTime), /** @scrutinizer ignore-type */ date('Y', $timeOutTime));
Loading history...
3589
                // If the midnight time of the expire-day is greater than the current time,
3590
                // we may set the timeOutTime to the new midnighttime.
3591
                if ($midnightTime > $GLOBALS['EXEC_TIME']) {
3592
                    $cacheTimeout = $midnightTime - $GLOBALS['EXEC_TIME'];
3593
                }
3594
            }
3595
3596
            // Calculate the timeout time for records on the page and adjust cache timeout if necessary
3597
            $cacheTimeout = min($this->calculatePageCacheTimeout(), $cacheTimeout);
3598
3599
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['get_cache_timeout'] ?? [] as $_funcRef) {
3600
                $params = ['cacheTimeout' => $cacheTimeout];
3601
                $cacheTimeout = GeneralUtility::callUserFunction($_funcRef, $params, $this);
3602
            }
3603
            $runtimeCache->set($cachedCacheLifetimeIdentifier, $cacheTimeout);
3604
            $cachedCacheLifetime = $cacheTimeout;
3605
        }
3606
        return $cachedCacheLifetime;
3607
    }
3608
3609
    /*********************************************
3610
     *
3611
     * Localization and character set conversion
3612
     *
3613
     *********************************************/
3614
    /**
3615
     * Split Label function for front-end applications.
3616
     *
3617
     * @param string $input Key string. Accepts the "LLL:" prefix.
3618
     * @return string Label value, if any.
3619
     */
3620
    public function sL($input)
3621
    {
3622
        return $this->languageService->sL($input);
3623
    }
3624
3625
    /**
3626
     * Sets all internal measures what language the page should be rendered.
3627
     * This is not for records, but rather the HTML / charset and the locallang labels
3628
     */
3629
    protected function setOutputLanguage()
3630
    {
3631
        $this->languageService = LanguageService::createFromSiteLanguage($this->language);
3632
        // Always disable debugging for TSFE
3633
        $this->languageService->debugKey = false;
3634
    }
3635
3636
    /**
3637
     * Converts input string from utf-8 to metaCharset IF the two charsets are different.
3638
     *
3639
     * @param string $content Content to be converted.
3640
     * @return string Converted content string.
3641
     * @throws \RuntimeException if an invalid charset was configured
3642
     */
3643
    public function convOutputCharset($content)
3644
    {
3645
        if ($this->metaCharset !== 'utf-8') {
3646
            /** @var CharsetConverter $charsetConverter */
3647
            $charsetConverter = GeneralUtility::makeInstance(CharsetConverter::class);
3648
            try {
3649
                $content = $charsetConverter->conv($content, 'utf-8', $this->metaCharset);
3650
            } catch (UnknownCharsetException $e) {
3651
                throw new \RuntimeException('Invalid config.metaCharset: ' . $e->getMessage(), 1508916185);
3652
            }
3653
        }
3654
        return $content;
3655
    }
3656
3657
    /**
3658
     * Calculates page cache timeout according to the records with starttime/endtime on the page.
3659
     *
3660
     * @return int Page cache timeout or PHP_INT_MAX if cannot be determined
3661
     */
3662
    protected function calculatePageCacheTimeout()
3663
    {
3664
        $result = PHP_INT_MAX;
3665
        // Get the configuration
3666
        $tablesToConsider = $this->getCurrentPageCacheConfiguration();
3667
        // Get the time, rounded to the minute (do not pollute MySQL cache!)
3668
        // It is ok that we do not take seconds into account here because this
3669
        // value will be subtracted later. So we never get the time "before"
3670
        // the cache change.
3671
        $now = $GLOBALS['ACCESS_TIME'];
3672
        // Find timeout by checking every table
3673
        foreach ($tablesToConsider as $tableDef) {
3674
            $result = min($result, $this->getFirstTimeValueForRecord($tableDef, $now));
3675
        }
3676
        // We return + 1 second just to ensure that cache is definitely regenerated
3677
        return $result === PHP_INT_MAX ? PHP_INT_MAX : $result - $now + 1;
3678
    }
3679
3680
    /**
3681
     * Obtains a list of table/pid pairs to consider for page caching.
3682
     *
3683
     * TS configuration looks like this:
3684
     *
3685
     * The cache lifetime of all pages takes starttime and endtime of news records of page 14 into account:
3686
     * config.cache.all = tt_news:14
3687
     *
3688
     * The cache.lifetime of the current page allows to take records (e.g. fe_users) into account:
3689
     * config.cache.all = fe_users:current
3690
     *
3691
     * The cache lifetime of page 42 takes starttime and endtime of news records of page 15 and addresses of page 16 into account:
3692
     * config.cache.42 = tt_news:15,tt_address:16
3693
     *
3694
     * @return array Array of 'tablename:pid' pairs. There is at least a current page id in the array
3695
     * @see TypoScriptFrontendController::calculatePageCacheTimeout()
3696
     */
3697
    protected function getCurrentPageCacheConfiguration()
3698
    {
3699
        $result = ['tt_content:' . $this->id];
3700
        if (isset($this->config['config']['cache.'][$this->id])) {
3701
            $result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $this->id, $this->config['config']['cache.'][$this->id])));
3702
        }
3703
        if (isset($this->config['config']['cache.']['all'])) {
3704
            $result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $this->id, $this->config['config']['cache.']['all'])));
3705
        }
3706
        return array_unique($result);
3707
    }
3708
3709
    /**
3710
     * Find the minimum starttime or endtime value in the table and pid that is greater than the current time.
3711
     *
3712
     * @param string $tableDef Table definition (format tablename:pid)
3713
     * @param int $now "Now" time value
3714
     * @throws \InvalidArgumentException
3715
     * @return int Value of the next start/stop time or PHP_INT_MAX if not found
3716
     * @see TypoScriptFrontendController::calculatePageCacheTimeout()
3717
     */
3718
    protected function getFirstTimeValueForRecord($tableDef, $now)
3719
    {
3720
        $now = (int)$now;
3721
        $result = PHP_INT_MAX;
3722
        [$tableName, $pid] = GeneralUtility::trimExplode(':', $tableDef);
3723
        if (empty($tableName) || empty($pid)) {
3724
            throw new \InvalidArgumentException('Unexpected value for parameter $tableDef. Expected <tablename>:<pid>, got \'' . htmlspecialchars($tableDef) . '\'.', 1307190365);
3725
        }
3726
3727
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
3728
            ->getQueryBuilderForTable($tableName);
3729
        $queryBuilder->getRestrictions()
3730
            ->removeByType(StartTimeRestriction::class)
3731
            ->removeByType(EndTimeRestriction::class);
3732
        $timeFields = [];
3733
        $timeConditions = $queryBuilder->expr()->orX();
3734
        foreach (['starttime', 'endtime'] as $field) {
3735
            if (isset($GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field])) {
3736
                $timeFields[$field] = $GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field];
3737
                $queryBuilder->addSelectLiteral(
3738
                    'MIN('
3739
                        . 'CASE WHEN '
3740
                        . $queryBuilder->expr()->lte(
3741
                            $timeFields[$field],
3742
                            $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)
3743
                        )
3744
                        . ' THEN NULL ELSE ' . $queryBuilder->quoteIdentifier($timeFields[$field]) . ' END'
3745
                        . ') AS ' . $queryBuilder->quoteIdentifier($timeFields[$field])
3746
                );
3747
                $timeConditions->add(
3748
                    $queryBuilder->expr()->gt(
3749
                        $timeFields[$field],
3750
                        $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)
3751
                    )
3752
                );
3753
            }
3754
        }
3755
3756
        // if starttime or endtime are defined, evaluate them
3757
        if (!empty($timeFields)) {
3758
            // find the timestamp, when the current page's content changes the next time
3759
            $row = $queryBuilder
3760
                ->from($tableName)
3761
                ->where(
3762
                    $queryBuilder->expr()->eq(
3763
                        'pid',
3764
                        $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
3765
                    ),
3766
                    $timeConditions
3767
                )
3768
                ->execute()
3769
                ->fetch();
3770
3771
            if ($row) {
3772
                foreach ($timeFields as $timeField => $_) {
3773
                    // if a MIN value is found, take it into account for the
3774
                    // cache lifetime we have to filter out start/endtimes < $now,
3775
                    // as the SQL query also returns rows with starttime < $now
3776
                    // and endtime > $now (and using a starttime from the past
3777
                    // would be wrong)
3778
                    if ($row[$timeField] !== null && (int)$row[$timeField] > $now) {
3779
                        $result = min($result, (int)$row[$timeField]);
3780
                    }
3781
                }
3782
            }
3783
        }
3784
3785
        return $result;
3786
    }
3787
3788
    /**
3789
     * Fetches the originally requested id, falls back to $this->id
3790
     *
3791
     * @return int the originally requested page uid
3792
     * @see fetch_the_id()
3793
     */
3794
    public function getRequestedId()
3795
    {
3796
        return $this->requestedId ?: $this->id;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->requestedId ?: $this->id also could return the type string which is incompatible with the documented return type integer.
Loading history...
3797
    }
3798
3799
    /**
3800
     * Acquire a page specific lock
3801
     *
3802
     *
3803
     * The schematics here is:
3804
     * - First acquire an access lock. This is using the type of the requested lock as key.
3805
     *   Since the number of types is rather limited we can use the type as key as it will only
3806
     *   eat up a limited number of lock resources on the system (files, semaphores)
3807
     * - Second, we acquire the actual lock (named page lock). We can be sure we are the only process at this
3808
     *   very moment, hence we either get the lock for the given key or we get an error as we request a non-blocking mode.
3809
     *
3810
     * Interleaving two locks is extremely important, because the actual page lock uses a hash value as key (see callers
3811
     * of this function). If we would simply employ a normal blocking lock, we would get a potentially unlimited
3812
     * (number of pages at least) number of different locks. Depending on the available locking methods on the system
3813
     * we might run out of available resources. (e.g. maximum limit of semaphores is a system setting and applies
3814
     * to the whole system)
3815
     * We therefore must make sure that page locks are destroyed again if they are not used anymore, such that
3816
     * we never use more locking resources than parallel requests to different pages (hashes).
3817
     * In order to ensure this, we need to guarantee that no other process is waiting on a page lock when
3818
     * the process currently having the lock on the page lock is about to release the lock again.
3819
     * This can only be achieved by using a non-blocking mode, such that a process is never put into wait state
3820
     * by the kernel, but only checks the availability of the lock. The access lock is our guard to be sure
3821
     * that no two processes are at the same time releasing/destroying a page lock, whilst the other one tries to
3822
     * get a lock for this page lock.
3823
     * The only drawback of this implementation is that we basically have to poll the availability of the page lock.
3824
     *
3825
     * Note that the access lock resources are NEVER deleted/destroyed, otherwise the whole thing would be broken.
3826
     *
3827
     * @param string $type
3828
     * @param string $key
3829
     * @throws \InvalidArgumentException
3830
     * @throws \RuntimeException
3831
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
3832
     */
3833
    protected function acquireLock($type, $key)
3834
    {
3835
        $lockFactory = GeneralUtility::makeInstance(LockFactory::class);
3836
        $this->locks[$type]['accessLock'] = $lockFactory->createLocker($type);
3837
3838
        $this->locks[$type]['pageLock'] = $lockFactory->createLocker(
3839
            $key,
3840
            LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
3841
        );
3842
3843
        do {
3844
            if (!$this->locks[$type]['accessLock']->acquire()) {
3845
                throw new \RuntimeException('Could not acquire access lock for "' . $type . '"".', 1294586098);
3846
            }
3847
3848
            try {
3849
                $locked = $this->locks[$type]['pageLock']->acquire(
3850
                    LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
3851
                );
3852
            } catch (LockAcquireWouldBlockException $e) {
3853
                // somebody else has the lock, we keep waiting
3854
3855
                // first release the access lock
3856
                $this->locks[$type]['accessLock']->release();
3857
                // now lets make a short break (100ms) until we try again, since
3858
                // the page generation by the lock owner will take a while anyways
3859
                usleep(100000);
3860
                continue;
3861
            }
3862
            $this->locks[$type]['accessLock']->release();
3863
            if ($locked) {
3864
                break;
3865
            }
3866
            throw new \RuntimeException('Could not acquire page lock for ' . $key . '.', 1460975877);
3867
        } while (true);
3868
    }
3869
3870
    /**
3871
     * Release a page specific lock
3872
     *
3873
     * @param string $type
3874
     * @throws \InvalidArgumentException
3875
     * @throws \RuntimeException
3876
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
3877
     */
3878
    protected function releaseLock($type)
3879
    {
3880
        if ($this->locks[$type]['accessLock']) {
3881
            if (!$this->locks[$type]['accessLock']->acquire()) {
3882
                throw new \RuntimeException('Could not acquire access lock for "' . $type . '"".', 1460975902);
3883
            }
3884
3885
            $this->locks[$type]['pageLock']->release();
3886
            $this->locks[$type]['pageLock']->destroy();
3887
            $this->locks[$type]['pageLock'] = null;
3888
3889
            $this->locks[$type]['accessLock']->release();
3890
            $this->locks[$type]['accessLock'] = null;
3891
        }
3892
    }
3893
3894
    /**
3895
     * Send additional headers from config.additionalHeaders
3896
     */
3897
    protected function getAdditionalHeaders(): array
3898
    {
3899
        if (!isset($this->config['config']['additionalHeaders.'])) {
3900
            return [];
3901
        }
3902
        $additionalHeaders = [];
3903
        ksort($this->config['config']['additionalHeaders.']);
3904
        foreach ($this->config['config']['additionalHeaders.'] as $options) {
3905
            if (!is_array($options)) {
3906
                continue;
3907
            }
3908
            $header = trim($options['header'] ?? '');
3909
            if ($header === '') {
3910
                continue;
3911
            }
3912
            $additionalHeaders[] = [
3913
                'header' => $header,
3914
                // "replace existing headers" is turned on by default, unless turned off
3915
                'replace' => ($options['replace'] ?? '') !== '0',
3916
                'statusCode' => (int)($options['httpResponseCode'] ?? 0) ?: null
3917
            ];
3918
        }
3919
        return $additionalHeaders;
3920
    }
3921
3922
    /**
3923
     * Returns the current BE user.
3924
     *
3925
     * @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication
3926
     */
3927
    protected function getBackendUser()
3928
    {
3929
        return $GLOBALS['BE_USER'];
3930
    }
3931
3932
    /**
3933
     * @return TimeTracker
3934
     */
3935
    protected function getTimeTracker()
3936
    {
3937
        return GeneralUtility::makeInstance(TimeTracker::class);
3938
    }
3939
3940
    /**
3941
     * Return the global instance of this class.
3942
     *
3943
     * Intended to be used as prototype factory for this class, see Services.yaml.
3944
     * This is required as long as TypoScriptFrontendController needs request
3945
     * dependent constructor parameters. Once that has been refactored this
3946
     * factory will be removed.
3947
     *
3948
     * @return TypoScriptFrontendController
3949
     * @internal
3950
     */
3951
    public static function getGlobalInstance(): ?self
3952
    {
3953
        if ($GLOBALS['TSFE'] instanceof self) {
3954
            return $GLOBALS['TSFE'];
3955
        }
3956
3957
        if (!(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_FE)) {
3958
            // Return null for now (together with shared: false in Services.yaml) as TSFE might not be available in backend context
3959
            // That's not an error then
3960
            return null;
3961
        }
3962
3963
        throw new \LogicException('TypoScriptFrontendController was tried to be injected before initial creation', 1538370377);
3964
    }
3965
3966
    public function getLanguage(): SiteLanguage
3967
    {
3968
        return $this->language;
3969
    }
3970
3971
    public function getSite(): Site
3972
    {
3973
        return $this->site;
3974
    }
3975
3976
    public function getContext(): Context
3977
    {
3978
        return $this->context;
3979
    }
3980
3981
    public function getPageArguments(): PageArguments
3982
    {
3983
        return $this->pageArguments;
3984
    }
3985
3986
    /**
3987
     * Deprecation messages for TYPO3 10 - public properties of TSFE which have been (re)moved
3988
     */
3989
3990
    /**
3991
     * Checks if the property of the given name is set.
3992
     *
3993
     * Unmarked protected properties must return false as usual.
3994
     * Marked properties are evaluated by isset().
3995
     *
3996
     * This method is not called for public properties.
3997
     *
3998
     * @param string $propertyName
3999
     * @return bool
4000
     */
4001
    public function __isset(string $propertyName)
4002
    {
4003
        switch ($propertyName) {
4004
            case 'domainStartPage':
4005
                trigger_error('Property $TSFE->domainStartPage is not in use anymore as this information is now stored within the Site object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4006
                return  true;
0 ignored issues
show
Coding Style introduced by
Language constructs must be followed by a single space; expected 1 space but found 2
Loading history...
4007
            case 'cHash':
4008
                trigger_error('Property $TSFE->cHash is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4009
                return isset($this->pageArguments->getArguments()['cHash']);
4010
            case 'cHash_array':
4011
                trigger_error('Property $TSFE->cHash_array is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4012
                $value = $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments);
4013
                return !empty($value);
4014
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
4015
            case 'sys_language_isocode':
4016
                trigger_error('Property $TSFE->sys_language_isocode is not in use anymore as this information is now stored within the SiteLanguage object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4017
                return isset($this->$propertyName);
4018
            case 'divSection':
4019
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4020
                return isset($this->$propertyName);
4021
            case 'fePreview':
4022
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
4023
                return $this->context->hasAspect('frontend.preview');
4024
            case 'forceTemplateParsing':
4025
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
4026
                return $this->context->hasAspect('typoscript') && $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing');
4027
        }
4028
        return false;
4029
    }
4030
4031
    /**
4032
     * Gets the value of the property of the given name if tagged.
4033
     *
4034
     * The evaluation is done in the assumption that this method is never
4035
     * reached for a public property.
4036
     *
4037
     * @param string $propertyName
4038
     * @return mixed
4039
     */
4040
    public function __get(string $propertyName)
4041
    {
4042
        switch ($propertyName) {
4043
            case 'domainStartPage':
4044
                trigger_error('Property $TSFE->domainStartPage is not in use anymore as this information is now stored within the Site object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4045
                return $this->site->getRootPageId();
4046
            case 'cHash':
4047
                trigger_error('Property $TSFE->cHash is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4048
                return $this->pageArguments->getArguments()['cHash'] ?? false;
4049
            case 'cHash_array':
4050
                trigger_error('Property $TSFE->cHash_array is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4051
                return $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments);
4052
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
4053
            case 'sys_language_isocode':
4054
                trigger_error('Property $TSFE->sys_language_isocode is not in use anymore as this information is now stored within the SiteLanguage object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4055
                return $this->sys_language_isocode ?? $this->language->getTwoLetterIsoCode();
4056
            case 'divSection':
4057
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4058
                break;
4059
            case 'fePreview':
4060
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
4061
                if ($this->context->hasAspect('frontend.preview')) {
4062
                    return $this->context->getPropertyFromAspect('frontend.preview', 'isPreview');
4063
                }
4064
                break;
4065
            case 'forceTemplateParsing':
4066
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
4067
                if ($this->context->hasAspect('typoscript')) {
4068
                    return $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing');
4069
                }
4070
                break;
4071
        }
4072
        return $this->$propertyName;
4073
    }
4074
4075
    /**
4076
     * Sets the property of the given name if tagged.
4077
     *
4078
     * Additionally it's allowed to set unknown properties.
4079
     *
4080
     * The evaluation is done in the assumption that this method is never
4081
     * reached for a public property.
4082
     *
4083
     * @param string $propertyName
4084
     * @param mixed $propertyValue
4085
     */
4086
    public function __set(string $propertyName, $propertyValue)
4087
    {
4088
        switch ($propertyName) {
4089
            case 'domainStartPage':
4090
                trigger_error('Property $TSFE->domainStartPage is not in use anymore as this information is now stored within the Site object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4091
                break;
4092
            case 'cHash':
4093
                trigger_error('Property $TSFE->cHash is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4094
                break;
4095
            case 'cHash_array':
4096
                trigger_error('Property $TSFE->cHash_array is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4097
                break;
4098
            case 'sys_language_isocode':
4099
                trigger_error('Property $TSFE->sys_language_isocode is not in use anymore as this information is now stored within the SiteLanguage object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4100
                break;
4101
            case 'divSection':
4102
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4103
                break;
4104
            case 'fePreview':
4105
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
4106
                $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, (bool)$propertyValue));
4107
                break;
4108
            case 'forceTemplateParsing':
4109
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
4110
                $this->context->setAspect('typoscript', GeneralUtility::makeInstance(TypoScriptAspect::class, (bool)$propertyValue));
4111
                break;
4112
        }
4113
        $this->$propertyName = $propertyValue;
4114
    }
4115
4116
    /**
4117
     * Unsets the property of the given name if tagged.
4118
     *
4119
     * @param string $propertyName
4120
     */
4121
    public function __unset(string $propertyName)
4122
    {
4123
        switch ($propertyName) {
4124
            case 'domainStartPage':
4125
                trigger_error('Property $TSFE->domainStartPage is not in use anymore as this information is now stored within the Site object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4126
                break;
4127
            case 'cHash':
4128
                trigger_error('Property $TSFE->cHash is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4129
                break;
4130
            case 'cHash_array':
4131
                trigger_error('Property $TSFE->cHash_array is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4132
                break;
4133
            case 'sys_language_isocode':
4134
                trigger_error('Property $TSFE->sys_language_isocode is not in use anymore as this information is now stored within the SiteLanguage object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4135
                break;
4136
            case 'divSection':
4137
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4138
                break;
4139
            case 'fePreview':
4140
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
4141
                $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, false));
4142
                break;
4143
            case 'forceTemplateParsing':
4144
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
4145
                $this->context->setAspect('typoscript', GeneralUtility::makeInstance(TypoScriptAspect::class, false));
4146
                break;
4147
        }
4148
        unset($this->$propertyName);
4149
    }
4150
}
4151