[TASK] Migrate to short array syntax
[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\ConnectionPool;
19 use TYPO3\CMS\Core\DataHandling\DataHandler;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21
22 /**
23 * Class that hooks into TCEmain and listens for updates to pages to update the
24 * treelist cache
25 */
26 class TreelistCacheUpdateHooks
27 {
28 /**
29 * Should not be manipulated from others except through the
30 * configuration provided @see __construct()
31 *
32 * @var array
33 */
34 private $updateRequiringFields = [
35 'pid',
36 'php_tree_stop',
37 'extendToSubpages'
38 ];
39
40 /**
41 * Constructor, adds update requiring fields to the default ones
42 */
43 public function __construct()
44 {
45 // As enableFields can be set dynamically we add them here
46 $pagesEnableFields = $GLOBALS['TCA']['pages']['ctrl']['enablecolumns'];
47 foreach ($pagesEnableFields as $pagesEnableField) {
48 $this->updateRequiringFields[] = $pagesEnableField;
49 }
50 $this->updateRequiringFields[] = $GLOBALS['TCA']['pages']['ctrl']['delete'];
51 // Extension can add fields to the pages table that require an
52 // update of the treelist cache, too; so we also add those
53 // example: $TYPO3_CONF_VARS['BE']['additionalTreelistUpdateFields'] .= ',my_field';
54 if (!empty($GLOBALS['TYPO3_CONF_VARS']['BE']['additionalTreelistUpdateFields'])) {
55 $additionalTreelistUpdateFields = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['BE']['additionalTreelistUpdateFields'], true);
56 $this->updateRequiringFields = array_merge($this->updateRequiringFields, $additionalTreelistUpdateFields);
57 }
58 }
59
60 /**
61 * waits for TCEmain commands and looks for changed pages, if found further
62 * changes take place to determine whether the cache needs to be updated
63 *
64 * @param string $status TCEmain operation status, either 'new' or 'update'
65 * @param string $table The DB table the operation was carried out on
66 * @param mixed $recordId The record's uid for update records, a string to look the record's uid up after it has been created
67 * @param array $updatedFields Array of changed fiels and their new values
68 * @param DataHandler $tceMain TCEmain parent object
69 * @return void
70 */
71 public function processDatamap_afterDatabaseOperations($status, $table, $recordId, array $updatedFields, DataHandler $tceMain)
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 = $tceMain->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 $tceMain The TCEmain parent object
103 * @return void
104 */
105 public function processCmdmap_postProcess($command, $table, $recordId, $commandValue, DataHandler $tceMain)
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 TCEmain 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 $tceMain TCEmain parent object
140 * @return void
141 */
142 public function moveRecord_firstElementPostProcess($table, $recordId, $destinationPid, array $movedRecord, array $updatedFields, DataHandler $tceMain)
143 {
144 if ($table == 'pages' && $this->requiresUpdate($updatedFields)) {
145 $affectedPageUid = $recordId;
146 $affectedPageOldPid = $movedRecord['pid'];
147 $affectedPageNewPid = $updatedFields['pid'];
148 $clearCacheActions = $this->determineClearCacheActions('update', $updatedFields);
149 // Clear treelist entries for old parent page
150 $this->processClearCacheActions($affectedPageUid, $affectedPageOldPid, $updatedFields, $clearCacheActions);
151 // Clear treelist entries for new parent page
152 $this->processClearCacheActions($affectedPageUid, $affectedPageNewPid, $updatedFields, $clearCacheActions);
153 }
154 }
155
156 /**
157 * Waits for TCEmain commands and looks for moved pages, if found further
158 * changes take place to determine whether the cache needs to be updated
159 *
160 * @param string $table Table name of the moved record
161 * @param int $recordId The record's uid
162 * @param int $destinationPid The record's destination page id
163 * @param int $originalDestinationPid (negative) page id th page has been moved after
164 * @param array $movedRecord The record that moved
165 * @param array $updatedFields Array of changed fields
166 * @param DataHandler $tceMain TCEmain parent object
167 * @return void
168 */
169 public function moveRecord_afterAnotherElementPostProcess($table, $recordId, $destinationPid, $originalDestinationPid, array $movedRecord, array $updatedFields, DataHandler $tceMain)
170 {
171 if ($table == 'pages' && $this->requiresUpdate($updatedFields)) {
172 $affectedPageUid = $recordId;
173 $affectedPageOldPid = $movedRecord['pid'];
174 $affectedPageNewPid = $updatedFields['pid'];
175 $clearCacheActions = $this->determineClearCacheActions('update', $updatedFields);
176 // Clear treelist entries for old parent page
177 $this->processClearCacheActions($affectedPageUid, $affectedPageOldPid, $updatedFields, $clearCacheActions);
178 // Clear treelist entries for new parent page
179 $this->processClearCacheActions($affectedPageUid, $affectedPageNewPid, $updatedFields, $clearCacheActions);
180 }
181 }
182
183 /**
184 * Checks whether the change requires an update of the treelist cache
185 *
186 * @param array $updatedFields Array of changed fields
187 * @return bool TRUE if the treelist cache needs to be updated, FALSE if no update to the cache is required
188 */
189 protected function requiresUpdate(array $updatedFields)
190 {
191 $requiresUpdate = false;
192 $updatedFieldNames = array_keys($updatedFields);
193 foreach ($updatedFieldNames as $updatedFieldName) {
194 if (in_array($updatedFieldName, $this->updateRequiringFields)) {
195 $requiresUpdate = true;
196 break;
197 }
198 }
199 return $requiresUpdate;
200 }
201
202 /**
203 * Calls the cache maintainance functions according to the determined actions
204 *
205 * @param int $affectedPage uid of the affected page
206 * @param int $affectedParentPage parent uid of the affected page
207 * @param array $updatedFields Array of updated fields and their new values
208 * @param array $actions Array of actions to carry out
209 * @return void
210 */
211 protected function processClearCacheActions($affectedPage, $affectedParentPage, $updatedFields, array $actions)
212 {
213 $actionNames = array_keys($actions);
214 foreach ($actionNames as $actionName) {
215 switch ($actionName) {
216 case 'allParents':
217 $this->clearCacheForAllParents($affectedParentPage);
218 break;
219 case 'setExpiration':
220 // Only used when setting an end time for a page
221 $expirationTime = $updatedFields['endtime'];
222 $this->setCacheExpiration($affectedPage, $expirationTime);
223 break;
224 case 'uidInTreelist':
225 $this->clearCacheWhereUidInTreelist($affectedPage);
226 break;
227 }
228 }
229 // From time to time clean the cache from expired entries
230 // (theoretically every 1000 calls)
231 $randomNumber = rand(1, 1000);
232 if ($randomNumber == 500) {
233 $this->removeExpiredCacheEntries();
234 }
235 }
236
237 /**
238 * Clears the treelist cache for all parents of a changed page.
239 * gets called after creating a new page and after moving a page
240 *
241 * @param int $affectedParentPage Parent page id of the changed page, the page to start clearing from
242 * @return void
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('pid', $rootLineIds)
260 )
261 ->execute();
262 }
263 }
264
265 /**
266 * Clears the treelist cache for all pages where the affected page is found
267 * in the treelist
268 *
269 * @param int $affectedPage ID of the changed page
270 * @return void
271 */
272 protected function clearCacheWhereUidInTreelist($affectedPage)
273 {
274 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
275 ->getQueryBuilderForTable('cache_treelist');
276 $queryBuilder
277 ->delete('cache_treelist')
278 ->where(
279 $queryBuilder->expr()->inSet('treelist', (int)$affectedPage)
280 )
281 ->execute();
282 }
283
284 /**
285 * Sets an expiration time for all cache entries having the changed page in
286 * the treelist.
287 *
288 * @param int $affectedPage Uid of the changed page
289 * @param int $expirationTime
290 * @return void
291 */
292 protected function setCacheExpiration($affectedPage, $expirationTime)
293 {
294 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
295 ->getQueryBuilderForTable('cache_treelist');
296 $queryBuilder
297 ->update('cache_treelist')
298 ->where(
299 $queryBuilder->expr()->inSet('treelist', (int)$affectedPage)
300 )
301 ->set('expires', $expirationTime)
302 ->execute();
303 }
304
305 /**
306 * Removes all expired treelist cache entries
307 *
308 * @return void
309 */
310 protected function removeExpiredCacheEntries()
311 {
312 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
313 ->getQueryBuilderForTable('cache_treelist');
314 $queryBuilder
315 ->delete('cache_treelist')
316 ->where(
317 $queryBuilder->expr()->lte('expires', (int)$GLOBALS['EXEC_TIME'])
318 )
319 ->execute();
320 }
321
322 /**
323 * Determines what happened to the page record, this is necessary to clear
324 * as less cache entries as needed later
325 *
326 * @param string $status TCEmain operation status, either 'new' or 'update'
327 * @param array $updatedFields Array of updated fields
328 * @return string List of actions that happened to the page record
329 */
330 protected function determineClearCacheActions($status, $updatedFields)
331 {
332 $actions = [];
333 if ($status == 'new') {
334 // New page
335 $actions['allParents'] = true;
336 } elseif ($status == 'update') {
337 $updatedFieldNames = array_keys($updatedFields);
338 foreach ($updatedFieldNames as $updatedFieldName) {
339 switch ($updatedFieldName) {
340 case 'pid':
341
342 case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['disabled']:
343
344 case $GLOBALS['TCA']['pages']['ctrl']['delete']:
345
346 case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['starttime']:
347
348 case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['fe_group']:
349
350 case 'extendToSubpages':
351
352 case 't3ver_wsid':
353
354 case 'php_tree_stop':
355 // php_tree_stop
356 $actions['allParents'] = true;
357 $actions['uidInTreelist'] = true;
358 break;
359 case $GLOBALS['TCA']['pages']['ctrl']['enablecolumns']['endtime']:
360 // end time set/unset
361 // When setting an end time the cache entry needs an
362 // expiration time. When unsetting the end time the
363 // page must become listed in the treelist again.
364 if ($updatedFields['endtime'] > 0) {
365 $actions['setExpiration'] = true;
366 } else {
367 $actions['uidInTreelist'] = true;
368 }
369 break;
370 default:
371 if (in_array($updatedFieldName, $this->updateRequiringFields)) {
372 $actions['uidInTreelist'] = true;
373 }
374 }
375 }
376 }
377 return $actions;
378 }
379 }