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