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