Completed
Push — master ( 4dc794...0f9475 )
by
unknown
15:26
created

generatePage_postProcessing()   A

Complexity

Conditions 6
Paths 16

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

3521
                $midnightTime = mktime(0, 0, 0, date('m', $timeOutTime), date('d', $timeOutTime), /** @scrutinizer ignore-type */ date('Y', $timeOutTime));
Loading history...
3522
                // If the midnight time of the expire-day is greater than the current time,
3523
                // we may set the timeOutTime to the new midnighttime.
3524
                if ($midnightTime > $GLOBALS['EXEC_TIME']) {
3525
                    $cacheTimeout = $midnightTime - $GLOBALS['EXEC_TIME'];
3526
                }
3527
            }
3528
3529
            // Calculate the timeout time for records on the page and adjust cache timeout if necessary
3530
            $cacheTimeout = min($this->calculatePageCacheTimeout(), $cacheTimeout);
3531
3532
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['get_cache_timeout'] ?? [] as $_funcRef) {
3533
                $params = ['cacheTimeout' => $cacheTimeout];
3534
                $cacheTimeout = GeneralUtility::callUserFunction($_funcRef, $params, $this);
3535
            }
3536
            $runtimeCache->set($cachedCacheLifetimeIdentifier, $cacheTimeout);
3537
            $cachedCacheLifetime = $cacheTimeout;
3538
        }
3539
        return $cachedCacheLifetime;
3540
    }
3541
3542
    /*********************************************
3543
     *
3544
     * Localization and character set conversion
3545
     *
3546
     *********************************************/
3547
    /**
3548
     * Split Label function for front-end applications.
3549
     *
3550
     * @param string $input Key string. Accepts the "LLL:" prefix.
3551
     * @return string Label value, if any.
3552
     */
3553
    public function sL($input)
3554
    {
3555
        return $this->languageService->sL($input);
3556
    }
3557
3558
    /**
3559
     * Sets all internal measures what language the page should be rendered.
3560
     * This is not for records, but rather the HTML / charset and the locallang labels
3561
     */
3562
    protected function setOutputLanguage()
3563
    {
3564
        $this->languageService = LanguageService::createFromSiteLanguage($this->language);
3565
        // Always disable debugging for TSFE
3566
        $this->languageService->debugKey = false;
3567
    }
3568
3569
    /**
3570
     * Converts input string from utf-8 to metaCharset IF the two charsets are different.
3571
     *
3572
     * @param string $content Content to be converted.
3573
     * @return string Converted content string.
3574
     * @throws \RuntimeException if an invalid charset was configured
3575
     */
3576
    public function convOutputCharset($content)
3577
    {
3578
        if ($this->metaCharset !== 'utf-8') {
3579
            /** @var CharsetConverter $charsetConverter */
3580
            $charsetConverter = GeneralUtility::makeInstance(CharsetConverter::class);
3581
            try {
3582
                $content = $charsetConverter->conv($content, 'utf-8', $this->metaCharset);
3583
            } catch (UnknownCharsetException $e) {
3584
                throw new \RuntimeException('Invalid config.metaCharset: ' . $e->getMessage(), 1508916185);
3585
            }
3586
        }
3587
        return $content;
3588
    }
3589
3590
    /**
3591
     * Calculates page cache timeout according to the records with starttime/endtime on the page.
3592
     *
3593
     * @return int Page cache timeout or PHP_INT_MAX if cannot be determined
3594
     */
3595
    protected function calculatePageCacheTimeout()
