MediaWiki  master
ChangeTagsStore.php
Go to the documentation of this file.
1 <?php
23 
24 use BadMethodCallException;
25 use InvalidArgumentException;
26 use ManualLogEntry;
37 use Psr\Log\LoggerInterface;
38 use RecentChange;
39 use WANObjectCache;
44 
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  $softwareTags = array_intersect(
138  $availableSoftwareTags,
139  self::DEFINED_SOFTWARE_TAGS
140  );
141 
142  return $softwareTags;
143  }
144 
156  public function getTagsWithData(
157  IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null
158  ): array {
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!' );
163  }
164 
165  $conds = array_filter(
166  [
167  'ct_rc_id' => $rc_id,
168  'ct_rev_id' => $rev_id,
169  'ct_log_id' => $log_id,
170  ]
171  );
172  $result = $db->newSelectQueryBuilder()
173  ->select( [ 'ct_tag_id', 'ct_params' ] )
174  ->from( self::CHANGE_TAG )
175  ->where( $conds )
176  ->caller( __METHOD__ )
177  ->fetchResultSet();
178 
179  $tags = [];
180  foreach ( $result as $row ) {
181  $tagName = $this->changeTagDefStore->getName( (int)$row->ct_tag_id );
182  $tags[$tagName] = $row->ct_params;
183  }
184 
185  return $tags;
186  }
187 
195  public function makeTagSummarySubquery( $tables ) {
196  // Normalize to arrays
197  $tables = (array)$tables;
198 
199  // Figure out which ID field to use
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';
208  } else {
209  throw new InvalidArgumentException( 'Unable to determine appropriate JOIN condition for tagging.' );
210  }
211 
212  $tagTables = [ self::CHANGE_TAG, self::CHANGE_TAG_DEF ];
213  $join_cond_ts_tags = [ self::CHANGE_TAG_DEF => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
214  $field = 'ctd_name';
215 
216  return $this->dbProvider->getReplicaDatabase()
217  ->buildGroupConcatField( ',', $tagTables, $field, $join_cond, $join_cond_ts_tags );
218  }
219 
228  public function defineTag( $tag ) {
229  $dbw = $this->dbProvider->getPrimaryDatabase();
230  $dbw->newInsertQueryBuilder()
231  ->insertInto( self::CHANGE_TAG_DEF )
232  ->row( [
233  'ctd_name' => $tag,
234  'ctd_user_defined' => 1,
235  'ctd_count' => 0
236  ] )
237  ->onDuplicateKeyUpdate()
238  ->uniqueIndexFields( [ 'ctd_name' ] )
239  ->set( [ 'ctd_user_defined' => 1 ] )
240  ->caller( __METHOD__ )->execute();
241 
242  // clear the memcache of defined tags
243  $this->purgeTagCacheAll();
244  }
245 
254  public function undefineTag( $tag ) {
255  $dbw = $this->dbProvider->getPrimaryDatabase();
256 
257  $dbw->newUpdateQueryBuilder()
258  ->update( self::CHANGE_TAG_DEF )
259  ->set( [ 'ctd_user_defined' => 0 ] )
260  ->where( [ 'ctd_name' => $tag ] )
261  ->caller( __METHOD__ )->execute();
262 
263  $dbw->newDeleteQueryBuilder()
264  ->deleteFrom( self::CHANGE_TAG_DEF )
265  ->where( [ 'ctd_name' => $tag, 'ctd_count' => 0 ] )
266  ->caller( __METHOD__ )->execute();
267 
268  // clear the memcache of defined tags
269  $this->purgeTagCacheAll();
270  }
271 
286  public function logTagManagementAction( string $action, string $tag, string $reason,
287  UserIdentity $user, $tagCount = null, array $logEntryTags = []
288  ) {
289  $dbw = $this->dbProvider->getPrimaryDatabase();
290 
291  $logEntry = new ManualLogEntry( 'managetags', $action );
292  $logEntry->setPerformer( $user );
293  // target page is not relevant, but it has to be set, so we just put in
294  // the title of Special:Tags
295  $logEntry->setTarget( Title::newFromText( 'Special:Tags' ) );
296  $logEntry->setComment( $reason );
297 
298  $params = [ '4::tag' => $tag ];
299  if ( $tagCount !== null ) {
300  $params['5:number:count'] = $tagCount;
301  }
302  $logEntry->setParameters( $params );
303  $logEntry->setRelations( [ 'Tag' => $tag ] );
304  $logEntry->addTags( $logEntryTags );
305 
306  $logId = $logEntry->insert( $dbw );
307  $logEntry->publish( $logId );
308  return $logId;
309  }
310 
323  public function deleteTagEverywhere( $tag ) {
324  $dbw = $this->dbProvider->getPrimaryDatabase();
325  $dbw->startAtomic( __METHOD__ );
326 
327  // fetch tag id, this must be done before calling undefineTag(), see T225564
328  $tagId = $this->changeTagDefStore->getId( $tag );
329 
330  // set ctd_user_defined = 0
331  $this->undefineTag( $tag );
332 
333  // delete from change_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__ );
343 
344  // give extensions a chance
345  $status = Status::newGood();
346  $this->hookRunner->onChangeTagAfterDelete( $tag, $status );
347  // let's not allow error results, as the actual tag deletion succeeded
348  if ( !$status->isOK() ) {
349  $this->logger->debug( 'ChangeTagAfterDelete error condition downgraded to warning' );
350  $status->setOK( true );
351  }
352 
353  // clear the memcache of defined tags
354  $this->purgeTagCacheAll();
355 
356  return $status;
357  }
358 
364  public function 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' ) );
369 
370  $this->changeTagDefStore->reloadMap();
371  }
372 
379  public function tagUsageStatistics(): array {
380  $fname = __METHOD__;
381  $dbProvider = $this->dbProvider;
382 
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 ) {
387  $dbr = $dbProvider->getReplicaDatabase();
388  $res = $dbr->newSelectQueryBuilder()
389  ->select( [ 'ctd_name', 'ctd_count' ] )
390  ->from( self::CHANGE_TAG_DEF )
391  ->orderBy( 'ctd_count', SelectQueryBuilder::SORT_DESC )
392  ->caller( $fname )
393  ->fetchResultSet();
394 
395  $out = [];
396  foreach ( $res as $row ) {
397  $out[$row->ctd_name] = $row->ctd_count;
398  }
399 
400  return $out;
401  },
402  [
403  'checkKeys' => [ $this->wanCache->makeKey( 'tags-usage-statistics' ) ],
404  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
405  'pcTTL' => WANObjectCache::TTL_PROC_LONG
406  ]
407  );
408  }
409 
418  public function listExplicitlyDefinedTags() {
419  $fname = __METHOD__;
420  $dbProvider = $this->dbProvider;
421 
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 ) {
426  $dbr = $dbProvider->getReplicaDatabase();
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 ] )
432  ->caller( $fname )
433  ->fetchFieldValues();
434 
435  return array_unique( $tags );
436  },
437  [
438  'checkKeys' => [ $this->wanCache->makeKey( 'valid-tags-db' ) ],
439  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
440  'pcTTL' => WANObjectCache::TTL_PROC_LONG
441  ]
442  );
443  }
444 
454  public function listSoftwareDefinedTags() {
455  // core defined tags
456  $tags = $this->getSoftwareTags( true );
457  if ( !$this->hookContainer->isRegistered( 'ListDefinedTags' ) ) {
458  return $tags;
459  }
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 ) {
466  $setOpts += Database::getCacheSetOptions( $dbProvider->getReplicaDatabase() );
467  $hookRunner->onListDefinedTags( $tags );
468  return array_unique( $tags );
469  },
470  [
471  'checkKeys' => [ $this->wanCache->makeKey( 'valid-tags-hook' ) ],
472  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
473  'pcTTL' => WANObjectCache::TTL_PROC_LONG
474  ]
475  );
476  }
477 
488  public function getTags( IReadableDatabase $db, $rc_id = null, $rev_id = null, $log_id = null ) {
489  return array_keys( $this->getTagsWithData( $db, $rc_id, $rev_id, $log_id ) );
490  }
491 
499  public function listDefinedTags() {
500  $tags1 = $this->listExplicitlyDefinedTags();
501  $tags2 = $this->listSoftwareDefinedTags();
502  return array_values( array_unique( array_merge( $tags1, $tags2 ) ) );
503  }
504 
532  public function updateTags( $tagsToAdd, $tagsToRemove, &$rc_id = null,
533  &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null,
534  UserIdentity $user = null
535  ) {
536  $tagsToAdd = array_filter(
537  (array)$tagsToAdd, // Make sure we're submitting all tags...
538  static function ( $value ) {
539  return ( $value ?? '' ) !== '';
540  }
541  );
542  $tagsToRemove = array_filter(
543  (array)$tagsToRemove,
544  static function ( $value ) {
545  return ( $value ?? '' ) !== '';
546  }
547  );
548 
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!' );
552  }
553 
554  $dbw = $this->dbProvider->getPrimaryDatabase();
555 
556  // Might as well look for rcids and so on.
557  if ( !$rc_id ) {
558  // Info might be out of date, somewhat fractionally, on replica DB.
559  // LogEntry/LogPage and WikiPage match rev/log/rc timestamps,
560  // so use that relation to avoid full table scans.
561  if ( $log_id ) {
562  $rc_id = $dbw->newSelectQueryBuilder()
563  ->select( 'rc_id' )
564  ->from( 'logging' )
565  ->join( 'recentchanges', null, [
566  'rc_timestamp = log_timestamp',
567  'rc_logid = log_id'
568  ] )
569  ->where( [ 'log_id' => $log_id ] )
570  ->caller( __METHOD__ )
571  ->fetchField();
572  } elseif ( $rev_id ) {
573  $rc_id = $dbw->newSelectQueryBuilder()
574  ->select( 'rc_id' )
575  ->from( 'revision' )
576  ->join( 'recentchanges', null, [
577  'rc_this_oldid = rev_id'
578  ] )
579  ->where( [ 'rev_id' => $rev_id ] )
580  ->caller( __METHOD__ )
581  ->fetchField();
582  }
583  } elseif ( !$log_id && !$rev_id ) {
584  // Info might be out of date, somewhat fractionally, on replica DB.
585  $log_id = $dbw->newSelectQueryBuilder()
586  ->select( 'rc_logid' )
587  ->from( 'recentchanges' )
588  ->where( [ 'rc_id' => $rc_id ] )
589  ->caller( __METHOD__ )
590  ->fetchField();
591  $rev_id = $dbw->newSelectQueryBuilder()
592  ->select( 'rc_this_oldid' )
593  ->from( 'recentchanges' )
594  ->where( [ 'rc_id' => $rc_id ] )
595  ->caller( __METHOD__ )
596  ->fetchField();
597  }
598 
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__ )
605  ->fetchField();
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__ )
612  ->fetchField();
613  }
614 
615  $prevTags = $this->getTags( $dbw, $rc_id, $rev_id, $log_id );
616 
617  // add tags
618  $tagsToAdd = array_values( array_diff( $tagsToAdd, $prevTags ) );
619  $newTags = array_unique( array_merge( $prevTags, $tagsToAdd ) );
620 
621  // remove tags
622  $tagsToRemove = array_values( array_intersect( $tagsToRemove, $newTags ) );
623  $newTags = array_values( array_diff( $newTags, $tagsToRemove ) );
624 
625  sort( $prevTags );
626  sort( $newTags );
627  if ( $prevTags == $newTags ) {
628  return [ [], [], $prevTags ];
629  }
630 
631  // insert a row into change_tag for each new tag
632  if ( count( $tagsToAdd ) ) {
633  $changeTagMapping = [];
634  foreach ( $tagsToAdd as $tag ) {
635  $changeTagMapping[$tag] = $this->changeTagDefStore->acquireId( $tag );
636  }
637  $fname = __METHOD__;
638  // T207881: update the counts at the end of the transaction
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();
645  }, $fname );
646 
647  $tagsRows = [];
648  foreach ( $tagsToAdd as $tag ) {
649  // Filter so we don't insert NULLs as zero accidentally.
650  // Keep in mind that $rc_id === null means "I don't care/know about the
651  // rc_id, just delete $tag on this revision/log entry". It doesn't
652  // mean "only delete tags on this revision/log WHERE rc_id IS NULL".
653  $tagsRows[] = array_filter(
654  [
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,
660  ]
661  );
662 
663  }
664 
665  $dbw->newInsertQueryBuilder()
666  ->insertInto( self::CHANGE_TAG )
667  ->ignore()
668  ->rows( $tagsRows )
669  ->caller( __METHOD__ )->execute();
670  }
671 
672  // delete from change_tag
673  if ( count( $tagsToRemove ) ) {
674  $fname = __METHOD__;
675  foreach ( $tagsToRemove as $tag ) {
676  $conds = array_filter(
677  [
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 ),
682  ]
683  );
684  $dbw->newDeleteQueryBuilder()
685  ->deleteFrom( self::CHANGE_TAG )
686  ->where( $conds )
687  ->caller( __METHOD__ )->execute();
688  if ( $dbw->affectedRows() ) {
689  // T207881: update the counts at the end of the transaction
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();
696 
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();
701  }, $fname );
702  }
703  }
704  }
705 
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 );
709 
710  return [ $tagsToAdd, $tagsToRemove, $prevTags ];
711  }
712 
726  public function addTags( $tags, $rc_id = null, $rev_id = null,
727  $log_id = null, $params = null, RecentChange $rc = null
728  ) {
729  $result = $this->updateTags( $tags, null, $rc_id, $rev_id, $log_id, $params, $rc );
730  return (bool)$result[0];
731  }
732 
739  public function listSoftwareActivatedTags() {
740  // core active tags
741  $tags = $this->getSoftwareTags();
742  if ( !$this->hookContainer->isRegistered( 'ChangeTagsListActive' ) ) {
743  return $tags;
744  }
745  $hookRunner = $this->hookRunner;
746  $dbProvider = $this->dbProvider;
747 
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 ) {
752  $setOpts += Database::getCacheSetOptions( $dbProvider->getReplicaDatabase() );
753 
754  // Ask extensions which tags they consider active
755  $hookRunner->onChangeTagsListActive( $tags );
756  return $tags;
757  },
758  [
759  'checkKeys' => [ $this->wanCache->makeKey( 'active-tags' ) ],
760  'lockTSE' => WANObjectCache::TTL_MINUTE * 5,
761  'pcTTL' => WANObjectCache::TTL_PROC_LONG
762  ]
763  );
764  }
765 
786  public function modifyDisplayQuery( &$tables, &$fields, &$conds,
787  &$join_conds, &$options, $filter_tag = '', bool $exclude = false
788  ) {
789  $useTagFilter = $this->options->get( MainConfigNames::UseTagFilter );
790 
791  // Normalize to arrays
792  $tables = (array)$tables;
793  $fields = (array)$fields;
794  $conds = (array)$conds;
795  $options = (array)$options;
796 
797  $fields['ts_tags'] = $this->makeTagSummarySubquery( $tables );
798  // We use an alias and qualify the conditions in case there are
799  // multiple joins to this table.
800  // In particular for compatibility with the RC filters that extension Translate does.
801 
802  // Figure out which ID field to use
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';
811  } else {
812  throw new InvalidArgumentException( 'Unable to determine appropriate JOIN condition for tagging.' );
813  }
814 
815  if ( !$useTagFilter ) {
816  return;
817  }
818 
819  if ( !is_array( $filter_tag ) ) {
820  // some callers provide false or null
821  $filter_tag = (string)$filter_tag;
822  }
823 
824  if ( $filter_tag !== [] && $filter_tag !== '' ) {
825  // Somebody wants to filter on a tag.
826  // Add an INNER JOIN on change_tag
827  $filterTagIds = [];
828  foreach ( (array)$filter_tag as $filterTagName ) {
829  try {
830  $filterTagIds[] = $this->changeTagDefStore->getId( $filterTagName );
831  } catch ( NameTableAccessException $exception ) {
832  }
833  }
834 
835  if ( $exclude ) {
836  if ( $filterTagIds !== [] ) {
837  $tables[self::DISPLAY_TABLE_ALIAS] = self::CHANGE_TAG;
838  $join_conds[self::DISPLAY_TABLE_ALIAS] = [
839  'LEFT JOIN',
840  [ $join_cond, self::DISPLAY_TABLE_ALIAS . '.ct_tag_id' => $filterTagIds ]
841  ];
842  $conds[self::DISPLAY_TABLE_ALIAS . '.ct_tag_id'] = null;
843  }
844  } else {
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;
849  } else {
850  // all tags were invalid, return nothing
851  $conds[] = '0=1';
852  }
853 
854  if (
855  is_array( $filter_tag ) && count( $filter_tag ) > 1 &&
856  !in_array( 'DISTINCT', $options )
857  ) {
858  $options[] = 'DISTINCT';
859  }
860  }
861  }
862  }
863 }
if(!defined('MW_SETUP_CALLBACK'))
Definition: WebStart.php:88
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.
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...
Definition: HookRunner.php:568
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:58
Exception representing a failure to look up a row from a name table.
Represents a title within MediaWiki.
Definition: Title.php:76
Creates User objects.
Definition: UserFactory.php:41
Utility class for creating new RC entries.
Multi-datacenter aware caching interface.
Build SELECT queries with a fluent interface.
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.