41 'mw-contentmodelchange',
43 'mw-removed-redirect',
44 'mw-changed-redirect-target',
63 wfWarn(
'wgSoftwareTags should be associative array of enabled tags.
64 Please refer to documentation for the list of tags you can enable' );
68 $availableSoftwareTags = !$all ?
72 $softwareTags = array_intersect(
73 $availableSoftwareTags,
74 self::$definedSoftwareTags
104 $tags = explode(
',', $tags );
106 foreach ( $tags as $tag ) {
111 if ( $description ===
false ) {
116 [
'class' =>
'mw-tag-marker ' .
117 Sanitizer::escapeClass(
"mw-tag-marker-$tag" ) ],
120 $classes[] = Sanitizer::escapeClass(
"mw-tag-$tag" );
123 if ( !$displayTags ) {
127 $markers =
$context->msg(
'tag-list-wrapper' )
128 ->numParams( count( $displayTags ) )
129 ->rawParams( implode(
' ', $displayTags ) )
131 $markers =
Xml::tags(
'span', [
'class' =>
'mw-tag-markers' ], $markers );
133 return [ $markers, $classes ];
151 if ( !$msg->exists() ) {
153 return (
new RawMessage(
'$1', [ Message::plaintextParam( $tag ) ] ) )
158 ->inLanguage( $msg->getLanguage() );
161 if ( $msg->isDisabled() ) {
185 return $msg ? $msg->parse() :
false;
201 $msg =
$context->msg(
"tag-$tag-description" );
202 if ( !$msg->exists() ) {
205 if ( $msg->isDisabled() ) {
228 if ( !$originalDesc ) {
232 $taglessDesc = Sanitizer::stripAllTags( $originalDesc->parse() );
234 return $context->getLanguage()->truncateForVisual( $taglessDesc, $length );
251 public static function addTags( $tags, $rc_id =
null, $rev_id =
null,
254 $result =
self::updateTags( $tags,
null, $rc_id, $rev_id, $log_id, $params, $rc );
255 return (
bool)$result[0];
288 public static function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id =
null,
289 &$rev_id =
null, &$log_id =
null, $params =
null,
RecentChange $rc =
null,
292 $tagsToAdd = array_filter( (array)$tagsToAdd );
293 $tagsToRemove = array_filter( (array)$tagsToRemove );
295 if ( !$rc_id && !$rev_id && !$log_id ) {
296 throw new MWException(
'At least one of: RCID, revision ID, and log ID MUST be ' .
297 'specified when adding or removing a tag from a change!' );
308 $rc_id = $dbw->selectField(
309 [
'logging',
'recentchanges' ],
313 'rc_timestamp = log_timestamp',
318 } elseif ( $rev_id ) {
319 $rc_id = $dbw->selectField(
320 [
'revision',
'recentchanges' ],
324 'rc_timestamp = rev_timestamp',
325 'rc_this_oldid = rev_id'
330 } elseif ( !$log_id && !$rev_id ) {
332 $log_id = $dbw->selectField(
335 [
'rc_id' => $rc_id ],
338 $rev_id = $dbw->selectField(
341 [
'rc_id' => $rc_id ],
346 if ( $log_id && !$rev_id ) {
347 $rev_id = $dbw->selectField(
350 [
'ls_field' =>
'associated_rev_id',
'ls_log_id' => $log_id ],
353 } elseif ( !$log_id && $rev_id ) {
354 $log_id = $dbw->selectField(
357 [
'ls_field' =>
'associated_rev_id',
'ls_value' => $rev_id ],
365 $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
366 $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
369 $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
370 $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
374 if ( $prevTags == $newTags ) {
375 return [ [], [], $prevTags ];
379 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
380 if ( count( $tagsToAdd ) ) {
381 $changeTagMapping = [];
382 foreach ( $tagsToAdd as $tag ) {
383 $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
387 $dbw->onTransactionPreCommitOrIdle(
function () use ( $dbw, $tagsToAdd, $fname ) {
390 [
'ctd_count = ctd_count + 1' ],
391 [
'ctd_name' => $tagsToAdd ],
397 foreach ( $tagsToAdd as $tag ) {
402 $tagsRows[] = array_filter(
404 'ct_rc_id' => $rc_id,
405 'ct_log_id' => $log_id,
406 'ct_rev_id' => $rev_id,
407 'ct_params' => $params,
408 'ct_tag_id' => $changeTagMapping[$tag] ??
null,
414 $dbw->insert(
'change_tag', $tagsRows, __METHOD__, [
'IGNORE' ] );
418 if ( count( $tagsToRemove ) ) {
420 foreach ( $tagsToRemove as $tag ) {
421 $conds = array_filter(
423 'ct_rc_id' => $rc_id,
424 'ct_log_id' => $log_id,
425 'ct_rev_id' => $rev_id,
426 'ct_tag_id' => $changeTagDefStore->getId( $tag ),
429 $dbw->delete(
'change_tag', $conds, __METHOD__ );
430 if ( $dbw->affectedRows() ) {
432 $dbw->onTransactionPreCommitOrIdle(
function () use ( $dbw, $tag, $fname ) {
435 [
'ctd_count = ctd_count - 1' ],
436 [
'ctd_name' => $tag ],
442 [
'ctd_name' => $tag,
'ctd_count' => 0,
'ctd_user_defined' => 0 ],
450 Hooks::run(
'ChangeTagsAfterUpdateTags', [ $tagsToAdd, $tagsToRemove, $prevTags,
451 $rc_id, $rev_id, $log_id, $params, $rc, $user ] );
453 return [ $tagsToAdd, $tagsToRemove, $prevTags ];
466 public static function getTags(
IDatabase $db, $rc_id =
null, $rev_id =
null, $log_id =
null ) {
467 $conds = array_filter(
469 'ct_rc_id' => $rc_id,
470 'ct_rev_id' => $rev_id,
471 'ct_log_id' => $log_id,
483 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
484 foreach ( $tagIds as $tagId ) {
485 $tags[] = $changeTagDefStore->getName( (
int)$tagId );
503 $count = count( $tags );
505 $lang->commaList( $tags ), $count );
522 if ( !is_null( $user ) ) {
524 ->userHasRight( $user,
'applychangetags' )
527 } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
534 Hooks::run(
'ChangeTagsAllowedAdd', [ &$allowedTags, $tags, $user ] );
535 $disallowedTags = array_diff( $tags, $allowedTags );
536 if ( $disallowedTags ) {
538 'tags-apply-not-allowed-multi', $disallowedTags );
565 array $tags, $rc_id, $rev_id, $log_id, $params,
User $user
569 if ( !$result->isOK() ) {
570 $result->value =
null;
594 public static function canUpdateTags( array $tagsToAdd, array $tagsToRemove,
597 if ( !is_null( $user ) ) {
599 ->userHasRight( $user,
'changetags' )
602 } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
611 $diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
614 'tags-update-add-not-allowed-multi', $diff );
618 if ( $tagsToRemove ) {
623 $intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
626 'tags-update-remove-not-allowed-multi', $intersect );
664 $rc_id, $rev_id, $log_id, $params, $reason,
User $user
666 if ( is_null( $tagsToAdd ) ) {
669 if ( is_null( $tagsToRemove ) ) {
672 if ( !$tagsToAdd && !$tagsToRemove ) {
683 if ( !$result->isOK() ) {
684 $result->value =
null;
694 list( $tagsAdded, $tagsRemoved, $initialTags ) =
self::updateTags( $tagsToAdd,
695 $tagsToRemove, $rc_id, $rev_id, $log_id, $params,
null, $user );
696 if ( !$tagsAdded && !$tagsRemoved ) {
707 $logEntry->setPerformer( $user );
708 $logEntry->setComment( $reason );
714 $logEntry->setTarget( $rev->getTitle() );
716 } elseif ( $log_id ) {
723 if ( !$logEntry->getTarget() ) {
729 '4::revid' => $rev_id,
730 '5::logid' => $log_id,
731 '6:list:tagsAdded' => $tagsAdded,
732 '7:number:tagsAddedCount' => count( $tagsAdded ),
733 '8:list:tagsRemoved' => $tagsRemoved,
734 '9:number:tagsRemovedCount' => count( $tagsRemoved ),
735 'initialTags' => $initialTags,
737 $logEntry->setParameters( $logParams );
738 $logEntry->setRelations( [
'Tag' => array_merge( $tagsAdded, $tagsRemoved ) ] );
741 $logId = $logEntry->insert( $dbw );
743 $logEntry->publish( $logId,
'udp' );
747 'addedTags' => $tagsAdded,
748 'removedTags' => $tagsRemoved,
773 &$join_conds, &$options, $filter_tag =
''
778 $tables = (array)$tables;
779 $fields = (array)$fields;
780 $conds = (array)$conds;
781 $options = (array)$options;
786 if ( in_array(
'recentchanges', $tables ) ) {
787 $join_cond =
'ct_rc_id=rc_id';
788 } elseif ( in_array(
'logging', $tables ) ) {
789 $join_cond =
'ct_log_id=log_id';
790 } elseif ( in_array(
'revision', $tables ) ) {
791 $join_cond =
'ct_rev_id=rev_id';
792 } elseif ( in_array(
'archive', $tables ) ) {
793 $join_cond =
'ct_rev_id=ar_rev_id';
795 throw new MWException(
'Unable to determine appropriate JOIN condition for tagging.' );
802 $tables[] =
'change_tag';
803 $join_conds[
'change_tag'] = [
'JOIN', $join_cond ];
805 $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
806 foreach ( (array)$filter_tag as $filterTagName ) {
808 $filterTagIds[] = $changeTagDefStore->getId( $filterTagName );
816 if ( $filterTagIds !== [] ) {
817 $conds[
'ct_tag_id'] = $filterTagIds;
821 is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
822 !in_array(
'DISTINCT', $options )
824 $options[] =
'DISTINCT';
839 $tables = (array)$tables;
842 if ( in_array(
'recentchanges', $tables ) ) {
843 $join_cond =
'ct_rc_id=rc_id';
844 } elseif ( in_array(
'logging', $tables ) ) {
845 $join_cond =
'ct_log_id=log_id';
846 } elseif ( in_array(
'revision', $tables ) ) {
847 $join_cond =
'ct_rev_id=rev_id';
848 } elseif ( in_array(
'archive', $tables ) ) {
849 $join_cond =
'ct_rev_id=ar_rev_id';
851 throw new MWException(
'Unable to determine appropriate JOIN condition for tagging.' );
854 $tagTables = [
'change_tag',
'change_tag_def' ];
855 $join_cond_ts_tags = [
'change_tag_def' => [
'JOIN',
'ct_tag_id=ctd_id' ] ];
859 ',', $tagTables, $field, $join_cond, $join_cond_ts_tags
882 if ( !$config->get(
'UseTagFilter' ) || !count( self::listDefinedTags() ) ) {
889 [
'for' =>
'tagfilter' ],
890 $context->msg(
'tag-filter' )->parse()
895 $data[] =
new OOUI\TextInputWidget( [
897 'name' =>
'tagfilter',
898 'value' => $selected,
899 'classes' =>
'mw-tagfilter-input',
906 [
'class' =>
'mw-tagfilter-input mw-ui-input mw-ui-input-inline',
'id' =>
'tagfilter' ]
925 'ctd_user_defined' => 1,
932 [
'ctd_user_defined' => 1 ],
953 [
'ctd_user_defined' => 0 ],
954 [
'ctd_name' => $tag ],
960 [
'ctd_name' => $tag,
'ctd_count' => 0 ],
983 User $user, $tagCount =
null, array $logEntryTags = []
988 $logEntry->setPerformer( $user );
992 $logEntry->setComment( $reason );
994 $params = [
'4::tag' => $tag ];
995 if ( !is_null( $tagCount ) ) {
996 $params[
'5:number:count'] = $tagCount;
998 $logEntry->setParameters( $params );
999 $logEntry->setRelations( [
'Tag' => $tag ] );
1000 $logEntry->addTags( $logEntryTags );
1002 $logId = $logEntry->insert( $dbw );
1003 $logEntry->publish( $logId );
1017 if ( !is_null( $user ) ) {
1019 ->userHasRight( $user,
'managechangetags' )
1022 } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1031 if ( in_array( $tag, $definedTags ) ) {
1037 if ( !isset( $tagUsage[$tag] ) ) {
1062 $ignoreWarnings =
false, array $logEntryTags = []
1066 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1067 $result->value =
null;
1076 null, $logEntryTags );
1091 if ( !is_null( $user ) ) {
1093 ->userHasRight( $user,
'managechangetags' )
1096 } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1103 if ( !in_array( $tag, $explicitlyDefinedTags ) ) {
1127 $ignoreWarnings =
false, array $logEntryTags = []
1131 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1132 $result->value =
null;
1141 null, $logEntryTags );
1155 if ( $tag ===
'' ) {
1163 if ( strpos( $tag,
',' ) !==
false || strpos( $tag,
'|' ) !==
false
1164 || strpos( $tag,
'/' ) !==
false ) {
1170 if ( is_null(
$title ) ) {
1190 if ( !is_null( $user ) ) {
1192 ->userHasRight( $user,
'managechangetags' )
1195 } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1207 if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) {
1213 Hooks::run(
'ChangeTagCanCreate', [ $tag, $user, &$canCreateResult ] );
1214 return $canCreateResult;
1237 $ignoreWarnings =
false, array $logEntryTags = []
1241 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1242 $result->value =
null;
1251 null, $logEntryTags );
1270 $dbw->startAtomic( __METHOD__ );
1273 $tagId = MediaWikiServices::getInstance()->getChangeTagDefStore()->getId( $tag );
1279 $dbw->delete(
'change_tag', [
'ct_tag_id' => $tagId ], __METHOD__ );
1280 $dbw->delete(
'change_tag_def', [
'ctd_name' => $tag ], __METHOD__ );
1281 $dbw->endAtomic( __METHOD__ );
1288 wfDebug(
'ChangeTagAfterDelete error condition downgraded to warning' );
1310 if ( !is_null( $user ) ) {
1312 ->userHasRight( $user,
'deletechangetags' )
1315 } elseif ( $user->getBlock() && $user->getBlock()->isSitewide() ) {
1320 if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) {
1324 if ( isset( $tagUsage[$tag] ) && $tagUsage[$tag] > self::MAX_DELETE_USES ) {
1325 return Status::newFatal(
'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
1329 if ( in_array( $tag, $softwareDefined ) ) {
1360 $ignoreWarnings =
false, array $logEntryTags = []
1364 if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
1365 $result->value =
null;
1371 $hitcount = $tagUsage[$tag] ?? 0;
1375 if ( !$deleteResult->isOK() ) {
1376 return $deleteResult;
1381 $hitcount, $logEntryTags );
1383 $deleteResult->value = $logId;
1384 return $deleteResult;
1399 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1400 return $cache->getWithSetCallback(
1401 $cache->makeKey(
'active-tags' ),
1403 function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags ) {
1407 Hooks::run(
'ChangeTagsListActive', [ &$tags ] );
1411 'checkKeys' => [
$cache->makeKey(
'active-tags' ) ],
1427 return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
1439 $fname = __METHOD__;
1441 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1442 return $cache->getWithSetCallback(
1443 $cache->makeKey(
'valid-tags-db' ),
1445 function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1448 $setOpts += Database::getCacheSetOptions(
$dbr );
1450 $tags =
$dbr->selectFieldValues(
1453 [
'ctd_user_defined' => 1 ],
1457 return array_filter( array_unique( $tags ) );
1460 'checkKeys' => [
$cache->makeKey(
'valid-tags-db' ) ],
1482 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1483 return $cache->getWithSetCallback(
1484 $cache->makeKey(
'valid-tags-hook' ),
1486 function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags ) {
1490 return array_filter( array_unique( $tags ) );
1493 'checkKeys' => [
$cache->makeKey(
'valid-tags-hook' ) ],
1506 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1508 $cache->touchCheckKey(
$cache->makeKey(
'active-tags' ) );
1509 $cache->touchCheckKey(
$cache->makeKey(
'valid-tags-db' ) );
1510 $cache->touchCheckKey(
$cache->makeKey(
'valid-tags-hook' ) );
1511 $cache->touchCheckKey(
$cache->makeKey(
'tags-usage-statistics' ) );
1513 MediaWikiServices::getInstance()->getChangeTagDefStore()->reloadMap();
1523 $fname = __METHOD__;
1525 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1526 return $cache->getWithSetCallback(
1527 $cache->makeKey(
'tags-usage-statistics' ),
1529 function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
1533 [
'ctd_name',
'ctd_count' ],
1536 [
'ORDER BY' =>
'ctd_count DESC' ]
1540 foreach (
$res as $row ) {
1541 $out[$row->ctd_name] = $row->ctd_count;
1547 'checkKeys' => [
$cache->makeKey(
'tags-usage-statistics' ) ],
1569 return MediaWikiServices::getInstance()->getPermissionManager()
1570 ->userHasRight( $user,
'changetags' ) &&
1571 (bool)self::listExplicitlyDefinedTags();