MediaWiki master
ChangeTagsStore.php
Go to the documentation of this file.
1<?php
22
23use InvalidArgumentException;
35use Psr\Log\LoggerInterface;
36use RecentChange;
43
51
55 private const CHANGE_TAG = 'change_tag';
56
60 private const CHANGE_TAG_DEF = 'change_tag_def';
61
62 public const DISPLAY_TABLE_ALIAS = 'changetagdisplay';
63
67 public const CONSTRUCTOR_OPTIONS = [
70 ];
71
75 private const DEFINED_SOFTWARE_TAGS = [
76 'mw-contentmodelchange',
77 'mw-new-redirect',
78 'mw-removed-redirect',
79 'mw-changed-redirect-target',
80 'mw-blank',
81 'mw-replace',
82 'mw-rollback',
83 'mw-undo',
84 'mw-manual-revert',
85 'mw-reverted',
86 'mw-server-side-upload',
87 ];
88
89 private IConnectionProvider $dbProvider;
90 private LoggerInterface $logger;
91 private ServiceOptions $options;
92 private NameTableStore $changeTagDefStore;
93 private WANObjectCache $wanCache;
94 private HookRunner $hookRunner;
95 private UserFactory $userFactory;
96 private HookContainer $hookContainer;
97
98 public function __construct(
99 IConnectionProvider $dbProvider,
100 NameTableStore $changeTagDefStore,
101 WANObjectCache $wanCache,
102 HookContainer $hookContainer,
103 LoggerInterface $logger,
104 UserFactory $userFactory,
105 ServiceOptions $options
106 ) {
107 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
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 );
116 }
117
125 public function getSoftwareTags( $all = false ): array {
126 $coreTags = $this->options->get( MainConfigNames::SoftwareTags );
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' );
130 return [];
131 }
132
133 $availableSoftwareTags = !$all ?
134 array_keys( array_filter( $coreTags ) ) :
135 array_keys( $coreTags );
136
137 return array_intersect(
138 $availableSoftwareTags,
139 self::DEFINED_SOFTWARE_TAGS
140 );
141 }
142
154 public function getTagsWithData(
155 IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null
156 ): array {
157 if ( !$rc_id && !$rev_id && !$log_id ) {
158 throw new InvalidArgumentException(
159 'At least one of: RCID, revision ID, and log ID MUST be ' .
160 'specified when loading tags from a change!' );
161 }
162
163 $conds = array_filter(
164 [
165 'ct_rc_id' => $rc_id,
166 'ct_rev_id' => $rev_id,
167 'ct_log_id' => $log_id,
168 ]
169 );
170 $result = $db->newSelectQueryBuilder()
171 ->select( [ 'ct_tag_id', 'ct_params' ] )
172 ->from( self::CHANGE_TAG )
173 ->where( $conds )
174 ->caller( __METHOD__ )
175 ->fetchResultSet();
176
177 $tags = [];
178 foreach ( $result as $row ) {
179 $tagName = $this->changeTagDefStore->getName( (int)$row->ct_tag_id );
180 $tags[$tagName] = $row->ct_params;
181 }
182
183 return $tags;
184 }
185
193 public function makeTagSummarySubquery( $tables ) {
194 // Normalize to arrays
195 $tables = (array)$tables;
196
197 // Figure out which ID field to use
198 if ( in_array( 'recentchanges', $tables ) ) {
199 $join_cond = 'ct_rc_id=rc_id';
200 } elseif ( in_array( 'logging', $tables ) ) {
201 $join_cond = 'ct_log_id=log_id';
202 } elseif ( in_array( 'revision', $tables ) ) {
203 $join_cond = 'ct_rev_id=rev_id';
204 } elseif ( in_array( 'archive', $tables ) ) {
205 $join_cond = 'ct_rev_id=ar_rev_id';
206 } else {
207 throw new InvalidArgumentException( 'Unable to determine appropriate JOIN condition for tagging.' );
208 }
209
210 return $this->dbProvider->getReplicaDatabase()
211 ->newSelectQueryBuilder()
212 ->table( self::CHANGE_TAG )
213 ->join( self::CHANGE_TAG_DEF, null, 'ct_tag_id=ctd_id' )
214 ->field( 'ctd_name' )
215 ->where( $join_cond )
216 ->buildGroupConcatField( ',' );
217 }
218
227 public function defineTag( $tag ) {
228 $dbw = $this->dbProvider->getPrimaryDatabase();
229 $dbw->newInsertQueryBuilder()
230 ->insertInto( self::CHANGE_TAG_DEF )
231 ->row( [
232 'ctd_name' => $tag,
233 'ctd_user_defined' => 1,
234 'ctd_count' => 0
235 ] )
236 ->onDuplicateKeyUpdate()
237 ->uniqueIndexFields( [ 'ctd_name' ] )
238 ->set( [ 'ctd_user_defined' => 1 ] )
239 ->caller( __METHOD__ )->execute();
240
241 // clear the memcache of defined tags
242 $this->purgeTagCacheAll();
243 }
244
253 public function undefineTag( $tag ) {
254 $dbw = $this->dbProvider->getPrimaryDatabase();
255
256 $dbw->newUpdateQueryBuilder()
257 ->update( self::CHANGE_TAG_DEF )
258 ->set( [ 'ctd_user_defined' => 0 ] )
259 ->where( [ 'ctd_name' => $tag ] )
260 ->caller( __METHOD__ )->execute();
261
262 $dbw->newDeleteQueryBuilder()
263 ->deleteFrom( self::CHANGE_TAG_DEF )
264 ->where( [ 'ctd_name' => $tag, 'ctd_count' => 0 ] )
265 ->caller( __METHOD__ )->execute();
266
267 // clear the memcache of defined tags
268 $this->purgeTagCacheAll();
269 }
270
285 public function logTagManagementAction( string $action, string $tag, string $reason,
286 UserIdentity $user, $tagCount = null, array $logEntryTags = []
287 ) {
288 $dbw = $this->dbProvider->getPrimaryDatabase();
289
290 $logEntry = new ManualLogEntry( 'managetags', $action );
291 $logEntry->setPerformer( $user );
292 // target page is not relevant, but it has to be set, so we just put in
293 // the title of Special:Tags
294 $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
295 $logEntry->setComment( $reason );
296
297 $params = [ '4::tag' => $tag ];
298 if ( $tagCount !== null ) {
299 $params['5:number:count'] = $tagCount;
300 }
301 $logEntry->setParameters( $params );
302 $logEntry->setRelations( [ 'Tag' => $tag ] );
303 $logEntry->addTags( $logEntryTags );
304
305 $logId = $logEntry->insert( $dbw );
306 $logEntry->publish( $logId );
307 return $logId;
308 }
309
322 public function deleteTagEverywhere( $tag ) {
323 $dbw = $this->dbProvider->getPrimaryDatabase();
324 $dbw->startAtomic( __METHOD__ );
325
326 // fetch tag id, this must be done before calling undefineTag(), see T225564
327 $tagId = $this->changeTagDefStore->getId( $tag );
328
329 // set ctd_user_defined = 0
330 $this->undefineTag( $tag );
331
332 // delete from change_tag
333 $dbw->newDeleteQueryBuilder()
334 ->deleteFrom( self::CHANGE_TAG )
335 ->where( [ 'ct_tag_id' => $tagId ] )
336 ->caller( __METHOD__ )->execute();
337 $dbw->newDeleteQueryBuilder()
338 ->deleteFrom( self::CHANGE_TAG_DEF )
339 ->where( [ 'ctd_name' => $tag ] )
340 ->caller( __METHOD__ )->execute();
341 $dbw->endAtomic( __METHOD__ );
342
343 // give extensions a chance
344 $status = Status::newGood();
345 $this->hookRunner->onChangeTagAfterDelete( $tag, $status );
346 // let's not allow error results, as the actual tag deletion succeeded
347 if ( !$status->isOK() ) {
348 $this->logger->debug( 'ChangeTagAfterDelete error condition downgraded to warning' );
349 $status->setOK( true );
350 }
351
352 // clear the memcache of defined tags
353 $this->purgeTagCacheAll();
354
355 return $status;
356 }
357
363 public function purgeTagCacheAll() {
364 $this->wanCache->touchCheckKey( $this->wanCache->makeKey( 'active-tags' ) );
365 $this->wanCache->touchCheckKey( $this->wanCache->makeKey( 'valid-tags-db' ) );
366 $this->wanCache->touchCheckKey( $this->wanCache->makeKey( 'valid-tags-hook' ) );
367 $this->wanCache->touchCheckKey( $this->wanCache->makeKey( 'tags-usage-statistics' ) );
368
369 $this->changeTagDefStore->reloadMap();
370 }
371
378 public function tagUsageStatistics(): array {
379 $fname = __METHOD__;
380 $dbProvider = $this->dbProvider;
381
382 return $this->wanCache->getWithSetCallback(
383 $this->wanCache->makeKey( 'tags-usage-statistics' ),
384 WANObjectCache::TTL_MINUTE * 5,
385 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname, $dbProvider ) {
386 $dbr = $dbProvider->getReplicaDatabase();
387 $res = $dbr->newSelectQueryBuilder()
388 ->select( [ 'ctd_name', 'ctd_count' ] )
389 ->from( self::CHANGE_TAG_DEF )
390 ->orderBy( 'ctd_count', SelectQueryBuilder::SORT_DESC )
391 ->caller( $fname )
392 ->fetchResultSet();
393
394 $out = [];
395 foreach ( $res as $row ) {
396 $out[$row->ctd_name] = $row->ctd_count;
397 }
398
399 return $out;
400 },
401 [
402 'checkKeys' => [ $this->wanCache->makeKey( 'tags-usage-statistics' ) ],
403 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
404 'pcTTL' => WANObjectCache::TTL_PROC_LONG
405 ]
406 );
407 }
408
417 public function listExplicitlyDefinedTags() {
418 $fname = __METHOD__;
419 $dbProvider = $this->dbProvider;
420
421 return $this->wanCache->getWithSetCallback(
422 $this->wanCache->makeKey( 'valid-tags-db' ),
423 WANObjectCache::TTL_MINUTE * 5,
424 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname, $dbProvider ) {
425 $dbr = $dbProvider->getReplicaDatabase();
426 $setOpts += Database::getCacheSetOptions( $dbr );
427 $tags = $dbr->newSelectQueryBuilder()
428 ->select( 'ctd_name' )
429 ->from( self::CHANGE_TAG_DEF )
430 ->where( [ 'ctd_user_defined' => 1 ] )
431 ->caller( $fname )
432 ->fetchFieldValues();
433
434 return array_unique( $tags );
435 },
436 [
437 'checkKeys' => [ $this->wanCache->makeKey( 'valid-tags-db' ) ],
438 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
439 'pcTTL' => WANObjectCache::TTL_PROC_LONG
440 ]
441 );
442 }
443
453 public function listSoftwareDefinedTags() {
454 // core defined tags
455 $tags = $this->getSoftwareTags( true );
456 if ( !$this->hookContainer->isRegistered( 'ListDefinedTags' ) ) {
457 return $tags;
458 }
459 $hookRunner = $this->hookRunner;
460 $dbProvider = $this->dbProvider;
461 return $this->wanCache->getWithSetCallback(
462 $this->wanCache->makeKey( 'valid-tags-hook' ),
463 WANObjectCache::TTL_MINUTE * 5,
464 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner, $dbProvider ) {
465 $setOpts += Database::getCacheSetOptions( $dbProvider->getReplicaDatabase() );
466 $hookRunner->onListDefinedTags( $tags );
467 return array_unique( $tags );
468 },
469 [
470 'checkKeys' => [ $this->wanCache->makeKey( 'valid-tags-hook' ) ],
471 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
472 'pcTTL' => WANObjectCache::TTL_PROC_LONG
473 ]
474 );
475 }
476
487 public function getTags( IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
488 return array_keys( $this->getTagsWithData( $db, $rc_id, $rev_id, $log_id ) );
489 }
490
498 public function listDefinedTags() {
499 $tags1 = $this->listExplicitlyDefinedTags();
500 $tags2 = $this->listSoftwareDefinedTags();
501 return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
502 }
503
531 public function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
532 &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
533 UserIdentity $user = null
534 ) {
535 $tagsToAdd = array_filter(
536 (array)$tagsToAdd, // Make sure we're submitting all tags...
537 static function ( $value ) {
538 return ( $value ?? '' ) !== '';
539 }
540 );
541 $tagsToRemove = array_filter(
542 (array)$tagsToRemove,
543 static function ( $value ) {
544 return ( $value ?? '' ) !== '';
545 }
546 );
547
548 if ( !$rc_id && !$rev_id && !$log_id ) {
549 throw new InvalidArgumentException( 'At least one of: RCID, revision ID, and log ID MUST be ' .
550 'specified when adding or removing a tag from a change!' );
551 }
552
553 $dbw = $this->dbProvider->getPrimaryDatabase();
554
555 // Might as well look for rcids and so on.
556 if ( !$rc_id ) {
557 // Info might be out of date, somewhat fractionally, on replica DB.
558 // LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
559 // so use that relation to avoid full table scans.
560 if ( $log_id ) {
561 $rc_id = $dbw->newSelectQueryBuilder()
562 ->select( 'rc_id' )
563 ->from( 'logging' )
564 ->join( 'recentchanges', null, [
565 'rc_timestamp = log_timestamp',
566 'rc_logid = log_id'
567 ] )
568 ->where( [ 'log_id' => $log_id ] )
569 ->caller( __METHOD__ )
570 ->fetchField();
571 } elseif ( $rev_id ) {
572 $rc_id = $dbw->newSelectQueryBuilder()
573 ->select( 'rc_id' )
574 ->from( 'revision' )
575 ->join( 'recentchanges', null, [
576 'rc_this_oldid = rev_id'
577 ] )
578 ->where( [ 'rev_id' => $rev_id ] )
579 ->caller( __METHOD__ )
580 ->fetchField();
581 }
582 } elseif ( !$log_id && !$rev_id ) {
583 // Info might be out of date, somewhat fractionally, on replica DB.
584 $log_id = $dbw->newSelectQueryBuilder()
585 ->select( 'rc_logid' )
586 ->from( 'recentchanges' )
587 ->where( [ 'rc_id' => $rc_id ] )
588 ->caller( __METHOD__ )
589 ->fetchField();
590 $rev_id = $dbw->newSelectQueryBuilder()
591 ->select( 'rc_this_oldid' )
592 ->from( 'recentchanges' )
593 ->where( [ 'rc_id' => $rc_id ] )
594 ->caller( __METHOD__ )
595 ->fetchField();
596 }
597
598 if ( $log_id && !$rev_id ) {
599 $rev_id = $dbw->newSelectQueryBuilder()
600 ->select( 'ls_value' )
601 ->from( 'log_search' )
602 ->where( [ 'ls_field' => 'associated_rev_id', 'ls_log_id' => $log_id ] )
603 ->caller( __METHOD__ )
604 ->fetchField();
605 } elseif ( !$log_id && $rev_id ) {
606 $log_id = $dbw->newSelectQueryBuilder()
607 ->select( 'ls_log_id' )
608 ->from( 'log_search' )
609 ->where( [ 'ls_field' => 'associated_rev_id', 'ls_value' => (string)$rev_id ] )
610 ->caller( __METHOD__ )
611 ->fetchField();
612 }
613
614 $prevTags = $this->getTags( $dbw, $rc_id, $rev_id, $log_id );
615
616 // add tags
617 $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
618 $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
619
620 // remove tags
621 $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
622 $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
623
624 sort( $prevTags );
625 sort( $newTags );
626 if ( $prevTags == $newTags ) {
627 return [ [], [], $prevTags ];
628 }
629
630 // insert a row into change_tag for each new tag
631 if ( count( $tagsToAdd ) ) {
632 $changeTagMapping = [];
633 foreach ( $tagsToAdd as $tag ) {
634 $changeTagMapping[$tag] = $this->changeTagDefStore->acquireId( $tag );
635 }
636 $fname = __METHOD__;
637 // T207881: update the counts at the end of the transaction
638 $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tagsToAdd, $fname ) {
639 $dbw->newUpdateQueryBuilder()
640 ->update( self::CHANGE_TAG_DEF )
641 ->set( [ 'ctd_count' => new RawSQLValue( 'ctd_count + 1' ) ] )
642 ->where( [ 'ctd_name' => $tagsToAdd ] )
643 ->caller( $fname )->execute();
644 }, $fname );
645
646 $tagsRows = [];
647 foreach ( $tagsToAdd as $tag ) {
648 // Filter so we don't insert NULLs as zero accidentally.
649 // Keep in mind that $rc_id === null means "I don't care/know about the
650 // rc_id, just delete $tag on this revision/log entry". It doesn't
651 // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
652 $tagsRows[] = array_filter(
653 [
654 'ct_rc_id' => $rc_id,
655 'ct_log_id' => $log_id,
656 'ct_rev_id' => $rev_id,
657 'ct_params' => $params,
658 'ct_tag_id' => $changeTagMapping[$tag] ?? null,
659 ]
660 );
661
662 }
663
664 $dbw->newInsertQueryBuilder()
665 ->insertInto( self::CHANGE_TAG )
666 ->ignore()
667 ->rows( $tagsRows )
668 ->caller( __METHOD__ )->execute();
669 }
670
671 // delete from change_tag
672 if ( count( $tagsToRemove ) ) {
673 $fname = __METHOD__;
674 foreach ( $tagsToRemove as $tag ) {
675 $conds = array_filter(
676 [
677 'ct_rc_id' => $rc_id,
678 'ct_log_id' => $log_id,
679 'ct_rev_id' => $rev_id,
680 'ct_tag_id' => $this->changeTagDefStore->getId( $tag ),
681 ]
682 );
683 $dbw->newDeleteQueryBuilder()
684 ->deleteFrom( self::CHANGE_TAG )
685 ->where( $conds )
686 ->caller( __METHOD__ )->execute();
687 if ( $dbw->affectedRows() ) {
688 // T207881: update the counts at the end of the transaction
689 $dbw->onTransactionPreCommitOrIdle( static function () use ( $dbw, $tag, $fname ) {
690 $dbw->newUpdateQueryBuilder()
691 ->update( self::CHANGE_TAG_DEF )
692 ->set( [ 'ctd_count' => new RawSQLValue( 'ctd_count - 1' ) ] )
693 ->where( [ 'ctd_name' => $tag ] )
694 ->caller( $fname )->execute();
695
696 $dbw->newDeleteQueryBuilder()
697 ->deleteFrom( self::CHANGE_TAG_DEF )
698 ->where( [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ] )
699 ->caller( $fname )->execute();
700 }, $fname );
701 }
702 }
703 }
704
705 $userObj = $user ? $this->userFactory->newFromUserIdentity( $user ) : null;
706 $this->hookRunner->onChangeTagsAfterUpdateTags(
707 $tagsToAdd, $tagsToRemove, $prevTags, $rc_id, $rev_id, $log_id, $params, $rc, $userObj );
708
709 return [ $tagsToAdd, $tagsToRemove, $prevTags ];
710 }
711
725 public function addTags( $tags, $rc_id = null, $rev_id = null,
726 $log_id = null, $params = null, RecentChange $rc = null
727 ) {
728 $result = $this->updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
729 return (bool)$result[0];
730 }
731
738 public function listSoftwareActivatedTags() {
739 // core active tags
740 $tags = $this->getSoftwareTags();
741 if ( !$this->hookContainer->isRegistered( 'ChangeTagsListActive' ) ) {
742 return $tags;
743 }
744 $hookRunner = $this->hookRunner;
745 $dbProvider = $this->dbProvider;
746
747 return $this->wanCache->getWithSetCallback(
748 $this->wanCache->makeKey( 'active-tags' ),
749 WANObjectCache::TTL_MINUTE * 5,
750 static function ( $oldValue, &$ttl, array &$setOpts ) use ( $tags, $hookRunner, $dbProvider ) {
751 $setOpts += Database::getCacheSetOptions( $dbProvider->getReplicaDatabase() );
752
753 // Ask extensions which tags they consider active
754 $hookRunner->onChangeTagsListActive( $tags );
755 return $tags;
756 },
757 [
758 'checkKeys' => [ $this->wanCache->makeKey( 'active-tags' ) ],
759 'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
760 'pcTTL' => WANObjectCache::TTL_PROC_LONG
761 ]
762 );
763 }
764
787 public function modifyDisplayQuery( &$tables, &$fields, &$conds,
788 &$join_conds, &$options, $filter_tag = '', bool $exclude = false
789 ) {
790 $useTagFilter = $this->options->get( MainConfigNames::UseTagFilter );
791
792 // Normalize to arrays
793 $tables = (array)$tables;
794 $fields = (array)$fields;
795 $conds = (array)$conds;
796 $options = (array)$options;
797
798 $fields['ts_tags'] = $this->makeTagSummarySubquery( $tables );
799 // We use an alias and qualify the conditions in case there are
800 // multiple joins to this table.
801 // In particular for compatibility with the RC filters that extension Translate does.
802
803 // Figure out which ID field to use
804 if ( in_array( 'recentchanges', $tables ) ) {
805 $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rc_id=rc_id';
806 } elseif ( in_array( 'logging', $tables ) ) {
807 $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_log_id=log_id';
808 } elseif ( in_array( 'revision', $tables ) ) {
809 $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rev_id=rev_id';
810 } elseif ( in_array( 'archive', $tables ) ) {
811 $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rev_id=ar_rev_id';
812 } else {
813 throw new InvalidArgumentException( 'Unable to determine appropriate JOIN condition for tagging.' );
814 }
815
816 if ( !$useTagFilter ) {
817 return;
818 }
819
820 if ( !is_array( $filter_tag ) ) {
821 // some callers provide false or null
822 $filter_tag = (string)$filter_tag;
823 }
824
825 if ( $filter_tag !== [] && $filter_tag !== '' ) {
826 // Somebody wants to filter on a tag.
827 // Add an INNER JOIN on change_tag
828 $filterTagIds = [];
829 foreach ( (array)$filter_tag as $filterTagName ) {
830 try {
831 $filterTagIds[] = $this->changeTagDefStore->getId( $filterTagName );
832 } catch ( NameTableAccessException $exception ) {
833 }
834 }
835
836 if ( $exclude ) {
837 if ( $filterTagIds !== [] ) {
838 $tables[self::DISPLAY_TABLE_ALIAS] = self::CHANGE_TAG;
839 $join_conds[self::DISPLAY_TABLE_ALIAS] = [
840 'LEFT JOIN',
841 [ $join_cond, self::DISPLAY_TABLE_ALIAS . '.ct_tag_id' => $filterTagIds ]
842 ];
843 $conds[self::DISPLAY_TABLE_ALIAS . '.ct_tag_id'] = null;
844 }
845 } else {
846 $tables[self::DISPLAY_TABLE_ALIAS] = self::CHANGE_TAG;
847 $join_conds[self::DISPLAY_TABLE_ALIAS] = [ 'JOIN', $join_cond ];
848 if ( $filterTagIds !== [] ) {
849 $conds[self::DISPLAY_TABLE_ALIAS . '.ct_tag_id'] = $filterTagIds;
850 } else {
851 // all tags were invalid, return nothing
852 $conds[] = '0=1';
853 }
854
855 if (
856 is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
857 !in_array( 'DISTINCT', $options )
858 ) {
859 $options[] = 'DISTINCT';
860 }
861 }
862 }
863 }
864
883 SelectQueryBuilder $queryBuilder,
884 $table,
885 $filter_tag = '',
886 bool $exclude = false
887 ) {
888 $useTagFilter = $this->options->get( MainConfigNames::UseTagFilter );
889 $queryBuilder->field( $this->makeTagSummarySubquery( [ $table ] ), 'ts_tags' );
890
891 // We use an alias and qualify the conditions in case there are
892 // multiple joins to this table.
893 // In particular for compatibility with the RC filters that extension Translate does.
894 // Figure out which ID field to use
895 if ( $table === 'recentchanges' ) {
896 $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rc_id=rc_id';
897 } elseif ( $table === 'logging' ) {
898 $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_log_id=log_id';
899 } elseif ( $table === 'revision' ) {
900 $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rev_id=rev_id';
901 } elseif ( $table === 'archive' ) {
902 $join_cond = self::DISPLAY_TABLE_ALIAS . '.ct_rev_id=ar_rev_id';
903 } else {
904 throw new InvalidArgumentException( 'Unable to determine appropriate JOIN condition for tagging.' );
905 }
906
907 if ( !$useTagFilter ) {
908 return;
909 }
910
911 if ( !is_array( $filter_tag ) ) {
912 // some callers provide false or null
913 $filter_tag = (string)$filter_tag;
914 }
915
916 if ( $filter_tag !== [] && $filter_tag !== '' ) {
917 // Somebody wants to filter on a tag.
918 // Add an INNER JOIN on change_tag
919 $filterTagIds = [];
920 foreach ( (array)$filter_tag as $filterTagName ) {
921 try {
922 $filterTagIds[] = $this->changeTagDefStore->getId( $filterTagName );
923 } catch ( NameTableAccessException $exception ) {
924 }
925 }
926
927 if ( $exclude ) {
928 if ( $filterTagIds !== [] ) {
929 $queryBuilder->leftJoin(
930 self::CHANGE_TAG,
931 self::DISPLAY_TABLE_ALIAS,
932 [ $join_cond, self::DISPLAY_TABLE_ALIAS . '.ct_tag_id' => $filterTagIds ]
933 );
934 $queryBuilder->where( [ self::DISPLAY_TABLE_ALIAS . '.ct_tag_id' => null ] );
935 }
936 } else {
937 $queryBuilder->join(
938 self::CHANGE_TAG,
939 self::DISPLAY_TABLE_ALIAS,
940 $join_cond
941 );
942 if ( $filterTagIds !== [] ) {
943 $queryBuilder->where( [ self::DISPLAY_TABLE_ALIAS . '.ct_tag_id' => $filterTagIds ] );
944 } else {
945 // all tags were invalid, return nothing
946 $queryBuilder->where( '0=1' );
947 }
948
949 if (
950 is_array( $filter_tag ) && count( $filter_tag ) > 1
951 ) {
952 $queryBuilder->distinct();
953 }
954 }
955 }
956 }
957}
array $params
The job parameters.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Class for creating new log entries and inserting them into the database.
Read-write access to the change_tags table.
getTagsWithData(IReadableDatabase $db, $rc_id=null, $rev_id=null, $log_id=null)
Return all the tags associated with the given recent change ID, revision ID, and/or log entry ID,...
listDefinedTags()
Basically lists defined tags which count even if they aren't applied to anything.
deleteTagEverywhere( $tag)
Permanently removes all traces of a tag from the DB.
listExplicitlyDefinedTags()
Lists tags explicitly defined in the change_tag_def table of the database.
tagUsageStatistics()
Returns a map of any tags used on the wiki to number of edits tagged with them, ordered descending by...
logTagManagementAction(string $action, string $tag, string $reason, UserIdentity $user, $tagCount=null, array $logEntryTags=[])
Writes a tag action into the tag management log.
makeTagSummarySubquery( $tables)
Make the tag summary subquery based on the given tables and return it.
modifyDisplayQueryBuilder(SelectQueryBuilder $queryBuilder, $table, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query builder object.
defineTag( $tag)
Set ctd_user_defined = 1 in change_tag_def without checking that the tag name is valid.
updateTags( $tagsToAdd, $tagsToRemove, &$rc_id=null, &$rev_id=null, &$log_id=null, $params=null, RecentChange $rc=null, UserIdentity $user=null)
Add and remove tags to/from a change given its rc_id, rev_id and/or log_id, without verifying that th...
purgeTagCacheAll()
Invalidates the short-term cache of defined tags used by the list*DefinedTags functions,...
__construct(IConnectionProvider $dbProvider, NameTableStore $changeTagDefStore, WANObjectCache $wanCache, HookContainer $hookContainer, LoggerInterface $logger, UserFactory $userFactory, ServiceOptions $options)
undefineTag( $tag)
Update ctd_user_defined = 0 field in change_tag_def.
getSoftwareTags( $all=false)
Loads defined core tags, checks for invalid types (if not array), and filters for supported and enabl...
listSoftwareActivatedTags()
Lists those tags which core or extensions report as being "active".
addTags( $tags, $rc_id=null, $rev_id=null, $log_id=null, $params=null, RecentChange $rc=null)
Add tags to a change given its rc_id, rev_id and/or log_id.
listSoftwareDefinedTags()
Lists tags defined by core or extensions using the ListDefinedTags hook.
modifyDisplayQuery(&$tables, &$fields, &$conds, &$join_conds, &$options, $filter_tag='', bool $exclude=false)
Applies all tags-related changes to a query.
getTags(IReadableDatabase $db, $rc_id=null, $rev_id=null, $log_id=null)
Return all the tags associated with the given recent change ID, revision ID, and/or log entry ID.
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
onChangeTagsListActive(&$tags)
Use this hook to nominate which of the tags your extension uses are in active use.
onListDefinedTags(&$tags)
This hook is called when trying to find all defined tags.
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()
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Exception representing a failure to look up a row from a name table.
Represents a title within MediaWiki.
Definition Title.php:78
Creates User objects.
Utility class for creating and reading rows in the recentchanges table.
Multi-datacenter aware caching interface.
join( $table, $alias=null, $conds=[])
Inner join a table or group of tables.
leftJoin( $table, $alias=null, $conds=[])
Left join a table or group of tables.
Raw SQL value to be used in query builders.
Build SELECT queries with a fluent interface.
distinct()
Enable the DISTINCT option.
field( $field, $alias=null)
Add a single field to the query, optionally with an alias.
where( $conds)
Add conditions to the query.
Interface for objects representing user identity.
Provide primary and replica IDatabase connections.
getReplicaDatabase( $domain=false, $group=null)
Get connection to a replica database.
A database connection without write operations.