Completed
Pull Request — master (#1536)
by Damian
02:49
created

RemoveOrphanedPagesTask::Link()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
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
use SilverStripe\Security\Permission;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Permission.

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...
7
use SilverStripe\Security\Security;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Security.

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...
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 Link() {
61
		return $this->class;
62
	}
63
64
	public function init() {
65
		parent::init();
66
67
		if(!Permission::check('ADMIN')) {
68
			return Security::permissionFailure($this);
69
		}
70
	}
71
72
	public function index() {
73
		Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js');
74
		Requirements::customCSS('#OrphanIDs .middleColumn {width: auto;}');
75
		Requirements::customCSS('#OrphanIDs label {display: inline;}');
76
77
		return $this->renderWith('BlankPage');
78
	}
79
80
	public function Form() {
81
		$fields = new FieldList();
82
		$source = array();
83
84
		$fields->push(new HeaderField(
85
			'Header',
86
			_t('RemoveOrphanedPagesTask.HEADER', 'Remove all orphaned pages task')
87
		));
88
		$fields->push(new LiteralField(
89
			'Description',
90
			$this->description
91
		));
92
93
		$orphans = $this->getOrphanedPages($this->orphanedSearchClass);
94
		if($orphans) foreach($orphans as $orphan) {
95
			$latestVersion = Versioned::get_latest_version($this->orphanedSearchClass, $orphan->ID);
96
			$latestAuthor = DataObject::get_by_id('SilverStripe\\Security\\Member', $latestVersion->AuthorID);
97
			$orphanBaseTable = DataObject::getSchema()->baseDataTable($this->orphanedSearchClass);
98
			$liveRecord = Versioned::get_one_by_stage(
99
				$this->orphanedSearchClass,
100
				'Live',
101
				array("\"$orphanBaseTable\".\"ID\"" => $orphan->ID)
102
			);
103
			$label = sprintf(
104
				'<a href="admin/pages/edit/show/%d">%s</a> <small>(#%d, Last Modified Date: %s, Last Modifier: %s, %s)</small>',
105
				$orphan->ID,
106
				$orphan->Title,
107
				$orphan->ID,
108
				$orphan->dbObject('LastEdited')->Nice(),
109
				($latestAuthor) ? $latestAuthor->Title : 'unknown',
110
				($liveRecord) ? 'is published' : 'not published'
111
			);
112
			$source[$orphan->ID] = $label;
113
		}
114
115
		if($orphans && $orphans->Count()) {
116
			$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...
117
			$fields->push(new LiteralField(
118
				'SelectAllLiteral',
119
				sprintf(
120
					'<p><a href="#" onclick="javascript:jQuery(\'#Form_Form_OrphanIDs :checkbox\').attr(\'checked\', \'checked\'); return false;">%s</a>&nbsp;',
121
					_t('RemoveOrphanedPagesTask.SELECTALL', 'select all')
122
				)
123
			));
124
			$fields->push(new LiteralField(
125
				'UnselectAllLiteral',
126
				sprintf(
127
					'<a href="#" onclick="javascript:jQuery(\'#Form_Form_OrphanIDs :checkbox\').attr(\'checked\', \'\'); return false;">%s</a></p>',
128
					_t('RemoveOrphanedPagesTask.UNSELECTALL', 'unselect all')
129
				)
130
			));
131
			$fields->push(new OptionsetField(
132
				'OrphanOperation',
133
				_t('RemoveOrphanedPagesTask.CHOOSEOPERATION', 'Choose operation:'),
134
				array(
135
					'rebase' => _t(
136
						'RemoveOrphanedPagesTask.OPERATION_REBASE',
137
						sprintf(
138
							'Rebase selected to a new holder page "%s" and unpublish. None of these pages will show up for website visitors.',
139
							$this->rebaseHolderTitle()
140
						)
141
					),
142
					'remove' => _t('RemoveOrphanedPagesTask.OPERATION_REMOVE', 'Remove selected from all stages (WARNING: Will destroy all selected pages from both stage and live)'),
143
				),
144
				'rebase'
145
			));
146
			$fields->push(new LiteralField(
147
				'Warning',
148
				sprintf('<p class="message">%s</p>',
149
					_t(
150
						'RemoveOrphanedPagesTask.DELETEWARNING',
151
						'Warning: These operations are not reversible. Please handle with care.'
152
					)
153
				)
154
			));
155
		} else {
156
			$fields->push(new LiteralField(
157
				'NotFoundLabel',
158
				sprintf(
159
					'<p class="message">%s</p>',
160
					_t('RemoveOrphanedPagesTask.NONEFOUND', 'No orphans found')
161
				)
162
			));
163
		}
164
165
		$form = new Form(
166
			$this,
167
			'Form',
168
			$fields,
169
			new FieldList(
170
				new FormAction('doSubmit', _t('RemoveOrphanedPagesTask.BUTTONRUN', 'Run'))
171
			)
172
		);
173
174
		if(!$orphans || !$orphans->Count()) {
175
			$form->makeReadonly();
176
		}
177
178
		return $form;
179
	}
180
181
	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...
182
		// @todo Merge with BuildTask functionality
183
	}
184
185
	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...
186
		set_time_limit(60*10); // 10 minutes
