SpecialRevisionDelete   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 653
Duplicated Lines 9.65 %

Coupling/Cohesion

Components 1
Dependencies 24

Importance

Changes 0
Metric Value
dl 63
loc 653
rs 1.2661
c 0
b 0
f 0
wmc 67
lcom 1
cbo 24

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A doesWrites() 0 3 1
F execute() 31 123 17
B showConvenienceLinks() 0 36 4
A getLogQueryCond() 0 10 1
B tryShowFile() 7 55 5
A getList() 0 9 2
C showForm() 0 105 8
A addUsageText() 0 15 3
C buildCheckBoxes() 0 66 8
C submit() 5 38 8
A success() 11 11 1
A failure() 9 9 1
B extractBitParams() 0 16 5
A save() 0 5 1
A getGroupName() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SpecialRevisionDelete often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SpecialRevisionDelete, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Implements Special:Revisiondelete
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup SpecialPage
22
 */
23
24
/**
25
 * Special page allowing users with the appropriate permissions to view
26
 * and hide revisions. Log items can also be hidden.
27
 *
28
 * @ingroup SpecialPage
29
 */
30
class SpecialRevisionDelete extends UnlistedSpecialPage {
31
	/** @var bool Was the DB modified in this request */
32
	protected $wasSaved = false;
33
34
	/** @var bool True if the submit button was clicked, and the form was posted */
35
	private $submitClicked;
36
37
	/** @var array Target ID list */
38
	private $ids;
39
40
	/** @var string Archive name, for reviewing deleted files */
41
	private $archiveName;
42
43
	/** @var string Edit token for securing image views against XSS */
44
	private $token;
45
46
	/** @var Title Title object for target parameter */
47
	private $targetObj;
48
49
	/** @var string Deletion type, may be revision, archive, oldimage, filearchive, logging. */
50
	private $typeName;
51
52
	/** @var array Array of checkbox specs (message, name, deletion bits) */
53
	private $checks;
54
55
	/** @var array UI Labels about the current type */
56
	private $typeLabels;
57
58
	/** @var RevDelList RevDelList object, storing the list of items to be deleted/undeleted */
59
	private $revDelList;
60
61
	/** @var bool Whether user is allowed to perform the action */
62
	private $mIsAllowed;
63
64
	/** @var string */
65
	private $otherReason;
66
67
	/**
68
	 * UI labels for each type.
69
	 */
70
	private static $UILabels = [
71
		'revision' => [
72
			'check-label' => 'revdelete-hide-text',
73
			'success' => 'revdelete-success',
74
			'failure' => 'revdelete-failure',
75
			'text' => 'revdelete-text-text',
76
			'selected'=> 'revdelete-selected-text',
77
		],
78
		'archive' => [
79
			'check-label' => 'revdelete-hide-text',
80
			'success' => 'revdelete-success',
81
			'failure' => 'revdelete-failure',
82
			'text' => 'revdelete-text-text',
83
			'selected'=> 'revdelete-selected-text',
84
		],
85
		'oldimage' => [
86
			'check-label' => 'revdelete-hide-image',
87
			'success' => 'revdelete-success',
88
			'failure' => 'revdelete-failure',
89
			'text' => 'revdelete-text-file',
90
			'selected'=> 'revdelete-selected-file',
91
		],
92
		'filearchive' => [
93
			'check-label' => 'revdelete-hide-image',
94
			'success' => 'revdelete-success',
95
			'failure' => 'revdelete-failure',
96
			'text' => 'revdelete-text-file',
97
			'selected'=> 'revdelete-selected-file',
98
		],
99
		'logging' => [
100
			'check-label' => 'revdelete-hide-name',
101
			'success' => 'logdelete-success',
102
			'failure' => 'logdelete-failure',
103
			'text' => 'logdelete-text',
104
			'selected' => 'logdelete-selected',
105
		],
106
	];
107
108
	public function __construct() {
109
		parent::__construct( 'Revisiondelete', 'deleterevision' );
110
	}
111
112
	public function doesWrites() {
113
		return true;
114
	}
115
116
	public function execute( $par ) {
117
		$this->useTransactionalTimeLimit();
118
119
		$this->checkPermissions();
120
		$this->checkReadOnly();
121
122
		$output = $this->getOutput();
123
		$user = $this->getUser();
124
125
		// Check blocks
126
		if ( $user->isBlocked() ) {
127
			throw new UserBlockedError( $user->getBlock() );
0 ignored issues
show
Bug introduced by
It seems like $user->getBlock() can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
128
		}
129
130
		$this->setHeaders();
131
		$this->outputHeader();
132
		$request = $this->getRequest();
133
		$this->submitClicked = $request->wasPosted() && $request->getBool( 'wpSubmit' );
134
		# Handle our many different possible input types.
135
		$ids = $request->getVal( 'ids' );
136 View Code Duplication
		if ( !is_null( $ids ) ) {
137
			# Allow CSV, for backwards compatibility, or a single ID for show/hide links
138
			$this->ids = explode( ',', $ids );
139
		} else {
140
			# Array input
141
			$this->ids = array_keys( $request->getArray( 'ids', [] ) );
142
		}
143
		// $this->ids = array_map( 'intval', $this->ids );
144
		$this->ids = array_unique( array_filter( $this->ids ) );
145
146
		$this->typeName = $request->getVal( 'type' );
147
		$this->targetObj = Title::newFromText( $request->getText( 'target' ) );
148
149
		# For reviewing deleted files...
150
		$this->archiveName = $request->getVal( 'file' );
151
		$this->token = $request->getVal( 'token' );
152
		if ( $this->archiveName && $this->targetObj ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->archiveName of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
153
			$this->tryShowFile( $this->archiveName );
154
155
			return;
156
		}
157
158
		$this->typeName = RevisionDeleter::getCanonicalTypeName( $this->typeName );
159
160
		# No targets?
161
		if ( !$this->typeName || count( $this->ids ) == 0 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->typeName of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
162
			throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
163
		}
164
165
		# Allow the list type to adjust the passed target
166
		$this->targetObj = RevisionDeleter::suggestTarget(
167
			$this->typeName,
168
			$this->targetObj,
169
			$this->ids
170
		);
171
172
		# We need a target page!
173
		if ( $this->targetObj === null ) {
174
			$output->addWikiMsg( 'undelete-header' );
175
176
			return;
177
		}
178
179
		$this->typeLabels = self::$UILabels[$this->typeName];
180
		$list = $this->getList();
181
		$list->reset();
182
		$this->mIsAllowed = $user->isAllowed( RevisionDeleter::getRestriction( $this->typeName ) );
183
		$canViewSuppressedOnly = $this->getUser()->isAllowed( 'viewsuppressed' ) &&
184
			!$this->getUser()->isAllowed( 'suppressrevision' );
185
		$pageIsSuppressed = $list->areAnySuppressed();
186
		$this->mIsAllowed = $this->mIsAllowed && !( $canViewSuppressedOnly && $pageIsSuppressed );
187
188
		$this->otherReason = $request->getVal( 'wpReason' );
189
		# Give a link to the logs/hist for this page
190
		$this->showConvenienceLinks();
191
192
		# Initialise checkboxes
193
		$this->checks = [
194
			# Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name
195
			[ $this->typeLabels['check-label'], 'wpHidePrimary',
196
				RevisionDeleter::getRevdelConstant( $this->typeName )
197
			],
198
			[ 'revdelete-hide-comment', 'wpHideComment', Revision::DELETED_COMMENT ],
199
			[ 'revdelete-hide-user', 'wpHideUser', Revision::DELETED_USER ]
200
		];
201
		if ( $user->isAllowed( 'suppressrevision' ) ) {
202
			$this->checks[] = [ 'revdelete-hide-restricted',
203
				'wpHideRestricted', Revision::DELETED_RESTRICTED ];
204
		}
205
206
		# Either submit or create our form
207
		if ( $this->mIsAllowed && $this->submitClicked ) {
208
			$this->submit( $request );
209
		} else {
210
			$this->showForm();
211
		}
212
213 View Code Duplication
		if ( $user->isAllowed( 'deletedhistory' ) ) {
214
			$qc = $this->getLogQueryCond();
215
			# Show relevant lines from the deletion log
216
			$deleteLogPage = new LogPage( 'delete' );
217
			$output->addHTML( "<h2>" . $deleteLogPage->getName()->escaped() . "</h2>\n" );
218
			LogEventsList::showLogExtract(
219
				$output,
220
				'delete',
221
				$this->targetObj,
222
				'', /* user */
223
				[ 'lim' => 25, 'conds' => $qc, 'useMaster' => $this->wasSaved ]
224
			);
225
		}
226
		# Show relevant lines from the suppression log
227 View Code Duplication
		if ( $user->isAllowed( 'suppressionlog' ) ) {
228
			$suppressLogPage = new LogPage( 'suppress' );
229
			$output->addHTML( "<h2>" . $suppressLogPage->getName()->escaped() . "</h2>\n" );
230
			LogEventsList::showLogExtract(
231
				$output,
232
				'suppress',
233
				$this->targetObj,
234
				'',
235
				[ 'lim' => 25, 'conds' => $qc, 'useMaster' => $this->wasSaved ]
0 ignored issues
show
Bug introduced by
The variable $qc 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...
236
			);
237
		}
238
	}
239
240
	/**
241
	 * Show some useful links in the subtitle
242
	 */
243
	protected function showConvenienceLinks() {
244
		# Give a link to the logs/hist for this page
245
		if ( $this->targetObj ) {
246
			// Also set header tabs to be for the target.
247
			$this->getSkin()->setRelevantTitle( $this->targetObj );
248
249
			$links = [];
250
			$links[] = Linker::linkKnown(
251
				SpecialPage::getTitleFor( 'Log' ),
252
				$this->msg( 'viewpagelogs' )->escaped(),
253
				[],
254
				[ 'page' => $this->targetObj->getPrefixedText() ]
255
			);
256
			if ( !$this->targetObj->isSpecialPage() ) {
257
				# Give a link to the page history
258
				$links[] = Linker::linkKnown(
259
					$this->targetObj,
260
					$this->msg( 'pagehist' )->escaped(),
261
					[],
262
					[ 'action' => 'history' ]
263
				);
264
				# Link to deleted edits
265
				if ( $this->getUser()->isAllowed( 'undelete' ) ) {
266
					$undelete = SpecialPage::getTitleFor( 'Undelete' );
267
					$links[] = Linker::linkKnown(
268
						$undelete,
269
						$this->msg( 'deletedhist' )->escaped(),
270
						[],
271
						[ 'target' => $this->targetObj->getPrefixedDBkey() ]
272
					);
273
				}
274
			}
275
			# Logs themselves don't have histories or archived revisions
276
			$this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) );
277
		}
