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

includes/specials/SpecialEditTags.php (1 issue)

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() );
0 ignored issues
show
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...
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;
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() );
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();
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();
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() );
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