MediaWiki  master
WatchedItemStore.php
Go to the documentation of this file.
1 <?php
2 
3 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
10 use Wikimedia\Assert\Assert;
16 use Wikimedia\ScopedCallback;
17 
27 
31  public const CONSTRUCTOR_OPTIONS = [
32  'UpdateRowsPerQuery',
33  'WatchlistExpiry',
34  'WatchlistExpiryMaxDuration',
35  ];
36 
40  private $lbFactory;
41 
45  private $loadBalancer;
46 
50  private $queueGroup;
51 
55  private $stash;
56 
60  private $readOnlyMode;
61 
65  private $cache;
66 
71 
79  private $cacheIndex = [];
80 
85 
90 
94  private $nsInfo;
95 
99  private $revisionLookup;
100 
104  private $stats;
105 
109  private $expiryEnabled;
110 
114  private $hookRunner;
115 
120 
132  public function __construct(
133  ServiceOptions $options,
141  HookContainer $hookContainer
142  ) {
143  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
144  $this->updateRowsPerQuery = $options->get( 'UpdateRowsPerQuery' );
145  $this->expiryEnabled = $options->get( 'WatchlistExpiry' );
146  $this->maxExpiryDuration = $options->get( 'WatchlistExpiryMaxDuration' );
147 
148  $this->lbFactory = $lbFactory;
149  $this->loadBalancer = $lbFactory->getMainLB();
150  $this->queueGroup = $queueGroup;
151  $this->stash = $stash;
152  $this->cache = $cache;
153  $this->readOnlyMode = $readOnlyMode;
154  $this->stats = new NullStatsdDataFactory();
155  $this->deferredUpdatesAddCallableUpdateCallback =
156  [ DeferredUpdates::class, 'addCallableUpdate' ];
157  $this->nsInfo = $nsInfo;
158  $this->revisionLookup = $revisionLookup;
159  $this->hookRunner = new HookRunner( $hookContainer );
160 
161  $this->latestUpdateCache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
162  }
163 
167  public function setStatsdDataFactory( StatsdDataFactoryInterface $stats ) {
168  $this->stats = $stats;
169  }
170 
182  public function overrideDeferredUpdatesAddCallableUpdateCallback( callable $callback ) {
183  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
184  throw new MWException(
185  'Cannot override DeferredUpdates::addCallableUpdate callback in operation.'
186  );
187  }
189  $this->deferredUpdatesAddCallableUpdateCallback = $callback;
190  return new ScopedCallback( function () use ( $previousValue ) {
191  $this->deferredUpdatesAddCallableUpdateCallback = $previousValue;
192  } );
193  }
194 
195  private function getCacheKey( UserIdentity $user, LinkTarget $target ) {
196  return $this->cache->makeKey(
197  (string)$target->getNamespace(),
198  $target->getDBkey(),
199  (string)$user->getId()
200  );
201  }
202 
203  private function cache( WatchedItem $item ) {
204  $user = $item->getUserIdentity();
205  $target = $item->getLinkTarget();
206  $key = $this->getCacheKey( $user, $target );
207  $this->cache->set( $key, $item );
208  $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] = $key;
209  $this->stats->increment( 'WatchedItemStore.cache' );
210  }
211 
212  private function uncache( UserIdentity $user, LinkTarget $target ) {
213  $this->cache->delete( $this->getCacheKey( $user, $target ) );
214  unset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] );
215  $this->stats->increment( 'WatchedItemStore.uncache' );
216  }
217 
218  private function uncacheLinkTarget( LinkTarget $target ) {
219  $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget' );
220  if ( !isset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] ) ) {
221  return;
222  }
223  foreach ( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] as $key ) {
224  $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget.items' );
225  $this->cache->delete( $key );
226  }
227  }
228 
229  private function uncacheUser( UserIdentity $user ) {
230  $this->stats->increment( 'WatchedItemStore.uncacheUser' );
231  foreach ( $this->cacheIndex as $ns => $dbKeyArray ) {
232  foreach ( $dbKeyArray as $dbKey => $userArray ) {
233  if ( isset( $userArray[$user->getId()] ) ) {
234  $this->stats->increment( 'WatchedItemStore.uncacheUser.items' );
235  $this->cache->delete( $userArray[$user->getId()] );
236  }
237  }
238  }
239 
240  $pageSeenKey = $this->getPageSeenTimestampsKey( $user );
241  $this->latestUpdateCache->delete( $pageSeenKey );
242  $this->stash->delete( $pageSeenKey );
243  }
244 
251  private function getCached( UserIdentity $user, LinkTarget $target ) {
252  return $this->cache->get( $this->getCacheKey( $user, $target ) );
253  }
254 
260  private function getConnectionRef( $dbIndex ) {
261  return $this->loadBalancer->getConnectionRef( $dbIndex, [ 'watchlist' ] );
262  }
263 
274  public function clearUserWatchedItems( UserIdentity $user ) {
275  if ( $this->mustClearWatchedItemsUsingJobQueue( $user ) ) {
276  return false;
277  }
278 
279  $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
280 
281  if ( $this->expiryEnabled ) {
282  $ticket = $this->lbFactory->getEmptyTransactionTicket( __METHOD__ );
283  // First fetch the wl_ids.
284  $wlIds = $dbw->selectFieldValues( 'watchlist', 'wl_id', [
285  'wl_user' => $user->getId()
286  ], __METHOD__ );
287 
288  if ( $wlIds ) {
289  // Delete rows from both the watchlist and watchlist_expiry tables.
290  $dbw->delete(
291  'watchlist',
292  [ 'wl_id' => $wlIds ],
293  __METHOD__
294  );
295 
296  $dbw->delete(
297  'watchlist_expiry',
298  [ 'we_item' => $wlIds ],
299  __METHOD__
300  );
301  }
302  $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
303  } else {
304  $dbw->delete(
305  'watchlist',
306  [ 'wl_user' => $user->getId() ],
307  __METHOD__
308  );
309  }
310 
311  $this->uncacheAllItemsForUser( $user );
312 
313  return true;
314  }
315 
316  public function mustClearWatchedItemsUsingJobQueue( UserIdentity $user ): bool {
317  return $this->countWatchedItems( $user ) > $this->updateRowsPerQuery;
318  }
319 
320  private function uncacheAllItemsForUser( UserIdentity $user ) {
321  $userId = $user->getId();
322  foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
323  foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
324  if ( array_key_exists( $userId, $userIndex ) ) {
325  $this->cache->delete( $userIndex[$userId] );
326  unset( $this->cacheIndex[$ns][$dbKey][$userId] );
327  }
328  }
329  }
330 
331  // Cleanup empty cache keys
332  foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
333  foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
334  if ( empty( $this->cacheIndex[$ns][$dbKey] ) ) {
335  unset( $this->cacheIndex[$ns][$dbKey] );
336  }
337  }
338  if ( empty( $this->cacheIndex[$ns] ) ) {
339  unset( $this->cacheIndex[$ns] );
340  }
341  }
342  }
343 
352  $job = ClearUserWatchlistJob::newForUser( $user, $this->getMaxId() );
353  $this->queueGroup->push( $job );
354  }
355 
359  public function enqueueWatchlistExpiryJob( float $watchlistPurgeRate ): void {
360  $max = mt_getrandmax();
361  if ( mt_rand( 0, $max ) < $max * $watchlistPurgeRate ) {
362  // The higher the watchlist purge rate, the more likely we are to enqueue a job.
363  $this->queueGroup->push( new WatchlistExpiryJob() );
364  }
365  }
366 
371  public function getMaxId() {
372  $dbr = $this->getConnectionRef( DB_REPLICA );
373  return (int)$dbr->selectField(
374  'watchlist',
375  'MAX(wl_id)',
376  '',
377  __METHOD__
378  );
379  }
380 
386  public function countWatchedItems( UserIdentity $user ) {
387  $dbr = $this->getConnectionRef( DB_REPLICA );
388  $tables = [ 'watchlist' ];
389  $conds = [ 'wl_user' => $user->getId() ];
390  $joinConds = [];
391 
392  if ( $this->expiryEnabled ) {
393  $tables[] = 'watchlist_expiry';
394  $joinConds[ 'watchlist_expiry' ] = [ 'LEFT JOIN', 'wl_id = we_item' ];
395  $conds[] = 'we_expiry IS NULL OR we_expiry > ' . $dbr->addQuotes( $dbr->timestamp() );
396  }
397 
398  $return = (int)$dbr->selectField(
399  $tables,
400  'COUNT(*)',
401  $conds,
402  __METHOD__,
403  [],
404  $joinConds
405  );
406 
407  return $return;
408  }
409 
415  public function countWatchers( LinkTarget $target ) {
416  $dbr = $this->getConnectionRef( DB_REPLICA );
417  $tables = [ 'watchlist' ];
418  $conds = [
419  'wl_namespace' => $target->getNamespace(),
420  'wl_title' => $target->getDBkey()
421  ];
422  $joinConds = [];
423 
424  if ( $this->expiryEnabled ) {
425  $tables[] = 'watchlist_expiry';
426  $joinConds[ 'watchlist_expiry' ] = [ 'LEFT JOIN', 'wl_id = we_item' ];
427  $conds[] = 'we_expiry IS NULL OR we_expiry > ' . $dbr->addQuotes( $dbr->timestamp() );
428  }
429 
430  $return = (int)$dbr->selectField(
431  $tables,
432  'COUNT(*)',
433  $conds,
434  __METHOD__,
435  [],
436  $joinConds
437  );
438 
439  return $return;
440  }
441 
448  public function countVisitingWatchers( LinkTarget $target, $threshold ) {
449  $dbr = $this->getConnectionRef( DB_REPLICA );
450  $tables = [ 'watchlist' ];
451  $conds = [
452  'wl_namespace' => $target->getNamespace(),
453  'wl_title' => $target->getDBkey(),
454  'wl_notificationtimestamp >= ' .
455  $dbr->addQuotes( $dbr->timestamp( $threshold ) ) .
456  ' OR wl_notificationtimestamp IS NULL'
457  ];
458  $joinConds = [];
459 
460  if ( $this->expiryEnabled ) {
461  $tables[] = 'watchlist_expiry';
462  $joinConds[ 'watchlist_expiry' ] = [ 'LEFT JOIN', 'wl_id = we_item' ];
463  $conds[] = 'we_expiry IS NULL OR we_expiry > ' . $dbr->addQuotes( $dbr->timestamp() );
464  }
465 
466  $visitingWatchers = (int)$dbr->selectField(
467  $tables,
468  'COUNT(*)',
469  $conds,
470  __METHOD__,
471  [],
472  $joinConds
473  );
474 
475  return $visitingWatchers;
476  }
477 
483  public function removeWatchBatchForUser( UserIdentity $user, array $titles ) {
484  if ( $this->readOnlyMode->isReadOnly() ) {
485  return false;
486  }
487  if ( !$user->isRegistered() ) {
488  return false;
489  }
490  if ( !$titles ) {
491  return true;
492  }
493 
494  $rows = $this->getTitleDbKeysGroupedByNamespace( $titles );
495  $this->uncacheTitlesForUser( $user, $titles );
496 
497  $dbw = $this->getConnectionRef( DB_MASTER );
498  $ticket = count( $titles ) > $this->updateRowsPerQuery ?
499  $this->lbFactory->getEmptyTransactionTicket( __METHOD__ ) : null;
500  $affectedRows = 0;
501 
502  // Batch delete items per namespace.
503  foreach ( $rows as $namespace => $namespaceTitles ) {
504  $rowBatches = array_chunk( $namespaceTitles, $this->updateRowsPerQuery );
505  foreach ( $rowBatches as $toDelete ) {
506  // First fetch the wl_ids.
507  $wlIds = $dbw->selectFieldValues( 'watchlist', 'wl_id', [
508  'wl_user' => $user->getId(),
509  'wl_namespace' => $namespace,
510  'wl_title' => $toDelete
511  ], __METHOD__ );
512 
513  if ( $wlIds ) {
514  // Delete rows from both the watchlist and watchlist_expiry tables.
515  $dbw->delete(
516  'watchlist',
517  [ 'wl_id' => $wlIds ],
518  __METHOD__
519  );
520  $affectedRows += $dbw->affectedRows();
521 
522  if ( $this->expiryEnabled ) {
523  $dbw->delete(
524  'watchlist_expiry',
525  [ 'we_item' => $wlIds ],
526  __METHOD__
527  );
528  $affectedRows += $dbw->affectedRows();
529  }
530  }
531 
532  if ( $ticket ) {
533  $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
534  }
535  }
536  }
537 
538  return (bool)$affectedRows;
539  }
540 
547  public function countWatchersMultiple( array $targets, array $options = [] ) {
548  $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ];
549 
550  $dbr = $this->getConnectionRef( DB_REPLICA );
551 
552  if ( array_key_exists( 'minimumWatchers', $options ) ) {
553  $dbOptions['HAVING'] = 'COUNT(*) >= ' . (int)$options['minimumWatchers'];
554  }
555 
556  $lb = new LinkBatch( $targets );
557 
558  $tables = [ 'watchlist' ];
559  $conds = [ $lb->constructSet( 'wl', $dbr ) ];
560  $joinConds = [];
561 
562  if ( $this->expiryEnabled ) {
563  $tables[] = 'watchlist_expiry';
564  $joinConds[ 'watchlist_expiry' ] = [ 'LEFT JOIN', 'wl_id = we_item' ];
565  $conds[] = 'we_expiry IS NULL OR we_expiry > ' . $dbr->addQuotes( $dbr->timestamp() );
566  }
567 
568  $res = $dbr->select(
569  $tables,
570  [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ],
571  $conds,
572  __METHOD__,
573  $dbOptions,
574  $joinConds
575  );
576 
577  $watchCounts = [];
578  foreach ( $targets as $linkTarget ) {
579  $watchCounts[$linkTarget->getNamespace()][$linkTarget->getDBkey()] = 0;
580  }
581 
582  foreach ( $res as $row ) {
583  $watchCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
584  }
585 
586  return $watchCounts;
587  }
588 
596  array $targetsWithVisitThresholds,
597  $minimumWatchers = null
598  ) {
599  if ( $targetsWithVisitThresholds === [] ) {
600  // No titles requested => no results returned
601  return [];
602  }
603 
604  $dbr = $this->getConnectionRef( DB_REPLICA );
605 
606  $conds = [ $this->getVisitingWatchersCondition( $dbr, $targetsWithVisitThresholds ) ];
607 
608  $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ];
609  if ( $minimumWatchers !== null ) {
610  $dbOptions['HAVING'] = 'COUNT(*) >= ' . (int)$minimumWatchers;
611  }
612 
613  $tables = [ 'watchlist' ];
614  $joinConds = [];
615 
616  if ( $this->expiryEnabled ) {
617  $tables[] = 'watchlist_expiry';
618  $joinConds[ 'watchlist_expiry' ] = [ 'LEFT JOIN', 'wl_id = we_item' ];
619  $conds[] = 'we_expiry IS NULL OR we_expiry > ' . $dbr->addQuotes( $dbr->timestamp() );
620  }
621 
622  $res = $dbr->select(
623  $tables,
624  [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
625  $conds,
626  __METHOD__,
627  $dbOptions,
628  $joinConds
629  );
630 
631  $watcherCounts = [];
632  foreach ( $targetsWithVisitThresholds as list( $target ) ) {
633  /* @var LinkTarget $target */
634  $watcherCounts[$target->getNamespace()][$target->getDBkey()] = 0;
635  }
636 
637  foreach ( $res as $row ) {
638  $watcherCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
639  }
640 
641  return $watcherCounts;
642  }
643 
652  IDatabase $db,
653  array $targetsWithVisitThresholds
654  ) {
655  $missingTargets = [];
656  $namespaceConds = [];
657  foreach ( $targetsWithVisitThresholds as list( $target, $threshold ) ) {
658  if ( $threshold === null ) {
659  $missingTargets[] = $target;
660  continue;
661  }
662  /* @var LinkTarget $target */
663  $namespaceConds[$target->getNamespace()][] = $db->makeList( [
664  'wl_title = ' . $db->addQuotes( $target->getDBkey() ),
665  $db->makeList( [
666  'wl_notificationtimestamp >= ' . $db->addQuotes( $db->timestamp( $threshold ) ),
667  'wl_notificationtimestamp IS NULL'
668  ], LIST_OR )
669  ], LIST_AND );
670  }
671 
672  $conds = [];
673  foreach ( $namespaceConds as $namespace => $pageConds ) {
674  $conds[] = $db->makeList( [
675  'wl_namespace = ' . $namespace,
676  '(' . $db->makeList( $pageConds, LIST_OR ) . ')'
677  ], LIST_AND );
678  }
679 
680  if ( $missingTargets ) {
681  $lb = new LinkBatch( $missingTargets );
682  $conds[] = $lb->constructSet( 'wl', $db );
683  }
684 
685  return $db->makeList( $conds, LIST_OR );
686  }
687 
694  public function getWatchedItem( UserIdentity $user, LinkTarget $target ) {
695  if ( !$user->isRegistered() ) {
696  return false;
697  }
698 
699  $cached = $this->getCached( $user, $target );
700  if ( $cached && !$cached->isExpired() ) {
701  $this->stats->increment( 'WatchedItemStore.getWatchedItem.cached' );
702  return $cached;
703  }
704  $this->stats->increment( 'WatchedItemStore.getWatchedItem.load' );
705  return $this->loadWatchedItem( $user, $target );
706  }
707 
714  public function loadWatchedItem( UserIdentity $user, LinkTarget $target ) {
715  // Only registered user can have a watchlist
716  if ( !$user->isRegistered() ) {
717  return false;
718  }
719 
720  $dbr = $this->getConnectionRef( DB_REPLICA );
721 
722  $row = $this->fetchWatchedItems(
723  $dbr,
724  $user,
725  [ 'wl_notificationtimestamp' ],
726  [],
727  $target
728  );
729 
730  if ( !$row ) {
731  return false;
732  }
733 
734  $item = $this->getWatchedItemFromRow( $user, $target, $row );
735  $this->cache( $item );
736 
737  return $item;
738  }
739 
746  public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) {
747  $options += [ 'forWrite' => false ];
748 
749  $dbOptions = [];
750  if ( array_key_exists( 'sort', $options ) ) {
751  Assert::parameter(
752  ( in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ) ),
753  '$options[\'sort\']',
754  'must be SORT_ASC or SORT_DESC'
755  );
756  $dbOptions['ORDER BY'] = [
757  "wl_namespace {$options['sort']}",
758  "wl_title {$options['sort']}"
759  ];
760  }
761  $db = $this->getConnectionRef( $options['forWrite'] ? DB_MASTER : DB_REPLICA );
762 
763  $res = $this->fetchWatchedItems(
764  $db,
765  $user,
766  [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
767  $dbOptions
768  );
769 
770  $watchedItems = [];
771  foreach ( $res as $row ) {
772  $target = new TitleValue( (int)$row->wl_namespace, $row->wl_title );
773  // @todo: Should we add these to the process cache?
774  $watchedItems[] = $this->getWatchedItemFromRow( $user, $target, $row );
775  }
776 
777  return $watchedItems;
778  }
779 
787  private function getWatchedItemFromRow(
788  UserIdentity $user,
789  LinkTarget $target,
790  stdClass $row
791  ): WatchedItem {
792  return new WatchedItem(
793  $user,
794  $target,
796  $row->wl_notificationtimestamp, $user, $target ),
797  wfTimestampOrNull( TS_MW, $row->we_expiry ?? null )
798  );
799  }
800 
813  private function fetchWatchedItems(
814  IDatabase $db,
815  UserIdentity $user,
816  array $vars,
817  array $options = [],
818  ?LinkTarget $target = null
819  ) {
820  $dbMethod = 'select';
821  $conds = [ 'wl_user' => $user->getId() ];
822 
823  if ( $target ) {
824  $dbMethod = 'selectRow';
825  $conds = array_merge( $conds, [
826  'wl_namespace' => $target->getNamespace(),
827  'wl_title' => $target->getDBkey(),
828  ] );
829  }
830 
831  if ( $this->expiryEnabled ) {
832  $vars[] = 'we_expiry';
833  $conds[] = 'we_expiry IS NULL OR we_expiry > ' . $db->addQuotes( $db->timestamp() );
834 
835  return $db->{$dbMethod}(
836  [ 'watchlist', 'watchlist_expiry' ],
837  $vars,
838  $conds,
839  __METHOD__,
840  $options,
841  [ 'watchlist_expiry' => [ 'LEFT JOIN', [ 'wl_id = we_item' ] ] ]
842  );
843  }
844 
845  return $db->{$dbMethod}(
846  'watchlist',
847  $vars,
848  $conds,
849  __METHOD__,
850  $options
851  );
852  }
853 
860  public function isWatched( UserIdentity $user, LinkTarget $target ) {
861  return (bool)$this->getWatchedItem( $user, $target );
862  }
863 
871  public function isTempWatched( UserIdentity $user, LinkTarget $target ): bool {
872  $item = $this->getWatchedItem( $user, $target );
873  return $item && $item->getExpiry();
874  }
875 
882  public function getNotificationTimestampsBatch( UserIdentity $user, array $targets ) {
883  $timestamps = [];
884  foreach ( $targets as $target ) {
885  $timestamps[$target->getNamespace()][$target->getDBkey()] = false;
886  }
887 
888  if ( !$user->isRegistered() ) {
889  return $timestamps;
890  }
891 
892  $targetsToLoad = [];
893  foreach ( $targets as $target ) {
894  $cachedItem = $this->getCached( $user, $target );
895  if ( $cachedItem ) {
896  $timestamps[$target->getNamespace()][$target->getDBkey()] =
897  $cachedItem->getNotificationTimestamp();
898  } else {
899  $targetsToLoad[] = $target;
900  }
901  }
902 
903  if ( !$targetsToLoad ) {
904  return $timestamps;
905  }
906 
907  $dbr = $this->getConnectionRef( DB_REPLICA );
908 
909  $lb = new LinkBatch( $targetsToLoad );
910  $res = $dbr->select(
911  'watchlist',
912  [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
913  [
914  $lb->constructSet( 'wl', $dbr ),
915  'wl_user' => $user->getId(),
916  ],
917  __METHOD__
918  );
919 
920  foreach ( $res as $row ) {
921  $target = new TitleValue( (int)$row->wl_namespace, $row->wl_title );
922  $timestamps[$row->wl_namespace][$row->wl_title] =
923  $this->getLatestNotificationTimestamp(
924  $row->wl_notificationtimestamp, $user, $target );
925  }
926 
927  return $timestamps;
928  }
929 
938  public function addWatch( UserIdentity $user, LinkTarget $target, ?string $expiry = null ) {
939  $this->addWatchBatchForUser( $user, [ $target ], $expiry );
940  }
941 
954  public function addWatchBatchForUser(
955  UserIdentity $user,
956  array $targets,
957  ?string $expiry = null
958  ) {
959  if ( $this->readOnlyMode->isReadOnly() ) {
960  return false;
961  }
962  // Only registered user can have a watchlist
963  if ( !$user->isRegistered() ) {
964  return false;
965  }
966 
967  if ( !$targets ) {
968  return true;
969  }
970 
971  $rows = [];
972  $items = [];
973  foreach ( $targets as $target ) {
974  $rows[] = [
975  'wl_user' => $user->getId(),
976  'wl_namespace' => $target->getNamespace(),
977  'wl_title' => $target->getDBkey(),
978  'wl_notificationtimestamp' => null,
979  ];
980  $items[] = new WatchedItem(
981  $user,
982  $target,
983  null,
984  $expiry
985  );
986  $this->uncache( $user, $target );
987  }
988 
989  $dbw = $this->getConnectionRef( DB_MASTER );
990  $ticket = count( $targets ) > $this->updateRowsPerQuery ?
991  $this->lbFactory->getEmptyTransactionTicket( __METHOD__ ) : null;
992  $affectedRows = 0;
993  $rowBatches = array_chunk( $rows, $this->updateRowsPerQuery );
994  foreach ( $rowBatches as $toInsert ) {
995  // Use INSERT IGNORE to avoid overwriting the notification timestamp
996  // if there's already an entry for this page
997  $dbw->insert( 'watchlist', $toInsert, __METHOD__, [ 'IGNORE' ] );
998  $affectedRows += $dbw->affectedRows();
999 
1000  if ( $this->expiryEnabled ) {
1001  $affectedRows += $this->updateOrDeleteExpiries( $dbw, $user->getId(), $toInsert, $expiry );
1002  }
1003 
1004  if ( $ticket ) {
1005  $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
1006  }
1007  }
1008  // Update process cache to ensure skin doesn't claim that the current
1009  // page is unwatched in the response of action=watch itself (T28292).
1010  // This would otherwise be re-queried from a replica by isWatched().
1011  foreach ( $items as $item ) {
1012  $this->cache( $item );
1013  }
1014 
1015  return (bool)$affectedRows;
1016  }
1017 
1027  private function updateOrDeleteExpiries(
1028  IDatabase $dbw,
1029  int $userId,
1030  array $rows,
1031  ?string $expiry = null
1032  ): int {
1033  if ( ExpiryDef::expiryExceedsMax( $expiry, $this->maxExpiryDuration ) ) {
1034  $expiry = ExpiryDef::normalizeExpiry( $this->maxExpiryDuration );
1035  } else {
1036  $expiry = ExpiryDef::normalizeExpiry( $expiry );
1037  }
1038 
1039  if ( !$expiry ) {
1040  // Either expiry was invalid or null (shouldn't change), 0 rows affected.
1041  return 0;
1042  }
1043 
1044  // Build the giant `(...) OR (...)` part to be used with WHERE.
1045  $conds = [];
1046  foreach ( $rows as $row ) {
1047  $conds[] = $dbw->makeList(
1048  [
1049  'wl_user' => $userId,
1050  'wl_namespace' => $row['wl_namespace'],
1051  'wl_title' => $row['wl_title']
1052  ],
1054  );
1055  }
1056  $cond = $dbw->makeList( $conds, $dbw::LIST_OR );
1057 
1058  if ( wfIsInfinity( $expiry ) ) {
1059  // Rows should be deleted rather than updated.
1060  $dbw->deleteJoin(
1061  'watchlist_expiry',
1062  'watchlist',
1063  'we_item',
1064  'wl_id',
1065  [ $cond ],
1066  __METHOD__
1067  );
1068 
1069  return $dbw->affectedRows();
1070  }
1071 
1072  return $this->updateExpiries( $dbw, $expiry, $cond );
1073  }
1074 
1082  private function updateExpiries( IDatabase $dbw, string $expiry, string $cond ): int {
1083  // First fetch the wl_ids from the watchlist table.
1084  // We'd prefer to do a INSERT/SELECT in the same query with IDatabase::insertSelect(),
1085  // but it doesn't allow us to use the "ON DUPLICATE KEY UPDATE" clause.
1086  $wlIds = (array)$dbw->selectFieldValues( 'watchlist', 'wl_id', $cond, __METHOD__ );
1087 
1088  $expiry = $dbw->timestamp( $expiry );
1089 
1090  $weRows = array_map( function ( $wlId ) use ( $expiry, $dbw ) {
1091  return [
1092  'we_item' => $wlId,
1093  'we_expiry' => $expiry
1094  ];
1095  }, $wlIds );
1096 
1097  // Insert into watchlist_expiry, updating the expiry for duplicate rows.
1098  $dbw->upsert(
1099  'watchlist_expiry',
1100  $weRows,
1101  'we_item',
1102  [ 'we_expiry' => $expiry ],
1103  __METHOD__
1104  );
1105 
1106  return $dbw->affectedRows();
1107  }
1108 
1115  public function removeWatch( UserIdentity $user, LinkTarget $target ) {
1116  return $this->removeWatchBatchForUser( $user, [ $target ] );
1117  }
1118 
1137  UserIdentity $user, $timestamp, array $targets = []
1138  ) {
1139  // Only registered user can have a watchlist
1140  if ( !$user->isRegistered() || $this->readOnlyMode->isReadOnly() ) {
1141  return false;
1142  }
1143 
1144  if ( !$targets ) {
1145  // Backwards compatibility
1146  $this->resetAllNotificationTimestampsForUser( $user, $timestamp );
1147  return true;
1148  }
1149 
1150  $rows = $this->getTitleDbKeysGroupedByNamespace( $targets );
1151 
1152  $dbw = $this->getConnectionRef( DB_MASTER );
1153  if ( $timestamp !== null ) {
1154  $timestamp = $dbw->timestamp( $timestamp );
1155  }
1156  $ticket = $this->lbFactory->getEmptyTransactionTicket( __METHOD__ );
1157  $affectedSinceWait = 0;
1158 
1159  // Batch update items per namespace
1160  foreach ( $rows as $namespace => $namespaceTitles ) {
1161  $rowBatches = array_chunk( $namespaceTitles, $this->updateRowsPerQuery );
1162  foreach ( $rowBatches as $toUpdate ) {
1163  $dbw->update(
1164  'watchlist',
1165  [ 'wl_notificationtimestamp' => $timestamp ],
1166  [
1167  'wl_user' => $user->getId(),
1168  'wl_namespace' => $namespace,
1169  'wl_title' => $toUpdate
1170  ],
1171  __METHOD__
1172  );
1173  $affectedSinceWait += $dbw->affectedRows();
1174  // Wait for replication every time we've touched updateRowsPerQuery rows
1175  if ( $affectedSinceWait >= $this->updateRowsPerQuery ) {
1176  $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
1177  $affectedSinceWait = 0;
1178  }
1179  }
1180  }
1181 
1182  $this->uncacheUser( $user );
1183 
1184  return true;
1185  }
1186 
1188  $timestamp, UserIdentity $user, LinkTarget $target
1189  ) {
1190  $timestamp = wfTimestampOrNull( TS_MW, $timestamp );
1191  if ( $timestamp === null ) {
1192  return null; // no notification
1193  }
1194 
1195  $seenTimestamps = $this->getPageSeenTimestamps( $user );
1196  if (
1197  $seenTimestamps &&
1198  $seenTimestamps->get( $this->getPageSeenKey( $target ) ) >= $timestamp
1199  ) {
1200  // If a reset job did not yet run, then the "seen" timestamp will be higher
1201  return null;
1202  }
1203 
1204  return $timestamp;
1205  }
1206 
1213  public function resetAllNotificationTimestampsForUser( UserIdentity $user, $timestamp = null ) {
1214  // Only registered user can have a watchlist
1215  if ( !$user->isRegistered() ) {
1216  return;
1217  }
1218 
1219  // If the page is watched by the user (or may be watched), update the timestamp
1221  'userId' => $user->getId(), 'timestamp' => $timestamp, 'casTime' => time()
1222  ] );
1223 
1224  // Try to run this post-send
1225  // Calls DeferredUpdates::addCallableUpdate in normal operation
1226  call_user_func(
1227  $this->deferredUpdatesAddCallableUpdateCallback,
1228  function () use ( $job ) {
1229  $job->run();
1230  }
1231  );
1232  }
1233 
1242  UserIdentity $editor, LinkTarget $target, $timestamp
1243  ) {
1244  $dbw = $this->getConnectionRef( DB_MASTER );
1245  $selectTables = [ 'watchlist' ];
1246  $selectConds = [
1247  'wl_user != ' . intval( $editor->getId() ),
1248  'wl_namespace' => $target->getNamespace(),
1249  'wl_title' => $target->getDBkey(),
1250  'wl_notificationtimestamp IS NULL',
1251  ];
1252  $selectJoin = [];
1253 
1254  if ( $this->expiryEnabled ) {
1255  $selectTables[] = 'watchlist_expiry';
1256  $selectConds[] = 'we_expiry IS NULL OR we_expiry > ' .
1257  $dbw->addQuotes( $dbw->timestamp() );
1258  $selectJoin = [ 'watchlist_expiry' => [ 'LEFT JOIN', 'wl_id = we_item' ] ];
1259  }
1260 
1261  $uids = $dbw->selectFieldValues(
1262  $selectTables,
1263  'wl_user',
1264  $selectConds,
1265  __METHOD__,
1266  [],
1267  $selectJoin
1268  );
1269 
1270  $watchers = array_map( 'intval', $uids );
1271  if ( $watchers ) {
1272  // Update wl_notificationtimestamp for all watching users except the editor
1273  $fname = __METHOD__;
1275  function () use ( $timestamp, $watchers, $target, $fname ) {
1276  $dbw = $this->getConnectionRef( DB_MASTER );
1277  $ticket = $this->lbFactory->getEmptyTransactionTicket( $fname );
1278 
1279  $watchersChunks = array_chunk( $watchers, $this->updateRowsPerQuery );
1280  foreach ( $watchersChunks as $watchersChunk ) {
1281  $dbw->update( 'watchlist',
1282  [ /* SET */
1283  'wl_notificationtimestamp' => $dbw->timestamp( $timestamp )
1284  ], [ /* WHERE - TODO Use wl_id T130067 */
1285  'wl_user' => $watchersChunk,
1286  'wl_namespace' => $target->getNamespace(),
1287  'wl_title' => $target->getDBkey(),
1288  ], $fname
1289  );
1290  if ( count( $watchersChunks ) > 1 ) {
1291  $this->lbFactory->commitAndWaitForReplication(
1292  $fname, $ticket, [ 'domain' => $dbw->getDomainID() ]
1293  );
1294  }
1295  }
1296  $this->uncacheLinkTarget( $target );
1297  },
1299  $dbw
1300  );
1301  }
1302 
1303  return $watchers;
1304  }
1305 
1315  UserIdentity $user, LinkTarget $title, $force = '', $oldid = 0
1316  ) {
1317  $time = time();
1318 
1319  // Only registered user can have a watchlist
1320  if ( $this->readOnlyMode->isReadOnly() || !$user->isRegistered() ) {
1321  return false;
1322  }
1323 
1324  // Hook expects User and Title, not UserIdentity and LinkTarget
1325  $userObj = User::newFromId( $user->getId() );
1326  $titleObj = Title::castFromLinkTarget( $title );
1327  if ( !$this->hookRunner->onBeforeResetNotificationTimestamp(
1328  $userObj, $titleObj, $force, $oldid )
1329  ) {
1330  return false;
1331  }
1332  if ( !$userObj->equals( $user ) ) {
1333  $user = $userObj;
1334  }
1335  if ( !$titleObj->equals( $title ) ) {
1336  $title = $titleObj;
1337  }
1338 
1339  $item = null;
1340  if ( $force != 'force' ) {
1341  $item = $this->loadWatchedItem( $user, $title );
1342  if ( !$item || $item->getNotificationTimestamp() === null ) {
1343  return false;
1344  }
1345  }
1346 
1347  // Get the timestamp (TS_MW) of this revision to track the latest one seen
1348  $id = $oldid;
1349  $seenTime = null;
1350  if ( !$id ) {
1351  $latestRev = $this->revisionLookup->getRevisionByTitle( $title );
1352  if ( $latestRev ) {
1353  $id = $latestRev->getId();
1354  // Save a DB query
1355  $seenTime = $latestRev->getTimestamp();
1356  }
1357  }
1358  if ( $seenTime === null ) {
1359  $seenTime = $this->revisionLookup->getTimestampFromId( $id );
1360  }
1361 
1362  // Mark the item as read immediately in lightweight storage
1363  $this->stash->merge(
1364  $this->getPageSeenTimestampsKey( $user ),
1365  function ( $cache, $key, $current ) use ( $title, $seenTime ) {
1366  $value = $current ?: new MapCacheLRU( 300 );
1367  $subKey = $this->getPageSeenKey( $title );
1368 
1369  if ( $seenTime > $value->get( $subKey ) ) {
1370  // Revision is newer than the last one seen
1371  $value->set( $subKey, $seenTime );
1372  $this->latestUpdateCache->set( $key, $value, BagOStuff::TTL_PROC_LONG );
1373  } elseif ( $seenTime === false ) {
1374  // Revision does not exist
1375  $value->set( $subKey, wfTimestamp( TS_MW ) );
1376  $this->latestUpdateCache->set( $key, $value, BagOStuff::TTL_PROC_LONG );
1377  } else {
1378  return false; // nothing to update
1379  }
1380 
1381  return $value;
1382  },
1383  BagOStuff::TTL_HOUR
1384  );
1385 
1386  // If the page is watched by the user (or may be watched), update the timestamp
1387  $job = new ActivityUpdateJob(
1388  $title,
1389  [
1390  'type' => 'updateWatchlistNotification',
1391  'userid' => $user->getId(),
1392  'notifTime' => $this->getNotificationTimestamp( $user, $title, $item, $force, $oldid ),
1393  'curTime' => $time
1394  ]
1395  );
1396  // Try to enqueue this post-send
1397  $this->queueGroup->lazyPush( $job );
1398 
1399  $this->uncache( $user, $title );
1400 
1401  return true;
1402  }
1403 
1408  private function getPageSeenTimestamps( UserIdentity $user ) {
1409  $key = $this->getPageSeenTimestampsKey( $user );
1410 
1411  return $this->latestUpdateCache->getWithSetCallback(
1412  $key,
1413  BagOStuff::TTL_PROC_LONG,
1414  function () use ( $key ) {
1415  return $this->stash->get( $key ) ?: null;
1416  }
1417  );
1418  }
1419 
1424  private function getPageSeenTimestampsKey( UserIdentity $user ) {
1425  return $this->stash->makeGlobalKey(
1426  'watchlist-recent-updates',
1427  $this->lbFactory->getLocalDomainID(),
1428  $user->getId()
1429  );
1430  }
1431 
1436  private function getPageSeenKey( LinkTarget $target ) {
1437  return "{$target->getNamespace()}:{$target->getDBkey()}";
1438  }
1439 
1448  private function getNotificationTimestamp(
1449  UserIdentity $user, LinkTarget $title, $item, $force, $oldid
1450  ) {
1451  if ( !$oldid ) {
1452  // No oldid given, assuming latest revision; clear the timestamp.
1453  return null;
1454  }
1455 
1456  $oldRev = $this->revisionLookup->getRevisionById( $oldid );
1457  if ( !$oldRev ) {
1458  // Oldid given but does not exist (probably deleted)
1459  return false;
1460  }
1461 
1462  $nextRev = $this->revisionLookup->getNextRevision( $oldRev );
1463  if ( !$nextRev ) {
1464  // Oldid given and is the latest revision for this title; clear the timestamp.
1465  return null;
1466  }
1467 
1468  if ( $item === null ) {
1469  $item = $this->loadWatchedItem( $user, $title );
1470  }
1471 
1472  if ( !$item ) {
1473  // This can only happen if $force is enabled.
1474  return null;
1475  }
1476 
1477  // Oldid given and isn't the latest; update the timestamp.
1478  // This will result in no further notification emails being sent!
1479  $notificationTimestamp = $this->revisionLookup->getTimestampFromId( $oldid );
1480  // @FIXME: this should use getTimestamp() for consistency with updates on new edits
1481  // $notificationTimestamp = $nextRev->getTimestamp(); // first unseen revision timestamp
1482 
1483  // We need to go one second to the future because of various strict comparisons
1484  // throughout the codebase
1485  $ts = new MWTimestamp( $notificationTimestamp );
1486  $ts->timestamp->add( new DateInterval( 'PT1S' ) );
1487  $notificationTimestamp = $ts->getTimestamp( TS_MW );
1488 
1489  if ( $notificationTimestamp < $item->getNotificationTimestamp() ) {
1490  if ( $force != 'force' ) {
1491  return false;
1492  } else {
1493  // This is a little silly‚Ķ
1494  return $item->getNotificationTimestamp();
1495  }
1496  }
1497 
1498  return $notificationTimestamp;
1499  }
1500 
1507  public function countUnreadNotifications( UserIdentity $user, $unreadLimit = null ) {
1508  $dbr = $this->getConnectionRef( DB_REPLICA );
1509 
1510  $queryOptions = [];
1511  if ( $unreadLimit !== null ) {
1512  $unreadLimit = (int)$unreadLimit;
1513  $queryOptions['LIMIT'] = $unreadLimit;
1514  }
1515 
1516  $conds = [
1517  'wl_user' => $user->getId(),
1518  'wl_notificationtimestamp IS NOT NULL'
1519  ];
1520 
1521  $rowCount = $dbr->selectRowCount( 'watchlist', '1', $conds, __METHOD__, $queryOptions );
1522 
1523  if ( $unreadLimit === null ) {
1524  return $rowCount;
1525  }
1526 
1527  if ( $rowCount >= $unreadLimit ) {
1528  return true;
1529  }
1530 
1531  return $rowCount;
1532  }
1533 
1539  public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget ) {
1540  // Duplicate first the subject page, then the talk page
1541  $this->duplicateEntry(
1542  $this->nsInfo->getSubjectPage( $oldTarget ),
1543  $this->nsInfo->getSubjectPage( $newTarget )
1544  );
1545  $this->duplicateEntry(
1546  $this->nsInfo->getTalkPage( $oldTarget ),
1547  $this->nsInfo->getTalkPage( $newTarget )
1548  );
1549  }
1550 
1556  public function duplicateEntry( LinkTarget $oldTarget, LinkTarget $newTarget ) {
1557  $dbw = $this->getConnectionRef( DB_MASTER );
1558 
1559  $result = $dbw->select(
1560  'watchlist',
1561  [ 'wl_user', 'wl_notificationtimestamp' ],
1562  [
1563  'wl_namespace' => $oldTarget->getNamespace(),
1564  'wl_title' => $oldTarget->getDBkey(),
1565  ],
1566  __METHOD__,
1567  [ 'FOR UPDATE' ]
1568  );
1569 
1570  $newNamespace = $newTarget->getNamespace();
1571  $newDBkey = $newTarget->getDBkey();
1572 
1573  # Construct array to replace into the watchlist
1574  $values = [];
1575  foreach ( $result as $row ) {
1576  $values[] = [
1577  'wl_user' => $row->wl_user,
1578  'wl_namespace' => $newNamespace,
1579  'wl_title' => $newDBkey,
1580  'wl_notificationtimestamp' => $row->wl_notificationtimestamp,
1581  ];
1582  }
1583 
1584  if ( !empty( $values ) ) {
1585  # Perform replace
1586  # Note that multi-row replace is very efficient for MySQL but may be inefficient for
1587  # some other DBMSes, mostly due to poor simulation by us
1588  $dbw->replace(
1589  'watchlist',
1590  [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
1591  $values,
1592  __METHOD__
1593  );
1594  }
1595  }
1596 
1601  private function getTitleDbKeysGroupedByNamespace( array $titles ) {
1602  $rows = [];
1603  foreach ( $titles as $title ) {
1604  // Group titles by namespace.
1605  $rows[ $title->getNamespace() ][] = $title->getDBkey();
1606  }
1607  return $rows;
1608  }
1609 
1614  private function uncacheTitlesForUser( UserIdentity $user, array $titles ) {
1615  foreach ( $titles as $title ) {
1616  $this->uncache( $user, $title );
1617  }
1618  }
1619 
1623  public function countExpired(): int {
1624  $dbr = $this->getConnectionRef( DB_REPLICA );
1625  return $dbr->selectRowCount(
1626  'watchlist_expiry',
1627  '*',
1628  [ 'we_expiry <= ' . $dbr->addQuotes( $dbr->timestamp() ) ],
1629  __METHOD__
1630  );
1631  }
1632 
1636  public function removeExpired( int $limit, bool $deleteOrphans = false ): void {
1637  $dbr = $this->getConnectionRef( DB_REPLICA );
1638  $dbw = $this->getConnectionRef( DB_MASTER );
1639  $ticket = $this->lbFactory->getEmptyTransactionTicket( __METHOD__ );
1640 
1641  // Get a batch of watchlist IDs to delete.
1642  $toDelete = $dbr->selectFieldValues(
1643  'watchlist_expiry',
1644  'we_item',
1645  [ 'we_expiry <= ' . $dbr->addQuotes( $dbr->timestamp() ) ],
1646  __METHOD__,
1647  [ 'LIMIT' => $limit ]
1648  );
1649  if ( count( $toDelete ) > 0 ) {
1650  // Delete them from the watchlist and watchlist_expiry table.
1651  $dbw->delete(
1652  'watchlist',
1653  [ 'wl_id' => $toDelete ],
1654  __METHOD__
1655  );
1656  $dbw->delete(
1657  'watchlist_expiry',
1658  [ 'we_item' => $toDelete ],
1659  __METHOD__
1660  );
1661  }
1662 
1663  // Also delete any orphaned or null-expiry watchlist_expiry rows
1664  // (they should not exist, but might because not everywhere knows about the expiry table yet).
1665  if ( $deleteOrphans ) {
1666  $expiryToDelete = $dbr->selectFieldValues(
1667  [ 'watchlist_expiry', 'watchlist' ],
1668  'we_item',
1669  $dbr->makeList(
1670  [ 'wl_id' => null, 'we_expiry' => null ],
1672  ),
1673  __METHOD__,
1674  [],
1675  [ 'watchlist' => [ 'LEFT JOIN', 'wl_id = we_item' ] ]
1676  );
1677  if ( count( $expiryToDelete ) > 0 ) {
1678  $dbw->delete(
1679  'watchlist_expiry',
1680  [ 'we_item' => $expiryToDelete ],
1681  __METHOD__
1682  );
1683  }
1684  }
1685 
1686  $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
1687  }
1688 }
WatchedItemStore\getLatestNotificationTimestamp
getLatestNotificationTimestamp( $timestamp, UserIdentity $user, LinkTarget $target)
Convert $timestamp to TS_MW or return null if the page was visited since then by $user.
Definition: WatchedItemStore.php:1187
MWTimestamp
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:34
WatchedItemStore\$cacheIndex
array[][] $cacheIndex
Looks like $cacheIndex[Namespace ID][Target DB Key][User Id] => 'key' The index is needed so that on ...
Definition: WatchedItemStore.php:79
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:564
Wikimedia\Rdbms\IDatabase\affectedRows
affectedRows()
Get the number of rows affected by the last write query.
WatchedItemStore\uncacheAllItemsForUser
uncacheAllItemsForUser(UserIdentity $user)
Definition: WatchedItemStore.php:320
ActivityUpdateJob
Job for updating user activity like "last viewed" timestamps.
Definition: ActivityUpdateJob.php:36
WatchedItemStore\loadWatchedItem
loadWatchedItem(UserIdentity $user, LinkTarget $target)
Definition: WatchedItemStore.php:714
WatchedItemStore\updateNotificationTimestamp
updateNotificationTimestamp(UserIdentity $editor, LinkTarget $target, $timestamp)
Definition: WatchedItemStore.php:1241
HashBagOStuff
Simple store for keeping values in an associative array for the current process.
Definition: HashBagOStuff.php:32
LinkBatch
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition: LinkBatch.php:35
WatchedItemStore\getPageSeenTimestamps
getPageSeenTimestamps(UserIdentity $user)
Definition: WatchedItemStore.php:1408
WatchedItemStore\clearUserWatchedItems
clearUserWatchedItems(UserIdentity $user)
Deletes ALL watched items for the given user when under $updateRowsPerQuery entries exist.
Definition: WatchedItemStore.php:274
WatchedItemStore\getVisitingWatchersCondition
getVisitingWatchersCondition(IDatabase $db, array $targetsWithVisitThresholds)
Generates condition for the query used in a batch count visiting watchers.
Definition: WatchedItemStore.php:651
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1808
WatchedItemStore\getNotificationTimestampsBatch
getNotificationTimestampsBatch(UserIdentity $user, array $targets)
Definition: WatchedItemStore.php:882
ReadOnlyMode
A service class for fetching the wiki's current read-only mode.
Definition: ReadOnlyMode.php:11
ClearUserWatchlistJob\newForUser
static newForUser(UserIdentity $user, $maxWatchlistId)
Definition: ClearUserWatchlistJob.php:32
Wikimedia\Rdbms\IDatabase\upsert
upsert( $table, array $rows, $uniqueKeys, array $set, $fname=__METHOD__)
Upsert the given row(s) into a table.
WatchedItemStore\$nsInfo
NamespaceInfo $nsInfo
Definition: WatchedItemStore.php:94
WatchedItemStore\countVisitingWatchers
countVisitingWatchers(LinkTarget $target, $threshold)
Definition: WatchedItemStore.php:448
NullStatsdDataFactory
Definition: NullStatsdDataFactory.php:10
ClearWatchlistNotificationsJob
Job for clearing all of the "last viewed" timestamps for a user's watchlist, or setting them all to t...
Definition: ClearWatchlistNotificationsJob.php:36
WatchedItemStore\countWatchedItems
countWatchedItems(UserIdentity $user)
Definition: WatchedItemStore.php:386
WatchedItemStore\duplicateAllAssociatedEntries
duplicateAllAssociatedEntries(LinkTarget $oldTarget, LinkTarget $newTarget)
Definition: WatchedItemStore.php:1539
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:71
WatchedItemStore\addWatch
addWatch(UserIdentity $user, LinkTarget $target, ?string $expiry=null)
Definition: WatchedItemStore.php:938
WatchedItemStore\$loadBalancer
LoadBalancer $loadBalancer
Definition: WatchedItemStore.php:45
Wikimedia\Rdbms\ILBFactory\getMainLB
getMainLB( $domain=false)
Get a cached (tracked) load balancer object.
WatchedItemStore\enqueueWatchlistExpiryJob
enqueueWatchlistExpiryJob(float $watchlistPurgeRate)
Probabilistically add a job to purge the expired watchlist items.1.35The value of the $wgWatchlistPur...
Definition: WatchedItemStore.php:359
$res
$res
Definition: testCompression.php:57
WatchedItemStore\getNotificationTimestamp
getNotificationTimestamp(UserIdentity $user, LinkTarget $title, $item, $force, $oldid)
Definition: WatchedItemStore.php:1448
WatchedItemStore\countExpired
countExpired()
Get the number of watchlist items that expire before the current time.1.35int
Definition: WatchedItemStore.php:1623
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:32
WatchedItemStore\$cache
HashBagOStuff $cache
Definition: WatchedItemStore.php:65
Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
WatchedItemStore\CONSTRUCTOR_OPTIONS
const CONSTRUCTOR_OPTIONS
Definition: WatchedItemStore.php:31
LIST_AND
const LIST_AND
Definition: Defines.php:48
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
$dbr
$dbr
Definition: testCompression.php:54
WatchedItemStore\$lbFactory
ILBFactory $lbFactory
Definition: WatchedItemStore.php:40
Wikimedia\ParamValidator\TypeDef\ExpiryDef
Type definition for expiry timestamps.
Definition: ExpiryDef.php:16
MediaWiki\Linker\LinkTarget\getNamespace
getNamespace()
Get the namespace index.
WatchedItemStore\$readOnlyMode
ReadOnlyMode $readOnlyMode
Definition: WatchedItemStore.php:60
Wikimedia\Rdbms\IDatabase\timestamp
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
LIST_OR
const LIST_OR
Definition: Defines.php:51
WatchedItemStore\$expiryEnabled
bool $expiryEnabled
Correlates to $wgWatchlistExpiry feature flag.
Definition: WatchedItemStore.php:109
WatchedItemStore\overrideDeferredUpdatesAddCallableUpdateCallback
overrideDeferredUpdatesAddCallableUpdateCallback(callable $callback)
Overrides the DeferredUpdates::addCallableUpdate callback This is intended for use while testing and ...
Definition: WatchedItemStore.php:182
MWException
MediaWiki exception.
Definition: MWException.php:29
WatchedItemStore\getPageSeenTimestampsKey
getPageSeenTimestampsKey(UserIdentity $user)
Definition: WatchedItemStore.php:1424
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:25
WatchedItemStore\isTempWatched
isTempWatched(UserIdentity $user, LinkTarget $target)
Check if the user is temporarily watching the page.
Definition: WatchedItemStore.php:871
Wikimedia\Rdbms\IDatabase\deleteJoin
deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname=__METHOD__)
DELETE where the condition is a join.
MediaWiki\User\UserIdentity\isRegistered
isRegistered()
Wikimedia\Rdbms\IResultWrapper
Result wrapper for grabbing data queried from an IDatabase object.
Definition: IResultWrapper.php:24
WatchedItemStore\resetNotificationTimestamp
resetNotificationTimestamp(UserIdentity $user, LinkTarget $title, $force='', $oldid=0)
Definition: WatchedItemStore.php:1314
WatchedItemStore\$queueGroup
JobQueueGroup $queueGroup
Definition: WatchedItemStore.php:50
wfTimestampOrNull
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
Definition: GlobalFunctions.php:1824
WatchedItemStore\duplicateEntry
duplicateEntry(LinkTarget $oldTarget, LinkTarget $newTarget)
Definition: WatchedItemStore.php:1556
MapCacheLRU
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:38
WatchedItemStore\removeWatch
removeWatch(UserIdentity $user, LinkTarget $target)
Definition: WatchedItemStore.php:1115
DeferredUpdates\POSTSEND
const POSTSEND
Definition: DeferredUpdates.php:85
$title
$title
Definition: testCompression.php:38
WatchedItemStore\$maxExpiryDuration
string null $maxExpiryDuration
Maximum configured relative expiry.
Definition: WatchedItemStore.php:119
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
WatchedItemStore\getMaxId
getMaxId()
Definition: WatchedItemStore.php:371
WatchedItemStore\updateOrDeleteExpiries
updateOrDeleteExpiries(IDatabase $dbw, int $userId, array $rows, ?string $expiry=null)
Insert/update expiries, or delete them if the expiry is 'infinity'.
Definition: WatchedItemStore.php:1027
DB_MASTER
const DB_MASTER
Definition: defines.php:26
WatchedItemStore\isWatched
isWatched(UserIdentity $user, LinkTarget $target)
Definition: WatchedItemStore.php:860
WatchedItemStore\uncacheLinkTarget
uncacheLinkTarget(LinkTarget $target)
Definition: WatchedItemStore.php:218
WatchedItem\getUserIdentity
getUserIdentity()
Definition: WatchedItem.php:107
WatchedItemStore\getWatchedItemFromRow
getWatchedItemFromRow(UserIdentity $user, LinkTarget $target, stdClass $row)
Construct a new WatchedItem given a row from watchlist/watchlist_expiry.
Definition: WatchedItemStore.php:787
WatchedItemStore\removeExpired
removeExpired(int $limit, bool $deleteOrphans=false)
Remove some number of expired watchlist items.1.35The number of items to remove. Whether to also dele...
Definition: WatchedItemStore.php:1636
WatchedItemStore\getPageSeenKey
getPageSeenKey(LinkTarget $target)
Definition: WatchedItemStore.php:1436
WatchedItemStore\getConnectionRef
getConnectionRef( $dbIndex)
Definition: WatchedItemStore.php:260
WatchedItemStore\$deferredUpdatesAddCallableUpdateCallback
callable null $deferredUpdatesAddCallableUpdateCallback
Definition: WatchedItemStore.php:84
Wikimedia\Rdbms\LoadBalancer
Database connection, tracking, load balancing, and transaction manager for a cluster.
Definition: LoadBalancer.php:42
WatchedItemStore\$revisionLookup
RevisionLookup $revisionLookup
Definition: WatchedItemStore.php:99
WatchedItemStore\countUnreadNotifications
countUnreadNotifications(UserIdentity $user, $unreadLimit=null)
Definition: WatchedItemStore.php:1507
StatsdAwareInterface
Describes a Statsd aware interface.
Definition: StatsdAwareInterface.php:13
WatchedItemStore\mustClearWatchedItemsUsingJobQueue
mustClearWatchedItemsUsingJobQueue(UserIdentity $user)
Does the size of the users watchlist require clearUserWatchedItemsUsingJobQueue() to be used instead ...
Definition: WatchedItemStore.php:316
WatchedItemStore\resetAllNotificationTimestampsForUser
resetAllNotificationTimestampsForUser(UserIdentity $user, $timestamp=null)
Schedule a DeferredUpdate that sets all of the "last viewed" timestamps for a given user to the same ...
Definition: WatchedItemStore.php:1213
wfIsInfinity
wfIsInfinity( $str)
Determine input string is represents as infinity.
Definition: GlobalFunctions.php:2829
WatchedItem
Representation of a pair of user and title for watchlist entries.
Definition: WatchedItem.php:33
MediaWiki\Linker\LinkTarget\getDBkey
getDBkey()
Get the main part with underscores.
WatchedItemStore\setNotificationTimestampsForUser
setNotificationTimestampsForUser(UserIdentity $user, $timestamp, array $targets=[])
Set the "last viewed" timestamps for certain titles on a user's watchlist.
Definition: WatchedItemStore.php:1136
WatchedItemStore\getWatchedItem
getWatchedItem(UserIdentity $user, LinkTarget $target)
Definition: WatchedItemStore.php:694
WatchedItemStore\$latestUpdateCache
HashBagOStuff $latestUpdateCache
Definition: WatchedItemStore.php:70
WatchedItemStore\uncacheTitlesForUser
uncacheTitlesForUser(UserIdentity $user, array $titles)
Definition: WatchedItemStore.php:1614
WatchedItemStore
Storage layer class for WatchedItems.
Definition: WatchedItemStore.php:26
WatchlistExpiryJob
Definition: WatchlistExpiryJob.php:5
WatchedItemStore\getCached
getCached(UserIdentity $user, LinkTarget $target)
Definition: WatchedItemStore.php:251
WatchedItemStore\getCacheKey
getCacheKey(UserIdentity $user, LinkTarget $target)
Definition: WatchedItemStore.php:195
WatchedItemStore\uncache
uncache(UserIdentity $user, LinkTarget $target)
Definition: WatchedItemStore.php:212
MediaWiki\User\UserIdentity\getId
getId()
$cache
$cache
Definition: mcc.php:33
Wikimedia\Rdbms\IDatabase\addQuotes
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
WatchedItemStore\$stats
StatsdDataFactoryInterface $stats
Definition: WatchedItemStore.php:104
WatchedItemStore\$stash
BagOStuff $stash
Definition: WatchedItemStore.php:55
WatchedItemStore\countWatchers
countWatchers(LinkTarget $target)
Definition: WatchedItemStore.php:415
$job
if(count( $args)< 1) $job
Definition: recompressTracked.php:50
WatchedItemStore\__construct
__construct(ServiceOptions $options, ILBFactory $lbFactory, JobQueueGroup $queueGroup, BagOStuff $stash, HashBagOStuff $cache, ReadOnlyMode $readOnlyMode, NamespaceInfo $nsInfo, RevisionLookup $revisionLookup, HookContainer $hookContainer)
Definition: WatchedItemStore.php:132
WatchedItemStore\updateExpiries
updateExpiries(IDatabase $dbw, string $expiry, string $cond)
Update the expiries for items found with the given $cond.
Definition: WatchedItemStore.php:1082
WatchedItem\getLinkTarget
getLinkTarget()
Definition: WatchedItem.php:114
Wikimedia\Rdbms\IDatabase\selectFieldValues
selectFieldValues( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a list of single field values from result rows.
WatchedItemStore\countVisitingWatchersMultiple
countVisitingWatchersMultiple(array $targetsWithVisitThresholds, $minimumWatchers=null)
Definition: WatchedItemStore.php:595
WatchedItemStore\setStatsdDataFactory
setStatsdDataFactory(StatsdDataFactoryInterface $stats)
Definition: WatchedItemStore.php:167
WatchedItemStore\uncacheUser
uncacheUser(UserIdentity $user)
Definition: WatchedItemStore.php:229
MediaWiki\Config\ServiceOptions\get
get( $key)
Definition: ServiceOptions.php:84
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:44
WatchedItemStore\$hookRunner
HookRunner $hookRunner
Definition: WatchedItemStore.php:114
NamespaceInfo
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Definition: NamespaceInfo.php:35
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:571
WatchedItemStore\getWatchedItemsForUser
getWatchedItemsForUser(UserIdentity $user, array $options=[])
Definition: WatchedItemStore.php:746
WatchedItemStore\addWatchBatchForUser
addWatchBatchForUser(UserIdentity $user, array $targets, ?string $expiry=null)
Add multiple items to the user's watchlist.
Definition: WatchedItemStore.php:954
Title\castFromLinkTarget
static castFromLinkTarget( $linkTarget)
Same as newFromLinkTarget, but if passed null, returns null.
Definition: Title.php:305
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
WatchedItemStoreInterface
Definition: WatchedItemStoreInterface.php:30
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:145
WatchedItemStore\getTitleDbKeysGroupedByNamespace
getTitleDbKeysGroupedByNamespace(array $titles)
Definition: WatchedItemStore.php:1601
Wikimedia\Rdbms\IDatabase\makeList
makeList(array $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
WatchedItemStore\$updateRowsPerQuery
int $updateRowsPerQuery
Definition: WatchedItemStore.php:89
WatchedItemStore\countWatchersMultiple
countWatchersMultiple(array $targets, array $options=[])
Definition: WatchedItemStore.php:547
WatchedItemStore\fetchWatchedItems
fetchWatchedItems(IDatabase $db, UserIdentity $user, array $vars, array $options=[], ?LinkTarget $target=null)
Fetches either a single or all watched items for the given user.
Definition: WatchedItemStore.php:813
MediaWiki\Config\ServiceOptions\assertRequiredOptions
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Definition: ServiceOptions.php:62
Wikimedia\Rdbms\ILBFactory
An interface for generating database load balancers.
Definition: ILBFactory.php:33
WatchedItemStore\removeWatchBatchForUser
removeWatchBatchForUser(UserIdentity $user, array $titles)
Definition: WatchedItemStore.php:483
JobQueueGroup
Class to handle enqueueing of background jobs.
Definition: JobQueueGroup.php:30
WatchedItemStore\clearUserWatchedItemsUsingJobQueue
clearUserWatchedItemsUsingJobQueue(UserIdentity $user)
Queues a job that will clear the users watchlist using the Job Queue.
Definition: WatchedItemStore.php:351
WatchedItemStore\cache
cache(WatchedItem $item)
Definition: WatchedItemStore.php:203
TitleValue
Represents a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:39