278
	}
279
280
	/**
281
	 * Get the condition used for fetching log snippets
282
	 * @return array
283
	 */
284
	protected function getLogQueryCond() {
285
		$conds = [];
286
		// Revision delete logs for these item
287
		$conds['log_type'] = [ 'delete', 'suppress' ];
288
		$conds['log_action'] = $this->getList()->getLogAction();
289
		$conds['ls_field'] = RevisionDeleter::getRelationType( $this->typeName );
290
		$conds['ls_value'] = $this->ids;
291
292
		return $conds;
293
	}
294
295
	/**
296
	 * Show a deleted file version requested by the visitor.
297
	 * @todo Mostly copied from Special:Undelete. Refactor.
298
	 * @param string $archiveName
299
	 * @throws MWException
300
	 * @throws PermissionsError
301
	 */
302
	protected function tryShowFile( $archiveName ) {
303
		$repo = RepoGroup::singleton()->getLocalRepo();
304
		$oimage = $repo->newFromArchiveName( $this->targetObj, $archiveName );
305
		$oimage->load();
306
		// Check if user is allowed to see this file
307
		if ( !$oimage->exists() ) {
308
			$this->getOutput()->addWikiMsg( 'revdelete-no-file' );
309
310
			return;
311
		}
312
		$user = $this->getUser();
313 View Code Duplication
		if ( !$oimage->userCan( File::DELETED_FILE, $user ) ) {
314
			if ( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) {
315
				throw new PermissionsError( 'suppressrevision' );
316
			} else {
317
				throw new PermissionsError( 'deletedtext' );
318
			}
319
		}
320
		if ( !$user->matchEditToken( $this->token, $archiveName ) ) {
321
			$lang = $this->getLanguage();
322
			$this->getOutput()->addWikiMsg( 'revdelete-show-file-confirm',
323
				$this->targetObj->getText(),
324
				$lang->userDate( $oimage->getTimestamp(), $user ),
325
				$lang->userTime( $oimage->getTimestamp(), $user ) );
326
			$this->getOutput()->addHTML(
327
				Xml::openElement( 'form', [
328
					'method' => 'POST',
329
					'action' => $this->getPageTitle()->getLocalURL( [
330
							'target' => $this->targetObj->getPrefixedDBkey(),
331
							'file' => $archiveName,
332
							'token' => $user->getEditToken( $archiveName ),
333
						] )
334
					]
335
				) .
336
				Xml::submitButton( $this->msg( 'revdelete-show-file-submit' )->text() ) .
337
				'</form>'
338
			);
339
340
			return;
341
		}
342
		$this->getOutput()->disable();
343
		# We mustn't allow the output to be CDN cached, otherwise
344
		# if an admin previews a deleted image, and it's cached, then
345
		# a user without appropriate permissions can toddle off and
346
		# nab the image, and CDN will serve it
347
		$this->getRequest()->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
348
		$this->getRequest()->response()->header(
349
			'Cache-Control: no-cache, no-store, max-age=0, must-revalidate'
350
		);
351
		$this->getRequest()->response()->header( 'Pragma: no-cache' );
352
353
		$key = $oimage->getStorageKey();
354
		$path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key;
0 ignored issues
show
Security Bug introduced by
It seems like $key defined by $oimage->getStorageKey() on line 353 can also be of type false; however, FileRepo::getDeletedHashPath() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
355
		$repo->streamFile( $path );
356
	}
