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  global $wgSoftwareTags;
160  $softwareTags = [];
161 
162  if ( !is_array( $wgSoftwareTags ) ) {
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( $wgSoftwareTags ) ) :
170  array_keys( $wgSoftwareTags );
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  $displayTags = [];
206  foreach ( $tags as $tag ) {
207  if ( !$tag ) {
208  continue;
209  }
210  $description = self::tagDescription( $tag, $context );
211  if ( $description === false ) {
212  continue;
213  }
214  $displayTags[] = Xml::tags(
215  'span',
216  [ 'class' => 'mw-tag-marker ' .
217  Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
218  $description
219  );
220  $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
221  }
222 
223  if ( !$displayTags ) {
224  return [ '', [] ];
225  }
226 
227  $markers = $context->msg( 'tag-list-wrapper' )
228  ->numParams( count( $displayTags ) )
229  ->rawParams( implode( ' ', $displayTags ) )
230  ->parse();
231  $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
232 
233  return [ $markers, $classes ];
234  }
235 
249  public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
250  $msg = $context->msg( "tag-$tag" );
251  if ( !$msg->exists() ) {
252  // No such message
253  return ( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) )
254  // HACK MessageLocalizer doesn't have a way to set the right language on a RawMessage,
255  // so extract the language from $msg and use that.
256  // The language doesn't really matter, but we need to set it to avoid requesting
257  // the user's language from session-less entry points (T227233)
258  ->inLanguage( $msg->getLanguage() );
259 
260  }
261  if ( $msg->isDisabled() ) {
262  // The message exists but is disabled, hide the tag.
263  return false;
264  }
265 
266  // Message exists and isn't disabled, use it.
267  return $msg;
268  }
269 
283  public static function tagDescription( $tag, MessageLocalizer $context ) {
284  $msg = self::tagShortDescriptionMessage( $tag, $context );
285  return $msg ? $msg->parse() : false;
286  }
287 
300  public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
301  $msg = $context->msg( "tag-$tag-description" );
302  if ( !$msg->exists() ) {
303  return false;
304  }
305  if ( $msg->isDisabled() ) {
306  // The message exists but is disabled, hide the description.
307  return false;
308  }
309 
310  // Message exists and isn't disabled, use it.
311  return $msg;
312  }
313 
328  public static function addTags( $tags, $rc_id = null, $rev_id = null,
329  $log_id = null, $params = null, RecentChange $rc = null
330  ) {
331  $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
332  return (bool)$result[0];
333  }
334 
365  public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
366  &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
367  UserIdentity $user = null
368  ) {
369  $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags...
370  $tagsToRemove = array_filter( (array)$tagsToRemove );
371 
372  if ( !$rc_id && !$rev_id && !$log_id ) {
373  throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
374  'specified when adding or removing a tag from a change!' );
375  }
376 
377  $dbw = wfGetDB( DB_PRIMARY );
378 
379  // Might as well look for rcids and so on.
380  if ( !$rc_id ) {
381  // Info might be out of date, somewhat fractionally, on replica DB.
382  // LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
383  // so use that relation to avoid full table scans.
384  if ( $log_id ) {
385  $rc_id = $dbw->selectField(
386  [ 'logging', 'recentchanges' ],
387  'rc_id',
388  [
389  'log_id' => $log_id,
390  'rc_timestamp = log_timestamp',
391  'rc_logid = log_id'
392  ],
393  __METHOD__
394  );
395  } elseif ( $rev_id ) {
396  $rc_id = $dbw->selectField(
397  [ 'revision', 'recentchanges' ],
398  'rc_id',
399  [
400  'rev_id' => $rev_id,
401  'rc_this_oldid = rev_id'
402  ],
403  __METHOD__
404  );
405  }
406  } elseif ( !$log_id && !$rev_id ) {
407  // Info might be out of date, somewhat fractionally, on replica DB.
408  $log_id = $dbw->selectField(
409  'recentchanges',
410  'rc_logid',
411  [ 'rc_id' => $rc_id ],
412  __METHOD__
413  );
414  $rev_id = $dbw->selectField(
415  'recentchanges',
416  'rc_this_oldid',
417  [ 'rc_id' => $rc_id ],
418  __METHOD__
419  );
420  }
421 
422  if ( $log_id && !$rev_id ) {
423  $rev_id = $dbw->selectField(
424  'log_search',
425  'ls_value',
426  [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ],
427  __METHOD__
428  );
429  } elseif ( !$log_id && $rev_id ) {
430  $log_id = $dbw->selectField(
431  'log_search',
432  'ls_log_id',
433  [ 'ls_field' => 'associated_rev_id', 'ls_value' => (string)$rev_id ],
434  __METHOD__
435  );
436  }
437 
438  $prevTags = self::getTags( $dbw, $rc_id, $rev_id, $log_id );
439 
440  // add tags
441  $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
442  $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
443 
444  // remove tags
445  $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
446  $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
447 
448  sort( $prevTags );
449  sort( $newTags );
450  if ( $prevTags == $newTags ) {
451  return [ [], [], $prevTags ];
452  }
453 
454  // insert a row into change_tag for each new tag
455  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
456  if ( count( $tagsToAdd ) ) {
457  $changeTagMapping = [];
458  foreach ( $tagsToAdd as $tag ) {
459  $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
460  }
461  $fname = __METHOD__;
462  // T207881: update the counts at the end of the transaction
463  $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tagsToAdd, $fname ) {
464  $dbw->update(
465  'change_tag_def',
466  [ 'ctd_count = ctd_count + 1' ],
467  [ 'ctd_name' => $tagsToAdd ],
468  $fname
469  );
470  }, $fname );
471 
472  $tagsRows = [];
473  foreach ( $tagsToAdd as $tag ) {
474  // Filter so we don't insert NULLs as zero accidentally.
475  // Keep in mind that $rc_id === null means "I don't care/know about the
476  // rc_id, just delete $tag on this revision/log entry". It doesn't
477  // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
478  $tagsRows[] = array_filter(
479  [
480  'ct_rc_id' => $rc_id,
481  'ct_log_id' => $log_id,
482  'ct_rev_id' => $rev_id,
483  'ct_params' => $params,
484  'ct_tag_id' => $changeTagMapping[$tag] ?? null,
485  ]
486  );
487 
488  }
489 
490  $dbw->insert( 'change_tag', $tagsRows, __METHOD__, [ 'IGNORE' ] );
491  }
492 
493  // delete from change_tag
494  if ( count( $tagsToRemove ) ) {
495  $fname = __METHOD__;
496  foreach ( $tagsToRemove as $tag ) {
497  $conds = array_filter(
498  [
499  'ct_rc_id' => $rc_id,
500  'ct_log_id' => $log_id,
501  'ct_rev_id' => $rev_id,
502  'ct_tag_id' => $changeTagDefStore->getId( $tag ),
503  ]
504  );
505  $dbw->delete( 'change_tag', $conds, __METHOD__ );
506  if ( $dbw->affectedRows() ) {
507  // T207881: update the counts at the end of the transaction
508  $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tag, $fname ) {
509  $dbw->update(
510  'change_tag_def',
511  [ 'ctd_count = ctd_count - 1' ],
512  [ 'ctd_name' => $tag ],
513  $fname
514  );
515 
516  $dbw->delete(
517  'change_tag_def',
518  [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
519  $fname
520  );
521  }, $fname );
522  }
523  }
524  }
525 
526  $userObj = $user ? MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $user ) : null;
527  Hooks::runner()->onChangeTagsAfterUpdateTags( $tagsToAdd, $tagsToRemove, $prevTags,
528  $rc_id, $rev_id, $log_id, $params, $rc, $userObj );
529 
530  return [ $tagsToAdd, $tagsToRemove, $prevTags ];
531  }
532 
545  public static function getTagsWithData(
546  IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null
547  ) {
548  if ( !$rc_id && !$rev_id && !$log_id ) {
549  throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
550  'specified when loading tags from a change!' );
551  }
552 
553  $conds = array_filter(
554  [
555  'ct_rc_id' => $rc_id,
556  'ct_rev_id' => $rev_id,
557  'ct_log_id' => $log_id,
558  ]
559  );
560 
561  $result = $db->select(
562  'change_tag',
563  [ 'ct_tag_id', 'ct_params' ],
564  $conds,
565  __METHOD__
566  );
567 
568  $tags = [];
569  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
570  foreach ( $result as $row ) {
571  $tagName = $changeTagDefStore->getName( (int)$row->ct_tag_id );
572  $tags[$tagName] = $row->ct_params;
573  }
574 
575  return $tags;
576  }
577 
588  public static function getTags( IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
589  return array_keys( self::getTagsWithData( $db, $rc_id, $rev_id, $log_id ) );
590  }
591 
602  protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
603  $lang = RequestContext::getMain()->getLanguage();
604  $tags = array_values( $tags );
605  $count = count( $tags );
606  $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
607  $lang->commaList( $tags ), $count );
608  $status->value = $tags;
609  return $status;
610  }
611 
625  public static function canAddTagsAccompanyingChange( array $tags, Authority $performer = null ) {
626  $user = null;
627  if ( $performer !== null ) {
628  if ( !$performer->isAllowed( 'applychangetags' ) ) {
629  return Status::newFatal( 'tags-apply-no-permission' );
630  }
631 
632  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
633  return Status::newFatal(
634  'tags-apply-blocked',
635  $performer->getUser()->getName()
636  );
637  }
638 
639  // ChangeTagsAllowedAdd hook still needs a full User object
640  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
641  }
642 
643  // to be applied, a tag has to be explicitly defined
644  $allowedTags = self::listExplicitlyDefinedTags();
645  Hooks::runner()->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
646  $disallowedTags = array_diff( $tags, $allowedTags );
647  if ( $disallowedTags ) {
648  return self::restrictedTagError( 'tags-apply-not-allowed-one',
649  'tags-apply-not-allowed-multi', $disallowedTags );
650  }
651 
652  return Status::newGood();
653  }
654 
675  public static function addTagsAccompanyingChangeWithChecks(
676  array $tags, $rc_id, $rev_id, $log_id, $params, Authority $performer
677  ) {
678  // are we allowed to do this?
679  $result = self::canAddTagsAccompanyingChange( $tags, $performer );
680  if ( !$result->isOK() ) {
681  $result->value = null;
682  return $result;
683  }
684 
685  // do it!
686  self::addTags( $tags, $rc_id, $rev_id, $log_id, $params );
687 
688  return Status::newGood( true );
689  }
690 
705  public static function canUpdateTags(
706  array $tagsToAdd,
707  array $tagsToRemove,
708  Authority $performer = null
709  ) {
710  if ( $performer !== null ) {
711  if ( !$performer->isAllowed( 'changetags' ) ) {
712  return Status::newFatal( 'tags-update-no-permission' );
713  }
714 
715  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
716  return Status::newFatal(
717  'tags-update-blocked',
718  $performer->getUser()->getName()
719  );
720  }
721  }
722 
723  if ( $tagsToAdd ) {
724  // to be added, a tag has to be explicitly defined
725  // @todo Allow extensions to define tags that can be applied by users...
726  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
727  $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
728  if ( $diff ) {
729  return self::restrictedTagError( 'tags-update-add-not-allowed-one',
730  'tags-update-add-not-allowed-multi', $diff );
731  }
732  }
733 
734  if ( $tagsToRemove ) {
735  // to be removed, a tag must not be defined by an extension, or equivalently it
736  // has to be either explicitly defined or not defined at all
737  // (assuming no edge case of a tag both explicitly-defined and extension-defined)
738  $softwareDefinedTags = self::listSoftwareDefinedTags();
739  $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
740  if ( $intersect ) {
741  return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
742  'tags-update-remove-not-allowed-multi', $intersect );
743  }
744  }
745 
746  return Status::newGood();
747  }
748 
779  public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
780  $rc_id, $rev_id, $log_id, $params, $reason, Authority $performer
781  ) {
782  if ( $tagsToAdd === null ) {
783  $tagsToAdd = [];
784  }
785  if ( $tagsToRemove === null ) {
786  $tagsToRemove = [];
787  }
788  if ( !$tagsToAdd && !$tagsToRemove ) {
789  // no-op, don't bother
790  return Status::newGood( (object)[
791  'logId' => null,
792  'addedTags' => [],
793  'removedTags' => [],
794  ] );
795  }
796 
797  // are we allowed to do this?
798  $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $performer );
799  if ( !$result->isOK() ) {
800  $result->value = null;
801  return $result;
802  }
803 
804  // basic rate limiting
805  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
806  if ( $user->pingLimiter( 'changetag' ) ) {
807  return Status::newFatal( 'actionthrottledtext' );
808  }
809 
810  // do it!
811  list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd,
812  $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user );
813  if ( !$tagsAdded && !$tagsRemoved ) {
814  // no-op, don't log it
815  return Status::newGood( (object)[
816  'logId' => null,
817  'addedTags' => [],
818  'removedTags' => [],
819  ] );
820  }
821 
822  // log it
823  $logEntry = new ManualLogEntry( 'tag', 'update' );
824  $logEntry->setPerformer( $performer->getUser() );
825  $logEntry->setComment( $reason );
826 
827  // find the appropriate target page
828  if ( $rev_id ) {
829  $revisionRecord = MediaWikiServices::getInstance()
830  ->getRevisionLookup()
831  ->getRevisionById( $rev_id );
832  if ( $revisionRecord ) {
833  $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
834  }
835  } elseif ( $log_id ) {
836  // This function is from revision deletion logic and has nothing to do with
837  // change tags, but it appears to be the only other place in core where we
838  // perform logged actions on log items.
839  $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
840  }
841 
842  if ( !$logEntry->getTarget() ) {
843  // target is required, so we have to set something
844  $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
845  }
846 
847  $logParams = [
848  '4::revid' => $rev_id,
849  '5::logid' => $log_id,
850  '6:list:tagsAdded' => $tagsAdded,
851  '7:number:tagsAddedCount' => count( $tagsAdded ),
852  '8:list:tagsRemoved' => $tagsRemoved,
853  '9:number:tagsRemovedCount' => count( $tagsRemoved ),
854  'initialTags' => $initialTags,
855  ];
856  $logEntry->setParameters( $logParams );
857  $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
858 
859  $dbw = wfGetDB( DB_PRIMARY );
860  $logId = $logEntry->insert( $dbw );
861  // Only send this to UDP, not RC, similar to patrol events
862  $logEntry->publish( $logId, 'udp' );
863 
864  return Status::newGood( (object)[
865  'logId' => $logId,
866  'addedTags' => $tagsAdded,
867  'removedTags' => $tagsRemoved,
868  ] );
869  }
870 
891  public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
892  &$join_conds, &$options, $filter_tag = ''
893  ) {
894  global $wgUseTagFilter;
895 
896  // Normalize to arrays
897  $tables = (array)$tables;
898  $fields = (array)$fields;
899  $conds = (array)$conds;
900  $options = (array)$options;
901 
902  $fields['ts_tags'] = self::makeTagSummarySubquery( $tables );
903 
904  // Figure out which ID field to use
905  if ( in_array( 'recentchanges', $tables ) ) {
906  $join_cond = 'ct_rc_id=rc_id';
907  } elseif ( in_array( 'logging', $tables ) ) {
908  $join_cond = 'ct_log_id=log_id';
909  } elseif ( in_array( 'revision', $tables ) ) {
910  $join_cond = 'ct_rev_id=rev_id';
911  } elseif ( in_array( 'archive', $tables ) ) {
912  $join_cond = 'ct_rev_id=ar_rev_id';
913  } else {
914  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
915  }
916 
917  if ( $wgUseTagFilter && $filter_tag ) {
918  // Somebody wants to filter on a tag.
919  // Add an INNER JOIN on change_tag
920 
921  $tagTable = 'change_tag';
922  if ( self::$avoidReopeningTablesForTesting && defined( 'MW_PHPUNIT_TEST' ) ) {
923  $db = wfGetDB( DB_REPLICA );
924 
925  if ( $db->getType() === 'mysql' ) {
926  // When filtering by tag, we are using the change_tag table twice:
927  // Once in a join for filtering, and once in a sub-query to list all
928  // tags for each revision. This does not work with temporary tables
929  // on some versions of MySQL, which causes phpunit tests to fail.
930  // As a hacky workaround, we copy the temporary table, and join
931  // against the copy. It is acknowledged that this is quite horrific.
932  // Discuss at T256006.
933 
934  $tagTable = 'change_tag_for_display_query';
935  $db->query(
936  'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $db->tableName( $tagTable )
937  . ' LIKE ' . $db->tableName( 'change_tag' ),
938  __METHOD__
939  );
940  $db->query(
941  'INSERT IGNORE INTO ' . $db->tableName( $tagTable )
942  . ' SELECT * FROM ' . $db->tableName( 'change_tag' ),
943  __METHOD__
944  );
945  }
946  }
947 
948  $tables[] = $tagTable;
949  $join_conds[$tagTable] = [ 'JOIN', $join_cond ];
950  $filterTagIds = [];
951  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
952  foreach ( (array)$filter_tag as $filterTagName ) {
953  try {
954  $filterTagIds[] = $changeTagDefStore->getId( $filterTagName );
955  } catch ( NameTableAccessException $exception ) {
956  // Return nothing.
957  $conds[] = '0=1';
958  break;
959  }
960  }
961 
962  if ( $filterTagIds !== [] ) {
963  $conds['ct_tag_id'] = $filterTagIds;
964  }
965 
966  if (
967  is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
968  !in_array( 'DISTINCT', $options )
969  ) {
970  $options[] = 'DISTINCT';
971  }
972  }
973  }
974 
983  public static function makeTagSummarySubquery( $tables ) {
984  // Normalize to arrays
985  $tables = (array)$tables;
986 
987  // Figure out which ID field to use
988  if ( in_array( 'recentchanges', $tables ) ) {
989  $join_cond = 'ct_rc_id=rc_id';
990  } elseif ( in_array( 'logging', $tables ) ) {
991  $join_cond = 'ct_log_id=log_id';
992  } elseif ( in_array( 'revision', $tables ) ) {
993  $join_cond = 'ct_rev_id=rev_id';
994  } elseif ( in_array( 'archive', $tables ) ) {
995  $join_cond = 'ct_rev_id=ar_rev_id';
996  } else {
997  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
998  }
999 
1000  $tagTables = [ 'change_tag', 'change_tag_def' ];
1001  $join_cond_ts_tags = [ 'change_tag_def' => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
1002  $field = 'ctd_name';
1003 
1004  return wfGetDB( DB_REPLICA )->buildGroupConcatField(
1005  ',', $tagTables, $field, $join_cond, $join_cond_ts_tags
1006  );
1007  }
1008 
1020  public static function buildTagFilterSelector(
1021  $selected = '', $ooui = false, IContextSource $context = null
1022  ) {
1023  if ( !$context ) {
1024  $context = RequestContext::getMain();
1025  }
1026 
1027  $config = $context->getConfig();
1028  if ( !$config->get( 'UseTagFilter' ) || !count( self::listDefinedTags() ) ) {
1029  return [];
1030  }
1031 
1032  $data = [
1034  'label',
1035  [ 'for' => 'tagfilter' ],
1036  $context->msg( 'tag-filter' )->parse()
1037  )
1038  ];
1039 
1040  if ( $ooui ) {
1041  $data[] = new OOUI\TextInputWidget( [
1042  'id' => 'tagfilter',
1043  'name' => 'tagfilter',
1044  'value' => $selected,
1045  'classes' => 'mw-tagfilter-input',
1046  ] );
1047  } else {
1048  $data[] = Xml::input(
1049  'tagfilter',
1050  20,
1051  $selected,
1052  [ 'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline', 'id' => 'tagfilter' ]
1053  );
1054  }
1055 
1056  return $data;
1057  }
1058 
1067  public static function defineTag( $tag ) {
1068  $dbw = wfGetDB( DB_PRIMARY );
1069  $tagDef = [
1070  'ctd_name' => $tag,
1071  'ctd_user_defined' => 1,
1072  'ctd_count' => 0
1073  ];
1074  $dbw->upsert(
1075  'change_tag_def',
1076  $tagDef,
1077  'ctd_name',
1078  [ 'ctd_user_defined' => 1 ],
1079  __METHOD__
1080  );
1081 
1082  // clear the memcache of defined tags
1084  }
1085 
1094  public static function undefineTag( $tag ) {
1095  $dbw = wfGetDB( DB_PRIMARY );
1096 
1097  $dbw->update(
1098  'change_tag_def',
1099  [ 'ctd_user_defined' => 0 ],
1100  [ 'ctd_name' => $tag ],
1101  __METHOD__
1102  );
1103 
1104  $dbw->delete(
1105  'change_tag_def',
1106  [ 'ctd_name' => $tag, 'ctd_count' => 0 ],
1107  __METHOD__
1108  );
1109 
1110  // clear the memcache of defined tags
1112  }
1113 
1128  protected static function logTagManagementAction( $action, $tag, $reason,
1129  UserIdentity $user, $tagCount = null, array $logEntryTags = []
1130  ) {
1131  $dbw = wfGetDB( DB_PRIMARY );
1132 
1133  $logEntry = new ManualLogEntry( 'managetags', $action );
1134  $logEntry->setPerformer( $user );
1135  // target page is not relevant, but it has to be set, so we just put in
1136  // the title of Special:Tags
1137  $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
1138  $logEntry->setComment( $reason );
1139 
1140  $params = [ '4::tag' => $tag ];
1141  if ( $tagCount !== null ) {
1142  $params['5:number:count'] = $tagCount;
1143  }
1144  $logEntry->setParameters( $params );
1145  $logEntry->setRelations( [ 'Tag' => $tag ] );
1146  $logEntry->addTags( $logEntryTags );
1147 
1148  $logId = $logEntry->insert( $dbw );
1149  $logEntry->publish( $logId );
1150  return $logId;
1151  }
1152 
1162  public static function canActivateTag( $tag, Authority $performer = null ) {
1163  if ( $performer !== null ) {
1164  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1165  return Status::newFatal( 'tags-manage-no-permission' );
1166  }
1167  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1168  return Status::newFatal(
1169  'tags-manage-blocked',
1170  $performer->getUser()->getName()
1171  );
1172  }
1173  }
1174 
1175  // defined tags cannot be activated (a defined tag is either extension-
1176  // defined, in which case the extension chooses whether or not to active it;
1177  // or user-defined, in which case it is considered active)
1178  $definedTags = self::listDefinedTags();
1179  if ( in_array( $tag, $definedTags ) ) {
1180  return Status::newFatal( 'tags-activate-not-allowed', $tag );
1181  }
1182 
1183  // non-existing tags cannot be activated
1184  $tagUsage = self::tagUsageStatistics();
1185  if ( !isset( $tagUsage[$tag] ) ) { // we already know the tag is undefined
1186  return Status::newFatal( 'tags-activate-not-found', $tag );
1187  }
1188 
1189  return Status::newGood();
1190  }
1191 
1209  public static function activateTagWithChecks( $tag, $reason, Authority $performer,
1210  $ignoreWarnings = false, array $logEntryTags = []
1211  ) {
1212  // are we allowed to do this?
1213  $result = self::canActivateTag( $tag, $performer );
1214  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1215  $result->value = null;
1216  return $result;
1217  }
1218 
1219  // do it!
1220  self::defineTag( $tag );
1221 
1222  // log it
1223  $logId = self::logTagManagementAction( 'activate', $tag, $reason, $performer->getUser(),
1224  null, $logEntryTags );
1225 
1226  return Status::newGood( $logId );
1227  }
1228 
1238  public static function canDeactivateTag( $tag, Authority $performer = null ) {
1239  if ( $performer !== null ) {
1240  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1241  return Status::newFatal( 'tags-manage-no-permission' );
1242  }
1243  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1244  return Status::newFatal(
1245  'tags-manage-blocked',
1246  $performer->getUser()->getName()
1247  );
1248  }
1249  }
1250 
1251  // only explicitly-defined tags can be deactivated
1252  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
1253  if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
1254  return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
1255  }
1256  return Status::newGood();
1257  }
1258 
1276  public static function deactivateTagWithChecks( $tag, $reason, Authority $performer,
1277  $ignoreWarnings = false, array $logEntryTags = []
1278  ) {
1279  // are we allowed to do this?
1280  $result = self::canDeactivateTag( $tag, $performer );
1281  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1282  $result->value = null;
1283  return $result;
1284  }
1285 
1286  // do it!
1287  self::undefineTag( $tag );
1288 
1289  // log it
1290  $logId = self::logTagManagementAction( 'deactivate', $tag, $reason,
1291  $performer->getUser(), null, $logEntryTags );
1292 
1293  return Status::newGood( $logId );
1294  }
1295 
1303  public static function isTagNameValid( $tag ) {
1304  // no empty tags
1305  if ( $tag === '' ) {
1306  return Status::newFatal( 'tags-create-no-name' );
1307  }
1308 
1309  // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
1310  // pipe (used as a delimiter between multiple tags in
1311  // SpecialRecentchanges and friends), or slashes (would break tag description messages in
1312  // MediaWiki namespace)
1313  if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
1314  || strpos( $tag, '/' ) !== false ) {
1315  return Status::newFatal( 'tags-create-invalid-chars' );
1316  }
1317 
1318  // could the MediaWiki namespace description messages be created?
1319  $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
1320  if ( $title === null ) {
1321  return Status::newFatal( 'tags-create-invalid-title-chars' );
1322  }
1323 
1324  return Status::newGood();
1325  }
1326 
1339  public static function canCreateTag( $tag, Authority $performer = null ) {
1340  $user = null;
1341  if ( $performer !== null ) {
1342  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1343  return Status::newFatal( 'tags-manage-no-permission' );
1344  }
1345  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1346  return Status::newFatal(
1347  'tags-manage-blocked',
1348  $performer->getUser()->getName()
1349  );
1350  }
1351  // ChangeTagCanCreate hook still needs a full User object
1352  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
1353  }
1354 
1355  $status = self::isTagNameValid( $tag );
1356  if ( !$status->isGood() ) {
1357  return $status;
1358  }
1359 
1360  // does the tag already exist?
1361  $tagUsage = self::tagUsageStatistics();
1362  if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) {
1363  return Status::newFatal( 'tags-create-already-exists', $tag );
1364  }
1365 
1366  // check with hooks
1367  $canCreateResult = Status::newGood();
1368  Hooks::runner()->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1369  return $canCreateResult;
1370  }
1371 
1391  public static function createTagWithChecks( $tag, $reason, Authority $performer,
1392  $ignoreWarnings = false, array $logEntryTags = []
1393  ) {
1394  // are we allowed to do this?
1395  $result = self::canCreateTag( $tag, $performer );
1396  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1397  $result->value = null;
1398  return $result;
1399  }
1400 
1401  // do it!
1402  self::defineTag( $tag );
1403 
1404  // log it
1405  $logId = self::logTagManagementAction( 'create', $tag, $reason,
1406  $performer->getUser(), null, $logEntryTags );
1407 
1408  return Status::newGood( $logId );
1409  }
1410 
1423  public static function deleteTagEverywhere( $tag ) {
1424  $dbw = wfGetDB( DB_PRIMARY );
1425  $dbw->startAtomic( __METHOD__ );
1426 
1427  // fetch tag id, this must be done before calling undefineTag(), see T225564
1428  $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag );
1429 
1430  // set ctd_user_defined = 0
1431  self::undefineTag( $tag );
1432 
1433  // delete from change_tag
1434  $dbw->delete( 'change_tag', [ 'ct_tag_id' => $tagId ], __METHOD__ );
1435  $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag ], __METHOD__ );
1436  $dbw->endAtomic( __METHOD__ );
1437 
1438  // give extensions a chance
1439  $status = Status::newGood();
1440  Hooks::runner()->onChangeTagAfterDelete( $tag, $status );
1441  // let's not allow error results, as the actual tag deletion succeeded
1442  if ( !$status->isOK() ) {
1443  wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' );
1444  $status->setOK( true );
1445  }
1446 
1447  // clear the memcache of defined tags
1449 
1450  return $status;
1451  }
1452 
1465  public static function canDeleteTag( $tag, Authority $performer = null, int $flags = 0 ) {
1466  $tagUsage = self::tagUsageStatistics();
1467  $user = null;
1468  if ( $performer !== null ) {
1469  if ( !$performer->isAllowed( 'deletechangetags' ) ) {
1470  return Status::newFatal( 'tags-delete-no-permission' );
1471  }
1472  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1473  return Status::newFatal(
1474  'tags-manage-blocked',
1475  $performer->getUser()->getName()
1476  );
1477  }
1478  // ChangeTagCanDelete hook still needs a full User object
1479  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
1480  }
1481 
1482  if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) {
1483  return Status::newFatal( 'tags-delete-not-found', $tag );
1484  }
1485 
1486  if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1487  isset( $tagUsage[$tag] ) &&
1488  $tagUsage[$tag] > self::MAX_DELETE_USES
1489  ) {
1490  return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1491  }
1492 
1493  $softwareDefined = self::listSoftwareDefinedTags();
1494  if ( in_array( $tag, $softwareDefined ) ) {
1495  // extension-defined tags can't be deleted unless the extension
1496  // specifically allows it
1497  $status = Status::newFatal( 'tags-delete-not-allowed' );
1498  } else {
1499  // user-defined tags are deletable unless otherwise specified
1500  $status = Status::newGood();
1501  }
1502 
1503  Hooks::runner()->onChangeTagCanDelete( $tag, $user, $status );
1504  return $status;
1505  }
1506 
1524  public static function deleteTagWithChecks( $tag, $reason, Authority $performer,
1525  $ignoreWarnings = false, array $logEntryTags = []
1526  ) {
1527  // are we allowed to do this?
1528  $result = self::canDeleteTag( $tag, $performer );
1529  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1530  $result->value = null;
1531  return $result;
1532  }
1533 
1534  // store the tag usage statistics
1535  $tagUsage = self::tagUsageStatistics();
1536  $hitcount = $tagUsage[$tag] ?? 0;
1537 
1538  // do it!
1539  $deleteResult = self::deleteTagEverywhere( $tag );
1540  if ( !$deleteResult->isOK() ) {
1541  return $deleteResult;
1542  }
1543 
1544  // log it
1545  $logId = self::logTagManagementAction( 'delete', $tag, $reason, $performer->getUser(),
1546  $hitcount, $logEntryTags );
1547 
1548  $deleteResult->value = $logId;
1549  return $deleteResult;
1550  }
1551 
1558  public static function listSoftwareActivatedTags() {
1559  // core active tags
1560  $tags = self::getSoftwareTags();
1561  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1562  if ( !$hookContainer->isRegistered( 'ChangeTagsListActive' ) ) {
1563  return $tags;
1564  }
1565  $hookRunner = new HookRunner( $hookContainer );
1566  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1567  return $cache->getWithSetCallback(
1568  $cache->makeKey( 'active-tags' ),
1569  WANObjectCache::TTL_MINUTE * 5,
1570  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1571  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1572 
1573  // Ask extensions which tags they consider active
1574  $hookRunner->onChangeTagsListActive( $tags );
1575  return $tags;
1576  },
1577  [
1578  'checkKeys' => [ $cache->makeKey( 'active-tags' ) ],
1579  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1580  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1581  ]
1582  );
1583  }
1584 
1592  public static function listDefinedTags() {
1594  $tags2 = self::listSoftwareDefinedTags();
1595  return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
1596  }
1597 
1606  public static function listExplicitlyDefinedTags() {
1607  $fname = __METHOD__;
1608 
1609  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1610  return $cache->getWithSetCallback(
1611  $cache->makeKey( 'valid-tags-db' ),
1612  WANObjectCache::TTL_MINUTE * 5,
1613  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1614  $dbr = wfGetDB( DB_REPLICA );
1615 
1616  $setOpts += Database::getCacheSetOptions( $dbr );
1617 
1618  $tags = $dbr->selectFieldValues(
1619  'change_tag_def',
1620  'ctd_name',
1621  [ 'ctd_user_defined' => 1 ],
1622  $fname
1623  );
1624 
1625  return array_filter( array_unique( $tags ) );
1626  },
1627  [
1628  'checkKeys' => [ $cache->makeKey( 'valid-tags-db' ) ],
1629  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1630  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1631  ]
1632  );
1633  }
1634 
1644  public static function listSoftwareDefinedTags() {
1645  // core defined tags
1646  $tags = self::getSoftwareTags( true );
1647  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1648  if ( !$hookContainer->isRegistered( 'ListDefinedTags' ) ) {
1649  return $tags;
1650  }
1651  $hookRunner = new HookRunner( $hookContainer );
1652  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1653  return $cache->getWithSetCallback(
1654  $cache->makeKey( 'valid-tags-hook' ),
1655  WANObjectCache::TTL_MINUTE * 5,
1656  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1657  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1658 
1659  $hookRunner->onListDefinedTags( $tags );
1660  return array_filter( array_unique( $tags ) );
1661  },
1662  [
1663  'checkKeys' => [ $cache->makeKey( 'valid-tags-hook' ) ],
1664  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1665  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1666  ]
1667  );
1668  }
1669 
1675  public static function purgeTagCacheAll() {
1676  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1677 
1678  $cache->touchCheckKey( $cache->makeKey( 'active-tags' ) );
1679  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-db' ) );
1680  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-hook' ) );
1681  $cache->touchCheckKey( $cache->makeKey( 'tags-usage-statistics' ) );
1682 
1683  MediaWikiServices::getInstance()->getChangeTagDefStore()->reloadMap();
1684  }
1685 
1692  public static function tagUsageStatistics() {
1693  $fname = __METHOD__;
1694 
1695  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1696  return $cache->getWithSetCallback(
1697  $cache->makeKey( 'tags-usage-statistics' ),
1698  WANObjectCache::TTL_MINUTE * 5,
1699  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1700  $dbr = wfGetDB( DB_REPLICA );
1701  $res = $dbr->select(
1702  'change_tag_def',
1703  [ 'ctd_name', 'ctd_count' ],
1704  [],
1705  $fname,
1706  [ 'ORDER BY' => 'ctd_count DESC' ]
1707  );
1708 
1709  $out = [];
1710  foreach ( $res as $row ) {
1711  $out[$row->ctd_name] = $row->ctd_count;
1712  }
1713 
1714  return $out;
1715  },
1716  [
1717  'checkKeys' => [ $cache->makeKey( 'tags-usage-statistics' ) ],
1718  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1719  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1720  ]
1721  );
1722  }
1723 
1738  public static function showTagEditingUI( Authority $performer ) {
1739  return $performer->isAllowed( 'changetags' ) && (bool)self::listExplicitlyDefinedTags();
1740  }
1741 }
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:983
$wgUseTagFilter
$wgUseTagFilter
Allow filtering by change tag in recentchanges, history, etc Has no effect if no tags are defined.
Definition: DefaultSettings.php:8152
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:415
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:1675
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:1391
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:186
$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:1162
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:1209
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:80
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:779
SpecialPage\getTitleFor
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Definition: SpecialPage.php:107
Sanitizer\escapeClass
static escapeClass( $class)
Given a value, escape it so that it can be used as a CSS class and return it.
Definition: Sanitizer.php:972
ChangeTags\deleteTagWithChecks
static deleteTagWithChecks( $tag, $reason, Authority $performer, $ignoreWarnings=false, array $logEntryTags=[])
Deletes a tag, checking whether it is allowed first, and adding a log entry afterwards.
Definition: ChangeTags.php:1524
ChangeTags\buildTagFilterSelector
static buildTagFilterSelector( $selected='', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
Definition: ChangeTags.php:1020
$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:602
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:1303
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:249
ChangeTags\listSoftwareDefinedTags
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
Definition: ChangeTags.php:1644
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:1423
ChangeTags\modifyDisplayQuery
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='')
Applies all tags-related changes to a query.
Definition: ChangeTags.php:891
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:588
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:1094
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:2226
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:1238
ChangeTags\listDefinedTags
static listDefinedTags()
Basically lists defined tags which count even if they aren't applied to anything.
Definition: ChangeTags.php:1592
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:1067
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:675
$wgSoftwareTags
array $wgSoftwareTags
List of core tags to enable.
Definition: DefaultSettings.php:8173
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:1606
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:625
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:706
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
Message\plaintextParam
static plaintextParam( $plaintext)
Definition: Message.php:1202
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:132
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:1339
ChangeTags\listSoftwareActivatedTags
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
Definition: ChangeTags.php:1558
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:365
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:705
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:1276
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:210
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:1128
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:44
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:1692
ChangeTags\TAG_REVERTED
const TAG_REVERTED
The tagged edit is reverted by a subsequent edit (which is tagged by one of TAG_ROLLBACK,...
Definition: ChangeTags.php:86
Xml\input
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition: Xml.php:280
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1043
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:555
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:1738
ChangeTags\tagLongDescriptionMessage
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
Definition: ChangeTags.php:300
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:545
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:328
ChangeTags\tagDescription
static tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
Definition: ChangeTags.php:283
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:1465