Completed
Push — master ( 1d0f8f...fad04d )
by
unknown
19:27 queued 15s
created

setPageCacheContent()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 27
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 19
c 2
b 0
f 0
nc 4
nop 3
dl 0
loc 27
rs 9.6333
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Frontend\Controller;
17
18
use Psr\Http\Message\ResponseInterface;
19
use Psr\Http\Message\ServerRequestInterface;
20
use Psr\Log\LoggerAwareInterface;
21
use Psr\Log\LoggerAwareTrait;
22
use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
23
use TYPO3\CMS\Core\Cache\CacheManager;
24
use TYPO3\CMS\Core\Charset\CharsetConverter;
25
use TYPO3\CMS\Core\Charset\UnknownCharsetException;
26
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
27
use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader;
28
use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser;
29
use TYPO3\CMS\Core\Context\Context;
30
use TYPO3\CMS\Core\Context\DateTimeAspect;
31
use TYPO3\CMS\Core\Context\LanguageAspect;
32
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
33
use TYPO3\CMS\Core\Context\TypoScriptAspect;
34
use TYPO3\CMS\Core\Context\UserAspect;
35
use TYPO3\CMS\Core\Context\VisibilityAspect;
36
use TYPO3\CMS\Core\Context\WorkspaceAspect;
37
use TYPO3\CMS\Core\Core\Environment;
38
use TYPO3\CMS\Core\Database\Connection;
39
use TYPO3\CMS\Core\Database\ConnectionPool;
40
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
41
use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
42
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
43
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
44
use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
45
use TYPO3\CMS\Core\Error\Http\ServiceUnavailableException;
46
use TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException;
47
use TYPO3\CMS\Core\Exception\Page\RootLineException;
48
use TYPO3\CMS\Core\Http\ImmediateResponseException;
49
use TYPO3\CMS\Core\Http\ServerRequestFactory;
50
use TYPO3\CMS\Core\Localization\LanguageService;
51
use TYPO3\CMS\Core\Localization\Locales;
52
use TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException;
53
use TYPO3\CMS\Core\Locking\LockFactory;
54
use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
55
use TYPO3\CMS\Core\Page\AssetCollector;
56
use TYPO3\CMS\Core\Page\PageRenderer;
57
use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager;
58
use TYPO3\CMS\Core\Resource\Exception;
59
use TYPO3\CMS\Core\Resource\StorageRepository;
60
use TYPO3\CMS\Core\Routing\PageArguments;
61
use TYPO3\CMS\Core\Site\Entity\Site;
62
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
63
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
64
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
65
use TYPO3\CMS\Core\Type\Bitmask\Permission;
66
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
67
use TYPO3\CMS\Core\TypoScript\TemplateService;
68
use TYPO3\CMS\Core\Utility\ArrayUtility;
69
use TYPO3\CMS\Core\Utility\GeneralUtility;
70
use TYPO3\CMS\Core\Utility\HttpUtility;
71
use TYPO3\CMS\Core\Utility\MathUtility;
72
use TYPO3\CMS\Core\Utility\PathUtility;
73
use TYPO3\CMS\Core\Utility\RootlineUtility;
74
use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
75
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
76
use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
77
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
78
use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
79
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
80
use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
81
82
/**
83
 * Class for the built TypoScript based frontend. Instantiated in
84
 * \TYPO3\CMS\Frontend\Http\RequestHandler as the global object TSFE.
85
 *
86
 * Main frontend class, instantiated in \TYPO3\CMS\Frontend\Http\RequestHandler
87
 * as the global object TSFE.
88
 *
89
 * This class has a lot of functions and internal variable which are used from
90
 * \TYPO3\CMS\Frontend\Http\RequestHandler
91
 *
92
 * The class is instantiated as $GLOBALS['TSFE'] in \TYPO3\CMS\Frontend\Http\RequestHandler.
93
 *
94
 * The use of this class should be inspired by the order of function calls as
95
 * found in \TYPO3\CMS\Frontend\Http\RequestHandler.
96
 */
