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