TYPO3 CMS  TYPO3_8-7
DeletedRecords.php
Go to the documentation of this file.
1 <?php
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
32 
37 {
43  protected $deletedRows = [];
44 
50  protected $limit = '';
51 
57  protected $table = [];
58 
64  protected $recyclerHelper;
65 
71  public $label;
72 
78  public $title;
79 
80  /************************************************************
81  * GET DATA FUNCTIONS
82  *
83  *
84  ************************************************************/
96  public function loadData($id, $table, $depth, $limit = '', $filter = '')
97  {
98  // set the limit
99  $this->limit = trim($limit);
100  if ($table) {
101  if (in_array($table, RecyclerUtility::getModifyableTables(), true)) {
102  $this->table[] = $table;
103  $this->setData($id, $table, $depth, $filter);
104  }
105  } else {
106  foreach (RecyclerUtility::getModifyableTables() as $tableKey) {
107  // only go into this table if the limit allows it
108  if ($this->limit !== '') {
109  $parts = GeneralUtility::intExplode(',', $this->limit, true);
110  // abort loop if LIMIT 0,0
111  if ($parts[0] === 0 && $parts[1] === 0) {
112  break;
113  }
114  }
115  $this->table[] = $tableKey;
116  $this->setData($id, $tableKey, $depth, $filter);
117  }
118  }
119  return $this;
120  }
121 
131  public function getTotalCount($id, $table, $depth, $filter)
132  {
133  $deletedRecords = $this->loadData($id, $table, $depth, '', $filter)->getDeletedRows();
134  $countTotal = 0;
135  foreach ($this->table as $tableName) {
136  $countTotal += count($deletedRecords[$tableName] ?? []);
137  }
138  return $countTotal;
139  }
140 
149  protected function setData($id, $table, $depth, $filter)
150  {
151  $deletedField = RecyclerUtility::getDeletedField($table);
152  if (!$deletedField) {
153  return;
154  }
155 
156  $id = (int)$id;
157  $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
158  $firstResult = 0;
159  $maxResults = 0;
160 
161  // get the limit
162  if (!empty($this->limit)) {
163  // count the number of deleted records for this pid
164  $queryBuilder = $this->getFilteredQueryBuilder($table, $id, $depth, $filter);
165  $queryBuilder->getRestrictions()->removeAll();
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  list($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()
267  ->removeAll()
268  ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
269 
270  // create the filter WHERE-clause
271  $filterConstraint = null;
272  if (trim($filter) !== '') {
273  $filterConstraint = $queryBuilder->expr()->like(
274  $GLOBALS['TCA'][$table]['ctrl']['label'],
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  list($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  list($table, $uid) = explode(':', $record);
389  // get all parent pages and cover them
390  $pid = RecyclerUtility::getPidOfUid($uid, $table);
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  $record = $queryBuilder
440  ->select('uid', 'pid')
441  ->from('pages')
442  ->where(
443  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
444  $queryBuilder->expr()->eq($GLOBALS['TCA']['pages']['ctrl']['delete'], 1)
445  )
446  ->execute()
447  ->fetch();
448  if ($record) {
449  $pages[] = $record['uid'];
450  if ((int)$record['pid'] !== 0) {
451  $this->getDeletedParentPages($record['pid'], $pages);
452  }
453  }
454 
455  return $pages;
456  }
457 
458  /************************************************************
459  * SETTER FUNCTIONS
460  ************************************************************/
467  public function setDeletedRows($table, array $row)
468  {
469  $this->deletedRows[$table][] = $row;
470  }
471 
472  /************************************************************
473  * GETTER FUNCTIONS
474  ************************************************************/
480  public function getDeletedRows()
481  {
482  return $this->deletedRows;
483  }
484 
490  public function getTable()
491  {
492  return $this->table;
493  }
494 
503  protected function getTreeList(int $id, int $depth, int $begin = 0): array
504  {
505  $cache = $this->getCache();
506  $identifier = md5($id . '_' . $depth . '_' . $begin);
507  $pageTree = $cache->get($identifier);
508  if ($pageTree === false) {
509  $pageTree = $this->resolveTree($id, $depth, $begin, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
510  $cache->set($identifier, $pageTree);
511  }
512 
513  return $pageTree;
514  }
515 
523  protected function resolveTree(int $id, int $depth, int $begin = 0, string $permsClause = ''): array
524  {
525  $depth = (int)$depth;
526  $begin = (int)$begin;
527  $id = abs((int)$id);
528  $theList = [];
529  if ($begin === 0) {
530  $theList[] = $id;
531  }
532  if ($depth > 0) {
533  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
534  $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
535  $statement = $queryBuilder->select('uid')
536  ->from('pages')
537  ->where(
538  $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)),
540  )
541  ->execute();
542  while ($row = $statement->fetch()) {
543  if ($begin <= 0) {
544  $theList[] = $row['uid'];
545  }
546  if ($depth > 1) {
547  $theList = array_merge($theList, $this->resolveTree($row['uid'], $depth - 1, $begin - 1, $permsClause));
548  }
549  }
550  }
551  return $theList;
552  }
553 
559  protected function getCache(): FrontendInterface
560  {
561  return GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
562  }
563 
570  {
571  return $GLOBALS['BE_USER'];
572  }
573 }
static intExplode($delimiter, $string, $removeEmptyValues=false, $limit=0)
undeleteData($recordsArray, $recursive=false)
static makeInstance($className,... $constructorArguments)
static getMaxBindParameters(AbstractPlatform $platform)
getTotalCount($id, $table, $depth, $filter)
getFilteredQueryBuilder(string $table, int $pid, int $depth, string $filter)
loadData($id, $table, $depth, $limit='', $filter='')
static stripLogicalOperatorPrefix(string $constraint)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
resolveTree(int $id, int $depth, int $begin=0, string $permsClause='')
getTreeList(int $id, int $depth, int $begin=0)