Completed
Push — master ( 3dc97c...4996b5 )
by
unknown
37:18 queued 23:26
created

recursivelyReplaceIntPlaceholdersInContent()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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