97
class TypoScriptFrontendController implements LoggerAwareInterface
98
{
99
    use LoggerAwareTrait;
100
    use PublicPropertyDeprecationTrait;
101
102
    /**
103
     * @var string[]
104
     */
105
    private $deprecatedPublicProperties = [
106
        'imagesOnPage' => 'Using TSFE->imagesOnPage is deprecated and will no longer work with TYPO3 v11.0. Use AssetCollector()->getMedia() instead.',
107
        'lastImageInfo' => 'Using TSFE->lastImageInfo is deprecated and will no longer work with TYPO3 v11.0.'
108
    ];
109
110
    /**
111
     * The page id (int)
112
     * @var string
113
     */
114
    public $id = '';
115
116
    /**
117
     * The type (read-only)
118
     * @var int|string
119
     */
120
    public $type = '';
121
122
    /**
123
     * @var Site
124
     */
125
    protected $site;
126
127
    /**
128
     * @var SiteLanguage
129
     */
130
    protected $language;
131
132
    /**
133
     * The submitted cHash
134
     * @var string
135
     * @internal
136
     * @deprecated will be removed in TYPO3 v11.0. don't use it anymore, as this is now within the PageArguments property.
137
     */
138
    protected $cHash = '';
139
140
    /**
141
     * @var PageArguments
142
     * @internal
143
     */
144
    protected $pageArguments;
145
146
    /**
147
     * Page will not be cached. Write only TRUE. Never clear value (some other
148
     * code might have reasons to set it TRUE).
149
     * @var bool
150
     */
151
    public $no_cache = false;
152
153
    /**
154
     * The rootLine (all the way to tree root, not only the current site!)
155
     * @var array
156
     */
157
    public $rootLine = [];
158
159
    /**
160
     * The pagerecord
161
     * @var array
162
     */
163
    public $page = [];
164
165
    /**
166
     * This will normally point to the same value as id, but can be changed to
167
     * point to another page from which content will then be displayed instead.
168
     * @var int
169
     */
170
    public $contentPid = 0;
171
172
    /**
173
     * Gets set when we are processing a page of type mounpoint with enabled overlay in getPageAndRootline()
174
     * Used later in checkPageForMountpointRedirect() to determine the final target URL where the user
175
     * should be redirected to.
176
     *
177
     * @var array|null
178
     */
179
    protected $originalMountPointPage;
180
181
    /**
182
     * Gets set when we are processing a page of type shortcut in the early stages
183
     * of the request when we do not know about languages yet, used later in the request
184
     * to determine the correct shortcut in case a translation changes the shortcut
185
     * target
186
     * @var array|null
187
     * @see checkTranslatedShortcut()
188
     */
189
    protected $originalShortcutPage;
190
191
    /**
192
     * sys_page-object, pagefunctions
193
     *
194
     * @var PageRepository|string
195
     */
196
    public $sys_page = '';
197
198
    /**
199
     * Is set to 1 if a pageNotFound handler could have been called.
200
     * @var int
201
     * @internal
202
     */
203
    public $pageNotFound = 0;
204
205
    /**
206
     * Domain start page
207
     * @var int
208
     * @internal
209
     * @deprecated will be removed in TYPO3 v11.0. don't use it anymore, as this is now within the Site. see $this->site->getRootPageId()
210
     */
211
    protected $domainStartPage = 0;
212
213
    /**
214
     * Array containing a history of why a requested page was not accessible.
215
     * @var array
216
     */
217
    protected $pageAccessFailureHistory = [];
218
219
    /**
220
     * @var string
221
     * @internal
222
     */
223
    public $MP = '';
224
225
    /**
226
     * The frontend user
227
     *
228
     * @var FrontendUserAuthentication|string
229
     */
230
    public $fe_user = '';
231
232
    /**
233
     * Shows whether logins are allowed in branch
234
     * @var bool
235
     */
236
    protected $loginAllowedInBranch = true;
237
238
    /**
239
     * Shows specific mode (all or groups)
240
     * @var string
241
     * @internal
242
     */
243
    protected $loginAllowedInBranch_mode = '';
244
245
    /**
246
     * Flag indication that preview is active. This is based on the login of a
247
     * backend user and whether the backend user has read access to the current
248
     * page.
249
     * @var int
250
     * @internal
251
     * @deprecated will be removed in TYPO3 v11.0. don't use it anymore, as this is now within PreviewAspect
252
     */
253
    protected $fePreview = 0;
254
255
    /**
256
     * Value that contains the simulated usergroup if any
257
     * @var int
258
     * @internal only to be used in AdminPanel, and within TYPO3 Core
259
     */
260
    public $simUserGroup = 0;
261
262
    /**
263
     * "CONFIG" object from TypoScript. Array generated based on the TypoScript
264
     * configuration of the current page. Saved with the cached pages.
265
     * @var array
266
     */
267
    public $config = [];
268
269
    /**
270
     * The TypoScript template object. Used to parse the TypoScript template
271
     *
272
     * @var TemplateService
273
     */
274
    public $tmpl;
275
276
    /**
277
     * Is set to the time-to-live time of cached pages. Default is 60*60*24, which is 24 hours.
278
     *
279
     * @var int
280
     * @internal
281
     */
282
    protected $cacheTimeOutDefault = 86400;
283
284
    /**
285
     * Set internally if cached content is fetched from the database.
286
     *
287
     * @var bool
288
     * @internal
289
     */
290
    protected $cacheContentFlag = false;
291
292
    /**
293
     * Set to the expire time of cached content
294
     * @var int
295
     * @internal
296
     */
297
    protected $cacheExpires = 0;
298
299
    /**
300
     * Set if cache headers allowing caching are sent.
301
     * @var bool
302
     * @internal
303
     */
304
    protected $isClientCachable = false;
305
306
    /**
307
     * Used by template fetching system. This array is an identification of
308
     * the template. If $this->all is empty it's because the template-data is not
309
     * cached, which it must be.
310
     * @var array
311
     * @internal
312
     */
313
    public $all = [];
314
315
    /**
316
     * Toplevel - objArrayName, eg 'page'
317
     * @var string
318
     * @internal should only be used by TYPO3 Core
319
     */
320
    public $sPre = '';
321
322
    /**
323
     * TypoScript configuration of the page-object pointed to by sPre.
324
     * $this->tmpl->setup[$this->sPre.'.']
325
     * @var array|string
326
     * @internal should only be used by TYPO3 Core
327
     */
328
    public $pSetup = '';
329
330
    /**
331
     * This hash is unique to the template, the $this->id and $this->type vars and
332
     * the list of groups. Used to get and later store the cached data
333
     * @var string
334
     * @internal
335
     */
336
    public $newHash = '';
337
338
    /**
339
     * This flag is set before the page is generated IF $this->no_cache is set. If this
340
     * flag is set after the page content was generated, $this->no_cache is forced to be set.
341
     * This is done in order to make sure that PHP code from Plugins / USER scripts does not falsely
342
     * clear the no_cache flag.
343
     * @var bool
344
     * @internal
345
     */
346
    protected $no_cacheBeforePageGen = false;
347
348
    /**
349
     * Passed to TypoScript template class and tells it to force template rendering
350
     * @var bool
351
     * @deprecated
352
     */
353
    private $forceTemplateParsing = false;
354
355
    /**
356
     * The array which cHash_calc is based on, see PageArgumentValidator class.
357
     * @var array
358
     * @internal
359
     * @deprecated will be removed in TYPO3 v11.0. don't use it anymore, see getRelevantParametersForCachingFromPageArguments()
360
     */
361
    protected $cHash_array = [];
362
363
    /**
364
     * May be set to the pagesTSconfig
365
     * @var array|string
366
     * @internal
367
     */
368
    protected $pagesTSconfig = '';
369
370
    /**
371
     * Eg. insert JS-functions in this array ($additionalHeaderData) to include them
372
     * once. Use associative keys.
373
     *
374
     * Keys in use:
375
     *
376
     * used to accumulate additional HTML-code for the header-section,
377
     * <head>...</head>. Insert either associative keys (like
378
     * additionalHeaderData['myStyleSheet'], see reserved keys above) or num-keys
379
     * (like additionalHeaderData[] = '...')
380
     *
381
     * @var array
382
     */
383
    public $additionalHeaderData = [];
384
385
    /**
386
     * Used to accumulate additional HTML-code for the footer-section of the template
387
     * @var array
388
     */
389
    public $additionalFooterData = [];
390
391
    /**
392
     * Used to accumulate additional JavaScript-code. Works like
393
     * additionalHeaderData. Reserved keys at 'openPic' and 'mouseOver'
394
     *
395
     * @var array
396
     * @internal only used by TYPO3 Core, use AssetCollector or PageRenderer to add JavaScript
397
     */
398
    public $additionalJavaScript = [];
399
400
    /**
401
     * Used to accumulate additional Style code. Works like additionalHeaderData.
402
     *
403
     * @var array
404
     * @internal only used by TYPO3 Core, use AssetCollector or PageRenderer to add CSS
405
     */
406
    public $additionalCSS = [];
407
408
    /**
409
     * @var string
410
     * @internal only used by TYPO3 Core, use AssetCollector or PageRenderer to add inline JavaScript
411
     */
412
    public $JSCode;
413
414
    /**
415
     * @var string
416
     * @internal only used by TYPO3 Core, use AssetCollector or PageRenderer to add inline JavaScript
417
     */
418
    public $inlineJS;
419
420
    /**
421
     * Used to accumulate DHTML-layers.
422
     * @var string
423
     * @deprecated since TYPO3 v10.2, will be removed in TYPO3 v11, use custom USER_INT objects instead.
424
     */
425
    public $divSection = '';
426
427
    /**
428
     * Default internal target
429
     * @var string
430
     */
431
    public $intTarget = '';
432
433
    /**
434
     * Default external target
435
     * @var string
436
     */
437
    public $extTarget = '';
438
439
    /**
440
     * Default file link target
441
     * @var string
442
     */
443
    public $fileTarget = '';
444
445
    /**
446
     * If set, typolink() function encrypts email addresses.
447
     * @var string|int
448
     */
449
    public $spamProtectEmailAddresses = 0;
450
451
    /**
452
     * Absolute Reference prefix
453
     * @var string
454
     */
455
    public $absRefPrefix = '';
456
457
    /**
458
     * <A>-tag parameters
459
     * @var string
460
     */
461
    public $ATagParams = '';
462
463
    /**
464
     * Search word regex, calculated if there has been search-words send. This is
465
     * used to mark up the found search words on a page when jumped to from a link
466
     * in a search-result.
467
     * @var string
468
     * @internal
469
     */
470
    public $sWordRegEx = '';
471
472
    /**
473
     * Is set to the incoming array sword_list in case of a page-view jumped to from
474
     * a search-result.
475
     * @var string
476
     * @internal
477
     */
478
    public $sWordList = '';
479
480
    /**
481
     * A string prepared for insertion in all links on the page as url-parameters.
482
     * Based on configuration in TypoScript where you defined which GET_VARS you
483
     * would like to pass on.
484
     * @var string
485
     */
486
    public $linkVars = '';
487
488
    /**
489
     * If set, edit icons are rendered aside content records. Must be set only if
490
     * the ->beUserLogin flag is set and set_no_cache() must be called as well.
491
     * @var string
492
     */
493
    public $displayEditIcons = '';
494
495
    /**
496
     * If set, edit icons are rendered aside individual fields of content. Must be
497
     * set only if the ->beUserLogin flag is set and set_no_cache() must be called as
498
     * well.
499
     * @var string
500
     */
501
    public $displayFieldEditIcons = '';
502
503
    /**
504
     * Is set to the iso code of the current language
505
     * @var string
506
     * @deprecated will be removed in TYPO3 v11.0. don't use it anymore, as this is now within SiteLanguage->getTwoLetterIsoCode()
507
     */
508
    protected $sys_language_isocode = '';
509
510
    /**
511
     * 'Global' Storage for various applications. Keys should be 'tx_'.extKey for
512
     * extensions.
513
     * @var array
514
     */
515
    public $applicationData = [];
516
517
    /**
518
     * @var array
519
     */
520
    public $register = [];
521
522
    /**
523
     * Stack used for storing array and retrieving register arrays (see
524
     * LOAD_REGISTER and RESTORE_REGISTER)
525
     * @var array
526
     */
527
    public $registerStack = [];
528
529
    /**
530
     * Checking that the function is not called eternally. This is done by
531
     * interrupting at a depth of 50
532
     * @var int
533
     */
534
    public $cObjectDepthCounter = 50;
535
536
    /**
537
     * Used by RecordContentObject and ContentContentObject to ensure the a records is NOT
538
     * rendered twice through it!
539
     * @var array
540
     */
541
    public $recordRegister = [];
542
543
    /**
544
     * This is set to the [table]:[uid] of the latest record rendered. Note that
545
     * class ContentObjectRenderer has an equal value, but that is pointing to the
546
     * record delivered in the $data-array of the ContentObjectRenderer instance, if
547
     * the cObjects CONTENT or RECORD created that instance
548
     * @var string
549
     */
550
    public $currentRecord = '';
551
552
    /**
553
     * Used by class \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject
554
     * to keep track of access-keys.
555
     * @var array
556
     */
557
    public $accessKey = [];
558
559
    /**
560
     * Numerical array where image filenames are added if they are referenced in the
561
     * rendered document. This includes only TYPO3 generated/inserted images.
562
     * @var array
563
     * @deprecated
564
     */
565
    private $imagesOnPage = [];
566
567
    /**
568
     * Is set in ContentObjectRenderer->cImage() function to the info-array of the
569
     * most recent rendered image. The information is used in ImageTextContentObject
570
     * @var array
571
     * @deprecated
572
     */
573
    private $lastImageInfo = [];
574
575
    /**
576
     * Used to generate page-unique keys. Point is that uniqid() functions is very
577
     * slow, so a unikey key is made based on this, see function uniqueHash()
578
     * @var int
579
     * @internal
580
     */
581
    protected $uniqueCounter = 0;
582
583
    /**
584
     * @var string
585
     * @internal
586
     */
587
    protected $uniqueString = '';
588
589
    /**
590
     * This value will be used as the title for the page in the indexer (if
591
     * indexing happens)
592
     * @var string
593
     * @internal only used by TYPO3 Core, use PageTitle API instead.
594
     */
595
    public $indexedDocTitle = '';
596
597
    /**
598
     * The base URL set for the page header.
599
     * @var string
600
     */
601
    public $baseUrl = '';
602
603
    /**
604
     * Page content render object
605
     *
606
     * @var ContentObjectRenderer|string
607
     */
608
    public $cObj = '';
609
610
    /**
611
     * All page content is accumulated in this variable. See RequestHandler
612
     * @var string
613
     */
614
    public $content = '';
615
616
    /**
617
     * Output charset of the websites content. This is the charset found in the
618
     * header, meta tag etc. If different than utf-8 a conversion
619
     * happens before output to browser. Defaults to utf-8.
620
     * @var string
621
     */
622
    public $metaCharset = 'utf-8';
623
624
    /**
625
     * Internal calculations for labels
626
     *
627
     * @var LanguageService
628
     */
629
    protected $languageService;
630
631
    /**
632
     * @var LockingStrategyInterface[][]
633
     */
634
    protected $locks = [];
635
636
    /**
637
     * @var PageRenderer
638
     */
639
    protected $pageRenderer;
640
641
    /**
642
     * The page cache object, use this to save pages to the cache and to
643
     * retrieve them again
644
     *
645
     * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
646
     */
647
    protected $pageCache;
648
649
    /**
650
     * @var array
651
     */
652
    protected $pageCacheTags = [];
653
654
    /**
655
     * Content type HTTP header being sent in the request.
656
     * @todo Ticket: #63642 Should be refactored to a request/response model later
657
     * @internal Should only be used by TYPO3 core for now
658
     *
659
     * @var string
660
     */
661
    protected $contentType = 'text/html';
662
663
    /**
664
     * Doctype to use
665
     *
666
     * @var string
667
     */
668
    public $xhtmlDoctype = '';
669
670
    /**
671
     * @var int
672
     */
673
    public $xhtmlVersion;
674
675
    /**
676
     * Originally requested id from the initial $_GET variable
677
     *
678
     * @var int
679
     */
680
    protected $requestedId;
681
682
    /**
683
     * The context for keeping the current state, mostly related to current page information,
684
     * backend user / frontend user access, workspaceId
685
     *
686
     * @var Context
687
     */
688
    protected $context;
689
690
    /**
691
     * Since TYPO3 v10.0, TSFE is composed out of
692
     *  - Context
693
     *  - Site
694
     *  - SiteLanguage
695
     *  - PageArguments (containing ID, Type, cHash and MP arguments)
696
     *
697
     * With TYPO3 v11, they will become mandatory and the method arguments will become strongly typed.
698
     * For TYPO3 v10 this is built in a way to ensure maximum compatibility.
699
     *
700
     * Also sets a unique string (->uniqueString) for this script instance; A md5 hash of the microtime()
701
     *
702
     * @param Context|array|null $context the Context object to work on, previously defined to set TYPO3_CONF_VARS
703
     * @param mixed|SiteInterface $siteOrId The resolved site to work on, previously this was the value of GeneralUtility::_GP('id')
704
     * @param SiteLanguage|int|string $siteLanguageOrType The resolved language to work on, previously the value of GeneralUtility::_GP('type')
705
     * @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')
706
     * @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
707
     * @param string|null $_2 previously was used to define the jumpURL, use the PageArguments object instead, will be removed in TYPO3 v11.0
708
     * @param string|null $MP The value of GeneralUtility::_GP('MP'), use the PageArguments object instead, will be removed in TYPO3 v11.0
709
     */
710
    public function __construct($context = null, $siteOrId = null, $siteLanguageOrType = null, $pageArguments = null, $cHashOrFrontendUser = null, $_2 = null, $MP = null)
711
    {
712
        $this->initializeContextWithGlobalFallback($context);
713
714
        // Fetch the request for fetching data (site/language/pageArguments) for compatibility reasons, not needed
715
        // in TYPO3 v11.0 anymore.
716
        /** @var ServerRequestInterface $request */
717
        $request = $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals();
718
719
        $this->initializeSiteWithCompatibility($siteOrId, $request);
720
        $this->initializeSiteLanguageWithCompatibility($siteLanguageOrType, $request);
721
        $pageArguments = $this->buildPageArgumentsWithFallback($pageArguments, $request);
722
        $pageArguments = $this->initializeFrontendUserOrUpdateCHashArgument($cHashOrFrontendUser, $pageArguments);
723
        $pageArguments = $this->initializeLegacyMountPointArgument($MP, $pageArguments);
724
725
        $this->setPageArguments($pageArguments);
726
727
        $this->uniqueString = md5(microtime());
728
        $this->initPageRenderer();
729
        $this->initCaches();
730
        // Initialize LLL behaviour
731
        $this->setOutputLanguage();
732
    }
733
734
    /**
735
     * Various initialize methods used for fallback, which can be simplified in TYPO3 v11.0
736
     */
737
    /**
738
     * Used to set $this->context. The first argument was $GLOBALS[TYPO3_CONF_VARS] (array) until TYPO3 v8,
739
     * so no type hint possible.
740
     *
741
     * @param Context|array|null $context
742
     */
743
    private function initializeContextWithGlobalFallback($context): void
744
    {
745
        if ($context instanceof Context) {
746
            $this->context = $context;
747
        } else {
748
            // Use the global context for now
749
            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);
750
            $this->context = GeneralUtility::makeInstance(Context::class);
751
        }
752
        if (!$this->context->hasAspect('frontend.preview')) {
753
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class));
754
        }