357
358
	/**
359
	 * Get the list object for this request
360
	 * @return RevDelList
361
	 */
362
	protected function getList() {
363
		if ( is_null( $this->revDelList ) ) {
364
			$this->revDelList = RevisionDeleter::createList(
365
				$this->typeName, $this->getContext(), $this->targetObj, $this->ids
366
			);
367
		}
368
369
		return $this->revDelList;
370
	}
371
372
	/**
373
	 * Show a list of items that we will operate on, and show a form with checkboxes
374
	 * which will allow the user to choose new visibility settings.
375
	 */
376
	protected function showForm() {
377
		$userAllowed = true;
378
379
		// Messages: revdelete-selected-text, revdelete-selected-file, logdelete-selected
380
		$out = $this->getOutput();
381
		$out->wrapWikiMsg( "<strong>$1</strong>", [ $this->typeLabels['selected'],
382
			$this->getLanguage()->formatNum( count( $this->ids ) ), $this->targetObj->getPrefixedText() ] );
383
384
		$this->addHelpLink( 'Help:RevisionDelete' );
385
		$out->addHTML( "<ul>" );
386
387
		$numRevisions = 0;
388
		// Live revisions...
389
		$list = $this->getList();
390
		// @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
391
		for ( $list->reset(); $list->current(); $list->next() ) {
392
			// @codingStandardsIgnoreEnd
393
			$item = $list->current();
394
395
			if ( !$item->canView() ) {
0 ignored issues
show
Bug introduced by
The method canView() does not seem to exist on object<Revision>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
396
				if ( !$this->submitClicked ) {
397
					throw new PermissionsError( 'suppressrevision' );
398
				}
399
				$userAllowed = false;
400
			}
401
402
			$numRevisions++;
403
			$out->addHTML( $item->getHTML() );
0 ignored issues
show
Bug introduced by
The method getHTML() does not seem to exist on object<Revision>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
404
		}
405
406
		if ( !$numRevisions ) {
407
			throw new ErrorPageError( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
408
		}
409
410
		$out->addHTML( "</ul>" );
411
		// Explanation text
412
		$this->addUsageText();
413
414
		// Normal sysops can always see what they did, but can't always change it
415
		if ( !$userAllowed ) {
416
			return;
417
		}
418
419
		// Show form if the user can submit
420
		if ( $this->mIsAllowed ) {
421
			$out->addModuleStyles( 'mediawiki.special' );
422
423
			$form = Xml::openElement( 'form', [ 'method' => 'post',
424
					'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ),
425
					'id' => 'mw-revdel-form-revisions' ] ) .
426
				Xml::fieldset( $this->msg( 'revdelete-legend' )->text() ) .
427
				$this->buildCheckBoxes() .
428
				Xml::openElement( 'table' ) .
429
				"<tr>\n" .
430
					'<td class="mw-label">' .
431
						Xml::label( $this->msg( 'revdelete-log' )->text(), 'wpRevDeleteReasonList' ) .
432
					'</td>' .
433
					'<td class="mw-input">' .
434
						Xml::listDropDown( 'wpRevDeleteReasonList',
435
							$this->msg( 'revdelete-reason-dropdown' )->inContentLanguage()->text(),
436
							$this->msg( 'revdelete-reasonotherlist' )->inContentLanguage()->text(),
437
							$this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' ), 'wpReasonDropDown'
438
						) .
439
					'</td>' .
440
				"</tr><tr>\n" .
441
					'<td class="mw-label">' .
442
						Xml::label( $this->msg( 'revdelete-otherreason' )->text(), 'wpReason' ) .
443
					'</td>' .
444
					'<td class="mw-input">' .
445
						Xml::input(
446
							'wpReason',
447
							60,
448
							$this->otherReason,
449
							[ 'id' => 'wpReason', 'maxlength' => 100 ]
450
						) .
451
					'</td>' .
452
				"</tr><tr>\n" .
453
					'<td></td>' .
454
					'<td class="mw-submit">' .
455
						Xml::submitButton( $this->msg( 'revdelete-submit', $numRevisions )->text(),
456
							[ 'name' => 'wpSubmit' ] ) .
457
					'</td>' .
458
				"</tr>\n" .
459
				Xml::closeElement( 'table' ) .
460
				Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) .
461
				Html::hidden( 'target', $this->targetObj->getPrefixedText() ) .
462
				Html::hidden( 'type', $this->typeName ) .
463
				Html::hidden( 'ids', implode( ',', $this->ids ) ) .
464
				Xml::closeElement( 'fieldset' ) . "\n" .
465
				Xml::closeElement( 'form' ) . "\n";
466
			// Show link to edit the dropdown reasons
467
			if ( $this->getUser()->isAllowed( 'editinterface' ) ) {
468
				$link = Linker::linkKnown(
469
					$this->msg( 'revdelete-reason-dropdown' )->inContentLanguage()->getTitle(),
470
					$this->msg( 'revdelete-edit-reasonlist' )->escaped(),
471
					[],
472
					[ 'action' => 'edit' ]
473
				);
474
				$form .= Xml::tags( 'p', [ 'class' => 'mw-revdel-editreasons' ], $link ) . "\n";
475
			}
476
		} else {
477
			$form = '';
478
		}
479
		$out->addHTML( $form );
480
	}
