Completed
Pull Request — master (#1534)
by Damian
03:19
created

RemoveOrphanedPagesTask   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 311
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 37
c 3
b 0
f 0
lcom 1
cbo 13
dl 0
loc 311
rs 8.6

10 Methods

Rating   Name   Duplication   Size   Complexity  
A Link() 0 3 1
A init() 0 7 2
A index() 0 7 1
D Form() 0 100 9
A run() 0 3 1
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;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Versioned.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
4
use SilverStripe\ORM\DataObject;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, DataObject.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
5
use SilverStripe\ORM\ArrayList;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, ArrayList.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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