755
    }
756
757
    /**
758
     * Second argument of the constructor. Until TYPO3 v10, this was the Page ID (int/string) but since TYPO3 v10.0
759
     * this can also be a SiteInterface object, which will be mandatory in TYPO3 v11.0. If no Site object is given,
760
     * this is fetched from the given request object.
761
     *
762
     * @param SiteInterface|int|string $siteOrId
763
     * @param ServerRequestInterface $request
764
     */
765
    private function initializeSiteWithCompatibility($siteOrId, ServerRequestInterface $request): void
766
    {
767
        if ($siteOrId instanceof SiteInterface) {
768
            $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...
769
        } else {
770
            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);
771
            $this->id = $siteOrId;
772
            if ($request->getAttribute('site') instanceof SiteInterface) {
773
                $this->site = $request->getAttribute('site');
774
            } else {
775
                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);
776
            }
777
        }
778
    }
779
780
    /**
781
     * Until TYPO3 v10.0, the third argument of the constructor was given from GET/POST "type" to define the page type
782
     * Since TYPO3 v10.0, this argument is requested to be of type SiteLanguage, which will be mandatory in TYPO3 v11.0.
783
     * If no SiteLanguage object is given, this is fetched from the given request object.
784
     *
785
     * @param SiteLanguage|int|string $siteLanguageOrType
786
     * @param ServerRequestInterface $request
787
     */
788
    private function initializeSiteLanguageWithCompatibility($siteLanguageOrType, ServerRequestInterface $request): void
789
    {
790
        if ($siteLanguageOrType instanceof SiteLanguage) {
791
            $this->language = $siteLanguageOrType;
792
        } else {
793
            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);
794
            $this->type = $siteLanguageOrType;
795
            if ($request->getAttribute('language') instanceof SiteLanguage) {
796
                $this->language = $request->getAttribute('language');
797
            } else {
798
                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);
799
            }
800
        }
801
    }
802
803
    /**
804
     * Since TYPO3 v10.0, the fourth constructor argument should be of type PageArguments. However, until TYPO3 v8,
805
     * this was the GET/POST parameter "no_cache". If no PageArguments object is given, the given request is checked
806
     * for the PageArguments.
807
     *
808
     * @param bool|string|PageArguments|null $pageArguments
809
     * @param ServerRequestInterface $request
810
     * @return PageArguments
811
     */
812
    private function buildPageArgumentsWithFallback($pageArguments, ServerRequestInterface $request): PageArguments
813
    {
814
        if ($pageArguments instanceof PageArguments) {
815
            return $pageArguments;
816
        }
817
        if ($request->getAttribute('routing') instanceof PageArguments) {
818
            return $request->getAttribute('routing');
819
        }
820
        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);
821
        $queryParams = $request->getQueryParams();
822
        $pageId = $this->id ?: ($queryParams['id'] ?? $request->getParsedBody()['id'] ?? 0);
823
        $pageType = $this->type ?: ($queryParams['type'] ?? $request->getParsedBody()['type'] ?? 0);
824
        return new PageArguments((int)$pageId, (string)$pageType, [], $queryParams);
825
    }
826
827
    /**
828
     * Since TYPO3 v10.0, the fifth constructor argument is expected to to be of Type FrontendUserAuthentication.
829
     * However, up until TYPO3 v9.5 this argument was used to define the "cHash" GET/POST parameter. In order to
830
     * ensure maximum compatibility, a deprecation is triggered if an old argument is still used, and PageArguments
831
     * are updated accordingly, and returned.
832
     *
833
     * @param string|FrontendUserAuthentication|null $cHashOrFrontendUser
834
     * @param PageArguments $pageArguments
835
     * @return PageArguments
836
     */
837
    private function initializeFrontendUserOrUpdateCHashArgument($cHashOrFrontendUser, PageArguments $pageArguments): PageArguments
838
    {
839
        if ($cHashOrFrontendUser === null) {
840
            return $pageArguments;
841
        }
842
        if ($cHashOrFrontendUser instanceof FrontendUserAuthentication) {
843
            $this->fe_user = $cHashOrFrontendUser;
844
            return $pageArguments;
845
        }
846
        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);
847
        return new PageArguments(
848
            $pageArguments->getPageId(),
849
            $pageArguments->getPageType(),
850
            $pageArguments->getRouteArguments(),
851
            array_replace_recursive($pageArguments->getStaticArguments(), ['cHash' => $cHashOrFrontendUser]),
852
            $pageArguments->getDynamicArguments()
853
        );
854
    }
855
856
    /**
857
     * Since TYPO3 v10.0 the seventh constructor argument is not needed anymore, as all data is already provided by
858
     * the given PageArguments object. However, if a specific MP parameter is given anyways, the PageArguments object
859
     * is updated and returned.
860
     *
861
     * @param string|null $MP
862
     * @param PageArguments $pageArguments
863
     * @return PageArguments
864
     */
865
    private function initializeLegacyMountPointArgument(?string $MP, PageArguments $pageArguments): PageArguments
866
    {
867
        if ($MP === null) {
868
            return $pageArguments;
869
        }
870
        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);
871
        if (!$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
872
            return $pageArguments;
873
        }