481
482
	/**
483
	 * Show some introductory text
484
	 * @todo FIXME: Wikimedia-specific policy text
485
	 */
486
	protected function addUsageText() {
487
		// Messages: revdelete-text-text, revdelete-text-file, logdelete-text
488
		$this->getOutput()->wrapWikiMsg(
489
			"<strong>$1</strong>\n$2", $this->typeLabels['text'],
490
			'revdelete-text-others'
491
		);
492
493
		if ( $this->getUser()->isAllowed( 'suppressrevision' ) ) {
494
			$this->getOutput()->addWikiMsg( 'revdelete-suppress-text' );
495
		}
496
497
		if ( $this->mIsAllowed ) {
498
			$this->getOutput()->addWikiMsg( 'revdelete-confirm' );
499
		}
500
	}
501
502
	/**
503
	 * @return string HTML
504
	 */
505
	protected function buildCheckBoxes() {
506
		$html = '<table>';
507
		// If there is just one item, use checkboxes
508
		$list = $this->getList();
509
		if ( $list->length() == 1 ) {
510
			$list->reset();
511
			$bitfield = $list->current()->getBits(); // existing field
0 ignored issues
show
Bug introduced by
The method getBits() does not seem to exist on object<Revision>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
512
513
			if ( $this->submitClicked ) {
514
				$bitfield = RevisionDeleter::extractBitfield( $this->extractBitParams(), $bitfield );
515
			}
516
517
			foreach ( $this->checks as $item ) {
518
				// Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name,
519
				// revdelete-hide-comment, revdelete-hide-user, revdelete-hide-restricted
520
				list( $message, $name, $field ) = $item;
521
				$innerHTML = Xml::checkLabel(
522
					$this->msg( $message )->text(),
523
					$name,
524
					$name,
525
					$bitfield & $field
526
				);
527
528
				if ( $field == Revision::DELETED_RESTRICTED ) {
529
					$innerHTML = "<b>$innerHTML</b>";
530
				}
531
532
				$line = Xml::tags( 'td', [ 'class' => 'mw-input' ], $innerHTML );
533
				$html .= "<tr>$line</tr>\n";
534
			}
535
		} else {
536
			// Otherwise, use tri-state radios
537
			$html .= '<tr>';
538
			$html .= '<th class="mw-revdel-checkbox">'
539
				. $this->msg( 'revdelete-radio-same' )->escaped() . '</th>';
540
			$html .= '<th class="mw-revdel-checkbox">'
541
				. $this->msg( 'revdelete-radio-unset' )->escaped() . '</th>';
542
			$html .= '<th class="mw-revdel-checkbox">'
543
				. $this->msg( 'revdelete-radio-set' )->escaped() . '</th>';
544
			$html .= "<th></th></tr>\n";
545
			foreach ( $this->checks as $item ) {
546
				// Messages: revdelete-hide-text, revdelete-hide-image, revdelete-hide-name,
547
				// revdelete-hide-comment, revdelete-hide-user, revdelete-hide-restricted
548
				list( $message, $name, $field ) = $item;
549
				// If there are several items, use third state by default...
550
				if ( $this->submitClicked ) {
551
					$selected = $this->getRequest()->getInt( $name, 0 /* unchecked */ );
552
				} else {
553
					$selected = -1; // use existing field
554
				}
555
				$line = '<td class="mw-revdel-checkbox">' . Xml::radio( $name, -1, $selected == -1 ) . '</td>';
556
				$line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 0, $selected == 0 ) . '</td>';
557
				$line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 1, $selected == 1 ) . '</td>';
558
				$label = $this->msg( $message )->escaped();
559
				if ( $field == Revision::DELETED_RESTRICTED ) {
560
					$label = "<b>$label</b>";
561
				}
562
				$line .= "<td>$label</td>";
563
				$html .= "<tr>$line</tr>\n";
564
			}
