1 <?php
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
23
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> ',
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
184 }
185
186 function doSubmit($data, $form) {
187 set_time_limit(60*10);
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
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 316 317 318 319 320 321 322 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.
-