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