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';
90  public const TAG_SERVER_SIDE_UPLOAD = 'mw-server-side-upload';
94  public const TAG_ADD_MEDIA = 'mw-add-media';
98  public const TAG_REMOVE_MEDIA = 'mw-remove-media';
107 
111  public const BYPASS_MAX_USAGE_CHECK = 1;
112 
118  private const MAX_DELETE_USES = 5000;
119 
123  private const DEFINED_SOFTWARE_TAGS = [
124  'mw-contentmodelchange',
125  'mw-new-redirect',
126  'mw-removed-redirect',
127  'mw-changed-redirect-target',
128  'mw-blank',
129  'mw-replace',
130  'mw-rollback',
131  'mw-undo',
132  'mw-manual-revert',
133  'mw-reverted',
134  'mw-server-side-upload',
135  'mw-add-media',
136  'mw-remove-media',
137  ];
138 
149  public static $avoidReopeningTablesForTesting = false;
150 
158  public static function getSoftwareTags( $all = false ) {
159  $coreTags = MediaWikiServices::getInstance()->getMainConfig()->get( 'SoftwareTags' );
160  $softwareTags = [];
161 
162  if ( !is_array( $coreTags ) ) {
163  wfWarn( 'wgSoftwareTags should be associative array of enabled tags.
164  Please refer to documentation for the list of tags you can enable' );
165  return $softwareTags;
166  }
167 
168  $availableSoftwareTags = !$all ?
169  array_keys( array_filter( $coreTags ) ) :
170  array_keys( $coreTags );
171 
172  $softwareTags = array_intersect(
173  $availableSoftwareTags,
174  self::DEFINED_SOFTWARE_TAGS
175  );
176 
177  return $softwareTags;
178  }
179 
194  public static function formatSummaryRow( $tags, $page, IContextSource $context = null ) {
195  if ( !$tags ) {
196  return [ '', [] ];
197  }
198  if ( !$context ) {
199  $context = RequestContext::getMain();
200  }
201 
202  $classes = [];
203 
204  $tags = explode( ',', $tags );
205  $order = array_flip( self::listDefinedTags() );
206  usort( $tags, static function ( $a, $b ) use ( $order ) {
207  return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
208  } );
209 
210  $displayTags = [];
211  foreach ( $tags as $tag ) {
212  if ( !$tag ) {
213  continue;
214  }
215  $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
216  $description = self::tagDescription( $tag, $context );
217  if ( $description === false ) {
218  continue;
219  }
220  $displayTags[] = Xml::tags(
221  'span',
222  [ 'class' => 'mw-tag-marker ' .
223  Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
224  $description
225  );
226  }
227 
228  if ( !$displayTags ) {
229  return [ '', $classes ];
230  }
231 
232  $markers = $context->msg( 'tag-list-wrapper' )
233  ->numParams( count( $displayTags ) )
234  ->rawParams( implode( ' ', $displayTags ) )
235  ->parse();
236  $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
237 
238  return [ $markers, $classes ];
239  }
240 
254  public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
255  $msg = $context->msg( "tag-$tag" );
256  if ( !$msg->exists() ) {
257  // No such message
258  return ( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) )
259  // HACK MessageLocalizer doesn't have a way to set the right language on a RawMessage,
260  // so extract the language from $msg and use that.
261  // The language doesn't really matter, but we need to set it to avoid requesting
262  // the user's language from session-less entry points (T227233)
263  ->inLanguage( $msg->getLanguage() )
264  ->setInterfaceMessageFlag( true );
265  }
266  if ( $msg->isDisabled() ) {
267  // The message exists but is disabled, hide the tag.
268  return false;
269  }
270 
271  // Message exists and isn't disabled, use it.
272  return $msg;
273  }
274 
288  public static function tagDescription( $tag, MessageLocalizer $context ) {
289  $msg = self::tagShortDescriptionMessage( $tag, $context );
290  return $msg ? $msg->parse() : false;
291  }
292 
305  public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
306  $msg = $context->msg( "tag-$tag-description" );
307  if ( !$msg->exists() ) {
308  return false;
309  }
310  if ( $msg->isDisabled() ) {
311  // The message exists but is disabled, hide the description.
312  return false;
313  }
314 
315  // Message exists and isn't disabled, use it.
316  return $msg;
317  }
318 
333  public static function addTags( $tags, $rc_id = null, $rev_id = null,
334  $log_id = null, $params = null, RecentChange $rc = null
335  ) {
336  $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
337  return (bool)$result[0];
338  }
339 
370  public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
371  &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
372  UserIdentity $user = null
373  ) {
374  $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags...
375  $tagsToRemove = array_filter( (array)$tagsToRemove );
376 
377  if ( !$rc_id && !$rev_id && !$log_id ) {
378  throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
379  'specified when adding or removing a tag from a change!' );
380  }
381 
382  $dbw = wfGetDB( DB_PRIMARY );
383 
384  // Might as well look for rcids and so on.
385  if ( !$rc_id ) {
386  // Info might be out of date, somewhat fractionally, on replica DB.
387  // LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
388  // so use that relation to avoid full table scans.
389  if ( $log_id ) {
390  $rc_id = $dbw->selectField(
391  [ 'logging', 'recentchanges' ],
392  'rc_id',
393  [
394  'log_id' => $log_id,
395  'rc_timestamp = log_timestamp',
396  'rc_logid = log_id'
397  ],
398  __METHOD__
399  );
400  } elseif ( $rev_id ) {
401  $rc_id = $dbw->selectField(
402  [ 'revision', 'recentchanges' ],
403  'rc_id',
404  [
405  'rev_id' => $rev_id,
406  'rc_this_oldid = rev_id'
407  ],
408  __METHOD__
409  );
410  }
411  } elseif ( !$log_id && !$rev_id ) {
412  // Info might be out of date, somewhat fractionally, on replica DB.
413  $log_id = $dbw->selectField(
414  'recentchanges',
415  'rc_logid',
416  [ 'rc_id' => $rc_id ],
417  __METHOD__
418  );
419  $rev_id = $dbw->selectField(
420  'recentchanges',
421  'rc_this_oldid',
422  [ 'rc_id' => $rc_id ],
423  __METHOD__
424  );
425  }
426 
427  if ( $log_id && !$rev_id ) {
428  $rev_id = $dbw->selectField(
429  'log_search',
430  'ls_value',
431  [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ],
432  __METHOD__
433  );
434  } elseif ( !$log_id && $rev_id ) {
435  $log_id = $dbw->selectField(
436  'log_search',
437  'ls_log_id',
438  [ 'ls_field' => 'associated_rev_id', 'ls_value' => (string)$rev_id ],
439  __METHOD__
440  );
441  }
442 
443  $prevTags = self::getTags( $dbw, $rc_id, $rev_id, $log_id );
444 
445  // add tags
446  $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
447  $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
448 
449  // remove tags
450  $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
451  $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
452 
453  sort( $prevTags );
454  sort( $newTags );
455  if ( $prevTags == $newTags ) {
456  return [ [], [], $prevTags ];
457  }
458 
459  // insert a row into change_tag for each new tag
460  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
461  if ( count( $tagsToAdd ) ) {
462  $changeTagMapping = [];
463  foreach ( $tagsToAdd as $tag ) {
464  $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
465  }
466  $fname = __METHOD__;
467  // T207881: update the counts at the end of the transaction
468  $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tagsToAdd, $fname ) {
469  $dbw->update(
470  'change_tag_def',
471  [ 'ctd_count = ctd_count + 1' ],
472  [ 'ctd_name' => $tagsToAdd ],
473  $fname
474  );
475  }, $fname );
476 
477  $tagsRows = [];
478  foreach ( $tagsToAdd as $tag ) {
479  // Filter so we don't insert NULLs as zero accidentally.
480  // Keep in mind that $rc_id === null means "I don't care/know about the
481  // rc_id, just delete $tag on this revision/log entry". It doesn't
482  // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
483  $tagsRows[] = array_filter(
484  [
485  'ct_rc_id' => $rc_id,
486  'ct_log_id' => $log_id,
487  'ct_rev_id' => $rev_id,
488  'ct_params' => $params,
489  'ct_tag_id' => $changeTagMapping[$tag] ?? null,
490  ]
491  );
492 
493  }
494 
495  $dbw->insert( 'change_tag', $tagsRows, __METHOD__, [ 'IGNORE' ] );
496  }
497 
498  // delete from change_tag
499  if ( count( $tagsToRemove ) ) {
500  $fname = __METHOD__;
501  foreach ( $tagsToRemove as $tag ) {
502  $conds = array_filter(
503  [
504  'ct_rc_id' => $rc_id,
505  'ct_log_id' => $log_id,
506  'ct_rev_id' => $rev_id,
507  'ct_tag_id' => $changeTagDefStore->getId( $tag ),
508  ]
509  );
510  $dbw->delete( 'change_tag', $conds, __METHOD__ );
511  if ( $dbw->affectedRows() ) {
512  // T207881: update the counts at the end of the transaction
513  $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tag, $fname ) {
514  $dbw->update(
515  'change_tag_def',
516  [ 'ctd_count = ctd_count - 1' ],
517  [ 'ctd_name' => $tag ],
518  $fname
519  );
520 
521  $dbw->delete(
522  'change_tag_def',
523  [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
524  $fname
525  );
526  }, $fname );
527  }
528  }
529  }
530 
531  $userObj = $user ? MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $user ) : null;
532  Hooks::runner()->onChangeTagsAfterUpdateTags( $tagsToAdd, $tagsToRemove, $prevTags,
533  $rc_id, $rev_id, $log_id, $params, $rc, $userObj );
534 
535  return [ $tagsToAdd, $tagsToRemove, $prevTags ];
536  }
537 
550  public static function getTagsWithData(
551  IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null
552  ) {
553  if ( !$rc_id && !$rev_id && !$log_id ) {
554  throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
555  'specified when loading tags from a change!' );
556  }
557 
558  $conds = array_filter(
559  [
560  'ct_rc_id' => $rc_id,
561  'ct_rev_id' => $rev_id,
562  'ct_log_id' => $log_id,
563  ]
564  );
565 
566  $result = $db->select(
567  'change_tag',
568  [ 'ct_tag_id', 'ct_params' ],
569  $conds,
570  __METHOD__
571  );
572 
573  $tags = [];
574  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
575  foreach ( $result as $row ) {
576  $tagName = $changeTagDefStore->getName( (int)$row->ct_tag_id );
577  $tags[$tagName] = $row->ct_params;
578  }
579 
580  return $tags;
581  }
582 
593  public static function getTags( IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
594  return array_keys( self::getTagsWithData( $db, $rc_id, $rev_id, $log_id ) );
595  }
596 
607  protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
608  $lang = RequestContext::getMain()->getLanguage();
609  $tags = array_values( $tags );
610  $count = count( $tags );
611  $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
612  $lang->commaList( $tags ), $count );
613  $status->value = $tags;
614  return $status;
615  }
616 
630  public static function canAddTagsAccompanyingChange( array $tags, Authority $performer = null ) {
631  $user = null;
632  if ( $performer !== null ) {
633  if ( !$performer->isAllowed( 'applychangetags' ) ) {
634  return Status::newFatal( 'tags-apply-no-permission' );
635  }
636 
637  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
638  return Status::newFatal(
639  'tags-apply-blocked',
640  $performer->getUser()->getName()
641  );
642  }
643 
644  // ChangeTagsAllowedAdd hook still needs a full User object
645  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
646  }
647 
648  // to be applied, a tag has to be explicitly defined
649  $allowedTags = self::listExplicitlyDefinedTags();
650  Hooks::runner()->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
651  $disallowedTags = array_diff( $tags, $allowedTags );
652  if ( $disallowedTags ) {
653  return self::restrictedTagError( 'tags-apply-not-allowed-one',
654  'tags-apply-not-allowed-multi', $disallowedTags );
655  }
656 
657  return Status::newGood();
658  }
659 
680  public static function addTagsAccompanyingChangeWithChecks(
681  array $tags, $rc_id, $rev_id, $log_id, $params, Authority $performer
682  ) {
683  // are we allowed to do this?
684  $result = self::canAddTagsAccompanyingChange( $tags, $performer );
685  if ( !$result->isOK() ) {
686  $result->value = null;
687  return $result;
688  }
689 
690  // do it!
691  self::addTags( $tags, $rc_id, $rev_id, $log_id, $params );
692 
693  return Status::newGood( true );
694  }
695 
710  public static function canUpdateTags(
711  array $tagsToAdd,
712  array $tagsToRemove,
713  Authority $performer = null
714  ) {
715  if ( $performer !== null ) {
716  if ( !$performer->isAllowed( 'changetags' ) ) {
717  return Status::newFatal( 'tags-update-no-permission' );
718  }
719 
720  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
721  return Status::newFatal(
722  'tags-update-blocked',
723  $performer->getUser()->getName()
724  );
725  }
726  }
727 
728  if ( $tagsToAdd ) {
729  // to be added, a tag has to be explicitly defined
730  // @todo Allow extensions to define tags that can be applied by users...
731  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
732  $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
733  if ( $diff ) {
734  return self::restrictedTagError( 'tags-update-add-not-allowed-one',
735  'tags-update-add-not-allowed-multi', $diff );
736  }
737  }
738 
739  if ( $tagsToRemove ) {
740  // to be removed, a tag must not be defined by an extension, or equivalently it
741  // has to be either explicitly defined or not defined at all
742  // (assuming no edge case of a tag both explicitly-defined and extension-defined)
743  $softwareDefinedTags = self::listSoftwareDefinedTags();
744  $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
745  if ( $intersect ) {
746  return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
747  'tags-update-remove-not-allowed-multi', $intersect );
748  }
749  }
750 
751  return Status::newGood();
752  }
753 
784  public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
785  $rc_id, $rev_id, $log_id, $params, $reason, Authority $performer
786  ) {
787  if ( $tagsToAdd === null ) {
788  $tagsToAdd = [];
789  }
790  if ( $tagsToRemove === null ) {
791  $tagsToRemove = [];
792  }
793  if ( !$tagsToAdd && !$tagsToRemove ) {
794  // no-op, don't bother
795  return Status::newGood( (object)[
796  'logId' => null,
797  'addedTags' => [],
798  'removedTags' => [],
799  ] );
800  }
801 
802  // are we allowed to do this?
803  $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $performer );
804  if ( !$result->isOK() ) {
805  $result->value = null;
806  return $result;
807  }
808 
809  // basic rate limiting
810  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
811  if ( $user->pingLimiter( 'changetag' ) ) {
812  return Status::newFatal( 'actionthrottledtext' );
813  }
814 
815  // do it!
816  list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd,
817  $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user );
818  if ( !$tagsAdded && !$tagsRemoved ) {
819  // no-op, don't log it
820  return Status::newGood( (object)[
821  'logId' => null,
822  'addedTags' => [],
823  'removedTags' => [],
824  ] );
825  }
826 
827  // log it
828  $logEntry = new ManualLogEntry( 'tag', 'update' );
829  $logEntry->setPerformer( $performer->getUser() );
830  $logEntry->setComment( $reason );
831 
832  // find the appropriate target page
833  if ( $rev_id ) {
834  $revisionRecord = MediaWikiServices::getInstance()
835  ->getRevisionLookup()
836  ->getRevisionById( $rev_id );
837  if ( $revisionRecord ) {
838  $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
839  }
840  } elseif ( $log_id ) {
841  // This function is from revision deletion logic and has nothing to do with
842  // change tags, but it appears to be the only other place in core where we
843  // perform logged actions on log items.
844  $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
845  }
846 
847  if ( !$logEntry->getTarget() ) {
848  // target is required, so we have to set something
849  $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
850  }
851 
852  $logParams = [
853  '4::revid' => $rev_id,
854  '5::logid' => $log_id,
855  '6:list:tagsAdded' => $tagsAdded,
856  '7:number:tagsAddedCount' => count( $tagsAdded ),
857  '8:list:tagsRemoved' => $tagsRemoved,
858  '9:number:tagsRemovedCount' => count( $tagsRemoved ),
859  'initialTags' => $initialTags,
860  ];
861  $logEntry->setParameters( $logParams );
862  $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
863 
864  $dbw = wfGetDB( DB_PRIMARY );
865  $logId = $logEntry->insert( $dbw );
866  // Only send this to UDP, not RC, similar to patrol events
867  $logEntry->publish( $logId, 'udp' );
868 
869  return Status::newGood( (object)[
870  'logId' => $logId,
871  'addedTags' => $tagsAdded,
872  'removedTags' => $tagsRemoved,
873  ] );
874  }
875 
896  public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
897  &$join_conds, &$options, $filter_tag = ''
898  ) {
899  $useTagFilter = MediaWikiServices::getInstance()->getMainConfig()->get( 'UseTagFilter' );
900 
901  // Normalize to arrays
902  $tables = (array)$tables;
903  $fields = (array)$fields;
904  $conds = (array)$conds;
905  $options = (array)$options;
906 
907  $fields['ts_tags'] = self::makeTagSummarySubquery( $tables );
908 
909  // Figure out which ID field to use
910  if ( in_array( 'recentchanges', $tables ) ) {
911  $join_cond = 'ct_rc_id=rc_id';
912  } elseif ( in_array( 'logging', $tables ) ) {
913  $join_cond = 'ct_log_id=log_id';
914  } elseif ( in_array( 'revision', $tables ) ) {
915  $join_cond = 'ct_rev_id=rev_id';
916  } elseif ( in_array( 'archive', $tables ) ) {
917  $join_cond = 'ct_rev_id=ar_rev_id';
918  } else {
919  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
920  }
921 
922  if ( $useTagFilter && $filter_tag ) {
923  // Somebody wants to filter on a tag.
924  // Add an INNER JOIN on change_tag
925  $tagTable = self::getDisplayTableName();
926  $tables[] = $tagTable;
927  $join_conds[$tagTable] = [ 'JOIN', $join_cond ];
928  $filterTagIds = [];
929  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
930  foreach ( (array)$filter_tag as $filterTagName ) {
931  try {
932  $filterTagIds[] = $changeTagDefStore->getId( $filterTagName );
933  } catch ( NameTableAccessException $exception ) {
934  // Return nothing.
935  $conds[] = '0=1';
936  break;
937  }
938  }
939 
940  if ( $filterTagIds !== [] ) {
941  $conds['ct_tag_id'] = $filterTagIds;
942  }
943 
944  if (
945  is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
946  !in_array( 'DISTINCT', $options )
947  ) {
948  $options[] = 'DISTINCT';
949  }
950  }
951  }
952 
959  public static function getDisplayTableName() {
960  $tagTable = 'change_tag';
961  if ( self::$avoidReopeningTablesForTesting && defined( 'MW_PHPUNIT_TEST' ) ) {
962  $db = wfGetDB( DB_REPLICA );
963 
964  if ( $db->getType() === 'mysql' ) {
965  // When filtering by tag, we are using the change_tag table twice:
966  // Once in a join for filtering, and once in a sub-query to list all
967  // tags for each revision. This does not work with temporary tables
968  // on some versions of MySQL, which causes phpunit tests to fail.
969  // As a hacky workaround, we copy the temporary table, and join
970  // against the copy. It is acknowledged that this is quite horrific.
971  // Discuss at T256006.
972 
973  $tagTable = 'change_tag_for_display_query';
974  if ( !$db->tableExists( $tagTable ) ) {
975  $db->query(
976  'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $db->tableName( $tagTable )
977  . ' LIKE ' . $db->tableName( 'change_tag' ),
978  __METHOD__
979  );
980  $db->query(
981  'INSERT IGNORE INTO ' . $db->tableName( $tagTable )
982  . ' SELECT * FROM ' . $db->tableName( 'change_tag' ),
983  __METHOD__
984  );
985  }
986  }
987  }
988  return $tagTable;
989  }
990 
999  public static function makeTagSummarySubquery( $tables ) {
1000  // Normalize to arrays
1001  $tables = (array)$tables;
1002 
1003  // Figure out which ID field to use
1004  if ( in_array( 'recentchanges', $tables ) ) {
1005  $join_cond = 'ct_rc_id=rc_id';
1006  } elseif ( in_array( 'logging', $tables ) ) {
1007  $join_cond = 'ct_log_id=log_id';
1008  } elseif ( in_array( 'revision', $tables ) ) {
1009  $join_cond = 'ct_rev_id=rev_id';
1010  } elseif ( in_array( 'archive', $tables ) ) {
1011  $join_cond = 'ct_rev_id=ar_rev_id';
1012  } else {
1013  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
1014  }
1015 
1016  $tagTables = [ 'change_tag', 'change_tag_def' ];
1017  $join_cond_ts_tags = [ 'change_tag_def' => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
1018  $field = 'ctd_name';
1019 
1020  return wfGetDB( DB_REPLICA )->buildGroupConcatField(
1021  ',', $tagTables, $field, $join_cond, $join_cond_ts_tags
1022  );
1023  }
1024 
1036  public static function buildTagFilterSelector(
1037  $selected = '', $ooui = false, IContextSource $context = null
1038  ) {
1039  if ( !$context ) {
1040  $context = RequestContext::getMain();
1041  }
1042 
1043  $config = $context->getConfig();
1044  if ( !$config->get( 'UseTagFilter' ) || !count( self::listDefinedTags() ) ) {
1045  return [];
1046  }
1047 
1048  $data = [
1050  'label',
1051  [ 'for' => 'tagfilter' ],
1052  $context->msg( 'tag-filter' )->parse()
1053  )
1054  ];
1055 
1056  if ( $ooui ) {
1057  $data[] = new OOUI\TextInputWidget( [
1058  'id' => 'tagfilter',
1059  'name' => 'tagfilter',
1060  'value' => $selected,
1061  'classes' => 'mw-tagfilter-input',
1062  ] );
1063  } else {
1064  $data[] = Xml::input(
1065  'tagfilter',
1066  20,
1067  $selected,
1068  [ 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' ]
1069  );
1070  }
1071 
1072  return $data;
1073  }
1074 
1083  public static function defineTag( $tag ) {
1084  $dbw = wfGetDB( DB_PRIMARY );
1085  $tagDef = [
1086  'ctd_name' => $tag,
1087  'ctd_user_defined' => 1,
1088  'ctd_count' => 0
1089  ];
1090  $dbw->upsert(
1091  'change_tag_def',
1092  $tagDef,
1093  'ctd_name',
1094  [ 'ctd_user_defined' => 1 ],
1095  __METHOD__
1096  );
1097 
1098  // clear the memcache of defined tags
1100  }
1101 
1110  public static function undefineTag( $tag ) {
1111  $dbw = wfGetDB( DB_PRIMARY );
1112 
1113  $dbw->update(
1114  'change_tag_def',
1115  [ 'ctd_user_defined' => 0 ],
1116  [ 'ctd_name' => $tag ],
1117  __METHOD__
1118  );
1119 
1120  $dbw->delete(
1121  'change_tag_def',
1122  [ 'ctd_name' => $tag, 'ctd_count' => 0 ],
1123  __METHOD__
1124  );
1125 
1126  // clear the memcache of defined tags
1128  }
1129 
1144  protected static function logTagManagementAction( $action, $tag, $reason,
1145  UserIdentity $user, $tagCount = null, array $logEntryTags = []
1146  ) {
1147  $dbw = wfGetDB( DB_PRIMARY );
1148 
1149  $logEntry = new ManualLogEntry( 'managetags', $action );
1150  $logEntry->setPerformer( $user );
1151  // target page is not relevant, but it has to be set, so we just put in
1152  // the title of Special:Tags
1153  $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
1154  $logEntry->setComment( $reason );
1155 
1156  $params = [ '4::tag' => $tag ];
1157  if ( $tagCount !== null ) {
1158  $params['5:number:count'] = $tagCount;
1159  }
1160  $logEntry->setParameters( $params );
1161  $logEntry->setRelations( [ 'Tag' => $tag ] );
1162  $logEntry->addTags( $logEntryTags );
1163 
1164  $logId = $logEntry->insert( $dbw );
1165  $logEntry->publish( $logId );
1166  return $logId;
1167  }
1168 
1178  public static function canActivateTag( $tag, Authority $performer = null ) {
1179  if ( $performer !== null ) {
1180  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1181  return Status::newFatal( 'tags-manage-no-permission' );
1182  }
1183  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1184  return Status::newFatal(
1185  'tags-manage-blocked',
1186  $performer->getUser()->getName()
1187  );
1188  }
1189  }
1190 
1191  // defined tags cannot be activated (a defined tag is either extension-
1192  // defined, in which case the extension chooses whether or not to active it;
1193  // or user-defined, in which case it is considered active)
1194  $definedTags = self::listDefinedTags();
1195  if ( in_array( $tag, $definedTags ) ) {
1196  return Status::newFatal( 'tags-activate-not-allowed', $tag );
1197  }
1198 
1199  // non-existing tags cannot be activated
1200  $tagUsage = self::tagUsageStatistics();
1201  if ( !isset( $tagUsage[$tag] ) ) { // we already know the tag is undefined
1202  return Status::newFatal( 'tags-activate-not-found', $tag );
1203  }
1204 
1205  return Status::newGood();
1206  }
1207 
1225  public static function activateTagWithChecks( $tag, $reason, Authority $performer,
1226  $ignoreWarnings = false, array $logEntryTags = []
1227  ) {
1228  // are we allowed to do this?
1229  $result = self::canActivateTag( $tag, $performer );
1230  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1231  $result->value = null;
1232  return $result;
1233  }
1234 
1235  // do it!
1236  self::defineTag( $tag );
1237 
1238  // log it
1239  $logId = self::logTagManagementAction( 'activate', $tag, $reason, $performer->getUser(),
1240  null, $logEntryTags );
1241 
1242  return Status::newGood( $logId );
1243  }
1244 
1254  public static function canDeactivateTag( $tag, Authority $performer = null ) {
1255  if ( $performer !== null ) {
1256  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1257  return Status::newFatal( 'tags-manage-no-permission' );
1258  }
1259  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1260  return Status::newFatal(
1261  'tags-manage-blocked',
1262  $performer->getUser()->getName()
1263  );
1264  }
1265  }
1266 
1267  // only explicitly-defined tags can be deactivated
1268  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
1269  if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
1270  return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
1271  }
1272  return Status::newGood();
1273  }
1274 
1292  public static function deactivateTagWithChecks( $tag, $reason, Authority $performer,
1293  $ignoreWarnings = false, array $logEntryTags = []
1294  ) {
1295  // are we allowed to do this?
1296  $result = self::canDeactivateTag( $tag, $performer );
1297  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1298  $result->value = null;
1299  return $result;
1300  }
1301 
1302  // do it!
1303  self::undefineTag( $tag );
1304 
1305  // log it
1306  $logId = self::logTagManagementAction( 'deactivate', $tag, $reason,
1307  $performer->getUser(), null, $logEntryTags );
1308 
1309  return Status::newGood( $logId );
1310  }
1311 
1319  public static function isTagNameValid( $tag ) {
1320  // no empty tags
1321  if ( $tag === '' ) {
1322  return Status::newFatal( 'tags-create-no-name' );
1323  }
1324 
1325  // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
1326  // pipe (used as a delimiter between multiple tags in
1327  // SpecialRecentchanges and friends), or slashes (would break tag description messages in
1328  // MediaWiki namespace)
1329  if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
1330  || strpos( $tag, '/' ) !== false ) {
1331  return Status::newFatal( 'tags-create-invalid-chars' );
1332  }
1333 
1334  // could the MediaWiki namespace description messages be created?
1335  $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
1336  if ( $title === null ) {
1337  return Status::newFatal( 'tags-create-invalid-title-chars' );
1338  }
1339 
1340  return Status::newGood();
1341  }
1342 
1355  public static function canCreateTag( $tag, Authority $performer = null ) {
1356  $user = null;
1357  if ( $performer !== null ) {
1358  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1359  return Status::newFatal( 'tags-manage-no-permission' );
1360  }
1361  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1362  return Status::newFatal(
1363  'tags-manage-blocked',
1364  $performer->getUser()->getName()
1365  );
1366  }
1367  // ChangeTagCanCreate hook still needs a full User object
1368  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
1369  }
1370 
1371  $status = self::isTagNameValid( $tag );
1372  if ( !$status->isGood() ) {
1373  return $status;
1374  }
1375 
1376  // does the tag already exist?
1377  $tagUsage = self::tagUsageStatistics();
1378  if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) {
1379  return Status::newFatal( 'tags-create-already-exists', $tag );
1380  }
1381 
1382  // check with hooks
1383  $canCreateResult = Status::newGood();
1384  Hooks::runner()->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1385  return $canCreateResult;
1386  }
1387 
1407  public static function createTagWithChecks( $tag, $reason, Authority $performer,
1408  $ignoreWarnings = false, array $logEntryTags = []
1409  ) {
1410  // are we allowed to do this?
1411  $result = self::canCreateTag( $tag, $performer );
1412  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1413  $result->value = null;
1414  return $result;
1415  }
1416 
1417  // do it!
1418  self::defineTag( $tag );
1419 
1420  // log it
1421  $logId = self::logTagManagementAction( 'create', $tag, $reason,
1422  $performer->getUser(), null, $logEntryTags );
1423 
1424  return Status::newGood( $logId );
1425  }
1426 
1439  public static function deleteTagEverywhere( $tag ) {
1440  $dbw = wfGetDB( DB_PRIMARY );
1441  $dbw->startAtomic( __METHOD__ );
1442 
1443  // fetch tag id, this must be done before calling undefineTag(), see T225564
1444  $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag );
1445 
1446  // set ctd_user_defined = 0
1447  self::undefineTag( $tag );
1448 
1449  // delete from change_tag
1450  $dbw->delete( 'change_tag', [ 'ct_tag_id' => $tagId ], __METHOD__ );
1451  $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag ], __METHOD__ );
1452  $dbw->endAtomic( __METHOD__ );
1453 
1454  // give extensions a chance
1455  $status = Status::newGood();
1456  Hooks::runner()->onChangeTagAfterDelete( $tag, $status );
1457  // let's not allow error results, as the actual tag deletion succeeded
1458  if ( !$status->isOK() ) {
1459  wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' );
1460  $status->setOK( true );
1461  }
1462 
1463  // clear the memcache of defined tags
1465 
1466  return $status;
1467  }
1468 
1481  public static function canDeleteTag( $tag, Authority $performer = null, int $flags = 0 ) {
1482  $tagUsage = self::tagUsageStatistics();
1483  $user = null;
1484  if ( $performer !== null ) {
1485  if ( !$performer->isAllowed( 'deletechangetags' ) ) {
1486  return Status::newFatal( 'tags-delete-no-permission' );
1487  }
1488  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1489  return Status::newFatal(
1490  'tags-manage-blocked',
1491  $performer->getUser()->getName()
1492  );
1493  }
1494  // ChangeTagCanDelete hook still needs a full User object
1495  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
1496  }
1497 
1498  if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) {
1499  return Status::newFatal( 'tags-delete-not-found', $tag );
1500  }
1501 
1502  if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1503  isset( $tagUsage[$tag] ) &&
1504  $tagUsage[$tag] > self::MAX_DELETE_USES
1505  ) {
1506  return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1507  }
1508 
1509  $softwareDefined = self::listSoftwareDefinedTags();
1510  if ( in_array( $tag, $softwareDefined ) ) {
1511  // extension-defined tags can't be deleted unless the extension
1512  // specifically allows it
1513  $status = Status::newFatal( 'tags-delete-not-allowed' );
1514  } else {
1515  // user-defined tags are deletable unless otherwise specified
1516  $status = Status::newGood();
1517  }
1518 
1519  Hooks::runner()->onChangeTagCanDelete( $tag, $user, $status );
1520  return $status;
1521  }
1522 
1540  public static function deleteTagWithChecks( $tag, $reason, Authority $performer,
1541  $ignoreWarnings = false, array $logEntryTags = []
1542  ) {
1543  // are we allowed to do this?
1544  $result = self::canDeleteTag( $tag, $performer );
1545  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1546  $result->value = null;
1547  return $result;
1548  }
1549 
1550  // store the tag usage statistics
1551  $tagUsage = self::tagUsageStatistics();
1552  $hitcount = $tagUsage[$tag] ?? 0;
1553 
1554  // do it!
1555  $deleteResult = self::deleteTagEverywhere( $tag );
1556  if ( !$deleteResult->isOK() ) {
1557  return $deleteResult;
1558  }
1559 
1560  // log it
1561  $logId = self::logTagManagementAction( 'delete', $tag, $reason, $performer->getUser(),
1562  $hitcount, $logEntryTags );
1563 
1564  $deleteResult->value = $logId;
1565  return $deleteResult;
1566  }
1567 
1574  public static function listSoftwareActivatedTags() {
1575  // core active tags
1576  $tags = self::getSoftwareTags();
1577  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1578  if ( !$hookContainer->isRegistered( 'ChangeTagsListActive' ) ) {
1579  return $tags;
1580  }
1581  $hookRunner = new HookRunner( $hookContainer );
1582  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1583  return $cache->getWithSetCallback(
1584  $cache->makeKey( 'active-tags' ),
1585  WANObjectCache::TTL_MINUTE * 5,
1586  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1587  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1588 
1589  // Ask extensions which tags they consider active
1590  $hookRunner->onChangeTagsListActive( $tags );
1591  return $tags;
1592  },
1593  [
1594  'checkKeys' => [ $cache->makeKey( 'active-tags' ) ],
1595  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1596  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1597  ]
1598  );
1599  }
1600 
1608  public static function listDefinedTags() {
1610  $tags2 = self::listSoftwareDefinedTags();
1611  return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
1612  }
1613 
1622  public static function listExplicitlyDefinedTags() {
1623  $fname = __METHOD__;
1624 
1625  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1626  return $cache->getWithSetCallback(
1627  $cache->makeKey( 'valid-tags-db' ),
1628  WANObjectCache::TTL_MINUTE * 5,
1629  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1630  $dbr = wfGetDB( DB_REPLICA );
1631 
1632  $setOpts += Database::getCacheSetOptions( $dbr );
1633 
1634  $tags = $dbr->selectFieldValues(
1635  'change_tag_def',
1636  'ctd_name',
1637  [ 'ctd_user_defined' => 1 ],
1638  $fname
1639  );
1640 
1641  return array_filter( array_unique( $tags ) );
1642  },
1643  [
1644  'checkKeys' => [ $cache->makeKey( 'valid-tags-db' ) ],
1645  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1646  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1647  ]
1648  );
1649  }
1650 
1660  public static function listSoftwareDefinedTags() {
1661  // core defined tags
1662  $tags = self::getSoftwareTags( true );
1663  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1664  if ( !$hookContainer->isRegistered( 'ListDefinedTags' ) ) {
1665  return $tags;
1666  }
1667  $hookRunner = new HookRunner( $hookContainer );
1668  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1669  return $cache->getWithSetCallback(
1670  $cache->makeKey( 'valid-tags-hook' ),
1671  WANObjectCache::TTL_MINUTE * 5,
1672  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1673  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1674 
1675  $hookRunner->onListDefinedTags( $tags );
1676  return array_filter( array_unique( $tags ) );
1677  },
1678  [
1679  'checkKeys' => [ $cache->makeKey( 'valid-tags-hook' ) ],
1680  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1681  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1682  ]
1683  );
1684  }
1685 
1691  public static function purgeTagCacheAll() {
1692  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1693 
1694  $cache->touchCheckKey( $cache->makeKey( 'active-tags' ) );
1695  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-db' ) );
1696  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-hook' ) );
1697  $cache->touchCheckKey( $cache->makeKey( 'tags-usage-statistics' ) );
1698 
1699  MediaWikiServices::getInstance()->getChangeTagDefStore()->reloadMap();
1700  }
1701 
1708  public static function tagUsageStatistics() {
1709  $fname = __METHOD__;
1710 
1711  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1712  return $cache->getWithSetCallback(
1713  $cache->makeKey( 'tags-usage-statistics' ),
1714  WANObjectCache::TTL_MINUTE * 5,
1715  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1716  $dbr = wfGetDB( DB_REPLICA );
1717  $res = $dbr->select(
1718  'change_tag_def',
1719  [ 'ctd_name', 'ctd_count' ],
1720  [],
1721  $fname,
1722  [ 'ORDER BY' => 'ctd_count DESC' ]
1723  );
1724 
1725  $out = [];
1726  foreach ( $res as $row ) {
1727  $out[$row->ctd_name] = $row->ctd_count;
1728  }
1729 
1730  return $out;
1731  },
1732  [
1733  'checkKeys' => [ $cache->makeKey( 'tags-usage-statistics' ) ],
1734  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1735  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1736  ]
1737  );
1738  }
1739 
1754  public static function showTagEditingUI( Authority $performer ) {
1755  return $performer->isAllowed( 'changetags' ) && (bool)self::listExplicitlyDefinedTags();
1756  }
1757 }
ChangeTags\REVERT_TAGS
const REVERT_TAGS
List of tags which denote a revert of some sort.
Definition: ChangeTags.php:102
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:52
ChangeTags\makeTagSummarySubquery
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
Definition: ChangeTags.php:999
ChangeTags\DEFINED_SOFTWARE_TAGS
const DEFINED_SOFTWARE_TAGS
A list of tags defined and used by MediaWiki itself.
Definition: ChangeTags.php:123
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:377
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:1691
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:1407
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:203
$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:1178
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:1225
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:81
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:784
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:131
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:1540
ChangeTags\buildTagFilterSelector
static buildTagFilterSelector( $selected='', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
Definition: ChangeTags.php:1036
$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:607
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:1319
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:111
$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:254
ChangeTags\listSoftwareDefinedTags
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
Definition: ChangeTags.php:1660
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:1439
ChangeTags\modifyDisplayQuery
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
Definition: ChangeTags.php:896
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:593
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:1110
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:2186
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:1254
ChangeTags\getDisplayTableName
static getDisplayTableName()
Get the name of the change_tag table to use for modifyDisplayQuery().
Definition: ChangeTags.php:959
ChangeTags\listDefinedTags
static listDefinedTags()
Basically lists defined tags which count even if they aren't applied to anything.
Definition: ChangeTags.php:1608
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:158
$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:118
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:1083
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:680
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:894
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
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
ChangeTags\listExplicitlyDefinedTags
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
Definition: ChangeTags.php:1622
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:630
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:674
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
Message\plaintextParam
static plaintextParam( $plaintext)
Definition: Message.php:1248
DB_PRIMARY
const DB_PRIMARY
Definition: defines.php:27
ChangeTags\TAG_ADD_MEDIA
const TAG_ADD_MEDIA
The tagged edit included additions of media.
Definition: ChangeTags.php:94
Xml\tags
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:133
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
ChangeTags\TAG_SERVER_SIDE_UPLOAD
const TAG_SERVER_SIDE_UPLOAD
This tagged edit was performed while importing media files using the importImages....
Definition: ChangeTags.php:90
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:70
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:484
ChangeTags\canCreateTag
static canCreateTag( $tag, Authority $performer=null)
Is it OK to allow the user to create this tag?
Definition: ChangeTags.php:1355
ChangeTags\listSoftwareActivatedTags
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
Definition: ChangeTags.php:1574
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:370
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:58
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:710
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:1292
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:213
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:1144
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:45
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:1708
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:281
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:1043
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:557
ChangeTags\$avoidReopeningTablesForTesting
static bool $avoidReopeningTablesForTesting
If true, this class attempts to avoid reopening database tables within the same query,...
Definition: ChangeTags.php:149
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:1754
ChangeTags\tagLongDescriptionMessage
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
Definition: ChangeTags.php:305
ChangeTags\TAG_REMOVE_MEDIA
const TAG_REMOVE_MEDIA
The tagged edit included removals of media.
Definition: ChangeTags.php:98
ChangeTags\formatSummaryRow
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:194
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:550
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:333
ChangeTags\tagDescription
static tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
Definition: ChangeTags.php:288
ChangeTags\MEDIA_TAGS
const MEDIA_TAGS
List of tags which denote a change in media.
Definition: ChangeTags.php:106
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:1481