Completed
Push — master ( 8a4acb...f1bc20 )
by
unknown
14:06
created

getFirstTimeValueForRecord()   B

Complexity

Conditions 10
Paths 10

Size

Total Lines 68
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 42
nc 10
nop 2
dl 0
loc 68
rs 7.6666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

3310
                $midnightTime = mktime(0, 0, 0, date('m', $timeOutTime), /** @scrutinizer ignore-type */ date('d', $timeOutTime), date('Y', $timeOutTime));
Loading history...
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

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