Completed
Pull Request — master (#1458)
by Sam
04:45 queued 02:13
created

RemoveOrphanedPagesTask::rebaseHolderTitle()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
/**
3
 * Identify "orphaned" pages which point to a parent
4
 * that no longer exists in a specific stage.
5
 * Shows the pages to an administrator, who can then
6
 * decide which pages to remove by ticking a checkbox
7
 * and manually executing the removal.
8
 *
9
 * Caution: Pages also count as orphans if they don't
10
 * have parents in this stage, even if the parent has a representation
11
 * in the other stage:
12
 * - A live child is orphaned if its parent was deleted from live, but still exists on stage
13
 * - A stage child is orphaned if its parent was deleted from stage, but still exists on live
14
 *
15
 * See {@link RemoveOrphanedPagesTaskTest} for an example sitetree
16
 * before and after orphan removal.
17
 *
18
 * @author Ingo Schommer (<firstname>@silverstripe.com), SilverStripe Ltd.
19
 *
20
 * @package cms
21
 * @subpackage tasks
22
 */
23
//class RemoveOrphanedPagesTask extends BuildTask {
24
class RemoveOrphanedPagesTask extends Controller {
25
26
	private 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
	public function Link() {
52
		return $this->class;
53
	}
54
55
	public function init() {
56
		parent::init();
57
58
		if(!Permission::check('ADMIN')) {
59
			return Security::permissionFailure($this);
60
		}
61
	}
62
63
	public function index() {
64
		Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/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
	public function Form() {
72
		$fields = new FieldList();
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
			$orphanBaseClass = ClassInfo::baseDataClass($this->orphanedSearchClass);
89
			$stageRecord = Versioned::get_one_by_stage(
0 ignored issues
show
Unused Code introduced by
$stageRecord is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
90
				$this->orphanedSearchClass,
91
				'Stage',
92
				array("\"$orphanBaseClass\".\"ID\"" => $orphan->ID)
93
			);
94
			$liveRecord = Versioned::get_one_by_stage(
95
				$this->orphanedSearchClass,
96
				'Live',
97
				array("\"$orphanBaseClass\".\"ID\"" => $orphan->ID)
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
				Date::create($orphan->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));
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) {
178
		// @todo Merge with BuildTask functionality
179
	}
180
181
	public function doSubmit($data, $form) {
182
		set_time_limit(60*10); // 10 minutes
183
184
		if(!isset($data['OrphanIDs']) || !isset($data['OrphanOperation'])) return false;
185
186
		switch($data['OrphanOperation']) {
187
			case 'remove':
188
				$successIDs = $this->removeOrphans($data['OrphanIDs']);
189
				break;
190
			case 'rebase':
191
				$successIDs = $this->rebaseOrphans($data['OrphanIDs']);
192
				break;
193
			default:
194
				user_error(sprintf("Unknown operation: '%s'", $data['OrphanOperation']), E_USER_ERROR);
195
		}
196
197
		$content = '';
198
		if($successIDs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $successIDs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
199
			$content .= "<ul>";
200
			foreach($successIDs as $id => $label) {
0 ignored issues
show
Bug introduced by
The variable $successIDs does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
201
				$content .= sprintf('<li>%s</li>', $label);
202
			}
203
			$content .= "</ul>";
204
		} else {
205
			$content = _t('RemoveOrphanedPagesTask.NONEREMOVED', 'None removed');
206
		}
207
208
		return $this->customise(array(
209
			'Content' => $content,
210
			'Form' => ' '
211
		))->renderWith('BlankPage');
212
	}
213
214
	protected function removeOrphans($orphanIDs) {
215
		$removedOrphans = array();
216
		$orphanBaseClass = ClassInfo::baseDataClass($this->orphanedSearchClass);
217
		foreach($orphanIDs as $id) {
218
			/** @var SiteTree $stageRecord */
219
			$stageRecord = Versioned::get_one_by_stage(
220
				$this->orphanedSearchClass,
221
				'Stage',
222
				array("\"$orphanBaseClass\".\"ID\"" => $id)
223
			);
224
			if($stageRecord) {
225
				$removedOrphans[$stageRecord->ID] = sprintf('Removed %s (#%d) from Stage', $stageRecord->Title, $stageRecord->ID);
226
				$stageRecord->delete();
227
				$stageRecord->destroy();
228
				unset($stageRecord);
229
			}
230
			/** @var SiteTree $liveRecord */
231
			$liveRecord = Versioned::get_one_by_stage(
232
				$this->orphanedSearchClass,
233
				'Live',
234
				array("\"$orphanBaseClass\".\"ID\"" => $id)
235
			);
236
			if($liveRecord) {
237
				$removedOrphans[$liveRecord->ID] = sprintf('Removed %s (#%d) from Live', $liveRecord->Title, $liveRecord->ID);
238
				$liveRecord->doUnpublish();
239
				$liveRecord->destroy();
240
				unset($liveRecord);
241
			}
242
		}
243
244
		return $removedOrphans;
245
	}
246
247
	protected function rebaseHolderTitle() {
248
		return sprintf('Rebased Orphans (%s)', date('d/m/Y g:ia', time()));
249
	}
250
251
	protected function rebaseOrphans($orphanIDs) {
252
		$holder = new SiteTree();
253
		$holder->ShowInMenus = 0;
254
		$holder->ShowInSearch = 0;
255
		$holder->ParentID = 0;
256
		$holder->Title = $this->rebaseHolderTitle();
257
		$holder->write();
258
259
		$removedOrphans = array();
260
		$orphanBaseClass = ClassInfo::baseDataClass($this->orphanedSearchClass);
261
		foreach($orphanIDs as $id) {
262
			$stageRecord = Versioned::get_one_by_stage(
263
				$this->orphanedSearchClass,
264
				'Stage',
265
				array("\"$orphanBaseClass\".\"ID\"" => $id)
266
			);
267
			if($stageRecord) {
268
				$removedOrphans[$stageRecord->ID] = sprintf('Rebased %s (#%d)', $stageRecord->Title, $stageRecord->ID);
269
				$stageRecord->ParentID = $holder->ID;
270
				$stageRecord->ShowInMenus = 0;
271
				$stageRecord->ShowInSearch = 0;
272
				$stageRecord->write();
273
				$stageRecord->doUnpublish();
274
				$stageRecord->destroy();
275
				//unset($stageRecord);
276
			}
277
			$liveRecord = Versioned::get_one_by_stage(
278
				$this->orphanedSearchClass,
279
				'Live',
280
				array("\"$orphanBaseClass\".\"ID\"" => $id)
281
			);
282
			if($liveRecord) {
283
				$removedOrphans[$liveRecord->ID] = sprintf('Rebased %s (#%d)', $liveRecord->Title, $liveRecord->ID);
284
				$liveRecord->ParentID = $holder->ID;
285
				$liveRecord->ShowInMenus = 0;
286
				$liveRecord->ShowInSearch = 0;
287
				$liveRecord->write();
288
				if(!$stageRecord) $liveRecord->doRestoreToStage();
289
				$liveRecord->doUnpublish();
290
				$liveRecord->destroy();
291
				unset($liveRecord);
292
			}
293
			if($stageRecord) {
294
				unset($stageRecord);
295
			}
296
		}
297
298
		return $removedOrphans;
299
	}
300
301
	/**
302
	 * Gets all orphans from "Stage" and "Live" stages.
303
	 *
304
	 * @param string $class
305
	 * @param array $filter
306
	 * @param string $sort
307
	 * @param string $join
308
	 * @param int|array $limit
309
	 * @return SS_List
310
	 */
311
	public function getOrphanedPages($class = 'SiteTree', $filter = array(), $sort = null, $join = null, $limit = null) {
312
		// Alter condition
313
		if(empty($filter)) $where = array();
314
		elseif(is_array($filter)) $where = $filter;
315
		else $where = array($filter);
316
		$where[] = array("\"$class\".\"ParentID\" != ?" => 0);
317
		$where[] = '"Parents"."ID" IS NULL';
318
319
		$orphans = new ArrayList();
320
		foreach(array(Versioned::DRAFT, Versioned::LIVE) as $stage) {
321
			$joinByStage = $join;
0 ignored issues
show
Unused Code introduced by
$joinByStage is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
322
			$table = $class;
323
			$table .= ($stage == 'Live') ? '_Live' : '';
324
			$stageOrphans = Versioned::get_by_stage(
325
				$class,
326
				$stage,
327
				$where,
328
				$sort,
329
				null,
330
				$limit
331
			)->leftJoin($table, "\"$table\".\"ParentID\" = \"Parents\".\"ID\"", "Parents");
332
			$orphans->merge($stageOrphans);
333
		}
334
335
		$orphans->removeDuplicates();
336
337
		return $orphans;
338
	}
339
}
340