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';
113
118
122 public const BYPASS_MAX_USAGE_CHECK = 1;
123
129 private const MAX_DELETE_USES = 5000;
130
134 private const CHANGE_TAG = 'change_tag';
135
136 public const DISPLAY_TABLE_ALIAS = 'changetagdisplay';
137
147 public const TAG_SET_ACTIVE_ONLY = true;
148 public const TAG_SET_ALL = false;
149
158 public const USE_ALL_TAGS = true;
159 public const USE_SOFTWARE_TAGS_ONLY = false;
160
169 public static function getSoftwareTags( $all = false ) {
170 wfDeprecated( __METHOD__, '1.41' );
171 return MediaWikiServices::getInstance()->getChangeTagsStore()->getSoftwareTags( $all );
172 }
173
187 public static function formatSummaryRow( $tags, $unused, ?MessageLocalizer $localizer = null ) {
188 if ( $tags === '' || $tags === null ) {
189 return [ '', [] ];
190 }
191 if ( !$localizer ) {
192 $localizer = RequestContext::getMain();
193 }
194
195 $classes = [];
196
197 $tags = explode( ',', $tags );
198 $order = array_flip( MediaWikiServices::getInstance()->getChangeTagsStore()->listDefinedTags() );
199 usort( $tags, static function ( $a, $b ) use ( $order ) {
200 return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
201 } );
202
203 $displayTags = [];
204 foreach ( $tags as $tag ) {
205 if ( $tag === '' ) {
206 continue;
207 }
208 $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
209 $description = self::tagDescription( $tag, $localizer );
210 if ( $description === false ) {
211 continue;
212 }
213 $displayTags[] = Html::rawElement(
214 'span',
215 [ 'class' => 'mw-tag-marker ' .
216 Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
217 $description
218 );
219 }
220
221 if ( !$displayTags ) {
222 return [ '', $classes ];
223 }
224
225 $markers = $localizer->msg( 'tag-list-wrapper' )
226 ->numParams( count( $displayTags ) )
227 ->rawParams( implode( ' ', $displayTags ) )
228 ->parse();
229 $markers = Html::rawElement( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
230
231 return [ $markers, $classes ];
232 }
233
247 public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
248 $msg = $context->msg( "tag-$tag" );
249 if ( !$msg->exists() ) {
250 // No such message
251 // Pass through ->msg(), even though it seems redundant, to avoid requesting
252 // the user's language from session-less entry points (T227233)
253 return $context->msg( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) );
254 }
255 if ( $msg->isDisabled() ) {
256 // The message exists but is disabled, hide the tag.
257 return false;
258 }
259
260 // Message exists and isn't disabled, use it.
261 return $msg;
262 }
263
276 public static function tagHelpLink( $tag, MessageLocalizer $context ) {
277 $msg = $context->msg( "tag-$tag-helppage" )->inContentLanguage();
278 if ( !$msg->isDisabled() ) {
279 return Skin::makeInternalOrExternalUrl( $msg->text() ) ?: null;
280 }
281 return null;
282 }
283
295 public static function tagDescription( $tag, MessageLocalizer $context ) {
296 $msg = self::tagShortDescriptionMessage( $tag, $context );
297 $link = self::tagHelpLink( $tag, $context );
298 if ( $msg && $link ) {
299 $label = $msg->parse();
300 // Avoid invalid HTML caused by link wrapping if the label already contains a link
301 if ( !str_contains( $label, '<a ' ) ) {
302 return Html::rawElement( 'a', [ 'href' => $link ], $label );
303 }
304 }
305 return $msg ? $msg->parse() : false;
306 }
307
320 public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
321 $msg = $context->msg( "tag-$tag-description" );
322 return $msg->isDisabled() ? false : $msg;
323 }
324
339 public static function addTags( $tags, $rc_id = null, $rev_id = null,
340 $log_id = null, $params = null, ?RecentChange $rc = null
341 ) {
342 wfDeprecated( __METHOD__, '1.41' );
343 return MediaWikiServices::getInstance()->getChangeTagsStore()->addTags(
344 $tags, $rc_id, $rev_id, $log_id, $params, $rc
345 );
346 }
347
378 public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
379 &$rev_id = null, &$log_id = null, $params = null, ?RecentChange $rc = null,
380 ?UserIdentity $user = null
381 ) {
382 wfDeprecated( __METHOD__, '1.41' );
383 return MediaWikiServices::getInstance()->getChangeTagsStore()->updateTags(
384 $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, $rc, $user
385 );
386 }
387
400 public static function getTagsWithData(
401 IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null
402 ) {
403 wfDeprecated( __METHOD__, '1.41' );
404 return MediaWikiServices::getInstance()->getChangeTagsStore()->getTagsWithData( $db, $rc_id, $rev_id, $log_id );
405 }
406
418 public static function getTags( IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
419 wfDeprecated( __METHOD__, '1.41' );
420 return MediaWikiServices::getInstance()->getChangeTagsStore()->getTags( $db, $rc_id, $rev_id, $log_id );
421 }
422
433 protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
434 $lang = RequestContext::getMain()->getLanguage();
435 $tags = array_values( $tags );
436 $count = count( $tags );
437 $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
438 $lang->commaList( $tags ), $count );
439 $status->value = $tags;
440 return $status;
441 }
442
457 public static function canAddTagsAccompanyingChange(
458 array $tags,
459 ?Authority $performer = null,
460 $checkBlock = true
461 ) {
462 $user = null;
463 $services = MediaWikiServices::getInstance();
464 if ( $performer !== null ) {
465 if ( !$performer->isAllowed( 'applychangetags' ) ) {
466 return Status::newFatal( 'tags-apply-no-permission' );
467 }
468
469 if ( $checkBlock && $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
470 return Status::newFatal(
471 'tags-apply-blocked',
472 $performer->getUser()->getName()
473 );
474 }
475
476 // ChangeTagsAllowedAdd hook still needs a full User object
477 $user = $services->getUserFactory()->newFromAuthority( $performer );
478 }
479
480 // to be applied, a tag has to be explicitly defined
481 $allowedTags = $services->getChangeTagsStore()->listExplicitlyDefinedTags();
482 ( new HookRunner( $services->getHookContainer() ) )->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
483 $disallowedTags = array_diff( $tags, $allowedTags );
484 if ( $disallowedTags ) {
485 return self::restrictedTagError( 'tags-apply-not-allowed-one',
486 'tags-apply-not-allowed-multi', $disallowedTags );
487 }
488
489 return Status::newGood();
490 }
491
506 public static function canUpdateTags(
507 array $tagsToAdd,
508 array $tagsToRemove,
509 ?Authority $performer = null
510 ) {
511 if ( $performer !== null ) {
512 if ( !$performer->isDefinitelyAllowed( 'changetags' ) ) {
513 return Status::newFatal( 'tags-update-no-permission' );
514 }
515
516 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
517 return Status::newFatal(
518 'tags-update-blocked',
519 $performer->getUser()->getName()
520 );
521 }
522 }
523
524 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
525 if ( $tagsToAdd ) {
526 // to be added, a tag has to be explicitly defined
527 // @todo Allow extensions to define tags that can be applied by users...
528 $explicitlyDefinedTags = $changeTagsStore->listExplicitlyDefinedTags();
529 $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
530 if ( $diff ) {
531 return self::restrictedTagError( 'tags-update-add-not-allowed-one',
532 'tags-update-add-not-allowed-multi', $diff );
533 }
534 }
535
536 if ( $tagsToRemove ) {
537 // to be removed, a tag must not be defined by an extension, or equivalently it
538 // has to be either explicitly defined or not defined at all
539 // (assuming no edge case of a tag both explicitly-defined and extension-defined)
540 $softwareDefinedTags = $changeTagsStore->listSoftwareDefinedTags();
541 $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
542 if ( $intersect ) {
543 return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
544 'tags-update-remove-not-allowed-multi', $intersect );
545 }
546 }
547
548 return Status::newGood();
549 }
550
581 public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
582 $rc_id, $rev_id, $log_id, $params, string $reason, Authority $performer
583 ) {
584 if ( !$tagsToAdd && !$tagsToRemove ) {
585 // no-op, don't bother
586 return Status::newGood( (object)[
587 'logId' => null,
588 'addedTags' => [],
589 'removedTags' => [],
590 ] );
591 }
592
593 $tagsToAdd ??= [];
594 $tagsToRemove ??= [];
595
596 // are we allowed to do this?
597 $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $performer );
598 if ( !$result->isOK() ) {
599 $result->value = null;
600 return $result;
601 }
602
603 // basic rate limiting
604 $status = PermissionStatus::newEmpty();
605 if ( !$performer->authorizeAction( 'changetags', $status ) ) {
606 return Status::wrap( $status );
607 }
608
609 // do it!
610 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
611 [ $tagsAdded, $tagsRemoved, $initialTags ] = $changeTagsStore->updateTags( $tagsToAdd,
612 $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $performer->getUser() );
613 if ( !$tagsAdded && !$tagsRemoved ) {
614 // no-op, don't log it
615 return Status::newGood( (object)[
616 'logId' => null,
617 'addedTags' => [],
618 'removedTags' => [],
619 ] );
620 }
621
622 // log it
623 $logEntry = new ManualLogEntry( 'tag', 'update' );
624 $logEntry->setPerformer( $performer->getUser() );
625 $logEntry->setComment( $reason );
626
627 // find the appropriate target page
628 if ( $rev_id ) {
629 $revisionRecord = MediaWikiServices::getInstance()
630 ->getRevisionLookup()
631 ->getRevisionById( $rev_id );
632 if ( $revisionRecord ) {
633 $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
634 }
635 } elseif ( $log_id ) {
636 // This function is from revision deletion logic and has nothing to do with
637 // change tags, but it appears to be the only other place in core where we
638 // perform logged actions on log items.
639 $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
640 }
641
642 if ( !$logEntry->getTarget() ) {
643 // target is required, so we have to set something
644 $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
645 }
646
647 $logParams = [
648 '4::revid' => $rev_id,
649 '5::logid' => $log_id,
650 '6:list:tagsAdded' => $tagsAdded,
651 '7:number:tagsAddedCount' => count( $tagsAdded ),
652 '8:list:tagsRemoved' => $tagsRemoved,
653 '9:number:tagsRemovedCount' => count( $tagsRemoved ),
654 'initialTags' => $initialTags,
655 ];
656 $logEntry->setParameters( $logParams );
657 $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
658
659 $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
660 $logId = $logEntry->insert( $dbw );
661 // Only send this to UDP, not RC, similar to patrol events
662 $logEntry->publish( $logId, 'udp' );
663
664 return Status::newGood( (object)[
665 'logId' => $logId,
666 'addedTags' => $tagsAdded,
667 'removedTags' => $tagsRemoved,
668 ] );
669 }
670
691 public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
692 &$join_conds, &$options, $filter_tag = '', bool $exclude = false
693 ) {
694 wfDeprecated( __METHOD__, '1.41' );
695 MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQuery(
696 $tables,
697 $fields,
698 $conds,
699 $join_conds,
700 $options,
701 $filter_tag,
702 $exclude
703 );
704 }
705
715 public static function getDisplayTableName() {
716 wfDeprecated( __METHOD__, '1.41' );
717 return self::CHANGE_TAG;
718 }
719
728 public static function makeTagSummarySubquery( $tables ) {
729 wfDeprecated( __METHOD__, '1.41' );
730 return MediaWikiServices::getInstance()->getChangeTagsStore()->makeTagSummarySubquery( $tables );
731 }
732
748 public static function buildTagFilterSelector(
749 $selected = '', $ooui = false, ?IContextSource $context = null,
750 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
751 bool $useAllTags = self::USE_ALL_TAGS
752 ) {
753 if ( !$context ) {
754 $context = RequestContext::getMain();
755 }
756
757 $config = $context->getConfig();
758 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
759 if ( !$config->get( MainConfigNames::UseTagFilter ) ||
760 !count( $changeTagsStore->listDefinedTags() ) ) {
761 return null;
762 }
763
765 $context,
766 $context->getLanguage(),
767 $activeOnly,
768 $useAllTags,
769 true
770 );
771
772 $autocomplete = [];
773 foreach ( $tags as $tagInfo ) {
774 $autocomplete[ $tagInfo['label'] ] = $tagInfo['name'];
775 }
776
777 $data = [];
778 $data[0] = Html::rawElement(
779 'label',
780 [ 'for' => 'tagfilter' ],
781 $context->msg( 'tag-filter' )->parse()
782 );
783
784 if ( $ooui ) {
785 $options = Html::listDropdownOptionsOoui( $autocomplete );
786
787 $data[1] = new \OOUI\ComboBoxInputWidget( [
788 'id' => 'tagfilter',
789 'name' => 'tagfilter',
790 'value' => $selected,
791 'classes' => 'mw-tagfilter-input',
792 'options' => $options,
793 ] );
794 } else {
795 $optionsHtml = '';
796 foreach ( $autocomplete as $label => $name ) {
797 $optionsHtml .= Html::element( 'option', [ 'value' => $name ], $label );
798 }
799 $datalistHtml = Html::rawElement( 'datalist', [ 'id' => 'tagfilter-datalist' ], $optionsHtml );
800
801 $data[1] = Html::input(
802 'tagfilter',
803 $selected,
804 'text',
805 [
806 'class' => [ 'mw-tagfilter-input', 'mw-ui-input', 'mw-ui-input-inline' ],
807 'size' => 20,
808 'id' => 'tagfilter',
809 'list' => 'tagfilter-datalist',
810 ]
811 ) . $datalistHtml;
812 }
813
814 return $data;
815 }
816
826 public static function defineTag( $tag ) {
827 wfDeprecated( __METHOD__, '1.41' );
828 MediaWikiServices::getInstance()->getChangeTagsStore()->defineTag( $tag );
829 }
830
840 public static function canActivateTag( $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 // defined tags cannot be activated (a defined tag is either extension-
854 // defined, in which case the extension chooses whether or not to active it;
855 // or user-defined, in which case it is considered active)
856 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
857 $definedTags = $changeTagsStore->listDefinedTags();
858 if ( in_array( $tag, $definedTags ) ) {
859 return Status::newFatal( 'tags-activate-not-allowed', $tag );
860 }
861
862 // non-existing tags cannot be activated
863 if ( !isset( $changeTagsStore->tagUsageStatistics()[$tag] ) ) { // we already know the tag is undefined
864 return Status::newFatal( 'tags-activate-not-found', $tag );
865 }
866
867 return Status::newGood();
868 }
869
887 public static function activateTagWithChecks( string $tag, string $reason, Authority $performer,
888 bool $ignoreWarnings = false, array $logEntryTags = []
889 ) {
890 // are we allowed to do this?
891 $result = self::canActivateTag( $tag, $performer );
892 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
893 $result->value = null;
894 return $result;
895 }
896 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
897
898 $changeTagsStore->defineTag( $tag );
899
900 $logId = $changeTagsStore->logTagManagementAction( 'activate', $tag, $reason, $performer->getUser(),
901 null, $logEntryTags );
902
903 return Status::newGood( $logId );
904 }
905
915 public static function canDeactivateTag( $tag, ?Authority $performer = null ) {
916 if ( $performer !== null ) {
917 if ( !$performer->isAllowed( 'managechangetags' ) ) {
918 return Status::newFatal( 'tags-manage-no-permission' );
919 }
920 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
921 return Status::newFatal(
922 'tags-manage-blocked',
923 $performer->getUser()->getName()
924 );
925 }
926 }
927
928 // only explicitly-defined tags can be deactivated
929 $explicitlyDefinedTags = MediaWikiServices::getInstance()->getChangeTagsStore()->listExplicitlyDefinedTags();
930 if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
931 return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
932 }
933 return Status::newGood();
934 }
935
953 public static function deactivateTagWithChecks( string $tag, string $reason, Authority $performer,
954 bool $ignoreWarnings = false, array $logEntryTags = []
955 ) {
956 // are we allowed to do this?
957 $result = self::canDeactivateTag( $tag, $performer );
958 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
959 $result->value = null;
960 return $result;
961 }
962 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
963
964 $changeTagsStore->undefineTag( $tag );
965
966 $logId = $changeTagsStore->logTagManagementAction( 'deactivate', $tag, $reason,
967 $performer->getUser(), null, $logEntryTags );
968
969 return Status::newGood( $logId );
970 }
971
979 public static function isTagNameValid( $tag ) {
980 // no empty tags
981 if ( $tag === '' ) {
982 return Status::newFatal( 'tags-create-no-name' );
983 }
984
985 // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
986 // pipe (used as a delimiter between multiple tags in
987 // SpecialRecentchanges and friends), or slashes (would break tag description messages in
988 // MediaWiki namespace)
989 if ( str_contains( $tag, ',' ) || str_contains( $tag, '|' ) || str_contains( $tag, '/' ) ) {
990 return Status::newFatal( 'tags-create-invalid-chars' );
991 }
992
993 // could the MediaWiki namespace description messages be created?
994 $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
995 if ( $title === null ) {
996 return Status::newFatal( 'tags-create-invalid-title-chars' );
997 }
998
999 return Status::newGood();
1000 }
1001
1014 public static function canCreateTag( $tag, ?Authority $performer = null ) {
1015 $user = null;
1016 $services = MediaWikiServices::getInstance();
1017 if ( $performer !== null ) {
1018 if ( !$performer->isAllowed( 'managechangetags' ) ) {
1019 return Status::newFatal( 'tags-manage-no-permission' );
1020 }
1021 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1022 return Status::newFatal(
1023 'tags-manage-blocked',
1024 $performer->getUser()->getName()
1025 );
1026 }
1027 // ChangeTagCanCreate hook still needs a full User object
1028 $user = $services->getUserFactory()->newFromAuthority( $performer );
1029 }
1030
1031 $status = self::isTagNameValid( $tag );
1032 if ( !$status->isGood() ) {
1033 return $status;
1034 }
1035
1036 // does the tag already exist?
1037 $changeTagsStore = $services->getChangeTagsStore();
1038 if (
1039 isset( $changeTagsStore->tagUsageStatistics()[$tag] ) ||
1040 in_array( $tag, $changeTagsStore->listDefinedTags() )
1041 ) {
1042 return Status::newFatal( 'tags-create-already-exists', $tag );
1043 }
1044
1045 // check with hooks
1046 $canCreateResult = Status::newGood();
1047 ( new HookRunner( $services->getHookContainer() ) )->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1048 return $canCreateResult;
1049 }
1050
1070 public static function createTagWithChecks( string $tag, string $reason, Authority $performer,
1071 bool $ignoreWarnings = false, array $logEntryTags = []
1072 ) {
1073 // are we allowed to do this?
1074 $result = self::canCreateTag( $tag, $performer );
1075 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1076 $result->value = null;
1077 return $result;
1078 }
1079
1080 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1081 $changeTagsStore->defineTag( $tag );
1082 $logId = $changeTagsStore->logTagManagementAction( 'create', $tag, $reason,
1083 $performer->getUser(), null, $logEntryTags );
1084
1085 return Status::newGood( $logId );
1086 }
1087
1101 public static function deleteTagEverywhere( $tag ) {
1102 wfDeprecated( __METHOD__, '1.41' );
1103 return MediaWikiServices::getInstance()->getChangeTagsStore()->deleteTagEverywhere( $tag );
1104 }
1105
1118 public static function canDeleteTag( $tag, ?Authority $performer = null, int $flags = 0 ) {
1119 $user = null;
1120 $services = MediaWikiServices::getInstance();
1121 if ( $performer !== null ) {
1122 if ( !$performer->isAllowed( 'deletechangetags' ) ) {
1123 return Status::newFatal( 'tags-delete-no-permission' );
1124 }
1125 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1126 return Status::newFatal(
1127 'tags-manage-blocked',
1128 $performer->getUser()->getName()
1129 );
1130 }
1131 // ChangeTagCanDelete hook still needs a full User object
1132 $user = $services->getUserFactory()->newFromAuthority( $performer );
1133 }
1134
1135 $changeTagsStore = $services->getChangeTagsStore();
1136 $tagUsage = $changeTagsStore->tagUsageStatistics();
1137 if (
1138 !isset( $tagUsage[$tag] ) &&
1139 !in_array( $tag, $changeTagsStore->listDefinedTags() )
1140 ) {
1141 return Status::newFatal( 'tags-delete-not-found', $tag );
1142 }
1143
1144 if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1145 isset( $tagUsage[$tag] ) &&
1146 $tagUsage[$tag] > self::MAX_DELETE_USES
1147 ) {
1148 return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1149 }
1150
1151 $softwareDefined = $changeTagsStore->listSoftwareDefinedTags();
1152 if ( in_array( $tag, $softwareDefined ) ) {
1153 // extension-defined tags can't be deleted unless the extension
1154 // specifically allows it
1155 $status = Status::newFatal( 'tags-delete-not-allowed' );
1156 } else {
1157 // user-defined tags are deletable unless otherwise specified
1158 $status = Status::newGood();
1159 }
1160
1161 ( new HookRunner( $services->getHookContainer() ) )->onChangeTagCanDelete( $tag, $user, $status );
1162 return $status;
1163 }
1164
1182 public static function deleteTagWithChecks( string $tag, string $reason, Authority $performer,
1183 bool $ignoreWarnings = false, array $logEntryTags = []
1184 ) {
1185 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1186 // are we allowed to do this?
1187 $result = self::canDeleteTag( $tag, $performer );
1188 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1189 $result->value = null;
1190 return $result;
1191 }
1192
1193 // store the tag usage statistics
1194 $hitcount = $changeTagsStore->tagUsageStatistics()[$tag] ?? 0;
1195
1196 // do it!
1197 $deleteResult = $changeTagsStore->deleteTagEverywhere( $tag );
1198 if ( !$deleteResult->isOK() ) {
1199 return $deleteResult;
1200 }
1201
1202 // log it
1203 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1204 $logId = $changeTagsStore->logTagManagementAction( 'delete', $tag, $reason, $performer->getUser(),
1205 $hitcount, $logEntryTags );
1206
1207 $deleteResult->value = $logId;
1208 return $deleteResult;
1209 }
1210
1218 public static function listSoftwareActivatedTags() {
1219 wfDeprecated( __METHOD__, '1.41' );
1220 return MediaWikiServices::getInstance()->getChangeTagsStore()->listSoftwareActivatedTags();
1221 }
1222
1231 public static function listDefinedTags() {
1232 wfDeprecated( __METHOD__, '1.41' );
1233 return MediaWikiServices::getInstance()->getChangeTagsStore()->listDefinedTags();
1234 }
1235
1245 public static function listExplicitlyDefinedTags() {
1246 wfDeprecated( __METHOD__, '1.41' );
1247 return MediaWikiServices::getInstance()->getChangeTagsStore()->listExplicitlyDefinedTags();
1248 }
1249
1260 public static function listSoftwareDefinedTags() {
1261 wfDeprecated( __METHOD__, '1.41' );
1262 return MediaWikiServices::getInstance()->getChangeTagsStore()->listSoftwareDefinedTags();
1263 }
1264
1271 public static function purgeTagCacheAll() {
1272 wfDeprecated( __METHOD__, '1.41' );
1273 MediaWikiServices::getInstance()->getChangeTagsStore()->purgeTagCacheAll();
1274 }
1275
1284 public static function tagUsageStatistics() {
1285 wfDeprecated( __METHOD__, '1.41' );
1286 return MediaWikiServices::getInstance()->getChangeTagsStore()->tagUsageStatistics();
1287 }
1288
1293 private const TAG_DESC_CHARACTER_LIMIT = 120;
1294
1323 public static function getChangeTagListSummary(
1324 MessageLocalizer $localizer,
1325 Language $lang,
1326 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
1327 bool $useAllTags = self::USE_ALL_TAGS
1328 ) {
1329 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1330
1331 if ( $useAllTags ) {
1332 $tagKeys = $changeTagsStore->listDefinedTags();
1333 $cacheKey = 'tags-list-summary';
1334 } else {
1335 $tagKeys = $changeTagsStore->getCoreDefinedTags();
1336 $cacheKey = 'core-software-tags-summary';
1337 }
1338
1339 // if $tagHitCounts exists, check against it later to determine whether or not to omit tags
1340 $tagHitCounts = null;
1341 if ( $activeOnly ) {
1342 $tagHitCounts = $changeTagsStore->tagUsageStatistics();
1343 } else {
1344 // The full set of tags should use a different cache key than the subset
1345 $cacheKey .= '-all';
1346 }
1347
1348 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1349 return $cache->getWithSetCallback(
1350 $cache->makeKey( $cacheKey, $lang->getCode() ),
1351 WANObjectCache::TTL_DAY,
1352 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $localizer, $tagKeys, $tagHitCounts ) {
1353 $result = [];
1354 foreach ( $tagKeys as $tagName ) {
1355 // Only list tags that are still actively defined
1356 if ( $tagHitCounts !== null ) {
1357 // Only list tags with more than 0 hits
1358 $hits = $tagHitCounts[$tagName] ?? 0;
1359 if ( $hits <= 0 ) {
1360 continue;
1361 }
1362 }
1363
1364 $labelMsg = self::tagShortDescriptionMessage( $tagName, $localizer );
1365 $helpLink = self::tagHelpLink( $tagName, $localizer );
1366 $descriptionMsg = self::tagLongDescriptionMessage( $tagName, $localizer );
1367 // Don't cache the message object, use the correct MessageLocalizer to parse later.
1368 $result[] = [
1369 'name' => $tagName,
1370 'labelMsg' => (bool)$labelMsg,
1371 'label' => $labelMsg ? $labelMsg->plain() : $tagName,
1372 'descriptionMsg' => (bool)$descriptionMsg,
1373 'description' => $descriptionMsg ? $descriptionMsg->plain() : '',
1374 'helpLink' => $helpLink,
1375 'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
1376 ];
1377 }
1378 return $result;
1379 }
1380 );
1381 }
1382
1398 public static function getChangeTagList(
1399 MessageLocalizer $localizer, Language $lang,
1400 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY, bool $useAllTags = self::USE_ALL_TAGS,
1401 $labelsOnly = false
1402 ) {
1403 $tags = self::getChangeTagListSummary( $localizer, $lang, $activeOnly, $useAllTags );
1404
1405 foreach ( $tags as &$tagInfo ) {
1406 if ( $tagInfo['labelMsg'] ) {
1407 // Optimization: Skip the parsing if the label contains only plain text (T344352)
1408 if ( wfEscapeWikiText( $tagInfo['label'] ) !== $tagInfo['label'] ) {
1409 // Use localizer with the correct page title to parse plain message from the cache.
1410 $labelMsg = new RawMessage( $tagInfo['label'] );
1411 $tagInfo['label'] = Sanitizer::stripAllTags( $localizer->msg( $labelMsg )->parse() );
1412 }
1413 } else {
1414 $tagInfo['label'] = $localizer->msg( 'tag-hidden', $tagInfo['name'] )->text();
1415 }
1416 // Optimization: Skip parsing the descriptions if not needed by the caller (T344352)
1417 if ( $labelsOnly ) {
1418 unset( $tagInfo['description'] );
1419 } elseif ( $tagInfo['descriptionMsg'] ) {
1420 // Optimization: Skip the parsing if the description contains only plain text (T344352)
1421 if ( wfEscapeWikiText( $tagInfo['description'] ) !== $tagInfo['description'] ) {
1422 $descriptionMsg = new RawMessage( $tagInfo['description'] );
1423 $tagInfo['description'] = Sanitizer::stripAllTags( $localizer->msg( $descriptionMsg )->parse() );
1424 }
1425 $tagInfo['description'] = $lang->truncateForVisual( $tagInfo['description'],
1426 self::TAG_DESC_CHARACTER_LIMIT );
1427 }
1428 unset( $tagInfo['labelMsg'] );
1429 unset( $tagInfo['descriptionMsg'] );
1430 }
1431
1432 // Instead of sorting by hit count (disabled for now), sort by display name
1433 usort( $tags, static function ( $a, $b ) {
1434 return strcasecmp( $a['label'], $b['label'] );
1435 } );
1436 return $tags;
1437 }
1438
1453 public static function showTagEditingUI( Authority $performer ) {
1454 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1455 return $performer->isAllowed( 'changetags' ) && (bool)$changeTagsStore->listExplicitlyDefinedTags();
1456 }
1457}
1458
1460class_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_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:1341
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:32
A StatusValue for permission errors.
Utility class for creating and reading rows in the recentchanges table.
The base class for all skins.
Definition Skin.php:43
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
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
Multi-datacenter aware caching interface.
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: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.
Interface for localizing messages in MediaWiki.
msg( $key,... $params)
This is the method for getting translated interface messages.
A database connection without write operations.
element(SerializerNode $parent, SerializerNode $node, $contents)