Completed
Push — master ( d26efc...ef3af1 )
by
unknown
25:11 queued 08:41
created

TypoScriptFrontendController::__get()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 20
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

3252
                $midnightTime = mktime(0, 0, 0, date('m', $timeOutTime), /** @scrutinizer ignore-type */ date('d', $timeOutTime), date('Y', $timeOutTime));
Loading history...
3253
                // If the midnight time of the expire-day is greater than the current time,
3254
                // we may set the timeOutTime to the new midnighttime.
3255
                if ($midnightTime > $GLOBALS['EXEC_TIME']) {
3256
                    $cacheTimeout = $midnightTime - $GLOBALS['EXEC_TIME'];
3257
                }
3258
            }
3259
3260
            // Calculate the timeout time for records on the page and adjust cache timeout if necessary
3261
            $cacheTimeout = min($this->calculatePageCacheTimeout(), $cacheTimeout);
3262
3263
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['get_cache_timeout'] ?? [] as $_funcRef) {
3264
                $params = ['cacheTimeout' => $cacheTimeout];
3265
                $cacheTimeout = GeneralUtility::callUserFunction($_funcRef, $params, $this);
3266
            }
3267
            $runtimeCache->set($cachedCacheLifetimeIdentifier, $cacheTimeout);
3268
            $cachedCacheLifetime = $cacheTimeout;
3269
        }
3270
        return $cachedCacheLifetime;
3271
    }
3272
3273
    /*********************************************
3274
     *
3275
     * Localization and character set conversion
3276
     *
3277
     *********************************************/
3278
    /**
3279
     * Split Label function for front-end applications.
3280
     *
3281
     * @param string $input Key string. Accepts the "LLL:" prefix.
3282
     * @return string Label value, if any.
3283
     */
3284
    public function sL($input)
3285
    {
3286
        return $this->languageService->sL($input);
3287
    }
3288
3289
    /**
3290
     * Sets all internal measures what language the page should be rendered.
3291
     * This is not for records, but rather the HTML / charset and the locallang labels
3292
     */
3293
    protected function setOutputLanguage()
3294
    {
3295
        $this->languageService = LanguageService::createFromSiteLanguage($this->language);
3296
        // Always disable debugging for TSFE
3297
        $this->languageService->debugKey = false;
3298
    }
3299
3300
    /**
3301
     * Converts input string from utf-8 to metaCharset IF the two charsets are different.
3302
     *
3303
     * @param string $content Content to be converted.
3304
     * @return string Converted content string.
3305
     * @throws \RuntimeException if an invalid charset was configured
3306
     */
3307
    public function convOutputCharset($content)
3308
    {
3309
        if ($this->metaCharset !== 'utf-8') {
3310
            /** @var CharsetConverter $charsetConverter */
3311
            $charsetConverter = GeneralUtility::makeInstance(CharsetConverter::class);
3312
            try {
3313
                $content = $charsetConverter->conv($content, 'utf-8', $this->metaCharset);
3314
            } catch (UnknownCharsetException $e) {
3315
                throw new \RuntimeException('Invalid config.metaCharset: ' . $e->getMessage(), 1508916185);
3316
            }
3317
        }
3318
        return $content;
3319
    }
3320
3321
    /**
3322
     * Calculates page cache timeout according to the records with starttime/endtime on the page.
3323
     *
3324
     * @return int Page cache timeout or PHP_INT_MAX if cannot be determined
3325
     */
3326
    protected function calculatePageCacheTimeout()
3327
    {
3328
        $result = PHP_INT_MAX;
3329
        // Get the configuration
3330
        $tablesToConsider = $this->getCurrentPageCacheConfiguration();
3331
        // Get the time, rounded to the minute (do not pollute MySQL cache!)
3332
        // It is ok that we do not take seconds into account here because this
3333
        // value will be subtracted later. So we never get the time "before"
3334
        // the cache change.
3335
        $now = $GLOBALS['ACCESS_TIME'];
3336
        // Find timeout by checking every table
3337
        foreach ($tablesToConsider as $tableDef) {
3338
            $result = min($result, $this->getFirstTimeValueForRecord($tableDef, $now));
3339
        }
3340
        // We return + 1 second just to ensure that cache is definitely regenerated
3341
        return $result === PHP_INT_MAX ? PHP_INT_MAX : $result - $now + 1;
3342
    }
