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  public const BYPASS_MAX_USAGE_CHECK = 1;
41 
45  private static $definedSoftwareTags = [
46  'mw-contentmodelchange',
47  'mw-new-redirect',
48  'mw-removed-redirect',
49  'mw-changed-redirect-target',
50  'mw-blank',
51  'mw-replace',
52  'mw-rollback',
53  'mw-undo',
54  ];
55 
63  public static function getSoftwareTags( $all = false ) {
64  global $wgSoftwareTags;
65  $softwareTags = [];
66 
67  if ( !is_array( $wgSoftwareTags ) ) {
68  wfWarn( 'wgSoftwareTags should be associative array of enabled tags.
69  Please refer to documentation for the list of tags you can enable' );
70  return $softwareTags;
71  }
72 
73  $availableSoftwareTags = !$all ?
74  array_keys( array_filter( $wgSoftwareTags ) ) :
75  array_keys( $wgSoftwareTags );
76 
77  $softwareTags = array_intersect(
78  $availableSoftwareTags,
79  self::$definedSoftwareTags
80  );
81 
82  return $softwareTags;
83  }
84 
99  public static function formatSummaryRow( $tags, $page, IContextSource $context = null ) {
100  if ( !$tags ) {
101  return [ '', [] ];
102  }
103  if ( !$context ) {
105  }
106 
107  $classes = [];
108 
109  $tags = explode( ',', $tags );
110  $displayTags = [];
111  foreach ( $tags as $tag ) {
112  if ( !$tag ) {
113  continue;
114  }
115  $description = self::tagDescription( $tag, $context );
116  if ( $description === false ) {
117  continue;
118  }
119  $displayTags[] = Xml::tags(
120  'span',
121  [ 'class' => 'mw-tag-marker ' .
122  Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
123  $description
124  );
125  $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
126  }
127 
128  if ( !$displayTags ) {
129  return [ '', [] ];
130  }
131 
132  $markers = $context->msg( 'tag-list-wrapper' )
133  ->numParams( count( $displayTags ) )
134  ->rawParams( implode( ' ', $displayTags ) )
135  ->parse();
136  $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
137 
138  return [ $markers, $classes ];
139  }
140 
154  public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
155  $msg = $context->msg( "tag-$tag" );
156  if ( !$msg->exists() ) {
157  // No such message
158  return ( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) )
159  // HACK MessageLocalizer doesn't have a way to set the right language on a RawMessage,
160  // so extract the language from $msg and use that.
161  // The language doesn't really matter, but we need to set it to avoid requesting
162  // the user's language from session-less entry points (T227233)
163  ->inLanguage( $msg->getLanguage() );
164 
165  }
166  if ( $msg->isDisabled() ) {
167  // The message exists but is disabled, hide the tag.
168  return false;
169  }
170 
171  // Message exists and isn't disabled, use it.
172  return $msg;
173  }
174 
188  public static function tagDescription( $tag, MessageLocalizer $context ) {
190  return $msg ? $msg->parse() : false;
191  }
192 
205  public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
206  $msg = $context->msg( "tag-$tag-description" );
207  if ( !$msg->exists() ) {
208  return false;
209  }
210  if ( $msg->isDisabled() ) {
211  // The message exists but is disabled, hide the description.
212  return false;
213  }
214 
215  // Message exists and isn't disabled, use it.
216  return $msg;
217  }
218 
228  public static function truncateTagDescription( $tag, $length, IContextSource $context ) {
229  // FIXME: Make this accept MessageLocalizer and Language instead of IContextSource
230 
231  $originalDesc = self::tagLongDescriptionMessage( $tag, $context );
232  // If there is no tag description, return empty string
233  if ( !$originalDesc ) {
234  return '';
235  }
236 
237  $taglessDesc = Sanitizer::stripAllTags( $originalDesc->parse() );
238 
239  return $context->getLanguage()->truncateForVisual( $taglessDesc, $length );
240  }
241 
256  public static function addTags( $tags, $rc_id = null, $rev_id = null,
257  $log_id = null, $params = null, RecentChange $rc = null
258  ) {
259  $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
260  return (bool)$result[0];
261  }
262 
293  public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
294  &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
295  User $user = null
296  ) {
297  $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags...
298  $tagsToRemove = array_filter( (array)$tagsToRemove );
299 
300  if ( !$rc_id && !$rev_id && !$log_id ) {
301  throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
302  'specified when adding or removing a tag from a change!' );
303  }
304 
305  $dbw = wfGetDB( DB_MASTER );
306 
307  // Might as well look for rcids and so on.
308  if ( !$rc_id ) {
309  // Info might be out of date, somewhat fractionally, on replica DB.
310  // LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
311  // so use that relation to avoid full table scans.
312  if ( $log_id ) {
313  $rc_id = $dbw->selectField(
314  [ 'logging', 'recentchanges' ],
315  'rc_id',
316  [
317  'log_id' => $log_id,
318  'rc_timestamp = log_timestamp',
319  'rc_logid = log_id'
320  ],
321  __METHOD__
322  );
323  } elseif ( $rev_id ) {
324  $rc_id = $dbw->selectField(
325  [ 'revision', 'recentchanges' ],
326  'rc_id',
327  [
328  'rev_id' => $rev_id,
329  'rc_this_oldid = rev_id'
330  ],
331  __METHOD__
332  );
333  }
334  } elseif ( !$log_id && !$rev_id ) {
335  // Info might be out of date, somewhat fractionally, on replica DB.
336  $log_id = $dbw->selectField(
337  'recentchanges',
338  'rc_logid',
339  [ 'rc_id' => $rc_id ],
340  __METHOD__
341  );
342  $rev_id = $dbw->selectField(
343  'recentchanges',
344  'rc_this_oldid',
345  [ 'rc_id' => $rc_id ],
346  __METHOD__
347  );
348  }
349 
350  if ( $log_id && !$rev_id ) {
351  $rev_id = $dbw->selectField(
352  'log_search',
353  'ls_value',
354  [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ],
355  __METHOD__
356  );
357  } elseif ( !$log_id && $rev_id ) {
358  $log_id = $dbw->selectField(
359  'log_search',
360  'ls_log_id',
361  [ 'ls_field' => 'associated_rev_id', 'ls_value' => (string)$rev_id ],
362  __METHOD__
363  );
364  }
365 
366  $prevTags = self::getTags( $dbw, $rc_id, $rev_id, $log_id );
367 
368  // add tags
369  $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
370  $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
371 
372  // remove tags
373  $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
374  $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
375 
376  sort( $prevTags );
377  sort( $newTags );
378  if ( $prevTags == $newTags ) {
379  return [ [], [], $prevTags ];
380  }
381 
382  // insert a row into change_tag for each new tag
383  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
384  if ( count( $tagsToAdd ) ) {
385  $changeTagMapping = [];
386  foreach ( $tagsToAdd as $tag ) {
387  $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
388  }
389  $fname = __METHOD__;
390  // T207881: update the counts at the end of the transaction
391  $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tagsToAdd, $fname ) {
392  $dbw->update(
393  'change_tag_def',
394  [ 'ctd_count = ctd_count + 1' ],
395  [ 'ctd_name' => $tagsToAdd ],
396  $fname
397  );
398  } );
399 
400  $tagsRows = [];
401  foreach ( $tagsToAdd as $tag ) {
402  // Filter so we don't insert NULLs as zero accidentally.
403  // Keep in mind that $rc_id === null means "I don't care/know about the
404  // rc_id, just delete $tag on this revision/log entry". It doesn't
405  // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
406  $tagsRows[] = array_filter(
407  [
408  'ct_rc_id' => $rc_id,
409  'ct_log_id' => $log_id,
410  'ct_rev_id' => $rev_id,
411  'ct_params' => $params,
412  'ct_tag_id' => $changeTagMapping[$tag] ?? null,
413  ]
414  );
415 
416  }
417 
418  $dbw->insert( 'change_tag', $tagsRows, __METHOD__, [ 'IGNORE' ] );
419  }
420 
421  // delete from change_tag
422  if ( count( $tagsToRemove ) ) {
423  $fname = __METHOD__;
424  foreach ( $tagsToRemove as $tag ) {
425  $conds = array_filter(
426  [
427  'ct_rc_id' => $rc_id,
428  'ct_log_id' => $log_id,
429  'ct_rev_id' => $rev_id,
430  'ct_tag_id' => $changeTagDefStore->getId( $tag ),
431  ]
432  );
433  $dbw->delete( 'change_tag', $conds, __METHOD__ );
434  if ( $dbw->affectedRows() ) {
435  // T207881: update the counts at the end of the transaction
436  $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tag, $fname ) {
437  $dbw->update(
438  'change_tag_def',
439  [ 'ctd_count = ctd_count - 1' ],
440  [ 'ctd_name' => $tag ],
441  $fname
442  );
443 
444  $dbw->delete(
445  'change_tag_def',
446  [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
447  $fname
448  );
449  } );
450  }
451  }
452  }
453 
454  Hooks::run( 'ChangeTagsAfterUpdateTags', [ $tagsToAdd, $tagsToRemove, $prevTags,
455  $rc_id, $rev_id, $log_id, $params, $rc, $user ] );
456 
457  return [ $tagsToAdd, $tagsToRemove, $prevTags ];
458  }
459 
470  public static function getTags( IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
471  $conds = array_filter(
472  [
473  'ct_rc_id' => $rc_id,
474  'ct_rev_id' => $rev_id,
475  'ct_log_id' => $log_id,
476  ]
477  );
478 
479  $tagIds = $db->selectFieldValues(
480  'change_tag',
481  'ct_tag_id',
482  $conds,
483  __METHOD__
484  );
485 
486  $tags = [];
487  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
488  foreach ( $tagIds as $tagId ) {
489  $tags[] = $changeTagDefStore->getName( (int)$tagId );
490  }
491 
492  return $tags;
493  }
494 
505  protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
506  $lang = RequestContext::getMain()->getLanguage();
507  $count = count( $tags );
508  return Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
509  $lang->commaList( $tags ), $count );
510  }
511 
525  public static function canAddTagsAccompanyingChange( array $tags, User $user = null ) {
526  if ( $user !== null ) {
527  if ( !MediaWikiServices::getInstance()->getPermissionManager()
528  ->userHasRight( $user, 'applychangetags' )
529  ) {
530  return Status::newFatal( 'tags-apply-no-permission' );
531  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
532  return Status::newFatal( 'tags-apply-blocked', $user->getName() );
533  }
534  }
535 
536  // to be applied, a tag has to be explicitly defined
537  $allowedTags = self::listExplicitlyDefinedTags();
538  Hooks::run( 'ChangeTagsAllowedAdd', [ &$allowedTags, $tags, $user ] );
539  $disallowedTags = array_diff( $tags, $allowedTags );
540  if ( $disallowedTags ) {
541  return self::restrictedTagError( 'tags-apply-not-allowed-one',
542  'tags-apply-not-allowed-multi', $disallowedTags );
543  }
544 
545  return Status::newGood();
546  }
547 
568  public static function addTagsAccompanyingChangeWithChecks(
569  array $tags, $rc_id, $rev_id, $log_id, $params, User $user
570  ) {
571  // are we allowed to do this?
572  $result = self::canAddTagsAccompanyingChange( $tags, $user );
573  if ( !$result->isOK() ) {
574  $result->value = null;
575  return $result;
576  }
577 
578  // do it!
579  self::addTags( $tags, $rc_id, $rev_id, $log_id, $params );
580 
581  return Status::newGood( true );
582  }
583 
598  public static function canUpdateTags( array $tagsToAdd, array $tagsToRemove,
599  User $user = null
600  ) {
601  if ( $user !== null ) {
602  if ( !MediaWikiServices::getInstance()->getPermissionManager()
603  ->userHasRight( $user, 'changetags' )
604  ) {
605  return Status::newFatal( 'tags-update-no-permission' );
606  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
607  return Status::newFatal( 'tags-update-blocked', $user->getName() );
608  }
609  }
610 
611  if ( $tagsToAdd ) {
612  // to be added, a tag has to be explicitly defined
613  // @todo Allow extensions to define tags that can be applied by users...
614  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
615  $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
616  if ( $diff ) {
617  return self::restrictedTagError( 'tags-update-add-not-allowed-one',
618  'tags-update-add-not-allowed-multi', $diff );
619  }
620  }
621 
622  if ( $tagsToRemove ) {
623  // to be removed, a tag must not be defined by an extension, or equivalently it
624  // has to be either explicitly defined or not defined at all
625  // (assuming no edge case of a tag both explicitly-defined and extension-defined)
626  $softwareDefinedTags = self::listSoftwareDefinedTags();
627  $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
628  if ( $intersect ) {
629  return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
630  'tags-update-remove-not-allowed-multi', $intersect );
631  }
632  }
633 
634  return Status::newGood();
635  }
636 
667  public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
668  $rc_id, $rev_id, $log_id, $params, $reason, User $user
669  ) {
670  if ( $tagsToAdd === null ) {
671  $tagsToAdd = [];
672  }
673  if ( $tagsToRemove === null ) {
674  $tagsToRemove = [];
675  }
676  if ( !$tagsToAdd && !$tagsToRemove ) {
677  // no-op, don't bother
678  return Status::newGood( (object)[
679  'logId' => null,
680  'addedTags' => [],
681  'removedTags' => [],
682  ] );
683  }
684 
685  // are we allowed to do this?
686  $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $user );
687  if ( !$result->isOK() ) {
688  $result->value = null;
689  return $result;
690  }
691 
692  // basic rate limiting
693  if ( $user->pingLimiter( 'changetag' ) ) {
694  return Status::newFatal( 'actionthrottledtext' );
695  }
696 
697  // do it!
698  list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd,
699  $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user );
700  if ( !$tagsAdded && !$tagsRemoved ) {
701  // no-op, don't log it
702  return Status::newGood( (object)[
703  'logId' => null,
704  'addedTags' => [],
705  'removedTags' => [],
706  ] );
707  }
708 
709  // log it
710  $logEntry = new ManualLogEntry( 'tag', 'update' );
711  $logEntry->setPerformer( $user );
712  $logEntry->setComment( $reason );
713 
714  // find the appropriate target page
715  if ( $rev_id ) {
716  $rev = Revision::newFromId( $rev_id );
717  if ( $rev ) {
718  $logEntry->setTarget( $rev->getTitle() );
719  }
720  } elseif ( $log_id ) {
721  // This function is from revision deletion logic and has nothing to do with
722  // change tags, but it appears to be the only other place in core where we
723  // perform logged actions on log items.
724  $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
725  }
726 
727  if ( !$logEntry->getTarget() ) {
728  // target is required, so we have to set something
729  $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
730  }
731 
732  $logParams = [
733  '4::revid' => $rev_id,
734  '5::logid' => $log_id,
735  '6:list:tagsAdded' => $tagsAdded,
736  '7:number:tagsAddedCount' => count( $tagsAdded ),
737  '8:list:tagsRemoved' => $tagsRemoved,
738  '9:number:tagsRemovedCount' => count( $tagsRemoved ),
739  'initialTags' => $initialTags,
740  ];
741  $logEntry->setParameters( $logParams );
742  $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
743 
744  $dbw = wfGetDB( DB_MASTER );
745  $logId = $logEntry->insert( $dbw );
746  // Only send this to UDP, not RC, similar to patrol events
747  $logEntry->publish( $logId, 'udp' );
748 
749  return Status::newGood( (object)[
750  'logId' => $logId,
751  'addedTags' => $tagsAdded,
752  'removedTags' => $tagsRemoved,
753  ] );
754  }
755 
776  public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
777  &$join_conds, &$options, $filter_tag = ''
778  ) {
779  global $wgUseTagFilter;
780 
781  // Normalize to arrays
782  $tables = (array)$tables;
783  $fields = (array)$fields;
784  $conds = (array)$conds;
785  $options = (array)$options;
786 
787  $fields['ts_tags'] = self::makeTagSummarySubquery( $tables );
788 
789  // Figure out which ID field to use
790  if ( in_array( 'recentchanges', $tables ) ) {
791  $join_cond = 'ct_rc_id=rc_id';
792  } elseif ( in_array( 'logging', $tables ) ) {
793  $join_cond = 'ct_log_id=log_id';
794  } elseif ( in_array( 'revision', $tables ) ) {
795  $join_cond = 'ct_rev_id=rev_id';
796  } elseif ( in_array( 'archive', $tables ) ) {
797  $join_cond = 'ct_rev_id=ar_rev_id';
798  } else {
799  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
800  }
801 
802  if ( $wgUseTagFilter && $filter_tag ) {
803  // Somebody wants to filter on a tag.
804  // Add an INNER JOIN on change_tag
805 
806  $tables[] = 'change_tag';
807  $join_conds['change_tag'] = [ 'JOIN', $join_cond ];
808  $filterTagIds = [];
809  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
810  foreach ( (array)$filter_tag as $filterTagName ) {
811  try {
812  $filterTagIds[] = $changeTagDefStore->getId( $filterTagName );
813  } catch ( NameTableAccessException $exception ) {
814  // Return nothing.
815  $conds[] = '0=1';
816  break;
817  }
818  }
819 
820  if ( $filterTagIds !== [] ) {
821  $conds['ct_tag_id'] = $filterTagIds;
822  }
823 
824  if (
825  is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
826  !in_array( 'DISTINCT', $options )
827  ) {
828  $options[] = 'DISTINCT';
829  }
830  }
831  }
832 
841  public static function makeTagSummarySubquery( $tables ) {
842  // Normalize to arrays
843  $tables = (array)$tables;
844 
845  // Figure out which ID field to use
846  if ( in_array( 'recentchanges', $tables ) ) {
847  $join_cond = 'ct_rc_id=rc_id';
848  } elseif ( in_array( 'logging', $tables ) ) {
849  $join_cond = 'ct_log_id=log_id';
850  } elseif ( in_array( 'revision', $tables ) ) {
851  $join_cond = 'ct_rev_id=rev_id';
852  } elseif ( in_array( 'archive', $tables ) ) {
853  $join_cond = 'ct_rev_id=ar_rev_id';
854  } else {
855  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
856  }
857 
858  $tagTables = [ 'change_tag', 'change_tag_def' ];
859  $join_cond_ts_tags = [ 'change_tag_def' => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
860  $field = 'ctd_name';
861 
862  return wfGetDB( DB_REPLICA )->buildGroupConcatField(
863  ',', $tagTables, $field, $join_cond, $join_cond_ts_tags
864  );
865  }
866 
878  public static function buildTagFilterSelector(
879  $selected = '', $ooui = false, IContextSource $context = null
880  ) {
881  if ( !$context ) {
883  }
884 
885  $config = $context->getConfig();
886  if ( !$config->get( 'UseTagFilter' ) || !count( self::listDefinedTags() ) ) {
887  return [];
888  }
889 
890  $data = [
891  Html::rawElement(
892  'label',
893  [ 'for' => 'tagfilter' ],
894  $context->msg( 'tag-filter' )->parse()
895  )
896  ];
897 
898  if ( $ooui ) {
899  $data[] = new OOUI\TextInputWidget( [
900  'id' => 'tagfilter',
901  'name' => 'tagfilter',
902  'value' => $selected,
903  'classes' => 'mw-tagfilter-input',
904  ] );
905  } else {
906  $data[] = Xml::input(
907  'tagfilter',
908  20,
909  $selected,
910  [ 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' ]
911  );
912  }
913 
914  return $data;
915  }
916 
925  public static function defineTag( $tag ) {
926  $dbw = wfGetDB( DB_MASTER );
927  $tagDef = [
928  'ctd_name' => $tag,
929  'ctd_user_defined' => 1,
930  'ctd_count' => 0
931  ];
932  $dbw->upsert(
933  'change_tag_def',
934  $tagDef,
935  [ 'ctd_name' ],
936  [ 'ctd_user_defined' => 1 ],
937  __METHOD__
938  );
939 
940  // clear the memcache of defined tags
942  }
943 
952  public static function undefineTag( $tag ) {
953  $dbw = wfGetDB( DB_MASTER );
954 
955  $dbw->update(
956  'change_tag_def',
957  [ 'ctd_user_defined' => 0 ],
958  [ 'ctd_name' => $tag ],
959  __METHOD__
960  );
961 
962  $dbw->delete(
963  'change_tag_def',
964  [ 'ctd_name' => $tag, 'ctd_count' => 0 ],
965  __METHOD__
966  );
967 
968  // clear the memcache of defined tags
970  }
971 
986  protected static function logTagManagementAction( $action, $tag, $reason,
987  User $user, $tagCount = null, array $logEntryTags = []
988  ) {
989  $dbw = wfGetDB( DB_MASTER );
990 
991  $logEntry = new ManualLogEntry( 'managetags', $action );
992  $logEntry->setPerformer( $user );
993  // target page is not relevant, but it has to be set, so we just put in
994  // the title of Special:Tags
995  $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
996  $logEntry->setComment( $reason );
997 
998  $params = [ '4::tag' => $tag ];
999  if ( $tagCount !== null ) {
1000  $params['5:number:count'] = $tagCount;
1001  }
1002  $logEntry->setParameters( $params );
1003  $logEntry->setRelations( [ 'Tag' => $tag ] );
1004  $logEntry->addTags( $logEntryTags );
1005 
1006  $logId = $logEntry->insert( $dbw );
1007  $logEntry->publish( $logId );
1008  return $logId;
1009  }
1010 
1020  public static function canActivateTag( $tag, User $user = null ) {
1021  if ( $user !== null ) {
1022  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1023  ->userHasRight( $user, 'managechangetags' )
1024  ) {
1025  return Status::newFatal( 'tags-manage-no-permission' );
1026  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1027  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1028  }
1029  }
1030 
1031  // defined tags cannot be activated (a defined tag is either extension-
1032  // defined, in which case the extension chooses whether or not to active it;
1033  // or user-defined, in which case it is considered active)
1034  $definedTags = self::listDefinedTags();
1035  if ( in_array( $tag, $definedTags ) ) {
1036  return Status::newFatal( 'tags-activate-not-allowed', $tag );
1037  }
1038 
1039  // non-existing tags cannot be activated
1040  $tagUsage = self::tagUsageStatistics();
1041  if ( !isset( $tagUsage[$tag] ) ) { // we already know the tag is undefined
1042  return Status::newFatal( 'tags-activate-not-found', $tag );
1043  }
1044 
1045  return Status::newGood();
1046  }
1047 
1065  public static function activateTagWithChecks( $tag, $reason, User $user,
1066  $ignoreWarnings = false, array $logEntryTags = []
1067  ) {
1068  // are we allowed to do this?
1069  $result = self::canActivateTag( $tag, $user );
1070  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1071  $result->value = null;
1072  return $result;
1073  }
1074 
1075  // do it!
1076  self::defineTag( $tag );
1077 
1078  // log it
1079  $logId = self::logTagManagementAction( 'activate', $tag, $reason, $user,
1080  null, $logEntryTags );
1081 
1082  return Status::newGood( $logId );
1083  }
1084 
1094  public static function canDeactivateTag( $tag, User $user = null ) {
1095  if ( $user !== null ) {
1096  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1097  ->userHasRight( $user, 'managechangetags' )
1098  ) {
1099  return Status::newFatal( 'tags-manage-no-permission' );
1100  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1101  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1102  }
1103  }
1104 
1105  // only explicitly-defined tags can be deactivated
1106  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
1107  if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
1108  return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
1109  }
1110  return Status::newGood();
1111  }
1112 
1130  public static function deactivateTagWithChecks( $tag, $reason, User $user,
1131  $ignoreWarnings = false, array $logEntryTags = []
1132  ) {
1133  // are we allowed to do this?
1134  $result = self::canDeactivateTag( $tag, $user );
1135  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1136  $result->value = null;
1137  return $result;
1138  }
1139 
1140  // do it!
1141  self::undefineTag( $tag );
1142 
1143  // log it
1144  $logId = self::logTagManagementAction( 'deactivate', $tag, $reason, $user,
1145  null, $logEntryTags );
1146 
1147  return Status::newGood( $logId );
1148  }
1149 
1157  public static function isTagNameValid( $tag ) {
1158  // no empty tags
1159  if ( $tag === '' ) {
1160  return Status::newFatal( 'tags-create-no-name' );
1161  }
1162 
1163  // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
1164  // pipe (used as a delimiter between multiple tags in
1165  // SpecialRecentchanges and friends), or slashes (would break tag description messages in
1166  // MediaWiki namespace)
1167  if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
1168  || strpos( $tag, '/' ) !== false ) {
1169  return Status::newFatal( 'tags-create-invalid-chars' );
1170  }
1171 
1172  // could the MediaWiki namespace description messages be created?
1173  $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
1174  if ( $title === null ) {
1175  return Status::newFatal( 'tags-create-invalid-title-chars' );
1176  }
1177 
1178  return Status::newGood();
1179  }
1180 
1193  public static function canCreateTag( $tag, User $user = null ) {
1194  if ( $user !== null ) {
1195  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1196  ->userHasRight( $user, 'managechangetags' )
1197  ) {
1198  return Status::newFatal( 'tags-manage-no-permission' );
1199  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1200  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1201  }
1202  }
1203 
1204  $status = self::isTagNameValid( $tag );
1205  if ( !$status->isGood() ) {
1206  return $status;
1207  }
1208 
1209  // does the tag already exist?
1210  $tagUsage = self::tagUsageStatistics();
1211  if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) {
1212  return Status::newFatal( 'tags-create-already-exists', $tag );
1213  }
1214 
1215  // check with hooks
1216  $canCreateResult = Status::newGood();
1217  Hooks::run( 'ChangeTagCanCreate', [ $tag, $user, &$canCreateResult ] );
1218  return $canCreateResult;
1219  }
1220 
1240  public static function createTagWithChecks( $tag, $reason, User $user,
1241  $ignoreWarnings = false, array $logEntryTags = []
1242  ) {
1243  // are we allowed to do this?
1244  $result = self::canCreateTag( $tag, $user );
1245  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1246  $result->value = null;
1247  return $result;
1248  }
1249 
1250  // do it!
1251  self::defineTag( $tag );
1252 
1253  // log it
1254  $logId = self::logTagManagementAction( 'create', $tag, $reason, $user,
1255  null, $logEntryTags );
1256 
1257  return Status::newGood( $logId );
1258  }
1259 
1272  public static function deleteTagEverywhere( $tag ) {
1273  $dbw = wfGetDB( DB_MASTER );
1274  $dbw->startAtomic( __METHOD__ );
1275 
1276  // fetch tag id, this must be done before calling undefineTag(), see T225564
1277  $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag );
1278 
1279  // set ctd_user_defined = 0
1280  self::undefineTag( $tag );
1281 
1282  // delete from change_tag
1283  $dbw->delete( 'change_tag', [ 'ct_tag_id' => $tagId ], __METHOD__ );
1284  $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag ], __METHOD__ );
1285  $dbw->endAtomic( __METHOD__ );
1286 
1287  // give extensions a chance
1288  $status = Status::newGood();
1289  Hooks::run( 'ChangeTagAfterDelete', [ $tag, &$status ] );
1290  // let's not allow error results, as the actual tag deletion succeeded
1291  if ( !$status->isOK() ) {
1292  wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' );
1293  $status->setOK( true );
1294  }
1295 
1296  // clear the memcache of defined tags
1298 
1299  return $status;
1300  }
1301 
1314  public static function canDeleteTag( $tag, User $user = null, int $flags = 0 ) {
1315  $tagUsage = self::tagUsageStatistics();
1316 
1317  if ( $user !== null ) {
1318  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1319  ->userHasRight( $user, 'deletechangetags' )
1320  ) {
1321  return Status::newFatal( 'tags-delete-no-permission' );
1322  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1323  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1324  }
1325  }
1326 
1327  if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) {
1328  return Status::newFatal( 'tags-delete-not-found', $tag );
1329  }
1330 
1331  if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1332  isset( $tagUsage[$tag] ) &&
1333  $tagUsage[$tag] > self::MAX_DELETE_USES
1334  ) {
1335  return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1336  }
1337 
1338  $softwareDefined = self::listSoftwareDefinedTags();
1339  if ( in_array( $tag, $softwareDefined ) ) {
1340  // extension-defined tags can't be deleted unless the extension
1341  // specifically allows it
1342  $status = Status::newFatal( 'tags-delete-not-allowed' );
1343  } else {
1344  // user-defined tags are deletable unless otherwise specified
1345  $status = Status::newGood();
1346  }
1347 
1348  Hooks::run( 'ChangeTagCanDelete', [ $tag, $user, &$status ] );
1349  return $status;
1350  }
1351 
1369  public static function deleteTagWithChecks( $tag, $reason, User $user,
1370  $ignoreWarnings = false, array $logEntryTags = []
1371  ) {
1372  // are we allowed to do this?
1373  $result = self::canDeleteTag( $tag, $user );
1374  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1375  $result->value = null;
1376  return $result;
1377  }
1378 
1379  // store the tag usage statistics
1380  $tagUsage = self::tagUsageStatistics();
1381  $hitcount = $tagUsage[$tag] ?? 0;
1382 
1383  // do it!
1384  $deleteResult = self::deleteTagEverywhere( $tag );
1385  if ( !$deleteResult->isOK() ) {
1386  return $deleteResult;
1387  }
1388 
1389  // log it
1390  $logId = self::logTagManagementAction( 'delete', $tag, $reason, $user,
1391  $hitcount, $logEntryTags );
1392 
1393  $deleteResult->value = $logId;
1394  return $deleteResult;
1395  }
1396 
1403  public static function listSoftwareActivatedTags() {
1404  // core active tags
1405  $tags = self::getSoftwareTags();
1406  if ( !Hooks::isRegistered( 'ChangeTagsListActive' ) ) {
1407  return $tags;
1408  }
1409  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1410  return $cache->getWithSetCallback(
1411  $cache->makeKey( 'active-tags' ),
1413  function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags ) {
1414  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1415 
1416  // Ask extensions which tags they consider active
1417  Hooks::run( 'ChangeTagsListActive', [ &$tags ] );
1418  return $tags;
1419  },
1420  [
1421  'checkKeys' => [ $cache->makeKey( 'active-tags' ) ],
1422  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1424  ]
1425  );
1426  }
1427 
1434  public static function listDefinedTags() {
1436  $tags2 = self::listSoftwareDefinedTags();
1437  return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
1438  }
1439 
1448  public static function listExplicitlyDefinedTags() {
1449  $fname = __METHOD__;
1450 
1451  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1452  return $cache->getWithSetCallback(
1453  $cache->makeKey( 'valid-tags-db' ),
1455  function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1456  $dbr = wfGetDB( DB_REPLICA );
1457 
1458  $setOpts += Database::getCacheSetOptions( $dbr );
1459 
1460  $tags = $dbr->selectFieldValues(
1461  'change_tag_def',
1462  'ctd_name',
1463  [ 'ctd_user_defined' => 1 ],
1464  $fname
1465  );
1466 
1467  return array_filter( array_unique( $tags ) );
1468  },
1469  [
1470  'checkKeys' => [ $cache->makeKey( 'valid-tags-db' ) ],
1471  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1473  ]
1474  );
1475  }
1476 
1486  public static function listSoftwareDefinedTags() {
1487  // core defined tags
1488  $tags = self::getSoftwareTags( true );
1489  if ( !Hooks::isRegistered( 'ListDefinedTags' ) ) {
1490  return $tags;
1491  }
1492  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1493  return $cache->getWithSetCallback(
1494  $cache->makeKey( 'valid-tags-hook' ),
1496  function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags ) {
1497  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1498 
1499  Hooks::run( 'ListDefinedTags', [ &$tags ] );
1500  return array_filter( array_unique( $tags ) );
1501  },
1502  [
1503  'checkKeys' => [ $cache->makeKey( 'valid-tags-hook' ) ],
1504  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1506  ]
1507  );
1508  }
1509 
1515  public static function purgeTagCacheAll() {
1516  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1517 
1518  $cache->touchCheckKey( $cache->makeKey( 'active-tags' ) );
1519  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-db' ) );
1520  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-hook' ) );
1521  $cache->touchCheckKey( $cache->makeKey( 'tags-usage-statistics' ) );
1522 
1523  MediaWikiServices::getInstance()->getChangeTagDefStore()->reloadMap();
1524  }
1525 
1532  public static function tagUsageStatistics() {
1533  $fname = __METHOD__;
1534 
1535  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1536  return $cache->getWithSetCallback(
1537  $cache->makeKey( 'tags-usage-statistics' ),
1539  function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1540  $dbr = wfGetDB( DB_REPLICA );
1541  $res = $dbr->select(
1542  'change_tag_def',
1543  [ 'ctd_name', 'ctd_count' ],
1544  [],
1545  $fname,
1546  [ 'ORDER BY' => 'ctd_count DESC' ]
1547  );
1548 
1549  $out = [];
1550  foreach ( $res as $row ) {
1551  $out[$row->ctd_name] = $row->ctd_count;
1552  }
1553 
1554  return $out;
1555  },
1556  [
1557  'checkKeys' => [ $cache->makeKey( 'tags-usage-statistics' ) ],
1558  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1560  ]
1561  );
1562  }
1563 
1578  public static function showTagEditingUI( User $user ) {
1579  return MediaWikiServices::getInstance()->getPermissionManager()
1580  ->userHasRight( $user, 'changetags' ) &&
1581  (bool)self::listExplicitlyDefinedTags();
1582  }
1583 }
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:49
ChangeTags\makeTagSummarySubquery
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
Definition: ChangeTags.php:841
$wgUseTagFilter
$wgUseTagFilter
Allow filtering by change tag in recentchanges, history, etc Has no effect if no tags are defined in ...
Definition: DefaultSettings.php:7028
Title\newFromText
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:317
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
ChangeTags\purgeTagCacheAll
static purgeTagCacheAll()
Invalidates the short-term cache of defined tags used by the list*DefinedTags functions,...
Definition: ChangeTags.php:1515
Revision\newFromId
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:119
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:130
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:35
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:71
ChangeTags\logTagManagementAction
static logTagManagementAction( $action, $tag, $reason, User $user, $tagCount=null, array $logEntryTags=[])
Writes a tag action into the tag management log.
Definition: ChangeTags.php:986
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
ChangeTags\showTagEditingUI
static showTagEditingUI(User $user)
Indicate whether change tag editing UI is relevant.
Definition: ChangeTags.php:1578
ChangeTags\buildTagFilterSelector
static buildTagFilterSelector( $selected='', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
Definition: ChangeTags.php:878
IExpiringStore\TTL_MINUTE
const TTL_MINUTE
Definition: IExpiringStore.php:33
User\pingLimiter
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition: User.php:1961
$res
$res
Definition: testCompression.php:54
ChangeTags\restrictedTagError
static restrictedTagError( $msgOne, $msgMulti, $tags)
Helper function to generate a fatal status with a 'not-allowed' type error.
Definition: ChangeTags.php:505
ChangeTags\isTagNameValid
static isTagNameValid( $tag)
Is the tag name valid?
Definition: ChangeTags.php:1157
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
MessageLocalizer
Interface for localizing messages in MediaWiki.
Definition: MessageLocalizer.php:28
ChangeTags\BYPASS_MAX_USAGE_CHECK
const BYPASS_MAX_USAGE_CHECK
Flag for canDeleteTag().
Definition: ChangeTags.php:40
$dbr
$dbr
Definition: testCompression.php:52
ChangeTags\truncateTagDescription
static truncateTagDescription( $tag, $length, IContextSource $context)
Get truncated message for the tag's long description.
Definition: ChangeTags.php:228
ChangeTags\tagShortDescriptionMessage
static tagShortDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's short description.
Definition: ChangeTags.php:154
ChangeTags\listSoftwareDefinedTags
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
Definition: ChangeTags.php:1486
ChangeTags\deleteTagEverywhere
static deleteTagEverywhere( $tag)
Permanently removes all traces of a tag from the DB.
Definition: ChangeTags.php:1272
ChangeTags\modifyDisplayQuery
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
Definition: ChangeTags.php:776
ChangeTags\getTags
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:470
MWException
MediaWiki exception.
Definition: MWException.php:26
ChangeTags\undefineTag
static undefineTag( $tag)
Update ctd_user_defined = 0 field in change_tag_def.
Definition: ChangeTags.php:952
ChangeTags
Definition: ChangeTags.php:29
getPermissionManager
getPermissionManager()
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2562
ChangeTags\listDefinedTags
static listDefinedTags()
Basically lists defined tags which count even if they aren't applied to anything.
Definition: ChangeTags.php:1434
ChangeTags\getSoftwareTags
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:63
$title
$title
Definition: testCompression.php:36
ChangeTags\MAX_DELETE_USES
const MAX_DELETE_USES
Can't delete tags with more than this many uses.
Definition: ChangeTags.php:35
ChangeTags\defineTag
static defineTag( $tag)
Set ctd_user_defined = 1 in change_tag_def without checking that the tag name is valid.
Definition: ChangeTags.php:925
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
$wgSoftwareTags
array $wgSoftwareTags
List of core tags to enable.
Definition: DefaultSettings.php:7044
DB_MASTER
const DB_MASTER
Definition: defines.php:26
ChangeTags\$definedSoftwareTags
static $definedSoftwareTags
A list of tags defined and used by MediaWiki itself.
Definition: ChangeTags.php:45
ChangeTags\canActivateTag
static canActivateTag( $tag, User $user=null)
Is it OK to allow the user to activate this tag?
Definition: ChangeTags.php:1020
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:913
ChangeTags\listExplicitlyDefinedTags
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
Definition: ChangeTags.php:1448
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:610
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
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:1314
ChangeTags\deactivateTagWithChecks
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.
Definition: ChangeTags.php:1130
RevDelLogList\suggestTarget
static suggestTarget( $target, array $ids)
Suggest a target for the revision deletion Optionally override this function.
Definition: RevDelLogList.php:45
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:447
ChangeTags\listSoftwareActivatedTags
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
Definition: ChangeTags.php:1403
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:53
$context
$context
Definition: load.php:40
Hooks\isRegistered
static isRegistered( $name)
Returns true if a hook has a function registered to it.
Definition: Hooks.php:80
ChangeTags\canAddTagsAccompanyingChange
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:525
$cache
$cache
Definition: mcc.php:33
ChangeTags\canCreateTag
static canCreateTag( $tag, User $user=null)
Is it OK to allow the user to create this tag?
Definition: ChangeTags.php:1193
ChangeTags\updateTags
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:293
MediaWiki\Storage\NameTableAccessException
Exception representing a failure to look up a row from a name table.
Definition: NameTableAccessException.php:32
Wikimedia\Rdbms\IDatabase\selectFieldValues
selectFieldValues( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a list of single field values from result rows.
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:38
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:1532
Xml\input
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition: Xml.php:274
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1065
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:68
RawMessage
Variant of the Message class.
Definition: RawMessage.php:34
ChangeTags\addTagsAccompanyingChangeWithChecks
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:568
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:1240
ChangeTags\canUpdateTags
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:598
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:52
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
ChangeTags\tagLongDescriptionMessage
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
Definition: ChangeTags.php:205
ChangeTags\deleteTagWithChecks
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.
Definition: ChangeTags.php:1369
ChangeTags\formatSummaryRow
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:99
ChangeTags\updateTagsWithChecks
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,...
Definition: ChangeTags.php:667
IExpiringStore\TTL_PROC_LONG
const TTL_PROC_LONG
Definition: IExpiringStore.php:42
ChangeTags\addTags
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:256
ChangeTags\tagDescription
static tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
Definition: ChangeTags.php:188
ChangeTags\activateTagWithChecks
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.
Definition: ChangeTags.php:1065
ChangeTags\canDeactivateTag
static canDeactivateTag( $tag, User $user=null)
Is it OK to allow the user to deactivate this tag?
Definition: ChangeTags.php:1094