3596
    {
3597
        $result = PHP_INT_MAX;
3598
        // Get the configuration
3599
        $tablesToConsider = $this->getCurrentPageCacheConfiguration();
3600
        // Get the time, rounded to the minute (do not pollute MySQL cache!)
3601
        // It is ok that we do not take seconds into account here because this
3602
        // value will be subtracted later. So we never get the time "before"
3603
        // the cache change.
3604
        $now = $GLOBALS['ACCESS_TIME'];
3605
        // Find timeout by checking every table
3606
        foreach ($tablesToConsider as $tableDef) {
3607
            $result = min($result, $this->getFirstTimeValueForRecord($tableDef, $now));
3608
        }
3609
        // We return + 1 second just to ensure that cache is definitely regenerated
3610
        return $result === PHP_INT_MAX ? PHP_INT_MAX : $result - $now + 1;
3611
    }
3612
3613
    /**
3614
     * Obtains a list of table/pid pairs to consider for page caching.
3615
     *
3616
     * TS configuration looks like this:
3617
     *
3618
     * The cache lifetime of all pages takes starttime and endtime of news records of page 14 into account:
3619
     * config.cache.all = tt_news:14
3620
     *
3621
     * The cache.lifetime of the current page allows to take records (e.g. fe_users) into account:
3622
     * config.cache.all = fe_users:current
3623
     *
3624
     * The cache lifetime of page 42 takes starttime and endtime of news records of page 15 and addresses of page 16 into account:
3625
     * config.cache.42 = tt_news:15,tt_address:16
3626
     *
3627
     * @return array Array of 'tablename:pid' pairs. There is at least a current page id in the array
3628
     * @see TypoScriptFrontendController::calculatePageCacheTimeout()
3629
     */
3630
    protected function getCurrentPageCacheConfiguration()
3631
    {
3632
        $result = ['tt_content:' . $this->id];
3633
        if (isset($this->config['config']['cache.'][$this->id])) {
3634
            $result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $this->id, $this->config['config']['cache.'][$this->id])));
3635
        }
3636
        if (isset($this->config['config']['cache.']['all'])) {
3637
            $result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $this->id, $this->config['config']['cache.']['all'])));
3638
        }
3639
        return array_unique($result);
3640
    }
3641
3642
    /**
3643
     * Find the minimum starttime or endtime value in the table and pid that is greater than the current time.
3644
     *
3645
     * @param string $tableDef Table definition (format tablename:pid)
3646
     * @param int $now "Now" time value
3647
     * @throws \InvalidArgumentException
3648
     * @return int Value of the next start/stop time or PHP_INT_MAX if not found
3649
     * @see TypoScriptFrontendController::calculatePageCacheTimeout()
3650
     */
3651
    protected function getFirstTimeValueForRecord($tableDef, $now)
3652
    {
3653
        $now = (int)$now;
3654
        $result = PHP_INT_MAX;
3655
        [$tableName, $pid] = GeneralUtility::trimExplode(':', $tableDef);
3656
        if (empty($tableName) || empty($pid)) {
3657
            throw new \InvalidArgumentException('Unexpected value for parameter $tableDef. Expected <tablename>:<pid>, got \'' . htmlspecialchars($tableDef) . '\'.', 1307190365);
3658
        }
3659
3660
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
3661
            ->getQueryBuilderForTable($tableName);
3662
        $queryBuilder->getRestrictions()
3663
            ->removeByType(StartTimeRestriction::class)
3664
            ->removeByType(EndTimeRestriction::class);
3665
        $timeFields = [];
3666
        $timeConditions = $queryBuilder->expr()->orX();
3667
        foreach (['starttime', 'endtime'] as $field) {
3668
            if (isset($GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field])) {
3669
                $timeFields[$field] = $GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field];
3670
                $queryBuilder->addSelectLiteral(
3671
                    'MIN('
3672
                        . 'CASE WHEN '
3673
                        . $queryBuilder->expr()->lte(
3674
                            $timeFields[$field],
3675
                            $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)
3676
                        )
3677
                        . ' THEN NULL ELSE ' . $queryBuilder->quoteIdentifier($timeFields[$field]) . ' END'
3678
                        . ') AS ' . $queryBuilder->quoteIdentifier($timeFields[$field])
3679
                );
