Completed
Push — master ( c1d8f4...533b50 )
by
unknown
29:29
created

getAdditionalHeaders()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

3318
                $midnightTime = mktime(0, 0, 0, date('m', $timeOutTime), date('d', $timeOutTime), /** @scrutinizer ignore-type */ date('Y', $timeOutTime));
Loading history...
3319
                // If the midnight time of the expire-day is greater than the current time,
3320
                // we may set the timeOutTime to the new midnighttime.
3321
                if ($midnightTime > $GLOBALS['EXEC_TIME']) {
3322
                    $cacheTimeout = $midnightTime - $GLOBALS['EXEC_TIME'];
3323
                }
3324
            }
3325
3326
            // Calculate the timeout time for records on the page and adjust cache timeout if necessary
3327
            $cacheTimeout = min($this->calculatePageCacheTimeout(), $cacheTimeout);
3328
3329
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['get_cache_timeout'] ?? [] as $_funcRef) {
3330
                $params = ['cacheTimeout' => $cacheTimeout];
3331
                $cacheTimeout = GeneralUtility::callUserFunction($_funcRef, $params, $this);
3332
            }
3333
            $runtimeCache->set($cachedCacheLifetimeIdentifier, $cacheTimeout);
3334
            $cachedCacheLifetime = $cacheTimeout;
3335
        }
3336
        return $cachedCacheLifetime;
3337
    }
3338
3339
    /*********************************************
3340
     *
3341
     * Localization and character set conversion
3342
     *
3343
     *********************************************/
3344
    /**
3345
     * Split Label function for front-end applications.
3346
     *
3347
     * @param string $input Key string. Accepts the "LLL:" prefix.
3348
     * @return string Label value, if any.
3349
     */
3350
    public function sL($input)
3351
    {
3352
        return $this->languageService->sL($input);
3353
    }
3354
3355
    /**
3356
     * Sets all internal measures what language the page should be rendered.
3357
     * This is not for records, but rather the HTML / charset and the locallang labels
3358
     */
3359
    protected function setOutputLanguage()
3360
    {
3361
        $this->languageService = LanguageService::createFromSiteLanguage($this->language);
3362
        // Always disable debugging for TSFE
3363
        $this->languageService->debugKey = false;
3364
    }
3365
3366
    /**
3367
     * Converts input string from utf-8 to metaCharset IF the two charsets are different.
3368
     *
3369
     * @param string $content Content to be converted.
3370
     * @return string Converted content string.
3371
     * @throws \RuntimeException if an invalid charset was configured
3372
     */
3373
    public function convOutputCharset($content)
3374
    {
3375
        if ($this->metaCharset !== 'utf-8') {
3376
            /** @var CharsetConverter $charsetConverter */
3377
            $charsetConverter = GeneralUtility::makeInstance(CharsetConverter::class);
3378
            try {
3379
                $content = $charsetConverter->conv($content, 'utf-8', $this->metaCharset);
3380
            } catch (UnknownCharsetException $e) {
3381
                throw new \RuntimeException('Invalid config.metaCharset: ' . $e->getMessage(), 1508916185);
3382
            }
3383
        }
3384
        return $content;
3385
    }
3386
3387
    /**
3388
     * Calculates page cache timeout according to the records with starttime/endtime on the page.
3389
     *
3390
     * @return int Page cache timeout or PHP_INT_MAX if cannot be determined
3391
     */
3392
    protected function calculatePageCacheTimeout()
3393
    {
3394
        $result = PHP_INT_MAX;
3395
        // Get the configuration
3396
        $tablesToConsider = $this->getCurrentPageCacheConfiguration();
3397
        // Get the time, rounded to the minute (do not pollute MySQL cache!)
3398
        // It is ok that we do not take seconds into account here because this
3399
        // value will be subtracted later. So we never get the time "before"
3400
        // the cache change.
3401
        $now = $GLOBALS['ACCESS_TIME'];
3402
        // Find timeout by checking every table
3403
        foreach ($tablesToConsider as $tableDef) {
3404
            $result = min($result, $this->getFirstTimeValueForRecord($tableDef, $now));
3405
        }
3406
        // We return + 1 second just to ensure that cache is definitely regenerated
3407
        return $result === PHP_INT_MAX ? PHP_INT_MAX : $result - $now + 1;
3408
    }
