24 use BadMethodCallException;
25 use InvalidArgumentException;
37 use Psr\Log\LoggerInterface;
55 private const CHANGE_TAG =
'change_tag';
60 private const CHANGE_TAG_DEF =
'change_tag_def';
75 private const DEFINED_SOFTWARE_TAGS = [
76 'mw-contentmodelchange',
78 'mw-removed-redirect',
79 'mw-changed-redirect-target',
86 'mw-server-side-upload',
90 private LoggerInterface $logger;
103 LoggerInterface $logger,
108 $this->dbProvider = $dbProvider;
109 $this->logger = $logger;
110 $this->options = $options;
111 $this->changeTagDefStore = $changeTagDefStore;
112 $this->wanCache = $wanCache;
113 $this->hookContainer = $hookContainer;
114 $this->userFactory = $userFactory;
115 $this->hookRunner =
new HookRunner( $hookContainer );
127 if ( !is_array( $coreTags ) ) {
128 $this->logger->warning(
'wgSoftwareTags should be associative array of enabled tags.
129 Please refer to documentation for the list of tags you can enable' );
133 $availableSoftwareTags = !$all ?
134 array_keys( array_filter( $coreTags ) ) :
135 array_keys( $coreTags );
137 $softwareTags = array_intersect(
138 $availableSoftwareTags,
139 self::DEFINED_SOFTWARE_TAGS
142 return $softwareTags;
159 if ( !$rc_id && !$rev_id && !$log_id ) {
160 throw new BadMethodCallException(
161 'At least one of: RCID, revision ID, and log ID MUST be ' .
162 'specified when loading tags from a change!' );
165 $conds = array_filter(
167 'ct_rc_id' => $rc_id,
168 'ct_rev_id' => $rev_id,
169 'ct_log_id' => $log_id,
172 $result = $db->newSelectQueryBuilder()
173 ->select( [
'ct_tag_id',
'ct_params' ] )
174 ->from( self::CHANGE_TAG )
176 ->caller( __METHOD__ )
180 foreach ( $result as $row ) {
181 $tagName = $this->changeTagDefStore->getName( (
int)$row->ct_tag_id );
182 $tags[$tagName] = $row->ct_params;
197 $tables = (array)$tables;
200 if ( in_array(
'recentchanges', $tables ) ) {
201 $join_cond =
'ct_rc_id=rc_id';
202 } elseif ( in_array(
'logging', $tables ) ) {
203 $join_cond =
'ct_log_id=log_id';
204 } elseif ( in_array(
'revision', $tables ) ) {
205 $join_cond =
'ct_rev_id=rev_id';
206 } elseif ( in_array(
'archive', $tables ) ) {
207 $join_cond =
'ct_rev_id=ar_rev_id';
209 throw new InvalidArgumentException(
'Unable to determine appropriate JOIN condition for tagging.' );
212 $tagTables = [ self::CHANGE_TAG, self::CHANGE_TAG_DEF ];
213 $join_cond_ts_tags = [ self::CHANGE_TAG_DEF => [
'JOIN',
'ct_tag_id=ctd_id' ] ];
216 return $this->dbProvider->getReplicaDatabase()
217 ->buildGroupConcatField(
',', $tagTables, $field, $join_cond, $join_cond_ts_tags );
229 $dbw = $this->dbProvider->getPrimaryDatabase();
230 $dbw->newInsertQueryBuilder()
231 ->insertInto( self::CHANGE_TAG_DEF )
234 'ctd_user_defined' => 1,
237 ->onDuplicateKeyUpdate()
238 ->uniqueIndexFields( [
'ctd_name' ] )
239 ->set( [
'ctd_user_defined' => 1 ] )
240 ->caller( __METHOD__ )->execute();
243 $this->purgeTagCacheAll();
255 $dbw = $this->dbProvider->getPrimaryDatabase();
257 $dbw->newUpdateQueryBuilder()
258 ->update( self::CHANGE_TAG_DEF )
259 ->set( [
'ctd_user_defined' => 0 ] )
260 ->where( [
'ctd_name' => $tag ] )
261 ->caller( __METHOD__ )->execute();
263 $dbw->newDeleteQueryBuilder()
264 ->deleteFrom( self::CHANGE_TAG_DEF )
265 ->where( [
'ctd_name' => $tag,
'ctd_count' => 0 ] )
266 ->caller( __METHOD__ )->execute();
269 $this->purgeTagCacheAll();
287 UserIdentity $user, $tagCount =
null, array $logEntryTags = []
289 $dbw = $this->dbProvider->getPrimaryDatabase();
292 $logEntry->setPerformer( $user );
295 $logEntry->setTarget( Title::newFromText(
'Special:Tags' ) );
296 $logEntry->setComment( $reason );
298 $params = [
'4::tag' => $tag ];
299 if ( $tagCount !==
null ) {
300 $params[
'5:number:count'] = $tagCount;
302 $logEntry->setParameters( $params );
303 $logEntry->setRelations( [
'Tag' => $tag ] );
304 $logEntry->addTags( $logEntryTags );
306 $logId = $logEntry->insert( $dbw );
307 $logEntry->publish( $logId );
324 $dbw = $this->dbProvider->getPrimaryDatabase();
325 $dbw->startAtomic( __METHOD__ );
328 $tagId = $this->changeTagDefStore->getId( $tag );
331 $this->undefineTag( $tag );
334 $dbw->newDeleteQueryBuilder()
335 ->deleteFrom( self::CHANGE_TAG )
336 ->where( [
'ct_tag_id' => $tagId ] )
337 ->caller( __METHOD__ )->execute();
338 $dbw->newDeleteQueryBuilder()
339 ->deleteFrom( self::CHANGE_TAG_DEF )
340 ->where( [
'ctd_name' => $tag ] )
341 ->caller( __METHOD__ )->execute();
342 $dbw->endAtomic( __METHOD__ );
345 $status = Status::newGood();
346 $this->hookRunner->onChangeTagAfterDelete( $tag, $status );
348 if ( !$status->isOK() ) {
349 $this->logger->debug(
'ChangeTagAfterDelete error condition downgraded to warning' );
350 $status->setOK(
true );
354 $this->purgeTagCacheAll();
365 $this->wanCache->touchCheckKey( $this->wanCache->makeKey(
'active-tags' ) );
366 $this->wanCache->touchCheckKey( $this->wanCache->makeKey(
'valid-tags-db' ) );
367 $this->wanCache->touchCheckKey( $this->wanCache->makeKey(
'valid-tags-hook' ) );
368 $this->wanCache->touchCheckKey( $this->wanCache->makeKey(
'tags-usage-statistics' ) );
370 $this->changeTagDefStore->reloadMap();
381 $dbProvider = $this->dbProvider;
383 return $this->wanCache->getWithSetCallback(
384 $this->wanCache->makeKey(
'tags-usage-statistics' ),
385 WANObjectCache::TTL_MINUTE * 5,
386 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname, $dbProvider ) {
388 $res = $dbr->newSelectQueryBuilder()
389 ->select( [
'ctd_name',
'ctd_count' ] )
390 ->from( self::CHANGE_TAG_DEF )
391 ->orderBy(
'ctd_count', SelectQueryBuilder::SORT_DESC )
396 foreach ( $res as $row ) {
397 $out[$row->ctd_name] = $row->ctd_count;
403 'checkKeys' => [ $this->wanCache->makeKey(
'tags-usage-statistics' ) ],
404 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
405 'pcTTL' => WANObjectCache::TTL_PROC_LONG
420 $dbProvider = $this->dbProvider;
422 return $this->wanCache->getWithSetCallback(
423 $this->wanCache->makeKey(
'valid-tags-db' ),
424 WANObjectCache::TTL_MINUTE * 5,
425 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname, $dbProvider ) {
427 $setOpts += Database::getCacheSetOptions( $dbr );
428 $tags = $dbr->newSelectQueryBuilder()
429 ->select(
'ctd_name' )
430 ->from( self::CHANGE_TAG_DEF )
431 ->where( [
'ctd_user_defined' => 1 ] )
433 ->fetchFieldValues();
435 return array_unique( $tags );
438 'checkKeys' => [ $this->wanCache->makeKey(
'valid-tags-db' ) ],
439 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
440 'pcTTL' => WANObjectCache::TTL_PROC_LONG
456 $tags = $this->getSoftwareTags(
true );
457 if ( !$this->hookContainer->isRegistered(
'ListDefinedTags' ) ) {
460 $hookRunner = $this->hookRunner;
461 $dbProvider = $this->dbProvider;
462 return $this->wanCache->getWithSetCallback(
463 $this->wanCache->makeKey(
'valid-tags-hook' ),
464 WANObjectCache::TTL_MINUTE * 5,
465 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner, $dbProvider ) {
468 return array_unique( $tags );
471 'checkKeys' => [ $this->wanCache->makeKey(
'valid-tags-hook' ) ],
472 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
473 'pcTTL' => WANObjectCache::TTL_PROC_LONG
489 return array_keys( $this->getTagsWithData( $db, $rc_id, $rev_id, $log_id ) );
500 $tags1 = $this->listExplicitlyDefinedTags();
501 $tags2 = $this->listSoftwareDefinedTags();
502 return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
532 public function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id =
null,
533 &$rev_id =
null, &$log_id =
null, $params =
null,
RecentChange $rc =
null,
536 $tagsToAdd = array_filter(
538 static function ( $value ) {
539 return ( $value ??
'' ) !==
'';
542 $tagsToRemove = array_filter(
543 (array)$tagsToRemove,
544 static function ( $value ) {
545 return ( $value ??
'' ) !==
'';
549 if ( !$rc_id && !$rev_id && !$log_id ) {
550 throw new BadMethodCallException(
'At least one of: RCID, revision ID, and log ID MUST be ' .
551 'specified when adding or removing a tag from a change!' );
554 $dbw = $this->dbProvider->getPrimaryDatabase();
562 $rc_id = $dbw->newSelectQueryBuilder()
565 ->join(
'recentchanges',
null, [
566 'rc_timestamp = log_timestamp',
569 ->where( [
'log_id' => $log_id ] )
570 ->caller( __METHOD__ )
572 } elseif ( $rev_id ) {
573 $rc_id = $dbw->newSelectQueryBuilder()
576 ->join(
'recentchanges',
null, [
577 'rc_this_oldid = rev_id'
579 ->where( [
'rev_id' => $rev_id ] )
580 ->caller( __METHOD__ )
583 } elseif ( !$log_id && !$rev_id ) {
585 $log_id = $dbw->newSelectQueryBuilder()
586 ->select(
'rc_logid' )
587 ->from(
'recentchanges' )
588 ->where( [
'rc_id' => $rc_id ] )
589 ->caller( __METHOD__ )
591 $rev_id = $dbw->newSelectQueryBuilder()
592 ->select(
'rc_this_oldid' )
593 ->from(
'recentchanges' )
594 ->where( [
'rc_id' => $rc_id ] )
595 ->caller( __METHOD__ )
599 if ( $log_id && !$rev_id ) {
600 $rev_id = $dbw->newSelectQueryBuilder()
601 ->select(
'ls_value' )
602 ->from(
'log_search' )
603 ->where( [
'ls_field' =>
'associated_rev_id',
'ls_log_id' => $log_id ] )
604 ->caller( __METHOD__ )
606 } elseif ( !$log_id && $rev_id ) {
607 $log_id = $dbw->newSelectQueryBuilder()
608 ->select(
'ls_log_id' )
609 ->from(
'log_search' )
610 ->where( [
'ls_field' =>
'associated_rev_id',
'ls_value' => (
string)$rev_id ] )
611 ->caller( __METHOD__ )
615 $prevTags = $this->getTags( $dbw, $rc_id, $rev_id, $log_id );
618 $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
619 $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
622 $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
623 $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
627 if ( $prevTags == $newTags ) {
628 return [ [], [], $prevTags ];
632 if ( count( $tagsToAdd ) ) {
633 $changeTagMapping = [];
634 foreach ( $tagsToAdd as $tag ) {
635 $changeTagMapping[$tag] = $this->changeTagDefStore->acquireId( $tag );
639 $dbw->onTransactionPreCommitOrIdle(
static function () use ( $dbw, $tagsToAdd, $fname ) {
640 $dbw->newUpdateQueryBuilder()
641 ->update( self::CHANGE_TAG_DEF )
642 ->
set( [
'ctd_count = ctd_count + 1' ] )
643 ->where( [
'ctd_name' => $tagsToAdd ] )
644 ->caller( $fname )->execute();
648 foreach ( $tagsToAdd as $tag ) {
653 $tagsRows[] = array_filter(
655 'ct_rc_id' => $rc_id,
656 'ct_log_id' => $log_id,
657 'ct_rev_id' => $rev_id,
658 'ct_params' => $params,
659 'ct_tag_id' => $changeTagMapping[$tag] ??
null,
665 $dbw->newInsertQueryBuilder()
666 ->insertInto( self::CHANGE_TAG )
669 ->caller( __METHOD__ )->execute();
673 if ( count( $tagsToRemove ) ) {
675 foreach ( $tagsToRemove as $tag ) {
676 $conds = array_filter(
678 'ct_rc_id' => $rc_id,
679 'ct_log_id' => $log_id,
680 'ct_rev_id' => $rev_id,
681 'ct_tag_id' => $this->changeTagDefStore->getId( $tag ),
684 $dbw->newDeleteQueryBuilder()
685 ->deleteFrom( self::CHANGE_TAG )
687 ->caller( __METHOD__ )->execute();
688 if ( $dbw->affectedRows() ) {
690 $dbw->onTransactionPreCommitOrIdle(
static function () use ( $dbw, $tag, $fname ) {
691 $dbw->newUpdateQueryBuilder()
692 ->update( self::CHANGE_TAG_DEF )
693 ->
set( [
'ctd_count = ctd_count - 1' ] )
694 ->where( [
'ctd_name' => $tag ] )
695 ->caller( $fname )->execute();
697 $dbw->newDeleteQueryBuilder()
698 ->deleteFrom( self::CHANGE_TAG_DEF )
699 ->where( [
'ctd_name' => $tag,
'ctd_count' => 0,
'ctd_user_defined' => 0 ] )
700 ->caller( $fname )->execute();
706 $userObj = $user ? $this->userFactory->newFromUserIdentity( $user ) :
null;
707 $this->hookRunner->onChangeTagsAfterUpdateTags(
708 $tagsToAdd, $tagsToRemove, $prevTags, $rc_id, $rev_id, $log_id, $params, $rc, $userObj );
710 return [ $tagsToAdd, $tagsToRemove, $prevTags ];
726 public function addTags( $tags, $rc_id =
null, $rev_id =
null,
729 $result = $this->updateTags( $tags,
null, $rc_id, $rev_id, $log_id, $params, $rc );
730 return (
bool)$result[0];
741 $tags = $this->getSoftwareTags();
742 if ( !$this->hookContainer->isRegistered(
'ChangeTagsListActive' ) ) {
745 $hookRunner = $this->hookRunner;
746 $dbProvider = $this->dbProvider;
748 return $this->wanCache->getWithSetCallback(
749 $this->wanCache->makeKey(
'active-tags' ),
750 WANObjectCache::TTL_MINUTE * 5,
751 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner, $dbProvider ) {
759 'checkKeys' => [ $this->wanCache->makeKey(
'active-tags' ) ],
760 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
761 'pcTTL' => WANObjectCache::TTL_PROC_LONG
787 &$join_conds, &$options, $filter_tag =
'',
bool $exclude =
false
789 $useTagFilter = $this->options->get( MainConfigNames::UseTagFilter );
792 $tables = (array)$tables;
793 $fields = (array)$fields;
794 $conds = (array)$conds;
795 $options = (array)$options;
797 $fields[
'ts_tags'] = $this->makeTagSummarySubquery( $tables );
803 if ( in_array(
'recentchanges', $tables ) ) {
804 $join_cond = self::DISPLAY_TABLE_ALIAS .
'.ct_rc_id=rc_id';
805 } elseif ( in_array(
'logging', $tables ) ) {
806 $join_cond = self::DISPLAY_TABLE_ALIAS .
'.ct_log_id=log_id';
807 } elseif ( in_array(
'revision', $tables ) ) {
808 $join_cond = self::DISPLAY_TABLE_ALIAS .
'.ct_rev_id=rev_id';
809 } elseif ( in_array(
'archive', $tables ) ) {
810 $join_cond = self::DISPLAY_TABLE_ALIAS .
'.ct_rev_id=ar_rev_id';
812 throw new InvalidArgumentException(
'Unable to determine appropriate JOIN condition for tagging.' );
815 if ( !$useTagFilter ) {
819 if ( !is_array( $filter_tag ) ) {
821 $filter_tag = (string)$filter_tag;
824 if ( $filter_tag !== [] && $filter_tag !==
'' ) {
828 foreach ( (array)$filter_tag as $filterTagName ) {
830 $filterTagIds[] = $this->changeTagDefStore->getId( $filterTagName );
836 if ( $filterTagIds !== [] ) {
837 $tables[self::DISPLAY_TABLE_ALIAS] = self::CHANGE_TAG;
838 $join_conds[self::DISPLAY_TABLE_ALIAS] = [
840 [ $join_cond, self::DISPLAY_TABLE_ALIAS .
'.ct_tag_id' => $filterTagIds ]
842 $conds[self::DISPLAY_TABLE_ALIAS .
'.ct_tag_id'] =
null;
845 $tables[self::DISPLAY_TABLE_ALIAS] = self::CHANGE_TAG;
846 $join_conds[self::DISPLAY_TABLE_ALIAS] = [
'JOIN', $join_cond ];
847 if ( $filterTagIds !== [] ) {
848 $conds[self::DISPLAY_TABLE_ALIAS .
'.ct_tag_id'] = $filterTagIds;
855 is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
856 !in_array(
'DISTINCT', $options )
858 $options[] =
'DISTINCT';
if(!defined('MW_SETUP_CALLBACK'))
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()
const SoftwareTags
Name constant for the SoftwareTags setting, for use with Config::get()
Utility class for creating new RC entries.
Multi-datacenter aware caching interface.