Completed
Branch master (939199)
by
unknown
39:35
created

includes/specials/SpecialEditTags.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * This program is free software; you can redistribute it and/or modify
4
 * it under the terms of the GNU General Public License as published by
5
 * the Free Software Foundation; either version 2 of the License, or
6
 * (at your option) any later version.
7
 *
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
 * GNU General Public License for more details.
12
 *
13
 * You should have received a copy of the GNU General Public License along
14
 * with this program; if not, write to the Free Software Foundation, Inc.,
15
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
 * http://www.gnu.org/copyleft/gpl.html
17
 *
18
 * @file
19
 * @ingroup SpecialPage
20
 */
21
22
/**
23
 * Special page for adding and removing change tags to individual revisions.
24
 * A lot of this is copied out of SpecialRevisiondelete.
25
 *
26
 * @ingroup SpecialPage
27
 * @since 1.25
28
 */
29
class SpecialEditTags extends UnlistedSpecialPage {
30
	/** @var bool Was the DB modified in this request */
31
	protected $wasSaved = false;
32
33
	/** @var bool True if the submit button was clicked, and the form was posted */
34
	private $submitClicked;
35
36
	/** @var array Target ID list */
37
	private $ids;
38
39
	/** @var Title Title object for target parameter */
40
	private $targetObj;
41
42
	/** @var string Deletion type, may be revision or logentry */
43
	private $typeName;
44
45
	/** @var ChangeTagsList Storing the list of items to be tagged */
46
	private $revList;
47
48
	/** @var bool Whether user is allowed to perform the action */
49
	private $isAllowed;
50
51
	/** @var string */
52
	private $reason;
53
54
	public function __construct() {
55
		parent::__construct( 'EditTags', 'changetags' );
56
	}
57
58
	public function doesWrites() {
59
		return true;
60
	}
61
62
	public function execute( $par ) {
63
		$this->checkPermissions();
64
		$this->checkReadOnly();
65
66
		$output = $this->getOutput();
67
		$user = $this->getUser();
68
		$request = $this->getRequest();
69
70
		// Check blocks
71
		if ( $user->isBlocked() ) {
72
			throw new UserBlockedError( $user->getBlock() );
73
		}
74
75
		$this->setHeaders();
76
		$this->outputHeader();
77
78
		$this->getOutput()->addModules( [ 'mediawiki.special.edittags',
79
			'mediawiki.special.edittags.styles' ] );
80
81
		$this->submitClicked = $request->wasPosted() && $request->getBool( 'wpSubmit' );
82
83
		// Handle our many different possible input types
84
		$ids = $request->getVal( 'ids' );
85 View Code Duplication
		if ( !is_null( $ids ) ) {
86
			// Allow CSV from the form hidden field, or a single ID for show/hide links
87
			$this->ids = explode( ',', $ids );
88
		} else {
89
			// Array input
90
			$this->ids = array_keys( $request->getArray( 'ids', [] ) );
91
		}
92
		$this->ids = array_unique( array_filter( $this->ids ) );
93
94
		// No targets?
95
		if ( count( $this->ids ) == 0 ) {
96
			throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
97
		}
98
99
		$this->typeName = $request->getVal( 'type' );
100
		$this->targetObj = Title::newFromText( $request->getText( 'target' ) );
101
102
		// sanity check of parameter
103
		switch ( $this->typeName ) {
104
			case 'logentry':
105
			case 'logging':
106
				$this->typeName = 'logentry';
107
				break;
108
			default:
109
				$this->typeName = 'revision';
110
				break;
111
		}
112
113
		// Allow the list type to adjust the passed target
114
		// Yuck! Copied straight out of SpecialRevisiondelete, but it does exactly
115
		// what we want
116
		$this->targetObj = RevisionDeleter::suggestTarget(
117
			$this->typeName === 'revision' ? 'revision' : 'logging',
118
			$this->targetObj,
119
			$this->ids
120
		);
121
122
		$this->isAllowed = $user->isAllowed( 'changetags' );
123
124
		$this->reason = $request->getVal( 'wpReason' );
125
		// We need a target page!
126
		if ( is_null( $this->targetObj ) ) {
127
			$output->addWikiMsg( 'undelete-header' );
128
			return;
129
		}
130
		// Give a link to the logs/hist for this page
131
		$this->showConvenienceLinks();
132
133
		// Either submit or create our form
134
		if ( $this->isAllowed && $this->submitClicked ) {
135
			$this->submit();
136
		} else {
137
			$this->showForm();
138
		}
139
140
		// Show relevant lines from the tag log
141
		$tagLogPage = new LogPage( 'tag' );
142
		$output->addHTML( "<h2>" . $tagLogPage->getName()->escaped() . "</h2>\n" );
143
		LogEventsList::showLogExtract(
144
			$output,
145
			'tag',
146
			$this->targetObj,
147
			'', /* user */
148
			[ 'lim' => 25, 'conds' => [], 'useMaster' => $this->wasSaved ]
149
		);
150
	}
151
152
	/**
153
	 * Show some useful links in the subtitle
154
	 */
155
	protected function showConvenienceLinks() {
156
		// Give a link to the logs/hist for this page
157
		if ( $this->targetObj ) {
158
			// Also set header tabs to be for the target.
159
			$this->getSkin()->setRelevantTitle( $this->targetObj );
160
161
			$links = [];
162
			$links[] = Linker::linkKnown(
163
				SpecialPage::getTitleFor( 'Log' ),
164
				$this->msg( 'viewpagelogs' )->escaped(),
165
				[],
166
				[
167
					'page' => $this->targetObj->getPrefixedText(),
168
					'hide_tag_log' => '0',
169
				]
170
			);
171
			if ( !$this->targetObj->isSpecialPage() ) {
172
				// Give a link to the page history
173
				$links[] = Linker::linkKnown(
174
					$this->targetObj,
175
					$this->msg( 'pagehist' )->escaped(),
176
					[],
177
					[ 'action' => 'history' ]
178
				);
179
			}
180
			// Link to Special:Tags
181
			$links[] = Linker::linkKnown(
182
				SpecialPage::getTitleFor( 'Tags' ),
183
				$this->msg( 'tags-edit-manage-link' )->escaped()
184
			);
185
			// Logs themselves don't have histories or archived revisions
186
			$this->getOutput()->addSubtitle( $this->getLanguage()->pipeList( $links ) );
187
		}
188
	}
189
190
	/**
191
	 * Get the list object for this request
192
	 * @return ChangeTagsList
193
	 */
194
	protected function getList() {
195
		if ( is_null( $this->revList ) ) {
196
			$this->revList = ChangeTagsList::factory( $this->typeName, $this->getContext(),
197
				$this->targetObj, $this->ids );
198
		}
199
200
		return $this->revList;
201
	}
202
203
	/**
204
	 * Show a list of items that we will operate on, and show a form which allows
205
	 * the user to modify the tags applied to those items.
206
	 */
207
	protected function showForm() {
208
		$userAllowed = true;
0 ignored issues
show
$userAllowed 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...
209
210
		$out = $this->getOutput();
211
		// Messages: tags-edit-revision-selected, tags-edit-logentry-selected
212
		$out->wrapWikiMsg( "<strong>$1</strong>", [
213
			"tags-edit-{$this->typeName}-selected",
214
			$this->getLanguage()->formatNum( count( $this->ids ) ),
215
			$this->targetObj->getPrefixedText()
216
		] );
217
218
		$this->addHelpLink( 'Help:Tags' );
219
		$out->addHTML( "<ul>" );
220
221
		$numRevisions = 0;
222
		// Live revisions...
223
		$list = $this->getList();
224
		// @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
225
		for ( $list->reset(); $list->current(); $list->next() ) {
226
			// @codingStandardsIgnoreEnd
227
			$item = $list->current();
228
			$numRevisions++;
229
			$out->addHTML( $item->getHTML() );
0 ignored issues
show
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...
230
		}
231
232
		if ( !$numRevisions ) {
233
			throw new ErrorPageError( 'tags-edit-nooldid-title', 'tags-edit-nooldid-text' );
234
		}
235
236
		$out->addHTML( "</ul>" );
237
		// Explanation text
238
		$out->wrapWikiMsg( '<p>$1</p>', "tags-edit-{$this->typeName}-explanation" );
239
240
		// Show form if the user can submit
241
		if ( $this->isAllowed ) {
242
			$form = Xml::openElement( 'form', [ 'method' => 'post',
243
					'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ),
244
					'id' => 'mw-revdel-form-revisions' ] ) .
245
				Xml::fieldset( $this->msg( "tags-edit-{$this->typeName}-legend",
246
					count( $this->ids ) )->text() ) .
247
				$this->buildCheckBoxes() .
248
				Xml::openElement( 'table' ) .
249
				"<tr>\n" .
250
					'<td class="mw-label">' .
251
						Xml::label( $this->msg( 'tags-edit-reason' )->text(), 'wpReason' ) .
252
					'</td>' .
253
					'<td class="mw-input">' .
254
						Xml::input(
255
							'wpReason',
256
							60,
257
							$this->reason,
258
							[ 'id' => 'wpReason', 'maxlength' => 100 ]
259
						) .
260
					'</td>' .
261
				"</tr><tr>\n" .
262
					'<td></td>' .
263
					'<td class="mw-submit">' .
264
						Xml::submitButton( $this->msg( "tags-edit-{$this->typeName}-submit",
265
							$numRevisions )->text(), [ 'name' => 'wpSubmit' ] ) .
266
					'</td>' .
267
				"</tr>\n" .
268
				Xml::closeElement( 'table' ) .
269
				Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) .
270
				Html::hidden( 'target', $this->targetObj->getPrefixedText() ) .
271
				Html::hidden( 'type', $this->typeName ) .
272
				Html::hidden( 'ids', implode( ',', $this->ids ) ) .
273
				Xml::closeElement( 'fieldset' ) . "\n" .
274
				Xml::closeElement( 'form' ) . "\n";
275
		} else {
276
			$form = '';
277
		}
278
		$out->addHTML( $form );
279
	}
