MediaWiki master
ChangeTags.php
Go to the documentation of this file.
1<?php
8
32
54 public const TAG_CONTENT_MODEL_CHANGE = 'mw-contentmodelchange';
59 public const TAG_NEW_REDIRECT = 'mw-new-redirect';
63 public const TAG_REMOVED_REDIRECT = 'mw-removed-redirect';
67 public const TAG_CHANGED_REDIRECT_TARGET = 'mw-changed-redirect-target';
71 public const TAG_BLANK = 'mw-blank';
75 public const TAG_REPLACE = 'mw-replace';
79 public const TAG_RECREATE = 'mw-recreated';
87 public const TAG_ROLLBACK = 'mw-rollback';
94 public const TAG_UNDO = 'mw-undo';
100 public const TAG_MANUAL_REVERT = 'mw-manual-revert';
108 public const TAG_REVERTED = 'mw-reverted';
112 public const TAG_SERVER_SIDE_UPLOAD = 'mw-server-side-upload';
117 public const TAG_IPBLOCK_APPEAL = 'mw-ipblock-appeal';
118
123
127 public const BYPASS_MAX_USAGE_CHECK = 1;
128
134 private const MAX_DELETE_USES = 5000;
135
139 private const CHANGE_TAG = 'change_tag';
140
141 public const DISPLAY_TABLE_ALIAS = 'changetagdisplay';
142
152 public const TAG_SET_ACTIVE_ONLY = true;
153 public const TAG_SET_ALL = false;
154
163 public const USE_ALL_TAGS = true;
164 public const USE_SOFTWARE_TAGS_ONLY = false;
165
174 public static function getSoftwareTags( $all = false ) {
175 wfDeprecated( __METHOD__, '1.41' );
176 return MediaWikiServices::getInstance()->getChangeTagsStore()->getSoftwareTags( $all );
177 }
178
192 public static function formatSummaryRow( $tags, $unused, ?MessageLocalizer $localizer = null ) {
193 if ( $tags === '' || $tags === null ) {
194 return [ '', [] ];
195 }
196 if ( !$localizer ) {
197 $localizer = RequestContext::getMain();
198 }
199
200 $classes = [];
201
202 $tags = explode( ',', $tags );
203 $order = array_flip( MediaWikiServices::getInstance()->getChangeTagsStore()->listDefinedTags() );
204 usort( $tags, static function ( $a, $b ) use ( $order ) {
205 return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
206 } );
207
208 $displayTags = [];
209 foreach ( $tags as $tag ) {
210 if ( $tag === '' ) {
211 continue;
212 }
213 $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
214 $description = self::tagDescription( $tag, $localizer );
215 if ( $description === false ) {
216 continue;
217 }
218 $displayTags[] = Html::rawElement(
219 'span',
220 [ 'class' => 'mw-tag-marker ' .
221 Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
222 $description
223 );
224 }
225
226 if ( !$displayTags ) {
227 return [ '', $classes ];
228 }
229
230 $markers = $localizer->msg( 'tag-list-wrapper' )
231 ->numParams( count( $displayTags ) )
232 ->rawParams( implode( ' ', $displayTags ) )
233 ->parse();
234 $markers = Html::rawElement( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
235
236 return [ $markers, $classes ];
237 }
238
252 public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
253 $msg = $context->msg( "tag-$tag" );
254 if ( !$msg->exists() ) {
255 // No such message
256 // Pass through ->msg(), even though it seems redundant, to avoid requesting
257 // the user's language from session-less entry points (T227233)
258 return $context->msg( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) );
259 }
260 if ( $msg->isDisabled() ) {
261 // The message exists but is disabled, hide the tag.
262 return false;
263 }
264
265 // Message exists and isn't disabled, use it.
266 return $msg;
267 }
268
281 public static function tagHelpLink( $tag, MessageLocalizer $context ) {
282 $msg = $context->msg( "tag-$tag-helppage" )->inContentLanguage();
283 if ( !$msg->isDisabled() ) {
284 return Skin::makeInternalOrExternalUrl( $msg->text() ) ?: null;
285 }
286 return null;
287 }
288
300 public static function tagDescription( $tag, MessageLocalizer $context ) {
301 $msg = self::tagShortDescriptionMessage( $tag, $context );
302 $link = self::tagHelpLink( $tag, $context );
303 if ( $msg && $link ) {
304 $label = $msg->parse();
305 // Avoid invalid HTML caused by link wrapping if the label already contains a link
306 if ( !str_contains( $label, '<a ' ) ) {
307 return Html::rawElement( 'a', [ 'href' => $link ], $label );
308 }
309 }
310 return $msg ? $msg->parse() : false;
311 }
312
325 public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
326 $msg = $context->msg( "tag-$tag-description" );
327 return $msg->isDisabled() ? false : $msg;
328 }
329
344 public static function addTags( $tags, $rc_id = null, $rev_id = null,
345 $log_id = null, $params = null, ?RecentChange $rc = null
346 ) {
347 wfDeprecated( __METHOD__, '1.41' );
348 return MediaWikiServices::getInstance()->getChangeTagsStore()->addTags(
349 $tags, $rc_id, $rev_id, $log_id, $params, $rc
350 );
351 }
352
383 public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
384 &$rev_id = null, &$log_id = null, $params = null, ?RecentChange $rc = null,
385 ?UserIdentity $user = null
386 ) {
387 wfDeprecated( __METHOD__, '1.41' );
388 return MediaWikiServices::getInstance()->getChangeTagsStore()->updateTags(
389 $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, $rc, $user
390 );
391 }
392
405 public static function getTagsWithData(
406 IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null
407 ) {
408 wfDeprecated( __METHOD__, '1.41' );
409 return MediaWikiServices::getInstance()->getChangeTagsStore()->getTagsWithData( $db, $rc_id, $rev_id, $log_id );
410 }
411
423 public static function getTags( IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
424 wfDeprecated( __METHOD__, '1.41' );
425 return MediaWikiServices::getInstance()->getChangeTagsStore()->getTags( $db, $rc_id, $rev_id, $log_id );
426 }
427
438 protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
439 $lang = RequestContext::getMain()->getLanguage();
440 $tags = array_values( $tags );
441 $count = count( $tags );
442 $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
443 $lang->commaList( $tags ), $count );
444 $status->value = $tags;
445 return $status;
446 }
447
462 public static function canAddTagsAccompanyingChange(
463 array $tags,
464 ?Authority $performer = null,
465 $checkBlock = true
466 ) {
467 $user = null;
468 $services = MediaWikiServices::getInstance();
469 if ( $performer !== null ) {
470 if ( !$performer->isAllowed( 'applychangetags' ) ) {
471 return Status::newFatal( 'tags-apply-no-permission' );
472 }
473
474 if ( $checkBlock && $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
475 return Status::newFatal(
476 'tags-apply-blocked',
477 $performer->getUser()->getName()
478 );
479 }
480
481 // ChangeTagsAllowedAdd hook still needs a full User object
482 $user = $services->getUserFactory()->newFromAuthority( $performer );
483 }
484
485 // to be applied, a tag has to be explicitly defined
486 $allowedTags = $services->getChangeTagsStore()->listExplicitlyDefinedTags();
487 ( new HookRunner( $services->getHookContainer() ) )->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
488 $disallowedTags = array_diff( $tags, $allowedTags );
489 if ( $disallowedTags ) {
490 return self::restrictedTagError( 'tags-apply-not-allowed-one',
491 'tags-apply-not-allowed-multi', $disallowedTags );
492 }
493
494 return Status::newGood();
495 }
496
511 public static function canUpdateTags(
512 array $tagsToAdd,
513 array $tagsToRemove,
514 ?Authority $performer = null
515 ) {
516 if ( $performer !== null ) {
517 if ( !$performer->isDefinitelyAllowed( 'changetags' ) ) {
518 return Status::newFatal( 'tags-update-no-permission' );
519 }
520
521 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
522 return Status::newFatal(
523 'tags-update-blocked',
524 $performer->getUser()->getName()
525 );
526 }
527 }
528
529 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
530 if ( $tagsToAdd ) {
531 // to be added, a tag has to be explicitly defined
532 // @todo Allow extensions to define tags that can be applied by users...
533 $explicitlyDefinedTags = $changeTagsStore->listExplicitlyDefinedTags();
534 $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
535 if ( $diff ) {
536 return self::restrictedTagError( 'tags-update-add-not-allowed-one',
537 'tags-update-add-not-allowed-multi', $diff );
538 }
539 }
540
541 if ( $tagsToRemove ) {
542 // to be removed, a tag must not be defined by an extension, or equivalently it
543 // has to be either explicitly defined or not defined at all
544 // (assuming no edge case of a tag both explicitly-defined and extension-defined)
545 $softwareDefinedTags = $changeTagsStore->listSoftwareDefinedTags();
546 $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
547 if ( $intersect ) {
548 return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
549 'tags-update-remove-not-allowed-multi', $intersect );
550 }
551 }
552
553 return Status::newGood();
554 }
555
586 public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
587 $rc_id, $rev_id, $log_id, $params, string $reason, Authority $performer
588 ) {
589 if ( !$tagsToAdd && !$tagsToRemove ) {
590 // no-op, don't bother
591 return Status::newGood( (object)[
592 'logId' => null,
593 'addedTags' => [],
594 'removedTags' => [],
595 ] );
596 }
597
598 $tagsToAdd ??= [];
599 $tagsToRemove ??= [];
600
601 // are we allowed to do this?
602 $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $performer );
603 if ( !$result->isOK() ) {
604 $result->value = null;
605 return $result;
606 }
607
608 // basic rate limiting
609 $status = PermissionStatus::newEmpty();
610 if ( !$performer->authorizeAction( 'changetags', $status ) ) {
611 return Status::wrap( $status );
612 }
613
614 // do it!
615 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
616 [ $tagsAdded, $tagsRemoved, $initialTags ] = $changeTagsStore->updateTags( $tagsToAdd,
617 $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $performer->getUser() );
618 if ( !$tagsAdded && !$tagsRemoved ) {
619 // no-op, don't log it
620 return Status::newGood( (object)[
621 'logId' => null,
622 'addedTags' => [],
623 'removedTags' => [],
624 ] );
625 }
626
627 // log it
628 $logEntry = new ManualLogEntry( 'tag', 'update' );
629 $logEntry->setPerformer( $performer->getUser() );
630 $logEntry->setComment( $reason );
631
632 // find the appropriate target page
633 if ( $rev_id ) {
634 $revisionRecord = MediaWikiServices::getInstance()
635 ->getRevisionLookup()
636 ->getRevisionById( $rev_id );
637 if ( $revisionRecord ) {
638 $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
639 }
640 } elseif ( $log_id ) {
641 // This function is from revision deletion logic and has nothing to do with
642 // change tags, but it appears to be the only other place in core where we
643 // perform logged actions on log items.
644 $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
645 }
646
647 if ( !$logEntry->getTarget() ) {
648 // target is required, so we have to set something
649 $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
650 }
651
652 $logParams = [
653 '4::revid' => $rev_id,
654 '5::logid' => $log_id,
655 '6:list:tagsAdded' => $tagsAdded,
656 '7:number:tagsAddedCount' => count( $tagsAdded ),
657 '8:list:tagsRemoved' => $tagsRemoved,
658 '9:number:tagsRemovedCount' => count( $tagsRemoved ),
659 'initialTags' => $initialTags,
660 ];
661 $logEntry->setParameters( $logParams );
662 $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
663
664 $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
665 $logId = $logEntry->insert( $dbw );
666 // Only send this to UDP, not RC, similar to patrol events
667 $logEntry->publish( $logId, 'udp' );
668
669 return Status::newGood( (object)[
670 'logId' => $logId,
671 'addedTags' => $tagsAdded,
672 'removedTags' => $tagsRemoved,
673 ] );
674 }
675
696 public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
697 &$join_conds, &$options, $filter_tag = '', bool $exclude = false
698 ) {
699 wfDeprecated( __METHOD__, '1.41' );
700 MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQuery(
701 $tables,
702 $fields,
703 $conds,
704 $join_conds,
705 $options,
706 $filter_tag,
707 $exclude
708 );
709 }
710
720 public static function getDisplayTableName() {
721 wfDeprecated( __METHOD__, '1.41' );
722 return self::CHANGE_TAG;
723 }
724
733 public static function makeTagSummarySubquery( $tables ) {
734 wfDeprecated( __METHOD__, '1.41' );
735 return MediaWikiServices::getInstance()->getChangeTagsStore()->makeTagSummarySubquery( $tables );
736 }
737
753 public static function buildTagFilterSelector(
754 $selected = '', $ooui = false, ?IContextSource $context = null,
755 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
756 bool $useAllTags = self::USE_ALL_TAGS
757 ) {
758 if ( !$context ) {
759 $context = RequestContext::getMain();
760 }
761
762 $config = $context->getConfig();
763 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
764 if ( !$config->get( MainConfigNames::UseTagFilter ) ||
765 !count( $changeTagsStore->listDefinedTags() ) ) {
766 return null;
767 }
768
770 $context,
771 $context->getLanguage(),
772 $activeOnly,
773 $useAllTags,
774 true
775 );
776
777 $autocomplete = [];
778 foreach ( $tags as $tagInfo ) {
779 $autocomplete[ $tagInfo['label'] ] = $tagInfo['name'];
780 }
781
782 $data = [];
783 $data[0] = Html::rawElement(
784 'label',
785 [ 'for' => 'tagfilter' ],
786 $context->msg( 'tag-filter' )->parse()
787 );
788
789 if ( $ooui ) {
790 $options = Html::listDropdownOptionsOoui( $autocomplete );
791
792 $data[1] = new \OOUI\ComboBoxInputWidget( [
793 'id' => 'tagfilter',
794 'name' => 'tagfilter',
795 'value' => $selected,
796 'classes' => 'mw-tagfilter-input',
797 'options' => $options,
798 ] );
799 } else {
800 $optionsHtml = '';
801 foreach ( $autocomplete as $label => $name ) {
802 $optionsHtml .= Html::element( 'option', [ 'value' => $name ], $label );
803 }
804 $datalistHtml = Html::rawElement( 'datalist', [ 'id' => 'tagfilter-datalist' ], $optionsHtml );
805
806 $data[1] = Html::input(
807 'tagfilter',
808 $selected,
809 'text',
810 [
811 'class' => [ 'mw-tagfilter-input', 'mw-ui-input', 'mw-ui-input-inline' ],
812 'size' => 20,
813 'id' => 'tagfilter',
814 'list' => 'tagfilter-datalist',
815 ]
816 ) . $datalistHtml;
817 }
818
819 return $data;
820 }
821
831 public static function defineTag( $tag ) {
832 wfDeprecated( __METHOD__, '1.41' );
833 MediaWikiServices::getInstance()->getChangeTagsStore()->defineTag( $tag );
834 }
835
845 public static function canActivateTag( $tag, ?Authority $performer = null ) {
846 if ( $performer !== null ) {
847 if ( !$performer->isAllowed( 'managechangetags' ) ) {
848 return Status::newFatal( 'tags-manage-no-permission' );
849 }
850 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
851 return Status::newFatal(
852 'tags-manage-blocked',
853 $performer->getUser()->getName()
854 );
855 }
856 }
857
858 // defined tags cannot be activated (a defined tag is either extension-
859 // defined, in which case the extension chooses whether or not to active it;
860 // or user-defined, in which case it is considered active)
861 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
862 $definedTags = $changeTagsStore->listDefinedTags();
863 if ( in_array( $tag, $definedTags ) ) {
864 return Status::newFatal( 'tags-activate-not-allowed', $tag );
865 }
866
867 // non-existing tags cannot be activated
868 if ( !isset( $changeTagsStore->tagUsageStatistics()[$tag] ) ) { // we already know the tag is undefined
869 return Status::newFatal( 'tags-activate-not-found', $tag );
870 }
871
872 return Status::newGood();
873 }
874
892 public static function activateTagWithChecks( string $tag, string $reason, Authority $performer,
893 bool $ignoreWarnings = false, array $logEntryTags = []
894 ) {
895 // are we allowed to do this?
896 $result = self::canActivateTag( $tag, $performer );
897 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
898 $result->value = null;
899 return $result;
900 }
901 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
902
903 $changeTagsStore->defineTag( $tag );
904
905 $logId = $changeTagsStore->logTagManagementAction( 'activate', $tag, $reason, $performer->getUser(),
906 null, $logEntryTags );
907
908 return Status::newGood( $logId );
909 }
910
920 public static function canDeactivateTag( $tag, ?Authority $performer = null ) {
921 if ( $performer !== null ) {
922 if ( !$performer->isAllowed( 'managechangetags' ) ) {
923 return Status::newFatal( 'tags-manage-no-permission' );
924 }
925 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
926 return Status::newFatal(
927 'tags-manage-blocked',
928 $performer->getUser()->getName()
929 );
930 }
931 }
932
933 // only explicitly-defined tags can be deactivated
934 $explicitlyDefinedTags = MediaWikiServices::getInstance()->getChangeTagsStore()->listExplicitlyDefinedTags();
935 if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
936 return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
937 }
938 return Status::newGood();
939 }
940
958 public static function deactivateTagWithChecks( string $tag, string $reason, Authority $performer,
959 bool $ignoreWarnings = false, array $logEntryTags = []
960 ) {
961 // are we allowed to do this?
962 $result = self::canDeactivateTag( $tag, $performer );
963 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
964 $result->value = null;
965 return $result;
966 }
967 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
968
969 $changeTagsStore->undefineTag( $tag );
970
971 $logId = $changeTagsStore->logTagManagementAction( 'deactivate', $tag, $reason,
972 $performer->getUser(), null, $logEntryTags );
973
974 return Status::newGood( $logId );
975 }
976
984 public static function isTagNameValid( $tag ) {
985 // no empty tags
986 if ( $tag === '' ) {
987 return Status::newFatal( 'tags-create-no-name' );
988 }
989
990 // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
991 // pipe (used as a delimiter between multiple tags in
992 // SpecialRecentchanges and friends), or slashes (would break tag description messages in
993 // MediaWiki namespace)
994 if ( str_contains( $tag, ',' ) || str_contains( $tag, '|' ) || str_contains( $tag, '/' ) ) {
995 return Status::newFatal( 'tags-create-invalid-chars' );
996 }
997
998 // could the MediaWiki namespace description messages be created?
999 $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
1000 if ( $title === null ) {
1001 return Status::newFatal( 'tags-create-invalid-title-chars' );
1002 }
1003
1004 return Status::newGood();
1005 }
1006
1019 public static function canCreateTag( $tag, ?Authority $performer = null ) {
1020 $user = null;
1021 $services = MediaWikiServices::getInstance();
1022 if ( $performer !== null ) {
1023 if ( !$performer->isAllowed( 'managechangetags' ) ) {
1024 return Status::newFatal( 'tags-manage-no-permission' );
1025 }
1026 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1027 return Status::newFatal(
1028 'tags-manage-blocked',
1029 $performer->getUser()->getName()
1030 );
1031 }
1032 // ChangeTagCanCreate hook still needs a full User object
1033 $user = $services->getUserFactory()->newFromAuthority( $performer );
1034 }
1035
1036 $status = self::isTagNameValid( $tag );
1037 if ( !$status->isGood() ) {
1038 return $status;
1039 }
1040
1041 // does the tag already exist?
1042 $changeTagsStore = $services->getChangeTagsStore();
1043 if (
1044 isset( $changeTagsStore->tagUsageStatistics()[$tag] ) ||
1045 in_array( $tag, $changeTagsStore->listDefinedTags() )
1046 ) {
1047 return Status::newFatal( 'tags-create-already-exists', $tag );
1048 }
1049
1050 // check with hooks
1051 $canCreateResult = Status::newGood();
1052 ( new HookRunner( $services->getHookContainer() ) )->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1053 return $canCreateResult;
1054 }
1055
1075 public static function createTagWithChecks( string $tag, string $reason, Authority $performer,
1076 bool $ignoreWarnings = false, array $logEntryTags = []
1077 ) {
1078 // are we allowed to do this?
1079 $result = self::canCreateTag( $tag, $performer );
1080 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1081 $result->value = null;
1082 return $result;
1083 }
1084
1085 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1086 $changeTagsStore->defineTag( $tag );
1087 $logId = $changeTagsStore->logTagManagementAction( 'create', $tag, $reason,
1088 $performer->getUser(), null, $logEntryTags );
1089
1090 return Status::newGood( $logId );
1091 }
1092
1106 public static function deleteTagEverywhere( $tag ) {
1107 wfDeprecated( __METHOD__, '1.41' );
1108 return MediaWikiServices::getInstance()->getChangeTagsStore()->deleteTagEverywhere( $tag );
1109 }
1110
1123 public static function canDeleteTag( $tag, ?Authority $performer = null, int $flags = 0 ) {
1124 $user = null;
1125 $services = MediaWikiServices::getInstance();
1126 if ( $performer !== null ) {
1127 if ( !$performer->isAllowed( 'deletechangetags' ) ) {
1128 return Status::newFatal( 'tags-delete-no-permission' );
1129 }
1130 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1131 return Status::newFatal(
1132 'tags-manage-blocked',
1133 $performer->getUser()->getName()
1134 );
1135 }
1136 // ChangeTagCanDelete hook still needs a full User object
1137 $user = $services->getUserFactory()->newFromAuthority( $performer );
1138 }
1139
1140 $changeTagsStore = $services->getChangeTagsStore();
1141 $tagUsage = $changeTagsStore->tagUsageStatistics();
1142 if (
1143 !isset( $tagUsage[$tag] ) &&
1144 !in_array( $tag, $changeTagsStore->listDefinedTags() )
1145 ) {
1146 return Status::newFatal( 'tags-delete-not-found', $tag );
1147 }
1148
1149 if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1150 isset( $tagUsage[$tag] ) &&
1151 $tagUsage[$tag] > self::MAX_DELETE_USES
1152 ) {
1153 return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1154 }
1155
1156 $softwareDefined = $changeTagsStore->listSoftwareDefinedTags();
1157 if ( in_array( $tag, $softwareDefined ) ) {
1158 // extension-defined tags can't be deleted unless the extension
1159 // specifically allows it
1160 $status = Status::newFatal( 'tags-delete-not-allowed' );
1161 } else {
1162 // user-defined tags are deletable unless otherwise specified
1163 $status = Status::newGood();
1164 }
1165
1166 ( new HookRunner( $services->getHookContainer() ) )->onChangeTagCanDelete( $tag, $user, $status );
1167 return $status;
1168 }
1169
1187 public static function deleteTagWithChecks( string $tag, string $reason, Authority $performer,
1188 bool $ignoreWarnings = false, array $logEntryTags = []
1189 ) {
1190 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1191 // are we allowed to do this?
1192 $result = self::canDeleteTag( $tag, $performer );
1193 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1194 $result->value = null;
1195 return $result;
1196 }
1197
1198 // store the tag usage statistics
1199 $hitcount = $changeTagsStore->tagUsageStatistics()[$tag] ?? 0;
1200
1201 // do it!
1202 $deleteResult = $changeTagsStore->deleteTagEverywhere( $tag );
1203 if ( !$deleteResult->isOK() ) {
1204 return $deleteResult;
1205 }
1206
1207 // log it
1208 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1209 $logId = $changeTagsStore->logTagManagementAction( 'delete', $tag, $reason, $performer->getUser(),
1210 $hitcount, $logEntryTags );
1211
1212 $deleteResult->value = $logId;
1213 return $deleteResult;
1214 }
1215
1223 public static function listSoftwareActivatedTags() {
1224 wfDeprecated( __METHOD__, '1.41' );
1225 return MediaWikiServices::getInstance()->getChangeTagsStore()->listSoftwareActivatedTags();
1226 }
1227
1236 public static function listDefinedTags() {
1237 wfDeprecated( __METHOD__, '1.41' );
1238 return MediaWikiServices::getInstance()->getChangeTagsStore()->listDefinedTags();
1239 }
1240
1250 public static function listExplicitlyDefinedTags() {
1251 wfDeprecated( __METHOD__, '1.41' );
1252 return MediaWikiServices::getInstance()->getChangeTagsStore()->listExplicitlyDefinedTags();
1253 }
1254
1265 public static function listSoftwareDefinedTags() {
1266 wfDeprecated( __METHOD__, '1.41' );
1267 return MediaWikiServices::getInstance()->getChangeTagsStore()->listSoftwareDefinedTags();
1268 }
1269
1276 public static function purgeTagCacheAll() {
1277 wfDeprecated( __METHOD__, '1.41' );
1278 MediaWikiServices::getInstance()->getChangeTagsStore()->purgeTagCacheAll();
1279 }
1280
1289 public static function tagUsageStatistics() {
1290 wfDeprecated( __METHOD__, '1.41' );
1291 return MediaWikiServices::getInstance()->getChangeTagsStore()->tagUsageStatistics();
1292 }
1293
1298 private const TAG_DESC_CHARACTER_LIMIT = 120;
1299
1328 public static function getChangeTagListSummary(
1329 MessageLocalizer $localizer,
1330 Language $lang,
1331 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
1332 bool $useAllTags = self::USE_ALL_TAGS
1333 ) {
1334 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1335
1336 if ( $useAllTags ) {
1337 $tagKeys = $changeTagsStore->listDefinedTags();
1338 $cacheKey = 'tags-list-summary';
1339 } else {
1340 $tagKeys = $changeTagsStore->getCoreDefinedTags();
1341 $cacheKey = 'core-software-tags-summary';
1342 }
1343
1344 // if $tagHitCounts exists, check against it later to determine whether or not to omit tags
1345 $tagHitCounts = null;
1346 if ( $activeOnly ) {
1347 $tagHitCounts = $changeTagsStore->tagUsageStatistics();
1348 } else {
1349 // The full set of tags should use a different cache key than the subset
1350 $cacheKey .= '-all';
1351 }
1352
1353 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1354 return $cache->getWithSetCallback(
1355 $cache->makeKey( $cacheKey, $lang->getCode() ),
1356 WANObjectCache::TTL_DAY,
1357 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $localizer, $tagKeys, $tagHitCounts ) {
1358 $result = [];
1359 foreach ( $tagKeys as $tagName ) {
1360 // Only list tags that are still actively defined
1361 if ( $tagHitCounts !== null ) {
1362 // Only list tags with more than 0 hits
1363 $hits = $tagHitCounts[$tagName] ?? 0;
1364 if ( $hits <= 0 ) {
1365 continue;
1366 }
1367 }
1368
1369 $labelMsg = self::tagShortDescriptionMessage( $tagName, $localizer );
1370 $helpLink = self::tagHelpLink( $tagName, $localizer );
1371 $descriptionMsg = self::tagLongDescriptionMessage( $tagName, $localizer );
1372 // Don't cache the message object, use the correct MessageLocalizer to parse later.
1373 $result[] = [
1374 'name' => $tagName,
1375 'labelMsg' => (bool)$labelMsg,
1376 'label' => $labelMsg ? $labelMsg->plain() : $tagName,
1377 'descriptionMsg' => (bool)$descriptionMsg,
1378 'description' => $descriptionMsg ? $descriptionMsg->plain() : '',
1379 'helpLink' => $helpLink,
1380 'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
1381 ];
1382 }
1383 return $result;
1384 }
1385 );
1386 }
1387
1403 public static function getChangeTagList(
1404 MessageLocalizer $localizer, Language $lang,
1405 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY, bool $useAllTags = self::USE_ALL_TAGS,
1406 $labelsOnly = false
1407 ) {
1408 $tags = self::getChangeTagListSummary( $localizer, $lang, $activeOnly, $useAllTags );
1409
1410 foreach ( $tags as &$tagInfo ) {
1411 if ( $tagInfo['labelMsg'] ) {
1412 // Optimization: Skip the parsing if the label contains only plain text (T344352)
1413 if ( wfEscapeWikiText( $tagInfo['label'] ) !== $tagInfo['label'] ) {
1414 // Use localizer with the correct page title to parse plain message from the cache.
1415 $labelMsg = new RawMessage( $tagInfo['label'] );
1416 $tagInfo['label'] = Sanitizer::stripAllTags( $localizer->msg( $labelMsg )->parse() );
1417 }
1418 } else {
1419 $tagInfo['label'] = $localizer->msg( 'tag-hidden', $tagInfo['name'] )->text();
1420 }
1421 // Optimization: Skip parsing the descriptions if not needed by the caller (T344352)
1422 if ( $labelsOnly ) {
1423 unset( $tagInfo['description'] );
1424 } elseif ( $tagInfo['descriptionMsg'] ) {
1425 // Optimization: Skip the parsing if the description contains only plain text (T344352)
1426 if ( wfEscapeWikiText( $tagInfo['description'] ) !== $tagInfo['description'] ) {
1427 $descriptionMsg = new RawMessage( $tagInfo['description'] );
1428 $tagInfo['description'] = Sanitizer::stripAllTags( $localizer->msg( $descriptionMsg )->parse() );
1429 }
1430 $tagInfo['description'] = $lang->truncateForVisual( $tagInfo['description'],
1431 self::TAG_DESC_CHARACTER_LIMIT );
1432 }
1433 unset( $tagInfo['labelMsg'] );
1434 unset( $tagInfo['descriptionMsg'] );
1435 }
1436
1437 // Instead of sorting by hit count (disabled for now), sort by display name
1438 usort( $tags, static function ( $a, $b ) {
1439 return strcasecmp( $a['label'], $b['label'] );
1440 } );
1441 return $tags;
1442 }
1443
1458 public static function showTagEditingUI( Authority $performer ) {
1459 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1460 return $performer->isAllowed( 'changetags' ) && (bool)$changeTagsStore->listExplicitlyDefinedTags();
1461 }
1462}
1463
1465class_alias( ChangeTags::class, 'ChangeTags' );
const NS_MEDIAWIKI
Definition Defines.php:59
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Recent changes tagging.
static canCreateTag( $tag, ?Authority $performer=null)
Is it OK to allow the user to create this tag?
const TAG_REVERTED
The tagged edit is reverted by a subsequent edit (which is tagged by one of TAG_ROLLBACK,...
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,...
static deleteTagEverywhere( $tag)
Permanently removes all traces of a tag from the DB.
static defineTag( $tag)
Set ctd_user_defined = 1 in change_tag_def without checking that the tag name is valid.
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 tagShortDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's short description.
const TAG_SERVER_SIDE_UPLOAD
This tagged edit was performed while importing media files using the importImages....
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 BYPASS_MAX_USAGE_CHECK
Flag for canDeleteTag().
const TAG_REMOVED_REDIRECT
The tagged edit turns a redirect page into a non-redirect.
static buildTagFilterSelector( $selected='', $ooui=false, ?IContextSource $context=null, bool $activeOnly=self::TAG_SET_ACTIVE_ONLY, bool $useAllTags=self::USE_ALL_TAGS)
Build a text box to select a change tag.
static getChangeTagListSummary(MessageLocalizer $localizer, Language $lang, bool $activeOnly=self::TAG_SET_ACTIVE_ONLY, bool $useAllTags=self::USE_ALL_TAGS)
Get information about change tags, without parsing messages, for tag filter dropdown menus.
static getChangeTagList(MessageLocalizer $localizer, Language $lang, bool $activeOnly=self::TAG_SET_ACTIVE_ONLY, bool $useAllTags=self::USE_ALL_TAGS, $labelsOnly=false)
Get information about change tags for tag filter dropdown menus.
static getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
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 makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
static listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
const TAG_SET_ACTIVE_ONLY
Constants that can be used to set the activeOnly parameter for calling self::buildCustomTagFilterSele...
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 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.
const TAG_RECREATE
The tagged edit recreates a page that has been previously deleted.
static canDeactivateTag( $tag, ?Authority $performer=null)
Is it OK to allow the user to deactivate this tag?
const TAG_BLANK
The tagged edit blanks the page (replaces it with the empty string).
const REVERT_TAGS
List of tags which denote a revert of some sort.
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.
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...
const TAG_CONTENT_MODEL_CHANGE
The tagged edit changes the content model of the page.
static isTagNameValid( $tag)
Is the tag name valid?
static listDefinedTags()
Basically lists defined tags which count even if they aren't applied to anything.
const TAG_NEW_REDIRECT
The tagged edit creates a new redirect (either by creating a new page or turning an existing page int...
const TAG_IPBLOCK_APPEAL
This tagged temporary account auto-creation was performed via Special:Mytalk from an IP address that ...
const TAG_CHANGED_REDIRECT_TARGET
The tagged edit changes the target of a redirect page.
static listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
static tagUsageStatistics()
Returns a map of any tags used on the wiki to number of edits tagged with them, ordered descending by...
static tagLongDescriptionMessage( $tag, MessageLocalizer $context)
Get the message object for the tag's long description.
static listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
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 tagDescription( $tag, MessageLocalizer $context)
Get a short description for a tag.
const USE_ALL_TAGS
Constants that can be used to set the useAllTags parameter for calling self::buildCustomTagFilterSele...
const TAG_ROLLBACK
The tagged edit is a rollback (undoes the previous edit and all immediately preceding edits by the sa...
static modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
static purgeTagCacheAll()
Invalidates the short-term cache of defined tags used by the list*DefinedTags functions,...
const TAG_MANUAL_REVERT
The tagged edit restores the page to an earlier revision.
static showTagEditingUI(Authority $performer)
Indicate whether change tag editing UI is relevant.
const TAG_REPLACE
The tagged edit removes more than 90% of the content of the page.
static formatSummaryRow( $tags, $unused, ?MessageLocalizer $localizer=null)
Creates HTML for the given tags.
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,...
const TAG_UNDO
The tagged edit is was performed via the "undo" link.
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 restrictedTagError( $msgOne, $msgMulti, $tags)
Helper function to generate a fatal status with a 'not-allowed' type error.
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 canActivateTag( $tag, ?Authority $performer=null)
Is it OK to allow the user to activate this tag?
static tagHelpLink( $tag, MessageLocalizer $context)
Get the tag's help link.
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:43
Base class for language-specific code.
Definition Language.php:68
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.
Variant of the Message class.
Class for creating new log entries and inserting them into the database.
A class containing constants representing the names of configuration variables.
const UseTagFilter
Name constant for the UseTagFilter setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
static plaintextParam( $plaintext)
Definition Message.php:1344
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:32
A StatusValue for permission errors.
Utility class for creating and reading rows in the recentchanges table.
List for logging table items.
static suggestTarget( $target, array $ids)
Suggest a target for the revision deletion Optionally override this function.1.22 Title|null
The base class for all skins.
Definition Skin.php:52
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Represents a title within MediaWiki.
Definition Title.php:69
Multi-datacenter aware caching interface.
Interface for objects which can provide a MediaWiki context on request.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:23
isAllowed(string $permission, ?PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
getUser()
Returns the performer of the actions associated with this authority.
authorizeAction(string $action, ?PermissionStatus $status=null)
Authorize an action.
Interface for objects representing user identity.
A database connection without write operations.
element(SerializerNode $parent, SerializerNode $node, $contents)