Completed
Push — master ( e93ef4...162351 )
by
unknown
63:24 queued 45:53
created

TypoScriptFrontendController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 5
dl 0
loc 12
rs 9.9666
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Frontend\Controller;
17
18
use Psr\Http\Message\ResponseInterface;
19
use Psr\Http\Message\ServerRequestInterface;
20
use Psr\Log\LoggerAwareInterface;
21
use Psr\Log\LoggerAwareTrait;
22
use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
23
use TYPO3\CMS\Core\Cache\CacheManager;
24
use TYPO3\CMS\Core\Charset\CharsetConverter;
25
use TYPO3\CMS\Core\Charset\UnknownCharsetException;
26
use TYPO3\CMS\Core\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\Http\ServerRequestFactory;
48
use TYPO3\CMS\Core\Localization\LanguageService;
49
use TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException;
50
use TYPO3\CMS\Core\Locking\LockFactory;
51
use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
52
use TYPO3\CMS\Core\Page\AssetCollector;
53
use TYPO3\CMS\Core\Page\PageRenderer;
54
use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager;
55
use TYPO3\CMS\Core\Resource\Exception;
56
use TYPO3\CMS\Core\Resource\StorageRepository;
57
use TYPO3\CMS\Core\Routing\PageArguments;
58
use TYPO3\CMS\Core\Site\Entity\Site;
59
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
60
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
61
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
62
use TYPO3\CMS\Core\Type\Bitmask\Permission;
63
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
64
use TYPO3\CMS\Core\TypoScript\TemplateService;
65
use TYPO3\CMS\Core\Utility\ArrayUtility;
66
use TYPO3\CMS\Core\Utility\GeneralUtility;
67
use TYPO3\CMS\Core\Utility\HttpUtility;
68
use TYPO3\CMS\Core\Utility\MathUtility;
69
use TYPO3\CMS\Core\Utility\PathUtility;
70
use TYPO3\CMS\Core\Utility\RootlineUtility;
71
use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
72
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
73
use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
74
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
75
use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
76
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
77
use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
78
79
/**
80
 * Class for the built TypoScript based frontend. Instantiated in
81
 * \TYPO3\CMS\Frontend\Http\RequestHandler as the global object TSFE.
82
 *
83
 * Main frontend class, instantiated in \TYPO3\CMS\Frontend\Http\RequestHandler
84
 * as the global object TSFE.
85
 *
86
 * This class has a lot of functions and internal variable which are used from
87
 * \TYPO3\CMS\Frontend\Http\RequestHandler
88
 *
89
 * The class is instantiated as $GLOBALS['TSFE'] in \TYPO3\CMS\Frontend\Http\RequestHandler.
90
 *
91
 * The use of this class should be inspired by the order of function calls as
92
 * found in \TYPO3\CMS\Frontend\Http\RequestHandler.
93
 */