280
281
	/**
282
	 * @return string HTML
283
	 */
284
	protected function buildCheckBoxes() {
285
		// If there is just one item, provide the user with a multi-select field
286
		$list = $this->getList();
287
		$tags = [];
288
		if ( $list->length() == 1 ) {
289
			$list->reset();
290
			$tags = $list->current()->getTags();
0 ignored issues
show
The method getTags() 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...
291
			if ( $tags ) {
292
				$tags = explode( ',', $tags );
293
			} else {
294
				$tags = [];
295
			}
296
297
			$html = '<table id="mw-edittags-tags-selector">';
298
			$html .= '<tr><td>' . $this->msg( 'tags-edit-existing-tags' )->escaped() .
299
				'</td><td>';
300
			if ( $tags ) {
301
				$html .= $this->getLanguage()->commaList( array_map( 'htmlspecialchars', $tags ) );
302
			} else {
303
				$html .= $this->msg( 'tags-edit-existing-tags-none' )->parse();
304
			}
305
			$html .= '</td></tr>';
306
			$tagSelect = $this->getTagSelect( $tags, $this->msg( 'tags-edit-new-tags' )->plain() );
307
			$html .= '<tr><td>' . $tagSelect[0] . '</td><td>' . $tagSelect[1];
308
		} else {
309
			// Otherwise, use a multi-select field for adding tags, and a list of
310
			// checkboxes for removing them
311
312
			// @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
313
			for ( $list->reset(); $list->current(); $list->next() ) {
314
				// @codingStandardsIgnoreEnd
315
				$currentTags = $list->current()->getTags();
0 ignored issues
show
The method getTags() 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...
316
				if ( $currentTags ) {
317
					$tags = array_merge( $tags, explode( ',', $currentTags ) );
318
				}
319
			}
320
			$tags = array_unique( $tags );
321
322
			$html = '<table id="mw-edittags-tags-selector-multi"><tr><td>';
323
			$tagSelect = $this->getTagSelect( [], $this->msg( 'tags-edit-add' )->plain() );
324
			$html .= '<p>' . $tagSelect[0] . '</p>' . $tagSelect[1] . '</td><td>';
325
			$html .= Xml::element( 'p', null, $this->msg( 'tags-edit-remove' )->plain() );
326
			$html .= Xml::checkLabel( $this->msg( 'tags-edit-remove-all-tags' )->plain(),
327
				'wpRemoveAllTags', 'mw-edittags-remove-all' );
328
			$i = 0; // used for generating checkbox IDs only
329
			foreach ( $tags as $tag ) {
330
				$html .= Xml::element( 'br' ) . "\n" . Xml::checkLabel( $tag,
331
					'wpTagsToRemove[]', 'mw-edittags-remove-' . $i++, false, [
332
						'value' => $tag,
333
						'class' => 'mw-edittags-remove-checkbox',
334
					] );
335
			}
336
		}
337
338
		// also output the tags currently applied as a hidden form field, so we
339
		// know what to remove from the revision/log entry when the form is submitted
340
		$html .= Html::hidden( 'wpExistingTags', implode( ',', $tags ) );
341
		$html .= '</td></tr></table>';
342
343
		return $html;
344
	}
