‪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;
32 use TYPO3\CMS\Core\Imaging\IconSize;
39 
45 #[AsController]
47 {
51  protected array ‪$conf = [];
52 
53  public function ‪__construct(
54  protected readonly ‪BackendViewFactory $backendViewFactory,
55  protected readonly ‪FrontendInterface $runtimeCache,
56  protected readonly ‪IconFactory $iconFactory,
57  protected readonly ‪ConnectionPool $connectionPool,
58  protected readonly ‪RecordHistory $recordHistory
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  foreach ($deletedRowsArray as $table => $rows) {
168  $groupedRecords[$table]['information'] = [
169  'table' => $table,
170  'title' => isset(‪$GLOBALS['TCA'][$table]['ctrl']['title']) ? $lang->sL(‪$GLOBALS['TCA'][$table]['ctrl']['title']) : BackendUtility::getNoRecordTitle(),
171  ];
172  foreach ($rows as $row) {
173  $pageTitle = $this->‪getPageTitle((int)$row['pid']);
174  $ownerInformation = $this->recordHistory->getCreationInformationForRecord($table, $row);
175  $ownerUid = (int)(is_array($ownerInformation) && $ownerInformation['actiontype'] === 'BE' ? $ownerInformation['userid'] : 0);
176  $backendUserName = $this->‪getBackendUserInformation($ownerUid);
177  $deleteUserUid = $this->recordHistory->getUserIdFromDeleteActionForRecord($table, (int)$row['uid']);
178 
179  $groupedRecords[$table]['records'][] = [
180  'uid' => $row['uid'],
181  'pid' => $row['pid'],
182  'icon' => $this->iconFactory->getIconForRecord($table, $row, IconSize::SMALL)->render(),
183  'pageTitle' => $pageTitle,
184  'crdate' => isset(‪$GLOBALS['TCA'][$table]['ctrl']['crdate']) ? BackendUtility::datetime($row[‪$GLOBALS['TCA'][$table]['ctrl']['crdate']]) : '',
185  'tstamp' => isset(‪$GLOBALS['TCA'][$table]['ctrl']['tstamp']) ? BackendUtility::datetime($row[‪$GLOBALS['TCA'][$table]['ctrl']['tstamp']]) : '',
186  'owner' => $backendUserName,
187  'owner_uid' => $ownerUid,
188  'title' => BackendUtility::getRecordTitle($table, $row),
189  'path' => $this->‪getRecordPath((int)$row['pid']),
190  'delete_user_uid' => $deleteUserUid,
191  'delete_user' => $this->‪getBackendUserInformation($deleteUserUid),
192  'isParentDeleted' => $table === 'pages' && $this->‪isParentPageDeleted((int)$row['pid']),
193  ];
194  }
195  }
196 
197  return $groupedRecords;
198  }
199 
203  protected function ‪getPageTitle(int $pageId): string
204  {
205  $cacheId = 'recycler-pagetitle-' . $pageId;
206  $pageTitle = $this->runtimeCache->get($cacheId);
207  if ($pageTitle === false) {
208  if ($pageId === 0) {
209  $pageTitle = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
210  } else {
211  $recordInfo = BackendUtility::getRecord('pages', (string)$pageId, '*', '', false);
212  $pageTitle = $recordInfo['title'] ?? '';
213  }
214  $this->runtimeCache->set($cacheId, $pageTitle);
215  }
216  return $pageTitle;
217  }
218 
222  protected function ‪getBackendUserInformation(int $userId): string
223  {
224  if ($userId === 0) {
225  return '';
226  }
227  $cacheId = 'recycler-user-' . $userId;
228  $username = $this->runtimeCache->get($cacheId);
229  if ($username === false) {
230  $backendUser = BackendUtility::getRecord('be_users', $userId, 'username', '', false);
231  if ($backendUser === null) {
232  $username = sprintf(
233  '[%s]',
234  ‪LocalizationUtility::translate('LLL:EXT:recycler/Resources/Private/Language/locallang.xlf:record.deleted')
235  );
236  } else {
237  $username = $backendUser['username'];
238  }
239  $this->runtimeCache->set($cacheId, $username);
240  }
241  return $username;
242  }
243 
249  protected function ‪setDataInSession(array $data): void
250  {
251  $beUser = $this->‪getBackendUser();
252  $recyclerUC = $beUser->uc['tx_recycler'] ?? [];
253  if (!empty(array_diff_assoc($data, $recyclerUC))) {
254  $beUser->uc['tx_recycler'] = array_merge($recyclerUC, $data);
255  $beUser->writeUC();
256  }
257  }
258 
267  protected function ‪getRecordPath(int ‪$uid): string
268  {
269  ‪$output = '/';
270  if (‪$uid === 0) {
271  return ‪$output;
272  }
273  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
274  $queryBuilder->getRestrictions()->removeAll();
275 
276  $loopCheck = 100;
277  while ($loopCheck > 0) {
278  $loopCheck--;
279 
280  $queryBuilder
281  ->select('uid', 'pid', 'title', 'deleted', 't3ver_oid', 't3ver_wsid', 't3ver_state')
282  ->from('pages')
283  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)));
284  $row = $queryBuilder->executeQuery()->fetchAssociative();
285  if ($row !== false) {
286  BackendUtility::workspaceOL('pages', $row);
287  if (is_array($row)) {
288  ‪$uid = (int)$row['pid'];
289  ‪$output = '/' . htmlspecialchars(‪GeneralUtility::fixed_lgd_cs($row['title'], 1000)) . ‪$output;
290  if ($row['deleted']) {
291  ‪$output = '<span class="text-danger">' . ‪$output . '</span>';
292  }
293  } else {
294  break;
295  }
296  } else {
297  break;
298  }
299  }
300  return ‪$output;
301  }
302 
306  protected function ‪isParentPageDeleted(int $pid): bool
307  {
308  if ($pid === 0) {
309  return false;
310  }
311  $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
312  $queryBuilder->getRestrictions()->removeAll();
313 
314  $deleted = $queryBuilder
315  ->select('deleted')
316  ->from('pages')
317  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($pid, ‪Connection::PARAM_INT)))
318  ->executeQuery()
319  ->fetchOne();
320 
321  return (bool)$deleted;
322  }
323 
329  protected function ‪getTables(int $startUid, int $depth): array
330  {
331  $deletedRecordsTotal = 0;
332  $lang = $this->‪getLanguageService();
333  $tables = [];
334 
335  foreach (‪RecyclerUtility::getModifyableTables() as $tableName) {
336  $deletedField = ‪RecyclerUtility::getDeletedField($tableName);
337  if ($deletedField) {
338  // Determine whether the table has deleted records:
339  $queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
340  $queryBuilder->getRestrictions()->removeAll();
341 
342  $deletedCount = $queryBuilder->count('uid')
343  ->from($tableName)
344  ->where(
345  $queryBuilder->expr()->neq(
346  $deletedField,
347  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
348  )
349  )
350  ->executeQuery()
351  ->fetchOne();
352 
353  if ($deletedCount) {
354  /* @var DeletedRecords $deletedDataObject */
355  $deletedDataObject = GeneralUtility::makeInstance(DeletedRecords::class);
356  $deletedData = $deletedDataObject->loadData($startUid, $tableName, $depth)->getDeletedRows();
357  if (isset($deletedData[$tableName])) {
358  if ($deletedRecordsInTable = count($deletedData[$tableName])) {
359  $deletedRecordsTotal += $deletedRecordsInTable;
360  $tables[] = [
361  $tableName,
362  $deletedRecordsInTable,
363  $lang->sL(‪$GLOBALS['TCA'][$tableName]['ctrl']['title'] ?? $tableName),
364  ];
365  }
366  }
367  }
368  }
369  }
370  $jsonArray = $tables;
371  array_unshift($jsonArray, [
372  '',
373  $deletedRecordsTotal,
374  $lang->sL('LLL:EXT:recycler/Resources/Private/Language/locallang.xlf:label_allrecordtypes'),
375  ]);
376  return $jsonArray;
377  }
378 
380  {
381  return ‪$GLOBALS['BE_USER'];
382  }
383 
385  {
386  return ‪$GLOBALS['LANG'];
387  }
388 }
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\getLanguageService
‪getLanguageService()
Definition: RecyclerAjaxController.php:384
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Core\Utility\GeneralUtility\fixed_lgd_cs
‪static string fixed_lgd_cs(string $string, int $chars, string $appendString='...')
Definition: GeneralUtility.php:92
‪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:203
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\getTables
‪array getTables(int $startUid, int $depth)
Definition: RecyclerAjaxController.php:329
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\$conf
‪array $conf
Definition: RecyclerAjaxController.php:51
‪TYPO3\CMS\Core\Imaging\IconFactory
Definition: IconFactory.php:34
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\setDataInSession
‪setDataInSession(array $data)
Definition: RecyclerAjaxController.php:249
‪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:379
‪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:267
‪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:62
‪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:114
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\isParentPageDeleted
‪isParentPageDeleted(int $pid)
Definition: RecyclerAjaxController.php:306
‪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\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:47
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\__construct
‪__construct(protected readonly BackendViewFactory $backendViewFactory, protected readonly FrontendInterface $runtimeCache, protected readonly IconFactory $iconFactory, protected readonly ConnectionPool $connectionPool, protected readonly RecordHistory $recordHistory)
Definition: RecyclerAjaxController.php:53
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Backend\Attribute\AsController
Definition: AsController.php:25
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:46
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪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:52
‪TYPO3\CMS\Recycler\Controller\RecyclerAjaxController\getBackendUserInformation
‪getBackendUserInformation(int $userId)
Definition: RecyclerAjaxController.php:222