does not support categories.
* wikidiff difference engine is no longer supported, anyone still using it are encouraged
to upgrade to wikidiff2 which is actively maintained and has better package availability.
+* Database logic was removed from WatchedItem and a WatchedItemStore was created:
+** WatchedItem::IGNORE_USER_RIGHTS and WatchedItem::CHECK_USER_RIGHTS were deprecated.
+ User::IGNORE_USER_RIGHTS and User::CHECK_USER_RIGHTS were introduced.
+** WatchedItem::fromUserTitle was deprecated in favour of the constructor.
+** WatchedItem::resetNotificationTimestamp was deprecated.
+** WatchedItem::batchAddWatch was deprecated.
+** WatchedItem::addWatch was deprecated.
+** WatchedItem::removeWatch was deprecated.
+** WatchedItem::isWatched was deprecated.
+** WatchedItem::duplicateEntries was deprecated.
+** User::getWatchedItem was removed.
== Compatibility ==
$watch = $this->watchthis;
// Do this in its own transaction to reduce contention...
DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
- if ( $watch == $user->isWatched( $title, WatchedItem::IGNORE_USER_RIGHTS ) ) {
+ if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
return; // nothing to change
}
WatchAction::doWatchOrUnwatch( $watch, $title, $user );
$oldsnamespace = MWNamespace::getSubject( $this->oldTitle->getNamespace() );
$newsnamespace = MWNamespace::getSubject( $this->newTitle->getNamespace() );
if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) {
- WatchedItem::duplicateEntries( $this->oldTitle, $this->newTitle );
+ $store = WatchedItemStore::getDefaultInstance();
+ $store->duplicateAllAssociatedEntries( $this->oldTitle, $this->newTitle );
}
Hooks::run(
$this->mNotificationTimestamp = [];
}
- $watchedItem = WatchedItem::fromUserTitle( $user, $this );
- $this->mNotificationTimestamp[$uid] = $watchedItem->getNotificationTimestamp();
+ $watchedItem = WatchedItemStore::getDefaultInstance()->getWatchedItem( $user, $this );
+ if ( $watchedItem ) {
+ $this->mNotificationTimestamp[$uid] = $watchedItem->getNotificationTimestamp();
+ } else {
+ $this->mNotificationTimestamp[$uid] = false;
+ }
return $this->mNotificationTimestamp[$uid];
}
<?php
/**
- * Accessor and mutator for watchlist entries.
- *
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* @file
* @ingroup Watchlist
*/
+use Wikimedia\Assert\Assert;
/**
* Representation of a pair of user and title for watchlist entries.
*
+ * @author Tim Starling
+ * @author Addshore
+ *
* @ingroup Watchlist
*/
class WatchedItem {
- /** @var Title */
- private $mTitle;
-
- /** @var User */
- private $mUser;
-
- /** @var int */
- private $mCheckRights;
-
- /** @var bool */
- private $loaded = false;
-
- /** @var bool */
- private $watched;
-
- /** @var string */
- private $timestamp;
/**
- * Constant to specify that user rights 'editmywatchlist' and
- * 'viewmywatchlist' should not be checked.
- * @since 1.22
+ * @deprecated since 1.27, see User::IGNORE_USER_RIGHTS
*/
- const IGNORE_USER_RIGHTS = 0;
+ const IGNORE_USER_RIGHTS = User::IGNORE_USER_RIGHTS;
/**
- * Constant to specify that user rights 'editmywatchlist' and
- * 'viewmywatchlist' should be checked.
- * @since 1.22
+ * @deprecated since 1.27, see User::CHECK_USER_RIGHTS
*/
- const CHECK_USER_RIGHTS = 1;
+ const CHECK_USER_RIGHTS = User::CHECK_USER_RIGHTS;
/**
- * Create a WatchedItem object with the given user and title
- * @since 1.22 $checkRights parameter added
- * @param User $user The user to use for (un)watching
- * @param Title $title The title we're going to (un)watch
- * @param int $checkRights Whether to check the 'viewmywatchlist' and 'editmywatchlist' rights.
- * Pass either WatchedItem::IGNORE_USER_RIGHTS or WatchedItem::CHECK_USER_RIGHTS.
- * @return WatchedItem
+ * @deprecated Internal class use only
*/
- public static function fromUserTitle( $user, $title,
- $checkRights = WatchedItem::CHECK_USER_RIGHTS
- ) {
- $wl = new WatchedItem;
- $wl->mUser = $user;
- $wl->mTitle = $title;
- $wl->mCheckRights = $checkRights;
-
- return $wl;
- }
+ const DEPRECATED_USAGE_TIMESTAMP = -100;
/**
- * Title being watched
- * @return Title
+ * @var bool
+ * @deprecated Internal class use only
*/
- protected function getTitle() {
- return $this->mTitle;
- }
+ public $checkRights = User::CHECK_USER_RIGHTS;
/**
- * Helper to retrieve the title namespace
- * @return int
+ * @var Title
+ * @deprecated Internal class use only
*/
- protected function getTitleNs() {
- return $this->getTitle()->getNamespace();
- }
+ private $title;
/**
- * Helper to retrieve the title DBkey
- * @return string
+ * @var LinkTarget
*/
- protected function getTitleDBkey() {
- return $this->getTitle()->getDBkey();
- }
+ private $linkTarget;
/**
- * Helper to retrieve the user id
- * @return int
+ * @var User
*/
- protected function getUserId() {
- return $this->mUser->getId();
- }
+ private $user;
/**
- * Return an array of conditions to select or update the appropriate database
- * row.
- *
- * @return array
+ * @var null|string the value of the wl_notificationtimestamp field
*/
- private function dbCond() {
- return [
- 'wl_user' => $this->getUserId(),
- 'wl_namespace' => $this->getTitleNs(),
- 'wl_title' => $this->getTitleDBkey(),
- ];
- }
+ private $notificationTimestamp;
/**
- * Load the object from the database
+ * @param User $user
+ * @param LinkTarget $linkTarget
+ * @param null|string $notificationTimestamp the value of the wl_notificationtimestamp field
+ * @param bool|null $checkRights DO NOT USE - used internally for backward compatibility
*/
- private function load() {
- if ( $this->loaded ) {
- return;
- }
- $this->loaded = true;
-
- // Only loggedin user can have a watchlist
- if ( $this->mUser->isAnon() ) {
- $this->watched = false;
- return;
- }
-
- // some pages cannot be watched
- if ( !$this->getTitle()->isWatchable() ) {
- $this->watched = false;
- return;
- }
-
- # Pages and their talk pages are considered equivalent for watching;
- # remember that talk namespaces are numbered as page namespace+1.
-
- $dbr = wfGetDB( DB_SLAVE );
- $row = $dbr->selectRow( 'watchlist', 'wl_notificationtimestamp',
- $this->dbCond(), __METHOD__ );
-
- if ( $row === false ) {
- $this->watched = false;
- } else {
- $this->watched = true;
- $this->timestamp = $row->wl_notificationtimestamp;
+ public function __construct(
+ User $user,
+ LinkTarget $linkTarget,
+ $notificationTimestamp,
+ $checkRights = null
+ ) {
+ $this->user = $user;
+ $this->linkTarget = $linkTarget;
+ $this->notificationTimestamp = $notificationTimestamp;
+ if ( $checkRights !== null ) {
+ $this->checkRights = $checkRights;
}
}
/**
- * Check permissions
- * @param string $what 'viewmywatchlist' or 'editmywatchlist'
- * @return bool
+ * @return User
*/
- private function isAllowed( $what ) {
- return !$this->mCheckRights || $this->mUser->isAllowed( $what );
+ public function getUser() {
+ return $this->user;
}
/**
- * Is mTitle being watched by mUser?
- * @return bool
+ * @return LinkTarget
*/
- public function isWatched() {
- if ( !$this->isAllowed( 'viewmywatchlist' ) ) {
- return false;
- }
-
- $this->load();
- return $this->watched;
+ public function getLinkTarget() {
+ return $this->linkTarget;
}
/**
* Get the notification timestamp of this entry.
*
- * @return bool|null|string False if the page is not watched, the value of
- * the wl_notificationtimestamp field otherwise
+ * @return bool|null|string
*/
public function getNotificationTimestamp() {
- if ( !$this->isAllowed( 'viewmywatchlist' ) ) {
- return false;
- }
-
- $this->load();
- if ( $this->watched ) {
- return $this->timestamp;
- } else {
- return false;
+ // Back compat for objects constructed using self::fromUserTitle
+ if ( $this->notificationTimestamp === self::DEPRECATED_USAGE_TIMESTAMP ) {
+ // wfDeprecated( __METHOD__, '1.27' );
+ if ( $this->checkRights && !$this->user->isAllowed( 'viewmywatchlist' ) ) {
+ return false;
+ }
+ $item = WatchedItemStore::getDefaultInstance()
+ ->loadWatchedItem( $this->user, $this->linkTarget );
+ if ( $item ) {
+ $this->notificationTimestamp = $item->getNotificationTimestamp();
+ } else {
+ $this->notificationTimestamp = false;
+ }
}
+ return $this->notificationTimestamp;
}
/**
- * Reset the notification timestamp of this entry
- *
- * @param bool $force Whether to force the write query to be executed even if the
- * page is not watched or the notification timestamp is already NULL.
- * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
+ * Back compat pre 1.27 with the WatchedItemStore introduction
+ * @todo remove in 1.28/9
+ * -------------------------------------------------
*/
- public function resetNotificationTimestamp(
- $force = '', $oldid = 0
- ) {
- // Only loggedin user can have a watchlist
- if ( wfReadOnly() || $this->mUser->isAnon() || !$this->isAllowed( 'editmywatchlist' ) ) {
- return;
- }
- if ( $force != 'force' ) {
- $this->load();
- if ( !$this->watched || $this->timestamp === null ) {
- return;
+ /**
+ * @return Title
+ * @deprecated Internal class use only
+ */
+ public function getTitle() {
+ if ( !$this->title ) {
+ if ( $this->linkTarget instanceof Title ) {
+ $this->title = $this->linkTarget;
+ } else {
+ $this->title = Title::newFromLinkTarget( $this->linkTarget );
}
}
+ return $this->title;
+ }
- $title = $this->getTitle();
- if ( !$oldid ) {
- // No oldid given, assuming latest revision; clear the timestamp.
- $notificationTimestamp = null;
- } elseif ( !$title->getNextRevisionID( $oldid ) ) {
- // Oldid given and is the latest revision for this title; clear the timestamp.
- $notificationTimestamp = null;
- } else {
- // See if the version marked as read is more recent than the one we're viewing.
- // Call load() if it wasn't called before due to $force.
- $this->load();
-
- if ( $this->timestamp === null ) {
- // This can only happen if $force is enabled.
- $notificationTimestamp = null;
- } else {
- // Oldid given and isn't the latest; update the timestamp.
- // This will result in no further notification emails being sent!
- $notificationTimestamp = Revision::getTimestampFromId( $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 );
+ /**
+ * @deprecated since 1.27 Use the constructor, WatchedItemStore::getWatchedItem()
+ * or WatchedItemStore::loadWatchedItem()
+ */
+ public static function fromUserTitle( $user, $title, $checkRights = User::CHECK_USER_RIGHTS ) {
+ // wfDeprecated( __METHOD__, '1.27' );
+ return new self( $user, $title, self::DEPRECATED_USAGE_TIMESTAMP, (bool)$checkRights );
+ }
- if ( $notificationTimestamp < $this->timestamp ) {
- if ( $force != 'force' ) {
- return;
- } else {
- // This is a little silly…
- $notificationTimestamp = $this->timestamp;
- }
- }
- }
+ /**
+ * @deprecated since 1.27 Use WatchedItemStore::resetNotificationTimestamp()
+ */
+ public function resetNotificationTimestamp( $force = '', $oldid = 0 ) {
+ // wfDeprecated( __METHOD__, '1.27' );
+ if ( $this->checkRights && !$this->user->isAllowed( 'editmywatchlist' ) ) {
+ return;
}
-
- // If the page is watched by the user (or may be watched), update the timestamp
- $job = new ActivityUpdateJob(
- $title,
- [
- 'type' => 'updateWatchlistNotification',
- 'userid' => $this->getUserId(),
- 'notifTime' => $notificationTimestamp,
- 'curTime' => time()
- ]
+ WatchedItemStore::getDefaultInstance()->resetNotificationTimestamp(
+ $this->user,
+ $this->getTitle(),
+ $force,
+ $oldid
);
- // Try to run this post-send
- DeferredUpdates::addCallableUpdate( function() use ( $job ) {
- $job->run();
- } );
-
- $this->timestamp = null;
}
/**
- * @param WatchedItem[] $items
- * @return bool
+ * @deprecated since 1.27 Use WatchedItemStore::addWatchBatch()
*/
public static function batchAddWatch( array $items ) {
-
- if ( wfReadOnly() ) {
- return false;
- }
-
- $rows = [];
- foreach ( $items as $item ) {
- // Only loggedin user can have a watchlist
- if ( $item->mUser->isAnon() || !$item->isAllowed( 'editmywatchlist' ) ) {
+ // wfDeprecated( __METHOD__, '1.27' );
+ $userTargetCombinations = [];
+ /** @var WatchedItem $watchedItem */
+ foreach ( $items as $watchedItem ) {
+ if ( $watchedItem->checkRights && !$watchedItem->getUser()->isAllowed( 'editmywatchlist' ) ) {
continue;
}
- $rows[] = [
- 'wl_user' => $item->getUserId(),
- 'wl_namespace' => MWNamespace::getSubject( $item->getTitleNs() ),
- 'wl_title' => $item->getTitleDBkey(),
- 'wl_notificationtimestamp' => null,
+ $userTargetCombinations[] = [
+ $watchedItem->getUser(),
+ $watchedItem->getTitle()->getSubjectPage()
];
- // Every single watched page needs now to be listed in watchlist;
- // namespace:page and namespace_talk:page need separate entries:
- $rows[] = [
- 'wl_user' => $item->getUserId(),
- 'wl_namespace' => MWNamespace::getTalk( $item->getTitleNs() ),
- 'wl_title' => $item->getTitleDBkey(),
- 'wl_notificationtimestamp' => null
+ $userTargetCombinations[] = [
+ $watchedItem->getUser(),
+ $watchedItem->getTitle()->getTalkPage()
];
- $item->watched = true;
- }
-
- if ( !$rows ) {
- return false;
- }
-
- $dbw = wfGetDB( 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' );
}
-
- return true;
+ $store = WatchedItemStore::getDefaultInstance();
+ return $store->addWatchBatch( $userTargetCombinations );
}
/**
- * Given a title and user (assumes the object is setup), add the watch to the database.
+ * @deprecated since 1.27 Use User::addWatch()
* @return bool
*/
public function addWatch() {
- return self::batchAddWatch( [ $this ] );
+ // wfDeprecated( __METHOD__, '1.27' );
+ $this->user->addWatch( $this->getTitle(), $this->checkRights );
+ return true;
}
/**
- * Same as addWatch, only the opposite.
+ * @deprecated since 1.27 Use User::removeWatch()
* @return bool
*/
public function removeWatch() {
-
- // Only loggedin user can have a watchlist
- if ( wfReadOnly() || $this->mUser->isAnon() || !$this->isAllowed( 'editmywatchlist' ) ) {
+ // wfDeprecated( __METHOD__, '1.27' );
+ if ( $this->checkRights && !$this->user->isAllowed( 'editmywatchlist' ) ) {
return false;
}
+ $this->user->removeWatch( $this->getTitle(), $this->checkRights );
+ return true;
+ }
- $success = false;
- $dbw = wfGetDB( DB_MASTER );
- $dbw->delete( 'watchlist',
- [
- 'wl_user' => $this->getUserId(),
- 'wl_namespace' => MWNamespace::getSubject( $this->getTitleNs() ),
- 'wl_title' => $this->getTitleDBkey(),
- ], __METHOD__
- );
- if ( $dbw->affectedRows() ) {
- $success = true;
- }
-
- # the following code compensates the new behavior, introduced by the
- # enotif patch, that every single watched page needs now to be listed
- # in watchlist namespace:page and namespace_talk:page had separate
- # entries: clear them
- $dbw->delete( 'watchlist',
- [
- 'wl_user' => $this->getUserId(),
- 'wl_namespace' => MWNamespace::getTalk( $this->getTitleNs() ),
- 'wl_title' => $this->getTitleDBkey(),
- ], __METHOD__
- );
-
- if ( $dbw->affectedRows() ) {
- $success = true;
- }
-
- $this->watched = false;
-
- return $success;
+ /**
+ * @deprecated since 1.27 Use User::isWatched()
+ * @return bool
+ */
+ public function isWatched() {
+ // wfDeprecated( __METHOD__, '1.27' );
+ return $this->user->isWatched( $this->getTitle(), $this->checkRights );
}
/**
- * @deprecated since 1.27. See WatchedItemStore::duplicateEntry
- *
- * @param Title $oldTitle
- * @param Title $newTitle
+ * @deprecated since 1.27 Use WatchedItemStore::duplicateAllAssociatedEntries()
*/
public static function duplicateEntries( Title $oldTitle, Title $newTitle ) {
+ // wfDeprecated( __METHOD__, '1.27' );
$store = WatchedItemStore::getDefaultInstance();
- $store->duplicateEntry( $oldTitle->getSubjectPage(), $newTitle->getSubjectPage() );
- $store->duplicateEntry( $oldTitle->getTalkPage(), $newTitle->getTalkPage() );
+ $store->duplicateAllAssociatedEntries( $oldTitle, $newTitle );
}
}
<?php
+use Wikimedia\Assert\Assert;
+
/**
* Storage layer class for WatchedItems.
- * Database interaction
+ * Database interaction.
*
* @author Addshore
*
*/
private $loadBalancer;
- public function __construct( LoadBalancer $loadBalancer ) {
+ /**
+ * @var BagOStuff
+ */
+ private $cache;
+
+ /**
+ * @var callable|null
+ */
+ private $deferredUpdatesAddCallableUpdateCallback;
+
+ /**
+ * @var callable|null
+ */
+ private $revisionGetTimestampFromIdCallback;
+
+ /**
+ * @var self|null
+ */
+ private static $instance;
+
+ public function __construct( LoadBalancer $loadBalancer, BagOStuff $cache ) {
$this->loadBalancer = $loadBalancer;
+ $this->cache = $cache;
+ $this->deferredUpdatesAddCallableUpdateCallback = [ 'DeferredUpdates', 'addCallableUpdate' ];
+ $this->revisionGetTimestampFromIdCallback = [ 'Revision', 'getTimestampFromId' ];
+ }
+
+ /**
+ * 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
+ *
+ * @throws MWException
+ */
+ public function overrideDeferredUpdatesAddCallableUpdateCallback( $callback ) {
+ if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+ throw new MWException(
+ 'Cannot override DeferredUpdates::addCallableUpdate callback in operation.'
+ );
+ }
+ Assert::parameterType( 'callable', $callback, '$callback' );
+ $this->deferredUpdatesAddCallableUpdateCallback = $callback;
+ }
+
+ /**
+ * 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
+ *
+ * @throws MWException
+ */
+ public function overrideRevisionGetTimestampFromIdCallback( $callback ) {
+ if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+ throw new MWException(
+ 'Cannot override Revision::getTimestampFromId callback in operation.'
+ );
+ }
+ Assert::parameterType( 'callable', $callback, '$callback' );
+ $this->revisionGetTimestampFromIdCallback = $callback;
+ }
+
+ /**
+ * Overrides the default instance of this class
+ * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
+ *
+ * @param WatchedItemStore $store
+ *
+ * @throws MWException
+ */
+ public static function overrideDefaultInstance( WatchedItemStore $store ) {
+ if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+ throw new MWException(
+ 'Cannot override ' . __CLASS__ . 'default instance in operation.'
+ );
+ }
+ self::$instance = $store;
}
/**
* @return self
*/
public static function getDefaultInstance() {
- static $instance;
- if ( !$instance ) {
- $instance = new self( wfGetLB() );
+ if ( !self::$instance ) {
+ self::$instance = new self(
+ wfGetLB(),
+ new HashBagOStuff( [ 'maxKeys' => 100 ] )
+ );
+ }
+ return self::$instance;
+ }
+
+ private function getCacheKey( User $user, LinkTarget $target ) {
+ return $this->cache->makeKey(
+ (string)$target->getNamespace(),
+ $target->getDBkey(),
+ (string)$user->getId()
+ );
+ }
+
+ private function cache( WatchedItem $item ) {
+ $this->cache->set(
+ $this->getCacheKey( $item->getUser(), $item->getLinkTarget() ),
+ $item
+ );
+ }
+
+ private function uncache( User $user, LinkTarget $target ) {
+ $this->cache->delete( $this->getCacheKey( $user, $target ) );
+ }
+
+ /**
+ * @param User $user
+ * @param LinkTarget $target
+ *
+ * @return WatchedItem|null
+ */
+ 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(),
+ ];
+ }
+
+ /**
+ * Get an item (may be cached)
+ *
+ * @param User $user
+ * @param LinkTarget $target
+ *
+ * @return WatchedItem|false
+ */
+ public function getWatchedItem( User $user, LinkTarget $target ) {
+ $cached = $this->getCached( $user, $target );
+ if ( $cached ) {
+ return $cached;
+ }
+ return $this->loadWatchedItem( $user, $target );
+ }
+
+ /**
+ * Loads an item from the db
+ *
+ * @param User $user
+ * @param LinkTarget $target
+ *
+ * @return WatchedItem|false
+ */
+ public function loadWatchedItem( User $user, LinkTarget $target ) {
+ // Only loggedin user can have a watchlist
+ if ( $user->isAnon() ) {
+ return false;
+ }
+
+ $dbr = $this->loadBalancer->getConnection( DB_SLAVE, [ 'watchlist' ] );
+ $row = $dbr->selectRow(
+ 'watchlist',
+ 'wl_notificationtimestamp',
+ $this->dbCond( $user, $target ),
+ __METHOD__
+ );
+ $this->loadBalancer->reuseConnection( $dbr );
+
+ if ( !$row ) {
+ return false;
+ }
+
+ $item = new WatchedItem(
+ $user,
+ $target,
+ $row->wl_notificationtimestamp
+ );
+ $this->cache( $item );
+
+ return $item;
+ }
+
+ /**
+ * Must be called separately for Subject & Talk namespaces
+ *
+ * @param User $user
+ * @param LinkTarget $target
+ *
+ * @return bool
+ */
+ public function isWatched( User $user, LinkTarget $target ) {
+ return (bool)$this->getWatchedItem( $user, $target );
+ }
+
+ /**
+ * Must be called separately for Subject & Talk namespaces
+ *
+ * @param User $user
+ * @param LinkTarget $target
+ */
+ public function addWatch( User $user, LinkTarget $target ) {
+ $this->addWatchBatch( [ [ $user, $target ] ] );
+ }
+
+ /**
+ * @param array[] $userTargetCombinations array of arrays containing [0] => User [1] => LinkTarget
+ *
+ * @return bool success
+ */
+ public function addWatchBatch( array $userTargetCombinations ) {
+ if ( $this->loadBalancer->getReadOnlyReason() !== false ) {
+ return false;
+ }
+
+ $rows = [];
+ foreach ( $userTargetCombinations as list( $user, $target ) ) {
+ /**
+ * @var User $user
+ * @var LinkTarget $target
+ */
+
+ // Only loggedin user can have a watchlist
+ if ( $user->isAnon() ) {
+ continue;
+ }
+ $rows[] = [
+ 'wl_user' => $user->getId(),
+ 'wl_namespace' => $target->getNamespace(),
+ 'wl_title' => $target->getDBkey(),
+ 'wl_notificationtimestamp' => null,
+ ];
+ $this->uncache( $user, $target );
+ }
+
+ if ( !$rows ) {
+ return false;
+ }
+
+ $dbw = $this->loadBalancer->getConnection( DB_MASTER, [ 'watchlist' ] );
+ 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' );
}
- return $instance;
+ $this->loadBalancer->reuseConnection( $dbw );
+
+ return true;
+ }
+
+ /**
+ * Removes the an entry for the User watching the LinkTarget
+ * Must be called separately for Subject & Talk namespaces
+ *
+ * @param User $user
+ * @param LinkTarget $target
+ *
+ * @return bool success
+ * @throws DBUnexpectedError
+ * @throws MWException
+ */
+ public function removeWatch( User $user, LinkTarget $target ) {
+ // Only logged in user can have a watchlist
+ if ( $this->loadBalancer->getReadOnlyReason() !== false || $user->isAnon() ) {
+ return false;
+ }
+
+ $this->uncache( $user, $target );
+
+ $dbw = $this->loadBalancer->getConnection( DB_MASTER, [ 'watchlist' ] );
+ $dbw->delete( 'watchlist',
+ [
+ 'wl_user' => $user->getId(),
+ 'wl_namespace' => $target->getNamespace(),
+ 'wl_title' => $target->getDBkey(),
+ ], __METHOD__
+ );
+ $success = (bool)$dbw->affectedRows();
+ $this->loadBalancer->reuseConnection( $dbw );
+
+ return $success;
+ }
+
+ /**
+ * Reset the notification timestamp of this entry
+ *
+ * @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
+ */
+ public function resetNotificationTimestamp( User $user, Title $title, $force = '', $oldid = 0 ) {
+ // Only loggedin user can have a watchlist
+ if ( $this->loadBalancer->getReadOnlyReason() !== false || $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;
+ }
+
+ /**
+ * 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.
+ *
+ * @param LinkTarget $oldTarget
+ * @param LinkTarget $newTarget
+ */
+ public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget ) {
+ if ( !$oldTarget instanceof Title ) {
+ $oldTarget = Title::newFromLinkTarget( $oldTarget );
+ }
+ if ( !$newTarget instanceof Title ) {
+ $newTarget = Title::newFromLinkTarget( $newTarget );
+ }
+
+ $this->duplicateEntry( $oldTarget->getSubjectPage(), $newTarget->getSubjectPage() );
+ $this->duplicateEntry( $oldTarget->getTalkPage(), $newTarget->getTalkPage() );
}
/**
->parseAsBlock() );
if ( $user->getBoolOption( 'watchrollback' ) ) {
- $user->addWatch( $this->page->getTitle(), WatchedItem::IGNORE_USER_RIGHTS );
+ $user->addWatch( $this->page->getTitle(), User::IGNORE_USER_RIGHTS );
}
$this->getOutput()->returnToMain( false, $this->getTitle() );
*/
public static function doWatchOrUnwatch( $watch, Title $title, User $user ) {
if ( $user->isLoggedIn() &&
- $user->isWatched( $title, WatchedItem::IGNORE_USER_RIGHTS ) != $watch
+ $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) != $watch
) {
// If the user doesn't have 'editmywatchlist', we still want to
// allow them to add but not remove items via edits and such.
if ( $watch ) {
- return self::doWatch( $title, $user, WatchedItem::IGNORE_USER_RIGHTS );
+ return self::doWatch( $title, $user, User::IGNORE_USER_RIGHTS );
} else {
return self::doUnwatch( $title, $user );
}
* @since 1.22 Returns Status, $checkRights parameter added
* @param Title $title Page to watch/unwatch
* @param User $user User who is watching/unwatching
- * @param int $checkRights Passed through to $user->addWatch()
+ * @param bool $checkRights Passed through to $user->addWatch()
+ * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS.
* @return Status
*/
- public static function doWatch( Title $title, User $user,
- $checkRights = WatchedItem::CHECK_USER_RIGHTS
+ public static function doWatch(
+ Title $title,
+ User $user,
+ $checkRights = User::CHECK_USER_RIGHTS
) {
- if ( $checkRights !== WatchedItem::IGNORE_USER_RIGHTS &&
- !$user->isAllowed( 'editmywatchlist' )
- ) {
+ if ( $checkRights && !$user->isAllowed( 'editmywatchlist' ) ) {
return User::newFatalPermissionDeniedStatus( 'editmywatchlist' );
}
*/
protected function getWatchlistValue( $watchlist, $titleObj, $userOption = null ) {
- $userWatching = $this->getUser()->isWatched( $titleObj, WatchedItem::IGNORE_USER_RIGHTS );
+ $userWatching = $this->getUser()->isWatched( $titleObj, User::IGNORE_USER_RIGHTS );
switch ( $watchlist ) {
case 'watch':
\DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
# Watch user's userpage and talk page
- $user->addWatch( $user->getUserPage(), \WatchedItem::IGNORE_USER_RIGHTS );
+ $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
return true;
}
WatchAction::doWatch(
Title::makeTitle( NS_USER, $target ),
$performer,
- WatchedItem::IGNORE_USER_RIGHTS
+ User::IGNORE_USER_RIGHTS
);
}
DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
// Watch user's userpage and talk page
- $u->addWatch( $u->getUserPage(), WatchedItem::IGNORE_USER_RIGHTS );
+ $u->addWatch( $u->getUserPage(), User::IGNORE_USER_RIGHTS );
return Status::newGood( $u );
}
WatchAction::doWatch(
$this->getLocalFile()->getTitle(),
$user,
- WatchedItem::IGNORE_USER_RIGHTS
+ User::IGNORE_USER_RIGHTS
);
}
Hooks::run( 'UploadComplete', [ &$this ] );
*/
const VERSION = 10;
- /**
- * Maximum items in $mWatchedItems
- */
- const MAX_WATCHED_ITEMS_CACHE = 100;
-
/**
* Exclude user options that are set to their default value.
* @since 1.25
*/
const GETOPTIONS_EXCLUDE_DEFAULTS = 1;
+ /**
+ * @since 1.27
+ */
+ const CHECK_USER_RIGHTS = true;
+
+ /**
+ * @since 1.27
+ */
+ const IGNORE_USER_RIGHTS = false;
+
/**
* Array of Strings List of member variables which are saved to the
* shared cache (memcached). Any operation which changes the
/** @var Block */
private $mBlockedFromCreateAccount = false;
- /** @var array */
- private $mWatchedItems = [];
-
/** @var integer User::READ_* constant bitfield used to load data */
protected $queryFlagsUsed = self::READ_NORMAL;
}
}
- /**
- * Get a WatchedItem for this user and $title.
- *
- * @since 1.22 $checkRights parameter added
- * @param Title $title
- * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
- * Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
- * @return WatchedItem
- */
- public function getWatchedItem( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
- $key = $checkRights . ':' . $title->getNamespace() . ':' . $title->getDBkey();
-
- if ( isset( $this->mWatchedItems[$key] ) ) {
- return $this->mWatchedItems[$key];
- }
-
- if ( count( $this->mWatchedItems ) >= self::MAX_WATCHED_ITEMS_CACHE ) {
- $this->mWatchedItems = [];
- }
-
- $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title, $checkRights );
- return $this->mWatchedItems[$key];
- }
-
/**
* Check the watched status of an article.
* @since 1.22 $checkRights parameter added
* @param Title $title Title of the article to look at
- * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
- * Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
+ * @param bool $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
+ * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS.
* @return bool
*/
- public function isWatched( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
- return $this->getWatchedItem( $title, $checkRights )->isWatched();
+ public function isWatched( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
+ if ( $title->isWatchable() && ( !$checkRights || $this->isAllowed( 'viewmywatchlist' ) ) ) {
+ return WatchedItemStore::getDefaultInstance()->isWatched( $this, $title );
+ }
+ return false;
}
/**
* Watch an article.
* @since 1.22 $checkRights parameter added
* @param Title $title Title of the article to look at
- * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
- * Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
- */
- public function addWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
- $this->getWatchedItem( $title, $checkRights )->addWatch();
+ * @param bool $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
+ * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS.
+ */
+ public function addWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
+ if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
+ WatchedItemStore::getDefaultInstance()->addWatchBatch( [
+ [ $this, $title->getSubjectPage() ],
+ [ $this, $title->getTalkPage() ],
+ ]
+ );
+ }
$this->invalidateCache();
}
* Stop watching an article.
* @since 1.22 $checkRights parameter added
* @param Title $title Title of the article to look at
- * @param int $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
- * Pass WatchedItem::CHECK_USER_RIGHTS or WatchedItem::IGNORE_USER_RIGHTS.
+ * @param bool $checkRights Whether to check 'viewmywatchlist'/'editmywatchlist' rights.
+ * Pass User::CHECK_USER_RIGHTS or User::IGNORE_USER_RIGHTS.
*/
- public function removeWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
- $this->getWatchedItem( $title, $checkRights )->removeWatch();
+ public function removeWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
+ if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
+ WatchedItemStore::getDefaultInstance()->removeWatch( $this, $title->getSubjectPage() );
+ WatchedItemStore::getDefaultInstance()->removeWatch( $this, $title->getTalkPage() );
+ }
$this->invalidateCache();
}
$force = 'force';
}
- $this->getWatchedItem( $title )->resetNotificationTimestamp(
- $force, $oldid
- );
+ WatchedItemStore::getDefaultInstance()
+ ->resetNotificationTimestamp( $this, $title, $force, $oldid );
}
/**
--- /dev/null
+<?php
+
+/**
+ * @author Addshore
+ *
+ * @group Database
+ *
+ * @covers WatchedItemStore
+ */
+class WatchedItemStoreIntegrationTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ parent::setUp();
+ self::$users['WatchedItemStoreIntegrationTestUser']
+ = new TestUser( 'WatchedItemStoreIntegrationTestUser' );
+ }
+
+ private function getUser() {
+ return self::$users['WatchedItemStoreIntegrationTestUser']->getUser();
+ }
+
+ public function testWatchAndUnWatchItem() {
+ $user = $this->getUser();
+ $title = Title::newFromText( 'WatchedItemStoreIntegrationTestPage' );
+ $store = WatchedItemStore::getDefaultInstance();
+ // Cleanup after previous tests
+ $store->removeWatch( $user, $title );
+
+ $this->assertFalse(
+ $store->isWatched( $user, $title ),
+ 'Page should not initially be watched'
+ );
+ $store->addWatch( $user, $title );
+ $this->assertTrue(
+ $store->isWatched( $user, $title ),
+ 'Page should be watched'
+ );
+ $store->removeWatch( $user, $title );
+ $this->assertFalse(
+ $store->isWatched( $user, $title ),
+ 'Page should be unwatched'
+ );
+ }
+
+ public function testResetNotificationTimestamp() {
+ $user = $this->getUser();
+ $otherUser = ( new TestUser( 'WatchedItemStoreIntegrationTestUser_otherUser' ) )->getUser();
+ $title = Title::newFromText( 'WatchedItemStoreIntegrationTestPage' );
+ $store = WatchedItemStore::getDefaultInstance();
+ $store->addWatch( $user, $title );
+ EmailNotification::updateWatchlistTimestamp( $otherUser, $title, '20150202010101' );
+
+ $this->assertNotNull( $store->loadWatchedItem( $user, $title )->getNotificationTimestamp() );
+ $this->assertTrue( $store->resetNotificationTimestamp( $user, $title ) );
+ $this->assertNull( $store->loadWatchedItem( $user, $title )->getNotificationTimestamp() );
+ }
+
+ public function testDuplicateAllAssociatedEntries() {
+ $user = $this->getUser();
+ $titleOld = Title::newFromText( 'WatchedItemStoreIntegrationTestPageOld' );
+ $titleNew = Title::newFromText( 'WatchedItemStoreIntegrationTestPageNew' );
+ $store = WatchedItemStore::getDefaultInstance();
+ $store->addWatch( $user, $titleOld->getSubjectPage() );
+ $store->addWatch( $user, $titleOld->getTalkPage() );
+ // Cleanup after previous tests
+ $store->removeWatch( $user, $titleNew->getSubjectPage() );
+ $store->removeWatch( $user, $titleNew->getTalkPage() );
+
+ $store->duplicateAllAssociatedEntries( $titleOld, $titleNew );
+
+ $this->assertTrue( $store->isWatched( $user, $titleOld->getSubjectPage() ) );
+ $this->assertTrue( $store->isWatched( $user, $titleOld->getTalkPage() ) );
+ $this->assertTrue( $store->isWatched( $user, $titleNew->getSubjectPage() ) );
+ $this->assertTrue( $store->isWatched( $user, $titleNew->getTalkPage() ) );
+ }
+
+}
+++ /dev/null
-<?php
-
-/**
- * @author Addshore
- *
- * @covers WatchedItemStore
- */
-class WatchedItemStoreTest extends PHPUnit_Framework_TestCase {
-
- /**
- * @return PHPUnit_Framework_MockObject_MockObject|IDatabase
- */
- private function getMockDb() {
- return $this->getMock( 'IDatabase' );
- }
-
- /**
- * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
- */
- private function getMockLoadBalancer( $mockDb ) {
- $mock = $this->getMockBuilder( 'LoadBalancer' )
- ->disableOriginalConstructor()
- ->getMock();
- $mock->expects( $this->any() )
- ->method( 'getConnection' )
- ->will( $this->returnValue( $mockDb ) );
- return $mock;
- }
-
- private function getFakeRow( $userId, $timestamp ) {
- $fakeRow = new stdClass();
- $fakeRow->wl_user = $userId;
- $fakeRow->wl_notificationtimestamp = $timestamp;
- return $fakeRow;
- }
-
- public function testDuplicateEntry_nothingToDuplicate() {
- $mockDb = $this->getMockDb();
- $mockDb->expects( $this->exactly( 1 ) )
- ->method( 'select' )
- ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
-
- $store = new WatchedItemStore( $this->getMockLoadBalancer( $mockDb ) );
-
- $store->duplicateEntry(
- Title::newFromText( 'Old_Title' ),
- Title::newFromText( 'New_Title' )
- );
- }
-
- public function testDuplicateEntry_somethingToDuplicate() {
- $fakeRows = [
- $this->getFakeRow( 1, '20151212010101' ),
- $this->getFakeRow( 2, null ),
- ];
-
- $mockDb = $this->getMockDb();
- $mockDb->expects( $this->at( 0 ) )
- ->method( 'select' )
- ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) );
- $mockDb->expects( $this->at( 1 ) )
- ->method( 'replace' )
- ->with(
- 'watchlist',
- [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
- [
- [
- 'wl_user' => 1,
- 'wl_namespace' => 0,
- 'wl_title' => 'New_Title',
- 'wl_notificationtimestamp' => '20151212010101',
- ],
- [
- 'wl_user' => 2,
- 'wl_namespace' => 0,
- 'wl_title' => 'New_Title',
- 'wl_notificationtimestamp' => null,
- ],
- ],
- $this->isType( 'string' )
- );
-
- $store = new WatchedItemStore( $this->getMockLoadBalancer( $mockDb ) );
-
- $store->duplicateEntry(
- Title::newFromText( 'Old_Title' ),
- Title::newFromText( 'New_Title' )
- );
- }
-
-}
--- /dev/null
+<?php
+
+/**
+ * @author Addshore
+ *
+ * @covers WatchedItemStore
+ */
+class WatchedItemStoreUnitTest extends PHPUnit_Framework_TestCase {
+
+ /**
+ * @return PHPUnit_Framework_MockObject_MockObject|IDatabase
+ */
+ private function getMockDb() {
+ return $this->getMock( IDatabase::class );
+ }
+
+ /**
+ * @return PHPUnit_Framework_MockObject_MockObject|LoadBalancer
+ */
+ private function getMockLoadBalancer( $mockDb ) {
+ $mock = $this->getMockBuilder( LoadBalancer::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->expects( $this->any() )
+ ->method( 'getConnection' )
+ ->will( $this->returnValue( $mockDb ) );
+ $mock->expects( $this->any() )
+ ->method( 'getReadOnlyReason' )
+ ->will( $this->returnValue( false ) );
+ return $mock;
+ }
+
+ /**
+ * @return PHPUnit_Framework_MockObject_MockObject|BagOStuff
+ */
+ private function getMockCache() {
+ $mock = $this->getMockBuilder( BagOStuff::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->expects( $this->any() )
+ ->method( 'makeKey' )
+ ->will( $this->returnCallback( function() {
+ return implode( ':', func_get_args() );
+ } ) );
+ return $mock;
+ }
+
+ /**
+ * @param int $id
+ * @return PHPUnit_Framework_MockObject_MockObject|User
+ */
+ private function getMockNonAnonUserWithId( $id ) {
+ $mock = $this->getMock( User::class );
+ $mock->expects( $this->any() )
+ ->method( 'isAnon' )
+ ->will( $this->returnValue( false ) );
+ $mock->expects( $this->any() )
+ ->method( 'getId' )
+ ->will( $this->returnValue( $id ) );
+ return $mock;
+ }
+
+ /**
+ * @return User
+ */
+ private function getAnonUser() {
+ return User::newFromName( 'Anon_User' );
+ }
+
+ private function getFakeRow( array $rowValues ) {
+ $fakeRow = new stdClass();
+ foreach ( $rowValues as $valueName => $value ) {
+ $fakeRow->$valueName = $value;
+ }
+ return $fakeRow;
+ }
+
+ public function testGetDefaultInstance() {
+ $instanceOne = WatchedItemStore::getDefaultInstance();
+ $instanceTwo = WatchedItemStore::getDefaultInstance();
+ $this->assertSame( $instanceOne, $instanceTwo );
+ }
+
+ public function testDuplicateEntry_nothingToDuplicate() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'select' )
+ ->with(
+ 'watchlist',
+ [
+ 'wl_user',
+ 'wl_notificationtimestamp',
+ ],
+ [
+ 'wl_namespace' => 0,
+ 'wl_title' => 'Old_Title',
+ ],
+ 'WatchedItemStore::duplicateEntry',
+ [ 'FOR UPDATE' ]
+ )
+ ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ new HashBagOStuff( [ 'maxKeys' => 100 ] )
+ );
+
+ $store->duplicateEntry(
+ Title::newFromText( 'Old_Title' ),
+ Title::newFromText( 'New_Title' )
+ );
+ }
+
+ public function testDuplicateEntry_somethingToDuplicate() {
+ $fakeRows = [
+ $this->getFakeRow( [ 'wl_user' => 1, 'wl_notificationtimestamp' => '20151212010101' ] ),
+ $this->getFakeRow( [ 'wl_user' => 2, 'wl_notificationtimestamp' => null ] ),
+ ];
+
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->at( 0 ) )
+ ->method( 'select' )
+ ->with(
+ 'watchlist',
+ [
+ 'wl_user',
+ 'wl_notificationtimestamp',
+ ],
+ [
+ 'wl_namespace' => 0,
+ 'wl_title' => 'Old_Title',
+ ]
+ )
+ ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) );
+ $mockDb->expects( $this->at( 1 ) )
+ ->method( 'replace' )
+ ->with(
+ 'watchlist',
+ [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
+ [
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'New_Title',
+ 'wl_notificationtimestamp' => '20151212010101',
+ ],
+ [
+ 'wl_user' => 2,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'New_Title',
+ 'wl_notificationtimestamp' => null,
+ ],
+ ],
+ $this->isType( 'string' )
+ );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ new HashBagOStuff( [ 'maxKeys' => 100 ] )
+ );
+
+ $store->duplicateEntry(
+ Title::newFromText( 'Old_Title' ),
+ Title::newFromText( 'New_Title' )
+ );
+ }
+
+ public function testDuplicateAllAssociatedEntries_nothingToDuplicate() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->at( 0 ) )
+ ->method( 'select' )
+ ->with(
+ 'watchlist',
+ [
+ 'wl_user',
+ 'wl_notificationtimestamp',
+ ],
+ [
+ 'wl_namespace' => 0,
+ 'wl_title' => 'Old_Title',
+ ]
+ )
+ ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
+ $mockDb->expects( $this->at( 1 ) )
+ ->method( 'select' )
+ ->with(
+ 'watchlist',
+ [
+ 'wl_user',
+ 'wl_notificationtimestamp',
+ ],
+ [
+ 'wl_namespace' => 1,
+ 'wl_title' => 'Old_Title',
+ ]
+ )
+ ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ new HashBagOStuff( [ 'maxKeys' => 100 ] )
+ );
+
+ $store->duplicateAllAssociatedEntries(
+ Title::newFromText( 'Old_Title' ),
+ Title::newFromText( 'New_Title' )
+ );
+ }
+
+ public function testDuplicateAllAssociatedEntries_somethingToDuplicate() {
+ $fakeRows = [
+ $this->getFakeRow( [ 'wl_user' => 1, 'wl_notificationtimestamp' => '20151212010101' ] ),
+ ];
+
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->at( 0 ) )
+ ->method( 'select' )
+ ->with(
+ 'watchlist',
+ [
+ 'wl_user',
+ 'wl_notificationtimestamp',
+ ],
+ [
+ 'wl_namespace' => 0,
+ 'wl_title' => 'Old_Title',
+ ]
+ )
+ ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) );
+ $mockDb->expects( $this->at( 1 ) )
+ ->method( 'replace' )
+ ->with(
+ 'watchlist',
+ [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
+ [
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'New_Title',
+ 'wl_notificationtimestamp' => '20151212010101',
+ ],
+ ],
+ $this->isType( 'string' )
+ );
+ $mockDb->expects( $this->at( 2 ) )
+ ->method( 'select' )
+ ->with(
+ 'watchlist',
+ [
+ 'wl_user',
+ 'wl_notificationtimestamp',
+ ],
+ [
+ 'wl_namespace' => 1,
+ 'wl_title' => 'Old_Title',
+ ]
+ )
+ ->will( $this->returnValue( new FakeResultWrapper( $fakeRows ) ) );
+ $mockDb->expects( $this->at( 3 ) )
+ ->method( 'replace' )
+ ->with(
+ 'watchlist',
+ [ [ 'wl_user', 'wl_namespace', 'wl_title' ] ],
+ [
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 1,
+ 'wl_title' => 'New_Title',
+ 'wl_notificationtimestamp' => '20151212010101',
+ ],
+ ],
+ $this->isType( 'string' )
+ );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ new HashBagOStuff( [ 'maxKeys' => 100 ] )
+ );
+
+ $store->duplicateAllAssociatedEntries(
+ Title::newFromText( 'Old_Title' ),
+ Title::newFromText( 'New_Title' )
+ );
+ }
+
+ public function testAddWatch_nonAnonymousUser() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'insert' )
+ ->with(
+ 'watchlist',
+ [
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'Some_Page',
+ 'wl_notificationtimestamp' => null,
+ ]
+ ]
+ );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->once() )
+ ->method( 'delete' )
+ ->with( '0:Some_Page:1' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $store->addWatch(
+ $this->getMockNonAnonUserWithId( 1 ),
+ Title::newFromText( 'Some_Page' )
+ );
+ }
+
+ public function testAddWatch_anonymousUser() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->never() )
+ ->method( 'insert' );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->never() )
+ ->method( 'delete' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $store->addWatch(
+ $this->getAnonUser(),
+ Title::newFromText( 'Some_Page' )
+ );
+ }
+
+ public function testAddWatchBatch_nonAnonymousUser() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'insert' )
+ ->with(
+ 'watchlist',
+ [
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'Some_Page',
+ 'wl_notificationtimestamp' => null,
+ ],
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 1,
+ 'wl_title' => 'Some_Page',
+ 'wl_notificationtimestamp' => null,
+ ]
+ ]
+ );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->exactly( 2 ) )
+ ->method( 'delete' );
+ $mockCache->expects( $this->at( 1 ) )
+ ->method( 'delete' )
+ ->with( '0:Some_Page:1' );
+ $mockCache->expects( $this->at( 3 ) )
+ ->method( 'delete' )
+ ->with( '1:Some_Page:1' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $mockUser = $this->getMockNonAnonUserWithId( 1 );
+
+ $this->assertTrue(
+ $store->addWatchBatch(
+ [
+ [ $mockUser, new TitleValue( 0, 'Some_Page' ) ],
+ [ $mockUser, new TitleValue( 1, 'Some_Page' ) ],
+ ]
+ )
+ );
+ }
+
+ public function testAddWatchBatch_anonymousUserCombinationsAreSkipped() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'insert' )
+ ->with(
+ 'watchlist',
+ [
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'Some_Page',
+ 'wl_notificationtimestamp' => null,
+ ]
+ ]
+ );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->once() )
+ ->method( 'delete' )
+ ->with( '0:Some_Page:1' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertTrue(
+ $store->addWatchBatch(
+ [
+ [ $this->getMockNonAnonUserWithId( 1 ), new TitleValue( 0, 'Some_Page' ) ],
+ [ $this->getAnonUser(), new TitleValue( 0, 'Other_Page' ) ],
+ ]
+ )
+ );
+ }
+
+ public function testAddWatchBatchReturnsFalse_whenOnlyGivenAnonymousUserCombinations() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->never() )
+ ->method( 'insert' );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->never() )
+ ->method( 'delete' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $anonUser = $this->getAnonUser();
+ $this->assertFalse(
+ $store->addWatchBatch(
+ [
+ [ $anonUser, new TitleValue( 0, 'Some_Page' ) ],
+ [ $anonUser, new TitleValue( 1, 'Other_Page' ) ],
+ ]
+ )
+ );
+ }
+
+ public function testAddWatchBatchReturnsFalse_whenGivenEmptyList() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->never() )
+ ->method( 'insert' );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->never() )
+ ->method( 'delete' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertFalse(
+ $store->addWatchBatch( [] )
+ );
+ }
+
+ public function testLoadWatchedItem_existingItem() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'selectRow' )
+ ->with(
+ 'watchlist',
+ 'wl_notificationtimestamp',
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'SomeDbKey',
+ ]
+ )
+ ->will( $this->returnValue(
+ $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
+ ) );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->once() )
+ ->method( 'set' )
+ ->with(
+ '0:SomeDbKey:1'
+ );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $watchedItem = $store->loadWatchedItem(
+ $this->getMockNonAnonUserWithId( 1 ),
+ new TitleValue( 0, 'SomeDbKey' )
+ );
+ $this->assertInstanceOf( 'WatchedItem', $watchedItem );
+ $this->assertEquals( 1, $watchedItem->getUser()->getId() );
+ $this->assertEquals( 'SomeDbKey', $watchedItem->getLinkTarget()->getDBkey() );
+ $this->assertEquals( 0, $watchedItem->getLinkTarget()->getNamespace() );
+ }
+
+ public function testLoadWatchedItem_noItem() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'selectRow' )
+ ->with(
+ 'watchlist',
+ 'wl_notificationtimestamp',
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'SomeDbKey',
+ ]
+ )
+ ->will( $this->returnValue( [] ) );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->never() )
+ ->method( 'delete' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertFalse(
+ $store->loadWatchedItem(
+ $this->getMockNonAnonUserWithId( 1 ),
+ new TitleValue( 0, 'SomeDbKey' )
+ )
+ );
+ }
+
+ public function testLoadWatchedItem_anonymousUser() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->never() )
+ ->method( 'selectRow' );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->never() )
+ ->method( 'delete' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertFalse(
+ $store->loadWatchedItem(
+ $this->getAnonUser(),
+ new TitleValue( 0, 'SomeDbKey' )
+ )
+ );
+ }
+
+ public function testRemoveWatch_existingItem() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'delete' )
+ ->with(
+ 'watchlist',
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'SomeDbKey',
+ ]
+ );
+ $mockDb->expects( $this->once() )
+ ->method( 'affectedRows' )
+ ->will( $this->returnValue( 1 ) );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->once() )
+ ->method( 'delete' )
+ ->with( '0:SomeDbKey:1' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertTrue(
+ $store->removeWatch(
+ $this->getMockNonAnonUserWithId( 1 ),
+ new TitleValue( 0, 'SomeDbKey' )
+ )
+ );
+ }
+
+ public function testRemoveWatch_noItem() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'delete' )
+ ->with(
+ 'watchlist',
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'SomeDbKey',
+ ]
+ );
+ $mockDb->expects( $this->once() )
+ ->method( 'affectedRows' )
+ ->will( $this->returnValue( 0 ) );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->once() )
+ ->method( 'delete' )
+ ->with( '0:SomeDbKey:1' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertFalse(
+ $store->removeWatch(
+ $this->getMockNonAnonUserWithId( 1 ),
+ new TitleValue( 0, 'SomeDbKey' )
+ )
+ );
+ }
+
+ public function testRemoveWatch_anonymousUser() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->never() )
+ ->method( 'delete' );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->never() )
+ ->method( 'delete' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertFalse(
+ $store->removeWatch(
+ $this->getAnonUser(),
+ new TitleValue( 0, 'SomeDbKey' )
+ )
+ );
+ }
+
+ public function testGetWatchedItem_existingItem() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'selectRow' )
+ ->with(
+ 'watchlist',
+ 'wl_notificationtimestamp',
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'SomeDbKey',
+ ]
+ )
+ ->will( $this->returnValue(
+ $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
+ ) );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->once() )
+ ->method( 'get' )
+ ->with(
+ '0:SomeDbKey:1'
+ )
+ ->will( $this->returnValue( null ) );
+ $mockCache->expects( $this->once() )
+ ->method( 'set' )
+ ->with(
+ '0:SomeDbKey:1'
+ );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $watchedItem = $store->getWatchedItem(
+ $this->getMockNonAnonUserWithId( 1 ),
+ new TitleValue( 0, 'SomeDbKey' )
+ );
+ $this->assertInstanceOf( 'WatchedItem', $watchedItem );
+ $this->assertEquals( 1, $watchedItem->getUser()->getId() );
+ $this->assertEquals( 'SomeDbKey', $watchedItem->getLinkTarget()->getDBkey() );
+ $this->assertEquals( 0, $watchedItem->getLinkTarget()->getNamespace() );
+ }
+
+ public function testGetWatchedItem_cachedItem() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->never() )
+ ->method( 'selectRow' );
+
+ $mockUser = $this->getMockNonAnonUserWithId( 1 );
+ $linkTarget = new TitleValue( 0, 'SomeDbKey' );
+ $cachedItem = new WatchedItem( $mockUser, $linkTarget, '20151212010101' );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->once() )
+ ->method( 'get' )
+ ->with(
+ '0:SomeDbKey:1'
+ )
+ ->will( $this->returnValue( $cachedItem ) );
+ $mockCache->expects( $this->never() )
+ ->method( 'set' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertEquals(
+ $cachedItem,
+ $store->getWatchedItem(
+ $mockUser,
+ $linkTarget
+ )
+ );
+ }
+
+ public function testGetWatchedItem_noItem() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'selectRow' )
+ ->with(
+ 'watchlist',
+ 'wl_notificationtimestamp',
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'SomeDbKey',
+ ]
+ )
+ ->will( $this->returnValue( [] ) );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->never() )
+ ->method( 'set' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertFalse(
+ $store->getWatchedItem(
+ $this->getMockNonAnonUserWithId( 1 ),
+ new TitleValue( 0, 'SomeDbKey' )
+ )
+ );
+ }
+
+ public function testGetWatchedItem_anonymousUser() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->never() )
+ ->method( 'selectRow' );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->never() )
+ ->method( 'set' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertFalse(
+ $store->getWatchedItem(
+ $this->getAnonUser(),
+ new TitleValue( 0, 'SomeDbKey' )
+ )
+ );
+ }
+
+ public function testIsWatchedItem_existingItem() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'selectRow' )
+ ->with(
+ 'watchlist',
+ 'wl_notificationtimestamp',
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'SomeDbKey',
+ ]
+ )
+ ->will( $this->returnValue(
+ $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
+ ) );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->once() )
+ ->method( 'set' )
+ ->with(
+ '0:SomeDbKey:1'
+ );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertTrue(
+ $store->isWatched(
+ $this->getMockNonAnonUserWithId( 1 ),
+ new TitleValue( 0, 'SomeDbKey' )
+ )
+ );
+ }
+
+ public function testIsWatchedItem_noItem() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'selectRow' )
+ ->with(
+ 'watchlist',
+ 'wl_notificationtimestamp',
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'SomeDbKey',
+ ]
+ )
+ ->will( $this->returnValue( [] ) );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->never() )
+ ->method( 'set' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertFalse(
+ $store->isWatched(
+ $this->getMockNonAnonUserWithId( 1 ),
+ new TitleValue( 0, 'SomeDbKey' )
+ )
+ );
+ }
+
+ public function testIsWatchedItem_anonymousUser() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->never() )
+ ->method( 'selectRow' );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->never() )
+ ->method( 'set' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertFalse(
+ $store->isWatched(
+ $this->getAnonUser(),
+ new TitleValue( 0, 'SomeDbKey' )
+ )
+ );
+ }
+
+ public function testResetNotificationTimestamp_anonymousUser() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->never() )
+ ->method( 'selectRow' );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->never() )
+ ->method( 'set' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertFalse(
+ $store->resetNotificationTimestamp(
+ $this->getAnonUser(),
+ Title::newFromText( 'SomeDbKey' )
+ )
+ );
+ }
+
+ public function testResetNotificationTimestamp_noItem() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'selectRow' )
+ ->with(
+ 'watchlist',
+ 'wl_notificationtimestamp',
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'SomeDbKey',
+ ]
+ )
+ ->will( $this->returnValue( [] ) );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->never() )
+ ->method( 'set' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ $this->assertFalse(
+ $store->resetNotificationTimestamp(
+ $this->getMockNonAnonUserWithId( 1 ),
+ Title::newFromText( 'SomeDbKey' )
+ )
+ );
+ }
+
+ public function testResetNotificationTimestamp_item() {
+ $user = $this->getMockNonAnonUserWithId( 1 );
+ $title = Title::newFromText( 'SomeDbKey' );
+
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'selectRow' )
+ ->with(
+ 'watchlist',
+ 'wl_notificationtimestamp',
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'SomeDbKey',
+ ]
+ )
+ ->will( $this->returnValue(
+ $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
+ ) );
+
+ $mockCache = $this->getMockCache();
+ $mockCache->expects( $this->once() )
+ ->method( 'set' )
+ ->with(
+ '0:SomeDbKey:1',
+ $this->isInstanceOf( WatchedItem::class )
+ );
+ $mockCache->expects( $this->once() )
+ ->method( 'delete' )
+ ->with( '0:SomeDbKey:1' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ // Note: This does not actually assert the job is correct
+ $callableCallCounter = 0;
+ $mockCallback = function( $callable ) use ( &$callableCallCounter ) {
+ $callableCallCounter++;
+ $this->assertInternalType( 'callable', $callable );
+ };
+ $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
+
+ $this->assertTrue(
+ $store->resetNotificationTimestamp(
+ $user,
+ $title
+ )
+ );
+ $this->assertEquals( 1, $callableCallCounter );
+ }
+
+ public function testResetNotificationTimestamp_noItemForced() {
+ $user = $this->getMockNonAnonUserWithId( 1 );
+ $title = Title::newFromText( 'SomeDbKey' );
+
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->never() )
+ ->method( 'selectRow' );
+
+ $mockCache = $this->getMockCache();
+ $mockDb->expects( $this->never() )
+ ->method( 'set' );
+ $mockDb->expects( $this->never() )
+ ->method( 'delete' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ // Note: This does not actually assert the job is correct
+ $callableCallCounter = 0;
+ $mockCallback = function( $callable ) use ( &$callableCallCounter ) {
+ $callableCallCounter++;
+ $this->assertInternalType( 'callable', $callable );
+ };
+ $store->overrideDeferredUpdatesAddCallableUpdateCallback( $mockCallback );
+
+ $this->assertTrue(
+ $store->resetNotificationTimestamp(
+ $user,
+ $title,
+ 'force'
+ )
+ );
+ $this->assertEquals( 1, $callableCallCounter );
+ }
+
+ /**
+ * @param $text
+ * @param int $ns
+ *
+ * @return PHPUnit_Framework_MockObject_MockObject|Title
+ */
+ private function getMockTitle( $text, $ns = 0 ) {
+ $title = $this->getMock( Title::class );
+ $title->expects( $this->any() )
+ ->method( 'getText' )
+ ->will( $this->returnValue( str_replace( '_', ' ', $text ) ) );
+ $title->expects( $this->any() )
+ ->method( 'getDbKey' )
+ ->will( $this->returnValue( str_replace( '_', ' ', $text ) ) );
+ $title->expects( $this->any() )
+ ->method( 'getNamespace' )
+ ->will( $this->returnValue( $ns ) );
+ return $title;
+ }
+
+ public function testResetNotificationTimestamp_oldidSpecifiedLatestRevisionForced() {
+ $user = $this->getMockNonAnonUserWithId( 1 );
+ $oldid = 22;
+ $title = $this->getMockTitle( 'SomeTitle' );
+ $title->expects( $this->once() )
+ ->method( 'getNextRevisionID' )
+ ->with( $oldid )
+ ->will( $this->returnValue( false ) );
+
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->never() )
+ ->method( 'selectRow' );
+
+ $mockCache = $this->getMockCache();
+ $mockDb->expects( $this->never() )
+ ->method( 'set' );
+ $mockDb->expects( $this->never() )
+ ->method( 'delete' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ // Note: This does not actually assert the job is correct
+ $callableCallCounter = 0;
+ $store->overrideDeferredUpdatesAddCallableUpdateCallback(
+ function( $callable ) use ( &$callableCallCounter ) {
+ $callableCallCounter++;
+ $this->assertInternalType( 'callable', $callable );
+ }
+ );
+
+ $this->assertTrue(
+ $store->resetNotificationTimestamp(
+ $user,
+ $title,
+ 'force',
+ $oldid
+ )
+ );
+ $this->assertEquals( 1, $callableCallCounter );
+ }
+
+ public function testResetNotificationTimestamp_oldidSpecifiedNotLatestRevisionForced() {
+ $user = $this->getMockNonAnonUserWithId( 1 );
+ $oldid = 22;
+ $title = $this->getMockTitle( 'SomeDbKey' );
+ $title->expects( $this->once() )
+ ->method( 'getNextRevisionID' )
+ ->with( $oldid )
+ ->will( $this->returnValue( 33 ) );
+
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'selectRow' )
+ ->with(
+ 'watchlist',
+ 'wl_notificationtimestamp',
+ [
+ 'wl_user' => 1,
+ 'wl_namespace' => 0,
+ 'wl_title' => 'SomeDbKey',
+ ]
+ )
+ ->will( $this->returnValue(
+ $this->getFakeRow( [ 'wl_notificationtimestamp' => '20151212010101' ] )
+ ) );
+
+ $mockCache = $this->getMockCache();
+ $mockDb->expects( $this->never() )
+ ->method( 'set' );
+ $mockDb->expects( $this->never() )
+ ->method( 'delete' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ $mockCache
+ );
+
+ // Note: This does not actually assert the job is correct
+ $addUpdateCallCounter = 0;
+ $store->overrideDeferredUpdatesAddCallableUpdateCallback(
+ function( $callable ) use ( &$addUpdateCallCounter ) {
+ $addUpdateCallCounter++;
+ $this->assertInternalType( 'callable', $callable );
+ }
+ );
+
+ $getTimestampCallCounter = 0;
+ $store->overrideRevisionGetTimestampFromIdCallback(
+ function( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) {
+ $getTimestampCallCounter++;
+ $this->assertEquals( $title, $titleParam );
+ $this->assertEquals( $oldid, $oldidParam );
+ }
+ );
+
+ $this->assertTrue(
+ $store->resetNotificationTimestamp(
+ $user,
+ $title,
+ 'force',
+ $oldid
+ )
+ );
+ $this->assertEquals( 1, $addUpdateCallCounter );
+ $this->assertEquals( 1, $getTimestampCallCounter );
+ }
+
+}
--- /dev/null
+<?php
+
+/**
+ * @author Addshore
+ *
+ * @covers WatchedItem
+ */
+class WatchedItemUnitTest extends PHPUnit_Framework_TestCase {
+
+ public function provideUserTitleTimestamp() {
+ return [
+ [ User::newFromId( 111 ), Title::newFromText( 'SomeTitle' ), null ],
+ [ User::newFromId( 111 ), Title::newFromText( 'SomeTitle' ), '20150101010101' ],
+ [ User::newFromId( 111 ), new TitleValue( 0, 'TVTitle', 'frag' ), '20150101010101' ],
+ ];
+ }
+
+ /**
+ * @return PHPUnit_Framework_MockObject_MockObject|WatchedItemStore
+ */
+ private function getMockWatchedItemStore() {
+ return $this->getMockBuilder( WatchedItemStore::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ }
+
+ /**
+ * @dataProvider provideUserTitleTimestamp
+ */
+ public function testConstruction( $user, LinkTarget $linkTarget, $notifTimestamp ) {
+ $item = new WatchedItem( $user, $linkTarget, $notifTimestamp );
+
+ $this->assertSame( $user, $item->getUser() );
+ $this->assertSame( $linkTarget, $item->getLinkTarget() );
+ $this->assertSame( $notifTimestamp, $item->getNotificationTimestamp() );
+
+ // The below tests the internal WatchedItem::getTitle method
+ $this->assertInstanceOf( 'Title', $item->getTitle() );
+ $this->assertSame( $linkTarget->getDBkey(), $item->getTitle()->getDBkey() );
+ $this->assertSame( $linkTarget->getFragment(), $item->getTitle()->getFragment() );
+ $this->assertSame( $linkTarget->getNamespace(), $item->getTitle()->getNamespace() );
+ $this->assertSame( $linkTarget->getText(), $item->getTitle()->getText() );
+ }
+
+ /**
+ * @dataProvider provideUserTitleTimestamp
+ */
+ public function testFromUserTitle( $user, $linkTarget, $timestamp ) {
+ $store = $this->getMockWatchedItemStore();
+ $store->expects( $this->once() )
+ ->method( 'loadWatchedItem' )
+ ->with( $user, $linkTarget )
+ ->will( $this->returnValue( new WatchedItem( $user, $linkTarget, $timestamp ) ) );
+ WatchedItemStore::overrideDefaultInstance( $store );
+
+ $item = WatchedItem::fromUserTitle( $user, $linkTarget, User::IGNORE_USER_RIGHTS );
+
+ $this->assertEquals( $user, $item->getUser() );
+ $this->assertEquals( $linkTarget, $item->getLinkTarget() );
+ $this->assertEquals( $timestamp, $item->getNotificationTimestamp() );
+ }
+
+ /**
+ * @dataProvider provideUserTitleTimestamp
+ */
+ public function testResetNotificationTimestamp( $user, $linkTarget, $timestamp ) {
+ $force = 'XXX';
+ $oldid = 999;
+
+ $store = $this->getMockWatchedItemStore();
+ $store->expects( $this->once() )
+ ->method( 'resetNotificationTimestamp' )
+ ->with( $user, $this->isInstanceOf( Title::class ), $force, $oldid )
+ ->will( $this->returnCallback(
+ function ( $user, Title $title, $force, $oldid ) use ( $linkTarget ) {
+ /** @var LinkTarget $linkTarget */
+ $this->assertInstanceOf( 'Title', $title );
+ $this->assertSame( $linkTarget->getDBkey(), $title->getDBkey() );
+ $this->assertSame( $linkTarget->getFragment(), $title->getFragment() );
+ $this->assertSame( $linkTarget->getNamespace(), $title->getNamespace() );
+ $this->assertSame( $linkTarget->getText(), $title->getText() );
+
+ return true;
+ }
+ ) );
+ WatchedItemStore::overrideDefaultInstance( $store );
+
+ $item = new WatchedItem( $user, $linkTarget, $timestamp );
+ $item->resetNotificationTimestamp( $force, $oldid );
+ }
+
+ public function testAddWatch() {
+ $title = Title::newFromText( 'SomeTitle' );
+ $timestamp = null;
+ $checkRights = 0;
+
+ /** @var User|PHPUnit_Framework_MockObject_MockObject $user */
+ $user = $this->getMock( User::class );
+ $user->expects( $this->once() )
+ ->method( 'addWatch' )
+ ->with( $title, $checkRights );
+
+ $item = new WatchedItem( $user, $title, $timestamp, $checkRights );
+ $this->assertTrue( $item->addWatch() );
+ }
+
+ public function testRemoveWatch() {
+ $title = Title::newFromText( 'SomeTitle' );
+ $timestamp = null;
+ $checkRights = 0;
+
+ /** @var User|PHPUnit_Framework_MockObject_MockObject $user */
+ $user = $this->getMock( User::class );
+ $user->expects( $this->once() )
+ ->method( 'removeWatch' )
+ ->with( $title, $checkRights );
+
+ $item = new WatchedItem( $user, $title, $timestamp, $checkRights );
+ $this->assertTrue( $item->removeWatch() );
+ }
+
+ public function provideBooleans() {
+ return [
+ [ true ],
+ [ false ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideBooleans
+ */
+ public function testIsWatched( $returnValue ) {
+ $title = Title::newFromText( 'SomeTitle' );
+ $timestamp = null;
+ $checkRights = 0;
+
+ /** @var User|PHPUnit_Framework_MockObject_MockObject $user */
+ $user = $this->getMock( User::class );
+ $user->expects( $this->once() )
+ ->method( 'isWatched' )
+ ->with( $title, $checkRights )
+ ->will( $this->returnValue( $returnValue ) );
+
+ $item = new WatchedItem( $user, $title, $timestamp, $checkRights );
+ $this->assertEquals( $returnValue, $item->isWatched() );
+ }
+
+ public function testDuplicateEntries() {
+ $oldTitle = Title::newFromText( 'OldTitle' );
+ $newTitle = Title::newFromText( 'NewTitle' );
+
+ $store = $this->getMockWatchedItemStore();
+ $store->expects( $this->once() )
+ ->method( 'duplicateAllAssociatedEntries' )
+ ->with( $oldTitle, $newTitle );
+ WatchedItemStore::overrideDefaultInstance( $store );
+
+ WatchedItem::duplicateEntries( $oldTitle, $newTitle );
+ }
+
+ public function testBatchAddWatch() {
+ /** @var WatchedItem[] $items */
+ $items = [
+ new WatchedItem( User::newFromId( 1 ), new TitleValue( 0, 'Title1' ), null ),
+ new WatchedItem( User::newFromId( 3 ), Title::newFromText( 'Title2' ), '20150101010101' ),
+ ];
+
+ $userTargetCombinations = [];
+ foreach ( $items as $item ) {
+ $userTargetCombinations[] = [ $item->getUser(), $item->getTitle()->getSubjectPage() ];
+ $userTargetCombinations[] = [ $item->getUser(), $item->getTitle()->getTalkPage() ];
+ }
+
+ $store = $this->getMockWatchedItemStore();
+ $store->expects( $this->once() )
+ ->method( 'addWatchBatch' )
+ ->with( $userTargetCombinations );
+ WatchedItemStore::overrideDefaultInstance( $store );
+
+ WatchedItem::batchAddWatch( $items );
+ }
+
+}