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