345
346
	/**
347
	 * Returns a <select multiple> element with a list of change tags that can be
348
	 * applied by users.
349
	 *
350
	 * @param array $selectedTags The tags that should be preselected in the
351
	 * list. Any tags in this list, but not in the list returned by
352
	 * ChangeTags::listExplicitlyDefinedTags, will be appended to the <select>
353
	 * element.
354
	 * @param string $label The text of a <label> to precede the <select>
355
	 * @return array HTML <label> element at index 0, HTML <select> element at
356
	 * index 1
357
	 */
358
	protected function getTagSelect( $selectedTags, $label ) {
359
		$result = [];
360
		$result[0] = Xml::label( $label, 'mw-edittags-tag-list' );
361
362
		$select = new XmlSelect( 'wpTagList[]', 'mw-edittags-tag-list', $selectedTags );
363
		$select->setAttribute( 'multiple', 'multiple' );
364
		$select->setAttribute( 'size', '8' );
365
366
		$tags = ChangeTags::listExplicitlyDefinedTags();
367
		$tags = array_unique( array_merge( $tags, $selectedTags ) );
368
369
		// Values of $tags are also used as <option> labels
370
		$select->addOptions( array_combine( $tags, $tags ) );
371
372
		$result[1] = $select->getHTML();
373
		return $result;
374
	}