565
		}
566
567
		$html .= '</table>';
568
569
		return $html;
570
	}
571
572
	/**
573
	 * UI entry point for form submission.
574
	 * @throws PermissionsError
575
	 * @return bool
576
	 */
577
	protected function submit() {
578
		# Check edit token on submission
579
		$token = $this->getRequest()->getVal( 'wpEditToken' );
580 View Code Duplication
		if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) {
581
			$this->getOutput()->addWikiMsg( 'sessionfailure' );
582
583
			return false;
584
		}
585
		$bitParams = $this->extractBitParams();
586
		// from dropdown
587
		$listReason = $this->getRequest()->getText( 'wpRevDeleteReasonList', 'other' );
588
		$comment = $listReason;
589
		if ( $comment === 'other' ) {
590
			$comment = $this->otherReason;
591
		} elseif ( $this->otherReason !== '' ) {
592
			// Entry from drop down menu + additional comment
593
			$comment .= $this->msg( 'colon-separator' )->inContentLanguage()->text()
594
				. $this->otherReason;
595
		}
596
		# Can the user set this field?
597
		if ( $bitParams[Revision::DELETED_RESTRICTED] == 1
598
			&& !$this->getUser()->isAllowed( 'suppressrevision' )
599
		) {
600
			throw new PermissionsError( 'suppressrevision' );
601
		}
