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