134 private const MAX_DELETE_USES = 5000;
139 private const CHANGE_TAG =
'change_tag';
193 if ( $tags ===
'' || $tags ===
null ) {
197 $localizer = RequestContext::getMain();
202 $tags = explode(
',', $tags );
204 usort( $tags,
static function ( $a, $b ) use ( $order ) {
205 return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
209 foreach ( $tags as $tag ) {
213 $classes[] = Sanitizer::escapeClass(
"mw-tag-$tag" );
215 if ( $description ===
false ) {
218 $displayTags[] = Html::rawElement(
220 [
'class' =>
'mw-tag-marker ' .
221 Sanitizer::escapeClass(
"mw-tag-marker-$tag" ) ],
226 if ( !$displayTags ) {
227 return [
'', $classes ];
230 $markers = $localizer->msg(
'tag-list-wrapper' )
231 ->numParams( count( $displayTags ) )
232 ->rawParams( implode(
' ', $displayTags ) )
234 $markers = Html::rawElement(
'span', [
'class' =>
'mw-tag-markers' ], $markers );
236 return [ $markers, $classes ];
253 $msg = $context->
msg(
"tag-$tag" );
254 if ( !$msg->exists() ) {
260 if ( $msg->isDisabled() ) {
282 $msg = $context->
msg(
"tag-$tag-helppage" )->inContentLanguage();
283 if ( !$msg->isDisabled() ) {
284 return Skin::makeInternalOrExternalUrl( $msg->text() ) ?:
null;
303 if ( $msg && $link ) {
304 $label = $msg->parse();
306 if ( !str_contains( $label,
'<a ' ) ) {
307 return Html::rawElement(
'a', [
'href' => $link ], $label );
310 return $msg ? $msg->parse() :
false;
326 $msg = $context->
msg(
"tag-$tag-description" );
327 return $msg->isDisabled() ? false : $msg;
344 public static function addTags( $tags, $rc_id =
null, $rev_id =
null,
345 $log_id =
null, $params =
null, ?
RecentChange $rc =
null
349 $tags, $rc_id, $rev_id, $log_id, $params, $rc
383 public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id =
null,
384 &$rev_id =
null, &$log_id =
null, $params =
null, ?
RecentChange $rc =
null,
389 $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, $rc, $user
439 $lang = RequestContext::getMain()->getLanguage();
440 $tags = array_values( $tags );
441 $count = count( $tags );
442 $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
443 $lang->commaList( $tags ), $count );
444 $status->value = $tags;
469 if ( $performer !==
null ) {
470 if ( !$performer->isAllowed(
'applychangetags' ) ) {
471 return Status::newFatal(
'tags-apply-no-permission' );
474 if ( $checkBlock && $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
475 return Status::newFatal(
476 'tags-apply-blocked',
477 $performer->getUser()->getName()
482 $user = $services->getUserFactory()->newFromAuthority( $performer );
486 $allowedTags = $services->getChangeTagsStore()->listExplicitlyDefinedTags();
487 (
new HookRunner( $services->getHookContainer() ) )->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
488 $disallowedTags = array_diff( $tags, $allowedTags );
489 if ( $disallowedTags ) {
491 'tags-apply-not-allowed-multi', $disallowedTags );
494 return Status::newGood();
516 if ( $performer !==
null ) {
517 if ( !$performer->isDefinitelyAllowed(
'changetags' ) ) {
518 return Status::newFatal(
'tags-update-no-permission' );
521 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
522 return Status::newFatal(
523 'tags-update-blocked',
524 $performer->getUser()->getName()
533 $explicitlyDefinedTags = $changeTagsStore->listExplicitlyDefinedTags();
534 $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
537 'tags-update-add-not-allowed-multi', $diff );
541 if ( $tagsToRemove ) {
545 $softwareDefinedTags = $changeTagsStore->listSoftwareDefinedTags();
546 $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
549 'tags-update-remove-not-allowed-multi', $intersect );
553 return Status::newGood();
587 $rc_id, $rev_id, $log_id, $params,
string $reason,
Authority $performer
589 if ( !$tagsToAdd && !$tagsToRemove ) {
591 return Status::newGood( (
object)[
599 $tagsToRemove ??= [];
603 if ( !$result->isOK() ) {
604 $result->value =
null;
609 $status = PermissionStatus::newEmpty();
611 return Status::wrap( $status );
616 [ $tagsAdded, $tagsRemoved, $initialTags ] = $changeTagsStore->updateTags( $tagsToAdd,
617 $tagsToRemove, $rc_id, $rev_id, $log_id, $params,
null, $performer->
getUser() );
618 if ( !$tagsAdded && !$tagsRemoved ) {
620 return Status::newGood( (
object)[
629 $logEntry->setPerformer( $performer->
getUser() );
630 $logEntry->setComment( $reason );
635 ->getRevisionLookup()
636 ->getRevisionById( $rev_id );
637 if ( $revisionRecord ) {
638 $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
640 } elseif ( $log_id ) {
647 if ( !$logEntry->getTarget() ) {
653 '4::revid' => $rev_id,
654 '5::logid' => $log_id,
655 '6:list:tagsAdded' => $tagsAdded,
656 '7:number:tagsAddedCount' => count( $tagsAdded ),
657 '8:list:tagsRemoved' => $tagsRemoved,
658 '9:number:tagsRemovedCount' => count( $tagsRemoved ),
659 'initialTags' => $initialTags,
661 $logEntry->setParameters( $logParams );
662 $logEntry->setRelations( [
'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
665 $logId = $logEntry->insert( $dbw );
667 $logEntry->publish( $logId,
'udp' );
669 return Status::newGood( (
object)[
671 'addedTags' => $tagsAdded,
672 'removedTags' => $tagsRemoved,
697 &$join_conds, &$options, $filter_tag =
'',
bool $exclude =
false
722 return self::CHANGE_TAG;
755 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
756 bool $useAllTags = self::USE_ALL_TAGS
759 $context = RequestContext::getMain();
762 $config = $context->getConfig();
765 !count( $changeTagsStore->listDefinedTags() ) ) {
771 $context->getLanguage(),
778 foreach ( $tags as $tagInfo ) {
779 $autocomplete[ $tagInfo[
'label'] ] = $tagInfo[
'name'];
783 $data[0] = Html::rawElement(
785 [
'for' =>
'tagfilter' ],
786 $context->msg(
'tag-filter' )->parse()
790 $options = Html::listDropdownOptionsOoui( $autocomplete );
792 $data[1] = new \OOUI\ComboBoxInputWidget( [
794 'name' =>
'tagfilter',
795 'value' => $selected,
796 'classes' =>
'mw-tagfilter-input',
797 'options' => $options,
801 foreach ( $autocomplete as $label => $name ) {
802 $optionsHtml .=
Html::element(
'option', [
'value' => $name ], $label );
804 $datalistHtml = Html::rawElement(
'datalist', [
'id' =>
'tagfilter-datalist' ], $optionsHtml );
806 $data[1] = Html::input(
811 'class' => [
'mw-tagfilter-input',
'mw-ui-input',
'mw-ui-input-inline' ],
814 'list' =>
'tagfilter-datalist',
846 if ( $performer !==
null ) {
847 if ( !$performer->isAllowed(
'managechangetags' ) ) {
848 return Status::newFatal(
'tags-manage-no-permission' );
850 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
851 return Status::newFatal(
852 'tags-manage-blocked',
853 $performer->getUser()->getName()
862 $definedTags = $changeTagsStore->listDefinedTags();
863 if ( in_array( $tag, $definedTags ) ) {
864 return Status::newFatal(
'tags-activate-not-allowed', $tag );
868 if ( !isset( $changeTagsStore->tagUsageStatistics()[$tag] ) ) {
869 return Status::newFatal(
'tags-activate-not-found', $tag );
872 return Status::newGood();
893 bool $ignoreWarnings =
false, array $logEntryTags = []
897 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
898 $result->value =
null;
903 $changeTagsStore->defineTag( $tag );
905 $logId = $changeTagsStore->logTagManagementAction(
'activate', $tag, $reason, $performer->
getUser(),
906 null, $logEntryTags );
908 return Status::newGood( $logId );
921 if ( $performer !==
null ) {
922 if ( !$performer->isAllowed(
'managechangetags' ) ) {
923 return Status::newFatal(
'tags-manage-no-permission' );
925 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
926 return Status::newFatal(
927 'tags-manage-blocked',
928 $performer->getUser()->getName()
935 if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
936 return Status::newFatal(
'tags-deactivate-not-allowed', $tag );
938 return Status::newGood();
959 bool $ignoreWarnings =
false, array $logEntryTags = []
963 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
964 $result->value =
null;
969 $changeTagsStore->undefineTag( $tag );
971 $logId = $changeTagsStore->logTagManagementAction(
'deactivate', $tag, $reason,
972 $performer->
getUser(),
null, $logEntryTags );
974 return Status::newGood( $logId );
987 return Status::newFatal(
'tags-create-no-name' );
994 if ( str_contains( $tag,
',' ) || str_contains( $tag,
'|' ) || str_contains( $tag,
'/' ) ) {
995 return Status::newFatal(
'tags-create-invalid-chars' );
999 $title = Title::makeTitleSafe(
NS_MEDIAWIKI,
"Tag-$tag-description" );
1000 if ( $title ===
null ) {
1001 return Status::newFatal(
'tags-create-invalid-title-chars' );
1004 return Status::newGood();
1022 if ( $performer !==
null ) {
1023 if ( !$performer->isAllowed(
'managechangetags' ) ) {
1024 return Status::newFatal(
'tags-manage-no-permission' );
1026 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1027 return Status::newFatal(
1028 'tags-manage-blocked',
1029 $performer->getUser()->getName()
1033 $user = $services->getUserFactory()->newFromAuthority( $performer );
1037 if ( !$status->isGood() ) {
1042 $changeTagsStore = $services->getChangeTagsStore();
1044 isset( $changeTagsStore->tagUsageStatistics()[$tag] ) ||
1045 in_array( $tag, $changeTagsStore->listDefinedTags() )
1047 return Status::newFatal(
'tags-create-already-exists', $tag );
1051 $canCreateResult = Status::newGood();
1052 (
new HookRunner( $services->getHookContainer() ) )->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1053 return $canCreateResult;
1076 bool $ignoreWarnings =
false, array $logEntryTags = []
1080 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1081 $result->value =
null;
1086 $changeTagsStore->defineTag( $tag );
1087 $logId = $changeTagsStore->logTagManagementAction(
'create', $tag, $reason,
1088 $performer->
getUser(),
null, $logEntryTags );
1090 return Status::newGood( $logId );
1126 if ( $performer !==
null ) {
1127 if ( !$performer->isAllowed(
'deletechangetags' ) ) {
1128 return Status::newFatal(
'tags-delete-no-permission' );
1130 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1131 return Status::newFatal(
1132 'tags-manage-blocked',
1133 $performer->getUser()->getName()
1137 $user = $services->getUserFactory()->newFromAuthority( $performer );
1140 $changeTagsStore = $services->getChangeTagsStore();
1141 $tagUsage = $changeTagsStore->tagUsageStatistics();
1143 !isset( $tagUsage[$tag] ) &&
1144 !in_array( $tag, $changeTagsStore->listDefinedTags() )
1146 return Status::newFatal(
'tags-delete-not-found', $tag );
1149 if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1150 isset( $tagUsage[$tag] ) &&
1151 $tagUsage[$tag] > self::MAX_DELETE_USES
1153 return Status::newFatal(
'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1156 $softwareDefined = $changeTagsStore->listSoftwareDefinedTags();
1157 if ( in_array( $tag, $softwareDefined ) ) {
1160 $status = Status::newFatal(
'tags-delete-not-allowed' );
1163 $status = Status::newGood();
1166 (
new HookRunner( $services->getHookContainer() ) )->onChangeTagCanDelete( $tag, $user, $status );
1188 bool $ignoreWarnings =
false, array $logEntryTags = []
1193 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1194 $result->value =
null;
1199 $hitcount = $changeTagsStore->tagUsageStatistics()[$tag] ?? 0;
1202 $deleteResult = $changeTagsStore->deleteTagEverywhere( $tag );
1203 if ( !$deleteResult->isOK() ) {
1204 return $deleteResult;
1209 $logId = $changeTagsStore->logTagManagementAction(
'delete', $tag, $reason, $performer->
getUser(),
1210 $hitcount, $logEntryTags );
1212 $deleteResult->value = $logId;
1213 return $deleteResult;
1298 private const TAG_DESC_CHARACTER_LIMIT = 120;
1331 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
1332 bool $useAllTags = self::USE_ALL_TAGS
1336 if ( $useAllTags ) {
1337 $tagKeys = $changeTagsStore->listDefinedTags();
1338 $cacheKey =
'tags-list-summary';
1340 $tagKeys = $changeTagsStore->getCoreDefinedTags();
1341 $cacheKey =
'core-software-tags-summary';
1345 $tagHitCounts =
null;
1346 if ( $activeOnly ) {
1347 $tagHitCounts = $changeTagsStore->tagUsageStatistics();
1350 $cacheKey .=
'-all';
1354 return $cache->getWithSetCallback(
1355 $cache->makeKey( $cacheKey, $lang->
getCode() ),
1356 WANObjectCache::TTL_DAY,
1357 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $localizer, $tagKeys, $tagHitCounts ) {
1359 foreach ( $tagKeys as $tagName ) {
1361 if ( $tagHitCounts !==
null ) {
1363 $hits = $tagHitCounts[$tagName] ?? 0;
1375 'labelMsg' => (bool)$labelMsg,
1376 'label' => $labelMsg ? $labelMsg->plain() : $tagName,
1377 'descriptionMsg' => (bool)$descriptionMsg,
1378 'description' => $descriptionMsg ? $descriptionMsg->plain() :
'',
1379 'helpLink' => $helpLink,
1380 'cssClass' => Sanitizer::escapeClass(
'mw-tag-' . $tagName ),
1405 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
bool $useAllTags = self::USE_ALL_TAGS,
1410 foreach ( $tags as &$tagInfo ) {
1411 if ( $tagInfo[
'labelMsg'] ) {
1415 $labelMsg =
new RawMessage( $tagInfo[
'label'] );
1416 $tagInfo[
'label'] = Sanitizer::stripAllTags( $localizer->
msg( $labelMsg )->parse() );
1419 $tagInfo[
'label'] = $localizer->
msg(
'tag-hidden', $tagInfo[
'name'] )->text();
1422 if ( $labelsOnly ) {
1423 unset( $tagInfo[
'description'] );
1424 } elseif ( $tagInfo[
'descriptionMsg'] ) {
1426 if (
wfEscapeWikiText( $tagInfo[
'description'] ) !== $tagInfo[
'description'] ) {
1427 $descriptionMsg =
new RawMessage( $tagInfo[
'description'] );
1428 $tagInfo[
'description'] = Sanitizer::stripAllTags( $localizer->
msg( $descriptionMsg )->parse() );
1431 self::TAG_DESC_CHARACTER_LIMIT );
1433 unset( $tagInfo[
'labelMsg'] );
1434 unset( $tagInfo[
'descriptionMsg'] );
1438 usort( $tags,
static function ( $a, $b ) {
1439 return strcasecmp( $a[
'label'], $b[
'label'] );
1460 return $performer->
isAllowed(
'changetags' ) && (bool)$changeTagsStore->listExplicitlyDefinedTags();
1465class_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,...
Interface for objects which can provide a MediaWiki context on request.