MediaWiki  1.31.0
WatchedItemStore.php
Go to the documentation of this file.
1 <?php
2 
4 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
7 use Wikimedia\Assert\Assert;
8 use Wikimedia\ScopedCallback;
10 
20 
24  private $loadBalancer;
25 
29  private $readOnlyMode;
30 
34  private $cache;
35 
42  private $cacheIndex = [];
43 
48 
53 
58 
62  private $stats;
63 
70  public function __construct(
75  ) {
76  $this->loadBalancer = $loadBalancer;
77  $this->cache = $cache;
78  $this->readOnlyMode = $readOnlyMode;
79  $this->stats = new NullStatsdDataFactory();
80  $this->deferredUpdatesAddCallableUpdateCallback =
81  [ DeferredUpdates::class, 'addCallableUpdate' ];
82  $this->revisionGetTimestampFromIdCallback =
83  [ Revision::class, 'getTimestampFromId' ];
84  $this->updateRowsPerQuery = $updateRowsPerQuery;
85  }
86 
90  public function setStatsdDataFactory( StatsdDataFactoryInterface $stats ) {
91  $this->stats = $stats;
92  }
93 
105  public function overrideDeferredUpdatesAddCallableUpdateCallback( callable $callback ) {
106  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
107  throw new MWException(
108  'Cannot override DeferredUpdates::addCallableUpdate callback in operation.'
109  );
110  }
112  $this->deferredUpdatesAddCallableUpdateCallback = $callback;
113  return new ScopedCallback( function () use ( $previousValue ) {
114  $this->deferredUpdatesAddCallableUpdateCallback = $previousValue;
115  } );
116  }
117 
128  public function overrideRevisionGetTimestampFromIdCallback( callable $callback ) {
129  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
130  throw new MWException(
131  'Cannot override Revision::getTimestampFromId callback in operation.'
132  );
133  }
135  $this->revisionGetTimestampFromIdCallback = $callback;
136  return new ScopedCallback( function () use ( $previousValue ) {
137  $this->revisionGetTimestampFromIdCallback = $previousValue;
138  } );
139  }
140 
141  private function getCacheKey( User $user, LinkTarget $target ) {
142  return $this->cache->makeKey(
143  (string)$target->getNamespace(),
144  $target->getDBkey(),
145  (string)$user->getId()
146  );
147  }
148 
149  private function cache( WatchedItem $item ) {
150  $user = $item->getUser();
151  $target = $item->getLinkTarget();
152  $key = $this->getCacheKey( $user, $target );
153  $this->cache->set( $key, $item );
154  $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] = $key;
155  $this->stats->increment( 'WatchedItemStore.cache' );
156  }
157 
158  private function uncache( User $user, LinkTarget $target ) {
159  $this->cache->delete( $this->getCacheKey( $user, $target ) );
160  unset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] );
161  $this->stats->increment( 'WatchedItemStore.uncache' );
162  }
163 
164  private function uncacheLinkTarget( LinkTarget $target ) {
165  $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget' );
166  if ( !isset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] ) ) {
167  return;
168  }
169  foreach ( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] as $key ) {
170  $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget.items' );
171  $this->cache->delete( $key );
172  }
173  }
174 
175  private function uncacheUser( User $user ) {
176  $this->stats->increment( 'WatchedItemStore.uncacheUser' );
177  foreach ( $this->cacheIndex as $ns => $dbKeyArray ) {
178  foreach ( $dbKeyArray as $dbKey => $userArray ) {
179  if ( isset( $userArray[$user->getId()] ) ) {
180  $this->stats->increment( 'WatchedItemStore.uncacheUser.items' );
181  $this->cache->delete( $userArray[$user->getId()] );
182  }
183  }
184  }
185  }
186 
193  private function getCached( User $user, LinkTarget $target ) {
194  return $this->cache->get( $this->getCacheKey( $user, $target ) );
195  }
196 
206  private function dbCond( User $user, LinkTarget $target ) {
207  return [
208  'wl_user' => $user->getId(),
209  'wl_namespace' => $target->getNamespace(),
210  'wl_title' => $target->getDBkey(),
211  ];
212  }
213 
220  private function getConnectionRef( $dbIndex ) {
221  return $this->loadBalancer->getConnectionRef( $dbIndex, [ 'watchlist' ] );
222  }
223 
234  public function clearUserWatchedItems( User $user ) {
235  if ( $this->countWatchedItems( $user ) > $this->updateRowsPerQuery ) {
236  return false;
237  }
238 
239  $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
240  $dbw->delete(
241  'watchlist',
242  [ 'wl_user' => $user->getId() ],
243  __METHOD__
244  );
245  $this->uncacheAllItemsForUser( $user );
246 
247  return true;
248  }
249 
250  private function uncacheAllItemsForUser( User $user ) {
251  $userId = $user->getId();
252  foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
253  foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
254  if ( array_key_exists( $userId, $userIndex ) ) {
255  $this->cache->delete( $userIndex[$userId] );
256  unset( $this->cacheIndex[$ns][$dbKey][$userId] );
257  }
258  }
259  }
260 
261  // Cleanup empty cache keys
262  foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
263  foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
264  if ( empty( $this->cacheIndex[$ns][$dbKey] ) ) {
265  unset( $this->cacheIndex[$ns][$dbKey] );
266  }
267  }
268  if ( empty( $this->cacheIndex[$ns] ) ) {
269  unset( $this->cacheIndex[$ns] );
270  }
271  }
272  }
273 
283  // TODO inject me.
284  JobQueueGroup::singleton()->push( $job );
285  }
286 
291  public function getMaxId() {
292  $dbr = $this->getConnectionRef( DB_REPLICA );
293  return (int)$dbr->selectField(
294  'watchlist',
295  'MAX(wl_id)',
296  '',
297  __METHOD__
298  );
299  }
300 
306  public function countWatchedItems( User $user ) {
307  $dbr = $this->getConnectionRef( DB_REPLICA );
308  $return = (int)$dbr->selectField(
309  'watchlist',
310  'COUNT(*)',
311  [
312  'wl_user' => $user->getId()
313  ],
314  __METHOD__
315  );
316 
317  return $return;
318  }
319 
325  public function countWatchers( LinkTarget $target ) {
326  $dbr = $this->getConnectionRef( DB_REPLICA );
327  $return = (int)$dbr->selectField(
328  'watchlist',
329  'COUNT(*)',
330  [
331  'wl_namespace' => $target->getNamespace(),
332  'wl_title' => $target->getDBkey(),
333  ],
334  __METHOD__
335  );
336 
337  return $return;
338  }
339 
346  public function countVisitingWatchers( LinkTarget $target, $threshold ) {
347  $dbr = $this->getConnectionRef( DB_REPLICA );
348  $visitingWatchers = (int)$dbr->selectField(
349  'watchlist',
350  'COUNT(*)',
351  [
352  'wl_namespace' => $target->getNamespace(),
353  'wl_title' => $target->getDBkey(),
354  'wl_notificationtimestamp >= ' .
355  $dbr->addQuotes( $dbr->timestamp( $threshold ) ) .
356  ' OR wl_notificationtimestamp IS NULL'
357  ],
358  __METHOD__
359  );
360 
361  return $visitingWatchers;
362  }
363 
370  public function countWatchersMultiple( array $targets, array $options = [] ) {
371  $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ];
372 
373  $dbr = $this->getConnectionRef( DB_REPLICA );
374 
375  if ( array_key_exists( 'minimumWatchers', $options ) ) {
376  $dbOptions['HAVING'] = 'COUNT(*) >= ' . (int)$options['minimumWatchers'];
377  }
378 
379  $lb = new LinkBatch( $targets );
380  $res = $dbr->select(
381  'watchlist',
382  [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ],
383  [ $lb->constructSet( 'wl', $dbr ) ],
384  __METHOD__,
385  $dbOptions
386  );
387 
388  $watchCounts = [];
389  foreach ( $targets as $linkTarget ) {
390  $watchCounts[$linkTarget->getNamespace()][$linkTarget->getDBkey()] = 0;
391  }
392 
393  foreach ( $res as $row ) {
394  $watchCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
395  }
396 
397  return $watchCounts;
398  }
399 
407  array $targetsWithVisitThresholds,
408  $minimumWatchers = null
409  ) {
410  $dbr = $this->getConnectionRef( DB_REPLICA );
411 
412  $conds = $this->getVisitingWatchersCondition( $dbr, $targetsWithVisitThresholds );
413 
414  $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ];
415  if ( $minimumWatchers !== null ) {
416  $dbOptions['HAVING'] = 'COUNT(*) >= ' . (int)$minimumWatchers;
417  }
418  $res = $dbr->select(
419  'watchlist',
420  [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
421  $conds,
422  __METHOD__,
423  $dbOptions
424  );
425 
426  $watcherCounts = [];
427  foreach ( $targetsWithVisitThresholds as list( $target ) ) {
428  /* @var LinkTarget $target */
429  $watcherCounts[$target->getNamespace()][$target->getDBkey()] = 0;
430  }
431 
432  foreach ( $res as $row ) {
433  $watcherCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
434  }
435 
436  return $watcherCounts;
437  }
438 
447  IDatabase $db,
448  array $targetsWithVisitThresholds
449  ) {
450  $missingTargets = [];
451  $namespaceConds = [];
452  foreach ( $targetsWithVisitThresholds as list( $target, $threshold ) ) {
453  if ( $threshold === null ) {
454  $missingTargets[] = $target;
455  continue;
456  }
457  /* @var LinkTarget $target */
458  $namespaceConds[$target->getNamespace()][] = $db->makeList( [
459  'wl_title = ' . $db->addQuotes( $target->getDBkey() ),
460  $db->makeList( [
461  'wl_notificationtimestamp >= ' . $db->addQuotes( $db->timestamp( $threshold ) ),
462  'wl_notificationtimestamp IS NULL'
463  ], LIST_OR )
464  ], LIST_AND );
465  }
466 
467  $conds = [];
468  foreach ( $namespaceConds as $namespace => $pageConds ) {
469  $conds[] = $db->makeList( [
470  'wl_namespace = ' . $namespace,
471  '(' . $db->makeList( $pageConds, LIST_OR ) . ')'
472  ], LIST_AND );
473  }
474 
475  if ( $missingTargets ) {
476  $lb = new LinkBatch( $missingTargets );
477  $conds[] = $lb->constructSet( 'wl', $db );
478  }
479 
480  return $db->makeList( $conds, LIST_OR );
481  }
482 
489  public function getWatchedItem( User $user, LinkTarget $target ) {
490  if ( $user->isAnon() ) {
491  return false;
492  }
493 
494  $cached = $this->getCached( $user, $target );
495  if ( $cached ) {
496  $this->stats->increment( 'WatchedItemStore.getWatchedItem.cached' );
497  return $cached;
498  }
499  $this->stats->increment( 'WatchedItemStore.getWatchedItem.load' );
500  return $this->loadWatchedItem( $user, $target );
501  }
502 
509  public function loadWatchedItem( User $user, LinkTarget $target ) {
510  // Only loggedin user can have a watchlist
511  if ( $user->isAnon() ) {
512  return false;
513  }
514 
515  $dbr = $this->getConnectionRef( DB_REPLICA );
516  $row = $dbr->selectRow(
517  'watchlist',
518  'wl_notificationtimestamp',
519  $this->dbCond( $user, $target ),
520  __METHOD__
521  );
522 
523  if ( !$row ) {
524  return false;
525  }
526 
527  $item = new WatchedItem(
528  $user,
529  $target,
530  wfTimestampOrNull( TS_MW, $row->wl_notificationtimestamp )
531  );
532  $this->cache( $item );
533 
534  return $item;
535  }
536 
543  public function getWatchedItemsForUser( User $user, array $options = [] ) {
544  $options += [ 'forWrite' => false ];
545 
546  $dbOptions = [];
547  if ( array_key_exists( 'sort', $options ) ) {
548  Assert::parameter(
549  ( in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ) ),
550  '$options[\'sort\']',
551  'must be SORT_ASC or SORT_DESC'
552  );
553  $dbOptions['ORDER BY'] = [
554  "wl_namespace {$options['sort']}",
555  "wl_title {$options['sort']}"
556  ];
557  }
558  $db = $this->getConnectionRef( $options['forWrite'] ? DB_MASTER : DB_REPLICA );
559 
560  $res = $db->select(
561  'watchlist',
562  [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
563  [ 'wl_user' => $user->getId() ],
564  __METHOD__,
565  $dbOptions
566  );
567 
568  $watchedItems = [];
569  foreach ( $res as $row ) {
570  // @todo: Should we add these to the process cache?
571  $watchedItems[] = new WatchedItem(
572  $user,
573  new TitleValue( (int)$row->wl_namespace, $row->wl_title ),
574  $row->wl_notificationtimestamp
575  );
576  }
577 
578  return $watchedItems;
579  }
580 
587  public function isWatched( User $user, LinkTarget $target ) {
588  return (bool)$this->getWatchedItem( $user, $target );
589  }
590 
597  public function getNotificationTimestampsBatch( User $user, array $targets ) {
598  $timestamps = [];
599  foreach ( $targets as $target ) {
600  $timestamps[$target->getNamespace()][$target->getDBkey()] = false;
601  }
602 
603  if ( $user->isAnon() ) {
604  return $timestamps;
605  }
606 
607  $targetsToLoad = [];
608  foreach ( $targets as $target ) {
609  $cachedItem = $this->getCached( $user, $target );
610  if ( $cachedItem ) {
611  $timestamps[$target->getNamespace()][$target->getDBkey()] =
612  $cachedItem->getNotificationTimestamp();
613  } else {
614  $targetsToLoad[] = $target;
615  }
616  }
617 
618  if ( !$targetsToLoad ) {
619  return $timestamps;
620  }
621 
622  $dbr = $this->getConnectionRef( DB_REPLICA );
623 
624  $lb = new LinkBatch( $targetsToLoad );
625  $res = $dbr->select(
626  'watchlist',
627  [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
628  [
629  $lb->constructSet( 'wl', $dbr ),
630  'wl_user' => $user->getId(),
631  ],
632  __METHOD__
633  );
634 
635  foreach ( $res as $row ) {
636  $timestamps[$row->wl_namespace][$row->wl_title] =
637  wfTimestampOrNull( TS_MW, $row->wl_notificationtimestamp );
638  }
639 
640  return $timestamps;
641  }
642 
648  public function addWatch( User $user, LinkTarget $target ) {
649  $this->addWatchBatchForUser( $user, [ $target ] );
650  }
651 
658  public function addWatchBatchForUser( User $user, array $targets ) {
659  if ( $this->readOnlyMode->isReadOnly() ) {
660  return false;
661  }
662  // Only loggedin user can have a watchlist
663  if ( $user->isAnon() ) {
664  return false;
665  }
666 
667  if ( !$targets ) {
668  return true;
669  }
670 
671  $rows = [];
672  $items = [];
673  foreach ( $targets as $target ) {
674  $rows[] = [
675  'wl_user' => $user->getId(),
676  'wl_namespace' => $target->getNamespace(),
677  'wl_title' => $target->getDBkey(),
678  'wl_notificationtimestamp' => null,
679  ];
680  $items[] = new WatchedItem(
681  $user,
682  $target,
683  null
684  );
685  $this->uncache( $user, $target );
686  }
687 
688  $dbw = $this->getConnectionRef( DB_MASTER );
689  foreach ( array_chunk( $rows, 100 ) as $toInsert ) {
690  // Use INSERT IGNORE to avoid overwriting the notification timestamp
691  // if there's already an entry for this page
692  $dbw->insert( 'watchlist', $toInsert, __METHOD__, 'IGNORE' );
693  }
694  // Update process cache to ensure skin doesn't claim that the current
695  // page is unwatched in the response of action=watch itself (T28292).
696  // This would otherwise be re-queried from a replica by isWatched().
697  foreach ( $items as $item ) {
698  $this->cache( $item );
699  }
700 
701  return true;
702  }
703 
710  public function removeWatch( User $user, LinkTarget $target ) {
711  // Only logged in user can have a watchlist
712  if ( $this->readOnlyMode->isReadOnly() || $user->isAnon() ) {
713  return false;
714  }
715 
716  $this->uncache( $user, $target );
717 
718  $dbw = $this->getConnectionRef( DB_MASTER );
719  $dbw->delete( 'watchlist',
720  [
721  'wl_user' => $user->getId(),
722  'wl_namespace' => $target->getNamespace(),
723  'wl_title' => $target->getDBkey(),
724  ], __METHOD__
725  );
726  $success = (bool)$dbw->affectedRows();
727 
728  return $success;
729  }
730 
738  public function setNotificationTimestampsForUser( User $user, $timestamp, array $targets = [] ) {
739  // Only loggedin user can have a watchlist
740  if ( $user->isAnon() ) {
741  return false;
742  }
743 
744  $dbw = $this->getConnectionRef( DB_MASTER );
745 
746  $conds = [ 'wl_user' => $user->getId() ];
747  if ( $targets ) {
748  $batch = new LinkBatch( $targets );
749  $conds[] = $batch->constructSet( 'wl', $dbw );
750  }
751 
752  if ( $timestamp !== null ) {
753  $timestamp = $dbw->timestamp( $timestamp );
754  }
755 
756  $success = $dbw->update(
757  'watchlist',
758  [ 'wl_notificationtimestamp' => $timestamp ],
759  $conds,
760  __METHOD__
761  );
762 
763  $this->uncacheUser( $user );
764 
765  return $success;
766  }
767 
769  // Only loggedin user can have a watchlist
770  if ( $user->isAnon() ) {
771  return;
772  }
773 
774  // If the page is watched by the user (or may be watched), update the timestamp
776  $user->getUserPage(),
777  [ 'userId' => $user->getId(), 'casTime' => time() ]
778  );
779 
780  // Try to run this post-send
781  // Calls DeferredUpdates::addCallableUpdate in normal operation
782  call_user_func(
783  $this->deferredUpdatesAddCallableUpdateCallback,
784  function () use ( $job ) {
785  $job->run();
786  }
787  );
788  }
789 
797  public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) {
798  $dbw = $this->getConnectionRef( DB_MASTER );
799  $uids = $dbw->selectFieldValues(
800  'watchlist',
801  'wl_user',
802  [
803  'wl_user != ' . intval( $editor->getId() ),
804  'wl_namespace' => $target->getNamespace(),
805  'wl_title' => $target->getDBkey(),
806  'wl_notificationtimestamp IS NULL',
807  ],
808  __METHOD__
809  );
810 
811  $watchers = array_map( 'intval', $uids );
812  if ( $watchers ) {
813  // Update wl_notificationtimestamp for all watching users except the editor
814  $fname = __METHOD__;
816  function () use ( $timestamp, $watchers, $target, $fname ) {
818 
819  $dbw = $this->getConnectionRef( DB_MASTER );
820  $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
821  $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
822 
823  $watchersChunks = array_chunk( $watchers, $wgUpdateRowsPerQuery );
824  foreach ( $watchersChunks as $watchersChunk ) {
825  $dbw->update( 'watchlist',
826  [ /* SET */
827  'wl_notificationtimestamp' => $dbw->timestamp( $timestamp )
828  ], [ /* WHERE - TODO Use wl_id T130067 */
829  'wl_user' => $watchersChunk,
830  'wl_namespace' => $target->getNamespace(),
831  'wl_title' => $target->getDBkey(),
832  ], $fname
833  );
834  if ( count( $watchersChunks ) > 1 ) {
835  $factory->commitAndWaitForReplication(
836  __METHOD__, $ticket, [ 'domain' => $dbw->getDomainID() ]
837  );
838  }
839  }
840  $this->uncacheLinkTarget( $target );
841  },
843  $dbw
844  );
845  }
846 
847  return $watchers;
848  }
849 
858  public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 ) {
859  // Only loggedin user can have a watchlist
860  if ( $this->readOnlyMode->isReadOnly() || $user->isAnon() ) {
861  return false;
862  }
863 
864  $item = null;
865  if ( $force != 'force' ) {
866  $item = $this->loadWatchedItem( $user, $title );
867  if ( !$item || $item->getNotificationTimestamp() === null ) {
868  return false;
869  }
870  }
871 
872  // If the page is watched by the user (or may be watched), update the timestamp
873  $job = new ActivityUpdateJob(
874  $title,
875  [
876  'type' => 'updateWatchlistNotification',
877  'userid' => $user->getId(),
878  'notifTime' => $this->getNotificationTimestamp( $user, $title, $item, $force, $oldid ),
879  'curTime' => time()
880  ]
881  );
882 
883  // Try to run this post-send
884  // Calls DeferredUpdates::addCallableUpdate in normal operation
885  call_user_func(
886  $this->deferredUpdatesAddCallableUpdateCallback,
887  function () use ( $job ) {
888  $job->run();
889  }
890  );
891 
892  $this->uncache( $user, $title );
893 
894  return true;
895  }
896 
897  private function getNotificationTimestamp( User $user, Title $title, $item, $force, $oldid ) {
898  if ( !$oldid ) {
899  // No oldid given, assuming latest revision; clear the timestamp.
900  return null;
901  }
902 
903  if ( !$title->getNextRevisionID( $oldid ) ) {
904  // Oldid given and is the latest revision for this title; clear the timestamp.
905  return null;
906  }
907 
908  if ( $item === null ) {
909  $item = $this->loadWatchedItem( $user, $title );
910  }
911 
912  if ( !$item ) {
913  // This can only happen if $force is enabled.
914  return null;
915  }
916 
917  // Oldid given and isn't the latest; update the timestamp.
918  // This will result in no further notification emails being sent!
919  // Calls Revision::getTimestampFromId in normal operation
920  $notificationTimestamp = call_user_func(
921  $this->revisionGetTimestampFromIdCallback,
922  $title,
923  $oldid
924  );
925 
926  // We need to go one second to the future because of various strict comparisons
927  // throughout the codebase
928  $ts = new MWTimestamp( $notificationTimestamp );
929  $ts->timestamp->add( new DateInterval( 'PT1S' ) );
930  $notificationTimestamp = $ts->getTimestamp( TS_MW );
931 
932  if ( $notificationTimestamp < $item->getNotificationTimestamp() ) {
933  if ( $force != 'force' ) {
934  return false;
935  } else {
936  // This is a little silly…
937  return $item->getNotificationTimestamp();
938  }
939  }
940 
941  return $notificationTimestamp;
942  }
943 
950  public function countUnreadNotifications( User $user, $unreadLimit = null ) {
951  $queryOptions = [];
952  if ( $unreadLimit !== null ) {
953  $unreadLimit = (int)$unreadLimit;
954  $queryOptions['LIMIT'] = $unreadLimit;
955  }
956 
957  $dbr = $this->getConnectionRef( DB_REPLICA );
958  $rowCount = $dbr->selectRowCount(
959  'watchlist',
960  '1',
961  [
962  'wl_user' => $user->getId(),
963  'wl_notificationtimestamp IS NOT NULL',
964  ],
965  __METHOD__,
966  $queryOptions
967  );
968 
969  if ( !isset( $unreadLimit ) ) {
970  return $rowCount;
971  }
972 
973  if ( $rowCount >= $unreadLimit ) {
974  return true;
975  }
976 
977  return $rowCount;
978  }
979 
985  public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget ) {
986  $oldTarget = Title::newFromLinkTarget( $oldTarget );
987  $newTarget = Title::newFromLinkTarget( $newTarget );
988 
989  $this->duplicateEntry( $oldTarget->getSubjectPage(), $newTarget->getSubjectPage() );
990  $this->duplicateEntry( $oldTarget->getTalkPage(), $newTarget->getTalkPage() );
991  }
992 
998  public function duplicateEntry( LinkTarget $oldTarget, LinkTarget $newTarget ) {
999  $dbw = $this->getConnectionRef( DB_MASTER );
1000 
1001  $result = $dbw->select(
1002  'watchlist',
1003  [ 'wl_user', 'wl_notificationtimestamp' ],
1004  [
1005  'wl_namespace' => $oldTarget->getNamespace(),
1006  'wl_title' => $oldTarget->getDBkey(),
1007  ],
1008  __METHOD__,
1009  [ 'FOR UPDATE' ]
1010  );
1011 
1012  $newNamespace = $newTarget->getNamespace();
1013  $newDBkey = $newTarget->getDBkey();
1014 
1015  # Construct array to replace into the watchlist
1016  $values = [];
1017  foreach ( $result as $row ) {
1018  $values[] = [
1019  'wl_user' => $row->wl_user,
1020  'wl_namespace' => $newNamespace,
1021  'wl_title' => $newDBkey,
1022  'wl_notificationtimestamp' => $row->wl_notificationtimestamp,
1023  ];
1024  }
1025 
1026  if ( !empty( $values ) ) {
1027  # Perform replace
1028  # Note that multi-row replace is very efficient for MySQL but may be inefficient for
1029  # some other DBMSes, mostly due to poor simulation by us
1030  $dbw->replace(
1031  'watchlist',
1032  [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
1033  $values,
1034  __METHOD__
1035  );
1036  }
1037  }
1038 
1039 }
WatchedItemStore\getNotificationTimestampsBatch
getNotificationTimestampsBatch(User $user, array $targets)
Definition: WatchedItemStore.php:597
MWTimestamp
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:32
WatchedItemStore\countUnreadNotifications
countUnreadNotifications(User $user, $unreadLimit=null)
Definition: WatchedItemStore.php:950
$user
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a account $user
Definition: hooks.txt:244
WatchedItemStore\overrideRevisionGetTimestampFromIdCallback
overrideRevisionGetTimestampFromIdCallback(callable $callback)
Overrides the Revision::getTimestampFromId callback This is intended for use while testing and will f...
Definition: WatchedItemStore.php:128
ActivityUpdateJob
Job for updating user activity like "last viewed" timestamps.
Definition: ActivityUpdateJob.php:34
WatchedItemStore\addWatch
addWatch(User $user, LinkTarget $target)
Definition: WatchedItemStore.php:648
false
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
HashBagOStuff
Simple store for keeping values in an associative array for the current process.
Definition: HashBagOStuff.php:31
LinkBatch
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition: LinkBatch.php:34
Wikimedia\Rdbms\IDatabase\makeList
makeList( $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
captcha-old.count
count
Definition: captcha-old.py:249
WatchedItemStore\getVisitingWatchersCondition
getVisitingWatchersCondition(IDatabase $db, array $targetsWithVisitThresholds)
Generates condition for the query used in a batch count visiting watchers.
Definition: WatchedItemStore.php:446
WatchedItemStore\removeWatch
removeWatch(User $user, LinkTarget $target)
Definition: WatchedItemStore.php:710
WatchedItemStore\getWatchedItem
getWatchedItem(User $user, LinkTarget $target)
Definition: WatchedItemStore.php:489
$result
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED! Use $magicWords in a file listed in $wgExtensionMessagesFiles instead. Use this to define synonyms of magic words depending of the language & $magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED! Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead. Use to define aliases of special pages names depending of the language & $specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
Definition: hooks.txt:1985
ReadOnlyMode
A service class for fetching the wiki's current read-only mode.
Definition: ReadOnlyMode.php:11
use
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:10
WatchedItemStore\countVisitingWatchers
countVisitingWatchers(LinkTarget $target, $threshold)
Definition: WatchedItemStore.php:346
NullStatsdDataFactory
Definition: NullStatsdDataFactory.php:10
ClearWatchlistNotificationsJob
Job for clearing all of the "last viewed" timestamps for a user's watchlist.
Definition: ClearWatchlistNotificationsJob.php:34
WatchedItemStore\duplicateAllAssociatedEntries
duplicateAllAssociatedEntries(LinkTarget $oldTarget, LinkTarget $newTarget)
Definition: WatchedItemStore.php:985
WatchedItemStore\uncacheUser
uncacheUser(User $user)
Definition: WatchedItemStore.php:175
WatchedItemStore\resetAllNotificationTimestampsForUser
resetAllNotificationTimestampsForUser(User $user)
Reset all watchlist notificaton timestamps for a user using the job queue.
Definition: WatchedItemStore.php:768
$res
$res
Definition: database.txt:21
WatchedItemStore\$loadBalancer
LoadBalancer $loadBalancer
Definition: WatchedItemStore.php:24
$success
$success
Definition: NoLocalSettings.php:42
WatchedItemStore\$cache
HashBagOStuff $cache
Definition: WatchedItemStore.php:34
php
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
LIST_AND
const LIST_AND
Definition: Defines.php:44
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
$dbr
$dbr
Definition: testCompression.php:50
WatchedItemStore\updateNotificationTimestamp
updateNotificationTimestamp(User $editor, LinkTarget $target, $timestamp)
Definition: WatchedItemStore.php:797
WatchedItemStore\resetNotificationTimestamp
resetNotificationTimestamp(User $user, Title $title, $force='', $oldid=0)
Definition: WatchedItemStore.php:858
MediaWiki\Linker\LinkTarget\getNamespace
getNamespace()
Get the namespace index.
WatchedItemStore\$readOnlyMode
ReadOnlyMode $readOnlyMode
Definition: WatchedItemStore.php:29
Wikimedia\Rdbms\IDatabase\timestamp
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
LIST_OR
const LIST_OR
Definition: Defines.php:47
WatchedItemStore\overrideDeferredUpdatesAddCallableUpdateCallback
overrideDeferredUpdatesAddCallableUpdateCallback(callable $callback)
Overrides the DeferredUpdates::addCallableUpdate callback This is intended for use while testing and ...
Definition: WatchedItemStore.php:105
MWException
MediaWiki exception.
Definition: MWException.php:26
$title
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:934
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget)
Create a new Title from a LinkTarget.
Definition: Title.php:244
WatchedItemStore\clearUserWatchedItems
clearUserWatchedItems(User $user)
Deletes ALL watched items for the given user when under $updateRowsPerQuery entries exist.
Definition: WatchedItemStore.php:234
wfTimestampOrNull
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
Definition: GlobalFunctions.php:1984
WatchedItemStore\duplicateEntry
duplicateEntry(LinkTarget $oldTarget, LinkTarget $newTarget)
Definition: WatchedItemStore.php:998
DeferredUpdates\POSTSEND
const POSTSEND
Definition: DeferredUpdates.php:61
$wgUpdateRowsPerQuery
$wgUpdateRowsPerQuery
Number of rows to update per query.
Definition: DefaultSettings.php:8381
global
when a variable name is used in a it is silently declared as a new masking the global
Definition: design.txt:93
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
WatchedItemStore\addWatchBatchForUser
addWatchBatchForUser(User $user, array $targets)
Definition: WatchedItemStore.php:658
WatchedItemStore\getMaxId
getMaxId()
Definition: WatchedItemStore.php:291
DB_MASTER
const DB_MASTER
Definition: defines.php:26
$editor
passed in as a query string parameter to the various URLs constructed here(i.e. $prevlink) $ldel you ll need to handle error etc yourself modifying $error and returning true will cause the contents of $error to be echoed at the top of the edit form as wikitext Return true without altering $error to allow the edit to proceed & $editor
Definition: hooks.txt:1275
WatchedItemStore\uncacheLinkTarget
uncacheLinkTarget(LinkTarget $target)
Definition: WatchedItemStore.php:164
string
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition: hooks.txt:175
list
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
WatchedItem\getUser
getUser()
Definition: WatchedItem.php:66
WatchedItemStore\getConnectionRef
getConnectionRef( $dbIndex)
Definition: WatchedItemStore.php:220
WatchedItemStore\$deferredUpdatesAddCallableUpdateCallback
callable null $deferredUpdatesAddCallableUpdateCallback
Definition: WatchedItemStore.php:47
Wikimedia\Rdbms\LoadBalancer
Database connection, tracking, load balancing, and transaction manager for a cluster.
Definition: LoadBalancer.php:40
$fname
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition: Setup.php:112
WatchedItemStore\uncache
uncache(User $user, LinkTarget $target)
Definition: WatchedItemStore.php:158
WatchedItemStore\dbCond
dbCond(User $user, LinkTarget $target)
Return an array of conditions to select or update the appropriate database row.
Definition: WatchedItemStore.php:206
WatchedItemStore\__construct
__construct(LoadBalancer $loadBalancer, HashBagOStuff $cache, ReadOnlyMode $readOnlyMode, $updateRowsPerQuery)
Definition: WatchedItemStore.php:70
StatsdAwareInterface
Describes a Statsd aware interface.
Definition: StatsdAwareInterface.php:11
WatchedItemStore\uncacheAllItemsForUser
uncacheAllItemsForUser(User $user)
Definition: WatchedItemStore.php:250
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:42
WatchedItem
Representation of a pair of user and title for watchlist entries.
Definition: WatchedItem.php:32
MediaWiki\Linker\LinkTarget\getDBkey
getDBkey()
Get the main part with underscores.
WatchedItemStore\setNotificationTimestampsForUser
setNotificationTimestampsForUser(User $user, $timestamp, array $targets=[])
Definition: WatchedItemStore.php:738
WatchedItemStore
Storage layer class for WatchedItems.
Definition: WatchedItemStore.php:19
WatchedItemStore\countWatchedItems
countWatchedItems(User $user)
Definition: WatchedItemStore.php:306
Title
Represents a title within MediaWiki.
Definition: Title.php:39
WatchedItemStore\getWatchedItemsForUser
getWatchedItemsForUser(User $user, array $options=[])
Definition: WatchedItemStore.php:543
$rows
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction $rows
Definition: hooks.txt:2604
$options
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1987
Wikimedia\Rdbms\IDatabase\addQuotes
addQuotes( $s)
Adds quotes and backslashes.
WatchedItemStore\$stats
StatsdDataFactoryInterface $stats
Definition: WatchedItemStore.php:62
WatchedItemStore\countWatchers
countWatchers(LinkTarget $target)
Definition: WatchedItemStore.php:325
WatchedItemStore\loadWatchedItem
loadWatchedItem(User $user, LinkTarget $target)
Definition: WatchedItemStore.php:509
$job
if(count( $args)< 1) $job
Definition: recompressTracked.php:47
JobQueueGroup\singleton
static singleton( $wiki=false)
Definition: JobQueueGroup.php:72
as
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
WatchedItemStore\getCached
getCached(User $user, LinkTarget $target)
Definition: WatchedItemStore.php:193
WatchedItem\getLinkTarget
getLinkTarget()
Definition: WatchedItem.php:73
WatchedItemStore\countVisitingWatchersMultiple
countVisitingWatchersMultiple(array $targetsWithVisitThresholds, $minimumWatchers=null)
Definition: WatchedItemStore.php:406
WatchedItemStore\setStatsdDataFactory
setStatsdDataFactory(StatsdDataFactoryInterface $stats)
Definition: WatchedItemStore.php:90
ClearUserWatchlistJob\newForUser
static newForUser(User $user, $maxWatchlistId)
Definition: ClearUserWatchlistJob.php:21
$batch
$batch
Definition: linkcache.txt:23
WatchedItemStore\clearUserWatchedItemsUsingJobQueue
clearUserWatchedItemsUsingJobQueue(User $user)
Queues a job that will clear the users watchlist using the Job Queue.
Definition: WatchedItemStore.php:281
class
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
MediaWikiServices
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
WatchedItemStore\isWatched
isWatched(User $user, LinkTarget $target)
Definition: WatchedItemStore.php:587
WatchedItemStore\$revisionGetTimestampFromIdCallback
callable null $revisionGetTimestampFromIdCallback
Definition: WatchedItemStore.php:52
WatchedItemStore\getNotificationTimestamp
getNotificationTimestamp(User $user, Title $title, $item, $force, $oldid)
Definition: WatchedItemStore.php:897
WatchedItemStoreInterface
Definition: WatchedItemStoreInterface.php:27
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:53
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:111
WatchedItemStore\getCacheKey
getCacheKey(User $user, LinkTarget $target)
Definition: WatchedItemStore.php:141
WatchedItemStore\$updateRowsPerQuery
int $updateRowsPerQuery
Definition: WatchedItemStore.php:57
WatchedItemStore\countWatchersMultiple
countWatchersMultiple(array $targets, array $options=[])
Definition: WatchedItemStore.php:370
array
the array() calling protocol came about after MediaWiki 1.4rc1.
WatchedItemStore\cache
cache(WatchedItem $item)
Definition: WatchedItemStore.php:149
TitleValue
Represents a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:35