e18d77380541aec97cef5128ca8d6747bc885817
[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 */
27 class TreelistCacheUpdateHooks
28 {
29 /**
30 * Should not be manipulated from others except through the
31 * configuration provided @see __construct()
32 *
33 * @var array
34 */
35 private $updateRequiringFields = [
36 'pid',
37 'php_tree_stop',
38 'extendToSubpages'
39 ];
40
41 /**
42 * Constructor, adds update requiring fields to the default ones
43 */
44 public function __construct()
45 {
46 // As enableFields can be set dynamically we add them here
47 $pagesEnableFields = $GLOBALS['TCA']['pages']['ctrl']['enablecolumns'];
48 foreach ($pagesEnableFields as $pagesEnableField) {
49 $this->updateRequiringFields[] = $pagesEnableField;
50 }
51 $this->updateRequiringFields[] = $GLOBALS['TCA']['pages']['ctrl']['delete'];
52 // Extension can add fields to the pages table that require an
53 // update of the treelist cache, too; so we also add those
54 // example: $TYPO3_CONF_VARS['BE']['additionalTreelistUpdateFields'] .= ',my_field';
55 if (!empty($GLOBALS['TYPO3_CONF_VARS']['BE']['additionalTreelistUpdateFields'])) {
56 $additionalTreelistUpdateFields = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['BE']['additionalTreelistUpdateFields'], true);
57 $this->updateRequiringFields = array_merge($this->updateRequiringFields, $additionalTreelistUpdateFields);
58 }
59 }
60
61 /**
62 * waits for DataHandler commands and looks for changed pages, if found further
63 * changes take place to determine whether the cache needs to be updated
64 *
65 * @param string $status DataHandler operation status, either 'new' or 'update'
66 * @param string $table The DB table the operation was carried out on
67 * @param mixed $recordId The record's uid for update records, a string to look the record's uid up after it has been created
68 * @param array $updatedFields Array of changed fields and their new values
69 * @param DataHandler $dataHandler DataHandler parent object
70 * @return void
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 * @return void
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 === 'swap'))) {
110 $affectedRecord = BackendUtility::getRecord($table, $recordId, '*', '', false);
111 $affectedPageUid = $affectedRecord['uid'];
112 $affectedPagePid = $affectedRecord['pid'];
113
114 // Faking the updated fields
115 $updatedFields = [];
116 if ($command === 'delete') {
117 $updatedFields['deleted'] = 1;
118 } else {
119 // page was published to live (swapped)
120 $updatedFields['t3ver_wsid'] = 0;
121 }
122 $clearCacheActions = $this->determineClearCacheActions(
123 'update',
124 $updatedFields
125 );
126
127 $this->processClearCacheActions($affectedPageUid, $affectedPagePid, $updatedFields, $clearCacheActions);
128 }
129 }
130
131 /**
132 * waits for DataHandler commands and looks for moved pages, if found further
133 * changes take place to determine whether the cache needs to be updated
134 *
135 * @param string $table Table name of the moved record
136 * @param int $recordId The record's uid
137 * @param int $destinationPid The record's destination page id
138 * @param array $movedRecord The record that moved
139 * @param array $updatedFields Array of changed fields
140 * @param DataHandler $dataHandler DataHandler parent object
141 * @return void
142 */
143 public function moveRecord_firstElementPostProcess($table, $recordId, $destinationPid, array $movedRecord, array $updatedFields, DataHandler $dataHandler)
144 {
145 if ($table === 'pages' && $this->requiresUpdate($updatedFields)) {
146 $affectedPageUid = $recordId;
147 $affectedPageOldPid = $movedRecord['pid'];
148 $affectedPageNewPid = $updatedFields['pid'];
149 $clearCacheActions = $this->determineClearCacheActions('update', $updatedFields);
150 // Clear treelist entries for old parent page
151 $this->processClearCacheActions($affectedPageUid, $affectedPageOldPid, $updatedFields, $clearCacheActions);
152 // Clear treelist entries for new parent page
153 $this->processClearCacheActions($affectedPageUid, $affectedPageNewPid, $updatedFields, $clearCacheActions);
154 }
155 }
156
157 /**
158 * Waits for DataHandler commands and looks for moved pages, if found further
159 * changes take place to determine whether the cache needs to be updated
160 *
161 * @param string $table Table name of the moved record
162 * @param int $recordId The record's uid
163 * @param int $destinationPid The record's destination page id
164 * @param int $originalDestinationPid (negative) page id th page has been moved after
165 * @param array $movedRecord The record that moved
166 * @param array $updatedFields Array of changed fields
167 * @param DataHandler $dataHandler DataHandler parent object
168 * @return void
169 */
170 public function moveRecord_afterAnotherElementPostProcess($table, $recordId, $destinationPid, $originalDestinationPid, array $movedRecord, array $updatedFields, DataHandler $dataHandler)
171 {
172 if ($table === 'pages' && $this->requiresUpdate($updatedFields)) {
173 $affectedPageUid = $recordId;
174 $affectedPageOldPid = $movedRecord['pid'];
175 $affectedPageNewPid = $updatedFields['pid'];
176 $clearCacheActions = $this->determineClearCacheActions('update', $updatedFields);
177 // Clear treelist entries for old parent page
178 $this->processClearCacheActions($affectedPageUid, $affectedPageOldPid, $updatedFields, $clearCacheActions);
179 // Clear treelist entries for new parent page
180 $this->processClearCacheActions($affectedPageUid, $affectedPageNewPid, $updatedFields, $clearCacheActions);
181 }
182 }
183
184 /**
185 * Checks whether the change requires an update of the treelist cache
186 *
187 * @param array $updatedFields Array of changed fields
188 * @return bool TRUE if the treelist cache needs to be updated, FALSE if no update to the cache is required
189 */
190 protected function requiresUpdate(array $updatedFields)
191 {
192 $requiresUpdate = false;
193 $updatedFieldNames = array_keys($updatedFields);
194 foreach ($updatedFieldNames as $updatedFieldName) {
195 if (in_array($updatedFieldName, $this->updateRequiringFields, true)) {
196 $requiresUpdate = true;
197 break;
198 }
199 }
200 return $requiresUpdate;
201 }
202
203 /**
204 * Calls the cache maintenance functions according to the determined actions
205 *
206 * @param int $affectedPage uid of the affected page
207 * @param int $affectedParentPage parent uid of the affected page
208 * @param array $updatedFields Array of updated fields and their new values
209 * @param array $actions Array of actions to carry out
210 * @return void
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 = rand(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 * @return void
244 */
245 protected function clearCacheForAllParents($affectedParentPage)
246 {
247 $rootLine = BackendUtility::BEgetRootLine($affectedParentPage);
248 $rootLineIds = [];
249 foreach ($rootLine as $page) {
250 if ($page['uid'] != 0) {
251 $rootLineIds[] = $page['uid'];
252 }
253 }
254 if (!empty($rootLineIds)) {
255 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
256 ->getQueryBuilderForTable('cache_treelist');
257 $queryBuilder
258 ->delete('cache_treelist')
259 ->where(
260 $queryBuilder->expr()->in(
261 'pid',
262 $queryBuilder->createNamedParameter($rootLineIds, Connection::PARAM_INT_ARRAY)
263 )
264 )
265 ->execute();
266 }
267 }
268
269 /**
270 * Clears the treelist cache for all pages where the affected page is found
271 * in the treelist
272 *
273 * @param int $affectedPage ID of the changed page
274 * @return void
275 */
276 protected function clearCacheWhereUidInTreelist($affectedPage)
277 {
278 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
279 ->getQueryBuilderForTable('cache_treelist');
280 $queryBuilder
281 ->delete('cache_treelist')
282 ->where(
283 $queryBuilder->expr()->inSet('treelist', (int)$affectedPage)
284 )
285 ->execute();
286 }
287
288 /**
289 * Sets an expiration time for all cache entries having the changed page in
290 * the treelist.
291 *
292 * @param int $affectedPage Uid of the changed page
293 * @param int $expirationTime
294 * @return void
295 */
296 protected function setCacheExpiration($affectedPage, $expirationTime)
297 {
298 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
299 ->getQueryBuilderForTable('cache_treelist');
300 $queryBuilder
301 ->update('cache_treelist')
302 ->where(
303 $queryBuilder->expr()->inSet('treelist', (int)$affectedPage)
304 )
305 ->set('expires', $expirationTime)
306 ->execute();
307 }
308
309 /**
310 * Removes all expired treelist cache entries
311 *
312 * @return void
313 */
314 protected function removeExpiredCacheEntries()
315 {
316 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
317 ->getQueryBuilderForTable('cache_treelist');
318 $queryBuilder
319 ->delete('cache_treelist')
320 ->where(
321 $queryBuilder->expr()->lte(
322 'expires',
323 $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
324 )
325 )
326 ->execute();
327 }
328
329 /**
330 * Determines what happened to the page record, this is necessary to clear
331 * as less cache entries as needed later
332 *
333 * @param string $status DataHandler operation status, either 'new' or 'update'
334 * @param array $updatedFields Array of updated fields
335 * @return string List of actions that happened to the page record
336 */
337 protected function determineClearCacheActions($status, $updatedFields)
338 {
339 $actions = [];
340 if ($status === 'new') {
341 // New page
342 $actions['allParents'] = true;
343 } elseif ($status === 'update') {
344 $updatedFieldNames = array_keys($updatedFields);
345 foreach ($updatedFieldNames as $updatedFieldName) {
346 switch ($updatedFieldName) {
347 case 'pid':
348
349 case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['disabled']:
350
351 case $GLOBALS['TCA']['pages']['ctrl']['delete']:
352
353 case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['starttime']:
354
355 case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['fe_group']:
356
357 case 'extendToSubpages':
358
359 case 't3ver_wsid':
360
361 case 'php_tree_stop':
362 // php_tree_stop
363 $actions['allParents'] = true;
364 $actions['uidInTreelist'] = true;
365 break;
366 case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['endtime']:
367 // end time set/unset
368 // When setting an end time the cache entry needs an
369 // expiration time. When unsetting the end time the
370 // page must become listed in the treelist again.
371 if ($updatedFields['endtime'] > 0) {
372 $actions['setExpiration'] = true;
373 } else {
374 $actions['uidInTreelist'] = true;
375 }
376 break;
377 default:
378 if (in_array($updatedFieldName, $this->updateRequiringFields, true)) {
379 $actions['uidInTreelist'] = true;
380 }
381 }
382 }
383 }
384 return $actions;
385 }
386 }