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