3343
3344
    /**
3345
     * Obtains a list of table/pid pairs to consider for page caching.
3346
     *
3347
     * TS configuration looks like this:
3348
     *
3349
     * The cache lifetime of all pages takes starttime and endtime of news records of page 14 into account:
3350
     * config.cache.all = tt_news:14
3351
     *
3352
     * The cache.lifetime of the current page allows to take records (e.g. fe_users) into account:
3353
     * config.cache.all = fe_users:current
3354
     *
3355
     * The cache lifetime of page 42 takes starttime and endtime of news records of page 15 and addresses of page 16 into account:
3356
     * config.cache.42 = tt_news:15,tt_address:16
3357
     *
3358
     * @return array Array of 'tablename:pid' pairs. There is at least a current page id in the array
3359
     * @see TypoScriptFrontendController::calculatePageCacheTimeout()
3360
     */
3361
    protected function getCurrentPageCacheConfiguration()
3362
    {
3363
        $result = ['tt_content:' . $this->id];
3364
        if (isset($this->config['config']['cache.'][$this->id])) {
3365
            $result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $this->id, $this->config['config']['cache.'][$this->id])));
3366
        }
3367
        if (isset($this->config['config']['cache.']['all'])) {
3368
            $result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $this->id, $this->config['config']['cache.']['all'])));
3369
        }
3370
        return array_unique($result);
3371
    }
3372
3373
    /**
3374
     * Find the minimum starttime or endtime value in the table and pid that is greater than the current time.
3375
     *
3376
     * @param string $tableDef Table definition (format tablename:pid)
3377
     * @param int $now "Now" time value
3378
     * @throws \InvalidArgumentException
3379
     * @return int Value of the next start/stop time or PHP_INT_MAX if not found
3380
     * @see TypoScriptFrontendController::calculatePageCacheTimeout()
3381
     */
3382
    protected function getFirstTimeValueForRecord($tableDef, $now)
3383
    {
3384
        $now = (int)$now;
3385
        $result = PHP_INT_MAX;
3386
        [$tableName, $pid] = GeneralUtility::trimExplode(':', $tableDef);
3387
        if (empty($tableName) || empty($pid)) {
3388
            throw new \InvalidArgumentException('Unexpected value for parameter $tableDef. Expected <tablename>:<pid>, got \'' . htmlspecialchars($tableDef) . '\'.', 1307190365);
3389
        }
3390
3391
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
3392
            ->getQueryBuilderForTable($tableName);
3393
        $queryBuilder->getRestrictions()
3394
            ->removeByType(StartTimeRestriction::class)
3395
            ->removeByType(EndTimeRestriction::class);
3396
        $timeFields = [];
3397
        $timeConditions = $queryBuilder->expr()->orX();
3398
        foreach (['starttime', 'endtime'] as $field) {
3399
            if (isset($GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field])) {
3400
                $timeFields[$field] = $GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field];
3401
                $queryBuilder->addSelectLiteral(
3402
                    'MIN('
3403
                        . 'CASE WHEN '
3404
                        . $queryBuilder->expr()->lte(
3405
                            $timeFields[$field],
3406
                            $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)
3407
                        )
3408
                        . ' THEN NULL ELSE ' . $queryBuilder->quoteIdentifier($timeFields[$field]) . ' END'
3409
                        . ') AS ' . $queryBuilder->quoteIdentifier($timeFields[$field])
3410
                );
3411
                $timeConditions->add(
3412
                    $queryBuilder->expr()->gt(
3413
                        $timeFields[$field],
3414
                        $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)
3415
                    )
3416
                );
3417
            }
3418
        }
3419
3420
        // if starttime or endtime are defined, evaluate them
