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