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