3421
        if (!empty($timeFields)) {
3422
            // find the timestamp, when the current page's content changes the next time
3423
            $row = $queryBuilder
3424
                ->from($tableName)
3425
                ->where(
3426
                    $queryBuilder->expr()->eq(
3427
                        'pid',
3428
                        $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
3429
                    ),
3430
                    $timeConditions
3431
                )
3432
                ->execute()
3433
                ->fetch();
3434
3435
            if ($row) {
3436
                foreach ($timeFields as $timeField => $_) {
3437
                    // if a MIN value is found, take it into account for the
3438
                    // cache lifetime we have to filter out start/endtimes < $now,
3439
                    // as the SQL query also returns rows with starttime < $now
3440
                    // and endtime > $now (and using a starttime from the past
3441
                    // would be wrong)
3442
                    if ($row[$timeField] !== null && (int)$row[$timeField] > $now) {
3443
                        $result = min($result, (int)$row[$timeField]);
3444
                    }
3445
                }
3446
            }
3447
        }
3448
3449
        return $result;
3450
    }
3451
3452
    /**
3453
     * Fetches the originally requested id, falls back to $this->id
3454
     *
3455
     * @return int the originally requested page uid
3456
     * @see fetch_the_id()
3457
     */
3458
    public function getRequestedId()
3459
    {
3460
        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...
3461
    }
3462
3463
    /**
3464
     * Acquire a page specific lock
3465
     *
3466
     *
3467
     * The schematics here is:
3468
     * - First acquire an access lock. This is using the type of the requested lock as key.
3469
     *   Since the number of types is rather limited we can use the type as key as it will only
3470
     *   eat up a limited number of lock resources on the system (files, semaphores)
3471
     * - Second, we acquire the actual lock (named page lock). We can be sure we are the only process at this
3472
     *   very moment, hence we either get the lock for the given key or we get an error as we request a non-blocking mode.
3473
     *
3474
     * Interleaving two locks is extremely important, because the actual page lock uses a hash value as key (see callers
3475
     * of this function). If we would simply employ a normal blocking lock, we would get a potentially unlimited
3476
     * (number of pages at least) number of different locks. Depending on the available locking methods on the system
3477
     * we might run out of available resources. (e.g. maximum limit of semaphores is a system setting and applies
3478
     * to the whole system)
3479
     * We therefore must make sure that page locks are destroyed again if they are not used anymore, such that
3480
     * we never use more locking resources than parallel requests to different pages (hashes).
3481
     * In order to ensure this, we need to guarantee that no other process is waiting on a page lock when
3482
     * the process currently having the lock on the page lock is about to release the lock again.
3483
     * This can only be achieved by using a non-blocking mode, such that a process is never put into wait state
3484
     * by the kernel, but only checks the availability of the lock. The access lock is our guard to be sure
3485
     * that no two processes are at the same time releasing/destroying a page lock, whilst the other one tries to
3486
     * get a lock for this page lock.
3487
     * The only drawback of this implementation is that we basically have to poll the availability of the page lock.
3488
     *
3489
     * Note that the access lock resources are NEVER deleted/destroyed, otherwise the whole thing would be broken.
3490
     *
3491
     * @param string $type
3492
     * @param string $key
3493
     * @throws \InvalidArgumentException
3494
     * @throws \RuntimeException
3495
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
3496
     */
3497
    protected function acquireLock($type, $key)
3498
    {
3499
        $lockFactory = GeneralUtility::makeInstance(LockFactory::class);
3500
        $this->locks[$type]['accessLock'] = $lockFactory->createLocker($type);
3501
3502
        $this->locks[$type]['pageLock'] = $lockFactory->createLocker(
3503
            $key,
3504
            LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
3505
        );
3506
3507
        do {
3508
            if (!$this->locks[$type]['accessLock']->acquire()) {
3509
                throw new \RuntimeException('Could not acquire access lock for "' . $type . '"".', 1294586098);
3510
            }
3511
3512
            try {
3513
                $locked = $this->locks[$type]['pageLock']->acquire(
3514
                    LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
3515
                );
3516
            } catch (LockAcquireWouldBlockException $e) {
3517
                // somebody else has the lock, we keep waiting
3518
3519
                // first release the access lock
3520
                $this->locks[$type]['accessLock']->release();
3521
                // now lets make a short break (100ms) until we try again, since
3522
                // the page generation by the lock owner will take a while anyways
3523
                usleep(100000);
3524
                continue;
3525
            }
3526
            $this->locks[$type]['accessLock']->release();
3527
            if ($locked) {
3528
                break;
3529
            }
3530
            throw new \RuntimeException('Could not acquire page lock for ' . $key . '.', 1460975877);
3531
        } while (true);
3532
    }
