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