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

includes/specials/SpecialTags.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
 * Implements Special:Tags
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
 * A special page that lists tags for edits
26
 *
27
 * @ingroup SpecialPage
28
 */
29
class SpecialTags extends SpecialPage {
30
31
	/**
32
	 * @var array List of explicitly defined tags
33
	 */
34
	protected $explicitlyDefinedTags;
35
36
	/**
37
	 * @var array List of software defined tags
38
	 */
39
	protected $softwareDefinedTags;
40
41
	/**
42
	 * @var array List of software activated tags
43
	 */
44
	protected $softwareActivatedTags;
45
46
	function __construct() {
47
		parent::__construct( 'Tags' );
48
	}
49
50
	function execute( $par ) {
51
		$this->setHeaders();
52
		$this->outputHeader();
53
54
		$request = $this->getRequest();
55
		switch ( $par ) {
56
			case 'delete':
57
				$this->showDeleteTagForm( $request->getVal( 'tag' ) );
58
				break;
59
			case 'activate':
60
				$this->showActivateDeactivateForm( $request->getVal( 'tag' ), true );
61
				break;
62
			case 'deactivate':
63
				$this->showActivateDeactivateForm( $request->getVal( 'tag' ), false );
64
				break;
65
			case 'create':
66
				// fall through, thanks to HTMLForm's logic
67
			default:
68
				$this->showTagList();
69
				break;
70
		}
71
	}
72
73
	function showTagList() {
74
		$out = $this->getOutput();
75
		$out->setPageTitle( $this->msg( 'tags-title' ) );
76
		$out->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>", 'tags-intro' );
77
78
		$user = $this->getUser();
79
		$userCanManage = $user->isAllowed( 'managechangetags' );
80
		$userCanDelete = $user->isAllowed( 'deletechangetags' );
81
		$userCanEditInterface = $user->isAllowed( 'editinterface' );
82
83
		// Show form to create a tag
84
		if ( $userCanManage ) {
85
			$fields = [
86
				'Tag' => [
87
					'type' => 'text',
88
					'label' => $this->msg( 'tags-create-tag-name' )->plain(),
89
					'required' => true,
90
				],
91
				'Reason' => [
92
					'type' => 'text',
93
					'label' => $this->msg( 'tags-create-reason' )->plain(),
94
					'size' => 50,
95
				],
96
				'IgnoreWarnings' => [
97
					'type' => 'hidden',
98
				],
99
			];
100
101
			$form = new HTMLForm( $fields, $this->getContext() );
102
			$form->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
103
			$form->setWrapperLegendMsg( 'tags-create-heading' );
104
			$form->setHeaderText( $this->msg( 'tags-create-explanation' )->parseAsBlock() );
105
			$form->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
106
			$form->setSubmitTextMsg( 'tags-create-submit' );
107
			$form->show();
108
109
			// If processCreateTagForm generated a redirect, there's no point
110
			// continuing with this, as the user is just going to end up getting sent
111
			// somewhere else. Additionally, if we keep going here, we end up
112
			// populating the memcache of tag data (see ChangeTags::listDefinedTags)
113
			// with out-of-date data from the replica DB, because the replica DB hasn't caught
114
			// up to the fact that a new tag has been created as part of an implicit,
115
			// as yet uncommitted transaction on master.
116
			if ( $out->getRedirect() !== '' ) {
117
				return;
118
			}
119
		}
120
121
		// Used to get hitcounts for #doTagRow()
122
		$tagStats = ChangeTags::tagUsageStatistics();
123
124
		// Used in #doTagRow()
125
		$this->explicitlyDefinedTags = array_fill_keys(
126
			ChangeTags::listExplicitlyDefinedTags(), true );
127
		$this->softwareDefinedTags = array_fill_keys(
128
			ChangeTags::listSoftwareDefinedTags(), true );
129
130
		// List all defined tags, even if they were never applied
131
		$definedTags = array_keys( $this->explicitlyDefinedTags + $this->softwareDefinedTags );
132
133
		// Show header only if there exists atleast one tag
134
		if ( !$tagStats && !$definedTags ) {
135
			return;
136
		}
137
138
		// Write the headers
139
		$html = Xml::tags( 'tr', null, Xml::tags( 'th', null, $this->msg( 'tags-tag' )->parse() ) .
140
			Xml::tags( 'th', null, $this->msg( 'tags-display-header' )->parse() ) .
141
			Xml::tags( 'th', null, $this->msg( 'tags-description-header' )->parse() ) .
142
			Xml::tags( 'th', null, $this->msg( 'tags-source-header' )->parse() ) .
143
			Xml::tags( 'th', null, $this->msg( 'tags-active-header' )->parse() ) .
144
			Xml::tags( 'th', null, $this->msg( 'tags-hitcount-header' )->parse() ) .
145
			( ( $userCanManage || $userCanDelete ) ?
146
				Xml::tags( 'th', [ 'class' => 'unsortable' ],
147
					$this->msg( 'tags-actions-header' )->parse() ) :
148
				'' )
149
		);
150
151
		// Used in #doTagRow()
152
		$this->softwareActivatedTags = array_fill_keys(
153
			ChangeTags::listSoftwareActivatedTags(), true );
154
155
		// Insert tags that have been applied at least once
156
		foreach ( $tagStats as $tag => $hitcount ) {
157
			$html .= $this->doTagRow( $tag, $hitcount, $userCanManage,
158
				$userCanDelete, $userCanEditInterface );
159
		}
160
		// Insert tags defined somewhere but never applied
161
		foreach ( $definedTags as $tag ) {
162
			if ( !isset( $tagStats[$tag] ) ) {
163
				$html .= $this->doTagRow( $tag, 0, $userCanManage, $userCanDelete, $userCanEditInterface );
164
			}
165
		}
166
167
		$out->addHTML( Xml::tags(
168
			'table',
169
			[ 'class' => 'mw-datatable sortable mw-tags-table' ],
170
			$html
171
		) );
172
	}
173
174
	function doTagRow( $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks ) {
175
		$newRow = '';
176
		$newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) );