3409
3410
    /**
3411
     * Obtains a list of table/pid pairs to consider for page caching.
3412
     *
3413
     * TS configuration looks like this:
3414
     *
3415
     * The cache lifetime of all pages takes starttime and endtime of news records of page 14 into account:
3416
     * config.cache.all = tt_news:14
3417
     *
3418
     * The cache.lifetime of the current page allows to take records (e.g. fe_users) into account:
3419
     * config.cache.all = fe_users:current
3420
     *
3421
     * The cache lifetime of page 42 takes starttime and endtime of news records of page 15 and addresses of page 16 into account:
3422
     * config.cache.42 = tt_news:15,tt_address:16
3423
     *
3424
     * @return array Array of 'tablename:pid' pairs. There is at least a current page id in the array
3425
     * @see TypoScriptFrontendController::calculatePageCacheTimeout()
3426
     */
3427
    protected function getCurrentPageCacheConfiguration()
3428
    {
3429
        $result = ['tt_content:' . $this->id];
3430
        if (isset($this->config['config']['cache.'][$this->id])) {
3431
            $result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $this->id, $this->config['config']['cache.'][$this->id])));
3432
        }
3433
        if (isset($this->config['config']['cache.']['all'])) {
3434
            $result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $this->id, $this->config['config']['cache.']['all'])));
3435
        }
3436
        return array_unique($result);
3437
    }
3438
3439
    /**
3440
     * Find the minimum starttime or endtime value in the table and pid that is greater than the current time.
3441
     *
3442
     * @param string $tableDef Table definition (format tablename:pid)
3443
     * @param int $now "Now" time value
3444
     * @throws \InvalidArgumentException
3445
     * @return int Value of the next start/stop time or PHP_INT_MAX if not found
3446
     * @see TypoScriptFrontendController::calculatePageCacheTimeout()
3447
     */
3448
    protected function getFirstTimeValueForRecord($tableDef, $now)
3449
    {
3450
        $now = (int)$now;
3451
        $result = PHP_INT_MAX;
3452
        [$tableName, $pid] = GeneralUtility::trimExplode(':', $tableDef);
3453
        if (empty($tableName) || empty($pid)) {
3454
            throw new \InvalidArgumentException('Unexpected value for parameter $tableDef. Expected <tablename>:<pid>, got \'' . htmlspecialchars($tableDef) . '\'.', 1307190365);
3455
        }
3456
3457
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
3458
            ->getQueryBuilderForTable($tableName);
3459
        $queryBuilder->getRestrictions()
3460
            ->removeByType(StartTimeRestriction::class)
3461
            ->removeByType(EndTimeRestriction::class);
3462
        $timeFields = [];
3463
        $timeConditions = $queryBuilder->expr()->orX();
3464
        foreach (['starttime', 'endtime'] as $field) {
3465
            if (isset($GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field])) {
3466
                $timeFields[$field] = $GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field];
3467
                $queryBuilder->addSelectLiteral(
3468
                    'MIN('
3469
                        . 'CASE WHEN '
3470
                        . $queryBuilder->expr()->lte(
3471
                            $timeFields[$field],
3472
                            $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)
3473
                        )
3474
                        . ' THEN NULL ELSE ' . $queryBuilder->quoteIdentifier($timeFields[$field]) . ' END'
3475
                        . ') AS ' . $queryBuilder->quoteIdentifier($timeFields[$field])
3476
                );
3477
                $timeConditions->add(
3478
                    $queryBuilder->expr()->gt(
3479
                        $timeFields[$field],
3480
                        $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)
3481
                    )
3482
                );
3483
            }
3484
        }
3485
3486
        // if starttime or endtime are defined, evaluate them
