Completed
Push — master ( 5a1675...744118 )
by Hamish
9s
created

RemoveOrphanedPagesTask   C

Complexity

Total Complexity 36

Size/Duplication

Total Lines 307
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 19

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 36
c 3
b 1
f 0
lcom 1
cbo 19
dl 0
loc 307
rs 6.05

9 Methods

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

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...
113
			$fields->push(new LiteralField(
114
				'SelectAllLiteral',
115
				sprintf(
116
					'<p><a href="#" onclick="javascript:jQuery(\'#Form_Form_OrphanIDs :checkbox\').attr(\'checked\', \'checked\'); return false;">%s</a>&nbsp;',
117
					_t('RemoveOrphanedPagesTask.SELECTALL', 'select all')
118
				)
119
			));
120
			$fields->push(new LiteralField(
121
				'UnselectAllLiteral',
122
				sprintf(
123
					'<a href="#" onclick="javascript:jQuery(\'#Form_Form_OrphanIDs :checkbox\').attr(\'checked\', \'\'); return false;">%s</a></p>',
124
					_t('RemoveOrphanedPagesTask.UNSELECTALL', 'unselect all')
125
				)
126
			));
127
			$fields->push(new OptionsetField(
128
				'OrphanOperation',
129
				_t('RemoveOrphanedPagesTask.CHOOSEOPERATION', 'Choose operation:'),
130
				array(
131
					'rebase' => _t(
132
						'RemoveOrphanedPagesTask.OPERATION_REBASE',
133
						sprintf(
134
							'Rebase selected to a new holder page "%s" and unpublish. None of these pages will show up for website visitors.',
135
							$this->rebaseHolderTitle()
136
						)
137
					),
138
					'remove' => _t('RemoveOrphanedPagesTask.OPERATION_REMOVE', 'Remove selected from all stages (WARNING: Will destroy all selected pages from both stage and live)'),
139
				),
140
				'rebase'
141
			));
142
			$fields->push(new LiteralField(
143
				'Warning',
144
				sprintf('<p class="message">%s</p>',
145
					_t(
146
						'RemoveOrphanedPagesTask.DELETEWARNING',
147
						'Warning: These operations are not reversible. Please handle with care.'
148
					)
149
				)
150
			));
151
		} else {
152
			$fields->push(new LiteralField(
153
				'NotFoundLabel',
154
				sprintf(
155
					'<p class="message">%s</p>',
156
					_t('RemoveOrphanedPagesTask.NONEFOUND', 'No orphans found')
157
				)
158
			));
159
		}
160
161
		$form = new Form(
162
			$this,
163
			'Form',
164
			$fields,
165
			new FieldList(
166
				new FormAction('doSubmit', _t('RemoveOrphanedPagesTask.BUTTONRUN', 'Run'))
167
			)
168
		);
169
170
		if(!$orphans || !$orphans->Count()) {
171
			$form->makeReadonly();
172
		}
173
174
		return $form;
175
	}
176
177
	public function run($request) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
178
		// @todo Merge with BuildTask functionality
179
	}
180
181
	public function doSubmit($data, $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
182
		set_time_limit(60*10); // 10 minutes
183
184
		if(!isset($data['OrphanIDs']) || !isset($data['OrphanOperation'])) return false;
185
186
		$successIDs = null;
187
		switch($data['OrphanOperation']) {
188
			case 'remove':
189
				$successIDs = $this->removeOrphans($data['OrphanIDs']);
190
				break;
191
			case 'rebase':
192
				$successIDs = $this->rebaseOrphans($data['OrphanIDs']);
193
				break;
194
			default:
195
				user_error(sprintf("Unknown operation: '%s'", $data['OrphanOperation']), E_USER_ERROR);
196
		}
197
198
		$content = '';
199
		if($successIDs) {
200
			$content .= "<ul>";
201
			foreach($successIDs as $id => $label) {
202
				$content .= sprintf('<li>%s</li>', $label);
203
			}
204
			$content .= "</ul>";
205
		} else {
206
			$content = _t('RemoveOrphanedPagesTask.NONEREMOVED', 'None removed');
207
		}
208
209
		return $this->customise(array(
210
			'Content' => $content,
211
			'Form' => ' '
212
		))->renderWith('BlankPage');
213
	}