177
178
		$linkRenderer = $this->getLinkRenderer();
179
		$disp = ChangeTags::tagDescription( $tag, $this->getContext() );
180 View Code Duplication
		if ( $showEditLinks ) {
181
			$disp .= ' ';
182
			$editLink = $linkRenderer->makeLink(
183
				$this->msg( "tag-$tag" )->inContentLanguage()->getTitle(),
184
				$this->msg( 'tags-edit' )->text()
185
			);
186
			$disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped();
187
		}
188
		$newRow .= Xml::tags( 'td', null, $disp );
0 ignored issues
show
It seems like $disp defined by \ChangeTags::tagDescript...g, $this->getContext()) on line 179 can also be of type false; however, Xml::tags() 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...
189
190
		$msg = $this->msg( "tag-$tag-description" );
191
		$desc = !$msg->exists() ? '' : $msg->parse();
192 View Code Duplication
		if ( $showEditLinks ) {
193
			$desc .= ' ';
194
			$editDescLink = $linkRenderer->makeLink(
195
				$this->msg( "tag-$tag-description" )->inContentLanguage()->getTitle(),
196
				$this->msg( 'tags-edit' )->text()
197
			);
198
			$desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped();
199
		}
200
		$newRow .= Xml::tags( 'td', null, $desc );
201
202
		$sourceMsgs = [];
203
		$isSoftware = isset( $this->softwareDefinedTags[$tag] );
204
		$isExplicit = isset( $this->explicitlyDefinedTags[$tag] );
205
		if ( $isSoftware ) {
206
			// TODO: Rename this message
207
			$sourceMsgs[] = $this->msg( 'tags-source-extension' )->escaped();
208
		}