94
class TypoScriptFrontendController implements LoggerAwareInterface
95
{
96
    use LoggerAwareTrait;
97
98
    /**
99
     * The page id (int)
100
     * @var string
101
     */
102
    public $id = '';
103
104
    /**
105
     * The type (read-only)
106
     * @var int|string
107
     */
108
    public $type = '';
109
110
    /**
111
     * @var Site
112
     */
113
    protected $site;
114
115
    /**
116
     * @var SiteLanguage
117
     */
118
    protected $language;
119
120
    /**
121
     * @var PageArguments
122
     * @internal
123
     */
124
    protected $pageArguments;
125
126
    /**
127
     * Page will not be cached. Write only TRUE. Never clear value (some other
128
     * code might have reasons to set it TRUE).
129
     * @var bool
130
     */
131
    public $no_cache = false;
132
133
    /**
134
     * The rootLine (all the way to tree root, not only the current site!)
135
     * @var array
136
     */
137
    public $rootLine = [];
138
139
    /**
140
     * The pagerecord
141
     * @var array
142
     */
143
    public $page = [];
144
145
    /**
146
     * This will normally point to the same value as id, but can be changed to
147
     * point to another page from which content will then be displayed instead.
148
     * @var int
149
     */
150
    public $contentPid = 0;
151
152
    /**
153
     * Gets set when we are processing a page of type mounpoint with enabled overlay in getPageAndRootline()
154
     * Used later in checkPageForMountpointRedirect() to determine the final target URL where the user
155
     * should be redirected to.
156
     *
157
     * @var array|null
158
     */
159
    protected $originalMountPointPage;
160
161
    /**
162
     * Gets set when we are processing a page of type shortcut in the early stages
163
     * of the request when we do not know about languages yet, used later in the request
164
     * to determine the correct shortcut in case a translation changes the shortcut
165
     * target
166
     * @var array|null
167
     * @see checkTranslatedShortcut()
168
     */
169
    protected $originalShortcutPage;
170
171
    /**
172
     * sys_page-object, pagefunctions
173
     *
174
     * @var PageRepository|string
175
     */
176
    public $sys_page = '';
177
178
    /**
179
     * Is set to 1 if a pageNotFound handler could have been called.
180
     * @var int
181
     * @internal
182
     */
183
    public $pageNotFound = 0;
184
185
    /**
186
     * Array containing a history of why a requested page was not accessible.
187
     * @var array
188
     */
189
    protected $pageAccessFailureHistory = [];
190
191
    /**
192
     * @var string
193
     * @internal
194
     */
195
    public $MP = '';
196
197
    /**
198
     * The frontend user
199
     *
200
     * @var FrontendUserAuthentication
201
     */
202
    public $fe_user;
203
204
    /**
205
     * Shows whether logins are allowed in branch
206
     * @var bool
207
     */
208
    protected $loginAllowedInBranch = true;
209
210
    /**
211
     * Shows specific mode (all or groups)
212
     * @var string
213
     * @internal
214
     */
215
    protected $loginAllowedInBranch_mode = '';
216
217
    /**
218
     * Value that contains the simulated usergroup if any
219
     * @var int
220
     * @internal only to be used in AdminPanel, and within TYPO3 Core
221
     */
222
    public $simUserGroup = 0;
223
224
    /**
225
     * "CONFIG" object from TypoScript. Array generated based on the TypoScript
226
     * configuration of the current page. Saved with the cached pages.
227
     * @var array
228
     */
229
    public $config = [];
230
231
    /**
232
     * The TypoScript template object. Used to parse the TypoScript template
233
     *
234
     * @var TemplateService
235
     */
236
    public $tmpl;
237
238
    /**
239
     * Is set to the time-to-live time of cached pages. Default is 60*60*24, which is 24 hours.
240
     *
241
     * @var int
242
     * @internal
243
     */
244
    protected $cacheTimeOutDefault = 86400;
245
246
    /**
247
     * Set internally if cached content is fetched from the database.
248
     *
249
     * @var bool
250
     * @internal
251
     */
252
    protected $cacheContentFlag = false;
253
254
    /**
255
     * Set to the expire time of cached content
256
     * @var int
257
     * @internal
258
     */
259
    protected $cacheExpires = 0;
260
261
    /**
262
     * Set if cache headers allowing caching are sent.
263
     * @var bool
264
     * @internal
265
     */
266
    protected $isClientCachable = false;
267
268
    /**
269
     * Used by template fetching system. This array is an identification of
270
     * the template. If $this->all is empty it's because the template-data is not
271
     * cached, which it must be.
272
     * @var array
273
     * @internal
274
     */
275
    public $all = [];
276
277
    /**
278
     * Toplevel - objArrayName, eg 'page'
279
     * @var string
280
     * @internal should only be used by TYPO3 Core
281
     */
282
    public $sPre = '';
283
284
    /**
285
     * TypoScript configuration of the page-object pointed to by sPre.
286
     * $this->tmpl->setup[$this->sPre.'.']
287
     * @var array|string
288
     * @internal should only be used by TYPO3 Core
289
     */
290
    public $pSetup = '';
291
292
    /**
293
     * This hash is unique to the template, the $this->id and $this->type vars and
294
     * the list of groups. Used to get and later store the cached data
295
     * @var string
296
     * @internal
297
     */
298
    public $newHash = '';
299
300
    /**
301
     * This flag is set before the page is generated IF $this->no_cache is set. If this
302
     * flag is set after the page content was generated, $this->no_cache is forced to be set.
303
     * This is done in order to make sure that PHP code from Plugins / USER scripts does not falsely
304
     * clear the no_cache flag.
305
     * @var bool
306
     * @internal
307
     */
308
    protected $no_cacheBeforePageGen = false;
309
310
    /**
311
     * May be set to the pagesTSconfig
312
     * @var array|string
313
     * @internal
314
     */
315
    protected $pagesTSconfig = '';
316
317
    /**
318
     * Eg. insert JS-functions in this array ($additionalHeaderData) to include them
319
     * once. Use associative keys.
320
     *
321
     * Keys in use:
322
     *
323
     * used to accumulate additional HTML-code for the header-section,
324
     * <head>...</head>. Insert either associative keys (like
325
     * additionalHeaderData['myStyleSheet'], see reserved keys above) or num-keys
326
     * (like additionalHeaderData[] = '...')
327
     *
328
     * @var array
329
     */
330
    public $additionalHeaderData = [];
331
332
    /**
333
     * Used to accumulate additional HTML-code for the footer-section of the template
334
     * @var array
335
     */
336
    public $additionalFooterData = [];
337
338
    /**
339
     * Default internal target
340
     * @var string
341
     */
342
    public $intTarget = '';
343
344
    /**
345
     * Default external target
346
     * @var string
347
     */
348
    public $extTarget = '';
349
350
    /**
351
     * Default file link target
352
     * @var string
353
     */
354
    public $fileTarget = '';
355
356
    /**
357
     * If set, typolink() function encrypts email addresses.
358
     * @var string|int
359
     */
360
    public $spamProtectEmailAddresses = 0;
361
362
    /**
363
     * Absolute Reference prefix
364
     * @var string
365
     */
366
    public $absRefPrefix = '';
367
368
    /**
369
     * <A>-tag parameters
370
     * @var string
371
     */
372
    public $ATagParams = '';
373
374
    /**
375
     * Search word regex, calculated if there has been search-words send. This is
376
     * used to mark up the found search words on a page when jumped to from a link
377
     * in a search-result.
378
     * @var string
379
     * @internal
380
     */
381
    public $sWordRegEx = '';
382
383
    /**
384
     * Is set to the incoming array sword_list in case of a page-view jumped to from
385
     * a search-result.
386
     * @var string
387
     * @internal
388
     */
389
    public $sWordList = '';
390
391
    /**
392
     * A string prepared for insertion in all links on the page as url-parameters.
393
     * Based on configuration in TypoScript where you defined which GET_VARS you
394
     * would like to pass on.
395
     * @var string
396
     */
397
    public $linkVars = '';
398
399
    /**
400
     * If set, edit icons are rendered aside content records. Must be set only if
401
     * the ->beUserLogin flag is set and set_no_cache() must be called as well.
402
     * @var string
403
     */
404
    public $displayEditIcons = '';
405
406
    /**
407
     * If set, edit icons are rendered aside individual fields of content. Must be
408
     * set only if the ->beUserLogin flag is set and set_no_cache() must be called as
409
     * well.
410
     * @var string
411
     */
412
    public $displayFieldEditIcons = '';
413
414
    /**
415
     * 'Global' Storage for various applications. Keys should be 'tx_'.extKey for
416
     * extensions.
417
     * @var array
418
     */
419
    public $applicationData = [];
420
421
    /**
422
     * @var array
423
     */
424
    public $register = [];
425
426
    /**
427
     * Stack used for storing array and retrieving register arrays (see
428
     * LOAD_REGISTER and RESTORE_REGISTER)
429
     * @var array
430
     */
431
    public $registerStack = [];
432
433
    /**
434
     * Checking that the function is not called eternally. This is done by
435
     * interrupting at a depth of 50
436
     * @var int
437
     */
438
    public $cObjectDepthCounter = 50;
439
440
    /**
441
     * Used by RecordContentObject and ContentContentObject to ensure the a records is NOT
442
     * rendered twice through it!
443
     * @var array
444
     */
445
    public $recordRegister = [];
446
447
    /**
448
     * This is set to the [table]:[uid] of the latest record rendered. Note that
449
     * class ContentObjectRenderer has an equal value, but that is pointing to the
450
     * record delivered in the $data-array of the ContentObjectRenderer instance, if
451
     * the cObjects CONTENT or RECORD created that instance
452
     * @var string
453
     */
454
    public $currentRecord = '';
455
456
    /**
457
     * Used by class \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject
458
     * to keep track of access-keys.
459
     * @var array
460
     */
461
    public $accessKey = [];
462
463
    /**
464
     * Used to generate page-unique keys. Point is that uniqid() functions is very
465
     * slow, so a unikey key is made based on this, see function uniqueHash()
466
     * @var int
467
     * @internal
468
     */
469
    protected $uniqueCounter = 0;
470
471
    /**
472
     * @var string
473
     * @internal
474
     */
475
    protected $uniqueString = '';
476
477
    /**
478
     * This value will be used as the title for the page in the indexer (if
479
     * indexing happens)
480
     * @var string
481
     * @internal only used by TYPO3 Core, use PageTitle API instead.
482
     */
483
    public $indexedDocTitle = '';
484
485
    /**
486
     * The base URL set for the page header.
487
     * @var string
488
     */
489
    public $baseUrl = '';
490
491
    /**
492
     * Page content render object
493
     *
494
     * @var ContentObjectRenderer|string
495
     */
496
    public $cObj = '';
497
498
    /**
499
     * All page content is accumulated in this variable. See RequestHandler
500
     * @var string
501
     */
502
    public $content = '';
503
504
    /**
505
     * Output charset of the websites content. This is the charset found in the
506
     * header, meta tag etc. If different than utf-8 a conversion
507
     * happens before output to browser. Defaults to utf-8.
508
     * @var string
509
     */
510
    public $metaCharset = 'utf-8';
511
512
    /**
513
     * Internal calculations for labels
514
     *
515
     * @var LanguageService
516
     */
517
    protected $languageService;
518
519
    /**
520
     * @var LockingStrategyInterface[][]
521
     */
522
    protected $locks = [];
523
524
    /**
525
     * @var PageRenderer
526
     */
527
    protected $pageRenderer;
528
529
    /**
530
     * The page cache object, use this to save pages to the cache and to
531
     * retrieve them again
532
     *
533
     * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
534
     */
535
    protected $pageCache;
536
537
    /**
538
     * @var array
539
     */
540
    protected $pageCacheTags = [];
541
542
    /**
543
     * Content type HTTP header being sent in the request.
544
     * @todo Ticket: #63642 Should be refactored to a request/response model later
545
     * @internal Should only be used by TYPO3 core for now
546
     *
547
     * @var string
548
     */
549
    protected $contentType = 'text/html';
550
551
    /**
552
     * Doctype to use
553
     *
554
     * @var string
555
     */
556
    public $xhtmlDoctype = '';
557
558
    /**
559
     * @var int
560
     */
561
    public $xhtmlVersion;
562
563
    /**
564
     * Originally requested id from the initial $_GET variable
565
     *
566
     * @var int
567
     */
568
    protected $requestedId;
569
570
    /**
571
     * The context for keeping the current state, mostly related to current page information,
572
     * backend user / frontend user access, workspaceId
573
     *
574
     * @var Context
575
     */
576
    protected $context;
577
578
    /**
579
     * Since TYPO3 v10.0, TSFE is composed out of
580
     *  - Context
581
     *  - Site
582
     *  - SiteLanguage
583
     *  - PageArguments (containing ID, Type, cHash and MP arguments)
584
     *
585
     * Also sets a unique string (->uniqueString) for this script instance; A md5 hash of the microtime()
586
     *
587
     * @param Context $context the Context object to work with
588
     * @param SiteInterface $site The resolved site to work with
589
     * @param SiteLanguage $siteLanguage The resolved language to work with
590
     * @param PageArguments $pageArguments The PageArguments object containing Page ID, type and GET parameters
591
     * @param FrontendUserAuthentication $frontendUser a FrontendUserAuthentication object
592
     */
593
    public function __construct(Context $context, SiteInterface $site, SiteLanguage $siteLanguage, PageArguments $pageArguments, FrontendUserAuthentication $frontendUser)
594
    {
595
        $this->initializeContext($context);
596
        $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...
597
        $this->language = $siteLanguage;
598
        $this->setPageArguments($pageArguments);
599
        $this->fe_user = $frontendUser;
600
        $this->uniqueString = md5(microtime());
601
        $this->initPageRenderer();
602
        $this->initCaches();
603
        // Initialize LLL behaviour
604
        $this->setOutputLanguage();
605
    }
606
607
    private function initializeContext(Context $context): void
608
    {
609
        $this->context = $context;
610
        if (!$this->context->hasAspect('frontend.preview')) {
611
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class));
612
        }