375
376
	/**
377
	 * UI entry point for form submission.
378
	 * @throws PermissionsError
379
	 * @return bool
380
	 */
381
	protected function submit() {
382
		// Check edit token on submission
383
		$request = $this->getRequest();
384
		$token = $request->getVal( 'wpEditToken' );
385 View Code Duplication
		if ( $this->submitClicked && !$this->getUser()->matchEditToken( $token ) ) {
386
			$this->getOutput()->addWikiMsg( 'sessionfailure' );
387
			return false;
388
		}
389
390
		// Evaluate incoming request data
391
		$tagList = $request->getArray( 'wpTagList' );
392
		if ( is_null( $tagList ) ) {
393
			$tagList = [];
394
		}
395
		$existingTags = $request->getVal( 'wpExistingTags' );
396
		if ( is_null( $existingTags ) || $existingTags === '' ) {
397
			$existingTags = [];
398
		} else {
399
			$existingTags = explode( ',', $existingTags );
400
		}
401
402
		if ( count( $this->ids ) > 1 ) {
403
			// multiple revisions selected
404
			$tagsToAdd = $tagList;
405
			if ( $request->getBool( 'wpRemoveAllTags' ) ) {
406
				$tagsToRemove = $existingTags;
407
			} else {
408
				$tagsToRemove = $request->getArray( 'wpTagsToRemove' );
409
			}
410
		} else {
411
			// single revision selected
412
			// The user tells us which tags they want associated to the revision.
413
			// We have to figure out which ones to add, and which to remove.
414
			$tagsToAdd = array_diff( $tagList, $existingTags );
415
			$tagsToRemove = array_diff( $existingTags, $tagList );
416
		}
417
418
		if ( !$tagsToAdd && !$tagsToRemove ) {
419
			$status = Status::newFatal( 'tags-edit-none-selected' );
420
		} else {
421
			$status = $this->getList()->updateChangeTagsOnAll( $tagsToAdd,
422
				$tagsToRemove, null, $this->reason, $this->getUser() );
0 ignored issues
show
It seems like $tagsToRemove defined by $request->getArray('wpTagsToRemove') on line 408 can also be of type null; however, ChangeTagsList::updateChangeTagsOnAll() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
423
		}
424
425
		if ( $status->isGood() ) {
426
			$this->success();
427
			return true;
428
		} else {
429
			$this->failure( $status );
430
			return false;
431
		}
432
	}
433
434
	/**
435
	 * Report that the submit operation succeeded
436
	 */
437 View Code Duplication
	protected function success() {
438
		$this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) );
439
		$this->getOutput()->wrapWikiMsg( "<div class=\"successbox\">\n$1\n</div>",
440
			'tags-edit-success' );
441
		$this->wasSaved = true;
442
		$this->revList->reloadFromMaster();
443
		$this->reason = ''; // no need to spew the reason back at the user
444
		$this->showForm();
445
	}
446
447
	/**
448
	 * Report that the submit operation failed
449
	 * @param Status $status
450
	 */
451 View Code Duplication
	protected function failure( $status ) {
452
		$this->getOutput()->setPageTitle( $this->msg( 'actionfailed' ) );
453
		$this->getOutput()->addWikiText( '<div class="errorbox">' .
454
			$status->getWikiText( 'tags-edit-failure' ) .
455
			'</div>'
456
		);
457
		$this->showForm();
458
	}
459
460
	public function getDescription() {
461
		return $this->msg( 'tags-edit-title' )->text();
462
	}
463
464
	protected function getGroupName() {
465
		return 'pagetools';
466
	}
467
}
468