Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / specialpage / ChangesListSpecialPageTest.php
blob74abd90723d1479cb26b365b6daad7f5f5a247ce
1 <?php
3 namespace MediaWiki\Tests\SpecialPage;
5 use ChangesListBooleanFilterGroup;
6 use ChangesListStringOptionsFilterGroup;
7 use MediaWiki\Context\RequestContext;
8 use MediaWiki\MainConfigNames;
9 use MediaWiki\Request\FauxRequest;
10 use MediaWiki\SpecialPage\ChangesListSpecialPage;
11 use MediaWiki\Tests\User\TempUser\TempUserTestTrait;
12 use MediaWiki\Title\Title;
13 use MediaWiki\User\User;
14 use Wikimedia\Rdbms\Database;
15 use Wikimedia\Rdbms\IExpression;
16 use Wikimedia\TestingAccessWrapper;
17 use Wikimedia\Timestamp\ConvertibleTimestamp;
19 /**
20 * Test class for ChangesListSpecialPage class
22 * Copyright © 2011-, Antoine Musso, Stephane Bisson, Matthew Flaschen
24 * @author Antoine Musso
25 * @author Stephane Bisson
26 * @author Matthew Flaschen
27 * @group Database
29 * @covers \MediaWiki\SpecialPage\ChangesListSpecialPage
31 class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase {
33 use TempUserTestTrait {
34 enableAutoCreateTempUser as _enableAutoCreateTempUser;
35 disableAutoCreateTempUser as _disableAutoCreateTempUser;
38 protected function setUp(): void {
39 $this->overrideConfigValue(
40 MainConfigNames::GroupPermissions,
41 [ '*' => [ 'edit' => true ] ]
43 parent::setUp();
44 $this->clearHooks();
47 /**
48 * @return ChangesListSpecialPage
50 protected function getPageAccessWrapper() {
51 $mock = $this->getMockBuilder( ChangesListSpecialPage::class )
52 ->setConstructorArgs(
54 'ChangesListSpecialPage',
55 '',
56 $this->getServiceContainer()->getUserIdentityUtils(),
57 $this->getServiceContainer()->getTempUserConfig()
60 ->onlyMethods( [ 'getPageTitle' ] )
61 ->getMockForAbstractClass();
63 $mock->method( 'getPageTitle' )->willReturn(
64 Title::makeTitle( NS_SPECIAL, 'ChangesListSpecialPage' )
67 $mock = TestingAccessWrapper::newFromObject(
68 $mock
71 return $mock;
74 private function buildQuery(
75 array $requestOptions,
76 ?User $user = null
77 ): array {
78 $context = new RequestContext;
79 $context->setRequest( new FauxRequest( $requestOptions ) );
80 if ( $user ) {
81 $context->setUser( $user );
84 $this->changesListSpecialPage->setContext( $context );
85 $formOptions = $this->changesListSpecialPage->setup( null );
87 # Filter out rc_timestamp conditions which depends on the test runtime
88 # This condition is not needed as of march 2, 2011 -- hashar
89 # @todo FIXME: Find a way to generate the correct rc_timestamp
91 $tables = [];
92 $fields = [];
93 $queryConditions = [];
94 $query_options = [];
95 $join_conds = [];
97 call_user_func_array(
98 [ $this->changesListSpecialPage, 'buildQuery' ],
100 &$tables,
101 &$fields,
102 &$queryConditions,
103 &$query_options,
104 &$join_conds,
105 $formOptions
109 $queryConditions = array_filter(
110 $queryConditions,
111 [ __CLASS__, 'filterOutRcTimestampCondition' ]
114 return $queryConditions;
118 * helper to test SpecialRecentchanges::buildQuery()
119 * @param array $expected
120 * @param array $requestOptions
121 * @param string $message
122 * @param User|null $user
124 private function assertConditions(
125 array $expected,
126 array $requestOptions,
127 string $message,
128 ?User $user = null
130 $queryConditions = $this->buildQuery( $requestOptions, $user );
132 $this->assertEquals(
133 $this->normalizeCondition( $expected ),
134 $this->normalizeCondition( $queryConditions ),
135 $message
139 private function normalizeCondition( array $conds ): array {
140 $dbr = $this->getDb();
141 $normalized = array_map(
142 static function ( $k, $v ) use ( $dbr ) {
143 if ( is_array( $v ) ) {
144 sort( $v );
146 // (Ab)use makeList() to format only this entry
147 return $dbr->makeList( [ $k => $v ], Database::LIST_AND );
149 array_keys( $conds ),
150 $conds
152 sort( $normalized );
153 return $normalized;
157 * @param array|string|IExpression $var
158 * @return bool false if condition begins with 'rc_timestamp '
160 private static function filterOutRcTimestampCondition( $var ): bool {
161 if ( $var instanceof IExpression ) {
162 $var = $var->toGeneralizedSql();
164 return is_array( $var ) || !str_contains( (string)$var, 'rc_timestamp ' );
167 public function testRcNsFilter() {
168 $this->assertConditions(
169 [ # expected
170 'rc_namespace = 0',
173 'namespace' => NS_MAIN,
175 "rc conditions with one namespace"
179 public function testRcNsFilterInversion() {
180 $this->assertConditions(
181 [ # expected
182 'rc_namespace != 0',
185 'namespace' => NS_MAIN,
186 'invert' => 1,
188 "rc conditions with namespace inverted"
192 public function testRcNsFilterMultiple() {
193 $this->assertConditions(
194 [ # expected
195 'rc_namespace IN (1,2,3)',
198 'namespace' => '1;2;3',
200 "rc conditions with multiple namespaces"
204 public function testRcNsFilterMultipleAssociated() {
205 $this->assertConditions(
206 [ # expected
207 'rc_namespace IN (0,1,4,5,6,7)',
210 'namespace' => '1;4;7',
211 'associated' => 1,
213 "rc conditions with multiple namespaces and associated"
217 public function testRcNsFilterAssociatedSpecial() {
218 $this->assertConditions(
219 [ # expected
220 'rc_namespace IN (-1,0,1)',
223 'namespace' => '1;-1',
224 'associated' => 1,
226 "rc conditions with associated and special namespace"
230 public function testRcNsFilterMultipleAssociatedInvert() {
231 $this->assertConditions(
232 [ # expected
233 'rc_namespace NOT IN (2,3,8,9)',
236 'namespace' => '2;3;9',
237 'associated' => 1,
238 'invert' => 1
240 "rc conditions with multiple namespaces, associated and inverted"
244 public function testRcNsFilterMultipleInvert() {
245 $this->assertConditions(
246 [ # expected
247 'rc_namespace NOT IN (1,2,3)',
250 'namespace' => '1;2;3',
251 'invert' => 1,
253 "rc conditions with multiple namespaces inverted"
257 public function testRcNsFilterAllContents() {
258 $namespaces = $this->getServiceContainer()->getNamespaceInfo()->getSubjectNamespaces();
259 $this->assertConditions(
260 [ # expected
261 'rc_namespace IN (' . $this->getDb()->makeList( $namespaces ) . ')',
264 'namespace' => 'all-contents',
266 "rc conditions with all-contents"
270 public function testRcNsFilterInvalid() {
271 $this->assertConditions(
272 [ # expected
275 'namespace' => 'invalid',
277 "rc conditions with invalid namespace"
281 public function testRcNsFilterPartialInvalid() {
282 $namespaces = array_merge(
283 [ 1 ],
284 $this->getServiceContainer()->getNamespaceInfo()->getSubjectNamespaces()
286 sort( $namespaces );
287 $this->assertConditions(
288 [ # expected
289 'rc_namespace IN (' . $this->getDb()->makeList( $namespaces ) . ')',
292 'namespace' => 'all-contents;1;invalid',
294 "rc conditions with invalid namespace"
298 public function testRcHidemyselfFilter() {
299 $user = $this->getTestUser()->getUser();
300 $this->assertConditions(
301 [ # expected
302 $this->getDb()->expr( 'actor_name', '!=', $user->getName() ),
305 'hidemyself' => 1,
307 "rc conditions: hidemyself=1 (logged in)",
308 $user
311 $user = User::newFromName( '10.11.12.13', false );
312 $this->assertConditions(
313 [ # expected
314 "actor_name != '10.11.12.13'",
317 'hidemyself' => 1,
319 "rc conditions: hidemyself=1 (anon)",
320 $user
324 public function testRcHidebyothersFilter() {
325 $user = $this->getTestUser()->getUser();
326 $this->assertConditions(
327 [ # expected
328 'actor_user' => $user->getId(),
331 'hidebyothers' => 1,
333 "rc conditions: hidebyothers=1 (logged in)",
334 $user
337 $user = User::newFromName( '10.11.12.13', false );
338 $this->assertConditions(
339 [ # expected
340 'actor_name' => '10.11.12.13',
343 'hidebyothers' => 1,
345 "rc conditions: hidebyothers=1 (anon)",
346 $user
350 public function testRcHidepageedits() {
351 $this->assertConditions(
352 [ # expected
353 "rc_type != 0",
356 'hidepageedits' => 1,
358 "rc conditions: hidepageedits=1"
362 public function testRcHidenewpages() {
363 $this->assertConditions(
364 [ # expected
365 "rc_type != 1",
368 'hidenewpages' => 1,
370 "rc conditions: hidenewpages=1"
374 public function testRcHidelog() {
375 $this->assertConditions(
376 [ # expected
377 "rc_type != 3",
380 'hidelog' => 1,
382 "rc conditions: hidelog=1"
386 public function testRcHidehumans() {
387 $this->assertConditions(
388 [ # expected
389 'rc_bot' => 1,
392 'hidebots' => 0,
393 'hidehumans' => 1,
395 "rc conditions: hidebots=0 hidehumans=1"
399 public function testRcHidepatrolledDisabledFilter() {
400 $this->overrideConfigValue( MainConfigNames::UseRCPatrol, false );
401 $this->changesListSpecialPage->filterGroups = [];
402 $user = $this->getTestUser()->getUser();
403 $this->assertConditions(
404 [ # expected
407 'hidepatrolled' => 1,
409 "rc conditions: hidepatrolled=1 (user not allowed)",
410 $user
414 public function testRcHideunpatrolledDisabledFilter() {
415 $this->overrideConfigValue( MainConfigNames::UseRCPatrol, false );
416 $this->changesListSpecialPage->filterGroups = [];
417 $user = $this->getTestUser()->getUser();
418 $this->assertConditions(
419 [ # expected
422 'hideunpatrolled' => 1,
424 "rc conditions: hideunpatrolled=1 (user not allowed)",
425 $user
429 public function testRcHidepatrolledFilter() {
430 $user = $this->getTestSysop()->getUser();
431 $this->assertConditions(
432 [ # expected
433 'rc_patrolled' => 0,
436 'hidepatrolled' => 1,
438 "rc conditions: hidepatrolled=1",
439 $user
443 public function testRcHideunpatrolledFilter() {
444 $user = $this->getTestSysop()->getUser();
445 $this->assertConditions(
446 [ # expected
447 'rc_patrolled' => [ 1, 2 ],
450 'hideunpatrolled' => 1,
452 "rc conditions: hideunpatrolled=1",
453 $user
457 public function testRcReviewStatusFilter() {
458 $user = $this->getTestSysop()->getUser();
459 $this->assertConditions(
460 [ # expected
461 'rc_patrolled' => 1,
464 'reviewStatus' => 'manual'
466 "rc conditions: reviewStatus=manual",
467 $user
469 $this->assertConditions(
470 [ # expected
471 'rc_patrolled' => [ 0, 2 ],
474 'reviewStatus' => 'unpatrolled;auto'
476 "rc conditions: reviewStatus=unpatrolled;auto",
477 $user
481 public function testRcHideminorFilter() {
482 $this->assertConditions(
483 [ # expected
484 'rc_minor = 0',
487 'hideminor' => 1,
489 "rc conditions: hideminor=1"
493 public function testRcHidemajorFilter() {
494 $this->assertConditions(
495 [ # expected
496 'rc_minor = 1',
499 'hidemajor' => 1,
501 "rc conditions: hidemajor=1"
505 public function testHideCategorization() {
506 $this->assertConditions(
508 # expected
509 "rc_type != 6"
512 'hidecategorization' => 1
514 "rc conditions: hidecategorization=1"
518 /** @see TempUserTestTrait::enableAutoCreateTempUser */
519 protected function enableAutoCreateTempUser( array $configOverrides = [] ): void {
520 $this->_enableAutoCreateTempUser( $configOverrides );
521 $this->changesListSpecialPage->setTempUserConfig( $this->getServiceContainer()->getTempUserConfig() );
524 /** @see TempUserTestTrait::disableAutoCreateTempUser */
525 protected function disableAutoCreateTempUser( array $configOverrides = [] ): void {
526 $this->_disableAutoCreateTempUser( $configOverrides );
527 $this->changesListSpecialPage->setTempUserConfig( $this->getServiceContainer()->getTempUserConfig() );
530 public function testRegistrationHideliu() {
531 $this->enableAutoCreateTempUser();
532 $tempUserMatchPattern = $this->getServiceContainer()->getTempUserConfig()
533 ->getMatchCondition( $this->getDb(), 'actor_name', IExpression::LIKE )
534 ->toSql( $this->getDb() );
535 $this->assertConditions(
537 "((actor_user IS NULL OR $tempUserMatchPattern))",
540 'hideliu' => 1,
542 "rc conditions: hideliu=1"
546 public function testRegistrationHideanons() {
547 $this->enableAutoCreateTempUser();
548 $tempUserMatchPattern = $this->getServiceContainer()->getTempUserConfig()
549 ->getMatchCondition( $this->getDb(), 'actor_name', IExpression::NOT_LIKE )
550 ->toSql( $this->getDb() );
551 $this->assertConditions(
553 "((actor_user IS NOT NULL AND $tempUserMatchPattern))",
556 'hideanons' => 1,
558 "rc conditions: hideanons=1"
562 public function testFilterUserExpLevelAll() {
563 $this->assertConditions(
565 # expected
568 'userExpLevel' => 'registered;unregistered;newcomer;learner;experienced',
570 "rc conditions: userExpLevel=registered;unregistered;newcomer;learner;experienced"
574 public function testFilterUserExpLevelRegisteredUnregistered() {
575 $this->assertConditions(
577 # expected
580 'userExpLevel' => 'registered;unregistered',
582 "rc conditions: userExpLevel=registered;unregistered"
586 public function testFilterUserExpLevelRegisteredUnregisteredLearner() {
587 $this->assertConditions(
589 # expected
592 'userExpLevel' => 'registered;unregistered;learner',
594 "rc conditions: userExpLevel=registered;unregistered;learner"
598 public function testFilterUserExpLevelAllExperienceLevels() {
599 $this->disableAutoCreateTempUser();
600 $this->assertConditions(
602 # expected
603 '(actor_user IS NOT NULL)',
606 'userExpLevel' => 'newcomer;learner;experienced',
608 "rc conditions: userExpLevel=newcomer;learner;experienced"
612 public function testFilterUserExpLevelRegistered() {
613 $this->disableAutoCreateTempUser();
614 $this->assertConditions(
616 # expected
617 '(actor_user IS NOT NULL)',
620 'userExpLevel' => 'registered',
622 "rc conditions: userExpLevel=registered"
626 public function testFilterUserExpLevelRegisteredTempAccountsEnabled() {
627 $this->enableAutoCreateTempUser();
628 $tempUserMatchPattern = $this->getServiceContainer()->getTempUserConfig()
629 ->getMatchCondition( $this->getDb(), 'actor_name', IExpression::NOT_LIKE )
630 ->toSql( $this->getDb() );
631 $this->assertConditions(
633 # expected
634 "((actor_user IS NOT NULL AND $tempUserMatchPattern))",
637 'userExpLevel' => 'registered',
639 "rc conditions: userExpLevel=registered"
643 public function testFilterUserExpLevelUnregistered() {
644 $this->disableAutoCreateTempUser();
645 $this->assertConditions(
647 # expected
648 '(actor_user IS NULL)'
651 'userExpLevel' => 'unregistered',
653 "rc conditions: userExpLevel=unregistered"
657 public function testFilterUserExpLevelUnregisteredTempAccountsEnabled() {
658 $this->enableAutoCreateTempUser();
659 $tempUserMatchPattern = $this->getServiceContainer()->getTempUserConfig()
660 ->getMatchCondition( $this->getDb(), 'actor_name', IExpression::LIKE )
661 ->toSql( $this->getDb() );
662 $this->assertConditions(
664 # expected
665 "((actor_user IS NULL OR $tempUserMatchPattern))",
668 'userExpLevel' => 'unregistered',
670 "rc conditions: userExpLevel=unregistered"
674 public function testFilterUserExpLevelRegisteredOrLearner() {
675 $this->disableAutoCreateTempUser();
676 $this->assertConditions(
678 # expected
679 '(actor_user IS NOT NULL)',
682 'userExpLevel' => 'registered;learner',
684 "rc conditions: userExpLevel=registered;learner"
688 public function testFilterUserExpLevelLearner() {
689 $this->disableAutoCreateTempUser();
690 ConvertibleTimestamp::setFakeTime( '20201231000000' );
691 $this->assertConditions(
693 # expected
694 "((actor_user IS NOT NULL AND "
695 . "(user_editcount >= 10 AND (user_registration IS NULL OR user_registration <= '{$this->getDb()->timestamp( '20201227000000' )}')) AND "
696 . "(user_editcount < 500 OR user_registration > '{$this->getDb()->timestamp( '20201201000000' )}')"
697 . "))"
700 'userExpLevel' => 'learner'
702 "rc conditions: userExpLevel=learner"
706 public function testFilterUserExpLevelLearnerWhenTemporaryAccountsEnabled() {
707 $this->enableAutoCreateTempUser();
708 ConvertibleTimestamp::setFakeTime( '20201231000000' );
710 $notLikeTempUserMatchExpression = $this->getServiceContainer()->getTempUserConfig()
711 ->getMatchCondition( $this->getDb(), 'actor_name', IExpression::NOT_LIKE )
712 ->toSql( $this->getDb() );
714 $this->assertConditions(
716 # expected
717 "(((actor_user IS NOT NULL AND $notLikeTempUserMatchExpression) AND "
718 . "(user_editcount >= 10 AND (user_registration IS NULL OR user_registration <= '{$this->getDb()->timestamp( '20201227000000' )}')) AND "
719 . "(user_editcount < 500 OR user_registration > '{$this->getDb()->timestamp( '20201201000000' )}')"
720 . "))"
723 'userExpLevel' => 'learner'
725 "rc conditions: userExpLevel=learner"
729 public function testFilterUserExpLevelUnregisteredOrExperienced() {
730 $this->disableAutoCreateTempUser();
731 ConvertibleTimestamp::setFakeTime( '20201231000000' );
732 $this->assertConditions(
734 # expected
735 "(actor_user IS NULL OR "
736 . "(actor_user IS NOT NULL AND "
737 . "(user_editcount >= 500 AND (user_registration IS NULL OR user_registration <= '{$this->getDb()->timestamp( '20201201000000' )}'))"
738 . "))"
741 'userExpLevel' => 'unregistered;experienced'
743 "rc conditions: userExpLevel=unregistered;experienced"
747 public function testFilterUserExpLevelUnregisteredOrExperiencedWhenTemporaryAccountsEnabled() {
748 $this->enableAutoCreateTempUser();
749 ConvertibleTimestamp::setFakeTime( '20201231000000' );
751 $notLikeTempUserMatchExpression = $this->getServiceContainer()->getTempUserConfig()
752 ->getMatchCondition( $this->getDb(), 'actor_name', IExpression::NOT_LIKE )
753 ->toSql( $this->getDb() );
754 $likeTempUserMatchExpression = $this->getServiceContainer()->getTempUserConfig()
755 ->getMatchCondition( $this->getDb(), 'actor_name', IExpression::LIKE )
756 ->toSql( $this->getDb() );
758 $this->assertConditions(
760 # expected
761 "((actor_user IS NULL OR $likeTempUserMatchExpression) OR "
762 . "((actor_user IS NOT NULL AND $notLikeTempUserMatchExpression) AND "
763 . "(user_editcount >= 500 AND (user_registration IS NULL OR user_registration <= '{$this->getDb()->timestamp( '20201201000000' )}'))"
764 . "))"
767 'userExpLevel' => 'unregistered;experienced'
769 "rc conditions: userExpLevel=unregistered;experienced"
773 public function testFilterUserExpLevelRegistrationRequiredToEditRemovesRegistrationFilters() {
774 $this->overrideConfigValue(
775 MainConfigNames::GroupPermissions,
776 [ '*' => [ 'edit' => false ] ]
778 parent::setUp();
779 $this->assertCount( 3, $this->changesListSpecialPage->filterGroupDefinitions[1]['filters'] );
781 $actualFilterGroupDefinitions = [];
782 foreach ( $this->changesListSpecialPage->filterGroupDefinitions[1]['filters'] as $key => $value ) {
783 if ( $value['name'] ) {
784 array_push( $actualFilterGroupDefinitions, $value['name'] );
787 $this->assertSame( [ "newcomer", "learner", "experienced" ], $actualFilterGroupDefinitions );
790 public function testFilterUserExpLevelRegistrationNotRequiredToEditDoesNotRemoveRegistrationFilters() {
791 $this->assertCount( 5, $this->changesListSpecialPage->filterGroupDefinitions[1]['filters'] );
793 $actualFilterGroupDefinitions = [];
794 foreach ( $this->changesListSpecialPage->filterGroupDefinitions[1]['filters'] as $key => $value ) {
795 if ( $value['name'] ) {
796 array_push( $actualFilterGroupDefinitions, $value['name'] );
799 $this->assertSame( [ "unregistered", "registered", "newcomer", "learner", "experienced" ], $actualFilterGroupDefinitions );
802 public function testFilterUserExpLevel() {
803 $now = time();
804 $this->overrideConfigValues( [
805 MainConfigNames::LearnerEdits => 10,
806 MainConfigNames::LearnerMemberSince => 4,
807 MainConfigNames::ExperiencedUserEdits => 500,
808 MainConfigNames::ExperiencedUserMemberSince => 30,
809 ] );
811 $this->createUsers( [
812 'Newcomer1' => [ 'edits' => 2, 'days' => 2 ],
813 'Newcomer2' => [ 'edits' => 12, 'days' => 3 ],
814 'Newcomer3' => [ 'edits' => 8, 'days' => 5 ],
815 'Learner1' => [ 'edits' => 15, 'days' => 10 ],
816 'Learner2' => [ 'edits' => 450, 'days' => 20 ],
817 'Learner3' => [ 'edits' => 460, 'days' => 33 ],
818 'Learner4' => [ 'edits' => 525, 'days' => 28 ],
819 'Experienced1' => [ 'edits' => 538, 'days' => 33 ],
820 ], $now );
822 // newcomers only
823 $this->assertArrayEquals(
824 [ 'Newcomer1', 'Newcomer2', 'Newcomer3' ],
825 $this->fetchUsers( [ 'newcomer' ], $now )
828 // newcomers and learner
829 $this->assertArrayEquals(
831 'Newcomer1', 'Newcomer2', 'Newcomer3',
832 'Learner1', 'Learner2', 'Learner3', 'Learner4',
834 $this->fetchUsers( [ 'newcomer', 'learner' ], $now )
837 // newcomers and more learner
838 $this->assertArrayEquals(
840 'Newcomer1', 'Newcomer2', 'Newcomer3',
841 'Experienced1',
843 $this->fetchUsers( [ 'newcomer', 'experienced' ], $now )
846 // learner only
847 $this->assertArrayEquals(
848 [ 'Learner1', 'Learner2', 'Learner3', 'Learner4' ],
849 $this->fetchUsers( [ 'learner' ], $now )
852 // more experienced only
853 $this->assertArrayEquals(
854 [ 'Experienced1' ],
855 $this->fetchUsers( [ 'experienced' ], $now )
858 // learner and more experienced
859 $this->assertArrayEquals(
861 'Learner1', 'Learner2', 'Learner3', 'Learner4',
862 'Experienced1',
864 $this->fetchUsers( [ 'learner', 'experienced' ], $now )
868 private function createUsers( array $specs, int $now ) {
869 $dbw = $this->getDb();
870 foreach ( $specs as $name => $spec ) {
871 User::createNew(
872 $name,
874 'editcount' => $spec['edits'],
875 'registration' => $dbw->timestamp( $this->daysAgo( $spec['days'], $now ) ),
876 'email' => 'ut',
882 private function fetchUsers( array $filters, int $now ): array {
883 $tables = [];
884 $conds = [];
885 $fields = [];
886 $query_options = [];
887 $join_conds = [];
889 sort( $filters );
891 call_user_func_array(
892 [ $this->changesListSpecialPage, 'filterOnUserExperienceLevel' ],
894 get_class( $this->changesListSpecialPage ),
895 $this->changesListSpecialPage->getContext(),
896 $this->changesListSpecialPage->getDB(),
897 &$tables,
898 &$fields,
899 &$conds,
900 &$query_options,
901 &$join_conds,
902 $filters,
903 $now
907 // @todo: This is not at all safe or sensible. It just blindly assumes
908 // nothing in $conds depends on any other tables.
909 $result = $this->getDb()->newSelectQueryBuilder()
910 ->select( 'user_name' )
911 ->from( 'user' )
912 ->leftJoin( 'actor', null, 'actor_user=user_id' )
913 ->where( $conds )
914 ->andWhere( [ 'user_email' => 'ut' ] )
915 ->fetchResultSet();
917 $usernames = [];
918 foreach ( $result as $row ) {
919 $usernames[] = $row->user_name;
922 return $usernames;
925 private function daysAgo( int $days, int $now ): int {
926 $secondsPerDay = 86400;
927 return $now - $days * $secondsPerDay;
930 public function testGetStructuredFilterJsData() {
931 $this->changesListSpecialPage->filterGroups = [];
933 $definition = [
935 'name' => 'gub-group',
936 'title' => 'gub-group-title',
937 'class' => ChangesListBooleanFilterGroup::class,
938 'filters' => [
940 'name' => 'hidefoo',
941 'label' => 'foo-label',
942 'description' => 'foo-description',
943 'default' => true,
944 'showHide' => 'showhidefoo',
945 'priority' => 2,
948 'name' => 'hidebar',
949 'label' => 'bar-label',
950 'description' => 'bar-description',
951 'default' => false,
952 'priority' => 4,
958 'name' => 'des-group',
959 'title' => 'des-group-title',
960 'class' => ChangesListStringOptionsFilterGroup::class,
961 'isFullCoverage' => true,
962 'filters' => [
964 'name' => 'grault',
965 'label' => 'grault-label',
966 'description' => 'grault-description',
969 'name' => 'garply',
970 'label' => 'garply-label',
971 'description' => 'garply-description',
974 'queryCallable' => static function () {
976 'default' => ChangesListStringOptionsFilterGroup::NONE,
980 'name' => 'unstructured',
981 'class' => ChangesListBooleanFilterGroup::class,
982 'filters' => [
984 'name' => 'hidethud',
985 'showHide' => 'showhidethud',
986 'default' => true,
990 'name' => 'hidemos',
991 'showHide' => 'showhidemos',
992 'default' => false,
999 $this->changesListSpecialPage->registerFiltersFromDefinitions( $definition );
1001 $this->assertArrayEquals(
1003 // Filters that only display in the unstructured UI are
1004 // are not included, and neither are groups that would
1005 // be empty due to the above.
1006 'groups' => [
1008 'name' => 'gub-group',
1009 'title' => 'gub-group-title',
1010 'type' => ChangesListBooleanFilterGroup::TYPE,
1011 'priority' => -1,
1012 'filters' => [
1014 'name' => 'hidebar',
1015 'label' => 'bar-label',
1016 'description' => 'bar-description',
1017 'default' => false,
1018 'priority' => 4,
1019 'cssClass' => null,
1020 'conflicts' => [],
1021 'subset' => [],
1022 'defaultHighlightColor' => null
1025 'name' => 'hidefoo',
1026 'label' => 'foo-label',
1027 'description' => 'foo-description',
1028 'default' => true,
1029 'priority' => 2,
1030 'cssClass' => null,
1031 'conflicts' => [],
1032 'subset' => [],
1033 'defaultHighlightColor' => null
1036 'fullCoverage' => true,
1037 'conflicts' => [],
1041 'name' => 'des-group',
1042 'title' => 'des-group-title',
1043 'type' => ChangesListStringOptionsFilterGroup::TYPE,
1044 'priority' => -2,
1045 'fullCoverage' => true,
1046 'filters' => [
1048 'name' => 'grault',
1049 'label' => 'grault-label',
1050 'description' => 'grault-description',
1051 'cssClass' => null,
1052 'priority' => -2,
1053 'conflicts' => [],
1054 'subset' => [],
1055 'defaultHighlightColor' => null
1058 'name' => 'garply',
1059 'label' => 'garply-label',
1060 'description' => 'garply-description',
1061 'cssClass' => null,
1062 'priority' => -3,
1063 'conflicts' => [],
1064 'subset' => [],
1065 'defaultHighlightColor' => null
1068 'conflicts' => [],
1069 'separator' => ';',
1070 'default' => ChangesListStringOptionsFilterGroup::NONE,
1073 'messageKeys' => [
1074 'gub-group-title',
1075 'bar-label',
1076 'bar-description',
1077 'foo-label',
1078 'foo-description',
1079 'des-group-title',
1080 'grault-label',
1081 'grault-description',
1082 'garply-label',
1083 'garply-description',
1086 $this->changesListSpecialPage->getStructuredFilterJsData(),
1087 /** ordered= */ false,
1088 /** named= */ true
1092 public function provideParseParameters() {
1093 return [
1094 [ 'hidebots', [ 'hidebots' => true ] ],
1096 [ 'bots', [ 'hidebots' => false ] ],
1098 [ 'hideminor', [ 'hideminor' => true ] ],
1100 [ 'minor', [ 'hideminor' => false ] ],
1102 [ 'hidemajor', [ 'hidemajor' => true ] ],
1104 [ 'hideliu', [ 'hideliu' => true ] ],
1106 [ 'hidepatrolled', [ 'hidepatrolled' => true ] ],
1108 [ 'hideunpatrolled', [ 'hideunpatrolled' => true ] ],
1110 [ 'hideanons', [ 'hideanons' => true ] ],
1112 [ 'hidemyself', [ 'hidemyself' => true ] ],
1114 [ 'hidebyothers', [ 'hidebyothers' => true ] ],
1116 [ 'hidehumans', [ 'hidehumans' => true ] ],
1118 [ 'hidepageedits', [ 'hidepageedits' => true ] ],
1120 [ 'pagedits', [ 'hidepageedits' => false ] ],
1122 [ 'hidenewpages', [ 'hidenewpages' => true ] ],
1124 [ 'hidecategorization', [ 'hidecategorization' => true ] ],
1126 [ 'hidelog', [ 'hidelog' => true ] ],
1129 'userExpLevel=learner;experienced',
1131 'userExpLevel' => 'learner;experienced'
1135 // A few random combos
1137 'bots,hideliu,hidemyself',
1139 'hidebots' => false,
1140 'hideliu' => true,
1141 'hidemyself' => true,
1146 'minor,hideanons,categorization',
1148 'hideminor' => false,
1149 'hideanons' => true,
1150 'hidecategorization' => false,
1155 'hidehumans,bots,hidecategorization',
1157 'hidehumans' => true,
1158 'hidebots' => false,
1159 'hidecategorization' => true,
1164 'hidemyself,userExpLevel=newcomer;learner,hideminor',
1166 'hidemyself' => true,
1167 'hideminor' => true,
1168 'userExpLevel' => 'newcomer;learner',
1174 public static function provideGetFilterConflicts() {
1175 return [
1177 "parameters" => [],
1178 "expectedConflicts" => false,
1181 "parameters" => [
1182 "hideliu" => true,
1183 "userExpLevel" => "newcomer",
1185 "expectedConflicts" => false,
1188 "parameters" => [
1189 "hideanons" => true,
1190 "userExpLevel" => "learner",
1192 "expectedConflicts" => false,
1195 "parameters" => [
1196 "hidemajor" => true,
1197 "hidenewpages" => true,
1198 "hidepageedits" => true,
1199 "hidecategorization" => false,
1200 "hidelog" => true,
1201 "hideWikidata" => true,
1203 "expectedConflicts" => true,
1206 "parameters" => [
1207 "hidemajor" => true,
1208 "hidenewpages" => false,
1209 "hidepageedits" => true,
1210 "hidecategorization" => false,
1211 "hidelog" => false,
1212 "hideWikidata" => true,
1214 "expectedConflicts" => true,
1217 "parameters" => [
1218 "hidemajor" => true,
1219 "hidenewpages" => false,
1220 "hidepageedits" => false,
1221 "hidecategorization" => true,
1222 "hidelog" => true,
1223 "hideWikidata" => true,
1225 "expectedConflicts" => false,
1228 "parameters" => [
1229 "hideminor" => true,
1230 "hidenewpages" => true,
1231 "hidepageedits" => true,
1232 "hidecategorization" => false,
1233 "hidelog" => true,
1234 "hideWikidata" => true,
1236 "expectedConflicts" => false,
1242 * @dataProvider provideGetFilterConflicts
1244 public function testGetFilterConflicts( $parameters, $expectedConflicts ) {
1245 $context = new RequestContext;
1246 $context->setRequest( new FauxRequest( $parameters ) );
1247 $this->changesListSpecialPage->setContext( $context );
1249 $this->assertEquals(
1250 $expectedConflicts,
1251 $this->changesListSpecialPage->areFiltersInConflict()
1255 public function validateOptionsProvider() {
1256 return [
1258 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 1 ],
1259 true,
1260 [ 'userExpLevel' => 'unregistered', 'hidebots' => 1, ],
1261 true,
1264 [ 'hideanons' => 1, 'hideliu' => 1, 'hidebots' => 0 ],
1265 true,
1266 [ 'hidebots' => 0, 'hidehumans' => 1 ],
1267 true,
1270 [ 'hideanons' => 1 ],
1271 true,
1272 [ 'userExpLevel' => 'registered' ],
1273 true,
1276 [ 'hideliu' => 1 ],
1277 true,
1278 [ 'userExpLevel' => 'unregistered' ],
1279 true,
1282 [ 'hideanons' => 1, 'hidebots' => 1 ],
1283 true,
1284 [ 'userExpLevel' => 'registered', 'hidebots' => 1 ],
1285 true,
1288 [ 'hideliu' => 1, 'hidebots' => 0 ],
1289 true,
1290 [ 'userExpLevel' => 'unregistered', 'hidebots' => 0 ],
1291 true,
1294 [ 'hidemyself' => 1, 'hidebyothers' => 1 ],
1295 true,
1297 true,
1300 [ 'hidebots' => 1, 'hidehumans' => 1 ],
1301 true,
1303 true,
1306 [ 'hidepatrolled' => 1, 'hideunpatrolled' => 1 ],
1307 true,
1309 true,
1312 [ 'hideminor' => 1, 'hidemajor' => 1 ],
1313 true,
1315 true,
1318 // changeType
1319 [ 'hidepageedits' => 1, 'hidenewpages' => 1, 'hidecategorization' => 1, 'hidelog' => 1, 'hidenewuserlog' => 1 ],
1320 true,
1322 true,