613
    }
614
615
    /**
616
     * Initializes the page renderer object
617
     */
618
    protected function initPageRenderer()
619
    {
620
        if ($this->pageRenderer !== null) {
621
            return;
622
        }
623
        $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
624
        $this->pageRenderer->setTemplateFile('EXT:frontend/Resources/Private/Templates/MainPage.html');
625
        // As initPageRenderer could be called in constructor and for USER_INTs, this information is only set
626
        // once - in order to not override any previous settings of PageRenderer.
627
        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...
628
            $this->pageRenderer->setLanguage($this->language->getTypo3Language());
629
        }
630
    }
631
632
    /**
633
     * @param string $contentType
634
     * @internal Should only be used by TYPO3 core for now
635
     */
636
    public function setContentType($contentType)
637
    {
638
        $this->contentType = $contentType;
639
    }
640
641
    /********************************************
642
     *
643
     * Initializing, resolving page id
644
     *
645
     ********************************************/
646
    /**
647
     * Initializes the caching system.
648
     */
649
    protected function initCaches()
650
    {
651
        $this->pageCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('pages');
652
    }
653
654
    /**
655
     * Initializes the front-end user groups.
656
     * Sets frontend.user aspect based on front-end user status.
657
     */
658
    public function initUserGroups()
659
    {
660
        $userGroups = [0];
661
        // This affects the hidden-flag selecting the fe_groups for the user!
662
        $this->fe_user->showHiddenRecords = $this->context->getPropertyFromAspect('visibility', 'includeHiddenContent', false);
663
        // no matter if we have an active user we try to fetch matching groups which can be set without an user (simulation for instance!)
664
        $this->fe_user->fetchGroupData();
665
        $isUserAndGroupSet = is_array($this->fe_user->user) && !empty($this->fe_user->groupData['uid']);
666
        if ($isUserAndGroupSet) {
667
            // group -2 is not an existing group, but denotes a 'default' group when a user IS logged in.
668
            // This is used to let elements be shown for all logged in users!
669
            $userGroups[] = -2;
670
            $groupsFromUserRecord = $this->fe_user->groupData['uid'];
671
        } else {
672
            // group -1 is not an existing group, but denotes a 'default' group when not logged in.
673
            // This is used to let elements be hidden, when a user is logged in!
674
            $userGroups[] = -1;
675
            if ($this->loginAllowedInBranch) {
676
                // 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.
677
                $groupsFromUserRecord = $this->fe_user->groupData['uid'];
678
            } else {
679
                // Set to blank since we will NOT risk any groups being set when no logins are allowed!
680
                $groupsFromUserRecord = [];
681
            }
682
        }
683
        // Clean up.
684
        // Make unique and sort the groups
685
        $groupsFromUserRecord = array_unique($groupsFromUserRecord);
686
        if (!empty($groupsFromUserRecord) && !$this->loginAllowedInBranch_mode) {
687
            sort($groupsFromUserRecord);
688
            $userGroups = array_merge($userGroups, array_map('intval', $groupsFromUserRecord));
689
        }
690
691
        $this->context->setAspect('frontend.user', GeneralUtility::makeInstance(UserAspect::class, $this->fe_user, $userGroups));
692
693
        // For every 60 seconds the is_online timestamp for a logged-in user is updated
694
        if ($isUserAndGroupSet) {
695
            $this->fe_user->updateOnlineTimestamp();
696
        }
697
698
        $this->logger->debug('Valid usergroups for TSFE: ' . implode(',', $userGroups));
699
    }
