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',
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  private function doTagRow(
183  $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks
184  ) {
185  $newRow = '';
186  $newRow .= Xml::tags( 'td', null, Xml::element( 'code', null, $tag ) );
187 
188  $linkRenderer = $this->getLinkRenderer();
189  $disp = ChangeTags::tagDescription( $tag, $this->getContext() );
190  if ( $showEditLinks ) {
191  $disp .= ' ';
192  $editLink = $linkRenderer->makeLink(
193  $this->msg( "tag-$tag" )->inContentLanguage()->getTitle(),
194  $this->msg( 'tags-edit' )->text(),
195  [],
196  [ 'action' => 'edit' ]
197  );
198  $disp .= $this->msg( 'parentheses' )->rawParams( $editLink )->escaped();
199  }
200  $newRow .= Xml::tags( 'td', null, $disp );
201 
202  $msg = $this->msg( "tag-$tag-description" );
203  $desc = !$msg->exists() ? '' : $msg->parse();
204  if ( $showEditLinks ) {
205  $desc .= ' ';
206  $editDescLink = $linkRenderer->makeLink(
207  $this->msg( "tag-$tag-description" )->inContentLanguage()->getTitle(),
208  $this->msg( 'tags-edit' )->text(),
209  [],
210  [ 'action' => 'edit' ]
211  );
212  $desc .= $this->msg( 'parentheses' )->rawParams( $editDescLink )->escaped();
213  }
214  $newRow .= Xml::tags( 'td', null, $desc );
215 
216  $sourceMsgs = [];
217  $isSoftware = isset( $this->softwareDefinedTags[$tag] );
218  $isExplicit = isset( $this->explicitlyDefinedTags[$tag] );
219  if ( $isSoftware ) {
220  // TODO: Rename this message
221  $sourceMsgs[] = $this->msg( 'tags-source-extension' )->escaped();
222  }
223  if ( $isExplicit ) {
224  $sourceMsgs[] = $this->msg( 'tags-source-manual' )->escaped();
225  }
226  if ( !$sourceMsgs ) {
227  $sourceMsgs[] = $this->msg( 'tags-source-none' )->escaped();
228  }
229  $newRow .= Xml::tags( 'td', null, implode( Xml::element( 'br' ), $sourceMsgs ) );
230 
231  $isActive = $isExplicit || isset( $this->softwareActivatedTags[$tag] );
232  $activeMsg = ( $isActive ? 'tags-active-yes' : 'tags-active-no' );
233  $newRow .= Xml::tags( 'td', null, $this->msg( $activeMsg )->escaped() );
234 
235  $hitcountLabelMsg = $this->msg( 'tags-hitcount' )->numParams( $hitcount );
236  if ( $this->getConfig()->get( 'UseTagFilter' ) ) {
237  $hitcountLabel = $linkRenderer->makeLink(
238  SpecialPage::getTitleFor( 'Recentchanges' ),
239  $hitcountLabelMsg->text(),
240  [],
241  [ 'tagfilter' => $tag ]
242  );
243  } else {
244  $hitcountLabel = $hitcountLabelMsg->escaped();
245  }
246 
247  // add raw $hitcount for sorting, because tags-hitcount contains numbers and letters
248  $newRow .= Xml::tags( 'td', [ 'data-sort-value' => $hitcount ], $hitcountLabel );
249 
250  $actionLinks = [];
251 
252  if ( $showDeleteActions && ChangeTags::canDeleteTag( $tag )->isOK() ) {
253  $actionLinks[] = $linkRenderer->makeKnownLink(
254  $this->getPageTitle( 'delete' ),
255  $this->msg( 'tags-delete' )->text(),
256  [],
257  [ 'tag' => $tag ] );
258  }
259 
260  if ( $showManageActions ) { // we've already checked that the user had the requisite userright
261  if ( ChangeTags::canActivateTag( $tag )->isOK() ) {
262  $actionLinks[] = $linkRenderer->makeKnownLink(
263  $this->getPageTitle( 'activate' ),
264  $this->msg( 'tags-activate' )->text(),
265  [],
266  [ 'tag' => $tag ] );
267  }
268 
269  if ( ChangeTags::canDeactivateTag( $tag )->isOK() ) {
270  $actionLinks[] = $linkRenderer->makeKnownLink(
271  $this->getPageTitle( 'deactivate' ),
272  $this->msg( 'tags-deactivate' )->text(),
273  [],
274  [ 'tag' => $tag ] );
275  }
276  }
277 
278  if ( $showDeleteActions || $showManageActions ) {
279  $newRow .= Xml::tags( 'td', null, $this->getLanguage()->pipeList( $actionLinks ) );
280  }
281 
282  return Xml::tags( 'tr', null, $newRow ) . "\n";
283  }
284 
285  public function processCreateTagForm( array $data, HTMLForm $form ) {
286  $context = $form->getContext();
287  $out = $context->getOutput();
288 
289  $tag = trim( strval( $data['Tag'] ) );
290  $ignoreWarnings = isset( $data['IgnoreWarnings'] ) && $data['IgnoreWarnings'] === '1';
291  $status = ChangeTags::createTagWithChecks( $tag, $data['Reason'],
292  $context->getUser(), $ignoreWarnings );
293 
294  if ( $status->isGood() ) {
295  $out->redirect( $this->getPageTitle()->getLocalURL() );
296  return true;
297  } elseif ( $status->isOK() ) {
298  // we have some warnings, so we show a confirmation form
299  $fields = [
300  'Tag' => [
301  'type' => 'hidden',
302  'default' => $data['Tag'],
303  ],
304  'Reason' => [
305  'type' => 'hidden',
306  'default' => $data['Reason'],
307  ],
308  'IgnoreWarnings' => [
309  'type' => 'hidden',
310  'default' => '1',
311  ],
312  ];
313 
314  // fool HTMLForm into thinking the form hasn't been submitted yet. Otherwise
315  // we get into an infinite loop!
316  $context->getRequest()->unsetVal( 'wpEditToken' );
317 
318  $headerText = $this->msg( 'tags-create-warnings-above', $tag,
319  count( $status->getWarningsArray() ) )->parseAsBlock() .
320  $out->parseAsInterface( $status->getWikiText() ) .
321  $this->msg( 'tags-create-warnings-below' )->parseAsBlock();
322 
323  $subform = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
324  $subform->setAction( $this->getPageTitle( 'create' )->getLocalURL() );
325  $subform->setWrapperLegendMsg( 'tags-create-heading' );
326  $subform->setHeaderText( $headerText );
327  $subform->setSubmitCallback( [ $this, 'processCreateTagForm' ] );
328  $subform->setSubmitTextMsg( 'htmlform-yes' );
329  $subform->show();
330 
331  $out->addBacklinkSubtitle( $this->getPageTitle() );
332  return true;
333  } else {
334  $out->wrapWikiTextAsInterface( 'error', $status->getWikiText() );
335  return false;
336  }
337  }
338 
339  protected function showDeleteTagForm( $tag ) {
340  $user = $this->getUser();
341  if ( !MediaWikiServices::getInstance()
343  ->userHasRight( $user, 'deletechangetags' ) ) {
344  throw new PermissionsError( 'deletechangetags' );
345  }
346 
347  $out = $this->getOutput();
348  $out->setPageTitle( $this->msg( 'tags-delete-title' ) );
349  $out->addBacklinkSubtitle( $this->getPageTitle() );
350 
351  // is the tag actually able to be deleted?
352  $canDeleteResult = ChangeTags::canDeleteTag( $tag, $user );
353  if ( !$canDeleteResult->isGood() ) {
354  $out->wrapWikiTextAsInterface( 'error', $canDeleteResult->getWikiText() );
355  if ( !$canDeleteResult->isOK() ) {
356  return;
357  }
358  }
359 
360  $preText = $this->msg( 'tags-delete-explanation-initial', $tag )->parseAsBlock();
361  $tagUsage = ChangeTags::tagUsageStatistics();
362  if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > 0 ) {
363  $preText .= $this->msg( 'tags-delete-explanation-in-use', $tag,
364  $tagUsage[$tag] )->parseAsBlock();
365  }
366  $preText .= $this->msg( 'tags-delete-explanation-warning', $tag )->parseAsBlock();
367 
368  // see if the tag is in use
369  $this->softwareActivatedTags = array_fill_keys(
371  if ( isset( $this->softwareActivatedTags[$tag] ) ) {
372  $preText .= $this->msg( 'tags-delete-explanation-active', $tag )->parseAsBlock();
373  }
374 
375  $fields = [];
376  $fields['Reason'] = [
377  'type' => 'text',
378  'label' => $this->msg( 'tags-delete-reason' )->plain(),
379  'size' => 50,
380  ];
381  $fields['HiddenTag'] = [
382  'type' => 'hidden',
383  'name' => 'tag',
384  'default' => $tag,
385  'required' => true,
386  ];
387 
388  $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
389  $form->setAction( $this->getPageTitle( 'delete' )->getLocalURL() );
390  // @phan-suppress-next-line PhanUndeclaredProperty
391  $form->tagAction = 'delete'; // custom property on HTMLForm object
392  $form->setSubmitCallback( [ $this, 'processTagForm' ] );
393  $form->setSubmitTextMsg( 'tags-delete-submit' );
394  $form->setSubmitDestructive(); // nasty!
395  $form->addPreText( $preText );
396  $form->show();
397  }
398 
399  protected function showActivateDeactivateForm( $tag, $activate ) {
400  $actionStr = $activate ? 'activate' : 'deactivate';
401 
402  $user = $this->getUser();
403  if ( !MediaWikiServices::getInstance()
405  ->userHasRight( $user, 'managechangetags' ) ) {
406  throw new PermissionsError( 'managechangetags' );
407  }
408 
409  $out = $this->getOutput();
410  // tags-activate-title, tags-deactivate-title
411  $out->setPageTitle( $this->msg( "tags-$actionStr-title" ) );
412  $out->addBacklinkSubtitle( $this->getPageTitle() );
413 
414  // is it possible to do this?
415  $func = $activate ? 'canActivateTag' : 'canDeactivateTag';
416  $result = ChangeTags::$func( $tag, $user );
417  if ( !$result->isGood() ) {
418  $out->wrapWikiTextAsInterface( 'error', $result->getWikiText() );
419  if ( !$result->isOK() ) {
420  return;
421  }
422  }
423 
424  // tags-activate-question, tags-deactivate-question
425  $preText = $this->msg( "tags-$actionStr-question", $tag )->parseAsBlock();
426 
427  $fields = [];
428  // tags-activate-reason, tags-deactivate-reason
429  $fields['Reason'] = [
430  'type' => 'text',
431  'label' => $this->msg( "tags-$actionStr-reason" )->plain(),
432  'size' => 50,
433  ];
434  $fields['HiddenTag'] = [
435  'type' => 'hidden',
436  'name' => 'tag',
437  'default' => $tag,
438  'required' => true,
439  ];
440 
441  $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
442  $form->setAction( $this->getPageTitle( $actionStr )->getLocalURL() );
443  // @phan-suppress-next-line PhanUndeclaredProperty
444  $form->tagAction = $actionStr;
445  $form->setSubmitCallback( [ $this, 'processTagForm' ] );
446  // tags-activate-submit, tags-deactivate-submit
447  $form->setSubmitTextMsg( "tags-$actionStr-submit" );
448  $form->addPreText( $preText );
449  $form->show();
450  }
451 
458  public function processTagForm( array $data, HTMLForm $form ) {
459  $context = $form->getContext();
460  $out = $context->getOutput();
461 
462  $tag = $data['HiddenTag'];
463  $status = call_user_func( [ ChangeTags::class, "{$form->tagAction}TagWithChecks" ],
464  $tag, $data['Reason'], $context->getUser(), true );
465 
466  if ( $status->isGood() ) {
467  $out->redirect( $this->getPageTitle()->getLocalURL() );
468  return true;
469  } elseif ( $status->isOK() && $form->tagAction === 'delete' ) {
470  // deletion succeeded, but hooks raised a warning
471  $out->addWikiTextAsInterface( $this->msg( 'tags-delete-warnings-after-delete', $tag,
472  count( $status->getWarningsArray() ) )->text() . "\n" .
473  $status->getWikitext() );
474  $out->addReturnTo( $this->getPageTitle() );
475  return true;
476  } else {
477  $out->wrapWikiTextAsInterface( 'error', $status->getWikitext() );
478  return false;
479  }
480  }
481 
487  public function getSubpagesForPrefixSearch() {
488  // The subpages does not have an own form, so not listing it at the moment
489  return [
490  // 'delete',
491  // 'activate',
492  // 'deactivate',
493  // 'create',
494  ];
495  }
496 
497  protected function getGroupName() {
498  return 'changes';
499  }
500 }
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:679
SpecialPage\msg
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
Definition: SpecialPage.php:810
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:40
SpecialPage\getOutput
getOutput()
Get the OutputPage being used for this instance.
Definition: SpecialPage.php:726
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:144
SpecialTags\processTagForm
processTagForm(array $data, HTMLForm $form)
Definition: SpecialTags.php:458
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:83
SpecialTags\processCreateTagForm
processCreateTagForm(array $data, HTMLForm $form)
Definition: SpecialTags.php:285
PermissionsError
Show an error when a user tries to do something they do not have the necessary permissions for.
Definition: PermissionsError.php:30
SpecialPage\getLanguage
getLanguage()
Shortcut to get user's language.
Definition: SpecialPage.php:756
SpecialTags\getSubpagesForPrefixSearch
getSubpagesForPrefixSearch()
Return an array of subpages that this special page will accept.
Definition: SpecialTags.php:487
ChangeTags\listSoftwareDefinedTags
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
Definition: ChangeTags.php:1491
SpecialPage\addHelpLink
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition: SpecialPage.php:846
SpecialPage\getConfig
getConfig()
Shortcut to get main config object.
Definition: SpecialPage.php:776
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:497
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:544
SpecialPage\getUser
getUser()
Shortcut to get the User executing this instance.
Definition: SpecialPage.php:736
ChangeTags\canActivateTag
static canActivateTag( $tag, User $user=null)
Is it OK to allow the user to activate this tag?
Definition: ChangeTags.php:1025
SpecialPage\getContext
getContext()
Gets the context this SpecialPage is executed in.
Definition: SpecialPage.php:699
ChangeTags\listExplicitlyDefinedTags
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
Definition: ChangeTags.php:1453
SpecialTags\showTagList
showTagList()
Definition: SpecialTags.php:76
SpecialPage
Parent class for all special pages.
Definition: SpecialPage.php:37
SpecialTags\showActivateDeactivateForm
showActivateDeactivateForm( $tag, $activate)
Definition: SpecialTags.php:399
SpecialTags\doTagRow
doTagRow( $tag, $hitcount, $showManageActions, $showDeleteActions, $showEditLinks)
Definition: SpecialTags.php:182
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:1319
SpecialPage\getRequest
getRequest()
Get the WebRequest being used for this instance.
Definition: SpecialPage.php:716
ChangeTags\listSoftwareActivatedTags
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
Definition: ChangeTags.php:1408
$context
$context
Definition: load.php:43
SpecialPage\getLinkRenderer
getLinkRenderer()
Definition: SpecialPage.php:922
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:339
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:1537
SpecialPage\$linkRenderer
MediaWiki Linker LinkRenderer null $linkRenderer
Definition: SpecialPage.php:67
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:1245
HTMLForm\factory
static factory( $displayFormat,... $arguments)
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:307
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:646
ChangeTags\tagDescription
static tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
Definition: ChangeTags.php:188
ChangeTags\canDeactivateTag
static canDeactivateTag( $tag, User $user=null)
Is it OK to allow the user to deactivate this tag?
Definition: ChangeTags.php:1099
HTMLForm
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition: HTMLForm.php:131