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