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