602
		# If the save went through, go to success message...
603
		$status = $this->save( $bitParams, $comment );
604
		if ( $status->isGood() ) {
605
			$this->success();
606
607
			return true;
608
		} else {
609
			# ...otherwise, bounce back to form...
610
			$this->failure( $status );
611
		}
612
613
		return false;
614
	}
615
616
	/**
617
	 * Report that the submit operation succeeded
618
	 */
619 View Code Duplication
	protected function success() {
620
		// Messages: revdelete-success, logdelete-success
621
		$this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) );
622
		$this->getOutput()->wrapWikiMsg(
623
			"<div class=\"successbox\">\n$1\n</div>",
624
			$this->typeLabels['success']
625
		);
626
		$this->wasSaved = true;
627
		$this->revDelList->reloadFromMaster();
628
		$this->showForm();
629
	}
630
631
	/**
632
	 * Report that the submit operation failed
633
	 * @param Status $status
634
	 */
635 View Code Duplication
	protected function failure( $status ) {
636
		// Messages: revdelete-failure, logdelete-failure
637
		$this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) );
638
		$this->getOutput()->addWikiText( '<div class="errorbox">' .
639
			$status->getWikiText( $this->typeLabels['failure'] ) .
640
			'</div>'
641
		);
642
		$this->showForm();
