MediaWiki  master
ChangeTags.php
Go to the documentation of this file.
1 <?php
29 
30 class ChangeTags {
36  private const MAX_DELETE_USES = 5000;
37 
41  public const BYPASS_MAX_USAGE_CHECK = 1;
42 
46  private static $definedSoftwareTags = [
47  'mw-contentmodelchange',
48  'mw-new-redirect',
49  'mw-removed-redirect',
50  'mw-changed-redirect-target',
51  'mw-blank',
52  'mw-replace',
53  'mw-rollback',
54  'mw-undo',
55  'mw-manual-revert',
56  'mw-reverted',
57  ];
58 
69  public static $avoidReopeningTablesForTesting = false;
70 
78  public static function getSoftwareTags( $all = false ) {
79  global $wgSoftwareTags;
80  $softwareTags = [];
81 
82  if ( !is_array( $wgSoftwareTags ) ) {
83  wfWarn( 'wgSoftwareTags should be associative array of enabled tags.
84  Please refer to documentation for the list of tags you can enable' );
85  return $softwareTags;
86  }
87 
88  $availableSoftwareTags = !$all ?
89  array_keys( array_filter( $wgSoftwareTags ) ) :
90  array_keys( $wgSoftwareTags );
91 
92  $softwareTags = array_intersect(
93  $availableSoftwareTags,
94  self::$definedSoftwareTags
95  );
96 
97  return $softwareTags;
98  }
99 
114  public static function formatSummaryRow( $tags, $page, IContextSource $context = null ) {
115  if ( !$tags ) {
116  return [ '', [] ];
117  }
118  if ( !$context ) {
119  $context = RequestContext::getMain();
120  }
121 
122  $classes = [];
123 
124  $tags = explode( ',', $tags );
125  $displayTags = [];
126  foreach ( $tags as $tag ) {
127  if ( !$tag ) {
128  continue;
129  }
130  $description = self::tagDescription( $tag, $context );
131  if ( $description === false ) {
132  continue;
133  }
134  $displayTags[] = Xml::tags(
135  'span',
136  [ 'class' => 'mw-tag-marker ' .
137  Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
138  $description
139  );
140  $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
141  }
142 
143  if ( !$displayTags ) {
144  return [ '', [] ];
145  }
146 
147  $markers = $context->msg( 'tag-list-wrapper' )
148  ->numParams( count( $displayTags ) )
149  ->rawParams( implode( ' ', $displayTags ) )
150  ->parse();
151  $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
152 
153  return [ $markers, $classes ];
154  }
155 
169  public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
170  $msg = $context->msg( "tag-$tag" );
171  if ( !$msg->exists() ) {
172  // No such message
173  return ( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) )
174  // HACK MessageLocalizer doesn't have a way to set the right language on a RawMessage,
175  // so extract the language from $msg and use that.
176  // The language doesn't really matter, but we need to set it to avoid requesting
177  // the user's language from session-less entry points (T227233)
178  ->inLanguage( $msg->getLanguage() );
179 
180  }
181  if ( $msg->isDisabled() ) {
182  // The message exists but is disabled, hide the tag.
183  return false;
184  }
185 
186  // Message exists and isn't disabled, use it.
187  return $msg;
188  }
189 
203  public static function tagDescription( $tag, MessageLocalizer $context ) {
204  $msg = self::tagShortDescriptionMessage( $tag, $context );
205  return $msg ? $msg->parse() : false;
206  }
207 
220  public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
221  $msg = $context->msg( "tag-$tag-description" );
222  if ( !$msg->exists() ) {
223  return false;
224  }
225  if ( $msg->isDisabled() ) {
226  // The message exists but is disabled, hide the description.
227  return false;
228  }
229 
230  // Message exists and isn't disabled, use it.
231  return $msg;
232  }
233 
244  public static function truncateTagDescription( $tag, $length, IContextSource $context ) {
245  wfDeprecated( __METHOD__, '1.35' );
246  // FIXME: Make this accept MessageLocalizer and Language instead of IContextSource
247 
248  $originalDesc = self::tagLongDescriptionMessage( $tag, $context );
249  // If there is no tag description, return empty string
250  if ( !$originalDesc ) {
251  return '';
252  }
253 
254  $taglessDesc = Sanitizer::stripAllTags( $originalDesc->parse() );
255 
256  return $context->getLanguage()->truncateForVisual( $taglessDesc, $length );
257  }
258 
273  public static function addTags( $tags, $rc_id = null, $rev_id = null,
274  $log_id = null, $params = null, RecentChange $rc = null
275  ) {
276  $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
277  return (bool)$result[0];
278  }
279 
310  public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
311  &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
312  User $user = null
313  ) {
314  $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags...
315  $tagsToRemove = array_filter( (array)$tagsToRemove );
316 
317  if ( !$rc_id && !$rev_id && !$log_id ) {
318  throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
319  'specified when adding or removing a tag from a change!' );
320  }
321 
322  $dbw = wfGetDB( DB_MASTER );
323 
324  // Might as well look for rcids and so on.
325  if ( !$rc_id ) {
326  // Info might be out of date, somewhat fractionally, on replica DB.
327  // LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
328  // so use that relation to avoid full table scans.
329  if ( $log_id ) {
330  $rc_id = $dbw->selectField(
331  [ 'logging', 'recentchanges' ],
332  'rc_id',
333  [
334  'log_id' => $log_id,
335  'rc_timestamp = log_timestamp',
336  'rc_logid = log_id'
337  ],
338  __METHOD__
339  );
340  } elseif ( $rev_id ) {
341  $rc_id = $dbw->selectField(
342  [ 'revision', 'recentchanges' ],
343  'rc_id',
344  [
345  'rev_id' => $rev_id,
346  'rc_this_oldid = rev_id'
347  ],
348  __METHOD__
349  );
350  }
351  } elseif ( !$log_id && !$rev_id ) {
352  // Info might be out of date, somewhat fractionally, on replica DB.
353  $log_id = $dbw->selectField(
354  'recentchanges',
355  'rc_logid',
356  [ 'rc_id' => $rc_id ],
357  __METHOD__
358  );
359  $rev_id = $dbw->selectField(
360  'recentchanges',
361  'rc_this_oldid',
362  [ 'rc_id' => $rc_id ],
363  __METHOD__
364  );
365  }
366 
367  if ( $log_id && !$rev_id ) {
368  $rev_id = $dbw->selectField(
369  'log_search',
370  'ls_value',
371  [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ],
372  __METHOD__
373  );
374  } elseif ( !$log_id && $rev_id ) {
375  $log_id = $dbw->selectField(
376  'log_search',
377  'ls_log_id',
378  [ 'ls_field' => 'associated_rev_id', 'ls_value' => (string)$rev_id ],
379  __METHOD__
380  );
381  }
382 
383  $prevTags = self::getTags( $dbw, $rc_id, $rev_id, $log_id );
384 
385  // add tags
386  $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
387  $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
388 
389  // remove tags
390  $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
391  $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
392 
393  sort( $prevTags );
394  sort( $newTags );
395  if ( $prevTags == $newTags ) {
396  return [ [], [], $prevTags ];
397  }
398 
399  // insert a row into change_tag for each new tag
400  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
401  if ( count( $tagsToAdd ) ) {
402  $changeTagMapping = [];
403  foreach ( $tagsToAdd as $tag ) {
404  $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
405  }
406  $fname = __METHOD__;
407  // T207881: update the counts at the end of the transaction
408  $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tagsToAdd, $fname ) {
409  $dbw->update(
410  'change_tag_def',
411  [ 'ctd_count = ctd_count + 1' ],
412  [ 'ctd_name' => $tagsToAdd ],
413  $fname
414  );
415  }, $fname );
416 
417  $tagsRows = [];
418  foreach ( $tagsToAdd as $tag ) {
419  // Filter so we don't insert NULLs as zero accidentally.
420  // Keep in mind that $rc_id === null means "I don't care/know about the
421  // rc_id, just delete $tag on this revision/log entry". It doesn't
422  // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
423  $tagsRows[] = array_filter(
424  [
425  'ct_rc_id' => $rc_id,
426  'ct_log_id' => $log_id,
427  'ct_rev_id' => $rev_id,
428  'ct_params' => $params,
429  'ct_tag_id' => $changeTagMapping[$tag] ?? null,
430  ]
431  );
432 
433  }
434 
435  $dbw->insert( 'change_tag', $tagsRows, __METHOD__, [ 'IGNORE' ] );
436  }
437 
438  // delete from change_tag
439  if ( count( $tagsToRemove ) ) {
440  $fname = __METHOD__;
441  foreach ( $tagsToRemove as $tag ) {
442  $conds = array_filter(
443  [
444  'ct_rc_id' => $rc_id,
445  'ct_log_id' => $log_id,
446  'ct_rev_id' => $rev_id,
447  'ct_tag_id' => $changeTagDefStore->getId( $tag ),
448  ]
449  );
450  $dbw->delete( 'change_tag', $conds, __METHOD__ );
451  if ( $dbw->affectedRows() ) {
452  // T207881: update the counts at the end of the transaction
453  $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tag, $fname ) {
454  $dbw->update(
455  'change_tag_def',
456  [ 'ctd_count = ctd_count - 1' ],
457  [ 'ctd_name' => $tag ],
458  $fname
459  );
460 
461  $dbw->delete(
462  'change_tag_def',
463  [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
464  $fname
465  );
466  }, $fname );
467  }
468  }
469  }
470 
471  Hooks::runner()->onChangeTagsAfterUpdateTags( $tagsToAdd, $tagsToRemove, $prevTags,
472  $rc_id, $rev_id, $log_id, $params, $rc, $user );
473 
474  return [ $tagsToAdd, $tagsToRemove, $prevTags ];
475  }
476 
487  public static function getTags( IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
488  $conds = array_filter(
489  [
490  'ct_rc_id' => $rc_id,
491  'ct_rev_id' => $rev_id,
492  'ct_log_id' => $log_id,
493  ]
494  );
495 
496  $tagIds = $db->selectFieldValues(
497  'change_tag',
498  'ct_tag_id',
499  $conds,
500  __METHOD__
501  );
502 
503  $tags = [];
504  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
505  foreach ( $tagIds as $tagId ) {
506  $tags[] = $changeTagDefStore->getName( (int)$tagId );
507  }
508 
509  return $tags;
510  }
511 
522  protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
523  $lang = RequestContext::getMain()->getLanguage();
524  $tags = array_values( $tags );
525  $count = count( $tags );
526  $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
527  $lang->commaList( $tags ), $count );
528  $status->value = $tags;
529  return $status;
530  }
531 
545  public static function canAddTagsAccompanyingChange( array $tags, User $user = null ) {
546  if ( $user !== null ) {
547  if ( !MediaWikiServices::getInstance()->getPermissionManager()
548  ->userHasRight( $user, 'applychangetags' )
549  ) {
550  return Status::newFatal( 'tags-apply-no-permission' );
551  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
552  return Status::newFatal( 'tags-apply-blocked', $user->getName() );
553  }
554  }
555 
556  // to be applied, a tag has to be explicitly defined
557  $allowedTags = self::listExplicitlyDefinedTags();
558  Hooks::runner()->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
559  $disallowedTags = array_diff( $tags, $allowedTags );
560  if ( $disallowedTags ) {
561  return self::restrictedTagError( 'tags-apply-not-allowed-one',
562  'tags-apply-not-allowed-multi', $disallowedTags );
563  }
564 
565  return Status::newGood();
566  }
567 
588  public static function addTagsAccompanyingChangeWithChecks(
589  array $tags, $rc_id, $rev_id, $log_id, $params, User $user
590  ) {
591  // are we allowed to do this?
592  $result = self::canAddTagsAccompanyingChange( $tags, $user );
593  if ( !$result->isOK() ) {
594  $result->value = null;
595  return $result;
596  }
597 
598  // do it!
599  self::addTags( $tags, $rc_id, $rev_id, $log_id, $params );
600 
601  return Status::newGood( true );
602  }
603 
618  public static function canUpdateTags( array $tagsToAdd, array $tagsToRemove,
619  User $user = null
620  ) {
621  if ( $user !== null ) {
622  if ( !MediaWikiServices::getInstance()->getPermissionManager()
623  ->userHasRight( $user, 'changetags' )
624  ) {
625  return Status::newFatal( 'tags-update-no-permission' );
626  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
627  return Status::newFatal( 'tags-update-blocked', $user->getName() );
628  }
629  }
630 
631  if ( $tagsToAdd ) {
632  // to be added, a tag has to be explicitly defined
633  // @todo Allow extensions to define tags that can be applied by users...
634  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
635  $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
636  if ( $diff ) {
637  return self::restrictedTagError( 'tags-update-add-not-allowed-one',
638  'tags-update-add-not-allowed-multi', $diff );
639  }
640  }
641 
642  if ( $tagsToRemove ) {
643  // to be removed, a tag must not be defined by an extension, or equivalently it
644  // has to be either explicitly defined or not defined at all
645  // (assuming no edge case of a tag both explicitly-defined and extension-defined)
646  $softwareDefinedTags = self::listSoftwareDefinedTags();
647  $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
648  if ( $intersect ) {
649  return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
650  'tags-update-remove-not-allowed-multi', $intersect );
651  }
652  }
653 
654  return Status::newGood();
655  }
656 
687  public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
688  $rc_id, $rev_id, $log_id, $params, $reason, User $user
689  ) {
690  if ( $tagsToAdd === null ) {
691  $tagsToAdd = [];
692  }
693  if ( $tagsToRemove === null ) {
694  $tagsToRemove = [];
695  }
696  if ( !$tagsToAdd && !$tagsToRemove ) {
697  // no-op, don't bother
698  return Status::newGood( (object)[
699  'logId' => null,
700  'addedTags' => [],
701  'removedTags' => [],
702  ] );
703  }
704 
705  // are we allowed to do this?
706  $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $user );
707  if ( !$result->isOK() ) {
708  $result->value = null;
709  return $result;
710  }
711 
712  // basic rate limiting
713  if ( $user->pingLimiter( 'changetag' ) ) {
714  return Status::newFatal( 'actionthrottledtext' );
715  }
716 
717  // do it!
718  list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd,
719  $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user );
720  if ( !$tagsAdded && !$tagsRemoved ) {
721  // no-op, don't log it
722  return Status::newGood( (object)[
723  'logId' => null,
724  'addedTags' => [],
725  'removedTags' => [],
726  ] );
727  }
728 
729  // log it
730  $logEntry = new ManualLogEntry( 'tag', 'update' );
731  $logEntry->setPerformer( $user );
732  $logEntry->setComment( $reason );
733 
734  // find the appropriate target page
735  if ( $rev_id ) {
736  $revisionRecord = MediaWikiServices::getInstance()
737  ->getRevisionLookup()
738  ->getRevisionById( $rev_id );
739  if ( $revisionRecord ) {
740  $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
741  }
742  } elseif ( $log_id ) {
743  // This function is from revision deletion logic and has nothing to do with
744  // change tags, but it appears to be the only other place in core where we
745  // perform logged actions on log items.
746  $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
747  }
748 
749  if ( !$logEntry->getTarget() ) {
750  // target is required, so we have to set something
751  $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
752  }
753 
754  $logParams = [
755  '4::revid' => $rev_id,
756  '5::logid' => $log_id,
757  '6:list:tagsAdded' => $tagsAdded,
758  '7:number:tagsAddedCount' => count( $tagsAdded ),
759  '8:list:tagsRemoved' => $tagsRemoved,
760  '9:number:tagsRemovedCount' => count( $tagsRemoved ),
761  'initialTags' => $initialTags,
762  ];
763  $logEntry->setParameters( $logParams );
764  $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
765 
766  $dbw = wfGetDB( DB_MASTER );
767  $logId = $logEntry->insert( $dbw );
768  // Only send this to UDP, not RC, similar to patrol events
769  $logEntry->publish( $logId, 'udp' );
770 
771  return Status::newGood( (object)[
772  'logId' => $logId,
773  'addedTags' => $tagsAdded,
774  'removedTags' => $tagsRemoved,
775  ] );
776  }
777 
798  public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
799  &$join_conds, &$options, $filter_tag = ''
800  ) {
801  global $wgUseTagFilter;
802 
803  // Normalize to arrays
804  $tables = (array)$tables;
805  $fields = (array)$fields;
806  $conds = (array)$conds;
807  $options = (array)$options;
808 
809  $fields['ts_tags'] = self::makeTagSummarySubquery( $tables );
810 
811  // Figure out which ID field to use
812  if ( in_array( 'recentchanges', $tables ) ) {
813  $join_cond = 'ct_rc_id=rc_id';
814  } elseif ( in_array( 'logging', $tables ) ) {
815  $join_cond = 'ct_log_id=log_id';
816  } elseif ( in_array( 'revision', $tables ) ) {
817  $join_cond = 'ct_rev_id=rev_id';
818  } elseif ( in_array( 'archive', $tables ) ) {
819  $join_cond = 'ct_rev_id=ar_rev_id';
820  } else {
821  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
822  }
823 
824  if ( $wgUseTagFilter && $filter_tag ) {
825  // Somebody wants to filter on a tag.
826  // Add an INNER JOIN on change_tag
827 
828  $tagTable = 'change_tag';
829  if ( self::$avoidReopeningTablesForTesting && defined( 'MW_PHPUNIT_TEST' ) ) {
830  $db = wfGetDB( DB_REPLICA );
831 
832  if ( $db->getType() === 'mysql' ) {
833  // When filtering by tag, we are using the change_tag table twice:
834  // Once in a join for filtering, and once in a sub-query to list all
835  // tags for each revision. This does not work with temporary tables
836  // on some versions of MySQL, which causes phpunit tests to fail.
837  // As a hacky workaround, we copy the temporary table, and join
838  // against the copy. It is acknowledge that this is quite horrific.
839  // Discuss at T256006.
840 
841  $tagTable = 'change_tag_for_display_query';
842  $db->query(
843  'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $db->tableName( $tagTable )
844  . ' LIKE ' . $db->tableName( 'change_tag' )
845  );
846  $db->query(
847  'INSERT IGNORE INTO ' . $db->tableName( $tagTable )
848  . ' SELECT * FROM ' . $db->tableName( 'change_tag' )
849  );
850  }
851  }
852 
853  $tables[] = $tagTable;
854  $join_conds[$tagTable] = [ 'JOIN', $join_cond ];
855  $filterTagIds = [];
856  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
857  foreach ( (array)$filter_tag as $filterTagName ) {
858  try {
859  $filterTagIds[] = $changeTagDefStore->getId( $filterTagName );
860  } catch ( NameTableAccessException $exception ) {
861  // Return nothing.
862  $conds[] = '0=1';
863  break;
864  }
865  }
866 
867  if ( $filterTagIds !== [] ) {
868  $conds['ct_tag_id'] = $filterTagIds;
869  }
870 
871  if (
872  is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
873  !in_array( 'DISTINCT', $options )
874  ) {
875  $options[] = 'DISTINCT';
876  }
877  }
878  }
879 
888  public static function makeTagSummarySubquery( $tables ) {
889  // Normalize to arrays
890  $tables = (array)$tables;
891 
892  // Figure out which ID field to use
893  if ( in_array( 'recentchanges', $tables ) ) {
894  $join_cond = 'ct_rc_id=rc_id';
895  } elseif ( in_array( 'logging', $tables ) ) {
896  $join_cond = 'ct_log_id=log_id';
897  } elseif ( in_array( 'revision', $tables ) ) {
898  $join_cond = 'ct_rev_id=rev_id';
899  } elseif ( in_array( 'archive', $tables ) ) {
900  $join_cond = 'ct_rev_id=ar_rev_id';
901  } else {
902  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
903  }
904 
905  $tagTables = [ 'change_tag', 'change_tag_def' ];
906  $join_cond_ts_tags = [ 'change_tag_def' => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
907  $field = 'ctd_name';
908 
909  return wfGetDB( DB_REPLICA )->buildGroupConcatField(
910  ',', $tagTables, $field, $join_cond, $join_cond_ts_tags
911  );
912  }
913 
925  public static function buildTagFilterSelector(
926  $selected = '', $ooui = false, IContextSource $context = null
927  ) {
928  if ( !$context ) {
929  $context = RequestContext::getMain();
930  }
931 
932  $config = $context->getConfig();
933  if ( !$config->get( 'UseTagFilter' ) || !count( self::listDefinedTags() ) ) {
934  return [];
935  }
936 
937  $data = [
939  'label',
940  [ 'for' => 'tagfilter' ],
941  $context->msg( 'tag-filter' )->parse()
942  )
943  ];
944 
945  if ( $ooui ) {
946  $data[] = new OOUI\TextInputWidget( [
947  'id' => 'tagfilter',
948  'name' => 'tagfilter',
949  'value' => $selected,
950  'classes' => 'mw-tagfilter-input',
951  ] );
952  } else {
953  $data[] = Xml::input(
954  'tagfilter',
955  20,
956  $selected,
957  [ 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' ]
958  );
959  }
960 
961  return $data;
962  }
963 
972  public static function defineTag( $tag ) {
973  $dbw = wfGetDB( DB_MASTER );
974  $tagDef = [
975  'ctd_name' => $tag,
976  'ctd_user_defined' => 1,
977  'ctd_count' => 0
978  ];
979  $dbw->upsert(
980  'change_tag_def',
981  $tagDef,
982  'ctd_name',
983  [ 'ctd_user_defined' => 1 ],
984  __METHOD__
985  );
986 
987  // clear the memcache of defined tags
989  }
990 
999  public static function undefineTag( $tag ) {
1000  $dbw = wfGetDB( DB_MASTER );
1001 
1002  $dbw->update(
1003  'change_tag_def',
1004  [ 'ctd_user_defined' => 0 ],
1005  [ 'ctd_name' => $tag ],
1006  __METHOD__
1007  );
1008 
1009  $dbw->delete(
1010  'change_tag_def',
1011  [ 'ctd_name' => $tag, 'ctd_count' => 0 ],
1012  __METHOD__
1013  );
1014 
1015  // clear the memcache of defined tags
1017  }
1018 
1033  protected static function logTagManagementAction( $action, $tag, $reason,
1034  User $user, $tagCount = null, array $logEntryTags = []
1035  ) {
1036  $dbw = wfGetDB( DB_MASTER );
1037 
1038  $logEntry = new ManualLogEntry( 'managetags', $action );
1039  $logEntry->setPerformer( $user );
1040  // target page is not relevant, but it has to be set, so we just put in
1041  // the title of Special:Tags
1042  $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
1043  $logEntry->setComment( $reason );
1044 
1045  $params = [ '4::tag' => $tag ];
1046  if ( $tagCount !== null ) {
1047  $params['5:number:count'] = $tagCount;
1048  }
1049  $logEntry->setParameters( $params );
1050  $logEntry->setRelations( [ 'Tag' => $tag ] );
1051  $logEntry->addTags( $logEntryTags );
1052 
1053  $logId = $logEntry->insert( $dbw );
1054  $logEntry->publish( $logId );
1055  return $logId;
1056  }
1057 
1067  public static function canActivateTag( $tag, User $user = null ) {
1068  if ( $user !== null ) {
1069  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1070  ->userHasRight( $user, 'managechangetags' )
1071  ) {
1072  return Status::newFatal( 'tags-manage-no-permission' );
1073  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1074  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1075  }
1076  }
1077 
1078  // defined tags cannot be activated (a defined tag is either extension-
1079  // defined, in which case the extension chooses whether or not to active it;
1080  // or user-defined, in which case it is considered active)
1081  $definedTags = self::listDefinedTags();
1082  if ( in_array( $tag, $definedTags ) ) {
1083  return Status::newFatal( 'tags-activate-not-allowed', $tag );
1084  }
1085 
1086  // non-existing tags cannot be activated
1087  $tagUsage = self::tagUsageStatistics();
1088  if ( !isset( $tagUsage[$tag] ) ) { // we already know the tag is undefined
1089  return Status::newFatal( 'tags-activate-not-found', $tag );
1090  }
1091 
1092  return Status::newGood();
1093  }
1094 
1112  public static function activateTagWithChecks( $tag, $reason, User $user,
1113  $ignoreWarnings = false, array $logEntryTags = []
1114  ) {
1115  // are we allowed to do this?
1116  $result = self::canActivateTag( $tag, $user );
1117  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1118  $result->value = null;
1119  return $result;
1120  }
1121 
1122  // do it!
1123  self::defineTag( $tag );
1124 
1125  // log it
1126  $logId = self::logTagManagementAction( 'activate', $tag, $reason, $user,
1127  null, $logEntryTags );
1128 
1129  return Status::newGood( $logId );
1130  }
1131 
1141  public static function canDeactivateTag( $tag, User $user = null ) {
1142  if ( $user !== null ) {
1143  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1144  ->userHasRight( $user, 'managechangetags' )
1145  ) {
1146  return Status::newFatal( 'tags-manage-no-permission' );
1147  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1148  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1149  }
1150  }
1151 
1152  // only explicitly-defined tags can be deactivated
1153  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
1154  if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
1155  return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
1156  }
1157  return Status::newGood();
1158  }
1159 
1177  public static function deactivateTagWithChecks( $tag, $reason, User $user,
1178  $ignoreWarnings = false, array $logEntryTags = []
1179  ) {
1180  // are we allowed to do this?
1181  $result = self::canDeactivateTag( $tag, $user );
1182  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1183  $result->value = null;
1184  return $result;
1185  }
1186 
1187  // do it!
1188  self::undefineTag( $tag );
1189 
1190  // log it
1191  $logId = self::logTagManagementAction( 'deactivate', $tag, $reason, $user,
1192  null, $logEntryTags );
1193 
1194  return Status::newGood( $logId );
1195  }
1196 
1204  public static function isTagNameValid( $tag ) {
1205  // no empty tags
1206  if ( $tag === '' ) {
1207  return Status::newFatal( 'tags-create-no-name' );
1208  }
1209 
1210  // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
1211  // pipe (used as a delimiter between multiple tags in
1212  // SpecialRecentchanges and friends), or slashes (would break tag description messages in
1213  // MediaWiki namespace)
1214  if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
1215  || strpos( $tag, '/' ) !== false ) {
1216  return Status::newFatal( 'tags-create-invalid-chars' );
1217  }
1218 
1219  // could the MediaWiki namespace description messages be created?
1220  $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
1221  if ( $title === null ) {
1222  return Status::newFatal( 'tags-create-invalid-title-chars' );
1223  }
1224 
1225  return Status::newGood();
1226  }
1227 
1240  public static function canCreateTag( $tag, User $user = null ) {
1241  if ( $user !== null ) {
1242  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1243  ->userHasRight( $user, 'managechangetags' )
1244  ) {
1245  return Status::newFatal( 'tags-manage-no-permission' );
1246  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1247  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1248  }
1249  }
1250 
1251  $status = self::isTagNameValid( $tag );
1252  if ( !$status->isGood() ) {
1253  return $status;
1254  }
1255 
1256  // does the tag already exist?
1257  $tagUsage = self::tagUsageStatistics();
1258  if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) {
1259  return Status::newFatal( 'tags-create-already-exists', $tag );
1260  }
1261 
1262  // check with hooks
1263  $canCreateResult = Status::newGood();
1264  Hooks::runner()->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1265  return $canCreateResult;
1266  }
1267 
1287  public static function createTagWithChecks( $tag, $reason, User $user,
1288  $ignoreWarnings = false, array $logEntryTags = []
1289  ) {
1290  // are we allowed to do this?
1291  $result = self::canCreateTag( $tag, $user );
1292  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1293  $result->value = null;
1294  return $result;
1295  }
1296 
1297  // do it!
1298  self::defineTag( $tag );
1299 
1300  // log it
1301  $logId = self::logTagManagementAction( 'create', $tag, $reason, $user,
1302  null, $logEntryTags );
1303 
1304  return Status::newGood( $logId );
1305  }
1306 
1319  public static function deleteTagEverywhere( $tag ) {
1320  $dbw = wfGetDB( DB_MASTER );
1321  $dbw->startAtomic( __METHOD__ );
1322 
1323  // fetch tag id, this must be done before calling undefineTag(), see T225564
1324  $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag );
1325 
1326  // set ctd_user_defined = 0
1327  self::undefineTag( $tag );
1328 
1329  // delete from change_tag
1330  $dbw->delete( 'change_tag', [ 'ct_tag_id' => $tagId ], __METHOD__ );
1331  $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag ], __METHOD__ );
1332  $dbw->endAtomic( __METHOD__ );
1333 
1334  // give extensions a chance
1335  $status = Status::newGood();
1336  Hooks::runner()->onChangeTagAfterDelete( $tag, $status );
1337  // let's not allow error results, as the actual tag deletion succeeded
1338  if ( !$status->isOK() ) {
1339  wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' );
1340  $status->setOK( true );
1341  }
1342 
1343  // clear the memcache of defined tags
1345 
1346  return $status;
1347  }
1348 
1361  public static function canDeleteTag( $tag, User $user = null, int $flags = 0 ) {
1362  $tagUsage = self::tagUsageStatistics();
1363 
1364  if ( $user !== null ) {
1365  if ( !MediaWikiServices::getInstance()->getPermissionManager()
1366  ->userHasRight( $user, 'deletechangetags' )
1367  ) {
1368  return Status::newFatal( 'tags-delete-no-permission' );
1369  } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1370  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1371  }
1372  }
1373 
1374  if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) {
1375  return Status::newFatal( 'tags-delete-not-found', $tag );
1376  }
1377 
1378  if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1379  isset( $tagUsage[$tag] ) &&
1380  $tagUsage[$tag] > self::MAX_DELETE_USES
1381  ) {
1382  return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1383  }
1384 
1385  $softwareDefined = self::listSoftwareDefinedTags();
1386  if ( in_array( $tag, $softwareDefined ) ) {
1387  // extension-defined tags can't be deleted unless the extension
1388  // specifically allows it
1389  $status = Status::newFatal( 'tags-delete-not-allowed' );
1390  } else {
1391  // user-defined tags are deletable unless otherwise specified
1392  $status = Status::newGood();
1393  }
1394 
1395  Hooks::runner()->onChangeTagCanDelete( $tag, $user, $status );
1396  return $status;
1397  }
1398 
1416  public static function deleteTagWithChecks( $tag, $reason, User $user,
1417  $ignoreWarnings = false, array $logEntryTags = []
1418  ) {
1419  // are we allowed to do this?
1420  $result = self::canDeleteTag( $tag, $user );
1421  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1422  $result->value = null;
1423  return $result;
1424  }
1425 
1426  // store the tag usage statistics
1427  $tagUsage = self::tagUsageStatistics();
1428  $hitcount = $tagUsage[$tag] ?? 0;
1429 
1430  // do it!
1431  $deleteResult = self::deleteTagEverywhere( $tag );
1432  if ( !$deleteResult->isOK() ) {
1433  return $deleteResult;
1434  }
1435 
1436  // log it
1437  $logId = self::logTagManagementAction( 'delete', $tag, $reason, $user,
1438  $hitcount, $logEntryTags );
1439 
1440  $deleteResult->value = $logId;
1441  return $deleteResult;
1442  }
1443 
1450  public static function listSoftwareActivatedTags() {
1451  // core active tags
1452  $tags = self::getSoftwareTags();
1453  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1454  if ( !$hookContainer->isRegistered( 'ChangeTagsListActive' ) ) {
1455  return $tags;
1456  }
1457  $hookRunner = new HookRunner( $hookContainer );
1458  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1459  return $cache->getWithSetCallback(
1460  $cache->makeKey( 'active-tags' ),
1461  WANObjectCache::TTL_MINUTE * 5,
1462  function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1463  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1464 
1465  // Ask extensions which tags they consider active
1466  $hookRunner->onChangeTagsListActive( $tags );
1467  return $tags;
1468  },
1469  [
1470  'checkKeys' => [ $cache->makeKey( 'active-tags' ) ],
1471  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1472  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1473  ]
1474  );
1475  }
1476 
1483  public static function listDefinedTags() {
1485  $tags2 = self::listSoftwareDefinedTags();
1486  return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
1487  }
1488 
1497  public static function listExplicitlyDefinedTags() {
1498  $fname = __METHOD__;
1499 
1500  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1501  return $cache->getWithSetCallback(
1502  $cache->makeKey( 'valid-tags-db' ),
1503  WANObjectCache::TTL_MINUTE * 5,
1504  function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1505  $dbr = wfGetDB( DB_REPLICA );
1506 
1507  $setOpts += Database::getCacheSetOptions( $dbr );
1508 
1509  $tags = $dbr->selectFieldValues(
1510  'change_tag_def',
1511  'ctd_name',
1512  [ 'ctd_user_defined' => 1 ],
1513  $fname
1514  );
1515 
1516  return array_filter( array_unique( $tags ) );
1517  },
1518  [
1519  'checkKeys' => [ $cache->makeKey( 'valid-tags-db' ) ],
1520  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1521  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1522  ]
1523  );
1524  }
1525 
1535  public static function listSoftwareDefinedTags() {
1536  // core defined tags
1537  $tags = self::getSoftwareTags( true );
1538  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1539  if ( !$hookContainer->isRegistered( 'ListDefinedTags' ) ) {
1540  return $tags;
1541  }
1542  $hookRunner = new HookRunner( $hookContainer );
1543  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1544  return $cache->getWithSetCallback(
1545  $cache->makeKey( 'valid-tags-hook' ),
1546  WANObjectCache::TTL_MINUTE * 5,
1547  function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1548  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1549 
1550  $hookRunner->onListDefinedTags( $tags );
1551  return array_filter( array_unique( $tags ) );
1552  },
1553  [
1554  'checkKeys' => [ $cache->makeKey( 'valid-tags-hook' ) ],
1555  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1556  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1557  ]
1558  );
1559  }
1560 
1566  public static function purgeTagCacheAll() {
1567  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1568 
1569  $cache->touchCheckKey( $cache->makeKey( 'active-tags' ) );
1570  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-db' ) );
1571  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-hook' ) );
1572  $cache->touchCheckKey( $cache->makeKey( 'tags-usage-statistics' ) );
1573 
1574  MediaWikiServices::getInstance()->getChangeTagDefStore()->reloadMap();
1575  }
1576 
1583  public static function tagUsageStatistics() {
1584  $fname = __METHOD__;
1585 
1586  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1587  return $cache->getWithSetCallback(
1588  $cache->makeKey( 'tags-usage-statistics' ),
1589  WANObjectCache::TTL_MINUTE * 5,
1590  function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1591  $dbr = wfGetDB( DB_REPLICA );
1592  $res = $dbr->select(
1593  'change_tag_def',
1594  [ 'ctd_name', 'ctd_count' ],
1595  [],
1596  $fname,
1597  [ 'ORDER BY' => 'ctd_count DESC' ]
1598  );
1599 
1600  $out = [];
1601  foreach ( $res as $row ) {
1602  $out[$row->ctd_name] = $row->ctd_count;
1603  }
1604 
1605  return $out;
1606  },
1607  [
1608  'checkKeys' => [ $cache->makeKey( 'tags-usage-statistics' ) ],
1609  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1610  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1611  ]
1612  );
1613  }
1614 
1629  public static function showTagEditingUI( User $user ) {
1630  return MediaWikiServices::getInstance()->getPermissionManager()
1631  ->userHasRight( $user, 'changetags' ) &&
1632  (bool)self::listExplicitlyDefinedTags();
1633  }
1634 }
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:50
ChangeTags\makeTagSummarySubquery
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
Definition: ChangeTags.php:888
$wgUseTagFilter
$wgUseTagFilter
Allow filtering by change tag in recentchanges, history, etc Has no effect if no tags are defined in ...
Definition: DefaultSettings.php:7451
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:328
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
ChangeTags\purgeTagCacheAll
static purgeTagCacheAll()
Invalidates the short-term cache of defined tags used by the list*DefinedTags functions,...
Definition: ChangeTags.php:1566
Sanitizer\stripAllTags
static stripAllTags( $html)
Take a fragment of (potentially invalid) HTML and return a version with any tags removed,...
Definition: Sanitizer.php:1574
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:160
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:73
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:1033
SpecialPage\getTitleFor
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Definition: SpecialPage.php:92
Sanitizer\escapeClass
static escapeClass( $class)
Given a value, escape it so that it can be used as a CSS class and return it.
Definition: Sanitizer.php:976
ChangeTags\showTagEditingUI
static showTagEditingUI(User $user)
Indicate whether change tag editing UI is relevant.
Definition: ChangeTags.php:1629
ChangeTags\buildTagFilterSelector
static buildTagFilterSelector( $selected='', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
Definition: ChangeTags.php:925
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:1661
$res
$res
Definition: testCompression.php:57
ChangeTags\restrictedTagError
static restrictedTagError( $msgOne, $msgMulti, $tags)
Helper function to generate a fatal status with a 'not-allowed' type error.
Definition: ChangeTags.php:522
ChangeTags\isTagNameValid
static isTagNameValid( $tag)
Is the tag name valid?
Definition: ChangeTags.php:1204
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:29
ChangeTags\BYPASS_MAX_USAGE_CHECK
const BYPASS_MAX_USAGE_CHECK
Flag for canDeleteTag().
Definition: ChangeTags.php:41
$dbr
$dbr
Definition: testCompression.php:54
ChangeTags\truncateTagDescription
static truncateTagDescription( $tag, $length, IContextSource $context)
Get truncated message for the tag's long description.
Definition: ChangeTags.php:244
ChangeTags\tagShortDescriptionMessage
static tagShortDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's short description.
Definition: ChangeTags.php:169
ChangeTags\listSoftwareDefinedTags
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
Definition: ChangeTags.php:1535
ChangeTags\deleteTagEverywhere
static deleteTagEverywhere( $tag)
Permanently removes all traces of a tag from the DB.
Definition: ChangeTags.php:1319
ChangeTags\modifyDisplayQuery
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
Definition: ChangeTags.php:798
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:487
MWException
MediaWiki exception.
Definition: MWException.php:29
ChangeTags\undefineTag
static undefineTag( $tag)
Update ctd_user_defined = 0 field in change_tag_def.
Definition: ChangeTags.php:999
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1027
MessageLocalizer\msg
msg( $key,... $params)
This is the method for getting translated interface messages.
ChangeTags
Definition: ChangeTags.php:30
getPermissionManager
getPermissionManager()
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2475
ChangeTags\listDefinedTags
static listDefinedTags()
Basically lists defined tags which count even if they aren't applied to anything.
Definition: ChangeTags.php:1483
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:78
$title
$title
Definition: testCompression.php:38
ChangeTags\MAX_DELETE_USES
const MAX_DELETE_USES
Can't delete tags with more than this many uses.
Definition: ChangeTags.php:36
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:972
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
$wgSoftwareTags
array $wgSoftwareTags
List of core tags to enable.
Definition: DefaultSettings.php:7470
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:46
ChangeTags\canActivateTag
static canActivateTag( $tag, User $user=null)
Is it OK to allow the user to activate this tag?
Definition: ChangeTags.php:1067
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:910
ChangeTags\listExplicitlyDefinedTags
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
Definition: ChangeTags.php:1497
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:617
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
Message\plaintextParam
static plaintextParam( $plaintext)
Definition: Message.php:1101
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:1361
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
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:1177
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:454
ChangeTags\listSoftwareActivatedTags
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
Definition: ChangeTags.php:1450
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:55
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:545
$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:1240
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
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:310
MediaWiki\Storage\NameTableAccessException
Exception representing a failure to look up a row from a name table.
Definition: NameTableAccessException.php:33
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:42
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:1583
Xml\input
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition: Xml.php:278
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:1074
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:77
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:562
ChangeTags\$avoidReopeningTablesForTesting
static bool $avoidReopeningTablesForTesting
If true, this class attempts to avoid reopening database tables within the same query,...
Definition: ChangeTags.php:69
RawMessage
Variant of the Message class.
Definition: RawMessage.php:35
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:588
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:1287
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 to/from a change?
Definition: ChangeTags.php:618
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:56
ChangeTags\tagLongDescriptionMessage
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
Definition: ChangeTags.php:220
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:1416
ChangeTags\formatSummaryRow
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:114
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:687
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:273
ChangeTags\tagDescription
static tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
Definition: ChangeTags.php:203
IContextSource\getLanguage
getLanguage()
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:1112
ChangeTags\canDeactivateTag
static canDeactivateTag( $tag, User $user=null)
Is it OK to allow the user to deactivate this tag?
Definition: ChangeTags.php:1141