‪TYPO3CMS  10.4
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 
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, \PDO::PARAM_INT)
174  )
175  )
176  ->execute()
177  ->fetchColumn(0);
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, \PDO::PARAM_INT)
239  )
240  )
241  ->orderBy('uid')
242  ->execute()
243  ->fetchAll();
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) . '%',
277  \PDO::PARAM_STR
278  )
279  );
281  $filterConstraint = $queryBuilder->expr()->orX(
282  $queryBuilder->expr()->eq(
283  'uid',
284  $queryBuilder->createNamedParameter($filter, \PDO::PARAM_INT)
285  ),
286  $queryBuilder->expr()->eq(
287  'pid',
288  $queryBuilder->createNamedParameter($filter, \PDO::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)) {
356  $tce = GeneralUtility::makeInstance(DataHandler::class);
357  $tce->start([], []);
358  $tce->disableDeleteClause();
359  foreach ($recordsArray as $record) {
360  [‪$table, $uid] = explode(':', $record);
361  $tce->deleteEl(‪$table, (int)$uid, true, true);
362  }
363  return true;
364  }
365  return false;
366  }
367 
368  /************************************************************
369  * UNDELETE FUNCTIONS
370  ************************************************************/
379  public function ‪undeleteData($recordsArray, $recursive = false)
380  {
381  $result = false;
382  $affectedRecords = 0;
383  $depth = 999;
384  if (is_array($recordsArray)) {
385  $this->deletedRows = [];
386  $cmd = [];
387  foreach ($recordsArray as $record) {
388  [‪$table, $uid] = explode(':', $record);
389  $uid = (int)$uid;
390  // get all parent pages and cover them
392  if ($pid > 0) {
393  $parentUidsToRecover = $this->‪getDeletedParentPages($pid);
394  $count = count($parentUidsToRecover);
395  for ($i = 0; $i < $count; ++$i) {
396  $parentUid = $parentUidsToRecover[$i];
397  $cmd['pages'][$parentUid]['undelete'] = 1;
398  $affectedRecords++;
399  }
400  if (isset($cmd['pages'])) {
401  // reverse the page list to recover it from top to bottom
402  $cmd['pages'] = array_reverse($cmd['pages'], true);
403  }
404  }
405  $cmd[‪$table][$uid]['undelete'] = 1;
406  $affectedRecords++;
407  if (‪$table === 'pages' && $recursive) {
408  $this->‪loadData($uid, '', $depth, '');
409  $childRecords = $this->‪getDeletedRows();
410  if (!empty($childRecords)) {
411  foreach ($childRecords as $childTable => $childRows) {
412  foreach ($childRows as $childRow) {
413  $cmd[$childTable][$childRow['uid']]['undelete'] = 1;
414  }
415  }
416  }
417  }
418  }
419  if ($cmd) {
420  $tce = GeneralUtility::makeInstance(DataHandler::class);
421  $tce->start([], $cmd);
422  $tce->process_cmdmap();
423  $result = $affectedRecords;
424  }
425  }
426  return $result;
427  }
428 
436  protected function ‪getDeletedParentPages($uid, &$pages = [])
437  {
438  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
439  $queryBuilder->getRestrictions()->removeAll()
440  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->‪getBackendUser()->workspace));
441  $record = $queryBuilder
442  ->select('uid', 'pid')
443  ->from('pages')
444  ->where(
445  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
446  $queryBuilder->expr()->eq(‪$GLOBALS['TCA']['pages']['ctrl']['delete'], 1)
447  )
448  ->execute()
449  ->fetch();
450  if ($record) {
451  $pages[] = $record['uid'];
452  if ((int)$record['pid'] !== 0) {
453  $this->‪getDeletedParentPages($record['pid'], $pages);
454  }
455  }
456 
457  return $pages;
458  }
459 
460  /************************************************************
461  * SETTER FUNCTIONS
462  ************************************************************/
469  public function ‪setDeletedRows(‪$table, array $row)
470  {
471  $this->deletedRows[‪$table][] = $row;
472  }
473 
474  /************************************************************
475  * GETTER FUNCTIONS
476  ************************************************************/
482  public function ‪getDeletedRows()
483  {
484  return ‪$this->deletedRows;
485  }
486 
492  public function ‪getTable()
493  {
494  return ‪$this->table;
495  }
496 
505  protected function ‪getTreeList(int $id, int $depth, int $begin = 0): array
506  {
507  $cache = $this->‪getCache();
508  $identifier = md5($id . '_' . $depth . '_' . $begin);
509  $pageTree = $cache->get($identifier);
510  if ($pageTree === false) {
511  $pageTree = $this->‪resolveTree($id, $depth, $begin, $this->‪getBackendUser()->getPagePermsClause(‪Permission::PAGE_SHOW));
512  $cache->set($identifier, $pageTree);
513  }
514 
515  return $pageTree;
516  }
517 
525  protected function ‪resolveTree(int $id, int $depth, int $begin = 0, string $permsClause = ''): array
526  {
527  $depth = (int)$depth;
528  $begin = (int)$begin;
529  $id = abs((int)$id);
530  $theList = [];
531  if ($begin === 0) {
532  $theList[] = $id;
533  }
534  if ($depth > 0) {
535  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
536  $queryBuilder->getRestrictions()->removeAll()
537  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->‪getBackendUser()->workspace));
538  $statement = $queryBuilder->select('uid')
539  ->from('pages')
540  ->where(
541  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)),
543  )
544  ->execute();
545  while ($row = $statement->fetch()) {
546  if ($begin <= 0) {
547  $theList[] = $row['uid'];
548  }
549  if ($depth > 1) {
550  $theList = array_merge($theList, $this->‪resolveTree($row['uid'], $depth - 1, $begin - 1, $permsClause));
551  }
552  }
553  }
554  return $theList;
555  }
556 
562  protected function ‪getCache(): ‪FrontendInterface
563  {
564  return GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
565  }
566 
572  protected function ‪getBackendUser(): ‪BackendUserAuthentication
573  {
574  return ‪$GLOBALS['BE_USER'];
575  }
576 }
‪TYPO3\CMS\Core\DataHandling\DataHandler
Definition: DataHandler.php:84
‪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:430
‪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\Type\Bitmask\Permission
Definition: Permission.php:24
‪TYPO3\CMS\Recycler\Utility\RecyclerUtility\getDeletedField
‪static string getDeletedField($tableName)
Definition: RecyclerUtility.php:128
‪TYPO3\CMS\Core\Database\Query\QueryBuilder
Definition: QueryBuilder.php:52
‪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:476
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\setDeletedRows
‪setDeletedRows($table, array $row)
Definition: DeletedRecords.php:463
‪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:556
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\$limit
‪string $limit
Definition: DeletedRecords.php:49
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:35
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_SHOW
‪const PAGE_SHOW
Definition: Permission.php:33
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\undeleteData
‪bool int undeleteData($recordsArray, $recursive=false)
Definition: DeletedRecords.php:373
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:36
‪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:42
‪TYPO3\CMS\Core\Database\Query\QueryHelper\stripLogicalOperatorPrefix
‪static string stripLogicalOperatorPrefix(string $constraint)
Definition: QueryHelper.php:165
‪TYPO3\CMS\Recycler\Utility\RecyclerUtility
Definition: RecyclerUtility.php:28
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪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:988
‪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:566
‪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:46
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\resolveTree
‪array resolveTree(int $id, int $depth, int $begin=0, string $permsClause='')
Definition: DeletedRecords.php:519
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getTreeList
‪array getTreeList(int $id, int $depth, int $begin=0)
Definition: DeletedRecords.php:499
‪TYPO3\CMS\Recycler\Domain\Model\DeletedRecords\getTable
‪array getTable()
Definition: DeletedRecords.php:486
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:39