MediaWiki master
ChangeTags.php
Go to the documentation of this file.
1<?php
22
46
68 public const TAG_CONTENT_MODEL_CHANGE = 'mw-contentmodelchange';
73 public const TAG_NEW_REDIRECT = 'mw-new-redirect';
77 public const TAG_REMOVED_REDIRECT = 'mw-removed-redirect';
81 public const TAG_CHANGED_REDIRECT_TARGET = 'mw-changed-redirect-target';
85 public const TAG_BLANK = 'mw-blank';
89 public const TAG_REPLACE = 'mw-replace';
93 public const TAG_RECREATE = 'mw-recreated';
101 public const TAG_ROLLBACK = 'mw-rollback';
108 public const TAG_UNDO = 'mw-undo';
114 public const TAG_MANUAL_REVERT = 'mw-manual-revert';
122 public const TAG_REVERTED = 'mw-reverted';
126 public const TAG_SERVER_SIDE_UPLOAD = 'mw-server-side-upload';
127
132
136 public const BYPASS_MAX_USAGE_CHECK = 1;
137
143 private const MAX_DELETE_USES = 5000;
144
148 private const CHANGE_TAG = 'change_tag';
149
150 public const DISPLAY_TABLE_ALIAS = 'changetagdisplay';
151
161 public const TAG_SET_ACTIVE_ONLY = true;
162 public const TAG_SET_ALL = false;
163
172 public const USE_ALL_TAGS = true;
173 public const USE_SOFTWARE_TAGS_ONLY = false;
174
183 public static function getSoftwareTags( $all = false ) {
184 wfDeprecated( __METHOD__, '1.41' );
185 return MediaWikiServices::getInstance()->getChangeTagsStore()->getSoftwareTags( $all );
186 }
187
201 public static function formatSummaryRow( $tags, $unused, ?MessageLocalizer $localizer = null ) {
202 if ( $tags === '' || $tags === null ) {
203 return [ '', [] ];
204 }
205 if ( !$localizer ) {
206 $localizer = RequestContext::getMain();
207 }
208
209 $classes = [];
210
211 $tags = explode( ',', $tags );
212 $order = array_flip( MediaWikiServices::getInstance()->getChangeTagsStore()->listDefinedTags() );
213 usort( $tags, static function ( $a, $b ) use ( $order ) {
214 return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
215 } );
216
217 $displayTags = [];
218 foreach ( $tags as $tag ) {
219 if ( $tag === '' ) {
220 continue;
221 }
222 $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
223 $description = self::tagDescription( $tag, $localizer );
224 if ( $description === false ) {
225 continue;
226 }
227 $displayTags[] = Html::rawElement(
228 'span',
229 [ 'class' => 'mw-tag-marker ' .
230 Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ],
231 $description
232 );
233 }
234
235 if ( !$displayTags ) {
236 return [ '', $classes ];
237 }
238
239 $markers = $localizer->msg( 'tag-list-wrapper' )
240 ->numParams( count( $displayTags ) )
241 ->rawParams( implode( ' ', $displayTags ) )
242 ->parse();
243 $markers = Html::rawElement( 'span', [ 'class' => 'mw-tag-markers' ], $markers );
244
245 return [ $markers, $classes ];
246 }
247
261 public static function tagShortDescriptionMessage( $tag, MessageLocalizer $context ) {
262 $msg = $context->msg( "tag-$tag" );
263 if ( !$msg->exists() ) {
264 // No such message
265 // Pass through ->msg(), even though it seems redundant, to avoid requesting
266 // the user's language from session-less entry points (T227233)
267 return $context->msg( new RawMessage( '$1', [ Message::plaintextParam( $tag ) ] ) );
268 }
269 if ( $msg->isDisabled() ) {
270 // The message exists but is disabled, hide the tag.
271 return false;
272 }
273
274 // Message exists and isn't disabled, use it.
275 return $msg;
276 }
277
290 public static function tagHelpLink( $tag, MessageLocalizer $context ) {
291 $msg = $context->msg( "tag-$tag-helppage" )->inContentLanguage();
292 if ( !$msg->isDisabled() ) {
293 return Skin::makeInternalOrExternalUrl( $msg->text() ) ?: null;
294 }
295 return null;
296 }
297
309 public static function tagDescription( $tag, MessageLocalizer $context ) {
310 $msg = self::tagShortDescriptionMessage( $tag, $context );
311 $link = self::tagHelpLink( $tag, $context );
312 if ( $msg && $link ) {
313 $label = $msg->parse();
314 // Avoid invalid HTML caused by link wrapping if the label already contains a link
315 if ( !str_contains( $label, '<a ' ) ) {
316 return Html::rawElement( 'a', [ 'href' => $link ], $label );
317 }
318 }
319 return $msg ? $msg->parse() : false;
320 }
321
334 public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
335 $msg = $context->msg( "tag-$tag-description" );
336 return $msg->isDisabled() ? false : $msg;
337 }
338
353 public static function addTags( $tags, $rc_id = null, $rev_id = null,
354 $log_id = null, $params = null, ?RecentChange $rc = null
355 ) {
356 wfDeprecated( __METHOD__, '1.41' );
357 return MediaWikiServices::getInstance()->getChangeTagsStore()->addTags(
358 $tags, $rc_id, $rev_id, $log_id, $params, $rc
359 );
360 }
361
392 public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
393 &$rev_id = null, &$log_id = null, $params = null, ?RecentChange $rc = null,
394 ?UserIdentity $user = null
395 ) {
396 wfDeprecated( __METHOD__, '1.41' );
397 return MediaWikiServices::getInstance()->getChangeTagsStore()->updateTags(
398 $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, $rc, $user
399 );
400 }
401
414 public static function getTagsWithData(
415 IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null
416 ) {
417 wfDeprecated( __METHOD__, '1.41' );
418 return MediaWikiServices::getInstance()->getChangeTagsStore()->getTagsWithData( $db, $rc_id, $rev_id, $log_id );
419 }
420
432 public static function getTags( IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
433 wfDeprecated( __METHOD__, '1.41' );
434 return MediaWikiServices::getInstance()->getChangeTagsStore()->getTags( $db, $rc_id, $rev_id, $log_id );
435 }
436
447 protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
448 $lang = RequestContext::getMain()->getLanguage();
449 $tags = array_values( $tags );
450 $count = count( $tags );
451 $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
452 $lang->commaList( $tags ), $count );
453 $status->value = $tags;
454 return $status;
455 }
456
471 public static function canAddTagsAccompanyingChange(
472 array $tags,
473 ?Authority $performer = null,
474 $checkBlock = true
475 ) {
476 $user = null;
477 $services = MediaWikiServices::getInstance();
478 if ( $performer !== null ) {
479 if ( !$performer->isAllowed( 'applychangetags' ) ) {
480 return Status::newFatal( 'tags-apply-no-permission' );
481 }
482
483 if ( $checkBlock && $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
484 return Status::newFatal(
485 'tags-apply-blocked',
486 $performer->getUser()->getName()
487 );
488 }
489
490 // ChangeTagsAllowedAdd hook still needs a full User object
491 $user = $services->getUserFactory()->newFromAuthority( $performer );
492 }
493
494 // to be applied, a tag has to be explicitly defined
495 $allowedTags = $services->getChangeTagsStore()->listExplicitlyDefinedTags();
496 ( new HookRunner( $services->getHookContainer() ) )->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
497 $disallowedTags = array_diff( $tags, $allowedTags );
498 if ( $disallowedTags ) {
499 return self::restrictedTagError( 'tags-apply-not-allowed-one',
500 'tags-apply-not-allowed-multi', $disallowedTags );
501 }
502
503 return Status::newGood();
504 }
505
520 public static function canUpdateTags(
521 array $tagsToAdd,
522 array $tagsToRemove,
523 ?Authority $performer = null
524 ) {
525 if ( $performer !== null ) {
526 if ( !$performer->isDefinitelyAllowed( 'changetags' ) ) {
527 return Status::newFatal( 'tags-update-no-permission' );
528 }
529
530 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
531 return Status::newFatal(
532 'tags-update-blocked',
533 $performer->getUser()->getName()
534 );
535 }
536 }
537
538 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
539 if ( $tagsToAdd ) {
540 // to be added, a tag has to be explicitly defined
541 // @todo Allow extensions to define tags that can be applied by users...
542 $explicitlyDefinedTags = $changeTagsStore->listExplicitlyDefinedTags();
543 $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
544 if ( $diff ) {
545 return self::restrictedTagError( 'tags-update-add-not-allowed-one',
546 'tags-update-add-not-allowed-multi', $diff );
547 }
548 }
549
550 if ( $tagsToRemove ) {
551 // to be removed, a tag must not be defined by an extension, or equivalently it
552 // has to be either explicitly defined or not defined at all
553 // (assuming no edge case of a tag both explicitly-defined and extension-defined)
554 $softwareDefinedTags = $changeTagsStore->listSoftwareDefinedTags();
555 $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
556 if ( $intersect ) {
557 return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
558 'tags-update-remove-not-allowed-multi', $intersect );
559 }
560 }
561
562 return Status::newGood();
563 }
564
595 public static function updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
596 $rc_id, $rev_id, $log_id, $params, string $reason, Authority $performer
597 ) {
598 if ( !$tagsToAdd && !$tagsToRemove ) {
599 // no-op, don't bother
600 return Status::newGood( (object)[
601 'logId' => null,
602 'addedTags' => [],
603 'removedTags' => [],
604 ] );
605 }
606
607 $tagsToAdd ??= [];
608 $tagsToRemove ??= [];
609
610 // are we allowed to do this?
611 $result = self::canUpdateTags( $tagsToAdd, $tagsToRemove, $performer );
612 if ( !$result->isOK() ) {
613 $result->value = null;
614 return $result;
615 }
616
617 // basic rate limiting
618 $status = PermissionStatus::newEmpty();
619 if ( !$performer->authorizeAction( 'changetags', $status ) ) {
620 return Status::wrap( $status );
621 }
622
623 // do it!
624 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
625 [ $tagsAdded, $tagsRemoved, $initialTags ] = $changeTagsStore->updateTags( $tagsToAdd,
626 $tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $performer->getUser() );
627 if ( !$tagsAdded && !$tagsRemoved ) {
628 // no-op, don't log it
629 return Status::newGood( (object)[
630 'logId' => null,
631 'addedTags' => [],
632 'removedTags' => [],
633 ] );
634 }
635
636 // log it
637 $logEntry = new ManualLogEntry( 'tag', 'update' );
638 $logEntry->setPerformer( $performer->getUser() );
639 $logEntry->setComment( $reason );
640
641 // find the appropriate target page
642 if ( $rev_id ) {
643 $revisionRecord = MediaWikiServices::getInstance()
644 ->getRevisionLookup()
645 ->getRevisionById( $rev_id );
646 if ( $revisionRecord ) {
647 $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
648 }
649 } elseif ( $log_id ) {
650 // This function is from revision deletion logic and has nothing to do with
651 // change tags, but it appears to be the only other place in core where we
652 // perform logged actions on log items.
653 $logEntry->setTarget( RevDelLogList::suggestTarget( null, [ $log_id ] ) );
654 }
655
656 if ( !$logEntry->getTarget() ) {
657 // target is required, so we have to set something
658 $logEntry->setTarget( SpecialPage::getTitleFor( 'Tags' ) );
659 }
660
661 $logParams = [
662 '4::revid' => $rev_id,
663 '5::logid' => $log_id,
664 '6:list:tagsAdded' => $tagsAdded,
665 '7:number:tagsAddedCount' => count( $tagsAdded ),
666 '8:list:tagsRemoved' => $tagsRemoved,
667 '9:number:tagsRemovedCount' => count( $tagsRemoved ),
668 'initialTags' => $initialTags,
669 ];
670 $logEntry->setParameters( $logParams );
671 $logEntry->setRelations( [ 'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
672
673 $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
674 $logId = $logEntry->insert( $dbw );
675 // Only send this to UDP, not RC, similar to patrol events
676 $logEntry->publish( $logId, 'udp' );
677
678 return Status::newGood( (object)[
679 'logId' => $logId,
680 'addedTags' => $tagsAdded,
681 'removedTags' => $tagsRemoved,
682 ] );
683 }
684
706 public static function modifyDisplayQuery( &$tables, &$fields, &$conds,
707 &$join_conds, &$options, $filter_tag = '', bool $exclude = false
708 ) {
709 wfDeprecated( __METHOD__, '1.41' );
710 MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQuery(
711 $tables,
712 $fields,
713 $conds,
714 $join_conds,
715 $options,
716 $filter_tag,
717 $exclude
718 );
719 }
720
730 public static function getDisplayTableName() {
731 wfDeprecated( __METHOD__, '1.41' );
732 return self::CHANGE_TAG;
733 }
734
743 public static function makeTagSummarySubquery( $tables ) {
744 wfDeprecated( __METHOD__, '1.41' );
745 return MediaWikiServices::getInstance()->getChangeTagsStore()->makeTagSummarySubquery( $tables );
746 }
747
763 public static function buildTagFilterSelector(
764 $selected = '', $ooui = false, ?IContextSource $context = null,
765 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
766 bool $useAllTags = self::USE_ALL_TAGS
767 ) {
768 if ( !$context ) {
769 $context = RequestContext::getMain();
770 }
771
772 $config = $context->getConfig();
773 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
774 if ( !$config->get( MainConfigNames::UseTagFilter ) ||
775 !count( $changeTagsStore->listDefinedTags() ) ) {
776 return null;
777 }
778
780 $context,
781 $context->getLanguage(),
782 $activeOnly,
783 $useAllTags,
784 true
785 );
786
787 $autocomplete = [];
788 foreach ( $tags as $tagInfo ) {
789 $autocomplete[ $tagInfo['label'] ] = $tagInfo['name'];
790 }
791
792 $data = [];
793 $data[0] = Html::rawElement(
794 'label',
795 [ 'for' => 'tagfilter' ],
796 $context->msg( 'tag-filter' )->parse()
797 );
798
799 if ( $ooui ) {
800 $options = Html::listDropdownOptionsOoui( $autocomplete );
801
802 $data[1] = new \OOUI\ComboBoxInputWidget( [
803 'id' => 'tagfilter',
804 'name' => 'tagfilter',
805 'value' => $selected,
806 'classes' => 'mw-tagfilter-input',
807 'options' => $options,
808 ] );
809 } else {
810 $optionsHtml = '';
811 foreach ( $autocomplete as $label => $name ) {
812 $optionsHtml .= Html::element( 'option', [ 'value' => $name ], $label );
813 }
814 $datalistHtml = Html::rawElement( 'datalist', [ 'id' => 'tagfilter-datalist' ], $optionsHtml );
815
816 $data[1] = Html::input(
817 'tagfilter',
818 $selected,
819 'text',
820 [
821 'class' => [ 'mw-tagfilter-input', 'mw-ui-input', 'mw-ui-input-inline' ],
822 'size' => 20,
823 'id' => 'tagfilter',
824 'list' => 'tagfilter-datalist',
825 ]
826 ) . $datalistHtml;
827 }
828
829 return $data;
830 }
831
841 public static function defineTag( $tag ) {
842 wfDeprecated( __METHOD__, '1.41' );
843 MediaWikiServices::getInstance()->getChangeTagsStore()->defineTag( $tag );
844 }
845
855 public static function canActivateTag( $tag, ?Authority $performer = null ) {
856 if ( $performer !== null ) {
857 if ( !$performer->isAllowed( 'managechangetags' ) ) {
858 return Status::newFatal( 'tags-manage-no-permission' );
859 }
860 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
861 return Status::newFatal(
862 'tags-manage-blocked',
863 $performer->getUser()->getName()
864 );
865 }
866 }
867
868 // defined tags cannot be activated (a defined tag is either extension-
869 // defined, in which case the extension chooses whether or not to active it;
870 // or user-defined, in which case it is considered active)
871 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
872 $definedTags = $changeTagsStore->listDefinedTags();
873 if ( in_array( $tag, $definedTags ) ) {
874 return Status::newFatal( 'tags-activate-not-allowed', $tag );
875 }
876
877 // non-existing tags cannot be activated
878 if ( !isset( $changeTagsStore->tagUsageStatistics()[$tag] ) ) { // we already know the tag is undefined
879 return Status::newFatal( 'tags-activate-not-found', $tag );
880 }
881
882 return Status::newGood();
883 }
884
902 public static function activateTagWithChecks( string $tag, string $reason, Authority $performer,
903 bool $ignoreWarnings = false, array $logEntryTags = []
904 ) {
905 // are we allowed to do this?
906 $result = self::canActivateTag( $tag, $performer );
907 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
908 $result->value = null;
909 return $result;
910 }
911 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
912
913 $changeTagsStore->defineTag( $tag );
914
915 $logId = $changeTagsStore->logTagManagementAction( 'activate', $tag, $reason, $performer->getUser(),
916 null, $logEntryTags );
917
918 return Status::newGood( $logId );
919 }
920
930 public static function canDeactivateTag( $tag, ?Authority $performer = null ) {
931 if ( $performer !== null ) {
932 if ( !$performer->isAllowed( 'managechangetags' ) ) {
933 return Status::newFatal( 'tags-manage-no-permission' );
934 }
935 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
936 return Status::newFatal(
937 'tags-manage-blocked',
938 $performer->getUser()->getName()
939 );
940 }
941 }
942
943 // only explicitly-defined tags can be deactivated
944 $explicitlyDefinedTags = MediaWikiServices::getInstance()->getChangeTagsStore()->listExplicitlyDefinedTags();
945 if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
946 return Status::newFatal( 'tags-deactivate-not-allowed', $tag );
947 }
948 return Status::newGood();
949 }
950
968 public static function deactivateTagWithChecks( string $tag, string $reason, Authority $performer,
969 bool $ignoreWarnings = false, array $logEntryTags = []
970 ) {
971 // are we allowed to do this?
972 $result = self::canDeactivateTag( $tag, $performer );
973 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
974 $result->value = null;
975 return $result;
976 }
977 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
978
979 $changeTagsStore->undefineTag( $tag );
980
981 $logId = $changeTagsStore->logTagManagementAction( 'deactivate', $tag, $reason,
982 $performer->getUser(), null, $logEntryTags );
983
984 return Status::newGood( $logId );
985 }
986
994 public static function isTagNameValid( $tag ) {
995 // no empty tags
996 if ( $tag === '' ) {
997 return Status::newFatal( 'tags-create-no-name' );
998 }
999
1000 // tags cannot contain commas (used to be used as a delimiter in tag_summary table),
1001 // pipe (used as a delimiter between multiple tags in
1002 // SpecialRecentchanges and friends), or slashes (would break tag description messages in
1003 // MediaWiki namespace)
1004 if ( strpos( $tag, ',' ) !== false || strpos( $tag, '|' ) !== false
1005 || strpos( $tag, '/' ) !== false ) {
1006 return Status::newFatal( 'tags-create-invalid-chars' );
1007 }
1008
1009 // could the MediaWiki namespace description messages be created?
1010 $title = Title::makeTitleSafe( NS_MEDIAWIKI, "Tag-$tag-description" );
1011 if ( $title === null ) {
1012 return Status::newFatal( 'tags-create-invalid-title-chars' );
1013 }
1014
1015 return Status::newGood();
1016 }
1017
1030 public static function canCreateTag( $tag, ?Authority $performer = null ) {
1031 $user = null;
1032 $services = MediaWikiServices::getInstance();
1033 if ( $performer !== null ) {
1034 if ( !$performer->isAllowed( 'managechangetags' ) ) {
1035 return Status::newFatal( 'tags-manage-no-permission' );
1036 }
1037 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1038 return Status::newFatal(
1039 'tags-manage-blocked',
1040 $performer->getUser()->getName()
1041 );
1042 }
1043 // ChangeTagCanCreate hook still needs a full User object
1044 $user = $services->getUserFactory()->newFromAuthority( $performer );
1045 }
1046
1047 $status = self::isTagNameValid( $tag );
1048 if ( !$status->isGood() ) {
1049 return $status;
1050 }
1051
1052 // does the tag already exist?
1053 $changeTagsStore = $services->getChangeTagsStore();
1054 if (
1055 isset( $changeTagsStore->tagUsageStatistics()[$tag] ) ||
1056 in_array( $tag, $changeTagsStore->listDefinedTags() )
1057 ) {
1058 return Status::newFatal( 'tags-create-already-exists', $tag );
1059 }
1060
1061 // check with hooks
1062 $canCreateResult = Status::newGood();
1063 ( new HookRunner( $services->getHookContainer() ) )->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1064 return $canCreateResult;
1065 }
1066
1086 public static function createTagWithChecks( string $tag, string $reason, Authority $performer,
1087 bool $ignoreWarnings = false, array $logEntryTags = []
1088 ) {
1089 // are we allowed to do this?
1090 $result = self::canCreateTag( $tag, $performer );
1091 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1092 $result->value = null;
1093 return $result;
1094 }
1095
1096 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1097 $changeTagsStore->defineTag( $tag );
1098 $logId = $changeTagsStore->logTagManagementAction( 'create', $tag, $reason,
1099 $performer->getUser(), null, $logEntryTags );
1100
1101 return Status::newGood( $logId );
1102 }
1103
1117 public static function deleteTagEverywhere( $tag ) {
1118 wfDeprecated( __METHOD__, '1.41' );
1119 return MediaWikiServices::getInstance()->getChangeTagsStore()->deleteTagEverywhere( $tag );
1120 }
1121
1134 public static function canDeleteTag( $tag, ?Authority $performer = null, int $flags = 0 ) {
1135 $user = null;
1136 $services = MediaWikiServices::getInstance();
1137 if ( $performer !== null ) {
1138 if ( !$performer->isAllowed( 'deletechangetags' ) ) {
1139 return Status::newFatal( 'tags-delete-no-permission' );
1140 }
1141 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1142 return Status::newFatal(
1143 'tags-manage-blocked',
1144 $performer->getUser()->getName()
1145 );
1146 }
1147 // ChangeTagCanDelete hook still needs a full User object
1148 $user = $services->getUserFactory()->newFromAuthority( $performer );
1149 }
1150
1151 $changeTagsStore = $services->getChangeTagsStore();
1152 $tagUsage = $changeTagsStore->tagUsageStatistics();
1153 if (
1154 !isset( $tagUsage[$tag] ) &&
1155 !in_array( $tag, $changeTagsStore->listDefinedTags() )
1156 ) {
1157 return Status::newFatal( 'tags-delete-not-found', $tag );
1158 }
1159
1160 if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1161 isset( $tagUsage[$tag] ) &&
1162 $tagUsage[$tag] > self::MAX_DELETE_USES
1163 ) {
1164 return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1165 }
1166
1167 $softwareDefined = $changeTagsStore->listSoftwareDefinedTags();
1168 if ( in_array( $tag, $softwareDefined ) ) {
1169 // extension-defined tags can't be deleted unless the extension
1170 // specifically allows it
1171 $status = Status::newFatal( 'tags-delete-not-allowed' );
1172 } else {
1173 // user-defined tags are deletable unless otherwise specified
1174 $status = Status::newGood();
1175 }
1176
1177 ( new HookRunner( $services->getHookContainer() ) )->onChangeTagCanDelete( $tag, $user, $status );
1178 return $status;
1179 }
1180
1198 public static function deleteTagWithChecks( string $tag, string $reason, Authority $performer,
1199 bool $ignoreWarnings = false, array $logEntryTags = []
1200 ) {
1201 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1202 // are we allowed to do this?
1203 $result = self::canDeleteTag( $tag, $performer );
1204 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1205 $result->value = null;
1206 return $result;
1207 }
1208
1209 // store the tag usage statistics
1210 $hitcount = $changeTagsStore->tagUsageStatistics()[$tag] ?? 0;
1211
1212 // do it!
1213 $deleteResult = $changeTagsStore->deleteTagEverywhere( $tag );
1214 if ( !$deleteResult->isOK() ) {
1215 return $deleteResult;
1216 }
1217
1218 // log it
1219 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1220 $logId = $changeTagsStore->logTagManagementAction( 'delete', $tag, $reason, $performer->getUser(),
1221 $hitcount, $logEntryTags );
1222
1223 $deleteResult->value = $logId;
1224 return $deleteResult;
1225 }
1226
1234 public static function listSoftwareActivatedTags() {
1235 wfDeprecated( __METHOD__, '1.41' );
1236 return MediaWikiServices::getInstance()->getChangeTagsStore()->listSoftwareActivatedTags();
1237 }
1238
1247 public static function listDefinedTags() {
1248 wfDeprecated( __METHOD__, '1.41' );
1249 return MediaWikiServices::getInstance()->getChangeTagsStore()->listDefinedTags();
1250 }
1251
1261 public static function listExplicitlyDefinedTags() {
1262 wfDeprecated( __METHOD__, '1.41' );
1263 return MediaWikiServices::getInstance()->getChangeTagsStore()->listExplicitlyDefinedTags();
1264 }
1265
1276 public static function listSoftwareDefinedTags() {
1277 wfDeprecated( __METHOD__, '1.41' );
1278 return MediaWikiServices::getInstance()->getChangeTagsStore()->listSoftwareDefinedTags();
1279 }
1280
1287 public static function purgeTagCacheAll() {
1288 wfDeprecated( __METHOD__, '1.41' );
1289 MediaWikiServices::getInstance()->getChangeTagsStore()->purgeTagCacheAll();
1290 }
1291
1300 public static function tagUsageStatistics() {
1301 wfDeprecated( __METHOD__, '1.41' );
1302 return MediaWikiServices::getInstance()->getChangeTagsStore()->tagUsageStatistics();
1303 }
1304
1309 private const TAG_DESC_CHARACTER_LIMIT = 120;
1310
1339 public static function getChangeTagListSummary(
1340 MessageLocalizer $localizer,
1341 Language $lang,
1342 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
1343 bool $useAllTags = self::USE_ALL_TAGS
1344 ) {
1345 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1346
1347 if ( $useAllTags ) {
1348 $tagKeys = $changeTagsStore->listDefinedTags();
1349 $cacheKey = 'tags-list-summary';
1350 } else {
1351 $tagKeys = $changeTagsStore->getCoreDefinedTags();
1352 $cacheKey = 'core-software-tags-summary';
1353 }
1354
1355 // if $tagHitCounts exists, check against it later to determine whether or not to omit tags
1356 $tagHitCounts = null;
1357 if ( $activeOnly ) {
1358 $tagHitCounts = $changeTagsStore->tagUsageStatistics();
1359 } else {
1360 // The full set of tags should use a different cache key than the subset
1361 $cacheKey .= '-all';
1362 }
1363
1364 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1365 return $cache->getWithSetCallback(
1366 $cache->makeKey( $cacheKey, $lang->getCode() ),
1367 WANObjectCache::TTL_DAY,
1368 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $localizer, $tagKeys, $tagHitCounts ) {
1369 $result = [];
1370 foreach ( $tagKeys as $tagName ) {
1371 // Only list tags that are still actively defined
1372 if ( $tagHitCounts !== null ) {
1373 // Only list tags with more than 0 hits
1374 $hits = $tagHitCounts[$tagName] ?? 0;
1375 if ( $hits <= 0 ) {
1376 continue;
1377 }
1378 }
1379
1380 $labelMsg = self::tagShortDescriptionMessage( $tagName, $localizer );
1381 $helpLink = self::tagHelpLink( $tagName, $localizer );
1382 $descriptionMsg = self::tagLongDescriptionMessage( $tagName, $localizer );
1383 // Don't cache the message object, use the correct MessageLocalizer to parse later.
1384 $result[] = [
1385 'name' => $tagName,
1386 'labelMsg' => (bool)$labelMsg,
1387 'label' => $labelMsg ? $labelMsg->plain() : $tagName,
1388 'descriptionMsg' => (bool)$descriptionMsg,
1389 'description' => $descriptionMsg ? $descriptionMsg->plain() : '',
1390 'helpLink' => $helpLink,
1391 'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
1392 ];
1393 }
1394 return $result;
1395 }
1396 );
1397 }
1398
1414 public static function getChangeTagList(
1415 MessageLocalizer $localizer, Language $lang,
1416 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY, bool $useAllTags = self::USE_ALL_TAGS,
1417 $labelsOnly = false
1418 ) {
1419 $tags = self::getChangeTagListSummary( $localizer, $lang, $activeOnly, $useAllTags );
1420
1421 foreach ( $tags as &$tagInfo ) {
1422 if ( $tagInfo['labelMsg'] ) {
1423 // Optimization: Skip the parsing if the label contains only plain text (T344352)
1424 if ( wfEscapeWikiText( $tagInfo['label'] ) !== $tagInfo['label'] ) {
1425 // Use localizer with the correct page title to parse plain message from the cache.
1426 $labelMsg = new RawMessage( $tagInfo['label'] );
1427 $tagInfo['label'] = Sanitizer::stripAllTags( $localizer->msg( $labelMsg )->parse() );
1428 }
1429 } else {
1430 $tagInfo['label'] = $localizer->msg( 'tag-hidden', $tagInfo['name'] )->text();
1431 }
1432 // Optimization: Skip parsing the descriptions if not needed by the caller (T344352)
1433 if ( $labelsOnly ) {
1434 unset( $tagInfo['description'] );
1435 } elseif ( $tagInfo['descriptionMsg'] ) {
1436 // Optimization: Skip the parsing if the description contains only plain text (T344352)
1437 if ( wfEscapeWikiText( $tagInfo['description'] ) !== $tagInfo['description'] ) {
1438 $descriptionMsg = new RawMessage( $tagInfo['description'] );
1439 $tagInfo['description'] = Sanitizer::stripAllTags( $localizer->msg( $descriptionMsg )->parse() );
1440 }
1441 $tagInfo['description'] = $lang->truncateForVisual( $tagInfo['description'],
1442 self::TAG_DESC_CHARACTER_LIMIT );
1443 }
1444 unset( $tagInfo['labelMsg'] );
1445 unset( $tagInfo['descriptionMsg'] );
1446 }
1447
1448 // Instead of sorting by hit count (disabled for now), sort by display name
1449 usort( $tags, static function ( $a, $b ) {
1450 return strcasecmp( $a['label'], $b['label'] );
1451 } );
1452 return $tags;
1453 }
1454
1469 public static function showTagEditingUI( Authority $performer ) {
1470 $changeTagsStore = MediaWikiServices::getInstance()->getChangeTagsStore();
1471 return $performer->isAllowed( 'changetags' ) && (bool)$changeTagsStore->listExplicitlyDefinedTags();
1472 }
1473}
1474
1476class_alias( ChangeTags::class, 'ChangeTags' );
const NS_MEDIAWIKI
Definition Defines.php:73
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:57
Base class for language-specific code.
Definition Language.php:81
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:157
static plaintextParam( $plaintext)
Definition Message.php:1356
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
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:58
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:54
Represents a title within MediaWiki.
Definition Title.php:78
List for logging table items.
static suggestTarget( $target, array $ids)
Suggest a target for the revision deletion Optionally override this function.
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:37
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)