MediaWiki  master
SpecialTags.php
Go to the documentation of this file.
1 <?php
25 
31 class SpecialTags extends SpecialPage {
32 
37 
42 
47 
48  function __construct() {
49  parent::__construct( 'Tags' );
50  }
51 
52  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  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  $user = $this->getUser();
82  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
83  $userCanManage = $permissionManager->userHasRight( $user, 'managechangetags' );
84  $userCanDelete = $permissionManager->userHasRight( $user, 'deletechangetags' );
85  $userCanEditInterface = $permissionManager->userHasRight( $user, '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  'label' => $this->msg( 'tags-create-reason' )->plain(),
98  'size' => 50,
99  ],
100  'IgnoreWarnings' => [
101  'type' => 'hidden',
102  ],
103  ];
104 
105  $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
106  $form->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
107  $form->setWrapperLegendMsg( 'tags-create-heading' );
108  $form->setHeaderText( $this->msg( 'tags-create-explanation' )->parseAsBlock() );
109  $form->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
110  $form->setSubmitTextMsg( 'tags-create-submit' );
111  $form->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 master.
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 atleast 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( 'jquery.tablesorter.styles' );
173  $out->addModules( 'jquery.tablesorter' );
174  $out->addHTML( Xml::tags(
175  'table',
176  [ 'class' => 'mw-datatable sortable mw-tags-table' ],
177  Xml::tags( 'thead', null, $thead ) .
178  Xml::tags( 'tbody', null, $tbody )
179  ) );
180  }
181 
182  function doTagRow( $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks ) {
183  $newRow = '';
184  $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) );
185 
186  $linkRenderer = $this->getLinkRenderer();
187  $disp = ChangeTags::tagDescription( $tag, $this->getContext() );
188  if ( $showEditLinks ) {
189  $disp .= ' ';
190  $editLink = $linkRenderer->makeLink(
191  $this->msg( "tag-$tag" )->inContentLanguage()->getTitle(),
192  $this->msg( 'tags-edit' )->text()
193  );
194  $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped();
195  }
196  $newRow .= Xml::tags( 'td', null, $disp );
197 
198  $msg = $this->msg( "tag-$tag-description" );
199  $desc = !$msg->exists() ? '' : $msg->parse();
200  if ( $showEditLinks ) {
201  $desc .= ' ';
202  $editDescLink = $linkRenderer->makeLink(
203  $this->msg( "tag-$tag-description" )->inContentLanguage()->getTitle(),
204  $this->msg( 'tags-edit' )->text()
205  );
206  $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped();
207  }
208  $newRow .= Xml::tags( 'td', null, $desc );
209 
210  $sourceMsgs = [];
211  $isSoftware = isset( $this->softwareDefinedTags[$tag] );
212  $isExplicit = isset( $this->explicitlyDefinedTags[$tag] );
213  if ( $isSoftware ) {
214  // TODO: Rename this message
215  $sourceMsgs[] = $this->msg( 'tags-source-extension' )->escaped();
216  }
217  if ( $isExplicit ) {
218  $sourceMsgs[] = $this->msg( 'tags-source-manual' )->escaped();
219  }
220  if ( !$sourceMsgs ) {
221  $sourceMsgs[] = $this->msg( 'tags-source-none' )->escaped();
222  }
223  $newRow .= Xml::tags( 'td', null, implode( Xml::element( 'br' ), $sourceMsgs ) );
224 
225  $isActive = $isExplicit || isset( $this->softwareActivatedTags[$tag] );
226  $activeMsg = ( $isActive ? 'tags-active-yes' : 'tags-active-no' );
227  $newRow .= Xml::tags( 'td', null, $this->msg( $activeMsg )->escaped() );
228 
229  $hitcountLabelMsg = $this->msg( 'tags-hitcount' )->numParams( $hitcount );
230  if ( $this->getConfig()->get( 'UseTagFilter' ) ) {
231  $hitcountLabel = $linkRenderer->makeLink(
232  SpecialPage::getTitleFor( 'Recentchanges' ),
233  $hitcountLabelMsg->text(),
234  [],
235  [ 'tagfilter' => $tag ]
236  );
237  } else {
238  $hitcountLabel = $hitcountLabelMsg->escaped();
239  }
240 
241  // add raw $hitcount for sorting, because tags-hitcount contains numbers and letters
242  $newRow .= Xml::tags( 'td', [ 'data-sort-value' => $hitcount ], $hitcountLabel );
243 
244  $actionLinks = [];
245 
246  if ( $showDeleteActions && ChangeTags::canDeleteTag( $tag )->isOK() ) {
247  $actionLinks[] = $linkRenderer->makeKnownLink(
248  $this->getPageTitle( 'delete' ),
249  $this->msg( 'tags-delete' )->text(),
250  [],
251  [ 'tag' => $tag ] );
252  }
253 
254  if ( $showManageActions ) { // we've already checked that the user had the requisite userright
255  if ( ChangeTags::canActivateTag( $tag )->isOK() ) {
256  $actionLinks[] = $linkRenderer->makeKnownLink(
257  $this->getPageTitle( 'activate' ),
258  $this->msg( 'tags-activate' )->text(),
259  [],
260  [ 'tag' => $tag ] );
261  }
262 
263  if ( ChangeTags::canDeactivateTag( $tag )->isOK() ) {
264  $actionLinks[] = $linkRenderer->makeKnownLink(
265  $this->getPageTitle( 'deactivate' ),
266  $this->msg( 'tags-deactivate' )->text(),
267  [],
268  [ 'tag' => $tag ] );
269  }
270  }
271 
272  if ( $showDeleteActions || $showManageActions ) {
273  $newRow .= Xml::tags( 'td', null, $this->getLanguage()->pipeList( $actionLinks ) );
274  }
275 
276  return Xml::tags( 'tr', null, $newRow ) . "\n";
277  }
278 
279  public function processCreateTagForm( array $data, HTMLForm $form ) {
280  $context = $form->getContext();
281  $out = $context->getOutput();
282 
283  $tag = trim( strval( $data['Tag'] ) );
284  $ignoreWarnings = isset( $data['IgnoreWarnings'] ) && $data['IgnoreWarnings'] === '1';
285  $status = ChangeTags::createTagWithChecks( $tag, $data['Reason'],
286  $context->getUser(), $ignoreWarnings );
287 
288  if ( $status->isGood() ) {
289  $out->redirect( $this->getPageTitle()->getLocalURL() );
290  return true;
291  } elseif ( $status->isOK() ) {
292  // we have some warnings, so we show a confirmation form
293  $fields = [
294  'Tag' => [
295  'type' => 'hidden',
296  'default' => $data['Tag'],
297  ],
298  'Reason' => [
299  'type' => 'hidden',
300  'default' => $data['Reason'],
301  ],
302  'IgnoreWarnings' => [
303  'type' => 'hidden',
304  'default' => '1',
305  ],
306  ];
307 
308  // fool HTMLForm into thinking the form hasn't been submitted yet. Otherwise
309  // we get into an infinite loop!
310  $context->getRequest()->unsetVal( 'wpEditToken' );
311 
312  $headerText = $this->msg( 'tags-create-warnings-above', $tag,
313  count( $status->getWarningsArray() ) )->parseAsBlock() .
314  $out->parseAsInterface( $status->getWikiText() ) .
315  $this->msg( 'tags-create-warnings-below' )->parseAsBlock();
316 
317  $subform = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
318  $subform->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
319  $subform->setWrapperLegendMsg( 'tags-create-heading' );
320  $subform->setHeaderText( $headerText );
321  $subform->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
322  $subform->setSubmitTextMsg( 'htmlform-yes' );
323  $subform->show();
324 
325  $out->addBacklinkSubtitle( $this->getPageTitle() );
326  return true;
327  } else {
328  $out->wrapWikiTextAsInterface( 'error', $status->getWikiText() );
329  return false;
330  }
331  }
332 
333  protected function showDeleteTagForm( $tag ) {
334  $user = $this->getUser();
335  if ( !MediaWikiServices::getInstance()
337  ->userHasRight( $user, 'deletechangetags' ) ) {
338  throw new PermissionsError( 'deletechangetags' );
339  }
340 
341  $out = $this->getOutput();
342  $out->setPageTitle( $this->msg( 'tags-delete-title' ) );
343  $out->addBacklinkSubtitle( $this->getPageTitle() );
344 
345  // is the tag actually able to be deleted?
346  $canDeleteResult = ChangeTags::canDeleteTag( $tag, $user );
347  if ( !$canDeleteResult->isGood() ) {
348  $out->wrapWikiTextAsInterface( 'error', $canDeleteResult->getWikiText() );
349  if ( !$canDeleteResult->isOK() ) {
350  return;
351  }
352  }
353 
354  $preText = $this->msg( 'tags-delete-explanation-initial', $tag )->parseAsBlock();
355  $tagUsage = ChangeTags::tagUsageStatistics();
356  if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > 0 ) {
357  $preText .= $this->msg( 'tags-delete-explanation-in-use', $tag,
358  $tagUsage[$tag] )->parseAsBlock();
359  }
360  $preText .= $this->msg( 'tags-delete-explanation-warning', $tag )->parseAsBlock();
361 
362  // see if the tag is in use
363  $this->softwareActivatedTags = array_fill_keys(
365  if ( isset( $this->softwareActivatedTags[$tag] ) ) {
366  $preText .= $this->msg( 'tags-delete-explanation-active', $tag )->parseAsBlock();
367  }
368 
369  $fields = [];
370  $fields['Reason'] = [
371  'type' => 'text',
372  'label' => $this->msg( 'tags-delete-reason' )->plain(),
373  'size' => 50,
374  ];
375  $fields['HiddenTag'] = [
376  'type' => 'hidden',
377  'name' => 'tag',
378  'default' => $tag,
379  'required' => true,
380  ];
381 
382  $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
383  $form->setAction( $this->getPageTitle( 'delete' )->getLocalURL() );
384  // @phan-suppress-next-line PhanUndeclaredProperty
385  $form->tagAction = 'delete'; // custom property on HTMLForm object
386  $form->setSubmitCallback( [ $this, 'processTagForm' ] );
387  $form->setSubmitTextMsg( 'tags-delete-submit' );
388  $form->setSubmitDestructive(); // nasty!
389  $form->addPreText( $preText );
390  $form->show();
391  }
392 
393  protected function showActivateDeactivateForm( $tag, $activate ) {
394  $actionStr = $activate ? 'activate' : 'deactivate';
395 
396  $user = $this->getUser();
397  if ( !MediaWikiServices::getInstance()
399  ->userHasRight( $user, 'managechangetags' ) ) {
400  throw new PermissionsError( 'managechangetags' );
401  }
402 
403  $out = $this->getOutput();
404  // tags-activate-title, tags-deactivate-title
405  $out->setPageTitle( $this->msg( "tags-$actionStr-title" ) );
406  $out->addBacklinkSubtitle( $this->getPageTitle() );
407 
408  // is it possible to do this?
409  $func = $activate ? 'canActivateTag' : 'canDeactivateTag';
410  $result = ChangeTags::$func( $tag, $user );
411  if ( !$result->isGood() ) {
412  $out->wrapWikiTextAsInterface( 'error', $result->getWikiText() );
413  if ( !$result->isOK() ) {
414  return;
415  }
416  }
417 
418  // tags-activate-question, tags-deactivate-question
419  $preText = $this->msg( "tags-$actionStr-question", $tag )->parseAsBlock();
420 
421  $fields = [];
422  // tags-activate-reason, tags-deactivate-reason
423  $fields['Reason'] = [
424  'type' => 'text',
425  'label' => $this->msg( "tags-$actionStr-reason" )->plain(),
426  'size' => 50,
427  ];
428  $fields['HiddenTag'] = [
429  'type' => 'hidden',
430  'name' => 'tag',
431  'default' => $tag,
432  'required' => true,
433  ];
434 
435  $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
436  $form->setAction( $this->getPageTitle( $actionStr )->getLocalURL() );
437  // @phan-suppress-next-line PhanUndeclaredProperty
438  $form->tagAction = $actionStr;
439  $form->setSubmitCallback( [ $this, 'processTagForm' ] );
440  // tags-activate-submit, tags-deactivate-submit
441  $form->setSubmitTextMsg( "tags-$actionStr-submit" );
442  $form->addPreText( $preText );
443  $form->show();
444  }
445 
452  public function processTagForm( array $data, HTMLForm $form ) {
453  $context = $form->getContext();
454  $out = $context->getOutput();
455 
456  $tag = $data['HiddenTag'];
457  $status = call_user_func( [ ChangeTags::class, "{$form->tagAction}TagWithChecks" ],
458  $tag, $data['Reason'], $context->getUser(), true );
459 
460  if ( $status->isGood() ) {
461  $out->redirect( $this->getPageTitle()->getLocalURL() );
462  return true;
463  } elseif ( $status->isOK() && $form->tagAction === 'delete' ) {
464  // deletion succeeded, but hooks raised a warning
465  $out->addWikiTextAsInterface( $this->msg( 'tags-delete-warnings-after-delete', $tag,
466  count( $status->getWarningsArray() ) )->text() . "\n" .
467  $status->getWikitext() );
468  $out->addReturnTo( $this->getPageTitle() );
469  return true;
470  } else {
471  $out->wrapWikiTextAsInterface( 'error', $status->getWikitext() );
472  return false;
473  }
474  }
475 
481  public function getSubpagesForPrefixSearch() {
482  // The subpages does not have an own form, so not listing it at the moment
483  return [
484  // 'delete',
485  // 'activate',
486  // 'deactivate',
487  // 'create',
488  ];
489  }
490 
491  protected function getGroupName() {
492  return 'changes';
493  }
494 }
$context
Definition: load.php:45
getContext()
Gets the context this SpecialPage is executed in.
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
static tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
Definition: ChangeTags.php:183
static canDeactivateTag( $tag, User $user=null)
Is it OK to allow the user to deactivate this tag?
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
getOutput()
Get the OutputPage being used for this instance.
showDeleteTagForm( $tag)
static canDeleteTag( $tag, User $user=null)
Is it OK to allow the user to delete this tag?
getPermissionManager()
showActivateDeactivateForm( $tag, $activate)
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
static factory( $displayFormat,... $arguments)
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:307
static canActivateTag( $tag, User $user=null)
Is it OK to allow the user to activate this tag?
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
execute( $par)
Definition: SpecialTags.php:52
getContext()
Get the base IContextSource object.
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:130
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&#39;t need a full Title object...
Definition: SpecialPage.php:83
array $softwareActivatedTags
List of software activated tags.
Definition: SpecialTags.php:46
static createTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Creates a tag by adding it to change_tag_def table.
array $explicitlyDefinedTags
List of explicitly defined tags.
Definition: SpecialTags.php:36
getUser()
Shortcut to get the User executing this instance.
getConfig()
Shortcut to get main config object.
Show an error when a user tries to do something they do not have the necessary permissions for...
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:41
getLanguage()
Shortcut to get user&#39;s language.
getRequest()
Get the WebRequest being used for this instance.
doTagRow( $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks)
A special page that lists tags for edits.
Definition: SpecialTags.php:31
processTagForm(array $data, HTMLForm $form)
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
getPageTitle( $subpage=false)
Get a self-referential title object.
return true
Definition: router.php:92
array $softwareDefinedTags
List of software defined tags.
Definition: SpecialTags.php:41
getSubpagesForPrefixSearch()
Return an array of subpages that this special page will accept.
processCreateTagForm(array $data, HTMLForm $form)
static tagUsageStatistics()
Returns a map of any tags used on the wiki to number of edits tagged with them, ordered descending by...
MediaWiki Linker LinkRenderer null $linkRenderer
Definition: SpecialPage.php:67