Completed
Push — master ( 91444b...ac9540 )
by Ingo
03:27
created

RemoveOrphanedPagesTask   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 322
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 37
c 2
b 1
f 0
lcom 1
cbo 9
dl 0
loc 322
rs 8.6

10 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 7 2
A Link() 0 5 1
A index() 0 7 1
D Form() 0 101 9
A run() 0 3 1
C doSubmit() 0 33 7
B removeOrphans() 0 32 4
A rebaseHolderTitle() 0 3 1
B rebaseOrphans() 0 53 6
B getOrphanedPages() 0 31 5
1
<?php
2
3
namespace SilverStripe\CMS\Tasks;
4
5
use SilverStripe\CMS\Model\SiteTree;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Forms\CheckboxSetField;
8
use SilverStripe\Forms\FieldList;
9
use SilverStripe\Forms\Form;
10
use SilverStripe\Forms\FormAction;
11
use SilverStripe\Forms\HeaderField;
12
use SilverStripe\Forms\LiteralField;
13
use SilverStripe\Forms\OptionsetField;
14
use SilverStripe\ORM\ArrayList;
15
use SilverStripe\ORM\DataObject;
16
use SilverStripe\ORM\SS_List;
17
use SilverStripe\ORM\Versioning\Versioned;
18
use SilverStripe\Security\Permission;
19
use SilverStripe\Security\Security;
20
use SilverStripe\View\Requirements;
21
22
/**
23
 * Identify "orphaned" pages which point to a parent
24
 * that no longer exists in a specific stage.
25
 * Shows the pages to an administrator, who can then
26
 * decide which pages to remove by ticking a checkbox
27
 * and manually executing the removal.
28
 *
29
 * Caution: Pages also count as orphans if they don't
30
 * have parents in this stage, even if the parent has a representation
31
 * in the other stage:
32
 * - A live child is orphaned if its parent was deleted from live, but still exists on stage
33
 * - A stage child is orphaned if its parent was deleted from stage, but still exists on live
34
 *
35
 * See {@link RemoveOrphanedPagesTaskTest} for an example sitetree
36
 * before and after orphan removal.
37
 *
38
 * @author Ingo Schommer (<firstname>@silverstripe.com), SilverStripe Ltd.
39
 */
