1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045 |
- <?php
- use Wikimedia\Rdbms\IDatabase;
- use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
- use MediaWiki\Linker\LinkTarget;
- use MediaWiki\MediaWikiServices;
- use Wikimedia\Assert\Assert;
- use Wikimedia\ScopedCallback;
- use Wikimedia\Rdbms\LoadBalancer;
- /**
- * Storage layer class for WatchedItems.
- * Database interaction & caching
- * TODO caching should be factored out into a CachingWatchedItemStore class
- *
- * @author Addshore
- * @since 1.27
- */
- class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterface {
- /**
- * @var LoadBalancer
- */
- private $loadBalancer;
- /**
- * @var ReadOnlyMode
- */
- private $readOnlyMode;
- /**
- * @var HashBagOStuff
- */
- private $cache;
- /**
- * @var array[] Looks like $cacheIndex[Namespace ID][Target DB Key][User Id] => 'key'
- * The index is needed so that on mass changes all relevant items can be un-cached.
- * For example: Clearing a users watchlist of all items or updating notification timestamps
- * for all users watching a single target.
- */
- private $cacheIndex = [];
- /**
- * @var callable|null
- */
- private $deferredUpdatesAddCallableUpdateCallback;
- /**
- * @var callable|null
- */
- private $revisionGetTimestampFromIdCallback;
- /**
- * @var int
- */
- private $updateRowsPerQuery;
- /**
- * @var StatsdDataFactoryInterface
- */
- private $stats;
- /**
- * @param LoadBalancer $loadBalancer
- * @param HashBagOStuff $cache
- * @param ReadOnlyMode $readOnlyMode
- * @param int $updateRowsPerQuery
- */
- public function __construct(
- LoadBalancer $loadBalancer,
- HashBagOStuff $cache,
- ReadOnlyMode $readOnlyMode,
- $updateRowsPerQuery
- ) {
- $this->loadBalancer = $loadBalancer;
- $this->cache = $cache;
- $this->readOnlyMode = $readOnlyMode;
- $this->stats = new NullStatsdDataFactory();
- $this->deferredUpdatesAddCallableUpdateCallback =
- [ DeferredUpdates::class, 'addCallableUpdate' ];
- $this->revisionGetTimestampFromIdCallback =
- [ Revision::class, 'getTimestampFromId' ];
- $this->updateRowsPerQuery = $updateRowsPerQuery;
- }
- /**
- * @param StatsdDataFactoryInterface $stats
- */
- public function setStatsdDataFactory( StatsdDataFactoryInterface $stats ) {
- $this->stats = $stats;
- }
- /**
- * Overrides the DeferredUpdates::addCallableUpdate callback
- * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
- *
- * @param callable $callback
- *
- * @see DeferredUpdates::addCallableUpdate for callback signiture
- *
- * @return ScopedCallback to reset the overridden value
- * @throws MWException
- */
- public function overrideDeferredUpdatesAddCallableUpdateCallback( callable $callback ) {
- if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
- throw new MWException(
- 'Cannot override DeferredUpdates::addCallableUpdate callback in operation.'
- );
- }
- $previousValue = $this->deferredUpdatesAddCallableUpdateCallback;
- $this->deferredUpdatesAddCallableUpdateCallback = $callback;
- return new ScopedCallback( function () use ( $previousValue ) {
- $this->deferredUpdatesAddCallableUpdateCallback = $previousValue;
- } );
- }
- /**
- * Overrides the Revision::getTimestampFromId callback
- * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
- *
- * @param callable $callback
- * @see Revision::getTimestampFromId for callback signiture
- *
- * @return ScopedCallback to reset the overridden value
- * @throws MWException
- */
- public function overrideRevisionGetTimestampFromIdCallback( callable $callback ) {
- if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
- throw new MWException(
- 'Cannot override Revision::getTimestampFromId callback in operation.'
- );
- }
- $previousValue = $this->revisionGetTimestampFromIdCallback;
- $this->revisionGetTimestampFromIdCallback = $callback;
- return new ScopedCallback( function () use ( $previousValue ) {
- $this->revisionGetTimestampFromIdCallback = $previousValue;
- } );
- }
- private function getCacheKey( User $user, LinkTarget $target ) {
- return $this->cache->makeKey(
- (string)$target->getNamespace(),
- $target->getDBkey(),
- (string)$user->getId()
- );
- }
- private function cache( WatchedItem $item ) {
- $user = $item->getUser();
- $target = $item->getLinkTarget();
- $key = $this->getCacheKey( $user, $target );
- $this->cache->set( $key, $item );
- $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] = $key;
- $this->stats->increment( 'WatchedItemStore.cache' );
- }
- private function uncache( User $user, LinkTarget $target ) {
- $this->cache->delete( $this->getCacheKey( $user, $target ) );
- unset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()][$user->getId()] );
- $this->stats->increment( 'WatchedItemStore.uncache' );
- }
- private function uncacheLinkTarget( LinkTarget $target ) {
- $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget' );
- if ( !isset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] ) ) {
- return;
- }
- foreach ( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] as $key ) {
- $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget.items' );
- $this->cache->delete( $key );
- }
- }
- private function uncacheUser( User $user ) {
- $this->stats->increment( 'WatchedItemStore.uncacheUser' );
- foreach ( $this->cacheIndex as $ns => $dbKeyArray ) {
- foreach ( $dbKeyArray as $dbKey => $userArray ) {
- if ( isset( $userArray[$user->getId()] ) ) {
- $this->stats->increment( 'WatchedItemStore.uncacheUser.items' );
- $this->cache->delete( $userArray[$user->getId()] );
- }
- }
- }
- }
- /**
- * @param User $user
- * @param LinkTarget $target
- *
- * @return WatchedItem|false
- */
- private function getCached( User $user, LinkTarget $target ) {
- return $this->cache->get( $this->getCacheKey( $user, $target ) );
- }
- /**
- * Return an array of conditions to select or update the appropriate database
- * row.
- *
- * @param User $user
- * @param LinkTarget $target
- *
- * @return array
- */
- private function dbCond( User $user, LinkTarget $target ) {
- return [
- 'wl_user' => $user->getId(),
- 'wl_namespace' => $target->getNamespace(),
- 'wl_title' => $target->getDBkey(),
- ];
- }
- /**
- * @param int $dbIndex DB_MASTER or DB_REPLICA
- *
- * @return IDatabase
- * @throws MWException
- */
- private function getConnectionRef( $dbIndex ) {
- return $this->loadBalancer->getConnectionRef( $dbIndex, [ 'watchlist' ] );
- }
- /**
- * Deletes ALL watched items for the given user when under
- * $updateRowsPerQuery entries exist.
- *
- * @since 1.30
- *
- * @param User $user
- *
- * @return bool true on success, false when too many items are watched
- */
- public function clearUserWatchedItems( User $user ) {
- if ( $this->countWatchedItems( $user ) > $this->updateRowsPerQuery ) {
- return false;
- }
- $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
- $dbw->delete(
- 'watchlist',
- [ 'wl_user' => $user->getId() ],
- __METHOD__
- );
- $this->uncacheAllItemsForUser( $user );
- return true;
- }
- private function uncacheAllItemsForUser( User $user ) {
- $userId = $user->getId();
- foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
- foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
- if ( array_key_exists( $userId, $userIndex ) ) {
- $this->cache->delete( $userIndex[$userId] );
- unset( $this->cacheIndex[$ns][$dbKey][$userId] );
- }
- }
- }
- // Cleanup empty cache keys
- foreach ( $this->cacheIndex as $ns => $dbKeyIndex ) {
- foreach ( $dbKeyIndex as $dbKey => $userIndex ) {
- if ( empty( $this->cacheIndex[$ns][$dbKey] ) ) {
- unset( $this->cacheIndex[$ns][$dbKey] );
- }
- }
- if ( empty( $this->cacheIndex[$ns] ) ) {
- unset( $this->cacheIndex[$ns] );
- }
- }
- }
- /**
- * Queues a job that will clear the users watchlist using the Job Queue.
- *
- * @since 1.31
- *
- * @param User $user
- */
- public function clearUserWatchedItemsUsingJobQueue( User $user ) {
- $job = ClearUserWatchlistJob::newForUser( $user, $this->getMaxId() );
- // TODO inject me.
- JobQueueGroup::singleton()->push( $job );
- }
- /**
- * @since 1.31
- * @return int The maximum current wl_id
- */
- public function getMaxId() {
- $dbr = $this->getConnectionRef( DB_REPLICA );
- return (int)$dbr->selectField(
- 'watchlist',
- 'MAX(wl_id)',
- '',
- __METHOD__
- );
- }
- /**
- * @since 1.31
- * @param User $user
- * @return int
- */
- public function countWatchedItems( User $user ) {
- $dbr = $this->getConnectionRef( DB_REPLICA );
- $return = (int)$dbr->selectField(
- 'watchlist',
- 'COUNT(*)',
- [
- 'wl_user' => $user->getId()
- ],
- __METHOD__
- );
- return $return;
- }
- /**
- * @since 1.27
- * @param LinkTarget $target
- * @return int
- */
- public function countWatchers( LinkTarget $target ) {
- $dbr = $this->getConnectionRef( DB_REPLICA );
- $return = (int)$dbr->selectField(
- 'watchlist',
- 'COUNT(*)',
- [
- 'wl_namespace' => $target->getNamespace(),
- 'wl_title' => $target->getDBkey(),
- ],
- __METHOD__
- );
- return $return;
- }
- /**
- * @since 1.27
- * @param LinkTarget $target
- * @param string|int $threshold
- * @return int
- */
- public function countVisitingWatchers( LinkTarget $target, $threshold ) {
- $dbr = $this->getConnectionRef( DB_REPLICA );
- $visitingWatchers = (int)$dbr->selectField(
- 'watchlist',
- 'COUNT(*)',
- [
- 'wl_namespace' => $target->getNamespace(),
- 'wl_title' => $target->getDBkey(),
- 'wl_notificationtimestamp >= ' .
- $dbr->addQuotes( $dbr->timestamp( $threshold ) ) .
- ' OR wl_notificationtimestamp IS NULL'
- ],
- __METHOD__
- );
- return $visitingWatchers;
- }
- /**
- * @since 1.27
- * @param LinkTarget[] $targets
- * @param array $options
- * @return array
- */
- public function countWatchersMultiple( array $targets, array $options = [] ) {
- $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ];
- $dbr = $this->getConnectionRef( DB_REPLICA );
- if ( array_key_exists( 'minimumWatchers', $options ) ) {
- $dbOptions['HAVING'] = 'COUNT(*) >= ' . (int)$options['minimumWatchers'];
- }
- $lb = new LinkBatch( $targets );
- $res = $dbr->select(
- 'watchlist',
- [ 'wl_title', 'wl_namespace', 'watchers' => 'COUNT(*)' ],
- [ $lb->constructSet( 'wl', $dbr ) ],
- __METHOD__,
- $dbOptions
- );
- $watchCounts = [];
- foreach ( $targets as $linkTarget ) {
- $watchCounts[$linkTarget->getNamespace()][$linkTarget->getDBkey()] = 0;
- }
- foreach ( $res as $row ) {
- $watchCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
- }
- return $watchCounts;
- }
- /**
- * @since 1.27
- * @param array $targetsWithVisitThresholds
- * @param int|null $minimumWatchers
- * @return array
- */
- public function countVisitingWatchersMultiple(
- array $targetsWithVisitThresholds,
- $minimumWatchers = null
- ) {
- if ( $targetsWithVisitThresholds === [] ) {
- // No titles requested => no results returned
- return [];
- }
- $dbr = $this->getConnectionRef( DB_REPLICA );
- $conds = $this->getVisitingWatchersCondition( $dbr, $targetsWithVisitThresholds );
- $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ];
- if ( $minimumWatchers !== null ) {
- $dbOptions['HAVING'] = 'COUNT(*) >= ' . (int)$minimumWatchers;
- }
- $res = $dbr->select(
- 'watchlist',
- [ 'wl_namespace', 'wl_title', 'watchers' => 'COUNT(*)' ],
- $conds,
- __METHOD__,
- $dbOptions
- );
- $watcherCounts = [];
- foreach ( $targetsWithVisitThresholds as list( $target ) ) {
- /* @var LinkTarget $target */
- $watcherCounts[$target->getNamespace()][$target->getDBkey()] = 0;
- }
- foreach ( $res as $row ) {
- $watcherCounts[$row->wl_namespace][$row->wl_title] = (int)$row->watchers;
- }
- return $watcherCounts;
- }
- /**
- * Generates condition for the query used in a batch count visiting watchers.
- *
- * @param IDatabase $db
- * @param array $targetsWithVisitThresholds array of pairs (LinkTarget, last visit threshold)
- * @return string
- */
- private function getVisitingWatchersCondition(
- IDatabase $db,
- array $targetsWithVisitThresholds
- ) {
- $missingTargets = [];
- $namespaceConds = [];
- foreach ( $targetsWithVisitThresholds as list( $target, $threshold ) ) {
- if ( $threshold === null ) {
- $missingTargets[] = $target;
- continue;
- }
- /* @var LinkTarget $target */
- $namespaceConds[$target->getNamespace()][] = $db->makeList( [
- 'wl_title = ' . $db->addQuotes( $target->getDBkey() ),
- $db->makeList( [
- 'wl_notificationtimestamp >= ' . $db->addQuotes( $db->timestamp( $threshold ) ),
- 'wl_notificationtimestamp IS NULL'
- ], LIST_OR )
- ], LIST_AND );
- }
- $conds = [];
- foreach ( $namespaceConds as $namespace => $pageConds ) {
- $conds[] = $db->makeList( [
- 'wl_namespace = ' . $namespace,
- '(' . $db->makeList( $pageConds, LIST_OR ) . ')'
- ], LIST_AND );
- }
- if ( $missingTargets ) {
- $lb = new LinkBatch( $missingTargets );
- $conds[] = $lb->constructSet( 'wl', $db );
- }
- return $db->makeList( $conds, LIST_OR );
- }
- /**
- * @since 1.27
- * @param User $user
- * @param LinkTarget $target
- * @return bool
- */
- public function getWatchedItem( User $user, LinkTarget $target ) {
- if ( $user->isAnon() ) {
- return false;
- }
- $cached = $this->getCached( $user, $target );
- if ( $cached ) {
- $this->stats->increment( 'WatchedItemStore.getWatchedItem.cached' );
- return $cached;
- }
- $this->stats->increment( 'WatchedItemStore.getWatchedItem.load' );
- return $this->loadWatchedItem( $user, $target );
- }
- /**
- * @since 1.27
- * @param User $user
- * @param LinkTarget $target
- * @return WatchedItem|bool
- */
- public function loadWatchedItem( User $user, LinkTarget $target ) {
- // Only loggedin user can have a watchlist
- if ( $user->isAnon() ) {
- return false;
- }
- $dbr = $this->getConnectionRef( DB_REPLICA );
- $row = $dbr->selectRow(
- 'watchlist',
- 'wl_notificationtimestamp',
- $this->dbCond( $user, $target ),
- __METHOD__
- );
- if ( !$row ) {
- return false;
- }
- $item = new WatchedItem(
- $user,
- $target,
- wfTimestampOrNull( TS_MW, $row->wl_notificationtimestamp )
- );
- $this->cache( $item );
- return $item;
- }
- /**
- * @since 1.27
- * @param User $user
- * @param array $options
- * @return WatchedItem[]
- */
- public function getWatchedItemsForUser( User $user, array $options = [] ) {
- $options += [ 'forWrite' => false ];
- $dbOptions = [];
- if ( array_key_exists( 'sort', $options ) ) {
- Assert::parameter(
- ( in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ) ),
- '$options[\'sort\']',
- 'must be SORT_ASC or SORT_DESC'
- );
- $dbOptions['ORDER BY'] = [
- "wl_namespace {$options['sort']}",
- "wl_title {$options['sort']}"
- ];
- }
- $db = $this->getConnectionRef( $options['forWrite'] ? DB_MASTER : DB_REPLICA );
- $res = $db->select(
- 'watchlist',
- [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
- [ 'wl_user' => $user->getId() ],
- __METHOD__,
- $dbOptions
- );
- $watchedItems = [];
- foreach ( $res as $row ) {
- // @todo: Should we add these to the process cache?
- $watchedItems[] = new WatchedItem(
- $user,
- new TitleValue( (int)$row->wl_namespace, $row->wl_title ),
- $row->wl_notificationtimestamp
- );
- }
- return $watchedItems;
- }
- /**
- * @since 1.27
- * @param User $user
- * @param LinkTarget $target
- * @return bool
- */
- public function isWatched( User $user, LinkTarget $target ) {
- return (bool)$this->getWatchedItem( $user, $target );
- }
- /**
- * @since 1.27
- * @param User $user
- * @param LinkTarget[] $targets
- * @return array
- */
- public function getNotificationTimestampsBatch( User $user, array $targets ) {
- $timestamps = [];
- foreach ( $targets as $target ) {
- $timestamps[$target->getNamespace()][$target->getDBkey()] = false;
- }
- if ( $user->isAnon() ) {
- return $timestamps;
- }
- $targetsToLoad = [];
- foreach ( $targets as $target ) {
- $cachedItem = $this->getCached( $user, $target );
- if ( $cachedItem ) {
- $timestamps[$target->getNamespace()][$target->getDBkey()] =
- $cachedItem->getNotificationTimestamp();
- } else {
- $targetsToLoad[] = $target;
- }
- }
- if ( !$targetsToLoad ) {
- return $timestamps;
- }
- $dbr = $this->getConnectionRef( DB_REPLICA );
- $lb = new LinkBatch( $targetsToLoad );
- $res = $dbr->select(
- 'watchlist',
- [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
- [
- $lb->constructSet( 'wl', $dbr ),
- 'wl_user' => $user->getId(),
- ],
- __METHOD__
- );
- foreach ( $res as $row ) {
- $timestamps[$row->wl_namespace][$row->wl_title] =
- wfTimestampOrNull( TS_MW, $row->wl_notificationtimestamp );
- }
- return $timestamps;
- }
- /**
- * @since 1.27
- * @param User $user
- * @param LinkTarget $target
- */
- public function addWatch( User $user, LinkTarget $target ) {
- $this->addWatchBatchForUser( $user, [ $target ] );
- }
- /**
- * @since 1.27
- * @param User $user
- * @param LinkTarget[] $targets
- * @return bool
- */
- public function addWatchBatchForUser( User $user, array $targets ) {
- if ( $this->readOnlyMode->isReadOnly() ) {
- return false;
- }
- // Only loggedin user can have a watchlist
- if ( $user->isAnon() ) {
- return false;
- }
- if ( !$targets ) {
- return true;
- }
- $rows = [];
- $items = [];
- foreach ( $targets as $target ) {
- $rows[] = [
- 'wl_user' => $user->getId(),
- 'wl_namespace' => $target->getNamespace(),
- 'wl_title' => $target->getDBkey(),
- 'wl_notificationtimestamp' => null,
- ];
- $items[] = new WatchedItem(
- $user,
- $target,
- null
- );
- $this->uncache( $user, $target );
- }
- $dbw = $this->getConnectionRef( DB_MASTER );
- foreach ( array_chunk( $rows, 100 ) as $toInsert ) {
- // Use INSERT IGNORE to avoid overwriting the notification timestamp
- // if there's already an entry for this page
- $dbw->insert( 'watchlist', $toInsert, __METHOD__, 'IGNORE' );
- }
- // Update process cache to ensure skin doesn't claim that the current
- // page is unwatched in the response of action=watch itself (T28292).
- // This would otherwise be re-queried from a replica by isWatched().
- foreach ( $items as $item ) {
- $this->cache( $item );
- }
- return true;
- }
- /**
- * @since 1.27
- * @param User $user
- * @param LinkTarget $target
- * @return bool
- */
- public function removeWatch( User $user, LinkTarget $target ) {
- // Only logged in user can have a watchlist
- if ( $this->readOnlyMode->isReadOnly() || $user->isAnon() ) {
- return false;
- }
- $this->uncache( $user, $target );
- $dbw = $this->getConnectionRef( DB_MASTER );
- $dbw->delete( 'watchlist',
- [
- 'wl_user' => $user->getId(),
- 'wl_namespace' => $target->getNamespace(),
- 'wl_title' => $target->getDBkey(),
- ], __METHOD__
- );
- $success = (bool)$dbw->affectedRows();
- return $success;
- }
- /**
- * @since 1.27
- * @param User $user
- * @param string|int $timestamp
- * @param LinkTarget[] $targets
- * @return bool
- */
- public function setNotificationTimestampsForUser( User $user, $timestamp, array $targets = [] ) {
- // Only loggedin user can have a watchlist
- if ( $user->isAnon() ) {
- return false;
- }
- $dbw = $this->getConnectionRef( DB_MASTER );
- $conds = [ 'wl_user' => $user->getId() ];
- if ( $targets ) {
- $batch = new LinkBatch( $targets );
- $conds[] = $batch->constructSet( 'wl', $dbw );
- }
- if ( $timestamp !== null ) {
- $timestamp = $dbw->timestamp( $timestamp );
- }
- $success = $dbw->update(
- 'watchlist',
- [ 'wl_notificationtimestamp' => $timestamp ],
- $conds,
- __METHOD__
- );
- $this->uncacheUser( $user );
- return $success;
- }
- public function resetAllNotificationTimestampsForUser( User $user ) {
- // Only loggedin user can have a watchlist
- if ( $user->isAnon() ) {
- return;
- }
- // If the page is watched by the user (or may be watched), update the timestamp
- $job = new ClearWatchlistNotificationsJob(
- $user->getUserPage(),
- [ 'userId' => $user->getId(), 'casTime' => time() ]
- );
- // Try to run this post-send
- // Calls DeferredUpdates::addCallableUpdate in normal operation
- call_user_func(
- $this->deferredUpdatesAddCallableUpdateCallback,
- function () use ( $job ) {
- $job->run();
- }
- );
- }
- /**
- * @since 1.27
- * @param User $editor
- * @param LinkTarget $target
- * @param string|int $timestamp
- * @return int[]
- */
- public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) {
- $dbw = $this->getConnectionRef( DB_MASTER );
- $uids = $dbw->selectFieldValues(
- 'watchlist',
- 'wl_user',
- [
- 'wl_user != ' . intval( $editor->getId() ),
- 'wl_namespace' => $target->getNamespace(),
- 'wl_title' => $target->getDBkey(),
- 'wl_notificationtimestamp IS NULL',
- ],
- __METHOD__
- );
- $watchers = array_map( 'intval', $uids );
- if ( $watchers ) {
- // Update wl_notificationtimestamp for all watching users except the editor
- $fname = __METHOD__;
- DeferredUpdates::addCallableUpdate(
- function () use ( $timestamp, $watchers, $target, $fname ) {
- global $wgUpdateRowsPerQuery;
- $dbw = $this->getConnectionRef( DB_MASTER );
- $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
- $ticket = $factory->getEmptyTransactionTicket( __METHOD__ );
- $watchersChunks = array_chunk( $watchers, $wgUpdateRowsPerQuery );
- foreach ( $watchersChunks as $watchersChunk ) {
- $dbw->update( 'watchlist',
- [ /* SET */
- 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp )
- ], [ /* WHERE - TODO Use wl_id T130067 */
- 'wl_user' => $watchersChunk,
- 'wl_namespace' => $target->getNamespace(),
- 'wl_title' => $target->getDBkey(),
- ], $fname
- );
- if ( count( $watchersChunks ) > 1 ) {
- $factory->commitAndWaitForReplication(
- __METHOD__, $ticket, [ 'domain' => $dbw->getDomainID() ]
- );
- }
- }
- $this->uncacheLinkTarget( $target );
- },
- DeferredUpdates::POSTSEND,
- $dbw
- );
- }
- return $watchers;
- }
- /**
- * @since 1.27
- * @param User $user
- * @param Title $title
- * @param string $force
- * @param int $oldid
- * @return bool
- */
- public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 ) {
- // Only loggedin user can have a watchlist
- if ( $this->readOnlyMode->isReadOnly() || $user->isAnon() ) {
- return false;
- }
- $item = null;
- if ( $force != 'force' ) {
- $item = $this->loadWatchedItem( $user, $title );
- if ( !$item || $item->getNotificationTimestamp() === null ) {
- return false;
- }
- }
- // If the page is watched by the user (or may be watched), update the timestamp
- $job = new ActivityUpdateJob(
- $title,
- [
- 'type' => 'updateWatchlistNotification',
- 'userid' => $user->getId(),
- 'notifTime' => $this->getNotificationTimestamp( $user, $title, $item, $force, $oldid ),
- 'curTime' => time()
- ]
- );
- // Try to run this post-send
- // Calls DeferredUpdates::addCallableUpdate in normal operation
- call_user_func(
- $this->deferredUpdatesAddCallableUpdateCallback,
- function () use ( $job ) {
- $job->run();
- }
- );
- $this->uncache( $user, $title );
- return true;
- }
- private function getNotificationTimestamp( User $user, Title $title, $item, $force, $oldid ) {
- if ( !$oldid ) {
- // No oldid given, assuming latest revision; clear the timestamp.
- return null;
- }
- if ( !$title->getNextRevisionID( $oldid ) ) {
- // Oldid given and is the latest revision for this title; clear the timestamp.
- return null;
- }
- if ( $item === null ) {
- $item = $this->loadWatchedItem( $user, $title );
- }
- if ( !$item ) {
- // This can only happen if $force is enabled.
- return null;
- }
- // Oldid given and isn't the latest; update the timestamp.
- // This will result in no further notification emails being sent!
- // Calls Revision::getTimestampFromId in normal operation
- $notificationTimestamp = call_user_func(
- $this->revisionGetTimestampFromIdCallback,
- $title,
- $oldid
- );
- // We need to go one second to the future because of various strict comparisons
- // throughout the codebase
- $ts = new MWTimestamp( $notificationTimestamp );
- $ts->timestamp->add( new DateInterval( 'PT1S' ) );
- $notificationTimestamp = $ts->getTimestamp( TS_MW );
- if ( $notificationTimestamp < $item->getNotificationTimestamp() ) {
- if ( $force != 'force' ) {
- return false;
- } else {
- // This is a little silly…
- return $item->getNotificationTimestamp();
- }
- }
- return $notificationTimestamp;
- }
- /**
- * @since 1.27
- * @param User $user
- * @param int|null $unreadLimit
- * @return int|bool
- */
- public function countUnreadNotifications( User $user, $unreadLimit = null ) {
- $queryOptions = [];
- if ( $unreadLimit !== null ) {
- $unreadLimit = (int)$unreadLimit;
- $queryOptions['LIMIT'] = $unreadLimit;
- }
- $dbr = $this->getConnectionRef( DB_REPLICA );
- $rowCount = $dbr->selectRowCount(
- 'watchlist',
- '1',
- [
- 'wl_user' => $user->getId(),
- 'wl_notificationtimestamp IS NOT NULL',
- ],
- __METHOD__,
- $queryOptions
- );
- if ( !isset( $unreadLimit ) ) {
- return $rowCount;
- }
- if ( $rowCount >= $unreadLimit ) {
- return true;
- }
- return $rowCount;
- }
- /**
- * @since 1.27
- * @param LinkTarget $oldTarget
- * @param LinkTarget $newTarget
- */
- public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget ) {
- $oldTarget = Title::newFromLinkTarget( $oldTarget );
- $newTarget = Title::newFromLinkTarget( $newTarget );
- $this->duplicateEntry( $oldTarget->getSubjectPage(), $newTarget->getSubjectPage() );
- $this->duplicateEntry( $oldTarget->getTalkPage(), $newTarget->getTalkPage() );
- }
- /**
- * @since 1.27
- * @param LinkTarget $oldTarget
- * @param LinkTarget $newTarget
- */
- public function duplicateEntry( LinkTarget $oldTarget, LinkTarget $newTarget ) {
- $dbw = $this->getConnectionRef( DB_MASTER );
- $result = $dbw->select(
- 'watchlist',
- [ 'wl_user', 'wl_notificationtimestamp' ],
- [
- 'wl_namespace' => $oldTarget->getNamespace(),
- 'wl_title' => $oldTarget->getDBkey(),
- ],
- __METHOD__,
- [ 'FOR UPDATE' ]
- );
- $newNamespace = $newTarget->getNamespace();
- $newDBkey = $newTarget->getDBkey();
- # Construct array to replace into the watchlist
- $values = [];
- foreach ( $result as $row ) {
- $values[] = [
- 'wl_user' => $row->wl_user,
- 'wl_namespace' => $newNamespace,
- 'wl_title' => $newDBkey,
- 'wl_notificationtimestamp' => $row->wl_notificationtimestamp,
- ];
- }
- if ( !empty( $values ) ) {
- # Perform replace
- # Note that multi-row replace is very efficient for MySQL but may be inefficient for
- # some other DBMSes, mostly due to poor simulation by us
- $dbw->replace(
- 'watchlist',
- [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
- $values,
- __METHOD__
- );
- }
- }
- }
|