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