[FEATURE] Recover deleted pages recursively to top of rootline 44/36744/11
authorAndreas Fernandez <a.fernandez@scripting-base.de>
Sun, 8 Feb 2015 13:59:11 +0000 (14:59 +0100)
committerWouter Wolters <typo3@wouterwolters.nl>
Thu, 14 Jan 2016 21:43:16 +0000 (22:43 +0100)
If enabled for an user or being an admin, deleted pages above the
selected item are also recovered. If an user does not have this ability,
the recover button is locked.

Resolves: #1835
Releases: master
Change-Id: I98d9876ee95f36cda64fc0730169197b153a2e7f
Reviewed-on: https://review.typo3.org/36744
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters <typo3@wouterwolters.nl>
typo3/sysext/core/Documentation/Changelog/master/Feature-1835-RecoverPagesRecursivelyToTop.rst [new file with mode: 0644]
typo3/sysext/recycler/Classes/Controller/DeletedRecordsController.php
typo3/sysext/recycler/Classes/Controller/RecyclerAjaxController.php
typo3/sysext/recycler/Classes/Controller/RecyclerModuleController.php
typo3/sysext/recycler/Classes/Domain/Model/DeletedRecords.php
typo3/sysext/recycler/Classes/Utility/RecyclerUtility.php
typo3/sysext/recycler/Documentation/Configuration/Index.rst
typo3/sysext/recycler/Resources/Private/Language/locallang.xlf
typo3/sysext/recycler/Resources/Private/Partials/RecordsTable/DeletedRecord.html
typo3/sysext/recycler/Resources/Public/JavaScript/Recycler.js

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-1835-RecoverPagesRecursivelyToTop.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-1835-RecoverPagesRecursivelyToTop.rst
new file mode 100644 (file)
index 0000000..5c85b60
--- /dev/null
@@ -0,0 +1,9 @@
+=============================================================
+Feature: #1835 - Recover pages recursively to top of rootline
+=============================================================
+
+Description
+===========
+
+The Recycler now supports the recursive recovery of deleted pages to the top of the rootline. This feature is available
+for admin users only due to internal permission restrictions.
\ No newline at end of file
index a19a041..e179d8e 100644 (file)
@@ -77,7 +77,8 @@ class DeletedRecordsController
                         'owner_uid' => $row[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']],
                         'tableTitle' => $lang->sL($GLOBALS['TCA'][$table]['ctrl']['title']),
                         'title' => htmlspecialchars(BackendUtility::getRecordTitle($table, $row)),
-                        'path' => RecyclerUtility::getRecordPath($row['pid'])
+                        'path' => RecyclerUtility::getRecordPath($row['pid']),
+                        'isParentDeleted' => $table === 'pages' ? RecyclerUtility::isParentPageDeleted($row['pid']) : false
                     );
                 }
             }
index e8478cc..158ed84 100644 (file)
@@ -43,13 +43,17 @@ class RecyclerAjaxController
         // Configuration, variable assignment
         $this->conf['action'] = GeneralUtility::_GP('action');
         $this->conf['table'] = GeneralUtility::_GP('table') ? GeneralUtility::_GP('table') : '';
-        $this->conf['limit'] = GeneralUtility::_GP('limit') ? (int)GeneralUtility::_GP('limit') : 25;
+        if (isset($modTS['properties']['recordsPageLimit']) && (int)$modTS['properties']['recordsPageLimit'] > 0) {
+            $this->conf['limit'] = (int)$modTS['properties']['recordsPageLimit'];
+        } else {
+            $this->conf['limit'] = 25;
+        }
         $this->conf['start'] = GeneralUtility::_GP('start') ? (int)GeneralUtility::_GP('start') : 0;
         $this->conf['filterTxt'] = GeneralUtility::_GP('filterTxt') ? GeneralUtility::_GP('filterTxt') : '';
         $this->conf['startUid'] = GeneralUtility::_GP('startUid') ? (int)GeneralUtility::_GP('startUid') : 0;
         $this->conf['depth'] = GeneralUtility::_GP('depth') ? (int)GeneralUtility::_GP('depth') : 0;
         $this->conf['records'] = GeneralUtility::_GP('records') ? GeneralUtility::_GP('records') : null;
