MediaWiki  master
ChangeTags.php
Go to the documentation of this file.
1 <?php
32 
33 class ChangeTags {
37  public const TAG_CONTENT_MODEL_CHANGE = 'mw-contentmodelchange';
42  public const TAG_NEW_REDIRECT = 'mw-new-redirect';
46  public const TAG_REMOVED_REDIRECT = 'mw-removed-redirect';
50  public const TAG_CHANGED_REDIRECT_TARGET = 'mw-changed-redirect-target';
54  public const TAG_BLANK = 'mw-blank';
58  public const TAG_REPLACE = 'mw-replace';
66  public const TAG_ROLLBACK = 'mw-rollback';
73  public const TAG_UNDO = 'mw-undo';
79  public const TAG_MANUAL_REVERT = 'mw-manual-revert';
87  public const TAG_REVERTED = 'mw-reverted';
91  public const TAG_SERVER_SIDE_UPLOAD = 'mw-server-side-upload';
92 
97 
101  public const BYPASS_MAX_USAGE_CHECK = 1;
102 
108  private const MAX_DELETE_USES = 5000;
109 
113  private const DEFINED_SOFTWARE_TAGS = [
114  'mw-contentmodelchange',
115  'mw-new-redirect',
116  'mw-removed-redirect',
117  'mw-changed-redirect-target',
118  'mw-blank',
119  'mw-replace',
120  'mw-rollback',
121  'mw-undo',
122  'mw-manual-revert',
123  'mw-reverted',
124  'mw-server-side-upload',
125  ];
126 
137  public static $avoidReopeningTablesForTesting = false;
138 
146  public static function getSoftwareTags( $all = false ) {
147  $coreTags = MediaWikiServices::getInstance()->getMainConfig()->get(
148  MainConfigNames::SoftwareTags );
149  $softwareTags = [];
150 
151  if ( !is_array( $coreTags ) ) {
152  wfWarn( 'wgSoftwareTags should be associative array of enabled tags.
153  Please refer to documentation for the list of tags you can enable' );
154  return $softwareTags;
155  }
156 
157  $availableSoftwareTags = !$all ?
158  array_keys( array_filter( $coreTags ) ) :
159  array_keys( $coreTags );
160 
161  $softwareTags = array_intersect(
162  $availableSoftwareTags,
163  self::DEFINED_SOFTWARE_TAGS
164  );
165 
166  return $softwareTags;
167  }
168 
182  public static function formatSummaryRow( $tags, $page, MessageLocalizer $localizer = null ) {
183  if ( $tags === '' || $tags === null ) {
184  return [ '', [] ];
185  }
186  if ( !$localizer ) {
187  $localizer = RequestContext::getMain();
188  }
189 
190  $classes = [];
191 
192  $tags = explode( ',', $tags );
193  $order = array_flip( self::listDefinedTags() );
194  usort( $tags, static function ( $a, $b ) use ( $order ) {
195  return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
196  } );
197 
198  $displayTags = [];
199  foreach ( $tags as $tag ) {
200  if ( $tag === '' ) {
201  continue;
202  }
203  $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
204  $description = self::tagDescription( $tag, $localizer );
205  if ( $description === false ) {
206  continue;
207  }
208  $displayTags[] = Xml::tags(
209  'span',
210  [ 'class' => 'mw-tag-marker ' .
211  Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
212  $description
213  );
214  }
215 
216  if ( !$displayTags ) {
217  return [ '', $classes ];
218  }
219 
220  $markers = $localizer->msg( 'tag-list-wrapper' )
221  ->numParams( count( $displayTags ) )
222  ->rawParams( implode( ' ', $displayTags ) )
223  ->parse();
224  $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
225 
226  return [ $markers, $classes ];
227  }
228 
242  public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
243  $msg = $context->msg( "tag-$tag" );
244  if ( !$msg->exists() ) {
245  // No such message
246  // Pass through ->msg(), even though it seems redundant, to avoid requesting
247  // the user's language from session-less entry points (T227233)
248  return $context->msg( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) );
249  }
250  if ( $msg->isDisabled() ) {
251  // The message exists but is disabled, hide the tag.
252  return false;
253  }
254 
255  // Message exists and isn't disabled, use it.
256  return $msg;
257  }
258 
272  public static function tagDescription( $tag, MessageLocalizer $context ) {
273  $msg = self::tagShortDescriptionMessage( $tag, $context );
274  return $msg ? $msg->parse() : false;
275  }
276 
289  public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
290  $msg = $context->msg( "tag-$tag-description" );
291  if ( !$msg->exists() ) {
292  return false;
293  }
294  if ( $msg->isDisabled() ) {
295  // The message exists but is disabled, hide the description.
296  return false;
297  }
298 
299  // Message exists and isn't disabled, use it.
300  return $msg;
301  }
302 
317  public static function addTags( $tags, $rc_id = null, $rev_id = null,
318  $log_id = null, $params = null, RecentChange $rc = null
319  ) {
320  $result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
321  return (bool)$result[0];
322  }
323 
354  public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
355  &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
356  UserIdentity $user = null
357  ) {
358  $tagsToAdd = array_filter(
359  (array)$tagsToAdd, // Make sure we're submitting all tags...
360  static function ( $value ) {
361  return ( $value ?? '' ) !== '';
362  }
363  );
364  $tagsToRemove = array_filter(
365  (array)$tagsToRemove,
366  static function ( $value ) {
367  return ( $value ?? '' ) !== '';
368  }
369  );
370 
371  if ( !$rc_id && !$rev_id && !$log_id ) {
372  throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
373  'specified when adding or removing a tag from a change!' );
374  }
375 
376  $dbw = wfGetDB( DB_PRIMARY );
377 
378  // Might as well look for rcids and so on.
379  if ( !$rc_id ) {
380  // Info might be out of date, somewhat fractionally, on replica DB.
381  // LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
382  // so use that relation to avoid full table scans.
383  if ( $log_id ) {
384  $rc_id = $dbw->selectField(
385  [ 'logging', 'recentchanges' ],
386  'rc_id',
387  [
388  'log_id' => $log_id,
389  'rc_timestamp = log_timestamp',
390  'rc_logid = log_id'
391  ],
392  __METHOD__
393  );
394  } elseif ( $rev_id ) {
395  $rc_id = $dbw->selectField(
396  [ 'revision', 'recentchanges' ],
397  'rc_id',
398  [
399  'rev_id' => $rev_id,
400  'rc_this_oldid = rev_id'
401  ],
402  __METHOD__
403  );
404  }
405  } elseif ( !$log_id && !$rev_id ) {
406  // Info might be out of date, somewhat fractionally, on replica DB.
407  $log_id = $dbw->selectField(
408  'recentchanges',
409  'rc_logid',
410  [ 'rc_id' => $rc_id ],
411  __METHOD__
412  );
413  $rev_id = $dbw->selectField(
414  'recentchanges',
415  'rc_this_oldid',
416  [ 'rc_id' => $rc_id ],
417  __METHOD__
418  );
419  }
420 
421  if ( $log_id && !$rev_id ) {
422  $rev_id = $dbw->selectField(
423  'log_search',
424  'ls_value',
425  [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ],
426  __METHOD__
427  );
428  } elseif ( !$log_id && $rev_id ) {
429  $log_id = $dbw->selectField(
430  'log_search',
431  'ls_log_id',
432  [ 'ls_field' => 'associated_rev_id', 'ls_value' => (string)$rev_id ],
433  __METHOD__
434  );
435  }
436 
437  $prevTags = self::getTags( $dbw, $rc_id, $rev_id, $log_id );
438 
439  // add tags
440  $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
441  $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
442 
443  // remove tags
444  $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
445  $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
446 
447  sort( $prevTags );
448  sort( $newTags );
449  if ( $prevTags == $newTags ) {
450  return [ [], [], $prevTags ];
451  }
452 
453  // insert a row into change_tag for each new tag
454  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
455  if ( count( $tagsToAdd ) ) {
456  $changeTagMapping = [];
457  foreach ( $tagsToAdd as $tag ) {
458  $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
459  }
460  $fname = __METHOD__;
461  // T207881: update the counts at the end of the transaction
462  $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tagsToAdd, $fname ) {
463  $dbw->update(
464  'change_tag_def',
465  [ 'ctd_count = ctd_count + 1' ],
466  [ 'ctd_name' => $tagsToAdd ],
467  $fname
468  );
469  }, $fname );
470 
471  $tagsRows = [];
472  foreach ( $tagsToAdd as $tag ) {
473  // Filter so we don't insert NULLs as zero accidentally.
474  // Keep in mind that $rc_id === null means "I don't care/know about the
475  // rc_id, just delete $tag on this revision/log entry". It doesn't
476  // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
477  $tagsRows[] = array_filter(
478  [
479  'ct_rc_id' => $rc_id,
480  'ct_log_id' => $log_id,
481  'ct_rev_id' => $rev_id,
482  'ct_params' => $params,
483  'ct_tag_id' => $changeTagMapping[$tag] ?? null,
484  ]
485  );
486 
487  }
488 
489  $dbw->insert( 'change_tag', $tagsRows, __METHOD__, [ 'IGNORE' ] );
490  }
491 
492  // delete from change_tag
493  if ( count( $tagsToRemove ) ) {
494  $fname = __METHOD__;
495  foreach ( $tagsToRemove as $tag ) {
496  $conds = array_filter(
497  [
498  'ct_rc_id' => $rc_id,
499  'ct_log_id' => $log_id,
500  'ct_rev_id' => $rev_id,
501  'ct_tag_id' => $changeTagDefStore->getId( $tag ),
502  ]
503  );
504  $dbw->delete( 'change_tag', $conds, __METHOD__ );
505  if ( $dbw->affectedRows() ) {
506  // T207881: update the counts at the end of the transaction
507  $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tag, $fname ) {
508  $dbw->update(
509  'change_tag_def',
510  [ 'ctd_count = ctd_count - 1' ],
511  [ 'ctd_name' => $tag ],
512  $fname
513  );
514 
515  $dbw->delete(
516  'change_tag_def',
517  [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
518  $fname
519  );
520  }, $fname );
521  }
522  }
523  }
524 
525  $userObj = $user ? MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $user ) : null;
526  Hooks::runner()->onChangeTagsAfterUpdateTags( $tagsToAdd, $tagsToRemove, $prevTags,
527  $rc_id, $rev_id, $log_id, $params, $rc, $userObj );
528 
529  return [ $tagsToAdd, $tagsToRemove, $prevTags ];
530  }
531 
544  public static function getTagsWithData(
545  IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null
546  ) {
547  if ( !$rc_id && !$rev_id && !$log_id ) {
548  throw new MWException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
549  'specified when loading tags from a change!' );
550  }
551 
552  $conds = array_filter(
553  [
554  'ct_rc_id' => $rc_id,
555  'ct_rev_id' => $rev_id,
556  'ct_log_id' => $log_id,
557  ]
558  );
559 
560  $result = $db->select(
561  'change_tag',
562  [ 'ct_tag_id', 'ct_params' ],
563  $conds,
564  __METHOD__
565  );
566 
567  $tags = [];
568  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
569  foreach ( $result as $row ) {
570  $tagName = $changeTagDefStore->getName( (int)$row->ct_tag_id );
571  $tags[$tagName] = $row->ct_params;
572  }
573 
574  return $tags;
575  }
576 
587  public static function getTags( IDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
588  return array_keys( self::getTagsWithData( $db, $rc_id, $rev_id, $log_id ) );
589  }
590 
601  protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
602  $lang = RequestContext::getMain()->getLanguage();
603  $tags = array_values( $tags );
604  $count = count( $tags );
605  $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
606  $lang->commaList( $tags ), $count );
607  $status->value = $tags;
608  return $status;
609  }
610 
625  public static function canAddTagsAccompanyingChange(
626  array $tags,
627  Authority $performer = null,
628  $checkBlock = true
629  ) {
630  $user = null;
631  if ( $performer !== null ) {
632  if ( !$performer->isAllowed( 'applychangetags' ) ) {
633  return Status::newFatal( 'tags-apply-no-permission' );
634  }
635 
636  if ( $checkBlock && $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
637  return Status::newFatal(
638  'tags-apply-blocked',
639  $performer->getUser()->getName()
640  );
641  }
642 
643  // ChangeTagsAllowedAdd hook still needs a full User object
644  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
645  }
646 
647  // to be applied, a tag has to be explicitly defined
648  $allowedTags = self::listExplicitlyDefinedTags();
649  Hooks::runner()->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
650  $disallowedTags = array_diff( $tags, $allowedTags );
651  if ( $disallowedTags ) {
652  return self::restrictedTagError( 'tags-apply-not-allowed-one',
653  'tags-apply-not-allowed-multi', $disallowedTags );
654  }
655 
656  return Status::newGood();
657  }
658 
679  public static function addTagsAccompanyingChangeWithChecks(
680  array $tags, $rc_id, $rev_id, $log_id, $params, Authority $performer
681  ) {
682  // are we allowed to do this?
683  $result = self::canAddTagsAccompanyingChange( $tags, $performer );
684  if ( !$result->isOK() ) {
685  $result->value = null;
686  return $result;
687  }
688 
689  // do it!
690  self::addTags( $tags, $rc_id, $rev_id, $log_id, $params );
691 
692  return Status::newGood( true );
693  }
694 
709  public static function canUpdateTags(
710  array $tagsToAdd,
711  array $tagsToRemove,
712  Authority $performer = null
713  ) {
714  if ( $performer !== null ) {
715  if ( !$performer->isAllowed( 'changetags' ) ) {
716  return Status::newFatal( 'tags-update-no-permission' );
717  }
718 
719  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
720  return Status::newFatal(
721  'tags-update-blocked',
722  $performer->getUser()->getName()
723  );
724  }
725  }
726 
727  if ( $tagsToAdd ) {
728  // to be added, a tag has to be explicitly defined
729  // @todo Allow extensions to define tags that can be applied by users...
730  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
731  $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
732  if ( $diff ) {
733  return self::restrictedTagError( 'tags-update-add-not-allowed-one',
734  'tags-update-add-not-allowed-multi', $diff );
735  }
736  }
737 
738  if ( $tagsToRemove ) {
739  // to be removed, a tag must not be defined by an extension, or equivalently it
740  // has to be either explicitly defined or not defined at all
741  // (assuming no edge case of a tag both explicitly-defined and extension-defined)
742  $softwareDefinedTags = self::listSoftwareDefinedTags();
743  $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
744  if ( $intersect ) {
745  return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
746  'tags-update-remove-not-allowed-multi', $intersect );
747  }
748  }
749 
750  return Status::newGood();
751  }
752 
783  public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
784  $rc_id, $rev_id, $log_id, $params, $reason, Authority $performer
785  ) {
786  if ( $tagsToAdd === null ) {
787  $tagsToAdd = [];
788  }
789  if ( $tagsToRemove === null ) {
790  $tagsToRemove = [];
791  }
792  if ( !$tagsToAdd && !$tagsToRemove ) {
793  // no-op, don't bother
794  return Status::newGood( (object)[
795  'logId' => null,
796  'addedTags' => [],
797  'removedTags' => [],
798  ] );
799  }
800 
801  // are we allowed to do this?
802  $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $performer );
803  if ( !$result->isOK() ) {
804  $result->value = null;
805  return $result;
806  }
807 
808  // basic rate limiting
809  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
810  if ( $user->pingLimiter( 'changetag' ) ) {
811  return Status::newFatal( 'actionthrottledtext' );
812  }
813 
814  // do it!
815  list( $tagsAdded, $tagsRemoved, $initialTags ) = self::updateTags( $tagsToAdd,
816  $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user );
817  if ( !$tagsAdded && !$tagsRemoved ) {
818  // no-op, don't log it
819  return Status::newGood( (object)[
820  'logId' => null,
821  'addedTags' => [],
822  'removedTags' => [],
823  ] );
824  }
825 
826  // log it
827  $logEntry = new ManualLogEntry( 'tag', 'update' );
828  $logEntry->setPerformer( $performer->getUser() );
829  $logEntry->setComment( $reason );
830 
831  // find the appropriate target page
832  if ( $rev_id ) {
833  $revisionRecord = MediaWikiServices::getInstance()
834  ->getRevisionLookup()
835  ->getRevisionById( $rev_id );
836  if ( $revisionRecord ) {
837  $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
838  }
839  } elseif ( $log_id ) {
840  // This function is from revision deletion logic and has nothing to do with
841  // change tags, but it appears to be the only other place in core where we
842  // perform logged actions on log items.
843  $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
844  }
845 
846  if ( !$logEntry->getTarget() ) {
847  // target is required, so we have to set something
848  $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
849  }
850 
851  $logParams = [
852  '4::revid' => $rev_id,
853  '5::logid' => $log_id,
854  '6:list:tagsAdded' => $tagsAdded,
855  '7:number:tagsAddedCount' => count( $tagsAdded ),
856  '8:list:tagsRemoved' => $tagsRemoved,
857  '9:number:tagsRemovedCount' => count( $tagsRemoved ),
858  'initialTags' => $initialTags,
859  ];
860  $logEntry->setParameters( $logParams );
861  $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
862 
863  $dbw = wfGetDB( DB_PRIMARY );
864  $logId = $logEntry->insert( $dbw );
865  // Only send this to UDP, not RC, similar to patrol events
866  $logEntry->publish( $logId, 'udp' );
867 
868  return Status::newGood( (object)[
869  'logId' => $logId,
870  'addedTags' => $tagsAdded,
871  'removedTags' => $tagsRemoved,
872  ] );
873  }
874 
896  public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
897  &$join_conds, &$options, $filter_tag = '', bool $exclude = false
898  ) {
899  $useTagFilter = MediaWikiServices::getInstance()->getMainConfig()->get(
900  MainConfigNames::UseTagFilter );
901 
902  // Normalize to arrays
903  $tables = (array)$tables;
904  $fields = (array)$fields;
905  $conds = (array)$conds;
906  $options = (array)$options;
907 
908  $fields['ts_tags'] = self::makeTagSummarySubquery( $tables );
909 
910  // Figure out which ID field to use
911  if ( in_array( 'recentchanges', $tables ) ) {
912  $join_cond = 'ct_rc_id=rc_id';
913  } elseif ( in_array( 'logging', $tables ) ) {
914  $join_cond = 'ct_log_id=log_id';
915  } elseif ( in_array( 'revision', $tables ) ) {
916  $join_cond = 'ct_rev_id=rev_id';
917  } elseif ( in_array( 'archive', $tables ) ) {
918  $join_cond = 'ct_rev_id=ar_rev_id';
919  } else {
920  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
921  }
922 
923  if ( !$useTagFilter ) {
924  return;
925  }
926 
927  if ( !is_array( $filter_tag ) ) {
928  // some callers provide false or null
929  $filter_tag = (string)$filter_tag;
930  }
931 
932  if ( $filter_tag !== [] && $filter_tag !== '' ) {
933  // Somebody wants to filter on a tag.
934  // Add an INNER JOIN on change_tag
935  $tagTable = self::getDisplayTableName();
936  $filterTagIds = [];
937  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
938  foreach ( (array)$filter_tag as $filterTagName ) {
939  try {
940  $filterTagIds[] = $changeTagDefStore->getId( $filterTagName );
941  } catch ( NameTableAccessException $exception ) {
942  }
943  }
944 
945  if ( $exclude ) {
946  if ( $filterTagIds !== [] ) {
947  $tables[] = $tagTable;
948  $join_conds[$tagTable] = [
949  'LEFT JOIN',
950  [ $join_cond, 'ct_tag_id' => $filterTagIds ]
951  ];
952  $conds[] = "$tagTable.ct_tag_id IS NULL";
953  }
954  } else {
955  $tables[] = $tagTable;
956  $join_conds[$tagTable] = [ 'JOIN', $join_cond ];
957  if ( $filterTagIds !== [] ) {
958  $conds['ct_tag_id'] = $filterTagIds;
959  } else {
960  // all tags were invalid, return nothing
961  $conds[] = '0=1';
962  }
963 
964  if (
965  is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
966  !in_array( 'DISTINCT', $options )
967  ) {
968  $options[] = 'DISTINCT';
969  }
970  }
971  }
972  }
973 
980  public static function getDisplayTableName() {
981  $tagTable = 'change_tag';
982  if ( self::$avoidReopeningTablesForTesting && defined( 'MW_PHPUNIT_TEST' ) ) {
983  $db = wfGetDB( DB_REPLICA );
984 
985  if ( $db->getType() === 'mysql' ) {
986  // When filtering by tag, we are using the change_tag table twice:
987  // Once in a join for filtering, and once in a sub-query to list all
988  // tags for each revision. This does not work with temporary tables
989  // on some versions of MySQL, which causes phpunit tests to fail.
990  // As a hacky workaround, we copy the temporary table, and join
991  // against the copy. It is acknowledged that this is quite horrific.
992  // Discuss at T256006.
993 
994  $tagTable = 'change_tag_for_display_query';
995  if ( !$db->tableExists( $tagTable ) ) {
996  $db->query(
997  'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $db->tableName( $tagTable )
998  . ' LIKE ' . $db->tableName( 'change_tag' ),
999  __METHOD__
1000  );
1001  $db->query(
1002  'INSERT IGNORE INTO ' . $db->tableName( $tagTable )
1003  . ' SELECT * FROM ' . $db->tableName( 'change_tag' ),
1004  __METHOD__
1005  );
1006  }
1007  }
1008  }
1009  return $tagTable;
1010  }
1011 
1020  public static function makeTagSummarySubquery( $tables ) {
1021  // Normalize to arrays
1022  $tables = (array)$tables;
1023 
1024  // Figure out which ID field to use
1025  if ( in_array( 'recentchanges', $tables ) ) {
1026  $join_cond = 'ct_rc_id=rc_id';
1027  } elseif ( in_array( 'logging', $tables ) ) {
1028  $join_cond = 'ct_log_id=log_id';
1029  } elseif ( in_array( 'revision', $tables ) ) {
1030  $join_cond = 'ct_rev_id=rev_id';
1031  } elseif ( in_array( 'archive', $tables ) ) {
1032  $join_cond = 'ct_rev_id=ar_rev_id';
1033  } else {
1034  throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
1035  }
1036 
1037  $tagTables = [ 'change_tag', 'change_tag_def' ];
1038  $join_cond_ts_tags = [ 'change_tag_def' => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
1039  $field = 'ctd_name';
1040 
1041  return wfGetDB( DB_REPLICA )->buildGroupConcatField(
1042  ',', $tagTables, $field, $join_cond, $join_cond_ts_tags
1043  );
1044  }
1045 
1057  public static function buildTagFilterSelector(
1058  $selected = '', $ooui = false, IContextSource $context = null
1059  ) {
1060  if ( !$context ) {
1061  $context = RequestContext::getMain();
1062  }
1063 
1064  $config = $context->getConfig();
1065  if ( !$config->get( MainConfigNames::UseTagFilter ) ||
1066  !count( self::listDefinedTags() ) ) {
1067  return [];
1068  }
1069 
1070  $tags = self::getChangeTagList( $context, $context->getLanguage() );
1071  $autocomplete = [];
1072  foreach ( $tags as $tagInfo ) {
1073  $autocomplete[ $tagInfo['label'] ] = $tagInfo['name'];
1074  }
1075 
1076  $data = [
1078  'label',
1079  [ 'for' => 'tagfilter' ],
1080  $context->msg( 'tag-filter' )->parse()
1081  )
1082  ];
1083 
1084  if ( $ooui ) {
1085  $options = Xml::listDropDownOptionsOoui( $autocomplete );
1086 
1087  $data[] = new OOUI\ComboBoxInputWidget( [
1088  'id' => 'tagfilter',
1089  'name' => 'tagfilter',
1090  'value' => $selected,
1091  'classes' => 'mw-tagfilter-input',
1092  'options' => $options,
1093  ] );
1094  } else {
1095  $datalist = new XmlSelect( false, 'tagfilter-datalist' );
1096  $datalist->setTagName( 'datalist' );
1097  $datalist->addOptions( $autocomplete );
1098 
1099  $data[] = Xml::input(
1100  'tagfilter',
1101  20,
1102  $selected,
1103  [
1104  'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline',
1105  'id' => 'tagfilter',
1106  'list' => 'tagfilter-datalist',
1107  ]
1108  ) . $datalist->getHTML();
1109  }
1110 
1111  return $data;
1112  }
1113 
1122  public static function defineTag( $tag ) {
1123  $dbw = wfGetDB( DB_PRIMARY );
1124  $tagDef = [
1125  'ctd_name' => $tag,
1126  'ctd_user_defined' => 1,
1127  'ctd_count' => 0
1128  ];
1129  $dbw->upsert(
1130  'change_tag_def',
1131  $tagDef,
1132  'ctd_name',
1133  [ 'ctd_user_defined' => 1 ],
1134  __METHOD__
1135  );
1136 
1137  // clear the memcache of defined tags
1139  }
1140 
1149  public static function undefineTag( $tag ) {
1150  $dbw = wfGetDB( DB_PRIMARY );
1151 
1152  $dbw->update(
1153  'change_tag_def',
1154  [ 'ctd_user_defined' => 0 ],
1155  [ 'ctd_name' => $tag ],
1156  __METHOD__
1157  );
1158 
1159  $dbw->delete(
1160  'change_tag_def',
1161  [ 'ctd_name' => $tag, 'ctd_count' => 0 ],
1162  __METHOD__
1163  );
1164 
1165  // clear the memcache of defined tags
1167  }
1168 
1183  protected static function logTagManagementAction( $action, $tag, $reason,
1184  UserIdentity $user, $tagCount = null, array $logEntryTags = []
1185  ) {
1186  $dbw = wfGetDB( DB_PRIMARY );
1187 
1188  $logEntry = new ManualLogEntry( 'managetags', $action );
1189  $logEntry->setPerformer( $user );
1190  // target page is not relevant, but it has to be set, so we just put in
1191  // the title of Special:Tags
1192  $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
1193  $logEntry->setComment( $reason );
1194 
1195  $params = [ '4::tag' => $tag ];
1196  if ( $tagCount !== null ) {
1197  $params['5:number:count'] = $tagCount;
1198  }
1199  $logEntry->setParameters( $params );
1200  $logEntry->setRelations( [ 'Tag' => $tag ] );
1201  $logEntry->addTags( $logEntryTags );
1202 
1203  $logId = $logEntry->insert( $dbw );
1204  $logEntry->publish( $logId );
1205  return $logId;
1206  }
1207 
1217  public static function canActivateTag( $tag, Authority $performer = null ) {
1218  if ( $performer !== null ) {
1219  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1220  return Status::newFatal( 'tags-manage-no-permission' );
1221  }
1222  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1223  return Status::newFatal(
1224  'tags-manage-blocked',
1225  $performer->getUser()->getName()
1226  );
1227  }
1228  }
1229 
1230  // defined tags cannot be activated (a defined tag is either extension-
1231  // defined, in which case the extension chooses whether or not to active it;
1232  // or user-defined, in which case it is considered active)
1233  $definedTags = self::listDefinedTags();
1234  if ( in_array( $tag, $definedTags ) ) {
1235  return Status::newFatal( 'tags-activate-not-allowed', $tag );
1236  }
1237 
1238  // non-existing tags cannot be activated
1239  $tagUsage = self::tagUsageStatistics();
1240  if ( !isset( $tagUsage[$tag] ) ) { // we already know the tag is undefined
1241  return Status::newFatal( 'tags-activate-not-found', $tag );
1242  }
1243 
1244  return Status::newGood();
1245  }
1246 
1264  public static function activateTagWithChecks( $tag, $reason, Authority $performer,
1265  $ignoreWarnings = false, array $logEntryTags = []
1266  ) {
1267  // are we allowed to do this?
1268  $result = self::canActivateTag( $tag, $performer );
1269  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1270  $result->value = null;
1271  return $result;
1272  }
1273 
1274  // do it!
1275  self::defineTag( $tag );
1276 
1277  // log it
1278  $logId = self::logTagManagementAction( 'activate', $tag, $reason, $performer->getUser(),
1279  null, $logEntryTags );
1280 
1281  return Status::newGood( $logId );
1282  }
1283 
1293  public static function canDeactivateTag( $tag, Authority $performer = null ) {
1294  if ( $performer !== null ) {
1295  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1296  return Status::newFatal( 'tags-manage-no-permission' );
1297  }
1298  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1299  return Status::newFatal(
1300  'tags-manage-blocked',
1301  $performer->getUser()->getName()
1302  );
1303  }
1304  }
1305 
1306  // only explicitly-defined tags can be deactivated
1307  $explicitlyDefinedTags = self::listExplicitlyDefinedTags();
1308  if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
1309  return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
1310  }
1311  return Status::newGood();
1312  }
1313 
1331  public static function deactivateTagWithChecks( $tag, $reason, Authority $performer,
1332  $ignoreWarnings = false, array $logEntryTags = []
1333  ) {
1334  // are we allowed to do this?
1335  $result = self::canDeactivateTag( $tag, $performer );
1336  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1337  $result->value = null;
1338  return $result;
1339  }
1340 
1341  // do it!
1342  self::undefineTag( $tag );
1343 
1344  // log it
1345  $logId = self::logTagManagementAction( 'deactivate', $tag, $reason,
1346  $performer->getUser(), null, $logEntryTags );
1347 
1348  return Status::newGood( $logId );
1349  }
1350 
1358  public static function isTagNameValid( $tag ) {
1359  // no empty tags
1360  if ( $tag === '' ) {
1361  return Status::newFatal( 'tags-create-no-name' );
1362  }
1363 
1364  // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
1365  // pipe (used as a delimiter between multiple tags in
1366  // SpecialRecentchanges and friends), or slashes (would break tag description messages in
1367  // MediaWiki namespace)
1368  if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
1369  || strpos( $tag, '/' ) !== false ) {
1370  return Status::newFatal( 'tags-create-invalid-chars' );
1371  }
1372 
1373  // could the MediaWiki namespace description messages be created?
1374  $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
1375  if ( $title === null ) {
1376  return Status::newFatal( 'tags-create-invalid-title-chars' );
1377  }
1378 
1379  return Status::newGood();
1380  }
1381 
1394  public static function canCreateTag( $tag, Authority $performer = null ) {
1395  $user = null;
1396  if ( $performer !== null ) {
1397  if ( !$performer->isAllowed( 'managechangetags' ) ) {
1398  return Status::newFatal( 'tags-manage-no-permission' );
1399  }
1400  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1401  return Status::newFatal(
1402  'tags-manage-blocked',
1403  $performer->getUser()->getName()
1404  );
1405  }
1406  // ChangeTagCanCreate hook still needs a full User object
1407  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
1408  }
1409 
1410  $status = self::isTagNameValid( $tag );
1411  if ( !$status->isGood() ) {
1412  return $status;
1413  }
1414 
1415  // does the tag already exist?
1416  $tagUsage = self::tagUsageStatistics();
1417  if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) {
1418  return Status::newFatal( 'tags-create-already-exists', $tag );
1419  }
1420 
1421  // check with hooks
1422  $canCreateResult = Status::newGood();
1423  Hooks::runner()->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1424  return $canCreateResult;
1425  }
1426 
1446  public static function createTagWithChecks( $tag, $reason, Authority $performer,
1447  $ignoreWarnings = false, array $logEntryTags = []
1448  ) {
1449  // are we allowed to do this?
1450  $result = self::canCreateTag( $tag, $performer );
1451  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1452  $result->value = null;
1453  return $result;
1454  }
1455 
1456  // do it!
1457  self::defineTag( $tag );
1458 
1459  // log it
1460  $logId = self::logTagManagementAction( 'create', $tag, $reason,
1461  $performer->getUser(), null, $logEntryTags );
1462 
1463  return Status::newGood( $logId );
1464  }
1465 
1478  public static function deleteTagEverywhere( $tag ) {
1479  $dbw = wfGetDB( DB_PRIMARY );
1480  $dbw->startAtomic( __METHOD__ );
1481 
1482  // fetch tag id, this must be done before calling undefineTag(), see T225564
1483  $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag );
1484 
1485  // set ctd_user_defined = 0
1486  self::undefineTag( $tag );
1487 
1488  // delete from change_tag
1489  $dbw->delete( 'change_tag', [ 'ct_tag_id' => $tagId ], __METHOD__ );
1490  $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag ], __METHOD__ );
1491  $dbw->endAtomic( __METHOD__ );
1492 
1493  // give extensions a chance
1494  $status = Status::newGood();
1495  Hooks::runner()->onChangeTagAfterDelete( $tag, $status );
1496  // let's not allow error results, as the actual tag deletion succeeded
1497  if ( !$status->isOK() ) {
1498  wfDebug( 'ChangeTagAfterDelete error condition downgraded to warning' );
1499  $status->setOK( true );
1500  }
1501 
1502  // clear the memcache of defined tags
1504 
1505  return $status;
1506  }
1507 
1520  public static function canDeleteTag( $tag, Authority $performer = null, int $flags = 0 ) {
1521  $tagUsage = self::tagUsageStatistics();
1522  $user = null;
1523  if ( $performer !== null ) {
1524  if ( !$performer->isAllowed( 'deletechangetags' ) ) {
1525  return Status::newFatal( 'tags-delete-no-permission' );
1526  }
1527  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1528  return Status::newFatal(
1529  'tags-manage-blocked',
1530  $performer->getUser()->getName()
1531  );
1532  }
1533  // ChangeTagCanDelete hook still needs a full User object
1534  $user = MediaWikiServices::getInstance()->getUserFactory()->newFromAuthority( $performer );
1535  }
1536 
1537  if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) {
1538  return Status::newFatal( 'tags-delete-not-found', $tag );
1539  }
1540 
1541  if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1542  isset( $tagUsage[$tag] ) &&
1543  $tagUsage[$tag] > self::MAX_DELETE_USES
1544  ) {
1545  return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1546  }
1547 
1548  $softwareDefined = self::listSoftwareDefinedTags();
1549  if ( in_array( $tag, $softwareDefined ) ) {
1550  // extension-defined tags can't be deleted unless the extension
1551  // specifically allows it
1552  $status = Status::newFatal( 'tags-delete-not-allowed' );
1553  } else {
1554  // user-defined tags are deletable unless otherwise specified
1555  $status = Status::newGood();
1556  }
1557 
1558  Hooks::runner()->onChangeTagCanDelete( $tag, $user, $status );
1559  return $status;
1560  }
1561 
1579  public static function deleteTagWithChecks( $tag, $reason, Authority $performer,
1580  $ignoreWarnings = false, array $logEntryTags = []
1581  ) {
1582  // are we allowed to do this?
1583  $result = self::canDeleteTag( $tag, $performer );
1584  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1585  $result->value = null;
1586  return $result;
1587  }
1588 
1589  // store the tag usage statistics
1590  $tagUsage = self::tagUsageStatistics();
1591  $hitcount = $tagUsage[$tag] ?? 0;
1592 
1593  // do it!
1594  $deleteResult = self::deleteTagEverywhere( $tag );
1595  if ( !$deleteResult->isOK() ) {
1596  return $deleteResult;
1597  }
1598 
1599  // log it
1600  $logId = self::logTagManagementAction( 'delete', $tag, $reason, $performer->getUser(),
1601  $hitcount, $logEntryTags );
1602 
1603  $deleteResult->value = $logId;
1604  return $deleteResult;
1605  }
1606 
1613  public static function listSoftwareActivatedTags() {
1614  // core active tags
1615  $tags = self::getSoftwareTags();
1616  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1617  if ( !$hookContainer->isRegistered( 'ChangeTagsListActive' ) ) {
1618  return $tags;
1619  }
1620  $hookRunner = new HookRunner( $hookContainer );
1621  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1622  return $cache->getWithSetCallback(
1623  $cache->makeKey( 'active-tags' ),
1624  WANObjectCache::TTL_MINUTE * 5,
1625  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1626  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1627 
1628  // Ask extensions which tags they consider active
1629  $hookRunner->onChangeTagsListActive( $tags );
1630  return $tags;
1631  },
1632  [
1633  'checkKeys' => [ $cache->makeKey( 'active-tags' ) ],
1634  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1635  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1636  ]
1637  );
1638  }
1639 
1647  public static function listDefinedTags() {
1649  $tags2 = self::listSoftwareDefinedTags();
1650  return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
1651  }
1652 
1661  public static function listExplicitlyDefinedTags() {
1662  $fname = __METHOD__;
1663 
1664  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1665  return $cache->getWithSetCallback(
1666  $cache->makeKey( 'valid-tags-db' ),
1667  WANObjectCache::TTL_MINUTE * 5,
1668  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1669  $dbr = wfGetDB( DB_REPLICA );
1670 
1671  $setOpts += Database::getCacheSetOptions( $dbr );
1672 
1673  $tags = $dbr->selectFieldValues(
1674  'change_tag_def',
1675  'ctd_name',
1676  [ 'ctd_user_defined' => 1 ],
1677  $fname
1678  );
1679 
1680  return array_unique( $tags );
1681  },
1682  [
1683  'checkKeys' => [ $cache->makeKey( 'valid-tags-db' ) ],
1684  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1685  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1686  ]
1687  );
1688  }
1689 
1699  public static function listSoftwareDefinedTags() {
1700  // core defined tags
1701  $tags = self::getSoftwareTags( true );
1702  $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1703  if ( !$hookContainer->isRegistered( 'ListDefinedTags' ) ) {
1704  return $tags;
1705  }
1706  $hookRunner = new HookRunner( $hookContainer );
1707  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1708  return $cache->getWithSetCallback(
1709  $cache->makeKey( 'valid-tags-hook' ),
1710  WANObjectCache::TTL_MINUTE * 5,
1711  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
1712  $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
1713 
1714  $hookRunner->onListDefinedTags( $tags );
1715  return array_unique( $tags );
1716  },
1717  [
1718  'checkKeys' => [ $cache->makeKey( 'valid-tags-hook' ) ],
1719  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1720  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1721  ]
1722  );
1723  }
1724 
1730  public static function purgeTagCacheAll() {
1731  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1732 
1733  $cache->touchCheckKey( $cache->makeKey( 'active-tags' ) );
1734  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-db' ) );
1735  $cache->touchCheckKey( $cache->makeKey( 'valid-tags-hook' ) );
1736  $cache->touchCheckKey( $cache->makeKey( 'tags-usage-statistics' ) );
1737 
1738  MediaWikiServices::getInstance()->getChangeTagDefStore()->reloadMap();
1739  }
1740 
1747  public static function tagUsageStatistics() {
1748  $fname = __METHOD__;
1749 
1750  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1751  return $cache->getWithSetCallback(
1752  $cache->makeKey( 'tags-usage-statistics' ),
1753  WANObjectCache::TTL_MINUTE * 5,
1754  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1755  $dbr = wfGetDB( DB_REPLICA );
1756  $res = $dbr->select(
1757  'change_tag_def',
1758  [ 'ctd_name', 'ctd_count' ],
1759  [],
1760  $fname,
1761  [ 'ORDER BY' => 'ctd_count DESC' ]
1762  );
1763 
1764  $out = [];
1765  foreach ( $res as $row ) {
1766  $out[$row->ctd_name] = $row->ctd_count;
1767  }
1768 
1769  return $out;
1770  },
1771  [
1772  'checkKeys' => [ $cache->makeKey( 'tags-usage-statistics' ) ],
1773  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
1774  'pcTTL' => WANObjectCache::TTL_PROC_LONG
1775  ]
1776  );
1777  }
1778 
1783  private const TAG_DESC_CHARACTER_LIMIT = 120;
1784 
1809  public static function getChangeTagListSummary( MessageLocalizer $localizer, Language $lang ) {
1810  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1811  return $cache->getWithSetCallback(
1812  $cache->makeKey( 'tags-list-summary', $lang->getCode() ),
1813  WANObjectCache::TTL_DAY,
1814  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $localizer ) {
1815  $tagHitCounts = self::tagUsageStatistics();
1816 
1817  $result = [];
1818  // Only list tags that are still actively defined
1819  foreach ( self::listDefinedTags() as $tagName ) {
1820  // Only list tags with more than 0 hits
1821  $hits = $tagHitCounts[$tagName] ?? 0;
1822  if ( $hits <= 0 ) {
1823  continue;
1824  }
1825 
1826  $labelMsg = self::tagShortDescriptionMessage( $tagName, $localizer );
1827  $descriptionMsg = self::tagLongDescriptionMessage( $tagName, $localizer );
1828  // Don't cache the message object, use the correct MessageLocalizer to parse later.
1829  $result[] = [
1830  'name' => $tagName,
1831  'labelMsg' => (bool)$labelMsg,
1832  'label' => $labelMsg ? $labelMsg->plain() : $tagName,
1833  'descriptionMsg' => (bool)$descriptionMsg,
1834  'description' => $descriptionMsg ? $descriptionMsg->plain() : '',
1835  'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
1836  ];
1837  }
1838  return $result;
1839  }
1840  );
1841  }
1842 
1855  public static function getChangeTagList( MessageLocalizer $localizer, Language $lang ) {
1856  $tags = self::getChangeTagListSummary( $localizer, $lang );
1857  foreach ( $tags as &$tagInfo ) {
1858  if ( $tagInfo['labelMsg'] ) {
1859  // Use localizer with the correct page title to parse plain message from the cache.
1860  $labelMsg = new RawMessage( $tagInfo['label'] );
1861  $tagInfo['label'] = Sanitizer::stripAllTags( $localizer->msg( $labelMsg )->parse() );
1862  } else {
1863  $tagInfo['label'] = $localizer->msg( 'tag-hidden', $tagInfo['name'] )->text();
1864  }
1865  if ( $tagInfo['descriptionMsg'] ) {
1866  $descriptionMsg = new RawMessage( $tagInfo['description'] );
1867  $tagInfo['description'] = $lang->truncateForVisual(
1868  Sanitizer::stripAllTags( $localizer->msg( $descriptionMsg )->parse() ),
1869  self::TAG_DESC_CHARACTER_LIMIT
1870  );
1871  }
1872  unset( $tagInfo['labelMsg'] );
1873  unset( $tagInfo['descriptionMsg'] );
1874  }
1875 
1876  // Instead of sorting by hit count (disabled for now), sort by display name
1877  usort( $tags, static function ( $a, $b ) {
1878  return strcasecmp( $a['label'], $b['label'] );
1879  } );
1880  return $tags;
1881  }
1882 
1897  public static function showTagEditingUI( Authority $performer ) {
1898  return $performer->isAllowed( 'changetags' ) && (bool)self::listExplicitlyDefinedTags();
1899  }
1900 }
const NS_MEDIAWIKI
Definition: Defines.php:72
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
const TAG_MANUAL_REVERT
The tagged edit restores the page to an earlier revision.
Definition: ChangeTags.php:79
const TAG_SERVER_SIDE_UPLOAD
This tagged edit was performed while importing media files using the importImages....
Definition: ChangeTags.php:91
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
Definition: ChangeTags.php:896
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:587
const TAG_REMOVED_REDIRECT
The tagged edit turns a redirect page into a non-redirect.
Definition: ChangeTags.php:46
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
static canDeactivateTag( $tag, Authority $performer=null)
Is it OK to allow the user to deactivate this tag?
static buildTagFilterSelector( $selected='', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
static logTagManagementAction( $action, $tag, $reason, UserIdentity $user, $tagCount=null, array $logEntryTags=[])
Writes a tag action into the tag management log.
static canCreateTag( $tag, Authority $performer=null)
Is it OK to allow the user to create this tag?
const TAG_REPLACE
The tagged edit removes more than 90% of the content of the page.
Definition: ChangeTags.php:58
const TAG_CONTENT_MODEL_CHANGE
The tagged edit changes the content model of the page.
Definition: ChangeTags.php:37
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
Definition: ChangeTags.php:289
static showTagEditingUI(Authority $performer)
Indicate whether change tag editing UI is relevant.
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:146
const TAG_CHANGED_REDIRECT_TARGET
The tagged edit changes the target of a redirect page.
Definition: ChangeTags.php:50
const TAG_REVERTED
The tagged edit is reverted by a subsequent edit (which is tagged by one of TAG_ROLLBACK,...
Definition: ChangeTags.php:87
static restrictedTagError( $msgOne, $msgMulti, $tags)
Helper function to generate a fatal status with a 'not-allowed' type error.
Definition: ChangeTags.php:601
const TAG_ROLLBACK
The tagged edit is a rollback (undoes the previous edit and all immediately preceding edits by the sa...
Definition: ChangeTags.php:66
static getChangeTagListSummary(MessageLocalizer $localizer, Language $lang)
Get information about change tags, without parsing messages, for tag filter dropdown menus.
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.
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
static getChangeTagList(MessageLocalizer $localizer, Language $lang)
Get information about change tags for tag filter dropdown menus.
const BYPASS_MAX_USAGE_CHECK
Flag for canDeleteTag().
Definition: ChangeTags.php:101
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
static undefineTag( $tag)
Update ctd_user_defined = 0 field in change_tag_def.
static canActivateTag( $tag, Authority $performer=null)
Is it OK to allow the user to activate this tag?
static purgeTagCacheAll()
Invalidates the short-term cache of defined tags used by the list*DefinedTags functions,...
static addTags( $tags, $rc_id=null, $rev_id=null, $log_id=null, $params=null, RecentChange $rc=null)
Add tags to a change given its rc_id, rev_id and/or log_id.
Definition: ChangeTags.php:317
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:679
static canDeleteTag( $tag, Authority $performer=null, int $flags=0)
Is it OK to allow the user to delete this tag?
const DEFINED_SOFTWARE_TAGS
A list of tags defined and used by MediaWiki itself.
Definition: ChangeTags.php:113
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.
static getDisplayTableName()
Get the name of the change_tag table to use for modifyDisplayQuery().
Definition: ChangeTags.php:980
static formatSummaryRow( $tags, $page, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:182
static tagUsageStatistics()
Returns a map of any tags used on the wiki to number of edits tagged with them, ordered descending by...
static tagShortDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's short description.
Definition: ChangeTags.php:242
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:544
static listDefinedTags()
Basically lists defined tags which count even if they aren't applied to anything.
static tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
Definition: ChangeTags.php:272
static createTagWithChecks( $tag, $reason, Authority $performer, $ignoreWarnings=false, array $logEntryTags=[])
Creates a tag by adding it to change_tag_def table.
static defineTag( $tag)
Set ctd_user_defined = 1 in change_tag_def without checking that the tag name is valid.
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:783
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.
const TAG_UNDO
The tagged edit is was performed via the "undo" link.
Definition: ChangeTags.php:73
static canAddTagsAccompanyingChange(array $tags, Authority $performer=null, $checkBlock=true)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
Definition: ChangeTags.php:625
const TAG_DESC_CHARACTER_LIMIT
Maximum length of a tag description in UTF-8 characters.
const TAG_BLANK
The tagged edit blanks the page (replaces it with the empty string).
Definition: ChangeTags.php:54
static isTagNameValid( $tag)
Is the tag name valid?
const REVERT_TAGS
List of tags which denote a revert of some sort.
Definition: ChangeTags.php:96
static deleteTagEverywhere( $tag)
Permanently removes all traces of a tag from the DB.
const MAX_DELETE_USES
Can't delete tags with more than this many uses.
Definition: ChangeTags.php:108
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:42
static bool $avoidReopeningTablesForTesting
If true, this class attempts to avoid reopening database tables within the same query,...
Definition: ChangeTags.php:137
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:709
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
static updateTags( $tagsToAdd, $tagsToRemove, &$rc_id=null, &$rev_id=null, &$log_id=null, $params=null, RecentChange $rc=null, UserIdentity $user=null)
Add and remove tags to/from a change given its rc_id, rev_id and/or log_id, without verifying that th...
Definition: ChangeTags.php:354
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
Base class for language-specific code.
Definition: Language.php:53
MediaWiki exception.
Definition: MWException.php:29
Class for creating new log entries and inserting them into the database.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:561
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Exception representing a failure to look up a row from a name table.
static plaintextParam( $plaintext)
Definition: Message.php:1287
Variant of the Message class.
Definition: RawMessage.php:35
Utility class for creating new RC entries.
static getMain()
Get the RequestContext object associated with the main request.
static suggestTarget( $target, array $ids)
Suggest a target for the revision deletion Optionally override this function.
static escapeClass( $class)
Given a value, escape it so that it can be used as a CSS class and return it.
Definition: Sanitizer.php:1106
static stripAllTags( $html)
Take a fragment of (potentially invalid) HTML and return a version with any tags removed,...
Definition: Sanitizer.php:1710
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:370
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:664
Class for generating HTML <select> or <datalist> elements.
Definition: XmlSelect.php:26
static listDropDownOptionsOoui( $options)
Convert options for a drop-down box into a format accepted by OOUI\DropdownInputWidget etc.
Definition: Xml.php:600
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition: Xml.php:283
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:134
Interface for objects which can provide a MediaWiki context on request.
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
getUser()
Returns the performer of the actions associated with this authority.
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Interface for objects representing user identity.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:39
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
$cache
Definition: mcc.php:33
const DB_REPLICA
Definition: defines.php:26
const DB_PRIMARY
Definition: defines.php:28
if(!isset( $args[0])) $lang