139 private const MAX_DELETE_USES = 5000;
144 private const CHANGE_TAG =
'change_tag';
196 if ( $tags ===
'' || $tags ===
null ) {
201 "Calling ChangeTags::formatSummaryRow without a localizer is deprecated.",
204 $localizer = RequestContext::getMain();
209 $tags = explode(
',', $tags );
211 usort( $tags,
static function ( $a, $b ) use ( $order ) {
212 return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
216 foreach ( $tags as $tag ) {
220 $classes[] = Sanitizer::escapeClass(
"mw-tag-$tag" );
222 if ( $description ===
false ) {
225 $displayTags[] = Html::rawElement(
227 [
'class' =>
'mw-tag-marker ' .
228 Sanitizer::escapeClass(
"mw-tag-marker-$tag" ) ],
233 if ( !$displayTags ) {
234 return [
'', $classes ];
237 $markers = $localizer->msg(
'tag-list-wrapper' )
238 ->numParams( count( $displayTags ) )
239 ->rawParams( implode(
' ', $displayTags ) )
241 $markers = Html::rawElement(
'span', [
'class' =>
'mw-tag-markers' ], $markers );
243 return [ $markers, $classes ];
260 $msg = $context->
msg(
"tag-$tag" );
261 if ( !$msg->exists() ) {
267 if ( $msg->isDisabled() ) {
289 $msg = $context->
msg(
"tag-$tag-helppage" )->inContentLanguage();
290 if ( !$msg->isDisabled() ) {
291 return Skin::makeInternalOrExternalUrl( $msg->text() ) ?:
null;
310 if ( $msg && $link ) {
311 $label = $msg->parse();
313 if ( !str_contains( $label,
'<a ' ) ) {
314 return Html::rawElement(
'a', [
'href' => $link ], $label );
317 return $msg ? $msg->parse() :
false;
333 $msg = $context->
msg(
"tag-$tag-description" );
334 return $msg->isDisabled() ? false : $msg;
351 public static function addTags( $tags, $rc_id =
null, $rev_id =
null,
352 $log_id =
null, $params =
null, ?
RecentChange $rc =
null
356 $tags, $rc_id, $rev_id, $log_id, $params, $rc
390 public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id =
null,
391 &$rev_id =
null, &$log_id =
null, $params =
null, ?
RecentChange $rc =
null,
396 $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, $rc, $user
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;
476 if ( $performer !==
null ) {
477 if ( !$performer->isAllowed(
'applychangetags' ) ) {
478 return Status::newFatal(
'tags-apply-no-permission' );
481 if ( $checkBlock && $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
482 return Status::newFatal(
483 'tags-apply-blocked',
484 $performer->getUser()->getName()
489 $user = $services->getUserFactory()->newFromAuthority( $performer );
493 $allowedTags = $services->getChangeTagsStore()->listExplicitlyDefinedTags();
494 (
new HookRunner( $services->getHookContainer() ) )->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
495 $disallowedTags = array_diff( $tags, $allowedTags );
496 if ( $disallowedTags ) {
498 'tags-apply-not-allowed-multi', $disallowedTags );
501 return Status::newGood();
523 if ( $performer !==
null ) {
524 if ( !$performer->isDefinitelyAllowed(
'changetags' ) ) {
525 return Status::newFatal(
'tags-update-no-permission' );
528 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
529 return Status::newFatal(
530 'tags-update-blocked',
531 $performer->getUser()->getName()
540 $explicitlyDefinedTags = $changeTagsStore->listExplicitlyDefinedTags();
541 $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
544 'tags-update-add-not-allowed-multi', $diff );
548 if ( $tagsToRemove ) {
552 $softwareDefinedTags = $changeTagsStore->listSoftwareDefinedTags();
553 $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
556 'tags-update-remove-not-allowed-multi', $intersect );
560 return Status::newGood();
594 $rc_id, $rev_id, $log_id, $params,
string $reason,
Authority $performer
596 if ( !$tagsToAdd && !$tagsToRemove ) {
598 return Status::newGood( (
object)[
606 $tagsToRemove ??= [];
610 if ( !$result->isOK() ) {
611 $result->value =
null;
616 $status = PermissionStatus::newEmpty();
618 return Status::wrap( $status );
623 [ $tagsAdded, $tagsRemoved, $initialTags ] = $changeTagsStore->updateTags( $tagsToAdd,
624 $tagsToRemove, $rc_id, $rev_id, $log_id, $params,
null, $performer->
getUser() );
625 if ( !$tagsAdded && !$tagsRemoved ) {
627 return Status::newGood( (
object)[
636 $logEntry->setPerformer( $performer->
getUser() );
637 $logEntry->setComment( $reason );
642 ->getRevisionLookup()
643 ->getRevisionById( $rev_id );
644 if ( $revisionRecord ) {
645 $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
647 } elseif ( $log_id ) {
654 if ( !$logEntry->getTarget() ) {
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,
668 $logEntry->setParameters( $logParams );
669 $logEntry->setRelations( [
'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
672 $logId = $logEntry->insert( $dbw );
674 $logEntry->publish( $logId,
'udp' );
676 return Status::newGood( (
object)[
678 'addedTags' => $tagsAdded,
679 'removedTags' => $tagsRemoved,
704 &$join_conds, &$options, $filter_tag =
'',
bool $exclude =
false
729 return self::CHANGE_TAG;
760 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
761 bool $useAllTags = self::USE_ALL_TAGS
765 "Calling ChangeTags::buildTagFilterSelector without a localizer is deprecated.",
768 $context = RequestContext::getMain();
771 $config = $context->getConfig();
774 !count( $changeTagsStore->listDefinedTags() ) ) {
780 $context->getLanguage(),
787 foreach ( $tags as $tagInfo ) {
788 $autocomplete[ $tagInfo[
'label'] ] = $tagInfo[
'name'];
792 $data[0] = Html::rawElement(
794 [
'for' =>
'tagfilter' ],
795 $context->msg(
'tag-filter' )->parse()
799 $options = Html::listDropdownOptionsOoui( $autocomplete );
801 $data[1] = new \OOUI\ComboBoxInputWidget( [
803 'name' =>
'tagfilter',
804 'value' => $selected,
805 'classes' =>
'mw-tagfilter-input',
806 'options' => $options,
810 foreach ( $autocomplete as $label => $name ) {
811 $optionsHtml .=
Html::element(
'option', [
'value' => $name ], $label );
813 $datalistHtml = Html::rawElement(
'datalist', [
'id' =>
'tagfilter-datalist' ], $optionsHtml );
815 $data[1] = Html::input(
820 'class' => [
'mw-tagfilter-input' ],
823 'list' =>
'tagfilter-datalist',
855 if ( $performer !==
null ) {
856 if ( !$performer->isAllowed(
'managechangetags' ) ) {
857 return Status::newFatal(
'tags-manage-no-permission' );
859 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
860 return Status::newFatal(
861 'tags-manage-blocked',
862 $performer->getUser()->getName()
871 $definedTags = $changeTagsStore->listDefinedTags();
872 if ( in_array( $tag, $definedTags ) ) {
873 return Status::newFatal(
'tags-activate-not-allowed', $tag );
877 if ( !isset( $changeTagsStore->tagUsageStatistics()[$tag] ) ) {
878 return Status::newFatal(
'tags-activate-not-found', $tag );
881 return Status::newGood();
902 bool $ignoreWarnings =
false, array $logEntryTags = []
906 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
907 $result->value =
null;
912 $changeTagsStore->defineTag( $tag );
914 $logId = $changeTagsStore->logTagManagementAction(
'activate', $tag, $reason, $performer->
getUser(),
915 null, $logEntryTags );
917 return Status::newGood( $logId );
930 if ( $performer !==
null ) {
931 if ( !$performer->isAllowed(
'managechangetags' ) ) {
932 return Status::newFatal(
'tags-manage-no-permission' );
934 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
935 return Status::newFatal(
936 'tags-manage-blocked',
937 $performer->getUser()->getName()
944 if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
945 return Status::newFatal(
'tags-deactivate-not-allowed', $tag );
947 return Status::newGood();
968 bool $ignoreWarnings =
false, array $logEntryTags = []
972 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
973 $result->value =
null;
978 $changeTagsStore->undefineTag( $tag );
980 $logId = $changeTagsStore->logTagManagementAction(
'deactivate', $tag, $reason,
981 $performer->
getUser(),
null, $logEntryTags );
983 return Status::newGood( $logId );
996 return Status::newFatal(
'tags-create-no-name' );
1003 if ( str_contains( $tag,
',' ) || str_contains( $tag,
'|' ) || str_contains( $tag,
'/' ) ) {
1004 return Status::newFatal(
'tags-create-invalid-chars' );
1008 $title = Title::makeTitleSafe(
NS_MEDIAWIKI,
"Tag-$tag-description" );
1009 if ( $title ===
null ) {
1010 return Status::newFatal(
'tags-create-invalid-title-chars' );
1013 return Status::newGood();
1031 if ( $performer !==
null ) {
1032 if ( !$performer->isAllowed(
'managechangetags' ) ) {
1033 return Status::newFatal(
'tags-manage-no-permission' );
1035 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1036 return Status::newFatal(
1037 'tags-manage-blocked',
1038 $performer->getUser()->getName()
1042 $user = $services->getUserFactory()->newFromAuthority( $performer );
1046 if ( !$status->isGood() ) {
1051 $changeTagsStore = $services->getChangeTagsStore();
1053 isset( $changeTagsStore->tagUsageStatistics()[$tag] ) ||
1054 in_array( $tag, $changeTagsStore->listDefinedTags() )
1056 return Status::newFatal(
'tags-create-already-exists', $tag );
1060 $canCreateResult = Status::newGood();
1061 (
new HookRunner( $services->getHookContainer() ) )->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1062 return $canCreateResult;
1085 bool $ignoreWarnings =
false, array $logEntryTags = []
1089 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1090 $result->value =
null;
1095 $changeTagsStore->defineTag( $tag );
1096 $logId = $changeTagsStore->logTagManagementAction(
'create', $tag, $reason,
1097 $performer->
getUser(),
null, $logEntryTags );
1099 return Status::newGood( $logId );
1135 if ( $performer !==
null ) {
1136 if ( !$performer->isAllowed(
'deletechangetags' ) ) {
1137 return Status::newFatal(
'tags-delete-no-permission' );
1139 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1140 return Status::newFatal(
1141 'tags-manage-blocked',
1142 $performer->getUser()->getName()
1146 $user = $services->getUserFactory()->newFromAuthority( $performer );
1149 $changeTagsStore = $services->getChangeTagsStore();
1150 $tagUsage = $changeTagsStore->tagUsageStatistics();
1152 !isset( $tagUsage[$tag] ) &&
1153 !in_array( $tag, $changeTagsStore->listDefinedTags() )
1155 return Status::newFatal(
'tags-delete-not-found', $tag );
1158 if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1159 isset( $tagUsage[$tag] ) &&
1160 $tagUsage[$tag] > self::MAX_DELETE_USES
1162 return Status::newFatal(
'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1165 $softwareDefined = $changeTagsStore->listSoftwareDefinedTags();
1166 if ( in_array( $tag, $softwareDefined ) ) {
1169 $status = Status::newFatal(
'tags-delete-not-allowed' );
1172 $status = Status::newGood();
1175 (
new HookRunner( $services->getHookContainer() ) )->onChangeTagCanDelete( $tag, $user, $status );
1197 bool $ignoreWarnings =
false, array $logEntryTags = []
1202 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1203 $result->value =
null;
1208 $hitcount = $changeTagsStore->tagUsageStatistics()[$tag] ?? 0;
1211 $deleteResult = $changeTagsStore->deleteTagEverywhere( $tag );
1212 if ( !$deleteResult->isOK() ) {
1213 return $deleteResult;
1218 $logId = $changeTagsStore->logTagManagementAction(
'delete', $tag, $reason, $performer->
getUser(),
1219 $hitcount, $logEntryTags );
1221 $deleteResult->value = $logId;
1222 return $deleteResult;
1307 private const TAG_DESC_CHARACTER_LIMIT = 120;
1340 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
1341 bool $useAllTags = self::USE_ALL_TAGS
1345 if ( $useAllTags ) {
1346 $tagKeys = $changeTagsStore->listDefinedTags();
1347 $cacheKey =
'tags-list-summary';
1349 $tagKeys = $changeTagsStore->getCoreDefinedTags();
1350 $cacheKey =
'core-software-tags-summary';
1354 $tagHitCounts =
null;
1355 if ( $activeOnly ) {
1356 $tagHitCounts = $changeTagsStore->tagUsageStatistics();
1359 $cacheKey .=
'-all';
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 ) {
1368 foreach ( $tagKeys as $tagName ) {
1370 if ( $tagHitCounts !==
null ) {
1372 $hits = $tagHitCounts[$tagName] ?? 0;
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 ),
1414 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
bool $useAllTags = self::USE_ALL_TAGS,
1419 foreach ( $tags as &$tagInfo ) {
1420 if ( $tagInfo[
'labelMsg'] ) {
1424 $labelMsg =
new RawMessage( $tagInfo[
'label'] );
1425 $tagInfo[
'label'] = Sanitizer::stripAllTags( $localizer->
msg( $labelMsg )->parse() );
1428 $tagInfo[
'label'] = $localizer->
msg(
'tag-hidden', $tagInfo[
'name'] )->text();
1431 if ( $labelsOnly ) {
1432 unset( $tagInfo[
'description'] );
1433 } elseif ( $tagInfo[
'descriptionMsg'] ) {
1435 if (
wfEscapeWikiText( $tagInfo[
'description'] ) !== $tagInfo[
'description'] ) {
1436 $descriptionMsg =
new RawMessage( $tagInfo[
'description'] );
1437 $tagInfo[
'description'] = Sanitizer::stripAllTags( $localizer->
msg( $descriptionMsg )->parse() );
1440 self::TAG_DESC_CHARACTER_LIMIT );
1442 unset( $tagInfo[
'labelMsg'] );
1443 unset( $tagInfo[
'descriptionMsg'] );
1447 usort( $tags,
static function ( $a, $b ) {
1448 return strcasecmp( $a[
'label'], $b[
'label'] );
1469 return $performer->
isAllowed(
'changetags' ) && (bool)$changeTagsStore->listExplicitlyDefinedTags();
1474class_alias( ChangeTags::class,
'ChangeTags' );
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.
Group all the pieces relevant to the context of a request into one instance.
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()
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,...
Interface for objects which can provide a MediaWiki context on request.