MediaWiki  master
ChangeTags.php
Go to the documentation of this file.
1 <?php
37 
38 class ChangeTags {
42  public const TAG_CONTENT_MODEL_CHANGE = 'mw-contentmodelchange';
47  public const TAG_NEW_REDIRECT = 'mw-new-redirect';
51  public const TAG_REMOVED_REDIRECT = 'mw-removed-redirect';
55  public const TAG_CHANGED_REDIRECT_TARGET = 'mw-changed-redirect-target';
59  public const TAG_BLANK = 'mw-blank';
63  public const TAG_REPLACE = 'mw-replace';
71  public const TAG_ROLLBACK = 'mw-rollback';
78  public const TAG_UNDO = 'mw-undo';
84  public const TAG_MANUAL_REVERT = 'mw-manual-revert';
92  public const TAG_REVERTED = 'mw-reverted';
96  public const TAG_SERVER_SIDE_UPLOAD = 'mw-server-side-upload';
97 
102 
106  public const BYPASS_MAX_USAGE_CHECK = 1;
107 
113  private const MAX_DELETE_USES = 5000;
114 
118  private const CHANGE_TAG = 'change_tag';
119 
120  public const DISPLAY_TABLE_ALIAS = 'changetagdisplay';
121 
130  public static function getSoftwareTags( $all = false ) {
131  return MediaWikiServices::getInstance()->getChangeTagsStore()->getSoftwareTags( $all );
132  }
133 
147  public static function formatSummaryRow( $tags, $unused, MessageLocalizer $localizer = null ) {
148  if ( $tags === '' || $tags === null ) {
149  return [ '', [] ];
150  }
151  if ( !$localizer ) {
152  $localizer = RequestContext::getMain();
153  }
154 
155  $classes = [];
156 
157  $tags = explode( ',', $tags );
158  $order = array_flip( MediaWikiServices::getInstance()->getChangeTagsStore()->listDefinedTags() );
159  usort( $tags, static function ( $a, $b ) use ( $order ) {
160  return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
161  } );
162 
163  $displayTags = [];
164  foreach ( $tags as $tag ) {
165  if ( $tag === '' ) {
166  continue;
167  }
168  $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
169  $description = self::tagDescription( $tag, $localizer );
170  if ( $description === false ) {
171  continue;
172  }
173  $displayTags[] = Xml::tags(
174  'span',
175  [ 'class' => 'mw-tag-marker ' .
176  Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
177  $description
178  );
179  }
180 
181  if ( !$displayTags ) {
182  return [ '', $classes ];
183  }
184 
185  $markers = $localizer->msg( 'tag-list-wrapper' )
186  ->numParams( count( $displayTags ) )
187  ->rawParams( implode( ' ', $displayTags ) )
188  ->parse();
189  $markers = Xml::tags( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
190 
191  return [ $markers, $classes ];
192  }
193 
207  public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
208  $msg = $context->msg( "tag-$tag" );
209  if ( !$msg->exists() ) {
210  // No such message
211  // Pass through ->msg(), even though it seems redundant, to avoid requesting
212  // the user's language from session-less entry points (T227233)
213  return $context->msg( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) );
214  }
215  if ( $msg->isDisabled() ) {
216  // The message exists but is disabled, hide the tag.
217  return false;
218  }
219 
220  // Message exists and isn't disabled, use it.
221  return $msg;
222  }
223 
237  public static function tagDescription( $tag, MessageLocalizer $context ) {
238  $msg = self::tagShortDescriptionMessage( $tag, $context );
239  return $msg ? $msg->parse() : false;
240  }
241 
254  public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
255  $msg = $context->msg( "tag-$tag-description" );
256  if ( !$msg->exists() ) {
257  return false;
258  }
259  if ( $msg->isDisabled() ) {
260  // The message exists but is disabled, hide the description.
261  return false;
262  }
263 
264  // Message exists and isn't disabled, use it.
265  return $msg;
266  }
267 
282  public static function addTags( $tags, $rc_id = null, $rev_id = null,
283  $log_id = null, $params = null, RecentChange $rc = null
284  ) {
285  return MediaWikiServices::getInstance()->getChangeTagsStore()->addTags(
286  $tags, $rc_id, $rev_id, $log_id, $params, $rc
287  );
288  }
289 
320  public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
321  &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
322  UserIdentity $user = null
323  ) {
324  return MediaWikiServices::getInstance()->getChangeTagsStore()->updateTags(
325  $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, $rc, $user
326  );
327  }
328 
341  public static function getTagsWithData(
342  IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null
343  ) {
344  return MediaWikiServices::getInstance()->getChangeTagsStore()->getTagsWithData( $db, $rc_id, $rev_id, $log_id );
345  }
346 
358  public static function getTags( IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
359  return MediaWikiServices::getInstance()->getChangeTagsStore()->getTags( $db, $rc_id, $rev_id, $log_id );
360  }
361 
372  protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
373  $lang = RequestContext::getMain()->getLanguage();
374  $tags = array_values( $tags );
375  $count = count( $tags );
376  $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
377  $lang->commaList( $tags ), $count );
378  $status->value = $tags;
379  return $status;
380  }
381 
396  public static function canAddTagsAccompanyingChange(
397  array $tags,
398  Authority $performer = null,
399  $checkBlock = true
400  ) {
401  $user = null;
402  $services = MediaWikiServices::getInstance();
403  if ( $performer !== null ) {
404  if ( !$performer->isAllowed( 'applychangetags' ) ) {
405  return Status::newFatal( 'tags-apply-no-permission' );
406  }
407 
408  if ( $checkBlock && $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
409  return Status::newFatal(
410  'tags-apply-blocked',
411  $performer->getUser()->getName()
412  );
413  }
414 
415  // ChangeTagsAllowedAdd hook still needs a full User object
416  $user = $services->getUserFactory()->newFromAuthority( $performer );
417  }
418 
419  // to be applied, a tag has to be explicitly defined
420  $allowedTags = $services->getChangeTagsStore()->listExplicitlyDefinedTags();
421  ( new HookRunner( $services->getHookContainer() ) )->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
422  $disallowedTags = array_diff( $tags, $allowedTags );
423  if ( $disallowedTags ) {
424  return self::restrictedTagError( 'tags-apply-not-allowed-one',
425  'tags-apply-not-allowed-multi', $disallowedTags );
426  }
427 
428  return Status::newGood();
429  }
430 
445  public static function canUpdateTags(
446  array $tagsToAdd,
447  array $tagsToRemove,
448  Authority $performer = null
449  ) {
450  if ( $performer !== null ) {
451  if ( !$performer->isDefinitelyAllowed( 'changetags' ) ) {
452  return Status::newFatal( 'tags-update-no-permission' );
453  }
454 
455  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
456  return Status::newFatal(
457  'tags-update-blocked',
458  $performer->getUser()->getName()
459  );
460  }
461  }
462 
463  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
464  if ( $tagsToAdd ) {
465  // to be added, a tag has to be explicitly defined
466  // @todo Allow extensions to define tags that can be applied by users...
467  $explicitlyDefinedTags = $changeTagStore->listExplicitlyDefinedTags();
468  $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
469  if ( $diff ) {
470  return self::restrictedTagError( 'tags-update-add-not-allowed-one',
471  'tags-update-add-not-allowed-multi', $diff );
472  }
473  }
474 
475  if ( $tagsToRemove ) {
476  // to be removed, a tag must not be defined by an extension, or equivalently it
477  // has to be either explicitly defined or not defined at all
478  // (assuming no edge case of a tag both explicitly-defined and extension-defined)
479  $softwareDefinedTags = $changeTagStore->listSoftwareDefinedTags();
480  $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
481  if ( $intersect ) {
482  return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
483  'tags-update-remove-not-allowed-multi', $intersect );
484  }
485  }
486 
487  return Status::newGood();
488  }
489 
520  public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
521  $rc_id, $rev_id, $log_id, $params, string $reason, Authority $performer
522  ) {
523  if ( !$tagsToAdd && !$tagsToRemove ) {
524  // no-op, don't bother
525  return Status::newGood( (object)[
526  'logId' => null,
527  'addedTags' => [],
528  'removedTags' => [],
529  ] );
530  }
531 
532  $tagsToAdd ??= [];
533  $tagsToRemove ??= [];
534 
535  // are we allowed to do this?
536  $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $performer );
537  if ( !$result->isOK() ) {
538  $result->value = null;
539  return $result;
540  }
541 
542  // basic rate limiting
543  $status = PermissionStatus::newEmpty();
544  if ( !$performer->authorizeAction( 'changetags', $status ) ) {
545  return Status::wrap( $status );
546  }
547 
548  // do it!
549  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
550  [ $tagsAdded, $tagsRemoved, $initialTags ] = $changeTagStore->updateTags( $tagsToAdd,
551  $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $performer->getUser() );
552  if ( !$tagsAdded && !$tagsRemoved ) {
553  // no-op, don't log it
554  return Status::newGood( (object)[
555  'logId' => null,
556  'addedTags' => [],
557  'removedTags' => [],
558  ] );
559  }
560 
561  // log it
562  $logEntry = new ManualLogEntry( 'tag', 'update' );
563  $logEntry->setPerformer( $performer->getUser() );
564  $logEntry->setComment( $reason );
565 
566  // find the appropriate target page
567  if ( $rev_id ) {
568  $revisionRecord = MediaWikiServices::getInstance()
569  ->getRevisionLookup()
570  ->getRevisionById( $rev_id );
571  if ( $revisionRecord ) {
572  $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
573  }
574  } elseif ( $log_id ) {
575  // This function is from revision deletion logic and has nothing to do with
576  // change tags, but it appears to be the only other place in core where we
577  // perform logged actions on log items.
578  $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
579  }
580 
581  if ( !$logEntry->getTarget() ) {
582  // target is required, so we have to set something
583  $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
584  }
585 
586  $logParams = [
587  '4::revid' => $rev_id,
588  '5::logid' => $log_id,
589  '6:list:tagsAdded' => $tagsAdded,
590  '7:number:tagsAddedCount' => count( $tagsAdded ),
591  '8:list:tagsRemoved' => $tagsRemoved,
592  '9:number:tagsRemovedCount' => count( $tagsRemoved ),
593  'initialTags' => $initialTags,
594  ];
595  $logEntry->setParameters( $logParams );
596  $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
597 
598  $dbw = MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->getPrimaryDatabase();
599  $logId = $logEntry->insert( $dbw );
600  // Only send this to UDP, not RC, similar to patrol events
601  $logEntry->publish( $logId, 'udp' );
602 
603  return Status::newGood( (object)[
604  'logId' => $logId,
605  'addedTags' => $tagsAdded,
606  'removedTags' => $tagsRemoved,
607  ] );
608  }
609 
631  public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
632  &$join_conds, &$options, $filter_tag = '', bool $exclude = false
633  ) {
634  MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQuery(
635  $tables,
636  $fields,
637  $conds,
638  $join_conds,
639  $options,
640  $filter_tag,
641  $exclude
642  );
643  }
644 
652  public static function getDisplayTableName() {
653  return self::CHANGE_TAG;
654  }
655 
664  public static function makeTagSummarySubquery( $tables ) {
665  return MediaWikiServices::getInstance()->getChangeTagsStore()->makeTagSummarySubquery( $tables );
666  }
667 
679  public static function buildTagFilterSelector(
680  $selected = '', $ooui = false, IContextSource $context = null
681  ) {
682  if ( !$context ) {
683  $context = RequestContext::getMain();
684  }
685 
686  $config = $context->getConfig();
687  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
688  if ( !$config->get( MainConfigNames::UseTagFilter ) ||
689  !count( $changeTagStore->listDefinedTags() ) ) {
690  return [];
691  }
692 
693  $tags = self::getChangeTagList( $context, $context->getLanguage() );
694  $autocomplete = [];
695  foreach ( $tags as $tagInfo ) {
696  $autocomplete[ $tagInfo['label'] ] = $tagInfo['name'];
697  }
698 
699  $data = [
700  Html::rawElement(
701  'label',
702  [ 'for' => 'tagfilter' ],
703  $context->msg( 'tag-filter' )->parse()
704  )
705  ];
706 
707  if ( $ooui ) {
708  $options = Xml::listDropDownOptionsOoui( $autocomplete );
709 
710  $data[] = new OOUI\ComboBoxInputWidget( [
711  'id' => 'tagfilter',
712  'name' => 'tagfilter',
713  'value' => $selected,
714  'classes' => 'mw-tagfilter-input',
715  'options' => $options,
716  ] );
717  } else {
718  $datalist = new XmlSelect( false, 'tagfilter-datalist' );
719  $datalist->setTagName( 'datalist' );
720  $datalist->addOptions( $autocomplete );
721 
722  $data[] = Xml::input(
723  'tagfilter',
724  20,
725  $selected,
726  [
727  'class' => 'mw-tagfilter-input mw-ui-input mw-ui-input-inline',
728  'id' => 'tagfilter',
729  'list' => 'tagfilter-datalist',
730  ]
731  ) . $datalist->getHTML();
732  }
733 
734  return $data;
735  }
736 
746  public static function defineTag( $tag ) {
747  MediaWikiServices::getInstance()->getChangeTagsStore()->defineTag( $tag );
748  }
749 
759  public static function canActivateTag( $tag, Authority $performer = null ) {
760  if ( $performer !== null ) {
761  if ( !$performer->isAllowed( 'managechangetags' ) ) {
762  return Status::newFatal( 'tags-manage-no-permission' );
763  }
764  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
765  return Status::newFatal(
766  'tags-manage-blocked',
767  $performer->getUser()->getName()
768  );
769  }
770  }
771 
772  // defined tags cannot be activated (a defined tag is either extension-
773  // defined, in which case the extension chooses whether or not to active it;
774  // or user-defined, in which case it is considered active)
775  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
776  $definedTags = $changeTagStore->listDefinedTags();
777  if ( in_array( $tag, $definedTags ) ) {
778  return Status::newFatal( 'tags-activate-not-allowed', $tag );
779  }
780 
781  // non-existing tags cannot be activated
782  if ( !isset( $changeTagStore->tagUsageStatistics()[$tag] ) ) { // we already know the tag is undefined
783  return Status::newFatal( 'tags-activate-not-found', $tag );
784  }
785 
786  return Status::newGood();
787  }
788 
806  public static function activateTagWithChecks( string $tag, string $reason, Authority $performer,
807  bool $ignoreWarnings = false, array $logEntryTags = []
808  ) {
809  // are we allowed to do this?
810  $result = self::canActivateTag( $tag, $performer );
811  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
812  $result->value = null;
813  return $result;
814  }
815  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
816 
817  $changeTagStore->defineTag( $tag );
818 
819  $logId = $changeTagStore->logTagManagementAction( 'activate', $tag, $reason, $performer->getUser(),
820  null, $logEntryTags );
821 
822  return Status::newGood( $logId );
823  }
824 
834  public static function canDeactivateTag( $tag, Authority $performer = null ) {
835  if ( $performer !== null ) {
836  if ( !$performer->isAllowed( 'managechangetags' ) ) {
837  return Status::newFatal( 'tags-manage-no-permission' );
838  }
839  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
840  return Status::newFatal(
841  'tags-manage-blocked',
842  $performer->getUser()->getName()
843  );
844  }
845  }
846 
847  // only explicitly-defined tags can be deactivated
848  $explicitlyDefinedTags = MediaWikiServices::getInstance()->getChangeTagsStore()->listExplicitlyDefinedTags();
849  if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
850  return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
851  }
852  return Status::newGood();
853  }
854 
872  public static function deactivateTagWithChecks( string $tag, string $reason, Authority $performer,
873  bool $ignoreWarnings = false, array $logEntryTags = []
874  ) {
875  // are we allowed to do this?
876  $result = self::canDeactivateTag( $tag, $performer );
877  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
878  $result->value = null;
879  return $result;
880  }
881  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
882 
883  $changeTagStore->undefineTag( $tag );
884 
885  $logId = $changeTagStore->logTagManagementAction( 'deactivate', $tag, $reason,
886  $performer->getUser(), null, $logEntryTags );
887 
888  return Status::newGood( $logId );
889  }
890 
898  public static function isTagNameValid( $tag ) {
899  // no empty tags
900  if ( $tag === '' ) {
901  return Status::newFatal( 'tags-create-no-name' );
902  }
903 
904  // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
905  // pipe (used as a delimiter between multiple tags in
906  // SpecialRecentchanges and friends), or slashes (would break tag description messages in
907  // MediaWiki namespace)
908  if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
909  || strpos( $tag, '/' ) !== false ) {
910  return Status::newFatal( 'tags-create-invalid-chars' );
911  }
912 
913  // could the MediaWiki namespace description messages be created?
914  $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
915  if ( $title === null ) {
916  return Status::newFatal( 'tags-create-invalid-title-chars' );
917  }
918 
919  return Status::newGood();
920  }
921 
934  public static function canCreateTag( $tag, Authority $performer = null ) {
935  $user = null;
936  $services = MediaWikiServices::getInstance();
937  if ( $performer !== null ) {
938  if ( !$performer->isAllowed( 'managechangetags' ) ) {
939  return Status::newFatal( 'tags-manage-no-permission' );
940  }
941  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
942  return Status::newFatal(
943  'tags-manage-blocked',
944  $performer->getUser()->getName()
945  );
946  }
947  // ChangeTagCanCreate hook still needs a full User object
948  $user = $services->getUserFactory()->newFromAuthority( $performer );
949  }
950 
951  $status = self::isTagNameValid( $tag );
952  if ( !$status->isGood() ) {
953  return $status;
954  }
955 
956  // does the tag already exist?
957  $changeTagStore = $services->getChangeTagsStore();
958  if (
959  isset( $changeTagStore->tagUsageStatistics()[$tag] ) ||
960  in_array( $tag, $changeTagStore->listDefinedTags() )
961  ) {
962  return Status::newFatal( 'tags-create-already-exists', $tag );
963  }
964 
965  // check with hooks
966  $canCreateResult = Status::newGood();
967  ( new HookRunner( $services->getHookContainer() ) )->onChangeTagCanCreate( $tag, $user, $canCreateResult );
968  return $canCreateResult;
969  }
970 
990  public static function createTagWithChecks( string $tag, string $reason, Authority $performer,
991  bool $ignoreWarnings = false, array $logEntryTags = []
992  ) {
993  // are we allowed to do this?
994  $result = self::canCreateTag( $tag, $performer );
995  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
996  $result->value = null;
997  return $result;
998  }
999 
1000  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1001  $changeTagStore->defineTag( $tag );
1002  $logId = $changeTagStore->logTagManagementAction( 'create', $tag, $reason,
1003  $performer->getUser(), null, $logEntryTags );
1004 
1005  return Status::newGood( $logId );
1006  }
1007 
1021  public static function deleteTagEverywhere( $tag ) {
1022  return MediaWikiServices::getInstance()->getChangeTagsStore()->deleteTagEverywhere( $tag );
1023  }
1024 
1037  public static function canDeleteTag( $tag, Authority $performer = null, int $flags = 0 ) {
1038  $user = null;
1039  $services = MediaWikiServices::getInstance();
1040  if ( $performer !== null ) {
1041  if ( !$performer->isAllowed( 'deletechangetags' ) ) {
1042  return Status::newFatal( 'tags-delete-no-permission' );
1043  }
1044  if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1045  return Status::newFatal(
1046  'tags-manage-blocked',
1047  $performer->getUser()->getName()
1048  );
1049  }
1050  // ChangeTagCanDelete hook still needs a full User object
1051  $user = $services->getUserFactory()->newFromAuthority( $performer );
1052  }
1053 
1054  $changeTagStore = $services->getChangeTagsStore();
1055  $tagUsage = $changeTagStore->tagUsageStatistics();
1056  if (
1057  !isset( $tagUsage[$tag] ) &&
1058  !in_array( $tag, $changeTagStore->listDefinedTags() )
1059  ) {
1060  return Status::newFatal( 'tags-delete-not-found', $tag );
1061  }
1062 
1063  if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1064  isset( $tagUsage[$tag] ) &&
1065  $tagUsage[$tag] > self::MAX_DELETE_USES
1066  ) {
1067  return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1068  }
1069 
1070  $softwareDefined = $changeTagStore->listSoftwareDefinedTags();
1071  if ( in_array( $tag, $softwareDefined ) ) {
1072  // extension-defined tags can't be deleted unless the extension
1073  // specifically allows it
1074  $status = Status::newFatal( 'tags-delete-not-allowed' );
1075  } else {
1076  // user-defined tags are deletable unless otherwise specified
1077  $status = Status::newGood();
1078  }
1079 
1080  ( new HookRunner( $services->getHookContainer() ) )->onChangeTagCanDelete( $tag, $user, $status );
1081  return $status;
1082  }
1083 
1101  public static function deleteTagWithChecks( string $tag, string $reason, Authority $performer,
1102  bool $ignoreWarnings = false, array $logEntryTags = []
1103  ) {
1104  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1105  // are we allowed to do this?
1106  $result = self::canDeleteTag( $tag, $performer );
1107  if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1108  $result->value = null;
1109  return $result;
1110  }
1111 
1112  // store the tag usage statistics
1113  $hitcount = $changeTagStore->tagUsageStatistics()[$tag] ?? 0;
1114 
1115  // do it!
1116  $deleteResult = $changeTagStore->deleteTagEverywhere( $tag );
1117  if ( !$deleteResult->isOK() ) {
1118  return $deleteResult;
1119  }
1120 
1121  // log it
1122  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1123  $logId = $changeTagStore->logTagManagementAction( 'delete', $tag, $reason, $performer->getUser(),
1124  $hitcount, $logEntryTags );
1125 
1126  $deleteResult->value = $logId;
1127  return $deleteResult;
1128  }
1129 
1137  public static function listSoftwareActivatedTags() {
1138  return MediaWikiServices::getInstance()->getChangeTagsStore()->listSoftwareActivatedTags();
1139  }
1140 
1149  public static function listDefinedTags() {
1150  return MediaWikiServices::getInstance()->getChangeTagsStore()->listDefinedTags();
1151  }
1152 
1162  public static function listExplicitlyDefinedTags() {
1163  return MediaWikiServices::getInstance()->getChangeTagsStore()->listExplicitlyDefinedTags();
1164  }
1165 
1176  public static function listSoftwareDefinedTags() {
1177  return MediaWikiServices::getInstance()->getChangeTagsStore()->listSoftwareDefinedTags();
1178  }
1179 
1186  public static function purgeTagCacheAll() {
1187  MediaWikiServices::getInstance()->getChangeTagsStore()->purgeTagCacheAll();
1188  }
1189 
1198  public static function tagUsageStatistics() {
1199  return MediaWikiServices::getInstance()->getChangeTagsStore()->tagUsageStatistics();
1200  }
1201 
1206  private const TAG_DESC_CHARACTER_LIMIT = 120;
1207 
1232  public static function getChangeTagListSummary( MessageLocalizer $localizer, Language $lang ) {
1233  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1234  return $cache->getWithSetCallback(
1235  $cache->makeKey( 'tags-list-summary', $lang->getCode() ),
1236  WANObjectCache::TTL_DAY,
1237  static function ( $oldValue, &$ttl, array &$setOpts ) use ( $localizer ) {
1238  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1239  $tagHitCounts = $changeTagStore->tagUsageStatistics();
1240 
1241  $result = [];
1242  // Only list tags that are still actively defined
1243  foreach ( $changeTagStore->listDefinedTags() as $tagName ) {
1244  // Only list tags with more than 0 hits
1245  $hits = $tagHitCounts[$tagName] ?? 0;
1246  if ( $hits <= 0 ) {
1247  continue;
1248  }
1249 
1250  $labelMsg = self::tagShortDescriptionMessage( $tagName, $localizer );
1251  $descriptionMsg = self::tagLongDescriptionMessage( $tagName, $localizer );
1252  // Don't cache the message object, use the correct MessageLocalizer to parse later.
1253  $result[] = [
1254  'name' => $tagName,
1255  'labelMsg' => (bool)$labelMsg,
1256  'label' => $labelMsg ? $labelMsg->plain() : $tagName,
1257  'descriptionMsg' => (bool)$descriptionMsg,
1258  'description' => $descriptionMsg ? $descriptionMsg->plain() : '',
1259  'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
1260  ];
1261  }
1262  return $result;
1263  }
1264  );
1265  }
1266 
1279  public static function getChangeTagList( MessageLocalizer $localizer, Language $lang ) {
1280  $tags = self::getChangeTagListSummary( $localizer, $lang );
1281  foreach ( $tags as &$tagInfo ) {
1282  if ( $tagInfo['labelMsg'] ) {
1283  // Use localizer with the correct page title to parse plain message from the cache.
1284  $labelMsg = new RawMessage( $tagInfo['label'] );
1285  $tagInfo['label'] = Sanitizer::stripAllTags( $localizer->msg( $labelMsg )->parse() );
1286  } else {
1287  $tagInfo['label'] = $localizer->msg( 'tag-hidden', $tagInfo['name'] )->text();
1288  }
1289  if ( $tagInfo['descriptionMsg'] ) {
1290  $descriptionMsg = new RawMessage( $tagInfo['description'] );
1291  $tagInfo['description'] = $lang->truncateForVisual(
1292  Sanitizer::stripAllTags( $localizer->msg( $descriptionMsg )->parse() ),
1293  self::TAG_DESC_CHARACTER_LIMIT
1294  );
1295  }
1296  unset( $tagInfo['labelMsg'] );
1297  unset( $tagInfo['descriptionMsg'] );
1298  }
1299 
1300  // Instead of sorting by hit count (disabled for now), sort by display name
1301  usort( $tags, static function ( $a, $b ) {
1302  return strcasecmp( $a['label'], $b['label'] );
1303  } );
1304  return $tags;
1305  }
1306 
1321  public static function showTagEditingUI( Authority $performer ) {
1322  $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1323  return $performer->isAllowed( 'changetags' ) && (bool)$changeTagStore->listExplicitlyDefinedTags();
1324  }
1325 }
const NS_MEDIAWIKI
Definition: Defines.php:72
static getTagsWithData(IReadableDatabase $db, $rc_id=null, $rev_id=null, $log_id=null)
Return all the tags associated with the given recent change ID, revision ID, and/or log entry ID,...
Definition: ChangeTags.php:341
const TAG_MANUAL_REVERT
The tagged edit restores the page to an earlier revision.
Definition: ChangeTags.php:84
const TAG_SERVER_SIDE_UPLOAD
This tagged edit was performed while importing media files using the importImages....
Definition: ChangeTags.php:96
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
Definition: ChangeTags.php:631
const TAG_REMOVED_REDIRECT
The tagged edit turns a redirect page into a non-redirect.
Definition: ChangeTags.php:51
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?
Definition: ChangeTags.php:834
static buildTagFilterSelector( $selected='', $ooui=false, IContextSource $context=null)
Build a text box to select a change tag.
Definition: ChangeTags.php:679
static canCreateTag( $tag, Authority $performer=null)
Is it OK to allow the user to create this tag?
Definition: ChangeTags.php:934
const TAG_REPLACE
The tagged edit removes more than 90% of the content of the page.
Definition: ChangeTags.php:63
const TAG_CONTENT_MODEL_CHANGE
The tagged edit changes the content model of the page.
Definition: ChangeTags.php:42
static updateTagsWithChecks( $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, string $reason, Authority $performer)
Adds and/or removes tags to/from a given change, checking whether it is allowed first,...
Definition: ChangeTags.php:520
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
Definition: ChangeTags.php:254
static deactivateTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Deactivates a tag, checking whether it is allowed first, and adding a log entry afterwards.
Definition: ChangeTags.php:872
static showTagEditingUI(Authority $performer)
Indicate whether change tag editing UI is relevant.
static deleteTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Deletes a tag, checking whether it is allowed first, and adding a log entry afterwards.
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
Definition: ChangeTags.php:130
const TAG_CHANGED_REDIRECT_TARGET
The tagged edit changes the target of a redirect page.
Definition: ChangeTags.php:55
const TAG_REVERTED
The tagged edit is reverted by a subsequent edit (which is tagged by one of TAG_ROLLBACK,...
Definition: ChangeTags.php:92
static restrictedTagError( $msgOne, $msgMulti, $tags)
Helper function to generate a fatal status with a 'not-allowed' type error.
Definition: ChangeTags.php:372
const TAG_ROLLBACK
The tagged edit is a rollback (undoes the previous edit and all immediately preceding edits by the sa...
Definition: ChangeTags.php:71
static getChangeTagListSummary(MessageLocalizer $localizer, Language $lang)
Get information about change tags, without parsing messages, for tag filter dropdown menus.
static makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
Definition: ChangeTags.php:664
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:106
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
static canActivateTag( $tag, Authority $performer=null)
Is it OK to allow the user to activate this tag?
Definition: ChangeTags.php:759
static formatSummaryRow( $tags, $unused, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:147
const DISPLAY_TABLE_ALIAS
Definition: ChangeTags.php:120
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:282
static canDeleteTag( $tag, Authority $performer=null, int $flags=0)
Is it OK to allow the user to delete this tag?
static getDisplayTableName()
Get the name of the change_tag table to use for modifyDisplayQuery().
Definition: ChangeTags.php:652
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:207
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:237
static defineTag( $tag)
Set ctd_user_defined = 1 in change_tag_def without checking that the tag name is valid.
Definition: ChangeTags.php:746
static createTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Creates a tag by adding it to change_tag_def table.
Definition: ChangeTags.php:990
static getTags(IReadableDatabase $db, $rc_id=null, $rev_id=null, $log_id=null)
Return all the tags associated with the given recent change ID, revision ID, and/or log entry ID.
Definition: ChangeTags.php:358
const TAG_UNDO
The tagged edit is was performed via the "undo" link.
Definition: ChangeTags.php:78
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:396
const TAG_BLANK
The tagged edit blanks the page (replaces it with the empty string).
Definition: ChangeTags.php:59
static isTagNameValid( $tag)
Is the tag name valid?
Definition: ChangeTags.php:898
const REVERT_TAGS
List of tags which denote a revert of some sort.
Definition: ChangeTags.php:101
static activateTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Activates a tag, checking whether it is allowed first, and adding a log entry afterwards.
Definition: ChangeTags.php:806
static deleteTagEverywhere( $tag)
Permanently removes all traces of a tag from the DB.
const TAG_NEW_REDIRECT
The tagged edit creates a new redirect (either by creating a new page or turning an existing page int...
Definition: ChangeTags.php:47
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:445
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:320
Base class for language-specific code.
Definition: Language.php:63
truncateForVisual( $string, $length, $ellipsis='...', $adjustLength=true)
Truncate a string to a specified number of characters, appending an optional string (e....
Definition: Language.php:3416
getCode()
Get the internal language code for this language object.
Definition: Language.php:4024
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:568
This class is a collection of static functions that serve two purposes:
Definition: Html.php:57
Variant of the Message class.
Definition: RawMessage.php:40
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
HTML sanitizer for MediaWiki.
Definition: Sanitizer.php:46
A StatusValue for permission errors.
Parent class for all special pages.
Definition: SpecialPage.php:65
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
Represents a title within MediaWiki.
Definition: Title.php:76
static plaintextParam( $plaintext)
Definition: Message.php:1275
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.
Class for generating HTML <select> or <datalist> elements.
Definition: XmlSelect.php:28
static listDropDownOptionsOoui( $options)
Convert options for a drop-down box into a format accepted by OOUI\DropdownInputWidget etc.
Definition: Xml.php:604
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition: Xml.php:287
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:141
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
authorizeAction(string $action, PermissionStatus $status=null)
Authorize an action.
getUser()
Returns the performer of the actions associated with this authority.
isAllowed(string $permission, PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
Interface for objects representing user identity.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
A database connection without write operations.