143 private const MAX_DELETE_USES = 5000;
148 private const CHANGE_TAG =
'change_tag';
202 if ( $tags ===
'' || $tags ===
null ) {
206 $localizer = RequestContext::getMain();
211 $tags = explode(
',', $tags );
213 usort( $tags,
static function ( $a, $b ) use ( $order ) {
214 return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
218 foreach ( $tags as $tag ) {
222 $classes[] = Sanitizer::escapeClass(
"mw-tag-$tag" );
224 if ( $description ===
false ) {
227 $displayTags[] = Html::rawElement(
229 [
'class' =>
'mw-tag-marker ' .
230 Sanitizer::escapeClass(
"mw-tag-marker-$tag" ) ],
235 if ( !$displayTags ) {
236 return [
'', $classes ];
239 $markers = $localizer->msg(
'tag-list-wrapper' )
240 ->numParams( count( $displayTags ) )
241 ->rawParams( implode(
' ', $displayTags ) )
243 $markers = Html::rawElement(
'span', [
'class' =>
'mw-tag-markers' ], $markers );
245 return [ $markers, $classes ];
262 $msg = $context->
msg(
"tag-$tag" );
263 if ( !$msg->exists() ) {
269 if ( $msg->isDisabled() ) {
291 $msg = $context->
msg(
"tag-$tag-helppage" )->inContentLanguage();
292 if ( !$msg->isDisabled() ) {
293 return Skin::makeInternalOrExternalUrl( $msg->text() ) ?:
null;
312 if ( $msg && $link ) {
313 $label = $msg->parse();
315 if ( !str_contains( $label,
'<a ' ) ) {
316 return Html::rawElement(
'a', [
'href' => $link ], $label );
319 return $msg ? $msg->parse() :
false;
335 $msg = $context->
msg(
"tag-$tag-description" );
336 return $msg->isDisabled() ? false : $msg;
353 public static function addTags( $tags, $rc_id =
null, $rev_id =
null,
354 $log_id =
null, $params =
null, ?
RecentChange $rc =
null
358 $tags, $rc_id, $rev_id, $log_id, $params, $rc
392 public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id =
null,
393 &$rev_id =
null, &$log_id =
null, $params =
null, ?
RecentChange $rc =
null,
398 $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, $rc, $user
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;
478 if ( $performer !==
null ) {
479 if ( !$performer->isAllowed(
'applychangetags' ) ) {
480 return Status::newFatal(
'tags-apply-no-permission' );
483 if ( $checkBlock && $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
484 return Status::newFatal(
485 'tags-apply-blocked',
486 $performer->getUser()->getName()
491 $user = $services->getUserFactory()->newFromAuthority( $performer );
495 $allowedTags = $services->getChangeTagsStore()->listExplicitlyDefinedTags();
496 (
new HookRunner( $services->getHookContainer() ) )->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
497 $disallowedTags = array_diff( $tags, $allowedTags );
498 if ( $disallowedTags ) {
500 'tags-apply-not-allowed-multi', $disallowedTags );
503 return Status::newGood();
525 if ( $performer !==
null ) {
526 if ( !$performer->isDefinitelyAllowed(
'changetags' ) ) {
527 return Status::newFatal(
'tags-update-no-permission' );
530 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
531 return Status::newFatal(
532 'tags-update-blocked',
533 $performer->getUser()->getName()
542 $explicitlyDefinedTags = $changeTagsStore->listExplicitlyDefinedTags();
543 $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
546 'tags-update-add-not-allowed-multi', $diff );
550 if ( $tagsToRemove ) {
554 $softwareDefinedTags = $changeTagsStore->listSoftwareDefinedTags();
555 $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
558 'tags-update-remove-not-allowed-multi', $intersect );
562 return Status::newGood();
596 $rc_id, $rev_id, $log_id, $params,
string $reason,
Authority $performer
598 if ( !$tagsToAdd && !$tagsToRemove ) {
600 return Status::newGood( (
object)[
608 $tagsToRemove ??= [];
612 if ( !$result->isOK() ) {
613 $result->value =
null;
618 $status = PermissionStatus::newEmpty();
620 return Status::wrap( $status );
625 [ $tagsAdded, $tagsRemoved, $initialTags ] = $changeTagsStore->updateTags( $tagsToAdd,
626 $tagsToRemove, $rc_id, $rev_id, $log_id, $params,
null, $performer->
getUser() );
627 if ( !$tagsAdded && !$tagsRemoved ) {
629 return Status::newGood( (
object)[
638 $logEntry->setPerformer( $performer->
getUser() );
639 $logEntry->setComment( $reason );
644 ->getRevisionLookup()
645 ->getRevisionById( $rev_id );
646 if ( $revisionRecord ) {
647 $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
649 } elseif ( $log_id ) {
656 if ( !$logEntry->getTarget() ) {
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,
670 $logEntry->setParameters( $logParams );
671 $logEntry->setRelations( [
'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
674 $logId = $logEntry->insert( $dbw );
676 $logEntry->publish( $logId,
'udp' );
678 return Status::newGood( (
object)[
680 'addedTags' => $tagsAdded,
681 'removedTags' => $tagsRemoved,
707 &$join_conds, &$options, $filter_tag =
'',
bool $exclude =
false
732 return self::CHANGE_TAG;
765 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
766 bool $useAllTags = self::USE_ALL_TAGS
769 $context = RequestContext::getMain();
772 $config = $context->getConfig();
775 !count( $changeTagsStore->listDefinedTags() ) ) {
781 $context->getLanguage(),
788 foreach ( $tags as $tagInfo ) {
789 $autocomplete[ $tagInfo[
'label'] ] = $tagInfo[
'name'];
793 $data[0] = Html::rawElement(
795 [
'for' =>
'tagfilter' ],
796 $context->msg(
'tag-filter' )->parse()
800 $options = Html::listDropdownOptionsOoui( $autocomplete );
802 $data[1] = new \OOUI\ComboBoxInputWidget( [
804 'name' =>
'tagfilter',
805 'value' => $selected,
806 'classes' =>
'mw-tagfilter-input',
807 'options' => $options,
811 foreach ( $autocomplete as $label => $name ) {
812 $optionsHtml .=
Html::element(
'option', [
'value' => $name ], $label );
814 $datalistHtml = Html::rawElement(
'datalist', [
'id' =>
'tagfilter-datalist' ], $optionsHtml );
816 $data[1] = Html::input(
821 'class' => [
'mw-tagfilter-input',
'mw-ui-input',
'mw-ui-input-inline' ],
824 'list' =>
'tagfilter-datalist',
856 if ( $performer !==
null ) {
857 if ( !$performer->isAllowed(
'managechangetags' ) ) {
858 return Status::newFatal(
'tags-manage-no-permission' );
860 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
861 return Status::newFatal(
862 'tags-manage-blocked',
863 $performer->getUser()->getName()
872 $definedTags = $changeTagsStore->listDefinedTags();
873 if ( in_array( $tag, $definedTags ) ) {
874 return Status::newFatal(
'tags-activate-not-allowed', $tag );
878 if ( !isset( $changeTagsStore->tagUsageStatistics()[$tag] ) ) {
879 return Status::newFatal(
'tags-activate-not-found', $tag );
882 return Status::newGood();
903 bool $ignoreWarnings =
false, array $logEntryTags = []
907 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
908 $result->value =
null;
913 $changeTagsStore->defineTag( $tag );
915 $logId = $changeTagsStore->logTagManagementAction(
'activate', $tag, $reason, $performer->
getUser(),
916 null, $logEntryTags );
918 return Status::newGood( $logId );
931 if ( $performer !==
null ) {
932 if ( !$performer->isAllowed(
'managechangetags' ) ) {
933 return Status::newFatal(
'tags-manage-no-permission' );
935 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
936 return Status::newFatal(
937 'tags-manage-blocked',
938 $performer->getUser()->getName()
945 if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
946 return Status::newFatal(
'tags-deactivate-not-allowed', $tag );
948 return Status::newGood();
969 bool $ignoreWarnings =
false, array $logEntryTags = []
973 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
974 $result->value =
null;
979 $changeTagsStore->undefineTag( $tag );
981 $logId = $changeTagsStore->logTagManagementAction(
'deactivate', $tag, $reason,
982 $performer->
getUser(),
null, $logEntryTags );
984 return Status::newGood( $logId );
997 return Status::newFatal(
'tags-create-no-name' );
1004 if ( strpos( $tag,
',' ) !==
false || strpos( $tag,
'|' ) !==
false
1005 || strpos( $tag,
'/' ) !==
false ) {
1006 return Status::newFatal(
'tags-create-invalid-chars' );
1010 $title = Title::makeTitleSafe(
NS_MEDIAWIKI,
"Tag-$tag-description" );
1011 if ( $title ===
null ) {
1012 return Status::newFatal(
'tags-create-invalid-title-chars' );
1015 return Status::newGood();
1033 if ( $performer !==
null ) {
1034 if ( !$performer->isAllowed(
'managechangetags' ) ) {
1035 return Status::newFatal(
'tags-manage-no-permission' );
1037 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1038 return Status::newFatal(
1039 'tags-manage-blocked',
1040 $performer->getUser()->getName()
1044 $user = $services->getUserFactory()->newFromAuthority( $performer );
1048 if ( !$status->isGood() ) {
1053 $changeTagsStore = $services->getChangeTagsStore();
1055 isset( $changeTagsStore->tagUsageStatistics()[$tag] ) ||
1056 in_array( $tag, $changeTagsStore->listDefinedTags() )
1058 return Status::newFatal(
'tags-create-already-exists', $tag );
1062 $canCreateResult = Status::newGood();
1063 (
new HookRunner( $services->getHookContainer() ) )->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1064 return $canCreateResult;
1087 bool $ignoreWarnings =
false, array $logEntryTags = []
1091 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1092 $result->value =
null;
1097 $changeTagsStore->defineTag( $tag );
1098 $logId = $changeTagsStore->logTagManagementAction(
'create', $tag, $reason,
1099 $performer->
getUser(),
null, $logEntryTags );
1101 return Status::newGood( $logId );
1137 if ( $performer !==
null ) {
1138 if ( !$performer->isAllowed(
'deletechangetags' ) ) {
1139 return Status::newFatal(
'tags-delete-no-permission' );
1141 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1142 return Status::newFatal(
1143 'tags-manage-blocked',
1144 $performer->getUser()->getName()
1148 $user = $services->getUserFactory()->newFromAuthority( $performer );
1151 $changeTagsStore = $services->getChangeTagsStore();
1152 $tagUsage = $changeTagsStore->tagUsageStatistics();
1154 !isset( $tagUsage[$tag] ) &&
1155 !in_array( $tag, $changeTagsStore->listDefinedTags() )
1157 return Status::newFatal(
'tags-delete-not-found', $tag );
1160 if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1161 isset( $tagUsage[$tag] ) &&
1162 $tagUsage[$tag] > self::MAX_DELETE_USES
1164 return Status::newFatal(
'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1167 $softwareDefined = $changeTagsStore->listSoftwareDefinedTags();
1168 if ( in_array( $tag, $softwareDefined ) ) {
1171 $status = Status::newFatal(
'tags-delete-not-allowed' );
1174 $status = Status::newGood();
1177 (
new HookRunner( $services->getHookContainer() ) )->onChangeTagCanDelete( $tag, $user, $status );
1199 bool $ignoreWarnings =
false, array $logEntryTags = []
1204 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1205 $result->value =
null;
1210 $hitcount = $changeTagsStore->tagUsageStatistics()[$tag] ?? 0;
1213 $deleteResult = $changeTagsStore->deleteTagEverywhere( $tag );
1214 if ( !$deleteResult->isOK() ) {
1215 return $deleteResult;
1220 $logId = $changeTagsStore->logTagManagementAction(
'delete', $tag, $reason, $performer->
getUser(),
1221 $hitcount, $logEntryTags );
1223 $deleteResult->value = $logId;
1224 return $deleteResult;
1309 private const TAG_DESC_CHARACTER_LIMIT = 120;
1342 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
1343 bool $useAllTags = self::USE_ALL_TAGS
1347 if ( $useAllTags ) {
1348 $tagKeys = $changeTagsStore->listDefinedTags();
1349 $cacheKey =
'tags-list-summary';
1351 $tagKeys = $changeTagsStore->getCoreDefinedTags();
1352 $cacheKey =
'core-software-tags-summary';
1356 $tagHitCounts =
null;
1357 if ( $activeOnly ) {
1358 $tagHitCounts = $changeTagsStore->tagUsageStatistics();
1361 $cacheKey .=
'-all';
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 ) {
1370 foreach ( $tagKeys as $tagName ) {
1372 if ( $tagHitCounts !==
null ) {
1374 $hits = $tagHitCounts[$tagName] ?? 0;
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 ),
1416 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
bool $useAllTags = self::USE_ALL_TAGS,
1421 foreach ( $tags as &$tagInfo ) {
1422 if ( $tagInfo[
'labelMsg'] ) {
1426 $labelMsg =
new RawMessage( $tagInfo[
'label'] );
1427 $tagInfo[
'label'] = Sanitizer::stripAllTags( $localizer->
msg( $labelMsg )->parse() );
1430 $tagInfo[
'label'] = $localizer->
msg(
'tag-hidden', $tagInfo[
'name'] )->text();
1433 if ( $labelsOnly ) {
1434 unset( $tagInfo[
'description'] );
1435 } elseif ( $tagInfo[
'descriptionMsg'] ) {
1437 if (
wfEscapeWikiText( $tagInfo[
'description'] ) !== $tagInfo[
'description'] ) {
1438 $descriptionMsg =
new RawMessage( $tagInfo[
'description'] );
1439 $tagInfo[
'description'] = Sanitizer::stripAllTags( $localizer->
msg( $descriptionMsg )->parse() );
1442 self::TAG_DESC_CHARACTER_LIMIT );
1444 unset( $tagInfo[
'labelMsg'] );
1445 unset( $tagInfo[
'descriptionMsg'] );
1449 usort( $tags,
static function ( $a, $b ) {
1450 return strcasecmp( $a[
'label'], $b[
'label'] );
1471 return $performer->
isAllowed(
'changetags' ) && (bool)$changeTagsStore->listExplicitlyDefinedTags();
1476class_alias( ChangeTags::class,
'ChangeTags' );
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.
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,...
List for logging table items.
static suggestTarget( $target, array $ids)
Suggest a target for the revision deletion Optionally override this function.
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.