MediaWiki  1.32.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()
76  );
77  }
78 
82  private function getMockDb() {
83  $mock = $this->getMockBuilder( Database::class )
84  ->disableOriginalConstructor()
85  ->getMock();
86 
87  $mock->expects( $this->any() )
88  ->method( 'makeList' )
89  ->with(
90  $this->isType( 'array' ),
91  $this->isType( 'int' )
92  )
93  ->will( $this->returnCallback( function ( $a, $conj ) {
94  $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
95  $conds = [];
96  foreach ( $a as $k => $v ) {
97  if ( is_int( $k ) ) {
98  $conds[] = "($v)";
99  } elseif ( is_array( $v ) ) {
100  $conds[] = "($k IN ('" . implode( "','", $v ) . "'))";
101  } else {
102  $conds[] = "($k = '$v')";
103  }
104  }
105  return implode( $sqlConj, $conds );
106  } ) );
107 
108  $mock->expects( $this->any() )
109  ->method( 'addQuotes' )
110  ->will( $this->returnCallback( function ( $value ) {
111  return "'$value'";
112  } ) );
113 
114  $mock->expects( $this->any() )
115  ->method( 'timestamp' )
116  ->will( $this->returnArgument( 0 ) );
117 
118  $mock->expects( $this->any() )
119  ->method( 'bitAnd' )
120  ->willReturnCallback( function ( $a, $b ) {
121  return "($a & $b)";
122  } );
123 
124  return $mock;
125  }
126 
131  private function getMockLoadBalancer( $mockDb ) {
132  $mock = $this->getMockBuilder( LoadBalancer::class )
133  ->disableOriginalConstructor()
134  ->getMock();
135  $mock->expects( $this->any() )
136  ->method( 'getConnectionRef' )
137  ->with( DB_REPLICA )
138  ->will( $this->returnValue( $mockDb ) );
139  return $mock;
140  }
141 
146  private function getMockNonAnonUserWithId( $id ) {
147  $mock = $this->getMockBuilder( User::class )->getMock();
148  $mock->expects( $this->any() )
149  ->method( 'isAnon' )
150  ->will( $this->returnValue( false ) );
151  $mock->expects( $this->any() )
152  ->method( 'getId' )
153  ->will( $this->returnValue( $id ) );
154  return $mock;
155  }
156 
161  private function getMockUnrestrictedNonAnonUserWithId( $id ) {
162  $mock = $this->getMockNonAnonUserWithId( $id );
163  $mock->expects( $this->any() )
164  ->method( 'isAllowed' )
165  ->will( $this->returnValue( true ) );
166  $mock->expects( $this->any() )
167  ->method( 'isAllowedAny' )
168  ->will( $this->returnValue( true ) );
169  $mock->expects( $this->any() )
170  ->method( 'useRCPatrol' )
171  ->will( $this->returnValue( true ) );
172  return $mock;
173  }
174 
180  private function getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction ) {
181  $mock = $this->getMockNonAnonUserWithId( $id );
182 
183  $mock->expects( $this->any() )
184  ->method( 'isAllowed' )
185  ->will( $this->returnCallback( function ( $action ) use ( $notAllowedAction ) {
186  return $action !== $notAllowedAction;
187  } ) );
188  $mock->expects( $this->any() )
189  ->method( 'isAllowedAny' )
190  ->will( $this->returnCallback( function () use ( $notAllowedAction ) {
191  $actions = func_get_args();
192  return !in_array( $notAllowedAction, $actions );
193  } ) );
194 
195  return $mock;
196  }
197 
203  $mock = $this->getMockNonAnonUserWithId( $id );
204 
205  $mock->expects( $this->any() )
206  ->method( 'isAllowed' )
207  ->will( $this->returnValue( true ) );
208  $mock->expects( $this->any() )
209  ->method( 'isAllowedAny' )
210  ->will( $this->returnValue( true ) );
211 
212  $mock->expects( $this->any() )
213  ->method( 'useRCPatrol' )
214  ->will( $this->returnValue( false ) );
215  $mock->expects( $this->any() )
216  ->method( 'useNPPatrol' )
217  ->will( $this->returnValue( false ) );
218 
219  return $mock;
220  }
221 
222  private function getMockAnonUser() {
223  $mock = $this->getMockBuilder( User::class )->getMock();
224  $mock->expects( $this->any() )
225  ->method( 'isAnon' )
226  ->will( $this->returnValue( true ) );
227  return $mock;
228  }
229 
230  private function getFakeRow( array $rowValues ) {
231  $fakeRow = new stdClass();
232  foreach ( $rowValues as $valueName => $value ) {
233  $fakeRow->$valueName = $value;
234  }
235  return $fakeRow;
236  }
237 
239  $mockDb = $this->getMockDb();
240  $mockDb->expects( $this->once() )
241  ->method( 'select' )
242  ->with(
243  [ 'recentchanges', 'watchlist', 'page' ],
244  [
245  'rc_id',
246  'rc_namespace',
247  'rc_title',
248  'rc_timestamp',
249  'rc_type',
250  'rc_deleted',
251  'wl_notificationtimestamp',
252  'rc_cur_id',
253  'rc_this_oldid',
254  'rc_last_oldid',
255  ],
256  [
257  'wl_user' => 1,
258  '(rc_this_oldid=page_latest) OR (rc_type=3)',
259  ],
260  $this->isType( 'string' ),
261  [
262  'LIMIT' => 3,
263  ],
264  [
265  'watchlist' => [
266  'INNER JOIN',
267  [
268  'wl_namespace=rc_namespace',
269  'wl_title=rc_title'
270  ]
271  ],
272  'page' => [
273  'LEFT JOIN',
274  'rc_cur_id=page_id',
275  ],
276  ]
277  )
278  ->will( $this->returnValue( [
279  $this->getFakeRow( [
280  'rc_id' => 1,
281  'rc_namespace' => 0,
282  'rc_title' => 'Foo1',
283  'rc_timestamp' => '20151212010101',
284  'rc_type' => RC_NEW,
285  'rc_deleted' => 0,
286  'wl_notificationtimestamp' => '20151212010101',
287  ] ),
288  $this->getFakeRow( [
289  'rc_id' => 2,
290  'rc_namespace' => 1,
291  'rc_title' => 'Foo2',
292  'rc_timestamp' => '20151212010102',
293  'rc_type' => RC_NEW,
294  'rc_deleted' => 0,
295  'wl_notificationtimestamp' => null,
296  ] ),
297  $this->getFakeRow( [
298  'rc_id' => 3,
299  'rc_namespace' => 1,
300  'rc_title' => 'Foo3',
301  'rc_timestamp' => '20151212010103',
302  'rc_type' => RC_NEW,
303  'rc_deleted' => 0,
304  'wl_notificationtimestamp' => null,
305  ] ),
306  ] ) );
307 
308  $queryService = $this->newService( $mockDb );
310 
311  $startFrom = null;
312  $items = $queryService->getWatchedItemsWithRecentChangeInfo(
313  $user, [ 'limit' => 2 ], $startFrom
314  );
315 
316  $this->assertInternalType( 'array', $items );
317  $this->assertCount( 2, $items );
318 
319  foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
320  $this->assertInstanceOf( WatchedItem::class, $watchedItem );
321  $this->assertInternalType( 'array', $recentChangeInfo );
322  }
323 
324  $this->assertEquals(
325  new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
326  $items[0][0]
327  );
328  $this->assertEquals(
329  [
330  'rc_id' => 1,
331  'rc_namespace' => 0,
332  'rc_title' => 'Foo1',
333  'rc_timestamp' => '20151212010101',
334  'rc_type' => RC_NEW,
335  'rc_deleted' => 0,
336  ],
337  $items[0][1]
338  );
339 
340  $this->assertEquals(
341  new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
342  $items[1][0]
343  );
344  $this->assertEquals(
345  [
346  'rc_id' => 2,
347  'rc_namespace' => 1,
348  'rc_title' => 'Foo2',
349  'rc_timestamp' => '20151212010102',
350  'rc_type' => RC_NEW,
351  'rc_deleted' => 0,
352  ],
353  $items[1][1]
354  );
355 
356  $this->assertEquals( [ '20151212010103', 3 ], $startFrom );
357  }
358 
360  $mockDb = $this->getMockDb();
361  $mockDb->expects( $this->once() )
362  ->method( 'select' )
363  ->with(
364  [ 'recentchanges', 'watchlist', 'page', 'extension_dummy_table' ],
365  [
366  'rc_id',
367  'rc_namespace',
368  'rc_title',
369  'rc_timestamp',
370  'rc_type',
371  'rc_deleted',
372  'wl_notificationtimestamp',
373  'rc_cur_id',
374  'rc_this_oldid',
375  'rc_last_oldid',
376  'extension_dummy_field',
377  ],
378  [
379  'wl_user' => 1,
380  '(rc_this_oldid=page_latest) OR (rc_type=3)',
381  'extension_dummy_cond',
382  ],
383  $this->isType( 'string' ),
384  [
385  'extension_dummy_option',
386  ],
387  [
388  'watchlist' => [
389  'INNER JOIN',
390  [
391  'wl_namespace=rc_namespace',
392  'wl_title=rc_title'
393  ]
394  ],
395  'page' => [
396  'LEFT JOIN',
397  'rc_cur_id=page_id',
398  ],
399  'extension_dummy_join_cond' => [],
400  ]
401  )
402  ->will( $this->returnValue( [
403  $this->getFakeRow( [
404  'rc_id' => 1,
405  'rc_namespace' => 0,
406  'rc_title' => 'Foo1',
407  'rc_timestamp' => '20151212010101',
408  'rc_type' => RC_NEW,
409  'rc_deleted' => 0,
410  'wl_notificationtimestamp' => '20151212010101',
411  ] ),
412  $this->getFakeRow( [
413  'rc_id' => 2,
414  'rc_namespace' => 1,
415  'rc_title' => 'Foo2',
416  'rc_timestamp' => '20151212010102',
417  'rc_type' => RC_NEW,
418  'rc_deleted' => 0,
419  'wl_notificationtimestamp' => null,
420  ] ),
421  ] ) );
422 
424 
425  $mockExtension = $this->getMockBuilder( WatchedItemQueryServiceExtension::class )
426  ->getMock();
427  $mockExtension->expects( $this->once() )
428  ->method( 'modifyWatchedItemsWithRCInfoQuery' )
429  ->with(
430  $this->identicalTo( $user ),
431  $this->isType( 'array' ),
432  $this->isInstanceOf( IDatabase::class ),
433  $this->isType( 'array' ),
434  $this->isType( 'array' ),
435  $this->isType( 'array' ),
436  $this->isType( 'array' ),
437  $this->isType( 'array' )
438  )
439  ->will( $this->returnCallback( function (
440  $user, $options, $db, &$tables, &$fields, &$conds, &$dbOptions, &$joinConds
441  ) {
442  $tables[] = 'extension_dummy_table';
443  $fields[] = 'extension_dummy_field';
444  $conds[] = 'extension_dummy_cond';
445  $dbOptions[] = 'extension_dummy_option';
446  $joinConds['extension_dummy_join_cond'] = [];
447  } ) );
448  $mockExtension->expects( $this->once() )
449  ->method( 'modifyWatchedItemsWithRCInfo' )
450  ->with(
451  $this->identicalTo( $user ),
452  $this->isType( 'array' ),
453  $this->isInstanceOf( IDatabase::class ),
454  $this->isType( 'array' ),
455  $this->anything(),
456  $this->anything() // Can't test for null here, PHPUnit applies this after the callback
457  )
458  ->will( $this->returnCallback( function ( $user, $options, $db, &$items, $res, &$startFrom ) {
459  foreach ( $items as $i => &$item ) {
460  $item[1]['extension_dummy_field'] = $i;
461  }
462  unset( $item );
463 
464  $this->assertNull( $startFrom );
465  $startFrom = [ '20160203123456', 42 ];
466  } ) );
467 
468  $queryService = $this->newService( $mockDb );
469  TestingAccessWrapper::newFromObject( $queryService )->extensions = [ $mockExtension ];
470 
471  $startFrom = null;
472  $items = $queryService->getWatchedItemsWithRecentChangeInfo(
473  $user, [], $startFrom
474  );
475 
476  $this->assertInternalType( 'array', $items );
477  $this->assertCount( 2, $items );
478 
479  foreach ( $items as list( $watchedItem, $recentChangeInfo ) ) {
480  $this->assertInstanceOf( WatchedItem::class, $watchedItem );
481  $this->assertInternalType( 'array', $recentChangeInfo );
482  }
483 
484  $this->assertEquals(
485  new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
486  $items[0][0]
487  );
488  $this->assertEquals(
489  [
490  'rc_id' => 1,
491  'rc_namespace' => 0,
492  'rc_title' => 'Foo1',
493  'rc_timestamp' => '20151212010101',
494  'rc_type' => RC_NEW,
495  'rc_deleted' => 0,
496  'extension_dummy_field' => 0,
497  ],
498  $items[0][1]
499  );
500 
501  $this->assertEquals(
502  new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
503  $items[1][0]
504  );
505  $this->assertEquals(
506  [
507  'rc_id' => 2,
508  'rc_namespace' => 1,
509  'rc_title' => 'Foo2',
510  'rc_timestamp' => '20151212010102',
511  'rc_type' => RC_NEW,
512  'rc_deleted' => 0,
513  'extension_dummy_field' => 1,
514  ],
515  $items[1][1]
516  );
517 
518  $this->assertEquals( [ '20160203123456', 42 ], $startFrom );
519  }
520 
522  return [
523  [
524  [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_FLAGS ] ],
525  null,
526  [],
527  [ 'rc_type', 'rc_minor', 'rc_bot' ],
528  [],
529  [],
530  [],
531  ],
532  [
533  [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER ] ],
534  null,
535  [ 'actormigration' => 'table' ],
536  [ 'rc_user_text' => 'actormigration_user_text' ],
537  [],
538  [],
539  [ 'actormigration' => 'join' ],
540  ],
541  [
542  [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_USER_ID ] ],
543  null,
544  [ 'actormigration' => 'table' ],
545  [ 'rc_user' => 'actormigration_user' ],
546  [],
547  [],
548  [ 'actormigration' => 'join' ],
549  ],
550  [
551  [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_COMMENT ] ],
552  null,
553  [ 'commentstore' => 'table' ],
554  [ 'commentstore' => 'field' ],
555  [],
556  [],
557  [ 'commentstore' => 'join' ],
558  ],
559  [
560  [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_PATROL_INFO ] ],
561  null,
562  [],
563  [ 'rc_patrolled', 'rc_log_type' ],
564  [],
565  [],
566  [],
567  ],
568  [
569  [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_SIZES ] ],
570  null,
571  [],
572  [ 'rc_old_len', 'rc_new_len' ],
573  [],
574  [],
575  [],
576  ],
577  [
578  [ 'includeFields' => [ WatchedItemQueryService::INCLUDE_LOG_INFO ] ],
579  null,
580  [],
581  [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ],
582  [],
583  [],
584  [],
585  ],
586  [
587  [ 'namespaceIds' => [ 0, 1 ] ],
588  null,
589  [],
590  [],
591  [ 'wl_namespace' => [ 0, 1 ] ],
592  [],
593  [],
594  ],
595  [
596  [ 'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ] ],
597  null,
598  [],
599  [],
600  [ 'wl_namespace' => [ 0, 1 ] ],
601  [],
602  [],
603  ],
604  [
605  [ 'rcTypes' => [ RC_EDIT, RC_NEW ] ],
606  null,
607  [],
608  [],
609  [ 'rc_type' => [ RC_EDIT, RC_NEW ] ],
610  [],
611  [],
612  ],
613  [
615  null,
616  [],
617  [],
618  [],
619  [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
620  [],
621  ],
622  [
624  null,
625  [],
626  [],
627  [],
628  [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
629  [],
630  ],
631  [
632  [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'start' => '20151212010101' ],
633  null,
634  [],
635  [],
636  [ "rc_timestamp <= '20151212010101'" ],
637  [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
638  [],
639  ],
640  [
641  [ 'dir' => WatchedItemQueryService::DIR_OLDER, 'end' => '20151212010101' ],
642  null,
643  [],
644  [],
645  [ "rc_timestamp >= '20151212010101'" ],
646  [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
647  [],
648  ],
649  [
650  [
652  'start' => '20151212020101',
653  'end' => '20151212010101'
654  ],
655  null,
656  [],
657  [],
658  [ "rc_timestamp <= '20151212020101'", "rc_timestamp >= '20151212010101'" ],
659  [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
660  [],
661  ],
662  [
663  [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'start' => '20151212010101' ],
664  null,
665  [],
666  [],
667  [ "rc_timestamp >= '20151212010101'" ],
668  [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
669  [],
670  ],
671  [
672  [ 'dir' => WatchedItemQueryService::DIR_NEWER, 'end' => '20151212010101' ],
673  null,
674  [],
675  [],
676  [ "rc_timestamp <= '20151212010101'" ],
677  [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
678  [],
679  ],
680  [
681  [
683  'start' => '20151212010101',
684  'end' => '20151212020101'
685  ],
686  null,
687  [],
688  [],
689  [ "rc_timestamp >= '20151212010101'", "rc_timestamp <= '20151212020101'" ],
690  [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
691  [],
692  ],
693  [
694  [ 'limit' => 10 ],
695  null,
696  [],
697  [],
698  [],
699  [ 'LIMIT' => 11 ],
700  [],
701  ],
702  [
703  [ 'limit' => "10; DROP TABLE watchlist;\n--" ],
704  null,
705  [],
706  [],
707  [],
708  [ 'LIMIT' => 11 ],
709  [],
710  ],
711  [
712  [ 'filters' => [ WatchedItemQueryService::FILTER_MINOR ] ],
713  null,
714  [],
715  [],
716  [ 'rc_minor != 0' ],
717  [],
718  [],
719  ],
720  [
721  [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_MINOR ] ],
722  null,
723  [],
724  [],
725  [ 'rc_minor = 0' ],
726  [],
727  [],
728  ],
729  [
730  [ 'filters' => [ WatchedItemQueryService::FILTER_BOT ] ],
731  null,
732  [],
733  [],
734  [ 'rc_bot != 0' ],
735  [],
736  [],
737  ],
738  [
739  [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_BOT ] ],
740  null,
741  [],
742  [],
743  [ 'rc_bot = 0' ],
744  [],
745  [],
746  ],
747  [
748  [ 'filters' => [ WatchedItemQueryService::FILTER_ANON ] ],
749  null,
750  [ 'actormigration' => 'table' ],
751  [],
752  [ 'actormigration is anon' ],
753  [],
754  [ 'actormigration' => 'join' ],
755  ],
756  [
757  [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_ANON ] ],
758  null,
759  [ 'actormigration' => 'table' ],
760  [],
761  [ 'actormigration is not anon' ],
762  [],
763  [ 'actormigration' => 'join' ],
764  ],
765  [
766  [ 'filters' => [ WatchedItemQueryService::FILTER_PATROLLED ] ],
767  null,
768  [],
769  [],
770  [ 'rc_patrolled != 0' ],
771  [],
772  [],
773  ],
774  [
776  null,
777  [],
778  [],
779  [ 'rc_patrolled' => 0 ],
780  [],
781  [],
782  ],
783  [
784  [ 'filters' => [ WatchedItemQueryService::FILTER_UNREAD ] ],
785  null,
786  [],
787  [],
788  [ 'rc_timestamp >= wl_notificationtimestamp' ],
789  [],
790  [],
791  ],
792  [
793  [ 'filters' => [ WatchedItemQueryService::FILTER_NOT_UNREAD ] ],
794  null,
795  [],
796  [],
797  [ 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp' ],
798  [],
799  [],
800  ],
801  [
802  [ 'onlyByUser' => 'SomeOtherUser' ],
803  null,
804  [ 'actormigration' => 'table' ],
805  [],
806  [ 'actormigration_conds' ],
807  [],
808  [ 'actormigration' => 'join' ],
809  ],
810  [
811  [ 'notByUser' => 'SomeOtherUser' ],
812  null,
813  [ 'actormigration' => 'table' ],
814  [],
815  [ 'NOT(actormigration_conds)' ],
816  [],
817  [ 'actormigration' => 'join' ],
818  ],
819  [
821  [ '20151212010101', 123 ],
822  [],
823  [],
824  [
825  "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
826  ],
827  [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
828  [],
829  ],
830  [
832  [ '20151212010101', 123 ],
833  [],
834  [],
835  [
836  "(rc_timestamp > '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id >= 123))"
837  ],
838  [ 'ORDER BY' => [ 'rc_timestamp', 'rc_id' ] ],
839  [],
840  ],
841  [
843  [ '20151212010101', "123; DROP TABLE watchlist;\n--" ],
844  [],
845  [],
846  [
847  "(rc_timestamp < '20151212010101') OR ((rc_timestamp = '20151212010101') AND (rc_id <= 123))"
848  ],
849  [ 'ORDER BY' => [ 'rc_timestamp DESC', 'rc_id DESC' ] ],
850  [],
851  ],
852  ];
853  }
854 
859  array $options,
860  $startFrom,
861  array $expectedExtraTables,
862  array $expectedExtraFields,
863  array $expectedExtraConds,
864  array $expectedDbOptions,
865  array $expectedExtraJoinConds
866  ) {
867  $expectedTables = array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables );
868  $expectedFields = array_merge(
869  [
870  'rc_id',
871  'rc_namespace',
872  'rc_title',
873  'rc_timestamp',
874  'rc_type',
875  'rc_deleted',
876  'wl_notificationtimestamp',
877 
878  'rc_cur_id',
879  'rc_this_oldid',
880  'rc_last_oldid',
881  ],
882  $expectedExtraFields
883  );
884  $expectedConds = array_merge(
885  [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)', ],
886  $expectedExtraConds
887  );
888  $expectedJoinConds = array_merge(
889  [
890  'watchlist' => [
891  'INNER JOIN',
892  [
893  'wl_namespace=rc_namespace',
894  'wl_title=rc_title'
895  ]
896  ],
897  'page' => [
898  'LEFT JOIN',
899  'rc_cur_id=page_id',
900  ],
901  ],
902  $expectedExtraJoinConds
903  );
904 
905  $mockDb = $this->getMockDb();
906  $mockDb->expects( $this->once() )
907  ->method( 'select' )
908  ->with(
909  $expectedTables,
910  $expectedFields,
911  $expectedConds,
912  $this->isType( 'string' ),
913  $expectedDbOptions,
914  $expectedJoinConds
915  )
916  ->will( $this->returnValue( [] ) );
917 
918  $queryService = $this->newService( $mockDb );
920 
921  $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
922 
923  $this->assertEmpty( $items );
924  $this->assertNull( $startFrom );
925  }
926 
927  public function filterPatrolledOptionProvider() {
928  return [
931  ];
932  }
933 
938  $filtersOption
939  ) {
940  $mockDb = $this->getMockDb();
941  $mockDb->expects( $this->once() )
942  ->method( 'select' )
943  ->with(
944  [ 'recentchanges', 'watchlist', 'page' ],
945  $this->isType( 'array' ),
946  [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
947  $this->isType( 'string' ),
948  $this->isType( 'array' ),
949  $this->isType( 'array' )
950  )
951  ->will( $this->returnValue( [] ) );
952 
954 
955  $queryService = $this->newService( $mockDb );
956  $items = $queryService->getWatchedItemsWithRecentChangeInfo(
957  $user,
958  [ 'filters' => [ $filtersOption ] ]
959  );
960 
961  $this->assertEmpty( $items );
962  }
963 
964  public function mysqlIndexOptimizationProvider() {
965  return [
966  [
967  'mysql',
968  [],
969  [ "rc_timestamp > ''" ],
970  ],
971  [
972  'mysql',
973  [ 'start' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
974  [ "rc_timestamp <= '20151212010101'" ],
975  ],
976  [
977  'mysql',
978  [ 'end' => '20151212010101', 'dir' => WatchedItemQueryService::DIR_OLDER ],
979  [ "rc_timestamp >= '20151212010101'" ],
980  ],
981  [
982  'postgres',
983  [],
984  [],
985  ],
986  ];
987  }
988 
993  $dbType,
994  array $options,
995  array $expectedExtraConds
996  ) {
997  $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
998  $conds = array_merge( $commonConds, $expectedExtraConds );
999 
1000  $mockDb = $this->getMockDb();
1001  $mockDb->expects( $this->once() )
1002  ->method( 'select' )
1003  ->with(
1004  [ 'recentchanges', 'watchlist', 'page' ],
1005  $this->isType( 'array' ),
1006  $conds,
1007  $this->isType( 'string' ),
1008  $this->isType( 'array' ),
1009  $this->isType( 'array' )
1010  )
1011  ->will( $this->returnValue( [] ) );
1012  $mockDb->expects( $this->any() )
1013  ->method( 'getType' )
1014  ->will( $this->returnValue( $dbType ) );
1015 
1016  $queryService = $this->newService( $mockDb );
1018 
1019  $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1020 
1021  $this->assertEmpty( $items );
1022  }
1023 
1025  return [
1026  [
1027  [],
1028  'deletedhistory',
1029  [],
1030  [
1031  '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
1033  ],
1034  [],
1035  ],
1036  [
1037  [],
1038  'suppressrevision',
1039  [],
1040  [
1041  '(rc_type != ' . RC_LOG . ') OR (' .
1042  '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
1044  ],
1045  [],
1046  ],
1047  [
1048  [],
1049  'viewsuppressed',
1050  [],
1051  [
1052  '(rc_type != ' . RC_LOG . ') OR (' .
1053  '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
1055  ],
1056  [],
1057  ],
1058  [
1059  [ 'onlyByUser' => 'SomeOtherUser' ],
1060  'deletedhistory',
1061  [ 'actormigration' => 'table' ],
1062  [
1063  'actormigration_conds',
1064  '(rc_deleted & ' . Revision::DELETED_USER . ') != ' . Revision::DELETED_USER,
1065  '(rc_type != ' . RC_LOG . ') OR ((rc_deleted & ' . LogPage::DELETED_ACTION . ') != ' .
1067  ],
1068  [ 'actormigration' => 'join' ],
1069  ],
1070  [
1071  [ 'onlyByUser' => 'SomeOtherUser' ],
1072  'suppressrevision',
1073  [ 'actormigration' => 'table' ],
1074  [
1075  'actormigration_conds',
1076  '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
1078  '(rc_type != ' . RC_LOG . ') OR (' .
1079  '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
1081  ],
1082  [ 'actormigration' => 'join' ],
1083  ],
1084  [
1085  [ 'onlyByUser' => 'SomeOtherUser' ],
1086  'viewsuppressed',
1087  [ 'actormigration' => 'table' ],
1088  [
1089  'actormigration_conds',
1090  '(rc_deleted & ' . ( Revision::DELETED_USER | Revision::DELETED_RESTRICTED ) . ') != ' .
1092  '(rc_type != ' . RC_LOG . ') OR (' .
1093  '(rc_deleted & ' . ( LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED ) . ') != ' .
1095  ],
1096  [ 'actormigration' => 'join' ],
1097  ],
1098  ];
1099  }
1100 
1105  array $options,
1106  $notAllowedAction,
1107  array $expectedExtraTables,
1108  array $expectedExtraConds,
1109  array $expectedExtraJoins
1110  ) {
1111  $commonConds = [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ];
1112  $conds = array_merge( $commonConds, $expectedExtraConds );
1113 
1114  $mockDb = $this->getMockDb();
1115  $mockDb->expects( $this->once() )
1116  ->method( 'select' )
1117  ->with(
1118  array_merge( [ 'recentchanges', 'watchlist', 'page' ], $expectedExtraTables ),
1119  $this->isType( 'array' ),
1120  $conds,
1121  $this->isType( 'string' ),
1122  $this->isType( 'array' ),
1123  array_merge( [
1124  'watchlist' => [ 'INNER JOIN', [ 'wl_namespace=rc_namespace', 'wl_title=rc_title' ] ],
1125  'page' => [ 'LEFT JOIN', 'rc_cur_id=page_id' ],
1126  ], $expectedExtraJoins )
1127  )
1128  ->will( $this->returnValue( [] ) );
1129 
1130  $user = $this->getMockNonAnonUserWithIdAndRestrictedPermissions( 1, $notAllowedAction );
1131 
1132  $queryService = $this->newService( $mockDb );
1133  $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options );
1134 
1135  $this->assertEmpty( $items );
1136  }
1137 
1139  $mockDb = $this->getMockDb();
1140  $mockDb->expects( $this->once() )
1141  ->method( 'select' )
1142  ->with(
1143  [ 'recentchanges', 'watchlist' ],
1144  [
1145  'rc_id',
1146  'rc_namespace',
1147  'rc_title',
1148  'rc_timestamp',
1149  'rc_type',
1150  'rc_deleted',
1151  'wl_notificationtimestamp',
1152 
1153  'rc_cur_id',
1154  'rc_this_oldid',
1155  'rc_last_oldid',
1156  ],
1157  [ 'wl_user' => 1, ],
1158  $this->isType( 'string' ),
1159  [],
1160  [
1161  'watchlist' => [
1162  'INNER JOIN',
1163  [
1164  'wl_namespace=rc_namespace',
1165  'wl_title=rc_title'
1166  ]
1167  ],
1168  ]
1169  )
1170  ->will( $this->returnValue( [] ) );
1171 
1172  $queryService = $this->newService( $mockDb );
1174 
1175  $items = $queryService->getWatchedItemsWithRecentChangeInfo( $user, [ 'allRevisions' => true ] );
1176 
1177  $this->assertEmpty( $items );
1178  }
1179 
1181  return [
1182  [
1183  [ 'rcTypes' => [ 1337 ] ],
1184  null,
1185  'Bad value for parameter $options[\'rcTypes\']',
1186  ],
1187  [
1188  [ 'rcTypes' => [ 'edit' ] ],
1189  null,
1190  'Bad value for parameter $options[\'rcTypes\']',
1191  ],
1192  [
1193  [ 'rcTypes' => [ RC_EDIT, 1337 ] ],
1194  null,
1195  'Bad value for parameter $options[\'rcTypes\']',
1196  ],
1197  [
1198  [ 'dir' => 'foo' ],
1199  null,
1200  'Bad value for parameter $options[\'dir\']',
1201  ],
1202  [
1203  [ 'start' => '20151212010101' ],
1204  null,
1205  'Bad value for parameter $options[\'dir\']: must be provided',
1206  ],
1207  [
1208  [ 'end' => '20151212010101' ],
1209  null,
1210  'Bad value for parameter $options[\'dir\']: must be provided',
1211  ],
1212  [
1213  [],
1214  [ '20151212010101', 123 ],
1215  'Bad value for parameter $options[\'dir\']: must be provided',
1216  ],
1217  [
1219  '20151212010101',
1220  'Bad value for parameter $startFrom: must be a two-element array',
1221  ],
1222  [
1224  [ '20151212010101' ],
1225  'Bad value for parameter $startFrom: must be a two-element array',
1226  ],
1227  [
1229  [ '20151212010101', 123, 'foo' ],
1230  'Bad value for parameter $startFrom: must be a two-element array',
1231  ],
1232  [
1233  [ 'watchlistOwner' => $this->getMockUnrestrictedNonAnonUserWithId( 2 ) ],
1234  null,
1235  'Bad value for parameter $options[\'watchlistOwnerToken\']',
1236  ],
1237  [
1238  [ 'watchlistOwner' => 'Other User', 'watchlistOwnerToken' => 'some-token' ],
1239  null,
1240  'Bad value for parameter $options[\'watchlistOwner\']',
1241  ],
1242  ];
1243  }
1244 
1249  array $options,
1250  $startFrom,
1251  $expectedInExceptionMessage
1252  ) {
1253  $mockDb = $this->getMockDb();
1254  $mockDb->expects( $this->never() )
1255  ->method( $this->anything() );
1256 
1257  $queryService = $this->newService( $mockDb );
1259 
1260  $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
1261  $queryService->getWatchedItemsWithRecentChangeInfo( $user, $options, $startFrom );
1262  }
1263 
1265  $mockDb = $this->getMockDb();
1266  $mockDb->expects( $this->once() )
1267  ->method( 'select' )
1268  ->with(
1269  [ 'recentchanges', 'watchlist', 'page' ],
1270  [
1271  'rc_id',
1272  'rc_namespace',
1273  'rc_title',
1274  'rc_timestamp',
1275  'rc_type',
1276  'rc_deleted',
1277  'wl_notificationtimestamp',
1278  'rc_cur_id',
1279  ],
1280  [ 'wl_user' => 1, '(rc_this_oldid=page_latest) OR (rc_type=3)' ],
1281  $this->isType( 'string' ),
1282  [],
1283  [
1284  'watchlist' => [
1285  'INNER JOIN',
1286  [
1287  'wl_namespace=rc_namespace',
1288  'wl_title=rc_title'
1289  ]
1290  ],
1291  'page' => [
1292  'LEFT JOIN',
1293  'rc_cur_id=page_id',
1294  ],
1295  ]
1296  )
1297  ->will( $this->returnValue( [] ) );
1298 
1299  $queryService = $this->newService( $mockDb );
1301 
1302  $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1303  $user,
1304  [ 'usedInGenerator' => true ]
1305  );
1306 
1307  $this->assertEmpty( $items );
1308  }
1309 
1311  $mockDb = $this->getMockDb();
1312  $mockDb->expects( $this->once() )
1313  ->method( 'select' )
1314  ->with(
1315  [ 'recentchanges', 'watchlist' ],
1316  [
1317  'rc_id',
1318  'rc_namespace',
1319  'rc_title',
1320  'rc_timestamp',
1321  'rc_type',
1322  'rc_deleted',
1323  'wl_notificationtimestamp',
1324  'rc_this_oldid',
1325  ],
1326  [ 'wl_user' => 1 ],
1327  $this->isType( 'string' ),
1328  [],
1329  [
1330  'watchlist' => [
1331  'INNER JOIN',
1332  [
1333  'wl_namespace=rc_namespace',
1334  'wl_title=rc_title'
1335  ]
1336  ],
1337  ]
1338  )
1339  ->will( $this->returnValue( [] ) );
1340 
1341  $queryService = $this->newService( $mockDb );
1343 
1344  $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1345  $user,
1346  [ 'usedInGenerator' => true, 'allRevisions' => true, ]
1347  );
1348 
1349  $this->assertEmpty( $items );
1350  }
1351 
1353  $mockDb = $this->getMockDb();
1354  $mockDb->expects( $this->once() )
1355  ->method( 'select' )
1356  ->with(
1357  $this->isType( 'array' ),
1358  $this->isType( 'array' ),
1359  [
1360  'wl_user' => 2,
1361  '(rc_this_oldid=page_latest) OR (rc_type=3)',
1362  ],
1363  $this->isType( 'string' ),
1364  $this->isType( 'array' ),
1365  $this->isType( 'array' )
1366  )
1367  ->will( $this->returnValue( [] ) );
1368 
1369  $queryService = $this->newService( $mockDb );
1371  $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1372  $otherUser->expects( $this->once() )
1373  ->method( 'getOption' )
1374  ->with( 'watchlisttoken' )
1375  ->willReturn( '0123456789abcdef' );
1376 
1377  $items = $queryService->getWatchedItemsWithRecentChangeInfo(
1378  $user,
1379  [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => '0123456789abcdef' ]
1380  );
1381 
1382  $this->assertEmpty( $items );
1383  }
1384 
1385  public function invalidWatchlistTokenProvider() {
1386  return [
1387  [ 'wrongToken' ],
1388  [ '' ],
1389  ];
1390  }
1391 
1396  $mockDb = $this->getMockDb();
1397  $mockDb->expects( $this->never() )
1398  ->method( $this->anything() );
1399 
1400  $queryService = $this->newService( $mockDb );
1402  $otherUser = $this->getMockUnrestrictedNonAnonUserWithId( 2 );
1403  $otherUser->expects( $this->once() )
1404  ->method( 'getOption' )
1405  ->with( 'watchlisttoken' )
1406  ->willReturn( '0123456789abcdef' );
1407 
1408  $this->setExpectedException( ApiUsageException::class, 'Incorrect watchlist token provided' );
1409  $queryService->getWatchedItemsWithRecentChangeInfo(
1410  $user,
1411  [ 'watchlistOwner' => $otherUser, 'watchlistOwnerToken' => $token ]
1412  );
1413  }
1414 
1415  public function testGetWatchedItemsForUser() {
1416  $mockDb = $this->getMockDb();
1417  $mockDb->expects( $this->once() )
1418  ->method( 'select' )
1419  ->with(
1420  'watchlist',
1421  [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1422  [ 'wl_user' => 1 ]
1423  )
1424  ->will( $this->returnValue( [
1425  $this->getFakeRow( [
1426  'wl_namespace' => 0,
1427  'wl_title' => 'Foo1',
1428  'wl_notificationtimestamp' => '20151212010101',
1429  ] ),
1430  $this->getFakeRow( [
1431  'wl_namespace' => 1,
1432  'wl_title' => 'Foo2',
1433  'wl_notificationtimestamp' => null,
1434  ] ),
1435  ] ) );
1436 
1437  $queryService = $this->newService( $mockDb );
1438  $user = $this->getMockNonAnonUserWithId( 1 );
1439 
1440  $items = $queryService->getWatchedItemsForUser( $user );
1441 
1442  $this->assertInternalType( 'array', $items );
1443  $this->assertCount( 2, $items );
1444  $this->assertContainsOnlyInstancesOf( WatchedItem::class, $items );
1445  $this->assertEquals(
1446  new WatchedItem( $user, new TitleValue( 0, 'Foo1' ), '20151212010101' ),
1447  $items[0]
1448  );
1449  $this->assertEquals(
1450  new WatchedItem( $user, new TitleValue( 1, 'Foo2' ), null ),
1451  $items[1]
1452  );
1453  }
1454 
1456  return [
1457  [
1458  [ 'namespaceIds' => [ 0, 1 ], ],
1459  [ 'wl_namespace' => [ 0, 1 ], ],
1460  []
1461  ],
1462  [
1463  [ 'sort' => WatchedItemQueryService::SORT_ASC, ],
1464  [],
1465  [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1466  ],
1467  [
1468  [
1469  'namespaceIds' => [ 0 ],
1471  ],
1472  [ 'wl_namespace' => [ 0 ], ],
1473  [ 'ORDER BY' => 'wl_title ASC' ]
1474  ],
1475  [
1476  [ 'limit' => 10 ],
1477  [],
1478  [ 'LIMIT' => 10 ]
1479  ],
1480  [
1481  [
1482  'namespaceIds' => [ 0, "1; DROP TABLE watchlist;\n--" ],
1483  'limit' => "10; DROP TABLE watchlist;\n--",
1484  ],
1485  [ 'wl_namespace' => [ 0, 1 ], ],
1486  [ 'LIMIT' => 10 ]
1487  ],
1488  [
1490  [ 'wl_notificationtimestamp IS NOT NULL' ],
1491  []
1492  ],
1493  [
1495  [ 'wl_notificationtimestamp IS NULL' ],
1496  []
1497  ],
1498  [
1499  [ 'sort' => WatchedItemQueryService::SORT_DESC, ],
1500  [],
1501  [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1502  ],
1503  [
1504  [
1505  'namespaceIds' => [ 0 ],
1507  ],
1508  [ 'wl_namespace' => [ 0 ], ],
1509  [ 'ORDER BY' => 'wl_title DESC' ]
1510  ],
1511  ];
1512  }
1513 
1518  array $options,
1519  array $expectedConds,
1520  array $expectedDbOptions
1521  ) {
1522  $mockDb = $this->getMockDb();
1523  $user = $this->getMockNonAnonUserWithId( 1 );
1524 
1525  $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1526  $mockDb->expects( $this->once() )
1527  ->method( 'select' )
1528  ->with(
1529  'watchlist',
1530  [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1531  $expectedConds,
1532  $this->isType( 'string' ),
1533  $expectedDbOptions
1534  )
1535  ->will( $this->returnValue( [] ) );
1536 
1537  $queryService = $this->newService( $mockDb );
1538 
1539  $items = $queryService->getWatchedItemsForUser( $user, $options );
1540  $this->assertEmpty( $items );
1541  }
1542 
1544  return [
1545  [
1546  [
1547  'from' => new TitleValue( 0, 'SomeDbKey' ),
1549  ],
1550  [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1551  [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1552  ],
1553  [
1554  [
1555  'from' => new TitleValue( 0, 'SomeDbKey' ),
1557  ],
1558  [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1559  [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1560  ],
1561  [
1562  [
1563  'until' => new TitleValue( 0, 'SomeDbKey' ),
1565  ],
1566  [ "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))", ],
1567  [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1568  ],
1569  [
1570  [
1571  'until' => new TitleValue( 0, 'SomeDbKey' ),
1573  ],
1574  [ "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))", ],
1575  [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1576  ],
1577  [
1578  [
1579  'from' => new TitleValue( 0, 'AnotherDbKey' ),
1580  'until' => new TitleValue( 0, 'SomeOtherDbKey' ),
1581  'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1583  ],
1584  [
1585  "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1586  "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1587  "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'SomeDbKey'))",
1588  ],
1589  [ 'ORDER BY' => [ 'wl_namespace ASC', 'wl_title ASC' ] ]
1590  ],
1591  [
1592  [
1593  'from' => new TitleValue( 0, 'SomeOtherDbKey' ),
1594  'until' => new TitleValue( 0, 'AnotherDbKey' ),
1595  'startFrom' => new TitleValue( 0, 'SomeDbKey' ),
1597  ],
1598  [
1599  "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeOtherDbKey'))",
1600  "(wl_namespace > 0) OR ((wl_namespace = 0) AND (wl_title >= 'AnotherDbKey'))",
1601  "(wl_namespace < 0) OR ((wl_namespace = 0) AND (wl_title <= 'SomeDbKey'))",
1602  ],
1603  [ 'ORDER BY' => [ 'wl_namespace DESC', 'wl_title DESC' ] ]
1604  ],
1605  ];
1606  }
1607 
1612  array $options,
1613  array $expectedConds,
1614  array $expectedDbOptions
1615  ) {
1616  $user = $this->getMockNonAnonUserWithId( 1 );
1617 
1618  $expectedConds = array_merge( [ 'wl_user' => 1 ], $expectedConds );
1619 
1620  $mockDb = $this->getMockDb();
1621  $mockDb->expects( $this->any() )
1622  ->method( 'addQuotes' )
1623  ->will( $this->returnCallback( function ( $value ) {
1624  return "'$value'";
1625  } ) );
1626  $mockDb->expects( $this->any() )
1627  ->method( 'makeList' )
1628  ->with(
1629  $this->isType( 'array' ),
1630  $this->isType( 'int' )
1631  )
1632  ->will( $this->returnCallback( function ( $a, $conj ) {
1633  $sqlConj = $conj === LIST_AND ? ' AND ' : ' OR ';
1634  return implode( $sqlConj, array_map( function ( $s ) {
1635  return '(' . $s . ')';
1636  }, $a
1637  ) );
1638  } ) );
1639  $mockDb->expects( $this->once() )
1640  ->method( 'select' )
1641  ->with(
1642  'watchlist',
1643  [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
1644  $expectedConds,
1645  $this->isType( 'string' ),
1646  $expectedDbOptions
1647  )
1648  ->will( $this->returnValue( [] ) );
1649 
1650  $queryService = $this->newService( $mockDb );
1651 
1652  $items = $queryService->getWatchedItemsForUser( $user, $options );
1653  $this->assertEmpty( $items );
1654  }
1655 
1657  return [
1658  [
1659  [ 'sort' => 'foo' ],
1660  'Bad value for parameter $options[\'sort\']'
1661  ],
1662  [
1663  [ 'filter' => 'foo' ],
1664  'Bad value for parameter $options[\'filter\']'
1665  ],
1666  [
1667  [ 'from' => new TitleValue( 0, 'SomeDbKey' ), ],
1668  'Bad value for parameter $options[\'sort\']: must be provided'
1669  ],
1670  [
1671  [ 'until' => new TitleValue( 0, 'SomeDbKey' ), ],
1672  'Bad value for parameter $options[\'sort\']: must be provided'
1673  ],
1674  [
1675  [ 'startFrom' => new TitleValue( 0, 'SomeDbKey' ), ],
1676  'Bad value for parameter $options[\'sort\']: must be provided'
1677  ],
1678  ];
1679  }
1680 
1685  array $options,
1686  $expectedInExceptionMessage
1687  ) {
1688  $queryService = $this->newService( $this->getMockDb() );
1689 
1690  $this->setExpectedException( InvalidArgumentException::class, $expectedInExceptionMessage );
1691  $queryService->getWatchedItemsForUser( $this->getMockNonAnonUserWithId( 1 ), $options );
1692  }
1693 
1695  $mockDb = $this->getMockDb();
1696 
1697  $mockDb->expects( $this->never() )
1698  ->method( $this->anything() );
1699 
1700  $queryService = $this->newService( $mockDb );
1701 
1702  $items = $queryService->getWatchedItemsForUser( $this->getMockAnonUser() );
1703  $this->assertEmpty( $items );
1704  }
1705 
1706 }
Revision\DELETED_USER
const DELETED_USER
Definition: Revision.php:49
WatchedItemQueryServiceUnitTest\testGetWatchedItemsForUser
testGetWatchedItemsForUser()
Definition: WatchedItemQueryServiceUnitTest.php:1415
WatchedItemQueryServiceUnitTest\mysqlIndexOptimizationProvider
mysqlIndexOptimizationProvider()
Definition: WatchedItemQueryServiceUnitTest.php:964
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization
testGetWatchedItemsWithRecentChangeInfo_mysqlIndexOptimization( $dbType, array $options, array $expectedExtraConds)
mysqlIndexOptimizationProvider
Definition: WatchedItemQueryServiceUnitTest.php:992
$user
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a account $user
Definition: hooks.txt:244
Revision\DELETED_RESTRICTED
const DELETED_RESTRICTED
Definition: Revision.php:50
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken
testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerAndInvalidToken( $token)
invalidWatchlistTokenProvider
Definition: WatchedItemQueryServiceUnitTest.php:1395
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions
testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorAllRevisionsOptions()
Definition: WatchedItemQueryServiceUnitTest.php:1310
WatchedItemQueryService\INCLUDE_COMMENT
const INCLUDE_COMMENT
Definition: WatchedItemQueryService.php:26
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo
testGetWatchedItemsWithRecentChangeInfo()
Definition: WatchedItemQueryServiceUnitTest.php:238
WatchedItemQueryServiceUnitTest\getWatchedItemsWithRecentChangeInfoOptionsProvider
getWatchedItemsWithRecentChangeInfoOptionsProvider()
Definition: WatchedItemQueryServiceUnitTest.php:521
WatchedItemQueryServiceUnitTest
WatchedItemQueryService.
Definition: WatchedItemQueryServiceUnitTest.php:9
WatchedItemQueryServiceUnitTest\getMockAnonUser
getMockAnonUser()
Definition: WatchedItemQueryServiceUnitTest.php:222
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult
testGetWatchedItemsWithRecentChangeInfo_allRevisionsOptionAndEmptyResult()
Definition: WatchedItemQueryServiceUnitTest.php:1138
WatchedItemQueryServiceUnitTest\getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider
getWatchedItemsWithRecentChangeInfoInvalidOptionsProvider()
Definition: WatchedItemQueryServiceUnitTest.php:1180
WatchedItemQueryServiceUnitTest\getMockNonAnonUserWithIdAndRestrictedPermissions
getMockNonAnonUserWithIdAndRestrictedPermissions( $id, $notAllowedAction)
Definition: WatchedItemQueryServiceUnitTest.php:180
WatchedItemQueryService\FILTER_PATROLLED
const FILTER_PATROLLED
Definition: WatchedItemQueryService.php:42
WatchedItemQueryServiceUnitTest\getMockLoadBalancer
getMockLoadBalancer( $mockDb)
Definition: WatchedItemQueryServiceUnitTest.php:131
WatchedItemQueryServiceUnitTest\testGetWatchedItemsForUser_invalidOptionThrowsException
testGetWatchedItemsForUser_invalidOptionThrowsException(array $options, $expectedInExceptionMessage)
getWatchedItemsForUserInvalidOptionsProvider
Definition: WatchedItemQueryServiceUnitTest.php:1684
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:1018
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:359
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:858
$s
$s
Definition: mergeMessageFileList.php:187
$res
$res
Definition: database.txt:21
WatchedItemQueryServiceUnitTest\newService
newService( $mockDb)
Definition: WatchedItemQueryServiceUnitTest.php:71
WatchedItemQueryServiceUnitTest\filterPatrolledOptionProvider
filterPatrolledOptionProvider()
Definition: WatchedItemQueryServiceUnitTest.php:927
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:202
WatchedItemQueryServiceUnitTest\getWatchedItemsForUserInvalidOptionsProvider
getWatchedItemsForUserInvalidOptionsProvider()
Definition: WatchedItemQueryServiceUnitTest.php:1656
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:16
WatchedItemQueryService
Definition: WatchedItemQueryService.php:18
WatchedItemQueryService\INCLUDE_PATROL_INFO
const INCLUDE_PATROL_INFO
Definition: WatchedItemQueryService.php:27
WatchedItemQueryServiceUnitTest\getMockActorMigration
getMockActorMigration()
Definition: WatchedItemQueryServiceUnitTest.php:36
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:1694
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:1385
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:1455
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:1248
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:1104
$value
$value
Definition: styleTest.css.php:49
WatchedItemQueryServiceUnitTest\getMockUnrestrictedNonAnonUserWithId
getMockUnrestrictedNonAnonUserWithId( $id)
Definition: WatchedItemQueryServiceUnitTest.php:161
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:1517
WatchedItemQueryService\DIR_OLDER
const DIR_OLDER
Definition: WatchedItemQueryService.php:20
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult
testGetWatchedItemsWithRecentChangeInfo_usedInGeneratorOptionAndEmptyResult()
Definition: WatchedItemQueryServiceUnitTest.php:1264
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult
testGetWatchedItemsWithRecentChangeInfo_watchlistOwnerOptionAndEmptyResult()
Definition: WatchedItemQueryServiceUnitTest.php:1352
$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:2036
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:1543
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:82
LogPage\DELETED_RESTRICTED
const DELETED_RESTRICTED
Definition: LogPage.php:37
WatchedItemQueryServiceUnitTest\testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights
testGetWatchedItemsWithRecentChangeInfo_filterPatrolledAndUserWithNoPatrolRights( $filtersOption)
filterPatrolledOptionProvider
Definition: WatchedItemQueryServiceUnitTest.php:937
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:146
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:1611
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:230
WatchedItemQueryServiceUnitTest\userPermissionRelatedExtraChecksProvider
userPermissionRelatedExtraChecksProvider()
Definition: WatchedItemQueryServiceUnitTest.php:1024
MediaWikiTestCase\$db
Database $db
Primary database.
Definition: MediaWikiTestCase.php:60
TitleValue
Represents a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:36