3487
        if (!empty($timeFields)) {
3488
            // find the timestamp, when the current page's content changes the next time
3489
            $row = $queryBuilder
3490
                ->from($tableName)
3491
                ->where(
3492
                    $queryBuilder->expr()->eq(
3493
                        'pid',
3494
                        $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
3495
                    ),
3496
                    $timeConditions
3497
                )
3498
                ->execute()
3499
                ->fetch();
3500
3501
            if ($row) {
3502
                foreach ($timeFields as $timeField => $_) {
3503
                    // if a MIN value is found, take it into account for the
3504
                    // cache lifetime we have to filter out start/endtimes < $now,
3505
                    // as the SQL query also returns rows with starttime < $now
3506
                    // and endtime > $now (and using a starttime from the past
3507
                    // would be wrong)
3508
                    if ($row[$timeField] !== null && (int)$row[$timeField] > $now) {
3509
                        $result = min($result, (int)$row[$timeField]);
3510
                    }
3511
                }
3512
            }
3513
        }
3514
3515
        return $result;
3516
    }
3517
3518
    /**
3519
     * Fetches the originally requested id, falls back to $this->id
3520
     *
3521
     * @return int the originally requested page uid
3522
     * @see fetch_the_id()
3523
     */
3524
    public function getRequestedId()
3525
    {
3526
        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...
3527
    }
3528
3529
    /**
3530
     * Acquire a page specific lock
3531
     *
3532
     *
3533
     * The schematics here is:
3534
     * - First acquire an access lock. This is using the type of the requested lock as key.
3535
     *   Since the number of types is rather limited we can use the type as key as it will only
3536
     *   eat up a limited number of lock resources on the system (files, semaphores)
3537
     * - Second, we acquire the actual lock (named page lock). We can be sure we are the only process at this
3538
     *   very moment, hence we either get the lock for the given key or we get an error as we request a non-blocking mode.
3539
     *
3540
     * Interleaving two locks is extremely important, because the actual page lock uses a hash value as key (see callers
3541
     * of this function). If we would simply employ a normal blocking lock, we would get a potentially unlimited
3542
     * (number of pages at least) number of different locks. Depending on the available locking methods on the system
3543
     * we might run out of available resources. (e.g. maximum limit of semaphores is a system setting and applies
3544
     * to the whole system)
3545
     * We therefore must make sure that page locks are destroyed again if they are not used anymore, such that
3546
     * we never use more locking resources than parallel requests to different pages (hashes).
3547
     * In order to ensure this, we need to guarantee that no other process is waiting on a page lock when
3548
     * the process currently having the lock on the page lock is about to release the lock again.
3549
     * This can only be achieved by using a non-blocking mode, such that a process is never put into wait state
3550
     * by the kernel, but only checks the availability of the lock. The access lock is our guard to be sure
3551
     * that no two processes are at the same time releasing/destroying a page lock, whilst the other one tries to
3552
     * get a lock for this page lock.
3553
     * The only drawback of this implementation is that we basically have to poll the availability of the page lock.
3554
     *
3555
     * Note that the access lock resources are NEVER deleted/destroyed, otherwise the whole thing would be broken.
3556
     *
3557
     * @param string $type
3558
     * @param string $key
3559
     * @throws \InvalidArgumentException
3560
     * @throws \RuntimeException
3561
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
3562
     */
3563
    protected function acquireLock($type, $key)
3564
    {
3565
        $lockFactory = GeneralUtility::makeInstance(LockFactory::class);
3566
        $this->locks[$type]['accessLock'] = $lockFactory->createLocker($type);
3567
3568
        $this->locks[$type]['pageLock'] = $lockFactory->createLocker(
3569
            $key,
3570
            LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
3571
        );
3572
3573
        do {
3574
            if (!$this->locks[$type]['accessLock']->acquire()) {
3575
                throw new \RuntimeException('Could not acquire access lock for "' . $type . '"".', 1294586098);
3576
            }
3577
3578
            try {
3579
                $locked = $this->locks[$type]['pageLock']->acquire(
3580
                    LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
3581
                );
3582
            } catch (LockAcquireWouldBlockException $e) {
3583
                // somebody else has the lock, we keep waiting
3584
3585
                // first release the access lock
3586
                $this->locks[$type]['accessLock']->release();
3587
                // now lets make a short break (100ms) until we try again, since
3588
                // the page generation by the lock owner will take a while anyways
3589
                usleep(100000);
3590
                continue;
3591
            }
3592
            $this->locks[$type]['accessLock']->release();
3593
            if ($locked) {
3594
                break;
3595
            }
3596
            throw new \RuntimeException('Could not acquire page lock for ' . $key . '.', 1460975877);
3597
        } while (true);
3598
    }