874
        return new PageArguments(
875
            $pageArguments->getPageId(),
876
            $pageArguments->getPageType(),
877
            $pageArguments->getRouteArguments(),
878
            array_replace_recursive($pageArguments->getStaticArguments(), ['MP' => $MP]),
879
            $pageArguments->getDynamicArguments()
880
        );
881
    }
882
883
    /**
884
     * Initializes the page renderer object
885
     */
886
    protected function initPageRenderer()
887
    {
888
        if ($this->pageRenderer !== null) {
889
            return;
890
        }
891
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
892
        $this->pageRenderer->setTemplateFile('EXT:frontend/Resources/Private/Templates/MainPage.html');
893
        // As initPageRenderer could be called in constructor and for USER_INTs, this information is only set
894
        // once - in order to not override any previous settings of PageRenderer.
895
        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...
896
            $this->pageRenderer->setLanguage($this->language->getTypo3Language());
897
        }
898
    }
899
900
    /**
901
     * @param string $contentType
902
     * @internal Should only be used by TYPO3 core for now
903
     */
904
    public function setContentType($contentType)
905
    {
906
        $this->contentType = $contentType;
907
    }
908
909
    /********************************************
910
     *
911
     * Initializing, resolving page id
912
     *
913
     ********************************************/
914
    /**
915
     * Initializes the caching system.
916
     */
917
    protected function initCaches()
918
    {
919
        $this->pageCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('pages');
920
    }
921
922
    /**
923
     * Initializes the front-end user groups.
924
     * Sets frontend.user aspect based on front-end user status.
925
     */
926
    public function initUserGroups()
927
    {
928
        $userGroups = [0];
929
        // This affects the hidden-flag selecting the fe_groups for the user!
930
        $this->fe_user->showHiddenRecords = $this->context->getPropertyFromAspect('visibility', 'includeHiddenContent', false);
931
        // no matter if we have an active user we try to fetch matching groups which can be set without an user (simulation for instance!)
932
        $this->fe_user->fetchGroupData();
933
        $isUserAndGroupSet = is_array($this->fe_user->user) && !empty($this->fe_user->groupData['uid']);
934
        if ($isUserAndGroupSet) {
935
            // group -2 is not an existing group, but denotes a 'default' group when a user IS logged in.
936
            // This is used to let elements be shown for all logged in users!
937
            $userGroups[] = -2;
938
            $groupsFromUserRecord = $this->fe_user->groupData['uid'];
939
        } else {
940
            // group -1 is not an existing group, but denotes a 'default' group when not logged in.
941
            // This is used to let elements be hidden, when a user is logged in!
942
            $userGroups[] = -1;
943
            if ($this->loginAllowedInBranch) {
944
                // 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.
945
                $groupsFromUserRecord = $this->fe_user->groupData['uid'];
946
            } else {
947
                // Set to blank since we will NOT risk any groups being set when no logins are allowed!
948
                $groupsFromUserRecord = [];
949
            }
950
        }
951
        // Clean up.
952
        // Make unique and sort the groups
953
        $groupsFromUserRecord = array_unique($groupsFromUserRecord);
954
        if (!empty($groupsFromUserRecord) && !$this->loginAllowedInBranch_mode) {
955
            sort($groupsFromUserRecord);
956
            $userGroups = array_merge($userGroups, array_map('intval', $groupsFromUserRecord));
957
        }
958
959
        $this->context->setAspect('frontend.user', GeneralUtility::makeInstance(UserAspect::class, $this->fe_user ?: null, $userGroups));
960
961
        // For every 60 seconds the is_online timestamp for a logged-in user is updated
962
        if ($isUserAndGroupSet) {
963
            $this->fe_user->updateOnlineTimestamp();
964
        }
965
966
        $this->logger->debug('Valid usergroups for TSFE: ' . implode(',', $userGroups));
967
    }
968
969
    /**
970
     * Checking if a user is logged in or a group constellation different from "0,-1"
971
     *
972
     * @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!)
973
     */
974
    public function isUserOrGroupSet()
975
    {
976
        /** @var UserAspect $userAspect */
977
        $userAspect = $this->context->getAspect('frontend.user');
978
        return $userAspect->isUserOrGroupSet();
979
    }
980
981
    /**
982
     * Clears the preview-flags, sets sim_exec_time to current time.
983
     * Hidden pages must be hidden as default, $GLOBALS['SIM_EXEC_TIME'] is set to $GLOBALS['EXEC_TIME']
984
     * in bootstrap initializeGlobalTimeVariables(). Alter it by adding or subtracting seconds.
985
     */
986
    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...
987
    {
988
        if ($this->context->getPropertyFromAspect('frontend.preview', 'isPreview')
989
            || $GLOBALS['EXEC_TIME'] !== $GLOBALS['SIM_EXEC_TIME']
990
            || $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages', false)
991
            || $this->context->getPropertyFromAspect('visibility', 'includeHiddenContent', false)
992
        ) {
993
            $GLOBALS['SIM_EXEC_TIME'] = $GLOBALS['EXEC_TIME'];
994
            $GLOBALS['SIM_ACCESS_TIME'] = $GLOBALS['ACCESS_TIME'];
995
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class));
996
            $this->context->setAspect('date', GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $GLOBALS['SIM_EXEC_TIME'])));
997
            $this->context->setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class));
998
        }
999
    }
1000
1001
    /**
1002
     * Checks if a backend user is logged in
1003
     *
1004
     * @return bool whether a backend user is logged in
1005
     */
1006
    public function isBackendUserLoggedIn()
1007
    {
1008
        return (bool)$this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false);
1009
    }
1010
1011
    /**
1012
     * Determines the id and evaluates any preview settings
1013
     * Basically this function is about determining whether a backend user is logged in,
1014
     * if he has read access to the page and if he's previewing the page.
1015
     * That all determines which id to show and how to initialize the id.
1016
     */
1017
    public function determineId()
1018
    {
1019
        // Call pre processing function for id determination
1020
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PreProcessing'] ?? [] as $functionReference) {
1021
            $parameters = ['parentObject' => $this];
1022
            GeneralUtility::callUserFunction($functionReference, $parameters, $this);
1023
        }
1024
        // If there is a Backend login we are going to check for any preview settings
1025
        $originalFrontendUserGroups = $this->applyPreviewSettings($this->getBackendUser());
1026
        // If the front-end is showing a preview, caching MUST be disabled.
1027
        $isPreview = $this->context->getPropertyFromAspect('frontend.preview', 'isPreview');
1028
        if ($isPreview) {
1029
            $this->disableCache();
1030
        }
1031
        // Now, get the id, validate access etc:
1032
        $this->fetch_the_id();
1033
        // Check if backend user has read access to this page. If not, recalculate the id.
1034
        if ($this->isBackendUserLoggedIn() && $isPreview && !$this->getBackendUser()->doesUserHaveAccess($this->page, Permission::PAGE_SHOW)) {
1035
            // Resetting
1036
            $this->clear_preview();
1037
            $this->fe_user->user[$this->fe_user->usergroup_column] = $originalFrontendUserGroups;
1038
            // Fetching the id again, now with the preview settings reset.
1039
            $this->fetch_the_id();
1040
        }
1041
        // Checks if user logins are blocked for a certain branch and if so, will unset user login and re-fetch ID.
1042
        $this->loginAllowedInBranch = $this->checkIfLoginAllowedInBranch();
1043
        // Logins are not allowed, but there is a login, so will we run this.
1044
        if (!$this->loginAllowedInBranch && $this->isUserOrGroupSet()) {
1045
            if ($this->loginAllowedInBranch_mode === 'all') {
1046
                // Clear out user and group:
1047
                $this->fe_user->hideActiveLogin();
1048
                $userGroups = [0, -1];
1049
            } else {
1050
                $userGroups = [0, -2];
1051
            }
1052
            $this->context->setAspect('frontend.user', GeneralUtility::makeInstance(UserAspect::class, $this->fe_user ?: null, $userGroups));
1053
            // Fetching the id again, now with the preview settings reset.
1054
            $this->fetch_the_id();
1055
        }
1056
        // Final cleaning.
1057
        // Make sure it's an integer
1058
        $this->id = ($this->contentPid = (int)$this->id);
1059
        // Make sure it's an integer
1060
        $this->type = (int)$this->type;
1061
        // Call post processing function for id determination:
1062
        $_params = ['pObj' => &$this];
1063
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc'] ?? [] as $_funcRef) {
1064
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1065
        }
1066
    }
1067
1068
    /**
1069
     * Evaluates admin panel or workspace settings to see if
1070
     * visibility settings like
1071
     * - Preview Aspect: isPreview
1072
     * - Visibility Aspect: includeHiddenPages
1073
     * - Visibility Aspect: includeHiddenContent
1074
     * - $simUserGroup
1075
     * should be applied to the current object.
1076
     *
1077
     * @param FrontendBackendUserAuthentication $backendUser
1078
     * @return string|null null if no changes to the current frontend usergroups have been made, otherwise the original list of frontend usergroups
1079
     * @internal
1080
     */
1081
    protected function applyPreviewSettings($backendUser = null)
1082
    {
1083
        if (!$backendUser) {
1084
            return null;
1085
        }
1086
        $originalFrontendUserGroup = null;
1087
        if ($this->fe_user->user) {
1088
            $originalFrontendUserGroup = $this->context->getPropertyFromAspect('frontend.user', 'groupIds');
1089
        }
1090
1091
        // The preview flag is set if the current page turns out to be hidden
1092
        if ($this->id && $this->determineIdIsHiddenPage()) {
1093
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, true));
1094
            /** @var VisibilityAspect $aspect */
1095
            $aspect = $this->context->getAspect('visibility');
1096
            $newAspect = GeneralUtility::makeInstance(VisibilityAspect::class, true, $aspect->includeHiddenContent(), $aspect->includeDeletedRecords());