700
701
    /**
702
     * Checking if a user is logged in or a group constellation different from "0,-1"
703
     *
704
     * @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!)
705
     */
706
    public function isUserOrGroupSet()
707
    {
708
        /** @var UserAspect $userAspect */
709
        $userAspect = $this->context->getAspect('frontend.user');
710
        return $userAspect->isUserOrGroupSet();
711
    }
712
713
    /**
714
     * Clears the preview-flags, sets sim_exec_time to current time.
715
     * Hidden pages must be hidden as default, $GLOBALS['SIM_EXEC_TIME'] is set to $GLOBALS['EXEC_TIME']
716
     * in bootstrap initializeGlobalTimeVariables(). Alter it by adding or subtracting seconds.
717
     */
718
    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...
719
    {
720
        if ($this->context->getPropertyFromAspect('frontend.preview', 'isPreview')
721
            || $GLOBALS['EXEC_TIME'] !== $GLOBALS['SIM_EXEC_TIME']
722
            || $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages', false)
723
            || $this->context->getPropertyFromAspect('visibility', 'includeHiddenContent', false)
724
        ) {
725
            $GLOBALS['SIM_EXEC_TIME'] = $GLOBALS['EXEC_TIME'];
726
            $GLOBALS['SIM_ACCESS_TIME'] = $GLOBALS['ACCESS_TIME'];
727
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class));
728
            $this->context->setAspect('date', GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $GLOBALS['SIM_EXEC_TIME'])));
