silverstripe-cms/tasks/RemoveOrphanedPagesTask.php
Damian Mooyman 1c907dd227 API Support versioned File management
API Decouple File and ErrorPage
API Link tracking is now only performed on stage (in lieu of versioned relationships)
API Refactor versioned API methods out of SiteTree and into Versioned
2016-02-22 12:25:25 +13:00

340 lines
9.9 KiB
PHP

<?php
/**
* Identify "orphaned" pages which point to a parent
* that no longer exists in a specific stage.
* Shows the pages to an administrator, who can then
* decide which pages to remove by ticking a checkbox
* and manually executing the removal.
*
* Caution: Pages also count as orphans if they don't
* have parents in this stage, even if the parent has a representation
* in the other stage:
* - A live child is orphaned if its parent was deleted from live, but still exists on stage
* - A stage child is orphaned if its parent was deleted from stage, but still exists on live
*
* See {@link RemoveOrphanedPagesTaskTest} for an example sitetree
* before and after orphan removal.
*
* @author Ingo Schommer (<firstname>@silverstripe.com), SilverStripe Ltd.
*
* @package cms
* @subpackage tasks
*/
//class RemoveOrphanedPagesTask extends BuildTask {
class RemoveOrphanedPagesTask extends Controller {
private static $allowed_actions = array(
'index' => 'ADMIN',
'Form' => 'ADMIN',
'run' => 'ADMIN',
'handleAction' => 'ADMIN',
);
protected $title = 'Removed orphaned pages without existing parents from both stage and live';
protected $description = "
<p>
Identify 'orphaned' pages which point to a parent
that no longer exists in a specific stage.
</p>
<p>
Caution: Pages also count as orphans if they don't
have parents in this stage, even if the parent has a representation
in the other stage:<br />
- A live child is orphaned if its parent was deleted from live, but still exists on stage<br />
- A stage child is orphaned if its parent was deleted from stage, but still exists on live
</p>
";
protected $orphanedSearchClass = 'SiteTree';
public function Link() {
return $this->class;
}
public function init() {
parent::init();
if(!Permission::check('ADMIN')) {
return Security::permissionFailure($this);
}
}
public function index() {
Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
Requirements::customCSS('#OrphanIDs .middleColumn {width: auto;}');
Requirements::customCSS('#OrphanIDs label {display: inline;}');
return $this->renderWith('BlankPage');
}
public function Form() {
$fields = new FieldList();
$source = array();
$fields->push(new HeaderField(
'Header',
_t('RemoveOrphanedPagesTask.HEADER', 'Remove all orphaned pages task')
));
$fields->push(new LiteralField(
'Description',
$this->description
));
$orphans = $this->getOrphanedPages($this->orphanedSearchClass);
if($orphans) foreach($orphans as $orphan) {
$latestVersion = Versioned::get_latest_version($this->orphanedSearchClass, $orphan->ID);
$latestAuthor = DataObject::get_by_id('Member', $latestVersion->AuthorID);
$orphanBaseClass = ClassInfo::baseDataClass($this->orphanedSearchClass);
$stageRecord = Versioned::get_one_by_stage(
$this->orphanedSearchClass,
'Stage',
array("\"$orphanBaseClass\".\"ID\"" => $orphan->ID)
);
$liveRecord = Versioned::get_one_by_stage(
$this->orphanedSearchClass,
'Live',
array("\"$orphanBaseClass\".\"ID\"" => $orphan->ID)
);
$label = sprintf(
'<a href="admin/pages/edit/show/%d">%s</a> <small>(#%d, Last Modified Date: %s, Last Modifier: %s, %s)</small>',
$orphan->ID,
$orphan->Title,
$orphan->ID,
Date::create($orphan->LastEdited)->Nice(),
($latestAuthor) ? $latestAuthor->Title : 'unknown',
($liveRecord) ? 'is published' : 'not published'
);
$source[$orphan->ID] = $label;
}
if($orphans && $orphans->Count()) {
$fields->push(new CheckboxSetField('OrphanIDs', false, $source));
$fields->push(new LiteralField(
'SelectAllLiteral',
sprintf(
'<p><a href="#" onclick="javascript:jQuery(\'#Form_Form_OrphanIDs :checkbox\').attr(\'checked\', \'checked\'); return false;">%s</a>&nbsp;',
_t('RemoveOrphanedPagesTask.SELECTALL', 'select all')
)
));
$fields->push(new LiteralField(
'UnselectAllLiteral',
sprintf(
'<a href="#" onclick="javascript:jQuery(\'#Form_Form_OrphanIDs :checkbox\').attr(\'checked\', \'\'); return false;">%s</a></p>',
_t('RemoveOrphanedPagesTask.UNSELECTALL', 'unselect all')
)
));
$fields->push(new OptionSetField(
'OrphanOperation',
_t('RemoveOrphanedPagesTask.CHOOSEOPERATION', 'Choose operation:'),
array(
'rebase' => _t(
'RemoveOrphanedPagesTask.OPERATION_REBASE',
sprintf(
'Rebase selected to a new holder page "%s" and unpublish. None of these pages will show up for website visitors.',
$this->rebaseHolderTitle()
)
),
'remove' => _t('RemoveOrphanedPagesTask.OPERATION_REMOVE', 'Remove selected from all stages (WARNING: Will destroy all selected pages from both stage and live)'),
),
'rebase'
));
$fields->push(new LiteralField(
'Warning',
sprintf('<p class="message">%s</p>',
_t(
'RemoveOrphanedPagesTask.DELETEWARNING',
'Warning: These operations are not reversible. Please handle with care.'
)
)
));
} else {
$fields->push(new LiteralField(
'NotFoundLabel',
sprintf(
'<p class="message">%s</p>',
_t('RemoveOrphanedPagesTask.NONEFOUND', 'No orphans found')
)
));
}
$form = new Form(
$this,
'Form',
$fields,
new FieldList(
new FormAction('doSubmit', _t('RemoveOrphanedPagesTask.BUTTONRUN', 'Run'))
)
);
if(!$orphans || !$orphans->Count()) {
$form->makeReadonly();
}
return $form;
}
public function run($request) {
// @todo Merge with BuildTask functionality
}
public function doSubmit($data, $form) {
set_time_limit(60*10); // 10 minutes
if(!isset($data['OrphanIDs']) || !isset($data['OrphanOperation'])) return false;
switch($data['OrphanOperation']) {
case 'remove':
$successIDs = $this->removeOrphans($data['OrphanIDs']);
break;
case 'rebase':
$successIDs = $this->rebaseOrphans($data['OrphanIDs']);
break;
default:
user_error(sprintf("Unknown operation: '%s'", $data['OrphanOperation']), E_USER_ERROR);
}
$content = '';
if($successIDs) {
$content .= "<ul>";
foreach($successIDs as $id => $label) {
$content .= sprintf('<li>%s</li>', $label);
}
$content .= "</ul>";
} else {
$content = _t('RemoveOrphanedPagesTask.NONEREMOVED', 'None removed');
}
return $this->customise(array(
'Content' => $content,
'Form' => ' '
))->renderWith('BlankPage');
}
protected function removeOrphans($orphanIDs) {
$removedOrphans = array();
$orphanBaseClass = ClassInfo::baseDataClass($this->orphanedSearchClass);
foreach($orphanIDs as $id) {
/** @var SiteTree $stageRecord */
$stageRecord = Versioned::get_one_by_stage(
$this->orphanedSearchClass,
'Stage',
array("\"$orphanBaseClass\".\"ID\"" => $id)
);
if($stageRecord) {
$removedOrphans[$stageRecord->ID] = sprintf('Removed %s (#%d) from Stage', $stageRecord->Title, $stageRecord->ID);
$stageRecord->delete();
$stageRecord->destroy();
unset($stageRecord);
}
/** @var SiteTree $liveRecord */
$liveRecord = Versioned::get_one_by_stage(
$this->orphanedSearchClass,
'Live',
array("\"$orphanBaseClass\".\"ID\"" => $id)
);
if($liveRecord) {
$removedOrphans[$liveRecord->ID] = sprintf('Removed %s (#%d) from Live', $liveRecord->Title, $liveRecord->ID);
$liveRecord->doUnpublish();
$liveRecord->destroy();
unset($liveRecord);
}
}
return $removedOrphans;
}
protected function rebaseHolderTitle() {
return sprintf('Rebased Orphans (%s)', date('d/m/Y g:ia', time()));
}
protected function rebaseOrphans($orphanIDs) {
$holder = new SiteTree();
$holder->ShowInMenus = 0;
$holder->ShowInSearch = 0;
$holder->ParentID = 0;
$holder->Title = $this->rebaseHolderTitle();
$holder->write();
$removedOrphans = array();
$orphanBaseClass = ClassInfo::baseDataClass($this->orphanedSearchClass);
foreach($orphanIDs as $id) {
$stageRecord = Versioned::get_one_by_stage(
$this->orphanedSearchClass,
'Stage',
array("\"$orphanBaseClass\".\"ID\"" => $id)
);
if($stageRecord) {
$removedOrphans[$stageRecord->ID] = sprintf('Rebased %s (#%d)', $stageRecord->Title, $stageRecord->ID);
$stageRecord->ParentID = $holder->ID;
$stageRecord->ShowInMenus = 0;
$stageRecord->ShowInSearch = 0;
$stageRecord->write();
$stageRecord->doUnpublish();
$stageRecord->destroy();
//unset($stageRecord);
}
$liveRecord = Versioned::get_one_by_stage(
$this->orphanedSearchClass,
'Live',
array("\"$orphanBaseClass\".\"ID\"" => $id)
);
if($liveRecord) {
$removedOrphans[$liveRecord->ID] = sprintf('Rebased %s (#%d)', $liveRecord->Title, $liveRecord->ID);
$liveRecord->ParentID = $holder->ID;
$liveRecord->ShowInMenus = 0;
$liveRecord->ShowInSearch = 0;
$liveRecord->write();
if(!$stageRecord) $liveRecord->doRestoreToStage();
$liveRecord->doUnpublish();
$liveRecord->destroy();
unset($liveRecord);
}
if($stageRecord) {
unset($stageRecord);
}
}
return $removedOrphans;
}
/**
* Gets all orphans from "Stage" and "Live" stages.
*
* @param string $class
* @param array $filter
* @param string $sort
* @param string $join
* @param int|array $limit
* @return SS_List
*/
public function getOrphanedPages($class = 'SiteTree', $filter = array(), $sort = null, $join = null, $limit = null) {
// Alter condition
if(empty($filter)) $where = array();
elseif(is_array($filter)) $where = $filter;
else $where = array($filter);
$where[] = array("\"$class\".\"ParentID\" != ?" => 0);
$where[] = '"Parents"."ID" IS NULL';
$orphans = new ArrayList();
foreach(array('Stage', 'Live') as $stage) {
$joinByStage = $join;
$table = $class;
$table .= ($stage == 'Live') ? '_Live' : '';
$stageOrphans = Versioned::get_by_stage(
$class,
$stage,
$where,
$sort,
null,
$limit
)->leftJoin($table, "\"$table\".\"ParentID\" = \"Parents\".\"ID\"", "Parents");
$orphans->merge($stageOrphans);
}
$orphans->removeDuplicates();
return $orphans;
}
}