3680
                $timeConditions->add(
3681
                    $queryBuilder->expr()->gt(
3682
                        $timeFields[$field],
3683
                        $queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)
3684
                    )
3685
                );
3686
            }
3687
        }
3688
3689
        // if starttime or endtime are defined, evaluate them
3690
        if (!empty($timeFields)) {
3691
            // find the timestamp, when the current page's content changes the next time
3692
            $row = $queryBuilder
3693
                ->from($tableName)
3694
                ->where(
3695
                    $queryBuilder->expr()->eq(
3696
                        'pid',
3697
                        $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT)
3698
                    ),
3699
                    $timeConditions
3700
                )
3701
                ->execute()
3702
                ->fetch();
3703
3704
            if ($row) {
3705
                foreach ($timeFields as $timeField => $_) {
3706
                    // if a MIN value is found, take it into account for the
3707
                    // cache lifetime we have to filter out start/endtimes < $now,
3708
                    // as the SQL query also returns rows with starttime < $now
3709
                    // and endtime > $now (and using a starttime from the past
3710
                    // would be wrong)
3711
                    if ($row[$timeField] !== null && (int)$row[$timeField] > $now) {
3712
                        $result = min($result, (int)$row[$timeField]);
3713
                    }
3714
                }
3715
            }
3716
        }
3717
3718
        return $result;
3719
    }
3720
3721
    /**
3722
     * Fetches the originally requested id, falls back to $this->id
3723
     *
3724
     * @return int the originally requested page uid
3725
     * @see fetch_the_id()
3726
     */
3727
    public function getRequestedId()
3728
    {
3729
        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...
3730
    }
3731
3732
    /**
3733
     * Acquire a page specific lock
3734
     *
3735
     *
3736
     * The schematics here is:
3737
     * - First acquire an access lock. This is using the type of the requested lock as key.
3738
     *   Since the number of types is rather limited we can use the type as key as it will only
3739
     *   eat up a limited number of lock resources on the system (files, semaphores)
3740
     * - Second, we acquire the actual lock (named page lock). We can be sure we are the only process at this
3741
     *   very moment, hence we either get the lock for the given key or we get an error as we request a non-blocking mode.
3742
     *
3743
     * Interleaving two locks is extremely important, because the actual page lock uses a hash value as key (see callers
3744
     * of this function). If we would simply employ a normal blocking lock, we would get a potentially unlimited
3745
     * (number of pages at least) number of different locks. Depending on the available locking methods on the system
3746
     * we might run out of available resources. (e.g. maximum limit of semaphores is a system setting and applies
3747
     * to the whole system)
3748
     * We therefore must make sure that page locks are destroyed again if they are not used anymore, such that
3749
     * we never use more locking resources than parallel requests to different pages (hashes).
3750
     * In order to ensure this, we need to guarantee that no other process is waiting on a page lock when
3751
     * the process currently having the lock on the page lock is about to release the lock again.
3752
     * This can only be achieved by using a non-blocking mode, such that a process is never put into wait state
3753
     * by the kernel, but only checks the availability of the lock. The access lock is our guard to be sure
3754
     * that no two processes are at the same time releasing/destroying a page lock, whilst the other one tries to
3755
     * get a lock for this page lock.
3756
     * The only drawback of this implementation is that we basically have to poll the availability of the page lock.
3757
     *
3758
     * Note that the access lock resources are NEVER deleted/destroyed, otherwise the whole thing would be broken.
3759
     *
3760
     * @param string $type
3761
     * @param string $key
3762
     * @throws \InvalidArgumentException
3763
     * @throws \RuntimeException
3764
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
3765
     */
3766
    protected function acquireLock($type, $key)