729
            $this->context->setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class));
730
        }
731
    }
732
733
    /**
734
     * Checks if a backend user is logged in
735
     *
736
     * @return bool whether a backend user is logged in
737
     */
738
    public function isBackendUserLoggedIn()
739
    {
740
        return (bool)$this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false);
741
    }
742
743
    /**
744
     * Determines the id and evaluates any preview settings
745
     * Basically this function is about determining whether a backend user is logged in,
746
     * if he has read access to the page and if he's previewing the page.
747
     * That all determines which id to show and how to initialize the id.
748
     *
749
     * @param ServerRequestInterface|null $request
750
     */
751
    public function determineId(ServerRequestInterface $request = null)
752
    {
753
        $request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals();
754
        // Call pre processing function for id determination
755
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PreProcessing'] ?? [] as $functionReference) {
756
            $parameters = ['parentObject' => $this];
757
            GeneralUtility::callUserFunction($functionReference, $parameters, $this);
758
        }
759
        // If there is a Backend login we are going to check for any preview settings
760
        $originalFrontendUserGroups = $this->applyPreviewSettings($this->getBackendUser());
0 ignored issues
show
Bug introduced by
The method getBackendUser() does not exist on null. ( Ignorable by Annotation )

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

760
        $originalFrontendUserGroups = $this->applyPreviewSettings($this->/** @scrutinizer ignore-call */ getBackendUser());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
761
        // If the front-end is showing a preview, caching MUST be disabled.
762
        $isPreview = $this->context->getPropertyFromAspect('frontend.preview', 'isPreview');
763
        if ($isPreview) {
764
            $this->disableCache();
765
        }
766
        // Now, get the id, validate access etc:
767
        $this->fetch_the_id($request);
768
        // Check if backend user has read access to this page. If not, recalculate the id.
769
        if ($this->isBackendUserLoggedIn() && $isPreview && !$this->getBackendUser()->doesUserHaveAccess($this->page, Permission::PAGE_SHOW)) {
770
            // Resetting
771
            $this->clear_preview();
772
            $this->fe_user->user[$this->fe_user->usergroup_column] = $originalFrontendUserGroups;
773
            // Fetching the id again, now with the preview settings reset.
774
            $this->fetch_the_id($request);
775
        }
776
        // Checks if user logins are blocked for a certain branch and if so, will unset user login and re-fetch ID.
777
        $this->loginAllowedInBranch = $this->checkIfLoginAllowedInBranch();
778
        // Logins are not allowed, but there is a login, so will we run this.
779
        if (!$this->loginAllowedInBranch && $this->isUserOrGroupSet()) {
780
            if ($this->loginAllowedInBranch_mode === 'all') {
781
                // Clear out user and group:
782
                $this->fe_user->hideActiveLogin();
783
                $userGroups = [0, -1];
784
            } else {
785
                $userGroups = [0, -2];
786
            }
787
            $this->context->setAspect('frontend.user', GeneralUtility::makeInstance(UserAspect::class, $this->fe_user, $userGroups));
788
            // Fetching the id again, now with the preview settings reset.
789
            $this->fetch_the_id($request);
790
        }
791
        // Final cleaning.
792
        // Make sure it's an integer
793
        $this->id = ($this->contentPid = (int)$this->id);
794
        // Make sure it's an integer
795
        $this->type = (int)$this->type;
796
        // Call post processing function for id determination:
797
        $_params = ['pObj' => &$this];
798
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc'] ?? [] as $_funcRef) {
799
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
800
        }
801
    }
802
803
    /**
804
     * Evaluates admin panel or workspace settings to see if
805
     * visibility settings like
806
     * - Preview Aspect: isPreview
807
     * - Visibility Aspect: includeHiddenPages
808
     * - Visibility Aspect: includeHiddenContent
809
     * - $simUserGroup
810
     * should be applied to the current object.
811
     *
812
     * @param FrontendBackendUserAuthentication $backendUser
813
     * @return string|null null if no changes to the current frontend usergroups have been made, otherwise the original list of frontend usergroups
814
     * @internal
815
     */
816
    protected function applyPreviewSettings($backendUser = null)
817
    {
818
        if (!$backendUser) {
819
            return null;
820
        }
821
        $originalFrontendUserGroup = null;
822
        if ($this->fe_user->user) {
823
            $originalFrontendUserGroup = $this->context->getPropertyFromAspect('frontend.user', 'groupIds');
824
        }
825
826
        // The preview flag is set if the current page turns out to be hidden
827
        if ($this->id && $this->determineIdIsHiddenPage()) {
828
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, true));
829
            /** @var VisibilityAspect $aspect */
830
            $aspect = $this->context->getAspect('visibility');
831
            $newAspect = GeneralUtility::makeInstance(VisibilityAspect::class, true, $aspect->includeHiddenContent(), $aspect->includeDeletedRecords());
832
            $this->context->setAspect('visibility', $newAspect);
833
        }
834
        // The preview flag will be set if an offline workspace will be previewed
835
        if ($this->whichWorkspace() > 0) {
836
            $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, true));
837
        }
838
        return $this->context->getPropertyFromAspect('frontend.preview', 'preview', false) ? $originalFrontendUserGroup : null;
839
    }
840
841
    /**
842
     * Checks if the page is hidden in the active workspace.
843
     * If it is hidden, preview flags will be set.
844
     *
845
     * @return bool
846
     */
847
    protected function determineIdIsHiddenPage()
