Merge "changetags: Move more functions from ChangeTags to ChangeTagsStore"

This commit is contained in:
jenkins-bot 2023-06-08 08:05:24 +00:00 committed by Gerrit Code Review
commit a8620ac709
4 changed files with 339 additions and 282 deletions

View file

@ -194,6 +194,9 @@ because of Phabricator reports.
without deprecation.
* ApiQuery::getNamedDB() and ApiQueryBase::selectNamedDB(), deprecated in
1.39, have been removed.
* ChangeTags::addTagsAccompanyingChangeWithChecks() and
ChangeTags::undefineTag() unused everywhere, have been removed without
deprecation.
* SelectQueryBuilder::lockForUpdate(), deprecated in 1.40 and unused,
has been removed without hard deprecation.
* …

View file

@ -415,6 +415,7 @@ return [
$services->getMainWANObjectCache(),
$services->getHookContainer(),
LoggerFactory::getInstance( 'ChangeTags' ),
$services->getUserFactory(),
new ServiceOptions(
ChangeTagsStore::CONSTRUCTOR_OPTIONS,
$services->getMainConfig()

View file

@ -30,7 +30,6 @@ use MediaWiki\Permissions\Authority;
use MediaWiki\Storage\NameTableAccessException;
use MediaWiki\Title\Title;
use MediaWiki\User\UserIdentity;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IReadableDatabase;
class ChangeTags {
@ -115,11 +114,6 @@ class ChangeTags {
*/
private const CHANGE_TAG = 'change_tag';
/**
* Name of change_tag_def table
*/
private const CHANGE_TAG_DEF = 'change_tag_def';
public const DISPLAY_TABLE_ALIAS = 'changetagdisplay';
/**
@ -170,7 +164,7 @@ class ChangeTags {
$classes = [];
$tags = explode( ',', $tags );
$order = array_flip( self::listDefinedTags() );
$order = array_flip( MediaWikiServices::getInstance()->getChangeTagsStore()->listDefinedTags() );
usort( $tags, static function ( $a, $b ) use ( $order ) {
return ( $order[ $a ] ?? INF ) <=> ( $order[ $b ] ?? INF );
} );
@ -283,6 +277,7 @@ class ChangeTags {
/**
* Add tags to a change given its rc_id, rev_id and/or log_id
*
* @deprecated since 1.41 use ChangeTagsStore instead.
* @param string|string[] $tags Tags to add to the change
* @param int|null $rc_id The rc_id of the change to add the tags to
* @param int|null $rev_id The rev_id of the change to add the tags to
@ -296,8 +291,9 @@ class ChangeTags {
public static function addTags( $tags, $rc_id = null, $rev_id = null,
$log_id = null, $params = null, RecentChange $rc = null
) {
$result = self::updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
return (bool)$result[0];
return MediaWikiServices::getInstance()->getChangeTagsStore()->addTags(
$tags, $rc_id, $rev_id, $log_id, $params, $rc
);
}
/**
@ -309,6 +305,7 @@ class ChangeTags {
* have registered using the ListDefinedTags hook. When dealing with user
* input, call updateTagsWithChecks() instead.
*
* @deprecated since 1.41 use ChangeTagStore::updateTags()
* @param string|array|null $tagsToAdd Tags to add to the change
* @param string|array|null $tagsToRemove Tags to remove from the change
* @param int|null &$rc_id The rc_id of the change to add the tags to.
@ -333,179 +330,9 @@ class ChangeTags {
&$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
UserIdentity $user = null
) {
$tagsToAdd = array_filter(
(array)$tagsToAdd, // Make sure we're submitting all tags...
static function ( $value ) {
return ( $value ?? '' ) !== '';
}
return MediaWikiServices::getInstance()->getChangeTagsStore()->updateTags(
$tagsToAdd, $tagsToRemove, $rc_id, $rev_id, $log_id, $params, $rc, $user
);
$tagsToRemove = array_filter(
(array)$tagsToRemove,
static function ( $value ) {
return ( $value ?? '' ) !== '';
}
);
if ( !$rc_id && !$rev_id && !$log_id ) {
throw new BadMethodCallException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
'specified when adding or removing a tag from a change!' );
}
$dbw = MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->getPrimaryDatabase();
// Might as well look for rcids and so on.
if ( !$rc_id ) {
// Info might be out of date, somewhat fractionally, on replica DB.
// LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
// so use that relation to avoid full table scans.
if ( $log_id ) {
$rc_id = $dbw->newSelectQueryBuilder()
->select( 'rc_id' )
->from( 'logging' )
->join( 'recentchanges', null, [
'rc_timestamp = log_timestamp',
'rc_logid = log_id'
] )
->where( [ 'log_id' => $log_id ] )
->caller( __METHOD__ )
->fetchField();
} elseif ( $rev_id ) {
$rc_id = $dbw->newSelectQueryBuilder()
->select( 'rc_id' )
->from( 'revision' )
->join( 'recentchanges', null, [
'rc_this_oldid = rev_id'
] )
->where( [ 'rev_id' => $rev_id ] )
->caller( __METHOD__ )
->fetchField();
}
} elseif ( !$log_id && !$rev_id ) {
// Info might be out of date, somewhat fractionally, on replica DB.
$log_id = $dbw->newSelectQueryBuilder()
->select( 'rc_logid' )
->from( 'recentchanges' )
->where( [ 'rc_id' => $rc_id ] )
->caller( __METHOD__ )
->fetchField();
$rev_id = $dbw->newSelectQueryBuilder()
->select( 'rc_this_oldid' )
->from( 'recentchanges' )
->where( [ 'rc_id' => $rc_id ] )
->caller( __METHOD__ )
->fetchField();
}
if ( $log_id && !$rev_id ) {
$rev_id = $dbw->newSelectQueryBuilder()
->select( 'ls_value' )
->from( 'log_search' )
->where( [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ] )
->caller( __METHOD__ )
->fetchField();
} elseif ( !$log_id && $rev_id ) {
$log_id = $dbw->newSelectQueryBuilder()
->select( 'ls_log_id' )
->from( 'log_search' )
->where( [ 'ls_field' => 'associated_rev_id', 'ls_value' => (string)$rev_id ] )
->caller( __METHOD__ )
->fetchField();
}
$prevTags = self::getTags( $dbw, $rc_id, $rev_id, $log_id );
// add tags
$tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
$newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
// remove tags
$tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
$newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
sort( $prevTags );
sort( $newTags );
if ( $prevTags == $newTags ) {
return [ [], [], $prevTags ];
}
// insert a row into change_tag for each new tag
$changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
if ( count( $tagsToAdd ) ) {
$changeTagMapping = [];
foreach ( $tagsToAdd as $tag ) {
$changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
}
$fname = __METHOD__;
// T207881: update the counts at the end of the transaction
$dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tagsToAdd, $fname ) {
$dbw->update(
self::CHANGE_TAG_DEF,
[ 'ctd_count = ctd_count + 1' ],
[ 'ctd_name' => $tagsToAdd ],
$fname
);
}, $fname );
$tagsRows = [];
foreach ( $tagsToAdd as $tag ) {
// Filter so we don't insert NULLs as zero accidentally.
// Keep in mind that $rc_id === null means "I don't care/know about the
// rc_id, just delete $tag on this revision/log entry". It doesn't
// mean "only delete tags on this revision/log WHERE rc_id IS NULL".
$tagsRows[] = array_filter(
[
'ct_rc_id' => $rc_id,
'ct_log_id' => $log_id,
'ct_rev_id' => $rev_id,
'ct_params' => $params,
'ct_tag_id' => $changeTagMapping[$tag] ?? null,
]
);
}
$dbw->insert( self::CHANGE_TAG, $tagsRows, __METHOD__, [ 'IGNORE' ] );
}
// delete from change_tag
if ( count( $tagsToRemove ) ) {
$fname = __METHOD__;
foreach ( $tagsToRemove as $tag ) {
$conds = array_filter(
[
'ct_rc_id' => $rc_id,
'ct_log_id' => $log_id,
'ct_rev_id' => $rev_id,
'ct_tag_id' => $changeTagDefStore->getId( $tag ),
]
);
$dbw->delete( self::CHANGE_TAG, $conds, __METHOD__ );
if ( $dbw->affectedRows() ) {
// T207881: update the counts at the end of the transaction
$dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tag, $fname ) {
$dbw->update(
self::CHANGE_TAG_DEF,
[ 'ctd_count = ctd_count - 1' ],
[ 'ctd_name' => $tag ],
$fname
);
$dbw->delete(
self::CHANGE_TAG_DEF,
[ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
$fname
);
}, $fname );
}
}
}
$services = MediaWikiServices::getInstance();
$userObj = $user ? $services->getUserFactory()->newFromUserIdentity( $user ) : null;
( new HookRunner( $services->getHookContainer() ) )->onChangeTagsAfterUpdateTags(
$tagsToAdd, $tagsToRemove, $prevTags, $rc_id, $rev_id, $log_id, $params, $rc, $userObj );
return [ $tagsToAdd, $tagsToRemove, $prevTags ];
}
/**
@ -530,6 +357,7 @@ class ChangeTags {
* Return all the tags associated with the given recent change ID,
* revision ID, and/or log entry ID.
*
* @deprecated since 1.41 use ChangeTagStore::getTags()
* @param IReadableDatabase $db the database to query
* @param int|null $rc_id
* @param int|null $rev_id
@ -537,7 +365,7 @@ class ChangeTags {
* @return string[]
*/
public static function getTags( IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
return array_keys( self::getTagsWithData( $db, $rc_id, $rev_id, $log_id ) );
return MediaWikiServices::getInstance()->getChangeTagsStore()->getTags( $db, $rc_id, $rev_id, $log_id );
}
/**
@ -598,7 +426,7 @@ class ChangeTags {
}
// to be applied, a tag has to be explicitly defined
$allowedTags = self::listExplicitlyDefinedTags();
$allowedTags = $services->getChangeTagsStore()->listExplicitlyDefinedTags();
( new HookRunner( $services->getHookContainer() ) )->onChangeTagsAllowedAdd( $allowedTags, $tags, $user );
$disallowedTags = array_diff( $tags, $allowedTags );
if ( $disallowedTags ) {
@ -609,42 +437,6 @@ class ChangeTags {
return Status::newGood();
}
/**
* Adds tags to a given change, checking whether it is allowed first, but
* without adding a log entry. Useful for cases where the tag is being added
* along with the action that generated the change (e.g. tagging an edit as
* it is being made).
*
* Extensions should not use this function, unless directly handling a user
* request to add a particular tag. Normally, extensions should call
* ChangeTags::updateTags() instead.
*
* @param string[] $tags Tags to apply
* @param int|null $rc_id The rc_id of the change to add the tags to
* @param int|null $rev_id The rev_id of the change to add the tags to
* @param int|null $log_id The log_id of the change to add the tags to
* @param string $params Params to put in the ct_params field of table
* 'change_tag' when adding tags
* @param Authority $performer Who to give credit for the action
* @return Status
* @since 1.25
*/
public static function addTagsAccompanyingChangeWithChecks(
array $tags, $rc_id, $rev_id, $log_id, $params, Authority $performer
) {
// are we allowed to do this?
$result = self::canAddTagsAccompanyingChange( $tags, $performer );
if ( !$result->isOK() ) {
$result->value = null;
return $result;
}
// do it!
self::addTags( $tags, $rc_id, $rev_id, $log_id, $params );
return Status::newGood( true );
}
/**
* Is it OK to allow the user to adds and remove the given tags to/from a
* change?
@ -677,10 +469,11 @@ class ChangeTags {
}
}
$changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
if ( $tagsToAdd ) {
// to be added, a tag has to be explicitly defined
// @todo Allow extensions to define tags that can be applied by users...
$explicitlyDefinedTags = self::listExplicitlyDefinedTags();
$explicitlyDefinedTags = $changeTagStore->listExplicitlyDefinedTags();
$diff = array_diff( $tagsToAdd, $explicitlyDefinedTags );
if ( $diff ) {
return self::restrictedTagError( 'tags-update-add-not-allowed-one',
@ -692,7 +485,7 @@ class ChangeTags {
// to be removed, a tag must not be defined by an extension, or equivalently it
// has to be either explicitly defined or not defined at all
// (assuming no edge case of a tag both explicitly-defined and extension-defined)
$softwareDefinedTags = self::listSoftwareDefinedTags();
$softwareDefinedTags = $changeTagStore->listSoftwareDefinedTags();
$intersect = array_intersect( $tagsToRemove, $softwareDefinedTags );
if ( $intersect ) {
return self::restrictedTagError( 'tags-update-remove-not-allowed-one',
@ -762,7 +555,8 @@ class ChangeTags {
}
// do it!
[ $tagsAdded, $tagsRemoved, $initialTags ] = self::updateTags( $tagsToAdd,
$changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
[ $tagsAdded, $tagsRemoved, $initialTags ] = $changeTagStore->updateTags( $tagsToAdd,
$tagsToRemove, $rc_id, $rev_id, $log_id, $params, null, $user );
if ( !$tagsAdded && !$tagsRemoved ) {
// no-op, don't log it
@ -854,7 +648,7 @@ class ChangeTags {
$conds = (array)$conds;
$options = (array)$options;
$fields['ts_tags'] = self::makeTagSummarySubquery( $tables );
$fields['ts_tags'] = MediaWikiServices::getInstance()->getChangeTagsStore()->makeTagSummarySubquery( $tables );
// We use an alias and qualify the conditions in case there are
// multiple joins to this table.
// In particular for compatibility with the RC filters that extension Translate does.
@ -992,8 +786,9 @@ class ChangeTags {
}
$config = $context->getConfig();
$changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
if ( !$config->get( MainConfigNames::UseTagFilter ) ||
!count( self::listDefinedTags() ) ) {
!count( $changeTagStore->listDefinedTags() ) ) {
return [];
}
@ -1054,19 +849,6 @@ class ChangeTags {
MediaWikiServices::getInstance()->getChangeTagsStore()->defineTag( $tag );
}
/**
* Update ctd_user_defined = 0 field in change_tag_def.
* The tag may remain in use by extensions, and may still show up as 'defined'
* if an extension is setting it from the ListDefinedTags hook.
*
* @deprecated since 1.41 use ChangeTagsStore
* @param string $tag Tag to remove
* @since 1.25
*/
public static function undefineTag( $tag ) {
MediaWikiServices::getInstance()->getChangeTagsStore()->undefineTag( $tag );
}
/**
* Is it OK to allow the user to activate this tag?
*
@ -1092,14 +874,14 @@ class ChangeTags {
// defined tags cannot be activated (a defined tag is either extension-
// defined, in which case the extension chooses whether or not to active it;
// or user-defined, in which case it is considered active)
$definedTags = self::listDefinedTags();
$changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
$definedTags = $changeTagStore->listDefinedTags();
if ( in_array( $tag, $definedTags ) ) {
return Status::newFatal( 'tags-activate-not-allowed', $tag );
}
// non-existing tags cannot be activated
$tagUsage = self::tagUsageStatistics();
if ( !isset( $tagUsage[$tag] ) ) { // we already know the tag is undefined
if ( !isset( $changeTagStore->tagUsageStatistics()[$tag] ) ) { // we already know the tag is undefined
return Status::newFatal( 'tags-activate-not-found', $tag );
}
@ -1274,8 +1056,11 @@ class ChangeTags {
}
// does the tag already exist?
$tagUsage = self::tagUsageStatistics();
if ( isset( $tagUsage[$tag] ) || in_array( $tag, self::listDefinedTags() ) ) {
$changeTagStore = $services->getChangeTagsStore();
if (
isset( $changeTagStore->tagUsageStatistics()[$tag] ) ||
in_array( $tag, $changeTagStore->listDefinedTags() )
) {
return Status::newFatal( 'tags-create-already-exists', $tag );
}
@ -1314,11 +1099,8 @@ class ChangeTags {
return $result;
}
// do it!
self::defineTag( $tag );
// log it
$changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
$changeTagStore->defineTag( $tag );
$logId = $changeTagStore->logTagManagementAction( 'create', $tag, $reason,
$performer->getUser(), null, $logEntryTags );
@ -1355,7 +1137,6 @@ class ChangeTags {
* @since 1.25
*/
public static function canDeleteTag( $tag, Authority $performer = null, int $flags = 0 ) {
$tagUsage = self::tagUsageStatistics();
$user = null;
$services = MediaWikiServices::getInstance();
if ( $performer !== null ) {
@ -1372,7 +1153,12 @@ class ChangeTags {
$user = $services->getUserFactory()->newFromAuthority( $performer );
}
if ( !isset( $tagUsage[$tag] ) && !in_array( $tag, self::listDefinedTags() ) ) {
$changeTagStore = $services->getChangeTagsStore();
$tagUsage = $changeTagStore->tagUsageStatistics();
if (
!isset( $tagUsage[$tag] ) &&
!in_array( $tag, $changeTagStore->listDefinedTags() )
) {
return Status::newFatal( 'tags-delete-not-found', $tag );
}
@ -1383,7 +1169,7 @@ class ChangeTags {
return Status::newFatal( 'tags-delete-too-many-uses', $tag, self::MAX_DELETE_USES );
}
$softwareDefined = self::listSoftwareDefinedTags();
$softwareDefined = $changeTagStore->listSoftwareDefinedTags();
if ( in_array( $tag, $softwareDefined ) ) {
// extension-defined tags can't be deleted unless the extension
// specifically allows it
@ -1417,6 +1203,7 @@ class ChangeTags {
public static function deleteTagWithChecks( string $tag, string $reason, Authority $performer,
bool $ignoreWarnings = false, array $logEntryTags = []
) {
$changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
// are we allowed to do this?
$result = self::canDeleteTag( $tag, $performer );
if ( $ignoreWarnings ? !$result->isOK() : !$result->isGood() ) {
@ -1425,11 +1212,10 @@ class ChangeTags {
}
// store the tag usage statistics
$tagUsage = self::tagUsageStatistics();
$hitcount = $tagUsage[$tag] ?? 0;
$hitcount = $changeTagStore->tagUsageStatistics()[$tag] ?? 0;
// do it!
$deleteResult = self::deleteTagEverywhere( $tag );
$deleteResult = $changeTagStore->deleteTagEverywhere( $tag );
if ( !$deleteResult->isOK() ) {
return $deleteResult;
}
@ -1446,34 +1232,12 @@ class ChangeTags {
/**
* Lists those tags which core or extensions report as being "active".
*
* @deprecated since 1.41 use ChangeTagsStore instead
* @return array
* @since 1.25
*/
public static function listSoftwareActivatedTags() {
// core active tags
$tags = self::getSoftwareTags();
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
if ( !$hookContainer->isRegistered( 'ChangeTagsListActive' ) ) {
return $tags;
}
$hookRunner = new HookRunner( $hookContainer );
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
return $cache->getWithSetCallback(
$cache->makeKey( 'active-tags' ),
WANObjectCache::TTL_MINUTE * 5,
static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
$setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
// Ask extensions which tags they consider active
$hookRunner->onChangeTagsListActive( $tags );
return $tags;
},
[
'checkKeys' => [ $cache->makeKey( 'active-tags' ) ],
'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
'pcTTL' => WANObjectCache::TTL_PROC_LONG
]
);
return MediaWikiServices::getInstance()->getChangeTagsStore()->listSoftwareActivatedTags();
}
/**
@ -1481,12 +1245,11 @@ class ChangeTags {
* It returns a union of the results of listExplicitlyDefinedTags() and
* listSoftwareDefinedTags()
*
* @deprecated since 1.41 use ChangeTagsStore instead
* @return string[] Array of strings: tags
*/
public static function listDefinedTags() {
$tags1 = self::listExplicitlyDefinedTags();
$tags2 = self::listSoftwareDefinedTags();
return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
return MediaWikiServices::getInstance()->getChangeTagsStore()->listDefinedTags();
}
/**
@ -1574,11 +1337,12 @@ class ChangeTags {
$cache->makeKey( 'tags-list-summary', $lang->getCode() ),
WANObjectCache::TTL_DAY,
static function ( $oldValue, &$ttl, array &$setOpts ) use ( $localizer ) {
$tagHitCounts = self::tagUsageStatistics();
$changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
$tagHitCounts = $changeTagStore->tagUsageStatistics();
$result = [];
// Only list tags that are still actively defined
foreach ( self::listDefinedTags() as $tagName ) {
foreach ( $changeTagStore->listDefinedTags() as $tagName ) {
// Only list tags with more than 0 hits
$hits = $tagHitCounts[$tagName] ?? 0;
if ( $hits <= 0 ) {
@ -1657,6 +1421,7 @@ class ChangeTags {
* @return bool
*/
public static function showTagEditingUI( Authority $performer ) {
return $performer->isAllowed( 'changetags' ) && (bool)self::listExplicitlyDefinedTags();
$changeTagStore = MediaWikiServices::getInstance()->getChangeTagsStore();
return $performer->isAllowed( 'changetags' ) && (bool)$changeTagStore->listExplicitlyDefinedTags();
}
}

View file

@ -30,8 +30,10 @@ use MediaWiki\HookContainer\HookRunner;
use MediaWiki\MainConfigNames;
use MediaWiki\Storage\NameTableStore;
use MediaWiki\Title\Title;
use MediaWiki\User\UserFactory;
use MediaWiki\User\UserIdentity;
use Psr\Log\LoggerInterface;
use RecentChange;
use Status;
use WANObjectCache;
use Wikimedia\Rdbms\Database;
@ -86,6 +88,7 @@ class ChangeTagsStore {
private NameTableStore $changeTagDefStore;
private WANObjectCache $wanCache;
private HookRunner $hookRunner;
private UserFactory $userFactory;
private HookContainer $hookContainer;
public function __construct(
@ -94,6 +97,7 @@ class ChangeTagsStore {
WANObjectCache $wanCache,
HookContainer $hookContainer,
LoggerInterface $logger,
UserFactory $userFactory,
ServiceOptions $options
) {
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
@ -103,6 +107,7 @@ class ChangeTagsStore {
$this->changeTagDefStore = $changeTagDefStore;
$this->wanCache = $wanCache;
$this->hookContainer = $hookContainer;
$this->userFactory = $userFactory;
$this->hookRunner = new HookRunner( $hookContainer );
}
@ -462,4 +467,287 @@ class ChangeTagsStore {
]
);
}
/**
* Return all the tags associated with the given recent change ID,
* revision ID, and/or log entry ID.
*
* @param IReadableDatabase $db the database to query
* @param int|null $rc_id
* @param int|null $rev_id
* @param int|null $log_id
* @return string[]
*/
public function getTags( IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
return array_keys( $this->getTagsWithData( $db, $rc_id, $rev_id, $log_id ) );
}
/**
* Basically lists defined tags which count even if they aren't applied to anything.
* It returns a union of the results of listExplicitlyDefinedTags() and
* listSoftwareDefinedTags()
*
* @return string[] Array of strings: tags
*/
public function listDefinedTags() {
$tags1 = $this->listExplicitlyDefinedTags();
$tags2 = $this->listSoftwareDefinedTags();
return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
}
/**
* Add and remove tags to/from a change given its rc_id, rev_id and/or log_id,
* without verifying that the tags exist or are valid. If a tag is present in
* both $tagsToAdd and $tagsToRemove, it will be removed.
*
* This function should only be used by extensions to manipulate tags they
* have registered using the ListDefinedTags hook. When dealing with user
* input, call updateTagsWithChecks() instead.
*
* @param string|array|null $tagsToAdd Tags to add to the change
* @param string|array|null $tagsToRemove Tags to remove from the change
* @param int|null &$rc_id The rc_id of the change to add the tags to.
* Pass a variable whose value is null if the rc_id is not relevant or unknown.
* @param int|null &$rev_id The rev_id of the change to add the tags to.
* Pass a variable whose value is null if the rev_id is not relevant or unknown.
* @param int|null &$log_id The log_id of the change to add the tags to.
* Pass a variable whose value is null if the log_id is not relevant or unknown.
* @param string|null $params Params to put in the ct_params field of table
* 'change_tag' when adding tags
* @param RecentChange|null $rc Recent change being tagged, in case the tagging accompanies
* the action
* @param UserIdentity|null $user Tagging user, in case the tagging is subsequent to the tagged action
*
* @return array Index 0 is an array of tags actually added, index 1 is an
* array of tags actually removed, index 2 is an array of tags present on the
* revision or log entry before any changes were made
*/
public function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
&$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
UserIdentity $user = null
) {
$tagsToAdd = array_filter(
(array)$tagsToAdd, // Make sure we're submitting all tags...
static function ( $value ) {
return ( $value ?? '' ) !== '';
}
);
$tagsToRemove = array_filter(
(array)$tagsToRemove,
static function ( $value ) {
return ( $value ?? '' ) !== '';
}
);
if ( !$rc_id && !$rev_id && !$log_id ) {
throw new BadMethodCallException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
'specified when adding or removing a tag from a change!' );
}
$dbw = $this->dbProvider->getPrimaryDatabase();
// Might as well look for rcids and so on.
if ( !$rc_id ) {
// Info might be out of date, somewhat fractionally, on replica DB.
// LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
// so use that relation to avoid full table scans.
if ( $log_id ) {
$rc_id = $dbw->newSelectQueryBuilder()
->select( 'rc_id' )
->from( 'logging' )
->join( 'recentchanges', null, [
'rc_timestamp = log_timestamp',
'rc_logid = log_id'
] )
->where( [ 'log_id' => $log_id ] )
->caller( __METHOD__ )
->fetchField();
} elseif ( $rev_id ) {
$rc_id = $dbw->newSelectQueryBuilder()
->select( 'rc_id' )
->from( 'revision' )
->join( 'recentchanges', null, [
'rc_this_oldid = rev_id'
] )
->where( [ 'rev_id' => $rev_id ] )
->caller( __METHOD__ )
->fetchField();
}
} elseif ( !$log_id && !$rev_id ) {
// Info might be out of date, somewhat fractionally, on replica DB.
$log_id = $dbw->newSelectQueryBuilder()
->select( 'rc_logid' )
->from( 'recentchanges' )
->where( [ 'rc_id' => $rc_id ] )
->caller( __METHOD__ )
->fetchField();
$rev_id = $dbw->newSelectQueryBuilder()
->select( 'rc_this_oldid' )
->from( 'recentchanges' )
->where( [ 'rc_id' => $rc_id ] )
->caller( __METHOD__ )
->fetchField();
}
if ( $log_id && !$rev_id ) {
$rev_id = $dbw->newSelectQueryBuilder()
->select( 'ls_value' )
->from( 'log_search' )
->where( [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ] )
->caller( __METHOD__ )
->fetchField();
} elseif ( !$log_id && $rev_id ) {
$log_id = $dbw->newSelectQueryBuilder()
->select( 'ls_log_id' )
->from( 'log_search' )
->where( [ 'ls_field' => 'associated_rev_id', 'ls_value' => (string)$rev_id ] )
->caller( __METHOD__ )
->fetchField();
}
$prevTags = $this->getTags( $dbw, $rc_id, $rev_id, $log_id );
// add tags
$tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
$newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
// remove tags
$tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
$newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
sort( $prevTags );
sort( $newTags );
if ( $prevTags == $newTags ) {
return [ [], [], $prevTags ];
}
// insert a row into change_tag for each new tag
if ( count( $tagsToAdd ) ) {
$changeTagMapping = [];
foreach ( $tagsToAdd as $tag ) {
$changeTagMapping[$tag] = $this->changeTagDefStore->acquireId( $tag );
}
$fname = __METHOD__;
// T207881: update the counts at the end of the transaction
$dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tagsToAdd, $fname ) {
$dbw->update(
self::CHANGE_TAG_DEF,
[ 'ctd_count = ctd_count + 1' ],
[ 'ctd_name' => $tagsToAdd ],
$fname
);
}, $fname );
$tagsRows = [];
foreach ( $tagsToAdd as $tag ) {
// Filter so we don't insert NULLs as zero accidentally.
// Keep in mind that $rc_id === null means "I don't care/know about the
// rc_id, just delete $tag on this revision/log entry". It doesn't
// mean "only delete tags on this revision/log WHERE rc_id IS NULL".
$tagsRows[] = array_filter(
[
'ct_rc_id' => $rc_id,
'ct_log_id' => $log_id,
'ct_rev_id' => $rev_id,
'ct_params' => $params,
'ct_tag_id' => $changeTagMapping[$tag] ?? null,
]
);
}
$dbw->insert( self::CHANGE_TAG, $tagsRows, __METHOD__, [ 'IGNORE' ] );
}
// delete from change_tag
if ( count( $tagsToRemove ) ) {
$fname = __METHOD__;
foreach ( $tagsToRemove as $tag ) {
$conds = array_filter(
[
'ct_rc_id' => $rc_id,
'ct_log_id' => $log_id,
'ct_rev_id' => $rev_id,
'ct_tag_id' => $this->changeTagDefStore->getId( $tag ),
]
);
$dbw->delete( self::CHANGE_TAG, $conds, __METHOD__ );
if ( $dbw->affectedRows() ) {
// T207881: update the counts at the end of the transaction
$dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tag, $fname ) {
$dbw->update(
self::CHANGE_TAG_DEF,
[ 'ctd_count = ctd_count - 1' ],
[ 'ctd_name' => $tag ],
$fname
);
$dbw->delete(
self::CHANGE_TAG_DEF,
[ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
$fname
);
}, $fname );
}
}
}
$userObj = $user ? $this->userFactory->newFromUserIdentity( $user ) : null;
$this->hookRunner->onChangeTagsAfterUpdateTags(
$tagsToAdd, $tagsToRemove, $prevTags, $rc_id, $rev_id, $log_id, $params, $rc, $userObj );
return [ $tagsToAdd, $tagsToRemove, $prevTags ];
}
/**
* Add tags to a change given its rc_id, rev_id and/or log_id
*
* @param string|string[] $tags Tags to add to the change
* @param int|null $rc_id The rc_id of the change to add the tags to
* @param int|null $rev_id The rev_id of the change to add the tags to
* @param int|null $log_id The log_id of the change to add the tags to
* @param string|null $params Params to put in the ct_params field of table 'change_tag'
* @param RecentChange|null $rc Recent change, in case the tagging accompanies the action
* (this should normally be the case)
*
* @return bool False if no changes are made, otherwise true
*/
public function addTags( $tags, $rc_id = null, $rev_id = null,
$log_id = null, $params = null, RecentChange $rc = null
) {
$result = $this->updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
return (bool)$result[0];
}
/**
* Lists those tags which core or extensions report as being "active".
*
* @return array
* @since 1.41
*/
public function listSoftwareActivatedTags() {
// core active tags
$tags = $this->getSoftwareTags();
if ( !$this->hookContainer->isRegistered( 'ChangeTagsListActive' ) ) {
return $tags;
}
$hookRunner = $this->hookRunner;
return $this->wanCache->getWithSetCallback(
$this->wanCache->makeKey( 'active-tags' ),
WANObjectCache::TTL_MINUTE * 5,
static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner ) {
$setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) );
// Ask extensions which tags they consider active
$hookRunner->onChangeTagsListActive( $tags );
return $tags;
},
[
'checkKeys' => [ $this->wanCache->makeKey( 'active-tags' ) ],
'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
'pcTTL' => WANObjectCache::TTL_PROC_LONG
]
);
}
}