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