848
    {
849
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
850
            ->getQueryBuilderForTable('pages');
851
        $queryBuilder
852
            ->getRestrictions()
853
            ->removeAll()
854
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
855
856
        $queryBuilder
857
            ->select('uid', 'hidden', 'starttime', 'endtime')
858
            ->from('pages')
859
            ->where(
860
                $queryBuilder->expr()->gte('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
861
            )
862
            ->setMaxResults(1);
863
864
        // $this->id always points to the ID of the default language page, so we check
865
        // the current site language to determine if we need to fetch a translation but consider fallbacks
866
        if ($this->language->getLanguageId() > 0) {
867
            $languagesToCheck = array_merge([$this->language->getLanguageId()], $this->language->getFallbackLanguageIds());
868
            // Check for the language and all its fallbacks
869
            $constraint = $queryBuilder->expr()->andX(
870
                $queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)),
871
                $queryBuilder->expr()->in('sys_language_uid', $queryBuilder->createNamedParameter(array_filter($languagesToCheck), Connection::PARAM_INT_ARRAY))
872
            );
873
            // If the fallback language Ids also contains the default language, this needs to be considered
874
            if (in_array(0, $languagesToCheck, true)) {
875
                $constraint = $queryBuilder->expr()->orX(
876
                    $constraint,
877
                    // Ensure to also fetch the default record
878
                    $queryBuilder->expr()->andX(
879
                        $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)),
880
                        $queryBuilder->expr()->in('sys_language_uid', 0)
881
                    )
882
                );
883
            }
884
            // Ensure that the translated records are shown first (maxResults is set to 1)
885
            $queryBuilder->orderBy('sys_language_uid', 'DESC');
886
        } else {
887
            $constraint = $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT));
888
        }
889
        $queryBuilder->andWhere($constraint);
890
891
        $page = $queryBuilder->execute()->fetch();
892
893
        if ($this->whichWorkspace() > 0) {
894
            // Fetch overlay of page if in workspace and check if it is hidden
895
            $customContext = clone $this->context;
896
            $customContext->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $this->whichWorkspace()));
897
            $customContext->setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class));
898
            $pageSelectObject = GeneralUtility::makeInstance(PageRepository::class, $customContext);
899
            $targetPage = $pageSelectObject->getWorkspaceVersionOfRecord($this->whichWorkspace(), 'pages', $page['uid']);
900
            // Also checks if the workspace version is NOT hidden but the live version is in fact still hidden
901
            $result = $targetPage === -1 || $targetPage === -2 || (is_array($targetPage) && $targetPage['hidden'] == 0 && $page['hidden'] == 1);
902
        } else {
903
            $result = is_array($page) && ($page['hidden'] || $page['starttime'] > $GLOBALS['SIM_EXEC_TIME'] || $page['endtime'] != 0 && $page['endtime'] <= $GLOBALS['SIM_EXEC_TIME']);
904
        }
905
        return $result;
906
    }
907
908
    /**
909
     * Resolves the page id and sets up several related properties.
910
     *
911
     * If $this->id is not set at all or is not a plain integer, the method
912
     * does it's best to set the value to an integer. Resolving is based on
913
     * this options:
914
     *
915
     * - Splitting $this->id if it contains an additional type parameter.
916
     * - Finding the domain record start page
917
     * - First visible page
918
     * - Relocating the id below the domain record if outside
919
     *
920
     * The following properties may be set up or updated:
921
     *
922
     * - id
923
     * - requestedId
924
     * - type
925
     * - sys_page
926
     * - sys_page->where_groupAccess
927
     * - sys_page->where_hid_del
928
     * - Context: FrontendUser Aspect
929
     * - no_cache
930
     * - register['SYS_LASTCHANGED']
931
     * - pageNotFound
932
     *
933
     * Via getPageAndRootlineWithDomain()
934
     *
935
     * - rootLine
936
     * - page
937
     * - MP
938
     * - originalShortcutPage
939
     * - originalMountPointPage
940
     * - pageAccessFailureHistory['direct_access']
941
     * - pageNotFound
942
     *
943
     * @todo:
944
     *
945
     * On the first impression the method does to much. This is increased by
946
     * the fact, that is is called repeated times by the method determineId.
947
     * The reasons are manifold.
948
     *
949
     * 1.) The first part, the creation of sys_page and the type
950
     * resolution don't need to be repeated. They could be separated to be
951
     * called only once.
952
     *
953
     * 2.) The user group setup could be done once on a higher level.
954
     *
955
     * 3.) The workflow of the resolution could be elaborated to be less
956
     * tangled. Maybe the check of the page id to be below the domain via the
957
     * root line doesn't need to be done each time, but for the final result
958
     * only.
959
     *
960
     * 4.) The root line does not need to be directly addressed by this class.
961
     * A root line is always related to one page. The rootline could be handled
962
     * indirectly by page objects. Page objects still don't exist.
963
     *
964
     * @internal
965
     * @param ServerRequestInterface|null $request
966
     */
967
    public function fetch_the_id(ServerRequestInterface $request = null)
0 ignored issues
show
Coding Style introduced by
Method name "TypoScriptFrontendController::fetch_the_id" is not in camel caps format
Loading history...
968
    {
969
        $request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals();
970
        $timeTracker = $this->getTimeTracker();
971
        $timeTracker->push('fetch_the_id initialize/');
972
        // Set the valid usergroups for FE
973
        $this->initUserGroups();
974
        // Initialize the PageRepository has to be done after the frontend usergroups are initialized / resolved, as
975
        // frontend group aspect is modified before
976
        $this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context);
977
        // The id and type is set to the integer-value - just to be sure...
