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