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