MediaWiki REL1_37
SpecialTags.php
Go to the documentation of this file.
1<?php
29class SpecialTags extends SpecialPage {
30
35
40
45
46 public function __construct() {
47 parent::__construct( 'Tags' );
48 }
49
50 public function execute( $par ) {
51 $this->setHeaders();
52 $this->outputHeader();
53 $this->addHelpLink( 'Manual:Tags' );
54
55 $request = $this->getRequest();
56 switch ( $par ) {
57 case 'delete':
58 $this->showDeleteTagForm( $request->getVal( 'tag' ) );
59 break;
60 case 'activate':
61 $this->showActivateDeactivateForm( $request->getVal( 'tag' ), true );
62 break;
63 case 'deactivate':
64 $this->showActivateDeactivateForm( $request->getVal( 'tag' ), false );
65 break;
66 case 'create':
67 // fall through, thanks to HTMLForm's logic
68 default:
69 $this->showTagList();
70 break;
71 }
72 }
73
74 private function showTagList() {
75 $out = $this->getOutput();
76 $out->setPageTitle( $this->msg( 'tags-title' ) );
77 $out->wrapWikiMsg( "<div class='mw-tags-intro'>\n$1\n</div>", 'tags-intro' );
78
79 $authority = $this->getAuthority();
80 $userCanManage = $authority->isAllowed( 'managechangetags' );
81 $userCanDelete = $authority->isAllowed( 'deletechangetags' );
82 $userCanEditInterface = $authority->isAllowed( 'editinterface' );
83
84 // Show form to create a tag
85 if ( $userCanManage ) {
86 $fields = [
87 'Tag' => [
88 'type' => 'text',
89 'label' => $this->msg( 'tags-create-tag-name' )->plain(),
90 'required' => true,
91 ],
92 'Reason' => [
93 'type' => 'text',
94 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
95 'label' => $this->msg( 'tags-create-reason' )->plain(),
96 'size' => 50,
97 ],
98 'IgnoreWarnings' => [
99 'type' => 'hidden',
100 ],
101 ];
102
103 HTMLForm::factory( 'ooui', $fields, $this->getContext() )
104 ->setAction( $this->getPageTitle( 'create' )->getLocalURL() )
105 ->setWrapperLegendMsg( 'tags-create-heading' )
106 ->setHeaderText( $this->msg( 'tags-create-explanation' )->parseAsBlock() )
107 ->setSubmitCallback( [ $this, 'processCreateTagForm' ] )
108 ->setSubmitTextMsg( 'tags-create-submit' )
109 ->show();
110
111 // If processCreateTagForm generated a redirect, there's no point
112 // continuing with this, as the user is just going to end up getting sent
113 // somewhere else. Additionally, if we keep going here, we end up
114 // populating the memcache of tag data (see ChangeTags::listDefinedTags)
115 // with out-of-date data from the replica DB, because the replica DB hasn't caught
116 // up to the fact that a new tag has been created as part of an implicit,
117 // as yet uncommitted transaction on primary DB.
118 if ( $out->getRedirect() !== '' ) {
119 return;
120 }
121 }
122
123 // Used to get hitcounts for #doTagRow()
124 $tagStats = ChangeTags::tagUsageStatistics();
125
126 // Used in #doTagRow()
127 $this->explicitlyDefinedTags = array_fill_keys(
129 $this->softwareDefinedTags = array_fill_keys(
131
132 // List all defined tags, even if they were never applied
133 $definedTags = array_keys( $this->explicitlyDefinedTags + $this->softwareDefinedTags );
134
135 // Show header only if there exists atleast one tag
136 if ( !$tagStats && !$definedTags ) {
137 return;
138 }
139
140 // Write the headers
141 $thead = Xml::tags( 'tr', null, Xml::tags( 'th', null, $this->msg( 'tags-tag' )->parse() ) .
142 Xml::tags( 'th', null, $this->msg( 'tags-display-header' )->parse() ) .
143 Xml::tags( 'th', null, $this->msg( 'tags-description-header' )->parse() ) .
144 Xml::tags( 'th', null, $this->msg( 'tags-source-header' )->parse() ) .
145 Xml::tags( 'th', null, $this->msg( 'tags-active-header' )->parse() ) .
146 Xml::tags( 'th', null, $this->msg( 'tags-hitcount-header' )->parse() ) .
147 ( ( $userCanManage || $userCanDelete ) ?
148 Xml::tags( 'th', [ 'class' => 'unsortable' ],
149 $this->msg( 'tags-actions-header' )->parse() ) :
150 '' )
151 );
152
153 $tbody = '';
154 // Used in #doTagRow()
155 $this->softwareActivatedTags = array_fill_keys(
157
158 // Insert tags that have been applied at least once
159 foreach ( $tagStats as $tag => $hitcount ) {
160 $tbody .= $this->doTagRow( $tag, $hitcount, $userCanManage,
161 $userCanDelete, $userCanEditInterface );
162 }
163 // Insert tags defined somewhere but never applied
164 foreach ( $definedTags as $tag ) {
165 if ( !isset( $tagStats[$tag] ) ) {
166 $tbody .= $this->doTagRow( $tag, 0, $userCanManage, $userCanDelete, $userCanEditInterface );
167 }
168 }
169
170 $out->addModuleStyles( [
171 'jquery.tablesorter.styles',
172 'mediawiki.pager.tablePager'
173 ] );
174 $out->addModules( 'jquery.tablesorter' );
175 $out->addHTML( Xml::tags(
176 'table',
177 [ 'class' => 'mw-datatable sortable mw-tags-table' ],
178 Xml::tags( 'thead', null, $thead ) .
179 Xml::tags( 'tbody', null, $tbody )
180 ) );
181 }
182
183 private function doTagRow(
184 $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks
185 ) {
186 $newRow = '';
187 $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) );
188
190 $disp = ChangeTags::tagDescription( $tag, $this->getContext() );
191 if ( $disp === false ) {
192 $disp = Xml::element( 'em', null, $this->msg( 'tags-hidden' )->text() );
193 }
194 if ( $showEditLinks ) {
195 $disp .= ' ';
196 $editLink = $linkRenderer->makeLink(
197 $this->msg( "tag-$tag" )->inContentLanguage()->getTitle(),
198 $this->msg( 'tags-edit' )->text(),
199 [],
200 [ 'action' => 'edit' ]
201 );
202 $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped();
203 }
204 $newRow .= Xml::tags( 'td', null, $disp );
205
206 $msg = $this->msg( "tag-$tag-description" );
207 $desc = !$msg->exists() ? '' : $msg->parse();
208 if ( $showEditLinks ) {
209 $desc .= ' ';
210 $editDescLink = $linkRenderer->makeLink(
211 $this->msg( "tag-$tag-description" )->inContentLanguage()->getTitle(),
212 $this->msg( 'tags-edit' )->text(),
213 [],
214 [ 'action' => 'edit' ]
215 );
216 $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped();
217 }
218 $newRow .= Xml::tags( 'td', null, $desc );
219
220 $sourceMsgs = [];
221 $isSoftware = isset( $this->softwareDefinedTags[$tag] );
222 $isExplicit = isset( $this->explicitlyDefinedTags[$tag] );
223 if ( $isSoftware ) {
224 // TODO: Rename this message
225 $sourceMsgs[] = $this->msg( 'tags-source-extension' )->escaped();
226 }
227 if ( $isExplicit ) {
228 $sourceMsgs[] = $this->msg( 'tags-source-manual' )->escaped();
229 }
230 if ( !$sourceMsgs ) {
231 $sourceMsgs[] = $this->msg( 'tags-source-none' )->escaped();
232 }
233 $newRow .= Xml::tags( 'td', null, implode( Xml::element( 'br' ), $sourceMsgs ) );
234
235 $isActive = $isExplicit || isset( $this->softwareActivatedTags[$tag] );
236 $activeMsg = ( $isActive ? 'tags-active-yes' : 'tags-active-no' );
237 $newRow .= Xml::tags( 'td', null, $this->msg( $activeMsg )->escaped() );
238
239 $hitcountLabelMsg = $this->msg( 'tags-hitcount' )->numParams( $hitcount );
240 if ( $this->getConfig()->get( 'UseTagFilter' ) ) {
241 $hitcountLabel = $linkRenderer->makeLink(
242 SpecialPage::getTitleFor( 'Recentchanges' ),
243 $hitcountLabelMsg->text(),
244 [],
245 [ 'tagfilter' => $tag ]
246 );
247 } else {
248 $hitcountLabel = $hitcountLabelMsg->escaped();
249 }
250
251 // add raw $hitcount for sorting, because tags-hitcount contains numbers and letters
252 $newRow .= Xml::tags( 'td', [ 'data-sort-value' => $hitcount ], $hitcountLabel );
253
254 $actionLinks = [];
255
256 if ( $showDeleteActions && ChangeTags::canDeleteTag( $tag )->isOK() ) {
257 $actionLinks[] = $linkRenderer->makeKnownLink(
258 $this->getPageTitle( 'delete' ),
259 $this->msg( 'tags-delete' )->text(),
260 [],
261 [ 'tag' => $tag ] );
262 }
263
264 if ( $showManageActions ) { // we've already checked that the user had the requisite userright
265 if ( ChangeTags::canActivateTag( $tag )->isOK() ) {
266 $actionLinks[] = $linkRenderer->makeKnownLink(
267 $this->getPageTitle( 'activate' ),
268 $this->msg( 'tags-activate' )->text(),
269 [],
270 [ 'tag' => $tag ] );
271 }
272
273 if ( ChangeTags::canDeactivateTag( $tag )->isOK() ) {
274 $actionLinks[] = $linkRenderer->makeKnownLink(
275 $this->getPageTitle( 'deactivate' ),
276 $this->msg( 'tags-deactivate' )->text(),
277 [],
278 [ 'tag' => $tag ] );
279 }
280 }
281
282 if ( $showDeleteActions || $showManageActions ) {
283 $newRow .= Xml::tags( 'td', null, $this->getLanguage()->pipeList( $actionLinks ) );
284 }
285
286 return Xml::tags( 'tr', null, $newRow ) . "\n";
287 }
288
289 public function processCreateTagForm( array $data, HTMLForm $form ) {
290 $context = $form->getContext();
291 $out = $context->getOutput();
292
293 $tag = trim( strval( $data['Tag'] ) );
294 $ignoreWarnings = isset( $data['IgnoreWarnings'] ) && $data['IgnoreWarnings'] === '1';
295 $status = ChangeTags::createTagWithChecks( $tag, $data['Reason'],
296 $context->getUser(), $ignoreWarnings );
297
298 if ( $status->isGood() ) {
299 $out->redirect( $this->getPageTitle()->getLocalURL() );
300 return true;
301 } elseif ( $status->isOK() ) {
302 // we have some warnings, so we show a confirmation form
303 $fields = [
304 'Tag' => [
305 'type' => 'hidden',
306 'default' => $data['Tag'],
307 ],
308 'Reason' => [
309 'type' => 'hidden',
310 'default' => $data['Reason'],
311 ],
312 'IgnoreWarnings' => [
313 'type' => 'hidden',
314 'default' => '1',
315 ],
316 ];
317
318 // fool HTMLForm into thinking the form hasn't been submitted yet. Otherwise
319 // we get into an infinite loop!
320 $context->getRequest()->unsetVal( 'wpEditToken' );
321
322 $headerText = $this->msg( 'tags-create-warnings-above', $tag,
323 count( $status->getWarningsArray() ) )->parseAsBlock() .
324 $out->parseAsInterface( $status->getWikiText() ) .
325 $this->msg( 'tags-create-warnings-below' )->parseAsBlock();
326
327 HTMLForm::factory( 'ooui', $fields, $this->getContext() )
328 ->setAction( $this->getPageTitle( 'create' )->getLocalURL() )
329 ->setWrapperLegendMsg( 'tags-create-heading' )
330 ->setHeaderText( $headerText )
331 ->setSubmitCallback( [ $this, 'processCreateTagForm' ] )
332 ->setSubmitTextMsg( 'htmlform-yes' )
333 ->show();
334
335 $out->addBacklinkSubtitle( $this->getPageTitle() );
336 return true;
337 } else {
338 $out->wrapWikiTextAsInterface( 'error', $status->getWikiText() );
339 return false;
340 }
341 }
342
343 protected function showDeleteTagForm( $tag ) {
344 $authority = $this->getAuthority();
345 if ( !$authority->isAllowed( 'deletechangetags' ) ) {
346 throw new PermissionsError( 'deletechangetags' );
347 }
348
349 $out = $this->getOutput();
350 $out->setPageTitle( $this->msg( 'tags-delete-title' ) );
351 $out->addBacklinkSubtitle( $this->getPageTitle() );
352
353 // is the tag actually able to be deleted?
354 $canDeleteResult = ChangeTags::canDeleteTag( $tag, $authority );
355 if ( !$canDeleteResult->isGood() ) {
356 $out->wrapWikiTextAsInterface( 'error', $canDeleteResult->getWikiText() );
357 if ( !$canDeleteResult->isOK() ) {
358 return;
359 }
360 }
361
362 $preText = $this->msg( 'tags-delete-explanation-initial', $tag )->parseAsBlock();
363 $tagUsage = ChangeTags::tagUsageStatistics();
364 if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > 0 ) {
365 $preText .= $this->msg( 'tags-delete-explanation-in-use', $tag,
366 $tagUsage[$tag] )->parseAsBlock();
367 }
368 $preText .= $this->msg( 'tags-delete-explanation-warning', $tag )->parseAsBlock();
369
370 // see if the tag is in use
371 $this->softwareActivatedTags = array_fill_keys(
373 if ( isset( $this->softwareActivatedTags[$tag] ) ) {
374 $preText .= $this->msg( 'tags-delete-explanation-active', $tag )->parseAsBlock();
375 }
376
377 $fields = [];
378 $fields['Reason'] = [
379 'type' => 'text',
380 'label' => $this->msg( 'tags-delete-reason' )->plain(),
381 'size' => 50,
382 ];
383 $fields['HiddenTag'] = [
384 'type' => 'hidden',
385 'name' => 'tag',
386 'default' => $tag,
387 'required' => true,
388 ];
389
390 HTMLForm::factory( 'ooui', $fields, $this->getContext() )
391 ->setAction( $this->getPageTitle( 'delete' )->getLocalURL() )
392 ->setSubmitCallback( function ( $data, $form ) {
393 return $this->processTagForm( $data, $form, 'delete' );
394 } )
395 ->setSubmitTextMsg( 'tags-delete-submit' )
396 ->setSubmitDestructive()
397 ->addPreText( $preText )
398 ->show();
399 }
400
401 protected function showActivateDeactivateForm( $tag, $activate ) {
402 $actionStr = $activate ? 'activate' : 'deactivate';
403
404 $authority = $this->getAuthority();
405 if ( !$authority->isAllowed( 'managechangetags' ) ) {
406 throw new PermissionsError( 'managechangetags' );
407 }
408
409 $out = $this->getOutput();
410 // tags-activate-title, tags-deactivate-title
411 $out->setPageTitle( $this->msg( "tags-$actionStr-title" ) );
412 $out->addBacklinkSubtitle( $this->getPageTitle() );
413
414 // is it possible to do this?
415 if ( $activate ) {
416 $result = ChangeTags::canActivateTag( $tag, $authority );
417 } else {
418 $result = ChangeTags::canDeactivateTag( $tag, $authority );
419 }
420 if ( !$result->isGood() ) {
421 $out->wrapWikiTextAsInterface( 'error', $result->getWikiText() );
422 if ( !$result->isOK() ) {
423 return;
424 }
425 }
426
427 // tags-activate-question, tags-deactivate-question
428 $preText = $this->msg( "tags-$actionStr-question", $tag )->parseAsBlock();
429
430 $fields = [];
431 // tags-activate-reason, tags-deactivate-reason
432 $fields['Reason'] = [
433 'type' => 'text',
434 'label' => $this->msg( "tags-$actionStr-reason" )->plain(),
435 'size' => 50,
436 ];
437 $fields['HiddenTag'] = [
438 'type' => 'hidden',
439 'name' => 'tag',
440 'default' => $tag,
441 'required' => true,
442 ];
443
444 HTMLForm::factory( 'ooui', $fields, $this->getContext() )
445 ->setAction( $this->getPageTitle( $actionStr )->getLocalURL() )
446 ->setSubmitCallback( function ( $data, $form ) use ( $actionStr ) {
447 return $this->processTagForm( $data, $form, $actionStr );
448 } )
449 // tags-activate-submit, tags-deactivate-submit
450 ->setSubmitTextMsg( "tags-$actionStr-submit" )
451 ->addPreText( $preText )
452 ->show();
453 }
454
461 public function processTagForm( array $data, HTMLForm $form, string $action ) {
462 $context = $form->getContext();
463 $out = $context->getOutput();
464
465 $tag = $data['HiddenTag'];
466 // activateTagWithChecks, deactivateTagWithChecks, deleteTagWithChecks
467 $status = call_user_func( [ ChangeTags::class, "{$action}TagWithChecks" ],
468 $tag, $data['Reason'], $context->getUser(), true );
469
470 if ( $status->isGood() ) {
471 $out->redirect( $this->getPageTitle()->getLocalURL() );
472 return true;
473 } elseif ( $status->isOK() && $action === 'delete' ) {
474 // deletion succeeded, but hooks raised a warning
475 $out->addWikiTextAsInterface( $this->msg( 'tags-delete-warnings-after-delete', $tag,
476 count( $status->getWarningsArray() ) )->text() . "\n" .
477 $status->getWikitext() );
478 $out->addReturnTo( $this->getPageTitle() );
479 return true;
480 } else {
481 $out->wrapWikiTextAsInterface( 'error', $status->getWikitext() );
482 return false;
483 }
484 }
485
491 public function getSubpagesForPrefixSearch() {
492 // The subpages does not have an own form, so not listing it at the moment
493 return [
494 // 'delete',
495 // 'activate',
496 // 'deactivate',
497 // 'create',
498 ];
499 }
500
501 protected function getGroupName() {
502 return 'changes';
503 }
504}
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
static canDeactivateTag( $tag, Authority $performer=null)
Is it OK to allow the user to deactivate this tag?
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
static canActivateTag( $tag, Authority $performer=null)
Is it OK to allow the user to activate this tag?
static canDeleteTag( $tag, Authority $performer=null, int $flags=0)
Is it OK to allow the user to delete this tag?
static tagUsageStatistics()
Returns a map of any tags used on the wiki to number of edits tagged with them, ordered descending by...
static tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
static createTagWithChecks( $tag, $reason, Authority $performer, $ignoreWarnings=false, array $logEntryTags=[])
Creates a tag by adding it to change_tag_def table.
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
getContext()
Get the base IContextSource object.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:143
makeKnownLink( $target, $text=null, array $extraAttribs=[], array $query=[])
makeLink( $target, $text=null, array $extraAttribs=[], array $query=[])
Show an error when a user tries to do something they do not have the necessary permissions for.
Parent class for all special pages.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
getContext()
Gets the context this SpecialPage is executed in.
LinkRenderer null $linkRenderer
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getAuthority()
Shortcut to get the Authority executing this instance.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
getLanguage()
Shortcut to get user's language.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
A special page that lists tags for edits.
array $softwareActivatedTags
List of software activated tags.
doTagRow( $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks)
showActivateDeactivateForm( $tag, $activate)
execute( $par)
Default execute method Checks user permissions.
processTagForm(array $data, HTMLForm $form, string $action)
getSubpagesForPrefixSearch()
Return an array of subpages that this special page will accept.
array $softwareDefinedTags
List of software defined tags.
array $explicitlyDefinedTags
List of explicitly defined tags.
showDeleteTagForm( $tag)
processCreateTagForm(array $data, HTMLForm $form)
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...