40
class RemoveOrphanedPagesTask extends Controller {
41
42
	private static $allowed_actions = array(
43
		'index' => 'ADMIN',
44
		'Form' => 'ADMIN',
45
		'run' => 'ADMIN',
46
		'handleAction' => 'ADMIN',
47
	);
48
49
	protected $title = 'Removed orphaned pages without existing parents from both stage and live';
50
51
	protected $description = "
52
<p>
53
Identify 'orphaned' pages which point to a parent
54
that no longer exists in a specific stage.
55
</p>
56
<p>
57
Caution: Pages also count as orphans if they don't
58
have parents in this stage, even if the parent has a representation
59
in the other stage:<br />
60
- A live child is orphaned if its parent was deleted from live, but still exists on stage<br />
61
- A stage child is orphaned if its parent was deleted from stage, but still exists on live
62
</p>
63
	";
64
65
	protected $orphanedSearchClass = 'SilverStripe\\CMS\\Model\\SiteTree';
66
67
	public function init() {
68
		parent::init();
69
70
		if(!Permission::check('ADMIN')) {
71
			Security::permissionFailure($this);
72
		}
73
	}
74
75
	public function Link($action = null)
76
	{
77
		/** @skipUpgrade */
78
		return Controller::join_links('RemoveOrphanedPagesTask', $action, '/');
79
	}
80
81
	public function index() {
82
		Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
83
		Requirements::customCSS('#OrphanIDs .middleColumn {width: auto;}');
84
		Requirements::customCSS('#OrphanIDs label {display: inline;}');
85
86
		return $this->renderWith('BlankPage');
87
	}
88
89
	public function Form() {
90
		$fields = new FieldList();
91
		$source = array();
92
93
		$fields->push(new HeaderField(
94
			'Header',
95
			_t('RemoveOrphanedPagesTask.HEADER', 'Remove all orphaned pages task')
96
		));
97
		$fields->push(new LiteralField(
98
			'Description',
99
			$this->description
100
		));
101
102
		$orphans = $this->getOrphanedPages($this->orphanedSearchClass);
103
		if($orphans) foreach($orphans as $orphan) {
104
			/** @var SiteTree $latestVersion */
105
			$latestVersion = Versioned::get_latest_version($this->orphanedSearchClass, $orphan->ID);
106
			$latestAuthor = DataObject::get_by_id('SilverStripe\\Security\\Member', $latestVersion->AuthorID);
107
			$orphanBaseTable = DataObject::getSchema()->baseDataTable($this->orphanedSearchClass);
108
			$liveRecord = Versioned::get_one_by_stage(
109
				$this->orphanedSearchClass,
110
				'Live',
111
				array("\"$orphanBaseTable\".\"ID\"" => $orphan->ID)
0 ignored issues
show
Documentation introduced by
array("\"{$orphanBaseTab...\"ID\"" => $orphan->ID) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
112
			);
113
			$label = sprintf(
114
				'<a href="admin/pages/edit/show/%d">%s</a> <small>(#%d, Last Modified Date: %s, Last Modifier: %s, %s)</small>',
115
				$orphan->ID,
116
				$orphan->Title,
117
				$orphan->ID,
118
				$orphan->dbObject('LastEdited')->Nice(),
119
				($latestAuthor) ? $latestAuthor->Title : 'unknown',
120
				($liveRecord) ? 'is published' : 'not published'
121
			);
122
			$source[$orphan->ID] = $label;
123
		}
124
125
		if($orphans && $orphans->count()) {
126
			$fields->push(new CheckboxSetField('OrphanIDs', false, $source));
127
			$fields->push(new LiteralField(
128
				'SelectAllLiteral',
129
				sprintf(
130
					'<p><a href="#" onclick="javascript:jQuery(\'#Form_Form_OrphanIDs :checkbox\').attr(\'checked\', \'checked\'); return false;">%s</a>&nbsp;',
131
					_t('RemoveOrphanedPagesTask.SELECTALL', 'select all')
132
				)
133
			));
134
			$fields->push(new LiteralField(
135
				'UnselectAllLiteral',
136
				sprintf(
137
					'<a href="#" onclick="javascript:jQuery(\'#Form_Form_OrphanIDs :checkbox\').attr(\'checked\', \'\'); return false;">%s</a></p>',
138
					_t('RemoveOrphanedPagesTask.UNSELECTALL', 'unselect all')
139
				)
140
			));
141
			$fields->push(new OptionsetField(
142
				'OrphanOperation',
143
				_t('RemoveOrphanedPagesTask.CHOOSEOPERATION', 'Choose operation:'),
144
				array(
145
					'rebase' => _t(
146
						'RemoveOrphanedPagesTask.OPERATION_REBASE',
147
						sprintf(
148
							'Rebase selected to a new holder page "%s" and unpublish. None of these pages will show up for website visitors.',
149
							$this->rebaseHolderTitle()
150
						)
151
					),
152
					'remove' => _t('RemoveOrphanedPagesTask.OPERATION_REMOVE', 'Remove selected from all stages (WARNING: Will destroy all selected pages from both stage and live)'),
153
				),
154
				'rebase'
155
			));
156
			$fields->push(new LiteralField(
157
				'Warning',
158
				sprintf('<p class="message">%s</p>',
159
					_t(
160
						'RemoveOrphanedPagesTask.DELETEWARNING',
161
						'Warning: These operations are not reversible. Please handle with care.'
162
					)
163
				)
164
			));
165
		} else {
166
			$fields->push(new LiteralField(
167
				'NotFoundLabel',
168
				sprintf(
169
					'<p class="message">%s</p>',
170
					_t('RemoveOrphanedPagesTask.NONEFOUND', 'No orphans found')
171
				)
172
			));
173
		}
174
175
		$form = new Form(
176
			$this,
177
			'SilverStripe\\Forms\\Form',
178
			$fields,
179
			new FieldList(
180
				new FormAction('doSubmit', _t('RemoveOrphanedPagesTask.BUTTONRUN', 'Run'))
181
			)
182
		);
183
184
		if(!$orphans || !$orphans->count()) {
185
			$form->makeReadonly();
186
		}
187
188
		return $form;
189
	}
190
191
	public function run($request) {
192
		// @todo Merge with BuildTask functionality
193
	}
194
195
	public function doSubmit($data, $form) {
196
		set_time_limit(60*10); // 10 minutes
197
198
		if(!isset($data['OrphanIDs']) || !isset($data['OrphanOperation'])) return false;
199
200
		$successIDs = null;
201
		switch($data['OrphanOperation']) {
202
			case 'remove':
203
				$successIDs = $this->removeOrphans($data['OrphanIDs']);
204
				break;
205
			case 'rebase':
206
				$successIDs = $this->rebaseOrphans($data['OrphanIDs']);
207
				break;
208
			default:
209
				user_error(sprintf("Unknown operation: '%s'", $data['OrphanOperation']), E_USER_ERROR);
210
		}
211
212
		$content = '';
213
		if($successIDs) {
214
			$content .= "<ul>";
215
			foreach($successIDs as $id => $label) {
216
				$content .= sprintf('<li>%s</li>', $label);
217
			}
218
			$content .= "</ul>";
219
		} else {
220
			$content = _t('RemoveOrphanedPagesTask.NONEREMOVED', 'None removed');
221
		}
222
223
		return $this->customise(array(
224
			'Content' => $content,
225
			'Form' => ' '
226
		))->renderWith('BlankPage');
227
	}
228
229
	protected function removeOrphans($orphanIDs) {
230
		$removedOrphans = array();
231
		$orphanBaseTable = DataObject::getSchema()->baseDataTable($this->orphanedSearchClass);
232
		foreach($orphanIDs as $id) {
233
			/** @var SiteTree $stageRecord */
234
			$stageRecord = Versioned::get_one_by_stage(
235
				$this->orphanedSearchClass,
236
				Versioned::DRAFT,
237
				array("\"$orphanBaseTable\".\"ID\"" => $id)
0 ignored issues
show
Documentation introduced by
array("\"{$orphanBaseTable}\".\"ID\"" => $id) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
238
			);
239
			if($stageRecord) {
240
				$removedOrphans[$stageRecord->ID] = sprintf('Removed %s (#%d) from Stage', $stageRecord->Title, $stageRecord->ID);
241
				$stageRecord->delete();
242
				$stageRecord->destroy();
243
				unset($stageRecord);
244
			}
245
			/** @var SiteTree $liveRecord */
246
			$liveRecord = Versioned::get_one_by_stage(
247
				$this->orphanedSearchClass,
248
				Versioned::LIVE,
249
				array("\"$orphanBaseTable\".\"ID\"" => $id)
0 ignored issues
show
Documentation introduced by
array("\"{$orphanBaseTable}\".\"ID\"" => $id) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
250
			);
251
			if($liveRecord) {
252
				$removedOrphans[$liveRecord->ID] = sprintf('Removed %s (#%d) from Live', $liveRecord->Title, $liveRecord->ID);
253
				$liveRecord->doUnpublish();
254
				$liveRecord->destroy();
255
				unset($liveRecord);
256
			}
257
		}
258
259
		return $removedOrphans;
260
	}
261
262
	protected function rebaseHolderTitle() {
263
		return sprintf('Rebased Orphans (%s)', date('d/m/Y g:ia', time()));
264
	}
265
266
	protected function rebaseOrphans($orphanIDs) {
267
		$holder = new SiteTree();
268
		$holder->ShowInMenus = 0;
269
		$holder->ShowInSearch = 0;
270
		$holder->ParentID = 0;
271
		$holder->Title = $this->rebaseHolderTitle();
272
		$holder->write();
273
274
		$removedOrphans = array();
275
		$orphanBaseTable = DataObject::getSchema()->baseDataTable($this->orphanedSearchClass);
276
		foreach($orphanIDs as $id) {
277
			/** @var SiteTree $stageRecord */
278
			$stageRecord = Versioned::get_one_by_stage(
279
				$this->orphanedSearchClass,
280
				'Stage',
281
				array("\"$orphanBaseTable\".\"ID\"" => $id)
0 ignored issues
show
Documentation introduced by
array("\"{$orphanBaseTable}\".\"ID\"" => $id) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
282
			);
283
			if($stageRecord) {
284
				$removedOrphans[$stageRecord->ID] = sprintf('Rebased %s (#%d)', $stageRecord->Title, $stageRecord->ID);
285
				$stageRecord->ParentID = $holder->ID;
286
				$stageRecord->ShowInMenus = 0;
287
				$stageRecord->ShowInSearch = 0;
288
				$stageRecord->write();
289
				$stageRecord->doUnpublish();
290
				$stageRecord->destroy();
291
				//unset($stageRecord);
292
			}
293
			/** @var SiteTree $liveRecord */
294
			$liveRecord = Versioned::get_one_by_stage(
295
				$this->orphanedSearchClass,
296
				'Live',
297
				array("\"$orphanBaseTable\".\"ID\"" => $id)
0 ignored issues
show
Documentation introduced by
array("\"{$orphanBaseTable}\".\"ID\"" => $id) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
298
			);
299
			if($liveRecord) {
300
				$removedOrphans[$liveRecord->ID] = sprintf('Rebased %s (#%d)', $liveRecord->Title, $liveRecord->ID);
301
				$liveRecord->ParentID = $holder->ID;
302
				$liveRecord->ShowInMenus = 0;
303
				$liveRecord->ShowInSearch = 0;
304
				$liveRecord->write();
305
				if(!$stageRecord) {
306
					$liveRecord->doRestoreToStage();
307
				}
308
				$liveRecord->doUnpublish();
309
				$liveRecord->destroy();
310
				unset($liveRecord);
311
			}
312
			if($stageRecord) {
313
				unset($stageRecord);
314
			}
315
		}
316
317
		return $removedOrphans;
318
	}
319
320
	/**
321
	 * Gets all orphans from "Stage" and "Live" stages.
322
	 *
323
	 * @param string $class
324
	 * @param array $filter
325
	 * @param string $sort
326
	 * @param string $join
327
	 * @param int|array $limit
328
	 * @return SS_List
329
	 */
330
	public function getOrphanedPages($class = 'SilverStripe\\CMS\\Model\\SiteTree', $filter = array(), $sort = null, $join = null, $limit = null) {
331
		// Alter condition
332
		$table = DataObject::getSchema()->tableName($class);
333
		if(empty($filter)) {
334
			$where = array();
335
		} elseif(is_array($filter)) {
336
			$where = $filter;
337
		} else {
338
			$where = array($filter);
339
		}
340
		$where[] = array("\"{$table}\".\"ParentID\" != ?" => 0);
341
		$where[] = '"Parents"."ID" IS NULL';
342
343
		$orphans = new ArrayList();
344
		foreach(array(Versioned::DRAFT, Versioned::LIVE) as $stage) {
345
			$table .= ($stage == Versioned::LIVE) ? '_Live' : '';
346
			$stageOrphans = Versioned::get_by_stage(
347
				$class,
348
				$stage,
349
				$where,
0 ignored issues
show
Documentation introduced by
$where is of type array<integer,array<stri...nteger,integer>|string>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
350
				$sort,
351
				null,
352
				$limit
0 ignored issues
show
Bug introduced by
It seems like $limit defined by parameter $limit on line 330 can also be of type array; however, SilverStripe\ORM\Version...rsioned::get_by_stage() does only seem to accept integer|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
353
			)->leftJoin($table, "\"$table\".\"ParentID\" = \"Parents\".\"ID\"", "Parents");
354
			$orphans->merge($stageOrphans);
355
		}
356
357
		$orphans->removeDuplicates();
358
359
		return $orphans;
360
	}
361
}
362