1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the TYPO3 CMS project. |
5
|
|
|
* |
6
|
|
|
* It is free software; you can redistribute it and/or modify it under |
7
|
|
|
* the terms of the GNU General Public License, either version 2 |
8
|
|
|
* of the License, or any later version. |
9
|
|
|
* |
10
|
|
|
* For the full copyright and license information, please read the |
11
|
|
|
* LICENSE.txt file that was distributed with this source code. |
12
|
|
|
* |
13
|
|
|
* The TYPO3 project - inspiring people to share! |
14
|
|
|
*/ |
15
|
|
|
|
16
|
|
|
namespace TYPO3\CMS\Frontend\Controller; |
17
|
|
|
|
18
|
|
|
use Psr\Http\Message\ResponseInterface; |
19
|
|
|
use Psr\Http\Message\ServerRequestInterface; |
20
|
|
|
use Psr\Log\LoggerAwareInterface; |
21
|
|
|
use Psr\Log\LoggerAwareTrait; |
22
|
|
|
use TYPO3\CMS\Backend\FrontendBackendUserAuthentication; |
23
|
|
|
use TYPO3\CMS\Core\Cache\CacheManager; |
24
|
|
|
use TYPO3\CMS\Core\Charset\CharsetConverter; |
25
|
|
|
use TYPO3\CMS\Core\Charset\UnknownCharsetException; |
26
|
|
|
use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader; |
27
|
|
|
use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser; |
28
|
|
|
use TYPO3\CMS\Core\Context\Context; |
29
|
|
|
use TYPO3\CMS\Core\Context\DateTimeAspect; |
30
|
|
|
use TYPO3\CMS\Core\Context\LanguageAspect; |
31
|
|
|
use TYPO3\CMS\Core\Context\LanguageAspectFactory; |
32
|
|
|
use TYPO3\CMS\Core\Context\UserAspect; |
33
|
|
|
use TYPO3\CMS\Core\Context\VisibilityAspect; |
34
|
|
|
use TYPO3\CMS\Core\Context\WorkspaceAspect; |
35
|
|
|
use TYPO3\CMS\Core\Core\Environment; |
36
|
|
|
use TYPO3\CMS\Core\Database\Connection; |
37
|
|
|
use TYPO3\CMS\Core\Database\ConnectionPool; |
38
|
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; |
39
|
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction; |
40
|
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction; |
41
|
|
|
use TYPO3\CMS\Core\Domain\Repository\PageRepository; |
42
|
|
|
use TYPO3\CMS\Core\Error\Http\AbstractServerErrorException; |
43
|
|
|
use TYPO3\CMS\Core\Error\Http\PageNotFoundException; |
44
|
|
|
use TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException; |
45
|
|
|
use TYPO3\CMS\Core\Exception\Page\RootLineException; |
46
|
|
|
use TYPO3\CMS\Core\Http\ApplicationType; |
47
|
|
|
use TYPO3\CMS\Core\Http\NormalizedParams; |
48
|
|
|
use TYPO3\CMS\Core\Http\PropagateResponseException; |
49
|
|
|
use TYPO3\CMS\Core\Http\ServerRequestFactory; |
50
|
|
|
use TYPO3\CMS\Core\Localization\LanguageService; |
51
|
|
|
use TYPO3\CMS\Core\Localization\LanguageServiceFactory; |
52
|
|
|
use TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException; |
53
|
|
|
use TYPO3\CMS\Core\Locking\LockFactory; |
54
|
|
|
use TYPO3\CMS\Core\Locking\LockingStrategyInterface; |
55
|
|
|
use TYPO3\CMS\Core\Page\AssetCollector; |
56
|
|
|
use TYPO3\CMS\Core\Page\PageRenderer; |
57
|
|
|
use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager; |
58
|
|
|
use TYPO3\CMS\Core\Resource\Exception; |
59
|
|
|
use TYPO3\CMS\Core\Resource\StorageRepository; |
60
|
|
|
use TYPO3\CMS\Core\Routing\PageArguments; |
61
|
|
|
use TYPO3\CMS\Core\Site\Entity\SiteInterface; |
62
|
|
|
use TYPO3\CMS\Core\Site\Entity\SiteLanguage; |
63
|
|
|
use TYPO3\CMS\Core\TimeTracker\TimeTracker; |
64
|
|
|
use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility; |
65
|
|
|
use TYPO3\CMS\Core\Type\Bitmask\Permission; |
66
|
|
|
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser; |
67
|
|
|
use TYPO3\CMS\Core\TypoScript\TemplateService; |
68
|
|
|
use TYPO3\CMS\Core\Utility\ArrayUtility; |
69
|
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility; |
70
|
|
|
use TYPO3\CMS\Core\Utility\HttpUtility; |
71
|
|
|
use TYPO3\CMS\Core\Utility\MathUtility; |
72
|
|
|
use TYPO3\CMS\Core\Utility\PathUtility; |
73
|
|
|
use TYPO3\CMS\Core\Utility\RootlineUtility; |
74
|
|
|
use TYPO3\CMS\Frontend\Aspect\PreviewAspect; |
75
|
|
|
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; |
76
|
|
|
use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher; |
77
|
|
|
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; |
78
|
|
|
use TYPO3\CMS\Frontend\Page\CacheHashCalculator; |
79
|
|
|
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons; |
80
|
|
|
use TYPO3\CMS\Frontend\Resource\FilePathSanitizer; |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Class for the built TypoScript based frontend. Instantiated in |
84
|
|
|
* \TYPO3\CMS\Frontend\Http\RequestHandler as the global object TSFE. |
85
|
|
|
* |
86
|
|
|
* Main frontend class, instantiated in \TYPO3\CMS\Frontend\Http\RequestHandler |
87
|
|
|
* as the global object TSFE. |
88
|
|
|
* |
89
|
|
|
* This class has a lot of functions and internal variable which are used from |
90
|
|
|
* \TYPO3\CMS\Frontend\Http\RequestHandler |
91
|
|
|
* |
92
|
|
|
* The class is instantiated as $GLOBALS['TSFE'] in \TYPO3\CMS\Frontend\Http\RequestHandler. |
93
|
|
|
* |
94
|
|
|
* The use of this class should be inspired by the order of function calls as |
95
|
|
|
* found in \TYPO3\CMS\Frontend\Http\RequestHandler. |
96
|
|
|
*/ |
97
|
|
|
class TypoScriptFrontendController implements LoggerAwareInterface |
98
|
|
|
{ |
99
|
|
|
use LoggerAwareTrait; |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* The page id (int) |
103
|
|
|
* @var string|int |
104
|
|
|
*/ |
105
|
|
|
public $id = ''; |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* The type (read-only) |
109
|
|
|
* @var int|string |
110
|
|
|
*/ |
111
|
|
|
public $type = ''; |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* @var SiteInterface |
115
|
|
|
*/ |
116
|
|
|
protected $site; |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* @var SiteLanguage |
120
|
|
|
*/ |
121
|
|
|
protected $language; |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* @var PageArguments |
125
|
|
|
* @internal |
126
|
|
|
*/ |
127
|
|
|
protected $pageArguments; |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* Page will not be cached. Write only TRUE. Never clear value (some other |
131
|
|
|
* code might have reasons to set it TRUE). |
132
|
|
|
* @var bool |
133
|
|
|
*/ |
134
|
|
|
public $no_cache = false; |
135
|
|
|
|
136
|
|
|
/** |
137
|
|
|
* The rootLine (all the way to tree root, not only the current site!) |
138
|
|
|
* @var array |
139
|
|
|
*/ |
140
|
|
|
public $rootLine = []; |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* The pagerecord |
144
|
|
|
* @var array |
145
|
|
|
*/ |
146
|
|
|
public $page = []; |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* This will normally point to the same value as id, but can be changed to |
150
|
|
|
* point to another page from which content will then be displayed instead. |
151
|
|
|
* @var int |
152
|
|
|
*/ |
153
|
|
|
public $contentPid = 0; |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Gets set when we are processing a page of type mounpoint with enabled overlay in getPageAndRootline() |
157
|
|
|
* Used later in checkPageForMountpointRedirect() to determine the final target URL where the user |
158
|
|
|
* should be redirected to. |
159
|
|
|
* |
160
|
|
|
* @var array|null |
161
|
|
|
*/ |
162
|
|
|
protected $originalMountPointPage; |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Gets set when we are processing a page of type shortcut in the early stages |
166
|
|
|
* of the request when we do not know about languages yet, used later in the request |
167
|
|
|
* to determine the correct shortcut in case a translation changes the shortcut |
168
|
|
|
* target |
169
|
|
|
* @var array|null |
170
|
|
|
* @see checkTranslatedShortcut() |
171
|
|
|
*/ |
172
|
|
|
protected $originalShortcutPage; |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* sys_page-object, pagefunctions |
176
|
|
|
* |
177
|
|
|
* @var PageRepository|string |
178
|
|
|
*/ |
179
|
|
|
public $sys_page = ''; |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Is set to 1 if a pageNotFound handler could have been called. |
183
|
|
|
* @var int |
184
|
|
|
* @internal |
185
|
|
|
*/ |
186
|
|
|
public $pageNotFound = 0; |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Array containing a history of why a requested page was not accessible. |
190
|
|
|
* @var array |
191
|
|
|
*/ |
192
|
|
|
protected $pageAccessFailureHistory = []; |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* @var string |
196
|
|
|
* @internal |
197
|
|
|
*/ |
198
|
|
|
public $MP = ''; |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* The frontend user |
202
|
|
|
* |
203
|
|
|
* @var FrontendUserAuthentication |
204
|
|
|
*/ |
205
|
|
|
public $fe_user; |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Shows whether logins are allowed in branch |
209
|
|
|
* @var bool |
210
|
|
|
*/ |
211
|
|
|
protected $loginAllowedInBranch = true; |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Shows specific mode (all or groups) |
215
|
|
|
* @var string |
216
|
|
|
* @internal |
217
|
|
|
*/ |
218
|
|
|
protected $loginAllowedInBranch_mode = ''; |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Value that contains the simulated usergroup if any |
222
|
|
|
* @var int |
223
|
|
|
* @internal only to be used in AdminPanel, and within TYPO3 Core |
224
|
|
|
*/ |
225
|
|
|
public $simUserGroup = 0; |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* "CONFIG" object from TypoScript. Array generated based on the TypoScript |
229
|
|
|
* configuration of the current page. Saved with the cached pages. |
230
|
|
|
* @var array |
231
|
|
|
*/ |
232
|
|
|
public $config = []; |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* The TypoScript template object. Used to parse the TypoScript template |
236
|
|
|
* |
237
|
|
|
* @var TemplateService |
238
|
|
|
*/ |
239
|
|
|
public $tmpl; |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Is set to the time-to-live time of cached pages. Default is 60*60*24, which is 24 hours. |
243
|
|
|
* |
244
|
|
|
* @var int |
245
|
|
|
* @internal |
246
|
|
|
*/ |
247
|
|
|
protected $cacheTimeOutDefault = 86400; |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Set internally if cached content is fetched from the database. |
251
|
|
|
* |
252
|
|
|
* @var bool |
253
|
|
|
* @internal |
254
|
|
|
*/ |
255
|
|
|
protected $cacheContentFlag = false; |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* Set to the expire time of cached content |
259
|
|
|
* @var int |
260
|
|
|
* @internal |
261
|
|
|
*/ |
262
|
|
|
protected $cacheExpires = 0; |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* Set if cache headers allowing caching are sent. |
266
|
|
|
* @var bool |
267
|
|
|
* @internal |
268
|
|
|
*/ |
269
|
|
|
protected $isClientCachable = false; |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* Used by template fetching system. This array is an identification of |
273
|
|
|
* the template. If $this->all is empty it's because the template-data is not |
274
|
|
|
* cached, which it must be. |
275
|
|
|
* @var array |
276
|
|
|
* @internal |
277
|
|
|
*/ |
278
|
|
|
public $all = []; |
279
|
|
|
|
280
|
|
|
/** |
281
|
|
|
* Toplevel - objArrayName, eg 'page' |
282
|
|
|
* @var string |
283
|
|
|
* @internal should only be used by TYPO3 Core |
284
|
|
|
*/ |
285
|
|
|
public $sPre = ''; |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* TypoScript configuration of the page-object pointed to by sPre. |
289
|
|
|
* $this->tmpl->setup[$this->sPre.'.'] |
290
|
|
|
* @var array|string |
291
|
|
|
* @internal should only be used by TYPO3 Core |
292
|
|
|
*/ |
293
|
|
|
public $pSetup = ''; |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* This hash is unique to the template, the $this->id and $this->type vars and |
297
|
|
|
* the list of groups. Used to get and later store the cached data |
298
|
|
|
* @var string |
299
|
|
|
* @internal |
300
|
|
|
*/ |
301
|
|
|
public $newHash = ''; |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* This flag is set before the page is generated IF $this->no_cache is set. If this |
305
|
|
|
* flag is set after the page content was generated, $this->no_cache is forced to be set. |
306
|
|
|
* This is done in order to make sure that PHP code from Plugins / USER scripts does not falsely |
307
|
|
|
* clear the no_cache flag. |
308
|
|
|
* @var bool |
309
|
|
|
* @internal |
310
|
|
|
*/ |
311
|
|
|
protected $no_cacheBeforePageGen = false; |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* May be set to the pagesTSconfig |
315
|
|
|
* @var array|string |
316
|
|
|
* @internal |
317
|
|
|
*/ |
318
|
|
|
protected $pagesTSconfig = ''; |
319
|
|
|
|
320
|
|
|
/** |
321
|
|
|
* Eg. insert JS-functions in this array ($additionalHeaderData) to include them |
322
|
|
|
* once. Use associative keys. |
323
|
|
|
* |
324
|
|
|
* Keys in use: |
325
|
|
|
* |
326
|
|
|
* used to accumulate additional HTML-code for the header-section, |
327
|
|
|
* <head>...</head>. Insert either associative keys (like |
328
|
|
|
* additionalHeaderData['myStyleSheet'], see reserved keys above) or num-keys |
329
|
|
|
* (like additionalHeaderData[] = '...') |
330
|
|
|
* |
331
|
|
|
* @var array |
332
|
|
|
*/ |
333
|
|
|
public $additionalHeaderData = []; |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* Used to accumulate additional HTML-code for the footer-section of the template |
337
|
|
|
* @var array |
338
|
|
|
*/ |
339
|
|
|
public $additionalFooterData = []; |
340
|
|
|
|
341
|
|
|
/** |
342
|
|
|
* Default internal target |
343
|
|
|
* @var string |
344
|
|
|
*/ |
345
|
|
|
public $intTarget = ''; |
346
|
|
|
|
347
|
|
|
/** |
348
|
|
|
* Default external target |
349
|
|
|
* @var string |
350
|
|
|
*/ |
351
|
|
|
public $extTarget = ''; |
352
|
|
|
|
353
|
|
|
/** |
354
|
|
|
* Default file link target |
355
|
|
|
* @var string |
356
|
|
|
*/ |
357
|
|
|
public $fileTarget = ''; |
358
|
|
|
|
359
|
|
|
/** |
360
|
|
|
* If set, typolink() function encrypts email addresses. |
361
|
|
|
* @var string|int |
362
|
|
|
*/ |
363
|
|
|
public $spamProtectEmailAddresses = 0; |
364
|
|
|
|
365
|
|
|
/** |
366
|
|
|
* Absolute Reference prefix |
367
|
|
|
* @var string |
368
|
|
|
*/ |
369
|
|
|
public $absRefPrefix = ''; |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* <A>-tag parameters |
373
|
|
|
* @var string |
374
|
|
|
*/ |
375
|
|
|
public $ATagParams = ''; |
376
|
|
|
|
377
|
|
|
/** |
378
|
|
|
* Search word regex, calculated if there has been search-words send. This is |
379
|
|
|
* used to mark up the found search words on a page when jumped to from a link |
380
|
|
|
* in a search-result. |
381
|
|
|
* @var string |
382
|
|
|
* @internal |
383
|
|
|
*/ |
384
|
|
|
public $sWordRegEx = ''; |
385
|
|
|
|
386
|
|
|
/** |
387
|
|
|
* Is set to the incoming array sword_list in case of a page-view jumped to from |
388
|
|
|
* a search-result. |
389
|
|
|
* @var string |
390
|
|
|
* @internal |
391
|
|
|
*/ |
392
|
|
|
public $sWordList = ''; |
393
|
|
|
|
394
|
|
|
/** |
395
|
|
|
* A string prepared for insertion in all links on the page as url-parameters. |
396
|
|
|
* Based on configuration in TypoScript where you defined which GET_VARS you |
397
|
|
|
* would like to pass on. |
398
|
|
|
* @var string |
399
|
|
|
*/ |
400
|
|
|
public $linkVars = ''; |
401
|
|
|
|
402
|
|
|
/** |
403
|
|
|
* If set, edit icons are rendered aside content records. Must be set only if |
404
|
|
|
* the ->beUserLogin flag is set and set_no_cache() must be called as well. |
405
|
|
|
* @var string |
406
|
|
|
*/ |
407
|
|
|
public $displayEditIcons = ''; |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* If set, edit icons are rendered aside individual fields of content. Must be |
411
|
|
|
* set only if the ->beUserLogin flag is set and set_no_cache() must be called as |
412
|
|
|
* well. |
413
|
|
|
* @var string |
414
|
|
|
*/ |
415
|
|
|
public $displayFieldEditIcons = ''; |
416
|
|
|
|
417
|
|
|
/** |
418
|
|
|
* 'Global' Storage for various applications. Keys should be 'tx_'.extKey for |
419
|
|
|
* extensions. |
420
|
|
|
* @var array |
421
|
|
|
*/ |
422
|
|
|
public $applicationData = []; |
423
|
|
|
|
424
|
|
|
/** |
425
|
|
|
* @var array |
426
|
|
|
*/ |
427
|
|
|
public $register = []; |
428
|
|
|
|
429
|
|
|
/** |
430
|
|
|
* Stack used for storing array and retrieving register arrays (see |
431
|
|
|
* LOAD_REGISTER and RESTORE_REGISTER) |
432
|
|
|
* @var array |
433
|
|
|
*/ |
434
|
|
|
public $registerStack = []; |
435
|
|
|
|
436
|
|
|
/** |
437
|
|
|
* Checking that the function is not called eternally. This is done by |
438
|
|
|
* interrupting at a depth of 50 |
439
|
|
|
* @var int |
440
|
|
|
*/ |
441
|
|
|
public $cObjectDepthCounter = 50; |
442
|
|
|
|
443
|
|
|
/** |
444
|
|
|
* Used by RecordContentObject and ContentContentObject to ensure the a records is NOT |
445
|
|
|
* rendered twice through it! |
446
|
|
|
* @var array |
447
|
|
|
*/ |
448
|
|
|
public $recordRegister = []; |
449
|
|
|
|
450
|
|
|
/** |
451
|
|
|
* This is set to the [table]:[uid] of the latest record rendered. Note that |
452
|
|
|
* class ContentObjectRenderer has an equal value, but that is pointing to the |
453
|
|
|
* record delivered in the $data-array of the ContentObjectRenderer instance, if |
454
|
|
|
* the cObjects CONTENT or RECORD created that instance |
455
|
|
|
* @var string |
456
|
|
|
*/ |
457
|
|
|
public $currentRecord = ''; |
458
|
|
|
|
459
|
|
|
/** |
460
|
|
|
* Used by class \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject |
461
|
|
|
* to keep track of access-keys. |
462
|
|
|
* @var array |
463
|
|
|
*/ |
464
|
|
|
public $accessKey = []; |
465
|
|
|
|
466
|
|
|
/** |
467
|
|
|
* Used to generate page-unique keys. Point is that uniqid() functions is very |
468
|
|
|
* slow, so a unikey key is made based on this, see function uniqueHash() |
469
|
|
|
* @var int |
470
|
|
|
* @internal |
471
|
|
|
*/ |
472
|
|
|
protected $uniqueCounter = 0; |
473
|
|
|
|
474
|
|
|
/** |
475
|
|
|
* @var string |
476
|
|
|
* @internal |
477
|
|
|
*/ |
478
|
|
|
protected $uniqueString = ''; |
479
|
|
|
|
480
|
|
|
/** |
481
|
|
|
* This value will be used as the title for the page in the indexer (if |
482
|
|
|
* indexing happens) |
483
|
|
|
* @var string |
484
|
|
|
* @internal only used by TYPO3 Core, use PageTitle API instead. |
485
|
|
|
*/ |
486
|
|
|
public $indexedDocTitle = ''; |
487
|
|
|
|
488
|
|
|
/** |
489
|
|
|
* The base URL set for the page header. |
490
|
|
|
* @var string |
491
|
|
|
*/ |
492
|
|
|
public $baseUrl = ''; |
493
|
|
|
|
494
|
|
|
/** |
495
|
|
|
* Page content render object |
496
|
|
|
* |
497
|
|
|
* @var ContentObjectRenderer |
498
|
|
|
*/ |
499
|
|
|
public $cObj; |
500
|
|
|
|
501
|
|
|
/** |
502
|
|
|
* All page content is accumulated in this variable. See RequestHandler |
503
|
|
|
* @var string |
504
|
|
|
*/ |
505
|
|
|
public $content = ''; |
506
|
|
|
|
507
|
|
|
/** |
508
|
|
|
* Output charset of the websites content. This is the charset found in the |
509
|
|
|
* header, meta tag etc. If different than utf-8 a conversion |
510
|
|
|
* happens before output to browser. Defaults to utf-8. |
511
|
|
|
* @var string |
512
|
|
|
*/ |
513
|
|
|
public $metaCharset = 'utf-8'; |
514
|
|
|
|
515
|
|
|
/** |
516
|
|
|
* Internal calculations for labels |
517
|
|
|
* |
518
|
|
|
* @var LanguageService |
519
|
|
|
*/ |
520
|
|
|
protected $languageService; |
521
|
|
|
|
522
|
|
|
/** |
523
|
|
|
* @var LockingStrategyInterface[][] |
524
|
|
|
*/ |
525
|
|
|
protected $locks = []; |
526
|
|
|
|
527
|
|
|
/** |
528
|
|
|
* @var PageRenderer |
529
|
|
|
*/ |
530
|
|
|
protected $pageRenderer; |
531
|
|
|
|
532
|
|
|
/** |
533
|
|
|
* The page cache object, use this to save pages to the cache and to |
534
|
|
|
* retrieve them again |
535
|
|
|
* |
536
|
|
|
* @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface |
537
|
|
|
*/ |
538
|
|
|
protected $pageCache; |
539
|
|
|
|
540
|
|
|
/** |
541
|
|
|
* @var array |
542
|
|
|
*/ |
543
|
|
|
protected $pageCacheTags = []; |
544
|
|
|
|
545
|
|
|
/** |
546
|
|
|
* Content type HTTP header being sent in the request. |
547
|
|
|
* @todo Ticket: #63642 Should be refactored to a request/response model later |
548
|
|
|
* @internal Should only be used by TYPO3 core for now |
549
|
|
|
* |
550
|
|
|
* @var string |
551
|
|
|
*/ |
552
|
|
|
protected $contentType = 'text/html'; |
553
|
|
|
|
554
|
|
|
/** |
555
|
|
|
* Doctype to use |
556
|
|
|
* |
557
|
|
|
* @var string |
558
|
|
|
*/ |
559
|
|
|
public $xhtmlDoctype = ''; |
560
|
|
|
|
561
|
|
|
/** |
562
|
|
|
* @var int |
563
|
|
|
*/ |
564
|
|
|
public $xhtmlVersion; |
565
|
|
|
|
566
|
|
|
/** |
567
|
|
|
* Originally requested id from the initial $_GET variable |
568
|
|
|
* |
569
|
|
|
* @var int |
570
|
|
|
*/ |
571
|
|
|
protected $requestedId; |
572
|
|
|
|
573
|
|
|
/** |
574
|
|
|
* The context for keeping the current state, mostly related to current page information, |
575
|
|
|
* backend user / frontend user access, workspaceId |
576
|
|
|
* |
577
|
|
|
* @var Context |
578
|
|
|
*/ |
579
|
|
|
protected $context; |
580
|
|
|
|
581
|
|
|
/** |
582
|
|
|
* Since TYPO3 v10.0, TSFE is composed out of |
583
|
|
|
* - Context |
584
|
|
|
* - Site |
585
|
|
|
* - SiteLanguage |
586
|
|
|
* - PageArguments (containing ID, Type, cHash and MP arguments) |
587
|
|
|
* |
588
|
|
|
* Also sets a unique string (->uniqueString) for this script instance; A md5 hash of the microtime() |
589
|
|
|
* |
590
|
|
|
* @param Context $context the Context object to work with |
591
|
|
|
* @param SiteInterface $site The resolved site to work with |
592
|
|
|
* @param SiteLanguage $siteLanguage The resolved language to work with |
593
|
|
|
* @param PageArguments $pageArguments The PageArguments object containing Page ID, type and GET parameters |
594
|
|
|
* @param FrontendUserAuthentication $frontendUser a FrontendUserAuthentication object |
595
|
|
|
*/ |
596
|
|
|
public function __construct(Context $context, SiteInterface $site, SiteLanguage $siteLanguage, PageArguments $pageArguments, FrontendUserAuthentication $frontendUser) |
597
|
|
|
{ |
598
|
|
|
$this->initializeContext($context); |
599
|
|
|
$this->site = $site; |
600
|
|
|
$this->language = $siteLanguage; |
601
|
|
|
$this->setPageArguments($pageArguments); |
602
|
|
|
$this->fe_user = $frontendUser; |
603
|
|
|
$this->uniqueString = md5(microtime()); |
604
|
|
|
$this->initPageRenderer(); |
605
|
|
|
$this->initCaches(); |
606
|
|
|
// Initialize LLL behaviour |
607
|
|
|
$this->setOutputLanguage(); |
608
|
|
|
} |
609
|
|
|
|
610
|
|
|
private function initializeContext(Context $context): void |
611
|
|
|
{ |
612
|
|
|
$this->context = $context; |
613
|
|
|
if (!$this->context->hasAspect('frontend.preview')) { |
614
|
|
|
$this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class)); |
615
|
|
|
} |
616
|
|
|
} |
617
|
|
|
|
618
|
|
|
/** |
619
|
|
|
* Initializes the page renderer object |
620
|
|
|
*/ |
621
|
|
|
protected function initPageRenderer() |
622
|
|
|
{ |
623
|
|
|
if ($this->pageRenderer !== null) { |
624
|
|
|
return; |
625
|
|
|
} |
626
|
|
|
$this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); |
627
|
|
|
$this->pageRenderer->setTemplateFile('EXT:frontend/Resources/Private/Templates/MainPage.html'); |
628
|
|
|
// As initPageRenderer could be called in constructor and for USER_INTs, this information is only set |
629
|
|
|
// once - in order to not override any previous settings of PageRenderer. |
630
|
|
|
if ($this->language instanceof SiteLanguage) { |
|
|
|
|
631
|
|
|
$this->pageRenderer->setLanguage($this->language->getTypo3Language()); |
632
|
|
|
} |
633
|
|
|
} |
634
|
|
|
|
635
|
|
|
/** |
636
|
|
|
* @param string $contentType |
637
|
|
|
* @internal Should only be used by TYPO3 core for now |
638
|
|
|
*/ |
639
|
|
|
public function setContentType($contentType) |
640
|
|
|
{ |
641
|
|
|
$this->contentType = $contentType; |
642
|
|
|
} |
643
|
|
|
|
644
|
|
|
/******************************************** |
645
|
|
|
* |
646
|
|
|
* Initializing, resolving page id |
647
|
|
|
* |
648
|
|
|
********************************************/ |
649
|
|
|
/** |
650
|
|
|
* Initializes the caching system. |
651
|
|
|
*/ |
652
|
|
|
protected function initCaches() |
653
|
|
|
{ |
654
|
|
|
$this->pageCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('pages'); |
655
|
|
|
} |
656
|
|
|
|
657
|
|
|
/** |
658
|
|
|
* Initializes the front-end user groups. |
659
|
|
|
* Sets frontend.user aspect based on front-end user status. |
660
|
|
|
*/ |
661
|
|
|
public function initUserGroups() |
662
|
|
|
{ |
663
|
|
|
$userAspect = $this->fe_user->createUserAspect((bool)$this->loginAllowedInBranch); |
664
|
|
|
$this->context->setAspect('frontend.user', $userAspect); |
665
|
|
|
} |
666
|
|
|
|
667
|
|
|
/** |
668
|
|
|
* Checking if a user is logged in or a group constellation different from "0,-1" |
669
|
|
|
* |
670
|
|
|
* @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!) |
671
|
|
|
*/ |
672
|
|
|
public function isUserOrGroupSet() |
673
|
|
|
{ |
674
|
|
|
/** @var UserAspect $userAspect */ |
675
|
|
|
$userAspect = $this->context->getAspect('frontend.user'); |
676
|
|
|
return $userAspect->isUserOrGroupSet(); |
677
|
|
|
} |
678
|
|
|
|
679
|
|
|
/** |
680
|
|
|
* Clears the preview-flags, sets sim_exec_time to current time. |
681
|
|
|
* Hidden pages must be hidden as default, $GLOBALS['SIM_EXEC_TIME'] is set to $GLOBALS['EXEC_TIME'] |
682
|
|
|
* in bootstrap initializeGlobalTimeVariables(). Alter it by adding or subtracting seconds. |
683
|
|
|
*/ |
684
|
|
|
public function clear_preview() |
685
|
|
|
{ |
686
|
|
|
if ($this->isInPreviewMode()) { |
687
|
|
|
$GLOBALS['SIM_EXEC_TIME'] = $GLOBALS['EXEC_TIME']; |
688
|
|
|
$GLOBALS['SIM_ACCESS_TIME'] = $GLOBALS['ACCESS_TIME']; |
689
|
|
|
$this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class)); |
690
|
|
|
$this->context->setAspect('date', GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $GLOBALS['SIM_EXEC_TIME']))); |
691
|
|
|
$this->context->setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class)); |
692
|
|
|
} |
693
|
|
|
} |
694
|
|
|
|
695
|
|
|
/** |
696
|
|
|
* Checks if a backend user is logged in |
697
|
|
|
* |
698
|
|
|
* @return bool whether a backend user is logged in |
699
|
|
|
*/ |
700
|
|
|
public function isBackendUserLoggedIn() |
701
|
|
|
{ |
702
|
|
|
return (bool)$this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false); |
703
|
|
|
} |
704
|
|
|
|
705
|
|
|
/** |
706
|
|
|
* Determines the id and evaluates any preview settings |
707
|
|
|
* Basically this function is about determining whether a backend user is logged in, |
708
|
|
|
* if he has read access to the page and if he's previewing the page. |
709
|
|
|
* That all determines which id to show and how to initialize the id. |
710
|
|
|
* |
711
|
|
|
* @param ServerRequestInterface|null $request |
712
|
|
|
*/ |
713
|
|
|
public function determineId(ServerRequestInterface $request = null) |
714
|
|
|
{ |
715
|
|
|
$request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); |
716
|
|
|
// Call pre processing function for id determination |
717
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PreProcessing'] ?? [] as $functionReference) { |
718
|
|
|
$parameters = ['parentObject' => $this]; |
719
|
|
|
GeneralUtility::callUserFunction($functionReference, $parameters, $this); |
720
|
|
|
} |
721
|
|
|
// If there is a Backend login we are going to check for any preview settings |
722
|
|
|
$originalFrontendUserGroups = $this->applyPreviewSettings($this->getBackendUser()); |
|
|
|
|
723
|
|
|
// If the front-end is showing a preview, caching MUST be disabled. |
724
|
|
|
$isPreview = $this->isInPreviewMode(); |
725
|
|
|
if ($isPreview) { |
726
|
|
|
$this->disableCache(); |
727
|
|
|
} |
728
|
|
|
// Now, get the id, validate access etc: |
729
|
|
|
$this->fetch_the_id($request); |
730
|
|
|
// Check if backend user has read access to this page. If not, recalculate the id. |
731
|
|
|
if ($this->isBackendUserLoggedIn() && $isPreview && !$this->getBackendUser()->doesUserHaveAccess($this->page, Permission::PAGE_SHOW)) { |
732
|
|
|
$this->unsetBackendUser(); |
733
|
|
|
// Resetting |
734
|
|
|
$this->clear_preview(); |
735
|
|
|
$this->fe_user->user[$this->fe_user->usergroup_column] = $originalFrontendUserGroups; |
736
|
|
|
// Fetching the id again, now with the preview settings reset. |
737
|
|
|
$this->fetch_the_id($request); |
738
|
|
|
} |
739
|
|
|
// Checks if user logins are blocked for a certain branch and if so, will unset user login and re-fetch ID. |
740
|
|
|
$this->loginAllowedInBranch = $this->checkIfLoginAllowedInBranch(); |
741
|
|
|
// Logins are not allowed, but there is a login, so will we run this. |
742
|
|
|
if (!$this->loginAllowedInBranch && $this->isUserOrGroupSet()) { |
743
|
|
|
// Clear out user, and the group will be re-set in >initUserGroups() due to |
744
|
|
|
// $this->loginAllowedInBranch = false |
745
|
|
|
if ($this->loginAllowedInBranch_mode === 'all') { |
746
|
|
|
$this->fe_user->hideActiveLogin(); |
747
|
|
|
} |
748
|
|
|
// Fetching the id again, now with the preview settings reset and respecting $this->loginAllowedInBranch = false |
749
|
|
|
$this->fetch_the_id($request); |
750
|
|
|
} |
751
|
|
|
// Final cleaning. |
752
|
|
|
// Make sure it's an integer |
753
|
|
|
$this->id = ($this->contentPid = (int)$this->id); |
754
|
|
|
// Make sure it's an integer |
755
|
|
|
$this->type = (int)$this->type; |
756
|
|
|
// Setting language and fetch translated page |
757
|
|
|
$this->settingLanguage($request); |
758
|
|
|
// Call post processing function for id determination: |
759
|
|
|
$_params = ['pObj' => &$this]; |
760
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc'] ?? [] as $_funcRef) { |
761
|
|
|
GeneralUtility::callUserFunction($_funcRef, $_params, $this); |
762
|
|
|
} |
763
|
|
|
} |
764
|
|
|
|
765
|
|
|
protected function unsetBackendUser(): void |
766
|
|
|
{ |
767
|
|
|
// Register an empty backend user as aspect |
768
|
|
|
unset($GLOBALS['BE_USER']); |
769
|
|
|
$this->context->setAspect('backend.user', GeneralUtility::makeInstance(UserAspect::class)); |
770
|
|
|
$this->context->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class)); |
771
|
|
|
} |
772
|
|
|
|
773
|
|
|
/** |
774
|
|
|
* Evaluates admin panel or workspace settings to see if |
775
|
|
|
* visibility settings like |
776
|
|
|
* - Preview Aspect: isPreview |
777
|
|
|
* - Visibility Aspect: includeHiddenPages |
778
|
|
|
* - Visibility Aspect: includeHiddenContent |
779
|
|
|
* - $simUserGroup |
780
|
|
|
* should be applied to the current object. |
781
|
|
|
* |
782
|
|
|
* @param FrontendBackendUserAuthentication $backendUser |
783
|
|
|
* @return string|null null if no changes to the current frontend usergroups have been made, otherwise the original list of frontend usergroups |
784
|
|
|
* @internal |
785
|
|
|
*/ |
786
|
|
|
protected function applyPreviewSettings($backendUser = null) |
787
|
|
|
{ |
788
|
|
|
if (!$backendUser) { |
789
|
|
|
return null; |
790
|
|
|
} |
791
|
|
|
$originalFrontendUserGroup = null; |
792
|
|
|
if ($this->fe_user->user) { |
793
|
|
|
$originalFrontendUserGroup = $this->context->getPropertyFromAspect('frontend.user', 'groupIds'); |
794
|
|
|
} |
795
|
|
|
|
796
|
|
|
// The preview flag is set if the current page turns out to be hidden |
797
|
|
|
if ($this->id && $this->determineIdIsHiddenPage()) { |
798
|
|
|
$this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, true)); |
799
|
|
|
/** @var VisibilityAspect $aspect */ |
800
|
|
|
$aspect = $this->context->getAspect('visibility'); |
801
|
|
|
$newAspect = GeneralUtility::makeInstance(VisibilityAspect::class, true, $aspect->includeHiddenContent(), $aspect->includeDeletedRecords()); |
802
|
|
|
$this->context->setAspect('visibility', $newAspect); |
803
|
|
|
} |
804
|
|
|
// The preview flag will be set if an offline workspace will be previewed |
805
|
|
|
if ($this->whichWorkspace() > 0) { |
806
|
|
|
$this->context->setAspect('frontend.preview', GeneralUtility::makeInstance(PreviewAspect::class, true)); |
807
|
|
|
} |
808
|
|
|
return $this->context->getPropertyFromAspect('frontend.preview', 'preview', false) ? $originalFrontendUserGroup : null; |
809
|
|
|
} |
810
|
|
|
|
811
|
|
|
/** |
812
|
|
|
* Checks if the page is hidden in the active workspace. |
813
|
|
|
* If it is hidden, preview flags will be set. |
814
|
|
|
* |
815
|
|
|
* @return bool |
816
|
|
|
*/ |
817
|
|
|
protected function determineIdIsHiddenPage() |
818
|
|
|
{ |
819
|
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) |
820
|
|
|
->getQueryBuilderForTable('pages'); |
821
|
|
|
$queryBuilder |
822
|
|
|
->getRestrictions() |
823
|
|
|
->removeAll() |
824
|
|
|
->add(GeneralUtility::makeInstance(DeletedRestriction::class)); |
825
|
|
|
|
826
|
|
|
$queryBuilder |
827
|
|
|
->select('uid', 'hidden', 'starttime', 'endtime') |
828
|
|
|
->from('pages') |
829
|
|
|
->where( |
830
|
|
|
$queryBuilder->expr()->gte('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)) |
831
|
|
|
) |
832
|
|
|
->setMaxResults(1); |
833
|
|
|
|
834
|
|
|
// $this->id always points to the ID of the default language page, so we check |
835
|
|
|
// the current site language to determine if we need to fetch a translation but consider fallbacks |
836
|
|
|
if ($this->language->getLanguageId() > 0) { |
837
|
|
|
$languagesToCheck = array_merge([$this->language->getLanguageId()], $this->language->getFallbackLanguageIds()); |
838
|
|
|
// Check for the language and all its fallbacks |
839
|
|
|
$constraint = $queryBuilder->expr()->andX( |
840
|
|
|
$queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)), |
841
|
|
|
$queryBuilder->expr()->in('sys_language_uid', $queryBuilder->createNamedParameter(array_filter($languagesToCheck), Connection::PARAM_INT_ARRAY)) |
842
|
|
|
); |
843
|
|
|
// If the fallback language Ids also contains the default language, this needs to be considered |
844
|
|
|
if (in_array(0, $languagesToCheck, true)) { |
845
|
|
|
$constraint = $queryBuilder->expr()->orX( |
846
|
|
|
$constraint, |
847
|
|
|
// Ensure to also fetch the default record |
848
|
|
|
$queryBuilder->expr()->andX( |
849
|
|
|
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)), |
850
|
|
|
$queryBuilder->expr()->in('sys_language_uid', 0) |
851
|
|
|
) |
852
|
|
|
); |
853
|
|
|
} |
854
|
|
|
// Ensure that the translated records are shown first (maxResults is set to 1) |
855
|
|
|
$queryBuilder->orderBy('sys_language_uid', 'DESC'); |
856
|
|
|
} else { |
857
|
|
|
$constraint = $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)); |
858
|
|
|
} |
859
|
|
|
$queryBuilder->andWhere($constraint); |
860
|
|
|
|
861
|
|
|
$page = $queryBuilder->execute()->fetch(); |
862
|
|
|
|
863
|
|
|
if ($this->whichWorkspace() > 0) { |
864
|
|
|
// Fetch overlay of page if in workspace and check if it is hidden |
865
|
|
|
$customContext = clone $this->context; |
866
|
|
|
$customContext->setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $this->whichWorkspace())); |
867
|
|
|
$customContext->setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class)); |
868
|
|
|
$pageSelectObject = GeneralUtility::makeInstance(PageRepository::class, $customContext); |
869
|
|
|
$targetPage = $pageSelectObject->getWorkspaceVersionOfRecord($this->whichWorkspace(), 'pages', $page['uid']); |
870
|
|
|
// Also checks if the workspace version is NOT hidden but the live version is in fact still hidden |
871
|
|
|
$result = $targetPage === -1 || $targetPage === -2 || (is_array($targetPage) && $targetPage['hidden'] == 0 && $page['hidden'] == 1); |
872
|
|
|
} else { |
873
|
|
|
$result = is_array($page) && ($page['hidden'] || $page['starttime'] > $GLOBALS['SIM_EXEC_TIME'] || $page['endtime'] != 0 && $page['endtime'] <= $GLOBALS['SIM_EXEC_TIME']); |
874
|
|
|
} |
875
|
|
|
return $result; |
876
|
|
|
} |
877
|
|
|
|
878
|
|
|
/** |
879
|
|
|
* Resolves the page id and sets up several related properties. |
880
|
|
|
* |
881
|
|
|
* If $this->id is not set at all or is not a plain integer, the method |
882
|
|
|
* does it's best to set the value to an integer. Resolving is based on |
883
|
|
|
* this options: |
884
|
|
|
* |
885
|
|
|
* - Splitting $this->id if it contains an additional type parameter. |
886
|
|
|
* - Finding the domain record start page |
887
|
|
|
* - First visible page |
888
|
|
|
* - Relocating the id below the domain record if outside |
889
|
|
|
* |
890
|
|
|
* The following properties may be set up or updated: |
891
|
|
|
* |
892
|
|
|
* - id |
893
|
|
|
* - requestedId |
894
|
|
|
* - type |
895
|
|
|
* - sys_page |
896
|
|
|
* - sys_page->where_groupAccess |
897
|
|
|
* - sys_page->where_hid_del |
898
|
|
|
* - Context: FrontendUser Aspect |
899
|
|
|
* - no_cache |
900
|
|
|
* - register['SYS_LASTCHANGED'] |
901
|
|
|
* - pageNotFound |
902
|
|
|
* |
903
|
|
|
* Via getPageAndRootlineWithDomain() |
904
|
|
|
* |
905
|
|
|
* - rootLine |
906
|
|
|
* - page |
907
|
|
|
* - MP |
908
|
|
|
* - originalShortcutPage |
909
|
|
|
* - originalMountPointPage |
910
|
|
|
* - pageAccessFailureHistory['direct_access'] |
911
|
|
|
* - pageNotFound |
912
|
|
|
* |
913
|
|
|
* @todo: |
914
|
|
|
* |
915
|
|
|
* On the first impression the method does to much. This is increased by |
916
|
|
|
* the fact, that is is called repeated times by the method determineId. |
917
|
|
|
* The reasons are manifold. |
918
|
|
|
* |
919
|
|
|
* 1.) The first part, the creation of sys_page and the type |
920
|
|
|
* resolution don't need to be repeated. They could be separated to be |
921
|
|
|
* called only once. |
922
|
|
|
* |
923
|
|
|
* 2.) The user group setup could be done once on a higher level. |
924
|
|
|
* |
925
|
|
|
* 3.) The workflow of the resolution could be elaborated to be less |
926
|
|
|
* tangled. Maybe the check of the page id to be below the domain via the |
927
|
|
|
* root line doesn't need to be done each time, but for the final result |
928
|
|
|
* only. |
929
|
|
|
* |
930
|
|
|
* 4.) The root line does not need to be directly addressed by this class. |
931
|
|
|
* A root line is always related to one page. The rootline could be handled |
932
|
|
|
* indirectly by page objects. Page objects still don't exist. |
933
|
|
|
* |
934
|
|
|
* @internal |
935
|
|
|
* @param ServerRequestInterface|null $request |
936
|
|
|
*/ |
937
|
|
|
public function fetch_the_id(ServerRequestInterface $request = null) |
938
|
|
|
{ |
939
|
|
|
$request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); |
940
|
|
|
$timeTracker = $this->getTimeTracker(); |
941
|
|
|
$timeTracker->push('fetch_the_id initialize/'); |
942
|
|
|
// Set the valid usergroups for FE |
943
|
|
|
$this->initUserGroups(); |
944
|
|
|
// Initialize the PageRepository has to be done after the frontend usergroups are initialized / resolved, as |
945
|
|
|
// frontend group aspect is modified before |
946
|
|
|
$this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context); |
947
|
|
|
// The id and type is set to the integer-value - just to be sure... |
948
|
|
|
$this->id = (int)$this->id; |
949
|
|
|
$this->type = (int)$this->type; |
950
|
|
|
$timeTracker->pull(); |
951
|
|
|
// We find the first page belonging to the current domain |
952
|
|
|
$timeTracker->push('fetch_the_id domain/'); |
953
|
|
|
if (!$this->id) { |
954
|
|
|
// If the id was not previously set, set it to the root page id of the site. |
955
|
|
|
$this->id = $this->site->getRootPageId(); |
956
|
|
|
} |
957
|
|
|
$timeTracker->pull(); |
958
|
|
|
$timeTracker->push('fetch_the_id rootLine/'); |
959
|
|
|
// We store the originally requested id |
960
|
|
|
$this->requestedId = $this->id; |
961
|
|
|
try { |
962
|
|
|
$this->getPageAndRootlineWithDomain($this->site->getRootPageId(), $request); |
963
|
|
|
} catch (ShortcutTargetPageNotFoundException $e) { |
964
|
|
|
$this->pageNotFound = 1; |
965
|
|
|
} |
966
|
|
|
$timeTracker->pull(); |
967
|
|
|
if ($this->pageNotFound) { |
968
|
|
|
switch ($this->pageNotFound) { |
969
|
|
|
case 1: |
970
|
|
|
$response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction( |
971
|
|
|
$request, |
972
|
|
|
'ID was not an accessible page', |
973
|
|
|
$this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED) |
974
|
|
|
); |
975
|
|
|
break; |
976
|
|
|
case 2: |
977
|
|
|
$response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction( |
978
|
|
|
$request, |
979
|
|
|
'Subsection was found and not accessible', |
980
|
|
|
$this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED) |
981
|
|
|
); |
982
|
|
|
break; |
983
|
|
|
case 3: |
984
|
|
|
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( |
985
|
|
|
$request, |
986
|
|
|
'ID was outside the domain', |
987
|
|
|
$this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH) |
988
|
|
|
); |
989
|
|
|
break; |
990
|
|
|
default: |
991
|
|
|
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( |
992
|
|
|
$request, |
993
|
|
|
'Unspecified error', |
994
|
|
|
$this->getPageAccessFailureReasons() |
995
|
|
|
); |
996
|
|
|
} |
997
|
|
|
throw new PropagateResponseException($response, 1533931329); |
998
|
|
|
} |
999
|
|
|
|
1000
|
|
|
$this->setRegisterValueForSysLastChanged($this->page); |
1001
|
|
|
|
1002
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['fetchPageId-PostProcessing'] ?? [] as $functionReference) { |
1003
|
|
|
$parameters = ['parentObject' => $this]; |
1004
|
|
|
GeneralUtility::callUserFunction($functionReference, $parameters, $this); |
1005
|
|
|
} |
1006
|
|
|
} |
1007
|
|
|
|
1008
|
|
|
/** |
1009
|
|
|
* Loads the page and root line records based on $this->id |
1010
|
|
|
* |
1011
|
|
|
* A final page and the matching root line are determined and loaded by |
1012
|
|
|
* the algorithm defined by this method. |
1013
|
|
|
* |
1014
|
|
|
* First it loads the initial page from the page repository for $this->id. |
1015
|
|
|
* If that can't be loaded directly, it gets the root line for $this->id. |
1016
|
|
|
* It walks up the root line towards the root page until the page |
1017
|
|
|
* repository can deliver a page record. (The loading restrictions of |
1018
|
|
|
* the root line records are more liberal than that of the page record.) |
1019
|
|
|
* |
1020
|
|
|
* Now the page type is evaluated and handled if necessary. If the page is |
1021
|
|
|
* a short cut, it is replaced by the target page. If the page is a mount |
1022
|
|
|
* point in overlay mode, the page is replaced by the mounted page. |
1023
|
|
|
* |
1024
|
|
|
* After this potential replacements are done, the root line is loaded |
1025
|
|
|
* (again) for this page record. It walks up the root line up to |
1026
|
|
|
* the first viewable record. |
1027
|
|
|
* |
1028
|
|
|
* (While upon the first accessibility check of the root line it was done |
1029
|
|
|
* by loading page by page from the page repository, this time the method |
1030
|
|
|
* checkRootlineForIncludeSection() is used to find the most distant |
1031
|
|
|
* accessible page within the root line.) |
1032
|
|
|
* |
1033
|
|
|
* Having found the final page id, the page record and the root line are |
1034
|
|
|
* loaded for last time by this method. |
1035
|
|
|
* |
1036
|
|
|
* Exceptions may be thrown for DOKTYPE_SPACER and not loadable page records |
1037
|
|
|
* or root lines. |
1038
|
|
|
* |
1039
|
|
|
* May set or update this properties: |
1040
|
|
|
* |
1041
|
|
|
* @see TypoScriptFrontendController::$id |
1042
|
|
|
* @see TypoScriptFrontendController::$MP |
1043
|
|
|
* @see TypoScriptFrontendController::$page |
1044
|
|
|
* @see TypoScriptFrontendController::$pageNotFound |
1045
|
|
|
* @see TypoScriptFrontendController::$pageAccessFailureHistory |
1046
|
|
|
* @see TypoScriptFrontendController::$originalMountPointPage |
1047
|
|
|
* @see TypoScriptFrontendController::$originalShortcutPage |
1048
|
|
|
* |
1049
|
|
|
* @throws \TYPO3\CMS\Core\Error\Http\ServiceUnavailableException |
1050
|
|
|
* @throws PageNotFoundException |
1051
|
|
|
*/ |
1052
|
|
|
protected function getPageAndRootline(ServerRequestInterface $request) |
1053
|
|
|
{ |
1054
|
|
|
$requestedPageRowWithoutGroupCheck = []; |
1055
|
|
|
$this->resolveTranslatedPageId(); |
1056
|
|
|
if (empty($this->page)) { |
1057
|
|
|
// If no page, we try to find the page before in the rootLine. |
1058
|
|
|
// Page is 'not found' in case the id itself was not an accessible page. code 1 |
1059
|
|
|
$this->pageNotFound = 1; |
1060
|
|
|
$requestedPageIsHidden = false; |
1061
|
|
|
try { |
1062
|
|
|
$hiddenField = $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['disabled'] ?? ''; |
1063
|
|
|
$includeHiddenPages = $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages') || $this->isBackendUserLoggedIn(); |
1064
|
|
|
if (!empty($hiddenField) && !$includeHiddenPages) { |
1065
|
|
|
// Page is "hidden" => 404 (deliberately done in default language, as this cascades to language overlays) |
1066
|
|
|
$rawPageRecord = $this->sys_page->getPage_noCheck($this->id); |
1067
|
|
|
$requestedPageIsHidden = (bool)$rawPageRecord[$hiddenField]; |
1068
|
|
|
} |
1069
|
|
|
|
1070
|
|
|
$requestedPageRowWithoutGroupCheck = $this->sys_page->getPage($this->id, true); |
1071
|
|
|
if (!empty($requestedPageRowWithoutGroupCheck)) { |
1072
|
|
|
$this->pageAccessFailureHistory['direct_access'][] = $requestedPageRowWithoutGroupCheck; |
1073
|
|
|
} |
1074
|
|
|
$this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get(); |
1075
|
|
|
if (!empty($this->rootLine)) { |
1076
|
|
|
$c = count($this->rootLine) - 1; |
1077
|
|
|
while ($c > 0) { |
1078
|
|
|
// Add to page access failure history: |
1079
|
|
|
$this->pageAccessFailureHistory['direct_access'][] = $this->rootLine[$c]; |
1080
|
|
|
// Decrease to next page in rootline and check the access to that, if OK, set as page record and ID value. |
1081
|
|
|
$c--; |
1082
|
|
|
$this->id = $this->rootLine[$c]['uid']; |
1083
|
|
|
$this->page = $this->sys_page->getPage($this->id); |
1084
|
|
|
if (!empty($this->page)) { |
1085
|
|
|
break; |
1086
|
|
|
} |
1087
|
|
|
} |
1088
|
|
|
} |
1089
|
|
|
} catch (RootLineException $e) { |
1090
|
|
|
$this->rootLine = []; |
1091
|
|
|
} |
1092
|
|
|
// If still no page... |
1093
|
|
|
if ($requestedPageIsHidden || (empty($requestedPageRowWithoutGroupCheck) && empty($this->page))) { |
1094
|
|
|
$message = 'The requested page does not exist!'; |
1095
|
|
|
$this->logPageAccessFailure($message, $request); |
1096
|
|
|
try { |
1097
|
|
|
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( |
1098
|
|
|
$request, |
1099
|
|
|
$message, |
1100
|
|
|
$this->getPageAccessFailureReasons(PageAccessFailureReasons::PAGE_NOT_FOUND) |
1101
|
|
|
); |
1102
|
|
|
throw new PropagateResponseException($response, 1533931330); |
1103
|
|
|
} catch (PageNotFoundException $e) { |
1104
|
|
|
throw new PageNotFoundException($message, 1301648780); |
1105
|
|
|
} |
1106
|
|
|
} |
1107
|
|
|
} |
1108
|
|
|
// Spacer and sysfolders is not accessible in frontend |
1109
|
|
|
if ($this->page['doktype'] == PageRepository::DOKTYPE_SPACER || $this->page['doktype'] == PageRepository::DOKTYPE_SYSFOLDER) { |
1110
|
|
|
$message = 'The requested page does not exist!'; |
1111
|
|
|
$this->logPageAccessFailure($message, $request); |
1112
|
|
|
try { |
1113
|
|
|
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( |
1114
|
|
|
$request, |
1115
|
|
|
$message, |
1116
|
|
|
$this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_INVALID_PAGETYPE) |
1117
|
|
|
); |
1118
|
|
|
throw new PropagateResponseException($response, 1533931343); |
1119
|
|
|
} catch (PageNotFoundException $e) { |
1120
|
|
|
throw new PageNotFoundException($message, 1301648781); |
1121
|
|
|
} |
1122
|
|
|
} |
1123
|
|
|
// Is the ID a link to another page?? |
1124
|
|
|
if ($this->page['doktype'] == PageRepository::DOKTYPE_SHORTCUT) { |
1125
|
|
|
// 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. |
1126
|
|
|
$this->MP = ''; |
1127
|
|
|
// saving the page so that we can check later - when we know |
1128
|
|
|
// about languages - whether we took the correct shortcut or |
1129
|
|
|
// whether a translation of the page overwrites the shortcut |
1130
|
|
|
// target and we need to follow the new target |
1131
|
|
|
$this->originalShortcutPage = $this->page; |
1132
|
|
|
$this->page = $this->sys_page->getPageShortcut($this->page['shortcut'], $this->page['shortcut_mode'], $this->page['uid']); |
1133
|
|
|
$this->id = $this->page['uid']; |
1134
|
|
|
} |
1135
|
|
|
// If the page is a mountpoint which should be overlaid with the contents of the mounted page, |
1136
|
|
|
// it must never be accessible directly, but only in the mountpoint context. Therefore we change |
1137
|
|
|
// the current ID and the user is redirected by checkPageForMountpointRedirect(). |
1138
|
|
|
if ($this->page['doktype'] == PageRepository::DOKTYPE_MOUNTPOINT && $this->page['mount_pid_ol']) { |
1139
|
|
|
$this->originalMountPointPage = $this->page; |
1140
|
|
|
$this->page = $this->sys_page->getPage($this->page['mount_pid']); |
1141
|
|
|
if (empty($this->page)) { |
1142
|
|
|
$message = 'This page (ID ' . $this->originalMountPointPage['uid'] . ') is of type "Mount point" and ' |
1143
|
|
|
. 'mounts a page which is not accessible (ID ' . $this->originalMountPointPage['mount_pid'] . ').'; |
1144
|
|
|
throw new PageNotFoundException($message, 1402043263); |
1145
|
|
|
} |
1146
|
|
|
// If the current page is a shortcut, the MP parameter will be replaced |
1147
|
|
|
if ($this->MP === '' || !empty($this->originalShortcutPage)) { |
1148
|
|
|
$this->MP = $this->page['uid'] . '-' . $this->originalMountPointPage['uid']; |
1149
|
|
|
} else { |
1150
|
|
|
$this->MP .= ',' . $this->page['uid'] . '-' . $this->originalMountPointPage['uid']; |
1151
|
|
|
} |
1152
|
|
|
$this->id = $this->page['uid']; |
1153
|
|
|
} |
1154
|
|
|
// Gets the rootLine |
1155
|
|
|
try { |
1156
|
|
|
$this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get(); |
1157
|
|
|
} catch (RootLineException $e) { |
1158
|
|
|
$this->rootLine = []; |
1159
|
|
|
} |
1160
|
|
|
// If not rootline we're off... |
1161
|
|
|
if (empty($this->rootLine)) { |
1162
|
|
|
$message = 'The requested page didn\'t have a proper connection to the tree-root!'; |
1163
|
|
|
$this->logPageAccessFailure($message, $request); |
1164
|
|
|
try { |
1165
|
|
|
$response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction( |
1166
|
|
|
$request, |
1167
|
|
|
$message, |
1168
|
|
|
$this->getPageAccessFailureReasons(PageAccessFailureReasons::ROOTLINE_BROKEN) |
1169
|
|
|
); |
1170
|
|
|
throw new PropagateResponseException($response, 1533931350); |
1171
|
|
|
} catch (AbstractServerErrorException $e) { |
1172
|
|
|
$this->logger->error($message, ['exception' => $e]); |
|
|
|
|
1173
|
|
|
$exceptionClass = get_class($e); |
1174
|
|
|
throw new $exceptionClass($message, 1301648167); |
1175
|
|
|
} |
1176
|
|
|
} |
1177
|
|
|
// Checking for include section regarding the hidden/starttime/endtime/fe_user (that is access control of a whole subbranch!) |
1178
|
|
|
if ($this->checkRootlineForIncludeSection()) { |
1179
|
|
|
if (empty($this->rootLine)) { |
1180
|
|
|
$message = 'The requested page was not accessible!'; |
1181
|
|
|
$this->logPageAccessFailure($message, $request); |
1182
|
|
|
try { |
1183
|
|
|
$response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction( |
1184
|
|
|
$request, |
1185
|
|
|
$message, |
1186
|
|
|
$this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_GENERAL) |
1187
|
|
|
); |
1188
|
|
|
throw new PropagateResponseException($response, 1533931351); |
1189
|
|
|
} catch (AbstractServerErrorException $e) { |
1190
|
|
|
$this->logger->warning($message); |
1191
|
|
|
$exceptionClass = get_class($e); |
1192
|
|
|
throw new $exceptionClass($message, 1301648234); |
1193
|
|
|
} |
1194
|
|
|
} else { |
1195
|
|
|
$el = reset($this->rootLine); |
1196
|
|
|
$this->id = $el['uid']; |
1197
|
|
|
$this->page = $this->sys_page->getPage($this->id); |
1198
|
|
|
try { |
1199
|
|
|
$this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get(); |
1200
|
|
|
} catch (RootLineException $e) { |
1201
|
|
|
$this->rootLine = []; |
1202
|
|
|
} |
1203
|
|
|
} |
1204
|
|
|
} |
1205
|
|
|
} |
1206
|
|
|
|
1207
|
|
|
/** |
1208
|
|
|
* If $this->id contains a translated page record, this needs to be resolved to the default language |
1209
|
|
|
* in order for all rootline functionality and access restrictions to be in place further on. |
1210
|
|
|
* |
1211
|
|
|
* Additionally, if a translated page is found, LanguageAspect is set as well. |
1212
|
|
|
*/ |
1213
|
|
|
protected function resolveTranslatedPageId() |
1214
|
|
|
{ |
1215
|
|
|
$this->page = $this->sys_page->getPage($this->id); |
|
|
|
|
1216
|
|
|
// Accessed a default language page record, nothing to resolve |
1217
|
|
|
if (empty($this->page) || (int)$this->page[$GLOBALS['TCA']['pages']['ctrl']['languageField']] === 0) { |
1218
|
|
|
return; |
1219
|
|
|
} |
1220
|
|
|
$languageId = (int)$this->page[$GLOBALS['TCA']['pages']['ctrl']['languageField']]; |
1221
|
|
|
$this->page = $this->sys_page->getPage($this->page[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']]); |
1222
|
|
|
$this->context->setAspect('language', GeneralUtility::makeInstance(LanguageAspect::class, $languageId)); |
1223
|
|
|
$this->id = $this->page['uid']; |
1224
|
|
|
} |
1225
|
|
|
|
1226
|
|
|
/** |
1227
|
|
|
* Checks if visibility of the page is blocked upwards in the root line. |
1228
|
|
|
* |
1229
|
|
|
* If any page in the root line is blocking visibility, true is returned. |
1230
|
|
|
* |
1231
|
|
|
* All pages from the blocking page downwards are removed from the root |
1232
|
|
|
* line, so that the remaining pages can be used to relocate the page up |
1233
|
|
|
* to lowest visible page. |
1234
|
|
|
* |
1235
|
|
|
* The blocking feature of a page must be turned on by setting the page |
1236
|
|
|
* record field 'extendToSubpages' to 1 in case of hidden, starttime, |
1237
|
|
|
* endtime or fe_group restrictions. |
1238
|
|
|
* |
1239
|
|
|
* Additionally this method checks for backend user sections in root line |
1240
|
|
|
* and if found evaluates if a backend user is logged in and has access. |
1241
|
|
|
* |
1242
|
|
|
* Recyclers are also checked and trigger page not found if found in root |
1243
|
|
|
* line. |
1244
|
|
|
* |
1245
|
|
|
* @todo Find a better name, i.e. checkVisibilityByRootLine |
1246
|
|
|
* @todo Invert boolean return value. Return true if visible. |
1247
|
|
|
* |
1248
|
|
|
* @return bool |
1249
|
|
|
*/ |
1250
|
|
|
protected function checkRootlineForIncludeSection(): bool |
1251
|
|
|
{ |
1252
|
|
|
$c = count($this->rootLine); |
1253
|
|
|
$removeTheRestFlag = false; |
1254
|
|
|
for ($a = 0; $a < $c; $a++) { |
1255
|
|
|
if (!$this->checkPagerecordForIncludeSection($this->rootLine[$a])) { |
1256
|
|
|
// Add to page access failure history and mark the page as not found |
1257
|
|
|
// Keep the rootline however to trigger an access denied error instead of a service unavailable error |
1258
|
|
|
$this->pageAccessFailureHistory['sub_section'][] = $this->rootLine[$a]; |
1259
|
|
|
$this->pageNotFound = 2; |
1260
|
|
|
} |
1261
|
|
|
|
1262
|
|
|
if ((int)$this->rootLine[$a]['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION) { |
1263
|
|
|
// If there is a backend user logged in, check if they have read access to the page: |
1264
|
|
|
if ($this->isBackendUserLoggedIn()) { |
1265
|
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) |
1266
|
|
|
->getQueryBuilderForTable('pages'); |
1267
|
|
|
|
1268
|
|
|
$queryBuilder |
1269
|
|
|
->getRestrictions() |
1270
|
|
|
->removeAll(); |
1271
|
|
|
|
1272
|
|
|
$row = $queryBuilder |
1273
|
|
|
->select('uid') |
1274
|
|
|
->from('pages') |
1275
|
|
|
->where( |
1276
|
|
|
$queryBuilder->expr()->eq( |
1277
|
|
|
'uid', |
1278
|
|
|
$queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT) |
1279
|
|
|
), |
1280
|
|
|
$this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW) |
1281
|
|
|
) |
1282
|
|
|
->execute() |
1283
|
|
|
->fetch(); |
1284
|
|
|
|
1285
|
|
|
// versionOL()? |
1286
|
|
|
if (!$row) { |
1287
|
|
|
// 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... |
1288
|
|
|
$removeTheRestFlag = true; |
1289
|
|
|
} |
1290
|
|
|
} else { |
1291
|
|
|
// Don't go here, if there is no backend user logged in. |
1292
|
|
|
$removeTheRestFlag = true; |
1293
|
|
|
} |
1294
|
|
|
} elseif ((int)$this->rootLine[$a]['doktype'] === PageRepository::DOKTYPE_RECYCLER) { |
1295
|
|
|
// page is in a recycler |
1296
|
|
|
$removeTheRestFlag = true; |
1297
|
|
|
} |
1298
|
|
|
if ($removeTheRestFlag) { |
1299
|
|
|
// Page is 'not found' in case a subsection was found and not accessible, code 2 |
1300
|
|
|
$this->pageNotFound = 2; |
1301
|
|
|
unset($this->rootLine[$a]); |
1302
|
|
|
} |
1303
|
|
|
} |
1304
|
|
|
return $removeTheRestFlag; |
1305
|
|
|
} |
1306
|
|
|
|
1307
|
|
|
/** |
1308
|
|
|
* Checks page record for enableFields |
1309
|
|
|
* Returns TRUE if enableFields does not disable the page record. |
1310
|
|
|
* Takes notice of the includeHiddenPages visibility aspect flag and uses SIM_ACCESS_TIME for start/endtime evaluation |
1311
|
|
|
* |
1312
|
|
|
* @param array $row The page record to evaluate (needs fields: hidden, starttime, endtime, fe_group) |
1313
|
|
|
* @param bool $bypassGroupCheck Bypass group-check |
1314
|
|
|
* @return bool TRUE, if record is viewable. |
1315
|
|
|
* @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getTreeList() |
1316
|
|
|
* @see checkPagerecordForIncludeSection() |
1317
|
|
|
*/ |
1318
|
|
|
public function checkEnableFields($row, $bypassGroupCheck = false) |
1319
|
|
|
{ |
1320
|
|
|
$_params = ['pObj' => $this, 'row' => &$row, 'bypassGroupCheck' => &$bypassGroupCheck]; |
1321
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['hook_checkEnableFields'] ?? [] as $_funcRef) { |
1322
|
|
|
// Call hooks: If one returns FALSE, method execution is aborted with result "This record is not available" |
1323
|
|
|
$return = GeneralUtility::callUserFunction($_funcRef, $_params, $this); |
1324
|
|
|
if ($return === false) { |
1325
|
|
|
return false; |
1326
|
|
|
} |
1327
|
|
|
} |
1328
|
|
|
if ((!$row['hidden'] || $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages', false)) |
1329
|
|
|
&& $row['starttime'] <= $GLOBALS['SIM_ACCESS_TIME'] |
1330
|
|
|
&& ($row['endtime'] == 0 || $row['endtime'] > $GLOBALS['SIM_ACCESS_TIME']) |
1331
|
|
|
&& ($bypassGroupCheck || $this->checkPageGroupAccess($row))) { |
1332
|
|
|
return true; |
1333
|
|
|
} |
1334
|
|
|
return false; |
1335
|
|
|
} |
1336
|
|
|
|
1337
|
|
|
/** |
1338
|
|
|
* Check group access against a page record |
1339
|
|
|
* |
1340
|
|
|
* @param array $row The page record to evaluate (needs field: fe_group) |
1341
|
|
|
* @return bool TRUE, if group access is granted. |
1342
|
|
|
* @internal |
1343
|
|
|
*/ |
1344
|
|
|
public function checkPageGroupAccess($row) |
1345
|
|
|
{ |
1346
|
|
|
/** @var UserAspect $userAspect */ |
1347
|
|
|
$userAspect = $this->context->getAspect('frontend.user'); |
1348
|
|
|
$pageGroupList = explode(',', $row['fe_group'] ?: 0); |
1349
|
|
|
return count(array_intersect($userAspect->getGroupIds(), $pageGroupList)) > 0; |
1350
|
|
|
} |
1351
|
|
|
|
1352
|
|
|
/** |
1353
|
|
|
* Checks if the current page of the root line is visible. |
1354
|
|
|
* |
1355
|
|
|
* If the field extendToSubpages is 0, access is granted, |
1356
|
|
|
* else the fields hidden, starttime, endtime, fe_group are evaluated. |
1357
|
|
|
* |
1358
|
|
|
* @todo Find a better name, i.e. isVisibleRecord() |
1359
|
|
|
* |
1360
|
|
|
* @param array $row The page record |
1361
|
|
|
* @return bool true if visible |
1362
|
|
|
* @internal |
1363
|
|
|
* @see checkEnableFields() |
1364
|
|
|
* @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getTreeList() |
1365
|
|
|
* @see checkRootlineForIncludeSection() |
1366
|
|
|
*/ |
1367
|
|
|
public function checkPagerecordForIncludeSection(array $row): bool |
1368
|
|
|
{ |
1369
|
|
|
return !$row['extendToSubpages'] || $this->checkEnableFields($row); |
1370
|
|
|
} |
1371
|
|
|
|
1372
|
|
|
/** |
1373
|
|
|
* 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!) |
1374
|
|
|
* |
1375
|
|
|
* @return bool returns TRUE if logins are OK, otherwise FALSE (and then the login user must be unset!) |
1376
|
|
|
*/ |
1377
|
|
|
public function checkIfLoginAllowedInBranch() |
1378
|
|
|
{ |
1379
|
|
|
// Initialize: |
1380
|
|
|
$c = count($this->rootLine); |
1381
|
|
|
$loginAllowed = true; |
1382
|
|
|
// Traverse root line from root and outwards: |
1383
|
|
|
for ($a = 0; $a < $c; $a++) { |
1384
|
|
|
// If a value is set for login state: |
1385
|
|
|
if ($this->rootLine[$a]['fe_login_mode'] > 0) { |
1386
|
|
|
// Determine state from value: |
1387
|
|
|
if ((int)$this->rootLine[$a]['fe_login_mode'] === 1) { |
1388
|
|
|
$loginAllowed = false; |
1389
|
|
|
$this->loginAllowedInBranch_mode = 'all'; |
1390
|
|
|
} elseif ((int)$this->rootLine[$a]['fe_login_mode'] === 3) { |
1391
|
|
|
$loginAllowed = false; |
1392
|
|
|
$this->loginAllowedInBranch_mode = 'groups'; |
1393
|
|
|
} else { |
1394
|
|
|
$loginAllowed = true; |
1395
|
|
|
} |
1396
|
|
|
} |
1397
|
|
|
} |
1398
|
|
|
return $loginAllowed; |
1399
|
|
|
} |
1400
|
|
|
|
1401
|
|
|
/** |
1402
|
|
|
* 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 |
1403
|
|
|
* |
1404
|
|
|
* @param string $failureReasonCode the error code to be attached (optional), see PageAccessFailureReasons list for details |
1405
|
|
|
* @return array Summary of why page access was not allowed. |
1406
|
|
|
*/ |
1407
|
|
|
public function getPageAccessFailureReasons(string $failureReasonCode = null) |
1408
|
|
|
{ |
1409
|
|
|
$output = []; |
1410
|
|
|
if ($failureReasonCode) { |
1411
|
|
|
$output['code'] = $failureReasonCode; |
1412
|
|
|
} |
1413
|
|
|
$combinedRecords = array_merge( |
1414
|
|
|
is_array($this->pageAccessFailureHistory['direct_access'] ?? false) ? $this->pageAccessFailureHistory['direct_access'] : [['fe_group' => 0]], |
1415
|
|
|
is_array($this->pageAccessFailureHistory['sub_section'] ?? false) ? $this->pageAccessFailureHistory['sub_section'] : [] |
1416
|
|
|
); |
1417
|
|
|
if (!empty($combinedRecords)) { |
1418
|
|
|
foreach ($combinedRecords as $k => $pagerec) { |
1419
|
|
|
// 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 |
1420
|
|
|
// 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! |
1421
|
|
|
if (!$k || $pagerec['extendToSubpages']) { |
1422
|
|
|
if ($pagerec['hidden'] ?? false) { |
1423
|
|
|
$output['hidden'][$pagerec['uid']] = true; |
1424
|
|
|
} |
1425
|
|
|
if (isset($pagerec['starttime']) && $pagerec['starttime'] > $GLOBALS['SIM_ACCESS_TIME']) { |
1426
|
|
|
$output['starttime'][$pagerec['uid']] = $pagerec['starttime']; |
1427
|
|
|
} |
1428
|
|
|
if (isset($pagerec['endtime']) && $pagerec['endtime'] != 0 && $pagerec['endtime'] <= $GLOBALS['SIM_ACCESS_TIME']) { |
1429
|
|
|
$output['endtime'][$pagerec['uid']] = $pagerec['endtime']; |
1430
|
|
|
} |
1431
|
|
|
if (!$this->checkPageGroupAccess($pagerec)) { |
1432
|
|
|
$output['fe_group'][$pagerec['uid']] = $pagerec['fe_group']; |
1433
|
|
|
} |
1434
|
|
|
} |
1435
|
|
|
} |
1436
|
|
|
} |
1437
|
|
|
return $output; |
1438
|
|
|
} |
1439
|
|
|
|
1440
|
|
|
/** |
1441
|
|
|
* Gets ->page and ->rootline information based on ->id. ->id may change during this operation. |
1442
|
|
|
* If not inside a site, then default to first page in site. |
1443
|
|
|
* |
1444
|
|
|
* @param int $rootPageId Page uid of the page where the found site is located |
1445
|
|
|
* @internal |
1446
|
|
|
*/ |
1447
|
|
|
public function getPageAndRootlineWithDomain($rootPageId, ServerRequestInterface $request) |
1448
|
|
|
{ |
1449
|
|
|
$this->getPageAndRootline($request); |
1450
|
|
|
// Checks if the $domain-startpage is in the rootLine. This is necessary so that references to page-id's via ?id=123 from other sites are not possible. |
1451
|
|
|
if (is_array($this->rootLine) && $this->rootLine !== []) { |
1452
|
|
|
$idFound = false; |
1453
|
|
|
foreach ($this->rootLine as $key => $val) { |
1454
|
|
|
if ($val['uid'] == $rootPageId) { |
1455
|
|
|
$idFound = true; |
1456
|
|
|
break; |
1457
|
|
|
} |
1458
|
|
|
} |
1459
|
|
|
if (!$idFound) { |
1460
|
|
|
// Page is 'not found' in case the id was outside the domain, code 3 |
1461
|
|
|
$this->pageNotFound = 3; |
1462
|
|
|
$this->id = $rootPageId; |
1463
|
|
|
// re-get the page and rootline if the id was not found. |
1464
|
|
|
$this->getPageAndRootline($request); |
1465
|
|
|
} |
1466
|
|
|
} |
1467
|
|
|
} |
1468
|
|
|
|
1469
|
|
|
/******************************************** |
1470
|
|
|
* |
1471
|
|
|
* Template and caching related functions. |
1472
|
|
|
* |
1473
|
|
|
*******************************************/ |
1474
|
|
|
|
1475
|
|
|
protected function setPageArguments(PageArguments $pageArguments): void |
1476
|
|
|
{ |
1477
|
|
|
$this->pageArguments = $pageArguments; |
1478
|
|
|
$this->id = $pageArguments->getPageId(); |
1479
|
|
|
$this->type = $pageArguments->getPageType() ?: 0; |
1480
|
|
|
if ($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) { |
1481
|
|
|
$this->MP = (string)($pageArguments->getArguments()['MP'] ?? ''); |
1482
|
|
|
// Ensure no additional arguments are given via the &MP=123-345,908-172 (e.g. "/") |
1483
|
|
|
$this->MP = preg_replace('/[^0-9,-]/', '', $this->MP); |
1484
|
|
|
} |
1485
|
|
|
} |
1486
|
|
|
|
1487
|
|
|
/** |
1488
|
|
|
* Fetches the arguments that are relevant for creating the hash base from the given PageArguments object. |
1489
|
|
|
* Excluded parameters are not taken into account when calculating the hash base. |
1490
|
|
|
* |
1491
|
|
|
* @param PageArguments $pageArguments |
1492
|
|
|
* @return array |
1493
|
|
|
*/ |
1494
|
|
|
protected function getRelevantParametersForCachingFromPageArguments(PageArguments $pageArguments): array |
1495
|
|
|
{ |
1496
|
|
|
$queryParams = $pageArguments->getDynamicArguments(); |
1497
|
|
|
if (!empty($queryParams) && ($pageArguments->getArguments()['cHash'] ?? false)) { |
1498
|
|
|
$queryParams['id'] = $pageArguments->getPageId(); |
1499
|
|
|
return GeneralUtility::makeInstance(CacheHashCalculator::class) |
1500
|
|
|
->getRelevantParameters(HttpUtility::buildQueryString($queryParams)); |
1501
|
|
|
} |
1502
|
|
|
return []; |
1503
|
|
|
} |
1504
|
|
|
|
1505
|
|
|
/** |
1506
|
|
|
* See if page is in cache and get it if so |
1507
|
|
|
* Stores the page content in $this->content if something is found. |
1508
|
|
|
* |
1509
|
|
|
* @param ServerRequestInterface|null $request if given this is used to determine values in headerNoCache() instead of the superglobal $_SERVER |
1510
|
|
|
* @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException |
1511
|
|
|
*/ |
1512
|
|
|
public function getFromCache(ServerRequestInterface $request = null) |
1513
|
|
|
{ |
1514
|
|
|
// clearing the content-variable, which will hold the pagecontent |
1515
|
|
|
$this->content = ''; |
1516
|
|
|
// Unsetting the lowlevel config |
1517
|
|
|
$this->config = []; |
1518
|
|
|
$this->cacheContentFlag = false; |
1519
|
|
|
|
1520
|
|
|
if ($this->no_cache) { |
1521
|
|
|
return; |
1522
|
|
|
} |
1523
|
|
|
|
1524
|
|
|
if (!$this->tmpl instanceof TemplateService) { |
|
|
|
|
1525
|
|
|
$this->tmpl = GeneralUtility::makeInstance(TemplateService::class, $this->context, null, $this); |
1526
|
|
|
} |
1527
|
|
|
|
1528
|
|
|
$pageSectionCacheContent = $this->tmpl->getCurrentPageData((int)$this->id, (string)$this->MP); |
1529
|
|
|
if (!is_array($pageSectionCacheContent)) { |
|
|
|
|
1530
|
|
|
// Nothing in the cache, we acquire an "exclusive lock" for the key now. |
1531
|
|
|
// We use the Registry to store this lock centrally, |
1532
|
|
|
// but we protect the access again with a global exclusive lock to avoid race conditions |
1533
|
|
|
|
1534
|
|
|
$this->acquireLock('pagesection', $this->id . '::' . $this->MP); |
1535
|
|
|
// |
1536
|
|
|
// from this point on we're the only one working on that page ($key) |
1537
|
|
|
// |
1538
|
|
|
|
1539
|
|
|
// query the cache again to see if the page data are there meanwhile |
1540
|
|
|
$pageSectionCacheContent = $this->tmpl->getCurrentPageData((int)$this->id, (string)$this->MP); |
1541
|
|
|
if (is_array($pageSectionCacheContent)) { |
1542
|
|
|
// we have the content, nice that some other process did the work for us already |
1543
|
|
|
$this->releaseLock('pagesection'); |
1544
|
|
|
} |
1545
|
|
|
// We keep the lock set, because we are the ones generating the page now and filling the cache. |
1546
|
|
|
// This indicates that we have to release the lock later in releaseLocks() |
1547
|
|
|
} |
1548
|
|
|
|
1549
|
|
|
if (is_array($pageSectionCacheContent)) { |
|
|
|
|
1550
|
|
|
// 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. |
1551
|
|
|
// If this hash is not the same in here in this section and after page-generation, then the page will not be properly cached! |
1552
|
|
|
// 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. |
1553
|
|
|
$pageSectionCacheContent = $this->tmpl->matching($pageSectionCacheContent); |
1554
|
|
|
ksort($pageSectionCacheContent); |
1555
|
|
|
$this->all = $pageSectionCacheContent; |
1556
|
|
|
} |
1557
|
|
|
|
1558
|
|
|
// Look for page in cache only if a shift-reload is not sent to the server. |
1559
|
|
|
$lockHash = $this->getLockHash(); |
1560
|
|
|
if (!$this->headerNoCache($request) && $this->all) { |
|
|
|
|
1561
|
|
|
// we got page section information (TypoScript), so lets see if there is also a cached version |
1562
|
|
|
// of this page in the pages cache. |
1563
|
|
|
$this->newHash = $this->getHash(); |
1564
|
|
|
$this->getTimeTracker()->push('Cache Row'); |
1565
|
|
|
$row = $this->getFromCache_queryRow(); |
1566
|
|
|
if (!is_array($row)) { |
|
|
|
|
1567
|
|
|
// nothing in the cache, we acquire an exclusive lock now |
1568
|
|
|
$this->acquireLock('pages', $lockHash); |
1569
|
|
|
// |
1570
|
|
|
// from this point on we're the only one working on that page ($lockHash) |
1571
|
|
|
// |
1572
|
|
|
|
1573
|
|
|
// query the cache again to see if the data are there meanwhile |
1574
|
|
|
$row = $this->getFromCache_queryRow(); |
1575
|
|
|
if (is_array($row)) { |
1576
|
|
|
// we have the content, nice that some other process did the work for us |
1577
|
|
|
$this->releaseLock('pages'); |
1578
|
|
|
} |
1579
|
|
|
// We keep the lock set, because we are the ones generating the page now and filling the cache. |
1580
|
|
|
// This indicates that we have to release the lock later in releaseLocks() |
1581
|
|
|
} |
1582
|
|
|
if (is_array($row)) { |
|
|
|
|
1583
|
|
|
$this->populatePageDataFromCache($row); |
1584
|
|
|
} |
1585
|
|
|
$this->getTimeTracker()->pull(); |
1586
|
|
|
} else { |
1587
|
|
|
// the user forced rebuilding the page cache or there was no pagesection information |
1588
|
|
|
// get a lock for the page content so other processes will not interrupt the regeneration |
1589
|
|
|
$this->acquireLock('pages', $lockHash); |
1590
|
|
|
} |
1591
|
|
|
} |
1592
|
|
|
|
1593
|
|
|
/** |
1594
|
|
|
* Returning the cached version of page with hash = newHash |
1595
|
|
|
* |
1596
|
|
|
* @return array Cached row, if any. Otherwise void. |
1597
|
|
|
*/ |
1598
|
|
|
public function getFromCache_queryRow() |
1599
|
|
|
{ |
1600
|
|
|
$this->getTimeTracker()->push('Cache Query'); |
1601
|
|
|
$row = $this->pageCache->get($this->newHash); |
1602
|
|
|
$this->getTimeTracker()->pull(); |
1603
|
|
|
return $row; |
1604
|
|
|
} |
1605
|
|
|
|
1606
|
|
|
/** |
1607
|
|
|
* This method properly sets the values given from the pages cache into the corresponding |
1608
|
|
|
* TSFE variables. The counterpart is setPageCacheContent() where all relevant information is fetched. |
1609
|
|
|
* This also contains all data that could be cached, even for pages that are partially cached, as they |
1610
|
|
|
* have non-cacheable content still to be rendered. |
1611
|
|
|
* |
1612
|
|
|
* @see getFromCache() |
1613
|
|
|
* @see setPageCacheContent() |
1614
|
|
|
* @param array $cachedData |
1615
|
|
|
*/ |
1616
|
|
|
protected function populatePageDataFromCache(array $cachedData): void |
1617
|
|
|
{ |
1618
|
|
|
// Call hook when a page is retrieved from cache |
1619
|
|
|
$_params = ['pObj' => &$this, 'cache_pages_row' => &$cachedData]; |
1620
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['pageLoadedFromCache'] ?? [] as $_funcRef) { |
1621
|
|
|
GeneralUtility::callUserFunction($_funcRef, $_params, $this); |
1622
|
|
|
} |
1623
|
|
|
// Fetches the lowlevel config stored with the cached data |
1624
|
|
|
$this->config = $cachedData['cache_data']; |
1625
|
|
|
// Getting the content |
1626
|
|
|
$this->content = $cachedData['content']; |
1627
|
|
|
// Setting flag, so we know, that some cached content has been loaded |
1628
|
|
|
$this->cacheContentFlag = true; |
1629
|
|
|
$this->cacheExpires = $cachedData['expires']; |
1630
|
|
|
// Restore the current tags as they can be retrieved by getPageCacheTags() |
1631
|
|
|
$this->pageCacheTags = $cachedData['cacheTags'] ?? []; |
1632
|
|
|
|
1633
|
|
|
// Restore page title information, this is needed to generate the page title for |
1634
|
|
|
// partially cached pages. |
1635
|
|
|
$this->page['title'] = $cachedData['pageTitleInfo']['title']; |
1636
|
|
|
$this->indexedDocTitle = $cachedData['pageTitleInfo']['indexedDocTitle']; |
1637
|
|
|
|
1638
|
|
|
if (isset($this->config['config']['debug'])) { |
1639
|
|
|
$debugCacheTime = (bool)$this->config['config']['debug']; |
1640
|
|
|
} else { |
1641
|
|
|
$debugCacheTime = !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']); |
1642
|
|
|
} |
1643
|
|
|
if ($debugCacheTime) { |
1644
|
|
|
$dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy']; |
1645
|
|
|
$timeFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm']; |
1646
|
|
|
$this->content .= LF . '<!-- Cached page generated ' . date($dateFormat . ' ' . $timeFormat, $cachedData['tstamp']) . '. Expires ' . date($dateFormat . ' ' . $timeFormat, $cachedData['expires']) . ' -->'; |
1647
|
|
|
} |
1648
|
|
|
} |
1649
|
|
|
|
1650
|
|
|
/** |
1651
|
|
|
* Detecting if shift-reload has been clicked |
1652
|
|
|
* Will not be called if re-generation of page happens by other reasons (for instance that the page is not in cache yet!) |
1653
|
|
|
* Also, a backend user MUST be logged in for the shift-reload to be detected due to DoS-attack-security reasons. |
1654
|
|
|
* |
1655
|
|
|
* @param ServerRequestInterface|null $request |
1656
|
|
|
* @return bool If shift-reload in client browser has been clicked, disable getting cached page (and regenerate it). |
1657
|
|
|
*/ |
1658
|
|
|
public function headerNoCache(ServerRequestInterface $request = null) |
1659
|
|
|
{ |
1660
|
|
|
if ($request instanceof ServerRequestInterface) { |
1661
|
|
|
$serverParams = $request->getServerParams(); |
1662
|
|
|
} else { |
1663
|
|
|
$serverParams = $_SERVER; |
1664
|
|
|
} |
1665
|
|
|
$disableAcquireCacheData = false; |
1666
|
|
|
if ($this->isBackendUserLoggedIn()) { |
1667
|
|
|
if (strtolower($serverParams['HTTP_CACHE_CONTROL'] ?? '') === 'no-cache' || strtolower($serverParams['HTTP_PRAGMA'] ?? '') === 'no-cache') { |
1668
|
|
|
$disableAcquireCacheData = true; |
1669
|
|
|
} |
1670
|
|
|
} |
1671
|
|
|
// Call hook for possible by-pass of requiring of page cache (for recaching purpose) |
1672
|
|
|
$_params = ['pObj' => &$this, 'disableAcquireCacheData' => &$disableAcquireCacheData]; |
1673
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['headerNoCache'] ?? [] as $_funcRef) { |
1674
|
|
|
GeneralUtility::callUserFunction($_funcRef, $_params, $this); |
1675
|
|
|
} |
1676
|
|
|
return $disableAcquireCacheData; |
1677
|
|
|
} |
1678
|
|
|
|
1679
|
|
|
/** |
1680
|
|
|
* Calculates the cache-hash |
1681
|
|
|
* This hash is unique to the template, the variables ->id, ->type, list of fe user groups, ->MP (Mount Points) and cHash array |
1682
|
|
|
* Used to get and later store the cached data. |
1683
|
|
|
* |
1684
|
|
|
* @return string MD5 hash of serialized hash base from createHashBase(), prefixed with page id |
1685
|
|
|
* @see getFromCache() |
1686
|
|
|
* @see getLockHash() |
1687
|
|
|
*/ |
1688
|
|
|
protected function getHash() |
1689
|
|
|
{ |
1690
|
|
|
return $this->id . '_' . md5($this->createHashBase(false)); |
1691
|
|
|
} |
1692
|
|
|
|
1693
|
|
|
/** |
1694
|
|
|
* Calculates the lock-hash |
1695
|
|
|
* This hash is unique to the above hash, except that it doesn't contain the template information in $this->all. |
1696
|
|
|
* |
1697
|
|
|
* @return string MD5 hash prefixed with page id |
1698
|
|
|
* @see getFromCache() |
1699
|
|
|
* @see getHash() |
1700
|
|
|
*/ |
1701
|
|
|
protected function getLockHash() |
1702
|
|
|
{ |
1703
|
|
|
$lockHash = $this->createHashBase(true); |
1704
|
|
|
return $this->id . '_' . md5($lockHash); |
1705
|
|
|
} |
1706
|
|
|
|
1707
|
|
|
/** |
1708
|
|
|
* Calculates the cache-hash (or the lock-hash) |
1709
|
|
|
* This hash is unique to the template, |
1710
|
|
|
* the variables ->id, ->type, list of frontend user groups, |
1711
|
|
|
* ->MP (Mount Points) and cHash array |
1712
|
|
|
* Used to get and later store the cached data. |
1713
|
|
|
* |
1714
|
|
|
* @param bool $createLockHashBase Whether to create the lock hash, which doesn't contain the "this->all" (the template information) |
1715
|
|
|
* @return string the serialized hash base |
1716
|
|
|
*/ |
1717
|
|
|
protected function createHashBase($createLockHashBase = false) |
1718
|
|
|
{ |
1719
|
|
|
// Fetch the list of user groups |
1720
|
|
|
/** @var UserAspect $userAspect */ |
1721
|
|
|
$userAspect = $this->context->getAspect('frontend.user'); |
1722
|
|
|
$hashParameters = [ |
1723
|
|
|
'id' => (int)$this->id, |
1724
|
|
|
'type' => (int)$this->type, |
1725
|
|
|
'groupIds' => (string)implode(',', $userAspect->getGroupIds()), |
1726
|
|
|
'MP' => (string)$this->MP, |
1727
|
|
|
'site' => $this->site->getIdentifier(), |
1728
|
|
|
// Ensure the language base is used for the hash base calculation as well, otherwise TypoScript and page-related rendering |
1729
|
|
|
// is not cached properly as we don't have any language-specific conditions anymore |
1730
|
|
|
'siteBase' => (string)$this->language->getBase(), |
1731
|
|
|
// additional variation trigger for static routes |
1732
|
|
|
'staticRouteArguments' => $this->pageArguments->getStaticArguments(), |
1733
|
|
|
// dynamic route arguments (if route was resolved) |
1734
|
|
|
'dynamicArguments' => $this->getRelevantParametersForCachingFromPageArguments($this->pageArguments), |
1735
|
|
|
]; |
1736
|
|
|
// Include the template information if we shouldn't create a lock hash |
1737
|
|
|
if (!$createLockHashBase) { |
1738
|
|
|
$hashParameters['all'] = $this->all; |
1739
|
|
|
} |
1740
|
|
|
// Call hook to influence the hash calculation |
1741
|
|
|
$_params = [ |
1742
|
|
|
'hashParameters' => &$hashParameters, |
1743
|
|
|
'createLockHashBase' => $createLockHashBase |
1744
|
|
|
]; |
1745
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['createHashBase'] ?? [] as $_funcRef) { |
1746
|
|
|
GeneralUtility::callUserFunction($_funcRef, $_params, $this); |
1747
|
|
|
} |
1748
|
|
|
return serialize($hashParameters); |
1749
|
|
|
} |
1750
|
|
|
|
1751
|
|
|
/** |
1752
|
|
|
* Checks if config-array exists already but if not, gets it |
1753
|
|
|
* |
1754
|
|
|
* @param ServerRequestInterface|null $request |
1755
|
|
|
* @throws \TYPO3\CMS\Core\Error\Http\InternalServerErrorException |
1756
|
|
|
* @throws \TYPO3\CMS\Core\Error\Http\ServiceUnavailableException |
1757
|
|
|
*/ |
1758
|
|
|
public function getConfigArray(ServerRequestInterface $request = null) |
1759
|
|
|
{ |
1760
|
|
|
$request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); |
1761
|
|
|
if (!$this->tmpl instanceof TemplateService) { |
|
|
|
|
1762
|
|
|
$this->tmpl = GeneralUtility::makeInstance(TemplateService::class, $this->context, null, $this); |
1763
|
|
|
} |
1764
|
|
|
|
1765
|
|
|
// 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 |
1766
|
|
|
if (empty($this->config) || $this->isINTincScript() || $this->context->getPropertyFromAspect('typoscript', 'forcedTemplateParsing')) { |
1767
|
|
|
$timeTracker = $this->getTimeTracker(); |
1768
|
|
|
$timeTracker->push('Parse template'); |
1769
|
|
|
// Start parsing the TS template. Might return cached version. |
1770
|
|
|
$this->tmpl->start($this->rootLine); |
1771
|
|
|
$timeTracker->pull(); |
1772
|
|
|
// At this point we have a valid pagesection_cache (generated in $this->tmpl->start()), |
1773
|
|
|
// so let all other processes proceed now. (They are blocked at the pagessection_lock in getFromCache()) |
1774
|
|
|
$this->releaseLock('pagesection'); |
1775
|
|
|
if ($this->tmpl->loaded) { |
1776
|
|
|
$timeTracker->push('Setting the config-array'); |
1777
|
|
|
// toplevel - objArrayName |
1778
|
|
|
$typoScriptPageTypeName = $this->tmpl->setup['types.'][$this->type]; |
1779
|
|
|
$this->sPre = $typoScriptPageTypeName; |
1780
|
|
|
$this->pSetup = $this->tmpl->setup[$typoScriptPageTypeName . '.']; |
1781
|
|
|
if (!is_array($this->pSetup)) { |
1782
|
|
|
$this->logger->alert('The page is not configured! [type={type}][{type_name}].', ['type' => $this->type, 'type_name' => $typoScriptPageTypeName]); |
1783
|
|
|
try { |
1784
|
|
|
$message = 'The page is not configured! [type=' . $this->type . '][' . $typoScriptPageTypeName . '].'; |
1785
|
|
|
$response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction( |
1786
|
|
|
$request, |
1787
|
|
|
$message, |
1788
|
|
|
['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED] |
1789
|
|
|
); |
1790
|
|
|
throw new PropagateResponseException($response, 1533931374); |
1791
|
|
|
} catch (AbstractServerErrorException $e) { |
1792
|
|
|
$explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.'; |
1793
|
|
|
$exceptionClass = get_class($e); |
1794
|
|
|
throw new $exceptionClass($message . ' ' . $explanation, 1294587217); |
1795
|
|
|
} |
1796
|
|
|
} else { |
1797
|
|
|
if (!isset($this->config['config'])) { |
1798
|
|
|
$this->config['config'] = []; |
1799
|
|
|
} |
1800
|
|
|
// Filling the config-array, first with the main "config." part |
1801
|
|
|
if (is_array($this->tmpl->setup['config.'] ?? null)) { |
1802
|
|
|
ArrayUtility::mergeRecursiveWithOverrule($this->tmpl->setup['config.'], $this->config['config']); |
1803
|
|
|
$this->config['config'] = $this->tmpl->setup['config.']; |
1804
|
|
|
} |
1805
|
|
|
// override it with the page/type-specific "config." |
1806
|
|
|
if (is_array($this->pSetup['config.'] ?? null)) { |
1807
|
|
|
ArrayUtility::mergeRecursiveWithOverrule($this->config['config'], $this->pSetup['config.']); |
1808
|
|
|
} |
1809
|
|
|
// Set default values for removeDefaultJS and inlineStyle2TempFile so CSS and JS are externalized if compatversion is higher than 4.0 |
1810
|
|
|
if (!isset($this->config['config']['removeDefaultJS'])) { |
1811
|
|
|
$this->config['config']['removeDefaultJS'] = 'external'; |
1812
|
|
|
} |
1813
|
|
|
if (!isset($this->config['config']['inlineStyle2TempFile'])) { |
1814
|
|
|
$this->config['config']['inlineStyle2TempFile'] = 1; |
1815
|
|
|
} |
1816
|
|
|
|
1817
|
|
|
if (!isset($this->config['config']['compressJs'])) { |
1818
|
|
|
$this->config['config']['compressJs'] = 0; |
1819
|
|
|
} |
1820
|
|
|
// Rendering charset of HTML page. |
1821
|
|
|
if (isset($this->config['config']['metaCharset']) && $this->config['config']['metaCharset'] !== 'utf-8') { |
1822
|
|
|
$this->metaCharset = $this->config['config']['metaCharset']; |
1823
|
|
|
} |
1824
|
|
|
// Setting default cache_timeout |
1825
|
|
|
if (isset($this->config['config']['cache_period'])) { |
1826
|
|
|
$this->set_cache_timeout_default((int)$this->config['config']['cache_period']); |
1827
|
|
|
} |
1828
|
|
|
|
1829
|
|
|
// Processing for the config_array: |
1830
|
|
|
$this->config['rootLine'] = $this->tmpl->rootLine; |
1831
|
|
|
// Class for render Header and Footer parts |
1832
|
|
|
if ($this->pSetup['pageHeaderFooterTemplateFile'] ?? false) { |
1833
|
|
|
try { |
1834
|
|
|
$file = GeneralUtility::makeInstance(FilePathSanitizer::class) |
1835
|
|
|
->sanitize((string)$this->pSetup['pageHeaderFooterTemplateFile']); |
1836
|
|
|
$this->pageRenderer->setTemplateFile($file); |
1837
|
|
|
} catch (Exception $e) { |
1838
|
|
|
// do nothing |
1839
|
|
|
} |
1840
|
|
|
} |
1841
|
|
|
} |
1842
|
|
|
$timeTracker->pull(); |
1843
|
|
|
} else { |
1844
|
|
|
$message = 'No TypoScript template found!'; |
1845
|
|
|
$this->logger->alert($message); |
1846
|
|
|
try { |
1847
|
|
|
$response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction( |
1848
|
|
|
$request, |
1849
|
|
|
$message, |
1850
|
|
|
['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_FOUND] |
1851
|
|
|
); |
1852
|
|
|
throw new PropagateResponseException($response, 1533931380); |
1853
|
|
|
} catch (AbstractServerErrorException $e) { |
1854
|
|
|
$exceptionClass = get_class($e); |
1855
|
|
|
throw new $exceptionClass($message, 1294587218); |
1856
|
|
|
} |
1857
|
|
|
} |
1858
|
|
|
} |
1859
|
|
|
|
1860
|
|
|
// No cache |
1861
|
|
|
// Set $this->no_cache TRUE if the config.no_cache value is set! |
1862
|
|
|
if ($this->config['config']['no_cache'] ?? false) { |
1863
|
|
|
$this->set_no_cache('config.no_cache is set', true); |
1864
|
|
|
} |
1865
|
|
|
|
1866
|
|
|
// Auto-configure settings when a site is configured |
1867
|
|
|
$this->config['config']['absRefPrefix'] = $this->config['config']['absRefPrefix'] ?? 'auto'; |
1868
|
|
|
|
1869
|
|
|
// Hook for postProcessing the configuration array |
1870
|
|
|
$params = ['config' => &$this->config['config']]; |
1871
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['configArrayPostProc'] ?? [] as $funcRef) { |
1872
|
|
|
GeneralUtility::callUserFunction($funcRef, $params, $this); |
1873
|
|
|
} |
1874
|
|
|
} |
1875
|
|
|
|
1876
|
|
|
/******************************************** |
1877
|
|
|
* |
1878
|
|
|
* Further initialization and data processing |
1879
|
|
|
* |
1880
|
|
|
*******************************************/ |
1881
|
|
|
/** |
1882
|
|
|
* Setting the language key that will be used by the current page. |
1883
|
|
|
* 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. |
1884
|
|
|
* |
1885
|
|
|
* @param ServerRequestInterface|null $request |
1886
|
|
|
* @internal |
1887
|
|
|
*/ |
1888
|
|
|
public function settingLanguage(ServerRequestInterface $request = null) |
1889
|
|
|
{ |
1890
|
|
|
$request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals(); |
1891
|
|
|
$_params = []; |
1892
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_preProcess'] ?? [] as $_funcRef) { |
1893
|
|
|
$ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction |
1894
|
|
|
GeneralUtility::callUserFunction($_funcRef, $_params, $ref); |
1895
|
|
|
} |
1896
|
|
|
|
1897
|
|
|
// Get values from site language |
1898
|
|
|
$languageAspect = LanguageAspectFactory::createFromSiteLanguage($this->language); |
1899
|
|
|
|
1900
|
|
|
$languageId = $languageAspect->getId(); |
1901
|
|
|
$languageContentId = $languageAspect->getContentId(); |
1902
|
|
|
|
1903
|
|
|
$pageTranslationVisibility = new PageTranslationVisibility((int)($this->page['l18n_cfg'] ?? 0)); |
1904
|
|
|
// If sys_language_uid is set to another language than default: |
1905
|
|
|
if ($languageAspect->getId() > 0) { |
1906
|
|
|
// check whether a shortcut is overwritten by a translated page |
1907
|
|
|
// we can only do this now, as this is the place where we get |
1908
|
|
|
// to know about translations |
1909
|
|
|
$this->checkTranslatedShortcut($languageAspect->getId(), $request); |
1910
|
|
|
// Request the overlay record for the sys_language_uid: |
1911
|
|
|
$olRec = $this->sys_page->getPageOverlay($this->id, $languageAspect->getId()); |
1912
|
|
|
if (empty($olRec)) { |
1913
|
|
|
// If requested translation is not available: |
1914
|
|
|
if ($pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists()) { |
1915
|
|
|
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( |
1916
|
|
|
$request, |
1917
|
|
|
'Page is not available in the requested language.', |
1918
|
|
|
['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE] |
1919
|
|
|
); |
1920
|
|
|
throw new PropagateResponseException($response, 1533931388); |
1921
|
|
|
} |
1922
|
|
|
switch ((string)$languageAspect->getLegacyLanguageMode()) { |
1923
|
|
|
case 'strict': |
1924
|
|
|
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( |
1925
|
|
|
$request, |
1926
|
|
|
'Page is not available in the requested language (strict).', |
1927
|
|
|
['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE_STRICT_MODE] |
1928
|
|
|
); |
1929
|
|
|
throw new PropagateResponseException($response, 1533931395); |
1930
|
|
|
case 'fallback': |
1931
|
|
|
case 'content_fallback': |
1932
|
|
|
// Setting content uid (but leaving the sys_language_uid) when a content_fallback |
1933
|
|
|
// value was found. |
1934
|
|
|
foreach ($languageAspect->getFallbackChain() ?? [] as $orderValue) { |
1935
|
|
|
if ($orderValue === '0' || $orderValue === 0 || $orderValue === '') { |
1936
|
|
|
$languageContentId = 0; |
1937
|
|
|
break; |
1938
|
|
|
} |
1939
|
|
|
if (MathUtility::canBeInterpretedAsInteger($orderValue) && !empty($this->sys_page->getPageOverlay($this->id, (int)$orderValue))) { |
1940
|
|
|
$languageContentId = (int)$orderValue; |
1941
|
|
|
break; |
1942
|
|
|
} |
1943
|
|
|
if ($orderValue === 'pageNotFound') { |
1944
|
|
|
// The existing fallbacks have not been found, but instead of continuing |
1945
|
|
|
// page rendering with default language, a "page not found" message should be shown |
1946
|
|
|
// instead. |
1947
|
|
|
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( |
1948
|
|
|
$request, |
1949
|
|
|
'Page is not available in the requested language (fallbacks did not apply).', |
1950
|
|
|
['code' => PageAccessFailureReasons::LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE] |
1951
|
|
|
); |
1952
|
|
|
throw new PropagateResponseException($response, 1533931402); |
1953
|
|
|
} |
1954
|
|
|
} |
1955
|
|
|
break; |
1956
|
|
|
case 'ignore': |
1957
|
|
|
$languageContentId = $languageAspect->getId(); |
1958
|
|
|
break; |
1959
|
|
|
default: |
1960
|
|
|
// Default is that everything defaults to the default language... |
1961
|
|
|
$languageId = ($languageContentId = 0); |
1962
|
|
|
} |
1963
|
|
|
} |
1964
|
|
|
|
1965
|
|
|
// Define the language aspect again now |
1966
|
|
|
$languageAspect = GeneralUtility::makeInstance( |
1967
|
|
|
LanguageAspect::class, |
1968
|
|
|
$languageId, |
1969
|
|
|
$languageContentId, |
1970
|
|
|
$languageAspect->getOverlayType(), |
1971
|
|
|
$languageAspect->getFallbackChain() |
1972
|
|
|
); |
1973
|
|
|
|
1974
|
|
|
// Setting sys_language if an overlay record was found (which it is only if a language is used) |
1975
|
|
|
// We'll do this every time since the language aspect might have changed now |
1976
|
|
|
// Doing this ensures that page properties like the page title are returned in the correct language |
1977
|
|
|
$this->page = $this->sys_page->getPageOverlay($this->page, $languageAspect->getContentId()); |
1978
|
|
|
|
1979
|
|
|
// Update SYS_LASTCHANGED for localized page record |
1980
|
|
|
$this->setRegisterValueForSysLastChanged($this->page); |
1981
|
|
|
} |
1982
|
|
|
|
1983
|
|
|
// Set the language aspect |
1984
|
|
|
$this->context->setAspect('language', $languageAspect); |
1985
|
|
|
|
1986
|
|
|
// Setting sys_language_uid inside sys-page by creating a new page repository |
1987
|
|
|
$this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context); |
1988
|
|
|
// If default language is not available: |
1989
|
|
|
if ((!$languageAspect->getContentId() || !$languageAspect->getId()) |
1990
|
|
|
&& $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage() |
1991
|
|
|
) { |
1992
|
|
|
$message = 'Page is not available in default language.'; |
1993
|
|
|
$this->logPageAccessFailure($message, $request); |
1994
|
|
|
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction( |
1995
|
|
|
$request, |
1996
|
|
|
$message, |
1997
|
|
|
['code' => PageAccessFailureReasons::LANGUAGE_DEFAULT_NOT_AVAILABLE] |
1998
|
|
|
); |
1999
|
|
|
throw new PropagateResponseException($response, 1533931423); |
2000
|
|
|
} |
2001
|
|
|
|
2002
|
|
|
if ($languageAspect->getId() > 0) { |
2003
|
|
|
$this->updateRootLinesWithTranslations(); |
2004
|
|
|
} |
2005
|
|
|
|
2006
|
|
|
$_params = []; |
2007
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_postProcess'] ?? [] as $_funcRef) { |
2008
|
|
|
GeneralUtility::callUserFunction($_funcRef, $_params, $this); |
2009
|
|
|
} |
2010
|
|
|
} |
2011
|
|
|
|
2012
|
|
|
/** |
2013
|
|
|
* Updating content of the two rootLines IF the language key is set! |
2014
|
|
|
*/ |
2015
|
|
|
protected function updateRootLinesWithTranslations() |
2016
|
|
|
{ |
2017
|
|
|
try { |
2018
|
|
|
$this->rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $this->id, $this->MP, $this->context)->get(); |
2019
|
|
|
} catch (RootLineException $e) { |
2020
|
|
|
$this->rootLine = []; |
2021
|
|
|
} |
2022
|
|
|
} |
2023
|
|
|
|
2024
|
|
|
/** |
2025
|
|
|
* Checks whether a translated shortcut page has a different shortcut |
2026
|
|
|
* target than the original language page. |
2027
|
|
|
* If that is the case, things get corrected to follow that alternative |
2028
|
|
|
* shortcut |
2029
|
|
|
* @param int $languageId |
2030
|
|
|
* @param ServerRequestInterface $request |
2031
|
|
|
*/ |
2032
|
|
|
protected function checkTranslatedShortcut(int $languageId, ServerRequestInterface $request) |
2033
|
|
|
{ |
2034
|
|
|
if (!is_null($this->originalShortcutPage)) { |
2035
|
|
|
$originalShortcutPageOverlay = $this->sys_page->getPageOverlay($this->originalShortcutPage['uid'], $languageId); |
2036
|
|
|
if (!empty($originalShortcutPageOverlay['shortcut']) && $originalShortcutPageOverlay['shortcut'] != $this->id) { |
2037
|
|
|
// the translation of the original shortcut page has a different shortcut target! |
2038
|
|
|
// set the correct page and id |
2039
|
|
|
$shortcut = $this->sys_page->getPageShortcut($originalShortcutPageOverlay['shortcut'], $originalShortcutPageOverlay['shortcut_mode'], $originalShortcutPageOverlay['uid']); |
2040
|
|
|
$this->id = ($this->contentPid = $shortcut['uid']); |
2041
|
|
|
$this->page = $this->sys_page->getPage($this->id); |
2042
|
|
|
// Fix various effects on things like menus f.e. |
2043
|
|
|
$this->fetch_the_id($request); |
2044
|
|
|
$this->tmpl->rootLine = array_reverse($this->rootLine); |
2045
|
|
|
} |
2046
|
|
|
} |
2047
|
|
|
} |
2048
|
|
|
|
2049
|
|
|
/** |
2050
|
|
|
* Calculates and sets the internal linkVars based upon the current request parameters |
2051
|
|
|
* and the setting "config.linkVars". |
2052
|
|
|
* |
2053
|
|
|
* @param array $queryParams $_GET (usually called with a PSR-7 $request->getQueryParams()) |
2054
|
|
|
*/ |
2055
|
|
|
public function calculateLinkVars(array $queryParams) |
2056
|
|
|
{ |
2057
|
|
|
$this->linkVars = ''; |
2058
|
|
|
$adminCommand = $queryParams['ADMCMD_prev'] ?? ''; |
2059
|
|
|
if (($adminCommand === 'LIVE' || $adminCommand === 'IGNORE') && $this->isBackendUserLoggedIn()) { |
2060
|
|
|
$this->config['config']['linkVars'] = ltrim(($this->config['config']['linkVars'] ?? '') . ',ADMCMD_prev', ','); |
2061
|
|
|
} |
2062
|
|
|
if (empty($this->config['config']['linkVars'])) { |
2063
|
|
|
return; |
2064
|
|
|
} |
2065
|
|
|
|
2066
|
|
|
$linkVars = $this->splitLinkVarsString((string)$this->config['config']['linkVars']); |
2067
|
|
|
|
2068
|
|
|
if (empty($linkVars)) { |
2069
|
|
|
return; |
2070
|
|
|
} |
2071
|
|
|
foreach ($linkVars as $linkVar) { |
2072
|
|
|
$test = ''; |
2073
|
|
|
if (preg_match('/^(.*)\\((.+)\\)$/', $linkVar, $match)) { |
2074
|
|
|
$linkVar = trim($match[1]); |
2075
|
|
|
$test = trim($match[2]); |
2076
|
|
|
} |
2077
|
|
|
|
2078
|
|
|
$keys = explode('|', $linkVar); |
2079
|
|
|
$numberOfLevels = count($keys); |
2080
|
|
|
$rootKey = trim($keys[0]); |
2081
|
|
|
if (!isset($queryParams[$rootKey])) { |
2082
|
|
|
continue; |
2083
|
|
|
} |
2084
|
|
|
$value = $queryParams[$rootKey]; |
2085
|
|
|
for ($i = 1; $i < $numberOfLevels; $i++) { |
2086
|
|
|
$currentKey = trim($keys[$i]); |
2087
|
|
|
if (isset($value[$currentKey])) { |
2088
|
|
|
$value = $value[$currentKey]; |
2089
|
|
|
} else { |
2090
|
|
|
$value = false; |
2091
|
|
|
break; |
2092
|
|
|
} |
2093
|
|
|
} |
2094
|
|
|
if ($value !== false) { |
2095
|
|
|
$parameterName = $keys[0]; |
2096
|
|
|
for ($i = 1; $i < $numberOfLevels; $i++) { |
2097
|
|
|
$parameterName .= '[' . $keys[$i] . ']'; |
2098
|
|
|
} |
2099
|
|
|
if (!is_array($value)) { |
2100
|
|
|
$temp = rawurlencode($value); |
2101
|
|
|
if ($test !== '' && !$this->isAllowedLinkVarValue($temp, $test)) { |
2102
|
|
|
// Error: This value was not allowed for this key |
2103
|
|
|
continue; |
2104
|
|
|
} |
2105
|
|
|
$value = '&' . $parameterName . '=' . $temp; |
2106
|
|
|
} else { |
2107
|
|
|
if ($test !== '' && $test !== 'array') { |
2108
|
|
|
// Error: This key must not be an array! |
2109
|
|
|
continue; |
2110
|
|
|
} |
2111
|
|
|
$value = HttpUtility::buildQueryString([$parameterName => $value], '&'); |
2112
|
|
|
} |
2113
|
|
|
$this->linkVars .= $value; |
2114
|
|
|
} |
2115
|
|
|
} |
2116
|
|
|
} |
2117
|
|
|
|
2118
|
|
|
/** |
2119
|
|
|
* Split the link vars string by "," but not if the "," is inside of braces |
2120
|
|
|
* |
2121
|
|
|
* @param string $string |
2122
|
|
|
* |
2123
|
|
|
* @return array |
2124
|
|
|
*/ |
2125
|
|
|
protected function splitLinkVarsString(string $string): array |
2126
|
|
|
{ |
2127
|
|
|
$tempCommaReplacementString = '###KASPER###'; |
2128
|
|
|
|
2129
|
|
|
// replace every "," wrapped in "()" by a "unique" string |
2130
|
|
|
$string = preg_replace_callback('/\((?>[^()]|(?R))*\)/', function ($result) use ($tempCommaReplacementString) { |
2131
|
|
|
return str_replace(',', $tempCommaReplacementString, $result[0]); |
2132
|
|
|
}, $string) ?? ''; |
2133
|
|
|
|
2134
|
|
|
$string = GeneralUtility::trimExplode(',', $string); |
2135
|
|
|
|
2136
|
|
|
// replace all "unique" strings back to "," |
2137
|
|
|
return str_replace($tempCommaReplacementString, ',', $string); |
2138
|
|
|
} |
2139
|
|
|
|
2140
|
|
|
/** |
2141
|
|
|
* Checks if the value defined in "config.linkVars" contains an allowed value. |
2142
|
|
|
* Otherwise, return FALSE which means the value will not be added to any links. |
2143
|
|
|
* |
2144
|
|
|
* @param string $haystack The string in which to find $needle |
2145
|
|
|
* @param string $needle The string to find in $haystack |
2146
|
|
|
* @return bool Returns TRUE if $needle matches or is found in $haystack |
2147
|
|
|
*/ |
2148
|
|
|
protected function isAllowedLinkVarValue(string $haystack, string $needle): bool |
2149
|
|
|
{ |
2150
|
|
|
$isAllowed = false; |
2151
|
|
|
// Integer |
2152
|
|
|
if ($needle === 'int' || $needle === 'integer') { |
2153
|
|
|
if (MathUtility::canBeInterpretedAsInteger($haystack)) { |
2154
|
|
|
$isAllowed = true; |
2155
|
|
|
} |
2156
|
|
|
} elseif (preg_match('/^\\/.+\\/[imsxeADSUXu]*$/', $needle)) { |
2157
|
|
|
// Regular expression, only "//" is allowed as delimiter |
2158
|
|
|
if (@preg_match($needle, $haystack)) { |
2159
|
|
|
$isAllowed = true; |
2160
|
|
|
} |
2161
|
|
|
} elseif (strpos($needle, '-') !== false) { |
2162
|
|
|
// Range |
2163
|
|
|
if (MathUtility::canBeInterpretedAsInteger($haystack)) { |
2164
|
|
|
$range = explode('-', $needle); |
2165
|
|
|
if ($range[0] <= $haystack && $range[1] >= $haystack) { |
2166
|
|
|
$isAllowed = true; |
2167
|
|
|
} |
2168
|
|
|
} |
2169
|
|
|
} elseif (strpos($needle, '|') !== false) { |
2170
|
|
|
// List |
2171
|
|
|
// Trim the input |
2172
|
|
|
$haystack = str_replace(' ', '', $haystack); |
2173
|
|
|
if (strpos('|' . $needle . '|', '|' . $haystack . '|') !== false) { |
2174
|
|
|
$isAllowed = true; |
2175
|
|
|
} |
2176
|
|
|
} elseif ((string)$needle === (string)$haystack) { |
2177
|
|
|
// String comparison |
2178
|
|
|
$isAllowed = true; |
2179
|
|
|
} |
2180
|
|
|
return $isAllowed; |
2181
|
|
|
} |
2182
|
|
|
|
2183
|
|
|
/** |
2184
|
|
|
* Returns URI of target page, if the current page is an overlaid mountpoint. |
2185
|
|
|
* |
2186
|
|
|
* If the current page is of type mountpoint and should be overlaid with the contents of the mountpoint page |
2187
|
|
|
* and is accessed directly, the user will be redirected to the mountpoint context. |
2188
|
|
|
* @internal |
2189
|
|
|
* @param ServerRequestInterface $request |
2190
|
|
|
* @return string|null |
2191
|
|
|
*/ |
2192
|
|
|
public function getRedirectUriForMountPoint(ServerRequestInterface $request): ?string |
2193
|
|
|
{ |
2194
|
|
|
if (!empty($this->originalMountPointPage) && (int)$this->originalMountPointPage['doktype'] === PageRepository::DOKTYPE_MOUNTPOINT) { |
2195
|
|
|
return $this->getUriToCurrentPageForRedirect($request); |
2196
|
|
|
} |
2197
|
|
|
|
2198
|
|
|
return null; |
2199
|
|
|
} |
2200
|
|
|
|
2201
|
|
|
/** |
2202
|
|
|
* Returns URI of target page, if the current page is a Shortcut. |
2203
|
|
|
* |
2204
|
|
|
* If the current page is of type shortcut and accessed directly via its URL, |
2205
|
|
|
* the user will be redirected to shortcut target. |
2206
|
|
|
* |
2207
|
|
|
* @internal |
2208
|
|
|
* @param ServerRequestInterface $request |
2209
|
|
|
* @return string|null |
2210
|
|
|
*/ |
2211
|
|
|
public function getRedirectUriForShortcut(ServerRequestInterface $request): ?string |
2212
|
|
|
{ |
2213
|
|
|
if (!empty($this->originalShortcutPage) && $this->originalShortcutPage['doktype'] == PageRepository::DOKTYPE_SHORTCUT) { |
2214
|
|
|
return $this->getUriToCurrentPageForRedirect($request); |
2215
|
|
|
} |
2216
|
|
|
|
2217
|
|
|
return null; |
2218
|
|
|
} |
2219
|
|
|
|
2220
|
|
|
/** |
2221
|
|
|
* Instantiate \TYPO3\CMS\Frontend\ContentObject to generate the correct target URL |
2222
|
|
|
* |
2223
|
|
|
* @param ServerRequestInterface $request |
2224
|
|
|
* @return string |
2225
|
|
|
*/ |
2226
|
|
|
protected function getUriToCurrentPageForRedirect(ServerRequestInterface $request): string |
2227
|
|
|
{ |
2228
|
|
|
$this->calculateLinkVars($request->getQueryParams()); |
2229
|
|
|
$parameter = $this->page['uid']; |
2230
|
|
|
if ($this->type && MathUtility::canBeInterpretedAsInteger($this->type)) { |
2231
|
|
|
$parameter .= ',' . $this->type; |
2232
|
|
|
} |
2233
|
|
|
return GeneralUtility::makeInstance(ContentObjectRenderer::class, $this)->typoLink_URL([ |
2234
|
|
|
'parameter' => $parameter, |
2235
|
|
|
'addQueryString' => true, |
2236
|
|
|
'addQueryString.' => ['exclude' => 'id'], |
2237
|
|
|
'forceAbsoluteUrl' => true |
2238
|
|
|
]); |
2239
|
|
|
} |
2240
|
|
|
|
2241
|
|
|
/******************************************** |
2242
|
|
|
* |
2243
|
|
|
* Page generation; cache handling |
2244
|
|
|
* |
2245
|
|
|
*******************************************/ |
2246
|
|
|
/** |
2247
|
|
|
* Returns TRUE if the page should be generated. |
2248
|
|
|
* That is if no URL handler is active and the cacheContentFlag is not set. |
2249
|
|
|
* |
2250
|
|
|
* @return bool |
2251
|
|
|
*/ |
2252
|
|
|
public function isGeneratePage() |
2253
|
|
|
{ |
2254
|
|
|
return !$this->cacheContentFlag; |
2255
|
|
|
} |
2256
|
|
|
|
2257
|
|
|
/** |
2258
|
|
|
* Set cache content to $this->content |
2259
|
|
|
*/ |
2260
|
|
|
protected function realPageCacheContent() |
2261
|
|
|
{ |
2262
|
|
|
// seconds until a cached page is too old |
2263
|
|
|
$cacheTimeout = $this->get_cache_timeout(); |
2264
|
|
|
$timeOutTime = $GLOBALS['EXEC_TIME'] + $cacheTimeout; |
2265
|
|
|
$usePageCache = true; |
2266
|
|
|
// Hook for deciding whether page cache should be written to the cache backend or not |
2267
|
|
|
// NOTE: as hooks are called in a loop, the last hook will have the final word (however each |
2268
|
|
|
// hook receives the current status of the $usePageCache flag) |
2269
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['usePageCache'] ?? [] as $className) { |
2270
|
|
|
$usePageCache = GeneralUtility::makeInstance($className)->usePageCache($this, $usePageCache); |
2271
|
|
|
} |
2272
|
|
|
// Write the page to cache, if necessary |
2273
|
|
|
if ($usePageCache) { |
2274
|
|
|
$this->setPageCacheContent($this->content, $this->config, $timeOutTime); |
2275
|
|
|
} |
2276
|
|
|
// Hook for cache post processing (eg. writing static files!) |
2277
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['insertPageIncache'] ?? [] as $className) { |
2278
|
|
|
GeneralUtility::makeInstance($className)->insertPageIncache($this, $timeOutTime); |
2279
|
|
|
} |
2280
|
|
|
} |
2281
|
|
|
|
2282
|
|
|
/** |
2283
|
|
|
* Sets cache content; Inserts the content string into the cache_pages cache. |
2284
|
|
|
* |
2285
|
|
|
* @param string $content The content to store in the HTML field of the cache table |
2286
|
|
|
* @param mixed $data The additional cache_data array, fx. $this->config |
2287
|
|
|
* @param int $expirationTstamp Expiration timestamp |
2288
|
|
|
* @see realPageCacheContent() |
2289
|
|
|
*/ |
2290
|
|
|
protected function setPageCacheContent($content, $data, $expirationTstamp) |
2291
|
|
|
{ |
2292
|
|
|
$cacheData = [ |
2293
|
|
|
'identifier' => $this->newHash, |
2294
|
|
|
'page_id' => $this->id, |
2295
|
|
|
'content' => $content, |
2296
|
|
|
'cache_data' => $data, |
2297
|
|
|
'expires' => $expirationTstamp, |
2298
|
|
|
'tstamp' => $GLOBALS['EXEC_TIME'], |
2299
|
|
|
'pageTitleInfo' => [ |
2300
|
|
|
'title' => $this->page['title'], |
2301
|
|
|
'indexedDocTitle' => $this->indexedDocTitle |
2302
|
|
|
] |
2303
|
|
|
]; |
2304
|
|
|
$this->cacheExpires = $expirationTstamp; |
2305
|
|
|
$this->pageCacheTags[] = 'pageId_' . $cacheData['page_id']; |
2306
|
|
|
// Respect the page cache when content of pid is shown |
2307
|
|
|
if ($this->id !== $this->contentPid) { |
2308
|
|
|
$this->pageCacheTags[] = 'pageId_' . $this->contentPid; |
2309
|
|
|
} |
2310
|
|
|
if (!empty($this->page['cache_tags'])) { |
2311
|
|
|
$tags = GeneralUtility::trimExplode(',', $this->page['cache_tags'], true); |
2312
|
|
|
$this->pageCacheTags = array_merge($this->pageCacheTags, $tags); |
2313
|
|
|
} |
2314
|
|
|
// Add the cache themselves as well, because they are fetched by getPageCacheTags() |
2315
|
|
|
$cacheData['cacheTags'] = $this->pageCacheTags; |
2316
|
|
|
$this->pageCache->set($this->newHash, $cacheData, $this->pageCacheTags, $expirationTstamp - $GLOBALS['EXEC_TIME']); |
2317
|
|
|
} |
2318
|
|
|
|
2319
|
|
|
/** |
2320
|
|
|
* Clears cache content (for $this->newHash) |
2321
|
|
|
*/ |
2322
|
|
|
public function clearPageCacheContent() |
2323
|
|
|
{ |
2324
|
|
|
$this->pageCache->remove($this->newHash); |
2325
|
|
|
} |
2326
|
|
|
|
2327
|
|
|
/** |
2328
|
|
|
* Sets sys last changed |
2329
|
|
|
* Setting the SYS_LASTCHANGED value in the pagerecord: This value will thus be set to the highest tstamp of records rendered on the page. This includes all records with no regard to hidden records, userprotection and so on. |
2330
|
|
|
* |
2331
|
|
|
* @see ContentObjectRenderer::lastChanged() |
2332
|
|
|
*/ |
2333
|
|
|
protected function setSysLastChanged() |
2334
|
|
|
{ |
2335
|
|
|
// We only update the info if browsing the live workspace |
2336
|
|
|
if ($this->page['SYS_LASTCHANGED'] < (int)$this->register['SYS_LASTCHANGED'] && !$this->doWorkspacePreview()) { |
2337
|
|
|
$connection = GeneralUtility::makeInstance(ConnectionPool::class) |
2338
|
|
|
->getConnectionForTable('pages'); |
2339
|
|
|
$pageId = $this->page['_PAGES_OVERLAY_UID'] ?? $this->id; |
2340
|
|
|
$connection->update( |
2341
|
|
|
'pages', |
2342
|
|
|
[ |
2343
|
|
|
'SYS_LASTCHANGED' => (int)$this->register['SYS_LASTCHANGED'] |
2344
|
|
|
], |
2345
|
|
|
[ |
2346
|
|
|
'uid' => (int)$pageId |
2347
|
|
|
] |
2348
|
|
|
); |
2349
|
|
|
} |
2350
|
|
|
} |
2351
|
|
|
|
2352
|
|
|
/** |
2353
|
|
|
* Set the SYS_LASTCHANGED register value, is also called when a translated page is in use, |
2354
|
|
|
* so the register reflects the state of the translated page, not the page in the default language. |
2355
|
|
|
* |
2356
|
|
|
* @param array $page |
2357
|
|
|
* @internal |
2358
|
|
|
*/ |
2359
|
|
|
protected function setRegisterValueForSysLastChanged(array $page): void |
2360
|
|
|
{ |
2361
|
|
|
$this->register['SYS_LASTCHANGED'] = (int)$page['tstamp']; |
2362
|
|
|
if ($this->register['SYS_LASTCHANGED'] < (int)$page['SYS_LASTCHANGED']) { |
2363
|
|
|
$this->register['SYS_LASTCHANGED'] = (int)$page['SYS_LASTCHANGED']; |
2364
|
|
|
} |
2365
|
|
|
} |
2366
|
|
|
|
2367
|
|
|
/** |
2368
|
|
|
* Release pending locks |
2369
|
|
|
* |
2370
|
|
|
* @internal |
2371
|
|
|
*/ |
2372
|
|
|
public function releaseLocks() |
2373
|
|
|
{ |
2374
|
|
|
$this->releaseLock('pagesection'); |
2375
|
|
|
$this->releaseLock('pages'); |
2376
|
|
|
} |
2377
|
|
|
|
2378
|
|
|
/** |
2379
|
|
|
* Adds tags to this page's cache entry, you can then f.e. remove cache |
2380
|
|
|
* entries by tag |
2381
|
|
|
* |
2382
|
|
|
* @param array $tags An array of tag |
2383
|
|
|
*/ |
2384
|
|
|
public function addCacheTags(array $tags) |
2385
|
|
|
{ |
2386
|
|
|
$this->pageCacheTags = array_merge($this->pageCacheTags, $tags); |
2387
|
|
|
} |
2388
|
|
|
|
2389
|
|
|
/** |
2390
|
|
|
* @return array |
2391
|
|
|
*/ |
2392
|
|
|
public function getPageCacheTags(): array |
2393
|
|
|
{ |
2394
|
|
|
return $this->pageCacheTags; |
2395
|
|
|
} |
2396
|
|
|
|
2397
|
|
|
/******************************************** |
2398
|
|
|
* |
2399
|
|
|
* Page generation; rendering and inclusion |
2400
|
|
|
* |
2401
|
|
|
*******************************************/ |
2402
|
|
|
/** |
2403
|
|
|
* Does some processing BEFORE the page content is generated / built. |
2404
|
|
|
*/ |
2405
|
|
|
public function generatePage_preProcessing() |
2406
|
|
|
{ |
2407
|
|
|
// Same codeline as in getFromCache(). But $this->all has been changed by |
2408
|
|
|
// \TYPO3\CMS\Core\TypoScript\TemplateService::start() in the meantime, so this must be called again! |
2409
|
|
|
$this->newHash = $this->getHash(); |
2410
|
|
|
|
2411
|
|
|
// Used as a safety check in case a PHP script is falsely disabling $this->no_cache during page generation. |
2412
|
|
|
$this->no_cacheBeforePageGen = $this->no_cache; |
2413
|
|
|
} |
2414
|
|
|
|
2415
|
|
|
/** |
2416
|
|
|
* Check the value of "content_from_pid" of the current page record, and see if the current request |
2417
|
|
|
* should actually show content from another page. |
2418
|
|
|
* |
2419
|
|
|
* By using $TSFE->getPageAndRootline() on the cloned object, all rootline restrictions (extendToSubPages) |
2420
|
|
|
* are evaluated as well. |
2421
|
|
|
* |
2422
|
|
|
* @param ServerRequestInterface $request |
2423
|
|
|
* @return int the current page ID or another one if resolved properly - usually set to $this->contentPid |
2424
|
|
|
*/ |
2425
|
|
|
protected function resolveContentPid(ServerRequestInterface $request): int |
2426
|
|
|
{ |
2427
|
|
|
if (!isset($this->page['content_from_pid']) || empty($this->page['content_from_pid'])) { |
2428
|
|
|
return (int)$this->id; |
2429
|
|
|
} |
2430
|
|
|
// make REAL copy of TSFE object - not reference! |
2431
|
|
|
$temp_copy_TSFE = clone $this; |
2432
|
|
|
// Set ->id to the content_from_pid value - we are going to evaluate this pid as was it a given id for a page-display! |
2433
|
|
|
$temp_copy_TSFE->id = $this->page['content_from_pid']; |
2434
|
|
|
$temp_copy_TSFE->MP = ''; |
2435
|
|
|
$temp_copy_TSFE->getPageAndRootline($request); |
2436
|
|
|
return (int)$temp_copy_TSFE->id; |
2437
|
|
|
} |
2438
|
|
|
/** |
2439
|
|
|
* Sets up TypoScript "config." options and set properties in $TSFE. |
2440
|
|
|
* |
2441
|
|
|
* @param ServerRequestInterface $request |
2442
|
|
|
*/ |
2443
|
|
|
public function preparePageContentGeneration(ServerRequestInterface $request) |
2444
|
|
|
{ |
2445
|
|
|
$this->getTimeTracker()->push('Prepare page content generation'); |
2446
|
|
|
$this->contentPid = $this->resolveContentPid($request); |
2447
|
|
|
// Global vars... |
2448
|
|
|
$this->indexedDocTitle = $this->page['title'] ?? null; |
2449
|
|
|
// Base url: |
2450
|
|
|
if (isset($this->config['config']['baseURL'])) { |
2451
|
|
|
$this->baseUrl = $this->config['config']['baseURL']; |
2452
|
|
|
} |
2453
|
|
|
// Internal and External target defaults |
2454
|
|
|
$this->intTarget = (string)($this->config['config']['intTarget'] ?? ''); |
2455
|
|
|
$this->extTarget = (string)($this->config['config']['extTarget'] ?? ''); |
2456
|
|
|
$this->fileTarget = (string)($this->config['config']['fileTarget'] ?? ''); |
2457
|
|
|
$this->spamProtectEmailAddresses = $this->config['config']['spamProtectEmailAddresses'] ?? 0; |
2458
|
|
|
if ($this->spamProtectEmailAddresses !== 'ascii') { |
2459
|
|
|
$this->spamProtectEmailAddresses = MathUtility::forceIntegerInRange($this->spamProtectEmailAddresses, -10, 10, 0); |
2460
|
|
|
} |
2461
|
|
|
// calculate the absolute path prefix |
2462
|
|
|
if (!empty($this->config['config']['absRefPrefix'])) { |
2463
|
|
|
$absRefPrefix = trim($this->config['config']['absRefPrefix']); |
2464
|
|
|
if ($absRefPrefix === 'auto') { |
2465
|
|
|
$this->absRefPrefix = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'); |
2466
|
|
|
} else { |
2467
|
|
|
$this->absRefPrefix = $absRefPrefix; |
2468
|
|
|
} |
2469
|
|
|
} else { |
2470
|
|
|
$this->absRefPrefix = ''; |
2471
|
|
|
} |
2472
|
|
|
$this->ATagParams = trim($this->config['config']['ATagParams'] ?? '') ? ' ' . trim($this->config['config']['ATagParams']) : ''; |
2473
|
|
|
$this->initializeSearchWordData($request->getParsedBody()['sword_list'] ?? $request->getQueryParams()['sword_list'] ?? null); |
2474
|
|
|
// linkVars |
2475
|
|
|
$this->calculateLinkVars($request->getQueryParams()); |
2476
|
|
|
// Setting XHTML-doctype from doctype |
2477
|
|
|
if (!isset($this->config['config']['xhtmlDoctype']) || !$this->config['config']['xhtmlDoctype']) { |
2478
|
|
|
$this->config['config']['xhtmlDoctype'] = $this->config['config']['doctype'] ?? ''; |
2479
|
|
|
} |
2480
|
|
|
if ($this->config['config']['xhtmlDoctype']) { |
2481
|
|
|
$this->xhtmlDoctype = $this->config['config']['xhtmlDoctype']; |
2482
|
|
|
// Checking XHTML-docytpe |
2483
|
|
|
switch ((string)$this->config['config']['xhtmlDoctype']) { |
2484
|
|
|
case 'xhtml_trans': |
2485
|
|
|
case 'xhtml_strict': |
2486
|
|
|
$this->xhtmlVersion = 100; |
2487
|
|
|
break; |
2488
|
|
|
case 'xhtml_basic': |
2489
|
|
|
$this->xhtmlVersion = 105; |
2490
|
|
|
break; |
2491
|
|
|
case 'xhtml_11': |
2492
|
|
|
case 'xhtml+rdfa_10': |
2493
|
|
|
$this->xhtmlVersion = 110; |
2494
|
|
|
break; |
2495
|
|
|
default: |
2496
|
|
|
$this->pageRenderer->setRenderXhtml(false); |
2497
|
|
|
$this->xhtmlDoctype = ''; |
2498
|
|
|
$this->xhtmlVersion = 0; |
2499
|
|
|
} |
2500
|
|
|
} else { |
2501
|
|
|
$this->pageRenderer->setRenderXhtml(false); |
2502
|
|
|
} |
2503
|
|
|
|
2504
|
|
|
// Global content object |
2505
|
|
|
$this->newCObj($request); |
2506
|
|
|
$this->getTimeTracker()->pull(); |
2507
|
|
|
} |
2508
|
|
|
|
2509
|
|
|
/** |
2510
|
|
|
* Fills the sWordList property and builds the regular expression in TSFE that can be used to split |
2511
|
|
|
* strings by the submitted search words. |
2512
|
|
|
* |
2513
|
|
|
* @param mixed $searchWords - usually an array, but we can't be sure (yet) |
2514
|
|
|
* @see sWordList |
2515
|
|
|
* @see sWordRegEx |
2516
|
|
|
*/ |
2517
|
|
|
protected function initializeSearchWordData($searchWords) |
2518
|
|
|
{ |
2519
|
|
|
$this->sWordRegEx = ''; |
2520
|
|
|
$this->sWordList = $searchWords ?? ''; |
2521
|
|
|
if (is_array($this->sWordList)) { |
2522
|
|
|
$space = !empty($this->config['config']['sword_standAlone'] ?? null) ? '[[:space:]]' : ''; |
2523
|
|
|
$regexpParts = []; |
2524
|
|
|
foreach ($this->sWordList as $val) { |
2525
|
|
|
if (trim($val) !== '') { |
2526
|
|
|
$regexpParts[] = $space . preg_quote($val, '/') . $space; |
2527
|
|
|
} |
2528
|
|
|
} |
2529
|
|
|
$this->sWordRegEx = implode('|', $regexpParts); |
2530
|
|
|
} |
2531
|
|
|
} |
2532
|
|
|
|
2533
|
|
|
/** |
2534
|
|
|
* Does processing of the content after the page content was generated. |
2535
|
|
|
* |
2536
|
|
|
* This includes caching the page, indexing the page (if configured) and setting sysLastChanged |
2537
|
|
|
*/ |
2538
|
|
|
public function generatePage_postProcessing() |
2539
|
|
|
{ |
2540
|
|
|
$this->setAbsRefPrefix(); |
2541
|
|
|
// This is to ensure, that the page is NOT cached if the no_cache parameter was set before the page was generated. This is a safety precaution, as it could have been unset by some script. |
2542
|
|
|
if ($this->no_cacheBeforePageGen) { |
2543
|
|
|
$this->set_no_cache('no_cache has been set before the page was generated - safety check', true); |
2544
|
|
|
} |
2545
|
|
|
// Hook for post-processing of page content cached/non-cached: |
2546
|
|
|
$_params = ['pObj' => &$this]; |
2547
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-all'] ?? [] as $_funcRef) { |
2548
|
|
|
GeneralUtility::callUserFunction($_funcRef, $_params, $this); |
2549
|
|
|
} |
2550
|
|
|
// Processing if caching is enabled: |
2551
|
|
|
if (!$this->no_cache) { |
2552
|
|
|
// Hook for post-processing of page content before being cached: |
2553
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-cached'] ?? [] as $_funcRef) { |
2554
|
|
|
GeneralUtility::callUserFunction($_funcRef, $_params, $this); |
2555
|
|
|
} |
2556
|
|
|
} |
2557
|
|
|
// Convert charset for output. Any hooks before (including indexed search) will have to convert from UTF-8 to the target |
2558
|
|
|
// charset as well. |
2559
|
|
|
$this->content = $this->convOutputCharset($this->content); |
2560
|
|
|
// Storing for cache: |
2561
|
|
|
if (!$this->no_cache) { |
2562
|
|
|
$this->realPageCacheContent(); |
2563
|
|
|
} |
2564
|
|
|
// Sets sys-last-change: |
2565
|
|
|
$this->setSysLastChanged(); |
2566
|
|
|
} |
2567
|
|
|
|
2568
|
|
|
/** |
2569
|
|
|
* Generate the page title, can be called multiple times, |
2570
|
|
|
* as PageTitleProvider might have been modified by an uncached plugin etc. |
2571
|
|
|
* |
2572
|
|
|
* @return string the generated page title |
2573
|
|
|
*/ |
2574
|
|
|
public function generatePageTitle(): string |
2575
|
|
|
{ |
2576
|
|
|
// Check for a custom pageTitleSeparator, and perform stdWrap on it |
2577
|
|
|
$pageTitleSeparator = (string)$this->cObj->stdWrapValue('pageTitleSeparator', $this->config['config'] ?? []); |
2578
|
|
|
if ($pageTitleSeparator !== '' && $pageTitleSeparator === ($this->config['config']['pageTitleSeparator'] ?? '')) { |
2579
|
|
|
$pageTitleSeparator .= ' '; |
2580
|
|
|
} |
2581
|
|
|
|
2582
|
|
|
$titleProvider = GeneralUtility::makeInstance(PageTitleProviderManager::class); |
2583
|
|
|
if (!empty($this->config['config']['pageTitleCache'])) { |
2584
|
|
|
$titleProvider->setPageTitleCache($this->config['config']['pageTitleCache']); |
2585
|
|
|
} |
2586
|
|
|
$pageTitle = $titleProvider->getTitle(); |
2587
|
|
|
$this->config['config']['pageTitleCache'] = $titleProvider->getPageTitleCache(); |
2588
|
|
|
|
2589
|
|
|
if ($pageTitle !== '') { |
2590
|
|
|
$this->indexedDocTitle = $pageTitle; |
2591
|
|
|
} |
2592
|
|
|
|
2593
|
|
|
$titleTagContent = $this->printTitle( |
2594
|
|
|
$pageTitle, |
2595
|
|
|
(bool)($this->config['config']['noPageTitle'] ?? false), |
2596
|
|
|
(bool)($this->config['config']['pageTitleFirst'] ?? false), |
2597
|
|
|
$pageTitleSeparator |
2598
|
|
|
); |
2599
|
|
|
$this->config['config']['pageTitle'] = $titleTagContent; |
2600
|
|
|
// stdWrap around the title tag |
2601
|
|
|
$titleTagContent = $this->cObj->stdWrapValue('pageTitle', $this->config['config']); |
2602
|
|
|
|
2603
|
|
|
// config.noPageTitle = 2 - means do not render the page title |
2604
|
|
|
if (isset($this->config['config']['noPageTitle']) && (int)$this->config['config']['noPageTitle'] === 2) { |
2605
|
|
|
$titleTagContent = ''; |
2606
|
|
|
} |
2607
|
|
|
if ($titleTagContent !== '') { |
2608
|
|
|
$this->pageRenderer->setTitle($titleTagContent); |
2609
|
|
|
} |
2610
|
|
|
return (string)$titleTagContent; |
2611
|
|
|
} |
2612
|
|
|
|
2613
|
|
|
/** |
2614
|
|
|
* Compiles the content for the page <title> tag. |
2615
|
|
|
* |
2616
|
|
|
* @param string $pageTitle The input title string, typically the "title" field of a page's record. |
2617
|
|
|
* @param bool $noTitle If set, then only the site title is outputted |
2618
|
|
|
* @param bool $showTitleFirst If set, then website title and $title is swapped |
2619
|
|
|
* @param string $pageTitleSeparator an alternative to the ": " as the separator between site title and page title |
2620
|
|
|
* @return string The page title on the form "[website title]: [input-title]". Not htmlspecialchar()'ed. |
2621
|
|
|
* @see generatePageTitle() |
2622
|
|
|
*/ |
2623
|
|
|
protected function printTitle(string $pageTitle, bool $noTitle = false, bool $showTitleFirst = false, string $pageTitleSeparator = ''): string |
2624
|
|
|
{ |
2625
|
|
|
$websiteTitle = $this->getWebsiteTitle(); |
2626
|
|
|
$pageTitle = $noTitle ? '' : $pageTitle; |
2627
|
|
|
if ($showTitleFirst) { |
2628
|
|
|
$temp = $websiteTitle; |
2629
|
|
|
$websiteTitle = $pageTitle; |
2630
|
|
|
$pageTitle = $temp; |
2631
|
|
|
} |
2632
|
|
|
// only show a separator if there are both site title and page title |
2633
|
|
|
if ($pageTitle === '' || $websiteTitle === '') { |
2634
|
|
|
$pageTitleSeparator = ''; |
2635
|
|
|
} elseif (empty($pageTitleSeparator)) { |
2636
|
|
|
// use the default separator if non given |
2637
|
|
|
$pageTitleSeparator = ': '; |
2638
|
|
|
} |
2639
|
|
|
return $websiteTitle . $pageTitleSeparator . $pageTitle; |
2640
|
|
|
} |
2641
|
|
|
|
2642
|
|
|
/** |
2643
|
|
|
* @return string |
2644
|
|
|
*/ |
2645
|
|
|
protected function getWebsiteTitle(): string |
2646
|
|
|
{ |
2647
|
|
|
if ($this->language instanceof SiteLanguage |
2648
|
|
|
&& trim($this->language->getWebsiteTitle()) !== '' |
2649
|
|
|
) { |
2650
|
|
|
return trim($this->language->getWebsiteTitle()); |
2651
|
|
|
} |
2652
|
|
|
if ($this->site instanceof SiteInterface |
2653
|
|
|
&& trim($this->site->getConfiguration()['websiteTitle'] ?? '') !== '' |
|
|
|
|
2654
|
|
|
) { |
2655
|
|
|
return trim($this->site->getConfiguration()['websiteTitle']); |
2656
|
|
|
} |
2657
|
|
|
|
2658
|
|
|
return ''; |
2659
|
|
|
} |
2660
|
|
|
|
2661
|
|
|
/** |
2662
|
|
|
* Processes the INTinclude-scripts |
2663
|
|
|
* |
2664
|
|
|
* @param ServerRequestInterface|null $request |
2665
|
|
|
*/ |
2666
|
|
|
public function INTincScript(ServerRequestInterface $request = null) |
2667
|
|
|
{ |
2668
|
|
|
$request = $request ?? $GLOBALS['TYPO3_REQUEST']; |
2669
|
|
|
$this->additionalHeaderData = $this->config['INTincScript_ext']['additionalHeaderData'] ?? []; |
2670
|
|
|
$this->additionalFooterData = $this->config['INTincScript_ext']['additionalFooterData'] ?? []; |
2671
|
|
|
if (empty($this->config['INTincScript_ext']['pageRenderer'])) { |
2672
|
|
|
$this->initPageRenderer(); |
2673
|
|
|
} else { |
2674
|
|
|
/** @var PageRenderer $pageRenderer */ |
2675
|
|
|
$pageRenderer = unserialize($this->config['INTincScript_ext']['pageRenderer']); |
2676
|
|
|
$this->pageRenderer->updateState($pageRenderer->getState()); |
2677
|
|
|
} |
2678
|
|
|
if (!empty($this->config['INTincScript_ext']['assetCollector'])) { |
2679
|
|
|
/** @var AssetCollector $assetCollector */ |
2680
|
|
|
$assetCollector = unserialize($this->config['INTincScript_ext']['assetCollector'], ['allowed_classes' => [AssetCollector::class]]); |
2681
|
|
|
GeneralUtility::makeInstance(AssetCollector::class)->updateState($assetCollector->getState()); |
2682
|
|
|
} |
2683
|
|
|
|
2684
|
|
|
$this->recursivelyReplaceIntPlaceholdersInContent($request); |
2685
|
|
|
$this->getTimeTracker()->push('Substitute header section'); |
2686
|
|
|
$this->INTincScript_loadJSCode(); |
2687
|
|
|
$this->generatePageTitle(); |
2688
|
|
|
|
2689
|
|
|
$this->content = str_replace( |
2690
|
|
|
[ |
2691
|
|
|
'<!--HD_' . $this->config['INTincScript_ext']['divKey'] . '-->', |
2692
|
|
|
'<!--FD_' . $this->config['INTincScript_ext']['divKey'] . '-->', |
2693
|
|
|
], |
2694
|
|
|
[ |
2695
|
|
|
$this->convOutputCharset(implode(LF, $this->additionalHeaderData)), |
2696
|
|
|
$this->convOutputCharset(implode(LF, $this->additionalFooterData)), |
2697
|
|
|
], |
2698
|
|
|
$this->pageRenderer->renderJavaScriptAndCssForProcessingOfUncachedContentObjects($this->content, $this->config['INTincScript_ext']['divKey']) |
2699
|
|
|
); |
2700
|
|
|
// Replace again, because header and footer data and page renderer replacements may introduce additional placeholders (see #44825) |
2701
|
|
|
$this->recursivelyReplaceIntPlaceholdersInContent($request); |
2702
|
|
|
$this->setAbsRefPrefix(); |
2703
|
|
|
$this->getTimeTracker()->pull(); |
2704
|
|
|
} |
2705
|
|
|
|
2706
|
|
|
/** |
2707
|
|
|
* Replaces INT placeholders (COA_INT and USER_INT) in $this->content |
2708
|
|
|
* In case the replacement adds additional placeholders, it loops |
2709
|
|
|
* until no new placeholders are found any more. |
2710
|
|
|
*/ |
2711
|
|
|
protected function recursivelyReplaceIntPlaceholdersInContent(ServerRequestInterface $request) |
2712
|
|
|
{ |
2713
|
|
|
do { |
2714
|
|
|
$nonCacheableData = $this->config['INTincScript']; |
2715
|
|
|
$this->processNonCacheableContentPartsAndSubstituteContentMarkers($nonCacheableData, $request); |
2716
|
|
|
// Check if there were new items added to INTincScript during the previous execution: |
2717
|
|
|
// array_diff_assoc throws notices if values are arrays but not strings. We suppress this here. |
2718
|
|
|
$nonCacheableData = @array_diff_assoc($this->config['INTincScript'], $nonCacheableData); |
2719
|
|
|
$reprocess = count($nonCacheableData) > 0; |
2720
|
|
|
} while ($reprocess); |
2721
|
|
|
} |
2722
|
|
|
|
2723
|
|
|
/** |
2724
|
|
|
* Processes the INTinclude-scripts and substitute in content. |
2725
|
|
|
* |
2726
|
|
|
* Takes $this->content, and splits the content by <!--INT_SCRIPT.12345 --> and then puts the content |
2727
|
|
|
* back together. |
2728
|
|
|
* |
2729
|
|
|
* @param array $nonCacheableData $GLOBALS['TSFE']->config['INTincScript'] or part of it |
2730
|
|
|
* @see INTincScript() |
2731
|
|
|
*/ |
2732
|
|
|
protected function processNonCacheableContentPartsAndSubstituteContentMarkers(array $nonCacheableData, ServerRequestInterface $request) |
2733
|
|
|
{ |
2734
|
|
|
$timeTracker = $this->getTimeTracker(); |
2735
|
|
|
$timeTracker->push('Split content'); |
2736
|
|
|
// Splits content with the key. |
2737
|
|
|
$contentSplitByUncacheableMarkers = explode('<!--INT_SCRIPT.', $this->content); |
2738
|
|
|
$this->content = ''; |
2739
|
|
|
$timeTracker->setTSlogMessage('Parts: ' . count($contentSplitByUncacheableMarkers)); |
2740
|
|
|
$timeTracker->pull(); |
2741
|
|
|
foreach ($contentSplitByUncacheableMarkers as $counter => $contentPart) { |
2742
|
|
|
// If the split had a comment-end after 32 characters it's probably a split-string |
2743
|
|
|
if (substr($contentPart, 32, 3) === '-->') { |
2744
|
|
|
$nonCacheableKey = 'INT_SCRIPT.' . substr($contentPart, 0, 32); |
2745
|
|
|
if (is_array($nonCacheableData[$nonCacheableKey])) { |
2746
|
|
|
$label = 'Include ' . $nonCacheableData[$nonCacheableKey]['type']; |
2747
|
|
|
$timeTracker->push($label); |
2748
|
|
|
$nonCacheableContent = ''; |
2749
|
|
|
$contentObjectRendererForNonCacheable = unserialize($nonCacheableData[$nonCacheableKey]['cObj']); |
2750
|
|
|
/* @var ContentObjectRenderer $contentObjectRendererForNonCacheable */ |
2751
|
|
|
$contentObjectRendererForNonCacheable->setRequest($request); |
2752
|
|
|
switch ($nonCacheableData[$nonCacheableKey]['type']) { |
2753
|
|
|
case 'COA': |
2754
|
|
|
$nonCacheableContent = $contentObjectRendererForNonCacheable->cObjGetSingle('COA', $nonCacheableData[$nonCacheableKey]['conf']); |
2755
|
|
|
break; |
2756
|
|
|
case 'FUNC': |
2757
|
|
|
$nonCacheableContent = $contentObjectRendererForNonCacheable->cObjGetSingle('USER', $nonCacheableData[$nonCacheableKey]['conf']); |
2758
|
|
|
break; |
2759
|
|
|
case 'POSTUSERFUNC': |
2760
|
|
|
$nonCacheableContent = $contentObjectRendererForNonCacheable->callUserFunction($nonCacheableData[$nonCacheableKey]['postUserFunc'], $nonCacheableData[$nonCacheableKey]['conf'], $nonCacheableData[$nonCacheableKey]['content']); |
2761
|
|
|
break; |
2762
|
|
|
} |
2763
|
|
|
$this->content .= $this->convOutputCharset($nonCacheableContent); |
2764
|
|
|
$this->content .= substr($contentPart, 35); |
2765
|
|
|
$timeTracker->pull($nonCacheableContent); |
2766
|
|
|
} else { |
2767
|
|
|
$this->content .= substr($contentPart, 35); |
2768
|
|
|
} |
2769
|
|
|
} elseif ($counter) { |
2770
|
|
|
// If it's not the first entry (which would be "0" of the array keys), then re-add the INT_SCRIPT part |
2771
|
|
|
$this->content .= '<!--INT_SCRIPT.' . $contentPart; |
2772
|
|
|
} else { |
2773
|
|
|
$this->content .= $contentPart; |
2774
|
|
|
} |
2775
|
|
|
} |
2776
|
|
|
} |
2777
|
|
|
|
2778
|
|
|
/** |
2779
|
|
|
* Loads the JavaScript/CSS code for INTincScript, if there are non-cacheable content objects |
2780
|
|
|
* it prepares the placeholders, otherwise populates options directly. |
2781
|
|
|
* |
2782
|
|
|
* @internal this method should be renamed as it does not only handle JS, but all additional header data |
2783
|
|
|
*/ |
2784
|
|
|
public function INTincScript_loadJSCode() |
2785
|
|
|
{ |
2786
|
|
|
// Prepare code and placeholders for additional header and footer files (and make sure that this isn't called twice) |
2787
|
|
|
if ($this->isINTincScript() && !isset($this->config['INTincScript_ext'])) { |
2788
|
|
|
$substituteHash = $this->uniqueHash(); |
2789
|
|
|
$this->config['INTincScript_ext']['divKey'] = $substituteHash; |
2790
|
|
|
// Storing the header-data array |
2791
|
|
|
$this->config['INTincScript_ext']['additionalHeaderData'] = $this->additionalHeaderData; |
2792
|
|
|
// Storing the footer-data array |
2793
|
|
|
$this->config['INTincScript_ext']['additionalFooterData'] = $this->additionalFooterData; |
2794
|
|
|
// Clearing the array |
2795
|
|
|
$this->additionalHeaderData = ['<!--HD_' . $substituteHash . '-->']; |
2796
|
|
|
// Clearing the array |
2797
|
|
|
$this->additionalFooterData = ['<!--FD_' . $substituteHash . '-->']; |
2798
|
|
|
} |
2799
|
|
|
} |
2800
|
|
|
|
2801
|
|
|
/** |
2802
|
|
|
* Determines if there are any INTincScripts to include = "non-cacheable" parts |
2803
|
|
|
* |
2804
|
|
|
* @return bool Returns TRUE if scripts are found |
2805
|
|
|
*/ |
2806
|
|
|
public function isINTincScript() |
2807
|
|
|
{ |
2808
|
|
|
return !empty($this->config['INTincScript']) && is_array($this->config['INTincScript']); |
2809
|
|
|
} |
2810
|
|
|
|
2811
|
|
|
/** |
2812
|
|
|
* Add HTTP headers to the response object. |
2813
|
|
|
* |
2814
|
|
|
* @param ResponseInterface $response |
2815
|
|
|
* @return ResponseInterface |
2816
|
|
|
*/ |
2817
|
|
|
public function applyHttpHeadersToResponse(ResponseInterface $response): ResponseInterface |
2818
|
|
|
{ |
2819
|
|
|
// Set header for charset-encoding unless disabled |
2820
|
|
|
if (empty($this->config['config']['disableCharsetHeader'])) { |
2821
|
|
|
$response = $response->withHeader('Content-Type', $this->contentType . '; charset=' . trim($this->metaCharset)); |
2822
|
|
|
} |
2823
|
|
|
// Set header for content language unless disabled |
2824
|
|
|
$contentLanguage = $this->language->getTwoLetterIsoCode(); |
2825
|
|
|
if (empty($this->config['config']['disableLanguageHeader']) && !empty($contentLanguage)) { |
2826
|
|
|
$response = $response->withHeader('Content-Language', trim($contentLanguage)); |
2827
|
|
|
} |
2828
|
|
|
// Set cache related headers to client (used to enable proxy / client caching!) |
2829
|
|
|
if (!empty($this->config['config']['sendCacheHeaders'])) { |
2830
|
|
|
$headers = $this->getCacheHeaders(); |
2831
|
|
|
foreach ($headers as $header => $value) { |
2832
|
|
|
$response = $response->withHeader($header, $value); |
2833
|
|
|
} |
2834
|
|
|
} |
2835
|
|
|
// Set additional headers if any have been configured via TypoScript |
2836
|
|
|
$additionalHeaders = $this->getAdditionalHeaders(); |
2837
|
|
|
foreach ($additionalHeaders as $headerConfig) { |
2838
|
|
|
[$header, $value] = GeneralUtility::trimExplode(':', $headerConfig['header'], false, 2); |
2839
|
|
|
if ($headerConfig['statusCode']) { |
2840
|
|
|
$response = $response->withStatus((int)$headerConfig['statusCode']); |
2841
|
|
|
} |
2842
|
|
|
if ($headerConfig['replace']) { |
2843
|
|
|
$response = $response->withHeader($header, $value); |
2844
|
|
|
} else { |
2845
|
|
|
$response = $response->withAddedHeader($header, $value); |
2846
|
|
|
} |
2847
|
|
|
} |
2848
|
|
|
return $response; |
2849
|
|
|
} |
2850
|
|
|
|
2851
|
|
|
/** |
2852
|
|
|
* Get cache headers good for client/reverse proxy caching. |
2853
|
|
|
* |
2854
|
|
|
* @return array |
2855
|
|
|
*/ |
2856
|
|
|
protected function getCacheHeaders(): array |
2857
|
|
|
{ |
2858
|
|
|
// Getting status whether we can send cache control headers for proxy caching: |
2859
|
|
|
$doCache = $this->isStaticCacheble(); |
2860
|
|
|
// This variable will be TRUE unless cache headers are configured to be sent ONLY if a branch does not allow logins and logins turns out to be allowed anyway... |
2861
|
|
|
$loginsDeniedCfg = empty($this->config['config']['sendCacheHeaders_onlyWhenLoginDeniedInBranch']) || empty($this->loginAllowedInBranch); |
2862
|
|
|
// Finally, when backend users are logged in, do not send cache headers at all (Admin Panel might be displayed for instance). |
2863
|
|
|
$this->isClientCachable = $doCache && !$this->isBackendUserLoggedIn() && !$this->doWorkspacePreview() && $loginsDeniedCfg; |
2864
|
|
|
if ($this->isClientCachable) { |
2865
|
|
|
$headers = [ |
2866
|
|
|
'Expires' => gmdate('D, d M Y H:i:s T', $this->cacheExpires), |
2867
|
|
|
'ETag' => '"' . md5($this->content) . '"', |
2868
|
|
|
'Cache-Control' => 'max-age=' . ($this->cacheExpires - $GLOBALS['EXEC_TIME']), |
2869
|
|
|
// no-cache |
2870
|
|
|
'Pragma' => 'public' |
2871
|
|
|
]; |
2872
|
|
|
} else { |
2873
|
|
|
// "no-store" is used to ensure that the client HAS to ask the server every time, and is not allowed to store anything at all |
2874
|
|
|
$headers = [ |
2875
|
|
|
'Cache-Control' => 'private, no-store' |
2876
|
|
|
]; |
2877
|
|
|
// Now, if a backend user is logged in, tell him in the Admin Panel log what the caching status would have been: |
2878
|
|
|
if ($this->isBackendUserLoggedIn()) { |
2879
|
|
|
if ($doCache) { |
2880
|
|
|
$this->getTimeTracker()->setTSlogMessage('Cache-headers with max-age "' . ($this->cacheExpires - $GLOBALS['EXEC_TIME']) . '" would have been sent'); |
2881
|
|
|
} else { |
2882
|
|
|
$reasonMsg = []; |
2883
|
|
|
if ($this->no_cache) { |
2884
|
|
|
$reasonMsg[] = 'Caching disabled (no_cache).'; |
2885
|
|
|
} |
2886
|
|
|
if ($this->isINTincScript()) { |
2887
|
|
|
$reasonMsg[] = '*_INT object(s) on page.'; |
2888
|
|
|
} |
2889
|
|
|
if (is_array($this->fe_user->user)) { |
2890
|
|
|
$reasonMsg[] = 'Frontend user logged in.'; |
2891
|
|
|
} |
2892
|
|
|
$this->getTimeTracker()->setTSlogMessage('Cache-headers would disable proxy caching! Reason(s): "' . implode(' ', $reasonMsg) . '"', 1); |
2893
|
|
|
} |
2894
|
|
|
} |
2895
|
|
|
} |
2896
|
|
|
return $headers; |
2897
|
|
|
} |
2898
|
|
|
|
2899
|
|
|
/** |
2900
|
|
|
* Reporting status whether we can send cache control headers for proxy caching or publishing to static files |
2901
|
|
|
* |
2902
|
|
|
* Rules are: |
2903
|
|
|
* no_cache cannot be set: If it is, the page might contain dynamic content and should never be cached. |
2904
|
|
|
* There can be no USER_INT objects on the page ("isINTincScript()") because they implicitly indicate dynamic content |
2905
|
|
|
* There can be no logged in user because user sessions are based on a cookie and thereby does not offer client caching a chance to know if the user is logged in. Actually, there will be a reverse problem here; If a page will somehow change when a user is logged in he may not see it correctly if the non-login version sent a cache-header! So do NOT use cache headers in page sections where user logins change the page content. (unless using such as realurl to apply a prefix in case of login sections) |
2906
|
|
|
* |
2907
|
|
|
* @return bool |
2908
|
|
|
*/ |
2909
|
|
|
public function isStaticCacheble() |
2910
|
|
|
{ |
2911
|
|
|
return !$this->no_cache && !$this->isINTincScript() && !$this->isUserOrGroupSet(); |
2912
|
|
|
} |
2913
|
|
|
|
2914
|
|
|
/******************************************** |
2915
|
|
|
* |
2916
|
|
|
* Various internal API functions |
2917
|
|
|
* |
2918
|
|
|
*******************************************/ |
2919
|
|
|
/** |
2920
|
|
|
* Creates an instance of ContentObjectRenderer in $this->cObj |
2921
|
|
|
* This instance is used to start the rendering of the TypoScript template structure |
2922
|
|
|
* |
2923
|
|
|
* @param ServerRequestInterface|null $request |
2924
|
|
|
*/ |
2925
|
|
|
public function newCObj(ServerRequestInterface $request = null) |
2926
|
|
|
{ |
2927
|
|
|
$this->cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class, $this); |
2928
|
|
|
$this->cObj->start($this->page, 'pages', $request); |
2929
|
|
|
} |
2930
|
|
|
|
2931
|
|
|
/** |
2932
|
|
|
* Converts relative paths in the HTML source to absolute paths for fileadmin/, typo3conf/ext/ and media/ folders. |
2933
|
|
|
* |
2934
|
|
|
* @internal |
2935
|
|
|
* @see \TYPO3\CMS\Frontend\Http\RequestHandler |
2936
|
|
|
* @see INTincScript() |
2937
|
|
|
*/ |
2938
|
|
|
public function setAbsRefPrefix() |
2939
|
|
|
{ |
2940
|
|
|
if (!$this->absRefPrefix) { |
2941
|
|
|
return; |
2942
|
|
|
} |
2943
|
|
|
$search = [ |
2944
|
|
|
'"typo3temp/', |
2945
|
|
|
'"' . PathUtility::stripPathSitePrefix(Environment::getExtensionsPath()) . '/', |
2946
|
|
|
'"' . PathUtility::stripPathSitePrefix(Environment::getBackendPath()) . '/ext/', |
2947
|
|
|
'"' . PathUtility::stripPathSitePrefix(Environment::getFrameworkBasePath()) . '/', |
2948
|
|
|
]; |
2949
|
|
|
$replace = [ |
2950
|
|
|
'"' . $this->absRefPrefix . 'typo3temp/', |
2951
|
|
|
'"' . $this->absRefPrefix . PathUtility::stripPathSitePrefix(Environment::getExtensionsPath()) . '/', |
2952
|
|
|
'"' . $this->absRefPrefix . PathUtility::stripPathSitePrefix(Environment::getBackendPath()) . '/ext/', |
2953
|
|
|
'"' . $this->absRefPrefix . PathUtility::stripPathSitePrefix(Environment::getFrameworkBasePath()) . '/', |
2954
|
|
|
]; |
2955
|
|
|
/** @var StorageRepository $storageRepository */ |
2956
|
|
|
$storageRepository = GeneralUtility::makeInstance(StorageRepository::class); |
2957
|
|
|
$storages = $storageRepository->findAll(); |
2958
|
|
|
foreach ($storages as $storage) { |
2959
|
|
|
if ($storage->getDriverType() === 'Local' && $storage->isPublic() && $storage->isOnline()) { |
2960
|
|
|
$folder = $storage->getPublicUrl($storage->getRootLevelFolder()); |
2961
|
|
|
$search[] = '"' . $folder; |
2962
|
|
|
$replace[] = '"' . $this->absRefPrefix . $folder; |
2963
|
|
|
} |
2964
|
|
|
} |
2965
|
|
|
// Process additional directories |
2966
|
|
|
$directories = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['FE']['additionalAbsRefPrefixDirectories'], true); |
2967
|
|
|
foreach ($directories as $directory) { |
2968
|
|
|
$search[] = '"' . $directory; |
2969
|
|
|
$replace[] = '"' . $this->absRefPrefix . $directory; |
2970
|
|
|
} |
2971
|
|
|
$this->content = str_replace( |
2972
|
|
|
$search, |
2973
|
|
|
$replace, |
2974
|
|
|
$this->content |
2975
|
|
|
); |
2976
|
|
|
} |
2977
|
|
|
|
2978
|
|
|
/** |
2979
|
|
|
* Prefixing the input URL with ->baseUrl If ->baseUrl is set and the input url is not absolute in some way. |
2980
|
|
|
* Designed as a wrapper functions for use with all frontend links that are processed by JavaScript (for "realurl" compatibility!). So each time a URL goes into window.open, window.location.href or otherwise, wrap it with this function! |
2981
|
|
|
* |
2982
|
|
|
* @param string $url Input URL, relative or absolute |
2983
|
|
|
* @return string Processed input value. |
2984
|
|
|
*/ |
2985
|
|
|
public function baseUrlWrap($url) |
2986
|
|
|
{ |
2987
|
|
|
if ($this->baseUrl) { |
2988
|
|
|
$urlParts = parse_url($url); |
2989
|
|
|
if (empty($urlParts['scheme']) && $url[0] !== '/') { |
2990
|
|
|
$url = $this->baseUrl . $url; |
2991
|
|
|
} |
2992
|
|
|
} |
2993
|
|
|
return $url; |
2994
|
|
|
} |
2995
|
|
|
|
2996
|
|
|
/** |
2997
|
|
|
* Logs access to deprecated TypoScript objects and properties. |
2998
|
|
|
* |
2999
|
|
|
* Dumps message to the TypoScript message log (admin panel) and the TYPO3 deprecation log. |
3000
|
|
|
* |
3001
|
|
|
* @param string $typoScriptProperty Deprecated object or property |
3002
|
|
|
* @param string $explanation Message or additional information |
3003
|
|
|
*/ |
3004
|
|
|
public function logDeprecatedTyposcript($typoScriptProperty, $explanation = '') |
3005
|
|
|
{ |
3006
|
|
|
$explanationText = $explanation !== '' ? ' - ' . $explanation : ''; |
3007
|
|
|
$this->getTimeTracker()->setTSlogMessage($typoScriptProperty . ' is deprecated.' . $explanationText, 2); |
3008
|
|
|
trigger_error('TypoScript property ' . $typoScriptProperty . ' is deprecated' . $explanationText, E_USER_DEPRECATED); |
3009
|
|
|
} |
3010
|
|
|
|
3011
|
|
|
/******************************************** |
3012
|
|
|
* PUBLIC ACCESSIBLE WORKSPACES FUNCTIONS |
3013
|
|
|
*******************************************/ |
3014
|
|
|
|
3015
|
|
|
/** |
3016
|
|
|
* Returns TRUE if workspace preview is enabled |
3017
|
|
|
* |
3018
|
|
|
* @return bool Returns TRUE if workspace preview is enabled |
3019
|
|
|
*/ |
3020
|
|
|
public function doWorkspacePreview() |
3021
|
|
|
{ |
3022
|
|
|
return $this->context->getPropertyFromAspect('workspace', 'isOffline', false); |
3023
|
|
|
} |
3024
|
|
|
|
3025
|
|
|
/** |
3026
|
|
|
* Returns the uid of the current workspace |
3027
|
|
|
* |
3028
|
|
|
* @return int returns workspace integer for which workspace is being preview. 0 if none (= live workspace). |
3029
|
|
|
*/ |
3030
|
|
|
public function whichWorkspace(): int |
3031
|
|
|
{ |
3032
|
|
|
return $this->context->getPropertyFromAspect('workspace', 'id', 0); |
|
|
|
|
3033
|
|
|
} |
3034
|
|
|
|
3035
|
|
|
/******************************************** |
3036
|
|
|
* |
3037
|
|
|
* Various external API functions - for use in plugins etc. |
3038
|
|
|
* |
3039
|
|
|
*******************************************/ |
3040
|
|
|
|
3041
|
|
|
/** |
3042
|
|
|
* Returns the pages TSconfig array based on the current ->rootLine |
3043
|
|
|
* |
3044
|
|
|
* @return array |
3045
|
|
|
*/ |
3046
|
|
|
public function getPagesTSconfig() |
3047
|
|
|
{ |
3048
|
|
|
if (!is_array($this->pagesTSconfig)) { |
3049
|
|
|
$contentHashCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash'); |
3050
|
|
|
$loader = GeneralUtility::makeInstance(PageTsConfigLoader::class); |
3051
|
|
|
$tsConfigString = $loader->load(array_reverse($this->rootLine)); |
3052
|
|
|
$parser = GeneralUtility::makeInstance( |
3053
|
|
|
PageTsConfigParser::class, |
3054
|
|
|
GeneralUtility::makeInstance(TypoScriptParser::class), |
3055
|
|
|
$contentHashCache |
3056
|
|
|
); |
3057
|
|
|
$this->pagesTSconfig = $parser->parse( |
3058
|
|
|
$tsConfigString, |
3059
|
|
|
GeneralUtility::makeInstance(ConditionMatcher::class, $this->context, $this->id, $this->rootLine), |
3060
|
|
|
$this->site |
3061
|
|
|
); |
3062
|
|
|
} |
3063
|
|
|
return $this->pagesTSconfig; |
3064
|
|
|
} |
3065
|
|
|
|
3066
|
|
|
/** |
3067
|
|
|
* Returns a unique md5 hash. |
3068
|
|
|
* There is no special magic in this, the only point is that you don't have to call md5(uniqid()) which is slow and by this you are sure to get a unique string each time in a little faster way. |
3069
|
|
|
* |
3070
|
|
|
* @param string $str Some string to include in what is hashed. Not significant at all. |
3071
|
|
|
* @return string MD5 hash of ->uniqueString, input string and uniqueCounter |
3072
|
|
|
*/ |
3073
|
|
|
public function uniqueHash($str = '') |
3074
|
|
|
{ |
3075
|
|
|
return md5($this->uniqueString . '_' . $str . $this->uniqueCounter++); |
3076
|
|
|
} |
3077
|
|
|
|
3078
|
|
|
/** |
3079
|
|
|
* Sets the cache-flag to 1. Could be called from user-included php-files in order to ensure that a page is not cached. |
3080
|
|
|
* |
3081
|
|
|
* @param string $reason An optional reason to be written to the log. |
3082
|
|
|
* @param bool $internal Whether the call is done from core itself (should only be used by core). |
3083
|
|
|
*/ |
3084
|
|
|
public function set_no_cache($reason = '', $internal = false) |
3085
|
|
|
{ |
3086
|
|
|
$context = []; |
3087
|
|
|
if ($reason !== '') { |
3088
|
|
|
$warning = '$TSFE->set_no_cache() was triggered. Reason: {reason}.'; |
3089
|
|
|
$context['reason'] = $reason; |
3090
|
|
|
} else { |
3091
|
|
|
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1); |
3092
|
|
|
// This is a hack to work around ___FILE___ resolving symbolic links |
3093
|
|
|
$realWebPath = PathUtility::dirname((string)realpath(Environment::getBackendPath())) . '/'; |
3094
|
|
|
$file = $trace[0]['file']; |
3095
|
|
|
if (strpos($file, $realWebPath) === 0) { |
3096
|
|
|
$file = str_replace($realWebPath, '', $file); |
3097
|
|
|
} else { |
3098
|
|
|
$file = str_replace(Environment::getPublicPath() . '/', '', $file); |
3099
|
|
|
} |
3100
|
|
|
$warning = '$GLOBALS[\'TSFE\']->set_no_cache() was triggered by {file} on line {line}.'; |
3101
|
|
|
$context['file'] = $file; |
3102
|
|
|
$context['line'] = $trace[0]['line']; |
3103
|
|
|
} |
3104
|
|
|
if (!$internal && $GLOBALS['TYPO3_CONF_VARS']['FE']['disableNoCacheParameter']) { |
3105
|
|
|
$warning .= ' However, $TYPO3_CONF_VARS[\'FE\'][\'disableNoCacheParameter\'] is set, so it will be ignored!'; |
3106
|
|
|
$this->getTimeTracker()->setTSlogMessage($warning, 2); |
3107
|
|
|
} else { |
3108
|
|
|
$warning .= ' Caching is disabled!'; |
3109
|
|
|
$this->disableCache(); |
3110
|
|
|
} |
3111
|
|
|
if ($internal && $this->isBackendUserLoggedIn()) { |
3112
|
|
|
$this->logger->notice($warning, $context); |
3113
|
|
|
} else { |
3114
|
|
|
$this->logger->warning($warning, $context); |
3115
|
|
|
} |
3116
|
|
|
} |
3117
|
|
|
|
3118
|
|
|
/** |
3119
|
|
|
* Disables caching of the current page. |
3120
|
|
|
* |
3121
|
|
|
* @internal |
3122
|
|
|
*/ |
3123
|
|
|
protected function disableCache() |
3124
|
|
|
{ |
3125
|
|
|
$this->no_cache = true; |
3126
|
|
|
} |
3127
|
|
|
|
3128
|
|
|
/** |
3129
|
|
|
* Sets the cache-timeout in seconds |
3130
|
|
|
* |
3131
|
|
|
* @param int $seconds Cache-timeout in seconds |
3132
|
|
|
*/ |
3133
|
|
|
public function set_cache_timeout_default($seconds) |
3134
|
|
|
{ |
3135
|
|
|
$seconds = (int)$seconds; |
3136
|
|
|
if ($seconds > 0) { |
3137
|
|
|
$this->cacheTimeOutDefault = $seconds; |
3138
|
|
|
} |
3139
|
|
|
} |
3140
|
|
|
|
3141
|
|
|
/** |
3142
|
|
|
* Get the cache timeout for the current page. |
3143
|
|
|
* |
3144
|
|
|
* @return int The cache timeout for the current page. |
3145
|
|
|
*/ |
3146
|
|
|
public function get_cache_timeout() |
3147
|
|
|
{ |
3148
|
|
|
/** @var \TYPO3\CMS\Core\Cache\Frontend\AbstractFrontend $runtimeCache */ |
3149
|
|
|
$runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime'); |
3150
|
|
|
$cachedCacheLifetimeIdentifier = 'core-tslib_fe-get_cache_timeout'; |
3151
|
|
|
$cachedCacheLifetime = $runtimeCache->get($cachedCacheLifetimeIdentifier); |
3152
|
|
|
if ($cachedCacheLifetime === false) { |
3153
|
|
|
if ($this->page['cache_timeout']) { |
3154
|
|
|
// Cache period was set for the page: |
3155
|
|
|
$cacheTimeout = $this->page['cache_timeout']; |
3156
|
|
|
} else { |
3157
|
|
|
// Cache period was set via TypoScript "config.cache_period", |
3158
|
|
|
// otherwise it's the default of 24 hours |
3159
|
|
|
$cacheTimeout = $this->cacheTimeOutDefault; |
3160
|
|
|
} |
3161
|
|
|
if (!empty($this->config['config']['cache_clearAtMidnight'])) { |
3162
|
|
|
$timeOutTime = $GLOBALS['EXEC_TIME'] + $cacheTimeout; |
3163
|
|
|
$midnightTime = mktime(0, 0, 0, (int)date('m', $timeOutTime), (int)date('d', $timeOutTime), (int)date('Y', $timeOutTime)); |
3164
|
|
|
// If the midnight time of the expire-day is greater than the current time, |
3165
|
|
|
// we may set the timeOutTime to the new midnighttime. |
3166
|
|
|
if ($midnightTime > $GLOBALS['EXEC_TIME']) { |
3167
|
|
|
$cacheTimeout = $midnightTime - $GLOBALS['EXEC_TIME']; |
3168
|
|
|
} |
3169
|
|
|
} |
3170
|
|
|
|
3171
|
|
|
// Calculate the timeout time for records on the page and adjust cache timeout if necessary |
3172
|
|
|
$cacheTimeout = min($this->calculatePageCacheTimeout(), $cacheTimeout); |
3173
|
|
|
|
3174
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['get_cache_timeout'] ?? [] as $_funcRef) { |
3175
|
|
|
$params = ['cacheTimeout' => $cacheTimeout]; |
3176
|
|
|
$cacheTimeout = GeneralUtility::callUserFunction($_funcRef, $params, $this); |
3177
|
|
|
} |
3178
|
|
|
$runtimeCache->set($cachedCacheLifetimeIdentifier, $cacheTimeout); |
3179
|
|
|
$cachedCacheLifetime = $cacheTimeout; |
3180
|
|
|
} |
3181
|
|
|
return $cachedCacheLifetime; |
3182
|
|
|
} |
3183
|
|
|
|
3184
|
|
|
/********************************************* |
3185
|
|
|
* |
3186
|
|
|
* Localization and character set conversion |
3187
|
|
|
* |
3188
|
|
|
*********************************************/ |
3189
|
|
|
/** |
3190
|
|
|
* Split Label function for front-end applications. |
3191
|
|
|
* |
3192
|
|
|
* @param string $input Key string. Accepts the "LLL:" prefix. |
3193
|
|
|
* @return string Label value, if any. |
3194
|
|
|
*/ |
3195
|
|
|
public function sL($input) |
3196
|
|
|
{ |
3197
|
|
|
return $this->languageService->sL($input); |
3198
|
|
|
} |
3199
|
|
|
|
3200
|
|
|
/** |
3201
|
|
|
* Sets all internal measures what language the page should be rendered. |
3202
|
|
|
* This is not for records, but rather the HTML / charset and the locallang labels |
3203
|
|
|
*/ |
3204
|
|
|
protected function setOutputLanguage() |
3205
|
|
|
{ |
3206
|
|
|
$this->languageService = GeneralUtility::makeInstance(LanguageServiceFactory::class)->createFromSiteLanguage($this->language); |
3207
|
|
|
// Always disable debugging for TSFE |
3208
|
|
|
$this->languageService->debugKey = false; |
3209
|
|
|
} |
3210
|
|
|
|
3211
|
|
|
/** |
3212
|
|
|
* Converts input string from utf-8 to metaCharset IF the two charsets are different. |
3213
|
|
|
* |
3214
|
|
|
* @param string $content Content to be converted. |
3215
|
|
|
* @return string Converted content string. |
3216
|
|
|
* @throws \RuntimeException if an invalid charset was configured |
3217
|
|
|
*/ |
3218
|
|
|
public function convOutputCharset($content) |
3219
|
|
|
{ |
3220
|
|
|
if ($this->metaCharset !== 'utf-8') { |
3221
|
|
|
/** @var CharsetConverter $charsetConverter */ |
3222
|
|
|
$charsetConverter = GeneralUtility::makeInstance(CharsetConverter::class); |
3223
|
|
|
try { |
3224
|
|
|
$content = $charsetConverter->conv($content, 'utf-8', $this->metaCharset); |
3225
|
|
|
} catch (UnknownCharsetException $e) { |
3226
|
|
|
throw new \RuntimeException('Invalid config.metaCharset: ' . $e->getMessage(), 1508916185); |
3227
|
|
|
} |
3228
|
|
|
} |
3229
|
|
|
return $content; |
3230
|
|
|
} |
3231
|
|
|
|
3232
|
|
|
/** |
3233
|
|
|
* Calculates page cache timeout according to the records with starttime/endtime on the page. |
3234
|
|
|
* |
3235
|
|
|
* @return int Page cache timeout or PHP_INT_MAX if cannot be determined |
3236
|
|
|
*/ |
3237
|
|
|
protected function calculatePageCacheTimeout() |
3238
|
|
|
{ |
3239
|
|
|
$result = PHP_INT_MAX; |
3240
|
|
|
// Get the configuration |
3241
|
|
|
$tablesToConsider = $this->getCurrentPageCacheConfiguration(); |
3242
|
|
|
// Get the time, rounded to the minute (do not pollute MySQL cache!) |
3243
|
|
|
// It is ok that we do not take seconds into account here because this |
3244
|
|
|
// value will be subtracted later. So we never get the time "before" |
3245
|
|
|
// the cache change. |
3246
|
|
|
$now = $GLOBALS['ACCESS_TIME']; |
3247
|
|
|
// Find timeout by checking every table |
3248
|
|
|
foreach ($tablesToConsider as $tableDef) { |
3249
|
|
|
$result = min($result, $this->getFirstTimeValueForRecord($tableDef, $now)); |
3250
|
|
|
} |
3251
|
|
|
// We return + 1 second just to ensure that cache is definitely regenerated |
3252
|
|
|
return $result === PHP_INT_MAX ? PHP_INT_MAX : $result - $now + 1; |
3253
|
|
|
} |
3254
|
|
|
|
3255
|
|
|
/** |
3256
|
|
|
* Obtains a list of table/pid pairs to consider for page caching. |
3257
|
|
|
* |
3258
|
|
|
* TS configuration looks like this: |
3259
|
|
|
* |
3260
|
|
|
* The cache lifetime of all pages takes starttime and endtime of news records of page 14 into account: |
3261
|
|
|
* config.cache.all = tt_news:14 |
3262
|
|
|
* |
3263
|
|
|
* The cache.lifetime of the current page allows to take records (e.g. fe_users) into account: |
3264
|
|
|
* config.cache.all = fe_users:current |
3265
|
|
|
* |
3266
|
|
|
* The cache lifetime of page 42 takes starttime and endtime of news records of page 15 and addresses of page 16 into account: |
3267
|
|
|
* config.cache.42 = tt_news:15,tt_address:16 |
3268
|
|
|
* |
3269
|
|
|
* @return array Array of 'tablename:pid' pairs. There is at least a current page id in the array |
3270
|
|
|
* @see TypoScriptFrontendController::calculatePageCacheTimeout() |
3271
|
|
|
*/ |
3272
|
|
|
protected function getCurrentPageCacheConfiguration() |
3273
|
|
|
{ |
3274
|
|
|
$result = ['tt_content:' . $this->id]; |
3275
|
|
|
if (isset($this->config['config']['cache.'][$this->id])) { |
3276
|
|
|
$result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $this->id, $this->config['config']['cache.'][$this->id]))); |
3277
|
|
|
} |
3278
|
|
|
if (isset($this->config['config']['cache.']['all'])) { |
3279
|
|
|
$result = array_merge($result, GeneralUtility::trimExplode(',', str_replace(':current', ':' . $this->id, $this->config['config']['cache.']['all']))); |
3280
|
|
|
} |
3281
|
|
|
return array_unique($result); |
3282
|
|
|
} |
3283
|
|
|
|
3284
|
|
|
/** |
3285
|
|
|
* Find the minimum starttime or endtime value in the table and pid that is greater than the current time. |
3286
|
|
|
* |
3287
|
|
|
* @param string $tableDef Table definition (format tablename:pid) |
3288
|
|
|
* @param int $now "Now" time value |
3289
|
|
|
* @throws \InvalidArgumentException |
3290
|
|
|
* @return int Value of the next start/stop time or PHP_INT_MAX if not found |
3291
|
|
|
* @see TypoScriptFrontendController::calculatePageCacheTimeout() |
3292
|
|
|
*/ |
3293
|
|
|
protected function getFirstTimeValueForRecord($tableDef, $now) |
3294
|
|
|
{ |
3295
|
|
|
$now = (int)$now; |
3296
|
|
|
$result = PHP_INT_MAX; |
3297
|
|
|
[$tableName, $pid] = GeneralUtility::trimExplode(':', $tableDef); |
3298
|
|
|
if (empty($tableName) || empty($pid)) { |
3299
|
|
|
throw new \InvalidArgumentException('Unexpected value for parameter $tableDef. Expected <tablename>:<pid>, got \'' . htmlspecialchars($tableDef) . '\'.', 1307190365); |
3300
|
|
|
} |
3301
|
|
|
|
3302
|
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) |
3303
|
|
|
->getQueryBuilderForTable($tableName); |
3304
|
|
|
$queryBuilder->getRestrictions() |
3305
|
|
|
->removeByType(StartTimeRestriction::class) |
3306
|
|
|
->removeByType(EndTimeRestriction::class); |
3307
|
|
|
$timeFields = []; |
3308
|
|
|
$timeConditions = $queryBuilder->expr()->orX(); |
3309
|
|
|
foreach (['starttime', 'endtime'] as $field) { |
3310
|
|
|
if (isset($GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field])) { |
3311
|
|
|
$timeFields[$field] = $GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field]; |
3312
|
|
|
$queryBuilder->addSelectLiteral( |
3313
|
|
|
'MIN(' |
3314
|
|
|
. 'CASE WHEN ' |
3315
|
|
|
. $queryBuilder->expr()->lte( |
3316
|
|
|
$timeFields[$field], |
3317
|
|
|
$queryBuilder->createNamedParameter($now, \PDO::PARAM_INT) |
3318
|
|
|
) |
3319
|
|
|
. ' THEN NULL ELSE ' . $queryBuilder->quoteIdentifier($timeFields[$field]) . ' END' |
3320
|
|
|
. ') AS ' . $queryBuilder->quoteIdentifier($timeFields[$field]) |
3321
|
|
|
); |
3322
|
|
|
$timeConditions->add( |
3323
|
|
|
$queryBuilder->expr()->gt( |
3324
|
|
|
$timeFields[$field], |
3325
|
|
|
$queryBuilder->createNamedParameter($now, \PDO::PARAM_INT) |
3326
|
|
|
) |
3327
|
|
|
); |
3328
|
|
|
} |
3329
|
|
|
} |
3330
|
|
|
|
3331
|
|
|
// if starttime or endtime are defined, evaluate them |
3332
|
|
|
if (!empty($timeFields)) { |
3333
|
|
|
// find the timestamp, when the current page's content changes the next time |
3334
|
|
|
$row = $queryBuilder |
3335
|
|
|
->from($tableName) |
3336
|
|
|
->where( |
3337
|
|
|
$queryBuilder->expr()->eq( |
3338
|
|
|
'pid', |
3339
|
|
|
$queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT) |
3340
|
|
|
), |
3341
|
|
|
$timeConditions |
3342
|
|
|
) |
3343
|
|
|
->execute() |
3344
|
|
|
->fetch(); |
3345
|
|
|
|
3346
|
|
|
if ($row) { |
3347
|
|
|
foreach ($timeFields as $timeField => $_) { |
3348
|
|
|
// if a MIN value is found, take it into account for the |
3349
|
|
|
// cache lifetime we have to filter out start/endtimes < $now, |
3350
|
|
|
// as the SQL query also returns rows with starttime < $now |
3351
|
|
|
// and endtime > $now (and using a starttime from the past |
3352
|
|
|
// would be wrong) |
3353
|
|
|
if ($row[$timeField] !== null && (int)$row[$timeField] > $now) { |
3354
|
|
|
$result = min($result, (int)$row[$timeField]); |
3355
|
|
|
} |
3356
|
|
|
} |
3357
|
|
|
} |
3358
|
|
|
} |
3359
|
|
|
|
3360
|
|
|
return $result; |
3361
|
|
|
} |
3362
|
|
|
|
3363
|
|
|
/** |
3364
|
|
|
* Fetches the originally requested id, falls back to $this->id |
3365
|
|
|
* |
3366
|
|
|
* @return int the originally requested page uid |
3367
|
|
|
* @see fetch_the_id() |
3368
|
|
|
*/ |
3369
|
|
|
public function getRequestedId() |
3370
|
|
|
{ |
3371
|
|
|
return $this->requestedId ?: $this->id; |
|
|
|
|
3372
|
|
|
} |
3373
|
|
|
|
3374
|
|
|
/** |
3375
|
|
|
* Acquire a page specific lock |
3376
|
|
|
* |
3377
|
|
|
* |
3378
|
|
|
* The schematics here is: |
3379
|
|
|
* - First acquire an access lock. This is using the type of the requested lock as key. |
3380
|
|
|
* Since the number of types is rather limited we can use the type as key as it will only |
3381
|
|
|
* eat up a limited number of lock resources on the system (files, semaphores) |
3382
|
|
|
* - Second, we acquire the actual lock (named page lock). We can be sure we are the only process at this |
3383
|
|
|
* very moment, hence we either get the lock for the given key or we get an error as we request a non-blocking mode. |
3384
|
|
|
* |
3385
|
|
|
* Interleaving two locks is extremely important, because the actual page lock uses a hash value as key (see callers |
3386
|
|
|
* of this function). If we would simply employ a normal blocking lock, we would get a potentially unlimited |
3387
|
|
|
* (number of pages at least) number of different locks. Depending on the available locking methods on the system |
3388
|
|
|
* we might run out of available resources. (e.g. maximum limit of semaphores is a system setting and applies |
3389
|
|
|
* to the whole system) |
3390
|
|
|
* We therefore must make sure that page locks are destroyed again if they are not used anymore, such that |
3391
|
|
|
* we never use more locking resources than parallel requests to different pages (hashes). |
3392
|
|
|
* In order to ensure this, we need to guarantee that no other process is waiting on a page lock when |
3393
|
|
|
* the process currently having the lock on the page lock is about to release the lock again. |
3394
|
|
|
* This can only be achieved by using a non-blocking mode, such that a process is never put into wait state |
3395
|
|
|
* by the kernel, but only checks the availability of the lock. The access lock is our guard to be sure |
3396
|
|
|
* that no two processes are at the same time releasing/destroying a page lock, whilst the other one tries to |
3397
|
|
|
* get a lock for this page lock. |
3398
|
|
|
* The only drawback of this implementation is that we basically have to poll the availability of the page lock. |
3399
|
|
|
* |
3400
|
|
|
* Note that the access lock resources are NEVER deleted/destroyed, otherwise the whole thing would be broken. |
3401
|
|
|
* |
3402
|
|
|
* @param string $type |
3403
|
|
|
* @param string $key |
3404
|
|
|
* @throws \InvalidArgumentException |
3405
|
|
|
* @throws \RuntimeException |
3406
|
|
|
* @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException |
3407
|
|
|
*/ |
3408
|
|
|
protected function acquireLock($type, $key) |
3409
|
|
|
{ |
3410
|
|
|
$lockFactory = GeneralUtility::makeInstance(LockFactory::class); |
3411
|
|
|
$this->locks[$type]['accessLock'] = $lockFactory->createLocker($type); |
3412
|
|
|
|
3413
|
|
|
$this->locks[$type]['pageLock'] = $lockFactory->createLocker( |
3414
|
|
|
$key, |
3415
|
|
|
LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK |
3416
|
|
|
); |
3417
|
|
|
|
3418
|
|
|
do { |
3419
|
|
|
if (!$this->locks[$type]['accessLock']->acquire()) { |
3420
|
|
|
throw new \RuntimeException('Could not acquire access lock for "' . $type . '"".', 1294586098); |
3421
|
|
|
} |
3422
|
|
|
|
3423
|
|
|
try { |
3424
|
|
|
$locked = $this->locks[$type]['pageLock']->acquire( |
3425
|
|
|
LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK |
3426
|
|
|
); |
3427
|
|
|
} catch (LockAcquireWouldBlockException $e) { |
3428
|
|
|
// somebody else has the lock, we keep waiting |
3429
|
|
|
|
3430
|
|
|
// first release the access lock |
3431
|
|
|
$this->locks[$type]['accessLock']->release(); |
3432
|
|
|
// now lets make a short break (100ms) until we try again, since |
3433
|
|
|
// the page generation by the lock owner will take a while anyways |
3434
|
|
|
usleep(100000); |
3435
|
|
|
continue; |
3436
|
|
|
} |
3437
|
|
|
$this->locks[$type]['accessLock']->release(); |
3438
|
|
|
if ($locked) { |
3439
|
|
|
break; |
3440
|
|
|
} |
3441
|
|
|
throw new \RuntimeException('Could not acquire page lock for ' . $key . '.', 1460975877); |
3442
|
|
|
} while (true); |
3443
|
|
|
} |
3444
|
|
|
|
3445
|
|
|
/** |
3446
|
|
|
* Release a page specific lock |
3447
|
|
|
* |
3448
|
|
|
* @param string $type |
3449
|
|
|
* @throws \InvalidArgumentException |
3450
|
|
|
* @throws \RuntimeException |
3451
|
|
|
* @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException |
3452
|
|
|
*/ |
3453
|
|
|
protected function releaseLock($type) |
3454
|
|
|
{ |
3455
|
|
|
if ($this->locks[$type]['accessLock'] ?? false) { |
3456
|
|
|
if (!$this->locks[$type]['accessLock']->acquire()) { |
3457
|
|
|
throw new \RuntimeException('Could not acquire access lock for "' . $type . '"".', 1460975902); |
3458
|
|
|
} |
3459
|
|
|
|
3460
|
|
|
$this->locks[$type]['pageLock']->release(); |
3461
|
|
|
$this->locks[$type]['pageLock']->destroy(); |
3462
|
|
|
$this->locks[$type]['pageLock'] = null; |
3463
|
|
|
|
3464
|
|
|
$this->locks[$type]['accessLock']->release(); |
3465
|
|
|
$this->locks[$type]['accessLock'] = null; |
3466
|
|
|
} |
3467
|
|
|
} |
3468
|
|
|
|
3469
|
|
|
/** |
3470
|
|
|
* Send additional headers from config.additionalHeaders |
3471
|
|
|
*/ |
3472
|
|
|
protected function getAdditionalHeaders(): array |
3473
|
|
|
{ |
3474
|
|
|
if (!isset($this->config['config']['additionalHeaders.'])) { |
3475
|
|
|
return []; |
3476
|
|
|
} |
3477
|
|
|
$additionalHeaders = []; |
3478
|
|
|
ksort($this->config['config']['additionalHeaders.']); |
3479
|
|
|
foreach ($this->config['config']['additionalHeaders.'] as $options) { |
3480
|
|
|
if (!is_array($options)) { |
3481
|
|
|
continue; |
3482
|
|
|
} |
3483
|
|
|
$header = trim($options['header'] ?? ''); |
3484
|
|
|
if ($header === '') { |
3485
|
|
|
continue; |
3486
|
|
|
} |
3487
|
|
|
$additionalHeaders[] = [ |
3488
|
|
|
'header' => $header, |
3489
|
|
|
// "replace existing headers" is turned on by default, unless turned off |
3490
|
|
|
'replace' => ($options['replace'] ?? '') !== '0', |
3491
|
|
|
'statusCode' => (int)($options['httpResponseCode'] ?? 0) ?: null |
3492
|
|
|
]; |
3493
|
|
|
} |
3494
|
|
|
return $additionalHeaders; |
3495
|
|
|
} |
3496
|
|
|
|
3497
|
|
|
protected function isInPreviewMode(): bool |
3498
|
|
|
{ |
3499
|
|
|
return $this->context->getPropertyFromAspect('frontend.preview', 'isPreview', false) |
3500
|
|
|
|| $GLOBALS['EXEC_TIME'] !== $GLOBALS['SIM_EXEC_TIME'] |
3501
|
|
|
|| $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages', false) |
3502
|
|
|
|| $this->context->getPropertyFromAspect('visibility', 'includeHiddenContent', false); |
3503
|
|
|
} |
3504
|
|
|
|
3505
|
|
|
/** |
3506
|
|
|
* Log the page access failure with additional request information |
3507
|
|
|
* |
3508
|
|
|
* @param string $message |
3509
|
|
|
* @param ServerRequestInterface $request |
3510
|
|
|
*/ |
3511
|
|
|
protected function logPageAccessFailure(string $message, ServerRequestInterface $request): void |
3512
|
|
|
{ |
3513
|
|
|
$context = ['pageId' => $this->id]; |
3514
|
|
|
if (($normalizedParams = $request->getAttribute('normalizedParams')) instanceof NormalizedParams) { |
3515
|
|
|
$context['requestUrl'] = $normalizedParams->getRequestUrl(); |
3516
|
|
|
} |
3517
|
|
|
$this->logger->error($message, $context); |
3518
|
|
|
} |
3519
|
|
|
|
3520
|
|
|
/** |
3521
|
|
|
* Returns the current BE user. |
3522
|
|
|
* |
3523
|
|
|
* @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication |
3524
|
|
|
*/ |
3525
|
|
|
protected function getBackendUser() |
3526
|
|
|
{ |
3527
|
|
|
return $GLOBALS['BE_USER']; |
3528
|
|
|
} |
3529
|
|
|
|
3530
|
|
|
/** |
3531
|
|
|
* @return TimeTracker |
3532
|
|
|
*/ |
3533
|
|
|
protected function getTimeTracker() |
3534
|
|
|
{ |
3535
|
|
|
return GeneralUtility::makeInstance(TimeTracker::class); |
3536
|
|
|
} |
3537
|
|
|
|
3538
|
|
|
/** |
3539
|
|
|
* Return the global instance of this class. |
3540
|
|
|
* |
3541
|
|
|
* Intended to be used as prototype factory for this class, see Services.yaml. |
3542
|
|
|
* This is required as long as TypoScriptFrontendController needs request |
3543
|
|
|
* dependent constructor parameters. Once that has been refactored this |
3544
|
|
|
* factory will be removed. |
3545
|
|
|
* |
3546
|
|
|
* @return TypoScriptFrontendController |
3547
|
|
|
* @internal |
3548
|
|
|
*/ |
3549
|
|
|
public static function getGlobalInstance(): ?self |
3550
|
|
|
{ |
3551
|
|
|
if (($GLOBALS['TSFE'] ?? null) instanceof self) { |
3552
|
|
|
return $GLOBALS['TSFE']; |
3553
|
|
|
} |
3554
|
|
|
|
3555
|
|
|
if (!($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface |
3556
|
|
|
|| !ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend() |
3557
|
|
|
) { |
3558
|
|
|
// Return null for now (together with shared: false in Services.yaml) as TSFE might not be available in backend context |
3559
|
|
|
// That's not an error then |
3560
|
|
|
return null; |
3561
|
|
|
} |
3562
|
|
|
|
3563
|
|
|
throw new \LogicException('TypoScriptFrontendController was tried to be injected before initial creation', 1538370377); |
3564
|
|
|
} |
3565
|
|
|
|
3566
|
|
|
public function getLanguage(): SiteLanguage |
3567
|
|
|
{ |
3568
|
|
|
return $this->language; |
3569
|
|
|
} |
3570
|
|
|
|
3571
|
|
|
public function getSite(): SiteInterface |
3572
|
|
|
{ |
3573
|
|
|
return $this->site; |
3574
|
|
|
} |
3575
|
|
|
|
3576
|
|
|
public function getContext(): Context |
3577
|
|
|
{ |
3578
|
|
|
return $this->context; |
3579
|
|
|
} |
3580
|
|
|
|
3581
|
|
|
public function getPageArguments(): PageArguments |
3582
|
|
|
{ |
3583
|
|
|
return $this->pageArguments; |
3584
|
|
|
} |
3585
|
|
|
} |
3586
|
|
|
|