MediaWiki  master
ChangeTags.php
Go to the documentation of this file.
1 <?php
28 
29 class ChangeTags {
35  const MAX_DELETE_USES = 5000;
36 
40  private static $definedSoftwareTags = [
41  'mw-contentmodelchange',
42  'mw-new-redirect',
43  'mw-removed-redirect',
44  'mw-changed-redirect-target',
45  'mw-blank',
46  'mw-replace',
47  'mw-rollback',
48  'mw-undo',
49  ];
50 
58  public static function getSoftwareTags( $all = false ) {
59  global $wgSoftwareTags;
60  $softwareTags = [];
61 
62  if ( !is_array( $wgSoftwareTags ) ) {
63  wfWarn( 'wgSoftwareTags should be associative array of enabled tags.
64  Please refer to documentation for the list of tags you can enable' );
65  return $softwareTags;
66  }
67 
68  $availableSoftwareTags = !$all ?
69  array_keys( array_filter( $wgSoftwareTags ) ) :
70  array_keys( $wgSoftwareTags );
71 
72  $softwareTags = array_intersect(
73  $availableSoftwareTags,
74  self::$definedSoftwareTags
75  );
76 
77  return $softwareTags;
78  }
79 
94  public static function formatSummaryRow( $tags, $page, IContextSource $context = null ) {
95  if ( !$tags ) {
96  return [ '', [] ];
97  }
98  if ( !$context ) {
100  }
101 
102  $classes = [];
103 
104  $tags = explode( ',', $tags );
105  $displayTags = [];
106  foreach ( $tags as $tag ) {
107  if ( !$tag ) {
108  continue;
109  }
110  $description = self::tagDescription( $tag, $context );
111  if ( $description === false ) {
112  continue;
113  }
114  $displayTags[] = Xml::tags(
115  'span',
116  [ 'class' => 'mw-tag-marker ' .
117  Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
118  $description
119  );
120  $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
121  }
122 
123  if ( !$displayTags ) {
124  return [ '', [] ];
125  }
126 
127  $markers = $context->msg( 'tag-list-wrapper' )
128  ->numParams( count( $displayTags ) )
129  ->rawParams( implode( ' ', $displayTags ) )
130  ->parse();
131  $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
132 
133  return [ $markers, $classes ];
134  }
135 
149  public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
150  $msg = $context->msg( "tag-$tag" );
151  if ( !$msg->exists() ) {
152  // No such message
153  return ( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) )
154  // HACK MessageLocalizer doesn't have a way to set the right language on a RawMessage,
155  // so extract the language from $msg and use that.
156  // The language doesn't really matter, but we need to set it to avoid requesting
157  // the user's language from session-less entry points (T227233)
158  ->inLanguage( $msg->getLanguage() );
159 
160  }
161  if ( $msg->isDisabled() ) {
162  // The message exists but is disabled, hide the tag.
163  return false;
164  }
165 
166  // Message exists and isn't disabled, use it.
167  return $msg;
168  }
169 
183  public static function tagDescription( $tag, MessageLocalizer $context ) {
184  $msg = self::tagShortDescriptionMessage( $tag, $context );
185  return $msg ? $msg->parse() : false;
186  }
187 
200  public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
201  $msg = $context->msg( "tag-$tag-description" );
202  if ( !$msg->exists() ) {
203  return false;
204  }
205  if ( $msg->isDisabled() ) {
206  // The message exists but is disabled, hide the description.
207  return false;
208  }
209 
210  // Message exists and isn't disabled, use it.
211  return $msg;
212  }
213 
223  public static function truncateTagDescription( $tag, $length, IContextSource $context ) {
224  // FIXME: Make this accept MessageLocalizer and Language instead of IContextSource
225 
226  $originalDesc = self::tagLongDescriptionMessage( $tag, $context );
227  // If there is no tag description, return empty string
228  if ( !$originalDesc ) {
229  return '';
230  }
231 
232  $taglessDesc = Sanitizer::stripAllTags( $originalDesc->parse() );
233 
234  return $context->getLanguage()->truncateForVisual( $taglessDesc, $length );
235  }
236 
251  public static function addTags( $tags, $rc_id = null, $rev_id = null,
252  $log_id = null, $params = null, RecentChange $rc = null
253  ) {
254  $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
255  return (bool)$result[0];
256  }
257 
288  public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
289  &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
290  User $user = null
291  ) {
292  $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags...
293  $tagsToRemove = array_filter( (array)$tagsToRemove );
294 
295  if ( !$rc_id && !$rev_id && !$log_id ) {
296  throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
297  'specified when adding or removing a tag from a change!' );
298  }
299 
300  $dbw = wfGetDB( DB_MASTER );
301 
302  // Might as well look for rcids and so on.
303  if ( !$rc_id ) {
304  // Info might be out of date, somewhat fractionally, on replica DB.
305  // LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
306  // so use that relation to avoid full table scans.
307  if ( $log_id ) {
308  $rc_id = $dbw->selectField(
309  [ 'logging', 'recentchanges' ],
310  'rc_id',
311  [
312  'log_id' => $log_id,
313  'rc_timestamp = log_timestamp',
314  'rc_logid = log_id'
315  ],
316  __METHOD__
317  );
318  } elseif ( $rev_id ) {
319  $rc_id = $dbw->selectField(
320  [ 'revision', 'recentchanges' ],
321  'rc_id',
322  [
323  'rev_id' => $rev_id,
324  'rc_timestamp = rev_timestamp',
325  'rc_this_oldid = rev_id'
326  ],
327  __METHOD__
328  );
329  }
330  } elseif ( !$log_id && !$rev_id ) {
331  // Info might be out of date, somewhat fractionally, on replica DB.
332  $log_id = $dbw->selectField(
333  'recentchanges',
334  'rc_logid',
335  [ 'rc_id' => $rc_id ],
336  __METHOD__
337  );
338  $rev_id = $dbw->selectField(
339  'recentchanges',
340  'rc_this_oldid',
341  [ 'rc_id' => $rc_id ],
342  __METHOD__
343  );
344  }
345 
346  if ( $log_id && !$rev_id ) {
347  $rev_id = $dbw->selectField(
348  'log_search',
349  'ls_value',
350  [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ],
351  __METHOD__
352  );
353  } elseif ( !$log_id && $rev_id ) {
354  $log_id = $dbw->selectField(
355  'log_search',
356  'ls_log_id',
357  [ 'ls_field' => 'associated_rev_id', 'ls_value' => $rev_id ],
358  __METHOD__
359  );
360  }
361 
362  $prevTags = self::getTags( $dbw, $rc_id, $rev_id, $log_id );
363 
364  // add tags
365  $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
366  $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
367 
368  // remove tags
369  $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
370  $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
371 
372  sort( $prevTags );
373  sort( $newTags );
374  if ( $prevTags == $newTags ) {
375  return [ [], [], $prevTags ];
376  }
377 
378  // insert a row into change_tag for each new tag
379  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
380  if ( count( $tagsToAdd ) ) {
381  $changeTagMapping = [];
382  foreach ( $tagsToAdd as $tag ) {
383  $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
384  }
385  $fname = __METHOD__;
386  // T207881: update the counts at the end of the transaction
387  $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tagsToAdd, $fname ) {
388  $dbw->update(
389  'change_tag_def',
390  [ 'ctd_count = ctd_count + 1' ],
391  [ 'ctd_name' => $tagsToAdd ],
392  $fname
393  );
394  } );
395 
396  $tagsRows = [];
397  foreach ( $tagsToAdd as $tag ) {
398  // Filter so we don't insert NULLs as zero accidentally.
399  // Keep in mind that $rc_id === null means "I don't care/know about the
400  // rc_id, just delete $tag on this revision/log entry". It doesn't
401  // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
402  $tagsRows[] = array_filter(
403  [
404  'ct_rc_id' => $rc_id,
405  'ct_log_id' => $log_id,
406  'ct_rev_id' => $rev_id,
407  'ct_params' => $params,
408  'ct_tag_id' => $changeTagMapping[$tag] ?? null,
409  ]
410  );
411 
412  }
413 
414  $dbw->insert( 'change_tag', $tagsRows, __METHOD__, [ 'IGNORE' ] );
415  }
416 
417  // delete from change_tag
418  if ( count( $tagsToRemove ) ) {
419  $fname = __METHOD__;
420  foreach ( $tagsToRemove as $tag ) {
421  $conds = array_filter(
422  [
423  'ct_rc_id' => $rc_id,
424  'ct_log_id' => $log_id,
425  'ct_rev_id' => $rev_id,
426  'ct_tag_id' => $changeTagDefStore->getId( $tag ),
427  ]
428  );
429  $dbw->delete( 'change_tag', $conds, __METHOD__ );
430  if ( $dbw->affectedRows() ) {
431  // T207881: update the counts at the end of the transaction
432  $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tag, $fname ) {
433  $dbw->update(
434  'change_tag_def',
435  [ 'ctd_count = ctd_count - 1' ],
436  [ 'ctd_name' => $tag ],
437  $fname
438  );
439 
440  $dbw->delete(
441  'change_tag_def',
442  [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
443  $fname
444  );
445  } );
446  }
447  }
448  }
449 
450  Hooks::run( 'ChangeTagsAfterUpdateTags', [ $tagsToAdd, $tagsToRemove, $prevTags,
451  $rc_id, $rev_id, $log_id, $params, $rc, $user ] );
452 
453  return [ $tagsToAdd, $tagsToRemove, $prevTags ];
454  }
455 
466  public static function getTags( IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
467  $conds = array_filter(
468  [
469  'ct_rc_id' => $rc_id,
470  'ct_rev_id' => $rev_id,
471  'ct_log_id' => $log_id,
472  ]
473  );
474 
475  $tagIds = $db->selectFieldValues(
476  'change_tag',
477  'ct_tag_id',
478  $conds,
479  __METHOD__
480  );
481 
482  $tags = [];
483  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
484  foreach ( $tagIds as $tagId ) {
485  $tags[] = $changeTagDefStore->getName( (int)$tagId );
486  }
487 
488  return $tags;
489  }
490 
501  protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
502  $lang = RequestContext::getMain()->getLanguage();
503  $count = count( $tags );
504  return Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
505  $lang->commaList( $tags ), $count );
506  }
507 
521  public static function canAddTagsAccompanyingChange( array $tags, User $user = null ) {
522  if ( !is_null( $user ) ) {
523  if ( !MediaWikiServices::getInstance()->getPermissionManager()
524  ->userHasRight( $user, 'applychangetags' )
525  ) {
526  return Status::newFatal( 'tags-apply-no-permission' );
527  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
528  return Status::newFatal( 'tags-apply-blocked', $user->getName() );
529  }
530  }
531 
532  // to be applied, a tag has to be explicitly defined
533  $allowedTags = self::listExplicitlyDefinedTags();
534  Hooks::run( 'ChangeTagsAllowedAdd', [ &$allowedTags, $tags, $user ] );
535  $disallowedTags = array_diff( $tags, $allowedTags );
536  if ( $disallowedTags ) {
537  return self::restrictedTagError( 'tags-apply-not-allowed-one',
538  'tags-apply-not-allowed-multi', $disallowedTags );
539  }
540 
541  return Status::newGood();
542  }
543 
564  public static function addTagsAccompanyingChangeWithChecks(
565  array $tags, $rc_id, $rev_id, $log_id, $params, User $user
566  ) {
567  // are we allowed to do this?
568  $result = self::canAddTagsAccompanyingChange( $tags, $user );
569  if ( !$result->isOK() ) {
570  $result->value = null;
571  return $result;
572  }
573 
574  // do it!
575  self::addTags( $tags, $rc_id, $rev_id, $log_id, $params );
576 
577  return Status::newGood( true );
578  }
579 
594  public static function canUpdateTags( array $tagsToAdd, array $tagsToRemove,
595  User $user = null
596  ) {
597  if ( !is_null( $user ) ) {
598  if ( !MediaWikiServices::getInstance()->getPermissionManager()
599  ->userHasRight( $user, 'changetags' )
600  ) {
601  return Status::newFatal( 'tags-update-no-permission' );
602  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
603  return Status::newFatal( 'tags-update-blocked', $user->getName() );
604  }
605  }
606 
607  if ( $tagsToAdd ) {
608  // to be added, a tag has to be explicitly defined
609  // @todo Allow extensions to define tags that can be applied by users...
610  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
611  $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
612  if ( $diff ) {
613  return self::restrictedTagError( 'tags-update-add-not-allowed-one',
614  'tags-update-add-not-allowed-multi', $diff );
615  }
616  }
617 
618  if ( $tagsToRemove ) {
619  // to be removed, a tag must not be defined by an extension, or equivalently it
620  // has to be either explicitly defined or not defined at all
621  // (assuming no edge case of a tag both explicitly-defined and extension-defined)
622  $softwareDefinedTags = self::listSoftwareDefinedTags();
623  $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
624  if ( $intersect ) {
625  return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
626  'tags-update-remove-not-allowed-multi', $intersect );
627  }
628  }
629 
630  return Status::newGood();
631  }
632 
663  public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
664  $rc_id, $rev_id, $log_id, $params, $reason, User $user
665  ) {
666  if ( is_null( $tagsToAdd ) ) {
667  $tagsToAdd = [];
668  }
669  if ( is_null( $tagsToRemove ) ) {
670  $tagsToRemove = [];
671  }
672  if ( !$tagsToAdd && !$tagsToRemove ) {
673  // no-op, don't bother
674  return Status::newGood( (object)[
675  'logId' => null,
676  'addedTags' => [],
677  'removedTags' => [],
678  ] );
679  }
680 
681  // are we allowed to do this?
682  $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $user );
683  if ( !$result->isOK() ) {
684  $result->value = null;
685  return $result;
686  }
687 
688  // basic rate limiting
689  if ( $user->pingLimiter( 'changetag' ) ) {
690  return Status::newFatal( 'actionthrottledtext' );
691  }
692 
693  // do it!
694  list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd,
695  $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user );
696  if ( !$tagsAdded && !$tagsRemoved ) {
697  // no-op, don't log it
698  return Status::newGood( (object)[
699  'logId' => null,
700  'addedTags' => [],
701  'removedTags' => [],
702  ] );
703  }
704 
705  // log it
706  $logEntry = new ManualLogEntry( 'tag', 'update' );
707  $logEntry->setPerformer( $user );
708  $logEntry->setComment( $reason );
709 
710  // find the appropriate target page
711  if ( $rev_id ) {
712  $rev = Revision::newFromId( $rev_id );
713  if ( $rev ) {
714  $logEntry->setTarget( $rev->getTitle() );
715  }
716  } elseif ( $log_id ) {
717  // This function is from revision deletion logic and has nothing to do with
718  // change tags, but it appears to be the only other place in core where we
719  // perform logged actions on log items.
720  $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
721  }
722 
723  if ( !$logEntry->getTarget() ) {
724  // target is required, so we have to set something
725  $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
726  }
727 
728  $logParams = [
729  '4::revid' => $rev_id,
730  '5::logid' => $log_id,
731  '6:list:tagsAdded' => $tagsAdded,
732  '7:number:tagsAddedCount' => count( $tagsAdded ),
733  '8:list:tagsRemoved' => $tagsRemoved,
734  '9:number:tagsRemovedCount' => count( $tagsRemoved ),
735  'initialTags' => $initialTags,
736  ];
737  $logEntry->setParameters( $logParams );
738  $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
739 
740  $dbw = wfGetDB( DB_MASTER );
741  $logId = $logEntry->insert( $dbw );
742  // Only send this to UDP, not RC, similar to patrol events
743  $logEntry->publish( $logId, 'udp' );
744 
745  return Status::newGood( (object)[
746  'logId' => $logId,
747  'addedTags' => $tagsAdded,
748  'removedTags' => $tagsRemoved,
749  ] );
750  }
751 
772  public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
773  &$join_conds, &$options, $filter_tag = ''
774  ) {
775  global $wgUseTagFilter;
776 
777  // Normalize to arrays
778  $tables = (array)$tables;
779  $fields = (array)$fields;
780  $conds = (array)$conds;
781  $options = (array)$options;
782 
783  $fields['ts_tags'] = self::makeTagSummarySubquery( $tables );
784 
785  // Figure out which ID field to use
786  if ( in_array( 'recentchanges', $tables ) ) {
787  $join_cond = 'ct_rc_id=rc_id';
788  } elseif ( in_array( 'logging', $tables ) ) {
789  $join_cond = 'ct_log_id=log_id';
790  } elseif ( in_array( 'revision', $tables ) ) {
791  $join_cond = 'ct_rev_id=rev_id';
792  } elseif ( in_array( 'archive', $tables ) ) {
793  $join_cond = 'ct_rev_id=ar_rev_id';
794  } else {
795  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
796  }
797 
798  if ( $wgUseTagFilter && $filter_tag ) {
799  // Somebody wants to filter on a tag.
800  // Add an INNER JOIN on change_tag
801 
802  $tables[] = 'change_tag';
803  $join_conds['change_tag'] = [ 'JOIN', $join_cond ];
804  $filterTagIds = [];
805  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
806  foreach ( (array)$filter_tag as $filterTagName ) {
807  try {
808  $filterTagIds[] = $changeTagDefStore->getId( $filterTagName );
809  } catch ( NameTableAccessException $exception ) {
810  // Return nothing.
811  $conds[] = '0';
812  break;
813  }
814  }
815 
816  if ( $filterTagIds !== [] ) {
817  $conds['ct_tag_id'] = $filterTagIds;
818  }
819 
820  if (
821  is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
822  !in_array( 'DISTINCT', $options )
823  ) {
824  $options[] = 'DISTINCT';
825  }
826  }
827  }
828 
837  public static function makeTagSummarySubquery( $tables ) {
838  // Normalize to arrays
839  $tables = (array)$tables;
840 
841  // Figure out which ID field to use
842  if ( in_array( 'recentchanges', $tables ) ) {
843  $join_cond = 'ct_rc_id=rc_id';
844  } elseif ( in_array( 'logging', $tables ) ) {
845  $join_cond = 'ct_log_id=log_id';
846  } elseif ( in_array( 'revision', $tables ) ) {
847  $join_cond = 'ct_rev_id=rev_id';
848  } elseif ( in_array( 'archive', $tables ) ) {
849  $join_cond = 'ct_rev_id=ar_rev_id';
850  } else {
851  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
852  }
853 
854  $tagTables = [ 'change_tag', 'change_tag_def' ];
855  $join_cond_ts_tags = [ 'change_tag_def' => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
856  $field = 'ctd_name';
857 
858  return wfGetDB( DB_REPLICA )->buildGroupConcatField(
859  ',', $tagTables, $field, $join_cond, $join_cond_ts_tags
860  );
861  }
862 
874  public static function buildTagFilterSelector(
875  $selected = '', $ooui = false, IContextSource $context = null
876  ) {
877  if ( !$context ) {
879  }
880 
881  $config = $context->getConfig();
882  if ( !$config->get( 'UseTagFilter' ) || !count( self::listDefinedTags() ) ) {
883  return [];
884  }
885 
886  $data = [
888  'label',
889  [ 'for' => 'tagfilter' ],
890  $context->msg( 'tag-filter' )->parse()
891  )
892  ];
893 
894  if ( $ooui ) {
895  $data[] = new OOUI\TextInputWidget( [
896  'id' => 'tagfilter',
897  'name' => 'tagfilter',
898  'value' => $selected,
899  'classes' => 'mw-tagfilter-input',
900  ] );
901  } else {
902  $data[] = Xml::input(
903  'tagfilter',
904  20,
905  $selected,
906  [ 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' ]
907  );
908  }
909 
910  return $data;
911  }
912 
921  public static function defineTag( $tag ) {
922  $dbw = wfGetDB( DB_MASTER );
923  $tagDef = [
924  'ctd_name' => $tag,
925  'ctd_user_defined' => 1,
926  'ctd_count' => 0
927  ];
928  $dbw->upsert(
929  'change_tag_def',
930  $tagDef,
931  [ 'ctd_name' ],
932  [ 'ctd_user_defined' => 1 ],
933  __METHOD__
934  );
935 
936  // clear the memcache of defined tags
937  self::purgeTagCacheAll();
938  }
939 
948  public static function undefineTag( $tag ) {
949  $dbw = wfGetDB( DB_MASTER );
950 
951  $dbw->update(
952  'change_tag_def',
953  [ 'ctd_user_defined' => 0 ],
954  [ 'ctd_name' => $tag ],
955  __METHOD__
956  );
957 
958  $dbw->delete(
959  'change_tag_def',
960  [ 'ctd_name' => $tag, 'ctd_count' => 0 ],
961  __METHOD__
962  );
963 
964  // clear the memcache of defined tags
965  self::purgeTagCacheAll();
966  }
967 
982  protected static function logTagManagementAction( $action, $tag, $reason,
983  User $user, $tagCount = null, array $logEntryTags = []
984  ) {
985  $dbw = wfGetDB( DB_MASTER );
986 
987  $logEntry = new ManualLogEntry( 'managetags', $action );
988  $logEntry->setPerformer( $user );
989  // target page is not relevant, but it has to be set, so we just put in
990  // the title of Special:Tags
991  $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
992  $logEntry->setComment( $reason );
993 
994  $params = [ '4::tag' => $tag ];
995  if ( !is_null( $tagCount ) ) {
996  $params['5:number:count'] = $tagCount;
997  }
998  $logEntry->setParameters( $params );
999  $logEntry->setRelations( [ 'Tag' => $tag ] );
1000  $logEntry->addTags( $logEntryTags );
1001 
1002  $logId = $logEntry->insert( $dbw );
1003  $logEntry->publish( $logId );
1004  return $logId;
1005  }
1006 
1016  public static function canActivateTag( $tag, User $user = null ) {
1017  if ( !is_null( $user ) ) {
1018  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1019  ->userHasRight( $user, 'managechangetags' )
1020  ) {
1021  return Status::newFatal( 'tags-manage-no-permission' );
1022  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1023  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1024  }
1025  }
1026 
1027  // defined tags cannot be activated (a defined tag is either extension-
1028  // defined, in which case the extension chooses whether or not to active it;
1029  // or user-defined, in which case it is considered active)
1030  $definedTags = self::listDefinedTags();
1031  if ( in_array( $tag, $definedTags ) ) {
1032  return Status::newFatal( 'tags-activate-not-allowed', $tag );
1033  }
1034 
1035  // non-existing tags cannot be activated
1036  $tagUsage = self::tagUsageStatistics();
1037  if ( !isset( $tagUsage[$tag] ) ) { // we already know the tag is undefined
1038  return Status::newFatal( 'tags-activate-not-found', $tag );
1039  }
1040 
1041  return Status::newGood();
1042  }
1043 
1061  public static function activateTagWithChecks( $tag, $reason, User $user,
1062  $ignoreWarnings = false, array $logEntryTags = []
1063  ) {
1064  // are we allowed to do this?
1065  $result = self::canActivateTag( $tag, $user );
1066  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1067  $result->value = null;
1068  return $result;
1069  }
1070 
1071  // do it!
1072  self::defineTag( $tag );
1073 
1074  // log it
1075  $logId = self::logTagManagementAction( 'activate', $tag, $reason, $user,
1076  null, $logEntryTags );
1077 
1078  return Status::newGood( $logId );
1079  }
1080 
1090  public static function canDeactivateTag( $tag, User $user = null ) {
1091  if ( !is_null( $user ) ) {
1092  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1093  ->userHasRight( $user, 'managechangetags' )
1094  ) {
1095  return Status::newFatal( 'tags-manage-no-permission' );
1096  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1097  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1098  }
1099  }
1100 
1101  // only explicitly-defined tags can be deactivated
1102  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
1103  if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
1104  return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
1105  }
1106  return Status::newGood();
1107  }
1108 
1126  public static function deactivateTagWithChecks( $tag, $reason, User $user,
1127  $ignoreWarnings = false, array $logEntryTags = []
1128  ) {
1129  // are we allowed to do this?
1130  $result = self::canDeactivateTag( $tag, $user );
1131  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1132  $result->value = null;
1133  return $result;
1134  }
1135 
1136  // do it!
1137  self::undefineTag( $tag );
1138 
1139  // log it
1140  $logId = self::logTagManagementAction( 'deactivate', $tag, $reason, $user,
1141  null, $logEntryTags );
1142 
1143  return Status::newGood( $logId );
1144  }
1145 
1153  public static function isTagNameValid( $tag ) {
1154  // no empty tags
1155  if ( $tag === '' ) {
1156  return Status::newFatal( 'tags-create-no-name' );
1157  }
1158 
1159  // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
1160  // pipe (used as a delimiter between multiple tags in
1161  // SpecialRecentchanges and friends), or slashes (would break tag description messages in
1162  // MediaWiki namespace)
1163  if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
1164  || strpos( $tag, '/' ) !== false ) {
1165  return Status::newFatal( 'tags-create-invalid-chars' );
1166  }
1167 
1168  // could the MediaWiki namespace description messages be created?
1169  $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
1170  if ( is_null( $title ) ) {
1171  return Status::newFatal( 'tags-create-invalid-title-chars' );
1172  }
1173 
1174  return Status::newGood();
1175  }
1176 
1189  public static function canCreateTag( $tag, User $user = null ) {
1190  if ( !is_null( $user ) ) {
1191  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1192  ->userHasRight( $user, 'managechangetags' )
1193  ) {
1194  return Status::newFatal( 'tags-manage-no-permission' );
1195  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1196  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1197  }
1198  }
1199 
1200  $status = self::isTagNameValid( $tag );
1201  if ( !$status->isGood() ) {
1202  return $status;
1203  }
1204 
1205  // does the tag already exist?
1206  $tagUsage = self::tagUsageStatistics();
1207  if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) {
1208  return Status::newFatal( 'tags-create-already-exists', $tag );
1209  }
1210 
1211  // check with hooks
1212  $canCreateResult = Status::newGood();
1213  Hooks::run( 'ChangeTagCanCreate', [ $tag, $user, &$canCreateResult ] );
1214  return $canCreateResult;
1215  }
1216 
1236  public static function createTagWithChecks( $tag, $reason, User $user,
1237  $ignoreWarnings = false, array $logEntryTags = []
1238  ) {
1239  // are we allowed to do this?
1240  $result = self::canCreateTag( $tag, $user );
1241  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1242  $result->value = null;
1243  return $result;
1244  }
1245 
1246  // do it!
1247  self::defineTag( $tag );
1248 
1249  // log it
1250  $logId = self::logTagManagementAction( 'create', $tag, $reason, $user,
1251  null, $logEntryTags );
1252 
1253  return Status::newGood( $logId );
1254  }
1255 
1268  public static function deleteTagEverywhere( $tag ) {
1269  $dbw = wfGetDB( DB_MASTER );
1270  $dbw->startAtomic( __METHOD__ );
1271 
1272  // fetch tag id, this must be done before calling undefineTag(), see T225564
1273  $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag );
1274 
1275  // set ctd_user_defined = 0
1276  self::undefineTag( $tag );
1277 
1278  // delete from change_tag
1279  $dbw->delete( 'change_tag', [ 'ct_tag_id' => $tagId ], __METHOD__ );
1280  $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag ], __METHOD__ );
1281  $dbw->endAtomic( __METHOD__ );
1282 
1283  // give extensions a chance
1284  $status = Status::newGood();
1285  Hooks::run( 'ChangeTagAfterDelete', [ $tag, &$status ] );
1286  // let's not allow error results, as the actual tag deletion succeeded
1287  if ( !$status->isOK() ) {
1288  wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' );
1289  $status->setOK( true );
1290  }
1291 
1292  // clear the memcache of defined tags
1293  self::purgeTagCacheAll();
1294 
1295  return $status;
1296  }
1297 
1307  public static function canDeleteTag( $tag, User $user = null ) {
1308  $tagUsage = self::tagUsageStatistics();
1309 
1310  if ( !is_null( $user ) ) {
1311  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1312  ->userHasRight( $user, 'deletechangetags' )
1313  ) {
1314  return Status::newFatal( 'tags-delete-no-permission' );
1315  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1316  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1317  }
1318  }
1319 
1320  if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) {
1321  return Status::newFatal( 'tags-delete-not-found', $tag );
1322  }
1323 
1324  if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > self::MAX_DELETE_USES ) {
1325  return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1326  }
1327 
1328  $softwareDefined = self::listSoftwareDefinedTags();
1329  if ( in_array( $tag, $softwareDefined ) ) {
1330  // extension-defined tags can't be deleted unless the extension
1331  // specifically allows it
1332  $status = Status::newFatal( 'tags-delete-not-allowed' );
1333  } else {
1334  // user-defined tags are deletable unless otherwise specified
1335  $status = Status::newGood();
1336  }
1337 
1338  Hooks::run( 'ChangeTagCanDelete', [ $tag, $user, &$status ] );
1339  return $status;
1340  }
1341 
1359  public static function deleteTagWithChecks( $tag, $reason, User $user,
1360  $ignoreWarnings = false, array $logEntryTags = []
1361  ) {
1362  // are we allowed to do this?
1363  $result = self::canDeleteTag( $tag, $user );
1364  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1365  $result->value = null;
1366  return $result;
1367  }
1368 
1369  // store the tag usage statistics
1370  $tagUsage = self::tagUsageStatistics();
1371  $hitcount = $tagUsage[$tag] ?? 0;
1372 
1373  // do it!
1374  $deleteResult = self::deleteTagEverywhere( $tag );
1375  if ( !$deleteResult->isOK() ) {
1376  return $deleteResult;
1377  }
1378 
1379  // log it
1380  $logId = self::logTagManagementAction( 'delete', $tag, $reason, $user,
1381  $hitcount, $logEntryTags );
1382 
1383  $deleteResult->value = $logId;
1384  return $deleteResult;
1385  }
1386 
1393  public static function listSoftwareActivatedTags() {
1394  // core active tags
1395  $tags = self::getSoftwareTags();
1396  if ( !Hooks::isRegistered( 'ChangeTagsListActive' ) ) {
1397  return $tags;
1398  }
1399  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1400  return $cache->getWithSetCallback(
1401  $cache->makeKey( 'active-tags' ),
1403  function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags ) {
1404  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1405 
1406  // Ask extensions which tags they consider active
1407  Hooks::run( 'ChangeTagsListActive', [ &$tags ] );
1408  return $tags;
1409  },
1410  [
1411  'checkKeys' => [ $cache->makeKey( 'active-tags' ) ],
1412  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1414  ]
1415  );
1416  }
1417 
1424  public static function listDefinedTags() {
1425  $tags1 = self::listExplicitlyDefinedTags();
1426  $tags2 = self::listSoftwareDefinedTags();
1427  return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
1428  }
1429 
1438  public static function listExplicitlyDefinedTags() {
1439  $fname = __METHOD__;
1440 
1441  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1442  return $cache->getWithSetCallback(
1443  $cache->makeKey( 'valid-tags-db' ),
1445  function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1446  $dbr = wfGetDB( DB_REPLICA );
1447 
1448  $setOpts += Database::getCacheSetOptions( $dbr );
1449 
1450  $tags = $dbr->selectFieldValues(
1451  'change_tag_def',
1452  'ctd_name',
1453  [ 'ctd_user_defined' => 1 ],
1454  $fname
1455  );
1456 
1457  return array_filter( array_unique( $tags ) );
1458  },
1459  [
1460  'checkKeys' => [ $cache->makeKey( 'valid-tags-db' ) ],
1461  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1463  ]
1464  );
1465  }
1466 
1476  public static function listSoftwareDefinedTags() {
1477  // core defined tags
1478  $tags = self::getSoftwareTags( true );
1479  if ( !Hooks::isRegistered( 'ListDefinedTags' ) ) {
1480  return $tags;
1481  }
1482  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1483  return $cache->getWithSetCallback(
1484  $cache->makeKey( 'valid-tags-hook' ),
1486  function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags ) {
1487  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1488 
1489  Hooks::run( 'ListDefinedTags', [ &$tags ] );
1490  return array_filter( array_unique( $tags ) );
1491  },
1492  [
1493  'checkKeys' => [ $cache->makeKey( 'valid-tags-hook' ) ],
1494  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1496  ]
1497  );
1498  }
1499 
1505  public static function purgeTagCacheAll() {
1506  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1507 
1508  $cache->touchCheckKey( $cache->makeKey( 'active-tags' ) );
1509  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-db' ) );
1510  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-hook' ) );
1511  $cache->touchCheckKey( $cache->makeKey( 'tags-usage-statistics' ) );
1512 
1513  MediaWikiServices::getInstance()->getChangeTagDefStore()->reloadMap();
1514  }
1515 
1522  public static function tagUsageStatistics() {
1523  $fname = __METHOD__;
1524 
1525  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1526  return $cache->getWithSetCallback(
1527  $cache->makeKey( 'tags-usage-statistics' ),
1529  function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1530  $dbr = wfGetDB( DB_REPLICA );
1531  $res = $dbr->select(
1532  'change_tag_def',
1533  [ 'ctd_name', 'ctd_count' ],
1534  [],
1535  $fname,
1536  [ 'ORDER BY' => 'ctd_count DESC' ]
1537  );
1538 
1539  $out = [];
1540  foreach ( $res as $row ) {
1541  $out[$row->ctd_name] = $row->ctd_count;
1542  }
1543 
1544  return $out;
1545  },
1546  [
1547  'checkKeys' => [ $cache->makeKey( 'tags-usage-statistics' ) ],
1548  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1550  ]
1551  );
1552  }
1553 
1568  public static function showTagEditingUI( User $user ) {
1569  return MediaWikiServices::getInstance()->getPermissionManager()
1570  ->userHasRight( $user, 'changetags' ) &&
1571  (bool)self::listExplicitlyDefinedTags();
1572  }
1573 }
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
Definition: ChangeTags.php:772
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
static listDefinedTags()
Basically lists defined tags which count even if they aren&#39;t applied to anything. ...
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
static tagShortDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag&#39;s short description.
Definition: ChangeTags.php:149
$context
Definition: load.php:45
static deleteTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Deletes a tag, checking whether it is allowed first, and adding a log entry afterwards.
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
$wgUseTagFilter
Allow filtering by change tag in recentchanges, history, etc Has no effect if no tags are defined in ...
static truncateTagDescription( $tag, $length, IContextSource $context)
Get truncated message for the tag&#39;s long description.
Definition: ChangeTags.php:223
Exception representing a failure to look up a row from a name table.
if(!isset( $args[0])) $lang
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
static $definedSoftwareTags
A list of tags defined and used by MediaWiki itself.
Definition: ChangeTags.php:40
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition: Xml.php:274
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.
static canUpdateTags(array $tagsToAdd, array $tagsToRemove, User $user=null)
Is it OK to allow the user to adds and remove the given tags tags to/from a change?
Definition: ChangeTags.php:594
const DB_MASTER
Definition: defines.php:26
static canDeleteTag( $tag, User $user=null)
Is it OK to allow the user to delete this tag?
static undefineTag( $tag)
Update ctd_user_defined = 0 field in change_tag_def.
Definition: ChangeTags.php:948
static getTags(IDatabase $db, $rc_id=null, $rev_id=null, $log_id=null)
Return all the tags associated with the given recent change ID, revision ID, and/or log entry ID...
Definition: ChangeTags.php:466
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
static logTagManagementAction( $action, $tag, $reason, User $user, $tagCount=null, array $logEntryTags=[])
Writes a tag action into the tag management log.
Definition: ChangeTags.php:982
getPermissionManager()
static deleteTagEverywhere( $tag)
Permanently removes all traces of a tag from the DB.
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag&#39;s long description.
Definition: ChangeTags.php:200
static stripAllTags( $html)
Take a fragment of (potentially invalid) HTML and return a version with any tags removed, encoded as plain text.
Definition: Sanitizer.php:2041
static canActivateTag( $tag, User $user=null)
Is it OK to allow the user to activate this tag?
static getMain()
Get the RequestContext object associated with the main request.
msg( $key,... $params)
This is the method for getting translated interface messages.
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
const MAX_DELETE_USES
Can&#39;t delete tags with more than this many uses.
Definition: ChangeTags.php:35
static restrictedTagError( $msgOne, $msgMulti, $tags)
Helper function to generate a fatal status with a &#39;not-allowed&#39; type error.
Definition: ChangeTags.php:501
$cache
Definition: mcc.php:33
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:130
static showTagEditingUI(User $user)
Indicate whether change tag editing UI is relevant.
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:58
static isTagNameValid( $tag)
Is the tag name valid?
static escapeClass( $class)
Given a value, escape it so that it can be used as a CSS class and return it.
Definition: Sanitizer.php:1418
static updateTags( $tagsToAdd, $tagsToRemove, &$rc_id=null, &$rev_id=null, &$log_id=null, $params=null, RecentChange $rc=null, User $user=null)
Add and remove tags to/from a change given its rc_id, rev_id and/or log_id, without verifying that th...
Definition: ChangeTags.php:288
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
const NS_MEDIAWIKI
Definition: Defines.php:68
array $wgSoftwareTags
List of core tags to enable.
static deactivateTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Deactivates a tag, checking whether it is allowed first, and adding a log entry afterwards.
static addTagsAccompanyingChangeWithChecks(array $tags, $rc_id, $rev_id, $log_id, $params, User $user)
Adds tags to a given change, checking whether it is allowed first, but without adding a log entry...
Definition: ChangeTags.php:564
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:612
static isRegistered( $name)
Returns true if a hook has a function registered to it.
Definition: Hooks.php:80
selectFieldValues( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a list of single field values from result rows.
static createTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Creates a tag by adding it to change_tag_def table.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
static purgeTagCacheAll()
Invalidates the short-term cache of defined tags used by the list*DefinedTags functions, as well as the tag statistics cache.
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:1908
static defineTag( $tag)
Set ctd_user_defined = 1 in change_tag_def without checking that the tag name is valid.
Definition: ChangeTags.php:921
Variant of the Message class.
Definition: RawMessage.php:34
static activateTagWithChecks( $tag, $reason, User $user, $ignoreWarnings=false, array $logEntryTags=[])
Activates a tag, checking whether it is allowed first, and adding a log entry afterwards.
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:94
static updateTagsWithChecks( $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, $reason, User $user)
Adds and/or removes tags to/from a given change, checking whether it is allowed first, and adding a log entry afterwards.
Definition: ChangeTags.php:663
static suggestTarget( $target, array $ids)
const DB_REPLICA
Definition: defines.php:25
static canAddTagsAccompanyingChange(array $tags, User $user=null)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
Definition: ChangeTags.php:521
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:119
static canCreateTag( $tag, User $user=null)
Is it OK to allow the user to create this tag?
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
Definition: ChangeTags.php:837
static buildTagFilterSelector( $selected='', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
Definition: ChangeTags.php:874
static addTags( $tags, $rc_id=null, $rev_id=null, $log_id=null, $params=null, RecentChange $rc=null)
Add tags to a change given its rc_id, rev_id and/or log_id.
Definition: ChangeTags.php:251
static plaintextParam( $plaintext)
Definition: Message.php:1104
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
static tagUsageStatistics()
Returns a map of any tags used on the wiki to number of edits tagged with them, ordered descending by...
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:319