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