643
	}
644
645
	/**
646
	 * Put together an array that contains -1, 0, or the *_deleted const for each bit
647
	 *
648
	 * @return array
649
	 */
650
	protected function extractBitParams() {
651
		$bitfield = [];
652
		foreach ( $this->checks as $item ) {
653
			list( /* message */, $name, $field ) = $item;
654
			$val = $this->getRequest()->getInt( $name, 0 /* unchecked */ );
655
			if ( $val < -1 || $val > 1 ) {
656
				$val = -1; // -1 for existing value
657
			}
658
			$bitfield[$field] = $val;
659
		}
660
		if ( !isset( $bitfield[Revision::DELETED_RESTRICTED] ) ) {
661
			$bitfield[Revision::DELETED_RESTRICTED] = 0;
662
		}
663
664
		return $bitfield;
665
	}
666
667
	/**
668
	 * Do the write operations. Simple wrapper for RevDel*List::setVisibility().
669
	 * @param array $bitPars ExtractBitParams() bitfield array
670
	 * @param string $reason
671
	 * @return Status
672
	 */
673
	protected function save( array $bitPars, $reason ) {
674
		return $this->getList()->setVisibility(
675
			[ 'value' => $bitPars, 'comment' => $reason ]
676
		);
677
	}
678
679
	protected function getGroupName() {
680
		return 'pagetools';
681
	}
682
}
683