1097
            $this->context->setAspect('visibility', $newAspect);
1098
        }
1099
        // The preview flag will be set if an offline workspace will be previewed
1100
        if ($this->whichWorkspace() > 0) {
1101
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, true));
1102
        }
1103
        return $this->context->getPropertyFromAspect('frontend.preview', 'preview', false) ? $originalFrontendUserGroup : null;
1104
    }
1105
1106
    /**
1107
     * Checks if the page is hidden in the active workspace.
1108
     * If it is hidden, preview flags will be set.
1109
     *
1110
     * @return bool
1111
     */
1112
    protected function determineIdIsHiddenPage()
1113
    {
1114
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1115
            ->getQueryBuilderForTable('pages');
1116
        $queryBuilder
1117
            ->getRestrictions()
1118
            ->removeAll()
1119
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1120
1121
        $queryBuilder
1122
            ->select('uid', 'hidden', 'starttime', 'endtime')
1123
            ->from('pages')
1124
            ->where(
1125
                $queryBuilder->expr()->gte('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
1126
            )
1127
            ->setMaxResults(1);
1128
1129
        // $this->id always points to the ID of the default language page, so we check
1130
        // the current site language to determine if we need to fetch a translation but consider fallbacks
1131
        if ($this->language->getLanguageId() > 0) {
1132
            $languagesToCheck = array_merge([$this->language->getLanguageId()], $this->language->getFallbackLanguageIds());
1133
            // Check for the language and all its fallbacks
1134
            $constraint = $queryBuilder->expr()->andX(
1135
                $queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)),
1136
                $queryBuilder->expr()->in('sys_language_uid', $queryBuilder->createNamedParameter(array_filter($languagesToCheck), Connection::PARAM_INT_ARRAY))
1137
            );
1138
            // If the fallback language Ids also contains the default language, this needs to be considered
1139
            if (in_array(0, $languagesToCheck, true)) {
1140
                $constraint = $queryBuilder->expr()->orX(
1141
                    $constraint,
1142
                    // Ensure to also fetch the default record
1143
                    $queryBuilder->expr()->andX(
1144
                        $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)),
1145
                        $queryBuilder->expr()->in('sys_language_uid', 0)
1146
                    )
1147
                );
1148
            }
1149
            // Ensure that the translated records are shown first (maxResults is set to 1)
1150
            $queryBuilder->orderBy('sys_language_uid', 'DESC');
1151
        } else {
1152
            $constraint = $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT));
1153
        }
1154
        $queryBuilder->andWhere($constraint);
1155
1156
        $page = $queryBuilder->execute()->fetch();
1157
1158
        if ($this->whichWorkspace() > 0) {
1159
            // Fetch overlay of page if in workspace and check if it is hidden
1160
            $customContext = clone $this->context;
1161
            $customContext->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $this->whichWorkspace()));
1162
            $customContext->setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class));
1163
            $pageSelectObject = GeneralUtility::makeInstance(PageRepository::class, $customContext);
1164
            $targetPage = $pageSelectObject->getWorkspaceVersionOfRecord($this->whichWorkspace(), 'pages', $page['uid']);
1165
            // Also checks if the workspace version is NOT hidden but the live version is in fact still hidden
1166
            $result = $targetPage === -1 || $targetPage === -2 || (is_array($targetPage) && $targetPage['hidden'] == 0 && $page['hidden'] == 1);
1167
        } else {
1168
            $result = is_array($page) && ($page['hidden'] || $page['starttime'] > $GLOBALS['SIM_EXEC_TIME'] || $page['endtime'] != 0 && $page['endtime'] <= $GLOBALS['SIM_EXEC_TIME']);
1169
        }
1170
        return $result;
1171
    }
1172
1173
    /**
1174
     * Resolves the page id and sets up several related properties.
1175
     *
1176
     * If $this->id is not set at all or is not a plain integer, the method
1177
     * does it's best to set the value to an integer. Resolving is based on
1178
     * this options:
1179
     *
1180
     * - Splitting $this->id if it contains an additional type parameter.
1181
     * - Finding the domain record start page
1182
     * - First visible page
1183
     * - Relocating the id below the domain record if outside
1184
     *
1185
     * The following properties may be set up or updated:
1186
     *
1187
     * - id
1188
     * - requestedId
1189
     * - type
1190
     * - sys_page
1191
     * - sys_page->where_groupAccess
1192
     * - sys_page->where_hid_del
1193
     * - Context: FrontendUser Aspect
1194
     * - no_cache
1195
     * - register['SYS_LASTCHANGED']
1196
     * - pageNotFound
1197
     *
1198
     * Via getPageAndRootlineWithDomain()
1199
     *
1200
     * - rootLine
1201
     * - page
1202
     * - MP
1203
     * - originalShortcutPage
1204
     * - originalMountPointPage
1205
     * - pageAccessFailureHistory['direct_access']
1206
     * - pageNotFound
1207
     *
1208
     * @todo:
1209
     *
1210
     * On the first impression the method does to much. This is increased by
1211
     * the fact, that is is called repeated times by the method determineId.
1212
     * The reasons are manifold.
1213
     *
1214
     * 1.) The first part, the creation of sys_page and the type
1215
     * resolution don't need to be repeated. They could be separated to be
1216
     * called only once.
1217
     *
1218
     * 2.) The user group setup could be done once on a higher level.
1219
     *
1220
     * 3.) The workflow of the resolution could be elaborated to be less
1221
     * tangled. Maybe the check of the page id to be below the domain via the
1222
     * root line doesn't need to be done each time, but for the final result
1223
     * only.
1224
     *
1225
     * 4.) The root line does not need to be directly addressed by this class.
1226
     * A root line is always related to one page. The rootline could be handled
1227
     * indirectly by page objects. Page objects still don't exist.
1228
     *
1229
     * @throws ServiceUnavailableException
1230
     * @internal
1231
     */
1232
    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...
1233
    {
1234
        $timeTracker = $this->getTimeTracker();
1235
        $timeTracker->push('fetch_the_id initialize/');
1236
        // Set the valid usergroups for FE
1237
        $this->initUserGroups();
1238
        // Initialize the PageRepository has to be done after the frontend usergroups are initialized / resolved, as
1239
        // frontend group aspect is modified before
1240
        $this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context);
1241
        // The id and type is set to the integer-value - just to be sure...
1242
        $this->id = (int)$this->id;
1243
        $this->type = (int)$this->type;
1244
        $timeTracker->pull();
1245
        // We find the first page belonging to the current domain
1246
        $timeTracker->push('fetch_the_id domain/');
1247
        if (!$this->id) {
1248
            // If the id was not previously set, set it to the root page id of the site.
1249
            $this->id = $this->site->getRootPageId();
1250
        }
1251
        $timeTracker->pull();
1252
        $timeTracker->push('fetch_the_id rootLine/');
1253
        // We store the originally requested id
1254
        $this->requestedId = $this->id;
1255
        try {
1256
            $this->getPageAndRootlineWithDomain($this->site->getRootPageId());
1257
        } catch (ShortcutTargetPageNotFoundException $e) {
1258
            $this->pageNotFound = 1;
1259
        }
1260
        $timeTracker->pull();
1261
        if ($this->pageNotFound) {
1262
            switch ($this->pageNotFound) {
1263
                case 1:
1264
                    $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
1265
                        $GLOBALS['TYPO3_REQUEST'],
1266
                        'ID was not an accessible page',
1267
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED)
1268
                    );
1269
                    break;
1270
                case 2:
1271
                    $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
1272
                        $GLOBALS['TYPO3_REQUEST'],
1273
                        'Subsection was found and not accessible',
1274
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED)
1275
                    );
1276
                    break;
1277
                case 3:
1278
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1279
                        $GLOBALS['TYPO3_REQUEST'],
1280
                        'ID was outside the domain',
1281
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH)
1282
                    );
1283
                    break;
1284
                default:
1285
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1286
                        $GLOBALS['TYPO3_REQUEST'],
1287
                        'Unspecified error',
1288
                        $this->getPageAccessFailureReasons()
1289
                    );
1290
            }
1291
            throw new ImmediateResponseException($response, 1533931329);
1292
        }
1293
1294
        $this->setRegisterValueForSysLastChanged($this->page);
1295
1296
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['fetchPageId-PostProcessing'] ?? [] as $functionReference) {
1297
            $parameters = ['parentObject' => $this];
1298
            GeneralUtility::callUserFunction($functionReference, $parameters, $this);
1299
        }
1300
    }
1301
1302
    /**
1303
     * Loads the page and root line records based on $this->id
1304
     *
1305
     * A final page and the matching root line are determined and loaded by
1306
     * the algorithm defined by this method.
1307
     *
1308
     * First it loads the initial page from the page repository for $this->id.
1309
     * If that can't be loaded directly, it gets the root line for $this->id.
1310
     * It walks up the root line towards the root page until the page
1311
     * repository can deliver a page record. (The loading restrictions of
1312
     * the root line records are more liberal than that of the page record.)
1313
     *
1314
     * Now the page type is evaluated and handled if necessary. If the page is
1315
     * a short cut, it is replaced by the target page. If the page is a mount
1316
     * point in overlay mode, the page is replaced by the mounted page.
1317
     *
1318
     * After this potential replacements are done, the root line is loaded
1319
     * (again) for this page record. It walks up the root line up to
1320
     * the first viewable record.
1321
     *
1322
     * (While upon the first accessibility check of the root line it was done
1323
     * by loading page by page from the page repository, this time the method
1324
     * checkRootlineForIncludeSection() is used to find the most distant
1325
     * accessible page within the root line.)
1326
     *
1327
     * Having found the final page id, the page record and the root line are
1328
     * loaded for last time by this method.
1329
     *
1330
     * Exceptions may be thrown for DOKTYPE_SPACER and not loadable page records
1331
     * or root lines.
1332
     *
1333
     * May set or update this properties:
1334
     *
1335
     * @see TypoScriptFrontendController::$id
1336
     * @see TypoScriptFrontendController::$MP
1337
     * @see TypoScriptFrontendController::$page
1338
     * @see TypoScriptFrontendController::$pageNotFound
1339
     * @see TypoScriptFrontendController::$pageAccessFailureHistory
1340
     * @see TypoScriptFrontendController::$originalMountPointPage
1341
     * @see TypoScriptFrontendController::$originalShortcutPage
1342
     *
1343
     * @throws ServiceUnavailableException
1344
     * @throws PageNotFoundException
1345
     */
