MediaWiki  master
ChangeTags.php
Go to the documentation of this file.
1 <?php
35 
36 class ChangeTags {
40  public const TAG_CONTENT_MODEL_CHANGE = 'mw-contentmodelchange';
45  public const TAG_NEW_REDIRECT = 'mw-new-redirect';
49  public const TAG_REMOVED_REDIRECT = 'mw-removed-redirect';
53  public const TAG_CHANGED_REDIRECT_TARGET = 'mw-changed-redirect-target';
57  public const TAG_BLANK = 'mw-blank';
61  public const TAG_REPLACE = 'mw-replace';
69  public const TAG_ROLLBACK = 'mw-rollback';
76  public const TAG_UNDO = 'mw-undo';
82  public const TAG_MANUAL_REVERT = 'mw-manual-revert';
90  public const TAG_REVERTED = 'mw-reverted';
94  public const TAG_SERVER_SIDE_UPLOAD = 'mw-server-side-upload';
95 
100 
104  public const BYPASS_MAX_USAGE_CHECK = 1;
105 
111  private const MAX_DELETE_USES = 5000;
112 
116  private const CHANGE_TAG = 'change_tag';
117 
121  private const CHANGE_TAG_DEF = 'change_tag_def';
122 
123  public const DISPLAY_TABLE_ALIAS = 'changetagdisplay';
124 
135  public static $avoidReopeningTablesForTesting = false;
136 
145  public static function getSoftwareTags( $all = false ) {
146  return MediaWikiServices::getInstance()->getChangeTagsStore()->getSoftwareTags( $all );
147  }
148 
162  public static function formatSummaryRow( $tags, $unused, MessageLocalizer $localizer = null ) {
163  if ( $tags === '' || $tags === null ) {
164  return [ '', [] ];
165  }
166  if ( !$localizer ) {
167  $localizer = RequestContext::getMain();
168  }
169 
170  $classes = [];
171 
172  $tags = explode( ',', $tags );
173  $order = array_flip( self::listDefinedTags() );
174  usort( $tags, static function ( $a, $b ) use ( $order ) {
175  return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
176  } );
177 
178  $displayTags = [];
179  foreach ( $tags as $tag ) {
180  if ( $tag === '' ) {
181  continue;
182  }
183  $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
184  $description = self::tagDescription( $tag, $localizer );
185  if ( $description === false ) {
186  continue;
187  }
188  $displayTags[] = Xml::tags(
189  'span',
190  [ 'class' => 'mw-tag-marker ' .
191  Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
192  $description
193  );
194  }
195 
196  if ( !$displayTags ) {
197  return [ '', $classes ];
198  }
199 
200  $markers = $localizer->msg( 'tag-list-wrapper' )
201  ->numParams( count( $displayTags ) )
202  ->rawParams( implode( ' ', $displayTags ) )
203  ->parse();
204  $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
205 
206  return [ $markers, $classes ];
207  }
208 
222  public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
223  $msg = $context->msg( "tag-$tag" );
224  if ( !$msg->exists() ) {
225  // No such message
226  // Pass through ->msg(), even though it seems redundant, to avoid requesting
227  // the user's language from session-less entry points (T227233)
228  return $context->msg( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) );
229  }
230  if ( $msg->isDisabled() ) {
231  // The message exists but is disabled, hide the tag.
232  return false;
233  }
234 
235  // Message exists and isn't disabled, use it.
236  return $msg;
237  }
238 
252  public static function tagDescription( $tag, MessageLocalizer $context ) {
253  $msg = self::tagShortDescriptionMessage( $tag, $context );
254  return $msg ? $msg->parse() : false;
255  }
256 
269  public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
270  $msg = $context->msg( "tag-$tag-description" );
271  if ( !$msg->exists() ) {
272  return false;
273  }
274  if ( $msg->isDisabled() ) {
275  // The message exists but is disabled, hide the description.
276  return false;
277  }
278 
279  // Message exists and isn't disabled, use it.
280  return $msg;
281  }
282 
296  public static function addTags( $tags, $rc_id = null, $rev_id = null,
297  $log_id = null, $params = null, RecentChange $rc = null
298  ) {
299  $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
300  return (bool)$result[0];
301  }
302 
332  public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
333  &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
334  UserIdentity $user = null
335  ) {
336  $tagsToAdd = array_filter(
337  (array)$tagsToAdd, // Make sure we're submitting all tags...
338  static function ( $value ) {
339  return ( $value ?? '' ) !== '';
340  }
341  );
342  $tagsToRemove = array_filter(
343  (array)$tagsToRemove,
344  static function ( $value ) {
345  return ( $value ?? '' ) !== '';
346  }
347  );
348 
349  if ( !$rc_id && !$rev_id && !$log_id ) {
350  throw new BadMethodCallException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
351  'specified when adding or removing a tag from a change!' );
352  }
353 
354  $dbw = MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->getPrimaryDatabase();
355 
356  // Might as well look for rcids and so on.
357  if ( !$rc_id ) {
358  // Info might be out of date, somewhat fractionally, on replica DB.
359  // LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
360  // so use that relation to avoid full table scans.
361  if ( $log_id ) {
362  $rc_id = $dbw->newSelectQueryBuilder()
363  ->select( 'rc_id' )
364  ->from( 'logging' )
365  ->join( 'recentchanges', null, [
366  'rc_timestamp = log_timestamp',
367  'rc_logid = log_id'
368  ] )
369  ->where( [ 'log_id' => $log_id ] )
370  ->caller( __METHOD__ )
371  ->fetchField();
372  } elseif ( $rev_id ) {
373  $rc_id = $dbw->newSelectQueryBuilder()
374  ->select( 'rc_id' )
375  ->from( 'revision' )
376  ->join( 'recentchanges', null, [
377  'rc_this_oldid = rev_id'
378  ] )
379  ->where( [ 'rev_id' => $rev_id ] )
380  ->caller( __METHOD__ )
381  ->fetchField();
382  }
383  } elseif ( !$log_id && !$rev_id ) {
384  // Info might be out of date, somewhat fractionally, on replica DB.
385  $log_id = $dbw->newSelectQueryBuilder()
386  ->select( 'rc_logid' )
387  ->from( 'recentchanges' )
388  ->where( [ 'rc_id' => $rc_id ] )
389  ->caller( __METHOD__ )
390  ->fetchField();
391  $rev_id = $dbw->newSelectQueryBuilder()
392  ->select( 'rc_this_oldid' )
393  ->from( 'recentchanges' )
394  ->where( [ 'rc_id' => $rc_id ] )
395  ->caller( __METHOD__ )
396  ->fetchField();
397  }
398 
399  if ( $log_id && !$rev_id ) {
400  $rev_id = $dbw->newSelectQueryBuilder()
401  ->select( 'ls_value' )
402  ->from( 'log_search' )
403  ->where( [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ] )
404  ->caller( __METHOD__ )
405  ->fetchField();
406  } elseif ( !$log_id && $rev_id ) {
407  $log_id = $dbw->newSelectQueryBuilder()
408  ->select( 'ls_log_id' )
409  ->from( 'log_search' )
410  ->where( [ 'ls_field' => 'associated_rev_id', 'ls_value' => (string)$rev_id ] )
411  ->caller( __METHOD__ )
412  ->fetchField();
413  }
414 
415  $prevTags = self::getTags( $dbw, $rc_id, $rev_id, $log_id );
416 
417  // add tags
418  $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
419  $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
420 
421  // remove tags
422  $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
423  $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
424 
425  sort( $prevTags );
426  sort( $newTags );
427  if ( $prevTags == $newTags ) {
428  return [ [], [], $prevTags ];
429  }
430 
431  // insert a row into change_tag for each new tag
432  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
433  if ( count( $tagsToAdd ) ) {
434  $changeTagMapping = [];
435  foreach ( $tagsToAdd as $tag ) {
436  $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
437  }
438  $fname = __METHOD__;
439  // T207881: update the counts at the end of the transaction
440  $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tagsToAdd, $fname ) {
441  $dbw->update(
442  self::CHANGE_TAG_DEF,
443  [ 'ctd_count = ctd_count + 1' ],
444  [ 'ctd_name' => $tagsToAdd ],
445  $fname
446  );
447  }, $fname );
448 
449  $tagsRows = [];
450  foreach ( $tagsToAdd as $tag ) {
451  // Filter so we don't insert NULLs as zero accidentally.
452  // Keep in mind that $rc_id === null means "I don't care/know about the
453  // rc_id, just delete $tag on this revision/log entry". It doesn't
454  // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
455  $tagsRows[] = array_filter(
456  [
457  'ct_rc_id' => $rc_id,
458  'ct_log_id' => $log_id,
459  'ct_rev_id' => $rev_id,
460  'ct_params' => $params,
461  'ct_tag_id' => $changeTagMapping[$tag] ?? null,
462  ]
463  );
464 
465  }
466 
467  $dbw->insert( self::CHANGE_TAG, $tagsRows, __METHOD__, [ 'IGNORE' ] );
468  }
469 
470  // delete from change_tag
471  if ( count( $tagsToRemove ) ) {
472  $fname = __METHOD__;
473  foreach ( $tagsToRemove as $tag ) {
474  $conds = array_filter(
475  [
476  'ct_rc_id' => $rc_id,
477  'ct_log_id' => $log_id,
478  'ct_rev_id' => $rev_id,
479  'ct_tag_id' => $changeTagDefStore->getId( $tag ),
480  ]
481  );
482  $dbw->delete( self::CHANGE_TAG, $conds, __METHOD__ );
483  if ( $dbw->affectedRows() ) {
484  // T207881: update the counts at the end of the transaction
485  $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tag, $fname ) {
486  $dbw->update(
487  self::CHANGE_TAG_DEF,
488  [ 'ctd_count = ctd_count - 1' ],
489  [ 'ctd_name' => $tag ],
490  $fname
491  );
492 
493  $dbw->delete(
494  self::CHANGE_TAG_DEF,
495  [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
496  $fname
497  );
498  }, $fname );
499  }
500  }
501  }
502 
503  $services = MediaWikiServices::getInstance();
504  $userObj = $user ? $services->getUserFactory()->newFromUserIdentity( $user ) : null;
505  ( new HookRunner( $services->getHookContainer() ) )->onChangeTagsAfterUpdateTags(
506  $tagsToAdd, $tagsToRemove, $prevTags, $rc_id, $rev_id, $log_id, $params, $rc, $userObj );
507 
508  return [ $tagsToAdd, $tagsToRemove, $prevTags ];
509  }
510 
523  public static function getTagsWithData(
524  IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null
525  ) {
526  return MediaWikiServices::getInstance()->getChangeTagsStore()->getTagsWithData( $db, $rc_id, $rev_id, $log_id );
527  }
528 
539  public static function getTags( IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
540  return array_keys( self::getTagsWithData( $db, $rc_id, $rev_id, $log_id ) );
541  }
542 
553  protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
554  $lang = RequestContext::getMain()->getLanguage();
555  $tags = array_values( $tags );
556  $count = count( $tags );
557  $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
558  $lang->commaList( $tags ), $count );
559  $status->value = $tags;
560  return $status;
561  }
562 
577  public static function canAddTagsAccompanyingChange(
578  array $tags,
579  Authority $performer = null,
580  $checkBlock = true
581  ) {
582  $user = null;
583  $services = MediaWikiServices::getInstance();
584  if ( $performer !== null ) {
585  if ( !$performer->isAllowed( 'applychangetags' ) ) {
586  return Status::newFatal( 'tags-apply-no-permission' );
587  }
588 
589  if ( $checkBlock && $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
590  return Status::newFatal(
591  'tags-apply-blocked',
592  $performer->getUser()->getName()
593  );
594  }
595 
596  // ChangeTagsAllowedAdd hook still needs a full User object
597  $user = $services->getUserFactory()->newFromAuthority( $performer );
598  }
599 
600  // to be applied, a tag has to be explicitly defined
601  $allowedTags = self::listExplicitlyDefinedTags();
602  ( new HookRunner( $services->getHookContainer() ) )->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
603  $disallowedTags = array_diff( $tags, $allowedTags );
604  if ( $disallowedTags ) {
605  return self::restrictedTagError( 'tags-apply-not-allowed-one',
606  'tags-apply-not-allowed-multi', $disallowedTags );
607  }
608 
609  return Status::newGood();
610  }
611 
632  public static function addTagsAccompanyingChangeWithChecks(
633  array $tags, $rc_id, $rev_id, $log_id, $params, Authority $performer
634  ) {
635  // are we allowed to do this?
636  $result = self::canAddTagsAccompanyingChange( $tags, $performer );
637  if ( !$result->isOK() ) {
638  $result->value = null;
639  return $result;
640  }
641 
642  // do it!
643  self::addTags( $tags, $rc_id, $rev_id, $log_id, $params );
644 
645  return Status::newGood( true );
646  }
647 
662  public static function canUpdateTags(
663  array $tagsToAdd,
664  array $tagsToRemove,
665  Authority $performer = null
666  ) {
667  if ( $performer !== null ) {
668  if ( !$performer->isAllowed( 'changetags' ) ) {
669  return Status::newFatal( 'tags-update-no-permission' );
670  }
671 
672  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
673  return Status::newFatal(
674  'tags-update-blocked',
675  $performer->getUser()->getName()
676  );
677  }
678  }
679 
680  if ( $tagsToAdd ) {
681  // to be added, a tag has to be explicitly defined
682  // @todo Allow extensions to define tags that can be applied by users...
683  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
684  $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
685  if ( $diff ) {
686  return self::restrictedTagError( 'tags-update-add-not-allowed-one',
687  'tags-update-add-not-allowed-multi', $diff );
688  }
689  }
690 
691  if ( $tagsToRemove ) {
692  // to be removed, a tag must not be defined by an extension, or equivalently it
693  // has to be either explicitly defined or not defined at all
694  // (assuming no edge case of a tag both explicitly-defined and extension-defined)
695  $softwareDefinedTags = self::listSoftwareDefinedTags();
696  $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
697  if ( $intersect ) {
698  return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
699  'tags-update-remove-not-allowed-multi', $intersect );
700  }
701  }
702 
703  return Status::newGood();
704  }
705 
736  public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
737  $rc_id, $rev_id, $log_id, $params, string $reason, Authority $performer
738  ) {
739  if ( !$tagsToAdd && !$tagsToRemove ) {
740  // no-op, don't bother
741  return Status::newGood( (object)[
742  'logId' => null,
743  'addedTags' => [],
744  'removedTags' => [],
745  ] );
746  }
747 
748  $tagsToAdd ??= [];
749  $tagsToRemove ??= [];
750 
751  // are we allowed to do this?
752  $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $performer );
753  if ( !$result->isOK() ) {
754  $result->value = null;
755  return $result;
756  }
757 
758  // basic rate limiting
759  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
760  if ( $user->pingLimiter( 'changetags' ) ) {
761  return Status::newFatal( 'actionthrottledtext' );
762  }
763 
764  // do it!
765  [ $tagsAdded, $tagsRemoved, $initialTags ] = self::updateTags( $tagsToAdd,
766  $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user );
767  if ( !$tagsAdded && !$tagsRemoved ) {
768  // no-op, don't log it
769  return Status::newGood( (object)[
770  'logId' => null,
771  'addedTags' => [],
772  'removedTags' => [],
773  ] );
774  }
775 
776  // log it
777  $logEntry = new ManualLogEntry( 'tag', 'update' );
778  $logEntry->setPerformer( $performer->getUser() );
779  $logEntry->setComment( $reason );
780 
781  // find the appropriate target page
782  if ( $rev_id ) {
783  $revisionRecord = MediaWikiServices::getInstance()
784  ->getRevisionLookup()
785  ->getRevisionById( $rev_id );
786  if ( $revisionRecord ) {
787  $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
788  }
789  } elseif ( $log_id ) {
790  // This function is from revision deletion logic and has nothing to do with
791  // change tags, but it appears to be the only other place in core where we
792  // perform logged actions on log items.
793  $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
794  }
795 
796  if ( !$logEntry->getTarget() ) {
797  // target is required, so we have to set something
798  $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
799  }
800 
801  $logParams = [
802  '4::revid' => $rev_id,
803  '5::logid' => $log_id,
804  '6:list:tagsAdded' => $tagsAdded,
805  '7:number:tagsAddedCount' => count( $tagsAdded ),
806  '8:list:tagsRemoved' => $tagsRemoved,
807  '9:number:tagsRemovedCount' => count( $tagsRemoved ),
808  'initialTags' => $initialTags,
809  ];
810  $logEntry->setParameters( $logParams );
811  $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
812 
813  $dbw = MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->getPrimaryDatabase();
814  $logId = $logEntry->insert( $dbw );
815  // Only send this to UDP, not RC, similar to patrol events
816  $logEntry->publish( $logId, 'udp' );
817 
818  return Status::newGood( (object)[
819  'logId' => $logId,
820  'addedTags' => $tagsAdded,
821  'removedTags' => $tagsRemoved,
822  ] );
823  }
824 
845  public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
846  &$join_conds, &$options, $filter_tag = '', bool $exclude = false
847  ) {
848  $useTagFilter = MediaWikiServices::getInstance()->getMainConfig()->get(
849  MainConfigNames::UseTagFilter );
850 
851  // Normalize to arrays
852  $tables = (array)$tables;
853  $fields = (array)$fields;
854  $conds = (array)$conds;
855  $options = (array)$options;
856 
857  $fields['ts_tags'] = self::makeTagSummarySubquery( $tables );
858  // We use an alias and qualify the conditions in case there are
859  // multiple joins to this table.
860  // In particular for compatibility with the RC filters that extension Translate does.
861 
862  // Figure out which ID field to use
863  if ( in_array( 'recentchanges', $tables ) ) {
864  $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rc_id=rc_id';
865  } elseif ( in_array( 'logging', $tables ) ) {
866  $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_log_id=log_id';
867  } elseif ( in_array( 'revision', $tables ) ) {
868  $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rev_id=rev_id';
869  } elseif ( in_array( 'archive', $tables ) ) {
870  $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rev_id=ar_rev_id';
871  } else {
872  throw new InvalidArgumentException( 'Unable to determine appropriate JOIN condition for tagging.' );
873  }
874 
875  if ( !$useTagFilter ) {
876  return;
877  }
878 
879  if ( !is_array( $filter_tag ) ) {
880  // some callers provide false or null
881  $filter_tag = (string)$filter_tag;
882  }
883 
884  if ( $filter_tag !== [] && $filter_tag !== '' ) {
885  // Somebody wants to filter on a tag.
886  // Add an INNER JOIN on change_tag
887  $tagTable = self::getDisplayTableName();
888  $filterTagIds = [];
889  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
890  foreach ( (array)$filter_tag as $filterTagName ) {
891  try {
892  $filterTagIds[] = $changeTagDefStore->getId( $filterTagName );
893  } catch ( NameTableAccessException $exception ) {
894  }
895  }
896 
897  if ( $exclude ) {
898  if ( $filterTagIds !== [] ) {
899  $tables[self::DISPLAY_TABLE_ALIAS] = $tagTable;
900  $join_conds[self::DISPLAY_TABLE_ALIAS] = [
901  'LEFT JOIN',
902  [ $join_cond, self::DISPLAY_TABLE_ALIAS . '.ct_tag_id' => $filterTagIds ]
903  ];
904  $conds[] = self::DISPLAY_TABLE_ALIAS . ".ct_tag_id IS NULL";
905  }
906  } else {
907  $tables[self::DISPLAY_TABLE_ALIAS] = $tagTable;
908  $join_conds[self::DISPLAY_TABLE_ALIAS] = [ 'JOIN', $join_cond ];
909  if ( $filterTagIds !== [] ) {
910  $conds[self::DISPLAY_TABLE_ALIAS . '.ct_tag_id'] = $filterTagIds;
911  } else {
912  // all tags were invalid, return nothing
913  $conds[] = '0=1';
914  }
915 
916  if (
917  is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
918  !in_array( 'DISTINCT', $options )
919  ) {
920  $options[] = 'DISTINCT';
921  }
922  }
923  }
924  }
925 
932  public static function getDisplayTableName() {
933  $tagTable = self::CHANGE_TAG;
934  if ( self::$avoidReopeningTablesForTesting && defined( 'MW_PHPUNIT_TEST' ) ) {
935  $db = wfGetDB( DB_REPLICA );
936 
937  if ( $db->getType() === 'mysql' ) {
938  // When filtering by tag, we are using the change_tag table twice:
939  // Once in a join for filtering, and once in a sub-query to list all
940  // tags for each revision. This does not work with temporary tables
941  // on some versions of MySQL, which causes phpunit tests to fail.
942  // As a hacky workaround, we copy the temporary table, and join
943  // against the copy. It is acknowledged that this is quite horrific.
944  // Discuss at T256006.
945 
946  $tagTable = 'change_tag_for_display_query';
947  if ( !$db->tableExists( $tagTable ) ) {
948  $db->query(
949  'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $db->tableName( $tagTable )
950  . ' LIKE ' . $db->tableName( self::CHANGE_TAG ),
951  __METHOD__
952  );
953  $db->query(
954  'INSERT IGNORE INTO ' . $db->tableName( $tagTable )
955  . ' SELECT * FROM ' . $db->tableName( self::CHANGE_TAG ),
956  __METHOD__
957  );
958  }
959  }
960  }
961  return $tagTable;
962  }
963 
972  public static function makeTagSummarySubquery( $tables ) {
973  return MediaWikiServices::getInstance()->getChangeTagsStore()->makeTagSummarySubquery( $tables );
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( MainConfigNames::UseTagFilter ) ||
996  !count( self::listDefinedTags() ) ) {
997  return [];
998  }
999 
1000  $tags = self::getChangeTagList( $context, $context->getLanguage() );
1001  $autocomplete = [];
1002  foreach ( $tags as $tagInfo ) {
1003  $autocomplete[ $tagInfo['label'] ] = $tagInfo['name'];
1004  }
1005 
1006  $data = [
1007  Html::rawElement(
1008  'label',
1009  [ 'for' => 'tagfilter' ],
1010  $context->msg( 'tag-filter' )->parse()
1011  )
1012  ];
1013 
1014  if ( $ooui ) {
1015  $options = Xml::listDropDownOptionsOoui( $autocomplete );
1016 
1017  $data[] = new OOUI\ComboBoxInputWidget( [
1018  'id' => 'tagfilter',
1019  'name' => 'tagfilter',
1020  'value' => $selected,
1021  'classes' => 'mw-tagfilter-input',
1022  'options' => $options,
1023  ] );
1024  } else {
1025  $datalist = new XmlSelect( false, 'tagfilter-datalist' );
1026  $datalist->setTagName( 'datalist' );
1027  $datalist->addOptions( $autocomplete );
1028 
1029  $data[] = Xml::input(
1030  'tagfilter',
1031  20,
1032  $selected,
1033  [
1034  'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline',
1035  'id' => 'tagfilter',
1036  'list' => 'tagfilter-datalist',
1037  ]
1038  ) . $datalist->getHTML();
1039  }
1040 
1041  return $data;
1042  }
1043 
1053  public static function defineTag( $tag ) {
1054  MediaWikiServices::getInstance()->getChangeTagsStore()->defineTag( $tag );
1055  }
1056 
1066  public static function undefineTag( $tag ) {
1067  MediaWikiServices::getInstance()->getChangeTagsStore()->undefineTag( $tag );
1068  }
1069 
1079  public static function canActivateTag( $tag, Authority $performer = null ) {
1080  if ( $performer !== null ) {
1081  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1082  return Status::newFatal( 'tags-manage-no-permission' );
1083  }
1084  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1085  return Status::newFatal(
1086  'tags-manage-blocked',
1087  $performer->getUser()->getName()
1088  );
1089  }
1090  }
1091 
1092  // defined tags cannot be activated (a defined tag is either extension-
1093  // defined, in which case the extension chooses whether or not to active it;
1094  // or user-defined, in which case it is considered active)
1095  $definedTags = self::listDefinedTags();
1096  if ( in_array( $tag, $definedTags ) ) {
1097  return Status::newFatal( 'tags-activate-not-allowed', $tag );
1098  }
1099 
1100  // non-existing tags cannot be activated
1101  $tagUsage = self::tagUsageStatistics();
1102  if ( !isset( $tagUsage[$tag] ) ) { // we already know the tag is undefined
1103  return Status::newFatal( 'tags-activate-not-found', $tag );
1104  }
1105 
1106  return Status::newGood();
1107  }
1108 
1126  public static function activateTagWithChecks( string $tag, string $reason, Authority $performer,
1127  bool $ignoreWarnings = false, array $logEntryTags = []
1128  ) {
1129  // are we allowed to do this?
1130  $result = self::canActivateTag( $tag, $performer );
1131  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1132  $result->value = null;
1133  return $result;
1134  }
1135 
1136  // do it!
1137  self::defineTag( $tag );
1138 
1139  // log it
1140  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1141  $logId = $changeTagStore->logTagManagementAction( 'activate', $tag, $reason, $performer->getUser(),
1142  null, $logEntryTags );
1143 
1144  return Status::newGood( $logId );
1145  }
1146 
1156  public static function canDeactivateTag( $tag, Authority $performer = null ) {
1157  if ( $performer !== null ) {
1158  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1159  return Status::newFatal( 'tags-manage-no-permission' );
1160  }
1161  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1162  return Status::newFatal(
1163  'tags-manage-blocked',
1164  $performer->getUser()->getName()
1165  );
1166  }
1167  }
1168 
1169  // only explicitly-defined tags can be deactivated
1170  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
1171  if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
1172  return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
1173  }
1174  return Status::newGood();
1175  }
1176 
1194  public static function deactivateTagWithChecks( string $tag, string $reason, Authority $performer,
1195  bool $ignoreWarnings = false, array $logEntryTags = []
1196  ) {
1197  // are we allowed to do this?
1198  $result = self::canDeactivateTag( $tag, $performer );
1199  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1200  $result->value = null;
1201  return $result;
1202  }
1203 
1204  // do it!
1205  self::undefineTag( $tag );
1206 
1207  // log it
1208  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1209  $logId = $changeTagStore->logTagManagementAction( 'deactivate', $tag, $reason,
1210  $performer->getUser(), null, $logEntryTags );
1211 
1212  return Status::newGood( $logId );
1213  }
1214 
1222  public static function isTagNameValid( $tag ) {
1223  // no empty tags
1224  if ( $tag === '' ) {
1225  return Status::newFatal( 'tags-create-no-name' );
1226  }
1227 
1228  // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
1229  // pipe (used as a delimiter between multiple tags in
1230  // SpecialRecentchanges and friends), or slashes (would break tag description messages in
1231  // MediaWiki namespace)
1232  if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
1233  || strpos( $tag, '/' ) !== false ) {
1234  return Status::newFatal( 'tags-create-invalid-chars' );
1235  }
1236 
1237  // could the MediaWiki namespace description messages be created?
1238  $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
1239  if ( $title === null ) {
1240  return Status::newFatal( 'tags-create-invalid-title-chars' );
1241  }
1242 
1243  return Status::newGood();
1244  }
1245 
1258  public static function canCreateTag( $tag, Authority $performer = null ) {
1259  $user = null;
1260  $services = MediaWikiServices::getInstance();
1261  if ( $performer !== null ) {
1262  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1263  return Status::newFatal( 'tags-manage-no-permission' );
1264  }
1265  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1266  return Status::newFatal(
1267  'tags-manage-blocked',
1268  $performer->getUser()->getName()
1269  );
1270  }
1271  // ChangeTagCanCreate hook still needs a full User object
1272  $user = $services->getUserFactory()->newFromAuthority( $performer );
1273  }
1274 
1275  $status = self::isTagNameValid( $tag );
1276  if ( !$status->isGood() ) {
1277  return $status;
1278  }
1279 
1280  // does the tag already exist?
1281  $tagUsage = self::tagUsageStatistics();
1282  if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) {
1283  return Status::newFatal( 'tags-create-already-exists', $tag );
1284  }
1285 
1286  // check with hooks
1287  $canCreateResult = Status::newGood();
1288  ( new HookRunner( $services->getHookContainer() ) )->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1289  return $canCreateResult;
1290  }
1291 
1311  public static function createTagWithChecks( string $tag, string $reason, Authority $performer,
1312  bool $ignoreWarnings = false, array $logEntryTags = []
1313  ) {
1314  // are we allowed to do this?
1315  $result = self::canCreateTag( $tag, $performer );
1316  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1317  $result->value = null;
1318  return $result;
1319  }
1320 
1321  // do it!
1322  self::defineTag( $tag );
1323 
1324  // log it
1325  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1326  $logId = $changeTagStore->logTagManagementAction( 'create', $tag, $reason,
1327  $performer->getUser(), null, $logEntryTags );
1328 
1329  return Status::newGood( $logId );
1330  }
1331 
1345  public static function deleteTagEverywhere( $tag ) {
1346  return MediaWikiServices::getInstance()->getChangeTagsStore()->deleteTagEverywhere( $tag );
1347  }
1348 
1361  public static function canDeleteTag( $tag, Authority $performer = null, int $flags = 0 ) {
1362  $tagUsage = self::tagUsageStatistics();
1363  $user = null;
1364  $services = MediaWikiServices::getInstance();
1365  if ( $performer !== null ) {
1366  if ( !$performer->isAllowed( 'deletechangetags' ) ) {
1367  return Status::newFatal( 'tags-delete-no-permission' );
1368  }
1369  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1370  return Status::newFatal(
1371  'tags-manage-blocked',
1372  $performer->getUser()->getName()
1373  );
1374  }
1375  // ChangeTagCanDelete hook still needs a full User object
1376  $user = $services->getUserFactory()->newFromAuthority( $performer );
1377  }
1378 
1379  if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) {
1380  return Status::newFatal( 'tags-delete-not-found', $tag );
1381  }
1382 
1383  if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1384  isset( $tagUsage[$tag] ) &&
1385  $tagUsage[$tag] > self::MAX_DELETE_USES
1386  ) {
1387  return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1388  }
1389 
1390  $softwareDefined = self::listSoftwareDefinedTags();
1391  if ( in_array( $tag, $softwareDefined ) ) {
1392  // extension-defined tags can't be deleted unless the extension
1393  // specifically allows it
1394  $status = Status::newFatal( 'tags-delete-not-allowed' );
1395  } else {
1396  // user-defined tags are deletable unless otherwise specified
1397  $status = Status::newGood();
1398  }
1399 
1400  ( new HookRunner( $services->getHookContainer() ) )->onChangeTagCanDelete( $tag, $user, $status );
1401  return $status;
1402  }
1403 
1421  public static function deleteTagWithChecks( string $tag, string $reason, Authority $performer,
1422  bool $ignoreWarnings = false, array $logEntryTags = []
1423  ) {
1424  // are we allowed to do this?
1425  $result = self::canDeleteTag( $tag, $performer );
1426  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1427  $result->value = null;
1428  return $result;
1429  }
1430 
1431  // store the tag usage statistics
1432  $tagUsage = self::tagUsageStatistics();
1433  $hitcount = $tagUsage[$tag] ?? 0;
1434 
1435  // do it!
1436  $deleteResult = self::deleteTagEverywhere( $tag );
1437  if ( !$deleteResult->isOK() ) {
1438  return $deleteResult;
1439  }
1440 
1441  // log it
1442  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1443  $logId = $changeTagStore->logTagManagementAction( 'delete', $tag, $reason, $performer->getUser(),
1444  $hitcount, $logEntryTags );
1445 
1446  $deleteResult->value = $logId;
1447  return $deleteResult;
1448  }
1449 
1456  public static function listSoftwareActivatedTags() {
1457  // core active tags
1458  $tags = self::getSoftwareTags();
1459  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1460  if ( !$hookContainer->isRegistered( 'ChangeTagsListActive' ) ) {
1461  return $tags;
1462  }
1463  $hookRunner = new HookRunner( $hookContainer );
1464  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1465  return $cache->getWithSetCallback(
1466  $cache->makeKey( 'active-tags' ),
1467  WANObjectCache::TTL_MINUTE * 5,
1468  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1469  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1470 
1471  // Ask extensions which tags they consider active
1472  $hookRunner->onChangeTagsListActive( $tags );
1473  return $tags;
1474  },
1475  [
1476  'checkKeys' => [ $cache->makeKey( 'active-tags' ) ],
1477  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1478  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1479  ]
1480  );
1481  }
1482 
1490  public static function listDefinedTags() {
1492  $tags2 = self::listSoftwareDefinedTags();
1493  return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
1494  }
1495 
1504  public static function listExplicitlyDefinedTags() {
1505  $fname = __METHOD__;
1506 
1507  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1508  return $cache->getWithSetCallback(
1509  $cache->makeKey( 'valid-tags-db' ),
1510  WANObjectCache::TTL_MINUTE * 5,
1511  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1512  $dbr = wfGetDB( DB_REPLICA );
1513 
1514  $setOpts += Database::getCacheSetOptions( $dbr );
1515  $tags = $dbr->newSelectQueryBuilder()
1516  ->select( 'ctd_name' )
1517  ->from( self::CHANGE_TAG_DEF )
1518  ->where( [ 'ctd_user_defined' => 1 ] )
1519  ->caller( $fname )
1520  ->fetchFieldValues();
1521 
1522  return array_unique( $tags );
1523  },
1524  [
1525  'checkKeys' => [ $cache->makeKey( 'valid-tags-db' ) ],
1526  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1527  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1528  ]
1529  );
1530  }
1531 
1541  public static function listSoftwareDefinedTags() {
1542  // core defined tags
1543  $tags = self::getSoftwareTags( true );
1544  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1545  if ( !$hookContainer->isRegistered( 'ListDefinedTags' ) ) {
1546  return $tags;
1547  }
1548  $hookRunner = new HookRunner( $hookContainer );
1549  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1550  return $cache->getWithSetCallback(
1551  $cache->makeKey( 'valid-tags-hook' ),
1552  WANObjectCache::TTL_MINUTE * 5,
1553  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1554  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1555 
1556  $hookRunner->onListDefinedTags( $tags );
1557  return array_unique( $tags );
1558  },
1559  [
1560  'checkKeys' => [ $cache->makeKey( 'valid-tags-hook' ) ],
1561  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1562  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1563  ]
1564  );
1565  }
1566 
1573  public static function purgeTagCacheAll() {
1574  MediaWikiServices::getInstance()->getChangeTagsStore()->purgeTagCacheAll();
1575  }
1576 
1585  public static function tagUsageStatistics() {
1586  return MediaWikiServices::getInstance()->getChangeTagsStore()->tagUsageStatistics();
1587  }
1588 
1593  private const TAG_DESC_CHARACTER_LIMIT = 120;
1594 
1619  public static function getChangeTagListSummary( MessageLocalizer $localizer, Language $lang ) {
1620  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1621  return $cache->getWithSetCallback(
1622  $cache->makeKey( 'tags-list-summary', $lang->getCode() ),
1623  WANObjectCache::TTL_DAY,
1624  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $localizer ) {
1625  $tagHitCounts = self::tagUsageStatistics();
1626 
1627  $result = [];
1628  // Only list tags that are still actively defined
1629  foreach ( self::listDefinedTags() as $tagName ) {
1630  // Only list tags with more than 0 hits
1631  $hits = $tagHitCounts[$tagName] ?? 0;
1632  if ( $hits <= 0 ) {
1633  continue;
1634  }
1635 
1636  $labelMsg = self::tagShortDescriptionMessage( $tagName, $localizer );
1637  $descriptionMsg = self::tagLongDescriptionMessage( $tagName, $localizer );
1638  // Don't cache the message object, use the correct MessageLocalizer to parse later.
1639  $result[] = [
1640  'name' => $tagName,
1641  'labelMsg' => (bool)$labelMsg,
1642  'label' => $labelMsg ? $labelMsg->plain() : $tagName,
1643  'descriptionMsg' => (bool)$descriptionMsg,
1644  'description' => $descriptionMsg ? $descriptionMsg->plain() : '',
1645  'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
1646  ];
1647  }
1648  return $result;
1649  }
1650  );
1651  }
1652 
1665  public static function getChangeTagList( MessageLocalizer $localizer, Language $lang ) {
1666  $tags = self::getChangeTagListSummary( $localizer, $lang );
1667  foreach ( $tags as &$tagInfo ) {
1668  if ( $tagInfo['labelMsg'] ) {
1669  // Use localizer with the correct page title to parse plain message from the cache.
1670  $labelMsg = new RawMessage( $tagInfo['label'] );
1671  $tagInfo['label'] = Sanitizer::stripAllTags( $localizer->msg( $labelMsg )->parse() );
1672  } else {
1673  $tagInfo['label'] = $localizer->msg( 'tag-hidden', $tagInfo['name'] )->text();
1674  }
1675  if ( $tagInfo['descriptionMsg'] ) {
1676  $descriptionMsg = new RawMessage( $tagInfo['description'] );
1677  $tagInfo['description'] = $lang->truncateForVisual(
1678  Sanitizer::stripAllTags( $localizer->msg( $descriptionMsg )->parse() ),
1679  self::TAG_DESC_CHARACTER_LIMIT
1680  );
1681  }
1682  unset( $tagInfo['labelMsg'] );
1683  unset( $tagInfo['descriptionMsg'] );
1684  }
1685 
1686  // Instead of sorting by hit count (disabled for now), sort by display name
1687  usort( $tags, static function ( $a, $b ) {
1688  return strcasecmp( $a['label'], $b['label'] );
1689  } );
1690  return $tags;
1691  }
1692 
1707  public static function showTagEditingUI( Authority $performer ) {
1708  return $performer->isAllowed( 'changetags' ) && (bool)self::listExplicitlyDefinedTags();
1709  }
1710 }
const NS_MEDIAWIKI
Definition: Defines.php:72
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
static getTagsWithData(IReadableDatabase $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:523
const TAG_MANUAL_REVERT
The tagged edit restores the page to an earlier revision.
Definition: ChangeTags.php:82
const TAG_SERVER_SIDE_UPLOAD
This tagged edit was performed while importing media files using the importImages....
Definition: ChangeTags.php:94
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
Definition: ChangeTags.php:845
const TAG_REMOVED_REDIRECT
The tagged edit turns a redirect page into a non-redirect.
Definition: ChangeTags.php:49
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
static canDeactivateTag( $tag, Authority $performer=null)
Is it OK to allow the user to deactivate this tag?
static buildTagFilterSelector( $selected='', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
Definition: ChangeTags.php:987
static canCreateTag( $tag, Authority $performer=null)
Is it OK to allow the user to create this tag?
const TAG_REPLACE
The tagged edit removes more than 90% of the content of the page.
Definition: ChangeTags.php:61
const TAG_CONTENT_MODEL_CHANGE
The tagged edit changes the content model of the page.
Definition: ChangeTags.php:40
static updateTagsWithChecks( $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, string $reason, Authority $performer)
Adds and/or removes tags to/from a given change, checking whether it is allowed first,...
Definition: ChangeTags.php:736
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
Definition: ChangeTags.php:269
static deactivateTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Deactivates a tag, checking whether it is allowed first, and adding a log entry afterwards.
static showTagEditingUI(Authority $performer)
Indicate whether change tag editing UI is relevant.
static deleteTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Deletes a tag, checking whether it is allowed first, and adding a log entry afterwards.
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:145
const TAG_CHANGED_REDIRECT_TARGET
The tagged edit changes the target of a redirect page.
Definition: ChangeTags.php:53
const TAG_REVERTED
The tagged edit is reverted by a subsequent edit (which is tagged by one of TAG_ROLLBACK,...
Definition: ChangeTags.php:90
static restrictedTagError( $msgOne, $msgMulti, $tags)
Helper function to generate a fatal status with a 'not-allowed' type error.
Definition: ChangeTags.php:553
const TAG_ROLLBACK
The tagged edit is a rollback (undoes the previous edit and all immediately preceding edits by the sa...
Definition: ChangeTags.php:69
static getChangeTagListSummary(MessageLocalizer $localizer, Language $lang)
Get information about change tags, without parsing messages, for tag filter dropdown menus.
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
Definition: ChangeTags.php:972
static getChangeTagList(MessageLocalizer $localizer, Language $lang)
Get information about change tags for tag filter dropdown menus.
const BYPASS_MAX_USAGE_CHECK
Flag for canDeleteTag().
Definition: ChangeTags.php:104
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
static undefineTag( $tag)
Update ctd_user_defined = 0 field in change_tag_def.
static canActivateTag( $tag, Authority $performer=null)
Is it OK to allow the user to activate this tag?
static formatSummaryRow( $tags, $unused, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:162
const DISPLAY_TABLE_ALIAS
Definition: ChangeTags.php:123
static purgeTagCacheAll()
Invalidates the short-term cache of defined tags used by the list*DefinedTags functions,...
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:296
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:632
static canDeleteTag( $tag, Authority $performer=null, int $flags=0)
Is it OK to allow the user to delete this tag?
static getDisplayTableName()
Get the name of the change_tag table to use for modifyDisplayQuery().
Definition: ChangeTags.php:932
static tagUsageStatistics()
Returns a map of any tags used on the wiki to number of edits tagged with them, ordered descending by...
static tagShortDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's short description.
Definition: ChangeTags.php:222
static listDefinedTags()
Basically lists defined tags which count even if they aren't applied to anything.
static tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
Definition: ChangeTags.php:252
static defineTag( $tag)
Set ctd_user_defined = 1 in change_tag_def without checking that the tag name is valid.
static createTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Creates a tag by adding it to change_tag_def table.
static getTags(IReadableDatabase $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:539
const TAG_UNDO
The tagged edit is was performed via the "undo" link.
Definition: ChangeTags.php:76
static canAddTagsAccompanyingChange(array $tags, Authority $performer=null, $checkBlock=true)
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:577
const TAG_BLANK
The tagged edit blanks the page (replaces it with the empty string).
Definition: ChangeTags.php:57
static isTagNameValid( $tag)
Is the tag name valid?
const REVERT_TAGS
List of tags which denote a revert of some sort.
Definition: ChangeTags.php:99
static activateTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Activates a tag, checking whether it is allowed first, and adding a log entry afterwards.
static deleteTagEverywhere( $tag)
Permanently removes all traces of a tag from the DB.
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:45
static bool $avoidReopeningTablesForTesting
If true, this class attempts to avoid reopening database tables within the same query,...
Definition: ChangeTags.php:135
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:662
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
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:332
Base class for language-specific code.
Definition: Language.php:57
Class for creating new log entries and inserting them into the database.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:565
This class is a collection of static functions that serve two purposes:
Definition: Html.php:55
Variant of the Message class.
Definition: RawMessage.php:40
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Exception representing a failure to look up a row from a name table.
Represents a title within MediaWiki.
Definition: Title.php:82
static plaintextParam( $plaintext)
Definition: Message.php:1267
Utility class for creating new RC entries.
static getMain()
Get the RequestContext object associated with the main request.
static suggestTarget( $target, array $ids)
Suggest a target for the revision deletion Optionally override this function.
static escapeClass( $class)
Given a value, escape it so that it can be used as a CSS class and return it.
Definition: Sanitizer.php:1108
static stripAllTags( $html)
Take a fragment of (potentially invalid) HTML and return a version with any tags removed,...
Definition: Sanitizer.php:1727
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,...
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
Class for generating HTML <select> or <datalist> elements.
Definition: XmlSelect.php:28
static listDropDownOptionsOoui( $options)
Convert options for a drop-down box into a format accepted by OOUI\DropdownInputWidget etc.
Definition: Xml.php:598
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition: Xml.php:281
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:135
Interface for objects which can provide a MediaWiki context on request.
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
getUser()
Returns the performer of the actions associated with this authority.
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Interface for objects representing user identity.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
A database connection without write operations.
const DB_REPLICA
Definition: defines.php:26
if(!isset( $args[0])) $lang