MediaWiki  master
ChangeTags.php
Go to the documentation of this file.
1 <?php
31 
32 class ChangeTags {
36  public const TAG_CONTENT_MODEL_CHANGE = 'mw-contentmodelchange';
41  public const TAG_NEW_REDIRECT = 'mw-new-redirect';
45  public const TAG_REMOVED_REDIRECT = 'mw-removed-redirect';
49  public const TAG_CHANGED_REDIRECT_TARGET = 'mw-changed-redirect-target';
53  public const TAG_BLANK = 'mw-blank';
57  public const TAG_REPLACE = 'mw-replace';
65  public const TAG_ROLLBACK = 'mw-rollback';
72  public const TAG_UNDO = 'mw-undo';
78  public const TAG_MANUAL_REVERT = 'mw-manual-revert';
86  public const TAG_REVERTED = 'mw-reverted';
87 
92 
96  public const BYPASS_MAX_USAGE_CHECK = 1;
97 
103  private const MAX_DELETE_USES = 5000;
104 
108  private const DEFINED_SOFTWARE_TAGS = [
109  'mw-contentmodelchange',
110  'mw-new-redirect',
111  'mw-removed-redirect',
112  'mw-changed-redirect-target',
113  'mw-blank',
114  'mw-replace',
115  'mw-rollback',
116  'mw-undo',
117  'mw-manual-revert',
118  'mw-reverted',
119  ];
120 
131  public static $avoidReopeningTablesForTesting = false;
132 
140  public static function getSoftwareTags( $all = false ) {
141  global $wgSoftwareTags;
142  $softwareTags = [];
143 
144  if ( !is_array( $wgSoftwareTags ) ) {
145  wfWarn( 'wgSoftwareTags should be associative array of enabled tags.
146  Please refer to documentation for the list of tags you can enable' );
147  return $softwareTags;
148  }
149 
150  $availableSoftwareTags = !$all ?
151  array_keys( array_filter( $wgSoftwareTags ) ) :
152  array_keys( $wgSoftwareTags );
153 
154  $softwareTags = array_intersect(
155  $availableSoftwareTags,
156  self::DEFINED_SOFTWARE_TAGS
157  );
158 
159  return $softwareTags;
160  }
161 
176  public static function formatSummaryRow( $tags, $page, IContextSource $context = null ) {
177  if ( !$tags ) {
178  return [ '', [] ];
179  }
180  if ( !$context ) {
181  $context = RequestContext::getMain();
182  }
183 
184  $classes = [];
185 
186  $tags = explode( ',', $tags );
187  $displayTags = [];
188  foreach ( $tags as $tag ) {
189  if ( !$tag ) {
190  continue;
191  }
192  $description = self::tagDescription( $tag, $context );
193  if ( $description === false ) {
194  continue;
195  }
196  $displayTags[] = Xml::tags(
197  'span',
198  [ 'class' => 'mw-tag-marker ' .
199  Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
200  $description
201  );
202  $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
203  }
204 
205  if ( !$displayTags ) {
206  return [ '', [] ];
207  }
208 
209  $markers = $context->msg( 'tag-list-wrapper' )
210  ->numParams( count( $displayTags ) )
211  ->rawParams( implode( ' ', $displayTags ) )
212  ->parse();
213  $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
214 
215  return [ $markers, $classes ];
216  }
217 
231  public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
232  $msg = $context->msg( "tag-$tag" );
233  if ( !$msg->exists() ) {
234  // No such message
235  return ( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) )
236  // HACK MessageLocalizer doesn't have a way to set the right language on a RawMessage,
237  // so extract the language from $msg and use that.
238  // The language doesn't really matter, but we need to set it to avoid requesting
239  // the user's language from session-less entry points (T227233)
240  ->inLanguage( $msg->getLanguage() );
241 
242  }
243  if ( $msg->isDisabled() ) {
244  // The message exists but is disabled, hide the tag.
245  return false;
246  }
247 
248  // Message exists and isn't disabled, use it.
249  return $msg;
250  }
251 
265  public static function tagDescription( $tag, MessageLocalizer $context ) {
266  $msg = self::tagShortDescriptionMessage( $tag, $context );
267  return $msg ? $msg->parse() : false;
268  }
269 
282  public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
283  $msg = $context->msg( "tag-$tag-description" );
284  if ( !$msg->exists() ) {
285  return false;
286  }
287  if ( $msg->isDisabled() ) {
288  // The message exists but is disabled, hide the description.
289  return false;
290  }
291 
292  // Message exists and isn't disabled, use it.
293  return $msg;
294  }
295 
310  public static function addTags( $tags, $rc_id = null, $rev_id = null,
311  $log_id = null, $params = null, RecentChange $rc = null
312  ) {
313  $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
314  return (bool)$result[0];
315  }
316 
347  public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
348  &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
349  UserIdentity $user = null
350  ) {
351  $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags...
352  $tagsToRemove = array_filter( (array)$tagsToRemove );
353 
354  if ( !$rc_id && !$rev_id && !$log_id ) {
355  throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
356  'specified when adding or removing a tag from a change!' );
357  }
358 
359  $dbw = wfGetDB( DB_MASTER );
360 
361  // Might as well look for rcids and so on.
362  if ( !$rc_id ) {
363  // Info might be out of date, somewhat fractionally, on replica DB.
364  // LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
365  // so use that relation to avoid full table scans.
366  if ( $log_id ) {
367  $rc_id = $dbw->selectField(
368  [ 'logging', 'recentchanges' ],
369  'rc_id',
370  [
371  'log_id' => $log_id,
372  'rc_timestamp = log_timestamp',
373  'rc_logid = log_id'
374  ],
375  __METHOD__
376  );
377  } elseif ( $rev_id ) {
378  $rc_id = $dbw->selectField(
379  [ 'revision', 'recentchanges' ],
380  'rc_id',
381  [
382  'rev_id' => $rev_id,
383  'rc_this_oldid = rev_id'
384  ],
385  __METHOD__
386  );
387  }
388  } elseif ( !$log_id && !$rev_id ) {
389  // Info might be out of date, somewhat fractionally, on replica DB.
390  $log_id = $dbw->selectField(
391  'recentchanges',
392  'rc_logid',
393  [ 'rc_id' => $rc_id ],
394  __METHOD__
395  );
396  $rev_id = $dbw->selectField(
397  'recentchanges',
398  'rc_this_oldid',
399  [ 'rc_id' => $rc_id ],
400  __METHOD__
401  );
402  }
403 
404  if ( $log_id && !$rev_id ) {
405  $rev_id = $dbw->selectField(
406  'log_search',
407  'ls_value',
408  [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ],
409  __METHOD__
410  );
411  } elseif ( !$log_id && $rev_id ) {
412  $log_id = $dbw->selectField(
413  'log_search',
414  'ls_log_id',
415  [ 'ls_field' => 'associated_rev_id', 'ls_value' => (string)$rev_id ],
416  __METHOD__
417  );
418  }
419 
420  $prevTags = self::getTags( $dbw, $rc_id, $rev_id, $log_id );
421 
422  // add tags
423  $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
424  $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
425 
426  // remove tags
427  $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
428  $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
429 
430  sort( $prevTags );
431  sort( $newTags );
432  if ( $prevTags == $newTags ) {
433  return [ [], [], $prevTags ];
434  }
435 
436  // insert a row into change_tag for each new tag
437  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
438  if ( count( $tagsToAdd ) ) {
439  $changeTagMapping = [];
440  foreach ( $tagsToAdd as $tag ) {
441  $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
442  }
443  $fname = __METHOD__;
444  // T207881: update the counts at the end of the transaction
445  $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tagsToAdd, $fname ) {
446  $dbw->update(
447  'change_tag_def',
448  [ 'ctd_count = ctd_count + 1' ],
449  [ 'ctd_name' => $tagsToAdd ],
450  $fname
451  );
452  }, $fname );
453 
454  $tagsRows = [];
455  foreach ( $tagsToAdd as $tag ) {
456  // Filter so we don't insert NULLs as zero accidentally.
457  // Keep in mind that $rc_id === null means "I don't care/know about the
458  // rc_id, just delete $tag on this revision/log entry". It doesn't
459  // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
460  $tagsRows[] = array_filter(
461  [
462  'ct_rc_id' => $rc_id,
463  'ct_log_id' => $log_id,
464  'ct_rev_id' => $rev_id,
465  'ct_params' => $params,
466  'ct_tag_id' => $changeTagMapping[$tag] ?? null,
467  ]
468  );
469 
470  }
471 
472  $dbw->insert( 'change_tag', $tagsRows, __METHOD__, [ 'IGNORE' ] );
473  }
474 
475  // delete from change_tag
476  if ( count( $tagsToRemove ) ) {
477  $fname = __METHOD__;
478  foreach ( $tagsToRemove as $tag ) {
479  $conds = array_filter(
480  [
481  'ct_rc_id' => $rc_id,
482  'ct_log_id' => $log_id,
483  'ct_rev_id' => $rev_id,
484  'ct_tag_id' => $changeTagDefStore->getId( $tag ),
485  ]
486  );
487  $dbw->delete( 'change_tag', $conds, __METHOD__ );
488  if ( $dbw->affectedRows() ) {
489  // T207881: update the counts at the end of the transaction
490  $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tag, $fname ) {
491  $dbw->update(
492  'change_tag_def',
493  [ 'ctd_count = ctd_count - 1' ],
494  [ 'ctd_name' => $tag ],
495  $fname
496  );
497 
498  $dbw->delete(
499  'change_tag_def',
500  [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
501  $fname
502  );
503  }, $fname );
504  }
505  }
506  }
507 
508  $userObj = $user ? MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $user ) : null;
509  Hooks::runner()->onChangeTagsAfterUpdateTags( $tagsToAdd, $tagsToRemove, $prevTags,
510  $rc_id, $rev_id, $log_id, $params, $rc, $userObj );
511 
512  return [ $tagsToAdd, $tagsToRemove, $prevTags ];
513  }
514 
526  public static function getTagsWithData(
527  IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null
528  ) {
529  $conds = array_filter(
530  [
531  'ct_rc_id' => $rc_id,
532  'ct_rev_id' => $rev_id,
533  'ct_log_id' => $log_id,
534  ]
535  );
536 
537  $result = $db->select(
538  'change_tag',
539  [ 'ct_tag_id', 'ct_params' ],
540  $conds,
541  __METHOD__
542  );
543 
544  $tags = [];
545  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
546  foreach ( $result as $row ) {
547  $tagName = $changeTagDefStore->getName( (int)$row->ct_tag_id );
548  $tags[$tagName] = $row->ct_params;
549  }
550 
551  return $tags;
552  }
553 
564  public static function getTags( IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
565  return array_keys( self::getTagsWithData( $db, $rc_id, $rev_id, $log_id ) );
566  }
567 
578  protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
579  $lang = RequestContext::getMain()->getLanguage();
580  $tags = array_values( $tags );
581  $count = count( $tags );
582  $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
583  $lang->commaList( $tags ), $count );
584  $status->value = $tags;
585  return $status;
586  }
587 
601  public static function canAddTagsAccompanyingChange( array $tags, Authority $performer = null ) {
602  $user = null;
603  if ( $performer !== null ) {
604  if ( !$performer->isAllowed( 'applychangetags' ) ) {
605  return Status::newFatal( 'tags-apply-no-permission' );
606  }
607 
608  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
609  if ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
610  return Status::newFatal( 'tags-apply-blocked', $user->getName() );
611  }
612  }
613 
614  // to be applied, a tag has to be explicitly defined
615  $allowedTags = self::listExplicitlyDefinedTags();
616  Hooks::runner()->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
617  $disallowedTags = array_diff( $tags, $allowedTags );
618  if ( $disallowedTags ) {
619  return self::restrictedTagError( 'tags-apply-not-allowed-one',
620  'tags-apply-not-allowed-multi', $disallowedTags );
621  }
622 
623  return Status::newGood();
624  }
625 
646  public static function addTagsAccompanyingChangeWithChecks(
647  array $tags, $rc_id, $rev_id, $log_id, $params, Authority $performer
648  ) {
649  // are we allowed to do this?
650  $result = self::canAddTagsAccompanyingChange( $tags, $performer );
651  if ( !$result->isOK() ) {
652  $result->value = null;
653  return $result;
654  }
655 
656  // do it!
657  self::addTags( $tags, $rc_id, $rev_id, $log_id, $params );
658 
659  return Status::newGood( true );
660  }
661 
676  public static function canUpdateTags(
677  array $tagsToAdd,
678  array $tagsToRemove,
679  Authority $performer = null
680  ) {
681  if ( $performer !== null ) {
682  if ( !$performer->isAllowed( 'changetags' ) ) {
683  return Status::newFatal( 'tags-update-no-permission' );
684  }
685 
686  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
687  if ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
688  return Status::newFatal( 'tags-update-blocked', $performer->getUser()->getName() );
689  }
690  }
691 
692  if ( $tagsToAdd ) {
693  // to be added, a tag has to be explicitly defined
694  // @todo Allow extensions to define tags that can be applied by users...
695  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
696  $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
697  if ( $diff ) {
698  return self::restrictedTagError( 'tags-update-add-not-allowed-one',
699  'tags-update-add-not-allowed-multi', $diff );
700  }
701  }
702 
703  if ( $tagsToRemove ) {
704  // to be removed, a tag must not be defined by an extension, or equivalently it
705  // has to be either explicitly defined or not defined at all
706  // (assuming no edge case of a tag both explicitly-defined and extension-defined)
707  $softwareDefinedTags = self::listSoftwareDefinedTags();
708  $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
709  if ( $intersect ) {
710  return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
711  'tags-update-remove-not-allowed-multi', $intersect );
712  }
713  }
714 
715  return Status::newGood();
716  }
717 
748  public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
749  $rc_id, $rev_id, $log_id, $params, $reason, Authority $performer
750  ) {
751  if ( $tagsToAdd === null ) {
752  $tagsToAdd = [];
753  }
754  if ( $tagsToRemove === null ) {
755  $tagsToRemove = [];
756  }
757  if ( !$tagsToAdd && !$tagsToRemove ) {
758  // no-op, don't bother
759  return Status::newGood( (object)[
760  'logId' => null,
761  'addedTags' => [],
762  'removedTags' => [],
763  ] );
764  }
765 
766  // are we allowed to do this?
767  $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $performer );
768  if ( !$result->isOK() ) {
769  $result->value = null;
770  return $result;
771  }
772 
773  // basic rate limiting
774  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
775  if ( $user->pingLimiter( 'changetag' ) ) {
776  return Status::newFatal( 'actionthrottledtext' );
777  }
778 
779  // do it!
780  list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd,
781  $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user );
782  if ( !$tagsAdded && !$tagsRemoved ) {
783  // no-op, don't log it
784  return Status::newGood( (object)[
785  'logId' => null,
786  'addedTags' => [],
787  'removedTags' => [],
788  ] );
789  }
790 
791  // log it
792  $logEntry = new ManualLogEntry( 'tag', 'update' );
793  $logEntry->setPerformer( $performer->getUser() );
794  $logEntry->setComment( $reason );
795 
796  // find the appropriate target page
797  if ( $rev_id ) {
798  $revisionRecord = MediaWikiServices::getInstance()
799  ->getRevisionLookup()
800  ->getRevisionById( $rev_id );
801  if ( $revisionRecord ) {
802  $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
803  }
804  } elseif ( $log_id ) {
805  // This function is from revision deletion logic and has nothing to do with
806  // change tags, but it appears to be the only other place in core where we
807  // perform logged actions on log items.
808  $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
809  }
810 
811  if ( !$logEntry->getTarget() ) {
812  // target is required, so we have to set something
813  $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
814  }
815 
816  $logParams = [
817  '4::revid' => $rev_id,
818  '5::logid' => $log_id,
819  '6:list:tagsAdded' => $tagsAdded,
820  '7:number:tagsAddedCount' => count( $tagsAdded ),
821  '8:list:tagsRemoved' => $tagsRemoved,
822  '9:number:tagsRemovedCount' => count( $tagsRemoved ),
823  'initialTags' => $initialTags,
824  ];
825  $logEntry->setParameters( $logParams );
826  $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
827 
828  $dbw = wfGetDB( DB_MASTER );
829  $logId = $logEntry->insert( $dbw );
830  // Only send this to UDP, not RC, similar to patrol events
831  $logEntry->publish( $logId, 'udp' );
832 
833  return Status::newGood( (object)[
834  'logId' => $logId,
835  'addedTags' => $tagsAdded,
836  'removedTags' => $tagsRemoved,
837  ] );
838  }
839 
860  public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
861  &$join_conds, &$options, $filter_tag = ''
862  ) {
863  global $wgUseTagFilter;
864 
865  // Normalize to arrays
866  $tables = (array)$tables;
867  $fields = (array)$fields;
868  $conds = (array)$conds;
869  $options = (array)$options;
870 
871  $fields['ts_tags'] = self::makeTagSummarySubquery( $tables );
872 
873  // Figure out which ID field to use
874  if ( in_array( 'recentchanges', $tables ) ) {
875  $join_cond = 'ct_rc_id=rc_id';
876  } elseif ( in_array( 'logging', $tables ) ) {
877  $join_cond = 'ct_log_id=log_id';
878  } elseif ( in_array( 'revision', $tables ) ) {
879  $join_cond = 'ct_rev_id=rev_id';
880  } elseif ( in_array( 'archive', $tables ) ) {
881  $join_cond = 'ct_rev_id=ar_rev_id';
882  } else {
883  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
884  }
885 
886  if ( $wgUseTagFilter && $filter_tag ) {
887  // Somebody wants to filter on a tag.
888  // Add an INNER JOIN on change_tag
889 
890  $tagTable = 'change_tag';
891  if ( self::$avoidReopeningTablesForTesting && defined( 'MW_PHPUNIT_TEST' ) ) {
892  $db = wfGetDB( DB_REPLICA );
893 
894  if ( $db->getType() === 'mysql' ) {
895  // When filtering by tag, we are using the change_tag table twice:
896  // Once in a join for filtering, and once in a sub-query to list all
897  // tags for each revision. This does not work with temporary tables
898  // on some versions of MySQL, which causes phpunit tests to fail.
899  // As a hacky workaround, we copy the temporary table, and join
900  // against the copy. It is acknowledge that this is quite horrific.
901  // Discuss at T256006.
902 
903  $tagTable = 'change_tag_for_display_query';
904  $db->query(
905  'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $db->tableName( $tagTable )
906  . ' LIKE ' . $db->tableName( 'change_tag' )
907  );
908  $db->query(
909  'INSERT IGNORE INTO ' . $db->tableName( $tagTable )
910  . ' SELECT * FROM ' . $db->tableName( 'change_tag' )
911  );
912  }
913  }
914 
915  $tables[] = $tagTable;
916  $join_conds[$tagTable] = [ 'JOIN', $join_cond ];
917  $filterTagIds = [];
918  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
919  foreach ( (array)$filter_tag as $filterTagName ) {
920  try {
921  $filterTagIds[] = $changeTagDefStore->getId( $filterTagName );
922  } catch ( NameTableAccessException $exception ) {
923  // Return nothing.
924  $conds[] = '0=1';
925  break;
926  }
927  }
928 
929  if ( $filterTagIds !== [] ) {
930  $conds['ct_tag_id'] = $filterTagIds;
931  }
932 
933  if (
934  is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
935  !in_array( 'DISTINCT', $options )
936  ) {
937  $options[] = 'DISTINCT';
938  }
939  }
940  }
941 
950  public static function makeTagSummarySubquery( $tables ) {
951  // Normalize to arrays
952  $tables = (array)$tables;
953 
954  // Figure out which ID field to use
955  if ( in_array( 'recentchanges', $tables ) ) {
956  $join_cond = 'ct_rc_id=rc_id';
957  } elseif ( in_array( 'logging', $tables ) ) {
958  $join_cond = 'ct_log_id=log_id';
959  } elseif ( in_array( 'revision', $tables ) ) {
960  $join_cond = 'ct_rev_id=rev_id';
961  } elseif ( in_array( 'archive', $tables ) ) {
962  $join_cond = 'ct_rev_id=ar_rev_id';
963  } else {
964  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
965  }
966 
967  $tagTables = [ 'change_tag', 'change_tag_def' ];
968  $join_cond_ts_tags = [ 'change_tag_def' => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
969  $field = 'ctd_name';
970 
971  return wfGetDB( DB_REPLICA )->buildGroupConcatField(
972  ',', $tagTables, $field, $join_cond, $join_cond_ts_tags
973  );
974  }
975 
987  public static function buildTagFilterSelector(
988  $selected = '', $ooui = false, IContextSource $context = null
989  ) {
990  if ( !$context ) {
991  $context = RequestContext::getMain();
992  }
993 
994  $config = $context->getConfig();
995  if ( !$config->get( 'UseTagFilter' ) || !count( self::listDefinedTags() ) ) {
996  return [];
997  }
998 
999  $data = [
1001  'label',
1002  [ 'for' => 'tagfilter' ],
1003  $context->msg( 'tag-filter' )->parse()
1004  )
1005  ];
1006 
1007  if ( $ooui ) {
1008  $data[] = new OOUI\TextInputWidget( [
1009  'id' => 'tagfilter',
1010  'name' => 'tagfilter',
1011  'value' => $selected,
1012  'classes' => 'mw-tagfilter-input',
1013  ] );
1014  } else {
1015  $data[] = Xml::input(
1016  'tagfilter',
1017  20,
1018  $selected,
1019  [ 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' ]
1020  );
1021  }
1022 
1023  return $data;
1024  }
1025 
1034  public static function defineTag( $tag ) {
1035  $dbw = wfGetDB( DB_MASTER );
1036  $tagDef = [
1037  'ctd_name' => $tag,
1038  'ctd_user_defined' => 1,
1039  'ctd_count' => 0
1040  ];
1041  $dbw->upsert(
1042  'change_tag_def',
1043  $tagDef,
1044  'ctd_name',
1045  [ 'ctd_user_defined' => 1 ],
1046  __METHOD__
1047  );
1048 
1049  // clear the memcache of defined tags
1051  }
1052 
1061  public static function undefineTag( $tag ) {
1062  $dbw = wfGetDB( DB_MASTER );
1063 
1064  $dbw->update(
1065  'change_tag_def',
1066  [ 'ctd_user_defined' => 0 ],
1067  [ 'ctd_name' => $tag ],
1068  __METHOD__
1069  );
1070 
1071  $dbw->delete(
1072  'change_tag_def',
1073  [ 'ctd_name' => $tag, 'ctd_count' => 0 ],
1074  __METHOD__
1075  );
1076 
1077  // clear the memcache of defined tags
1079  }
1080 
1095  protected static function logTagManagementAction( $action, $tag, $reason,
1096  UserIdentity $user, $tagCount = null, array $logEntryTags = []
1097  ) {
1098  $dbw = wfGetDB( DB_MASTER );
1099 
1100  $logEntry = new ManualLogEntry( 'managetags', $action );
1101  $logEntry->setPerformer( $user );
1102  // target page is not relevant, but it has to be set, so we just put in
1103  // the title of Special:Tags
1104  $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
1105  $logEntry->setComment( $reason );
1106 
1107  $params = [ '4::tag' => $tag ];
1108  if ( $tagCount !== null ) {
1109  $params['5:number:count'] = $tagCount;
1110  }
1111  $logEntry->setParameters( $params );
1112  $logEntry->setRelations( [ 'Tag' => $tag ] );
1113  $logEntry->addTags( $logEntryTags );
1114 
1115  $logId = $logEntry->insert( $dbw );
1116  $logEntry->publish( $logId );
1117  return $logId;
1118  }
1119 
1129  public static function canActivateTag( $tag, Authority $performer = null ) {
1130  if ( $performer !== null ) {
1131  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1132  return Status::newFatal( 'tags-manage-no-permission' );
1133  }
1134  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
1135  if ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1136  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1137  }
1138  }
1139 
1140  // defined tags cannot be activated (a defined tag is either extension-
1141  // defined, in which case the extension chooses whether or not to active it;
1142  // or user-defined, in which case it is considered active)
1143  $definedTags = self::listDefinedTags();
1144  if ( in_array( $tag, $definedTags ) ) {
1145  return Status::newFatal( 'tags-activate-not-allowed', $tag );
1146  }
1147 
1148  // non-existing tags cannot be activated
1149  $tagUsage = self::tagUsageStatistics();
1150  if ( !isset( $tagUsage[$tag] ) ) { // we already know the tag is undefined
1151  return Status::newFatal( 'tags-activate-not-found', $tag );
1152  }
1153 
1154  return Status::newGood();
1155  }
1156 
1174  public static function activateTagWithChecks( $tag, $reason, Authority $performer,
1175  $ignoreWarnings = false, array $logEntryTags = []
1176  ) {
1177  // are we allowed to do this?
1178  $result = self::canActivateTag( $tag, $performer );
1179  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1180  $result->value = null;
1181  return $result;
1182  }
1183 
1184  // do it!
1185  self::defineTag( $tag );
1186 
1187  // log it
1188  $logId = self::logTagManagementAction( 'activate', $tag, $reason, $performer->getUser(),
1189  null, $logEntryTags );
1190 
1191  return Status::newGood( $logId );
1192  }
1193 
1203  public static function canDeactivateTag( $tag, Authority $performer = null ) {
1204  if ( $performer !== null ) {
1205  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1206  return Status::newFatal( 'tags-manage-no-permission' );
1207  }
1208  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
1209  if ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1210  return Status::newFatal( 'tags-manage-blocked', $performer->getUser()->getName() );
1211  }
1212  }
1213 
1214  // only explicitly-defined tags can be deactivated
1215  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
1216  if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
1217  return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
1218  }
1219  return Status::newGood();
1220  }
1221 
1239  public static function deactivateTagWithChecks( $tag, $reason, Authority $performer,
1240  $ignoreWarnings = false, array $logEntryTags = []
1241  ) {
1242  // are we allowed to do this?
1243  $result = self::canDeactivateTag( $tag, $performer );
1244  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1245  $result->value = null;
1246  return $result;
1247  }
1248 
1249  // do it!
1250  self::undefineTag( $tag );
1251 
1252  // log it
1253  $logId = self::logTagManagementAction( 'deactivate', $tag, $reason,
1254  $performer->getUser(), null, $logEntryTags );
1255 
1256  return Status::newGood( $logId );
1257  }
1258 
1266  public static function isTagNameValid( $tag ) {
1267  // no empty tags
1268  if ( $tag === '' ) {
1269  return Status::newFatal( 'tags-create-no-name' );
1270  }
1271 
1272  // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
1273  // pipe (used as a delimiter between multiple tags in
1274  // SpecialRecentchanges and friends), or slashes (would break tag description messages in
1275  // MediaWiki namespace)
1276  if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
1277  || strpos( $tag, '/' ) !== false ) {
1278  return Status::newFatal( 'tags-create-invalid-chars' );
1279  }
1280 
1281  // could the MediaWiki namespace description messages be created?
1282  $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
1283  if ( $title === null ) {
1284  return Status::newFatal( 'tags-create-invalid-title-chars' );
1285  }
1286 
1287  return Status::newGood();
1288  }
1289 
1302  public static function canCreateTag( $tag, Authority $performer = null ) {
1303  $user = null;
1304  if ( $performer !== null ) {
1305  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1306  return Status::newFatal( 'tags-manage-no-permission' );
1307  }
1308  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
1309  if ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1310  return Status::newFatal( 'tags-manage-blocked', $performer->getUser()->getName() );
1311  }
1312  }
1313 
1314  $status = self::isTagNameValid( $tag );
1315  if ( !$status->isGood() ) {
1316  return $status;
1317  }
1318 
1319  // does the tag already exist?
1320  $tagUsage = self::tagUsageStatistics();
1321  if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) {
1322  return Status::newFatal( 'tags-create-already-exists', $tag );
1323  }
1324 
1325  // check with hooks
1326  $canCreateResult = Status::newGood();
1327  Hooks::runner()->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1328  return $canCreateResult;
1329  }
1330 
1350  public static function createTagWithChecks( $tag, $reason, Authority $performer,
1351  $ignoreWarnings = false, array $logEntryTags = []
1352  ) {
1353  // are we allowed to do this?
1354  $result = self::canCreateTag( $tag, $performer );
1355  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1356  $result->value = null;
1357  return $result;
1358  }
1359 
1360  // do it!
1361  self::defineTag( $tag );
1362 
1363  // log it
1364  $logId = self::logTagManagementAction( 'create', $tag, $reason,
1365  $performer->getUser(), null, $logEntryTags );
1366 
1367  return Status::newGood( $logId );
1368  }
1369 
1382  public static function deleteTagEverywhere( $tag ) {
1383  $dbw = wfGetDB( DB_MASTER );
1384  $dbw->startAtomic( __METHOD__ );
1385 
1386  // fetch tag id, this must be done before calling undefineTag(), see T225564
1387  $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag );
1388 
1389  // set ctd_user_defined = 0
1390  self::undefineTag( $tag );
1391 
1392  // delete from change_tag
1393  $dbw->delete( 'change_tag', [ 'ct_tag_id' => $tagId ], __METHOD__ );
1394  $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag ], __METHOD__ );
1395  $dbw->endAtomic( __METHOD__ );
1396 
1397  // give extensions a chance
1398  $status = Status::newGood();
1399  Hooks::runner()->onChangeTagAfterDelete( $tag, $status );
1400  // let's not allow error results, as the actual tag deletion succeeded
1401  if ( !$status->isOK() ) {
1402  wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' );
1403  $status->setOK( true );
1404  }
1405 
1406  // clear the memcache of defined tags
1408 
1409  return $status;
1410  }
1411 
1424  public static function canDeleteTag( $tag, Authority $performer = null, int $flags = 0 ) {
1425  $tagUsage = self::tagUsageStatistics();
1426  $user = null;
1427  if ( $performer !== null ) {
1428  if ( !$performer->isAllowed( 'deletechangetags' ) ) {
1429  return Status::newFatal( 'tags-delete-no-permission' );
1430  }
1431  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
1432  if ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1433  return Status::newFatal( 'tags-manage-blocked', $user->getName() );
1434  }
1435  }
1436 
1437  if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) {
1438  return Status::newFatal( 'tags-delete-not-found', $tag );
1439  }
1440 
1441  if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1442  isset( $tagUsage[$tag] ) &&
1443  $tagUsage[$tag] > self::MAX_DELETE_USES
1444  ) {
1445  return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1446  }
1447 
1448  $softwareDefined = self::listSoftwareDefinedTags();
1449  if ( in_array( $tag, $softwareDefined ) ) {
1450  // extension-defined tags can't be deleted unless the extension
1451  // specifically allows it
1452  $status = Status::newFatal( 'tags-delete-not-allowed' );
1453  } else {
1454  // user-defined tags are deletable unless otherwise specified
1455  $status = Status::newGood();
1456  }
1457 
1458  Hooks::runner()->onChangeTagCanDelete( $tag, $user, $status );
1459  return $status;
1460  }
1461 
1479  public static function deleteTagWithChecks( $tag, $reason, Authority $performer,
1480  $ignoreWarnings = false, array $logEntryTags = []
1481  ) {
1482  // are we allowed to do this?
1483  $result = self::canDeleteTag( $tag, $performer );
1484  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1485  $result->value = null;
1486  return $result;
1487  }
1488 
1489  // store the tag usage statistics
1490  $tagUsage = self::tagUsageStatistics();
1491  $hitcount = $tagUsage[$tag] ?? 0;
1492 
1493  // do it!
1494  $deleteResult = self::deleteTagEverywhere( $tag );
1495  if ( !$deleteResult->isOK() ) {
1496  return $deleteResult;
1497  }
1498 
1499  // log it
1500  $logId = self::logTagManagementAction( 'delete', $tag, $reason, $performer->getUser(),
1501  $hitcount, $logEntryTags );
1502 
1503  $deleteResult->value = $logId;
1504  return $deleteResult;
1505  }
1506 
1513  public static function listSoftwareActivatedTags() {
1514  // core active tags
1515  $tags = self::getSoftwareTags();
1516  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1517  if ( !$hookContainer->isRegistered( 'ChangeTagsListActive' ) ) {
1518  return $tags;
1519  }
1520  $hookRunner = new HookRunner( $hookContainer );
1521  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1522  return $cache->getWithSetCallback(
1523  $cache->makeKey( 'active-tags' ),
1524  WANObjectCache::TTL_MINUTE * 5,
1525  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1526  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1527 
1528  // Ask extensions which tags they consider active
1529  $hookRunner->onChangeTagsListActive( $tags );
1530  return $tags;
1531  },
1532  [
1533  'checkKeys' => [ $cache->makeKey( 'active-tags' ) ],
1534  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1535  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1536  ]
1537  );
1538  }
1539 
1547  public static function listDefinedTags() {
1549  $tags2 = self::listSoftwareDefinedTags();
1550  return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
1551  }
1552 
1561  public static function listExplicitlyDefinedTags() {
1562  $fname = __METHOD__;
1563 
1564  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1565  return $cache->getWithSetCallback(
1566  $cache->makeKey( 'valid-tags-db' ),
1567  WANObjectCache::TTL_MINUTE * 5,
1568  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1569  $dbr = wfGetDB( DB_REPLICA );
1570 
1571  $setOpts += Database::getCacheSetOptions( $dbr );
1572 
1573  $tags = $dbr->selectFieldValues(
1574  'change_tag_def',
1575  'ctd_name',
1576  [ 'ctd_user_defined' => 1 ],
1577  $fname
1578  );
1579 
1580  return array_filter( array_unique( $tags ) );
1581  },
1582  [
1583  'checkKeys' => [ $cache->makeKey( 'valid-tags-db' ) ],
1584  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1585  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1586  ]
1587  );
1588  }
1589 
1599  public static function listSoftwareDefinedTags() {
1600  // core defined tags
1601  $tags = self::getSoftwareTags( true );
1602  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1603  if ( !$hookContainer->isRegistered( 'ListDefinedTags' ) ) {
1604  return $tags;
1605  }
1606  $hookRunner = new HookRunner( $hookContainer );
1607  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1608  return $cache->getWithSetCallback(
1609  $cache->makeKey( 'valid-tags-hook' ),
1610  WANObjectCache::TTL_MINUTE * 5,
1611  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1612  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1613 
1614  $hookRunner->onListDefinedTags( $tags );
1615  return array_filter( array_unique( $tags ) );
1616  },
1617  [
1618  'checkKeys' => [ $cache->makeKey( 'valid-tags-hook' ) ],
1619  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1620  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1621  ]
1622  );
1623  }
1624 
1630  public static function purgeTagCacheAll() {
1631  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1632 
1633  $cache->touchCheckKey( $cache->makeKey( 'active-tags' ) );
1634  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-db' ) );
1635  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-hook' ) );
1636  $cache->touchCheckKey( $cache->makeKey( 'tags-usage-statistics' ) );
1637 
1638  MediaWikiServices::getInstance()->getChangeTagDefStore()->reloadMap();
1639  }
1640 
1647  public static function tagUsageStatistics() {
1648  $fname = __METHOD__;
1649 
1650  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1651  return $cache->getWithSetCallback(
1652  $cache->makeKey( 'tags-usage-statistics' ),
1653  WANObjectCache::TTL_MINUTE * 5,
1654  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1655  $dbr = wfGetDB( DB_REPLICA );
1656  $res = $dbr->select(
1657  'change_tag_def',
1658  [ 'ctd_name', 'ctd_count' ],
1659  [],
1660  $fname,
1661  [ 'ORDER BY' => 'ctd_count DESC' ]
1662  );
1663 
1664  $out = [];
1665  foreach ( $res as $row ) {
1666  $out[$row->ctd_name] = $row->ctd_count;
1667  }
1668 
1669  return $out;
1670  },
1671  [
1672  'checkKeys' => [ $cache->makeKey( 'tags-usage-statistics' ) ],
1673  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1674  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1675  ]
1676  );
1677  }
1678 
1693  public static function showTagEditingUI( Authority $performer ) {
1694  return $performer->isAllowed( 'changetags' ) && (bool)self::listExplicitlyDefinedTags();
1695  }
1696 }
ChangeTags\REVERT_TAGS
const REVERT_TAGS
List of tags which denote a revert of some sort.
Definition: ChangeTags.php:91
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:950
$wgUseTagFilter
$wgUseTagFilter
Allow filtering by change tag in recentchanges, history, etc Has no effect if no tags are defined.
Definition: DefaultSettings.php:7546
ChangeTags\DEFINED_SOFTWARE_TAGS
const DEFINED_SOFTWARE_TAGS
A list of tags defined and used by MediaWiki itself.
Definition: ChangeTags.php:108
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:363
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:72
ChangeTags\purgeTagCacheAll
static purgeTagCacheAll()
Invalidates the short-term cache of defined tags used by the list*DefinedTags functions,...
Definition: ChangeTags.php:1630
ChangeTags\createTagWithChecks
static createTagWithChecks( $tag, $reason, Authority $performer, $ignoreWarnings=false, array $logEntryTags=[])
Creates a tag by adding it to change_tag_def table.
Definition: ChangeTags.php:1350
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:173
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
ChangeTags\canActivateTag
static canActivateTag( $tag, Authority $performer=null)
Is it OK to allow the user to activate this tag?
Definition: ChangeTags.php:1129
ChangeTags\activateTagWithChecks
static activateTagWithChecks( $tag, $reason, Authority $performer, $ignoreWarnings=false, array $logEntryTags=[])
Activates a tag, checking whether it is allowed first, and adding a log entry afterwards.
Definition: ChangeTags.php:1174
ChangeTags\TAG_REMOVED_REDIRECT
const TAG_REMOVED_REDIRECT
The tagged edit turns a redirect page into a non-redirect.
Definition: ChangeTags.php:45
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:76
ChangeTags\updateTagsWithChecks
static updateTagsWithChecks( $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, $reason, Authority $performer)
Adds and/or removes tags to/from a given change, checking whether it is allowed first,...
Definition: ChangeTags.php:748
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:107
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:972
ChangeTags\deleteTagWithChecks
static deleteTagWithChecks( $tag, $reason, Authority $performer, $ignoreWarnings=false, array $logEntryTags=[])
Deletes a tag, checking whether it is allowed first, and adding a log entry afterwards.
Definition: ChangeTags.php:1479
ChangeTags\buildTagFilterSelector
static buildTagFilterSelector( $selected='', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
Definition: ChangeTags.php:987
$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:578
MediaWiki\Permissions\Authority\getUser
getUser()
Returns the performer of the actions associated with this authority.
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
ChangeTags\isTagNameValid
static isTagNameValid( $tag)
Is the tag name valid?
Definition: ChangeTags.php:1266
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:96
$dbr
$dbr
Definition: testCompression.php:54
ChangeTags\tagShortDescriptionMessage
static tagShortDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's short description.
Definition: ChangeTags.php:231
ChangeTags\listSoftwareDefinedTags
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
Definition: ChangeTags.php:1599
ChangeTags\TAG_CONTENT_MODEL_CHANGE
const TAG_CONTENT_MODEL_CHANGE
The tagged edit changes the content model of the page.
Definition: ChangeTags.php:36
ChangeTags\deleteTagEverywhere
static deleteTagEverywhere( $tag)
Permanently removes all traces of a tag from the DB.
Definition: ChangeTags.php:1382
ChangeTags\modifyDisplayQuery
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
Definition: ChangeTags.php:860
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:564
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:1061
MessageLocalizer\msg
msg( $key,... $params)
This is the method for getting translated interface messages.
ChangeTags
Definition: ChangeTags.php:32
ChangeTags\TAG_CHANGED_REDIRECT_TARGET
const TAG_CHANGED_REDIRECT_TARGET
The tagged edit changes the target of a redirect page.
Definition: ChangeTags.php:49
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2467
ChangeTags\TAG_MANUAL_REVERT
const TAG_MANUAL_REVERT
The tagged edit restores the page to an earlier revision.
Definition: ChangeTags.php:78
ChangeTags\canDeactivateTag
static canDeactivateTag( $tag, Authority $performer=null)
Is it OK to allow the user to deactivate this tag?
Definition: ChangeTags.php:1203
ChangeTags\listDefinedTags
static listDefinedTags()
Basically lists defined tags which count even if they aren't applied to anything.
Definition: ChangeTags.php:1547
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:140
$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:103
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:1034
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
ChangeTags\addTagsAccompanyingChangeWithChecks
static addTagsAccompanyingChangeWithChecks(array $tags, $rc_id, $rev_id, $log_id, $params, Authority $performer)
Adds tags to a given change, checking whether it is allowed first, but without adding a log entry.
Definition: ChangeTags.php:646
$wgSoftwareTags
array $wgSoftwareTags
List of core tags to enable.
Definition: DefaultSettings.php:7564
DB_MASTER
const DB_MASTER
Definition: defines.php:26
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:915
ChangeTags\TAG_BLANK
const TAG_BLANK
The tagged edit blanks the page (replaces it with the empty string).
Definition: ChangeTags.php:53
MediaWiki\Permissions\Authority
Definition: Authority.php:30
ChangeTags\listExplicitlyDefinedTags
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
Definition: ChangeTags.php:1561
ChangeTags\canAddTagsAccompanyingChange
static canAddTagsAccompanyingChange(array $tags, Authority $performer=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:601
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:652
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
Message\plaintextParam
static plaintextParam( $plaintext)
Definition: Message.php:1104
Xml\tags
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:132
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
ChangeTags\TAG_NEW_REDIRECT
const TAG_NEW_REDIRECT
The tagged edit creates a new redirect (either by creating a new page or turning an existing page int...
Definition: ChangeTags.php:41
RevDelLogList\suggestTarget
static suggestTarget( $target, array $ids)
Suggest a target for the revision deletion Optionally override this function.
Definition: RevDelLogList.php:74
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:476
ChangeTags\canCreateTag
static canCreateTag( $tag, Authority $performer=null)
Is it OK to allow the user to create this tag?
Definition: ChangeTags.php:1302
ChangeTags\listSoftwareActivatedTags
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
Definition: ChangeTags.php:1513
ChangeTags\updateTags
static updateTags( $tagsToAdd, $tagsToRemove, &$rc_id=null, &$rev_id=null, &$log_id=null, $params=null, RecentChange $rc=null, UserIdentity $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:347
ChangeTags\TAG_REPLACE
const TAG_REPLACE
The tagged edit removes more than 90% of the content of the page.
Definition: ChangeTags.php:57
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:57
ChangeTags\canUpdateTags
static canUpdateTags(array $tagsToAdd, array $tagsToRemove, Authority $performer=null)
Is it OK to allow the user to adds and remove the given tags to/from a change?
Definition: ChangeTags.php:676
ChangeTags\deactivateTagWithChecks
static deactivateTagWithChecks( $tag, $reason, Authority $performer, $ignoreWarnings=false, array $logEntryTags=[])
Deactivates a tag, checking whether it is allowed first, and adding a log entry afterwards.
Definition: ChangeTags.php:1239
MediaWiki\Permissions\Authority\isAllowed
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
$cache
$cache
Definition: mcc.php:33
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:212
MediaWiki\Storage\NameTableAccessException
Exception representing a failure to look up a row from a name table.
Definition: NameTableAccessException.php:33
ChangeTags\logTagManagementAction
static logTagManagementAction( $action, $tag, $reason, UserIdentity $user, $tagCount=null, array $logEntryTags=[])
Writes a tag action into the tag management log.
Definition: ChangeTags.php:1095
Wikimedia\Rdbms\IDatabase\select
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:43
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:1647
ChangeTags\TAG_REVERTED
const TAG_REVERTED
The tagged edit is reverted by a subsequent edit (which is tagged by one of TAG_ROLLBACK,...
Definition: ChangeTags.php:86
Xml\input
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition: Xml.php:280
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:1081
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:576
ChangeTags\$avoidReopeningTablesForTesting
static bool $avoidReopeningTablesForTesting
If true, this class attempts to avoid reopening database tables within the same query,...
Definition: ChangeTags.php:131
RawMessage
Variant of the Message class.
Definition: RawMessage.php:35
ChangeTags\TAG_ROLLBACK
const TAG_ROLLBACK
The tagged edit is a rollback (undoes the previous edit and all immediately preceding edits by the sa...
Definition: ChangeTags.php:65
ChangeTags\showTagEditingUI
static showTagEditingUI(Authority $performer)
Indicate whether change tag editing UI is relevant.
Definition: ChangeTags.php:1693
ChangeTags\tagLongDescriptionMessage
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
Definition: ChangeTags.php:282
ChangeTags\formatSummaryRow
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:176
ChangeTags\TAG_UNDO
const TAG_UNDO
The tagged edit is was performed via the "undo" link.
Definition: ChangeTags.php:72
ChangeTags\getTagsWithData
static getTagsWithData(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:526
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:310
ChangeTags\tagDescription
static tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
Definition: ChangeTags.php:265
ChangeTags\canDeleteTag
static canDeleteTag( $tag, Authority $performer=null, int $flags=0)
Is it OK to allow the user to delete this tag?
Definition: ChangeTags.php:1424