From 715cbe468ba52af5e7cac75008147bce03deb651 Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Tue, 11 Oct 2016 16:17:22 -0400 Subject: [PATCH] Add hooks for WatchedItemQueryService / ApiQueryWatchlist In order for an extension to add data to ApiQueryWatchlist, we need to provide a way to allow it to manipulate the database query made by WatchedItemQueryService. We also need some hooks in ApiQueryWatchlist to handle the marshalling of data to and from WatchedItemQueryService. To better handle hooking, this also moves some of the continuation logic from ApiQueryWatchlist to WatchedItemQueryService. Bug: T147939 Change-Id: Ie45376980f92da964a579887b28175c00fd8f57e --- autoload.php | 1 + docs/hooks.txt | 16 ++ includes/WatchedItemQueryService.php | 70 ++++- includes/WatchedItemQueryServiceExtension.php | 54 ++++ includes/api/ApiQueryWatchlist.php | 34 +-- .../WatchedItemQueryServiceUnitTest.php | 266 ++++++++++++++++-- 6 files changed, 387 insertions(+), 54 deletions(-) create mode 100644 includes/WatchedItemQueryServiceExtension.php diff --git a/autoload.php b/autoload.php index b96250d1fd..bbf4bd0359 100644 --- a/autoload.php +++ b/autoload.php @@ -1532,6 +1532,7 @@ $wgAutoloadLocalClasses = [ 'WatchAction' => __DIR__ . '/includes/actions/WatchAction.php', 'WatchedItem' => __DIR__ . '/includes/WatchedItem.php', 'WatchedItemQueryService' => __DIR__ . '/includes/WatchedItemQueryService.php', + 'WatchedItemQueryServiceExtension' => __DIR__ . '/includes/WatchedItemQueryServiceExtension.php', 'WatchedItemStore' => __DIR__ . '/includes/WatchedItemStore.php', 'WatchlistCleanup' => __DIR__ . '/maintenance/cleanupWatchlist.php', 'WebInstaller' => __DIR__ . '/includes/installer/WebInstaller.php', diff --git a/docs/hooks.txt b/docs/hooks.txt index 562d7b4b2f..ea662ccf3d 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -565,6 +565,18 @@ your callback to the $tokenFunctions array and return true (returning false makes no sense). &$tokenFunctions: array(action => callback) +'ApiQueryWatchlistExtractOutputData': Extract row data for ApiQueryWatchlist. +$module: ApiQueryWatchlist instance +$watchedItem: WatchedItem instance +$recentChangeInfo: Array of recent change info data +&$vals: Associative array of data to be output for the row + +'ApiQueryWatchlistPrepareWatchedItemQueryServiceOptions': Populate the options +to be passed from ApiQueryWatchlist to WatchedItemQueryService. +$module: ApiQueryWatchlist instance +$params: Array of parameters, as would be returned by $module->extractRequestParams() +&$options: Array of options for WatchedItemQueryService::getWatchedItemsWithRecentChangeInfo() + 'ApiRsdServiceApis': Add or remove APIs from the RSD services list. Each service should have its own entry in the $apis array and have a unique name, passed as key for the array that represents the service data. In this data array, the @@ -3735,6 +3747,10 @@ used to alter the SQL query which gets the list of wanted pages. &$user: user that watched &$page: WikiPage object watched +'WatchedItemQueryServiceExtensions': Create a WatchedItemQueryServiceExtension. +&$extensions: Add WatchedItemQueryServiceExtension objects to this array +$watchedItemQueryService: Service object + 'WatchlistEditorBeforeFormRender': Before building the Special:EditWatchlist form, used to manipulate the list of pages or preload data based on that list. &$watchlistInfo: array of watchlisted pages in diff --git a/includes/WatchedItemQueryService.php b/includes/WatchedItemQueryService.php index b7cdc53ab0..0c3d52a39f 100644 --- a/includes/WatchedItemQueryService.php +++ b/includes/WatchedItemQueryService.php @@ -50,10 +50,24 @@ class WatchedItemQueryService { */ private $loadBalancer; + /** @var WatchedItemQueryServiceExtension[]|null */ + private $extensions = null; + public function __construct( LoadBalancer $loadBalancer ) { $this->loadBalancer = $loadBalancer; } + /** + * @return WatchedItemQueryServiceExtension[] + */ + private function getExtensions() { + if ( $this->extensions === null ) { + $this->extensions = []; + Hooks::run( 'WatchedItemQueryServiceExtensions', [ &$this->extensions, $this ] ); + } + return $this->extensions; + } + /** * @return IDatabase * @throws MWException @@ -84,9 +98,6 @@ class WatchedItemQueryService { * timestamp to start enumerating from * 'end' => string (format accepted by wfTimestamp) requires 'dir' option, * timestamp to end enumerating - * 'startFrom' => [ string $rcTimestamp, int $rcId ] requires 'dir' option, - * return items starting from the RecentChange specified by this, - * $rcTimestamp should be in the format accepted by wfTimestamp * 'watchlistOwner' => User user whose watchlist items should be listed if different * than the one specified with $user param, * requires 'watchlistOwnerToken' option @@ -97,6 +108,7 @@ class WatchedItemQueryService { * generator ('rc_cur_id' or 'rc_this_oldid') if true, or all * id fields ('rc_cur_id', 'rc_this_oldid', 'rc_last_oldid') * if false (default) + * @param array|null &$startFrom Continuation value: [ string $rcTimestamp, int $rcId ] * @return array of pairs ( WatchedItem $watchedItem, string[] $recentChangeInfo ), * where $recentChangeInfo contains the following keys: * - 'rc_id', @@ -107,7 +119,9 @@ class WatchedItemQueryService { * - 'rc_deleted', * Additional keys could be added by specifying the 'includeFields' option */ - public function getWatchedItemsWithRecentChangeInfo( User $user, array $options = [] ) { + public function getWatchedItemsWithRecentChangeInfo( + User $user, array $options = [], &$startFrom = null + ) { $options += [ 'includeFields' => [], 'namespaceIds' => [], @@ -128,15 +142,19 @@ class WatchedItemQueryService { 'must be DIR_OLDER or DIR_NEWER' ); Assert::parameter( - !isset( $options['start'] ) && !isset( $options['end'] ) && !isset( $options['startFrom'] ) + !isset( $options['start'] ) && !isset( $options['end'] ) && $startFrom === null || isset( $options['dir'] ), '$options[\'dir\']', - 'must be provided when providing any of options: start, end, startFrom' + 'must be provided when providing the "start" or "end" options or the $startFrom parameter' ); Assert::parameter( - !isset( $options['startFrom'] ) - || ( is_array( $options['startFrom'] ) && count( $options['startFrom'] ) === 2 ), + !isset( $options['startFrom'] ), '$options[\'startFrom\']', + 'must not be provided, use $startFrom instead' + ); + Assert::parameter( + !isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ), + '$startFrom', 'must be a two-element array' ); if ( array_key_exists( 'watchlistOwner', $options ) ) { @@ -164,6 +182,21 @@ class WatchedItemQueryService { $dbOptions = $this->getWatchedItemsWithRCInfoQueryDbOptions( $options ); $joinConds = $this->getWatchedItemsWithRCInfoQueryJoinConds( $options ); + if ( $startFrom !== null ) { + $conds[] = $this->getStartFromConds( $db, $options, $startFrom ); + } + + foreach ( $this->getExtensions() as $extension ) { + $extension->modifyWatchedItemsWithRCInfoQuery( + $user, $options, $db, + $tables, + $fields, + $conds, + $dbOptions, + $joinConds + ); + } + $res = $db->select( $tables, $fields, @@ -173,8 +206,15 @@ class WatchedItemQueryService { $joinConds ); + $limit = isset( $dbOptions['LIMIT'] ) ? $dbOptions['LIMIT'] : INF; $items = []; + $startFrom = null; foreach ( $res as $row ) { + if ( --$limit <= 0 ) { + $startFrom = [ $row->rc_timestamp, $row->rc_id ]; + break; + } + $items[] = [ new WatchedItem( $user, @@ -185,6 +225,10 @@ class WatchedItemQueryService { ]; } + foreach ( $this->getExtensions() as $extension ) { + $extension->modifyWatchedItemsWithRCInfo( $user, $options, $db, $items, $res, $startFrom ); + } + return $items; } @@ -368,10 +412,6 @@ class WatchedItemQueryService { $conds[] = $deletedPageLogCond; } - if ( array_key_exists( 'startFrom', $options ) ) { - $conds[] = $this->getStartFromConds( $db, $options ); - } - return $conds; } @@ -499,9 +539,9 @@ class WatchedItemQueryService { return ''; } - private function getStartFromConds( IDatabase $db, array $options ) { + private function getStartFromConds( IDatabase $db, array $options, array $startFrom ) { $op = $options['dir'] === self::DIR_OLDER ? '<' : '>'; - list( $rcTimestamp, $rcId ) = $options['startFrom']; + list( $rcTimestamp, $rcId ) = $startFrom; $rcTimestamp = $db->addQuotes( $db->timestamp( $rcTimestamp ) ); $rcId = (int)$rcId; return $db->makeList( @@ -583,7 +623,7 @@ class WatchedItemQueryService { } if ( array_key_exists( 'limit', $options ) ) { - $dbOptions['LIMIT'] = (int)$options['limit']; + $dbOptions['LIMIT'] = (int)$options['limit'] + 1; } return $dbOptions; diff --git a/includes/WatchedItemQueryServiceExtension.php b/includes/WatchedItemQueryServiceExtension.php new file mode 100644 index 0000000000..8fcf1311df --- /dev/null +++ b/includes/WatchedItemQueryServiceExtension.php @@ -0,0 +1,54 @@ +dieContinueUsageIf( count( $cont ) != 2 ); $continueTimestamp = $cont[0]; $continueId = (int)$cont[1]; $this->dieContinueUsageIf( $continueId != $cont[1] ); - $options['startFrom'] = [ $continueTimestamp, $continueId ]; + $startFrom = [ $continueTimestamp, $continueId ]; } if ( $wlowner !== $user ) { @@ -169,33 +170,24 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $options['notByUser'] = $params['excludeuser']; } - $options['limit'] = $params['limit'] + 1; + $options['limit'] = $params['limit']; + + Hooks::run( 'ApiQueryWatchlistPrepareWatchedItemQueryServiceOptions', [ + $this, $params, &$options + ] ); $ids = []; $count = 0; $watchedItemQuery = MediaWikiServices::getInstance()->getWatchedItemQueryService(); - $items = $watchedItemQuery->getWatchedItemsWithRecentChangeInfo( $wlowner, $options ); + $items = $watchedItemQuery->getWatchedItemsWithRecentChangeInfo( $wlowner, $options, $startFrom ); foreach ( $items as list ( $watchedItem, $recentChangeInfo ) ) { /** @var WatchedItem $watchedItem */ - if ( ++$count > $params['limit'] ) { - // We've reached the one extra which shows that there are - // additional pages to be had. Stop here... - $this->setContinueEnumParameter( - 'continue', - $recentChangeInfo['rc_timestamp'] . '|' . $recentChangeInfo['rc_id'] - ); - break; - } - if ( is_null( $resultPageSet ) ) { $vals = $this->extractOutputData( $watchedItem, $recentChangeInfo ); $fit = $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( - 'continue', - $recentChangeInfo['rc_timestamp'] . '|' . $recentChangeInfo['rc_id'] - ); + $startFrom = [ $recentChangeInfo['rc_timestamp'], $recentChangeInfo['rc_id'] ]; break; } } else { @@ -207,6 +199,10 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } } + if ( $startFrom !== null ) { + $this->setContinueEnumParameter( 'continue', implode( '|', $startFrom ) ); + } + if ( is_null( $resultPageSet ) ) { $this->getResult()->addIndexedTagName( [ 'query', $this->getModuleName() ], @@ -396,6 +392,10 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $vals['suppressed'] = true; } + Hooks::run( 'ApiQueryWatchlistExtractOutputData', [ + $this, $watchedItem, $recentChangeInfo, &$vals + ] ); + return $vals; } diff --git a/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php b/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php index 92446ed950..93687df2d5 100644 --- a/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php +++ b/tests/phpunit/includes/WatchedItemQueryServiceUnitTest.php @@ -180,7 +180,9 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { '(rc_this_oldid=page_latest) OR (rc_type=3)', ], $this->isType( 'string' ), - [], + [ + 'LIMIT' => 3, + ], [ 'watchlist' => [ 'INNER JOIN', @@ -214,12 +216,184 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { 'rc_deleted' => 0, 'wl_notificationtimestamp' => null, ] ), + $this->getFakeRow( [ + 'rc_id' => 3, + 'rc_namespace' => 1, + 'rc_title' => 'Foo3', + 'rc_timestamp' => '20151212010103', + 'rc_type' => RC_NEW, + 'rc_deleted' => 0, + 'wl_notificationtimestamp' => null, + ] ), ] ) ); $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); - $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user ); + $startFrom = null; + $items = $queryService->getWatchedItemsWithRecentChangeInfo( + $user, [ 'limit' => 2 ], $startFrom + ); + + $this->assertInternalType( 'array', $items ); + $this->assertCount( 2, $items ); + + foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) { + $this->assertInstanceOf( WatchedItem::class, $watchedItem ); + $this->assertInternalType( 'array', $recentChangeInfo ); + } + + $this->assertEquals( + new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ), + $items[0][0] + ); + $this->assertEquals( + [ + 'rc_id' => 1, + 'rc_namespace' => 0, + 'rc_title' => 'Foo1', + 'rc_timestamp' => '20151212010101', + 'rc_type' => RC_NEW, + 'rc_deleted' => 0, + ], + $items[0][1] + ); + + $this->assertEquals( + new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ), + $items[1][0] + ); + $this->assertEquals( + [ + 'rc_id' => 2, + 'rc_namespace' => 1, + 'rc_title' => 'Foo2', + 'rc_timestamp' => '20151212010102', + 'rc_type' => RC_NEW, + 'rc_deleted' => 0, + ], + $items[1][1] + ); + + $this->assertEquals( [ '20151212010103', 3 ], $startFrom ); + } + + public function testGetWatchedItemsWithRecentChangeInfo_extension() { + $mockDb = $this->getMockDb(); + $mockDb->expects( $this->once() ) + ->method( 'select' ) + ->with( + [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ], + [ + 'rc_id', + 'rc_namespace', + 'rc_title', + 'rc_timestamp', + 'rc_type', + 'rc_deleted', + 'wl_notificationtimestamp', + 'rc_cur_id', + 'rc_this_oldid', + 'rc_last_oldid', + 'extension_dummy_field', + ], + [ + 'wl_user' => 1, + '(rc_this_oldid=page_latest) OR (rc_type=3)', + 'extension_dummy_cond', + ], + $this->isType( 'string' ), + [ + 'extension_dummy_option', + ], + [ + 'watchlist' => [ + 'INNER JOIN', + [ + 'wl_namespace=rc_namespace', + 'wl_title=rc_title' + ] + ], + 'page' => [ + 'LEFT JOIN', + 'rc_cur_id=page_id', + ], + 'extension_dummy_join_cond' => [], + ] + ) + ->will( $this->returnValue( [ + $this->getFakeRow( [ + 'rc_id' => 1, + 'rc_namespace' => 0, + 'rc_title' => 'Foo1', + 'rc_timestamp' => '20151212010101', + 'rc_type' => RC_NEW, + 'rc_deleted' => 0, + 'wl_notificationtimestamp' => '20151212010101', + ] ), + $this->getFakeRow( [ + 'rc_id' => 2, + 'rc_namespace' => 1, + 'rc_title' => 'Foo2', + 'rc_timestamp' => '20151212010102', + 'rc_type' => RC_NEW, + 'rc_deleted' => 0, + 'wl_notificationtimestamp' => null, + ] ), + ] ) ); + + $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); + + $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension::class ) + ->getMock(); + $mockExtension->expects( $this->once() ) + ->method( 'modifyWatchedItemsWithRCInfoQuery' ) + ->with( + $this->identicalTo( $user ), + $this->isType( 'array' ), + $this->isInstanceOf( IDatabase::class ), + $this->isType( 'array' ), + $this->isType( 'array' ), + $this->isType( 'array' ), + $this->isType( 'array' ), + $this->isType( 'array' ) + ) + ->will( $this->returnCallback( function ( + $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds + ) { + $tables[] = 'extension_dummy_table'; + $fields[] = 'extension_dummy_field'; + $conds[] = 'extension_dummy_cond'; + $dbOptions[] = 'extension_dummy_option'; + $joinConds['extension_dummy_join_cond'] = []; + } ) ); + $mockExtension->expects( $this->once() ) + ->method( 'modifyWatchedItemsWithRCInfo' ) + ->with( + $this->identicalTo( $user ), + $this->isType( 'array' ), + $this->isInstanceOf( IDatabase::class ), + $this->isType( 'array' ), + $this->anything(), + $this->anything() // Can't test for null here, PHPUnit applies this after the callback + ) + ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) { + foreach ( $items as $i => &$item ) { + $item[1]['extension_dummy_field'] = $i; + } + unset( $item ); + + $this->assertNull( $startFrom ); + $startFrom = [ '20160203123456', 42 ]; + } ) ); + + $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); + TestingAccessWrapper::newFromObject( $queryService )->extensions = [ $mockExtension ]; + + $startFrom = null; + $items = $queryService->getWatchedItemsWithRecentChangeInfo( + $user, [], $startFrom + ); $this->assertInternalType( 'array', $items ); $this->assertCount( 2, $items ); @@ -241,6 +415,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { 'rc_timestamp' => '20151212010101', 'rc_type' => RC_NEW, 'rc_deleted' => 0, + 'extension_dummy_field' => 0, ], $items[0][1] ); @@ -257,93 +432,110 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { 'rc_timestamp' => '20151212010102', 'rc_type' => RC_NEW, 'rc_deleted' => 0, + 'extension_dummy_field' => 1, ], $items[1][1] ); + + $this->assertEquals( [ '20160203123456', 42 ], $startFrom ); } public function getWatchedItemsWithRecentChangeInfoOptionsProvider() { return [ [ [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_FLAGS ] ], + null, [ 'rc_type', 'rc_minor', 'rc_bot' ], [], [], ], [ [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER ] ], + null, [ 'rc_user_text' ], [], [], ], [ [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER_ID ] ], + null, [ 'rc_user' ], [], [], ], [ [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ], + null, [ 'rc_comment' ], [], [], ], [ [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_PATROL_INFO ] ], + null, [ 'rc_patrolled', 'rc_log_type' ], [], [], ], [ [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_SIZES ] ], + null, [ 'rc_old_len', 'rc_new_len' ], [], [], ], [ [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_LOG_INFO ] ], + null, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ], [], [], ], [ [ 'namespaceIds' => [ 0, 1 ] ], + null, [], [ 'wl_namespace' => [ 0, 1 ] ], [], ], [ [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ], + null, [], [ 'wl_namespace' => [ 0, 1 ] ], [], ], [ [ 'rcTypes' => [ RC_EDIT, RC_NEW ] ], + null, [], [ 'rc_type' => [ RC_EDIT, RC_NEW ] ], [], ], [ [ 'dir' => WatchedItemQueryService::DIR_OLDER ], + null, [], [], [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ] ], [ [ 'dir' => WatchedItemQueryService::DIR_NEWER ], + null, [], [], [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ] ], [ [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'start' => '20151212010101' ], + null, [], [ "rc_timestamp <= '20151212010101'" ], [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ] ], [ [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'end' => '20151212010101' ], + null, [], [ "rc_timestamp >= '20151212010101'" ], [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ] @@ -354,18 +546,21 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { 'start' => '20151212020101', 'end' => '20151212010101' ], + null, [], [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ], [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ] ], [ [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'start' => '20151212010101' ], + null, [], [ "rc_timestamp >= '20151212010101'" ], [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ] ], [ [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'end' => '20151212010101' ], + null, [], [ "rc_timestamp <= '20151212010101'" ], [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ] @@ -376,96 +571,112 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { 'start' => '20151212010101', 'end' => '20151212020101' ], + null, [], [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ], [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ] ], [ [ 'limit' => 10 ], + null, [], [], - [ 'LIMIT' => 10 ], + [ 'LIMIT' => 11 ], ], [ [ 'limit' => "10; DROP TABLE watchlist;\n--" ], + null, [], [], - [ 'LIMIT' => 10 ], + [ 'LIMIT' => 11 ], ], [ [ 'filters' => [ WatchedItemQueryService::FILTER_MINOR ] ], + null, [], [ 'rc_minor != 0' ], [], ], [ [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_MINOR ] ], + null, [], [ 'rc_minor = 0' ], [], ], [ [ 'filters' => [ WatchedItemQueryService::FILTER_BOT ] ], + null, [], [ 'rc_bot != 0' ], [], ], [ [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_BOT ] ], + null, [], [ 'rc_bot = 0' ], [], ], [ [ 'filters' => [ WatchedItemQueryService::FILTER_ANON ] ], + null, [], [ 'rc_user = 0' ], [], ], [ [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_ANON ] ], + null, [], [ 'rc_user != 0' ], [], ], [ [ 'filters' => [ WatchedItemQueryService::FILTER_PATROLLED ] ], + null, [], [ 'rc_patrolled != 0' ], [], ], [ [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_PATROLLED ] ], + null, [], [ 'rc_patrolled = 0' ], [], ], [ [ 'filters' => [ WatchedItemQueryService::FILTER_UNREAD ] ], + null, [], [ 'rc_timestamp >= wl_notificationtimestamp' ], [], ], [ [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_UNREAD ] ], + null, [], [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ], [], ], [ [ 'onlyByUser' => 'SomeOtherUser' ], + null, [], [ 'rc_user_text' => 'SomeOtherUser' ], [], ], [ [ 'notByUser' => 'SomeOtherUser' ], + null, [], [ "rc_user_text != 'SomeOtherUser'" ], [], ], [ - [ 'startFrom' => [ '20151212010101', 123 ], 'dir' => WatchedItemQueryService::DIR_OLDER ], + [ 'dir' => WatchedItemQueryService::DIR_OLDER ], + [ '20151212010101', 123 ], [], [ "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))" @@ -473,7 +684,8 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ], ], [ - [ 'startFrom' => [ '20151212010101', 123 ], 'dir' => WatchedItemQueryService::DIR_NEWER ], + [ 'dir' => WatchedItemQueryService::DIR_NEWER ], + [ '20151212010101', 123 ], [], [ "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))" @@ -481,10 +693,8 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ], ], [ - [ - 'startFrom' => [ '20151212010101', "123; DROP TABLE watchlist;\n--" ], - 'dir' => WatchedItemQueryService::DIR_OLDER - ], + [ 'dir' => WatchedItemQueryService::DIR_OLDER ], + [ '20151212010101', "123; DROP TABLE watchlist;\n--" ], [], [ "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))" @@ -499,6 +709,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { */ public function testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult( array $options, + $startFrom, array $expectedExtraFields, array $expectedExtraConds, array $expectedDbOptions @@ -552,9 +763,10 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { $queryService = new WatchedItemQueryService( $this->getMockLoadBalancer( $mockDb ) ); $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); - $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options ); + $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom ); $this->assertEmpty( $items ); + $this->assertNull( $startFrom ); } public function filterPatrolledOptionProvider() { @@ -797,53 +1009,62 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { return [ [ [ 'rcTypes' => [ 1337 ] ], + null, 'Bad value for parameter $options[\'rcTypes\']', ], [ [ 'rcTypes' => [ 'edit' ] ], + null, 'Bad value for parameter $options[\'rcTypes\']', ], [ [ 'rcTypes' => [ RC_EDIT, 1337 ] ], + null, 'Bad value for parameter $options[\'rcTypes\']', ], [ [ 'dir' => 'foo' ], + null, 'Bad value for parameter $options[\'dir\']', ], [ [ 'start' => '20151212010101' ], + null, 'Bad value for parameter $options[\'dir\']: must be provided', ], [ [ 'end' => '20151212010101' ], + null, 'Bad value for parameter $options[\'dir\']: must be provided', ], [ - [ 'startFrom' => [ '20151212010101', 123 ] ], + [], + [ '20151212010101', 123 ], 'Bad value for parameter $options[\'dir\']: must be provided', ], [ - [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'startFrom' => '20151212010101' ], - 'Bad value for parameter $options[\'startFrom\']: must be a two-element array', + [ 'dir' => WatchedItemQueryService::DIR_OLDER ], + '20151212010101', + 'Bad value for parameter $startFrom: must be a two-element array', ], [ - [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'startFrom' => [ '20151212010101' ] ], - 'Bad value for parameter $options[\'startFrom\']: must be a two-element array', + [ 'dir' => WatchedItemQueryService::DIR_OLDER ], + [ '20151212010101' ], + 'Bad value for parameter $startFrom: must be a two-element array', ], [ - [ - 'dir' => WatchedItemQueryService::DIR_OLDER, - 'startFrom' => [ '20151212010101', 123, 'foo' ] - ], - 'Bad value for parameter $options[\'startFrom\']: must be a two-element array', + [ 'dir' => WatchedItemQueryService::DIR_OLDER ], + [ '20151212010101', 123, 'foo' ], + 'Bad value for parameter $startFrom: must be a two-element array', ], [ [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ], + null, 'Bad value for parameter $options[\'watchlistOwnerToken\']', ], [ [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ], + null, 'Bad value for parameter $options[\'watchlistOwner\']', ], ]; @@ -854,6 +1075,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { */ public function testGetWatchedItemsWithRecentChangeInfo_invalidOptions( array $options, + $startFrom, $expectedInExceptionMessage ) { $mockDb = $this->getMockDb(); @@ -864,7 +1086,7 @@ class WatchedItemQueryServiceUnitTest extends PHPUnit_Framework_TestCase { $user = $this->getMockUnrestrictedNonAnonUserWithId( 1 ); $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage ); - $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options ); + $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom ); } public function testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult() { -- 2.20.1