978
        $this->id = (int)$this->id;
979
        $this->type = (int)$this->type;
980
        $timeTracker->pull();
981
        // We find the first page belonging to the current domain
982
        $timeTracker->push('fetch_the_id domain/');
983
        if (!$this->id) {
984
            // If the id was not previously set, set it to the root page id of the site.
985
            $this->id = $this->site->getRootPageId();
986
        }
987
        $timeTracker->pull();
988
        $timeTracker->push('fetch_the_id rootLine/');
989
        // We store the originally requested id
990
        $this->requestedId = $this->id;
991
        try {
992
            $this->getPageAndRootlineWithDomain($this->site->getRootPageId(), $request);
993
        } catch (ShortcutTargetPageNotFoundException $e) {
994
            $this->pageNotFound = 1;
995
        }
996
        $timeTracker->pull();
997
        if ($this->pageNotFound) {
998
            switch ($this->pageNotFound) {
999
                case 1:
1000
                    $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
1001
                        $request,
1002
                        'ID was not an accessible page',
1003
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED)
1004
                    );
1005
                    break;
1006
                case 2:
1007
                    $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
1008
                        $request,
1009
                        'Subsection was found and not accessible',
1010
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED)
1011
                    );
1012
                    break;
1013
                case 3:
1014
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1015
                        $request,
1016
                        'ID was outside the domain',
1017
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH)
1018
                    );
1019
                    break;
1020
                default:
1021
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1022
                        $request,
1023
                        'Unspecified error',
1024
                        $this->getPageAccessFailureReasons()
1025
                    );
1026
            }
1027
            throw new ImmediateResponseException($response, 1533931329);
1028
        }
1029
1030
        $this->setRegisterValueForSysLastChanged($this->page);
1031
1032
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['fetchPageId-PostProcessing'] ?? [] as $functionReference) {
1033
            $parameters = ['parentObject' => $this];
1034
            GeneralUtility::callUserFunction($functionReference, $parameters, $this);
1035
        }
1036
    }
1037
1038
    /**
1039
     * Loads the page and root line records based on $this->id
1040
     *
1041
     * A final page and the matching root line are determined and loaded by
1042
     * the algorithm defined by this method.
1043
     *
1044
     * First it loads the initial page from the page repository for $this->id.
1045
     * If that can't be loaded directly, it gets the root line for $this->id.
1046
     * It walks up the root line towards the root page until the page
1047
     * repository can deliver a page record. (The loading restrictions of
1048
     * the root line records are more liberal than that of the page record.)
1049
     *
1050
     * Now the page type is evaluated and handled if necessary. If the page is
1051
     * a short cut, it is replaced by the target page. If the page is a mount
1052
     * point in overlay mode, the page is replaced by the mounted page.
1053
     *
1054
     * After this potential replacements are done, the root line is loaded
1055
     * (again) for this page record. It walks up the root line up to
1056
     * the first viewable record.
1057
     *
1058
     * (While upon the first accessibility check of the root line it was done
1059
     * by loading page by page from the page repository, this time the method
1060
     * checkRootlineForIncludeSection() is used to find the most distant
1061
     * accessible page within the root line.)
1062
     *
1063
     * Having found the final page id, the page record and the root line are
1064
     * loaded for last time by this method.
1065
     *
1066
     * Exceptions may be thrown for DOKTYPE_SPACER and not loadable page records
1067
     * or root lines.
1068
     *
1069
     * May set or update this properties:
1070
     *
1071
     * @see TypoScriptFrontendController::$id
1072
     * @see TypoScriptFrontendController::$MP
1073
     * @see TypoScriptFrontendController::$page
1074
     * @see TypoScriptFrontendController::$pageNotFound
1075
     * @see TypoScriptFrontendController::$pageAccessFailureHistory
1076
     * @see TypoScriptFrontendController::$originalMountPointPage
1077
     * @see TypoScriptFrontendController::$originalShortcutPage
1078
     *
1079
     * @throws ServiceUnavailableException
1080
     * @throws PageNotFoundException
1081
     */
1082
    protected function getPageAndRootline(ServerRequestInterface $request)
1083
    {
1084
        $requestedPageRowWithoutGroupCheck = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $requestedPageRowWithoutGroupCheck is dead and can be removed.
Loading history...
1085
        $this->resolveTranslatedPageId();
1086
        if (empty($this->page)) {
1087
            // If no page, we try to find the page before in the rootLine.
1088
            // Page is 'not found' in case the id itself was not an accessible page. code 1
1089
            $this->pageNotFound = 1;
1090
            try {
1091
                $requestedPageRowWithoutGroupCheck = $this->sys_page->getPage($this->id, true);
1092
                if (!empty($requestedPageRowWithoutGroupCheck)) {
1093
                    $this->pageAccessFailureHistory['direct_access'][] = $requestedPageRowWithoutGroupCheck;
1094
                }
1095
                $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1096
                if (!empty($this->rootLine)) {
1097
                    $c = count($this->rootLine) - 1;
1098
                    while ($c > 0) {
1099
                        // Add to page access failure history:
1100
                        $this->pageAccessFailureHistory['direct_access'][] = $this->rootLine[$c];
1101
                        // Decrease to next page in rootline and check the access to that, if OK, set as page record and ID value.
1102
                        $c--;
1103
                        $this->id = $this->rootLine[$c]['uid'];
1104
                        $this->page = $this->sys_page->getPage($this->id);
1105
                        if (!empty($this->page)) {
1106
                            break;
1107
                        }
1108
                    }
1109
                }
1110
            } catch (RootLineException $e) {
1111
                $this->rootLine = [];
1112
            }
1113
            // If still no page...
1114
            if (empty($requestedPageRowWithoutGroupCheck) && empty($this->page)) {
1115
                $message = 'The requested page does not exist!';
1116
                $this->logger->error($message);
1117
                try {
1118
                    $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1119
                        $request,
1120
                        $message,
1121
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND)
1122
                    );
1123
                    throw new ImmediateResponseException($response, 1533931330);
1124
                } catch (PageNotFoundException $e) {
1125
                    throw new PageNotFoundException($message, 1301648780);
1126
                }
1127
            }