3599
3600
    /**
3601
     * Release a page specific lock
3602
     *
3603
     * @param string $type
3604
     * @throws \InvalidArgumentException
3605
     * @throws \RuntimeException
3606
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
3607
     */
3608
    protected function releaseLock($type)
3609
    {
3610
        if ($this->locks[$type]['accessLock']) {
3611
            if (!$this->locks[$type]['accessLock']->acquire()) {
3612
                throw new \RuntimeException('Could not acquire access lock for "' . $type . '"".', 1460975902);
3613
            }
3614
3615
            $this->locks[$type]['pageLock']->release();
3616
            $this->locks[$type]['pageLock']->destroy();
3617
            $this->locks[$type]['pageLock'] = null;
3618
3619
            $this->locks[$type]['accessLock']->release();
3620
            $this->locks[$type]['accessLock'] = null;
3621
        }
3622
    }
3623
3624
    /**
3625
     * Send additional headers from config.additionalHeaders
3626
     */
3627
    protected function getAdditionalHeaders(): array
3628
    {
3629
        if (!isset($this->config['config']['additionalHeaders.'])) {
3630
            return [];
3631
        }
3632
        $additionalHeaders = [];
3633
        ksort($this->config['config']['additionalHeaders.']);
3634
        foreach ($this->config['config']['additionalHeaders.'] as $options) {
3635
            if (!is_array($options)) {
3636
                continue;
3637
            }
3638
            $header = trim($options['header'] ?? '');
3639
            if ($header === '') {
3640
                continue;
3641
            }
3642
            $additionalHeaders[] = [
3643
                'header' => $header,
3644
                // "replace existing headers" is turned on by default, unless turned off
3645
                'replace' => ($options['replace'] ?? '') !== '0',
3646
                'statusCode' => (int)($options['httpResponseCode'] ?? 0) ?: null
3647
            ];
3648
        }
3649
        return $additionalHeaders;
3650
    }
3651
3652
    /**
3653
     * Returns the current BE user.
3654
     *
3655
     * @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication
3656
     */
3657
    protected function getBackendUser()
3658
    {
3659
        return $GLOBALS['BE_USER'];
3660
    }
3661
3662
    /**
3663
     * @return TimeTracker
3664
     */
3665
    protected function getTimeTracker()
3666
    {
3667
        return GeneralUtility::makeInstance(TimeTracker::class);
3668
    }
3669
3670
    /**
3671
     * Return the global instance of this class.
3672
     *
3673
     * Intended to be used as prototype factory for this class, see Services.yaml.
3674
     * This is required as long as TypoScriptFrontendController needs request
3675
     * dependent constructor parameters. Once that has been refactored this
3676
     * factory will be removed.
3677
     *
3678
     * @return TypoScriptFrontendController
3679
     * @internal
3680
     */
3681
    public static function getGlobalInstance(): ?self
3682
    {
3683
        if (($GLOBALS['TSFE'] ?? null) instanceof self) {
3684
            return $GLOBALS['TSFE'];
3685
        }
3686
3687
        if (!(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_FE)) {
3688
            // Return null for now (together with shared: false in Services.yaml) as TSFE might not be available in backend context
3689
            // That's not an error then
3690
            return null;
3691
        }
3692
3693
        throw new \LogicException('TypoScriptFrontendController was tried to be injected before initial creation', 1538370377);
3694
    }
3695
3696
    public function getLanguage(): SiteLanguage
3697
    {
3698
        return $this->language;
3699
    }
3700
3701
    public function getSite(): Site
3702
    {
3703
        return $this->site;
3704
    }
3705
3706
    public function getContext(): Context
3707
    {
3708
        return $this->context;
3709
    }
3710
3711
    public function getPageArguments(): PageArguments
3712
    {
3713
        return $this->pageArguments;
3714
    }
3715
3716
    /**
3717
     * Deprecation messages for TYPO3 10 - public properties of TSFE which have been (re)moved
3718
     */