209
		if ( $isExplicit ) {
210
			$sourceMsgs[] = $this->msg( 'tags-source-manual' )->escaped();
211
		}
212
		if ( !$sourceMsgs ) {
213
			$sourceMsgs[] = $this->msg( 'tags-source-none' )->escaped();
214
		}
215
		$newRow .= Xml::tags( 'td', null, implode( Xml::element( 'br' ), $sourceMsgs ) );
216
217
		$isActive = $isExplicit || isset( $this->softwareActivatedTags[$tag] );
218
		$activeMsg = ( $isActive ? 'tags-active-yes' : 'tags-active-no' );
219
		$newRow .= Xml::tags( 'td', null, $this->msg( $activeMsg )->escaped() );
220
221
		$hitcountLabelMsg = $this->msg( 'tags-hitcount' )->numParams( $hitcount );
222
		if ( $this->getConfig()->get( 'UseTagFilter' ) ) {
223
			$hitcountLabel = $linkRenderer->makeLink(
224
				SpecialPage::getTitleFor( 'Recentchanges' ),
225
				$hitcountLabelMsg->text(),
226
				[],
227
				[ 'tagfilter' => $tag ]
228
			);
229
		} else {
230
			$hitcountLabel = $hitcountLabelMsg->escaped();
231
		}
232
233
		// add raw $hitcount for sorting, because tags-hitcount contains numbers and letters
234
		$newRow .= Xml::tags( 'td', [ 'data-sort-value' => $hitcount ], $hitcountLabel );
235
236
		// actions
237
		$actionLinks = [];
238
239
		// delete
240
		if ( $showDeleteActions && ChangeTags::canDeleteTag( $tag )->isOK() ) {
241
			$actionLinks[] = $linkRenderer->makeKnownLink(
242
				$this->getPageTitle( 'delete' ),
243
				$this->msg( 'tags-delete' )->text(),
244
				[],
245
				[ 'tag' => $tag ] );
246
		}
247
248
		if ( $showManageActions ) { // we've already checked that the user had the requisite userright
249
250
			// activate
251
			if ( ChangeTags::canActivateTag( $tag )->isOK() ) {
252
				$actionLinks[] = $linkRenderer->makeKnownLink(
253
					$this->getPageTitle( 'activate' ),
254
					$this->msg( 'tags-activate' )->text(),
255
					[],
256
					[ 'tag' => $tag ] );
257
			}
258
259
			// deactivate
260
			if ( ChangeTags::canDeactivateTag( $tag )->isOK() ) {
261
				$actionLinks[] = $linkRenderer->makeKnownLink(
262
					$this->getPageTitle( 'deactivate' ),
263
					$this->msg( 'tags-deactivate' )->text(),
264
					[],
265
					[ 'tag' => $tag ] );
266
			}
267
268
		}
269
270
		if ( $showDeleteActions || $showManageActions ) {
271
			$newRow .= Xml::tags( 'td', null, $this->getLanguage()->pipeList( $actionLinks ) );
272
		}
273
274
		return Xml::tags( 'tr', null, $newRow ) . "\n";
275
	}