3533
3534
    /**
3535
     * Release a page specific lock
3536
     *
3537
     * @param string $type
3538
     * @throws \InvalidArgumentException
3539
     * @throws \RuntimeException
3540
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
3541
     */
3542
    protected function releaseLock($type)
3543
    {
3544
        if ($this->locks[$type]['accessLock']) {
3545
            if (!$this->locks[$type]['accessLock']->acquire()) {
3546
                throw new \RuntimeException('Could not acquire access lock for "' . $type . '"".', 1460975902);
3547
            }
3548
3549
            $this->locks[$type]['pageLock']->release();
3550
            $this->locks[$type]['pageLock']->destroy();
3551
            $this->locks[$type]['pageLock'] = null;
3552
3553
            $this->locks[$type]['accessLock']->release();
3554
            $this->locks[$type]['accessLock'] = null;
3555
        }
3556
    }
3557
3558
    /**
3559
     * Send additional headers from config.additionalHeaders
3560
     */
3561
    protected function getAdditionalHeaders(): array
3562
    {
3563
        if (!isset($this->config['config']['additionalHeaders.'])) {
3564
            return [];
3565
        }
3566
        $additionalHeaders = [];
3567
        ksort($this->config['config']['additionalHeaders.']);
3568
        foreach ($this->config['config']['additionalHeaders.'] as $options) {
3569
            if (!is_array($options)) {
3570
                continue;
3571
            }
3572
            $header = trim($options['header'] ?? '');
3573
            if ($header === '') {
3574
                continue;
3575
            }
3576
            $additionalHeaders[] = [
3577
                'header' => $header,
3578
                // "replace existing headers" is turned on by default, unless turned off
3579
                'replace' => ($options['replace'] ?? '') !== '0',
3580
                'statusCode' => (int)($options['httpResponseCode'] ?? 0) ?: null
3581
            ];
3582
        }
3583
        return $additionalHeaders;
3584
    }
3585
3586
    /**
3587
     * Returns the current BE user.
3588
     *
3589
     * @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication
3590
     */
3591
    protected function getBackendUser()
3592
    {
3593
        return $GLOBALS['BE_USER'];
3594
    }
3595
3596
    /**
3597
     * @return TimeTracker
3598
     */
3599
    protected function getTimeTracker()
3600
    {
3601
        return GeneralUtility::makeInstance(TimeTracker::class);
3602
    }
3603
3604
    /**
3605
     * Return the global instance of this class.
3606
     *
3607
     * Intended to be used as prototype factory for this class, see Services.yaml.
3608
     * This is required as long as TypoScriptFrontendController needs request
3609
     * dependent constructor parameters. Once that has been refactored this
3610
     * factory will be removed.
3611
     *
3612
     * @return TypoScriptFrontendController
3613
     * @internal
3614
     */
3615
    public static function getGlobalInstance(): ?self
3616
    {
3617
        if (($GLOBALS['TSFE'] ?? null) instanceof self) {
3618
            return $GLOBALS['TSFE'];
3619
        }
3620
3621
        if (!(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_FE)) {
3622
            // Return null for now (together with shared: false in Services.yaml) as TSFE might not be available in backend context
3623
            // That's not an error then
3624
            return null;
3625
        }
3626
3627
        throw new \LogicException('TypoScriptFrontendController was tried to be injected before initial creation', 1538370377);
3628
    }
3629
3630
    public function getLanguage(): SiteLanguage
3631
    {
3632
        return $this->language;
3633
    }
3634
3635
    public function getSite(): Site
3636
    {
3637
        return $this->site;
3638
    }
3639
3640
    public function getContext(): Context
3641
    {
3642
        return $this->context;
3643
    }
3644
3645
    public function getPageArguments(): PageArguments
3646
    {
3647
        return $this->pageArguments;
3648
    }
3649
}
3650