1346
    protected function getPageAndRootline()
1347
    {
1348
        $requestedPageRowWithoutGroupCheck = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $requestedPageRowWithoutGroupCheck is dead and can be removed.
Loading history...
1349
        $this->resolveTranslatedPageId();
1350
        if (empty($this->page)) {
1351
            // If no page, we try to find the page before in the rootLine.
1352
            // Page is 'not found' in case the id itself was not an accessible page. code 1
1353
            $this->pageNotFound = 1;
1354
            try {
1355
                $requestedPageRowWithoutGroupCheck = $this->sys_page->getPage($this->id, true);
1356
                if (!empty($requestedPageRowWithoutGroupCheck)) {
1357
                    $this->pageAccessFailureHistory['direct_access'][] = $requestedPageRowWithoutGroupCheck;
1358
                }
1359
                $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1360
                if (!empty($this->rootLine)) {
1361
                    $c = count($this->rootLine) - 1;
1362
                    while ($c > 0) {
1363
                        // Add to page access failure history:
1364
                        $this->pageAccessFailureHistory['direct_access'][] = $this->rootLine[$c];
1365
                        // Decrease to next page in rootline and check the access to that, if OK, set as page record and ID value.
1366
                        $c--;
1367
                        $this->id = $this->rootLine[$c]['uid'];
1368
                        $this->page = $this->sys_page->getPage($this->id);
1369
                        if (!empty($this->page)) {
1370
                            break;
1371
                        }
1372
                    }
1373
                }
1374
            } catch (RootLineException $e) {
1375
                $this->rootLine = [];
1376
            }
1377
            // If still no page...
1378
            if (empty($requestedPageRowWithoutGroupCheck) && empty($this->page)) {
1379
                $message = 'The requested page does not exist!';
1380
                $this->logger->error($message);
1381
                try {
1382
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1383
                        $GLOBALS['TYPO3_REQUEST'],
1384
                        $message,
1385
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND)
1386
                    );
1387
                    throw new ImmediateResponseException($response, 1533931330);
1388
                } catch (PageNotFoundException $e) {
1389
                    throw new PageNotFoundException($message, 1301648780);
1390
                }
1391
            }
1392
        }
1393
        // Spacer and sysfolders is not accessible in frontend
1394
        if ($this->page['doktype'] == PageRepository::DOKTYPE_SPACER || $this->page['doktype'] == PageRepository::DOKTYPE_SYSFOLDER) {
1395
            $message = 'The requested page does not exist!';
1396
            $this->logger->error($message);
1397
            try {
1398
                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1399
                    $GLOBALS['TYPO3_REQUEST'],
1400
                    $message,
1401
                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_INVALID_PAGETYPE)
1402
                );
1403
                throw new ImmediateResponseException($response, 1533931343);
1404
            } catch (PageNotFoundException $e) {
1405
                throw new PageNotFoundException($message, 1301648781);
1406
            }
1407
        }
1408
        // Is the ID a link to another page??
1409
        if ($this->page['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
1410
            // 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.
1411
            $this->MP = '';
1412
            // saving the page so that we can check later - when we know
1413
            // about languages - whether we took the correct shortcut or
1414
            // whether a translation of the page overwrites the shortcut
1415
            // target and we need to follow the new target
1416
            $this->originalShortcutPage = $this->page;
1417
            $this->page = $this->sys_page->getPageShortcut($this->page['shortcut'], $this->page['shortcut_mode'], $this->page['uid']);
1418
            $this->id = $this->page['uid'];
1419
        }
1420
        // If the page is a mountpoint which should be overlaid with the contents of the mounted page,
1421
        // it must never be accessible directly, but only in the mountpoint context. Therefore we change
1422
        // the current ID and the user is redirected by checkPageForMountpointRedirect().
1423
        if ($this->page['doktype'] == PageRepository::DOKTYPE_MOUNTPOINT && $this->page['mount_pid_ol']) {
1424
            $this->originalMountPointPage = $this->page;
1425
            $this->page = $this->sys_page->getPage($this->page['mount_pid']);
1426
            if (empty($this->page)) {
1427
                $message = 'This page (ID ' . $this->originalMountPointPage['uid'] . ') is of type "Mount point" and '
1428
                    . 'mounts a page which is not accessible (ID ' . $this->originalMountPointPage['mount_pid'] . ').';
1429
                throw new PageNotFoundException($message, 1402043263);
1430
            }
1431
            // If the current page is a shortcut, the MP parameter will be replaced
1432
            if ($this->MP === '' || !empty($this->originalShortcutPage)) {
1433
                $this->MP = $this->page['uid'] . '-' . $this->originalMountPointPage['uid'];
1434
            } else {
1435
                $this->MP .= ',' . $this->page['uid'] . '-' . $this->originalMountPointPage['uid'];
1436
            }
1437
            $this->id = $this->page['uid'];
1438
        }
1439
        // Gets the rootLine
1440
        try {
1441
            $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1442
        } catch (RootLineException $e) {
1443
            $this->rootLine = [];
1444
        }
1445
        // If not rootline we're off...
1446
        if (empty($this->rootLine)) {
1447
            $message = 'The requested page didn\'t have a proper connection to the tree-root!';
1448
            $this->logger->error($message);
1449
            try {
1450
                $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
1451
                    $GLOBALS['TYPO3_REQUEST'],
1452
                    $message,
1453
                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ROOTLINE_BROKEN)
1454
                );
1455
                throw new ImmediateResponseException($response, 1533931350);
1456
            } catch (ServiceUnavailableException $e) {
1457
                throw new ServiceUnavailableException($message, 1301648167);
1458
            }
1459
        }
1460
        // Checking for include section regarding the hidden/starttime/endtime/fe_user (that is access control of a whole subbranch!)
1461
        if ($this->checkRootlineForIncludeSection()) {
1462
            if (empty($this->rootLine)) {
1463
                $message = 'The requested page was not accessible!';
1464
                try {
1465
                    $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
1466
                        $GLOBALS['TYPO3_REQUEST'],
1467
                        $message,
1468
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_GENERAL)
1469
                    );
1470
                    throw new ImmediateResponseException($response, 1533931351);
1471
                } catch (ServiceUnavailableException $e) {
1472
                    $this->logger->warning($message);
1473
                    throw new ServiceUnavailableException($message, 1301648234);
1474
                }
1475
            } else {
1476
                $el = reset($this->rootLine);
1477
                $this->id = $el['uid'];
1478
                $this->page = $this->sys_page->getPage($this->id);
1479
                try {
1480
                    $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1481
                } catch (RootLineException $e) {
1482
                    $this->rootLine = [];
1483
                }
1484
            }
1485
        }
1486
    }
1487
1488
    /**
1489
     * If $this->id contains a translated page record, this needs to be resolved to the default language
1490
     * in order for all rootline functionality and access restrictions to be in place further on.
1491
     *
1492
     * Additionally, if a translated page is found, LanguageAspect is set as well.
1493
     */
1494
    protected function resolveTranslatedPageId()
1495
    {
1496
        $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

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

3621
                $midnightTime = mktime(0, 0, 0, /** @scrutinizer ignore-type */ date('m', $timeOutTime), 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

3621
                $midnightTime = mktime(0, 0, 0, date('m', $timeOutTime), date('d', $timeOutTime), /** @scrutinizer ignore-type */ 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

3621
                $midnightTime = mktime(0, 0, 0, date('m', $timeOutTime), /** @scrutinizer ignore-type */ date('d', $timeOutTime), date('Y', $timeOutTime));
Loading history...
3622
                // If the midnight time of the expire-day is greater than the current time,
3623
                // we may set the timeOutTime to the new midnighttime.
3624
                if ($midnightTime > $GLOBALS['EXEC_TIME']) {
3625
                    $cacheTimeout = $midnightTime - $GLOBALS['EXEC_TIME'];
3626
                }
3627
            }
3628
3629
            // Calculate the timeout time for records on the page and adjust cache timeout if necessary
3630
            $cacheTimeout = min($this->calculatePageCacheTimeout(), $cacheTimeout);
3631
3632
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['get_cache_timeout'] ?? [] as $_funcRef) {
3633
                $params = ['cacheTimeout' => $cacheTimeout];
3634
                $cacheTimeout = GeneralUtility::callUserFunction($_funcRef, $params, $this);
3635
            }
3636
            $runtimeCache->set($cachedCacheLifetimeIdentifier, $cacheTimeout);
3637
            $cachedCacheLifetime = $cacheTimeout;
3638
        }
3639
        return $cachedCacheLifetime;
3640
    }
3641
3642
    /*********************************************
3643
     *
3644
     * Localization and character set conversion
3645
     *
3646
     *********************************************/
3647
    /**
3648
     * Split Label function for front-end applications.
3649
     *
3650
     * @param string $input Key string. Accepts the "LLL:" prefix.
3651
     * @return string Label value, if any.
3652
     */