3767
    {
3768
        $lockFactory = GeneralUtility::makeInstance(LockFactory::class);
3769
        $this->locks[$type]['accessLock'] = $lockFactory->createLocker($type);
3770
3771
        $this->locks[$type]['pageLock'] = $lockFactory->createLocker(
3772
            $key,
3773
            LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
3774
        );
3775
3776
        do {
3777
            if (!$this->locks[$type]['accessLock']->acquire()) {
3778
                throw new \RuntimeException('Could not acquire access lock for "' . $type . '"".', 1294586098);
3779
            }
3780
3781
            try {
3782
                $locked = $this->locks[$type]['pageLock']->acquire(
3783
                    LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
3784
                );
3785
            } catch (LockAcquireWouldBlockException $e) {
3786
                // somebody else has the lock, we keep waiting
3787
3788
                // first release the access lock
3789
                $this->locks[$type]['accessLock']->release();
3790
                // now lets make a short break (100ms) until we try again, since
3791
                // the page generation by the lock owner will take a while anyways
3792
                usleep(100000);
3793
                continue;
3794
            }
3795
            $this->locks[$type]['accessLock']->release();
3796
            if ($locked) {
3797
                break;
3798
            }
3799
            throw new \RuntimeException('Could not acquire page lock for ' . $key . '.', 1460975877);
3800
        } while (true);
3801
    }
3802
3803
    /**
3804
     * Release a page specific lock
3805
     *
3806
     * @param string $type
3807
     * @throws \InvalidArgumentException
3808
     * @throws \RuntimeException
3809
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
3810
     */
3811
    protected function releaseLock($type)
3812
    {
3813
        if ($this->locks[$type]['accessLock']) {
3814
            if (!$this->locks[$type]['accessLock']->acquire()) {
3815
                throw new \RuntimeException('Could not acquire access lock for "' . $type . '"".', 1460975902);
3816
            }
3817
3818
            $this->locks[$type]['pageLock']->release();
3819
            $this->locks[$type]['pageLock']->destroy();
3820
            $this->locks[$type]['pageLock'] = null;
3821
3822
            $this->locks[$type]['accessLock']->release();
3823
            $this->locks[$type]['accessLock'] = null;
3824
        }
3825
    }
3826
3827
    /**
3828
     * Send additional headers from config.additionalHeaders
3829
     */
3830
    protected function getAdditionalHeaders(): array
3831
    {
3832
        if (!isset($this->config['config']['additionalHeaders.'])) {
3833
            return [];
3834
        }
3835
        $additionalHeaders = [];
3836
        ksort($this->config['config']['additionalHeaders.']);
3837
        foreach ($this->config['config']['additionalHeaders.'] as $options) {
3838
            if (!is_array($options)) {
3839
                continue;
3840
            }
3841
            $header = trim($options['header'] ?? '');
3842
            if ($header === '') {
3843
                continue;
3844
            }
3845
            $additionalHeaders[] = [
3846
                'header' => $header,
3847
                // "replace existing headers" is turned on by default, unless turned off
3848
                'replace' => ($options['replace'] ?? '') !== '0',
3849
                'statusCode' => (int)($options['httpResponseCode'] ?? 0) ?: null
3850
            ];
3851
        }
3852
        return $additionalHeaders;
3853
    }
3854
3855
    /**
3856
     * Returns the current BE user.
3857
     *
3858
     * @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication
3859
     */
3860
    protected function getBackendUser()
3861
    {
3862
        return $GLOBALS['BE_USER'];
3863
    }
3864
3865
    /**
3866
     * @return TimeTracker
3867
     */
3868
    protected function getTimeTracker()
3869
    {
3870
        return GeneralUtility::makeInstance(TimeTracker::class);
3871
    }
3872
3873
    /**
3874
     * Return the global instance of this class.
3875
     *
3876
     * Intended to be used as prototype factory for this class, see Services.yaml.
3877
     * This is required as long as TypoScriptFrontendController needs request
3878
     * dependent constructor parameters. Once that has been refactored this
3879
     * factory will be removed.
3880
     *
3881
     * @return TypoScriptFrontendController
3882
     * @internal
3883
     */
