[BUGFIX] Correctly initialize SiteConfiguration in functional tests
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Hooks / TreelistCacheUpdateHooks.php
1 <?php
2 namespace TYPO3\CMS\Frontend\Hooks;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Database\Connection;
19 use TYPO3\CMS\Core\Database\ConnectionPool;
20 use TYPO3\CMS\Core\DataHandling\DataHandler;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22
23 /**
24 * Class that hooks into DataHandler and listens for updates to pages to update the
25 * treelist cache
26 * @internal this is a concrete TYPO3 hook implementation and solely used for EXT:frontend and not part of TYPO3's Core API.
27 */
28 class TreelistCacheUpdateHooks
29 {
30 /**
31 * Should not be manipulated from others except through the
32 * configuration provided @see __construct()
33 *
34 * @var array
35 */
36 private $updateRequiringFields = [
37 'pid',
38 'php_tree_stop',
39 'extendToSubpages'
40 ];
41
42 /**
43 * Constructor, adds update requiring fields to the default ones
44 */
45 public function __construct()
46 {
47 // As enableFields can be set dynamically we add them here
48 $pagesEnableFields = $GLOBALS['TCA']['pages']['ctrl']['enablecolumns'];
49 foreach ($pagesEnableFields as $pagesEnableField) {
50 $this->updateRequiringFields[] = $pagesEnableField;
51 }
52 $this->updateRequiringFields[] = $GLOBALS['TCA']['pages']['ctrl']['delete'];
53 // Extension can add fields to the pages table that require an
54 // update of the treelist cache, too; so we also add those
55 // example: $TYPO3_CONF_VARS['BE']['additionalTreelistUpdateFields'] .= ',my_field';
56 if (!empty($GLOBALS['TYPO3_CONF_VARS']['BE']['additionalTreelistUpdateFields'])) {
57 $additionalTreelistUpdateFields = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['BE']['additionalTreelistUpdateFields'], true);
58 $this->updateRequiringFields = array_merge($this->updateRequiringFields, $additionalTreelistUpdateFields);
59 }
60 }
61
62 /**
63 * waits for DataHandler commands and looks for changed pages, if found further
64 * changes take place to determine whether the cache needs to be updated
65 *
66 * @param string $status DataHandler operation status, either 'new' or 'update'
67 * @param string $table The DB table the operation was carried out on
68 * @param mixed $recordId The record's uid for update records, a string to look the record's uid up after it has been created
69 * @param array $updatedFields Array of changed fields and their new values
70 * @param DataHandler $dataHandler DataHandler parent object
71 */
72 public function processDatamap_afterDatabaseOperations($status, $table, $recordId, array $updatedFields, DataHandler $dataHandler)
73 {
74 if ($table === 'pages' && $this->requiresUpdate($updatedFields)) {
75 $affectedPagePid = 0;
76 $affectedPageUid = 0;
77 if ($status === 'new') {
78 // Detect new pages
79 // Resolve the uid
80 $affectedPageUid = $dataHandler->substNEWwithIDs[$recordId];
81 $affectedPagePid = $updatedFields['pid'];
82 } elseif ($status === 'update') {
83 // Detect updated pages
84 $affectedPageUid = $recordId;
85 // When updating a page the pid is not directly available so we
86 // need to retrieve it ourselves.
87 $fullPageRecord = BackendUtility::getRecord($table, $recordId);
88 $affectedPagePid = $fullPageRecord['pid'];
89 }
90 $clearCacheActions = $this->determineClearCacheActions($status, $updatedFields);
91 $this->processClearCacheActions($affectedPageUid, $affectedPagePid, $updatedFields, $clearCacheActions);
92 }
93 }
94
95 /**
96 * Waits for DataHandler commands and looks for deleted pages or swapped pages, if found
97 * further changes take place to determine whether the cache needs to be updated
98 *
99 * @param string $command The TCE command
100 * @param string $table The record's table
101 * @param int $recordId The record's uid
102 * @param array $commandValue The commands value, typically an array with more detailed command information
103 * @param DataHandler $dataHandler The DataHandler parent object
104 */
105 public function processCmdmap_postProcess($command, $table, $recordId, $commandValue, DataHandler $dataHandler)
106 {
107 $action = (is_array($commandValue) && isset($commandValue['action'])) ? (string)$commandValue['action'] : '';
108 if ($table === 'pages' && ($command === 'delete' || ($command === 'version' && $action === 'swap'))) {
109 $affectedRecord = BackendUtility::getRecord($table, $recordId, '*', '', false);
110 $affectedPageUid = $affectedRecord['uid'];
111 $affectedPagePid = $affectedRecord['pid'];
112
113 // Faking the updated fields
114 $updatedFields = [];
115 if ($command === 'delete') {
116 $updatedFields['deleted'] = 1;
117 } else {
118 // page was published to live (swapped)
119 $updatedFields['t3ver_wsid'] = 0;
120 }
121 $clearCacheActions = $this->determineClearCacheActions(
122 'update',
123 $updatedFields
124 );
125
126 $this->processClearCacheActions($affectedPageUid, $affectedPagePid, $updatedFields, $clearCacheActions);
127 }
128 }
129
130 /**
131 * waits for DataHandler commands and looks for moved pages, if found further
132 * changes take place to determine whether the cache needs to be updated
133 *
134 * @param string $table Table name of the moved record
135 * @param int $recordId The record's uid
136 * @param int $destinationPid The record's destination page id
137 * @param array $movedRecord The record that moved
138 * @param array $updatedFields Array of changed fields
139 * @param DataHandler $dataHandler DataHandler parent object
140 */
141 public function moveRecord_firstElementPostProcess($table, $recordId, $destinationPid, array $movedRecord, array $updatedFields, DataHandler $dataHandler)
142 {
143 if ($table === 'pages' && $this->requiresUpdate($updatedFields)) {
144 $affectedPageUid = $recordId;
145 $affectedPageOldPid = $movedRecord['pid'];
146 $affectedPageNewPid = $updatedFields['pid'];
147 $clearCacheActions = $this->determineClearCacheActions('update', $updatedFields);
148 // Clear treelist entries for old parent page
149 $this->processClearCacheActions($affectedPageUid, $affectedPageOldPid, $updatedFields, $clearCacheActions);
150 // Clear treelist entries for new parent page
151 $this->processClearCacheActions($affectedPageUid, $affectedPageNewPid, $updatedFields, $clearCacheActions);
152 }
153 }
154
155 /**
156 * Waits for DataHandler commands and looks for moved pages, if found further
157 * changes take place to determine whether the cache needs to be updated
158 *
159 * @param string $table Table name of the moved record
160 * @param int $recordId The record's uid
161 * @param int $destinationPid The record's destination page id
162 * @param int $originalDestinationPid (negative) page id th page has been moved after
163 * @param array $movedRecord The record that moved
164 * @param array $updatedFields Array of changed fields
165 * @param DataHandler $dataHandler DataHandler parent object
166 */
167 public function moveRecord_afterAnotherElementPostProcess($table, $recordId, $destinationPid, $originalDestinationPid, array $movedRecord, array $updatedFields, DataHandler $dataHandler)
168 {
169 if ($table === 'pages' && $this->requiresUpdate($updatedFields)) {
170 $affectedPageUid = $recordId;
171 $affectedPageOldPid = $movedRecord['pid'];
172 $affectedPageNewPid = $updatedFields['pid'];
173 $clearCacheActions = $this->determineClearCacheActions('update', $updatedFields);
174 // Clear treelist entries for old parent page
175 $this->processClearCacheActions($affectedPageUid, $affectedPageOldPid, $updatedFields, $clearCacheActions);
176 // Clear treelist entries for new parent page
177 $this->processClearCacheActions($affectedPageUid, $affectedPageNewPid, $updatedFields, $clearCacheActions);
178 }
179 }
180
181 /**
182 * Checks whether the change requires an update of the treelist cache
183 *
184 * @param array $updatedFields Array of changed fields
185 * @return bool TRUE if the treelist cache needs to be updated, FALSE if no update to the cache is required
186 */
187 protected function requiresUpdate(array $updatedFields)
188 {
189 $requiresUpdate = false;
190 $updatedFieldNames = array_keys($updatedFields);
191 foreach ($updatedFieldNames as $updatedFieldName) {
192 if (in_array($updatedFieldName, $this->updateRequiringFields, true)) {
193 $requiresUpdate = true;
194 break;
195 }
196 }
197 return $requiresUpdate;
198 }
199
200 /**
201 * Calls the cache maintenance functions according to the determined actions
202 *
203 * @param int $affectedPage uid of the affected page
204 * @param int $affectedParentPage parent uid of the affected page
205 * @param array $updatedFields Array of updated fields and their new values
206 * @param array $actions Array of actions to carry out
207 */
208 protected function processClearCacheActions($affectedPage, $affectedParentPage, $updatedFields, array $actions)
209 {
210 $actionNames = array_keys($actions);
211 foreach ($actionNames as $actionName) {
212 switch ($actionName) {
213 case 'allParents':
214 $this->clearCacheForAllParents($affectedParentPage);
215 break;
216 case 'setExpiration':
217 // Only used when setting an end time for a page
218 $expirationTime = $updatedFields['endtime'];
219 $this->setCacheExpiration($affectedPage, $expirationTime);
220 break;
221 case 'uidInTreelist':
222 $this->clearCacheWhereUidInTreelist($affectedPage);
223 break;
224 }
225 }
226 // From time to time clean the cache from expired entries
227 // (theoretically every 1000 calls)
228 $randomNumber = random_int(1, 1000);
229 if ($randomNumber === 500) {
230 $this->removeExpiredCacheEntries();
231 }
232 }
233
234 /**
235 * Clears the treelist cache for all parents of a changed page.
236 * gets called after creating a new page and after moving a page
237 *
238 * @param int $affectedParentPage Parent page id of the changed page, the page to start clearing from
239 */
240 protected function clearCacheForAllParents($affectedParentPage)
241 {
242 $rootLine = BackendUtility::BEgetRootLine($affectedParentPage);
243 $rootLineIds = [];
244 foreach ($rootLine as $page) {
245 if ($page['uid'] != 0) {
246 $rootLineIds[] = $page['uid'];
247 }
248 }
249 if (!empty($rootLineIds)) {
250 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
251 ->getQueryBuilderForTable('cache_treelist');
252 $queryBuilder
253 ->delete('cache_treelist')
254 ->where(
255 $queryBuilder->expr()->in(
256 'pid',
257 $queryBuilder->createNamedParameter($rootLineIds, Connection::PARAM_INT_ARRAY)
258 )
259 )
260 ->execute();
261 }
262 }
263
264 /**
265 * Clears the treelist cache for all pages where the affected page is found
266 * in the treelist
267 *
268 * @param int $affectedPage ID of the changed page
269 */
270 protected function clearCacheWhereUidInTreelist($affectedPage)
271 {
272 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
273 ->getQueryBuilderForTable('cache_treelist');
274 $queryBuilder
275 ->delete('cache_treelist')
276 ->where(
277 $queryBuilder->expr()->inSet('treelist', $queryBuilder->quote($affectedPage))
278 )
279 ->execute();
280 }
281
282 /**
283 * Sets an expiration time for all cache entries having the changed page in
284 * the treelist.
285 *
286 * @param int $affectedPage Uid of the changed page
287 * @param int $expirationTime
288 */
289 protected function setCacheExpiration($affectedPage, $expirationTime)
290 {
291 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
292 ->getQueryBuilderForTable('cache_treelist');
293 $queryBuilder
294 ->update('cache_treelist')
295 ->where(
296 $queryBuilder->expr()->inSet('treelist', $queryBuilder->quote($affectedPage))
297 )
298 ->set('expires', $expirationTime)
299 ->execute();
300 }
301
302 /**
303 * Removes all expired treelist cache entries
304 */
305 protected function removeExpiredCacheEntries()
306 {
307 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
308 ->getQueryBuilderForTable('cache_treelist');
309 $queryBuilder
310 ->delete('cache_treelist')
311 ->where(
312 $queryBuilder->expr()->lte(
313 'expires',
314 $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
315 )
316 )
317 ->execute();
318 }
319
320 /**
321 * Determines what happened to the page record, this is necessary to clear
322 * as less cache entries as needed later
323 *
324 * @param string $status DataHandler operation status, either 'new' or 'update'
325 * @param array $updatedFields Array of updated fields
326 * @return array List of actions that happened to the page record
327 */
328 protected function determineClearCacheActions($status, $updatedFields): array
329 {
330 $actions = [];
331 if ($status === 'new') {
332 // New page
333 $actions['allParents'] = true;
334 } elseif ($status === 'update') {
335 $updatedFieldNames = array_keys($updatedFields);
336 foreach ($updatedFieldNames as $updatedFieldName) {
337 switch ($updatedFieldName) {
338 case 'pid':
339
340 case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['disabled']:
341
342 case $GLOBALS['TCA']['pages']['ctrl']['delete']:
343
344 case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['starttime']:
345
346 case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['fe_group']:
347
348 case 'extendToSubpages':
349
350 case 't3ver_wsid':
351
352 case 'php_tree_stop':
353 // php_tree_stop
354 $actions['allParents'] = true;
355 $actions['uidInTreelist'] = true;
356 break;
357 case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['endtime']:
358 // end time set/unset
359 // When setting an end time the cache entry needs an
360 // expiration time. When unsetting the end time the
361 // page must become listed in the treelist again.
362 if ($updatedFields['endtime'] > 0) {
363 $actions['setExpiration'] = true;
364 } else {
365 $actions['uidInTreelist'] = true;
366 }
367 break;
368 default:
369 if (in_array($updatedFieldName, $this->updateRequiringFields, true)) {
370 $actions['uidInTreelist'] = true;
371 }
372 }
373 }
374 }
375 return $actions;
376 }
377 }