MediaWiki master
ChangeTags.php
Go to the documentation of this file.
1<?php
39
44 public const TAG_CONTENT_MODEL_CHANGE = 'mw-contentmodelchange';
49 public const TAG_NEW_REDIRECT = 'mw-new-redirect';
53 public const TAG_REMOVED_REDIRECT = 'mw-removed-redirect';
57 public const TAG_CHANGED_REDIRECT_TARGET = 'mw-changed-redirect-target';
61 public const TAG_BLANK = 'mw-blank';
65 public const TAG_REPLACE = 'mw-replace';
73 public const TAG_ROLLBACK = 'mw-rollback';
80 public const TAG_UNDO = 'mw-undo';
86 public const TAG_MANUAL_REVERT = 'mw-manual-revert';
94 public const TAG_REVERTED = 'mw-reverted';
98 public const TAG_SERVER_SIDE_UPLOAD = 'mw-server-side-upload';
99
104
108 public const BYPASS_MAX_USAGE_CHECK = 1;
109
115 private const MAX_DELETE_USES = 5000;
116
120 private const CHANGE_TAG = 'change_tag';
121
122 public const DISPLAY_TABLE_ALIAS = 'changetagdisplay';
123
132 public static function getSoftwareTags( $all = false ) {
133 return MediaWikiServices::getInstance()->getChangeTagsStore()->getSoftwareTags( $all );
134 }
135
149 public static function formatSummaryRow( $tags, $unused, MessageLocalizer $localizer = null ) {
150 if ( $tags === '' || $tags === null ) {
151 return [ '', [] ];
152 }
153 if ( !$localizer ) {
154 $localizer = RequestContext::getMain();
155 }
156
157 $classes = [];
158
159 $tags = explode( ',', $tags );
160 $order = array_flip( MediaWikiServices::getInstance()->getChangeTagsStore()->listDefinedTags() );
161 usort( $tags, static function ( $a, $b ) use ( $order ) {
162 return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
163 } );
164
165 $displayTags = [];
166 foreach ( $tags as $tag ) {
167 if ( $tag === '' ) {
168 continue;
169 }
170 $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
171 $description = self::tagDescription( $tag, $localizer );
172 if ( $description === false ) {
173 continue;
174 }
175 $displayTags[] = Html::rawElement(
176 'span',
177 [ 'class' => 'mw-tag-marker ' .
178 Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
179 $description
180 );
181 }
182
183 if ( !$displayTags ) {
184 return [ '', $classes ];
185 }
186
187 $markers = $localizer->msg( 'tag-list-wrapper' )
188 ->numParams( count( $displayTags ) )
189 ->rawParams( implode( ' ', $displayTags ) )
190 ->parse();
191 $markers = Html::rawElement( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
192
193 return [ $markers, $classes ];
194 }
195
209 public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
210 $msg = $context->msg( "tag-$tag" );
211 if ( !$msg->exists() ) {
212 // No such message
213 // Pass through ->msg(), even though it seems redundant, to avoid requesting
214 // the user's language from session-less entry points (T227233)
215 return $context->msg( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) );
216 }
217 if ( $msg->isDisabled() ) {
218 // The message exists but is disabled, hide the tag.
219 return false;
220 }
221
222 // Message exists and isn't disabled, use it.
223 return $msg;
224 }
225
239 public static function tagDescription( $tag, MessageLocalizer $context ) {
240 $msg = self::tagShortDescriptionMessage( $tag, $context );
241 return $msg ? $msg->parse() : false;
242 }
243
256 public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
257 $msg = $context->msg( "tag-$tag-description" );
258 if ( !$msg->exists() ) {
259 return false;
260 }
261 if ( $msg->isDisabled() ) {
262 // The message exists but is disabled, hide the description.
263 return false;
264 }
265
266 // Message exists and isn't disabled, use it.
267 return $msg;
268 }
269
284 public static function addTags( $tags, $rc_id = null, $rev_id = null,
285 $log_id = null, $params = null, RecentChange $rc = null
286 ) {
287 return MediaWikiServices::getInstance()->getChangeTagsStore()->addTags(
288 $tags, $rc_id, $rev_id, $log_id, $params, $rc
289 );
290 }
291
322 public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
323 &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
324 UserIdentity $user = null
325 ) {
326 return MediaWikiServices::getInstance()->getChangeTagsStore()->updateTags(
327 $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, $rc, $user
328 );
329 }
330
343 public static function getTagsWithData(
344 IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null
345 ) {
346 return MediaWikiServices::getInstance()->getChangeTagsStore()->getTagsWithData( $db, $rc_id, $rev_id, $log_id );
347 }
348
360 public static function getTags( IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
361 return MediaWikiServices::getInstance()->getChangeTagsStore()->getTags( $db, $rc_id, $rev_id, $log_id );
362 }
363
374 protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
375 $lang = RequestContext::getMain()->getLanguage();
376 $tags = array_values( $tags );
377 $count = count( $tags );
378 $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
379 $lang->commaList( $tags ), $count );
380 $status->value = $tags;
381 return $status;
382 }
383
398 public static function canAddTagsAccompanyingChange(
399 array $tags,
400 Authority $performer = null,
401 $checkBlock = true
402 ) {
403 $user = null;
404 $services = MediaWikiServices::getInstance();
405 if ( $performer !== null ) {
406 if ( !$performer->isAllowed( 'applychangetags' ) ) {
407 return Status::newFatal( 'tags-apply-no-permission' );
408 }
409
410 if ( $checkBlock && $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
411 return Status::newFatal(
412 'tags-apply-blocked',
413 $performer->getUser()->getName()
414 );
415 }
416
417 // ChangeTagsAllowedAdd hook still needs a full User object
418 $user = $services->getUserFactory()->newFromAuthority( $performer );
419 }
420
421 // to be applied, a tag has to be explicitly defined
422 $allowedTags = $services->getChangeTagsStore()->listExplicitlyDefinedTags();
423 ( new HookRunner( $services->getHookContainer() ) )->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
424 $disallowedTags = array_diff( $tags, $allowedTags );
425 if ( $disallowedTags ) {
426 return self::restrictedTagError( 'tags-apply-not-allowed-one',
427 'tags-apply-not-allowed-multi', $disallowedTags );
428 }
429
430 return Status::newGood();
431 }
432
447 public static function canUpdateTags(
448 array $tagsToAdd,
449 array $tagsToRemove,
450 Authority $performer = null
451 ) {
452 if ( $performer !== null ) {
453 if ( !$performer->isDefinitelyAllowed( 'changetags' ) ) {
454 return Status::newFatal( 'tags-update-no-permission' );
455 }
456
457 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
458 return Status::newFatal(
459 'tags-update-blocked',
460 $performer->getUser()->getName()
461 );
462 }
463 }
464
465 $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
466 if ( $tagsToAdd ) {
467 // to be added, a tag has to be explicitly defined
468 // @todo Allow extensions to define tags that can be applied by users...
469 $explicitlyDefinedTags = $changeTagStore->listExplicitlyDefinedTags();
470 $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
471 if ( $diff ) {
472 return self::restrictedTagError( 'tags-update-add-not-allowed-one',
473 'tags-update-add-not-allowed-multi', $diff );
474 }
475 }
476
477 if ( $tagsToRemove ) {
478 // to be removed, a tag must not be defined by an extension, or equivalently it
479 // has to be either explicitly defined or not defined at all
480 // (assuming no edge case of a tag both explicitly-defined and extension-defined)
481 $softwareDefinedTags = $changeTagStore->listSoftwareDefinedTags();
482 $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
483 if ( $intersect ) {
484 return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
485 'tags-update-remove-not-allowed-multi', $intersect );
486 }
487 }
488
489 return Status::newGood();
490 }
491
522 public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
523 $rc_id, $rev_id, $log_id, $params, string $reason, Authority $performer
524 ) {
525 if ( !$tagsToAdd && !$tagsToRemove ) {
526 // no-op, don't bother
527 return Status::newGood( (object)[
528 'logId' => null,
529 'addedTags' => [],
530 'removedTags' => [],
531 ] );
532 }
533
534 $tagsToAdd ??= [];
535 $tagsToRemove ??= [];
536
537 // are we allowed to do this?
538 $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $performer );
539 if ( !$result->isOK() ) {
540 $result->value = null;
541 return $result;
542 }
543
544 // basic rate limiting
545 $status = PermissionStatus::newEmpty();
546 if ( !$performer->authorizeAction( 'changetags', $status ) ) {
547 return Status::wrap( $status );
548 }
549
550 // do it!
551 $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
552 [ $tagsAdded, $tagsRemoved, $initialTags ] = $changeTagStore->updateTags( $tagsToAdd,
553 $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $performer->getUser() );
554 if ( !$tagsAdded && !$tagsRemoved ) {
555 // no-op, don't log it
556 return Status::newGood( (object)[
557 'logId' => null,
558 'addedTags' => [],
559 'removedTags' => [],
560 ] );
561 }
562
563 // log it
564 $logEntry = new ManualLogEntry( 'tag', 'update' );
565 $logEntry->setPerformer( $performer->getUser() );
566 $logEntry->setComment( $reason );
567
568 // find the appropriate target page
569 if ( $rev_id ) {
570 $revisionRecord = MediaWikiServices::getInstance()
571 ->getRevisionLookup()
572 ->getRevisionById( $rev_id );
573 if ( $revisionRecord ) {
574 $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
575 }
576 } elseif ( $log_id ) {
577 // This function is from revision deletion logic and has nothing to do with
578 // change tags, but it appears to be the only other place in core where we
579 // perform logged actions on log items.
580 $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
581 }
582
583 if ( !$logEntry->getTarget() ) {
584 // target is required, so we have to set something
585 $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
586 }
587
588 $logParams = [
589 '4::revid' => $rev_id,
590 '5::logid' => $log_id,
591 '6:list:tagsAdded' => $tagsAdded,
592 '7:number:tagsAddedCount' => count( $tagsAdded ),
593 '8:list:tagsRemoved' => $tagsRemoved,
594 '9:number:tagsRemovedCount' => count( $tagsRemoved ),
595 'initialTags' => $initialTags,
596 ];
597 $logEntry->setParameters( $logParams );
598 $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
599
600 $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
601 $logId = $logEntry->insert( $dbw );
602 // Only send this to UDP, not RC, similar to patrol events
603 $logEntry->publish( $logId, 'udp' );
604
605 return Status::newGood( (object)[
606 'logId' => $logId,
607 'addedTags' => $tagsAdded,
608 'removedTags' => $tagsRemoved,
609 ] );
610 }
611
633 public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
634 &$join_conds, &$options, $filter_tag = '', bool $exclude = false
635 ) {
636 MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQuery(
637 $tables,
638 $fields,
639 $conds,
640 $join_conds,
641 $options,
642 $filter_tag,
643 $exclude
644 );
645 }
646
656 public static function getDisplayTableName() {
657 return self::CHANGE_TAG;
658 }
659
668 public static function makeTagSummarySubquery( $tables ) {
669 return MediaWikiServices::getInstance()->getChangeTagsStore()->makeTagSummarySubquery( $tables );
670 }
671
683 public static function buildTagFilterSelector(
684 $selected = '', $ooui = false, IContextSource $context = null
685 ) {
686 if ( !$context ) {
687 $context = RequestContext::getMain();
688 }
689
690 $config = $context->getConfig();
691 $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
692 if ( !$config->get( MainConfigNames::UseTagFilter ) ||
693 !count( $changeTagStore->listDefinedTags() ) ) {
694 return [];
695 }
696
697 $tags = self::getChangeTagList( $context, $context->getLanguage() );
698 $autocomplete = [];
699 foreach ( $tags as $tagInfo ) {
700 $autocomplete[ $tagInfo['label'] ] = $tagInfo['name'];
701 }
702
703 $data = [
704 Html::rawElement(
705 'label',
706 [ 'for' => 'tagfilter' ],
707 $context->msg( 'tag-filter' )->parse()
708 )
709 ];
710
711 if ( $ooui ) {
712 $options = Html::listDropdownOptionsOoui( $autocomplete );
713
714 $data[] = new OOUI\ComboBoxInputWidget( [
715 'id' => 'tagfilter',
716 'name' => 'tagfilter',
717 'value' => $selected,
718 'classes' => 'mw-tagfilter-input',
719 'options' => $options,
720 ] );
721 } else {
722 $datalist = new XmlSelect( false, 'tagfilter-datalist' );
723 $datalist->setTagName( 'datalist' );
724 $datalist->addOptions( $autocomplete );
725
726 $data[] = Html::input(
727 'tagfilter',
728 $selected,
729 'text',
730 [
731 'class' => [ 'mw-tagfilter-input', 'mw-ui-input', 'mw-ui-input-inline' ],
732 'size' => 20,
733 'id' => 'tagfilter',
734 'list' => 'tagfilter-datalist',
735 ]
736 ) . $datalist->getHTML();
737 }
738
739 return $data;
740 }
741
751 public static function defineTag( $tag ) {
752 MediaWikiServices::getInstance()->getChangeTagsStore()->defineTag( $tag );
753 }
754
764 public static function canActivateTag( $tag, Authority $performer = null ) {
765 if ( $performer !== null ) {
766 if ( !$performer->isAllowed( 'managechangetags' ) ) {
767 return Status::newFatal( 'tags-manage-no-permission' );
768 }
769 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
770 return Status::newFatal(
771 'tags-manage-blocked',
772 $performer->getUser()->getName()
773 );
774 }
775 }
776
777 // defined tags cannot be activated (a defined tag is either extension-
778 // defined, in which case the extension chooses whether or not to active it;
779 // or user-defined, in which case it is considered active)
780 $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
781 $definedTags = $changeTagStore->listDefinedTags();
782 if ( in_array( $tag, $definedTags ) ) {
783 return Status::newFatal( 'tags-activate-not-allowed', $tag );
784 }
785
786 // non-existing tags cannot be activated
787 if ( !isset( $changeTagStore->tagUsageStatistics()[$tag] ) ) { // we already know the tag is undefined
788 return Status::newFatal( 'tags-activate-not-found', $tag );
789 }
790
791 return Status::newGood();
792 }
793
811 public static function activateTagWithChecks( string $tag, string $reason, Authority $performer,
812 bool $ignoreWarnings = false, array $logEntryTags = []
813 ) {
814 // are we allowed to do this?
815 $result = self::canActivateTag( $tag, $performer );
816 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
817 $result->value = null;
818 return $result;
819 }
820 $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
821
822 $changeTagStore->defineTag( $tag );
823
824 $logId = $changeTagStore->logTagManagementAction( 'activate', $tag, $reason, $performer->getUser(),
825 null, $logEntryTags );
826
827 return Status::newGood( $logId );
828 }
829
839 public static function canDeactivateTag( $tag, Authority $performer = null ) {
840 if ( $performer !== null ) {
841 if ( !$performer->isAllowed( 'managechangetags' ) ) {
842 return Status::newFatal( 'tags-manage-no-permission' );
843 }
844 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
845 return Status::newFatal(
846 'tags-manage-blocked',
847 $performer->getUser()->getName()
848 );
849 }
850 }
851
852 // only explicitly-defined tags can be deactivated
853 $explicitlyDefinedTags = MediaWikiServices::getInstance()->getChangeTagsStore()->listExplicitlyDefinedTags();
854 if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
855 return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
856 }
857 return Status::newGood();
858 }
859
877 public static function deactivateTagWithChecks( string $tag, string $reason, Authority $performer,
878 bool $ignoreWarnings = false, array $logEntryTags = []
879 ) {
880 // are we allowed to do this?
881 $result = self::canDeactivateTag( $tag, $performer );
882 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
883 $result->value = null;
884 return $result;
885 }
886 $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
887
888 $changeTagStore->undefineTag( $tag );
889
890 $logId = $changeTagStore->logTagManagementAction( 'deactivate', $tag, $reason,
891 $performer->getUser(), null, $logEntryTags );
892
893 return Status::newGood( $logId );
894 }
895
903 public static function isTagNameValid( $tag ) {
904 // no empty tags
905 if ( $tag === '' ) {
906 return Status::newFatal( 'tags-create-no-name' );
907 }
908
909 // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
910 // pipe (used as a delimiter between multiple tags in
911 // SpecialRecentchanges and friends), or slashes (would break tag description messages in
912 // MediaWiki namespace)
913 if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
914 || strpos( $tag, '/' ) !== false ) {
915 return Status::newFatal( 'tags-create-invalid-chars' );
916 }
917
918 // could the MediaWiki namespace description messages be created?
919 $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
920 if ( $title === null ) {
921 return Status::newFatal( 'tags-create-invalid-title-chars' );
922 }
923
924 return Status::newGood();
925 }
926
939 public static function canCreateTag( $tag, Authority $performer = null ) {
940 $user = null;
941 $services = MediaWikiServices::getInstance();
942 if ( $performer !== null ) {
943 if ( !$performer->isAllowed( 'managechangetags' ) ) {
944 return Status::newFatal( 'tags-manage-no-permission' );
945 }
946 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
947 return Status::newFatal(
948 'tags-manage-blocked',
949 $performer->getUser()->getName()
950 );
951 }
952 // ChangeTagCanCreate hook still needs a full User object
953 $user = $services->getUserFactory()->newFromAuthority( $performer );
954 }
955
956 $status = self::isTagNameValid( $tag );
957 if ( !$status->isGood() ) {
958 return $status;
959 }
960
961 // does the tag already exist?
962 $changeTagStore = $services->getChangeTagsStore();
963 if (
964 isset( $changeTagStore->tagUsageStatistics()[$tag] ) ||
965 in_array( $tag, $changeTagStore->listDefinedTags() )
966 ) {
967 return Status::newFatal( 'tags-create-already-exists', $tag );
968 }
969
970 // check with hooks
971 $canCreateResult = Status::newGood();
972 ( new HookRunner( $services->getHookContainer() ) )->onChangeTagCanCreate( $tag, $user, $canCreateResult );
973 return $canCreateResult;
974 }
975
995 public static function createTagWithChecks( string $tag, string $reason, Authority $performer,
996 bool $ignoreWarnings = false, array $logEntryTags = []
997 ) {
998 // are we allowed to do this?
999 $result = self::canCreateTag( $tag, $performer );
1000 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1001 $result->value = null;
1002 return $result;
1003 }
1004
1005 $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1006 $changeTagStore->defineTag( $tag );
1007 $logId = $changeTagStore->logTagManagementAction( 'create', $tag, $reason,
1008 $performer->getUser(), null, $logEntryTags );
1009
1010 return Status::newGood( $logId );
1011 }
1012
1026 public static function deleteTagEverywhere( $tag ) {
1027 return MediaWikiServices::getInstance()->getChangeTagsStore()->deleteTagEverywhere( $tag );
1028 }
1029
1042 public static function canDeleteTag( $tag, Authority $performer = null, int $flags = 0 ) {
1043 $user = null;
1044 $services = MediaWikiServices::getInstance();
1045 if ( $performer !== null ) {
1046 if ( !$performer->isAllowed( 'deletechangetags' ) ) {
1047 return Status::newFatal( 'tags-delete-no-permission' );
1048 }
1049 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1050 return Status::newFatal(
1051 'tags-manage-blocked',
1052 $performer->getUser()->getName()
1053 );
1054 }
1055 // ChangeTagCanDelete hook still needs a full User object
1056 $user = $services->getUserFactory()->newFromAuthority( $performer );
1057 }
1058
1059 $changeTagStore = $services->getChangeTagsStore();
1060 $tagUsage = $changeTagStore->tagUsageStatistics();
1061 if (
1062 !isset( $tagUsage[$tag] ) &&
1063 !in_array( $tag, $changeTagStore->listDefinedTags() )
1064 ) {
1065 return Status::newFatal( 'tags-delete-not-found', $tag );
1066 }
1067
1068 if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1069 isset( $tagUsage[$tag] ) &&
1070 $tagUsage[$tag] > self::MAX_DELETE_USES
1071 ) {
1072 return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1073 }
1074
1075 $softwareDefined = $changeTagStore->listSoftwareDefinedTags();
1076 if ( in_array( $tag, $softwareDefined ) ) {
1077 // extension-defined tags can't be deleted unless the extension
1078 // specifically allows it
1079 $status = Status::newFatal( 'tags-delete-not-allowed' );
1080 } else {
1081 // user-defined tags are deletable unless otherwise specified
1082 $status = Status::newGood();
1083 }
1084
1085 ( new HookRunner( $services->getHookContainer() ) )->onChangeTagCanDelete( $tag, $user, $status );
1086 return $status;
1087 }
1088
1106 public static function deleteTagWithChecks( string $tag, string $reason, Authority $performer,
1107 bool $ignoreWarnings = false, array $logEntryTags = []
1108 ) {
1109 $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1110 // are we allowed to do this?
1111 $result = self::canDeleteTag( $tag, $performer );
1112 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1113 $result->value = null;
1114 return $result;
1115 }
1116
1117 // store the tag usage statistics
1118 $hitcount = $changeTagStore->tagUsageStatistics()[$tag] ?? 0;
1119
1120 // do it!
1121 $deleteResult = $changeTagStore->deleteTagEverywhere( $tag );
1122 if ( !$deleteResult->isOK() ) {
1123 return $deleteResult;
1124 }
1125
1126 // log it
1127 $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1128 $logId = $changeTagStore->logTagManagementAction( 'delete', $tag, $reason, $performer->getUser(),
1129 $hitcount, $logEntryTags );
1130
1131 $deleteResult->value = $logId;
1132 return $deleteResult;
1133 }
1134
1142 public static function listSoftwareActivatedTags() {
1143 return MediaWikiServices::getInstance()->getChangeTagsStore()->listSoftwareActivatedTags();
1144 }
1145
1154 public static function listDefinedTags() {
1155 return MediaWikiServices::getInstance()->getChangeTagsStore()->listDefinedTags();
1156 }
1157
1167 public static function listExplicitlyDefinedTags() {
1168 return MediaWikiServices::getInstance()->getChangeTagsStore()->listExplicitlyDefinedTags();
1169 }
1170
1181 public static function listSoftwareDefinedTags() {
1182 return MediaWikiServices::getInstance()->getChangeTagsStore()->listSoftwareDefinedTags();
1183 }
1184
1191 public static function purgeTagCacheAll() {
1192 MediaWikiServices::getInstance()->getChangeTagsStore()->purgeTagCacheAll();
1193 }
1194
1203 public static function tagUsageStatistics() {
1204 return MediaWikiServices::getInstance()->getChangeTagsStore()->tagUsageStatistics();
1205 }
1206
1211 private const TAG_DESC_CHARACTER_LIMIT = 120;
1212
1237 public static function getChangeTagListSummary( MessageLocalizer $localizer, Language $lang ) {
1238 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1239 return $cache->getWithSetCallback(
1240 $cache->makeKey( 'tags-list-summary', $lang->getCode() ),
1241 WANObjectCache::TTL_DAY,
1242 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $localizer ) {
1243 $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1244 $tagHitCounts = $changeTagStore->tagUsageStatistics();
1245
1246 $result = [];
1247 // Only list tags that are still actively defined
1248 foreach ( $changeTagStore->listDefinedTags() as $tagName ) {
1249 // Only list tags with more than 0 hits
1250 $hits = $tagHitCounts[$tagName] ?? 0;
1251 if ( $hits <= 0 ) {
1252 continue;
1253 }
1254
1255 $labelMsg = self::tagShortDescriptionMessage( $tagName, $localizer );
1256 $descriptionMsg = self::tagLongDescriptionMessage( $tagName, $localizer );
1257 // Don't cache the message object, use the correct MessageLocalizer to parse later.
1258 $result[] = [
1259 'name' => $tagName,
1260 'labelMsg' => (bool)$labelMsg,
1261 'label' => $labelMsg ? $labelMsg->plain() : $tagName,
1262 'descriptionMsg' => (bool)$descriptionMsg,
1263 'description' => $descriptionMsg ? $descriptionMsg->plain() : '',
1264 'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
1265 ];
1266 }
1267 return $result;
1268 }
1269 );
1270 }
1271
1284 public static function getChangeTagList( MessageLocalizer $localizer, Language $lang ) {
1285 $tags = self::getChangeTagListSummary( $localizer, $lang );
1286 foreach ( $tags as &$tagInfo ) {
1287 if ( $tagInfo['labelMsg'] ) {
1288 // Use localizer with the correct page title to parse plain message from the cache.
1289 $labelMsg = new RawMessage( $tagInfo['label'] );
1290 $tagInfo['label'] = Sanitizer::stripAllTags( $localizer->msg( $labelMsg )->parse() );
1291 } else {
1292 $tagInfo['label'] = $localizer->msg( 'tag-hidden', $tagInfo['name'] )->text();
1293 }
1294 if ( $tagInfo['descriptionMsg'] ) {
1295 $descriptionMsg = new RawMessage( $tagInfo['description'] );
1296 $tagInfo['description'] = $lang->truncateForVisual(
1297 Sanitizer::stripAllTags( $localizer->msg( $descriptionMsg )->parse() ),
1298 self::TAG_DESC_CHARACTER_LIMIT
1299 );
1300 }
1301 unset( $tagInfo['labelMsg'] );
1302 unset( $tagInfo['descriptionMsg'] );
1303 }
1304
1305 // Instead of sorting by hit count (disabled for now), sort by display name
1306 usort( $tags, static function ( $a, $b ) {
1307 return strcasecmp( $a['label'], $b['label'] );
1308 } );
1309 return $tags;
1310 }
1311
1326 public static function showTagEditingUI( Authority $performer ) {
1327 $changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1328 return $performer->isAllowed( 'changetags' ) && (bool)$changeTagStore->listExplicitlyDefinedTags();
1329 }
1330}
const NS_MEDIAWIKI
Definition Defines.php:72
array $params
The job parameters.
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,...
const TAG_MANUAL_REVERT
The tagged edit restores the page to an earlier revision.
const TAG_SERVER_SIDE_UPLOAD
This tagged edit was performed while importing media files using the importImages....
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
const TAG_REMOVED_REDIRECT
The tagged edit turns a redirect page into a non-redirect.
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 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.
const TAG_CONTENT_MODEL_CHANGE
The tagged edit changes the content model of the page.
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,...
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
static deactivateTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Deactivates a tag, checking whether it is allowed first, and adding a log entry afterwards.
static showTagEditingUI(Authority $performer)
Indicate whether change tag editing UI is relevant.
static deleteTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Deletes a tag, checking whether it is allowed first, and adding a log entry afterwards.
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
const TAG_CHANGED_REDIRECT_TARGET
The tagged edit changes the target of a redirect page.
const TAG_REVERTED
The tagged edit is reverted by a subsequent edit (which is tagged by one of TAG_ROLLBACK,...
static restrictedTagError( $msgOne, $msgMulti, $tags)
Helper function to generate a fatal status with a 'not-allowed' type error.
const TAG_ROLLBACK
The tagged edit is a rollback (undoes the previous edit and all immediately preceding edits by the sa...
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.
static getChangeTagList(MessageLocalizer $localizer, Language $lang)
Get information about change tags for tag filter dropdown menus.
const BYPASS_MAX_USAGE_CHECK
Flag for canDeleteTag().
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?
static formatSummaryRow( $tags, $unused, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
const DISPLAY_TABLE_ALIAS
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.
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().
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.
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.
static defineTag( $tag)
Set ctd_user_defined = 1 in change_tag_def without checking that the tag name is valid.
static createTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Creates a tag by adding it to change_tag_def table.
static getTags(IReadableDatabase $db, $rc_id=null, $rev_id=null, $log_id=null)
Return all the tags associated with the given recent change ID, revision ID, and/or log entry ID.
const TAG_UNDO
The tagged edit is was performed via the "undo" link.
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...
const TAG_BLANK
The tagged edit blanks the page (replaces it with the empty string).
static isTagNameValid( $tag)
Is the tag name valid?
const REVERT_TAGS
List of tags which denote a revert of some sort.
static activateTagWithChecks(string $tag, string $reason, Authority $performer, bool $ignoreWarnings=false, array $logEntryTags=[])
Activates a tag, checking whether it is allowed first, and adding a log entry afterwards.
static deleteTagEverywhere( $tag)
Permanently removes all traces of a tag from the DB.
const TAG_NEW_REDIRECT
The tagged edit creates a new redirect (either by creating a new page or turning an existing page int...
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?
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...
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....
getCode()
Get the internal language code for this language object.
Class for creating new log entries and inserting them into the database.
Group all the pieces relevant to the context of a request into one instance.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
Variant of the Message class.
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.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Represents a title within MediaWiki.
Definition Title.php:78
Utility class for creating new RC entries.
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
Interface for objects which can provide a MediaWiki context on request.
This interface represents the authority associated with the current execution context,...
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.