276
277
	public function processCreateTagForm( array $data, HTMLForm $form ) {
278
		$context = $form->getContext();
279
		$out = $context->getOutput();
280
281
		$tag = trim( strval( $data['Tag'] ) );
282
		$ignoreWarnings = isset( $data['IgnoreWarnings'] ) && $data['IgnoreWarnings'] === '1';
283
		$status = ChangeTags::createTagWithChecks( $tag, $data['Reason'],
284
			$context->getUser(), $ignoreWarnings );
285
286
		if ( $status->isGood() ) {
287
			$out->redirect( $this->getPageTitle()->getLocalURL() );
288
			return true;
289
		} elseif ( $status->isOK() ) {
290
			// we have some warnings, so we show a confirmation form
291
			$fields = [
292
				'Tag' => [
293
					'type' => 'hidden',
294
					'default' => $data['Tag'],
295
				],
296
				'Reason' => [
297
					'type' => 'hidden',
298
					'default' => $data['Reason'],
299
				],
300
				'IgnoreWarnings' => [
301
					'type' => 'hidden',
302
					'default' => '1',
303
				],
304
			];
305
306
			// fool HTMLForm into thinking the form hasn't been submitted yet. Otherwise
307
			// we get into an infinite loop!
308
			$context->getRequest()->unsetVal( 'wpEditToken' );
309
310
			$headerText = $this->msg( 'tags-create-warnings-above', $tag,
311
				count( $status->getWarningsArray() ) )->parseAsBlock() .
312
				$out->parse( $status->getWikiText() ) .
313
				$this->msg( 'tags-create-warnings-below' )->parseAsBlock();
314
315
			$subform = new HTMLForm( $fields, $this->getContext() );
316
			$subform->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
317
			$subform->setWrapperLegendMsg( 'tags-create-heading' );
318
			$subform->setHeaderText( $headerText );
319
			$subform->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
320
			$subform->setSubmitTextMsg( 'htmlform-yes' );
321
			$subform->show();
322
323
			$out->addBacklinkSubtitle( $this->getPageTitle() );
324
			return true;
325
		} else {
326
			$out->addWikiText( "<div class=\"error\">\n" . $status->getWikiText() .
327
				"\n</div>" );
328
			return false;
329
		}
330
	}
331
332
	protected function showDeleteTagForm( $tag ) {
333
		$user = $this->getUser();
334
		if ( !$user->isAllowed( 'deletechangetags' ) ) {
335
			throw new PermissionsError( 'deletechangetags' );
336
		}
337
338
		$out = $this->getOutput();
339
		$out->setPageTitle( $this->msg( 'tags-delete-title' ) );
340
		$out->addBacklinkSubtitle( $this->getPageTitle() );
341
342
		// is the tag actually able to be deleted?
343
		$canDeleteResult = ChangeTags::canDeleteTag( $tag, $user );
344
		if ( !$canDeleteResult->isGood() ) {
345
			$out->addWikiText( "<div class=\"error\">\n" . $canDeleteResult->getWikiText() .
346
				"\n</div>" );
347
			if ( !$canDeleteResult->isOK() ) {
348
				return;
349
			}
350
		}
351
352
		$preText = $this->msg( 'tags-delete-explanation-initial', $tag )->parseAsBlock();
353
		$tagUsage = ChangeTags::tagUsageStatistics();
354
		if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > 0 ) {
355
			$preText .= $this->msg( 'tags-delete-explanation-in-use', $tag,
356
				$tagUsage[$tag] )->parseAsBlock();
357
		}
358
		$preText .= $this->msg( 'tags-delete-explanation-warning', $tag )->parseAsBlock();
359
360
		// see if the tag is in use
361
		$this->softwareActivatedTags = array_fill_keys(
362
			ChangeTags::listSoftwareActivatedTags(), true );
363
		if ( isset( $this->softwareActivatedTags[$tag] ) ) {
364
			$preText .= $this->msg( 'tags-delete-explanation-active', $tag )->parseAsBlock();
365
		}
366
367
		$fields = [];
368
		$fields['Reason'] = [
369
			'type' => 'text',
370
			'label' => $this->msg( 'tags-delete-reason' )->plain(),
371
			'size' => 50,
372
		];
373
		$fields['HiddenTag'] = [
374
			'type' => 'hidden',
375
			'name' => 'tag',
376
			'default' => $tag,
377
			'required' => true,
378
		];
379
380
		$form = new HTMLForm( $fields, $this->getContext() );
381
		$form->setAction( $this->getPageTitle( 'delete' )->getLocalURL() );
382
		$form->tagAction = 'delete'; // custom property on HTMLForm object
383
		$form->setSubmitCallback( [ $this, 'processTagForm' ] );
384
		$form->setSubmitTextMsg( 'tags-delete-submit' );