3653
    public function sL($input)
3654
    {
3655
        return $this->languageService->sL($input);
3656
    }
3657
3658
    /**
3659
     * Sets all internal measures what language the page should be rendered.
3660
     * This is not for records, but rather the HTML / charset and the locallang labels
3661
     */
3662
    protected function setOutputLanguage()
3663
    {
3664
        $this->languageService = LanguageService::createFromSiteLanguage($this->language);
3665
        // Always disable debugging for TSFE
3666
        $this->languageService->debugKey = false;
3667
    }
3668
3669
    /**
3670
     * Converts input string from utf-8 to metaCharset IF the two charsets are different.
3671
     *
3672
     * @param string $content Content to be converted.
3673
     * @return string Converted content string.
3674
     * @throws \RuntimeException if an invalid charset was configured
3675
     */
3676
    public function convOutputCharset($content)
3677
    {
3678
        if ($this->metaCharset !== 'utf-8') {
3679
            /** @var CharsetConverter $charsetConverter */
3680
            $charsetConverter = GeneralUtility::makeInstance(CharsetConverter::class);
3681
            try {
3682
                $content = $charsetConverter->conv($content, 'utf-8', $this->metaCharset);
3683
            } catch (UnknownCharsetException $e) {
3684
                throw new \RuntimeException('Invalid config.metaCharset: ' . $e->getMessage(), 1508916185);
3685
            }
3686
        }
3687
        return $content;
3688
    }
3689
3690
    /**
3691
     * Calculates page cache timeout according to the records with starttime/endtime on the page.
3692
     *
3693
     * @return int Page cache timeout or PHP_INT_MAX if cannot be determined
3694
     */
3695
    protected function calculatePageCacheTimeout()
3696
    {
3697
        $result = PHP_INT_MAX;
3698
        // Get the configuration
3699
        $tablesToConsider = $this->getCurrentPageCacheConfiguration();
3700
        // Get the time, rounded to the minute (do not pollute MySQL cache!)
3701
        // It is ok that we do not take seconds into account here because this
3702
        // value will be subtracted later. So we never get the time "before"
3703
        // the cache change.
3704
        $now = $GLOBALS['ACCESS_TIME'];
3705
        // Find timeout by checking every table
3706
        foreach ($tablesToConsider as $tableDef) {
3707
            $result = min($result, $this->getFirstTimeValueForRecord($tableDef, $now));
3708
        }
3709
        // We return + 1 second just to ensure that cache is definitely regenerated
3710
        return $result === PHP_INT_MAX ? PHP_INT_MAX : $result - $now + 1;
3711
    }
3712
3713
    /**
3714
     * Obtains a list of table/pid pairs to consider for page caching.
3715
     *
3716
     * TS configuration looks like this:
3717
     *
3718
     * The cache lifetime of all pages takes starttime and endtime of news records of page 14 into account:
3719
     * config.cache.all = tt_news:14
3720
     *
3721
     * The cache.lifetime of the current page allows to take records (e.g. fe_users) into account:
3722
     * config.cache.all = fe_users:current
3723
     *
3724
     * The cache lifetime of page 42 takes starttime and endtime of news records of page 15 and addresses of page 16 into account:
3725
     * config.cache.42 = tt_news:15,tt_address:16
3726
     *
3727
     * @return array Array of 'tablename:pid' pairs. There is at least a current page id in the array
3728
     * @see TypoScriptFrontendController::calculatePageCacheTimeout()
3729
     */
3730
    protected function getCurrentPageCacheConfiguration()
3731
    {
3732
        $result = ['tt_content:' . $this->id];
3733
        if (isset($this->config['config']['cache.'][$this->id])) {
3734
            $result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $this->id, $this->config['config']['cache.'][$this->id])));
3735
        }
3736
        if (isset($this->config['config']['cache.']['all'])) {
3737
            $result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $this->id, $this->config['config']['cache.']['all'])));
3738
        }
3739
        return array_unique($result);
3740
    }
3741
3742
    /**
3743
     * Find the minimum starttime or endtime value in the table and pid that is greater than the current time.
3744
     *
3745
     * @param string $tableDef Table definition (format tablename:pid)
3746
     * @param int $now "Now" time value
3747
     * @throws \InvalidArgumentException
3748
     * @return int Value of the next start/stop time or PHP_INT_MAX if not found
3749
     * @see TypoScriptFrontendController::calculatePageCacheTimeout()
3750
     */
3751
    protected function getFirstTimeValueForRecord($tableDef, $now)
3752
    {
3753
        $now = (int)$now;
3754
        $result = PHP_INT_MAX;
3755
        [$tableName, $pid] = GeneralUtility::trimExplode(':', $tableDef);
3756
        if (empty($tableName) || empty($pid)) {
3757
            throw new \InvalidArgumentException('Unexpected value for parameter $tableDef. Expected <tablename>:<pid>, got \'' . htmlspecialchars($tableDef) . '\'.', 1307190365);
3758
        }
3759
3760
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
3761
            ->getQueryBuilderForTable($tableName);
3762
        $queryBuilder->getRestrictions()
3763
            ->removeByType(StartTimeRestriction::class)
3764
            ->removeByType(EndTimeRestriction::class);
3765
        $timeFields = [];
3766
        $timeConditions = $queryBuilder->expr()->orX();
3767
        foreach (['starttime', 'endtime'] as $field) {
3768
            if (isset($GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field])) {
3769
                $timeFields[$field] = $GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field];
3770
                $queryBuilder->addSelectLiteral(
3771
                    'MIN('
3772
                        . 'CASE WHEN '
3773
                        . $queryBuilder->expr()->lte(
3774
                            $timeFields[$field],
3775
                            $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)
3776
                        )
3777
                        . ' THEN NULL ELSE ' . $queryBuilder->quoteIdentifier($timeFields[$field]) . ' END'
3778
                        . ') AS ' . $queryBuilder->quoteIdentifier($timeFields[$field])
3779
                );
3780
                $timeConditions->add(
3781
                    $queryBuilder->expr()->gt(
3782
                        $timeFields[$field],
3783
                        $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)
3784
                    )
3785
                );
3786
            }
3787
        }
3788
3789
        // if starttime or endtime are defined, evaluate them
3790
        if (!empty($timeFields)) {
3791
            // find the timestamp, when the current page's content changes the next time
3792
            $row = $queryBuilder
3793
                ->from($tableName)
3794
                ->where(
3795
                    $queryBuilder->expr()->eq(
3796
                        'pid',
3797
                        $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
3798
                    ),
3799
                    $timeConditions
3800
                )
3801
                ->execute()
3802
                ->fetch();
3803
3804
            if ($row) {
3805
                foreach ($timeFields as $timeField => $_) {
3806
                    // if a MIN value is found, take it into account for the
3807
                    // cache lifetime we have to filter out start/endtimes < $now,
3808
                    // as the SQL query also returns rows with starttime < $now
3809
                    // and endtime > $now (and using a starttime from the past
3810
                    // would be wrong)
3811
                    if ($row[$timeField] !== null && (int)$row[$timeField] > $now) {
3812
                        $result = min($result, (int)$row[$timeField]);
3813
                    }
3814
                }
3815
            }
3816
        }
3817
3818
        return $result;
3819
    }
3820
3821
    /**
3822
     * Fetches the originally requested id, falls back to $this->id
3823
     *
3824
     * @return int the originally requested page uid
3825
     * @see fetch_the_id()
3826
     */
3827
    public function getRequestedId()
3828
    {
3829
        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...
3830
    }
3831
3832
    /**
3833
     * Acquire a page specific lock
3834
     *
3835
     *
3836
     * The schematics here is:
3837
     * - First acquire an access lock. This is using the type of the requested lock as key.
3838
     *   Since the number of types is rather limited we can use the type as key as it will only
3839
     *   eat up a limited number of lock resources on the system (files, semaphores)
3840
     * - Second, we acquire the actual lock (named page lock). We can be sure we are the only process at this
3841
     *   very moment, hence we either get the lock for the given key or we get an error as we request a non-blocking mode.
3842
     *
3843
     * Interleaving two locks is extremely important, because the actual page lock uses a hash value as key (see callers
3844
     * of this function). If we would simply employ a normal blocking lock, we would get a potentially unlimited
3845
     * (number of pages at least) number of different locks. Depending on the available locking methods on the system
3846
     * we might run out of available resources. (e.g. maximum limit of semaphores is a system setting and applies
3847
     * to the whole system)
3848
     * We therefore must make sure that page locks are destroyed again if they are not used anymore, such that
3849
     * we never use more locking resources than parallel requests to different pages (hashes).
3850
     * In order to ensure this, we need to guarantee that no other process is waiting on a page lock when
3851
     * the process currently having the lock on the page lock is about to release the lock again.
3852
     * This can only be achieved by using a non-blocking mode, such that a process is never put into wait state
3853
     * by the kernel, but only checks the availability of the lock. The access lock is our guard to be sure
3854
     * that no two processes are at the same time releasing/destroying a page lock, whilst the other one tries to
3855
     * get a lock for this page lock.
3856
     * The only drawback of this implementation is that we basically have to poll the availability of the page lock.
3857
     *
3858
     * Note that the access lock resources are NEVER deleted/destroyed, otherwise the whole thing would be broken.
3859
     *
3860
     * @param string $type
3861
     * @param string $key
3862
     * @throws \InvalidArgumentException
3863
     * @throws \RuntimeException
3864
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
3865
     */
3866
    protected function acquireLock($type, $key)
