‪TYPO3CMS  11.5
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  protected ‪$recyclerHelper;
66 
72  public ‪$label;
73 
79  public ‪$title;
80 
81  /************************************************************
82  * GET DATA FUNCTIONS
83  *
84  *
85  ************************************************************/
97  public function ‪loadData($id, ‪$table, $depth, ‪$limit = '', $filter = '')
98  {
99  // set the limit
100  $this->limit = trim(‪$limit);
101  if (‪$table) {
102  if (in_array(‪$table, ‪RecyclerUtility::getModifyableTables(), true)) {
103  $this->table[] = ‪$table;
104  $this->‪setData($id, ‪$table, $depth, $filter);
105  }
106  } else {
107  foreach (‪RecyclerUtility::getModifyableTables() as $tableKey) {
108  // only go into this table if the limit allows it
109  if ($this->limit !== '') {
110  $parts = ‪GeneralUtility::intExplode(',', $this->limit, true);
111  // abort loop if LIMIT 0,0
112  if ($parts[0] === 0 && $parts[1] === 0) {
113  break;
114  }
115  }
116  $this->table[] = $tableKey;
117  $this->‪setData($id, $tableKey, $depth, $filter);
118  }
119  }
120  return $this;
121  }
122 
132  public function ‪getTotalCount($id, ‪$table, $depth, $filter)
133  {
134  $deletedRecords = $this->‪loadData($id, ‪$table, $depth, '', $filter)->‪getDeletedRows();
135  $countTotal = 0;
136  foreach ($this->table as $tableName) {
137  $countTotal += count($deletedRecords[$tableName] ?? []);
138  }
139  return $countTotal;
140  }
141 
150  protected function ‪setData($id, ‪$table, $depth, $filter)
151  {
153  if (!$deletedField) {
154  return;
155  }
156 
157  $id = (int)$id;
158  $tcaCtrl = ‪$GLOBALS['TCA'][‪$table]['ctrl'];
159  $firstResult = 0;
160  $maxResults = 0;
161 
162  // get the limit
163  if (!empty($this->limit)) {
164  // count the number of deleted records for this pid
165  $queryBuilder = $this->‪getFilteredQueryBuilder(‪$table, $id, $depth, $filter);
166 
167  $deletedCount = (int)$queryBuilder
168  ->count('*')
169  ->from(‪$table)
170  ->andWhere(
171  $queryBuilder->expr()->neq(
172  $deletedField,
173  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
174  )
175  )
176  ->executeQuery()
177  ->fetchOne();
178 
179  // split the limit
180  [$offset, $rowCount] = ‪GeneralUtility::intExplode(',', $this->limit, true);
181  // subtract the number of deleted records from the limit's offset
182  $result = $offset - $deletedCount;
183  // if the result is >= 0
184  if ($result >= 0) {
185  // store the new offset in the limit and go into the next depth
186  $offset = $result;
187  $this->limit = implode(',', [$offset, $rowCount]);
188  // do NOT query this depth; limit also does not need to be set, we set it anyways
189  $allowQuery = false;
190  } else {
191  // the offset for the temporary limit has to remain like the original offset
192  // in case the original offset was just crossed by the amount of deleted records
193  $tempOffset = 0;
194  if ($offset !== 0) {
195  $tempOffset = $offset;
196  }
197  // set the offset in the limit to 0
198  $newOffset = 0;
199  // convert to negative result to the positive equivalent
200  $absResult = abs($result);
201  // if the result now is > limit's row count
202  if ($absResult > $rowCount) {
203  // use the limit's row count as the temporary limit
204  $firstResult = $tempOffset;
205  $maxResults = $rowCount;
206  // set the limit's row count to 0
207  $this->limit = implode(',', [$newOffset, 0]);
208  } else {
209  // if the result now is <= limit's row count
210  // use the result as the temporary limit
211  $firstResult = $tempOffset;
212  $maxResults = $absResult;
213  // subtract the result from the row count
214  $newCount = $rowCount - $absResult;
215  // store the new result in the limit's row count
216  $this->limit = implode(',', [$newOffset, $newCount]);
217  }
218  // allow query for this depth
219  $allowQuery = true;
220  }
221  } else {
222  $allowQuery = true;
223  }
224  // query for actual deleted records
225  if ($allowQuery) {
226  $queryBuilder = $this->‪getFilteredQueryBuilder(‪$table, $id, $depth, $filter);
227  if ($firstResult) {
228  $queryBuilder->setFirstResult($firstResult);
229  }
230  if ($maxResults) {
231  $queryBuilder->setMaxResults($maxResults);
232  }
233  $recordsToCheck = $queryBuilder->select('*')
234  ->from(‪$table)
235  ->andWhere(
236  $queryBuilder->expr()->eq(
237  $deletedField,
238  $queryBuilder->createNamedParameter(1, ‪Connection::PARAM_INT)
239  )
240  )
241  ->orderBy('uid')
242  ->executeQuery()
243  ->fetchAllAssociative();
244 
245  if ($recordsToCheck !== false) {
246  $this->‪checkRecordAccess(‪$table, $recordsToCheck);
247  }
248  }
249  $this->label[‪$table] = $tcaCtrl['label'] ?? '';
250  $this->title[‪$table] = $tcaCtrl['title'] ?? '';
251  }
252 
262  protected function ‪getFilteredQueryBuilder(string ‪$table, int $pid, int $depth, string $filter): QueryBuilder
263  {
264  $pidList = $this->‪getTreeList($pid, $depth);
265  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable(‪$table);
266  $queryBuilder->getRestrictions()->removeAll()
267  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->‪getBackendUser()->workspace));
268 
269  // create the filter WHERE-clause
270  $filterConstraint = null;
271  if (trim($filter) !== '') {
272  $filterConstraint = $queryBuilder->expr()->comparison(
273  $queryBuilder->castFieldToTextType(‪$GLOBALS['TCA'][‪$table]['ctrl']['label']),
274  'LIKE',
275  $queryBuilder->createNamedParameter(
276  '%' . $queryBuilder->escapeLikeWildcards($filter) . '%',
278  )
279  );
281  $filterConstraint = $queryBuilder->expr()->orX(
282  $queryBuilder->expr()->eq(
283  'uid',
284  $queryBuilder->createNamedParameter($filter, ‪Connection::PARAM_INT)
285  ),
286  $queryBuilder->expr()->eq(
287  'pid',
288  $queryBuilder->createNamedParameter($filter, ‪Connection::PARAM_INT)
289  ),
290  $filterConstraint
291  );
292  }
293  }
294 
295  $maxBindParameters = ‪PlatformInformation::getMaxBindParameters($queryBuilder->getConnection()->getDatabasePlatform());
296  $pidConstraints = [];
297  foreach (array_chunk($pidList, $maxBindParameters - 10) as $chunk) {
298  $pidConstraints[] = $queryBuilder->expr()->in(
299  'pid',
300  $queryBuilder->createNamedParameter($chunk, Connection::PARAM_INT_ARRAY)
301  );
302  }
303  $queryBuilder->where(
304  $queryBuilder->expr()->andX(
305  $filterConstraint,
306  $queryBuilder->expr()->orX(...$pidConstraints)
307  )
308  );
309 
310  return $queryBuilder;
311  }
312 
319  protected function ‪checkRecordAccess(‪$table, array $rows)
320  {
321  $deleteField = '';
322  if (‪$table === 'pages') {
323  // The "checkAccess" method validates access to the passed table/rows. When access to
324  // a page record gets validated it is necessary to disable the "delete" field temporarily
325  // for the recycler.
326  // Else it wouldn't be possible to perform the check as many methods of BackendUtility
327  // like "BEgetRootLine", etc. will only work on non-deleted records.
328  $deleteField = ‪$GLOBALS['TCA'][‪$table]['ctrl']['delete'];
329  unset(‪$GLOBALS['TCA'][‪$table]['ctrl']['delete']);
330  }
331 
332  foreach ($rows as $row) {
334  $this->‪setDeletedRows($table, $row);
335  }
336  }
337 
338  if (‪$table === 'pages') {
339  ‪$GLOBALS['TCA'][‪$table]['ctrl']['delete'] = $deleteField;
340  }
341  }
342 
343  /************************************************************
344  * DELETE FUNCTIONS
345  ************************************************************/
352  public function ‪deleteData($recordsArray)
353  {
354  if (is_array($recordsArray)) {
355  $tce = GeneralUtility::makeInstance(DataHandler::class);
356  $tce->start([], []);
357  $tce->disableDeleteClause();
358  foreach ($recordsArray as $record) {
359  [‪$table, $uid] = explode(':', $record);
360  $tce->deleteEl(‪$table, (int)$uid, true, true);
361  }
362  return true;
363  }
364  return false;
365  }
366 
367  /************************************************************
368  * UNDELETE FUNCTIONS
369  ************************************************************/
378  public function ‪undeleteData($recordsArray, $recursive = false)
379  {
380  $result = false;
381  $affectedRecords = 0;
382  $depth = 999;
383  if (is_array($recordsArray)) {
384  $this->deletedRows = [];
385  $cmd = [];
386  foreach ($recordsArray as $record) {
387  [‪$table, $uid] = explode(':', $record);
388  $uid = (int)$uid;
389  // get all parent pages and cover them
391  if ($pid > 0) {
392  $parentUidsToRecover = $this->‪getDeletedParentPages($pid);
393  $count = count($parentUidsToRecover);
394  for ($i = 0; $i < $count; ++$i) {
395  $parentUid = $parentUidsToRecover[$i];
396  $cmd['pages'][$parentUid]['undelete'] = 1;
397  $affectedRecords++;
398  }
399  if (isset($cmd['pages'])) {
400  // reverse the page list to recover it from top to bottom
401  $cmd['pages'] = array_reverse($cmd['pages'], true);
402  }
403  }
404  $cmd[‪$table][$uid]['undelete'] = 1;
405  $affectedRecords++;
406  if (‪$table === 'pages' && $recursive) {
407  $this->‪loadData($uid, '', $depth, '');
408  $childRecords = $this->‪getDeletedRows();
409  if (!empty($childRecords)) {
410  foreach ($childRecords as $childTable => $childRows) {
411  foreach ($childRows as $childRow) {
412  $cmd[$childTable][$childRow['uid']]['undelete'] = 1;
413  }
414  }
415  }
416  }
417  }
418  if ($cmd) {
419  $tce = GeneralUtility::makeInstance(DataHandler::class);
420  $tce->start([], $cmd);
421  $tce->process_cmdmap();
422  $result = $affectedRecords;
423  }
424  }
425  return $result;
426  }
427 
435  protected function ‪getDeletedParentPages($uid, &$pages = [])
436  {
437  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
438  $queryBuilder->getRestrictions()->removeAll()
439  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->‪getBackendUser()->workspace));
440  $record = $queryBuilder
441  ->select('uid', 'pid')
442  ->from('pages')
443  ->where(
444  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, ‪Connection::PARAM_INT)),
445  $queryBuilder->expr()->eq(‪$GLOBALS['TCA']['pages']['ctrl']['delete'], 1)
446  )
447  ->executeQuery()
448  ->fetchAssociative();
449  if ($record) {
450  $pages[] = $record['uid'];
451  if ((int)$record['pid'] !== 0) {
452  $this->‪getDeletedParentPages($record['pid'], $pages);
453  }
454  }
455 
456  return $pages;
457  }
458 
459  /************************************************************
460  * SETTER FUNCTIONS
461  ************************************************************/
468  public function ‪setDeletedRows(‪$table, array $row)
469  {
470  $this->deletedRows[‪$table][] = $row;
471  }
472 
473  /************************************************************
474  * GETTER FUNCTIONS
475  ************************************************************/
481  public function ‪getDeletedRows()
482  {
483  return ‪$this->deletedRows;
484  }
485 
491  public function ‪getTable()
492  {
493  return ‪$this->table;
494  }
495 
504  protected function ‪getTreeList(int $id, int $depth, int $begin = 0): array
505  {
506  $cache = $this->‪getCache();
507  $identifier = md5($id . '_' . $depth . '_' . $begin);
508  $pageTree = $cache->get($identifier);
509  if ($pageTree === false) {
510  $pageTree = $this->‪resolveTree($id, $depth, $begin, $this->‪getBackendUser()->getPagePermsClause(‪Permission::PAGE_SHOW));
511  $cache->set($identifier, $pageTree);
512  }
513 
514  return $pageTree;
515  }
516 
524  protected function ‪resolveTree(int $id, int $depth, int $begin = 0, string $permsClause = ''): array
525  {
526  $depth = (int)$depth;
527  $begin = (int)$begin;
528  $id = abs((int)$id);
529  $theList = [];
530  if ($begin === 0) {
531  $theList[] = $id;
532  }
533  if ($depth > 0) {
534  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
535  $queryBuilder->getRestrictions()->removeAll()
536  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->‪getBackendUser()->workspace));
537  $statement = $queryBuilder->select('uid')
538  ->from('pages')
539  ->where(
540  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($id, ‪Connection::PARAM_INT)),
542  )
543  ->executeQuery();
544  while ($row = $statement->fetchAssociative()) {
545  if ($begin <= 0) {
546  $theList[] = $row['uid'];
547  }
548  if ($depth > 1) {
549  $theList = array_merge($theList, $this->‪resolveTree($row['uid'], $depth - 1, $begin - 1, $permsClause));
550  }
551  }
552  }
553  return $theList;
554  }
555 
561  protected function ‪getCache(): ‪FrontendInterface
562  {
563  return GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
564  }
565 
571  protected function ‪getBackendUser(): ‪BackendUserAuthentication
572  {
573  return ‪$GLOBALS['BE_USER'];
574  }
575 }
‪TYPO3\CMS\Core\DataHandling\DataHandler
Definition: DataHandler.php:86
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\loadData
‪DeletedRecords loadData($id, $table, $depth, $limit='', $filter='')
Definition: DeletedRecords.php:91
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getDeletedParentPages
‪array getDeletedParentPages($uid, &$pages=[])
Definition: DeletedRecords.php:429
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:49
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger($var)
Definition: MathUtility.php:74
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\$label
‪array $label
Definition: DeletedRecords.php:67
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getTotalCount
‪int getTotalCount($id, $table, $depth, $filter)
Definition: DeletedRecords.php:126
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\$table
‪array $table
Definition: DeletedRecords.php:55
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR
‪const PARAM_STR
Definition: Connection.php:54
‪TYPO3\CMS\Core\Type\Bitmask\Permission
Definition: Permission.php:26
‪TYPO3\CMS\Recycler\Utility\RecyclerUtility\getDeletedField
‪static string getDeletedField($tableName)
Definition: RecyclerUtility.php:128
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\checkRecordAccess
‪checkRecordAccess($table, array $rows)
Definition: DeletedRecords.php:313
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getDeletedRows
‪array getDeletedRows()
Definition: DeletedRecords.php:475
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\setDeletedRows
‪setDeletedRows($table, array $row)
Definition: DeletedRecords.php:462
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\$recyclerHelper
‪RecyclerUtility $recyclerHelper
Definition: DeletedRecords.php:61
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\$deletedRows
‪array $deletedRows
Definition: DeletedRecords.php:43
‪TYPO3\CMS\Recycler\Utility\RecyclerUtility\getModifyableTables
‪static getModifyableTables()
Definition: RecyclerUtility.php:221
‪TYPO3\CMS\Recycler\Utility\RecyclerUtility\getPidOfUid
‪static int getPidOfUid($uid, $table)
Definition: RecyclerUtility.php:168
‪TYPO3\CMS\Core\Database\Platform\PlatformInformation\getMaxBindParameters
‪static int getMaxBindParameters(AbstractPlatform $platform)
Definition: PlatformInformation.php:125
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:32
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getFilteredQueryBuilder
‪TYPO3 CMS Core Database Query QueryBuilder getFilteredQueryBuilder(string $table, int $pid, int $depth, string $filter)
Definition: DeletedRecords.php:256
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\setData
‪setData($id, $table, $depth, $filter)
Definition: DeletedRecords.php:144
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getCache
‪FrontendInterface getCache()
Definition: DeletedRecords.php:555
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\$limit
‪string $limit
Definition: DeletedRecords.php:49
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:36
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪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:372
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\deleteData
‪bool deleteData($recordsArray)
Definition: DeletedRecords.php:346
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:38
‪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:43
‪TYPO3\CMS\Core\Database\Query\QueryHelper\stripLogicalOperatorPrefix
‪static string stripLogicalOperatorPrefix(string $constraint)
Definition: QueryHelper.php:171
‪TYPO3\CMS\Recycler\Utility\RecyclerUtility
Definition: RecyclerUtility.php:29
‪$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\Core\Utility\GeneralUtility\intExplode
‪static int[] intExplode($delimiter, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:927
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\$title
‪array $title
Definition: DeletedRecords.php:73
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getBackendUser
‪BackendUserAuthentication getBackendUser()
Definition: DeletedRecords.php:565
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:22
‪TYPO3\CMS\Recycler\Domain\Model
Definition: DeletedRecords.php:16
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\resolveTree
‪array resolveTree(int $id, int $depth, int $begin=0, string $permsClause='')
Definition: DeletedRecords.php:518
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getTreeList
‪array getTreeList(int $id, int $depth, int $begin=0)
Definition: DeletedRecords.php:498
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getTable
‪array getTable()
Definition: DeletedRecords.php:485
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:40