[TASK] Use a reference variable to pass $this into hooks
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Controller / TypoScriptFrontendController.php
1 <?php
2 namespace TYPO3\CMS\Frontend\Controller;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Psr\Http\Message\ResponseInterface;
18 use Psr\Http\Message\ServerRequestInterface;
19 use Psr\Log\LoggerAwareInterface;
20 use Psr\Log\LoggerAwareTrait;
21 use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
22 use TYPO3\CMS\Core\Cache\CacheManager;
23 use TYPO3\CMS\Core\Charset\CharsetConverter;
24 use TYPO3\CMS\Core\Charset\UnknownCharsetException;
25 use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader;
26 use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser;
27 use TYPO3\CMS\Core\Context\Context;
28 use TYPO3\CMS\Core\Context\DateTimeAspect;
29 use TYPO3\CMS\Core\Context\LanguageAspect;
30 use TYPO3\CMS\Core\Context\LanguageAspectFactory;
31 use TYPO3\CMS\Core\Context\TypoScriptAspect;
32 use TYPO3\CMS\Core\Context\UserAspect;
33 use TYPO3\CMS\Core\Context\VisibilityAspect;
34 use TYPO3\CMS\Core\Context\WorkspaceAspect;
35 use TYPO3\CMS\Core\Core\Environment;
36 use TYPO3\CMS\Core\Database\Connection;
37 use TYPO3\CMS\Core\Database\ConnectionPool;
38 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
39 use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
40 use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
41 use TYPO3\CMS\Core\Domain\Repository\PageRepository;
42 use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
43 use TYPO3\CMS\Core\Error\Http\ServiceUnavailableException;
44 use TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException;
45 use TYPO3\CMS\Core\Exception\Page\RootLineException;
46 use TYPO3\CMS\Core\Http\ImmediateResponseException;
47 use TYPO3\CMS\Core\Http\ServerRequestFactory;
48 use TYPO3\CMS\Core\Localization\LanguageService;
49 use TYPO3\CMS\Core\Localization\Locales;
50 use TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException;
51 use TYPO3\CMS\Core\Locking\LockFactory;
52 use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
53 use TYPO3\CMS\Core\Page\PageRenderer;
54 use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager;
55 use TYPO3\CMS\Core\Resource\StorageRepository;
56 use TYPO3\CMS\Core\Routing\PageArguments;
57 use TYPO3\CMS\Core\Site\Entity\Site;
58 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
59 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
60 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
61 use TYPO3\CMS\Core\Type\Bitmask\Permission;
62 use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
63 use TYPO3\CMS\Core\TypoScript\TemplateService;
64 use TYPO3\CMS\Core\Utility\ArrayUtility;
65 use TYPO3\CMS\Core\Utility\GeneralUtility;
66 use TYPO3\CMS\Core\Utility\HttpUtility;
67 use TYPO3\CMS\Core\Utility\MathUtility;
68 use TYPO3\CMS\Core\Utility\PathUtility;
69 use TYPO3\CMS\Core\Utility\RootlineUtility;
70 use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
71 use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
72 use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
73 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
74 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
75 use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
76 use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
77
78 /**
79 * Class for the built TypoScript based frontend. Instantiated in
80 * \TYPO3\CMS\Frontend\Http\RequestHandler as the global object TSFE.
81 *
82 * Main frontend class, instantiated in \TYPO3\CMS\Frontend\Http\RequestHandler
83 * as the global object TSFE.
84 *
85 * This class has a lot of functions and internal variable which are used from
86 * \TYPO3\CMS\Frontend\Http\RequestHandler
87 *
88 * The class is instantiated as $GLOBALS['TSFE'] in \TYPO3\CMS\Frontend\Http\RequestHandler.
89 *
90 * The use of this class should be inspired by the order of function calls as
91 * found in \TYPO3\CMS\Frontend\Http\RequestHandler.
92 */
93 class TypoScriptFrontendController implements LoggerAwareInterface
94 {
95 use LoggerAwareTrait;
96
97 /**
98 * The page id (int)
99 * @var string
100 */
101 public $id = '';
102
103 /**
104 * The type (read-only)
105 * @var int
106 */
107 public $type = '';
108
109 /**
110 * @var Site
111 */
112 protected $site;
113
114 /**
115 * @var SiteLanguage
116 */
117 protected $language;
118
119 /**
120 * The submitted cHash
121 * @var string
122 * @internal
123 * @deprecated will be removed in TYPO3 v11.0. don't use it anymore, as this is now within the PageArguments property.
124 */
125 protected $cHash = '';
126
127 /**
128 * @var PageArguments
129 * @internal
130 */
131 protected $pageArguments;
132
133 /**
134 * Page will not be cached. Write only TRUE. Never clear value (some other
135 * code might have reasons to set it TRUE).
136 * @var bool
137 */
138 public $no_cache = false;
139
140 /**
141 * The rootLine (all the way to tree root, not only the current site!)
142 * @var array
143 */
144 public $rootLine = [];
145
146 /**
147 * The pagerecord
148 * @var array
149 */
150 public $page = [];
151
152 /**
153 * This will normally point to the same value as id, but can be changed to
154 * point to another page from which content will then be displayed instead.
155 * @var int
156 */
157 public $contentPid = 0;
158
159 /**
160 * Gets set when we are processing a page of type mounpoint with enabled overlay in getPageAndRootline()
161 * Used later in checkPageForMountpointRedirect() to determine the final target URL where the user
162 * should be redirected to.
163 *
164 * @var array|null
165 */
166 protected $originalMountPointPage;
167
168 /**
169 * Gets set when we are processing a page of type shortcut in the early stages
170 * of the request when we do not know about languages yet, used later in the request
171 * to determine the correct shortcut in case a translation changes the shortcut
172 * target
173 * @var array|null
174 * @see checkTranslatedShortcut()
175 */
176 protected $originalShortcutPage;
177
178 /**
179 * sys_page-object, pagefunctions
180 *
181 * @var PageRepository
182 */
183 public $sys_page = '';
184
185 /**
186 * Is set to 1 if a pageNotFound handler could have been called.
187 * @var int
188 * @internal
189 */
190 public $pageNotFound = 0;
191
192 /**
193 * Domain start page
194 * @var int
195 * @internal
196 * @deprecated will be removed in TYPO3 v11.0. don't use it anymore, as this is now within the Site. see $this->site->getRootPageId()
197 */
198 protected $domainStartPage = 0;
199
200 /**
201 * Array containing a history of why a requested page was not accessible.
202 * @var array
203 */
204 protected $pageAccessFailureHistory = [];
205
206 /**
207 * @var string
208 * @internal
209 */
210 public $MP = '';
211
212 /**
213 * The frontend user
214 *
215 * @var FrontendUserAuthentication
216 */
217 public $fe_user = '';
218
219 /**
220 * Shows whether logins are allowed in branch
221 * @var bool
222 */
223 protected $loginAllowedInBranch = true;
224
225 /**
226 * Shows specific mode (all or groups)
227 * @var string
228 * @internal
229 */
230 protected $loginAllowedInBranch_mode = '';
231
232 /**
233 * Flag indication that preview is active. This is based on the login of a
234 * backend user and whether the backend user has read access to the current
235 * page.
236 * @var int
237 * @internal
238 * @deprecated will be removed in TYPO3 v11.0. don't use it anymore, as this is now within PreviewAspect
239 */
240 protected $fePreview = 0;
241
242 /**
243 * Value that contains the simulated usergroup if any
244 * @var int
245 * @internal only to be used in AdminPanel, and within TYPO3 Core
246 */
247 public $simUserGroup = 0;
248
249 /**
250 * "CONFIG" object from TypoScript. Array generated based on the TypoScript
251 * configuration of the current page. Saved with the cached pages.
252 * @var array
253 */
254 public $config = [];
255
256 /**
257 * The TypoScript template object. Used to parse the TypoScript template
258 *
259 * @var TemplateService
260 */
261 public $tmpl;
262
263 /**
264 * Is set to the time-to-live time of cached pages. Default is 60*60*24, which is 24 hours.
265 *
266 * @var int
267 * @internal
268 */
269 protected $cacheTimeOutDefault = 86400;
270
271 /**
272 * Set internally if cached content is fetched from the database.
273 *
274 * @var bool
275 * @internal
276 */
277 protected $cacheContentFlag = false;
278
279 /**
280 * Set to the expire time of cached content
281 * @var int
282 * @internal
283 */
284 protected $cacheExpires = 0;
285
286 /**
287 * Set if cache headers allowing caching are sent.
288 * @var bool
289 * @internal
290 */
291 protected $isClientCachable = false;
292
293 /**
294 * Used by template fetching system. This array is an identification of
295 * the template. If $this->all is empty it's because the template-data is not
296 * cached, which it must be.
297 * @var array
298 */
299 public $all = [];
300
301 /**
302 * Toplevel - objArrayName, eg 'page'
303 * @var string
304 */
305 public $sPre = '';
306
307 /**
308 * TypoScript configuration of the page-object pointed to by sPre.
309 * $this->tmpl->setup[$this->sPre.'.']
310 * @var array
311 */
312 public $pSetup = '';
313
314 /**
315 * This hash is unique to the template, the $this->id and $this->type vars and
316 * the list of groups. Used to get and later store the cached data
317 * @var string
318 * @internal
319 */
320 public $newHash = '';
321
322 /**
323 * This flag is set before the page is generated IF $this->no_cache is set. If this
324 * flag is set after the page content was generated, $this->no_cache is forced to be set.
325 * This is done in order to make sure that PHP code from Plugins / USER scripts does not falsely
326 * clear the no_cache flag.
327 * @var bool
328 * @internal
329 */
330 protected $no_cacheBeforePageGen = false;
331
332 /**
333 * Passed to TypoScript template class and tells it to force template rendering
334 * @var bool
335 * @deprecated
336 */
337 private $forceTemplateParsing = false;
338
339 /**
340 * The array which cHash_calc is based on, see PageArgumentValidator class.
341 * @var array
342 * @internal
343 * @deprecated will be removed in TYPO3 v11.0. don't use it anymore, see getRelevantParametersForCachingFromPageArguments()
344 */
345 protected $cHash_array = [];
346
347 /**
348 * May be set to the pagesTSconfig
349 * @var array
350 * @internal
351 */
352 protected $pagesTSconfig = '';
353
354 /**
355 * Eg. insert JS-functions in this array ($additionalHeaderData) to include them
356 * once. Use associative keys.
357 *
358 * Keys in use:
359 *
360 * used to accumulate additional HTML-code for the header-section,
361 * <head>...</head>. Insert either associative keys (like
362 * additionalHeaderData['myStyleSheet'], see reserved keys above) or num-keys
363 * (like additionalHeaderData[] = '...')
364 *
365 * @var array
366 */
367 public $additionalHeaderData = [];
368
369 /**
370 * Used to accumulate additional HTML-code for the footer-section of the template
371 * @var array
372 */
373 public $additionalFooterData = [];
374
375 /**
376 * Used to accumulate additional JavaScript-code. Works like
377 * additionalHeaderData. Reserved keys at 'openPic' and 'mouseOver'
378 *
379 * @var array
380 */
381 public $additionalJavaScript = [];
382
383 /**
384 * Used to accumulate additional Style code. Works like additionalHeaderData.
385 *
386 * @var array
387 */
388 public $additionalCSS = [];
389
390 /**
391 * @var string
392 */
393 public $JSCode;
394
395 /**
396 * @var string
397 */
398 public $inlineJS;
399
400 /**
401 * Used to accumulate DHTML-layers.
402 * @var string
403 * @deprecated since TYPO3 v10.2, will be removed in TYPO3 v11, use custom USER_INT objects instead.
404 */
405 public $divSection = '';
406
407 /**
408 * Default internal target
409 * @var string
410 */
411 public $intTarget = '';
412
413 /**
414 * Default external target
415 * @var string
416 */
417 public $extTarget = '';
418
419 /**
420 * Default file link target
421 * @var string
422 */
423 public $fileTarget = '';
424
425 /**
426 * If set, typolink() function encrypts email addresses.
427 * @var string|int
428 */
429 public $spamProtectEmailAddresses = 0;
430
431 /**
432 * Absolute Reference prefix
433 * @var string
434 */
435 public $absRefPrefix = '';
436
437 /**
438 * <A>-tag parameters
439 * @var string
440 */
441 public $ATagParams = '';
442
443 /**
444 * Search word regex, calculated if there has been search-words send. This is
445 * used to mark up the found search words on a page when jumped to from a link
446 * in a search-result.
447 * @var string
448 * @internal
449 */
450 public $sWordRegEx = '';
451
452 /**
453 * Is set to the incoming array sword_list in case of a page-view jumped to from
454 * a search-result.
455 * @var string
456 * @internal
457 */
458 public $sWordList = '';
459
460 /**
461 * A string prepared for insertion in all links on the page as url-parameters.
462 * Based on configuration in TypoScript where you defined which GET_VARS you
463 * would like to pass on.
464 * @var string
465 */
466 public $linkVars = '';
467
468 /**
469 * If set, edit icons are rendered aside content records. Must be set only if
470 * the ->beUserLogin flag is set and set_no_cache() must be called as well.
471 * @var string
472 */
473 public $displayEditIcons = '';
474
475 /**
476 * If set, edit icons are rendered aside individual fields of content. Must be
477 * set only if the ->beUserLogin flag is set and set_no_cache() must be called as
478 * well.
479 * @var string
480 */
481 public $displayFieldEditIcons = '';
482
483 /**
484 * Is set to the iso code of the current language
485 * @var string
486 * @deprecated will be removed in TYPO3 v11.0. don't use it anymore, as this is now within SiteLanguage->getTwoLetterIsoCode()
487 */
488 protected $sys_language_isocode = '';
489
490 /**
491 * 'Global' Storage for various applications. Keys should be 'tx_'.extKey for
492 * extensions.
493 * @var array
494 */
495 public $applicationData = [];
496
497 /**
498 * @var array
499 */
500 public $register = [];
501
502 /**
503 * Stack used for storing array and retrieving register arrays (see
504 * LOAD_REGISTER and RESTORE_REGISTER)
505 * @var array
506 */
507 public $registerStack = [];
508
509 /**
510 * Checking that the function is not called eternally. This is done by
511 * interrupting at a depth of 50
512 * @var int
513 */
514 public $cObjectDepthCounter = 50;
515
516 /**
517 * Used by RecordContentObject and ContentContentObject to ensure the a records is NOT
518 * rendered twice through it!
519 * @var array
520 */
521 public $recordRegister = [];
522
523 /**
524 * This is set to the [table]:[uid] of the latest record rendered. Note that
525 * class ContentObjectRenderer has an equal value, but that is pointing to the
526 * record delivered in the $data-array of the ContentObjectRenderer instance, if
527 * the cObjects CONTENT or RECORD created that instance
528 * @var string
529 */
530 public $currentRecord = '';
531
532 /**
533 * Used by class \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject
534 * to keep track of access-keys.
535 * @var array
536 */
537 public $accessKey = [];
538
539 /**
540 * Numerical array where image filenames are added if they are referenced in the
541 * rendered document. This includes only TYPO3 generated/inserted images.
542 * @var array
543 */
544 public $imagesOnPage = [];
545
546 /**
547 * Is set in ContentObjectRenderer->cImage() function to the info-array of the
548 * most recent rendered image. The information is used in ImageTextContentObject
549 * @var array
550 */
551 public $lastImageInfo = [];
552
553 /**
554 * Used to generate page-unique keys. Point is that uniqid() functions is very
555 * slow, so a unikey key is made based on this, see function uniqueHash()
556 * @var int
557 * @internal
558 */
559 protected $uniqueCounter = 0;
560
561 /**
562 * @var string
563 * @internal
564 */
565 protected $uniqueString = '';
566
567 /**
568 * This value will be used as the title for the page in the indexer (if
569 * indexing happens)
570 * @var string
571 */
572 public $indexedDocTitle = '';
573
574 /**
575 * The base URL set for the page header.
576 * @var string
577 */
578 public $baseUrl = '';
579
580 /**
581 * Page content render object
582 *
583 * @var ContentObjectRenderer
584 */
585 public $cObj = '';
586
587 /**
588 * All page content is accumulated in this variable. See RequestHandler
589 * @var string
590 */
591 public $content = '';
592
593 /**
594 * Output charset of the websites content. This is the charset found in the
595 * header, meta tag etc. If different than utf-8 a conversion
596 * happens before output to browser. Defaults to utf-8.
597 * @var string
598 */
599 public $metaCharset = 'utf-8';
600
601 /**
602 * Internal calculations for labels
603 *
604 * @var LanguageService
605 */
606 protected $languageService;
607
608 /**
609 * @var LockingStrategyInterface[][]
610 */
611 protected $locks = [];
612
613 /**
614 * @var PageRenderer
615 */
616 protected $pageRenderer;
617
618 /**
619 * The page cache object, use this to save pages to the cache and to
620 * retrieve them again
621 *
622 * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
623 */
624 protected $pageCache;
625
626 /**
627 * @var array
628 */
629 protected $pageCacheTags = [];
630
631 /**
632 * Content type HTTP header being sent in the request.
633 * @todo Ticket: #63642 Should be refactored to a request/response model later
634 * @internal Should only be used by TYPO3 core for now
635 *
636 * @var string
637 */
638 protected $contentType = 'text/html';
639
640 /**
641 * Doctype to use
642 *
643 * @var string
644 */
645 public $xhtmlDoctype = '';
646
647 /**
648 * @var int
649 */
650 public $xhtmlVersion;
651
652 /**
653 * Originally requested id from the initial $_GET variable
654 *
655 * @var int
656 */
657 protected $requestedId;
658
659 /**
660 * The context for keeping the current state, mostly related to current page information,
661 * backend user / frontend user access, workspaceId
662 *
663 * @var Context
664 */
665 protected $context;
666
667 /**
668 * Since TYPO3 v10.0, TSFE is composed out of
669 * - Context
670 * - Site
671 * - SiteLanguage
672 * - PageArguments (containing ID, Type, cHash and MP arguments)
673 *
674 * With TYPO3 v11, they will become mandatory and the method arguments will become strongly typed.
675 * For TYPO3 v10 this is built in a way to ensure maximum compatibility.
676 *
677 * Also sets a unique string (->uniqueString) for this script instance; A md5 hash of the microtime()
678 *
679 * @param Context|array|null $context the Context object to work on, previously defined to set TYPO3_CONF_VARS
680 * @param mixed|SiteInterface $siteOrId The resolved site to work on, previously this was the value of GeneralUtility::_GP('id')
681 * @param SiteLanguage|int|string $siteLanguageOrType The resolved language to work on, previously the value of GeneralUtility::_GP('type')
682 * @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')
683 * @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
684 * @param string|null $_2 previously was used to define the jumpURL, use the PageArguments object instead, will be removed in TYPO3 v11.0
685 * @param string|null $MP The value of GeneralUtility::_GP('MP'), use the PageArguments object instead, will be removed in TYPO3 v11.0
686 */
687 public function __construct($context = null, $siteOrId = null, $siteLanguageOrType = null, $pageArguments = null, $cHashOrFrontendUser = null, $_2 = null, $MP = null)
688 {
689 $this->initializeContextWithGlobalFallback($context);
690
691 // Fetch the request for fetching data (site/language/pageArguments) for compatibility reasons, not needed
692 // in TYPO3 v11.0 anymore.
693 /** @var ServerRequestInterface $request */
694 $request = $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals();
695
696 $this->initializeSiteWithCompatibility($siteOrId, $request);
697 $this->initializeSiteLanguageWithCompatibility($siteLanguageOrType, $request);
698 $pageArguments = $this->buildPageArgumentsWithFallback($pageArguments, $request);
699 $pageArguments = $this->initializeFrontendUserOrUpdateCHashArgument($cHashOrFrontendUser, $pageArguments);
700 $pageArguments = $this->initializeLegacyMountPointArgument($MP, $pageArguments);
701
702 $this->setPageArguments($pageArguments);
703
704 $this->uniqueString = md5(microtime());
705 $this->initPageRenderer();
706 $this->initCaches();
707 // Initialize LLL behaviour
708 $this->setOutputLanguage();
709 }
710
711 /**
712 * Various initialize methods used for fallback, which can be simplified in TYPO3 v11.0
713 */
714
715 /**
716 * Used to set $this->context. The first argument was $GLOBALS[TYPO3_CONF_VARS] (array) until TYPO3 v8,
717 * so no type hint possible.
718 *
719 * @param Context|array|null $context
720 */
721 private function initializeContextWithGlobalFallback($context): void
722 {
723 if ($context instanceof Context) {
724 $this->context = $context;
725 } else {
726 // Use the global context for now
727 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);
728 $this->context = GeneralUtility::makeInstance(Context::class);
729 }
730 if (!$this->context->hasAspect('frontend.preview')) {
731 $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class));
732 }
733 }
734
735 /**
736 * Second argument of the constructor. Until TYPO3 v10, this was the Page ID (int/string) but since TYPO3 v10.0
737 * this can also be a SiteInterface object, which will be mandatory in TYPO3 v11.0. If no Site object is given,
738 * this is fetched from the given request object.
739 *
740 * @param SiteInterface|int|string $siteOrId
741 * @param ServerRequestInterface $request
742 */
743 private function initializeSiteWithCompatibility($siteOrId, ServerRequestInterface $request): void
744 {
745 if ($siteOrId instanceof SiteInterface) {
746 $this->site = $siteOrId;
747 } else {
748 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);
749 $this->id = $siteOrId;
750 if ($request->getAttribute('site') instanceof SiteInterface) {
751 $this->site = $request->getAttribute('site');
752 } else {
753 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);
754 }
755 }
756 }
757
758 /**
759 * Until TYPO3 v10.0, the third argument of the constructor was given from GET/POST "type" to define the page type
760 * Since TYPO3 v10.0, this argument is requested to be of type SiteLanguage, which will be mandatory in TYPO3 v11.0.
761 * If no SiteLanguage object is given, this is fetched from the given request object.
762 *
763 * @param SiteLanguage|int|string $siteLanguageOrType
764 * @param ServerRequestInterface $request
765 */
766 private function initializeSiteLanguageWithCompatibility($siteLanguageOrType, ServerRequestInterface $request): void
767 {
768 if ($siteLanguageOrType instanceof SiteLanguage) {
769 $this->language = $siteLanguageOrType;
770 } else {
771 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);
772 $this->type = $siteLanguageOrType;
773 if ($request->getAttribute('language') instanceof SiteLanguage) {
774 $this->language = $request->getAttribute('language');
775 } else {
776 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);
777 }
778 }
779 }
780
781 /**
782 * Since TYPO3 v10.0, the fourth constructor argument should be of type PageArguments. However, until TYPO3 v8,
783 * this was the GET/POST parameter "no_cache". If no PageArguments object is given, the given request is checked
784 * for the PageArguments.
785 *
786 * @param bool|string|PageArguments|null $pageArguments
787 * @param ServerRequestInterface $request
788 * @return PageArguments
789 */
790 private function buildPageArgumentsWithFallback($pageArguments, ServerRequestInterface $request): PageArguments
791 {
792 if ($pageArguments instanceof PageArguments) {
793 return $pageArguments;
794 }
795 if ($request->getAttribute('routing') instanceof PageArguments) {
796 return $request->getAttribute('routing');
797 }
798 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);
799 $queryParams = $request->getQueryParams();
800 $pageId = $this->id ?: ($queryParams['id'] ?? $request->getParsedBody()['id'] ?? 0);
801 $pageType = $this->type ?: ($queryParams['type'] ?? $request->getParsedBody()['type'] ?? 0);
802 return new PageArguments((int)$pageId, (string)$pageType, [], $queryParams);
803 }
804
805 /**
806 * Since TYPO3 v10.0, the fifth constructor argument is expected to to be of Type FrontendUserAuthentication.
807 * However, up until TYPO3 v9.5 this argument was used to define the "cHash" GET/POST parameter. In order to
808 * ensure maximum compatibility, a deprecation is triggered if an old argument is still used, and PageArguments
809 * are updated accordingly, and returned.
810 *
811 * @param string|FrontendUserAuthentication|null $cHashOrFrontendUser
812 * @param PageArguments $pageArguments
813 * @return PageArguments
814 */
815 private function initializeFrontendUserOrUpdateCHashArgument($cHashOrFrontendUser, PageArguments $pageArguments): PageArguments
816 {
817 if ($cHashOrFrontendUser === null) {
818 return $pageArguments;
819 }
820 if ($cHashOrFrontendUser instanceof FrontendUserAuthentication) {
821 $this->fe_user = $cHashOrFrontendUser;
822 return $pageArguments;
823 }
824 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);
825 return new PageArguments(
826 $pageArguments->getPageId(),
827 $pageArguments->getPageType(),
828 $pageArguments->getRouteArguments(),
829 array_replace_recursive($pageArguments->getStaticArguments(), ['cHash' => $cHashOrFrontendUser]),
830 $pageArguments->getDynamicArguments()
831 );
832 }
833
834 /**
835 * Since TYPO3 v10.0 the seventh constructor argument is not needed anymore, as all data is already provided by
836 * the given PageArguments object. However, if a specific MP parameter is given anyways, the PageArguments object
837 * is updated and returned.
838 *
839 * @param string|null $MP
840 * @param PageArguments $pageArguments
841 * @return PageArguments
842 */
843 private function initializeLegacyMountPointArgument(?string $MP, PageArguments $pageArguments): PageArguments
844 {
845 if ($MP === null) {
846 return $pageArguments;
847 }
848 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);
849 if (!$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
850 return $pageArguments;
851 }
852 return new PageArguments(
853 $pageArguments->getPageId(),
854 $pageArguments->getPageType(),
855 $pageArguments->getRouteArguments(),
856 array_replace_recursive($pageArguments->getStaticArguments(), ['MP' => $MP]),
857 $pageArguments->getDynamicArguments()
858 );
859 }
860
861 /**
862 * Initializes the page renderer object
863 */
864 protected function initPageRenderer()
865 {
866 if ($this->pageRenderer !== null) {
867 return;
868 }
869 $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
870 $this->pageRenderer->setTemplateFile('EXT:frontend/Resources/Private/Templates/MainPage.html');
871 // As initPageRenderer could be called in constructor and for USER_INTs, this information is only set
872 // once - in order to not override any previous settings of PageRenderer.
873 if ($this->language instanceof SiteLanguage) {
874 $this->pageRenderer->setLanguage($this->language->getTypo3Language());
875 }
876 }
877
878 /**
879 * @param string $contentType
880 * @internal Should only be used by TYPO3 core for now
881 */
882 public function setContentType($contentType)
883 {
884 $this->contentType = $contentType;
885 }
886
887 /********************************************
888 *
889 * Initializing, resolving page id
890 *
891 ********************************************/
892 /**
893 * Initializes the caching system.
894 */
895 protected function initCaches()
896 {
897 $this->pageCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('pages');
898 }
899
900 /**
901 * Initializes the front-end user groups.
902 * Sets frontend.user aspect based on front-end user status.
903 */
904 public function initUserGroups()
905 {
906 $userGroups = [0];
907 // This affects the hidden-flag selecting the fe_groups for the user!
908 $this->fe_user->showHiddenRecords = $this->context->getPropertyFromAspect('visibility', 'includeHiddenContent', false);
909 // no matter if we have an active user we try to fetch matching groups which can be set without an user (simulation for instance!)
910 $this->fe_user->fetchGroupData();
911 $isUserAndGroupSet = is_array($this->fe_user->user) && !empty($this->fe_user->groupData['uid']);
912 if ($isUserAndGroupSet) {
913 // group -2 is not an existing group, but denotes a 'default' group when a user IS logged in.
914 // This is used to let elements be shown for all logged in users!
915 $userGroups[] = -2;
916 $groupsFromUserRecord = $this->fe_user->groupData['uid'];
917 } else {
918 // group -1 is not an existing group, but denotes a 'default' group when not logged in.
919 // This is used to let elements be hidden, when a user is logged in!
920 $userGroups[] = -1;
921 if ($this->loginAllowedInBranch) {
922 // 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.
923 $groupsFromUserRecord = $this->fe_user->groupData['uid'];
924 } else {
925 // Set to blank since we will NOT risk any groups being set when no logins are allowed!
926 $groupsFromUserRecord = [];
927 }
928 }
929 // Clean up.
930 // Make unique and sort the groups
931 $groupsFromUserRecord = array_unique($groupsFromUserRecord);
932 if (!empty($groupsFromUserRecord) && !$this->loginAllowedInBranch_mode) {
933 sort($groupsFromUserRecord);
934 $userGroups = array_merge($userGroups, array_map('intval', $groupsFromUserRecord));
935 }
936
937 $this->context->setAspect('frontend.user', GeneralUtility::makeInstance(UserAspect::class, $this->fe_user ?: null, $userGroups));
938
939 // For every 60 seconds the is_online timestamp for a logged-in user is updated
940 if ($isUserAndGroupSet) {
941 $this->fe_user->updateOnlineTimestamp();
942 }
943
944 $this->logger->debug('Valid usergroups for TSFE: ' . implode(',', $userGroups));
945 }
946
947 /**
948 * Checking if a user is logged in or a group constellation different from "0,-1"
949 *
950 * @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!)
951 */
952 public function isUserOrGroupSet()
953 {
954 /** @var UserAspect $userAspect */
955 $userAspect = $this->context->getAspect('frontend.user');
956 return $userAspect->isUserOrGroupSet();
957 }
958
959 /**
960 * Clears the preview-flags, sets sim_exec_time to current time.
961 * Hidden pages must be hidden as default, $GLOBALS['SIM_EXEC_TIME'] is set to $GLOBALS['EXEC_TIME']
962 * in bootstrap initializeGlobalTimeVariables(). Alter it by adding or subtracting seconds.
963 */
964 public function clear_preview()
965 {
966 if ($this->context->getPropertyFromAspect('frontend.preview', 'isPreview')
967 || $GLOBALS['EXEC_TIME'] !== $GLOBALS['SIM_EXEC_TIME']
968 || $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages', false)
969 || $this->context->getPropertyFromAspect('visibility', 'includeHiddenContent', false)
970 ) {
971 $GLOBALS['SIM_EXEC_TIME'] = $GLOBALS['EXEC_TIME'];
972 $GLOBALS['SIM_ACCESS_TIME'] = $GLOBALS['ACCESS_TIME'];
973 $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class));
974 $this->context->setAspect('date', GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $GLOBALS['SIM_EXEC_TIME'])));
975 $this->context->setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class));
976 }
977 }
978
979 /**
980 * Checks if a backend user is logged in
981 *
982 * @return bool whether a backend user is logged in
983 */
984 public function isBackendUserLoggedIn()
985 {
986 return (bool)$this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false);
987 }
988
989 /**
990 * Determines the id and evaluates any preview settings
991 * Basically this function is about determining whether a backend user is logged in,
992 * if he has read access to the page and if he's previewing the page.
993 * That all determines which id to show and how to initialize the id.
994 */
995 public function determineId()
996 {
997 // Call pre processing function for id determination
998 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PreProcessing'] ?? [] as $functionReference) {
999 $parameters = ['parentObject' => $this];
1000 GeneralUtility::callUserFunction($functionReference, $parameters, $this);
1001 }
1002 // If there is a Backend login we are going to check for any preview settings
1003 $originalFrontendUserGroups = $this->applyPreviewSettings($this->getBackendUser());
1004 // If the front-end is showing a preview, caching MUST be disabled.
1005 $isPreview = $this->context->getPropertyFromAspect('frontend.preview', 'isPreview');
1006 if ($isPreview) {
1007 $this->disableCache();
1008 }
1009 // Now, get the id, validate access etc:
1010 $this->fetch_the_id();
1011 // Check if backend user has read access to this page. If not, recalculate the id.
1012 if ($this->isBackendUserLoggedIn() && $isPreview && !$this->getBackendUser()->doesUserHaveAccess($this->page, Permission::PAGE_SHOW)) {
1013 // Resetting
1014 $this->clear_preview();
1015 $this->fe_user->user[$this->fe_user->usergroup_column] = $originalFrontendUserGroups;
1016 // Fetching the id again, now with the preview settings reset.
1017 $this->fetch_the_id();
1018 }
1019 // Checks if user logins are blocked for a certain branch and if so, will unset user login and re-fetch ID.
1020 $this->loginAllowedInBranch = $this->checkIfLoginAllowedInBranch();
1021 // Logins are not allowed, but there is a login, so will we run this.
1022 if (!$this->loginAllowedInBranch && $this->isUserOrGroupSet()) {
1023 if ($this->loginAllowedInBranch_mode === 'all') {
1024 // Clear out user and group:
1025 $this->fe_user->hideActiveLogin();
1026 $userGroups = [0, -1];
1027 } else {
1028 $userGroups = [0, -2];
1029 }
1030 $this->context->setAspect('frontend.user', GeneralUtility::makeInstance(UserAspect::class, $this->fe_user ?: null, $userGroups));
1031 // Fetching the id again, now with the preview settings reset.
1032 $this->fetch_the_id();
1033 }
1034 // Final cleaning.
1035 // Make sure it's an integer
1036 $this->id = ($this->contentPid = (int)$this->id);
1037 // Make sure it's an integer
1038 $this->type = (int)$this->type;
1039 // Call post processing function for id determination:
1040 $_params = ['pObj' => &$this];
1041 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc'] ?? [] as $_funcRef) {
1042 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1043 }
1044 }
1045
1046 /**
1047 * Evaluates admin panel or workspace settings to see if
1048 * visibility settings like
1049 * - Preview Aspect: isPreview
1050 * - Visibility Aspect: includeHiddenPages
1051 * - Visibility Aspect: includeHiddenContent
1052 * - $simUserGroup
1053 * should be applied to the current object.
1054 *
1055 * @param FrontendBackendUserAuthentication $backendUser
1056 * @return string|null null if no changes to the current frontend usergroups have been made, otherwise the original list of frontend usergroups
1057 * @internal
1058 */
1059 protected function applyPreviewSettings($backendUser = null)
1060 {
1061 if (!$backendUser) {
1062 return null;
1063 }
1064 $originalFrontendUserGroup = null;
1065 if ($this->fe_user->user) {
1066 $originalFrontendUserGroup = $this->context->getPropertyFromAspect('frontend.user', 'groupIds');
1067 }
1068
1069 // The preview flag is set if the current page turns out to be hidden
1070 if ($this->id && $this->determineIdIsHiddenPage()) {
1071 $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, true));
1072 /** @var VisibilityAspect $aspect */
1073 $aspect = $this->context->getAspect('visibility');
1074 $newAspect = GeneralUtility::makeInstance(VisibilityAspect::class, true, $aspect->includeHiddenContent(), $aspect->includeDeletedRecords());
1075 $this->context->setAspect('visibility', $newAspect);
1076 }
1077 // The preview flag will be set if an offline workspace will be previewed
1078 if ($this->whichWorkspace() > 0) {
1079 $this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, true));
1080 }
1081 return $this->context->getPropertyFromAspect('frontend.preview', 'preview', false) ? $originalFrontendUserGroup : null;
1082 }
1083
1084 /**
1085 * Checks if the page is hidden in the active workspace.
1086 * If it is hidden, preview flags will be set.
1087 *
1088 * @return bool
1089 */
1090 protected function determineIdIsHiddenPage()
1091 {
1092 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1093 ->getQueryBuilderForTable('pages');
1094 $queryBuilder
1095 ->getRestrictions()
1096 ->removeAll()
1097 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1098
1099 $queryBuilder
1100 ->select('uid', 'hidden', 'starttime', 'endtime')
1101 ->from('pages')
1102 ->where(
1103 $queryBuilder->expr()->gte('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
1104 )
1105 ->setMaxResults(1);
1106
1107 // $this->id always points to the ID of the default language page, so we check
1108 // the current site language to determine if we need to fetch a translation but consider fallbacks
1109 if ($this->language->getLanguageId() > 0) {
1110 $languagesToCheck = array_merge([$this->language->getLanguageId()], $this->language->getFallbackLanguageIds());
1111 // Check for the language and all its fallbacks
1112 $constraint = $queryBuilder->expr()->andX(
1113 $queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)),
1114 $queryBuilder->expr()->in('sys_language_uid', $queryBuilder->createNamedParameter(array_filter($languagesToCheck), Connection::PARAM_INT_ARRAY))
1115 );
1116 // If the fallback language Ids also contains the default language, this needs to be considered
1117 if (in_array(0, $languagesToCheck, true)) {
1118 $constraint = $queryBuilder->expr()->orX(
1119 $constraint,
1120 // Ensure to also fetch the default record
1121 $queryBuilder->expr()->andX(
1122 $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)),
1123 $queryBuilder->expr()->in('sys_language_uid', 0)
1124 )
1125 );
1126 }
1127 // Ensure that the translated records are shown first (maxResults is set to 1)
1128 $queryBuilder->orderBy('sys_language_uid', 'DESC');
1129 } else {
1130 $constraint = $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT));
1131 }
1132 $queryBuilder->andWhere($constraint);
1133
1134 $page = $queryBuilder->execute()->fetch();
1135
1136 if ($this->whichWorkspace() > 0) {
1137 // Fetch overlay of page if in workspace and check if it is hidden
1138 $customContext = clone $this->context;
1139 $customContext->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $this->whichWorkspace()));
1140 $customContext->setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class));
1141 $pageSelectObject = GeneralUtility::makeInstance(PageRepository::class, $customContext);
1142 $targetPage = $pageSelectObject->getWorkspaceVersionOfRecord($this->whichWorkspace(), 'pages', $page['uid']);
1143 $result = $targetPage === -1 || $targetPage === -2;
1144 } else {
1145 $result = is_array($page) && ($page['hidden'] || $page['starttime'] > $GLOBALS['SIM_EXEC_TIME'] || $page['endtime'] != 0 && $page['endtime'] <= $GLOBALS['SIM_EXEC_TIME']);
1146 }
1147 return $result;
1148 }
1149
1150 /**
1151 * Resolves the page id and sets up several related properties.
1152 *
1153 * If $this->id is not set at all or is not a plain integer, the method
1154 * does it's best to set the value to an integer. Resolving is based on
1155 * this options:
1156 *
1157 * - Splitting $this->id if it contains an additional type parameter.
1158 * - Finding the domain record start page
1159 * - First visible page
1160 * - Relocating the id below the domain record if outside
1161 *
1162 * The following properties may be set up or updated:
1163 *
1164 * - id
1165 * - requestedId
1166 * - type
1167 * - sys_page
1168 * - sys_page->where_groupAccess
1169 * - sys_page->where_hid_del
1170 * - Context: FrontendUser Aspect
1171 * - no_cache
1172 * - register['SYS_LASTCHANGED']
1173 * - pageNotFound
1174 *
1175 * Via getPageAndRootlineWithDomain()
1176 *
1177 * - rootLine
1178 * - page
1179 * - MP
1180 * - originalShortcutPage
1181 * - originalMountPointPage
1182 * - pageAccessFailureHistory['direct_access']
1183 * - pageNotFound
1184 *
1185 * @todo:
1186 *
1187 * On the first impression the method does to much. This is increased by
1188 * the fact, that is is called repeated times by the method determineId.
1189 * The reasons are manifold.
1190 *
1191 * 1.) The first part, the creation of sys_page and the type
1192 * resolution don't need to be repeated. They could be separated to be
1193 * called only once.
1194 *
1195 * 2.) The user group setup could be done once on a higher level.
1196 *
1197 * 3.) The workflow of the resolution could be elaborated to be less
1198 * tangled. Maybe the check of the page id to be below the domain via the
1199 * root line doesn't need to be done each time, but for the final result
1200 * only.
1201 *
1202 * 4.) The root line does not need to be directly addressed by this class.
1203 * A root line is always related to one page. The rootline could be handled
1204 * indirectly by page objects. Page objects still don't exist.
1205 *
1206 * @throws ServiceUnavailableException
1207 * @internal
1208 */
1209 public function fetch_the_id()
1210 {
1211 $timeTracker = $this->getTimeTracker();
1212 $timeTracker->push('fetch_the_id initialize/');
1213 // Set the valid usergroups for FE
1214 $this->initUserGroups();
1215 // Initialize the PageRepository has to be done after the frontend usergroups are initialized / resolved, as
1216 // frontend group aspect is modified before
1217 $this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context);
1218 // The id and type is set to the integer-value - just to be sure...
1219 $this->id = (int)$this->id;
1220 $this->type = (int)$this->type;
1221 $timeTracker->pull();
1222 // We find the first page belonging to the current domain
1223 $timeTracker->push('fetch_the_id domain/');
1224 if (!$this->id) {
1225 // If the id was not previously set, set it to the root page id of the site.
1226 $this->id = $this->site->getRootPageId();
1227 }
1228 $timeTracker->pull();
1229 $timeTracker->push('fetch_the_id rootLine/');
1230 // We store the originally requested id
1231 $this->requestedId = $this->id;
1232 try {
1233 $this->getPageAndRootlineWithDomain($this->site->getRootPageId());
1234 } catch (ShortcutTargetPageNotFoundException $e) {
1235 $this->pageNotFound = 1;
1236 }
1237 $timeTracker->pull();
1238 if ($this->pageNotFound) {
1239 switch ($this->pageNotFound) {
1240 case 1:
1241 $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
1242 $GLOBALS['TYPO3_REQUEST'],
1243 'ID was not an accessible page',
1244 $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED)
1245 );
1246 break;
1247 case 2:
1248 $response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
1249 $GLOBALS['TYPO3_REQUEST'],
1250 'Subsection was found and not accessible',
1251 $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED)
1252 );
1253 break;
1254 case 3:
1255 $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1256 $GLOBALS['TYPO3_REQUEST'],
1257 'ID was outside the domain',
1258 $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH)
1259 );
1260 break;
1261 default:
1262 $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1263 $GLOBALS['TYPO3_REQUEST'],
1264 'Unspecified error',
1265 $this->getPageAccessFailureReasons()
1266 );
1267 }
1268 throw new ImmediateResponseException($response, 1533931329);
1269 }
1270
1271 $this->setRegisterValueForSysLastChanged($this->page);
1272
1273 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['fetchPageId-PostProcessing'] ?? [] as $functionReference) {
1274 $parameters = ['parentObject' => $this];
1275 GeneralUtility::callUserFunction($functionReference, $parameters, $this);
1276 }
1277 }
1278
1279 /**
1280 * Loads the page and root line records based on $this->id
1281 *
1282 * A final page and the matching root line are determined and loaded by
1283 * the algorithm defined by this method.
1284 *
1285 * First it loads the initial page from the page repository for $this->id.
1286 * If that can't be loaded directly, it gets the root line for $this->id.
1287 * It walks up the root line towards the root page until the page
1288 * repository can deliver a page record. (The loading restrictions of
1289 * the root line records are more liberal than that of the page record.)
1290 *
1291 * Now the page type is evaluated and handled if necessary. If the page is
1292 * a short cut, it is replaced by the target page. If the page is a mount
1293 * point in overlay mode, the page is replaced by the mounted page.
1294 *
1295 * After this potential replacements are done, the root line is loaded
1296 * (again) for this page record. It walks up the root line up to
1297 * the first viewable record.
1298 *
1299 * (While upon the first accessibility check of the root line it was done
1300 * by loading page by page from the page repository, this time the method
1301 * checkRootlineForIncludeSection() is used to find the most distant
1302 * accessible page within the root line.)
1303 *
1304 * Having found the final page id, the page record and the root line are
1305 * loaded for last time by this method.
1306 *
1307 * Exceptions may be thrown for DOKTYPE_SPACER and not loadable page records
1308 * or root lines.
1309 *
1310 * May set or update this properties:
1311 *
1312 * @see TypoScriptFrontendController::$id
1313 * @see TypoScriptFrontendController::$MP
1314 * @see TypoScriptFrontendController::$page
1315 * @see TypoScriptFrontendController::$pageNotFound
1316 * @see TypoScriptFrontendController::$pageAccessFailureHistory
1317 * @see TypoScriptFrontendController::$originalMountPointPage
1318 * @see TypoScriptFrontendController::$originalShortcutPage
1319 *
1320 * @throws ServiceUnavailableException
1321 * @throws PageNotFoundException
1322 */
1323 protected function getPageAndRootline()
1324 {
1325 $requestedPageRowWithoutGroupCheck = [];
1326 $this->resolveTranslatedPageId();
1327 if (empty($this->page)) {
1328 // If no page, we try to find the page before in the rootLine.
1329 // Page is 'not found' in case the id itself was not an accessible page. code 1
1330 $this->pageNotFound = 1;
1331 try {
1332 $requestedPageRowWithoutGroupCheck = $this->sys_page->getPage($this->id, true);
1333 if (!empty($requestedPageRowWithoutGroupCheck)) {
1334 $this->pageAccessFailureHistory['direct_access'][] = $requestedPageRowWithoutGroupCheck;
1335 }
1336 $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1337 if (!empty($this->rootLine)) {
1338 $c = count($this->rootLine) - 1;
1339 while ($c > 0) {
1340 // Add to page access failure history:
1341 $this->pageAccessFailureHistory['direct_access'][] = $this->rootLine[$c];
1342 // Decrease to next page in rootline and check the access to that, if OK, set as page record and ID value.
1343 $c--;
1344 $this->id = $this->rootLine[$c]['uid'];
1345 $this->page = $this->sys_page->getPage($this->id);
1346 if (!empty($this->page)) {
1347 break;
1348 }
1349 }
1350 }
1351 } catch (RootLineException $e) {
1352 $this->rootLine = [];
1353 }
1354 // If still no page...
1355 if (empty($requestedPageRowWithoutGroupCheck) && empty($this->page)) {
1356 $message = 'The requested page does not exist!';
1357 $this->logger->error($message);
1358 try {
1359 $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1360 $GLOBALS['TYPO3_REQUEST'],
1361 $message,
1362 $this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND)
1363 );
1364 throw new ImmediateResponseException($response, 1533931330);
1365 } catch (PageNotFoundException $e) {
1366 throw new PageNotFoundException($message, 1301648780);
1367 }
1368 }
1369 }
1370 // Spacer is not accessible in frontend
1371 if ($this->page['doktype'] == PageRepository::DOKTYPE_SPACER) {
1372 $message = 'The requested page does not exist!';
1373 $this->logger->error($message);
1374 try {
1375 $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1376 $GLOBALS['TYPO3_REQUEST'],
1377 $message,
1378 $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_INVALID_PAGETYPE)
1379 );
1380 throw new ImmediateResponseException($response, 1533931343);
1381 } catch (PageNotFoundException $e) {
1382 throw new PageNotFoundException($message, 1301648781);
1383 }
1384 }
1385 // Is the ID a link to another page??
1386 if ($this->page['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
1387 // 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.
1388 $this->MP = '';
1389 // saving the page so that we can check later - when we know
1390 // about languages - whether we took the correct shortcut or
1391 // whether a translation of the page overwrites the shortcut
1392 // target and we need to follow the new target
1393 $this->originalShortcutPage = $this->page;
1394 $this->page = $this->sys_page->getPageShortcut($this->page['shortcut'], $this->page['shortcut_mode'], $this->page['uid']);
1395 $this->id = $this->page['uid'];
1396 }
1397 // If the page is a mountpoint which should be overlaid with the contents of the mounted page,
1398 // it must never be accessible directly, but only in the mountpoint context. Therefore we change
1399 // the current ID and the user is redirected by checkPageForMountpointRedirect().
1400 if ($this->page['doktype'] == PageRepository::DOKTYPE_MOUNTPOINT && $this->page['mount_pid_ol']) {
1401 $this->originalMountPointPage = $this->page;
1402 $this->page = $this->sys_page->getPage($this->page['mount_pid']);
1403 if (empty($this->page)) {
1404 $message = 'This page (ID ' . $this->originalMountPointPage['uid'] . ') is of type "Mount point" and '
1405 . 'mounts a page which is not accessible (ID ' . $this->originalMountPointPage['mount_pid'] . ').';
1406 throw new PageNotFoundException($message, 1402043263);
1407 }
1408 // If the current page is a shortcut, the MP parameter will be replaced
1409 if ($this->MP === '' || !empty($this->originalShortcutPage)) {
1410 $this->MP = $this->page['uid'] . '-' . $this->originalMountPointPage['uid'];
1411 } else {
1412 $this->MP .= ',' . $this->page['uid'] . '-' . $this->originalMountPointPage['uid'];
1413 }
1414 $this->id = $this->page['uid'];
1415 }
1416 // Gets the rootLine
1417 try {
1418 $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1419 } catch (RootLineException $e) {
1420 $this->rootLine = [];
1421 }
1422 // If not rootline we're off...
1423 if (empty($this->rootLine)) {
1424 $message = 'The requested page didn\'t have a proper connection to the tree-root!';
1425 $this->logger->error($message);
1426 try {
1427 $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
1428 $GLOBALS['TYPO3_REQUEST'],
1429 $message,
1430 $this->getPageAccessFailureReasons(PageAccessFailureReasons::ROOTLINE_BROKEN)
1431 );
1432 throw new ImmediateResponseException($response, 1533931350);
1433 } catch (ServiceUnavailableException $e) {
1434 throw new ServiceUnavailableException($message, 1301648167);
1435 }
1436 }
1437 // Checking for include section regarding the hidden/starttime/endtime/fe_user (that is access control of a whole subbranch!)
1438 if ($this->checkRootlineForIncludeSection()) {
1439 if (empty($this->rootLine)) {
1440 $message = 'The requested page was not accessible!';
1441 try {
1442 $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
1443 $GLOBALS['TYPO3_REQUEST'],
1444 $message,
1445 $this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_GENERAL)
1446 );
1447 throw new ImmediateResponseException($response, 1533931351);
1448 } catch (ServiceUnavailableException $e) {
1449 $this->logger->warning($message);
1450 throw new ServiceUnavailableException($message, 1301648234);
1451 }
1452 } else {
1453 $el = reset($this->rootLine);
1454 $this->id = $el['uid'];
1455 $this->page = $this->sys_page->getPage($this->id);
1456 try {
1457 $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
1458 } catch (RootLineException $e) {
1459 $this->rootLine = [];
1460 }
1461 }
1462 }
1463 }
1464
1465 /**
1466 * If $this->id contains a translated page record, this needs to be resolved to the default language
1467 * in order for all rootline functionality and access restrictions to be in place further on.
1468 *
1469 * Additionally, if a translated page is found, LanguageAspect is set as well.
1470 */
1471 protected function resolveTranslatedPageId()
1472 {
1473 $this->page = $this->sys_page->getPage($this->id);
1474 // Accessed a default language page record, nothing to resolve
1475 if (empty($this->page) || (int)$this->page[$GLOBALS['TCA']['pages']['ctrl']['languageField']] === 0) {
1476 return;
1477 }
1478 $languageId = (int)$this->page[$GLOBALS['TCA']['pages']['ctrl']['languageField']];
1479 $this->page = $this->sys_page->getPage($this->page[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']]);
1480 $this->context->setAspect('language', GeneralUtility::makeInstance(LanguageAspect::class, $languageId));
1481 $this->id = $this->page['uid'];
1482 }
1483
1484 /**
1485 * Checks if visibility of the page is blocked upwards in the root line.
1486 *
1487 * If any page in the root line is blocking visibility, true is returend.
1488 *
1489 * All pages from the blocking page downwards are removed from the root
1490 * line, so that the remaining pages can be used to relocate the page up
1491 * to lowest visible page.
1492 *
1493 * The blocking feature of a page must be turned on by setting the page
1494 * record field 'extendToSubpages' to 1 in case of hidden, starttime,
1495 * endtime or fe_group restrictions.
1496 *
1497 * Additionally this method checks for backend user sections in root line
1498 * and if found evaluates if a backend user is logged in and has access.
1499 *
1500 * Recyclers are also checked and trigger page not found if found in root
1501 * line.
1502 *
1503 * @todo Find a better name, i.e. checkVisibilityByRootLine
1504 * @todo Invert boolean return value. Return true if visible.
1505 *
1506 * @return bool
1507 */
1508 protected function checkRootlineForIncludeSection(): bool
1509 {
1510 $c = count($this->rootLine);
1511 $removeTheRestFlag = false;
1512 for ($a = 0; $a < $c; $a++) {
1513 if (!$this->checkPagerecordForIncludeSection($this->rootLine[$a])) {
1514 // Add to page access failure history and mark the page as not found
1515 // Keep the rootline however to trigger an access denied error instead of a service unavailable error
1516 $this->pageAccessFailureHistory['sub_section'][] = $this->rootLine[$a];
1517 $this->pageNotFound = 2;
1518 }
1519
1520 if ((int)$this->rootLine[$a]['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION) {
1521 // If there is a backend user logged in, check if they have read access to the page:
1522 if ($this->isBackendUserLoggedIn()) {
1523 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1524 ->getQueryBuilderForTable('pages');
1525
1526 $queryBuilder
1527 ->getRestrictions()
1528 ->removeAll();
1529
1530 $row = $queryBuilder
1531 ->select('uid')
1532 ->from('pages')
1533 ->where(
1534 $queryBuilder->expr()->eq(
1535 'uid',
1536 $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
1537 ),
1538 $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)
1539 )
1540 ->execute()
1541 ->fetch();
1542
1543 // versionOL()?
1544 if (!$row) {
1545 // 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...
1546 $removeTheRestFlag = true;
1547 }
1548 } else {
1549 // Don't go here, if there is no backend user logged in.
1550 $removeTheRestFlag = true;
1551 }
1552 } elseif ((int)$this->rootLine[$a]['doktype'] === PageRepository::DOKTYPE_RECYCLER) {
1553 // page is in a recycler
1554 $removeTheRestFlag = true;
1555 }
1556 if ($removeTheRestFlag) {
1557 // Page is 'not found' in case a subsection was found and not accessible, code 2
1558 $this->pageNotFound = 2;
1559 unset($this->rootLine[$a]);
1560 }
1561 }
1562 return $removeTheRestFlag;
1563 }
1564
1565 /**
1566 * Checks page record for enableFields
1567 * Returns TRUE if enableFields does not disable the page record.
1568 * Takes notice of the includeHiddenPages visibility aspect flag and uses SIM_ACCESS_TIME for start/endtime evaluation
1569 *
1570 * @param array $row The page record to evaluate (needs fields: hidden, starttime, endtime, fe_group)
1571 * @param bool $bypassGroupCheck Bypass group-check
1572 * @return bool TRUE, if record is viewable.
1573 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getTreeList()
1574 * @see checkPagerecordForIncludeSection()
1575 */
1576 public function checkEnableFields($row, $bypassGroupCheck = false)
1577 {
1578 $_params = ['pObj' => $this, 'row' => &$row, 'bypassGroupCheck' => &$bypassGroupCheck];
1579 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['hook_checkEnableFields'] ?? [] as $_funcRef) {
1580 // Call hooks: If one returns FALSE, method execution is aborted with result "This record is not available"
1581 $return = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1582 if ($return === false) {
1583 return false;
1584 }
1585 }
1586 if ((!$row['hidden'] || $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages', false))
1587 && $row['starttime'] <= $GLOBALS['SIM_ACCESS_TIME']
1588 && ($row['endtime'] == 0 || $row['endtime'] > $GLOBALS['SIM_ACCESS_TIME'])
1589 && ($bypassGroupCheck || $this->checkPageGroupAccess($row))) {
1590 return true;
1591 }
1592 return false;
1593 }
1594
1595 /**
1596 * Check group access against a page record
1597 *
1598 * @param array $row The page record to evaluate (needs field: fe_group)
1599 * @return bool TRUE, if group access is granted.
1600 * @internal
1601 */
1602 public function checkPageGroupAccess($row)
1603 {
1604 /** @var UserAspect $userAspect */
1605 $userAspect = $this->context->getAspect('frontend.user');
1606 $pageGroupList = explode(',', $row['fe_group'] ?: 0);
1607 return count(array_intersect($userAspect->getGroupIds(), $pageGroupList)) > 0;
1608 }
1609
1610 /**
1611 * Checks if the current page of the root line is visible.
1612 *
1613 * If the field extendToSubpages is 0, access is granted,
1614 * else the fields hidden, starttime, endtime, fe_group are evaluated.
1615 *
1616 * @todo Find a better name, i.e. isVisibleRecord()
1617 *
1618 * @param array $row The page record
1619 * @return bool true if visible
1620 * @internal
1621 * @see checkEnableFields()
1622 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getTreeList()
1623 * @see checkRootlineForIncludeSection()
1624 */
1625 public function checkPagerecordForIncludeSection(array $row): bool
1626 {
1627 return !$row['extendToSubpages'] || $this->checkEnableFields($row);
1628 }
1629
1630 /**
1631 * 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!)
1632 *
1633 * @return bool returns TRUE if logins are OK, otherwise FALSE (and then the login user must be unset!)
1634 */
1635 public function checkIfLoginAllowedInBranch()
1636 {
1637 // Initialize:
1638 $c = count($this->rootLine);
1639 $loginAllowed = true;
1640 // Traverse root line from root and outwards:
1641 for ($a = 0; $a < $c; $a++) {
1642 // If a value is set for login state:
1643 if ($this->rootLine[$a]['fe_login_mode'] > 0) {
1644 // Determine state from value:
1645 if ((int)$this->rootLine[$a]['fe_login_mode'] === 1) {
1646 $loginAllowed = false;
1647 $this->loginAllowedInBranch_mode = 'all';
1648 } elseif ((int)$this->rootLine[$a]['fe_login_mode'] === 3) {
1649 $loginAllowed = false;
1650 $this->loginAllowedInBranch_mode = 'groups';
1651 } else {
1652 $loginAllowed = true;
1653 }
1654 }
1655 }
1656 return $loginAllowed;
1657 }
1658
1659 /**
1660 * 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
1661 *
1662 * @param string $failureReasonCode the error code to be attached (optional), see PageAccessFailureReasons list for details
1663 * @return array Summary of why page access was not allowed.
1664 */
1665 public function getPageAccessFailureReasons(string $failureReasonCode = null)
1666 {
1667 $output = [];
1668 if ($failureReasonCode) {
1669 $output['code'] = $failureReasonCode;
1670 }
1671 $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'] : []);
1672 if (!empty($combinedRecords)) {
1673 foreach ($combinedRecords as $k => $pagerec) {
1674 // 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
1675 // 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!
1676 if (!$k || $pagerec['extendToSubpages']) {
1677 if ($pagerec['hidden']) {
1678 $output['hidden'][$pagerec['uid']] = true;
1679 }
1680 if ($pagerec['starttime'] > $GLOBALS['SIM_ACCESS_TIME']) {
1681 $output['starttime'][$pagerec['uid']] = $pagerec['starttime'];
1682 }
1683 if ($pagerec['endtime'] != 0 && $pagerec['endtime'] <= $GLOBALS['SIM_ACCESS_TIME']) {
1684 $output['endtime'][$pagerec['uid']] = $pagerec['endtime'];
1685 }
1686 if (!$this->checkPageGroupAccess($pagerec)) {
1687 $output['fe_group'][$pagerec['uid']] = $pagerec['fe_group'];
1688 }
1689 }
1690 }
1691 }
1692 return $output;
1693 }
1694
1695 /**
1696 * Gets ->page and ->rootline information based on ->id. ->id may change during this operation.
1697 * If not inside a site, then default to first page in site.
1698 *
1699 * @param int $rootPageId Page uid of the page where the found site is located
1700 * @internal
1701 */
1702 public function getPageAndRootlineWithDomain($rootPageId)
1703 {
1704 $this->getPageAndRootline();
1705 // Checks if the $domain-startpage is in the rootLine. This is necessary so that references to page-id's from other domains are not possible.
1706 if ($rootPageId && is_array($this->rootLine) && $this->rootLine !== []) {
1707 $idFound = false;
1708 foreach ($this->rootLine as $key => $val) {
1709 if ($val['uid'] == $rootPageId) {
1710 $idFound = true;
1711 break;
1712 }
1713 }
1714 if (!$idFound) {
1715 // Page is 'not found' in case the id was outside the domain, code 3
1716 $this->pageNotFound = 3;
1717 $this->id = $rootPageId;
1718 // re-get the page and rootline if the id was not found.
1719 $this->getPageAndRootline();
1720 }
1721 }
1722 }
1723
1724 /********************************************
1725 *
1726 * Template and caching related functions.
1727 *
1728 *******************************************/
1729
1730 /**
1731 * Will disable caching if the cHash value was not set when having dynamic arguments in GET query parameters.
1732 * 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)
1733 *
1734 * @deprecated since TYPO3 v10.2, will be removed in TYPO3 v11. The PSR-15 middleware PageArgumentValidator is already taking care of this.
1735 */
1736 public function reqCHash()
1737 {
1738 trigger_error('TypoScriptFrontendController->reqCHash() is not needed anymore, as all functionality is handled via the PSR-15 PageArgumentValidator middleware already.', E_USER_DEPRECATED);
1739 if (!empty($this->pageArguments->getArguments()['cHash']) || empty($this->pageArguments->getDynamicArguments())) {
1740 return;
1741 }
1742 $queryParams = $this->pageArguments->getDynamicArguments();
1743 $queryParams['id'] = $this->pageArguments->getPageId();
1744 $argumentsThatWouldRequireCacheHash = GeneralUtility::makeInstance(CacheHashCalculator::class)
1745 ->getRelevantParameters(HttpUtility::buildQueryString($queryParams));
1746 if (empty($argumentsThatWouldRequireCacheHash)) {
1747 return;
1748 }
1749 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFoundOnCHashError']) {
1750 $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
1751 $GLOBALS['TYPO3_REQUEST'],
1752 'Request parameters could not be validated (&cHash empty)',
1753 ['code' => PageAccessFailureReasons::CACHEHASH_EMPTY]
1754 );
1755 throw new ImmediateResponseException($response, 1533931354);
1756 }
1757 $this->disableCache();
1758 $this->getTimeTracker()->setTSlogMessage('TSFE->reqCHash(): No &cHash parameter was sent for GET vars though required so caching is disabled', 2);
1759 }
1760
1761 protected function setPageArguments(PageArguments $pageArguments): void
1762 {
1763 $this->pageArguments = $pageArguments;
1764 $this->id = $pageArguments->getPageId();
1765 $this->type = $pageArguments->getPageType() ?: 0;
1766 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
1767 $this->MP = (string)($pageArguments->getArguments()['MP'] ?? '');
1768 }
1769 }
1770
1771 /**
1772 * Fetches the arguments that are relevant for creating the hash base from the given PageArguments object.
1773 * Excluded parameters are not taken into account when calculating the hash base.
1774 *
1775 * @param PageArguments $pageArguments
1776 * @return array
1777 */
1778 protected function getRelevantParametersForCachingFromPageArguments(PageArguments $pageArguments): array
1779 {
1780 $queryParams = $pageArguments->getDynamicArguments();
1781 if (!empty($queryParams) && $pageArguments->getArguments()['cHash'] ?? false) {
1782 $queryParams['id'] = $pageArguments->getPageId();
1783 return GeneralUtility::makeInstance(CacheHashCalculator::class)
1784 ->getRelevantParameters(HttpUtility::buildQueryString($queryParams));
1785 }
1786 return [];
1787 }
1788
1789 /**
1790 * See if page is in cache and get it if so
1791 * Stores the page content in $this->content if something is found.
1792 *
1793 * @throws \InvalidArgumentException
1794 * @throws \RuntimeException
1795 */
1796 public function getFromCache()
1797 {
1798 // clearing the content-variable, which will hold the pagecontent
1799 $this->content = '';
1800 // Unsetting the lowlevel config
1801 $this->config = [];
1802 $this->cacheContentFlag = false;
1803
1804 if ($this->no_cache) {
1805 return;
1806 }
1807
1808 if (!$this->tmpl instanceof TemplateService) {
1809 $this->tmpl = GeneralUtility::makeInstance(TemplateService::class, $this->context, null, $this);
1810 }
1811
1812 $pageSectionCacheContent = $this->tmpl->getCurrentPageData((int)$this->id, (string)$this->MP);
1813 if (!is_array($pageSectionCacheContent)) {
1814 // Nothing in the cache, we acquire an "exclusive lock" for the key now.
1815 // We use the Registry to store this lock centrally,
1816 // but we protect the access again with a global exclusive lock to avoid race conditions
1817
1818 $this->acquireLock('pagesection', $this->id . '::' . $this->MP);
1819 //
1820 // from this point on we're the only one working on that page ($key)
1821 //
1822
1823 // query the cache again to see if the page data are there meanwhile
1824 $pageSectionCacheContent = $this->tmpl->getCurrentPageData((int)$this->id, (string)$this->MP);
1825 if (is_array($pageSectionCacheContent)) {
1826 // we have the content, nice that some other process did the work for us already
1827 $this->releaseLock('pagesection');
1828 }
1829 // We keep the lock set, because we are the ones generating the page now and filling the cache.
1830 // This indicates that we have to release the lock later in releaseLocks()
1831 }
1832
1833 if (is_array($pageSectionCacheContent)) {
1834 // 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.
1835 // If this hash is not the same in here in this section and after page-generation, then the page will not be properly cached!
1836 // 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.
1837 $pageSectionCacheContent = $this->tmpl->matching($pageSectionCacheContent);
1838 ksort($pageSectionCacheContent);
1839 $this->all = $pageSectionCacheContent;
1840 }
1841
1842 // Look for page in cache only if a shift-reload is not sent to the server.
1843 $lockHash = $this->getLockHash();
1844 if (!$this->headerNoCache() && $this->all) {
1845 // we got page section information (TypoScript), so lets see if there is also a cached version
1846 // of this page in the pages cache.
1847 $this->newHash = $this->getHash();
1848 $this->getTimeTracker()->push('Cache Row');
1849 $row = $this->getFromCache_queryRow();
1850 if (!is_array($row)) {
1851 // nothing in the cache, we acquire an exclusive lock now
1852 $this->acquireLock('pages', $lockHash);
1853 //
1854 // from this point on we're the only one working on that page ($lockHash)
1855 //
1856
1857 // query the cache again to see if the data are there meanwhile
1858 $row = $this->getFromCache_queryRow();
1859 if (is_array($row)) {
1860 // we have the content, nice that some other process did the work for us
1861 $this->releaseLock('pages');
1862 }
1863 // We keep the lock set, because we are the ones generating the page now and filling the cache.
1864 // This indicates that we have to release the lock later in releaseLocks()
1865 }
1866 if (is_array($row)) {
1867 $this->populatePageDataFromCache($row);
1868 }
1869 $this->getTimeTracker()->pull();
1870 } else {
1871 // the user forced rebuilding the page cache or there was no pagesection information
1872 // get a lock for the page content so other processes will not interrupt the regeneration
1873 $this->acquireLock('pages', $lockHash);
1874 }
1875 }
1876
1877 /**
1878 * Returning the cached version of page with hash = newHash
1879 *
1880 * @return array Cached row, if any. Otherwise void.
1881 */
1882 public function getFromCache_queryRow()
1883 {
1884 $this->getTimeTracker()->push('Cache Query');
1885 $row = $this->pageCache->get($this->newHash);
1886 $this->getTimeTracker()->pull();
1887 return $row;
1888 }
1889
1890 /**
1891 * This method properly sets the values given from the pages cache into the corresponding
1892 * TSFE variables. The counterpart is setPageCacheContent() where all relevant information is fetched.
1893 * This also contains all data that could be cached, even for pages that are partially cached, as they
1894 * have non-cacheable content still to be rendered.
1895 *
1896 * @see getFromCache()
1897 * @see setPageCacheContent()
1898 * @param array $cachedData
1899 */
1900 protected function populatePageDataFromCache(array $cachedData): void
1901 {
1902 // Call hook when a page is retrieved from cache
1903 $_params = ['pObj' => &$this, 'cache_pages_row' => &$cachedData];
1904 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['pageLoadedFromCache'] ?? [] as $_funcRef) {
1905 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1906 }
1907 // Fetches the lowlevel config stored with the cached data
1908 $this->config = $cachedData['cache_data'];
1909 // Getting the content
1910 $this->content = $cachedData['content'];
1911 // Setting flag, so we know, that some cached content has been loaded
1912 $this->cacheContentFlag = true;
1913 $this->cacheExpires = $cachedData['expires'];
1914
1915 // Restore page title information, this is needed to generate the page title for
1916 // partially cached pages.
1917 $this->page['title'] = $cachedData['pageTitleInfo']['title'];
1918 $this->indexedDocTitle = $cachedData['pageTitleInfo']['indexedDocTitle'];
1919
1920 if (isset($this->config['config']['debug'])) {
1921 $debugCacheTime = (bool)$this->config['config']['debug'];
1922 } else {
1923 $debugCacheTime = !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']);
1924 }
1925 if ($debugCacheTime) {
1926 $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'];
1927 $timeFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
1928 $this->content .= LF . '<!-- Cached page generated ' . date($dateFormat . ' ' . $timeFormat, $cachedData['tstamp']) . '. Expires ' . date($dateFormat . ' ' . $timeFormat, $cachedData['expires']) . ' -->';
1929 }
1930 }
1931
1932 /**
1933 * Detecting if shift-reload has been clicked
1934 * Will not be called if re-generation of page happens by other reasons (for instance that the page is not in cache yet!)
1935 * Also, a backend user MUST be logged in for the shift-reload to be detected due to DoS-attack-security reasons.
1936 *
1937 * @return bool If shift-reload in client browser has been clicked, disable getting cached page (and regenerate it).
1938 */
1939 public function headerNoCache()
1940 {
1941 $disableAcquireCacheData = false;
1942 if ($this->isBackendUserLoggedIn()) {
1943 if (strtolower($_SERVER['HTTP_CACHE_CONTROL']) === 'no-cache' || strtolower($_SERVER['HTTP_PRAGMA']) === 'no-cache') {
1944 $disableAcquireCacheData = true;
1945 }
1946 }
1947 // Call hook for possible by-pass of requiring of page cache (for recaching purpose)
1948 $_params = ['pObj' => &$this, 'disableAcquireCacheData' => &$disableAcquireCacheData];
1949 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['headerNoCache'] ?? [] as $_funcRef) {
1950 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1951 }
1952 return $disableAcquireCacheData;
1953 }
1954
1955 /**
1956 * Calculates the cache-hash
1957 * This hash is unique to the template, the variables ->id, ->type, list of fe user groups, ->MP (Mount Points) and cHash array
1958 * Used to get and later store the cached data.
1959 *
1960 * @return string MD5 hash of serialized hash base from createHashBase()
1961 * @see getFromCache()
1962 * @see getLockHash()
1963 */
1964 protected function getHash()
1965 {
1966 return md5($this->createHashBase(false));
1967 }
1968
1969 /**
1970 * Calculates the lock-hash
1971 * This hash is unique to the above hash, except that it doesn't contain the template information in $this->all.
1972 *
1973 * @return string MD5 hash
1974 * @see getFromCache()
1975 * @see getHash()
1976 */
1977 protected function getLockHash()
1978 {
1979 $lockHash = $this->createHashBase(true);
1980 return md5($lockHash);
1981 }
1982
1983 /**
1984 * Calculates the cache-hash (or the lock-hash)
1985 * This hash is unique to the template,
1986 * the variables ->id, ->type, list of frontend user groups,
1987 * ->MP (Mount Points) and cHash array
1988 * Used to get and later store the cached data.
1989 *
1990 * @param bool $createLockHashBase Whether to create the lock hash, which doesn't contain the "this->all" (the template information)
1991 * @return string the serialized hash base
1992 */
1993 protected function createHashBase($createLockHashBase = false)
1994 {
1995 // Fetch the list of user groups
1996 /** @var UserAspect $userAspect */
1997 $userAspect = $this->context->getAspect('frontend.user');
1998 $hashParameters = [
1999 'id' => (int)$this->id,
2000 'type' => (int)$this->type,
2001 'groupIds' => (string)implode(',', $userAspect->getGroupIds()),
2002 'MP' => (string)$this->MP,
2003 'site' => $this->site->getIdentifier(),
2004 // Ensure the language base is used for the hash base calculation as well, otherwise TypoScript and page-related rendering
2005 // is not cached properly as we don't have any language-specific conditions anymore
2006 'siteBase' => (string)$this->language->getBase(),
2007 // additional variation trigger for static routes
2008 'staticRouteArguments' => $this->pageArguments->getStaticArguments(),
2009 // dynamic route arguments (if route was resolved)
2010 'dynamicArguments' => $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments),
2011 ];
2012 // Include the template information if we shouldn't create a lock hash
2013 if (!$createLockHashBase) {
2014 $hashParameters['all'] = $this->all;
2015 }
2016 // Call hook to influence the hash calculation
2017 $_params = [
2018 'hashParameters' => &$hashParameters,
2019 'createLockHashBase' => $createLockHashBase
2020 ];
2021 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['createHashBase'] ?? [] as $_funcRef) {
2022 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2023 }
2024 return serialize($hashParameters);
2025 }
2026
2027 /**
2028 * Checks if config-array exists already but if not, gets it
2029 *
2030 * @throws ServiceUnavailableException
2031 */
2032 public function getConfigArray()
2033 {
2034 if (!$this->tmpl instanceof TemplateService) {
2035 $this->tmpl = GeneralUtility::makeInstance(TemplateService::class, $this->context, null, $this);
2036 }
2037
2038 // 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
2039 if (empty($this->config) || $this->isINTincScript() || $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing')) {
2040 $timeTracker = $this->getTimeTracker();
2041 $timeTracker->push('Parse template');
2042 // Start parsing the TS template. Might return cached version.
2043 $this->tmpl->start($this->rootLine);
2044 $timeTracker->pull();
2045 // At this point we have a valid pagesection_cache (generated in $this->tmpl->start()),
2046 // so let all other processes proceed now. (They are blocked at the pagessection_lock in getFromCache())
2047 $this->releaseLock('pagesection');
2048 if ($this->tmpl->loaded) {
2049 $timeTracker->push('Setting the config-array');
2050 // toplevel - objArrayName
2051 $this->sPre = $this->tmpl->setup['types.'][$this->type];
2052 $this->pSetup = $this->tmpl->setup[$this->sPre . '.'];
2053 if (!is_array($this->pSetup)) {
2054 $message = 'The page is not configured! [type=' . $this->type . '][' . $this->sPre . '].';
2055 $this->logger->alert($message);
2056 try {
2057 $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
2058 $GLOBALS['TYPO3_REQUEST'],
2059 $message,
2060 ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED]
2061 );
2062 throw new ImmediateResponseException($response, 1533931374);
2063 } catch (PageNotFoundException $e) {
2064 $explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.';
2065 throw new ServiceUnavailableException($message . ' ' . $explanation, 1294587217);
2066 }
2067 } else {
2068 if (!isset($this->config['config'])) {
2069 $this->config['config'] = [];
2070 }
2071 // Filling the config-array, first with the main "config." part
2072 if (is_array($this->tmpl->setup['config.'])) {
2073 ArrayUtility::mergeRecursiveWithOverrule($this->tmpl->setup['config.'], $this->config['config']);
2074 $this->config['config'] = $this->tmpl->setup['config.'];
2075 }
2076 // override it with the page/type-specific "config."
2077 if (is_array($this->pSetup['config.'])) {
2078 ArrayUtility::mergeRecursiveWithOverrule($this->config['config'], $this->pSetup['config.']);
2079 }
2080 // Set default values for removeDefaultJS and inlineStyle2TempFile so CSS and JS are externalized if compatversion is higher than 4.0
2081 if (!isset($this->config['config']['removeDefaultJS'])) {
2082 $this->config['config']['removeDefaultJS'] = 'external';
2083 }
2084 if (!isset($this->config['config']['inlineStyle2TempFile'])) {
2085 $this->config['config']['inlineStyle2TempFile'] = 1;
2086 }
2087
2088 if (!isset($this->config['config']['compressJs'])) {
2089 $this->config['config']['compressJs'] = 0;
2090 }
2091 // Setting default cache_timeout
2092 if (isset($this->config['config']['cache_period'])) {
2093 $this->set_cache_timeout_default((int)$this->config['config']['cache_period']);
2094 }
2095
2096 // Processing for the config_array:
2097 $this->config['rootLine'] = $this->tmpl->rootLine;
2098 // Class for render Header and Footer parts
2099 if ($this->pSetup['pageHeaderFooterTemplateFile']) {
2100 try {
2101 $file = GeneralUtility::makeInstance(FilePathSanitizer::class)
2102 ->sanitize((string)$this->pSetup['pageHeaderFooterTemplateFile']);
2103 $this->pageRenderer->setTemplateFile($file);
2104 } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
2105 // do nothing
2106 }
2107 }
2108 }
2109 $timeTracker->pull();
2110 } else {
2111 $message = 'No TypoScript template found!';
2112 $this->logger->alert($message);
2113 try {
2114 $response = GeneralUtility::makeInstance(ErrorController::class)->unavailableAction(
2115 $GLOBALS['TYPO3_REQUEST'],
2116 $message,
2117 ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_FOUND]
2118 );
2119 throw new ImmediateResponseException($response, 1533931380);
2120 } catch (ServiceUnavailableException $e) {
2121 throw new ServiceUnavailableException($message, 1294587218);
2122 }
2123 }
2124 }
2125
2126 // No cache
2127 // Set $this->no_cache TRUE if the config.no_cache value is set!
2128 if ($this->config['config']['no_cache']) {
2129 $this->set_no_cache('config.no_cache is set', true);
2130 }
2131
2132 // Auto-configure settings when a site is configured
2133 $this->config['config']['absRefPrefix'] = $this->config['config']['absRefPrefix'] ?? 'auto';
2134
2135 // Hook for postProcessing the configuration array
2136 $params = ['config' => &$this->config['config']];
2137 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['configArrayPostProc'] ?? [] as $funcRef) {
2138 GeneralUtility::callUserFunction($funcRef, $params, $this);
2139 }
2140 }
2141
2142 /********************************************
2143 *
2144 * Further initialization and data processing
2145 *
2146 *******************************************/
2147
2148 /**
2149 * Setting the language key that will be used by the current page.
2150 * 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.
2151 *
2152 * @internal
2153 */
2154 public function settingLanguage()
2155 {
2156 $_params = [];
2157 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_preProcess'] ?? [] as $_funcRef) {
2158 $ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
2159 GeneralUtility::callUserFunction($_funcRef, $_params, $ref);
2160 }
2161
2162 // Rendering charset of HTML page.
2163 if (isset($this->config['config']['metaCharset']) && $this->config['config']['metaCharset'] !== 'utf-8') {
2164 $this->metaCharset = $this->config['config']['metaCharset'];
2165 }
2166
2167 // Get values from site language
2168 $languageAspect = LanguageAspectFactory::createFromSiteLanguage($this->language);
2169
2170 $languageId = $languageAspect->getId();
2171 $languageContentId = $languageAspect->getContentId();
2172
2173 // If sys_language_uid is set to another language than default:
2174 if ($languageAspect->getId() > 0) {
2175 // check whether a shortcut is overwritten by a translated page
2176 // we can only do this now, as this is the place where we get
2177 // to know about translations
2178 $this->checkTranslatedShortcut($languageAspect->getId());
2179 // Request the overlay record for the sys_language_uid:
2180 $olRec = $this->sys_page->getPageOverlay($this->id, $languageAspect->getId());
2181 if (empty($olRec)) {
2182 // If requested translation is not available:
2183 if (GeneralUtility::hideIfNotTranslated($this->page['l18n_cfg'])) {
2184 $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
2185 $GLOBALS['TYPO3_REQUEST'],
2186 'Page is not available in the requested language.',
2187 ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE]
2188 );
2189 throw new ImmediateResponseException($response, 1533931388);
2190 }
2191 switch ((string)$languageAspect->getLegacyLanguageMode()) {
2192 case 'strict':
2193 $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
2194 $GLOBALS['TYPO3_REQUEST'],
2195 'Page is not available in the requested language (strict).',
2196 ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE_STRICT_MODE]
2197 );
2198 throw new ImmediateResponseException($response, 1533931395);
2199 break;
2200 case 'fallback':
2201 case 'content_fallback':
2202 // Setting content uid (but leaving the sys_language_uid) when a content_fallback
2203 // value was found.
2204 foreach ($languageAspect->getFallbackChain() ?? [] as $orderValue) {
2205 if ($orderValue === '0' || $orderValue === 0 || $orderValue === '') {
2206 $languageContentId = 0;
2207 break;
2208 }
2209 if (MathUtility::canBeInterpretedAsInteger($orderValue) && !empty($this->sys_page->getPageOverlay($this->id, (int)$orderValue))) {
2210 $languageContentId = (int)$orderValue;
2211 break;
2212 }
2213 if ($orderValue === 'pageNotFound') {
2214 // The existing fallbacks have not been found, but instead of continuing
2215 // page rendering with default language, a "page not found" message should be shown
2216 // instead.
2217 $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
2218 $GLOBALS['TYPO3_REQUEST'],
2219 'Page is not available in the requested language (fallbacks did not apply).',
2220 ['code' => PageAccessFailureReasons::LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE]
2221 );
2222 throw new ImmediateResponseException($response, 1533931402);
2223 }
2224 }
2225 break;
2226 case 'ignore':
2227 $languageContentId = $languageAspect->getId();
2228 break;
2229 default:
2230 // Default is that everything defaults to the default language...
2231 $languageId = ($languageContentId = 0);
2232 }
2233 }
2234
2235 // Define the language aspect again now
2236 $languageAspect = GeneralUtility::makeInstance(
2237 LanguageAspect::class,
2238 $languageId,
2239 $languageContentId,
2240 $languageAspect->getOverlayType(),
2241 $languageAspect->getFallbackChain()
2242 );
2243
2244 // Setting sys_language if an overlay record was found (which it is only if a language is used)
2245 // We'll do this every time since the language aspect might have changed now
2246 // Doing this ensures that page properties like the page title are returned in the correct language
2247 $this->page = $this->sys_page->getPageOverlay($this->page, $languageAspect->getContentId());
2248
2249 // Update SYS_LASTCHANGED for localized page record
2250 $this->setRegisterValueForSysLastChanged($this->page);
2251 }
2252
2253 // Set the language aspect
2254 $this->context->setAspect('language', $languageAspect);
2255
2256 // Setting sys_language_uid inside sys-page by creating a new page repository
2257 $this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context);
2258 // If default language is not available:
2259 if ((!$languageAspect->getContentId() || !$languageAspect->getId())
2260 && GeneralUtility::hideIfDefaultLanguage($this->page['l18n_cfg'] ?? 0)
2261 ) {
2262 $message = 'Page is not available in default language.';
2263 $this->logger->error($message);
2264 $response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
2265 $GLOBALS['TYPO3_REQUEST'],
2266 $message,
2267 ['code' => PageAccessFailureReasons::LANGUAGE_DEFAULT_NOT_AVAILABLE]
2268 );
2269 throw new ImmediateResponseException($response, 1533931423);
2270 }
2271
2272 if ($languageAspect->getId() > 0) {
2273 $this->updateRootLinesWithTranslations();
2274 }
2275
2276 $_params = [];
2277 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_postProcess'] ?? [] as $_funcRef) {
2278 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2279 }
2280 }
2281
2282 /**
2283 * Updating content of the two rootLines IF the language key is set!
2284 */
2285 protected function updateRootLinesWithTranslations()
2286 {
2287 try {
2288 $this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get();
2289 } catch (RootLineException $e) {
2290 $this->rootLine = [];
2291 }
2292 $this->tmpl->updateRootlineData($this->rootLine);
2293 }
2294
2295 /**
2296 * Setting locale for frontend rendering
2297 * @deprecated will be removed in TYPO3 v11.0. Use Locales::setSystemLocaleFromSiteLanguage() instead.
2298 */
2299 public function settingLocale()
2300 {
2301 trigger_error('TSFE->settingLocale() will be removed in TYPO3 v11.0. Use Locales::setSystemLocaleFromSiteLanguage() instead, as this functionality is independent of TSFE.', E_USER_DEPRECATED);
2302 if ($this->language->getLocale() && !Locales::setSystemLocaleFromSiteLanguage($this->language)) {
2303 $this->getTimeTracker()->setTSlogMessage('Locale "' . htmlspecialchars($this->language->getLocale()) . '" not found.', 3);
2304 }
2305 }
2306
2307 /**
2308 * Checks whether a translated shortcut page has a different shortcut
2309 * target than the original language page.
2310 * If that is the case, things get corrected to follow that alternative
2311 * shortcut
2312 * @param int $languageId
2313 */
2314 protected function checkTranslatedShortcut(int $languageId)
2315 {
2316 if (!is_null($this->originalShortcutPage)) {
2317 $originalShortcutPageOverlay = $this->sys_page->getPageOverlay($this->originalShortcutPage['uid'], $languageId);
2318 if (!empty($originalShortcutPageOverlay['shortcut']) && $originalShortcutPageOverlay['shortcut'] != $this->id) {
2319 // the translation of the original shortcut page has a different shortcut target!
2320 // set the correct page and id
2321 $shortcut = $this->sys_page->getPageShortcut($originalShortcutPageOverlay['shortcut'], $originalShortcutPageOverlay['shortcut_mode'], $originalShortcutPageOverlay['uid']);
2322 $this->id = ($this->contentPid = $shortcut['uid']);
2323 $this->page = $this->sys_page->getPage($this->id);
2324 // Fix various effects on things like menus f.e.
2325 $this->fetch_the_id();
2326 $this->tmpl->rootLine = array_reverse($this->rootLine);
2327 }
2328 }
2329 }
2330
2331 /**
2332 * Calculates and sets the internal linkVars based upon the current request parameters
2333 * and the setting "config.linkVars".
2334 *
2335 * @param array $queryParams $_GET (usually called with a PSR-7 $request->getQueryParams())
2336 */
2337 public function calculateLinkVars(array $queryParams)
2338 {
2339 $this->linkVars = '';
2340 if (empty($this->config['config']['linkVars'])) {
2341 return;
2342 }
2343
2344 $linkVars = $this->splitLinkVarsString((string)$this->config['config']['linkVars']);
2345
2346 if (empty($linkVars)) {
2347 return;
2348 }
2349 foreach ($linkVars as $linkVar) {
2350 $test = $value = '';
2351 if (preg_match('/^(.*)\\((.+)\\)$/', $linkVar, $match)) {
2352 $linkVar = trim($match[1]);
2353 $test = trim($match[2]);
2354 }
2355
2356 $keys = explode('|', $linkVar);
2357 $numberOfLevels = count($keys);
2358 $rootKey = trim($keys[0]);
2359 if (!isset($queryParams[$rootKey])) {
2360 continue;
2361 }
2362 $value = $queryParams[$rootKey];
2363 for ($i = 1; $i < $numberOfLevels; $i++) {
2364 $currentKey = trim($keys[$i]);
2365 if (isset($value[$currentKey])) {
2366 $value = $value[$currentKey];
2367 } else {
2368 $value = false;
2369 break;
2370 }
2371 }
2372 if ($value !== false) {
2373 $parameterName = $keys[0];
2374 for ($i = 1; $i < $numberOfLevels; $i++) {
2375 $parameterName .= '[' . $keys[$i] . ']';
2376 }
2377 if (!is_array($value)) {
2378 $temp = rawurlencode($value);
2379 if ($test !== '' && !$this->isAllowedLinkVarValue($temp, $test)) {
2380 // Error: This value was not allowed for this key
2381 continue;
2382 }
2383 $value = '&' . $parameterName . '=' . $temp;
2384 } else {
2385 if ($test !== '' && $test !== 'array') {
2386 // Error: This key must not be an array!
2387 continue;
2388 }
2389 $value = HttpUtility::buildQueryString([$parameterName => $value], '&');
2390 }
2391 $this->linkVars .= $value;
2392 }
2393 }
2394 }
2395
2396 /**
2397 * Split the link vars string by "," but not if the "," is inside of braces
2398 *
2399 * @param $string
2400 *
2401 * @return array
2402 */
2403 protected function splitLinkVarsString(string $string): array
2404 {
2405 $tempCommaReplacementString = '###KASPER###';
2406
2407 // replace every "," wrapped in "()" by a "unique" string
2408 $string = preg_replace_callback('/\((?>[^()]|(?R))*\)/', function ($result) use ($tempCommaReplacementString) {
2409 return str_replace(',', $tempCommaReplacementString, $result[0]);
2410 }, $string);
2411
2412 $string = GeneralUtility::trimExplode(',', $string);
2413
2414 // replace all "unique" strings back to ","
2415 return str_replace($tempCommaReplacementString, ',', $string);
2416 }
2417
2418 /**
2419 * Checks if the value defined in "config.linkVars" contains an allowed value.
2420 * Otherwise, return FALSE which means the value will not be added to any links.
2421 *
2422 * @param string $haystack The string in which to find $needle
2423 * @param string $needle The string to find in $haystack
2424 * @return bool Returns TRUE if $needle matches or is found in $haystack
2425 */
2426 protected function isAllowedLinkVarValue(string $haystack, string $needle): bool
2427 {
2428 $isAllowed = false;
2429 // Integer
2430 if ($needle === 'int' || $needle === 'integer') {
2431 if (MathUtility::canBeInterpretedAsInteger($haystack)) {
2432 $isAllowed = true;
2433 }
2434 } elseif (preg_match('/^\\/.+\\/[imsxeADSUXu]*$/', $needle)) {
2435 // Regular expression, only "//" is allowed as delimiter
2436 if (@preg_match($needle, $haystack)) {
2437 $isAllowed = true;
2438 }
2439 } elseif (strpos($needle, '-') !== false) {
2440 // Range
2441 if (MathUtility::canBeInterpretedAsInteger($haystack)) {
2442 $range = explode('-', $needle);
2443 if ($range[0] <= $haystack && $range[1] >= $haystack) {
2444 $isAllowed = true;
2445 }
2446 }
2447 } elseif (strpos($needle, '|') !== false) {
2448 // List
2449 // Trim the input
2450 $haystack = str_replace(' ', '', $haystack);
2451 if (strpos('|' . $needle . '|', '|' . $haystack . '|') !== false) {
2452 $isAllowed = true;
2453 }
2454 } elseif ((string)$needle === (string)$haystack) {
2455 // String comparison
2456 $isAllowed = true;
2457 }
2458 return $isAllowed;
2459 }
2460
2461 /**
2462 * Returns URI of target page, if the current page is an overlaid mountpoint.
2463 *
2464 * If the current page is of type mountpoint and should be overlaid with the contents of the mountpoint page
2465 * and is accessed directly, the user will be redirected to the mountpoint context.
2466 * @internal
2467 * @param ServerRequestInterface $request
2468 * @return string|null
2469 */
2470 public function getRedirectUriForMountPoint(ServerRequestInterface $request): ?string
2471 {
2472 if (!empty($this->originalMountPointPage) && (int)$this->originalMountPointPage['doktype'] === PageRepository::DOKTYPE_MOUNTPOINT) {
2473 return $this->getUriToCurrentPageForRedirect($request);
2474 }
2475
2476 return null;
2477 }
2478
2479 /**
2480 * Returns URI of target page, if the current page is a Shortcut.
2481 *
2482 * If the current page is of type shortcut and accessed directly via its URL,
2483 * the user will be redirected to shortcut target.
2484 *
2485 * @internal
2486 * @param ServerRequestInterface $request
2487 * @return string|null
2488 */
2489 public function getRedirectUriForShortcut(ServerRequestInterface $request): ?string
2490 {
2491 if (!empty($this->originalShortcutPage) && $this->originalShortcutPage['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
2492 return $this->getUriToCurrentPageForRedirect($request);
2493 }
2494
2495 return null;
2496 }
2497
2498 /**
2499 * Instantiate \TYPO3\CMS\Frontend\ContentObject to generate the correct target URL
2500 *
2501 * @param ServerRequestInterface $request
2502 * @return string
2503 */
2504 protected function getUriToCurrentPageForRedirect(ServerRequestInterface $request): string
2505 {
2506 $this->calculateLinkVars($request->getQueryParams());
2507 $parameter = $this->page['uid'];
2508 if ($this->type && MathUtility::canBeInterpretedAsInteger($this->type)) {
2509 $parameter .= ',' . $this->type;
2510 }
2511 return GeneralUtility::makeInstance(ContentObjectRenderer::class, $this)->typoLink_URL([
2512 'parameter' => $parameter,
2513 'addQueryString' => true,
2514 'addQueryString.' => ['exclude' => 'id'],
2515 // ensure absolute URL is generated when having a valid Site
2516 'forceAbsoluteUrl' => $GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface
2517 && $GLOBALS['TYPO3_REQUEST']->getAttribute('site') instanceof Site
2518 ]);
2519 }
2520
2521 /********************************************
2522 *
2523 * Page generation; cache handling
2524 *
2525 *******************************************/
2526 /**
2527 * Returns TRUE if the page should be generated.
2528 * That is if no URL handler is active and the cacheContentFlag is not set.
2529 *
2530 * @return bool
2531 */
2532 public function isGeneratePage()
2533 {
2534 return !$this->cacheContentFlag;
2535 }
2536
2537 /**
2538 * Set cache content to $this->content
2539 */
2540 protected function realPageCacheContent()
2541 {
2542 // seconds until a cached page is too old
2543 $cacheTimeout = $this->get_cache_timeout();
2544 $timeOutTime = $GLOBALS['EXEC_TIME'] + $cacheTimeout;
2545 $usePageCache = true;
2546 // Hook for deciding whether page cache should be written to the cache backend or not
2547 // NOTE: as hooks are called in a loop, the last hook will have the final word (however each
2548 // hook receives the current status of the $usePageCache flag)
2549 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['usePageCache'] ?? [] as $className) {
2550 $usePageCache = GeneralUtility::makeInstance($className)->usePageCache($this, $usePageCache);
2551 }
2552 // Write the page to cache, if necessary
2553 if ($usePageCache) {
2554 $this->setPageCacheContent($this->content, $this->config, $timeOutTime);
2555 }
2556 // Hook for cache post processing (eg. writing static files!)
2557 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['insertPageIncache'] ?? [] as $className) {
2558 GeneralUtility::makeInstance($className)->insertPageIncache($this, $timeOutTime);
2559 }
2560 }
2561
2562 /**
2563 * Sets cache content; Inserts the content string into the cache_pages cache.
2564 *
2565 * @param string $content The content to store in the HTML field of the cache table
2566 * @param mixed $data The additional cache_data array, fx. $this->config
2567 * @param int $expirationTstamp Expiration timestamp
2568 * @see realPageCacheContent()
2569 */
2570 protected function setPageCacheContent($content, $data, $expirationTstamp)
2571 {
2572 $cacheData = [
2573 'identifier' => $this->newHash,
2574 'page_id' => $this->id,
2575 'content' => $content,
2576 'cache_data' => $data,
2577 'expires' => $expirationTstamp,
2578 'tstamp' => $GLOBALS['EXEC_TIME'],
2579 'pageTitleInfo' => [
2580 'title' => $this->page['title'],
2581 'indexedDocTitle' => $this->indexedDocTitle
2582 ]
2583 ];
2584 $this->cacheExpires = $expirationTstamp;
2585 $this->pageCacheTags[] = 'pageId_' . $cacheData['page_id'];
2586 if (!empty($this->page['cache_tags'])) {
2587 $tags = GeneralUtility::trimExplode(',', $this->page['cache_tags'], true);
2588 $this->pageCacheTags = array_merge($this->pageCacheTags, $tags);
2589 }
2590 $this->pageCache->set($this->newHash, $cacheData, $this->pageCacheTags, $expirationTstamp - $GLOBALS['EXEC_TIME']);
2591 }
2592
2593 /**
2594 * Clears cache content (for $this->newHash)
2595 */
2596 public function clearPageCacheContent()
2597 {
2598 $this->pageCache->remove($this->newHash);
2599 }
2600
2601 /**
2602 * Clears cache content for a list of page ids
2603 *
2604 * @param string $pidList A list of INTEGER numbers which points to page uids for which to clear entries in the cache_pages cache (page content cache)
2605 */
2606 protected function clearPageCacheContent_pidList($pidList)
2607 {
2608 $pageIds = GeneralUtility::trimExplode(',', $pidList);
2609 foreach ($pageIds as $pageId) {
2610 $this->pageCache->flushByTag('pageId_' . (int)$pageId);
2611 }
2612 }
2613
2614 /**
2615 * Sets sys last changed
2616 * 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.
2617 *
2618 * @see ContentObjectRenderer::lastChanged()
2619 */
2620 protected function setSysLastChanged()
2621 {
2622 // We only update the info if browsing the live workspace
2623 if ($this->page['SYS_LASTCHANGED'] < (int)$this->register['SYS_LASTCHANGED'] && !$this->doWorkspacePreview()) {
2624 $connection = GeneralUtility::makeInstance(ConnectionPool::class)
2625 ->getConnectionForTable('pages');
2626 $pageId = $this->page['_PAGES_OVERLAY_UID'] ?? $this->id;
2627 $connection->update(
2628 'pages',
2629 [
2630 'SYS_LASTCHANGED' => (int)$this->register['SYS_LASTCHANGED']
2631 ],
2632 [
2633 'uid' => (int)$pageId
2634 ]
2635 );
2636 }
2637 }
2638
2639 /**
2640 * Set the SYS_LASTCHANGED register value, is also called when a translated page is in use,
2641 * so the register reflects the state of the translated page, not the page in the default language.
2642 *
2643 * @param array $page
2644 * @internal
2645 */
2646 protected function setRegisterValueForSysLastChanged(array $page): void
2647 {
2648 $this->register['SYS_LASTCHANGED'] = (int)$page['tstamp'];
2649 if ($this->register['SYS_LASTCHANGED'] < (int)$page['SYS_LASTCHANGED']) {
2650 $this->register['SYS_LASTCHANGED'] = (int)$page['SYS_LASTCHANGED'];
2651 }
2652 }
2653
2654 /**
2655 * Release pending locks
2656 *
2657 * @internal
2658 */
2659 public function releaseLocks()
2660 {
2661 $this->releaseLock('pagesection');
2662 $this->releaseLock('pages');
2663 }
2664
2665 /**
2666 * Adds tags to this page's cache entry, you can then f.e. remove cache
2667 * entries by tag
2668 *
2669 * @param array $tags An array of tag
2670 */
2671 public function addCacheTags(array $tags)
2672 {
2673 $this->pageCacheTags = array_merge($this->pageCacheTags, $tags);
2674 }
2675
2676 /**
2677 * @return array
2678 */
2679 public function getPageCacheTags(): array
2680 {
2681 return $this->pageCacheTags;
2682 }
2683
2684 /********************************************
2685 *
2686 * Page generation; rendering and inclusion
2687 *
2688 *******************************************/
2689 /**
2690 * Does some processing BEFORE the page content is generated / built.
2691 */
2692 public function generatePage_preProcessing()
2693 {
2694 // Same codeline as in getFromCache(). But $this->all has been changed by
2695 // \TYPO3\CMS\Core\TypoScript\TemplateService::start() in the meantime, so this must be called again!
2696 $this->newHash = $this->getHash();
2697
2698 // Used as a safety check in case a PHP script is falsely disabling $this->no_cache during page generation.
2699 $this->no_cacheBeforePageGen = $this->no_cache;
2700 }
2701
2702 /**
2703 * Sets up TypoScript "config." options and set properties in $TSFE.
2704 *
2705 * @param ServerRequestInterface $request
2706 */
2707 public function preparePageContentGeneration(ServerRequestInterface $request)
2708 {
2709 $this->getTimeTracker()->push('Prepare page content generation');
2710 if (isset($this->page['content_from_pid']) && $this->page['content_from_pid'] > 0) {
2711 // make REAL copy of TSFE object - not reference!
2712 $temp_copy_TSFE = clone $this;
2713 // 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!
2714 $temp_copy_TSFE->id = $this->page['content_from_pid'];
2715 $temp_copy_TSFE->MP = '';
2716 $temp_copy_TSFE->getPageAndRootlineWithDomain($this->config['config']['content_from_pid_allowOutsideDomain'] ? 0 : $this->site->getRootPageId());
2717 $this->contentPid = (int)$temp_copy_TSFE->id;
2718 unset($temp_copy_TSFE);
2719 }
2720 // Global vars...
2721 $this->indexedDocTitle = $this->page['title'] ?? null;
2722 // Base url:
2723 if (isset($this->config['config']['baseURL'])) {
2724 $this->baseUrl = $this->config['config']['baseURL'];
2725 }
2726 // Internal and External target defaults
2727 $this->intTarget = (string)($this->config['config']['intTarget'] ?? '');
2728 $this->extTarget = (string)($this->config['config']['extTarget'] ?? '');
2729 $this->fileTarget = (string)($this->config['config']['fileTarget'] ?? '');
2730 $this->spamProtectEmailAddresses = $this->config['config']['spamProtectEmailAddresses'] ?? 0;
2731 if ($this->spamProtectEmailAddresses !== 'ascii') {
2732 $this->spamProtectEmailAddresses = MathUtility::forceIntegerInRange($this->spamProtectEmailAddresses, -10, 10, 0);
2733 }
2734 // calculate the absolute path prefix
2735 if (!empty($this->config['config']['absRefPrefix'])) {
2736 $absRefPrefix = trim($this->config['config']['absRefPrefix']);
2737 if ($absRefPrefix === 'auto') {
2738 $this->absRefPrefix = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
2739 } else {
2740 $this->absRefPrefix = $absRefPrefix;
2741 }
2742 } else {
2743 $this->absRefPrefix = '';
2744 }
2745 $this->ATagParams = trim($this->config['config']['ATagParams'] ?? '') ? ' ' . trim($this->config['config']['ATagParams']) : '';
2746 $this->initializeSearchWordData($request->getParsedBody()['sword_list'] ?? $request->getQueryParams()['sword_list'] ?? null);
2747 // linkVars
2748 $this->calculateLinkVars($request->getQueryParams());
2749 // Setting XHTML-doctype from doctype
2750 if (!isset($this->config['config']['xhtmlDoctype']) || !$this->config['config']['xhtmlDoctype']) {
2751 $this->config['config']['xhtmlDoctype'] = $this->config['config']['doctype'] ?? '';
2752 }
2753 if ($this->config['config']['xhtmlDoctype']) {
2754 $this->xhtmlDoctype = $this->config['config']['xhtmlDoctype'];
2755 // Checking XHTML-docytpe
2756 switch ((string)$this->config['config']['xhtmlDoctype']) {
2757 case 'xhtml_trans':
2758 case 'xhtml_strict':
2759 $this->xhtmlVersion = 100;
2760 break;
2761 case 'xhtml_basic':
2762 $this->xhtmlVersion = 105;
2763 break;
2764 case 'xhtml_11':
2765 case 'xhtml+rdfa_10':
2766 $this->xhtmlVersion = 110;
2767 break;
2768 default:
2769 $this->pageRenderer->setRenderXhtml(false);
2770 $this->xhtmlDoctype = '';
2771 $this->xhtmlVersion = 0;
2772 }
2773 } else {
2774 $this->pageRenderer->setRenderXhtml(false);
2775 }
2776
2777 // Global content object
2778 $this->newCObj();
2779 $this->getTimeTracker()->pull();
2780 }
2781
2782 /**
2783 * Fills the sWordList property and builds the regular expression in TSFE that can be used to split
2784 * strings by the submitted search words.
2785 *
2786 * @param mixed $searchWords - usually an array, but we can't be sure (yet)
2787 * @see sWordList
2788 * @see sWordRegEx
2789 */
2790 protected function initializeSearchWordData($searchWords)
2791 {
2792 $this->sWordRegEx = '';
2793 $this->sWordList = $searchWords ?? '';
2794 if (is_array($this->sWordList)) {
2795 $space = !empty($this->config['config']['sword_standAlone'] ?? null) ? '[[:space:]]' : '';
2796 $regexpParts = [];
2797 foreach ($this->sWordList as $val) {
2798 if (trim($val) !== '') {
2799 $regexpParts[] = $space . preg_quote($val, '/') . $space;
2800 }
2801 }
2802 $this->sWordRegEx = implode('|', $regexpParts);
2803 }
2804 }
2805
2806 /**
2807 * Does processing of the content after the page content was generated.
2808 *
2809 * This includes caching the page, indexing the page (if configured) and setting sysLastChanged
2810 */
2811 public function generatePage_postProcessing()
2812 {
2813 $this->setAbsRefPrefix();
2814 // 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.
2815 if ($this->no_cacheBeforePageGen) {
2816 $this->set_no_cache('no_cache has been set before the page was generated - safety check', true);
2817 }
2818 // Hook for post-processing of page content cached/non-cached:
2819 $_params = ['pObj' => &$this];
2820 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-all'] ?? [] as $_funcRef) {
2821 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2822 }
2823 // Processing if caching is enabled:
2824 if (!$this->no_cache) {
2825 // Hook for post-processing of page content before being cached:
2826 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-cached'] ?? [] as $_funcRef) {
2827 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2828 }
2829 }
2830 // Convert char-set for output: (should be BEFORE indexing of the content (changed 22/4 2005)),
2831 // because otherwise indexed search might convert from the wrong charset!
2832 // One thing is that the charset mentioned in the HTML header would be wrong since the output charset (metaCharset)
2833 // has not been converted to from utf-8. And indexed search will internally convert from metaCharset
2834 // to utf-8 so the content MUST be in metaCharset already!
2835 $this->content = $this->convOutputCharset($this->content);
2836 // Hook for indexing pages
2837 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['pageIndexing'] ?? [] as $className) {
2838 GeneralUtility::makeInstance($className)->hook_indexContent($this);
2839 }
2840 // Storing for cache:
2841 if (!$this->no_cache) {
2842 $this->realPageCacheContent();
2843 }
2844 // Sets sys-last-change:
2845 $this->setSysLastChanged();
2846 }
2847
2848 /**
2849 * Generate the page title, can be called multiple times,
2850 * as PageTitleProvider might have been modified by an uncached plugin etc.
2851 *
2852 * @return string the generated page title
2853 */
2854 public function generatePageTitle(): string
2855 {
2856 $pageTitleSeparator = '';
2857
2858 // Check for a custom pageTitleSeparator, and perform stdWrap on it
2859 if (isset($this->config['config']['pageTitleSeparator']) && $this->config['config']['pageTitleSeparator'] !== '') {
2860 $pageTitleSeparator = $this->config['config']['pageTitleSeparator'];
2861
2862 if (isset($this->config['config']['pageTitleSeparator.']) && is_array($this->config['config']['pageTitleSeparator.'])) {
2863 $pageTitleSeparator = $this->cObj->stdWrap($pageTitleSeparator, $this->config['config']['pageTitleSeparator.']);
2864 } else {
2865 $pageTitleSeparator .= ' ';
2866 }
2867 }
2868
2869 $titleProvider = GeneralUtility::makeInstance(PageTitleProviderManager::class);
2870 $pageTitle = $titleProvider->getTitle();
2871
2872 if ($pageTitle !== '') {
2873 $this->indexedDocTitle = $pageTitle;
2874 }
2875
2876 $titleTagContent = $this->printTitle(
2877 $pageTitle,
2878 (bool)($this->config['config']['noPageTitle'] ?? false),
2879 (bool)($this->config['config']['pageTitleFirst'] ?? false),
2880 $pageTitleSeparator
2881 );
2882 // stdWrap around the title tag
2883 if (isset($this->config['config']['pageTitle.']) && is_array($this->config['config']['pageTitle.'])) {
2884 $titleTagContent = $this->cObj->stdWrap($titleTagContent, $this->config['config']['pageTitle.']);
2885 }
2886
2887 // config.noPageTitle = 2 - means do not render the page title
2888 if (isset($this->config['config']['noPageTitle']) && (int)$this->config['config']['noPageTitle'] === 2) {
2889 $titleTagContent = '';
2890 }
2891 if ($titleTagContent !== '') {
2892 $this->pageRenderer->setTitle($titleTagContent);
2893 }
2894 return (string)$titleTagContent;
2895 }
2896
2897 /**
2898 * Compiles the content for the page <title> tag.
2899 *
2900 * @param string $pageTitle The input title string, typically the "title" field of a page's record.
2901 * @param bool $noTitle If set, then only the site title is outputted (from $this->setup['sitetitle'])
2902 * @param bool $showTitleFirst If set, then "sitetitle" and $title is swapped
2903 * @param string $pageTitleSeparator an alternative to the ": " as the separator between site title and page title
2904 * @return string The page title on the form "[sitetitle]: [input-title]". Not htmlspecialchar()'ed.
2905 * @see generatePageTitle()
2906 */
2907 protected function printTitle(string $pageTitle, bool $noTitle = false, bool $showTitleFirst = false, string $pageTitleSeparator = ''): string
2908 {
2909 $websiteTitle = $this->getWebsiteTitle();
2910 $pageTitle = $noTitle ? '' : $pageTitle;
2911 if ($showTitleFirst) {
2912 $temp = $websiteTitle;
2913 $websiteTitle = $pageTitle;
2914 $pageTitle = $temp;
2915 }
2916 // only show a separator if there are both site title and page title
2917 if ($pageTitle === '' || $websiteTitle === '') {
2918 $pageTitleSeparator = '';
2919 } elseif (empty($pageTitleSeparator)) {
2920 // use the default separator if non given
2921 $pageTitleSeparator = ': ';
2922 }
2923 return $websiteTitle . $pageTitleSeparator . $pageTitle;
2924 }
2925
2926 /**
2927 * @return string
2928 */
2929 protected function getWebsiteTitle(): string
2930 {
2931 if ($this->language instanceof SiteLanguage
2932 && trim($this->language->getWebsiteTitle()) !== ''
2933 ) {
2934 return trim($this->language->getWebsiteTitle());
2935 }
2936 if ($this->site instanceof SiteInterface
2937 && trim($this->site->getConfiguration()['websiteTitle']) !== ''
2938 ) {
2939 return trim($this->site->getConfiguration()['websiteTitle']);
2940 }
2941 if (!empty($this->tmpl->setup['sitetitle'])) {
2942 // @deprecated since TYPO3 v10.2 and will be removed in TYPO3 v11.0
2943 return trim($this->tmpl->setup['sitetitle']);
2944 }
2945
2946 return '';
2947 }
2948
2949 /**
2950 * Processes the INTinclude-scripts
2951 */
2952 public function INTincScript()
2953 {
2954 $this->additionalHeaderData = is_array($this->config['INTincScript_ext']['additionalHeaderData'] ?? false)
2955 ? $this->config['INTincScript_ext']['additionalHeaderData']
2956 : [];
2957 $this->additionalFooterData = is_array($this->config['INTincScript_ext']['additionalFooterData'] ?? false)
2958 ? $this->config['INTincScript_ext']['additionalFooterData']
2959 : [];
2960