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