MediaWiki  1.33.0
WatchedItemQueryServiceUnitTest.php
Go to the documentation of this file.
1 <?php
2 
4 use Wikimedia\TestingAccessWrapper;
5 
10 
11  use MediaWikiCoversValidator;
12 
16  private function getMockCommentStore() {
17  $mockStore = $this->getMockBuilder( CommentStore::class )
18  ->disableOriginalConstructor()
19  ->getMock();
20  $mockStore->expects( $this->any() )
21  ->method( 'getFields' )
22  ->willReturn( [ 'commentstore' => 'fields' ] );
23  $mockStore->expects( $this->any() )
24  ->method( 'getJoin' )
25  ->willReturn( [
26  'tables' => [ 'commentstore' => 'table' ],
27  'fields' => [ 'commentstore' => 'field' ],
28  'joins' => [ 'commentstore' => 'join' ],
29  ] );
30  return $mockStore;
31  }
32 
36  private function getMockActorMigration() {
37  $mockStore = $this->getMockBuilder( ActorMigration::class )
38  ->disableOriginalConstructor()
39  ->getMock();
40  $mockStore->expects( $this->any() )
41  ->method( 'getJoin' )
42  ->willReturn( [
43  'tables' => [ 'actormigration' => 'table' ],
44  'fields' => [
45  'rc_user' => 'actormigration_user',
46  'rc_user_text' => 'actormigration_user_text',
47  'rc_actor' => 'actormigration_actor',
48  ],
49  'joins' => [ 'actormigration' => 'join' ],
50  ] );
51  $mockStore->expects( $this->any() )
52  ->method( 'getWhere' )
53  ->willReturn( [
54  'tables' => [ 'actormigration' => 'table' ],
55  'conds' => 'actormigration_conds',
56  'joins' => [ 'actormigration' => 'join' ],
57  ] );
58  $mockStore->expects( $this->any() )
59  ->method( 'isAnon' )
60  ->willReturn( 'actormigration is anon' );
61  $mockStore->expects( $this->any() )
62  ->method( 'isNotAnon' )
63  ->willReturn( 'actormigration is not anon' );
64  return $mockStore;
65  }
66 
71  private function newService( $mockDb ) {
72  return new WatchedItemQueryService(
73  $this->getMockLoadBalancer( $mockDb ),
74  $this->getMockCommentStore(),
75  $this->getMockActorMigration(),
77  );
78  }
79 
83  private function getMockDb() {
84  $mock = $this->getMockBuilder( Database::class )
85  ->disableOriginalConstructor()
86  ->getMock();
87 
88  $mock->expects( $this->any() )
89  ->method( 'makeList' )
90  ->with(
91  $this->isType( 'array' ),
92  $this->isType( 'int' )
93  )
94  ->will( $this->returnCallback( function ( $a, $conj ) {
95  $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
96  $conds = [];
97  foreach ( $a as $k => $v ) {
98  if ( is_int( $k ) ) {
99  $conds[] = "($v)";
100  } elseif ( is_array( $v ) ) {
101  $conds[] = "($k IN ('" . implode( "','", $v ) . "'))";
102  } else {
103  $conds[] = "($k = '$v')";
104  }
105  }
106  return implode( $sqlConj, $conds );
107  } ) );
108 
109  $mock->expects( $this->any() )
110  ->method( 'addQuotes' )
111  ->will( $this->returnCallback( function ( $value ) {
112  return "'$value'";
113  } ) );
114 
115  $mock->expects( $this->any() )
116  ->method( 'timestamp' )
117  ->will( $this->returnArgument( 0 ) );
118 
119  $mock->expects( $this->any() )
120  ->method( 'bitAnd' )
121  ->willReturnCallback( function ( $a, $b ) {
122  return "($a & $b)";
123  } );
124 
125  return $mock;
126  }
127 
132  private function getMockLoadBalancer( $mockDb ) {
133  $mock = $this->getMockBuilder( LoadBalancer::class )
134  ->disableOriginalConstructor()
135  ->getMock();
136  $mock->expects( $this->any() )
137  ->method( 'getConnectionRef' )
138  ->with( DB_REPLICA )
139  ->will( $this->returnValue( $mockDb ) );
140  return $mock;
141  }
142 
147  private function getMockWatchedItemStore() {
148  $mock = $this->getMockBuilder( WatchedItemStore::class )
149  ->disableOriginalConstructor()
150  ->getMock();
151  $mock->expects( $this->any() )
152  ->method( 'getLatestNotificationTimestamp' )
153  ->will( $this->returnCallback( function ( $timestamp ) {
154  return $timestamp;
155  } ) );
156  return $mock;
157  }
158 
163  private function getMockNonAnonUserWithId( $id ) {
164  $mock = $this->getMockBuilder( User::class )->getMock();
165  $mock->expects( $this->any() )
166  ->method( 'isAnon' )
167  ->will( $this->returnValue( false ) );
168  $mock->expects( $this->any() )
169  ->method( 'getId' )
170  ->will( $this->returnValue( $id ) );
171  return $mock;
172  }
173 
178  private function getMockUnrestrictedNonAnonUserWithId( $id ) {
179  $mock = $this->getMockNonAnonUserWithId( $id );
180  $mock->expects( $this->any() )
181  ->method( 'isAllowed' )
182  ->will( $this->returnValue( true ) );
183  $mock->expects( $this->any() )
184  ->method( 'isAllowedAny' )
185  ->will( $this->returnValue( true ) );
186  $mock->expects( $this->any() )
187  ->method( 'useRCPatrol' )
188  ->will( $this->returnValue( true ) );
189  return $mock;
190  }
191 
197  private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
198  $mock = $this->getMockNonAnonUserWithId( $id );
199 
200  $mock->expects( $this->any() )
201  ->method( 'isAllowed' )
202  ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
203  return $action !== $notAllowedAction;
204  } ) );
205  $mock->expects( $this->any() )
206  ->method( 'isAllowedAny' )
207  ->will( $this->returnCallback( function () use ( $notAllowedAction ) {
208  $actions = func_get_args();
209  return !in_array( $notAllowedAction, $actions );
210  } ) );
211 
212  return $mock;
213  }
214 
220  $mock = $this->getMockNonAnonUserWithId( $id );
221 
222  $mock->expects( $this->any() )
223  ->method( 'isAllowed' )
224  ->will( $this->returnValue( true ) );
225  $mock->expects( $this->any() )
226  ->method( 'isAllowedAny' )
227  ->will( $this->returnValue( true ) );
228 
229  $mock->expects( $this->any() )
230  ->method( 'useRCPatrol' )
231  ->will( $this->returnValue( false ) );
232  $mock->expects( $this->any() )
233  ->method( 'useNPPatrol' )
234  ->will( $this->returnValue( false ) );
235 
236  return $mock;
237  }
238 
239  private function getMockAnonUser() {
240  $mock = $this->getMockBuilder( User::class )->getMock();
241  $mock->expects( $this->any() )
242  ->method( 'isAnon' )
243  ->will( $this->returnValue( true ) );
244  return $mock;
245  }
246 
247  private function getFakeRow( array $rowValues ) {
248  $fakeRow = new stdClass();
249  foreach ( $rowValues as $valueName => $value ) {
250  $fakeRow->$valueName = $value;
251  }
252  return $fakeRow;
253  }
254 
256  $mockDb = $this->getMockDb();
257  $mockDb->expects( $this->once() )
258  ->method( 'select' )
259  ->with(
260  [ 'recentchanges', 'watchlist', 'page' ],
261  [
262  'rc_id',
263  'rc_namespace',
264  'rc_title',
265  'rc_timestamp',
266  'rc_type',
267  'rc_deleted',
268  'wl_notificationtimestamp',
269  'rc_cur_id',
270  'rc_this_oldid',
271  'rc_last_oldid',
272  ],
273  [
274  'wl_user' => 1,
275  '(rc_this_oldid=page_latest) OR (rc_type=3)',
276  ],
277  $this->isType( 'string' ),
278  [
279  'LIMIT' => 3,
280  ],
281  [
282  'watchlist' => [
283  'JOIN',
284  [
285  'wl_namespace=rc_namespace',
286  'wl_title=rc_title'
287  ]
288  ],
289  'page' => [
290  'LEFT JOIN',
291  'rc_cur_id=page_id',
292  ],
293  ]
294  )
295  ->will( $this->returnValue( [
296  $this->getFakeRow( [
297  'rc_id' => 1,
298  'rc_namespace' => 0,
299  'rc_title' => 'Foo1',
300  'rc_timestamp' => '20151212010101',
301  'rc_type' => RC_NEW,
302  'rc_deleted' => 0,
303  'wl_notificationtimestamp' => '20151212010101',
304  ] ),
305  $this->getFakeRow( [
306  'rc_id' => 2,
307  'rc_namespace' => 1,
308  'rc_title' => 'Foo2',
309  'rc_timestamp' => '20151212010102',
310  'rc_type' => RC_NEW,
311  'rc_deleted' => 0,
312  'wl_notificationtimestamp' => null,
313  ] ),
314  $this->getFakeRow( [
315  'rc_id' => 3,
316  'rc_namespace' => 1,
317  'rc_title' => 'Foo3',
318  'rc_timestamp' => '20151212010103',
319  'rc_type' => RC_NEW,
320  'rc_deleted' => 0,
321  'wl_notificationtimestamp' => null,
322  ] ),
323  ] ) );
324 
325  $queryService = $this->newService( $mockDb );
327 
328  $startFrom = null;
329  $items = $queryService->getWatchedItemsWithRecentChangeInfo(
330  $user, [ 'limit' => 2 ], $startFrom
331  );
332 
333  $this->assertInternalType( 'array', $items );
334  $this->assertCount( 2, $items );
335 
336  foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
337  $this->assertInstanceOf( WatchedItem::class, $watchedItem );
338  $this->assertInternalType( 'array', $recentChangeInfo );
339  }
340 
341  $this->assertEquals(
342  new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
343  $items[0][0]
344  );
345  $this->assertEquals(
346  [
347  'rc_id' => 1,
348  'rc_namespace' => 0,
349  'rc_title' => 'Foo1',
350  'rc_timestamp' => '20151212010101',
351  'rc_type' => RC_NEW,
352  'rc_deleted' => 0,
353  ],
354  $items[0][1]
355  );
356 
357  $this->assertEquals(
358  new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
359  $items[1][0]
360  );
361  $this->assertEquals(
362  [
363  'rc_id' => 2,
364  'rc_namespace' => 1,
365  'rc_title' => 'Foo2',
366  'rc_timestamp' => '20151212010102',
367  'rc_type' => RC_NEW,
368  'rc_deleted' => 0,
369  ],
370  $items[1][1]
371  );
372 
373  $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
374  }
375 
377  $mockDb = $this->getMockDb();
378  $mockDb->expects( $this->once() )
379  ->method( 'select' )
380  ->with(
381  [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
382  [
383  'rc_id',
384  'rc_namespace',
385  'rc_title',
386  'rc_timestamp',
387  'rc_type',
388  'rc_deleted',
389  'wl_notificationtimestamp',
390  'rc_cur_id',
391  'rc_this_oldid',
392  'rc_last_oldid',
393  'extension_dummy_field',
394  ],
395  [
396  'wl_user' => 1,
397  '(rc_this_oldid=page_latest) OR (rc_type=3)',
398  'extension_dummy_cond',
399  ],
400  $this->isType( 'string' ),
401  [
402  'extension_dummy_option',
403  ],
404  [
405  'watchlist' => [
406  'JOIN',
407  [
408  'wl_namespace=rc_namespace',
409  'wl_title=rc_title'
410  ]
411  ],
412  'page' => [
413  'LEFT JOIN',
414  'rc_cur_id=page_id',
415  ],
416  'extension_dummy_join_cond' => [],
417  ]
418  )
419  ->will( $this->returnValue( [
420  $this->getFakeRow( [
421  'rc_id' => 1,
422  'rc_namespace' => 0,
423  'rc_title' => 'Foo1',
424  'rc_timestamp' => '20151212010101',
425  'rc_type' => RC_NEW,
426  'rc_deleted' => 0,
427  'wl_notificationtimestamp' => '20151212010101',
428  ] ),
429  $this->getFakeRow( [
430  'rc_id' => 2,
431  'rc_namespace' => 1,
432  'rc_title' => 'Foo2',
433  'rc_timestamp' => '20151212010102',
434  'rc_type' => RC_NEW,
435  'rc_deleted' => 0,
436  'wl_notificationtimestamp' => null,
437  ] ),
438  ] ) );
439 
441 
442  $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension::class )
443  ->getMock();
444  $mockExtension->expects( $this->once() )
445  ->method( 'modifyWatchedItemsWithRCInfoQuery' )
446  ->with(
447  $this->identicalTo( $user ),
448  $this->isType( 'array' ),
449  $this->isInstanceOf( IDatabase::class ),
450  $this->isType( 'array' ),
451  $this->isType( 'array' ),
452  $this->isType( 'array' ),
453  $this->isType( 'array' ),
454  $this->isType( 'array' )
455  )
456  ->will( $this->returnCallback( function (
457  $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
458  ) {
459  $tables[] = 'extension_dummy_table';
460  $fields[] = 'extension_dummy_field';
461  $conds[] = 'extension_dummy_cond';
462  $dbOptions[] = 'extension_dummy_option';
463  $joinConds['extension_dummy_join_cond'] = [];
464  } ) );
465  $mockExtension->expects( $this->once() )
466  ->method( 'modifyWatchedItemsWithRCInfo' )
467  ->with(
468  $this->identicalTo( $user ),
469  $this->isType( 'array' ),
470  $this->isInstanceOf( IDatabase::class ),
471  $this->isType( 'array' ),
472  $this->anything(),
473  $this->anything() // Can't test for null here, PHPUnit applies this after the callback
474  )
475  ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
476  foreach ( $items as $i => &$item ) {
477  $item[1]['extension_dummy_field'] = $i;
478  }
479  unset( $item );
480 
481  $this->assertNull( $startFrom );
482  $startFrom = [ '20160203123456', 42 ];
483  } ) );
484 
485  $queryService = $this->newService( $mockDb );
486  TestingAccessWrapper::newFromObject( $queryService )->extensions = [ $mockExtension ];
487 
488  $startFrom = null;
489  $items = $queryService->getWatchedItemsWithRecentChangeInfo(
490  $user, [], $startFrom
491  );
492 
493  $this->assertInternalType( 'array', $items );
494  $this->assertCount( 2, $items );
495 
496  foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
497  $this->assertInstanceOf( WatchedItem::class, $watchedItem );
498  $this->assertInternalType( 'array', $recentChangeInfo );
499  }
500 
501  $this->assertEquals(
502  new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
503  $items[0][0]
504  );
505  $this->assertEquals(
506  [
507  'rc_id' => 1,
508  'rc_namespace' => 0,
509  'rc_title' => 'Foo1',
510  'rc_timestamp' => '20151212010101',
511  'rc_type' => RC_NEW,
512  'rc_deleted' => 0,
513  'extension_dummy_field' => 0,
514  ],
515  $items[0][1]
516  );
517 
518  $this->assertEquals(
519  new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
520  $items[1][0]
521  );
522  $this->assertEquals(
523  [
524  'rc_id' => 2,
525  'rc_namespace' => 1,
526  'rc_title' => 'Foo2',
527  'rc_timestamp' => '20151212010102',
528  'rc_type' => RC_NEW,
529  'rc_deleted' => 0,
530  'extension_dummy_field' => 1,
531  ],
532  $items[1][1]
533  );
534 
535  $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
536  }
537 
539  return [
540  [
541  [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_FLAGS ] ],
542  null,
543  [],
544  [ 'rc_type', 'rc_minor', 'rc_bot' ],
545  [],
546  [],
547  [],
548  ],
549  [
550  [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER ] ],
551  null,
552  [ 'actormigration' => 'table' ],
553  [ 'rc_user_text' => 'actormigration_user_text' ],
554  [],
555  [],
556  [ 'actormigration' => 'join' ],
557  ],
558  [
559  [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER_ID ] ],
560  null,
561  [ 'actormigration' => 'table' ],
562  [ 'rc_user' => 'actormigration_user' ],
563  [],
564  [],
565  [ 'actormigration' => 'join' ],
566  ],
567  [
568  [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
569  null,
570  [ 'commentstore' => 'table' ],
571  [ 'commentstore' => 'field' ],
572  [],
573  [],
574  [ 'commentstore' => 'join' ],
575  ],
576  [
577  [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_PATROL_INFO ] ],
578  null,
579  [],
580  [ 'rc_patrolled', 'rc_log_type' ],
581  [],
582  [],
583  [],
584  ],
585  [
586  [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_SIZES ] ],
587  null,
588  [],
589  [ 'rc_old_len', 'rc_new_len' ],
590  [],
591  [],
592  [],
593  ],
594  [
595  [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_LOG_INFO ] ],
596  null,
597  [],
598  [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
599  [],
600  [],
601  [],
602  ],
603  [
604  [ 'namespaceIds' => [ 0, 1 ] ],
605  null,
606  [],
607  [],
608  [ 'wl_namespace' => [ 0, 1 ] ],
609  [],
610  [],
611  ],
612  [
613  [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
614  null,
615  [],
616  [],
617  [ 'wl_namespace' => [ 0, 1 ] ],
618  [],
619  [],
620  ],
621  [
622  [ 'rcTypes' => [ RC_EDIT, RC_NEW ] ],
623  null,
624  [],
625  [],
626  [ 'rc_type' => [ RC_EDIT, RC_NEW ] ],
627  [],
628  [],
629  ],
630  [
632  null,
633  [],
634  [],
635  [],
636  [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
637  [],
638  ],
639  [
641  null,
642  [],
643  [],
644  [],
645  [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
646  [],
647  ],
648  [
649  [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'start' => '20151212010101' ],
650  null,
651  [],
652  [],
653  [ "rc_timestamp <= '20151212010101'" ],
654  [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
655  [],
656  ],
657  [
658  [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'end' => '20151212010101' ],
659  null,
660  [],
661  [],
662  [ "rc_timestamp >= '20151212010101'" ],
663  [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
664  [],
665  ],
666  [
667  [
669  'start' => '20151212020101',
670  'end' => '20151212010101'
671  ],
672  null,
673  [],
674  [],
675  [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
676  [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
677  [],
678  ],
679  [
680  [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'start' => '20151212010101' ],
681  null,
682  [],
683  [],
684  [ "rc_timestamp >= '20151212010101'" ],
685  [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
686  [],
687  ],
688  [
689  [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'end' => '20151212010101' ],
690  null,
691  [],
692  [],
693  [ "rc_timestamp <= '20151212010101'" ],
694  [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
695  [],
696  ],
697  [
698  [
700  'start' => '20151212010101',
701  'end' => '20151212020101'
702  ],
703  null,
704  [],
705  [],
706  [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
707  [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
708  [],
709  ],
710  [
711  [ 'limit' => 10 ],
712  null,
713  [],
714  [],
715  [],
716  [ 'LIMIT' => 11 ],
717  [],
718  ],
719  [
720  [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
721  null,
722  [],
723  [],
724  [],
725  [ 'LIMIT' => 11 ],
726  [],
727  ],
728  [
729  [ 'filters' => [ WatchedItemQueryService::FILTER_MINOR ] ],
730  null,
731  [],
732  [],
733  [ 'rc_minor != 0' ],
734  [],
735  [],
736  ],
737  [
738  [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_MINOR ] ],
739  null,
740  [],
741  [],
742  [ 'rc_minor = 0' ],
743  [],
744  [],
745  ],
746  [
747  [ 'filters' => [ WatchedItemQueryService::FILTER_BOT ] ],
748  null,
749  [],
750  [],
751  [ 'rc_bot != 0' ],
752  [],
753  [],
754  ],
755  [
756  [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_BOT ] ],
757  null,
758  [],
759  [],
760  [ 'rc_bot = 0' ],
761  [],
762  [],
763  ],
764  [
765  [ 'filters' => [ WatchedItemQueryService::FILTER_ANON ] ],
766  null,
767  [ 'actormigration' => 'table' ],
768  [],
769  [ 'actormigration is anon' ],
770  [],
771  [ 'actormigration' => 'join' ],
772  ],
773  [
774  [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_ANON ] ],
775  null,
776  [ 'actormigration' => 'table' ],
777  [],
778  [ 'actormigration is not anon' ],
779  [],
780  [ 'actormigration' => 'join' ],
781  ],
782  [
783  [ 'filters' => [ WatchedItemQueryService::FILTER_PATROLLED ] ],
784  null,
785  [],
786  [],
787  [ 'rc_patrolled != 0' ],
788  [],
789  [],
790  ],
791  [
793  null,
794  [],
795  [],
796  [ 'rc_patrolled' => 0 ],
797  [],
798  [],
799  ],
800  [
801  [ 'filters' => [ WatchedItemQueryService::FILTER_UNREAD ] ],
802  null,
803  [],
804  [],
805  [ 'rc_timestamp >= wl_notificationtimestamp' ],
806  [],
807  [],
808  ],
809  [
810  [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_UNREAD ] ],
811  null,
812  [],
813  [],
814  [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
815  [],
816  [],
817  ],
818  [
819  [ 'onlyByUser' => 'SomeOtherUser' ],
820  null,
821  [ 'actormigration' => 'table' ],
822  [],
823  [ 'actormigration_conds' ],
824  [],
825  [ 'actormigration' => 'join' ],
826  ],
827  [
828  [ 'notByUser' => 'SomeOtherUser' ],
829  null,
830  [ 'actormigration' => 'table' ],
831  [],
832  [ 'NOT(actormigration_conds)' ],
833  [],
834  [ 'actormigration' => 'join' ],
835  ],
836  [
838  [ '20151212010101', 123 ],
839  [],
840  [],
841  [
842  "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
843  ],
844  [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
845  [],
846  ],
847  [
849  [ '20151212010101', 123 ],
850  [],
851  [],
852  [
853  "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
854  ],
855  [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
856  [],
857  ],
858  [
860  [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
861  [],
862  [],
863  [
864  "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
865  ],
866  [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
867  [],
868  ],
869  ];
870  }
871 
876  array $options,
877  $startFrom,
878  array $expectedExtraTables,
879  array $expectedExtraFields,
880  array $expectedExtraConds,
881  array $expectedDbOptions,
882  array $expectedExtraJoinConds
883  ) {
884  $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
885  $expectedFields = array_merge(
886  [
887  'rc_id',
888  'rc_namespace',
889  'rc_title',
890  'rc_timestamp',
891  'rc_type',
892  'rc_deleted',
893  'wl_notificationtimestamp',
894 
895  'rc_cur_id',
896  'rc_this_oldid',
897  'rc_last_oldid',
898  ],
899  $expectedExtraFields
900  );
901  $expectedConds = array_merge(
902  [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
903  $expectedExtraConds
904  );
905  $expectedJoinConds = array_merge(
906  [
907  'watchlist' => [
908  'JOIN',
909  [
910  'wl_namespace=rc_namespace',
911  'wl_title=rc_title'
912  ]
913  ],
914  'page' => [
915  'LEFT JOIN',
916  'rc_cur_id=page_id',
917  ],
918  ],
919  $expectedExtraJoinConds
920  );
921 
922  $mockDb = $this->getMockDb();
923  $mockDb->expects( $this->once() )
924  ->method( 'select' )
925  ->with(
926  $expectedTables,
927  $expectedFields,
928  $expectedConds,
929  $this->isType( 'string' ),
930  $expectedDbOptions,
931  $expectedJoinConds
932  )
933  ->will( $this->returnValue( [] ) );
934 
935  $queryService = $this->newService( $mockDb );
937 
938  $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
939 
940  $this->assertEmpty( $items );
941  $this->assertNull( $startFrom );
942  }
943 
944  public function filterPatrolledOptionProvider() {
945  return [
948  ];
949  }
950 
955  $filtersOption
956  ) {
957  $mockDb = $this->getMockDb();
958  $mockDb->expects( $this->once() )
959  ->method( 'select' )
960  ->with(
961  [ 'recentchanges', 'watchlist', 'page' ],
962  $this->isType( 'array' ),
963  [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
964  $this->isType( 'string' ),
965  $this->isType( 'array' ),
966  $this->isType( 'array' )
967  )
968  ->will( $this->returnValue( [] ) );
969 
971 
972  $queryService = $this->newService( $mockDb );
973  $items = $queryService->getWatchedItemsWithRecentChangeInfo(
974  $user,
975  [ 'filters' => [ $filtersOption ] ]
976  );
977 
978  $this->assertEmpty( $items );
979  }
980 
981  public function mysqlIndexOptimizationProvider() {
982  return [
983  [
984  'mysql',
985  [],
986  [ "rc_timestamp > ''" ],
987  ],
988  [
989  'mysql',
990  [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
991  [ "rc_timestamp <= '20151212010101'" ],
992  ],
993  [
994  'mysql',
995  [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
996  [ "rc_timestamp >= '20151212010101'" ],
997  ],
998  [
999  'postgres',
1000  [],
1001  [],
1002  ],
1003  ];
1004  }
1005 
1010  $dbType,
1011  array $options,
1012  array $expectedExtraConds
1013  ) {
1014  $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1015  $conds = array_merge( $commonConds, $expectedExtraConds );
1016 
1017  $mockDb = $this->getMockDb();
1018  $mockDb->expects( $this->once() )
1019  ->method( 'select' )
1020  ->with(
1021  [ 'recentchanges', 'watchlist', 'page' ],
1022  $this->isType( 'array' ),
1023  $conds,
1024  $this->isType( 'string' ),
1025  $this->isType( 'array' ),
1026  $this->isType( 'array' )
1027  )
1028  ->will( $this->returnValue( [] ) );
1029  $mockDb->expects( $this->any() )
1030  ->method( 'getType' )
1031  ->will( $this->returnValue( $dbType ) );
1032 
1033  $queryService = $this->newService( $mockDb );
1035 
1036  $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1037 
1038  $this->assertEmpty( $items );
1039  }
1040 
1042  return [
1043  [
1044  [],
1045  'deletedhistory',
1046  [],
1047  [
1048  '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
1050  ],
1051  [],
1052  ],
1053  [
1054  [],
1055  'suppressrevision',
1056  [],
1057  [
1058  '(rc_type != ' . RC_LOG . ') OR (' .
1059  '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
1061  ],
1062  [],
1063  ],
1064  [
1065  [],
1066  'viewsuppressed',
1067  [],
1068  [
1069  '(rc_type != ' . RC_LOG . ') OR (' .
1070  '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
1072  ],
1073  [],
1074  ],
1075  [
1076  [ 'onlyByUser' => 'SomeOtherUser' ],
1077  'deletedhistory',
1078  [ 'actormigration' => 'table' ],
1079  [
1080  'actormigration_conds',
1081  '(rc_deleted & ' . Revision::DELETED_USER . ') != ' . Revision::DELETED_USER,
1082  '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
1084  ],
1085  [ 'actormigration' => 'join' ],
1086  ],
1087  [
1088  [ 'onlyByUser' => 'SomeOtherUser' ],
1089  'suppressrevision',
1090  [ 'actormigration' => 'table' ],
1091  [
1092  'actormigration_conds',
1093  '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
1095  '(rc_type != ' . RC_LOG . ') OR (' .
1096  '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
1098  ],
1099  [ 'actormigration' => 'join' ],
1100  ],
1101  [
1102  [ 'onlyByUser' => 'SomeOtherUser' ],
1103  'viewsuppressed',
1104  [ 'actormigration' => 'table' ],
1105  [
1106  'actormigration_conds',
1107  '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
1109  '(rc_type != ' . RC_LOG . ') OR (' .
1110  '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
1112  ],
1113  [ 'actormigration' => 'join' ],
1114  ],
1115  ];
1116  }
1117 
1122  array $options,
1123  $notAllowedAction,
1124  array $expectedExtraTables,
1125  array $expectedExtraConds,
1126  array $expectedExtraJoins
1127  ) {
1128  $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1129  $conds = array_merge( $commonConds, $expectedExtraConds );
1130 
1131  $mockDb = $this->getMockDb();
1132  $mockDb->expects( $this->once() )
1133  ->method( 'select' )
1134  ->with(
1135  array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables ),
1136  $this->isType( 'array' ),
1137  $conds,
1138  $this->isType( 'string' ),
1139  $this->isType( 'array' ),
1140  array_merge( [
1141  'watchlist' => [ 'JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ],
1142  'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ],
1143  ], $expectedExtraJoins )
1144  )
1145  ->will( $this->returnValue( [] ) );
1146 
1147  $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
1148 
1149  $queryService = $this->newService( $mockDb );
1150  $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1151 
1152  $this->assertEmpty( $items );
1153  }
1154 
1156  $mockDb = $this->getMockDb();
1157  $mockDb->expects( $this->once() )
1158  ->method( 'select' )
1159  ->with(
1160  [ 'recentchanges', 'watchlist' ],
1161  [
1162  'rc_id',
1163  'rc_namespace',
1164  'rc_title',
1165  'rc_timestamp',
1166  'rc_type',
1167  'rc_deleted',
1168  'wl_notificationtimestamp',
1169 
1170  'rc_cur_id',
1171  'rc_this_oldid',
1172  'rc_last_oldid',
1173  ],
1174  [ 'wl_user' => 1, ],
1175  $this->isType( 'string' ),
1176  [],
1177  [
1178  'watchlist' => [
1179  'JOIN',
1180  [
1181  'wl_namespace=rc_namespace',
1182  'wl_title=rc_title'
1183  ]
1184  ],
1185  ]
1186  )
1187  ->will( $this->returnValue( [] ) );
1188 
1189  $queryService = $this->newService( $mockDb );
1191 
1192  $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1193 
1194  $this->assertEmpty( $items );
1195  }
1196 
1198  return [
1199  [
1200  [ 'rcTypes' => [ 1337 ] ],
1201  null,
1202  'Bad value for parameter $options[\'rcTypes\']',
1203  ],
1204  [
1205  [ 'rcTypes' => [ 'edit' ] ],
1206  null,
1207  'Bad value for parameter $options[\'rcTypes\']',
1208  ],
1209  [
1210  [ 'rcTypes' => [ RC_EDIT, 1337 ] ],
1211  null,
1212  'Bad value for parameter $options[\'rcTypes\']',
1213  ],
1214  [
1215  [ 'dir' => 'foo' ],
1216  null,
1217  'Bad value for parameter $options[\'dir\']',
1218  ],
1219  [
1220  [ 'start' => '20151212010101' ],
1221  null,
1222  'Bad value for parameter $options[\'dir\']: must be provided',
1223  ],
1224  [
1225  [ 'end' => '20151212010101' ],
1226  null,
1227  'Bad value for parameter $options[\'dir\']: must be provided',
1228  ],
1229  [
1230  [],
1231  [ '20151212010101', 123 ],
1232  'Bad value for parameter $options[\'dir\']: must be provided',
1233  ],
1234  [
1236  '20151212010101',
1237  'Bad value for parameter $startFrom: must be a two-element array',
1238  ],
1239  [
1241  [ '20151212010101' ],
1242  'Bad value for parameter $startFrom: must be a two-element array',
1243  ],
1244  [
1246  [ '20151212010101', 123, 'foo' ],
1247  'Bad value for parameter $startFrom: must be a two-element array',
1248  ],
1249  [
1250  [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1251  null,
1252  'Bad value for parameter $options[\'watchlistOwnerToken\']',
1253  ],
1254  [
1255  [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1256  null,
1257  'Bad value for parameter $options[\'watchlistOwner\']',
1258  ],
1259  ];
1260  }
1261 
1266  array $options,
1267  $startFrom,
1268  $expectedInExceptionMessage
1269  ) {
1270  $mockDb = $this->getMockDb();
1271  $mockDb->expects( $this->never() )
1272  ->method( $this->anything() );
1273 
1274  $queryService = $this->newService( $mockDb );
1276 
1277  $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
1278  $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1279  }
1280 
1282  $mockDb = $this->getMockDb();
1283  $mockDb->expects( $this->once() )
1284  ->method( 'select' )
1285  ->with(
1286  [ 'recentchanges', 'watchlist', 'page' ],
1287  [
1288  'rc_id',
1289  'rc_namespace',
1290  'rc_title',
1291  'rc_timestamp',
1292  'rc_type',
1293  'rc_deleted',
1294  'wl_notificationtimestamp',
1295  'rc_cur_id',
1296  ],
1297  [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1298  $this->isType( 'string' ),
1299  [],
1300  [
1301  'watchlist' => [
1302  'JOIN',
1303  [
1304  'wl_namespace=rc_namespace',
1305  'wl_title=rc_title'
1306  ]
1307  ],
1308  'page' => [
1309  'LEFT JOIN',
1310  'rc_cur_id=page_id',
1311  ],
1312  ]
1313  )
1314  ->will( $this->returnValue( [] ) );
1315 
1316  $queryService = $this->newService( $mockDb );
1318 
1319  $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1320  $user,
1321  [ 'usedInGenerator' => true ]
1322  );
1323 
1324  $this->assertEmpty( $items );
1325  }
1326 
1328  $mockDb = $this->getMockDb();
1329  $mockDb->expects( $this->once() )
1330  ->method( 'select' )
1331  ->with(
1332  [ 'recentchanges', 'watchlist' ],
1333  [
1334  'rc_id',
1335  'rc_namespace',
1336  'rc_title',
1337  'rc_timestamp',
1338  'rc_type',
1339  'rc_deleted',
1340  'wl_notificationtimestamp',
1341  'rc_this_oldid',
1342  ],
1343  [ 'wl_user' => 1 ],
1344  $this->isType( 'string' ),
1345  [],
1346  [
1347  'watchlist' => [
1348  'JOIN',
1349  [
1350  'wl_namespace=rc_namespace',
1351  'wl_title=rc_title'
1352  ]
1353  ],
1354  ]
1355  )
1356  ->will( $this->returnValue( [] ) );
1357 
1358  $queryService = $this->newService( $mockDb );
1360 
1361  $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1362  $user,
1363  [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1364  );
1365 
1366  $this->assertEmpty( $items );
1367  }
1368 
1370  $mockDb = $this->getMockDb();
1371  $mockDb->expects( $this->once() )
1372  ->method( 'select' )
1373  ->with(
1374  $this->isType( 'array' ),
1375  $this->isType( 'array' ),
1376  [
1377  'wl_user' => 2,
1378  '(rc_this_oldid=page_latest) OR (rc_type=3)',
1379  ],
1380  $this->isType( 'string' ),
1381  $this->isType( 'array' ),
1382  $this->isType( 'array' )
1383  )
1384  ->will( $this->returnValue( [] ) );
1385 
1386  $queryService = $this->newService( $mockDb );
1388  $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1389  $otherUser->expects( $this->once() )
1390  ->method( 'getOption' )
1391  ->with( 'watchlisttoken' )
1392  ->willReturn( '0123456789abcdef' );
1393 
1394  $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1395  $user,
1396  [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1397  );
1398 
1399  $this->assertEmpty( $items );
1400  }
1401 
1402  public function invalidWatchlistTokenProvider() {
1403  return [
1404  [ 'wrongToken' ],
1405  [ '' ],
1406  ];
1407  }
1408 
1413  $mockDb = $this->getMockDb();
1414  $mockDb->expects( $this->never() )
1415  ->method( $this->anything() );
1416 
1417  $queryService = $this->newService( $mockDb );
1419  $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1420  $otherUser->expects( $this->once() )
1421  ->method( 'getOption' )
1422  ->with( 'watchlisttoken' )
1423  ->willReturn( '0123456789abcdef' );
1424 
1425  $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' );
1426  $queryService->getWatchedItemsWithRecentChangeInfo(
1427  $user,
1428  [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1429  );
1430  }
1431 
1432  public function testGetWatchedItemsForUser() {
1433  $mockDb = $this->getMockDb();
1434  $mockDb->expects( $this->once() )
1435  ->method( 'select' )
1436  ->with(
1437  'watchlist',
1438  [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1439  [ 'wl_user' => 1 ]
1440  )
1441  ->will( $this->returnValue( [
1442  $this->getFakeRow( [
1443  'wl_namespace' => 0,
1444  'wl_title' => 'Foo1',
1445  'wl_notificationtimestamp' => '20151212010101',
1446  ] ),
1447  $this->getFakeRow( [
1448  'wl_namespace' => 1,
1449  'wl_title' => 'Foo2',
1450  'wl_notificationtimestamp' => null,
1451  ] ),
1452  ] ) );
1453 
1454  $queryService = $this->newService( $mockDb );
1455  $user = $this->getMockNonAnonUserWithId( 1 );
1456 
1457  $items = $queryService->getWatchedItemsForUser( $user );
1458 
1459  $this->assertInternalType( 'array', $items );
1460  $this->assertCount( 2, $items );
1461  $this->assertContainsOnlyInstancesOf( WatchedItem::class, $items );
1462  $this->assertEquals(
1463  new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1464  $items[0]
1465  );
1466  $this->assertEquals(
1467  new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1468  $items[1]
1469  );
1470  }
1471 
1473  return [
1474  [
1475  [ 'namespaceIds' => [ 0, 1 ], ],
1476  [ 'wl_namespace' => [ 0, 1 ], ],
1477  []
1478  ],
1479  [
1480  [ 'sort' => WatchedItemQueryService::SORT_ASC, ],
1481  [],
1482  [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1483  ],
1484  [
1485  [
1486  'namespaceIds' => [ 0 ],
1488  ],
1489  [ 'wl_namespace' => [ 0 ], ],
1490  [ 'ORDER BY' => 'wl_title ASC' ]
1491  ],
1492  [
1493  [ 'limit' => 10 ],
1494  [],
1495  [ 'LIMIT' => 10 ]
1496  ],
1497  [
1498  [
1499  'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1500  'limit' => "10; DROP TABLE watchlist;\n--",
1501  ],
1502  [ 'wl_namespace' => [ 0, 1 ], ],
1503  [ 'LIMIT' => 10 ]
1504  ],
1505  [
1507  [ 'wl_notificationtimestamp IS NOT NULL' ],
1508  []
1509  ],
1510  [
1512  [ 'wl_notificationtimestamp IS NULL' ],
1513  []
1514  ],
1515  [
1516  [ 'sort' => WatchedItemQueryService::SORT_DESC, ],
1517  [],
1518  [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1519  ],
1520  [
1521  [
1522  'namespaceIds' => [ 0 ],
1524  ],
1525  [ 'wl_namespace' => [ 0 ], ],
1526  [ 'ORDER BY' => 'wl_title DESC' ]
1527  ],
1528  ];
1529  }
1530 
1535  array $options,
1536  array $expectedConds,
1537  array $expectedDbOptions
1538  ) {
1539  $mockDb = $this->getMockDb();
1540  $user = $this->getMockNonAnonUserWithId( 1 );
1541 
1542  $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1543  $mockDb->expects( $this->once() )
1544  ->method( 'select' )
1545  ->with(
1546  'watchlist',
1547  [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1548  $expectedConds,
1549  $this->isType( 'string' ),
1550  $expectedDbOptions
1551  )
1552  ->will( $this->returnValue( [] ) );
1553 
1554  $queryService = $this->newService( $mockDb );
1555 
1556  $items = $queryService->getWatchedItemsForUser( $user, $options );
1557  $this->assertEmpty( $items );
1558  }
1559 
1561  return [
1562  [
1563  [
1564  'from' => new TitleValue( 0, 'SomeDbKey' ),
1566  ],
1567  [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1568  [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1569  ],
1570  [
1571  [
1572  'from' => new TitleValue( 0, 'SomeDbKey' ),
1574  ],
1575  [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1576  [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1577  ],
1578  [
1579  [
1580  'until' => new TitleValue( 0, 'SomeDbKey' ),
1582  ],
1583  [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1584  [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1585  ],
1586  [
1587  [
1588  'until' => new TitleValue( 0, 'SomeDbKey' ),
1590  ],
1591  [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1592  [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1593  ],
1594  [
1595  [
1596  'from' => new TitleValue( 0, 'AnotherDbKey' ),
1597  'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1598  'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1600  ],
1601  [
1602  "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1603  "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1604  "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1605  ],
1606  [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1607  ],
1608  [
1609  [
1610  'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1611  'until' => new TitleValue( 0, 'AnotherDbKey' ),
1612  'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1614  ],
1615  [
1616  "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1617  "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1618  "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1619  ],
1620  [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1621  ],
1622  ];
1623  }
1624 
1629  array $options,
1630  array $expectedConds,
1631  array $expectedDbOptions
1632  ) {
1633  $user = $this->getMockNonAnonUserWithId( 1 );
1634 
1635  $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1636 
1637  $mockDb = $this->getMockDb();
1638  $mockDb->expects( $this->any() )
1639  ->method( 'addQuotes' )
1640  ->will( $this->returnCallback( function ( $value ) {
1641  return "'$value'";
1642  } ) );
1643  $mockDb->expects( $this->any() )
1644  ->method( 'makeList' )
1645  ->with(
1646  $this->isType( 'array' ),
1647  $this->isType( 'int' )
1648  )
1649  ->will( $this->returnCallback( function ( $a, $conj ) {
1650  $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
1651  return implode( $sqlConj, array_map( function ( $s ) {
1652  return '(' . $s . ')';
1653  }, $a
1654  ) );
1655  } ) );
1656  $mockDb->expects( $this->once() )
1657  ->method( 'select' )
1658  ->with(
1659  'watchlist',
1660  [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1661  $expectedConds,
1662  $this->isType( 'string' ),
1663  $expectedDbOptions
1664  )
1665  ->will( $this->returnValue( [] ) );
1666 
1667  $queryService = $this->newService( $mockDb );
1668 
1669  $items = $queryService->getWatchedItemsForUser( $user, $options );
1670  $this->assertEmpty( $items );
1671  }
1672 
1674  return [
1675  [
1676  [ 'sort' => 'foo' ],
1677  'Bad value for parameter $options[\'sort\']'
1678  ],
1679  [
1680  [ 'filter' => 'foo' ],
1681  'Bad value for parameter $options[\'filter\']'
1682  ],
1683  [
1684  [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1685  'Bad value for parameter $options[\'sort\']: must be provided'
1686  ],
1687  [
1688  [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1689  'Bad value for parameter $options[\'sort\']: must be provided'
1690  ],
1691  [
1692  [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1693  'Bad value for parameter $options[\'sort\']: must be provided'
1694  ],
1695  ];
1696  }
1697 
1702  array $options,
1703  $expectedInExceptionMessage
1704  ) {
1705  $queryService = $this->newService( $this->getMockDb() );
1706 
1707  $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
1708  $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1709  }
1710 
1712  $mockDb = $this->getMockDb();
1713 
1714  $mockDb->expects( $this->never() )
1715  ->method( $this->anything() );
1716 
1717  $queryService = $this->newService( $mockDb );
1718 
1719  $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
1720  $this->assertEmpty( $items );
1721  }
1722 
1723 }
Revision\DELETED_USER
const DELETED_USER
Definition: Revision.php:48
WatchedItemQueryServiceUnitTest\testGetWatchedItemsForUser
testGetWatchedItemsForUser()
Definition: WatchedItemQueryServiceUnitTest.php:1432
WatchedItemQueryServiceUnitTest\mysqlIndexOptimizationProvider
mysqlIndexOptimizationProvider()
Definition: WatchedItemQueryServiceUnitTest.php:981
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization
testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization( $dbType, array $options, array $expectedExtraConds)
mysqlIndexOptimizationProvider
Definition: WatchedItemQueryServiceUnitTest.php:1009
$user
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1476
Revision\DELETED_RESTRICTED
const DELETED_RESTRICTED
Definition: Revision.php:49
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken
testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token)
invalidWatchlistTokenProvider
Definition: WatchedItemQueryServiceUnitTest.php:1412
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions
testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions()
Definition: WatchedItemQueryServiceUnitTest.php:1327
WatchedItemQueryService\INCLUDE_COMMENT
const INCLUDE_COMMENT
Definition: WatchedItemQueryService.php:26
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo
testGetWatchedItemsWithRecentChangeInfo()
Definition: WatchedItemQueryServiceUnitTest.php:255
WatchedItemQueryServiceUnitTest\getWatchedItemsWithRecentChangeInfoOptionsProvider
getWatchedItemsWithRecentChangeInfoOptionsProvider()
Definition: WatchedItemQueryServiceUnitTest.php:538
WatchedItemQueryServiceUnitTest
WatchedItemQueryService.
Definition: WatchedItemQueryServiceUnitTest.php:9
WatchedItemQueryServiceUnitTest\getMockAnonUser
getMockAnonUser()
Definition: WatchedItemQueryServiceUnitTest.php:239
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult
testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult()
Definition: WatchedItemQueryServiceUnitTest.php:1155
WatchedItemQueryServiceUnitTest\getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider()
Definition: WatchedItemQueryServiceUnitTest.php:1197
WatchedItemQueryServiceUnitTest\getMockNonAnonUserWithIdAndRestrictedPermissions
getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction)
Definition: WatchedItemQueryServiceUnitTest.php:197
WatchedItemQueryService\FILTER_PATROLLED
const FILTER_PATROLLED
Definition: WatchedItemQueryService.php:42
WatchedItemQueryServiceUnitTest\getMockLoadBalancer
getMockLoadBalancer( $mockDb)
Definition: WatchedItemQueryServiceUnitTest.php:132
WatchedItemQueryServiceUnitTest\testGetWatchedItemsForUser_invalidOptionThrowsException
testGetWatchedItemsForUser_invalidOptionThrowsException(array $options, $expectedInExceptionMessage)
getWatchedItemsForUserInvalidOptionsProvider
Definition: WatchedItemQueryServiceUnitTest.php:1701
RC_LOG
const RC_LOG
Definition: Defines.php:144
$tables
this hook is for auditing only RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition: hooks.txt:979
anything
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same so they can t rely on Unix and must forbid reads to even standard directories like tmp lest users read each others files We cannot assume that the user has the ability to install or run any programs not written as web accessible PHP scripts Since anything that works on cheap shared hosting will work if you have shell or root access MediaWiki s design is based around catering to the lowest common denominator Although we support higher end setups as the way many things work by default is tailored toward shared hosting These defaults are unconventional from the point of view of and they certainly aren t ideal for someone who s installing MediaWiki as MediaWiki does not conform to normal Unix filesystem layout Hopefully we ll offer direct support for standard layouts in the but for now *any change to the location of files is unsupported *Moving things and leaving symlinks will *probably *not break anything
Definition: distributors.txt:39
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_extension
testGetWatchedItemsWithRecentChangeInfo_extension()
Definition: WatchedItemQueryServiceUnitTest.php:376
RC_EDIT
const RC_EDIT
Definition: Defines.php:142
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult
testGetWatchedItemsWithRecentChangeInfo_optionsAndEmptyResult(array $options, $startFrom, array $expectedExtraTables, array $expectedExtraFields, array $expectedExtraConds, array $expectedDbOptions, array $expectedExtraJoinConds)
getWatchedItemsWithRecentChangeInfoOptionsProvider
Definition: WatchedItemQueryServiceUnitTest.php:875
$s
$s
Definition: mergeMessageFileList.php:186
$res
$res
Definition: database.txt:21
WatchedItemQueryServiceUnitTest\newService
newService( $mockDb)
Definition: WatchedItemQueryServiceUnitTest.php:71
WatchedItemQueryServiceUnitTest\filterPatrolledOptionProvider
filterPatrolledOptionProvider()
Definition: WatchedItemQueryServiceUnitTest.php:944
php
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
LIST_AND
const LIST_AND
Definition: Defines.php:43
WatchedItemQueryService\INCLUDE_LOG_INFO
const INCLUDE_LOG_INFO
Definition: WatchedItemQueryService.php:30
WatchedItemQueryServiceUnitTest\getMockNonAnonUserWithIdAndNoPatrolRights
getMockNonAnonUserWithIdAndNoPatrolRights( $id)
Definition: WatchedItemQueryServiceUnitTest.php:219
WatchedItemQueryServiceUnitTest\getWatchedItemsForUserInvalidOptionsProvider
getWatchedItemsForUserInvalidOptionsProvider()
Definition: WatchedItemQueryServiceUnitTest.php:1673
WatchedItemQueryService\FILTER_MINOR
const FILTER_MINOR
Definition: WatchedItemQueryService.php:36
WatchedItemQueryService\FILTER_NOT_CHANGED
const FILTER_NOT_CHANGED
Definition: WatchedItemQueryService.php:49
WatchedItemQueryService\INCLUDE_USER
const INCLUDE_USER
Definition: WatchedItemQueryService.php:24
WatchedItemQueryService\FILTER_NOT_BOT
const FILTER_NOT_BOT
Definition: WatchedItemQueryService.php:39
MediaWikiTestCase
Definition: MediaWikiTestCase.php:17
WatchedItemQueryService
Definition: WatchedItemQueryService.php:18
WatchedItemQueryService\INCLUDE_PATROL_INFO
const INCLUDE_PATROL_INFO
Definition: WatchedItemQueryService.php:27
WatchedItemQueryServiceUnitTest\getMockActorMigration
getMockActorMigration()
Definition: WatchedItemQueryServiceUnitTest.php:36
WatchedItemQueryServiceUnitTest\getMockWatchedItemStore
getMockWatchedItemStore()
Definition: WatchedItemQueryServiceUnitTest.php:147
use
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:10
WatchedItemQueryServiceUnitTest\testGetWatchedItemsForUser_userNotAllowedToViewWatchlist
testGetWatchedItemsForUser_userNotAllowedToViewWatchlist()
Definition: WatchedItemQueryServiceUnitTest.php:1711
WatchedItemQueryService\FILTER_BOT
const FILTER_BOT
Definition: WatchedItemQueryService.php:38
WatchedItemQueryService\INCLUDE_USER_ID
const INCLUDE_USER_ID
Definition: WatchedItemQueryService.php:25
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
WatchedItemQueryServiceUnitTest\invalidWatchlistTokenProvider
invalidWatchlistTokenProvider()
Definition: WatchedItemQueryServiceUnitTest.php:1402
array
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
WatchedItemQueryServiceUnitTest\provideGetWatchedItemsForUserOptions
provideGetWatchedItemsForUserOptions()
Definition: WatchedItemQueryServiceUnitTest.php:1472
list
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_invalidOptions
testGetWatchedItemsWithRecentChangeInfo_invalidOptions(array $options, $startFrom, $expectedInExceptionMessage)
getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
Definition: WatchedItemQueryServiceUnitTest.php:1265
LogPage\DELETED_ACTION
const DELETED_ACTION
Definition: LogPage.php:34
Wikimedia\Rdbms\LoadBalancer
Database connection, tracking, load balancing, and transaction manager for a cluster.
Definition: LoadBalancer.php:41
WatchedItemQueryService\DIR_NEWER
const DIR_NEWER
Definition: WatchedItemQueryService.php:21
any
they could even be mouse clicks or menu items whatever suits your program You should also get your if any
Definition: COPYING.txt:326
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks
testGetWatchedItemsWithRecentChangeInfo_userPermissionRelatedExtraChecks(array $options, $notAllowedAction, array $expectedExtraTables, array $expectedExtraConds, array $expectedExtraJoins)
userPermissionRelatedExtraChecksProvider
Definition: WatchedItemQueryServiceUnitTest.php:1121
$value
$value
Definition: styleTest.css.php:49
WatchedItemQueryServiceUnitTest\getMockUnrestrictedNonAnonUserWithId
getMockUnrestrictedNonAnonUserWithId( $id)
Definition: WatchedItemQueryServiceUnitTest.php:178
WatchedItemQueryService\SORT_DESC
const SORT_DESC
Definition: WatchedItemQueryService.php:52
WatchedItem
Representation of a pair of user and title for watchlist entries.
Definition: WatchedItem.php:32
WatchedItemQueryService\FILTER_NOT_PATROLLED
const FILTER_NOT_PATROLLED
Definition: WatchedItemQueryService.php:43
WatchedItemQueryService\SORT_ASC
const SORT_ASC
Definition: WatchedItemQueryService.php:51
WatchedItemQueryService\FILTER_NOT_MINOR
const FILTER_NOT_MINOR
Definition: WatchedItemQueryService.php:37
WatchedItemQueryServiceUnitTest\getMockCommentStore
getMockCommentStore()
Definition: WatchedItemQueryServiceUnitTest.php:16
RC_NEW
const RC_NEW
Definition: Defines.php:143
WatchedItemQueryService\INCLUDE_FLAGS
const INCLUDE_FLAGS
Definition: WatchedItemQueryService.php:23
WatchedItemQueryServiceUnitTest\testGetWatchedItemsForUser_optionsAndEmptyResult
testGetWatchedItemsForUser_optionsAndEmptyResult(array $options, array $expectedConds, array $expectedDbOptions)
provideGetWatchedItemsForUserOptions
Definition: WatchedItemQueryServiceUnitTest.php:1534
WatchedItemQueryService\DIR_OLDER
const DIR_OLDER
Definition: WatchedItemQueryService.php:20
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult
testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult()
Definition: WatchedItemQueryServiceUnitTest.php:1281
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult
testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult()
Definition: WatchedItemQueryServiceUnitTest.php:1369
$options
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1985
WatchedItemQueryService\FILTER_CHANGED
const FILTER_CHANGED
Definition: WatchedItemQueryService.php:48
WatchedItemQueryService\FILTER_NOT_UNREAD
const FILTER_NOT_UNREAD
Definition: WatchedItemQueryService.php:47
WatchedItemQueryServiceUnitTest\provideGetWatchedItemsForUser_fromUntilStartFromOptions
provideGetWatchedItemsForUser_fromUntilStartFromOptions()
Definition: WatchedItemQueryServiceUnitTest.php:1560
as
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
WatchedItemQueryService\FILTER_NOT_ANON
const FILTER_NOT_ANON
Definition: WatchedItemQueryService.php:41
WatchedItemQueryServiceUnitTest\getMockDb
getMockDb()
Definition: WatchedItemQueryServiceUnitTest.php:83
LogPage\DELETED_RESTRICTED
const DELETED_RESTRICTED
Definition: LogPage.php:37
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights
testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights( $filtersOption)
filterPatrolledOptionProvider
Definition: WatchedItemQueryServiceUnitTest.php:954
class
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
WatchedItemQueryServiceUnitTest\getMockNonAnonUserWithId
getMockNonAnonUserWithId( $id)
Definition: WatchedItemQueryServiceUnitTest.php:163
WatchedItemQueryService\INCLUDE_SIZES
const INCLUDE_SIZES
Definition: WatchedItemQueryService.php:29
WatchedItemQueryServiceUnitTest\testGetWatchedItemsForUser_fromUntilStartFromOptions
testGetWatchedItemsForUser_fromUntilStartFromOptions(array $options, array $expectedConds, array $expectedDbOptions)
provideGetWatchedItemsForUser_fromUntilStartFromOptions
Definition: WatchedItemQueryServiceUnitTest.php:1628
WatchedItemQueryService\FILTER_ANON
const FILTER_ANON
Definition: WatchedItemQueryService.php:40
WatchedItemQueryService\FILTER_UNREAD
const FILTER_UNREAD
Definition: WatchedItemQueryService.php:46
WatchedItemQueryServiceUnitTest\getFakeRow
getFakeRow(array $rowValues)
Definition: WatchedItemQueryServiceUnitTest.php:247
WatchedItemQueryServiceUnitTest\userPermissionRelatedExtraChecksProvider
userPermissionRelatedExtraChecksProvider()
Definition: WatchedItemQueryServiceUnitTest.php:1041
MediaWikiTestCase\$db
Database $db
Primary database.
Definition: MediaWikiTestCase.php:61
TitleValue
Represents a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:36