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