‪TYPO3CMS  ‪main
DeletedRecords.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
24 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
32 
38 {
44  protected ‪$deletedRows = [];
45 
51  protected ‪$limit = '';
52 
58  protected ‪$table = [];
59 
65  public ‪$label;
66 
72  public ‪$title;
73 
74  /************************************************************
75  * GET DATA FUNCTIONS
76  *
77  *
78  ************************************************************/
90  public function ‪loadData($id, ‪$table, $depth, ‪$limit = '', $filter = '')
91  {
92  // set the limit
93  $this->limit = trim(‪$limit);
94  if (‪$table) {
95  if (in_array(‪$table, ‪RecyclerUtility::getModifyableTables(), true)) {
96  $this->table[] = ‪$table;
97  $this->‪setData($id, ‪$table, $depth, $filter);
98  }
99  } else {
100  foreach (‪RecyclerUtility::getModifyableTables() as $tableKey) {
101  // only go into this table if the limit allows it
102  if ($this->limit !== '') {
103  $parts = ‪GeneralUtility::intExplode(',', $this->limit, true);
104  // abort loop if LIMIT 0,0
105  if ($parts[0] === 0 && $parts[1] === 0) {
106  break;
107  }
108  }
109  $this->table[] = $tableKey;
110  $this->‪setData($id, $tableKey, $depth, $filter);
111  }
112  }
113  return $this;
114  }
115 
125  public function ‪getTotalCount($id, ‪$table, $depth, $filter)
126  {
127  $deletedRecords = $this->‪loadData($id, ‪$table, $depth, '', $filter)->‪getDeletedRows();
128  $countTotal = 0;
129  foreach ($this->table as $tableName) {
130  $countTotal += count($deletedRecords[$tableName] ?? []);
131  }
132  return $countTotal;
133  }
134 
143  protected function ‪setData($id, ‪$table, $depth, $filter)
144  {
146  if (!$deletedField) {
147  return;
148  }
149 
150  $id = (int)$id;
151  $tcaCtrl = ‪$GLOBALS['TCA'][‪$table]['ctrl'];
152  $firstResult = 0;
153  $maxResults = 0;
154 
155  // get the limit
156  if (!empty($this->limit)) {
157  // count the number of deleted records for this pid
158  $queryBuilder = $this->‪getFilteredQueryBuilder(‪$table, $id, $depth, $filter);
159 
160  $deletedCount = (int)$queryBuilder
161  ->count('*')
162  ->from(‪$table)
163  ->andWhere(
164  $queryBuilder->expr()->neq(
165  $deletedField,
166  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
167  )
168  )
169  ->executeQuery()
170  ->fetchOne();
171 
172  // split the limit
173  [$offset, $rowCount] = ‪GeneralUtility::intExplode(',', $this->limit, true);
174  // subtract the number of deleted records from the limit's offset
175  $result = $offset - $deletedCount;
176  // if the result is >= 0
177  if ($result >= 0) {
178  // store the new offset in the limit and go into the next depth
179  $offset = $result;
180  $this->limit = implode(',', [$offset, $rowCount]);
181  // do NOT query this depth; limit also does not need to be set, we set it anyways
182  $allowQuery = false;
183  } else {
184  // the offset for the temporary limit has to remain like the original offset
185  // in case the original offset was just crossed by the amount of deleted records
186  $tempOffset = 0;
187  if ($offset !== 0) {
188  $tempOffset = $offset;
189  }
190  // set the offset in the limit to 0
191  $newOffset = 0;
192  // convert to negative result to the positive equivalent
193  $absResult = abs($result);
194  // if the result now is > limit's row count
195  if ($absResult > $rowCount) {
196  // use the limit's row count as the temporary limit
197  $firstResult = $tempOffset;
198  $maxResults = $rowCount;
199  // set the limit's row count to 0
200  $this->limit = implode(',', [$newOffset, 0]);
201  } else {
202  // if the result now is <= limit's row count
203  // use the result as the temporary limit
204  $firstResult = $tempOffset;
205  $maxResults = $absResult;
206  // subtract the result from the row count
207  $newCount = $rowCount - $absResult;
208  // store the new result in the limit's row count
209  $this->limit = implode(',', [$newOffset, $newCount]);
210  }
211  // allow query for this depth
212  $allowQuery = true;
213  }
214  } else {
215  $allowQuery = true;
216  }
217  // query for actual deleted records
218  if ($allowQuery) {
219  $queryBuilder = $this->‪getFilteredQueryBuilder(‪$table, $id, $depth, $filter);
220  if ($firstResult) {
221  $queryBuilder->setFirstResult($firstResult);
222  }
223  if ($maxResults) {
224  $queryBuilder->setMaxResults($maxResults);
225  }
226  $queryBuilder = $queryBuilder->select('*')
227  ->from(‪$table)
228  ->andWhere(
229  $queryBuilder->expr()->eq(
230  $deletedField,
231  $queryBuilder->createNamedParameter(1, ‪Connection::PARAM_INT)
232  )
233  );
234  if (‪$GLOBALS['TCA'][‪$table]['ctrl']['tstamp'] ?? false) {
235  $queryBuilder = $queryBuilder
236  ->orderBy(‪$GLOBALS['TCA'][‪$table]['ctrl']['tstamp'], 'desc')
237  ->addOrderBy('uid');
238  } else {
239  $queryBuilder = $queryBuilder->orderBy('uid');
240  }
241  $recordsToCheck = $queryBuilder->executeQuery()->fetchAllAssociative();
242  if ($recordsToCheck !== []) {
243  $this->‪checkRecordAccess($table, $recordsToCheck);
244  }
245  }
246  $this->label[‪$table] = $tcaCtrl['label'] ?? '';
247  $this->title[‪$table] = $tcaCtrl['title'] ?? '';
248  }
249 
253  protected function ‪getFilteredQueryBuilder(string ‪$table, int $pid, int $depth, string $filter): QueryBuilder
254  {
255  $pidList = $this->‪getTreeList($pid, $depth);
256  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(‪$table);
257  $queryBuilder->getRestrictions()->removeAll()
258  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->‪getBackendUser()->workspace));
259 
260  // create the filter WHERE-clause
261  $filterConstraint = null;
262  if (trim($filter) !== '') {
263  $filterConstraint = $queryBuilder->expr()->comparison(
264  $queryBuilder->castFieldToTextType(‪$GLOBALS['TCA'][‪$table]['ctrl']['label']),
265  'LIKE',
266  $queryBuilder->createNamedParameter(
267  '%' . $queryBuilder->escapeLikeWildcards($filter) . '%'
268  )
269  );
271  $filterConstraint = $queryBuilder->expr()->or(
272  $queryBuilder->expr()->eq(
273  'uid',
274  $queryBuilder->createNamedParameter($filter, ‪Connection::PARAM_INT)
275  ),
276  $queryBuilder->expr()->eq(
277  'pid',
278  $queryBuilder->createNamedParameter($filter, ‪Connection::PARAM_INT)
279  ),
280  $filterConstraint
281  );
282  }
283  }
284 
285  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($queryBuilder->getConnection()->getDatabasePlatform());
286  $pidConstraints = [];
287  foreach (array_chunk($pidList, $maxBindParameters - 10) as $chunk) {
288  $pidConstraints[] = $queryBuilder->expr()->in(
289  'pid',
290  $queryBuilder->createNamedParameter($chunk, ‪Connection::PARAM_INT_ARRAY)
291  );
292  }
293  $queryBuilder->where(
294  $queryBuilder->expr()->and(
295  $filterConstraint,
296  $queryBuilder->expr()->or(...$pidConstraints)
297  )
298  );
299 
300  return $queryBuilder;
301  }
302 
309  protected function ‪checkRecordAccess(‪$table, array $rows)
310  {
311  $deleteField = '';
312  if (‪$table === 'pages') {
313  // The "checkAccess" method validates access to the passed table/rows. When access to
314  // a page record gets validated it is necessary to disable the "delete" field temporarily
315  // for the recycler.
316  // Else it wouldn't be possible to perform the check as many methods of BackendUtility
317  // like "BEgetRootLine", etc. will only work on non-deleted records.
318  $deleteField = ‪$GLOBALS['TCA'][‪$table]['ctrl']['delete'];
319  unset(‪$GLOBALS['TCA'][‪$table]['ctrl']['delete']);
320  }
321 
322  foreach ($rows as $row) {
324  $this->‪setDeletedRows($table, $row);
325  }
326  }
327 
328  if (‪$table === 'pages') {
329  ‪$GLOBALS['TCA'][‪$table]['ctrl']['delete'] = $deleteField;
330  }
331  }
332 
333  /************************************************************
334  * DELETE FUNCTIONS
335  ************************************************************/
342  public function ‪deleteData($recordsArray)
343  {
344  if (is_array($recordsArray)) {
345  $tce = GeneralUtility::makeInstance(DataHandler::class);
346  $tce->start([], []);
347  $tce->disableDeleteClause();
348  foreach ($recordsArray as ‪$record) {
349  [‪$table, ‪$uid] = explode(':', ‪$record);
350  $tce->deleteEl(‪$table, (int)‪$uid, true, true);
351  }
352  return true;
353  }
354  return false;
355  }
356 
357  /************************************************************
358  * UNDELETE FUNCTIONS
359  ************************************************************/
368  public function ‪undeleteData($recordsArray, $recursive = false)
369  {
370  $result = false;
371  $affectedRecords = 0;
372  $depth = 999;
373  if (is_array($recordsArray)) {
374  $this->deletedRows = [];
375  $cmd = [];
376  foreach ($recordsArray as ‪$record) {
377  [‪$table, ‪$uid] = explode(':', ‪$record);
378  ‪$uid = (int)‪$uid;
379  // get all parent pages and cover them
381  if ($pid > 0) {
382  $parentUidsToRecover = $this->‪getDeletedParentPages($pid);
383  $count = count($parentUidsToRecover);
384  for ($i = 0; $i < $count; ++$i) {
385  $parentUid = $parentUidsToRecover[$i];
386  $cmd['pages'][$parentUid]['undelete'] = 1;
387  $affectedRecords++;
388  }
389  if (isset($cmd['pages'])) {
390  // reverse the page list to recover it from top to bottom
391  $cmd['pages'] = array_reverse($cmd['pages'], true);
392  }
393  }
394  $cmd[‪$table][‪$uid]['undelete'] = 1;
395  $affectedRecords++;
396  if (‪$table === 'pages' && $recursive) {
397  $this->‪loadData(‪$uid, '', $depth, '');
398  $childRecords = $this->‪getDeletedRows();
399  if (!empty($childRecords)) {
400  foreach ($childRecords as $childTable => $childRows) {
401  foreach ($childRows as $childRow) {
402  $cmd[$childTable][$childRow['uid']]['undelete'] = 1;
403  }
404  }
405  }
406  }
407  }
408  if ($cmd) {
409  $tce = GeneralUtility::makeInstance(DataHandler::class);
410  $tce->start([], $cmd);
411  $tce->process_cmdmap();
412  $result = $affectedRecords;
413  }
414  }
415  return $result;
416  }
417 
425  protected function ‪getDeletedParentPages(‪$uid, &$pages = [])
426  {
427  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
428  $queryBuilder->getRestrictions()->removeAll()
429  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->‪getBackendUser()->workspace));
430  ‪$record = $queryBuilder
431  ->select('uid', 'pid')
432  ->from('pages')
433  ->where(
434  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)),
435  $queryBuilder->expr()->eq(‪$GLOBALS['TCA']['pages']['ctrl']['delete'], 1)
436  )
437  ->executeQuery()
438  ->fetchAssociative();
439  if (‪$record) {
440  $pages[] = ‪$record['uid'];
441  if ((int)‪$record['pid'] !== 0) {
442  $this->‪getDeletedParentPages($record['pid'], $pages);
443  }
444  }
445 
446  return $pages;
447  }
448 
449  /************************************************************
450  * SETTER FUNCTIONS
451  ************************************************************/
458  public function ‪setDeletedRows(‪$table, array $row)
459  {
460  $this->deletedRows[‪$table][] = $row;
461  }
462 
463  /************************************************************
464  * GETTER FUNCTIONS
465  ************************************************************/
471  public function ‪getDeletedRows()
472  {
473  return ‪$this->deletedRows;
474  }
475 
481  public function ‪getTable()
482  {
483  return ‪$this->table;
484  }
485 
489  protected function ‪getTreeList(int $id, int $depth, int $begin = 0): array
490  {
491  $cache = $this->‪getCache();
492  ‪$identifier = md5($id . '_' . $depth . '_' . $begin);
493  $pageTree = $cache->get(‪$identifier);
494  if ($pageTree === false) {
495  $pageTree = $this->‪resolveTree($id, $depth, $begin, $this->‪getBackendUser()->getPagePermsClause(‪Permission::PAGE_SHOW));
496  $cache->set(‪$identifier, $pageTree);
497  }
498 
499  return $pageTree;
500  }
501 
502  protected function ‪resolveTree(int $id, int $depth, int $begin = 0, string $permsClause = ''): array
503  {
504  $depth = (int)$depth;
505  $begin = (int)$begin;
506  $id = abs((int)$id);
507  $theList = [];
508  if ($begin === 0) {
509  $theList[] = $id;
510  }
511  if ($depth > 0) {
512  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
513  $queryBuilder->getRestrictions()->removeAll()
514  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->‪getBackendUser()->workspace));
515  $statement = $queryBuilder->select('uid')
516  ->from('pages')
517  ->where(
518  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($id, ‪Connection::PARAM_INT)),
520  )
521  ->executeQuery();
522  while ($row = $statement->fetchAssociative()) {
523  if ($begin <= 0) {
524  $theList[] = $row['uid'];
525  }
526  if ($depth > 1) {
527  $theList = array_merge($theList, $this->‪resolveTree($row['uid'], $depth - 1, $begin - 1, $permsClause));
528  }
529  }
530  }
531  return $theList;
532  }
533 
537  protected function ‪getCache(): ‪FrontendInterface
538  {
539  return GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
540  }
541 
542  protected function ‪getBackendUser(): ‪BackendUserAuthentication
543  {
544  return ‪$GLOBALS['BE_USER'];
545  }
546 }
‪TYPO3\CMS\Core\DataHandling\DataHandler
Definition: DataHandler.php:94
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\loadData
‪DeletedRecords loadData($id, $table, $depth, $limit='', $filter='')
Definition: DeletedRecords.php:85
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getDeletedParentPages
‪array getDeletedParentPages($uid, &$pages=[])
Definition: DeletedRecords.php:420
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getTreeList
‪getTreeList(int $id, int $depth, int $begin=0)
Definition: DeletedRecords.php:484
‪TYPO3\CMS\Core\Database\Platform\PlatformInformation\getMaxBindParameters
‪static getMaxBindParameters(DoctrineAbstractPlatform $platform)
Definition: PlatformInformation.php:106
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\$label
‪array $label
Definition: DeletedRecords.php:61
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getTotalCount
‪int getTotalCount($id, $table, $depth, $filter)
Definition: DeletedRecords.php:120
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getCache
‪getCache()
Definition: DeletedRecords.php:532
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getBackendUser
‪getBackendUser()
Definition: DeletedRecords.php:537
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\$table
‪array $table
Definition: DeletedRecords.php:55
‪TYPO3\CMS\Core\Type\Bitmask\Permission
Definition: Permission.php:26
‪TYPO3\CMS\Recycler\Utility\RecyclerUtility\getDeletedField
‪static string getDeletedField($tableName)
Definition: RecyclerUtility.php:85
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\checkRecordAccess
‪checkRecordAccess($table, array $rows)
Definition: DeletedRecords.php:304
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getDeletedRows
‪array getDeletedRows()
Definition: DeletedRecords.php:466
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\setDeletedRows
‪setDeletedRows($table, array $row)
Definition: DeletedRecords.php:453
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\$deletedRows
‪array $deletedRows
Definition: DeletedRecords.php:43
‪TYPO3\CMS\Recycler\Utility\RecyclerUtility\getModifyableTables
‪static getModifyableTables()
Definition: RecyclerUtility.php:144
‪TYPO3\CMS\Recycler\Utility\RecyclerUtility\getPidOfUid
‪static int getPidOfUid($uid, $table)
Definition: RecyclerUtility.php:101
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:32
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\setData
‪setData($id, $table, $depth, $filter)
Definition: DeletedRecords.php:138
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\$limit
‪string $limit
Definition: DeletedRecords.php:49
‪TYPO3\CMS\Webhooks\Message\$record
‪identifier readonly int readonly array $record
Definition: PageModificationMessage.php:36
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:36
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:61
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_SHOW
‪const PAGE_SHOW
Definition: Permission.php:35
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\undeleteData
‪bool int undeleteData($recordsArray, $recursive=false)
Definition: DeletedRecords.php:363
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\deleteData
‪bool deleteData($recordsArray)
Definition: DeletedRecords.php:337
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords
Definition: DeletedRecords.php:38
‪TYPO3\CMS\Recycler\Utility\RecyclerUtility\checkAccess
‪static bool checkAccess($table, $row)
Definition: RecyclerUtility.php:45
‪TYPO3\CMS\Webhooks\Message\$uid
‪identifier readonly int $uid
Definition: PageModificationMessage.php:35
‪TYPO3\CMS\Core\Database\Query\QueryHelper\stripLogicalOperatorPrefix
‪static string stripLogicalOperatorPrefix(string $constraint)
Definition: QueryHelper.php:171
‪TYPO3\CMS\Recycler\Utility\RecyclerUtility
Definition: RecyclerUtility.php:31
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Database\Platform\PlatformInformation
Definition: PlatformInformation.php:33
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\$title
‪array $title
Definition: DeletedRecords.php:67
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Recycler\Domain\Model
Definition: DeletedRecords.php:16
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getFilteredQueryBuilder
‪getFilteredQueryBuilder(string $table, int $pid, int $depth, string $filter)
Definition: DeletedRecords.php:248
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\resolveTree
‪resolveTree(int $id, int $depth, int $begin=0, string $permsClause='')
Definition: DeletedRecords.php:497
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Utility\GeneralUtility\intExplode
‪static list< int > intExplode(string $delimiter, string $string, bool $removeEmptyValues=false)
Definition: GeneralUtility.php:751
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT_ARRAY
‪const PARAM_INT_ARRAY
Definition: Connection.php:72
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getTable
‪array getTable()
Definition: DeletedRecords.php:476
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:39