129 private const MAX_DELETE_USES = 5000;
134 private const CHANGE_TAG =
'change_tag';
188 if ( $tags ===
'' || $tags ===
null ) {
192 $localizer = RequestContext::getMain();
197 $tags = explode(
',', $tags );
199 usort( $tags,
static function ( $a, $b ) use ( $order ) {
200 return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
204 foreach ( $tags as $tag ) {
208 $classes[] = Sanitizer::escapeClass(
"mw-tag-$tag" );
210 if ( $description ===
false ) {
213 $displayTags[] = Html::rawElement(
215 [
'class' =>
'mw-tag-marker ' .
216 Sanitizer::escapeClass(
"mw-tag-marker-$tag" ) ],
221 if ( !$displayTags ) {
222 return [
'', $classes ];
225 $markers = $localizer->msg(
'tag-list-wrapper' )
226 ->numParams( count( $displayTags ) )
227 ->rawParams( implode(
' ', $displayTags ) )
229 $markers = Html::rawElement(
'span', [
'class' =>
'mw-tag-markers' ], $markers );
231 return [ $markers, $classes ];
248 $msg = $context->
msg(
"tag-$tag" );
249 if ( !$msg->exists() ) {
255 if ( $msg->isDisabled() ) {
277 $msg = $context->
msg(
"tag-$tag-helppage" )->inContentLanguage();
278 if ( !$msg->isDisabled() ) {
279 return Skin::makeInternalOrExternalUrl( $msg->text() ) ?:
null;
298 if ( $msg && $link ) {
299 $label = $msg->parse();
301 if ( !str_contains( $label,
'<a ' ) ) {
302 return Html::rawElement(
'a', [
'href' => $link ], $label );
305 return $msg ? $msg->parse() :
false;
321 $msg = $context->
msg(
"tag-$tag-description" );
322 return $msg->isDisabled() ? false : $msg;
339 public static function addTags( $tags, $rc_id =
null, $rev_id =
null,
340 $log_id =
null, $params =
null, ?
RecentChange $rc =
null
344 $tags, $rc_id, $rev_id, $log_id, $params, $rc
378 public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id =
null,
379 &$rev_id =
null, &$log_id =
null, $params =
null, ?
RecentChange $rc =
null,
384 $tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, $rc, $user
434 $lang = RequestContext::getMain()->getLanguage();
435 $tags = array_values( $tags );
436 $count = count( $tags );
437 $status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
438 $lang->commaList( $tags ), $count );
439 $status->value = $tags;
464 if ( $performer !==
null ) {
465 if ( !$performer->isAllowed(
'applychangetags' ) ) {
466 return Status::newFatal(
'tags-apply-no-permission' );
469 if ( $checkBlock && $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
470 return Status::newFatal(
471 'tags-apply-blocked',
472 $performer->getUser()->getName()
477 $user = $services->getUserFactory()->newFromAuthority( $performer );
481 $allowedTags = $services->getChangeTagsStore()->listExplicitlyDefinedTags();
482 (
new HookRunner( $services->getHookContainer() ) )->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
483 $disallowedTags = array_diff( $tags, $allowedTags );
484 if ( $disallowedTags ) {
486 'tags-apply-not-allowed-multi', $disallowedTags );
489 return Status::newGood();
511 if ( $performer !==
null ) {
512 if ( !$performer->isDefinitelyAllowed(
'changetags' ) ) {
513 return Status::newFatal(
'tags-update-no-permission' );
516 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
517 return Status::newFatal(
518 'tags-update-blocked',
519 $performer->getUser()->getName()
528 $explicitlyDefinedTags = $changeTagsStore->listExplicitlyDefinedTags();
529 $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
532 'tags-update-add-not-allowed-multi', $diff );
536 if ( $tagsToRemove ) {
540 $softwareDefinedTags = $changeTagsStore->listSoftwareDefinedTags();
541 $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
544 'tags-update-remove-not-allowed-multi', $intersect );
548 return Status::newGood();
582 $rc_id, $rev_id, $log_id, $params,
string $reason,
Authority $performer
584 if ( !$tagsToAdd && !$tagsToRemove ) {
586 return Status::newGood( (
object)[
594 $tagsToRemove ??= [];
598 if ( !$result->isOK() ) {
599 $result->value =
null;
604 $status = PermissionStatus::newEmpty();
606 return Status::wrap( $status );
611 [ $tagsAdded, $tagsRemoved, $initialTags ] = $changeTagsStore->updateTags( $tagsToAdd,
612 $tagsToRemove, $rc_id, $rev_id, $log_id, $params,
null, $performer->
getUser() );
613 if ( !$tagsAdded && !$tagsRemoved ) {
615 return Status::newGood( (
object)[
624 $logEntry->setPerformer( $performer->
getUser() );
625 $logEntry->setComment( $reason );
630 ->getRevisionLookup()
631 ->getRevisionById( $rev_id );
632 if ( $revisionRecord ) {
633 $logEntry->setTarget( $revisionRecord->getPageAsLinkTarget() );
635 } elseif ( $log_id ) {
642 if ( !$logEntry->getTarget() ) {
648 '4::revid' => $rev_id,
649 '5::logid' => $log_id,
650 '6:list:tagsAdded' => $tagsAdded,
651 '7:number:tagsAddedCount' => count( $tagsAdded ),
652 '8:list:tagsRemoved' => $tagsRemoved,
653 '9:number:tagsRemovedCount' => count( $tagsRemoved ),
654 'initialTags' => $initialTags,
656 $logEntry->setParameters( $logParams );
657 $logEntry->setRelations( [
'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
660 $logId = $logEntry->insert( $dbw );
662 $logEntry->publish( $logId,
'udp' );
664 return Status::newGood( (
object)[
666 'addedTags' => $tagsAdded,
667 'removedTags' => $tagsRemoved,
692 &$join_conds, &$options, $filter_tag =
'',
bool $exclude =
false
717 return self::CHANGE_TAG;
750 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
751 bool $useAllTags = self::USE_ALL_TAGS
754 $context = RequestContext::getMain();
757 $config = $context->getConfig();
760 !count( $changeTagsStore->listDefinedTags() ) ) {
766 $context->getLanguage(),
773 foreach ( $tags as $tagInfo ) {
774 $autocomplete[ $tagInfo[
'label'] ] = $tagInfo[
'name'];
778 $data[0] = Html::rawElement(
780 [
'for' =>
'tagfilter' ],
781 $context->msg(
'tag-filter' )->parse()
785 $options = Html::listDropdownOptionsOoui( $autocomplete );
787 $data[1] = new \OOUI\ComboBoxInputWidget( [
789 'name' =>
'tagfilter',
790 'value' => $selected,
791 'classes' =>
'mw-tagfilter-input',
792 'options' => $options,
796 foreach ( $autocomplete as $label => $name ) {
797 $optionsHtml .=
Html::element(
'option', [
'value' => $name ], $label );
799 $datalistHtml = Html::rawElement(
'datalist', [
'id' =>
'tagfilter-datalist' ], $optionsHtml );
801 $data[1] = Html::input(
806 'class' => [
'mw-tagfilter-input',
'mw-ui-input',
'mw-ui-input-inline' ],
809 'list' =>
'tagfilter-datalist',
841 if ( $performer !==
null ) {
842 if ( !$performer->isAllowed(
'managechangetags' ) ) {
843 return Status::newFatal(
'tags-manage-no-permission' );
845 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
846 return Status::newFatal(
847 'tags-manage-blocked',
848 $performer->getUser()->getName()
857 $definedTags = $changeTagsStore->listDefinedTags();
858 if ( in_array( $tag, $definedTags ) ) {
859 return Status::newFatal(
'tags-activate-not-allowed', $tag );
863 if ( !isset( $changeTagsStore->tagUsageStatistics()[$tag] ) ) {
864 return Status::newFatal(
'tags-activate-not-found', $tag );
867 return Status::newGood();
888 bool $ignoreWarnings =
false, array $logEntryTags = []
892 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
893 $result->value =
null;
898 $changeTagsStore->defineTag( $tag );
900 $logId = $changeTagsStore->logTagManagementAction(
'activate', $tag, $reason, $performer->
getUser(),
901 null, $logEntryTags );
903 return Status::newGood( $logId );
916 if ( $performer !==
null ) {
917 if ( !$performer->isAllowed(
'managechangetags' ) ) {
918 return Status::newFatal(
'tags-manage-no-permission' );
920 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
921 return Status::newFatal(
922 'tags-manage-blocked',
923 $performer->getUser()->getName()
930 if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
931 return Status::newFatal(
'tags-deactivate-not-allowed', $tag );
933 return Status::newGood();
954 bool $ignoreWarnings =
false, array $logEntryTags = []
958 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
959 $result->value =
null;
964 $changeTagsStore->undefineTag( $tag );
966 $logId = $changeTagsStore->logTagManagementAction(
'deactivate', $tag, $reason,
967 $performer->
getUser(),
null, $logEntryTags );
969 return Status::newGood( $logId );
982 return Status::newFatal(
'tags-create-no-name' );
989 if ( str_contains( $tag,
',' ) || str_contains( $tag,
'|' ) || str_contains( $tag,
'/' ) ) {
990 return Status::newFatal(
'tags-create-invalid-chars' );
994 $title = Title::makeTitleSafe(
NS_MEDIAWIKI,
"Tag-$tag-description" );
995 if ( $title ===
null ) {
996 return Status::newFatal(
'tags-create-invalid-title-chars' );
999 return Status::newGood();
1017 if ( $performer !==
null ) {
1018 if ( !$performer->isAllowed(
'managechangetags' ) ) {
1019 return Status::newFatal(
'tags-manage-no-permission' );
1021 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1022 return Status::newFatal(
1023 'tags-manage-blocked',
1024 $performer->getUser()->getName()
1028 $user = $services->getUserFactory()->newFromAuthority( $performer );
1032 if ( !$status->isGood() ) {
1037 $changeTagsStore = $services->getChangeTagsStore();
1039 isset( $changeTagsStore->tagUsageStatistics()[$tag] ) ||
1040 in_array( $tag, $changeTagsStore->listDefinedTags() )
1042 return Status::newFatal(
'tags-create-already-exists', $tag );
1046 $canCreateResult = Status::newGood();
1047 (
new HookRunner( $services->getHookContainer() ) )->onChangeTagCanCreate( $tag, $user, $canCreateResult );
1048 return $canCreateResult;
1071 bool $ignoreWarnings =
false, array $logEntryTags = []
1075 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1076 $result->value =
null;
1081 $changeTagsStore->defineTag( $tag );
1082 $logId = $changeTagsStore->logTagManagementAction(
'create', $tag, $reason,
1083 $performer->
getUser(),
null, $logEntryTags );
1085 return Status::newGood( $logId );
1121 if ( $performer !==
null ) {
1122 if ( !$performer->isAllowed(
'deletechangetags' ) ) {
1123 return Status::newFatal(
'tags-delete-no-permission' );
1125 if ( $performer->getBlock() && $performer->getBlock()->isSitewide() ) {
1126 return Status::newFatal(
1127 'tags-manage-blocked',
1128 $performer->getUser()->getName()
1132 $user = $services->getUserFactory()->newFromAuthority( $performer );
1135 $changeTagsStore = $services->getChangeTagsStore();
1136 $tagUsage = $changeTagsStore->tagUsageStatistics();
1138 !isset( $tagUsage[$tag] ) &&
1139 !in_array( $tag, $changeTagsStore->listDefinedTags() )
1141 return Status::newFatal(
'tags-delete-not-found', $tag );
1144 if ( $flags !== self::BYPASS_MAX_USAGE_CHECK &&
1145 isset( $tagUsage[$tag] ) &&
1146 $tagUsage[$tag] > self::MAX_DELETE_USES
1148 return Status::newFatal(
'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1151 $softwareDefined = $changeTagsStore->listSoftwareDefinedTags();
1152 if ( in_array( $tag, $softwareDefined ) ) {
1155 $status = Status::newFatal(
'tags-delete-not-allowed' );
1158 $status = Status::newGood();
1161 (
new HookRunner( $services->getHookContainer() ) )->onChangeTagCanDelete( $tag, $user, $status );
1183 bool $ignoreWarnings =
false, array $logEntryTags = []
1188 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1189 $result->value =
null;
1194 $hitcount = $changeTagsStore->tagUsageStatistics()[$tag] ?? 0;
1197 $deleteResult = $changeTagsStore->deleteTagEverywhere( $tag );
1198 if ( !$deleteResult->isOK() ) {
1199 return $deleteResult;
1204 $logId = $changeTagsStore->logTagManagementAction(
'delete', $tag, $reason, $performer->
getUser(),
1205 $hitcount, $logEntryTags );
1207 $deleteResult->value = $logId;
1208 return $deleteResult;
1293 private const TAG_DESC_CHARACTER_LIMIT = 120;
1326 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
1327 bool $useAllTags = self::USE_ALL_TAGS
1331 if ( $useAllTags ) {
1332 $tagKeys = $changeTagsStore->listDefinedTags();
1333 $cacheKey =
'tags-list-summary';
1335 $tagKeys = $changeTagsStore->getCoreDefinedTags();
1336 $cacheKey =
'core-software-tags-summary';
1340 $tagHitCounts =
null;
1341 if ( $activeOnly ) {
1342 $tagHitCounts = $changeTagsStore->tagUsageStatistics();
1345 $cacheKey .=
'-all';
1349 return $cache->getWithSetCallback(
1350 $cache->makeKey( $cacheKey, $lang->
getCode() ),
1351 WANObjectCache::TTL_DAY,
1352 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $localizer, $tagKeys, $tagHitCounts ) {
1354 foreach ( $tagKeys as $tagName ) {
1356 if ( $tagHitCounts !==
null ) {
1358 $hits = $tagHitCounts[$tagName] ?? 0;
1370 'labelMsg' => (bool)$labelMsg,
1371 'label' => $labelMsg ? $labelMsg->plain() : $tagName,
1372 'descriptionMsg' => (bool)$descriptionMsg,
1373 'description' => $descriptionMsg ? $descriptionMsg->plain() :
'',
1374 'helpLink' => $helpLink,
1375 'cssClass' => Sanitizer::escapeClass(
'mw-tag-' . $tagName ),
1400 bool $activeOnly = self::TAG_SET_ACTIVE_ONLY,
bool $useAllTags = self::USE_ALL_TAGS,
1405 foreach ( $tags as &$tagInfo ) {
1406 if ( $tagInfo[
'labelMsg'] ) {
1410 $labelMsg =
new RawMessage( $tagInfo[
'label'] );
1411 $tagInfo[
'label'] = Sanitizer::stripAllTags( $localizer->
msg( $labelMsg )->parse() );
1414 $tagInfo[
'label'] = $localizer->
msg(
'tag-hidden', $tagInfo[
'name'] )->text();
1417 if ( $labelsOnly ) {
1418 unset( $tagInfo[
'description'] );
1419 } elseif ( $tagInfo[
'descriptionMsg'] ) {
1421 if (
wfEscapeWikiText( $tagInfo[
'description'] ) !== $tagInfo[
'description'] ) {
1422 $descriptionMsg =
new RawMessage( $tagInfo[
'description'] );
1423 $tagInfo[
'description'] = Sanitizer::stripAllTags( $localizer->
msg( $descriptionMsg )->parse() );
1426 self::TAG_DESC_CHARACTER_LIMIT );
1428 unset( $tagInfo[
'labelMsg'] );
1429 unset( $tagInfo[
'descriptionMsg'] );
1433 usort( $tags,
static function ( $a, $b ) {
1434 return strcasecmp( $a[
'label'], $b[
'label'] );
1455 return $performer->
isAllowed(
'changetags' ) && (bool)$changeTagsStore->listExplicitlyDefinedTags();
1460class_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.1.22 Title|null
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.