3884
    public static function getGlobalInstance(): ?self
3885
    {
3886
        if ($GLOBALS['TSFE'] instanceof self) {
3887
            return $GLOBALS['TSFE'];
3888
        }
3889
3890
        if (!(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_FE)) {
3891
            // Return null for now (together with shared: false in Services.yaml) as TSFE might not be available in backend context
3892
            // That's not an error then
3893
            return null;
3894
        }
3895
3896
        throw new \LogicException('TypoScriptFrontendController was tried to be injected before initial creation', 1538370377);
3897
    }
3898
3899
    public function getLanguage(): SiteLanguage
3900
    {
3901
        return $this->language;
3902
    }
3903
3904
    public function getSite(): Site
3905
    {
3906
        return $this->site;
3907
    }
3908
3909
    public function getContext(): Context
3910
    {
3911
        return $this->context;
3912
    }
3913
3914
    public function getPageArguments(): PageArguments
3915
    {
3916
        return $this->pageArguments;
3917
    }
3918
3919
    /**
3920
     * Deprecation messages for TYPO3 10 - public properties of TSFE which have been (re)moved
3921
     */
3922
    /**
3923
     * Checks if the property of the given name is set.
3924
     *
3925
     * Unmarked protected properties must return false as usual.
3926
     * Marked properties are evaluated by isset().
3927
     *
3928
     * This method is not called for public properties.
3929
     *
3930
     * @param string $propertyName
3931
     * @return bool
3932
     */
3933
    public function __isset(string $propertyName)
