‪TYPO3CMS  ‪main
RecyclerAjaxController.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\Http\Message\ResponseInterface;
21 use Psr\Http\Message\ServerRequestInterface;
24 use TYPO3\CMS\Backend\Utility\BackendUtility;
33 use TYPO3\CMS\Core\Imaging\IconSize;
40 
46 #[Controller]
48 {
52  protected array ‪$conf = [];
53 
54  public function ‪__construct(
55  protected readonly ‪BackendViewFactory $backendViewFactory,
56  protected readonly ‪FrontendInterface $runtimeCache,
57  protected readonly ‪IconFactory $iconFactory,
58  protected readonly ‪ConnectionPool $connectionPool
59  ) {}
60 
64  public function ‪dispatch(ServerRequestInterface $request): ResponseInterface
65  {
66  $parsedBody = $request->getParsedBody();
67  $queryParams = $request->getQueryParams();
68 
69  $this->conf['action'] = $parsedBody['action'] ?? $queryParams['action'] ?? null;
70  $this->conf['table'] = $parsedBody['table'] ?? $queryParams['table'] ?? '';
71  $this->conf['limit'] = ‪MathUtility::forceIntegerInRange(
72  (int)($this->‪getBackendUser()->getTSConfig()['mod.']['recycler.']['recordsPageLimit'] ?? 25),
73  1
74  );
75  $this->conf['start'] = (int)($parsedBody['start'] ?? $queryParams['start'] ?? 0);
76  $this->conf['filterTxt'] = $parsedBody['filterTxt'] ?? $queryParams['filterTxt'] ?? '';
77  $this->conf['startUid'] = (int)($parsedBody['startUid'] ?? $queryParams['startUid'] ?? 0);
78  $this->conf['depth'] = (int)($parsedBody['depth'] ?? $queryParams['depth'] ?? 0);
79  $this->conf['records'] = $parsedBody['records'] ?? $queryParams['records'] ?? null;
80  $this->conf['recursive'] = (bool)($parsedBody['recursive'] ?? $queryParams['recursive'] ?? false);
81 
82  $content = null;
83  // Determine the scripts to execute
84  switch ($this->conf['action']) {
85  case 'getTables':
86  $this->‪setDataInSession(['depthSelection' => $this->conf['depth']]);
87 
88  $content = $this->‪getTables($this->conf['startUid'], $this->conf['depth']);
89  break;
90  case 'getDeletedRecords':
91  $this->‪setDataInSession([
92  'tableSelection' => $this->conf['table'],
93  'depthSelection' => $this->conf['depth'],
94  'resultLimit' => $this->conf['limit'],
95  ]);
96 
97  $model = GeneralUtility::makeInstance(DeletedRecords::class);
98  $model->loadData($this->conf['startUid'], $this->conf['table'], $this->conf['depth'], $this->conf['start'] . ',' . $this->conf['limit'], $this->conf['filterTxt']);
99  $deletedRowsArray = $model->getDeletedRows();
100 
101  $model = GeneralUtility::makeInstance(DeletedRecords::class);
102  $totalDeleted = $model->getTotalCount($this->conf['startUid'], $this->conf['table'], $this->conf['depth'], $this->conf['filterTxt']);
103 
104  $allowDelete = $this->‪getBackendUser()->isAdmin()
105  ?: (bool)($this->‪getBackendUser()->getTSConfig()['mod.']['recycler.']['allowDelete'] ?? false);
106 
107  $view = $this->backendViewFactory->create($request);
108  $view->assign('showTableHeader', empty($this->conf['table']));
109  $view->assign('showTableName', $this->‪getBackendUser()->shallDisplayDebugInformation());
110  $view->assign('allowDelete', $allowDelete);
111  $view->assign('groupedRecords', $this->‪transform($deletedRowsArray));
112  $content = [
113  'rows' => $view->render('Ajax/RecordsTable'),
114  'totalItems' => $totalDeleted,
115  ];
116  break;
117  case 'undoRecords':
118  if (empty($this->conf['records']) || !is_array($this->conf['records'])) {
119  $content = [
120  'success' => false,
121  'message' => ‪LocalizationUtility::translate('flashmessage.delete.norecordsselected', 'recycler'),
122  ];
123  break;
124  }
125 
126  $model = GeneralUtility::makeInstance(DeletedRecords::class);
127  $affectedRecords = $model->undeleteData($this->conf['records'], $this->conf['recursive']);
128  $messageKey = 'flashmessage.undo.' . ($affectedRecords !== false ? 'success' : 'failure') . '.' . ((int)$affectedRecords === 1 ? 'singular' : 'plural');
129  $content = [
130  'success' => true,
131  'message' => sprintf((string)‪LocalizationUtility::translate($messageKey, 'recycler'), $affectedRecords),
132  ];
133  break;
134  case 'deleteRecords':
135  if (empty($this->conf['records']) || !is_array($this->conf['records'])) {
136  $content = [
137  'success' => false,
138  'message' => ‪LocalizationUtility::translate('flashmessage.delete.norecordsselected', 'recycler'),
139  ];
140  break;
141  }
142 
143  $model = GeneralUtility::makeInstance(DeletedRecords::class);
144  $success = $model->deleteData($this->conf['records']);
145  $affectedRecords = count($this->conf['records']);
146  $messageKey = 'flashmessage.delete.' . ($success ? 'success' : 'failure') . '.' . ($affectedRecords === 1 ? 'singular' : 'plural');
147  $content = [
148  'success' => true,
149  'message' => sprintf((string)‪LocalizationUtility::translate($messageKey, 'recycler'), $affectedRecords),
150  ];
151  break;
152  }
153  return new ‪JsonResponse($content);
154  }
155 
162  protected function ‪transform(array $deletedRowsArray): array
163  {
164  $groupedRecords = [];
165  $lang = $this->‪getLanguageService();
166 
167  $recordHistory = GeneralUtility::makeInstance(RecordHistory::class);
168  foreach ($deletedRowsArray as $table => $rows) {
169  $groupedRecords[$table]['information'] = [
170  'table' => $table,
171  'title' => isset(‪$GLOBALS['TCA'][$table]['ctrl']['title']) ? $lang->sL(‪$GLOBALS['TCA'][$table]['ctrl']['title']) : BackendUtility::getNoRecordTitle(),
172  ];
173  foreach ($rows as $row) {
174  $pageTitle = $this->‪getPageTitle((int)$row['pid']);
175  $ownerInformation = $recordHistory->getCreationInformationForRecord($table, $row);
176  $ownerUid = (int)(is_array($ownerInformation) && $ownerInformation['actiontype'] === 'BE' ? $ownerInformation['userid'] : 0);
177  $backendUserName = $this->‪getBackendUserInformation($ownerUid);
178  $userIdWhoDeleted = $this->‪getUserWhoDeleted($table, (int)$row['uid']);
179 
180  $groupedRecords[$table]['records'][] = [
181  'uid' => $row['uid'],
182  'pid' => $row['pid'],
183  'icon' => $this->iconFactory->getIconForRecord($table, $row, IconSize::SMALL)->render(),
184  'pageTitle' => $pageTitle,
185  'crdate' => isset(‪$GLOBALS['TCA'][$table]['ctrl']['crdate']) ? BackendUtility::datetime($row[‪$GLOBALS['TCA'][$table]['ctrl']['crdate']]) : '',
186  'tstamp' => isset(‪$GLOBALS['TCA'][$table]['ctrl']['tstamp']) ? BackendUtility::datetime($row[‪$GLOBALS['TCA'][$table]['ctrl']['tstamp']]) : '',
187  'owner' => $backendUserName,
188  'owner_uid' => $ownerUid,
189  'title' => BackendUtility::getRecordTitle($table, $row),
190  'path' => $this->‪getRecordPath((int)$row['pid']),
191  'delete_user_uid' => $userIdWhoDeleted,
192  'delete_user' => $this->‪getBackendUserInformation($userIdWhoDeleted),
193  'isParentDeleted' => $table === 'pages' && $this->‪isParentPageDeleted((int)$row['pid']),
194  ];
195  }
196  }
197 
198  return $groupedRecords;
199  }
200 
204  protected function ‪getPageTitle(int $pageId): string
205  {
206  $cacheId = 'recycler-pagetitle-' . $pageId;
207  $pageTitle = $this->runtimeCache->get($cacheId);
208  if ($pageTitle === false) {
209  if ($pageId === 0) {
210  $pageTitle = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
211  } else {
212  $recordInfo = BackendUtility::getRecord('pages', (string)$pageId, '*', '', false);
213  $pageTitle = $recordInfo['title'] ?? '';
214  }
215  $this->runtimeCache->set($cacheId, $pageTitle);
216  }
217  return $pageTitle;
218  }
219 
223  protected function ‪getBackendUserInformation(int $userId): string
224  {
225  if ($userId === 0) {
226  return '';
227  }
228  $cacheId = 'recycler-user-' . $userId;
229  $username = $this->runtimeCache->get($cacheId);
230  if ($username === false) {
231  $backendUser = BackendUtility::getRecord('be_users', $userId, 'username', '', false);
232  if ($backendUser === null) {
233  $username = sprintf(
234  '[%s]',
235  ‪LocalizationUtility::translate('LLL:EXT:recycler/Resources/Private/Language/locallang.xlf:record.deleted')
236  );
237  } else {
238  $username = $backendUser['username'];
239  }
240  $this->runtimeCache->set($cacheId, $username);
241  }
242  return $username;
243  }
244 
249  protected function ‪getUserWhoDeleted(string $table, int ‪$uid): int
250  {
251  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_history');
252  $queryBuilder->select('userid')
253  ->from('sys_history')
254  ->where(
255  $queryBuilder->expr()->eq(
256  'tablename',
257  $queryBuilder->createNamedParameter($table)
258  ),
259  $queryBuilder->expr()->eq(
260  'usertype',
261  $queryBuilder->createNamedParameter('BE')
262  ),
263  $queryBuilder->expr()->eq(
264  'recuid',
265  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
266  ),
267  $queryBuilder->expr()->eq(
268  'actiontype',
269  $queryBuilder->createNamedParameter(‪RecordHistoryStore::ACTION_DELETE, ‪Connection::PARAM_INT)
270  )
271  )
272  ->setMaxResults(1);
273 
274  return (int)$queryBuilder->executeQuery()->fetchOne();
275  }
276 
282  protected function ‪setDataInSession(array $data): void
283  {
284  $beUser = $this->‪getBackendUser();
285  $recyclerUC = $beUser->uc['tx_recycler'] ?? [];
286  if (!empty(array_diff_assoc($data, $recyclerUC))) {
287  $beUser->uc['tx_recycler'] = array_merge($recyclerUC, $data);
288  $beUser->writeUC();
289  }
290  }
291 
300  protected function ‪getRecordPath(int ‪$uid): string
301  {
302  ‪$output = '/';
303  if (‪$uid === 0) {
304  return ‪$output;
305  }
306  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
307  $queryBuilder->getRestrictions()->removeAll();
308 
309  $loopCheck = 100;
310  while ($loopCheck > 0) {
311  $loopCheck--;
312 
313  $queryBuilder
314  ->select('uid', 'pid', 'title', 'deleted', 't3ver_oid', 't3ver_wsid', 't3ver_state')
315  ->from('pages')
316  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)));
317  $row = $queryBuilder->executeQuery()->fetchAssociative();
318  if ($row !== false) {
319  BackendUtility::workspaceOL('pages', $row);
320  if (is_array($row)) {
321  ‪$uid = (int)$row['pid'];
322  ‪$output = '/' . htmlspecialchars(‪GeneralUtility::fixed_lgd_cs($row['title'], 1000)) . ‪$output;
323  if ($row['deleted']) {
324  ‪$output = '<span class="text-danger">' . ‪$output . '</span>';
325  }
326  } else {
327  break;
328  }
329  } else {
330  break;
331  }
332  }
333  return ‪$output;
334  }
335 
339  protected function ‪isParentPageDeleted(int $pid): bool
340  {
341  if ($pid === 0) {
342  return false;
343  }
344  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
345  $queryBuilder->getRestrictions()->removeAll();
346 
347  $deleted = $queryBuilder
348  ->select('deleted')
349  ->from('pages')
350  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($pid, ‪Connection::PARAM_INT)))
351  ->executeQuery()
352  ->fetchOne();
353 
354  return (bool)$deleted;
355  }
356 
362  protected function ‪getTables(int $startUid, int $depth): array
363  {
364  $deletedRecordsTotal = 0;
365  $lang = $this->‪getLanguageService();
366  $tables = [];
367 
368  foreach (‪RecyclerUtility::getModifyableTables() as $tableName) {
369  $deletedField = ‪RecyclerUtility::getDeletedField($tableName);
370  if ($deletedField) {
371  // Determine whether the table has deleted records:
372  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
373  $queryBuilder->getRestrictions()->removeAll();
374 
375  $deletedCount = $queryBuilder->count('uid')
376  ->from($tableName)
377  ->where(
378  $queryBuilder->expr()->neq(
379  $deletedField,
380  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
381  )
382  )
383  ->executeQuery()
384  ->fetchOne();
385 
386  if ($deletedCount) {
387  /* @var DeletedRecords $deletedDataObject */
388  $deletedDataObject = GeneralUtility::makeInstance(DeletedRecords::class);
389  $deletedData = $deletedDataObject->loadData($startUid, $tableName, $depth)->getDeletedRows();
390  if (isset($deletedData[$tableName])) {
391  if ($deletedRecordsInTable = count($deletedData[$tableName])) {
392  $deletedRecordsTotal += $deletedRecordsInTable;
393  $tables[] = [
394  $tableName,
395  $deletedRecordsInTable,
396  $lang->sL(‪$GLOBALS['TCA'][$tableName]['ctrl']['title'] ?? $tableName),
397  ];
398  }
399  }
400  }
401  }
402  }
403  $jsonArray = $tables;
404  array_unshift($jsonArray, [
405  '',
406  $deletedRecordsTotal,
407  $lang->sL('LLL:EXT:recycler/Resources/Private/Language/locallang.xlf:label_allrecordtypes'),
408  ]);
409  return $jsonArray;
410  }
411 
413  {
414  return ‪$GLOBALS['BE_USER'];
415  }
416 
418  {
419  return ‪$GLOBALS['LANG'];
420  }
421 }
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\getLanguageService
‪getLanguageService()
Definition: RecyclerAjaxController.php:417
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:50
‪TYPO3\CMS\Core\Utility\GeneralUtility\fixed_lgd_cs
‪static string fixed_lgd_cs(string $string, int $chars, string $appendString='...')
Definition: GeneralUtility.php:91
‪TYPO3\CMS\Extbase\Utility\LocalizationUtility
Definition: LocalizationUtility.php:35
‪TYPO3\CMS\Backend\View\BackendViewFactory
Definition: BackendViewFactory.php:35
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\getPageTitle
‪getPageTitle(int $pageId)
Definition: RecyclerAjaxController.php:204
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\getTables
‪array getTables(int $startUid, int $depth)
Definition: RecyclerAjaxController.php:362
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\$conf
‪array $conf
Definition: RecyclerAjaxController.php:52
‪TYPO3\CMS\Backend\Attribute\Controller
Definition: Controller.php:25
‪TYPO3\CMS\Core\Imaging\IconFactory
Definition: IconFactory.php:34
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\getUserWhoDeleted
‪getUserWhoDeleted(string $table, int $uid)
Definition: RecyclerAjaxController.php:249
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\setDataInSession
‪setDataInSession(array $data)
Definition: RecyclerAjaxController.php:282
‪TYPO3\CMS\Recycler\Utility\RecyclerUtility\getDeletedField
‪static string getDeletedField($tableName)
Definition: RecyclerUtility.php:85
‪TYPO3\CMS\Recycler\Controller
Definition: RecyclerAjaxController.php:18
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\getBackendUser
‪getBackendUser()
Definition: RecyclerAjaxController.php:412
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore
Definition: RecordHistoryStore.php:31
‪TYPO3\CMS\Recycler\Utility\RecyclerUtility\getModifyableTables
‪static getModifyableTables()
Definition: RecyclerUtility.php:144
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\getRecordPath
‪string getRecordPath(int $uid)
Definition: RecyclerAjaxController.php:300
‪TYPO3\CMS\Backend\History\RecordHistory
Definition: RecordHistory.php:32
‪TYPO3\CMS\Extbase\Utility\LocalizationUtility\translate
‪static string null translate(string $key, ?string $extensionName=null, array $arguments=null, Locale|string $languageKey=null)
Definition: LocalizationUtility.php:47
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:60
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\transform
‪transform(array $deletedRowsArray)
Definition: RecyclerAjaxController.php:162
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\dispatch
‪dispatch(ServerRequestInterface $request)
Definition: RecyclerAjaxController.php:64
‪$output
‪$output
Definition: annotationChecker.php:119
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:39
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\isParentPageDeleted
‪isParentPageDeleted(int $pid)
Definition: RecyclerAjaxController.php:339
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords
Definition: DeletedRecords.php:38
‪TYPO3\CMS\Webhooks\Message\$uid
‪identifier readonly int $uid
Definition: PageModificationMessage.php:35
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\ACTION_DELETE
‪const ACTION_DELETE
Definition: RecordHistoryStore.php:35
‪TYPO3\CMS\Core\Http\JsonResponse
Definition: JsonResponse.php:28
‪TYPO3\CMS\Recycler\Utility\RecyclerUtility
Definition: RecyclerUtility.php:31
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController
Definition: RecyclerAjaxController.php:48
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:46
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:48
‪TYPO3\CMS\Core\Utility\MathUtility\forceIntegerInRange
‪static int forceIntegerInRange(mixed $theInt, int $min, int $max=2000000000, int $defaultValue=0)
Definition: MathUtility.php:34
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:51
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\getBackendUserInformation
‪getBackendUserInformation(int $userId)
Definition: RecyclerAjaxController.php:223
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\__construct
‪__construct(protected readonly BackendViewFactory $backendViewFactory, protected readonly FrontendInterface $runtimeCache, protected readonly IconFactory $iconFactory, protected readonly ConnectionPool $connectionPool)
Definition: RecyclerAjaxController.php:54