3719
    /**
3720
     * Checks if the property of the given name is set.
3721
     *
3722
     * Unmarked protected properties must return false as usual.
3723
     * Marked properties are evaluated by isset().
3724
     *
3725
     * This method is not called for public properties.
3726
     *
3727
     * @param string $propertyName
3728
     * @return bool
3729
     */
3730
    public function __isset(string $propertyName)
3731
    {
3732
        switch ($propertyName) {
3733
            case 'domainStartPage':
3734
                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);
3735
                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...
3736
            case 'sys_language_isocode':
3737
                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);
3738
                return isset($this->$propertyName);
3739
            case 'divSection':
3740
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
3741
                return isset($this->$propertyName);
3742
            case 'fePreview':
3743
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
3744
                return $this->context->hasAspect('frontend.preview');
3745
            case 'forceTemplateParsing':
3746
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
3747
                return $this->context->hasAspect('typoscript') && $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing');
3748
        }
3749
        return false;
3750
    }
3751
3752
    /**
3753
     * Gets the value of the property of the given name if tagged.
3754
     *
3755
     * The evaluation is done in the assumption that this method is never
3756
     * reached for a public property.
3757
     *
3758
     * @param string $propertyName
3759
     * @return mixed
3760
     */
3761
    public function __get(string $propertyName)
3762
    {
3763
        switch ($propertyName) {
3764
            case 'domainStartPage':
3765
                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);
3766
                return $this->site->getRootPageId();
3767
            case 'sys_language_isocode':
3768
                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);
3769
                return $this->sys_language_isocode ?? $this->language->getTwoLetterIsoCode();
3770
            case 'divSection':
3771
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
3772
                break;
3773
            case 'fePreview':
3774
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
3775
                if ($this->context->hasAspect('frontend.preview')) {
3776
                    return $this->context->getPropertyFromAspect('frontend.preview', 'isPreview');
3777
                }
3778
                break;
3779
            case 'forceTemplateParsing':
3780
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
3781
                if ($this->context->hasAspect('typoscript')) {
3782
                    return $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing');
3783
                }
3784
                break;
3785
        }
3786
        return $this->$propertyName;
3787
    }
3788
3789
    /**
3790
     * Sets the property of the given name if tagged.
3791
     *
3792
     * Additionally it's allowed to set unknown properties.
3793
     *
3794
     * The evaluation is done in the assumption that this method is never
3795
     * reached for a public property.
3796
     *
3797
     * @param string $propertyName
3798
     * @param mixed $propertyValue
3799
     */
3800
    public function __set(string $propertyName, $propertyValue)
3801
    {
3802
        switch ($propertyName) {
3803
            case 'domainStartPage':
3804
                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);
3805
                break;
3806
            case 'sys_language_isocode':
3807
                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);
3808
                break;
3809
            case 'divSection':
3810
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
3811
                break;
3812
            case 'fePreview':
3813
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
3814
                $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, (bool)$propertyValue));
3815
                break;
3816
            case 'forceTemplateParsing':
3817
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
3818
                $this->context->setAspect('typoscript', GeneralUtility::makeInstance(TypoScriptAspect::class, (bool)$propertyValue));
3819
                break;
3820
        }
3821
        $this->$propertyName = $propertyValue;
3822
    }
3823
3824
    /**
3825
     * Unsets the property of the given name if tagged.
3826
     *
3827
     * @param string $propertyName
3828
     */
3829
    public function __unset(string $propertyName)
3830
    {
3831
        switch ($propertyName) {
3832
            case 'domainStartPage':
3833
                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);
3834
                break;
3835
            case 'sys_language_isocode':
3836
                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);
3837
                break;
3838
            case 'divSection':
3839
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
3840
                break;
3841
            case 'fePreview':
3842
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
3843
                $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, false));
3844
                break;
3845
            case 'forceTemplateParsing':
3846
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
3847
                $this->context->setAspect('typoscript', GeneralUtility::makeInstance(TypoScriptAspect::class, false));
3848
                break;
3849
        }
3850
        unset($this->$propertyName);
3851
    }
3852
}
3853