1128
        }
1129
        // Spacer and sysfolders is not accessible in frontend
1130
        if ($this->page['doktype'] == PageRepository::DOKTYPE_SPACER || $this->page['doktype'] == PageRepository::DOKTYPE_SYSFOLDER) {
1131
            $message = 'The requested page does not exist!';
1132
            $this->logger->error($message);
1133
            try {
1134
                $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1135
                    $request,
1136
                    $message,
1137
                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_INVALID_PAGETYPE)
1138
                );
1139
                throw new ImmediateResponseException($response, 1533931343);
1140
            } catch (PageNotFoundException $e) {
1141
                throw new PageNotFoundException($message, 1301648781);
1142
            }
1143
        }
1144
        // Is the ID a link to another page??
1145
        if ($this->page['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
1146
            // 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.
1147
            $this->MP = '';
1148
            // saving the page so that we can check later - when we know
1149
            // about languages - whether we took the correct shortcut or
1150
            // whether a translation of the page overwrites the shortcut
1151
            // target and we need to follow the new target
1152
            $this->originalShortcutPage = $this->page;
1153
            $this->page = $this->sys_page->getPageShortcut($this->page['shortcut'], $this->page['shortcut_mode'], $this->page['uid']);
1154
            $this->id = $this->page['uid'];
1155
        }
1156
        // If the page is a mountpoint which should be overlaid with the contents of the mounted page,
1157
        // it must never be accessible directly, but only in the mountpoint context. Therefore we change
1158
        // the current ID and the user is redirected by checkPageForMountpointRedirect().
1159
        if ($this->page['doktype'] == PageRepository::DOKTYPE_MOUNTPOINT && $this->page['mount_pid_ol']) {
1160
            $this->originalMountPointPage = $this->page;
1161
            $this->page = $this->sys_page->getPage($this->page['mount_pid']);
1162
            if (empty($this->page)) {
1163
                $message = 'This page (ID ' . $this->originalMountPointPage['uid'] . ') is of type "Mount point" and '
1164
                    . 'mounts a page which is not accessible (ID ' . $this->originalMountPointPage['mount_pid'] . ').';
1165
                throw new PageNotFoundException($message, 1402043263);
1166
            }
1167
            // If the current page is a shortcut, the MP parameter will be replaced
1168
            if ($this->MP === '' || !empty($this->originalShortcutPage)) {
1169
                $this->MP = $this->page['uid'] . '-' . $this->originalMountPointPage['uid'];
1170
            } else {
1171
                $this->MP .= ',' . $this->page['uid'] . '-' . $this->originalMountPointPage['uid'];
1172
            }
1173
            $this->id = $this->page['uid'];
1174
        }
1175
        // Gets the rootLine
1176
        try {
1177
            $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1178
        } catch (RootLineException $e) {
1179
            $this->rootLine = [];
1180
        }
1181
        // If not rootline we're off...
1182
        if (empty($this->rootLine)) {
1183
            $message = 'The requested page didn\'t have a proper connection to the tree-root!';
1184
            $this->logger->error($message);
1185
            try {
1186
                $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
1187
                    $request,
1188
                    $message,
1189
                    $this->getPageAccessFailureReasons(PageAccessFailureReasons::ROOTLINE_BROKEN)
1190
                );
1191
                throw new ImmediateResponseException($response, 1533931350);
1192
            } catch (ServiceUnavailableException $e) {
1193
                throw new ServiceUnavailableException($message, 1301648167);
1194
            }
1195
        }
1196
        // Checking for include section regarding the hidden/starttime/endtime/fe_user (that is access control of a whole subbranch!)
1197
        if ($this->checkRootlineForIncludeSection()) {
1198
            if (empty($this->rootLine)) {
1199
                $message = 'The requested page was not accessible!';
1200
                try {
1201
                    $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
1202
                        $request,
1203
                        $message,
1204
                        $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_GENERAL)
1205
                    );
1206
                    throw new ImmediateResponseException($response, 1533931351);
1207
                } catch (ServiceUnavailableException $e) {
1208
                    $this->logger->warning($message);
1209
                    throw new ServiceUnavailableException($message, 1301648234);
1210
                }
1211
            } else {
1212
                $el = reset($this->rootLine);
1213
                $this->id = $el['uid'];
1214
                $this->page = $this->sys_page->getPage($this->id);
1215
                try {
1216
                    $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1217
                } catch (RootLineException $e) {
1218
                    $this->rootLine = [];
1219
                }
1220
            }
1221
        }
1222
    }
1223
1224
    /**
1225
     * If $this->id contains a translated page record, this needs to be resolved to the default language
1226
     * in order for all rootline functionality and access restrictions to be in place further on.
1227
     *
1228
     * Additionally, if a translated page is found, LanguageAspect is set as well.
1229
     */
1230
    protected function resolveTranslatedPageId()
1231
    {
1232
        $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

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

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

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

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

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

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