-        $this->conf['recursive'] = GeneralUtility::_GP('recursive') ? (bool)(int)GeneralUtility::_GP('recursive') : false;
+        $this->conf['recursive'] = GeneralUtility::_GP('recursive') ? (bool)GeneralUtility::_GP('recursive') : false;
     }
 
     /**
@@ -94,7 +98,7 @@ class RecyclerAjaxController
                 $recordsArray = $controller->transform($deletedRowsArray, $totalDeleted);
 
                 $modTS = $this->getBackendUser()->getTSConfig('mod.recycler');
-                $allowDelete = (bool)$this->getBackendUser()->user['admin'] ? true : (bool)$modTS['properties']['allowDelete'];
+                $allowDelete = $this->getBackendUser()->isAdmin() ? true : (bool)$modTS['properties']['allowDelete'];
 
                 $view->setTemplatePathAndFilename($extPath . 'Resources/Private/Templates/Ajax/RecordsTable.html');
                 $view->assign('records', $recordsArray['rows']);
@@ -116,9 +120,8 @@ class RecyclerAjaxController
 
                 /* @var $model DeletedRecords */
                 $model = GeneralUtility::makeInstance(DeletedRecords::class);
-                $success = $model->undeleteData($this->conf['records'], $this->conf['recursive']);
-                $affectedRecords = count($this->conf['records']);
-                $messageKey = 'flashmessage.undo.' . ($success ? 'success' : 'failure') . '.' . ($affectedRecords === 1 ? 'singular' : 'plural');
+                $affectedRecords = $model->undeleteData($this->conf['records'], $this->conf['recursive']);
+                $messageKey = 'flashmessage.undo.' . ($affectedRecords !== false ? 'success' : 'failure') . '.' . ((int)$affectedRecords === 1 ? 'singular' : 'plural');
                 $content = array(
                     'success' => true,
                     'message' => sprintf(LocalizationUtility::translate($messageKey, 'recycler'), $affectedRecords)
index 1bb2fd0..6bb26e7 100644 (file)
@@ -84,7 +84,7 @@ class RecyclerModuleController extends ActionController
         $backendUser = $this->getBackendUser();
         $this->perms_clause = $backendUser->getPagePermsClause(1);
         $this->pageRecord = \TYPO3\CMS\Backend\Utility\BackendUtility::readPageAccess($this->id, $this->perms_clause);
-        $this->isAccessibleForCurrentUser = $this->id && is_array($this->pageRecord) || !$this->id && $this->isCurrentUserAdmin();
+        $this->isAccessibleForCurrentUser = $this->id && is_array($this->pageRecord) || !$this->id && $this->getBackendUser()->isAdmin();
 
         // don't access in workspace
         if ($backendUser->workspace !== 0) {
@@ -93,7 +93,7 @@ class RecyclerModuleController extends ActionController
 
         // read configuration
         $modTS = $backendUser->getTSConfig('mod.recycler');
-        if ($this->isCurrentUserAdmin()) {
+        if ($this->getBackendUser()->isAdmin()) {
             $this->allowDelete = true;
         } else {
             $this->allowDelete = (bool)$modTS['properties']['allowDelete'];
@@ -170,17 +170,7 @@ class RecyclerModuleController extends ActionController
     }
 
     /**
-     * Determines whether the current user is admin.
-     *
-     * @return bool Whether the current user is admin
-     */
-    protected function isCurrentUserAdmin()
-    {
-        return (bool)$this->getBackendUser()->user['admin'];
-    }
-
-    /**
-     * Gets the JavaScript configuration for the Ext JS interface.
+     * Gets the JavaScript configuration.
      *
      * @return array The JavaScript configuration
      */
index 98d0f60..396a0ed 100644 (file)
@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Recycler\Domain\Model;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\DataHandling\DataHandler;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Recycler\Utility\RecyclerUtility;
@@ -310,22 +311,39 @@ class DeletedRecords
      *
      * @param array $recordsArray Representation of the records
      * @param bool $recursive Whether to recursively undelete
-     * @return bool
+     * @return bool|int
      */
     public function undeleteData($recordsArray, $recursive = false)
     {
         $result = false;
+        $affectedRecords = 0;
         $depth = 999;
         if (is_array($recordsArray)) {
             $this->deletedRows = array();
             $cmd = array();
             foreach ($recordsArray as $record) {
                 list($table, $uid) = explode(':', $record);
+                // get all parent pages and cover them
+                $pid = RecyclerUtility::getPidOfUid($uid, $table);
+                if ($pid > 0) {
+                    $parentUidsToRecover = $this->getDeletedParentPages($pid);
+                    $count = count($parentUidsToRecover);
+                    for ($i = 0; $i < $count; ++$i) {
+                        $parentUid = $parentUidsToRecover[$i];
+                        $cmd['pages'][$parentUid]['undelete'] = 1;
+                        $affectedRecords++;
+                    }
+                    if (isset($cmd['pages'])) {
+                        // reverse the page list to recover it from top to bottom
+                        $cmd['pages'] = array_reverse($cmd['pages'], TRUE);
+                    }
+                }
                 $cmd[$table][$uid]['undelete'] = 1;
+                $affectedRecords++;
                 if ($table === 'pages' && $recursive) {
                     $this->loadData($uid, '', $depth, '');
                     $childRecords = $this->getDeletedRows();
-                    if (count($childRecords) > 0) {
+                    if (!empty($childRecords)) {
                         foreach ($childRecords as $childTable => $childRows) {
                             foreach ($childRows as $childRow) {
                                 $cmd[$childTable][$childRow['uid']]['undelete'] = 1;
@@ -338,12 +356,33 @@ class DeletedRecords
                 $tce = GeneralUtility::makeInstance(DataHandler::class);
                 $tce->start(array(), $cmd);
                 $tce->process_cmdmap();
-                $result = true;
+                $result = $affectedRecords;
             }
         }
         return $result;
     }
 
+    /**
+     * Returns deleted parent pages
+     *
+     * @param int $uid
+     * @param array $pages
+     * @return array
+     */
+    protected function getDeletedParentPages($uid, &$pages = array()) {
+        $db = $this->getDatabaseConnection();
+        $res = $db->exec_SELECTquery('uid, pid', 'pages', 'uid=' . (int)$uid . ' AND ' . $GLOBALS['TCA']['pages']['ctrl']['delete'] . '=1');
+        if ($res !== false && $db->sql_num_rows($res) > 0) {
+            $record = $db->sql_fetch_assoc($res);
+            $pages[] = $record['uid'];
+            if ((int)$record['pid'] !== 0) {
+                $this->getDeletedParentPages($record['pid'], $pages);
+            }
+        }
+
+        return $pages;
+    }
+
     /************************************************************
      * SETTER FUNCTIONS
      ************************************************************/
index 5c1ebd3..34b1c9d 100644 (file)
@@ -48,6 +48,7 @@ class RecyclerUtility
         if (is_array($calcPRec)) {
             if ($table === 'pages') {
                 // If pages:
+                // @todo: find a decent way for non-admins to get deleted pages respecting the permissions WITHOUT some isInWebMount stuff.
                 $calculatedPermissions = $backendUser->calcPerms($calcPRec);
                 $hasAccess = (bool)($calculatedPermissions & Permission::PAGE_EDIT);
             } else {
@@ -134,6 +135,42 @@ class RecyclerUtility
     }
 
     /**
+     * Check if parent record is deleted
+     *
+     * @param int $pid
+     * @return bool
+     */
+    public static function isParentPageDeleted($pid) {
+        if ((int)$pid === 0) {
+            return false;
+        }
+        $db = static::getDatabaseConnection();
+        $res = $db->exec_SELECTquery('deleted', 'pages', 'uid=' . (int)$pid);
+        if ($res !== false) {
+            $record = $db->sql_fetch_assoc($res);
+            return (bool)$record['deleted'];
+        }
+        return false;
+    }
+
+    /**
+     * Get pid of uid
+     *
+     * @param int $uid
+     * @param string $table
+     * @return int
+     */
+    public static function getPidOfUid($uid, $table) {
+        $db = static::getDatabaseConnection();
+        $res = $db->exec_SELECTquery('pid', $table, 'uid=' . (int)$uid);
+        if ($res !== false) {
+            $record = $db->sql_fetch_assoc($res);
+            return $record['pid'];
+        }
+        return 0;
+    }
+
+    /**
      * Gets the TCA of the table used in the current context.
      *
      * @param string $tableName Name of the table to get TCA for
index 64f686b..3567d47 100644 (file)
@@ -25,15 +25,15 @@ recordsPageLimit
 
    Property
          recordsPageLimit
-   
+
    Data type
          integer
-   
+
    Description
          How many records displaying in grid per page. Default is 50.
-         
+
          Example:
-         
+
          mod.recycler.recordsPageLimit = 100
 
 
@@ -47,16 +47,14 @@ allowDelete
 
    Property
          allowDelete
-   
+
    Data type
          boolean
-   
+
    Description
          Editors are not allowed to delete any record by default. Setting this
          property allows editor deleting records.
-         
-         Example:
-         
-         mod.recycler.allowDelete = 1
 
+         Example:
 
+         mod.recycler.allowDelete = 1
\ No newline at end of file
index 0928df3..df72799 100644 (file)
@@ -40,7 +40,7 @@
                                <source>Recover records</source>
                        </trans-unit>
                        <trans-unit id="modal.undo.recursive">
-                               <source>Recover recursively</source>
+                               <source>Recover content and subpages recursively</source>
                        </trans-unit>
                        <trans-unit id="modal.deletecontent.text">
                                <source>Do you really want to delete the record "{0}" {1} permanently? You cannot revert this action!</source>
@@ -57,6 +57,9 @@
                        <trans-unit id="modal.undopage.text">
                                <source>Do you really want to recover the page "{0}" {1}, its contents and optionally all of its subpages?</source>
                        </trans-unit>
+                       <trans-unit id="modal.undo.parentpages">
+                               <source>All parent pages have been restored to ensure a valid rootline!</source>
+                       </trans-unit>
                        <trans-unit id="modal.massundo.text">
                                <source>Do you really want to recover all selected records and optionally potential subpages?</source>
                        </trans-unit>
index ed68ffa..c199162 100644 (file)
@@ -1,5 +1,5 @@
 {namespace core=TYPO3\CMS\Core\ViewHelpers}
-<tr data-uid="{record.uid}" data-table="{record.table}" data-recordtitle="{record.title}">
+<tr data-uid="{record.uid}" data-table="{record.table}" data-recordtitle="{record.title}" data-parent-deleted="{record.isParentDeleted}">
        <td nowrap="nowrap">{record.tableTitle}</td>
        <td nowrap="nowrap"><f:format.raw>{record.icon}</f:format.raw> {record.title}</td>
        <td nowrap="nowrap">{record.tstamp}</td>
index 2cbbc86..a779452 100644 (file)
@@ -400,6 +400,10 @@ define(['jquery', 'nprogress', 'TYPO3/CMS/Backend/Modal', 'TYPO3/CMS/Backend/jqu
                        recoverPages = table === 'pages';
                        messageText = recoverPages ? TYPO3.lang['modal.undopage.text'] : TYPO3.lang['modal.undocontent.text'];
                        messageText = Recycler.createMessage(messageText, [recordTitle, '[' + records + ']']);
+
+                       if (recoverPages && $tr.data('parentDeleted')) {
+                               messageText += TYPO3.lang['modal.undo.parentpages'];
+                       }
                }
 
                var $message = null;