385
		$form->setSubmitDestructive(); // nasty!
386
		$form->addPreText( $preText );
387
		$form->show();
388
	}
389
390
	protected function showActivateDeactivateForm( $tag, $activate ) {
391
		$actionStr = $activate ? 'activate' : 'deactivate';
392
393
		$user = $this->getUser();
394
		if ( !$user->isAllowed( 'managechangetags' ) ) {
395
			throw new PermissionsError( 'managechangetags' );
396
		}
397
398
		$out = $this->getOutput();
399
		// tags-activate-title, tags-deactivate-title
400
		$out->setPageTitle( $this->msg( "tags-$actionStr-title" ) );
401
		$out->addBacklinkSubtitle( $this->getPageTitle() );
402
403
		// is it possible to do this?
404
		$func = $activate ? 'canActivateTag' : 'canDeactivateTag';
405
		$result = ChangeTags::$func( $tag, $user );
406
		if ( !$result->isGood() ) {
407
			$out->addWikiText( "<div class=\"error\">\n" . $result->getWikiText() .
408
				"\n</div>" );
409
			if ( !$result->isOK() ) {
410
				return;
411
			}
412
		}
413
414
		// tags-activate-question, tags-deactivate-question
415
		$preText = $this->msg( "tags-$actionStr-question", $tag )->parseAsBlock();
416
417
		$fields = [];
418
		// tags-activate-reason, tags-deactivate-reason
419
		$fields['Reason'] = [
420
			'type' => 'text',
421
			'label' => $this->msg( "tags-$actionStr-reason" )->plain(),
422
			'size' => 50,
423
		];
424
		$fields['HiddenTag'] = [
425
			'type' => 'hidden',
426
			'name' => 'tag',
427
			'default' => $tag,
428
			'required' => true,
429
		];
430
431
		$form = new HTMLForm( $fields, $this->getContext() );
432
		$form->setAction( $this->getPageTitle( $actionStr )->getLocalURL() );
433
		$form->tagAction = $actionStr;
434
		$form->setSubmitCallback( [ $this, 'processTagForm' ] );
435
		// tags-activate-submit, tags-deactivate-submit
436
		$form->setSubmitTextMsg( "tags-$actionStr-submit" );
437
		$form->addPreText( $preText );
438
		$form->show();
439
	}
440
441
	public function processTagForm( array $data, HTMLForm $form ) {
442
		$context = $form->getContext();
443
		$out = $context->getOutput();
444
445
		$tag = $data['HiddenTag'];
446
		$status = call_user_func( [ 'ChangeTags', "{$form->tagAction}TagWithChecks" ],
447
			$tag, $data['Reason'], $context->getUser(), true );
448
449
		if ( $status->isGood() ) {
450
			$out->redirect( $this->getPageTitle()->getLocalURL() );
451
			return true;
452
		} elseif ( $status->isOK() && $form->tagAction === 'delete' ) {
453
			// deletion succeeded, but hooks raised a warning
454
			$out->addWikiText( $this->msg( 'tags-delete-warnings-after-delete', $tag,
455
				count( $status->getWarningsArray() ) )->text() . "\n" .
456
				$status->getWikitext() );
457
			$out->addReturnTo( $this->getPageTitle() );
458
			return true;
459
		} else {
460
			$out->addWikiText( "<div class=\"error\">\n" . $status->getWikitext() .
461
				"\n</div>" );
462
			return false;
463
		}
464
	}
465
466
	/**
467
	 * Return an array of subpages that this special page will accept.
468
	 *
469
	 * @return string[] subpages
470
	 */
471
	public function getSubpagesForPrefixSearch() {
472
		// The subpages does not have an own form, so not listing it at the moment
473
		return [
474
			// 'delete',
475
			// 'activate',
476
			// 'deactivate',
477
			// 'create',
478
		];
479
	}
480
481
	protected function getGroupName() {
482
		return 'changes';
483
	}
484
}
485