Completed
Push — master ( e6e5ff...3738e1 )
by
unknown
30:28 queued 15:43
created

initializeSiteWithCompatibility()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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