uawdijnntqw1x1x1
IP : 216.73.216.54
Hostname : neogeopocket.gameplayer.club
Kernel : Linux neogeopocket.gameplayer.club 5.15.0-173-generic #183-Ubuntu SMP Fri Mar 6 13:29:34 UTC 2026 x86_64
Disable Function : None :)
OS : Linux
PATH:
/
var
/
www
/
html
/
1c732
/
..
/
mediawiki
/
includes
/
..
/
includes
/
changetags
/
ChangeTagsStore.php
/
/
<?php /** * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * * @file */ namespace MediaWiki\ChangeTags; use InvalidArgumentException; use ManualLogEntry; use MediaWiki\Config\ServiceOptions; use MediaWiki\HookContainer\HookContainer; use MediaWiki\HookContainer\HookRunner; use MediaWiki\MainConfigNames; use MediaWiki\Status\Status; use MediaWiki\Storage\NameTableAccessException; use MediaWiki\Storage\NameTableStore; use MediaWiki\Title\Title; use MediaWiki\User\UserFactory; use MediaWiki\User\UserIdentity; use Psr\Log\LoggerInterface; use RecentChange; use Wikimedia\ObjectCache\WANObjectCache; use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\IConnectionProvider; use Wikimedia\Rdbms\IReadableDatabase; use Wikimedia\Rdbms\RawSQLValue; use Wikimedia\Rdbms\SelectQueryBuilder; /** * Read-write access to the change_tags table. * * @since 1.41 * @ingroup ChangeTags */ class ChangeTagsStore { /** * Name of change_tag table */ 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'; /** * @internal For use by ServiceWiring */ public const CONSTRUCTOR_OPTIONS = [ MainConfigNames::SoftwareTags, MainConfigNames::UseTagFilter, ]; /** * A list of tags defined and used by MediaWiki itself. */ private const DEFINED_SOFTWARE_TAGS = [ 'mw-contentmodelchange', 'mw-new-redirect', 'mw-removed-redirect', 'mw-changed-redirect-target', 'mw-blank', 'mw-replace', 'mw-rollback', 'mw-undo', 'mw-manual-revert', 'mw-reverted', 'mw-server-side-upload', ]; private IConnectionProvider $dbProvider; private LoggerInterface $logger; private ServiceOptions $options; private NameTableStore $changeTagDefStore; private WANObjectCache $wanCache; private HookRunner $hookRunner; private UserFactory $userFactory; private HookContainer $hookContainer; public function __construct( IConnectionProvider $dbProvider, NameTableStore $changeTagDefStore, WANObjectCache $wanCache, HookContainer $hookContainer, LoggerInterface $logger, UserFactory $userFactory, ServiceOptions $options ) { $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->dbProvider = $dbProvider; $this->logger = $logger; $this->options = $options; $this->changeTagDefStore = $changeTagDefStore; $this->wanCache = $wanCache; $this->hookContainer = $hookContainer; $this->userFactory = $userFactory; $this->hookRunner = new HookRunner( $hookContainer ); } /** * Loads defined core tags, checks for invalid types (if not array), * and filters for supported and enabled (if $all is false) tags only. * * @param bool $all If true, return all valid defined tags. Otherwise, return only enabled ones. * @return array Array of all defined/enabled tags. */ public function getSoftwareTags( $all = false ): array { $coreTags = $this->options->get( MainConfigNames::SoftwareTags ); if ( !is_array( $coreTags ) ) { $this->logger->warning( 'wgSoftwareTags should be associative array of enabled tags. Please refer to documentation for the list of tags you can enable' ); return []; } $availableSoftwareTags = !$all ? array_keys( array_filter( $coreTags ) ) : array_keys( $coreTags ); return array_intersect( $availableSoftwareTags, self::DEFINED_SOFTWARE_TAGS ); } /** * Return all the tags associated with the given recent change ID, * revision ID, and/or log entry ID, along with any data stored with the tag. * * @param IReadableDatabase $db the database to query * @param int|null $rc_id * @param int|null $rev_id * @param int|null $log_id * @return string[] Tag name => data. Data format is tag-specific. * @since 1.41 */ public function getTagsWithData( IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ): array { if ( !$rc_id && !$rev_id && !$log_id ) { throw new InvalidArgumentException( 'At least one of: RCID, revision ID, and log ID MUST be ' . 'specified when loading tags from a change!' ); } $conds = array_filter( [ 'ct_rc_id' => $rc_id, 'ct_rev_id' => $rev_id, 'ct_log_id' => $log_id, ] ); $result = $db->newSelectQueryBuilder() ->select( [ 'ct_tag_id', 'ct_params' ] ) ->from( self::CHANGE_TAG ) ->where( $conds ) ->caller( __METHOD__ ) ->fetchResultSet(); $tags = []; foreach ( $result as $row ) { $tagName = $this->changeTagDefStore->getName( (int)$row->ct_tag_id ); $tags[$tagName] = $row->ct_params; } return $tags; } /** * Make the tag summary subquery based on the given tables and return it. * * @param string|array $tables Table names, see Database::select * * @return string tag summary subqeury */ public function makeTagSummarySubquery( $tables ) { // Normalize to arrays $tables = (array)$tables; // Figure out which ID field to use if ( in_array( 'recentchanges', $tables ) ) { $join_cond = 'ct_rc_id=rc_id'; } elseif ( in_array( 'logging', $tables ) ) { $join_cond = 'ct_log_id=log_id'; } elseif ( in_array( 'revision', $tables ) ) { $join_cond = 'ct_rev_id=rev_id'; } elseif ( in_array( 'archive', $tables ) ) { $join_cond = 'ct_rev_id=ar_rev_id'; } else { throw new InvalidArgumentException( 'Unable to determine appropriate JOIN condition for tagging.' ); } return $this->dbProvider->getReplicaDatabase() ->newSelectQueryBuilder() ->table( self::CHANGE_TAG ) ->join( self::CHANGE_TAG_DEF, null, 'ct_tag_id=ctd_id' ) ->field( 'ctd_name' ) ->where( $join_cond ) ->buildGroupConcatField( ',' ); } /** * Set ctd_user_defined = 1 in change_tag_def without checking that the tag name is valid. * Extensions should NOT use this function; they can use the ListDefinedTags * hook instead. * * @param string $tag Tag to create * @since 1.41 */ public function defineTag( $tag ) { $dbw = $this->dbProvider->getPrimaryDatabase(); $dbw->newInsertQueryBuilder() ->insertInto( self::CHANGE_TAG_DEF ) ->row( [ 'ctd_name' => $tag, 'ctd_user_defined' => 1, 'ctd_count' => 0 ] ) ->onDuplicateKeyUpdate() ->uniqueIndexFields( [ 'ctd_name' ] ) ->set( [ 'ctd_user_defined' => 1 ] ) ->caller( __METHOD__ )->execute(); // clear the memcache of defined tags $this->purgeTagCacheAll(); } /** * 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. * * @param string $tag Tag to remove * @since 1.41 */ public function undefineTag( $tag ) { $dbw = $this->dbProvider->getPrimaryDatabase(); $dbw->newUpdateQueryBuilder() ->update( self::CHANGE_TAG_DEF ) ->set( [ 'ctd_user_defined' => 0 ] ) ->where( [ 'ctd_name' => $tag ] ) ->caller( __METHOD__ )->execute(); $dbw->newDeleteQueryBuilder() ->deleteFrom( self::CHANGE_TAG_DEF ) ->where( [ 'ctd_name' => $tag, 'ctd_count' => 0 ] ) ->caller( __METHOD__ )->execute(); // clear the memcache of defined tags $this->purgeTagCacheAll(); } /** * Writes a tag action into the tag management log. * * @param string $action * @param string $tag * @param string $reason * @param UserIdentity $user Who to attribute the action to * @param int|null $tagCount For deletion only, how many usages the tag had before * it was deleted. * @param array $logEntryTags Change tags to apply to the entry * that will be created in the tag management log * @return int ID of the inserted log entry * @since 1.41 */ public function logTagManagementAction( string $action, string $tag, string $reason, UserIdentity $user, $tagCount = null, array $logEntryTags = [] ) { $dbw = $this->dbProvider->getPrimaryDatabase(); $logEntry = new ManualLogEntry( 'managetags', $action ); $logEntry->setPerformer( $user ); // target page is not relevant, but it has to be set, so we just put in // the title of Special:Tags $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) ); $logEntry->setComment( $reason ); $params = [ '4::tag' => $tag ]; if ( $tagCount !== null ) { $params['5:number:count'] = $tagCount; } $logEntry->setParameters( $params ); $logEntry->setRelations( [ 'Tag' => $tag ] ); $logEntry->addTags( $logEntryTags ); $logId = $logEntry->insert( $dbw ); $logEntry->publish( $logId ); return $logId; } /** * Permanently removes all traces of a tag from the DB. Good for removing * misspelt or temporary tags. * * This function should be directly called by maintenance scripts only, never * by user-facing code. See deleteTagWithChecks() for functionality that can * safely be exposed to users. * * @param string $tag Tag to remove * @return Status The returned status will be good unless a hook changed it * @since 1.41 */ public function deleteTagEverywhere( $tag ) { $dbw = $this->dbProvider->getPrimaryDatabase(); $dbw->startAtomic( __METHOD__ ); // fetch tag id, this must be done before calling undefineTag(), see T225564 $tagId = $this->changeTagDefStore->getId( $tag ); // set ctd_user_defined = 0 $this->undefineTag( $tag ); // delete from change_tag $dbw->newDeleteQueryBuilder() ->deleteFrom( self::CHANGE_TAG ) ->where( [ 'ct_tag_id' => $tagId ] ) ->caller( __METHOD__ )->execute(); $dbw->newDeleteQueryBuilder() ->deleteFrom( self::CHANGE_TAG_DEF ) ->where( [ 'ctd_name' => $tag ] ) ->caller( __METHOD__ )->execute(); $dbw->endAtomic( __METHOD__ ); // give extensions a chance $status = Status::newGood(); $this->hookRunner->onChangeTagAfterDelete( $tag, $status ); // let's not allow error results, as the actual tag deletion succeeded if ( !$status->isOK() ) { $this->logger->debug( 'ChangeTagAfterDelete error condition downgraded to warning' ); $status->setOK( true ); } // clear the memcache of defined tags $this->purgeTagCacheAll(); return $status; } /** * Invalidates the short-term cache of defined tags used by the * list*DefinedTags functions, as well as the tag statistics cache. * @since 1.41 */ public function purgeTagCacheAll() { $this->wanCache->touchCheckKey( $this->wanCache->makeKey( 'active-tags' ) ); $this->wanCache->touchCheckKey( $this->wanCache->makeKey( 'valid-tags-db' ) ); $this->wanCache->touchCheckKey( $this->wanCache->makeKey( 'valid-tags-hook' ) ); $this->wanCache->touchCheckKey( $this->wanCache->makeKey( 'tags-usage-statistics' ) ); $this->changeTagDefStore->reloadMap(); } /** * Returns a map of any tags used on the wiki to number of edits * tagged with them, ordered descending by the hitcount. * This does not include tags defined somewhere that have never been applied. * @return array Array of string => int */ public function tagUsageStatistics(): array { $fname = __METHOD__; $dbProvider = $this->dbProvider; return $this->wanCache->getWithSetCallback( $this->wanCache->makeKey( 'tags-usage-statistics' ), WANObjectCache::TTL_MINUTE * 5, static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname, $dbProvider ) { $dbr = $dbProvider->getReplicaDatabase(); $res = $dbr->newSelectQueryBuilder() ->select( [ 'ctd_name', 'ctd_count' ] ) ->from( self::CHANGE_TAG_DEF ) ->orderBy( 'ctd_count', SelectQueryBuilder::SORT_DESC ) ->caller( $fname ) ->fetchResultSet(); $out = []; foreach ( $res as $row ) { $out[$row->ctd_name] = $row->ctd_count; } return $out; }, [ 'checkKeys' => [ $this->wanCache->makeKey( 'tags-usage-statistics' ) ], 'lockTSE' => WANObjectCache::TTL_MINUTE * 5, 'pcTTL' => WANObjectCache::TTL_PROC_LONG ] ); } /** * Lists tags explicitly defined in the `change_tag_def` table of the database. * * Tries memcached first. * * @return string[] Array of strings: tags * @since 1.25 */ public function listExplicitlyDefinedTags() { $fname = __METHOD__; $dbProvider = $this->dbProvider; return $this->wanCache->getWithSetCallback( $this->wanCache->makeKey( 'valid-tags-db' ), WANObjectCache::TTL_MINUTE * 5, static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname, $dbProvider ) { $dbr = $dbProvider->getReplicaDatabase(); $setOpts += Database::getCacheSetOptions( $dbr ); $tags = $dbr->newSelectQueryBuilder() ->select( 'ctd_name' ) ->from( self::CHANGE_TAG_DEF ) ->where( [ 'ctd_user_defined' => 1 ] ) ->caller( $fname ) ->fetchFieldValues(); return array_unique( $tags ); }, [ 'checkKeys' => [ $this->wanCache->makeKey( 'valid-tags-db' ) ], 'lockTSE' => WANObjectCache::TTL_MINUTE * 5, 'pcTTL' => WANObjectCache::TTL_PROC_LONG ] ); } /** * Lists tags defined by core or extensions using the ListDefinedTags hook. * Extensions need only define those tags they deem to be in active use. * * Tries memcached first. * * @return string[] Array of strings: tags * @since 1.25 */ public function listSoftwareDefinedTags() { // core defined tags $tags = $this->getSoftwareTags( true ); if ( !$this->hookContainer->isRegistered( 'ListDefinedTags' ) ) { return $tags; } $hookRunner = $this->hookRunner; $dbProvider = $this->dbProvider; return $this->wanCache->getWithSetCallback( $this->wanCache->makeKey( 'valid-tags-hook' ), WANObjectCache::TTL_MINUTE * 5, static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner, $dbProvider ) { $setOpts += Database::getCacheSetOptions( $dbProvider->getReplicaDatabase() ); $hookRunner->onListDefinedTags( $tags ); return array_unique( $tags ); }, [ 'checkKeys' => [ $this->wanCache->makeKey( 'valid-tags-hook' ) ], 'lockTSE' => WANObjectCache::TTL_MINUTE * 5, 'pcTTL' => WANObjectCache::TTL_PROC_LONG ] ); } /** * 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 InvalidArgumentException( '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->newUpdateQueryBuilder() ->update( self::CHANGE_TAG_DEF ) ->set( [ 'ctd_count' => new RawSQLValue( 'ctd_count + 1' ) ] ) ->where( [ 'ctd_name' => $tagsToAdd ] ) ->caller( $fname )->execute(); }, $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->newInsertQueryBuilder() ->insertInto( self::CHANGE_TAG ) ->ignore() ->rows( $tagsRows ) ->caller( __METHOD__ )->execute(); } // 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->newDeleteQueryBuilder() ->deleteFrom( self::CHANGE_TAG ) ->where( $conds ) ->caller( __METHOD__ )->execute(); if ( $dbw->affectedRows() ) { // T207881: update the counts at the end of the transaction $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tag, $fname ) { $dbw->newUpdateQueryBuilder() ->update( self::CHANGE_TAG_DEF ) ->set( [ 'ctd_count' => new RawSQLValue( 'ctd_count - 1' ) ] ) ->where( [ 'ctd_name' => $tag ] ) ->caller( $fname )->execute(); $dbw->newDeleteQueryBuilder() ->deleteFrom( self::CHANGE_TAG_DEF ) ->where( [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ] ) ->caller( $fname )->execute(); }, $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; $dbProvider = $this->dbProvider; return $this->wanCache->getWithSetCallback( $this->wanCache->makeKey( 'active-tags' ), WANObjectCache::TTL_MINUTE * 5, static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner, $dbProvider ) { $setOpts += Database::getCacheSetOptions( $dbProvider->getReplicaDatabase() ); // 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 ] ); } /** * Applies all tags-related changes to a query. * Handles selecting tags, and filtering. * Needs $tables to be set up properly, so we can figure out which join conditions to use. * * WARNING: If $filter_tag contains more than one tag and $exclude is false, this function * will add DISTINCT, which may cause performance problems for your query unless you put * the ID field of your table at the end of the ORDER BY, and set a GROUP BY equal to the * ORDER BY. For example, if you had ORDER BY foo_timestamp DESC, you will now need * GROUP BY foo_timestamp, foo_id ORDER BY foo_timestamp DESC, foo_id DESC. * * @deprecated since 1.41 use ChangeTagsStore::modifyDisplayQueryBuilder instead * * @param string|array &$tables Table names, see Database::select * @param string|array &$fields Fields used in query, see Database::select * @param string|array &$conds Conditions used in query, see Database::select * @param array &$join_conds Join conditions, see Database::select * @param string|array &$options Options, see Database::select * @param string|array|false|null $filter_tag Tag(s) to select on (OR) * @param bool $exclude If true, exclude tag(s) from $filter_tag (NOR) * */ public function modifyDisplayQuery( &$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag = '', bool $exclude = false ) { $useTagFilter = $this->options->get( MainConfigNames::UseTagFilter ); // Normalize to arrays $tables = (array)$tables; $fields = (array)$fields; $conds = (array)$conds; $options = (array)$options; $fields['ts_tags'] = $this->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. // Figure out which ID field to use if ( in_array( 'recentchanges', $tables ) ) { $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rc_id=rc_id'; } elseif ( in_array( 'logging', $tables ) ) { $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_log_id=log_id'; } elseif ( in_array( 'revision', $tables ) ) { $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rev_id=rev_id'; } elseif ( in_array( 'archive', $tables ) ) { $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rev_id=ar_rev_id'; } else { throw new InvalidArgumentException( 'Unable to determine appropriate JOIN condition for tagging.' ); } if ( !$useTagFilter ) { return; } if ( !is_array( $filter_tag ) ) { // some callers provide false or null $filter_tag = (string)$filter_tag; } if ( $filter_tag !== [] && $filter_tag !== '' ) { // Somebody wants to filter on a tag. // Add an INNER JOIN on change_tag $filterTagIds = []; foreach ( (array)$filter_tag as $filterTagName ) { try { $filterTagIds[] = $this->changeTagDefStore->getId( $filterTagName ); } catch ( NameTableAccessException $exception ) { } } if ( $exclude ) { if ( $filterTagIds !== [] ) { $tables[self::DISPLAY_TABLE_ALIAS] = self::CHANGE_TAG; $join_conds[self::DISPLAY_TABLE_ALIAS] = [ 'LEFT JOIN', [ $join_cond, self::DISPLAY_TABLE_ALIAS . '.ct_tag_id' => $filterTagIds ] ]; $conds[self::DISPLAY_TABLE_ALIAS . '.ct_tag_id'] = null; } } else { $tables[self::DISPLAY_TABLE_ALIAS] = self::CHANGE_TAG; $join_conds[self::DISPLAY_TABLE_ALIAS] = [ 'JOIN', $join_cond ]; if ( $filterTagIds !== [] ) { $conds[self::DISPLAY_TABLE_ALIAS . '.ct_tag_id'] = $filterTagIds; } else { // all tags were invalid, return nothing $conds[] = '0=1'; } if ( is_array( $filter_tag ) && count( $filter_tag ) > 1 && !in_array( 'DISTINCT', $options ) ) { $options[] = 'DISTINCT'; } } } } /** * Applies all tags-related changes to a query builder object. * * Handles selecting tags, and filtering. * * WARNING: If $filter_tag contains more than one tag and $exclude is false, this function * will add DISTINCT, which may cause performance problems for your query unless you put * the ID field of your table at the end of the ORDER BY, and set a GROUP BY equal to the * ORDER BY. For example, if you had ORDER BY foo_timestamp DESC, you will now need * GROUP BY foo_timestamp, foo_id ORDER BY foo_timestamp DESC, foo_id DESC. * * @param SelectQueryBuilder $queryBuilder Query builder to add the join * @param string $table Table name. Must be either of 'recentchanges', 'logging', 'revision', or 'archive' * @param string|array|false|null $filter_tag Tag(s) to select on (OR) * @param bool $exclude If true, exclude tag(s) from $filter_tag (NOR) * */ public function modifyDisplayQueryBuilder( SelectQueryBuilder $queryBuilder, $table, $filter_tag = '', bool $exclude = false ) { $useTagFilter = $this->options->get( MainConfigNames::UseTagFilter ); $queryBuilder->field( $this->makeTagSummarySubquery( [ $table ] ), 'ts_tags' ); // 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. // Figure out which ID field to use if ( $table === 'recentchanges' ) { $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rc_id=rc_id'; } elseif ( $table === 'logging' ) { $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_log_id=log_id'; } elseif ( $table === 'revision' ) { $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rev_id=rev_id'; } elseif ( $table === 'archive' ) { $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rev_id=ar_rev_id'; } else { throw new InvalidArgumentException( 'Unable to determine appropriate JOIN condition for tagging.' ); } if ( !$useTagFilter ) { return; } if ( !is_array( $filter_tag ) ) { // some callers provide false or null $filter_tag = (string)$filter_tag; } if ( $filter_tag !== [] && $filter_tag !== '' ) { // Somebody wants to filter on a tag. // Add an INNER JOIN on change_tag $filterTagIds = []; foreach ( (array)$filter_tag as $filterTagName ) { try { $filterTagIds[] = $this->changeTagDefStore->getId( $filterTagName ); } catch ( NameTableAccessException $exception ) { } } if ( $exclude ) { if ( $filterTagIds !== [] ) { $queryBuilder->leftJoin( self::CHANGE_TAG, self::DISPLAY_TABLE_ALIAS, [ $join_cond, self::DISPLAY_TABLE_ALIAS . '.ct_tag_id' => $filterTagIds ] ); $queryBuilder->where( [ self::DISPLAY_TABLE_ALIAS . '.ct_tag_id' => null ] ); } } else { $queryBuilder->join( self::CHANGE_TAG, self::DISPLAY_TABLE_ALIAS, $join_cond ); if ( $filterTagIds !== [] ) { $queryBuilder->where( [ self::DISPLAY_TABLE_ALIAS . '.ct_tag_id' => $filterTagIds ] ); } else { // all tags were invalid, return nothing $queryBuilder->where( '0=1' ); } if ( is_array( $filter_tag ) && count( $filter_tag ) > 1 ) { $queryBuilder->distinct(); } } } } }
/var/www/html/1c732/../mediawiki/includes/../includes/changetags/ChangeTagsStore.php