From 1ff58fc74663aff7f0b784315b959173e2a43dd9 Mon Sep 17 00:00:00 2001 From: addshore Date: Mon, 20 Mar 2017 13:18:22 +0000 Subject: [PATCH] Add switch for readonly watchlists Bug: T160062 Change-Id: I70d28df48f86e8cae4e454cf3f9097c65dc1d92b --- autoload.php | 1 + includes/DefaultSettings.php | 7 + includes/ServiceWiring.php | 5 + includes/specials/SpecialEditWatchlist.php | 5 + .../watcheditem/NoWriteWatchedItemStore.php | 134 ++++++++++ .../NoWriteWatchedItemStoreUnitTest.php | 246 ++++++++++++++++++ 6 files changed, 398 insertions(+) create mode 100644 includes/watcheditem/NoWriteWatchedItemStore.php create mode 100644 tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php diff --git a/autoload.php b/autoload.php index af0b2002eb..a1c36dc822 100644 --- a/autoload.php +++ b/autoload.php @@ -1049,6 +1049,7 @@ $wgAutoloadLocalClasses = [ 'NewFilesPager' => __DIR__ . '/includes/specials/pagers/NewFilesPager.php', 'NewPagesPager' => __DIR__ . '/includes/specials/pagers/NewPagesPager.php', 'NewUsersLogFormatter' => __DIR__ . '/includes/logging/NewUsersLogFormatter.php', + 'NoWriteWatchedItemStore' => __DIR__ . '/includes/watcheditem/NoWriteWatchedItemStore.php', 'NolinesImageGallery' => __DIR__ . '/includes/gallery/NolinesImageGallery.php', 'NorthernSamiUppercaseCollation' => __DIR__ . '/includes/collation/NorthernSamiUppercaseCollation.php', 'NotRecursiveIterator' => __DIR__ . '/includes/libs/iterators/NotRecursiveIterator.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 8091428970..ee10a6d340 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -6623,6 +6623,13 @@ $wgCommandLineDarkBg = false; */ $wgReadOnly = null; +/** + * Set this to true to put the wiki watchlists into read-only mode. + * @var bool + * @since 1.31 + */ +$wgReadOnlyWatchedItemStore = false; + /** * If this lock file exists (size > 0), the wiki will be forced into read-only mode. * Its contents will be shown to users as part of the read-only warning diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index 246b8381d9..79e5b848f7 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -166,6 +166,11 @@ return [ $services->getReadOnlyMode() ); $store->setStatsdDataFactory( $services->getStatsdDataFactory() ); + + if ( $services->getMainConfig()->get( 'ReadOnlyWatchedItemStore' ) ) { + $store = new NoWriteWatchedItemStore( $store ); + } + return $store; }, diff --git a/includes/specials/SpecialEditWatchlist.php b/includes/specials/SpecialEditWatchlist.php index d2940e4a02..09ea9ea6ac 100644 --- a/includes/specials/SpecialEditWatchlist.php +++ b/includes/specials/SpecialEditWatchlist.php @@ -29,6 +29,7 @@ use MediaWiki\Linker\LinkRenderer; use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\DBReadOnlyError; /** * Provides the UI through which users can perform editing @@ -451,6 +452,10 @@ class SpecialEditWatchlist extends UnlistedSpecialPage { * Remove all titles from a user's watchlist */ private function clearWatchlist() { + if ( $this->getConfig()->get( 'ReadOnlyWatchedItemStore' ) ) { + throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' ); + } + $dbw = wfGetDB( DB_MASTER ); $dbw->delete( 'watchlist', diff --git a/includes/watcheditem/NoWriteWatchedItemStore.php b/includes/watcheditem/NoWriteWatchedItemStore.php new file mode 100644 index 0000000000..1439421037 --- /dev/null +++ b/includes/watcheditem/NoWriteWatchedItemStore.php @@ -0,0 +1,134 @@ +actualStore = $actualStore; + } + + public function countWatchedItems( User $user ) { + return $this->actualStore->countWatchedItems( $user ); + } + + public function countWatchers( LinkTarget $target ) { + return $this->actualStore->countWatchers( $target ); + } + + public function countVisitingWatchers( LinkTarget $target, $threshold ) { + return $this->actualStore->countVisitingWatchers( $target, $threshold ); + } + + public function countWatchersMultiple( array $targets, array $options = [] ) { + return $this->actualStore->countVisitingWatchersMultiple( $targets, $options ); + } + + public function countVisitingWatchersMultiple( + array $targetsWithVisitThresholds, + $minimumWatchers = null + ) { + return $this->actualStore->countVisitingWatchersMultiple( + $targetsWithVisitThresholds, + $minimumWatchers + ); + } + + public function getWatchedItem( User $user, LinkTarget $target ) { + return $this->actualStore->getWatchedItem( $user, $target ); + } + + public function loadWatchedItem( User $user, LinkTarget $target ) { + return $this->actualStore->loadWatchedItem( $user, $target ); + } + + public function getWatchedItemsForUser( User $user, array $options = [] ) { + return $this->actualStore->getWatchedItemsForUser( $user, $options ); + } + + public function isWatched( User $user, LinkTarget $target ) { + return $this->actualStore->isWatched( $user, $target ); + } + + public function getNotificationTimestampsBatch( User $user, array $targets ) { + return $this->actualStore->getNotificationTimestampsBatch( $user, $targets ); + } + + public function countUnreadNotifications( User $user, $unreadLimit = null ) { + return $this->actualStore->countUnreadNotifications( $user, $unreadLimit ); + } + + public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget ) { + throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' ); + } + + public function duplicateEntry( LinkTarget $oldTarget, LinkTarget $newTarget ) { + throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' ); + } + + public function addWatch( User $user, LinkTarget $target ) { + throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' ); + } + + public function addWatchBatchForUser( User $user, array $targets ) { + throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' ); + } + + public function removeWatch( User $user, LinkTarget $target ) { + throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' ); + } + + public function setNotificationTimestampsForUser( + User $user, + $timestamp, + array $targets = [] + ) { + throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' ); + } + + public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) { + throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' ); + } + + public function resetNotificationTimestamp( + User $user, + Title $title, + $force = '', + $oldid = 0 + ) { + throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' ); + } + +} diff --git a/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php b/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php new file mode 100644 index 0000000000..a8761e39e9 --- /dev/null +++ b/tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php @@ -0,0 +1,246 @@ +getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->never() )->method( 'addWatch' ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $this->setExpectedException( DBReadOnlyError::class ); + $noWriteService->addWatch( $this->getTestSysop()->getUser(), new TitleValue( 0, 'Foo' ) ); + } + + public function testAddWatchBatchForUser() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->never() )->method( 'addWatchBatchForUser' ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $this->setExpectedException( DBReadOnlyError::class ); + $noWriteService->addWatchBatchForUser( $this->getTestSysop()->getUser(), [] ); + } + + public function testRemoveWatch() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->never() )->method( 'removeWatch' ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $this->setExpectedException( DBReadOnlyError::class ); + $noWriteService->removeWatch( $this->getTestSysop()->getUser(), new TitleValue( 0, 'Foo' ) ); + } + + public function testSetNotificationTimestampsForUser() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->never() )->method( 'setNotificationTimestampsForUser' ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $this->setExpectedException( DBReadOnlyError::class ); + $noWriteService->setNotificationTimestampsForUser( + $this->getTestSysop()->getUser(), + 'timestamp', + [] + ); + } + + public function testUpdateNotificationTimestamp() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->never() )->method( 'updateNotificationTimestamp' ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $this->setExpectedException( DBReadOnlyError::class ); + $noWriteService->updateNotificationTimestamp( + $this->getTestSysop()->getUser(), + new TitleValue( 0, 'Foo' ), + 'timestamp' + ); + } + + public function testResetNotificationTimestamp() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->never() )->method( 'resetNotificationTimestamp' ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $this->setExpectedException( DBReadOnlyError::class ); + $noWriteService->resetNotificationTimestamp( + $this->getTestSysop()->getUser(), + Title::newFromText( 'Foo' ) + ); + } + + public function testCountWatchedItems() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() )->method( 'countWatchedItems' )->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->countWatchedItems( + $this->getTestSysop()->getUser() + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testCountWatchers() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() )->method( 'countWatchers' )->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->countWatchers( + new TitleValue( 0, 'Foo' ) + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testCountVisitingWatchers() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() ) + ->method( 'countVisitingWatchers' ) + ->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->countVisitingWatchers( + new TitleValue( 0, 'Foo' ), + 9 + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testCountWatchersMultiple() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() ) + ->method( 'countVisitingWatchersMultiple' ) + ->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->countWatchersMultiple( + [ new TitleValue( 0, 'Foo' ) ], + [] + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testCountVisitingWatchersMultiple() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() ) + ->method( 'countVisitingWatchersMultiple' ) + ->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->countVisitingWatchersMultiple( + [ [ new TitleValue( 0, 'Foo' ), 99 ] ], + 11 + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testGetWatchedItem() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() )->method( 'getWatchedItem' )->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->getWatchedItem( + $this->getTestSysop()->getUser(), + new TitleValue( 0, 'Foo' ) + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testLoadWatchedItem() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() )->method( 'loadWatchedItem' )->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->loadWatchedItem( + $this->getTestSysop()->getUser(), + new TitleValue( 0, 'Foo' ) + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testGetWatchedItemsForUser() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() ) + ->method( 'getWatchedItemsForUser' ) + ->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->getWatchedItemsForUser( + $this->getTestSysop()->getUser(), + [] + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testIsWatched() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() )->method( 'isWatched' )->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->isWatched( + $this->getTestSysop()->getUser(), + new TitleValue( 0, 'Foo' ) + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testGetNotificationTimestampsBatch() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() ) + ->method( 'getNotificationTimestampsBatch' ) + ->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->getNotificationTimestampsBatch( + $this->getTestSysop()->getUser(), + [ new TitleValue( 0, 'Foo' ) ] + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testCountUnreadNotifications() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $innerService->expects( $this->once() ) + ->method( 'countUnreadNotifications' ) + ->willReturn( __METHOD__ ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $return = $noWriteService->countUnreadNotifications( + $this->getTestSysop()->getUser(), + 88 + ); + $this->assertEquals( __METHOD__, $return ); + } + + public function testDuplicateAllAssociatedEntries() { + /** @var WatchedItemStoreInterface|PHPUnit_Framework_MockObject_MockObject $innerService */ + $innerService = $this->getMockForAbstractClass( WatchedItemStoreInterface::class ); + $noWriteService = new NoWriteWatchedItemStore( $innerService ); + + $this->setExpectedException( DBReadOnlyError::class ); + $noWriteService->duplicateAllAssociatedEntries( + new TitleValue( 0, 'Foo' ), + new TitleValue( 0, 'Bar' ) + ); + } + +} -- 2.20.1