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.
+** EmailNotification::updateWatchlistTimestamp was deprecated.
+** User::getWatchedItem was removed.
== Compatibility ==
Hooks can return three possible values:
- * true: the hook has operated successfully
+ * No return value (or null): the hook has operated successfully. Previously,
+ true was required. This is the default since MediaWiki 1.23.
* "some string": an error occurred; processing should stop and the error
should be shown to the user
* false: the hook has successfully done the work necessary and the calling
$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 );
$repl2[] = preg_quote( substr( $prot, 0, -1 ), '/' );
}
}
- $repl2 = $repl2 ? '/\b(' . join( '|', $repl2 ) . '):/i' : '/^(?!)/';
+ $repl2 = $repl2 ? '/\b(' . implode( '|', $repl2 ) . '):/i' : '/^(?!)/';
}
$text = substr( strtr( "\n$text", $repl ), 1 );
$text = preg_replace( $repl2, '$1:', $text );
$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(
foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) {
$this->addVaryHeader( $header, $options );
}
- return 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) );
+ return 'Vary: ' . implode( ', ', array_keys( $this->mVaryHeader ) );
}
/**
if ( $this->mEnableClientCache ) {
if (
- $config->get( 'UseSquid' ) && !SessionManager::getGlobalSession()->isPersistent() &&
- !$this->isPrintable() && $this->mCdnMaxage != 0 && !$this->haveCacheVaryCookies()
+ $config->get( 'UseSquid' ) &&
+ !$response->hasCookies() &&
+ !SessionManager::getGlobalSession()->isPersistent() &&
+ !$this->isPrintable() &&
+ $this->mCdnMaxage != 0 &&
+ !$this->haveCacheVaryCookies()
) {
if ( $config->get( 'UseESI' ) ) {
# We'll purge the proxy cache explicitly, but require end user agents
// make sure we are using the right one. To detect changes over the course
// of a request, we remember a fingerprint of the config used to create the
// codec singleton, and re-create it if the fingerprint doesn't match.
- $fingerprint = spl_object_hash( $wgContLang ) . '|' . join( '+', $wgLocalInterwikis );
+ $fingerprint = spl_object_hash( $wgContLang ) . '|' . implode( '+', $wgLocalInterwikis );
if ( $fingerprint !== $titleCodecFingerprint ) {
$titleCodec = null;
$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' );
+ }
+ $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;
}
- return $instance;
+
+ $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;
+ }
+
+ /**
+ * @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
+ */
+ public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) {
+ $dbw = $this->loadBalancer->getConnection( DB_MASTER, [ 'watchlist' ] );
+ $res = $dbw->select( [ 'watchlist' ],
+ [ 'wl_user' ],
+ [
+ 'wl_user != ' . intval( $editor->getId() ),
+ 'wl_namespace' => $target->getNamespace(),
+ 'wl_title' => $target->getDBkey(),
+ 'wl_notificationtimestamp IS NULL',
+ ], __METHOD__
+ );
+
+ $watchers = [];
+ foreach ( $res as $row ) {
+ $watchers[] = intval( $row->wl_user );
+ }
+
+ if ( $watchers ) {
+ // Update wl_notificationtimestamp for all watching users except the editor
+ $fname = __METHOD__;
+ $dbw->onTransactionIdle(
+ function () use ( $dbw, $timestamp, $watchers, $target, $fname ) {
+ $dbw->update( 'watchlist',
+ [ /* SET */
+ 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp )
+ ], [ /* WHERE */
+ 'wl_user' => $watchers,
+ 'wl_namespace' => $target->getNamespace(),
+ 'wl_title' => $target->getDBkey(),
+ ], $fname
+ );
+ }
+ );
+ }
+
+ $this->loadBalancer->reuseConnection( $dbw );
+
+ return $watchers;
+ }
+
+ /**
+ * 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() );
}
/**
public function clearCookie( $name, $options = [] ) {
$this->setCookie( $name, '', time() - 31536000 /* 1 year */, $options );
}
+
+ /**
+ * Checks whether this request is performing cookie operations
+ *
+ * @return bool
+ * @since 1.27
+ */
+ public function hasCookies() {
+ return (bool)self::$setCookies;
+ }
}
/**
->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' );
}
$qs = $k;
$msg = self::escapeWikiText( $v );
if ( is_array( $msg ) ) {
- $msg = join( " ", $msg );
+ $msg = implode( ' ', $msg );
}
}
$parent = $module;
$manager = $parent->getModuleManager();
if ( $manager === null ) {
- $errorPath = join( '+', array_slice( $parts, 0, $i ) );
+ $errorPath = implode( '+', array_slice( $parts, 0, $i ) );
$this->dieUsage( "The module \"$errorPath\" has no submodules", 'badmodule' );
}
$module = $manager->getModule( $parts[$i] );
if ( $module === null ) {
- $errorPath = $i ? join( '+', array_slice( $parts, 0, $i ) ) : $parent->getModuleName();
+ $errorPath = $i ? implode( '+', array_slice( $parts, 0, $i ) ) : $parent->getModuleName();
$this->dieUsage(
"The module \"$errorPath\" does not have a submodule \"{$parts[$i]}\"",
'badmodule'
$p = $this->getModulePrefix();
$intersection = array_intersect( array_keys( array_filter( $params,
- [ $this, "parameterNotEmpty" ] ) ), $required );
+ [ $this, 'parameterNotEmpty' ] ) ), $required );
if ( count( $intersection ) > 1 ) {
$this->dieUsage(
$p = $this->getModulePrefix();
$intersection = array_intersect( array_keys( array_filter( $params,
- [ $this, "parameterNotEmpty" ] ) ), $required );
+ [ $this, 'parameterNotEmpty' ] ) ), $required );
if ( count( $intersection ) > 1 ) {
$this->dieUsage(
$p = $this->getModulePrefix();
$intersection = array_intersect(
- array_keys( array_filter( $params, [ $this, "parameterNotEmpty" ] ) ),
+ array_keys( array_filter( $params, [ $this, 'parameterNotEmpty' ] ) ),
$required
);
*/
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':
ApiBase::dieDebug(
__METHOD__,
"Boolean param $encParamName's default is set to '$default'. " .
- "Boolean parameters must default to false."
+ 'Boolean parameters must default to false.'
);
}
if ( $value !== null ) {
$this->dieUsage(
"File upload param $encParamName is not a file upload; " .
- "be sure to use multipart/form-data for your POST and include " .
- "a filename in the Content-Disposition header.",
+ 'be sure to use multipart/form-data for your POST and include ' .
+ 'a filename in the Content-Disposition header.',
"badupload_{$encParamName}"
);
}
if ( count( $unknown ) ) {
if ( $allowMultiple ) {
$s = count( $unknown ) > 1 ? 's' : '';
- $vals = implode( ", ", $unknown );
+ $vals = implode( ', ', $unknown );
$this->setWarning( "Unrecognized value$s for parameter '$valueName': $vals" );
} else {
$this->dieUsage(
],
'badaccess-group0' => [
'code' => 'permissiondenied',
- 'info' => "Permission denied"
+ 'info' => 'Permission denied'
], // Generic permission denied message
'badaccess-groups' => [
'code' => 'permissiondenied',
- 'info' => "Permission denied"
+ 'info' => 'Permission denied'
],
'titleprotected' => [
'code' => 'protectedtitle',
- 'info' => "This title has been protected from creation"
+ 'info' => 'This title has been protected from creation'
],
'nocreate-loggedin' => [
'code' => 'cantcreate',
],
'confirmedittext' => [
'code' => 'confirmemail',
- 'info' => "You must confirm your email address before you can edit"
+ 'info' => 'You must confirm your email address before you can edit'
],
'blockedtext' => [
'code' => 'blocked',
- 'info' => "You have been blocked from editing"
+ 'info' => 'You have been blocked from editing'
],
'autoblockedtext' => [
'code' => 'autoblocked',
- 'info' => "Your IP address has been blocked automatically, because it was used by a blocked user"
+ 'info' => 'Your IP address has been blocked automatically, because it was used by a blocked user'
],
// Miscellaneous interface messages
],
'alreadyrolled' => [
'code' => 'alreadyrolled',
- 'info' => "The page you tried to rollback was already rolled back"
+ 'info' => 'The page you tried to rollback was already rolled back'
],
'cantrollback' => [
'code' => 'onlyauthor',
- 'info' => "The page you tried to rollback only has one author"
+ 'info' => 'The page you tried to rollback only has one author'
],
'readonlytext' => [
'code' => 'readonly',
- 'info' => "The wiki is currently in read-only mode"
+ 'info' => 'The wiki is currently in read-only mode'
],
'sessionfailure' => [
'code' => 'badtoken',
- 'info' => "Invalid token" ],
+ 'info' => 'Invalid token' ],
'cannotdelete' => [
'code' => 'cantdelete',
'info' => "Couldn't delete \"\$1\". Maybe it was deleted already by someone else"
],
'immobile_namespace' => [
'code' => 'immobilenamespace',
- 'info' => "You tried to move pages from or to a namespace that is protected from moving"
+ 'info' => 'You tried to move pages from or to a namespace that is protected from moving'
],
'articleexists' => [
'code' => 'articleexists',
- 'info' => "The destination article already exists and is not a redirect to the source article"
+ 'info' => 'The destination article already exists and is not a redirect to the source article'
],
'protectedpage' => [
'code' => 'protectedpage',
],
'hookaborted' => [
'code' => 'hookaborted',
- 'info' => "The modification you tried to make was aborted by an extension hook"
+ 'info' => 'The modification you tried to make was aborted by an extension hook'
],
'cantmove-titleprotected' => [
'code' => 'protectedtitle',
- 'info' => "The destination article has been protected from creation"
+ 'info' => 'The destination article has been protected from creation'
],
'imagenocrossnamespace' => [
'code' => 'nonfilenamespace',
],
// 'badarticleerror' => shouldn't happen
// 'badtitletext' => shouldn't happen
- 'ip_range_invalid' => [ 'code' => 'invalidrange', 'info' => "Invalid IP range" ],
+ 'ip_range_invalid' => [ 'code' => 'invalidrange', 'info' => 'Invalid IP range' ],
'range_block_disabled' => [
'code' => 'rangedisabled',
- 'info' => "Blocking IP ranges has been disabled"
+ 'info' => 'Blocking IP ranges has been disabled'
],
'nosuchusershort' => [
'code' => 'nosuchuser',
'info' => "The user you specified doesn't exist"
],
- 'badipaddress' => [ 'code' => 'invalidip', 'info' => "Invalid IP address specified" ],
- 'ipb_expiry_invalid' => [ 'code' => 'invalidexpiry', 'info' => "Invalid expiry time" ],
+ 'badipaddress' => [ 'code' => 'invalidip', 'info' => 'Invalid IP address specified' ],
+ 'ipb_expiry_invalid' => [ 'code' => 'invalidexpiry', 'info' => 'Invalid expiry time' ],
'ipb_already_blocked' => [
'code' => 'alreadyblocked',
- 'info' => "The user you tried to block was already blocked"
+ 'info' => 'The user you tried to block was already blocked'
],
'ipb_blocked_as_range' => [
'code' => 'blockedasrange',
],
'ipb_cant_unblock' => [
'code' => 'cantunblock',
- 'info' => "The block you specified was not found. It may have been unblocked already"
+ 'info' => 'The block you specified was not found. It may have been unblocked already'
],
'mailnologin' => [
'code' => 'cantsend',
- 'info' => "You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email"
+ 'info' => 'You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email'
],
'ipbblocked' => [
'code' => 'ipbblocked',
],
'usermaildisabled' => [
'code' => 'usermaildisabled',
- 'info' => "User email has been disabled"
+ 'info' => 'User email has been disabled'
],
'blockedemailuser' => [
'code' => 'blockedfrommail',
- 'info' => "You have been blocked from sending email"
+ 'info' => 'You have been blocked from sending email'
],
'notarget' => [
'code' => 'notarget',
- 'info' => "You have not specified a valid target for this action"
+ 'info' => 'You have not specified a valid target for this action'
],
'noemail' => [
'code' => 'noemail',
- 'info' => "The user has not specified a valid email address, or has chosen not to receive email from other users"
+ 'info' => 'The user has not specified a valid email address, or has chosen not to receive email from other users'
],
'rcpatroldisabled' => [
'code' => 'patroldisabled',
- 'info' => "Patrolling is disabled on this wiki"
+ 'info' => 'Patrolling is disabled on this wiki'
],
'markedaspatrollederror-noautopatrol' => [
'code' => 'noautopatrol',
// API-specific messages
'readrequired' => [
'code' => 'readapidenied',
- 'info' => "You need read permission to use this module"
+ 'info' => 'You need read permission to use this module'
],
'writedisabled' => [
'code' => 'noapiwrite',
],
'unblock-notarget' => [
'code' => 'notarget',
- 'info' => "Either the id or the user parameter must be set"
+ 'info' => 'Either the id or the user parameter must be set'
],
'unblock-idanduser' => [
'code' => 'idanduser',
],
'createonly-exists' => [
'code' => 'articleexists',
- 'info' => "The article you tried to create has been created already"
+ 'info' => 'The article you tried to create has been created already'
],
'nocreate-missing' => [
'code' => 'missingtitle',
'noedit' => [ 'code' => 'noedit', 'info' => "You don't have permission to edit pages" ],
'wasdeleted' => [
'code' => 'pagedeleted',
- 'info' => "The page has been deleted since you fetched its timestamp"
+ 'info' => 'The page has been deleted since you fetched its timestamp'
],
'blankpage' => [
'code' => 'emptypage',
- 'info' => "Creating new, empty pages is not allowed"
+ 'info' => 'Creating new, empty pages is not allowed'
],
- 'editconflict' => [ 'code' => 'editconflict', 'info' => "Edit conflict detected" ],
- 'hashcheckfailed' => [ 'code' => 'badmd5', 'info' => "The supplied MD5 hash was incorrect" ],
+ 'editconflict' => [ 'code' => 'editconflict', 'info' => 'Edit conflict detected' ],
+ 'hashcheckfailed' => [ 'code' => 'badmd5', 'info' => 'The supplied MD5 hash was incorrect' ],
'missingtext' => [
'code' => 'notext',
- 'info' => "One of the text, appendtext, prependtext and undo parameters must be set"
+ 'info' => 'One of the text, appendtext, prependtext and undo parameters must be set'
],
'emptynewsection' => [
'code' => 'emptynewsection',
// Messages from WikiPage::doEit(]
'edit-hook-aborted' => [
'code' => 'edit-hook-aborted',
- 'info' => "Your edit was aborted by an ArticleSave hook"
+ 'info' => 'Your edit was aborted by an ArticleSave hook'
],
'edit-gone-missing' => [
'code' => 'edit-gone-missing',
'info' => "The page you tried to edit doesn't seem to exist anymore"
],
- 'edit-conflict' => [ 'code' => 'editconflict', 'info' => "Edit conflict detected" ],
+ 'edit-conflict' => [ 'code' => 'editconflict', 'info' => 'Edit conflict detected' ],
'edit-already-exists' => [
'code' => 'edit-already-exists',
'info' => 'It seems the page you tried to create already exist'
Hooks::run( 'APIGetDescription', [ &$this, &$desc ] );
$desc = self::escapeWikiText( $desc );
if ( is_array( $desc ) ) {
- $desc = join( "\n", $desc );
+ $desc = implode( "\n", $desc );
} else {
$desc = (string)$desc;
}
}
return $line;
}, $d );
- $d = join( ' ', $d );
+ $d = implode( ' ', $d );
}
if ( isset( $settings[ApiBase::PARAM_HELP_MSG] ) ) {
$examples
];
}
- $msg .= "Example" . ( count( $examples ) > 1 ? 's' : '' ) . ":\n";
+ $msg .= 'Example' . ( count( $examples ) > 1 ? 's' : '' ) . ":\n";
foreach ( $examples as $k => $v ) {
if ( is_numeric( $k ) ) {
$msg .= " $v\n";
} else {
$msgExample = " $v";
}
- $msgExample .= ":";
+ $msgExample .= ':';
$msg .= wordwrap( $msgExample, 100, "\n" ) . "\n $k\n";
}
}
* @return string
*/
private function indentExampleText( $item ) {
- return " " . $item;
+ return ' ' . $item;
}
/**
if ( isset( $paramSettings[self::PARAM_REQUIRED] )
&& $paramSettings[self::PARAM_REQUIRED]
) {
- $desc .= $paramPrefix . "This parameter is required";
+ $desc .= $paramPrefix . 'This parameter is required';
}
$type = isset( $paramSettings[self::PARAM_TYPE] )
}
break;
case 'upload':
- $desc .= $paramPrefix . "Must be posted as a file upload using multipart/form-data";
+ $desc .= $paramPrefix . 'Must be posted as a file upload using multipart/form-data';
break;
}
}
if ( !$isArray
|| $isArray && count( $type ) > self::LIMIT_SML1
) {
- $desc .= $paramPrefix . "Maximum number of values " .
- self::LIMIT_SML1 . " (" . self::LIMIT_SML2 . " for bots)";
+ $desc .= $paramPrefix . 'Maximum number of values ' .
+ self::LIMIT_SML1 . ' (' . self::LIMIT_SML2 . ' for bots)';
}
}
}
}
$paramName = $module->encodeParamName( $paramName );
if ( is_array( $paramValue ) ) {
- $paramValue = join( '|', $paramValue );
+ $paramValue = implode( '|', $paramValue );
}
$this->continuationData[$name][$paramName] = $paramValue;
}
$name = $module->getModuleName();
$paramName = $module->encodeParamName( $paramName );
if ( is_array( $paramValue ) ) {
- $paramValue = join( '|', $paramValue );
+ $paramValue = implode( '|', $paramValue );
}
$this->generatorContinuationData[$name][$paramName] = $paramValue;
}
$data += $kvp;
}
$data += $this->generatorParams;
- $generatorKeys = join( '|', array_keys( $this->generatorParams ) );
+ $generatorKeys = implode( '|', array_keys( $this->generatorParams ) );
} elseif ( $this->generatorContinuationData ) {
// All the generator-using modules are complete, but the
// generator isn't. Continue the generator and restart the
}
$data += $generatorParams;
$finishedModules = array_diff( $finishedModules, $this->generatedModules );
- $generatorKeys = join( '|', array_keys( $generatorParams ) );
+ $generatorKeys = implode( '|', array_keys( $generatorParams ) );
$batchcomplete = true;
} else {
// Generator and prop modules are all done. Mark it so.
// Set 'continue' if any continuation data is set or if the generator
// still needs to run
if ( $data || $generatorKeys !== '-' ) {
- $data['continue'] = $generatorKeys . '||' . join( '|', $finishedModules );
+ $data['continue'] = $generatorKeys . '||' . implode( '|', $finishedModules );
}
return [ $data, $batchcomplete ];
$section = $params['section'];
if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) {
$this->dieUsage( "The section parameter must be a valid section id or 'new'",
- "invalidsection" );
+ 'invalidsection' );
}
$content = $pageObj->getContent();
if ( $section !== '0' && $section != 'new'
!isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
$this->setWarning( "Property 'modules' was set but not 'jsconfigvars' " .
"or 'encodedjsconfigvars'. Configuration variables are necessary " .
- "for proper module usage." );
+ 'for proper module usage.' );
}
}
}
return '<p>' . htmlspecialchars( $revision->getUserText() ) . $msg .
htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) .
- "</p>\n<hr />\n<div>" . $html . "</div>";
+ "</p>\n<hr />\n<div>" . $html . '</div>';
}
return '';
'level' => $level,
'anchor' => $anchor,
'line' => $header,
- 'number' => join( '.', $tocnumber ),
+ 'number' => implode( '.', $tocnumber ),
'index' => false,
];
if ( empty( $options['noheader'] ) ) {
->parse();
}
if ( $extra ) {
- $info[] = join( ' ', $extra );
+ $info[] = implode( ' ', $extra );
}
}
}
}
if ( $description ) {
- $description = join( '', $description );
+ $description = implode( '', $description );
$description = preg_replace( '!\s*</([oud]l)>\s*<\1>\s*!', "\n", $description );
$help['parameters'] .= Html::rawElement( 'dd',
[ 'class' => 'description' ], $description );
Hooks::run( 'APIHelpModifyOutput', [ $module, &$help, $suboptions, &$haveModules ] );
- $out .= join( "\n", $help );
+ $out .= implode( "\n", $help );
}
return $out;
$tmpFile = TempFSFile::factory( 'rotate_', $ext );
$dstPath = $tmpFile->getPath();
$err = $handler->rotate( $file, [
- "srcPath" => $srcPath,
- "dstPath" => $dstPath,
- "rotation" => $rotation
+ 'srcPath' => $srcPath,
+ 'dstPath' => $dstPath,
+ 'rotation' => $rotation
] );
if ( !$err ) {
$comment = wfMessage(
if ( $module->needsToken() === true ) {
throw new MWException(
"Module '{$module->getModuleName()}' must be updated for the new token handling. " .
- "See documentation for ApiBase::needsToken for details."
+ 'See documentation for ApiBase::needsToken for details.'
);
}
if ( $module->needsToken() ) {
$this->dieUsageMsg( 'writerequired' );
} elseif ( $this->getRequest()->getHeader( 'Promise-Non-Write-API-Action' ) ) {
$this->dieUsage(
- "Promise-Non-Write-API-Action HTTP header cannot be sent to write API modules",
+ 'Promise-Non-Write-API-Action HTTP header cannot be sent to write API modules',
'promised-nonwrite-api'
);
}
// If a majority of slaves are too lagged then disallow writes
$slaveCount = wfGetLB()->getServerCount() - 1;
if ( $numLagged >= ceil( $slaveCount / 2 ) ) {
- $laggedServers = join( ', ', $laggedServers );
+ $laggedServers = implode( ', ', $laggedServers );
wfDebugLog(
'api-readonly',
"Api request failed as read only because the following DBs are lagged: $laggedServers"
$ret = $this->getRequest()->getVal( $name );
if ( $ret === null ) {
if ( $this->getRequest()->getArray( $name ) !== null ) {
- // See bug 10262 for why we don't just join( '|', ... ) the
+ // See bug 10262 for why we don't just implode( '|', ... ) the
// array.
$this->setWarning(
"Parameter '$name' uses unsupported PHP array syntax"
'level' => $level,
'anchor' => 'main/datatypes',
'line' => $header,
- 'number' => join( '.', $tocnumber ),
+ 'number' => implode( '.', $tocnumber ),
'index' => false,
];
}
'level' => $level,
'anchor' => 'main/credits',
'line' => $header,
- 'number' => join( '.', $tocnumber ),
+ 'number' => implode( '.', $tocnumber ),
'index' => false,
];
}
->inLanguage( 'en' )
->text();
$groups = User::getGroupsWithPermission( $right );
- $msg .= "* " . $right . " *\n $rightsMsg" .
+ $msg .= '* ' . $right . " *\n $rightsMsg" .
"\nGranted to:\n " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n";
}
$ns = implode( '|', SearchEngine::defaultNamespaces() );
if ( !$ns ) {
- $ns = "0";
+ $ns = '0';
}
switch ( $type ) {
case 'userjs':
// Allow non-default preferences prefixed with 'userjs-', to be set by user scripts
if ( strlen( $key ) > 255 ) {
- $validation = "key too long (no more than 255 bytes allowed)";
- } elseif ( preg_match( "/[^a-zA-Z0-9_-]/", $key ) !== 0 ) {
- $validation = "invalid key (only a-z, A-Z, 0-9, _, - allowed)";
+ $validation = 'key too long (no more than 255 bytes allowed)';
+ } elseif ( preg_match( '/[^a-zA-Z0-9_-]/', $key ) !== 0 ) {
+ $validation = 'invalid key (only a-z, A-Z, 0-9, _, - allowed)';
} else {
$validation = true;
}
break;
case 'special':
- $validation = "cannot be set by this module";
+ $validation = 'cannot be set by this module';
break;
case 'unused':
default:
- $validation = "not a valid preference";
+ $validation = 'not a valid preference';
break;
}
if ( $validation === true ) {
'special', 'missingIds', 'missingRevIds', 'missingTitles', 'interwikiTitles' ]
) {
$result = [];
- if ( in_array( "invalidTitles", $invalidChecks ) ) {
+ if ( in_array( 'invalidTitles', $invalidChecks ) ) {
self::addValues( $result, $this->getInvalidTitlesAndReasons(), 'invalid' );
}
- if ( in_array( "special", $invalidChecks ) ) {
+ if ( in_array( 'special', $invalidChecks ) ) {
self::addValues( $result, $this->getSpecialTitles(), 'special', 'title' );
}
- if ( in_array( "missingIds", $invalidChecks ) ) {
+ if ( in_array( 'missingIds', $invalidChecks ) ) {
self::addValues( $result, $this->getMissingPageIDs(), 'missing', 'pageid' );
}
- if ( in_array( "missingRevIds", $invalidChecks ) ) {
+ if ( in_array( 'missingRevIds', $invalidChecks ) ) {
self::addValues( $result, $this->getMissingRevisionIDs(), 'missing', 'revid' );
}
- if ( in_array( "missingTitles", $invalidChecks ) ) {
+ if ( in_array( 'missingTitles', $invalidChecks ) ) {
self::addValues( $result, $this->getMissingTitles(), 'missing' );
}
- if ( in_array( "interwikiTitles", $invalidChecks ) ) {
+ if ( in_array( 'interwikiTitles', $invalidChecks ) ) {
self::addValues( $result, $this->getInterwikiTitlesAsResult() );
}
foreach ( $msgs as $m ) {
$ret[] = $m->setContext( $this->context )->text();
}
- $res[$key] = join( "\n\n", $ret );
+ $res[$key] = implode( "\n\n", $ret );
if ( $joinLists ) {
$res[$key] = preg_replace( '!^(([*#:;])[^\n]*)\n\n(?=\2)!m', "$1\n", $res[$key] );
}
foreach ( $msgs as $m ) {
$ret[] = $m->setContext( $this->context )->parseAsBlock();
}
- $ret = join( "\n", $ret );
+ $ret = implode( "\n", $ret );
if ( $joinLists ) {
$ret = preg_replace( '!\s*</([oud]l)>\s*<\1>\s*!', "\n", $ret );
}
$this->section = $params['section'];
if ( !preg_match( '/^((T-)?\d+|new)$/', $this->section ) ) {
$this->dieUsage(
- "The section parameter must be a valid section id or 'new'", "invalidsection"
+ 'The section parameter must be a valid section id or "new"', 'invalidsection'
);
}
} else {
if ( isset( $prop['modules'] ) &&
!isset( $prop['jsconfigvars'] ) && !isset( $prop['encodedjsconfigvars'] ) ) {
- $this->setWarning( "Property 'modules' was set but not 'jsconfigvars' " .
- "or 'encodedjsconfigvars'. Configuration variables are necessary " .
- "for proper module usage." );
+ $this->setWarning( 'Property "modules" was set but not "jsconfigvars" ' .
+ 'or "encodedjsconfigvars". Configuration variables are necessary ' .
+ 'for proper module usage.' );
}
if ( isset( $prop['indicators'] ) ) {
if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
- $this->dieUsage( "parsetree is only supported for wikitext content", "notwikitext" );
+ $this->dieUsage( 'parsetree is only supported for wikitext content', 'notwikitext' );
}
$wgParser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
// Not cached (save or load)
$section = $content->getSection( $this->section );
if ( $section === false ) {
- $this->dieUsage( "There is no section {$this->section} in " . $what, 'nosuchsection' );
+ $this->dieUsage( "There is no section {$this->section} in $what", 'nosuchsection' );
}
if ( $section === null ) {
- $this->dieUsage( "Sections are not supported by " . $what, 'nosuchsection' );
+ $this->dieUsage( "Sections are not supported by $what", 'nosuchsection' );
$section = false;
}
if ( $matches ) {
$p = $this->getModulePrefix();
$this->dieUsage(
- "Cannot use {$p}prop=" . join( '|', array_keys( $matches ) ) . " with {$p}unique",
+ "Cannot use {$p}prop=" . implode( '|', array_keys( $matches ) ) . " with {$p}unique",
'params'
);
}
// Note we must keep the parameters for the first query constant
// This may be overridden at a later step
$title = $row->{$this->bl_title};
- $this->continueStr = join( '|', array_slice( $this->cont, 0, 2 ) ) .
+ $this->continueStr = implode( '|', array_slice( $this->cont, 0, 2 ) ) .
"|$ns|$title|{$row->from_ns}|{$row->page_id}";
break;
}
[ 'query', $this->getModuleName() ],
$idx, array_diff_key( $arr, [ 'redirlinks' => '' ] ) );
if ( !$fit ) {
- $this->continueStr = join( '|', array_slice( $this->cont, 0, 6 ) ) .
+ $this->continueStr = implode( '|', array_slice( $this->cont, 0, 6 ) ) .
"|$pageID";
break;
}
[ 'query', $this->getModuleName(), $idx, 'redirlinks' ],
null, $redir );
if ( !$fit ) {
- $this->continueStr = join( '|', array_slice( $this->cont, 0, 6 ) ) .
+ $this->continueStr = implode( '|', array_slice( $this->cont, 0, 6 ) ) .
"|$pageID|$key";
break;
}
foreach ( $sortby as $field => $v ) {
$cont[] = $row->$field;
}
- $this->setContinueEnumParameter( 'continue', join( '|', $cont ) );
+ $this->setContinueEnumParameter( 'continue', implode( '|', $cont ) );
}
public function getCacheMode( $params ) {
return [
'prop' => [
- ApiBase::PARAM_DFLT => join( '|', $props ),
+ ApiBase::PARAM_DFLT => implode( '|', $props ),
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => $props,
],
foreach ( $paramList as $name => $value ) {
if ( !$h->validateParam( $name, $value ) ) {
- $this->dieUsage( "Invalid value for {$p}urlparam ($name=$value)", "urlparam" );
+ $this->dieUsage( "Invalid value for {$p}urlparam ($name=$value)", 'urlparam' );
}
}
$vals['parsetree'] = $xml;
} else {
$vals['badcontentformatforparsetree'] = true;
- $this->setWarning( "Conversion to XML is supported for wikitext only, " .
+ $this->setWarning( 'Conversion to XML is supported for wikitext only, ' .
$title->getPrefixedDBkey() .
- " uses content model " . $content->getModel() );
+ ' uses content model ' . $content->getModel() );
}
}
}
ParserOptions::newFromContext( $this->getContext() )
);
} else {
- $this->setWarning( "Template expansion is supported for wikitext only, " .
+ $this->setWarning( 'Template expansion is supported for wikitext only, ' .
$title->getPrefixedDBkey() .
- " uses content model " . $content->getModel() );
+ ' uses content model ' . $content->getModel() );
$vals['badcontentformat'] = true;
$text = false;
}
$result = $this->getResult();
if ( !$params['filekey'] && !$params['sessionkey'] ) {
- $this->dieUsage( "One of filekey or sessionkey must be supplied", 'nofilekey' );
+ $this->dieUsage( 'One of filekey or sessionkey must be supplied', 'nofilekey' );
}
// Alias sessionkey to filekey, but give an existing filekey precedence.
}
// @todo Update exception handling here to understand current getFile exceptions
} catch ( UploadStashFileNotFoundException $e ) {
- $this->dieUsage( "File not found: " . $e->getMessage(), "invalidsessiondata" );
+ $this->dieUsage( 'File not found: ' . $e->getMessage(), 'invalidsessiondata' );
} catch ( UploadStashBadPathException $e ) {
- $this->dieUsage( "Bad path: " . $e->getMessage(), "invalidsessiondata" );
+ $this->dieUsage( 'Bad path: ' . $e->getMessage(), 'invalidsessiondata' );
}
}
if ( !$conflicts ) {
$arr[$name] += $value;
} else {
- $keys = join( ', ', array_keys( $conflicts ) );
+ $keys = implode( ', ', array_keys( $conflicts ) );
throw new RuntimeException(
"Conflicting keys ($keys) when attempting to merge element $name"
);
$value = $value->serializeForApiResult();
if ( is_object( $value ) ) {
throw new UnexpectedValueException(
- get_class( $oldValue ) . "::serializeForApiResult() returned an object of class " .
+ get_class( $oldValue ) . '::serializeForApiResult() returned an object of class ' .
get_class( $value )
);
}
return self::validateValue( $value );
} catch ( Exception $ex ) {
throw new UnexpectedValueException(
- get_class( $oldValue ) . "::serializeForApiResult() returned an invalid value: " .
+ get_class( $oldValue ) . '::serializeForApiResult() returned an invalid value: ' .
$ex->getMessage(),
0,
$ex
}
$value = $tmp;
} elseif ( is_float( $value ) && !is_finite( $value ) ) {
- throw new InvalidArgumentException( "Cannot add non-finite floats to ApiResult" );
+ throw new InvalidArgumentException( 'Cannot add non-finite floats to ApiResult' );
} elseif ( is_string( $value ) ) {
$value = $wgContLang->normalize( $value );
} elseif ( $value !== null && !is_scalar( $value ) ) {
) {
throw new RuntimeException(
"Attempting to set content element as $name when " . $arr[self::META_CONTENT] .
- " is already set as the content element"
+ ' is already set as the content element'
);
}
$arr[self::META_CONTENT] = $name;
$tmp = [];
return $tmp;
default:
- $fail = join( '.', array_slice( $path, 0, $i + 1 ) );
+ $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
throw new InvalidArgumentException( "Path $fail does not exist" );
}
}
if ( !is_array( $ret[$k] ) ) {
- $fail = join( '.', array_slice( $path, 0, $i + 1 ) );
+ $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
throw new InvalidArgumentException( "Path $fail is not an array" );
}
$ret = &$ret[$k];
if ( !ContentHandler::getForModelID( $params['contentmodel'] )
->isSupportedFormat( $params['contentformat'] )
) {
- $this->dieUsage( "Unsupported content model/format", 'badmodelformat' );
+ $this->dieUsage( 'Unsupported content model/format', 'badmodelformat' );
}
// Trim and fix newlines so the key SHA1's match (see RequestContext::getText())
$baseRev->getId()
);
if ( !$editContent ) {
- $this->dieUsage( "Could not merge updated section.", 'replacefailed' );
+ $this->dieUsage( 'Could not merge updated section.', 'replacefailed' );
}
if ( $currentRev->getId() == $baseRev->getId() ) {
// Base revision was still the latest; nothing to merge
public function execute() {
$this->setWarning(
- "action=tokens has been deprecated. Please use action=query&meta=tokens instead."
+ 'action=tokens has been deprecated. Please use action=query&meta=tokens instead.'
);
- $this->logFeatureUsage( "action=tokens" );
+ $this->logFeatureUsage( 'action=tokens' );
$params = $this->extractRequestParams();
$res = [
];
ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
- $msg = "Filetype not permitted: ";
+ $msg = 'Filetype not permitted: ';
if ( isset( $verification['blacklistedExt'] ) ) {
- $msg .= join( ', ', $verification['blacklistedExt'] );
+ $msg .= implode( ', ', $verification['blacklistedExt'] );
$extradata['blacklisted'] = array_values( $verification['blacklistedExt'] );
ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
} else {
$this->dieUsage( 'No such filekey: ' . $e->getMessage(), 'stashnosuchfilekey' );
break;
default:
- $this->dieUsage( $exceptionType . ": " . $e->getMessage(), 'stasherror' );
+ $this->dieUsage( $exceptionType . ': ' . $e->getMessage(), 'stasherror' );
break;
}
}
if ( $this->mParams['async'] ) {
$progress = UploadBase::getSessionStatus( $this->getUser(), $this->mParams['filekey'] );
if ( $progress && $progress['result'] === 'Poll' ) {
- $this->dieUsage( "Upload from stash already in progress.", 'publishfailed' );
+ $this->dieUsage( 'Upload from stash already in progress.', 'publishfailed' );
}
UploadBase::setSessionStatus(
$this->getUser(),
if ( $extraParams ) {
$p = $this->getModulePrefix();
$this->dieUsage(
- "The parameter {$p}title can not be used with " . implode( ", ", $extraParams ),
+ "The parameter {$p}title can not be used with " . implode( ', ', $extraParams ),
'invalidparammix'
);
}
);
}
return Html::rawElement( 'table', [ 'class' => 'mw-json' ],
- Html::rawElement( 'tbody', [], join( '', $rows ) )
+ Html::rawElement( 'tbody', [], implode( '', $rows ) )
);
}
);
}
return Html::rawElement( 'table', [ 'class' => 'mw-json' ],
- Html::rawElement( 'tbody', [], join( "\n", $rows ) )
+ Html::rawElement( 'tbody', [], implode( "\n", $rows ) )
);
}
$table = $this->tableName( $table );
// "INSERT INTO tables (a, b, c)"
- $sql = "INSERT INTO " . $table . " (" . join( ',', array_keys( $row ) ) . ')';
+ $sql = "INSERT INTO " . $table . " (" . implode( ',', array_keys( $row ) ) . ')';
$sql .= " VALUES (";
// for each value, append ":key"
/**
* Perform pre-connection load ratio adjustment.
- * @param array $loads
+ * @param array &$loads
* @param string|bool $group The selected query group. Default: false
* @param string|bool $wiki Default: false
*/
* @since 1.23
*/
protected static function getIIProps() {
- return join( '|', self::$imageInfoProps );
+ return implode( '|', self::$imageInfoProps );
}
/**
$prev_title = $row->cur_title;
$prev_namespace = $row->cur_namespace;
}
- $sql = "DELETE FROM $cur WHERE cur_id IN ( " . join( ',', $deleteId ) . ')';
+ $sql = "DELETE FROM $cur WHERE cur_id IN ( " . implode( ',', $deleteId ) . ')';
$this->db->query( $sql, __METHOD__ );
$this->output( wfTimestamp( TS_DB ) );
$this->output( "......<b>Deleted</b> " . $this->db->affectedRows() . " records.\n" );
protected $editor;
/**
+ * @deprecated since 1.27 use WatchedItemStore::updateNotificationTimestamp directly
+ *
* @param User $editor The editor that triggered the update. Their notification
* timestamp will not be updated(they have already seen it)
* @param LinkTarget $linkTarget The link target of the title to update timestamps for
* @param string $timestamp Set the update timestamp to this value
+ *
* @return int[] Array of user IDs
*/
public static function updateWatchlistTimestamp(
LinkTarget $linkTarget,
$timestamp
) {
- global $wgEnotifWatchlist, $wgShowUpdatedMarker;
-
- if ( !$wgEnotifWatchlist && !$wgShowUpdatedMarker ) {
+ // wfDeprecated( __METHOD__, '1.27' );
+ $config = RequestContext::getMain()->getConfig();
+ if ( !$config->get( 'EnotifWatchlist' ) && !$config->get( 'ShowUpdatedMarker' ) ) {
return [];
}
-
- $dbw = wfGetDB( DB_MASTER );
- $res = $dbw->select( [ 'watchlist' ],
- [ 'wl_user' ],
- [
- 'wl_user != ' . intval( $editor->getID() ),
- 'wl_namespace' => $linkTarget->getNamespace(),
- 'wl_title' => $linkTarget->getDBkey(),
- 'wl_notificationtimestamp IS NULL',
- ], __METHOD__
+ return WatchedItemStore::getDefaultInstance()->updateNotificationTimestamp(
+ $editor,
+ $linkTarget,
+ $timestamp
);
-
- $watchers = [];
- foreach ( $res as $row ) {
- $watchers[] = intval( $row->wl_user );
- }
-
- if ( $watchers ) {
- // Update wl_notificationtimestamp for all watching users except the editor
- $fname = __METHOD__;
- $dbw->onTransactionIdle(
- function () use ( $dbw, $timestamp, $watchers, $linkTarget, $fname ) {
- $dbw->update( 'watchlist',
- [ /* SET */
- 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp )
- ], [ /* WHERE */
- 'wl_user' => $watchers,
- 'wl_namespace' => $linkTarget->getNamespace(),
- 'wl_title' => $linkTarget->getDBkey(),
- ], $fname
- );
- }
- );
- }
-
- return $watchers;
}
/**
}
// update wl_notificationtimestamp for watchers
- $watchers = self::updateWatchlistTimestamp( $editor, $title, $timestamp );
+ $config = RequestContext::getMain()->getConfig();
+ $watchers = [];
+ if ( $config->get( 'EnotifWatchlist' ) || $config->get( 'ShowUpdatedMarker' ) ) {
+ $watchers = WatchedItemStore::getDefaultInstance()->updateNotificationTimestamp(
+ $editor,
+ $title,
+ $timestamp
+ );
+ }
$sendEmail = true;
// $watchers deals with $wgEnotifWatchlist.
$anchor = $safeHeadline;
$legacyAnchor = $legacyHeadline;
if ( isset( $refers[$arrayKey] ) ) {
- // @codingStandardsIgnoreStart
+ // @codingStandardsIgnoreStart
for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
// @codingStandardsIgnoreEnd
$anchor .= "_$i";
$refers[$arrayKey] = true;
}
if ( $legacyHeadline !== false && isset( $refers[$legacyArrayKey] ) ) {
- // @codingStandardsIgnoreStart
+ // @codingStandardsIgnoreStart
for ( $i = 2; isset( $refers["${legacyArrayKey}_$i"] ); ++$i );
// @codingStandardsIgnoreEnd
$legacyAnchor .= "_$i";
$sections[0] = $sections[0] . $toc . "\n";
}
- $full .= join( '', $sections );
+ $full .= implode( '', $sections );
if ( $this->mForceTocPosition ) {
return str_replace( '<!--MWTOC-->', $toc, $full );
}
}
- $searchon = $this->db->addQuotes( join( ',', $q ) );
+ $searchon = $this->db->addQuotes( implode( ',', $q ) );
$field = $this->getIndexField( $fulltext );
return "$field, $searchon";
}
if ( $missingKeys ) {
$this->logger->info( 'Session "{session}": Missing metadata: {missing}', [
'session' => $info,
- 'missing' => join( ', ', $missingKeys ),
+ 'missing' => implode( ', ', $missingKeys ),
] );
return false;
}
$new = true;
}
if ( is_array( $salt ) ) {
- $salt = join( '|', $salt );
+ $salt = implode( '|', $salt );
}
return new Token( $secret, (string)$salt, $new );
}
// Make sure there's exactly one
if ( count( $infos ) > 1 ) {
throw new \UnexpectedValueException(
- 'Multiple empty sessions tied for top priority: ' . join( ', ', $infos )
+ 'Multiple empty sessions tied for top priority: ' . implode( ', ', $infos )
);
} elseif ( count( $infos ) < 1 ) {
throw new \UnexpectedValueException( 'No provider could provide an empty session!' );
\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;
}
if ( count( $retInfos ) > 1 ) {
$ex = new \OverflowException(
- 'Multiple sessions for this request tied for top priority: ' . join( ', ', $retInfos )
+ 'Multiple sessions for this request tied for top priority: ' . implode( ', ', $retInfos )
);
$ex->sessionInfos = $retInfos;
throw $ex;
* @return string HTML fragment
*/
public static function capturePath( Title $title, IContextSource $context ) {
- global $wgOut, $wgTitle, $wgRequest, $wgUser, $wgLang;
-
- // Save current globals
- $oldTitle = $wgTitle;
- $oldOut = $wgOut;
- $oldRequest = $wgRequest;
- $oldUser = $wgUser;
- $oldLang = $wgLang;
-
- // Set the globals to the current context
+ global $wgTitle, $wgOut, $wgRequest, $wgUser, $wgLang;
+ $main = RequestContext::getMain();
+
+ // Save current globals and main context
+ $glob = [
+ 'title' => $wgTitle,
+ 'output' => $wgOut,
+ 'request' => $wgRequest,
+ 'user' => $wgUser,
+ 'language' => $wgLang,
+ ];
+ $ctx = [
+ 'title' => $main->getTitle(),
+ 'output' => $main->getOutput(),
+ 'request' => $main->getRequest(),
+ 'user' => $main->getUser(),
+ 'language' => $main->getLanguage(),
+ ];
+
+ // Override
$wgTitle = $title;
$wgOut = $context->getOutput();
$wgRequest = $context->getRequest();
$wgUser = $context->getUser();
$wgLang = $context->getLanguage();
+ $main->setTitle( $title );
+ $main->setOutput( $context->getOutput() );
+ $main->setRequest( $context->getRequest() );
+ $main->setUser( $context->getUser() );
+ $main->setLanguage( $context->getLanguage() );
// The useful part
$ret = self::executePath( $title, $context, true );
- // And restore the old globals
- $wgTitle = $oldTitle;
- $wgOut = $oldOut;
- $wgRequest = $oldRequest;
- $wgUser = $oldUser;
- $wgLang = $oldLang;
+ // Restore old globals and context
+ $wgTitle = $glob['title'];
+ $wgOut = $glob['output'];
+ $wgRequest = $glob['request'];
+ $wgUser = $glob['user'];
+ $wgLang = $glob['language'];
+ $main->setTitle( $ctx['title'] );
+ $main->setOutput( $ctx['output'] );
+ $main->setRequest( $ctx['request'] );
+ $main->setUser( $ctx['user'] );
+ $main->setLanguage( $ctx['language'] );
return $ret;
}
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 );
}
/**
$invalidKeys = array_diff( $keys, $validKeys );
if ( $invalidKeys ) {
throw new InvalidArgumentException(
- 'Array contains invalid keys: ' . join( ', ', $invalidKeys )
+ 'Array contains invalid keys: ' . implode( ', ', $invalidKeys )
);
}
$missingKeys = array_diff( $neededKeys, $keys );
if ( $missingKeys ) {
throw new InvalidArgumentException(
- 'Array is missing required keys: ' . join( ', ', $missingKeys )
+ 'Array is missing required keys: ' . implode( ', ', $missingKeys )
);
}
if ( !preg_match( "/[a-zA-Z_]/us", $word ) ) {
switch ( $case ) {
case 'genitive': # родительный падеж
- if ( ( join( '', array_slice( $ar[0], -4 ) ) == 'вики' )
- || ( join( '', array_slice( $ar[0], -4 ) ) == 'Вики' )
+ if ( ( implode( '', array_slice( $ar[0], -4 ) ) == 'вики' )
+ || ( implode( '', array_slice( $ar[0], -4 ) ) == 'Вики' )
) {
- } elseif ( join( '', array_slice( $ar[0], -2 ) ) == 'ї' ) {
- $word = join( '', array_slice( $ar[0], 0, -2 ) ) . 'їѩ';
+ } elseif ( implode( '', array_slice( $ar[0], -2 ) ) == 'ї' ) {
+ $word = implode( '', array_slice( $ar[0], 0, -2 ) ) . 'їѩ';
}
break;
case 'accusative': # винительный падеж
if ( !preg_match( "/[a-zA-Z_]/us", $word ) ) {
switch ( $case ) {
case 'genitive': # սեռական հոլով
- if ( join( '', array_slice( $ar[0], -1 ) ) == 'ա' ) {
- $word = join( '', array_slice( $ar[0], 0, -1 ) ) . 'այի';
- } elseif ( join( '', array_slice( $ar[0], -1 ) ) == 'ո' ) {
- $word = join( '', array_slice( $ar[0], 0, -1 ) ) . 'ոյի';
- } elseif ( join( '', array_slice( $ar[0], -4 ) ) == 'գիրք' ) {
- $word = join( '', array_slice( $ar[0], 0, -4 ) ) . 'գրքի';
+ if ( implode( '', array_slice( $ar[0], -1 ) ) == 'ա' ) {
+ $word = implode( '', array_slice( $ar[0], 0, -1 ) ) . 'այի';
+ } elseif ( implode( '', array_slice( $ar[0], -1 ) ) == 'ո' ) {
+ $word = implode( '', array_slice( $ar[0], 0, -1 ) ) . 'ոյի';
+ } elseif ( implode( '', array_slice( $ar[0], -4 ) ) == 'գիրք' ) {
+ $word = implode( '', array_slice( $ar[0], 0, -4 ) ) . 'գրքի';
} else {
$word .= 'ի';
}
if ( !preg_match( "/[a-zA-Z_]/us", $word ) ) {
switch ( $case ) {
case 'genitive': # родовий відмінок
- if ( join( '', array_slice( $ar[0], -2 ) ) === 'ія' ) {
- $word = join( '', array_slice( $ar[0], 0, -2 ) ) . 'ії';
- } elseif ( join( '', array_slice( $ar[0], -2 ) ) === 'ти' ) {
- $word = join( '', array_slice( $ar[0], 0, -2 ) ) . 'т';
- } elseif ( join( '', array_slice( $ar[0], -2 ) ) === 'ди' ) {
- $word = join( '', array_slice( $ar[0], 0, -2 ) ) . 'дів';
- } elseif ( join( '', array_slice( $ar[0], -3 ) ) === 'ник' ) {
- $word = join( '', array_slice( $ar[0], 0, -3 ) ) . 'ника';
+ if ( implode( '', array_slice( $ar[0], -2 ) ) === 'ія' ) {
+ $word = implode( '', array_slice( $ar[0], 0, -2 ) ) . 'ії';
+ } elseif ( implode( '', array_slice( $ar[0], -2 ) ) === 'ти' ) {
+ $word = implode( '', array_slice( $ar[0], 0, -2 ) ) . 'т';
+ } elseif ( implode( '', array_slice( $ar[0], -2 ) ) === 'ди' ) {
+ $word = implode( '', array_slice( $ar[0], 0, -2 ) ) . 'дів';
+ } elseif ( implode( '', array_slice( $ar[0], -3 ) ) === 'ник' ) {
+ $word = implode( '', array_slice( $ar[0], 0, -3 ) ) . 'ника';
}
break;
case 'accusative': # знахідний відмінок
- if ( join( '', array_slice( $ar[0], -2 ) ) === 'ія' ) {
- $word = join( '', array_slice( $ar[0], 0, -2 ) ) . 'ію';
+ if ( implode( '', array_slice( $ar[0], -2 ) ) === 'ія' ) {
+ $word = implode( '', array_slice( $ar[0], 0, -2 ) ) . 'ію';
}
break;
}
$ret .= sprintf( "%s times: function %s(%s) :\n",
$res['count'],
$res['function'],
- join( ', ', $res['arguments'] )
+ implode( ', ', $res['arguments'] )
);
$ret .= sprintf( " %6.2fms (%6.2fms each)\n",
$res['delta'] * 1000,
$this->addOption( 'regex', 'regex to filter variables with', false, true );
$this->addOption( 'iregex', 'same as --regex but case insensitive', false, true );
$this->addOption( 'settings', 'Space-separated list of wg* variables', false, true );
- $this->addOption( 'format', join( ', ', self::$outFormats ), false, true );
+ $this->addOption( 'format', implode( ', ', self::$outFormats ), false, true );
}
protected function validateParamsAndArgs() {
$this->output( "\nUpdating site statistics..." );
$counter->refresh();
$this->output( "done.\n" );
+ } else {
+ $this->output( "\nTo update the site statistics table, run the script "
+ . "with the --update option.\n" );
}
if ( $this->hasOption( 'active' ) ) {
--- /dev/null
+<?php
+
+/**
+ * @author Addshore
+ *
+ * @group Database
+ *
+ * @covers WatchedItem
+ */
+class WatchedItemIntegrationTest extends MediaWikiTestCase {
+
+ public function setUp() {
+ parent::setUp();
+ self::$users['WatchedItemIntegrationTestUser']
+ = new TestUser( 'WatchedItemIntegrationTestUser' );
+ }
+
+ private function getUser() {
+ return self::$users['WatchedItemIntegrationTestUser']->getUser();
+ }
+
+ public function testWatchAndUnWatchItem() {
+ $user = $this->getUser();
+ $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
+ // Cleanup after previous tests
+ WatchedItem::fromUserTitle( $user, $title )->removeWatch();
+
+ $this->assertFalse(
+ WatchedItem::fromUserTitle( $user, $title )->isWatched(),
+ 'Page should not initially be watched'
+ );
+ WatchedItem::fromUserTitle( $user, $title )->addWatch();
+ $this->assertTrue(
+ WatchedItem::fromUserTitle( $user, $title )->isWatched(),
+ 'Page should be watched'
+ );
+ WatchedItem::fromUserTitle( $user, $title )->removeWatch();
+ $this->assertFalse(
+ WatchedItem::fromUserTitle( $user, $title )->isWatched(),
+ 'Page should be unwatched'
+ );
+ }
+
+ public function testUpdateAndResetNotificationTimestamp() {
+ $user = $this->getUser();
+ $otherUser = ( new TestUser( 'WatchedItemIntegrationTestUser_otherUser' ) )->getUser();
+ $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
+ WatchedItem::fromUserTitle( $user, $title )->addWatch();
+ $this->assertNull( WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() );
+
+ EmailNotification::updateWatchlistTimestamp( $otherUser, $title, '20150202010101' );
+ $this->assertEquals(
+ '20150202010101',
+ WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp()
+ );
+
+ WatchedItem::fromUserTitle( $user, $title )->resetNotificationTimestamp();
+ $this->assertNull( WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp() );
+ }
+
+ public function testDuplicateAllAssociatedEntries() {
+ $user = $this->getUser();
+ $titleOld = Title::newFromText( 'WatchedItemIntegrationTestPageOld' );
+ $titleNew = Title::newFromText( 'WatchedItemIntegrationTestPageNew' );
+ WatchedItem::fromUserTitle( $user, $titleOld->getSubjectPage() )->addWatch();
+ WatchedItem::fromUserTitle( $user, $titleOld->getTalkPage() )->addWatch();
+ // Cleanup after previous tests
+ WatchedItem::fromUserTitle( $user, $titleNew->getSubjectPage() )->removeWatch();
+ WatchedItem::fromUserTitle( $user, $titleNew->getTalkPage() )->removeWatch();
+
+ WatchedItem::duplicateEntries( $titleOld, $titleNew );
+
+ $this->assertTrue(
+ WatchedItem::fromUserTitle( $user, $titleOld->getSubjectPage() )->isWatched()
+ );
+ $this->assertTrue(
+ WatchedItem::fromUserTitle( $user, $titleOld->getTalkPage() )->isWatched()
+ );
+ $this->assertTrue(
+ WatchedItem::fromUserTitle( $user, $titleNew->getSubjectPage() )->isWatched()
+ );
+ $this->assertTrue(
+ WatchedItem::fromUserTitle( $user, $titleNew->getTalkPage() )->isWatched()
+ );
+ }
+
+ public function testIsWatched_falseOnNotAllowed() {
+ $user = $this->getUser();
+ $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
+ WatchedItem::fromUserTitle( $user, $title )->addWatch();
+
+ $this->assertTrue( WatchedItem::fromUserTitle( $user, $title )->isWatched() );
+ $user->mRights = [];
+ $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->isWatched() );
+ }
+
+ public function testGetNotificationTimestamp_falseOnNotAllowed() {
+ $user = $this->getUser();
+ $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
+ WatchedItem::fromUserTitle( $user, $title )->addWatch();
+ WatchedItem::fromUserTitle( $user, $title )->resetNotificationTimestamp();
+
+ $this->assertEquals(
+ null,
+ WatchedItem::fromUserTitle( $user, $title )->getNotificationTimestamp()
+ );
+ $user->mRights = [];
+ $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->isWatched() );
+ }
+
+ public function testRemoveWatch_falseOnNotAllowed() {
+ $user = $this->getUser();
+ $title = Title::newFromText( 'WatchedItemIntegrationTestPage' );
+ WatchedItem::fromUserTitle( $user, $title )->addWatch();
+
+ $previousRights = $user->mRights;
+ $user->mRights = [];
+ $this->assertFalse( WatchedItem::fromUserTitle( $user, $title )->removeWatch() );
+ $user->mRights = $previousRights;
+ $this->assertTrue( WatchedItem::fromUserTitle( $user, $title )->removeWatch() );
+ }
+
+}
--- /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 testUpdateAndResetNotificationTimestamp() {
+ $user = $this->getUser();
+ $otherUser = ( new TestUser( 'WatchedItemStoreIntegrationTestUser_otherUser' ) )->getUser();
+ $title = Title::newFromText( 'WatchedItemStoreIntegrationTestPage' );
+ $store = WatchedItemStore::getDefaultInstance();
+ $store->addWatch( $user, $title );
+ $this->assertNull( $store->loadWatchedItem( $user, $title )->getNotificationTimestamp() );
+
+ $store->updateNotificationTimestamp( $otherUser, $title, '20150202010101' );
+ $this->assertEquals(
+ '20150202010101',
+ $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 );
+ }
+
+ public function testUpdateNotificationTimestamp_watchersExist() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'select' )
+ ->with(
+ [ 'watchlist' ],
+ [ 'wl_user' ],
+ [
+ 'wl_user != 1',
+ 'wl_namespace' => 0,
+ 'wl_title' => 'SomeDbKey',
+ 'wl_notificationtimestamp IS NULL'
+ ]
+ )
+ ->will(
+ $this->returnValue( [
+ $this->getFakeRow( [ 'wl_user' => '2' ] ),
+ $this->getFakeRow( [ 'wl_user' => '3' ] )
+ ] )
+ );
+ $mockDb->expects( $this->once() )
+ ->method( 'onTransactionIdle' )
+ ->with( $this->isType( 'callable' ) )
+ ->will( $this->returnCallback( function( $callable ) {
+ $callable();
+ } ) );
+ $mockDb->expects( $this->once() )
+ ->method( 'update' )
+ ->with(
+ 'watchlist',
+ [ 'wl_notificationtimestamp' => null ],
+ [
+ 'wl_user' => [ 2, 3 ],
+ 'wl_namespace' => 0,
+ 'wl_title' => 'SomeDbKey',
+ ]
+ );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ new HashBagOStuff( [ 'maxKeys' => 100 ] )
+ );
+
+ $this->assertEquals(
+ [ 2, 3 ],
+ $store->updateNotificationTimestamp(
+ $this->getMockNonAnonUserWithId( 1 ),
+ new TitleValue( 0, 'SomeDbKey' ),
+ '20151212010101'
+ )
+ );
+ }
+
+ public function testUpdateNotificationTimestamp_noWatchers() {
+ $mockDb = $this->getMockDb();
+ $mockDb->expects( $this->once() )
+ ->method( 'select' )
+ ->with(
+ [ 'watchlist' ],
+ [ 'wl_user' ],
+ [
+ 'wl_user != 1',
+ 'wl_namespace' => 0,
+ 'wl_title' => 'SomeDbKey',
+ 'wl_notificationtimestamp IS NULL'
+ ]
+ )
+ ->will(
+ $this->returnValue( [] )
+ );
+ $mockDb->expects( $this->never() )
+ ->method( 'onTransactionIdle' );
+ $mockDb->expects( $this->never() )
+ ->method( 'update' );
+
+ $store = new WatchedItemStore(
+ $this->getMockLoadBalancer( $mockDb ),
+ new HashBagOStuff( [ 'maxKeys' => 100 ] )
+ );
+
+ $watchers = $store->updateNotificationTimestamp(
+ $this->getMockNonAnonUserWithId( 1 ),
+ new TitleValue( 0, 'SomeDbKey' ),
+ '20151212010101'
+ );
+ $this->assertInternalType( 'array', $watchers );
+ $this->assertEmpty( $watchers );
+ }
+
+}
--- /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 );
+ }
+
+}
$points[] = $point['x'] . ',' . $point['y'];
}
- return join( " ", $points );
+ return implode( " ", $points );
}
/**
$components[] = mt_rand( 0, 255 );
}
- return 'rgb(' . join( ', ', $components ) . ')';
+ return 'rgb(' . implode( ', ', $components ) . ')';
}
/**
* in an instance property rather than APC.
*/
class ArrayBackedMemoizedCallable extends MemoizedCallable {
- public $cache = [];
+ private $cache = [];
protected function fetchResult( $key, &$success ) {
if ( array_key_exists( $key, $this->cache ) ) {
$this->readAttribute( $a, 'callableName' ),
$this->readAttribute( $b, 'callableName' )
);
+
+ $c = new ArrayBackedMemoizedCallable( function () {
+ return rand();
+ } );
+ $this->assertEquals( $c->invokeArgs(), $c->invokeArgs(), 'memoized random' );
}
/**
}
/* Python code to extract a header and convert to PHP format:
- * print '"%s"' % ''.join( '\\x%02X' % ord(c) for c in urllib.urlopen(url).read(36) )
+ * print '"%s"' % ''.implode( '\\x%02X' % ord(c) for c in urllib.urlopen(url).read(36) )
*/
$gotWarnings = count( $warnings );
if ( $gotWarnings !== $expectWarnings ) {
$this->fail( "Expected $expectWarnings warning(s), but got $gotWarnings:\n" .
- join( "\n", $warnings )
+ implode( "\n", $warnings )
);
}
}
foreach ( $globals as $k => $v ) {
$g[] = "$k=" . var_export( $v, 1 );
}
- $k = "Module $path with " . join( ', ', $g );
+ $k = "Module $path with " . implode( ', ', $g );
$ret[$k] = [ $path, $globals ];
}
}
$langKey = $languageCode . '_' . $key;
$messages[$langKey] = $template;
$tests[] = [
- 'name' => $languageCode . ' ' . $key . ' ' . join( ',', $args ),
+ 'name' => $languageCode . ' ' . $key . ' ' . implode( ',', $args ),
'key' => $langKey,
'args' => $args,
'result' => $result,