187
188
		if(!isset($data['OrphanIDs']) || !isset($data['OrphanOperation'])) return false;
189
190
		$successIDs = null;
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
		$orphanBaseTable = DataObject::getSchema()->baseDataTable($this->orphanedSearchClass);
222
		foreach($orphanIDs as $id) {
223
			/** @var SiteTree $stageRecord */
224
			$stageRecord = Versioned::get_one_by_stage(
225
				$this->orphanedSearchClass,
226
				Versioned::DRAFT,
227
				array("\"$orphanBaseTable\".\"ID\"" => $id)
228
			);
229
			if($stageRecord) {
230
				$removedOrphans[$stageRecord->ID] = sprintf('Removed %s (#%d) from Stage', $stageRecord->Title, $stageRecord->ID);
231
				$stageRecord->delete();
232
				$stageRecord->destroy();
233
				unset($stageRecord);
234
			}
235
			/** @var SiteTree $liveRecord */
236
			$liveRecord = Versioned::get_one_by_stage(
237
				$this->orphanedSearchClass,
238
				Versioned::LIVE,
239
				array("\"$orphanBaseTable\".\"ID\"" => $id)
240
			);
241
			if($liveRecord) {
242
				$removedOrphans[$liveRecord->ID] = sprintf('Removed %s (#%d) from Live', $liveRecord->Title, $liveRecord->ID);
243
				$liveRecord->doUnpublish();
244
				$liveRecord->destroy();
245
				unset($liveRecord);
246
			}
247
		}
248
249
		return $removedOrphans;
250
	}
251
252
	protected function rebaseHolderTitle() {
253
		return sprintf('Rebased Orphans (%s)', date('d/m/Y g:ia', time()));
254
	}
255
256
	protected function rebaseOrphans($orphanIDs) {
257
		$holder = new SiteTree();
258
		$holder->ShowInMenus = 0;
259
		$holder->ShowInSearch = 0;
260
		$holder->ParentID = 0;
261
		$holder->Title = $this->rebaseHolderTitle();
262
		$holder->write();
263
264
		$removedOrphans = array();
265
		$orphanBaseTable = DataObject::getSchema()->baseDataTable($this->orphanedSearchClass);
266
		foreach($orphanIDs as $id) {
267
			$stageRecord = Versioned::get_one_by_stage(
268
				$this->orphanedSearchClass,
269
				'Stage',
270
				array("\"$orphanBaseTable\".\"ID\"" => $id)
271
			);
272
			if($stageRecord) {
273
				$removedOrphans[$stageRecord->ID] = sprintf('Rebased %s (#%d)', $stageRecord->Title, $stageRecord->ID);
274
				$stageRecord->ParentID = $holder->ID;
275
				$stageRecord->ShowInMenus = 0;
276
				$stageRecord->ShowInSearch = 0;
277
				$stageRecord->write();
278
				$stageRecord->doUnpublish();
279
				$stageRecord->destroy();
280
				//unset($stageRecord);
281
			}
282
			$liveRecord = Versioned::get_one_by_stage(
283
				$this->orphanedSearchClass,
284
				'Live',
285
				array("\"$orphanBaseTable\".\"ID\"" => $id)
286
			);
287
			if($liveRecord) {
288
				$removedOrphans[$liveRecord->ID] = sprintf('Rebased %s (#%d)', $liveRecord->Title, $liveRecord->ID);
289
				$liveRecord->ParentID = $holder->ID;
290
				$liveRecord->ShowInMenus = 0;
291
				$liveRecord->ShowInSearch = 0;
292
				$liveRecord->write();
293
				if(!$stageRecord) $liveRecord->doRestoreToStage();
294
				$liveRecord->doUnpublish();
295
				$liveRecord->destroy();
296
				unset($liveRecord);
297
			}
298
			if($stageRecord) {
299
				unset($stageRecord);
300
			}
301
		}
302
303
		return $removedOrphans;
304
	}
305
306
	/**
307
	 * Gets all orphans from "Stage" and "Live" stages.
308
	 *
309
	 * @param string $class
310
	 * @param array $filter
311
	 * @param string $sort
312
	 * @param string $join
313
	 * @param int|array $limit
314
	 * @return SS_List
315
	 */
316
	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...
317
		// Alter condition
318
		if(empty($filter)) $where = array();
319
		elseif(is_array($filter)) $where = $filter;
320
		else $where = array($filter);
321
		$where[] = array("\"$class\".\"ParentID\" != ?" => 0);
322
		$where[] = '"Parents"."ID" IS NULL';
323
324
		$orphans = new ArrayList();
325
		foreach(array(Versioned::DRAFT, Versioned::LIVE) as $stage) {
326
			$table = DataObject::getSchema()->tableName($class);
327
			$table .= ($stage == Versioned::LIVE) ? '_Live' : '';
328
			$stageOrphans = Versioned::get_by_stage(
329
				$class,
330
				$stage,
331
				$where,
332
				$sort,
333
				null,
334
				$limit
335
			)->leftJoin($table, "\"$table\".\"ParentID\" = \"Parents\".\"ID\"", "Parents");
336
			$orphans->merge($stageOrphans);
337
		}
338
339
		$orphans->removeDuplicates();
340
341
		return $orphans;
342
	}
343
}
344