3934
    {
3935
        switch ($propertyName) {
3936
            case 'domainStartPage':
3937
                trigger_error('Property $TSFE->domainStartPage is not in use anymore as this information is now stored within the Site object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
3938
                return  true;
0 ignored issues
show
Coding Style introduced by
Language constructs must be followed by a single space; expected 1 space but found 2
Loading history...
3939
            case 'cHash':
3940
                trigger_error('Property $TSFE->cHash is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
3941
                return isset($this->pageArguments->getArguments()['cHash']);
3942
            case 'cHash_array':
3943
                trigger_error('Property $TSFE->cHash_array is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
3944
                $value = $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments);
3945
                return !empty($value);
3946
            case 'sys_language_isocode':
3947
                trigger_error('Property $TSFE->sys_language_isocode is not in use anymore as this information is now stored within the SiteLanguage object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
3948
                return isset($this->$propertyName);
3949
            case 'divSection':
3950
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
3951
                return isset($this->$propertyName);
3952
            case 'fePreview':
3953
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
3954
                return $this->context->hasAspect('frontend.preview');
3955
            case 'forceTemplateParsing':
3956
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
3957
                return $this->context->hasAspect('typoscript') && $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing');
3958
        }
3959
        return false;
3960
    }
3961
3962
    /**
3963
     * Gets the value of the property of the given name if tagged.
3964
     *
3965
     * The evaluation is done in the assumption that this method is never
3966
     * reached for a public property.
3967
     *
3968
     * @param string $propertyName
3969
     * @return mixed
3970
     */
3971
    public function __get(string $propertyName)
3972
    {
3973
        switch ($propertyName) {
3974
            case 'domainStartPage':
3975
                trigger_error('Property $TSFE->domainStartPage is not in use anymore as this information is now stored within the Site object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
3976
                return $this->site->getRootPageId();
3977
            case 'cHash':
3978
                trigger_error('Property $TSFE->cHash is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
3979
                return $this->pageArguments->getArguments()['cHash'] ?? false;
3980
            case 'cHash_array':
3981
                trigger_error('Property $TSFE->cHash_array is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
3982
                return $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments);
3983
            case 'sys_language_isocode':
3984
                trigger_error('Property $TSFE->sys_language_isocode is not in use anymore as this information is now stored within the SiteLanguage object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
3985
                return $this->sys_language_isocode ?? $this->language->getTwoLetterIsoCode();
3986
            case 'divSection':
3987
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
3988
                break;
3989
            case 'fePreview':
3990
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
3991
                if ($this->context->hasAspect('frontend.preview')) {
3992
                    return $this->context->getPropertyFromAspect('frontend.preview', 'isPreview');
3993
                }
3994
                break;
3995
            case 'forceTemplateParsing':
3996
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
3997
                if ($this->context->hasAspect('typoscript')) {
3998
                    return $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing');
3999
                }
4000
                break;
4001
        }
4002
        return $this->$propertyName;
4003
    }
4004
4005
    /**
4006
     * Sets the property of the given name if tagged.
4007
     *
4008
     * Additionally it's allowed to set unknown properties.
4009
     *
4010
     * The evaluation is done in the assumption that this method is never
4011
     * reached for a public property.
4012
     *
4013
     * @param string $propertyName
4014
     * @param mixed $propertyValue
4015
     */
4016
    public function __set(string $propertyName, $propertyValue)
4017
    {
4018
        switch ($propertyName) {
4019
            case 'domainStartPage':
4020
                trigger_error('Property $TSFE->domainStartPage is not in use anymore as this information is now stored within the Site object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4021
                break;
4022
            case 'cHash':
4023
                trigger_error('Property $TSFE->cHash is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4024
                break;
4025
            case 'cHash_array':
4026
                trigger_error('Property $TSFE->cHash_array is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4027
                break;
4028
            case 'sys_language_isocode':
4029
                trigger_error('Property $TSFE->sys_language_isocode is not in use anymore as this information is now stored within the SiteLanguage object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4030
                break;
4031
            case 'divSection':
4032
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4033
                break;
4034
            case 'fePreview':
4035
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
4036
                $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, (bool)$propertyValue));
4037
                break;
4038
            case 'forceTemplateParsing':
4039
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
4040
                $this->context->setAspect('typoscript', GeneralUtility::makeInstance(TypoScriptAspect::class, (bool)$propertyValue));
4041
                break;
4042
        }
4043
        $this->$propertyName = $propertyValue;
4044
    }
4045
4046
    /**
4047
     * Unsets the property of the given name if tagged.
4048
     *
4049
     * @param string $propertyName
4050
     */
4051
    public function __unset(string $propertyName)
4052
    {
4053
        switch ($propertyName) {
4054
            case 'domainStartPage':
4055
                trigger_error('Property $TSFE->domainStartPage is not in use anymore as this information is now stored within the Site object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4056
                break;
4057
            case 'cHash':
4058
                trigger_error('Property $TSFE->cHash is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4059
                break;
4060
            case 'cHash_array':
4061
                trigger_error('Property $TSFE->cHash_array is not in use anymore as this information is now stored within the PageArguments object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4062
                break;
4063
            case 'sys_language_isocode':
4064
                trigger_error('Property $TSFE->sys_language_isocode is not in use anymore as this information is now stored within the SiteLanguage object. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4065
                break;
4066
            case 'divSection':
4067
                trigger_error('Property $TSFE->divSection is not in use anymore. Will be removed in TYPO3 v11.0.', E_USER_DEPRECATED);
4068
                break;
4069
            case 'fePreview':
4070
                trigger_error('Property $TSFE->fePreview is not in use anymore as this information is now stored within the FrontendPreview aspect.', E_USER_DEPRECATED);
4071
                $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, false));
4072
                break;
4073
            case 'forceTemplateParsing':
4074
                trigger_error('Property $TSFE->forceTemplateParsing is not in use anymore as this information is now stored within the TypoScript aspect.', E_USER_DEPRECATED);
4075
                $this->context->setAspect('typoscript', GeneralUtility::makeInstance(TypoScriptAspect::class, false));
4076
                break;
4077
        }
4078
        unset($this->$propertyName);
4079
    }
4080
}
4081