3867
    {
3868
        $lockFactory = GeneralUtility::makeInstance(LockFactory::class);
3869
        $this->locks[$type]['accessLock'] = $lockFactory->createLocker($type);
3870
3871
        $this->locks[$type]['pageLock'] = $lockFactory->createLocker(
3872
            $key,
3873
            LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
3874
        );
3875
3876
        do {
3877
            if (!$this->locks[$type]['accessLock']->acquire()) {
3878
                throw new \RuntimeException('Could not acquire access lock for "' . $type . '"".', 1294586098);
3879
            }
3880
3881
            try {
3882
                $locked = $this->locks[$type]['pageLock']->acquire(
3883
                    LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
3884
                );
3885
            } catch (LockAcquireWouldBlockException $e) {
3886
                // somebody else has the lock, we keep waiting
3887
3888
                // first release the access lock
3889
                $this->locks[$type]['accessLock']->release();
3890
                // now lets make a short break (100ms) until we try again, since
3891
                // the page generation by the lock owner will take a while anyways
3892
                usleep(100000);
3893
                continue;
3894
            }
3895
            $this->locks[$type]['accessLock']->release();
3896
            if ($locked) {
3897
                break;
3898
            }
3899
            throw new \RuntimeException('Could not acquire page lock for ' . $key . '.', 1460975877);
3900
        } while (true);
3901
    }
3902
3903
    /**
3904
     * Release a page specific lock
3905
     *
3906
     * @param string $type
3907
     * @throws \InvalidArgumentException
3908
     * @throws \RuntimeException
3909
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
3910
     */
3911
    protected function releaseLock($type)
3912
    {
3913
        if ($this->locks[$type]['accessLock']) {
3914
            if (!$this->locks[$type]['accessLock']->acquire()) {
3915
                throw new \RuntimeException('Could not acquire access lock for "' . $type . '"".', 1460975902);
3916
            }
3917
3918
            $this->locks[$type]['pageLock']->release();
3919
            $this->locks[$type]['pageLock']->destroy();
3920
            $this->locks[$type]['pageLock'] = null;
3921
3922
            $this->locks[$type]['accessLock']->release();
3923
            $this->locks[$type]['accessLock'] = null;
3924
        }
3925
    }
3926
3927
    /**
3928
     * Send additional headers from config.additionalHeaders
3929
     */
3930
    protected function getAdditionalHeaders(): array
3931
    {
3932
        if (!isset($this->config['config']['additionalHeaders.'])) {
3933
            return [];
3934
        }
3935
        $additionalHeaders = [];
3936
        ksort($this->config['config']['additionalHeaders.']);
3937
        foreach ($this->config['config']['additionalHeaders.'] as $options) {
3938
            if (!is_array($options)) {
3939
                continue;
3940
            }
3941
            $header = trim($options['header'] ?? '');
3942
            if ($header === '') {
3943
                continue;
3944
            }
3945
            $additionalHeaders[] = [
3946
                'header' => $header,
3947
                // "replace existing headers" is turned on by default, unless turned off
3948
                'replace' => ($options['replace'] ?? '') !== '0',
3949
                'statusCode' => (int)($options['httpResponseCode'] ?? 0) ?: null
3950
            ];
3951
        }
3952
        return $additionalHeaders;
3953
    }
3954
3955
    /**
3956
     * Returns the current BE user.
3957
     *
3958
     * @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication
3959
     */
3960
    protected function getBackendUser()
3961
    {
3962
        return $GLOBALS['BE_USER'];
3963
    }
3964
3965
    /**
3966
     * @return TimeTracker
3967
     */
3968
    protected function getTimeTracker()
3969
    {
3970
        return GeneralUtility::makeInstance(TimeTracker::class);
3971
    }
3972
3973
    /**
3974
     * Return the global instance of this class.
3975
     *
3976
     * Intended to be used as prototype factory for this class, see Services.yaml.
3977
     * This is required as long as TypoScriptFrontendController needs request
3978
     * dependent constructor parameters. Once that has been refactored this
3979
     * factory will be removed.
3980
     *
3981
     * @return TypoScriptFrontendController
3982
     * @internal
3983
     */
3984
    public static function getGlobalInstance(): ?self
3985
    {
3986
        if ($GLOBALS['TSFE'] instanceof self) {
3987
            return $GLOBALS['TSFE'];
3988
        }
3989
3990
        if (!(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_FE)) {
3991
            // Return null for now (together with shared: false in Services.yaml) as TSFE might not be available in backend context
3992
            // That's not an error then
3993
            return null;
3994
        }
3995
3996
        throw new \LogicException('TypoScriptFrontendController was tried to be injected before initial creation', 1538370377);
3997
    }
3998
3999
    public function getLanguage(): SiteLanguage
4000
    {
4001
        return $this->language;
4002
    }
4003
4004
    public function getSite(): Site
4005
    {
4006
        return $this->site;
4007
    }
4008
4009
    public function getContext(): Context
4010
    {
4011
        return $this->context;
4012
    }
4013
4014
    public function getPageArguments(): PageArguments
4015
    {
4016
        return $this->pageArguments;
4017
    }
4018
4019
    /**
4020
     * Deprecation messages for TYPO3 10 - public properties of TSFE which have been (re)moved
4021
     */
4022
    /**
4023
     * Checks if the property of the given name is set.
4024
     *
4025
     * Unmarked protected properties must return false as usual.
4026
     * Marked properties are evaluated by isset().
4027
     *
4028
     * This method is not called for public properties.
4029
     *
4030
     * @param string $propertyName
4031
     * @return bool
4032
     */
4033
    public function __isset(string $propertyName)
4034
    {
4035
        switch ($propertyName) {
4036
            case 'domainStartPage':
4037
                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);
4038
                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...
4039
            case 'cHash':
4040
                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);
4041
                return isset($this->pageArguments->getArguments()['cHash']);
4042
            case 'cHash_array':
4043
                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);
4044
                $value = $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments);
4045
                return !empty($value);
4046
            case 'sys_language_isocode':
4047
                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);
4048
                return isset($this->$propertyName);
4049
            case 'divSection':
4050
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4051
                return isset($this->$propertyName);
4052
            case 'fePreview':
4053
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
4054
                return $this->context->hasAspect('frontend.preview');
4055
            case 'forceTemplateParsing':
4056
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
4057
                return $this->context->hasAspect('typoscript') && $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing');
4058
        }
4059
        return false;
4060
    }
4061
4062
    /**
4063
     * Gets the value of the property of the given name if tagged.
4064
     *
4065
     * The evaluation is done in the assumption that this method is never
4066
     * reached for a public property.
4067
     *
4068
     * @param string $propertyName
4069
     * @return mixed
4070
     */
4071
    public function __get(string $propertyName)
4072
    {
4073
        switch ($propertyName) {
4074
            case 'domainStartPage':
4075
                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);
4076
                return $this->site->getRootPageId();
4077
            case 'cHash':
4078
                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);
4079
                return $this->pageArguments->getArguments()['cHash'] ?? false;
4080
            case 'cHash_array':
4081
                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);
4082
                return $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments);
4083
            case 'sys_language_isocode':
4084
                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);
4085
                return $this->sys_language_isocode ?? $this->language->getTwoLetterIsoCode();
4086
            case 'divSection':
4087
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4088
                break;
4089
            case 'fePreview':
4090
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
4091
                if ($this->context->hasAspect('frontend.preview')) {
4092
                    return $this->context->getPropertyFromAspect('frontend.preview', 'isPreview');
4093
                }
4094
                break;
4095
            case 'forceTemplateParsing':
4096
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
4097
                if ($this->context->hasAspect('typoscript')) {
4098
                    return $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing');
4099
                }
4100
                break;
4101
        }
4102
        return $this->$propertyName;
4103
    }
4104
4105
    /**
4106
     * Sets the property of the given name if tagged.
4107
     *
4108
     * Additionally it's allowed to set unknown properties.
4109
     *
4110
     * The evaluation is done in the assumption that this method is never
4111
     * reached for a public property.
4112
     *
4113
     * @param string $propertyName
4114
     * @param mixed $propertyValue
4115
     */
4116
    public function __set(string $propertyName, $propertyValue)
4117
    {
4118
        switch ($propertyName) {
4119
            case 'domainStartPage':
4120
                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);
4121
                break;
4122
            case 'cHash':
4123
                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);
4124
                break;
4125
            case 'cHash_array':
4126
                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);
4127
                break;
4128
            case 'sys_language_isocode':
4129
                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);
4130
                break;
4131
            case 'divSection':
4132
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4133
                break;
4134
            case 'fePreview':
4135
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
4136
                $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, (bool)$propertyValue));
4137
                break;
4138
            case 'forceTemplateParsing':
4139
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
4140
                $this->context->setAspect('typoscript', GeneralUtility::makeInstance(TypoScriptAspect::class, (bool)$propertyValue));
4141
                break;
4142
        }
4143
        $this->$propertyName = $propertyValue;
4144
    }
4145
4146
    /**
4147
     * Unsets the property of the given name if tagged.
4148
     *
4149
     * @param string $propertyName
4150
     */
4151
    public function __unset(string $propertyName)
4152
    {
4153
        switch ($propertyName) {
4154
            case 'domainStartPage':
4155
                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);
4156
                break;
4157
            case 'cHash':
4158
                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);
4159
                break;
4160
            case 'cHash_array':
4161
                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);
4162
                break;
4163
            case 'sys_language_isocode':
4164
                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);
4165
                break;
4166
            case 'divSection':
4167
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4168
                break;
4169
            case 'fePreview':
4170
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
4171
                $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, false));
4172
                break;
4173
            case 'forceTemplateParsing':
4174
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
4175
                $this->context->setAspect('typoscript', GeneralUtility::makeInstance(TypoScriptAspect::class, false));
4176
                break;
4177
        }
4178
        unset($this->$propertyName);
4179
    }
4180
}
4181