214
215
	protected function removeOrphans($orphanIDs) {
216
		$removedOrphans = array();
217
		$orphanBaseTable = DataObject::getSchema()->baseDataTable($this->orphanedSearchClass);
218
		foreach($orphanIDs as $id) {
219
			/** @var SiteTree $stageRecord */
220
			$stageRecord = Versioned::get_one_by_stage(
221
				$this->orphanedSearchClass,
222
				Versioned::DRAFT,
223
				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...
224
			);
225
			if($stageRecord) {
226
				$removedOrphans[$stageRecord->ID] = sprintf('Removed %s (#%d) from Stage', $stageRecord->Title, $stageRecord->ID);
227
				$stageRecord->delete();
228
				$stageRecord->destroy();
229
				unset($stageRecord);
230
			}
231
			/** @var SiteTree $liveRecord */
232
			$liveRecord = Versioned::get_one_by_stage(
233
				$this->orphanedSearchClass,
234
				Versioned::LIVE,
235
				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...
236
			);
237
			if($liveRecord) {
238
				$removedOrphans[$liveRecord->ID] = sprintf('Removed %s (#%d) from Live', $liveRecord->Title, $liveRecord->ID);
239
				$liveRecord->doUnpublish();
0 ignored issues
show
Documentation Bug introduced by
The method doUnpublish does not exist on object<SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
240
				$liveRecord->destroy();
241
				unset($liveRecord);
242
			}
243
		}
244
245
		return $removedOrphans;
246
	}
247
248
	protected function rebaseHolderTitle() {
249
		return sprintf('Rebased Orphans (%s)', date('d/m/Y g:ia', time()));
250
	}
251
252
	protected function rebaseOrphans($orphanIDs) {
253
		$holder = new SiteTree();
254
		$holder->ShowInMenus = 0;
255
		$holder->ShowInSearch = 0;
256
		$holder->ParentID = 0;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
257
		$holder->Title = $this->rebaseHolderTitle();
258
		$holder->write();
259
260
		$removedOrphans = array();
261
		$orphanBaseTable = DataObject::getSchema()->baseDataTable($this->orphanedSearchClass);
262
		foreach($orphanIDs as $id) {
263
			$stageRecord = Versioned::get_one_by_stage(
264
				$this->orphanedSearchClass,
265
				'Stage',
266
				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...
267
			);
268
			if($stageRecord) {
269
				$removedOrphans[$stageRecord->ID] = sprintf('Rebased %s (#%d)', $stageRecord->Title, $stageRecord->ID);
270
				$stageRecord->ParentID = $holder->ID;
271
				$stageRecord->ShowInMenus = 0;
272
				$stageRecord->ShowInSearch = 0;
273
				$stageRecord->write();
274
				$stageRecord->doUnpublish();
275
				$stageRecord->destroy();
276
				//unset($stageRecord);
277
			}
278
			$liveRecord = Versioned::get_one_by_stage(
279
				$this->orphanedSearchClass,
280
				'Live',
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($liveRecord) {
284
				$removedOrphans[$liveRecord->ID] = sprintf('Rebased %s (#%d)', $liveRecord->Title, $liveRecord->ID);
285
				$liveRecord->ParentID = $holder->ID;
286
				$liveRecord->ShowInMenus = 0;
287
				$liveRecord->ShowInSearch = 0;
288
				$liveRecord->write();
289
				if(!$stageRecord) $liveRecord->doRestoreToStage();
290
				$liveRecord->doUnpublish();
291
				$liveRecord->destroy();
292
				unset($liveRecord);
293
			}
294
			if($stageRecord) {
295
				unset($stageRecord);
296
			}
297
		}
298
299
		return $removedOrphans;
300
	}
301
302
	/**
303
	 * Gets all orphans from "Stage" and "Live" stages.
304
	 *
305
	 * @param string $class
306
	 * @param array $filter
307
	 * @param string $sort
308
	 * @param string $join
309
	 * @param int|array $limit
310
	 * @return SS_List
311
	 */
312
	public function getOrphanedPages($class = 'SiteTree', $filter = array(), $sort = null, $join = null, $limit = null) {
0 ignored issues
show
Unused Code introduced by
The parameter $join is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
313
		// Alter condition
314
		if(empty($filter)) $where = array();
315
		elseif(is_array($filter)) $where = $filter;
316
		else $where = array($filter);
317
		$where[] = array("\"$class\".\"ParentID\" != ?" => 0);
318
		$where[] = '"Parents"."ID" IS NULL';
319
320
		$orphans = new ArrayList();
321
		foreach(array(Versioned::DRAFT, Versioned::LIVE) as $stage) {
322
			$table = DataObject::getSchema()->tableName($class);
323
			$table .= ($stage == Versioned::LIVE) ? '_Live' : '';
324
			$stageOrphans = Versioned::get_by_stage(
325
				$class,
326
				$stage,
327
				$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...
328
				$sort,
329
				null,
330
				$limit
0 ignored issues
show
Bug introduced by
It seems like $limit defined by parameter $limit on line 312 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...
331
			)->leftJoin($table, "\"$table\".\"ParentID\" = \"Parents\".\"ID\"", "Parents");
332
			$orphans->merge($stageOrphans);
333
		}
334
335
		$orphans->removeDuplicates();
336
337
		return $orphans;
338
	}
339
}
340