Merge "Fix ParserOutput::getText 'unwrap' flag for end-of-doc comment"
[lhc/web/wiklou.git] / includes / watcheditem / WatchedItemStore.php
index 60d8b76..35e824e 100644 (file)
@@ -7,23 +7,16 @@ use MediaWiki\MediaWikiServices;
 use Wikimedia\Assert\Assert;
 use Wikimedia\ScopedCallback;
 use Wikimedia\Rdbms\LoadBalancer;
-use Wikimedia\Rdbms\DBUnexpectedError;
 
 /**
  * Storage layer class for WatchedItems.
- * Database interaction.
- *
- * Uses database because this uses User::isAnon
- *
- * @group Database
+ * Database interaction & caching
+ * TODO caching should be factored out into a CachingWatchedItemStore class
  *
  * @author Addshore
  * @since 1.27
  */
-class WatchedItemStore implements StatsdAwareInterface {
-
-       const SORT_DESC = 'DESC';
-       const SORT_ASC = 'ASC';
+class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterface {
 
        /**
         * @var LoadBalancer
@@ -58,6 +51,11 @@ class WatchedItemStore implements StatsdAwareInterface {
         */
        private $revisionGetTimestampFromIdCallback;
 
+       /**
+        * @var int
+        */
+       private $updateRowsPerQuery;
+
        /**
         * @var StatsdDataFactoryInterface
         */
@@ -67,20 +65,28 @@ class WatchedItemStore implements StatsdAwareInterface {
         * @param LoadBalancer $loadBalancer
         * @param HashBagOStuff $cache
         * @param ReadOnlyMode $readOnlyMode
+        * @param int $updateRowsPerQuery
         */
        public function __construct(
                LoadBalancer $loadBalancer,
                HashBagOStuff $cache,
-               ReadOnlyMode $readOnlyMode
+               ReadOnlyMode $readOnlyMode,
+               $updateRowsPerQuery
        ) {
                $this->loadBalancer = $loadBalancer;
                $this->cache = $cache;
                $this->readOnlyMode = $readOnlyMode;
                $this->stats = new NullStatsdDataFactory();
-               $this->deferredUpdatesAddCallableUpdateCallback = [ 'DeferredUpdates', 'addCallableUpdate' ];
-               $this->revisionGetTimestampFromIdCallback = [ 'Revision', 'getTimestampFromId' ];
+               $this->deferredUpdatesAddCallableUpdateCallback =
+                       [ DeferredUpdates::class, 'addCallableUpdate' ];
+               $this->revisionGetTimestampFromIdCallback =
+                       [ Revision::class, 'getTimestampFromId' ];
+               $this->updateRowsPerQuery = $updateRowsPerQuery;
        }
 
+       /**
+        * @param StatsdDataFactoryInterface $stats
+        */
        public function setStatsdDataFactory( StatsdDataFactoryInterface $stats ) {
                $this->stats = $stats;
        }
@@ -216,11 +222,85 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Count the number of individual items that are watched by the user.
-        * If a subject and corresponding talk page are watched this will return 2.
+        * 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 ) {
@@ -238,8 +318,8 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
+        * @since 1.27
         * @param LinkTarget $target
-        *
         * @return int
         */
        public function countWatchers( LinkTarget $target ) {
@@ -258,14 +338,10 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Number of page watchers who also visited a "recent" edit
-        *
+        * @since 1.27
         * @param LinkTarget $target
-        * @param mixed $threshold timestamp accepted by wfTimestamp
-        *
+        * @param string|int $threshold
         * @return int
-        * @throws DBUnexpectedError
-        * @throws MWException
         */
        public function countVisitingWatchers( LinkTarget $target, $threshold ) {
                $dbr = $this->getConnectionRef( DB_REPLICA );
@@ -286,13 +362,10 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
+        * @since 1.27
         * @param LinkTarget[] $targets
-        * @param array $options Allowed keys:
-        *        'minimumWatchers' => int
-        *
-        * @return array multi dimensional like $return[$namespaceId][$titleString] = int $watchers
-        *         All targets will be present in the result. 0 either means no watchers or the number
-        *         of watchers was below the minimumWatchers option if passed.
+        * @param array $options
+        * @return array
         */
        public function countWatchersMultiple( array $targets, array $options = [] ) {
                $dbOptions = [ 'GROUP BY' => [ 'wl_namespace', 'wl_title' ] ];
@@ -325,19 +398,10 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Number of watchers of each page who have visited recent edits to that page
-        *
-        * @param array $targetsWithVisitThresholds array of pairs (LinkTarget $target, mixed $threshold),
-        *        $threshold is:
-        *        - a timestamp of the recent edit if $target exists (format accepted by wfTimestamp)
-        *        - null if $target doesn't exist
+        * @since 1.27
+        * @param array $targetsWithVisitThresholds
         * @param int|null $minimumWatchers
-        * @return array multi-dimensional like $return[$namespaceId][$titleString] = $watchers,
-        *         where $watchers is an int:
-        *         - if the page exists, number of users watching who have visited the page recently
-        *         - if the page doesn't exist, number of users that have the page on their watchlist
-        *         - 0 means there are no visiting watchers or their number is below the minimumWatchers
-        *         option (if passed).
+        * @return array
         */
        public function countVisitingWatchersMultiple(
                array $targetsWithVisitThresholds,
@@ -417,12 +481,10 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Get an item (may be cached)
-        *
+        * @since 1.27
         * @param User $user
         * @param LinkTarget $target
-        *
-        * @return WatchedItem|false
+        * @return bool
         */
        public function getWatchedItem( User $user, LinkTarget $target ) {
                if ( $user->isAnon() ) {
@@ -439,12 +501,10 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Loads an item from the db
-        *
+        * @since 1.27
         * @param User $user
         * @param LinkTarget $target
-        *
-        * @return WatchedItem|false
+        * @return bool
         */
        public function loadWatchedItem( User $user, LinkTarget $target ) {
                // Only loggedin user can have a watchlist
@@ -475,12 +535,9 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
+        * @since 1.27
         * @param User $user
-        * @param array $options Allowed keys:
-        *        'forWrite' => bool defaults to false
-        *        'sort' => string optional sorting by namespace ID and title
-        *                     one of the self::SORT_* constants
-        *
+        * @param array $options
         * @return WatchedItem[]
         */
        public function getWatchedItemsForUser( User $user, array $options = [] ) {
@@ -522,11 +579,9 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Must be called separately for Subject & Talk namespaces
-        *
+        * @since 1.27
         * @param User $user
         * @param LinkTarget $target
-        *
         * @return bool
         */
        public function isWatched( User $user, LinkTarget $target ) {
@@ -534,13 +589,10 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
+        * @since 1.27
         * @param User $user
         * @param LinkTarget[] $targets
-        *
-        * @return array multi-dimensional like $return[$namespaceId][$titleString] = $timestamp,
-        *         where $timestamp is:
-        *         - string|null value of wl_notificationtimestamp,
-        *         - false if $target is not watched by $user.
+        * @return array
         */
        public function getNotificationTimestampsBatch( User $user, array $targets ) {
                $timestamps = [];
@@ -589,8 +641,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Must be called separately for Subject & Talk namespaces
-        *
+        * @since 1.27
         * @param User $user
         * @param LinkTarget $target
         */
@@ -599,10 +650,10 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
+        * @since 1.27
         * @param User $user
         * @param LinkTarget[] $targets
-        *
-        * @return bool success
+        * @return bool
         */
        public function addWatchBatchForUser( User $user, array $targets ) {
                if ( $this->readOnlyMode->isReadOnly() ) {
@@ -651,15 +702,10 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Removes the an entry for the User watching the LinkTarget
-        * Must be called separately for Subject & Talk namespaces
-        *
+        * @since 1.27
         * @param User $user
         * @param LinkTarget $target
-        *
-        * @return bool success
-        * @throws DBUnexpectedError
-        * @throws MWException
+        * @return bool
         */
        public function removeWatch( User $user, LinkTarget $target ) {
                // Only logged in user can have a watchlist
@@ -683,11 +729,11 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * @param User $user The user to set the timestamp for
-        * @param string|null $timestamp Set the update timestamp to this value
-        * @param LinkTarget[] $targets List of targets to update. Default to all targets
-        *
-        * @return bool 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
@@ -720,12 +766,11 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * @param User $editor The editor that triggered the update. Their notification
-        *  timestamp will not be updated(they have already seen it)
-        * @param LinkTarget $target The target to update timestamps for
-        * @param string $timestamp Set the update timestamp to this value
-        *
-        * @return int[] Array of user IDs the timestamp has been updated for
+        * @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 );
@@ -781,16 +826,12 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Reset the notification timestamp of this entry
-        *
+        * @since 1.27
         * @param User $user
         * @param Title $title
-        * @param string $force Whether to force the write query to be executed even if the
-        *    page is not watched or the notification timestamp is already NULL.
-        *    'force' in order to force
-        * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
-        *
-        * @return bool success
+        * @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
@@ -879,11 +920,10 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
+        * @since 1.27
         * @param User $user
-        * @param int $unreadLimit
-        *
-        * @return int|bool The number of unread notifications
-        *                  true if greater than or equal to $unreadLimit
+        * @param int|null $unreadLimit
+        * @return int|bool
         */
        public function countUnreadNotifications( User $user, $unreadLimit = null ) {
                $queryOptions = [];
@@ -916,11 +956,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Check if the given title already is watched by the user, and if so
-        * add a watch for the new title.
-        *
-        * To be used for page renames and such.
-        *
+        * @since 1.27
         * @param LinkTarget $oldTarget
         * @param LinkTarget $newTarget
         */
@@ -933,12 +969,7 @@ class WatchedItemStore implements StatsdAwareInterface {
        }
 
        /**
-        * Check if the given title already is watched by the user, and if so
-        * add a watch for the new title.
-        *
-        * To be used for page renames and such.
-        * This must be called separately for Subject and Talk pages
-        *
+        * @since 1.27
         * @param LinkTarget $oldTarget
         * @param LinkTarget $newTarget
         */