Version: current

      1 <?php
      2 /**
      3  * Identify "orphaned" pages which point to a parent
      4  * that no longer exists in a specific stage.
      5  * Shows the pages to an administrator, who can then
      6  * decide which pages to remove by ticking a checkbox
      7  * and manually executing the removal.
      8  *
      9  * Caution: Pages also count as orphans if they don't
     10  * have parents in this stage, even if the parent has a representation
     11  * in the other stage:
     12  * - A live child is orphaned if its parent was deleted from live, but still exists on stage
     13  * - A stage child is orphaned if its parent was deleted from stage, but still exists on live
     14  *
     15  * See {@link RemoveOrphanedPagesTaskTest} for an example sitetree
     16  * before and after orphan removal.
     17  *
     18  * @author Ingo Schommer (<firstname>@silverstripe.com), SilverStripe Ltd.
     19  *
     20  * @package sapphire
     21  * @subpackage tasks
     22  */
     23 //class RemoveOrphanedPagesTask extends BuildTask {
     24 class RemoveOrphanedPagesTask extends Controller {
     25
     26     static $allowed_actions = array(
     27         'index' => 'ADMIN',
     28         'Form' => 'ADMIN',
     29         'run' => 'ADMIN',
     30         'handleAction' => 'ADMIN',
     31     );
     32
     33     protected $title = 'Removed orphaned pages without existing parents from both stage and live';
     34
     35     protected $description = "
     36 <p>
     37 Identify 'orphaned' pages which point to a parent
     38 that no longer exists in a specific stage.
     39 </p>
     40 <p>
     41 Caution: Pages also count as orphans if they don't
     42 have parents in this stage, even if the parent has a representation
     43 in the other stage:<br />
     44 - A live child is orphaned if its parent was deleted from live, but still exists on stage<br />
     45 - A stage child is orphaned if its parent was deleted from stage, but still exists on live
     46 </p>
     47     ";
     48
     49     protected $orphanedSearchClass = 'SiteTree';
     50
     51     function Link() {
     52         return $this->class;
     53     }
     54
     55     function init() {
     56         parent::init();
     57
     58         if(!Permission::check('ADMIN')) {
     59             return Security::permissionFailure($this);
     60         }
     61     }
     62
     63     function index() {
     64         Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
     65         Requirements::customCSS('#OrphanIDs .middleColumn {width: auto;}');
     66         Requirements::customCSS('#OrphanIDs label {display: inline;}');
     67
     68         return $this->renderWith('BlankPage');
     69     }
     70
     71     function Form() {
     72         $fields = new FieldSet();
     73         $source = array();
     74
     75         $fields->push(new HeaderField(
     76             'Header',
     77             _t('RemoveOrphanedPagesTask.HEADER', 'Remove all orphaned pages task')
     78         ));
     79         $fields->push(new LiteralField(
     80             'Description',
     81             $this->description
     82         ));
     83
     84         $orphans = $this->getOrphanedPages($this->orphanedSearchClass);
     85         if($orphans) foreach($orphans as $orphan) {
     86             $latestVersion = Versioned::get_latest_version($this->orphanedSearchClass, $orphan->ID);
     87             $latestAuthor = DataObject::get_by_id('Member', $latestVersion->AuthorID);
     88             $stageRecord = Versioned::get_one_by_stage(
     89                 $this->orphanedSearchClass,
     90                 'Stage',
     91                 sprintf("\"%s\".\"ID\" = %d",
     92                     ClassInfo::baseDataClass($this->orphanedSearchClass),
     93                     $orphan->ID
     94                 )
     95             );
     96             $liveRecord = Versioned::get_one_by_stage(
     97                 $this->orphanedSearchClass,
     98                 'Live',
     99                 sprintf("\"%s\".\"ID\" = %d",
    100                     ClassInfo::baseDataClass($this->orphanedSearchClass),
    101                     $orphan->ID
    102                 )
    103             );
    104             $label = sprintf(
    105                 '<a href="admin/show/%d">%s</a> <small>(#%d, Last Modified Date: %s, Last Modifier: %s, %s)</small>',
    106                 $orphan->ID,
    107                 $orphan->Title,
    108                 $orphan->ID,
    109                 DBField::create('Date', $orphan->LastEdited)->Nice(),
    110                 ($latestAuthor) ? $latestAuthor->Title : 'unknown',
    111                 ($liveRecord) ? 'is published' : 'not published'
    112             );
    113             $source[$orphan->ID] = $label;
    114         }
    115
    116         if($orphans && $orphans->Count()) {
    117             $fields->push(new CheckboxSetField('OrphanIDs', false, $source));
    118             $fields->push(new LiteralField(
    119                 'SelectAllLiteral',
    120                 sprintf(
    121                     '<p><a href="#" onclick="javascript:jQuery(\'#Form_Form_OrphanIDs :checkbox\').attr(\'checked\', \'checked\'); return false;">%s</a>&nbsp;',
    122                     _t('RemoveOrphanedPagesTask.SELECTALL', 'select all')
    123                 )
    124             ));
    125             $fields->push(new LiteralField(
    126                 'UnselectAllLiteral',
    127                 sprintf(
    128                     '<a href="#" onclick="javascript:jQuery(\'#Form_Form_OrphanIDs :checkbox\').attr(\'checked\', \'\'); return false;">%s</a></p>',
    129                     _t('RemoveOrphanedPagesTask.UNSELECTALL', 'unselect all')
    130                 )
    131             ));
    132             $fields->push(new OptionSetField(
    133                 'OrphanOperation',
    134                 _t('RemoveOrphanedPagesTask.CHOOSEOPERATION', 'Choose operation:'),
    135                 array(
    136                     'rebase' => _t(
    137                         'RemoveOrphanedPagesTask.OPERATION_REBASE',
    138                         sprintf(
    139                             'Rebase selected to a new holder page "%s" and unpublish. None of these pages will show up for website visitors.',
    140                             $this->rebaseHolderTitle()
    141                         )
    142                     ),
    143                     'remove' => _t('RemoveOrphanedPagesTask.OPERATION_REMOVE', 'Remove selected from all stages (WARNING: Will destroy all selected pages from both stage and live)'),
    144                 ),
    145                 'rebase'
    146             ));
    147             $fields->push(new LiteralField(
    148                 'Warning',
    149                 sprintf('<p class="message">%s</p>',
    150                     _t(
    151                         'RemoveOrphanedPagesTask.DELETEWARNING',
    152                         'Warning: These operations are not reversible. Please handle with care.'
    153                     )
    154                 )
    155             ));
    156         } else {
    157             $fields->push(new LiteralField(
    158                 'NotFoundLabel',
    159                 sprintf(
    160                     '<p class="message">%s</p>',
    161                     _t('RemoveOrphanedPagesTask.NONEFOUND', 'No orphans found')
    162                 )
    163             ));
    164         }
    165
    166         $form = new Form(
    167             $this,
    168             'Form',
    169             $fields,
    170             new FieldSet(
    171                 new FormAction('doSubmit', _t('RemoveOrphanedPagesTask.BUTTONRUN', 'Run'))
    172             )
    173         );
    174
    175         if(!$orphans || !$orphans->Count()) {
    176             $form->makeReadonly();
    177         }
    178
    179         return $form;
    180     }
    181
    182     function run($request) {
    183         // @todo Merge with BuildTask functionality
    184     }
    185
    186     function doSubmit($data, $form) {
    187         set_time_limit(60*10); // 10 minutes
    188
    189         if(!isset($data['OrphanIDs']) || !isset($data['OrphanOperation'])) return false;
    190
    191         switch($data['OrphanOperation']) {
    192             case 'remove':
    193                 $successIDs = $this->removeOrphans($data['OrphanIDs']);
    194                 break;
    195             case 'rebase':
    196                 $successIDs = $this->rebaseOrphans($data['OrphanIDs']);
    197                 break;
    198             default:
    199                 user_error(sprintf("Unknown operation: '%s'", $data['OrphanOperation']), E_USER_ERROR);
    200         }
    201
    202         $content = '';
    203         if($successIDs) {
    204             $content .= "<ul>";
    205             foreach($successIDs as $id => $label) {
    206                 $content .= sprintf('<li>%s</li>', $label);
    207             }
    208             $content .= "</ul>";
    209         } else {
    210             $content = _t('RemoveOrphanedPagesTask.NONEREMOVED', 'None removed');
    211         }
    212
    213         return $this->customise(array(
    214             'Content' => $content,
    215             'Form' => ' '
    216         ))->renderWith('BlankPage');
    217     }
    218
    219     protected function removeOrphans($orphanIDs) {
    220         $removedOrphans = array();
    221         foreach($orphanIDs as $id) {
    222             $stageRecord = Versioned::get_one_by_stage(
    223                 $this->orphanedSearchClass,
    224                 'Stage',
    225                 sprintf("\"%s\".\"ID\" = %d",
    226                     ClassInfo::baseDataClass($this->orphanedSearchClass),
    227                     $id
    228                 )
    229             );
    230             if($stageRecord) {
    231                 $removedOrphans[$stageRecord->ID] = sprintf('Removed %s (#%d) from Stage', $stageRecord->Title, $stageRecord->ID);
    232                 $stageRecord->delete();
    233                 $stageRecord->destroy();
    234                 unset($stageRecord);
    235             }
    236             $liveRecord = Versioned::get_one_by_stage(
    237                 $this->orphanedSearchClass,
    238                 'Live',
    239                 sprintf("\"%s\".\"ID\" = %d",
    240                     ClassInfo::baseDataClass($this->orphanedSearchClass),
    241                     $id
    242                 )
    243             );
    244             if($liveRecord) {
    245                 $removedOrphans[$liveRecord->ID] = sprintf('Removed %s (#%d) from Live', $liveRecord->Title, $liveRecord->ID);
    246                 $liveRecord->doDeleteFromLive();
    247                 $liveRecord->destroy();
    248                 unset($liveRecord);
    249             }
    250         }
    251
    252         return $removedOrphans;
    253     }
    254
    255     protected function rebaseHolderTitle() {
    256         return sprintf('Rebased Orphans (%s)', date('d/m/Y g:ia', time()));
    257     }
    258
    259     protected function rebaseOrphans($orphanIDs) {
    260         $holder = new SiteTree();
    261         $holder->ShowInMenus = 0;
    262         $holder->ShowInSearch = 0;
    263         $holder->ParentID = 0;
    264         $holder->Title = $this->rebaseHolderTitle();
    265         $holder->write();
    266
    267         $removedOrphans = array();
    268         foreach($orphanIDs as $id) {
    269             $stageRecord = Versioned::get_one_by_stage(
    270                 $this->orphanedSearchClass,
    271                 'Stage',
    272                 sprintf("\"%s\".\"ID\" = %d",
    273                     ClassInfo::baseDataClass($this->orphanedSearchClass),
    274                     $id
    275                 )
    276             );
    277             if($stageRecord) {
    278                 $removedOrphans[$stageRecord->ID] = sprintf('Rebased %s (#%d)', $stageRecord->Title, $stageRecord->ID);
    279                 $stageRecord->ParentID = $holder->ID;
    280                 $stageRecord->ShowInMenus = 0;
    281                 $stageRecord->ShowInSearch = 0;
    282                 $stageRecord->write();
    283                 $stageRecord->doUnpublish();
    284                 $stageRecord->destroy();
    285                 //unset($stageRecord);
    286             }
    287             $liveRecord = Versioned::get_one_by_stage(
    288                 $this->orphanedSearchClass,
    289                 'Live',
    290                 sprintf("\"%s\".\"ID\" = %d",
    291                     ClassInfo::baseDataClass($this->orphanedSearchClass),
    292                     $id
    293                 )
    294             );
    295             if($liveRecord) {
    296                 $removedOrphans[$liveRecord->ID] = sprintf('Rebased %s (#%d)', $liveRecord->Title, $liveRecord->ID);
    297                 $liveRecord->ParentID = $holder->ID;
    298                 $liveRecord->ShowInMenus = 0;
    299                 $liveRecord->ShowInSearch = 0;
    300                 $liveRecord->write();
    301                 if(!$stageRecord) $liveRecord->doRestoreToStage();
    302                 $liveRecord->doUnpublish();
    303                 $liveRecord->destroy();
    304                 unset($liveRecord);
    305             }
    306             if($stageRecord) {
    307                 unset($stageRecord);
    308             }
    309         }
    310
    311         return $removedOrphans;
    312     }
    313
    314     /**
    315      * Gets all orphans from "Stage" and "Live" stages.
    316      *
    317      * @param string $class
    318      * @param string $filter
    319      * @param string $sort
    320      * @param string $join
    321      * @param int|array $limit
    322      * @return DataObjectSet
    323      */
    324     function getOrphanedPages($class = 'SiteTree', $filter = '', $sort = null, $join = null, $limit = null) {
    325         $filter .= ($filter) ? ' AND ' : '';
    326         $filter .= sprintf("\"%s\".\"ParentID\" != 0 AND \"Parents\".\"ID\" IS NULL", $class);
    327
    328         $orphans = new DataObjectSet();
    329         foreach(array('Stage', 'Live') as $stage) {
    330             $joinByStage = $join;
    331             $table = $class;
    332             $table .= ($stage == 'Live') ? '_Live' : '';
    333             $joinByStage .= sprintf(
    334                 "LEFT JOIN \"%s\" AS \"Parents\" ON \"%s\".\"ParentID\" = \"Parents\".\"ID\"",
    335                 $table,
    336                 $table
    337             );
    338             $stageOrphans = Versioned::get_by_stage(
    339                 $class,
    340                 $stage,
    341                 $filter,
    342                 $sort,
    343                 $joinByStage,
    344                 $limit
    345             );
    346             $orphans->merge($stageOrphans);
    347         }
    348
    349         $orphans->removeDuplicates();
    350
    351         return $orphans;
    352     }
    353 }
    354 ?>

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -