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

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

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