1<?php
2
3namespace MediaWiki\Tests\Integration\Permissions;
4
5use Action;
6use ContentHandler;
7use FauxRequest;
8use MediaWiki\Block\DatabaseBlock;
9use MediaWiki\Block\Restriction\NamespaceRestriction;
10use MediaWiki\Block\Restriction\PageRestriction;
11use MediaWiki\Block\SystemBlock;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Revision\MutableRevisionRecord;
14use MediaWiki\Session\SessionId;
15use MediaWiki\Session\TestUtils;
16use MediaWikiLangTestCase;
17use RequestContext;
18use stdClass;
19use TestAllServiceOptionsUsed;
20use Title;
21use User;
22use Wikimedia\ScopedCallback;
23use Wikimedia\TestingAccessWrapper;
24
25/**
26 * @group Database
27 *
28 * See \MediaWiki\Tests\Unit\Permissions\PermissionManagerTest
29 * for unit tests
30 *
31 * @covers \MediaWiki\Permissions\PermissionManager
32 */
33class PermissionManagerTest extends MediaWikiLangTestCase {
34	use TestAllServiceOptionsUsed;
35
36	/**
37	 * @var string
38	 */
39	protected $userName, $altUserName;
40
41	/**
42	 * @var Title
43	 */
44	protected $title;
45
46	/**
47	 * @var User
48	 */
49	protected $user, $anonUser, $userUser, $altUser;
50
51	/** Constant for self::testIsBlockedFrom */
52	private const USER_TALK_PAGE = '<user talk page>';
53
54	protected function setUp() : void {
55		parent::setUp();
56
57		$localZone = 'UTC';
58		$localOffset = date( 'Z' ) / 60;
59
60		$this->setMwGlobals( [
61			'wgLocaltimezone' => $localZone,
62			'wgLocalTZoffset' => $localOffset,
63			'wgNamespaceProtection' => [
64				NS_MEDIAWIKI => 'editinterface',
65			],
66			'wgRevokePermissions' => [
67				'formertesters' => [
68					'runtest' => true
69				]
70			],
71			'wgAvailableRights' => [
72				'test',
73				'runtest',
74				'writetest',
75				'nukeworld',
76				'modifytest',
77				'editmyoptions',
78				'editinterface',
79
80				// Interface admin
81				'editsitejs',
82				'edituserjs',
83
84				// Admin
85				'delete',
86				'undelete',
87				'deletedhistory',
88				'deletedtext',
89			]
90		] );
91
92		$this->setGroupPermissions( 'unittesters', 'test', true );
93		$this->setGroupPermissions( 'unittesters', 'runtest', true );
94		$this->setGroupPermissions( 'unittesters', 'writetest', false );
95		$this->setGroupPermissions( 'unittesters', 'nukeworld', false );
96
97		$this->setGroupPermissions( 'testwriters', 'test', true );
98		$this->setGroupPermissions( 'testwriters', 'writetest', true );
99		$this->setGroupPermissions( 'testwriters', 'modifytest', true );
100
101		$this->setGroupPermissions( '*', 'editmyoptions', true );
102
103		$this->setGroupPermissions( 'interface-admin', 'editinterface', true );
104		$this->setGroupPermissions( 'interface-admin', 'editsitejs', true );
105		$this->setGroupPermissions( 'interface-admin', 'edituserjs', true );
106		$this->setGroupPermissions( 'sysop', 'editinterface', true );
107		$this->setGroupPermissions( 'sysop', 'delete', true );
108		$this->setGroupPermissions( 'sysop', 'undelete', true );
109		$this->setGroupPermissions( 'sysop', 'deletedhistory', true );
110		$this->setGroupPermissions( 'sysop', 'deletedtext', true );
111
112		// Without this testUserBlock will use a non-English context on non-English MediaWiki
113		// installations (because of how Title::checkUserBlock is implemented) and fail.
114		RequestContext::resetMain();
115
116		$this->userName = 'Useruser';
117		$this->altUserName = 'Altuseruser';
118		date_default_timezone_set( $localZone );
119
120		$this->title = Title::makeTitle( NS_MAIN, "Main Page" );
121		if ( !isset( $this->userUser ) || !( $this->userUser instanceof User ) ) {
122			$this->userUser = User::newFromName( $this->userName );
123
124			if ( !$this->userUser->getId() ) {
125				$this->userUser = User::createNew( $this->userName, [
126					"email" => "test@example.com",
127					"real_name" => "Test User" ] );
128				$this->userUser->load();
129			}
130
131			$this->altUser = User::newFromName( $this->altUserName );
132			if ( !$this->altUser->getId() ) {
133				$this->altUser = User::createNew( $this->altUserName, [
134					"email" => "alttest@example.com",
135					"real_name" => "Test User Alt" ] );
136				$this->altUser->load();
137			}
138
139			$this->anonUser = User::newFromId( 0 );
140
141			$this->user = $this->userUser;
142		}
143	}
144
145	protected function setTitle( $ns, $title = "Main_Page" ) {
146		$this->title = Title::makeTitle( $ns, $title );
147	}
148
149	protected function setUser( $userName = null ) {
150		if ( $userName === 'anon' ) {
151			$this->user = $this->anonUser;
152		} elseif ( $userName === null || $userName === $this->userName ) {
153			$this->user = $this->userUser;
154		} else {
155			$this->user = $this->altUser;
156		}
157	}
158
159	/**
160	 * @dataProvider provideSpecialsAndNSPermissions
161	 * @covers MediaWiki\Permissions\PermissionManager::checkSpecialsAndNSPermissions
162	 */
163	public function testSpecialsAndNSPermissions(
164		$namespace,
165		$userPerms,
166		$namespaceProtection,
167		$expectedPermErrors,
168		$expectedUserCan
169	) {
170		$this->setUser( $this->userName );
171		$this->setTitle( $namespace );
172
173		$this->mergeMwGlobalArrayValue( 'wgNamespaceProtection', $namespaceProtection );
174
175		$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
176
177		$this->overrideUserPermissions( $this->user, $userPerms );
178
179		$this->assertEquals(
180			$expectedPermErrors,
181			$permissionManager->getPermissionErrors( 'bogus', $this->user, $this->title )
182		);
183		$this->assertSame(
184			$expectedUserCan,
185			$permissionManager->userCan( 'bogus', $this->user, $this->title )
186		);
187	}
188
189	public function provideSpecialsAndNSPermissions() {
190		yield [
191			'namespace' => NS_SPECIAL,
192			'user permissions' => [],
193			'namespace protection' => [],
194			'expected permission errors' => [ [ 'badaccess-group0' ], [ 'ns-specialprotected' ] ],
195			'user can' => false,
196		];
197		yield [
198			'namespace' => NS_MAIN,
199			'user permissions' => [ 'bogus' ],
200			'namespace protection' => [],
201			'expected permission errors' => [],
202			'user can' => true,
203		];
204		yield [
205			'namespace' => NS_MAIN,
206			'user permissions' => [],
207			'namespace protection' => [],
208			'expected permission errors' => [ [ 'badaccess-group0' ] ],
209			'user can' => false,
210		];
211		yield [
212			'namespace' => NS_USER,
213			'user permissions' => [],
214			'namespace protection' => [ NS_USER => [ 'bogus' ] ],
215			'expected permission errors' => [ [ 'badaccess-group0' ], [ 'namespaceprotected', 'User', 'bogus' ] ],
216			'user can' => false,
217		];
218		yield [
219			'namespace' => NS_MEDIAWIKI,
220			'user permissions' => [ 'bogus' ],
221			'namespace protection' => [],
222			'expected permission errors' => [ [ 'protectedinterface', 'bogus' ] ],
223			'user can' => false,
224		];
225		yield [
226			'namespace' => NS_MAIN,
227			'user permissions' => [ 'bogus' ],
228			'namespace protection' => [],
229			'expected permission errors' => [],
230			'user can' => true,
231		];
232	}
233
234	/**
235	 * @covers \MediaWiki\Permissions\PermissionManager::checkCascadingSourcesRestrictions
236	 */
237	public function testCascadingSourcesRestrictions() {
238		$this->setTitle( NS_MAIN, "test page" );
239		$this->overrideUserPermissions( $this->user, [ "edit", "bogus" ] );
240
241		$this->title->mCascadeSources = [
242			Title::makeTitle( NS_MAIN, "Bogus" ),
243			Title::makeTitle( NS_MAIN, "UnBogus" )
244		];
245		$this->title->mCascadingRestrictions = [
246			"bogus" => [ 'bogus', "sysop", "protect", "" ]
247		];
248
249		$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
250
251		$this->assertFalse( $permissionManager->userCan( 'bogus', $this->user, $this->title ) );
252		$this->assertEquals( [
253			[ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ],
254			[ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ],
255			[ "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n", 'bogus' ] ],
256			$permissionManager->getPermissionErrors(
257				'bogus', $this->user, $this->title ) );
258
259		$this->assertTrue( $permissionManager->userCan( 'edit', $this->user, $this->title ) );
260		$this->assertEquals(
261			[],
262			$permissionManager->getPermissionErrors( 'edit', $this->user, $this->title )
263		);
264	}
265
266	/**
267	 * @dataProvider provideActionPermissions
268	 * @covers \MediaWiki\Permissions\PermissionManager::checkActionPermissions
269	 */
270	public function testActionPermissions(
271		$namespace,
272		$titleOverrides,
273		$action,
274		$userPerms,
275		$expectedPermErrors,
276		$expectedUserCan
277	) {
278		$this->setTitle( $namespace, "test page" );
279		$this->title->mTitleProtection['permission'] = '';
280		$this->title->mTitleProtection['user'] = $this->user->getId();
281		$this->title->mTitleProtection['expiry'] = 'infinity';
282		$this->title->mTitleProtection['reason'] = 'test';
283		$this->title->mCascadeRestriction = false;
284		$this->title->mRestrictionsLoaded = true;
285
286		if ( isset( $titleOverrides['protectedPermission' ] ) ) {
287			$this->title->mTitleProtection['permission'] = $titleOverrides['protectedPermission'];
288		}
289		if ( isset( $titleOverrides['interwiki'] ) ) {
290			$this->title->mInterwiki = $titleOverrides['interwiki'];
291		}
292
293		$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
294
295		$this->overrideUserPermissions( $this->user, $userPerms );
296
297		$this->assertEquals(
298			$expectedPermErrors,
299			$permissionManager->getPermissionErrors( $action, $this->user, $this->title )
300		);
301		$this->assertSame(
302			$expectedUserCan,
303			$permissionManager->userCan( $action, $this->user, $this->title )
304		);
305	}
306
307	public function provideActionPermissions() {
308		// title overrides can include "protectedPermission" to override
309		// $title->mTitleProtection['permission'], and "interwiki" to override
310		// $title->mInterwiki, for the few cases those are needed
311		yield [
312			'namespace' => NS_MAIN,
313			'title overrides' => [],
314			'action' => 'create',
315			'user permissions' => [ 'createpage' ],
316			'expected permission errors' => [ [ 'titleprotected', 'Useruser', 'test' ] ],
317			'user can' => false,
318		];
319		yield [
320			'namespace' => NS_MAIN,
321			'title overrides' => [ 'protectedPermission' => 'editprotected' ],
322			'action' => 'create',
323			'user permissions' => [ 'createpage', 'protect' ],
324			'expected permission errors' => [ [ 'titleprotected', 'Useruser', 'test' ] ],
325			'user can' => false,
326		];
327		yield [
328			'namespace' => NS_MAIN,
329			'title overrides' => [ 'protectedPermission' => 'editprotected' ],
330			'action' => 'create',
331			'user permissions' => [ 'createpage', 'editprotected' ],
332			'expected permission errors' => [],
333			'user can' => true,
334		];
335		yield [
336			'namespace' => NS_MEDIA,
337			'title overrides' => [],
338			'action' => 'move',
339			'user permissions' => [ 'move' ],
340			'expected permission errors' => [ [ 'immobile-source-namespace', 'Media' ] ],
341			'user can' => false,
342		];
343		yield [
344			'namespace' => NS_HELP,
345			'title overrides' => [],
346			'action' => 'move',
347			'user permissions' => [ 'move' ],
348			'expected permission errors' => [],
349			'user can' => true,
350		];
351		yield [
352			'namespace' => NS_HELP,
353			'title overrides' => [ 'interwiki' => 'no' ],
354			'action' => 'move',
355			'user permissions' => [ 'move' ],
356			'expected permission errors' => [ [ 'immobile-source-page' ] ],
357			'user can' => false,
358		];
359		yield [
360			'namespace' => NS_MEDIA,
361			'title overrides' => [],
362			'action' => 'move-target',
363			'user permissions' => [ 'move' ],
364			'expected permission errors' => [ [ 'immobile-target-namespace', 'Media' ] ],
365			'user can' => false,
366		];
367		yield [
368			'namespace' => NS_HELP,
369			'title overrides' => [],
370			'action' => 'move-target',
371			'user permissions' => [ 'move' ],
372			'expected permission errors' => [],
373			'user can' => true,
374		];
375		yield [
376			'namespace' => NS_HELP,
377			'title overrides' => [ 'interwiki' => 'no' ],
378			'action' => 'move-target',
379			'user permissions' => [ 'move' ],
380			'expected permission errors' => [ [ 'immobile-target-page' ] ],
381			'user can' => false,
382		];
383	}
384
385	/**
386	 * @dataProvider provideTestCheckUserBlockActions
387	 * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock
388	 */
389	public function testCheckUserBlockActions( $block, $restriction, $expected ) {
390		$this->setMwGlobals( [
391			'wgEmailConfirmToEdit' => false,
392		] );
393
394		if ( $restriction ) {
395			$pageRestriction = new PageRestriction( 0, $this->title->getArticleID() );
396			$pageRestriction->setTitle( $this->title );
397			$block->setRestrictions( [ $pageRestriction ] );
398		}
399
400		$user = $this->getMockBuilder( User::class )
401			->setMethods( [ 'getBlock' ] )
402			->getMock();
403		$user->method( 'getBlock' )
404			->willReturn( $block );
405
406		$this->overrideUserPermissions( $user, [
407			'createpage',
408			'edit',
409			'move',
410			'rollback',
411			'patrol',
412			'upload',
413			'purge'
414		] );
415
416		$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
417
418		// Check that user is blocked or unblocked from specific actions
419		foreach ( $expected as $action => $blocked ) {
420			$expectedErrorCount = $blocked ? 1 : 0;
421			$this->assertCount(
422				$expectedErrorCount,
423				$permissionManager->getPermissionErrors(
424					$action,
425					$user,
426					$this->title
427				)
428			);
429		}
430
431		// quickUserCan should ignore user blocks
432		$this->assertTrue(
433			$permissionManager->quickUserCan( 'move-target', $this->user, $this->title )
434		);
435	}
436
437	public static function provideTestCheckUserBlockActions() {
438		return [
439			'Sitewide autoblock' => [
440				new DatabaseBlock( [
441					'address' => '127.0.8.1',
442					'by' => 100,
443					'auto' => true,
444				] ),
445				false,
446				[
447					'edit' => true,
448					'move-target' => true,
449					'rollback' => true,
450					'patrol' => true,
451					'upload' => true,
452					'purge' => false,
453				]
454			],
455			'Sitewide block' => [
456				new DatabaseBlock( [
457					'address' => '127.0.8.1',
458					'by' => 100,
459				] ),
460				false,
461				[
462					'edit' => true,
463					'move-target' => true,
464					'rollback' => true,
465					'patrol' => true,
466					'upload' => true,
467					'purge' => false,
468				]
469			],
470			'Partial block without restriction against this page' => [
471				new DatabaseBlock( [
472					'address' => '127.0.8.1',
473					'by' => 100,
474					'sitewide' => false,
475				] ),
476				false,
477				[
478					'edit' => false,
479					'move-target' => false,
480					'rollback' => false,
481					'patrol' => false,
482					'upload' => false,
483					'purge' => false,
484				]
485			],
486			'Partial block with restriction against this page' => [
487				new DatabaseBlock( [
488					'address' => '127.0.8.1',
489					'by' => 100,
490					'sitewide' => false,
491				] ),
492				true,
493				[
494					'edit' => true,
495					'move-target' => true,
496					'rollback' => true,
497					'patrol' => true,
498					'upload' => false,
499					'purge' => false,
500				]
501			],
502			'System block' => [
503				new SystemBlock( [
504					'address' => '127.0.8.1',
505					'by' => 100,
506					'systemBlock' => 'test',
507				] ),
508				false,
509				[
510					'edit' => true,
511					'move-target' => true,
512					'rollback' => true,
513					'patrol' => true,
514					'upload' => true,
515					'purge' => false,
516				]
517			],
518			'No block' => [
519				null,
520				false,
521				[
522					'edit' => false,
523					'move-target' => false,
524					'rollback' => false,
525					'patrol' => false,
526					'upload' => false,
527					'purge' => false,
528				]
529			]
530		];
531	}
532
533	/**
534	 * @dataProvider provideTestCheckUserBlockMessage
535	 * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock
536	 */
537	public function testCheckUserBlockMessage( $blockType, $blockParams, $restriction, $expected ) {
538		$this->setMwGlobals( [
539			'wgEmailConfirmToEdit' => false,
540		] );
541
542		$block = new $blockType( array_merge( [
543			'address' => '127.0.8.1',
544			'by' => $this->user->getId(),
545			'reason' => 'Test reason',
546			'timestamp' => '20000101000000',
547			'expiry' => 0,
548		], $blockParams ) );
549
550		if ( $restriction ) {
551			$pageRestriction = new PageRestriction( 0, $this->title->getArticleID() );
552			$pageRestriction->setTitle( $this->title );
553			$block->setRestrictions( [ $pageRestriction ] );
554		}
555
556		$user = $this->getMockBuilder( User::class )
557			->setMethods( [ 'getBlock' ] )
558			->getMock();
559		$user->method( 'getBlock' )
560			->willReturn( $block );
561
562		$this->overrideUserPermissions( $user, [ 'edit' ] );
563
564		$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
565		$errors = $permissionManager->getPermissionErrors(
566			'edit',
567			$user,
568			$this->title
569		);
570
571		$this->assertEquals(
572			$expected['message'],
573			$errors[0][0]
574		);
575	}
576
577	public static function provideTestCheckUserBlockMessage() {
578		return [
579			'Sitewide autoblock' => [
580				DatabaseBlock::class,
581				[ 'auto' => true ],
582				false,
583				[
584					'message' => 'autoblockedtext',
585				],
586			],
587			'Sitewide block' => [
588				DatabaseBlock::class,
589				[],
590				false,
591				[
592					'message' => 'blockedtext',
593				],
594			],
595			'Partial block with restriction against this page' => [
596				DatabaseBlock::class,
597				[ 'sitewide' => false ],
598				true,
599				[
600					'message' => 'blockedtext-partial',
601				],
602			],
603			'System block' => [
604				SystemBlock::class,
605				[ 'systemBlock' => 'test' ],
606				false,
607				[
608					'message' => 'systemblockedtext',
609				],
610			],
611		];
612	}
613
614	/**
615	 * @dataProvider provideTestCheckUserBlockEmailConfirmToEdit
616	 * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock
617	 */
618	public function testCheckUserBlockEmailConfirmToEdit( $emailConfirmToEdit, $assertion ) {
619		$this->setMwGlobals( [
620			'wgEmailConfirmToEdit' => $emailConfirmToEdit,
621			'wgEmailAuthentication' => true,
622		] );
623
624		$this->overrideUserPermissions( $this->user, [
625			'edit',
626			'move',
627		] );
628
629		$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
630
631		$this->$assertion( [ 'confirmedittext' ],
632			$permissionManager->getPermissionErrors( 'edit', $this->user, $this->title ) );
633
634		// $wgEmailConfirmToEdit only applies to 'edit' action
635		$this->assertEquals( [],
636			$permissionManager->getPermissionErrors( 'move-target', $this->user, $this->title ) );
637	}
638
639	public static function provideTestCheckUserBlockEmailConfirmToEdit() {
640		return [
641			'User must confirm email to edit' => [
642				true,
643				'assertContains',
644			],
645			'User may edit without confirming email' => [
646				false,
647				'assertNotContains',
648			],
649		];
650	}
651
652	/**
653	 * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock
654	 *
655	 * Tests to determine that the passed in permission does not get mixed up with
656	 * an action of the same name.
657	 */
658	public function testCheckUserBlockActionPermission() {
659		$tester = $this->getMockBuilder( Action::class )
660					   ->disableOriginalConstructor()
661					   ->getMock();
662		$tester->method( 'getName' )
663			   ->willReturn( 'tester' );
664		$tester->method( 'getRestriction' )
665			   ->willReturn( 'test' );
666		$tester->method( 'requiresUnblock' )
667			   ->willReturn( false );
668
669		$this->setMwGlobals( [
670			'wgActions' => [
671				'tester' => $tester,
672			],
673			'wgGroupPermissions' => [
674				'*' => [
675					'tester' => true,
676				],
677			],
678		] );
679
680		$user = $this->getMockBuilder( User::class )
681			->setMethods( [ 'getBlock' ] )
682			->getMock();
683		$user->method( 'getBlock' )
684			->willReturn( new DatabaseBlock( [
685				'address' => '127.0.8.1',
686				'by' => $this->user->getId(),
687			] ) );
688
689		$this->assertCount( 1, MediaWikiServices::getInstance()->getPermissionManager()
690			->getPermissionErrors( 'tester', $user, $this->title )
691		);
692	}
693
694	/**
695	 * @covers \MediaWiki\Permissions\PermissionManager::isBlockedFrom
696	 */
697	public function testBlockInstanceCache() {
698		// First, check the user isn't blocked
699		$user = $this->getMutableTestUser()->getUser();
700		$ut = Title::makeTitle( NS_USER_TALK, $user->getName() );
701		$this->assertNull( $user->getBlock( false ), 'sanity check' );
702		$this->assertFalse( MediaWikiServices::getInstance()->getPermissionManager()
703			->isBlockedFrom( $user, $ut ), 'sanity check' );
704
705		// Block the user
706		$blocker = $this->getTestSysop()->getUser();
707		$block = new DatabaseBlock( [
708			'hideName' => true,
709			'allowUsertalk' => false,
710			'reason' => 'Because',
711		] );
712		$block->setTarget( $user );
713		$block->setBlocker( $blocker );
714		$blockStore = MediaWikiServices::getInstance()->getDatabaseBlockStore();
715		$res = $blockStore->insertBlock( $block );
716		$this->assertTrue( (bool)$res['id'], 'sanity check: Failed to insert block' );
717
718		// Clear cache and confirm it loaded the block properly
719		$user->clearInstanceCache();
720		$this->assertInstanceOf( DatabaseBlock::class, $user->getBlock( false ) );
721		$this->assertTrue( MediaWikiServices::getInstance()->getPermissionManager()
722			->isBlockedFrom( $user, $ut ) );
723
724		// Unblock
725		$blockStore->deleteBlock( $block );
726
727		// Clear cache and confirm it loaded the not-blocked properly
728		$user->clearInstanceCache();
729		$this->assertNull( $user->getBlock( false ) );
730		$this->assertFalse( MediaWikiServices::getInstance()->getPermissionManager()
731			->isBlockedFrom( $user, $ut ) );
732	}
733
734	/**
735	 * @covers \MediaWiki\Permissions\PermissionManager::isBlockedFrom
736	 * @dataProvider provideIsBlockedFrom
737	 * @param string|null $title Title to test.
738	 * @param bool $expect Expected result from User::isBlockedFrom()
739	 * @param array $options Additional test options:
740	 *  - 'blockAllowsUTEdit': (bool, default true) Value for $wgBlockAllowsUTEdit
741	 *  - 'allowUsertalk': (bool, default false) Passed to DatabaseBlock::__construct()
742	 *  - 'pageRestrictions': (array|null) If non-empty, page restriction titles for the block.
743	 */
744	public function testIsBlockedFrom( $title, $expect, array $options = [] ) {
745		$this->setMwGlobals( [
746			'wgBlockAllowsUTEdit' => $options['blockAllowsUTEdit'] ?? true,
747		] );
748
749		$user = $this->getTestUser()->getUser();
750
751		if ( $title === self::USER_TALK_PAGE ) {
752			$title = $user->getTalkPage();
753		} else {
754			$title = Title::newFromText( $title );
755		}
756
757		$restrictions = [];
758		foreach ( $options['pageRestrictions'] ?? [] as $pagestr ) {
759			$page = $this->getExistingTestPage(
760				$pagestr === self::USER_TALK_PAGE ? $user->getTalkPage() : $pagestr
761			);
762			$restrictions[] = new PageRestriction( 0, $page->getId() );
763		}
764		foreach ( $options['namespaceRestrictions'] ?? [] as $ns ) {
765			$restrictions[] = new NamespaceRestriction( 0, $ns );
766		}
767
768		$block = new DatabaseBlock( [
769			'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ),
770			'allowUsertalk' => $options['allowUsertalk'] ?? false,
771			'sitewide' => !$restrictions,
772		] );
773		$block->setTarget( $user );
774		$block->setBlocker( $this->getTestSysop()->getUser() );
775		if ( $restrictions ) {
776			$block->setRestrictions( $restrictions );
777		}
778		$blockStore = MediaWikiServices::getInstance()->getDatabaseBlockStore();
779		$blockStore->insertBlock( $block );
780
781		try {
782			$this->assertSame( $expect, MediaWikiServices::getInstance()->getPermissionManager()
783				->isBlockedFrom( $user, $title ) );
784		} finally {
785			$blockStore->deleteBlock( $block );
786		}
787	}
788
789	public static function provideIsBlockedFrom() {
790		return [
791			'Sitewide block, basic operation' => [ 'Test page', true ],
792			'Sitewide block, not allowing user talk' => [
793				self::USER_TALK_PAGE, true, [
794					'allowUsertalk' => false,
795				]
796			],
797			'Sitewide block, allowing user talk' => [
798				self::USER_TALK_PAGE, false, [
799					'allowUsertalk' => true,
800				]
801			],
802			'Sitewide block, allowing user talk but $wgBlockAllowsUTEdit is false' => [
803				self::USER_TALK_PAGE, true, [
804					'allowUsertalk' => true,
805					'blockAllowsUTEdit' => false,
806				]
807			],
808			'Partial block, blocking the page' => [
809				'Test page', true, [
810					'pageRestrictions' => [ 'Test page' ],
811				]
812			],
813			'Partial block, not blocking the page' => [
814				'Test page 2', false, [
815					'pageRestrictions' => [ 'Test page' ],
816				]
817			],
818			'Partial block, not allowing user talk but user talk page is not blocked' => [
819				self::USER_TALK_PAGE, false, [
820					'allowUsertalk' => false,
821					'pageRestrictions' => [ 'Test page' ],
822				]
823			],
824			'Partial block, allowing user talk but user talk page is blocked' => [
825				self::USER_TALK_PAGE, true, [
826					'allowUsertalk' => true,
827					'pageRestrictions' => [ self::USER_TALK_PAGE ],
828				]
829			],
830			'Partial block, user talk page is not blocked but $wgBlockAllowsUTEdit is false' => [
831				self::USER_TALK_PAGE, false, [
832					'allowUsertalk' => false,
833					'pageRestrictions' => [ 'Test page' ],
834					'blockAllowsUTEdit' => false,
835				]
836			],
837			'Partial block, user talk page is blocked and $wgBlockAllowsUTEdit is false' => [
838				self::USER_TALK_PAGE, true, [
839					'allowUsertalk' => true,
840					'pageRestrictions' => [ self::USER_TALK_PAGE ],
841					'blockAllowsUTEdit' => false,
842				]
843			],
844			'Partial user talk namespace block, not allowing user talk' => [
845				self::USER_TALK_PAGE, true, [
846					'allowUsertalk' => false,
847					'namespaceRestrictions' => [ NS_USER_TALK ],
848				]
849			],
850			'Partial user talk namespace block, allowing user talk' => [
851				self::USER_TALK_PAGE, false, [
852					'allowUsertalk' => true,
853					'namespaceRestrictions' => [ NS_USER_TALK ],
854				]
855			],
856			'Partial user talk namespace block, where $wgBlockAllowsUTEdit is false' => [
857				self::USER_TALK_PAGE, true, [
858					'allowUsertalk' => true,
859					'namespaceRestrictions' => [ NS_USER_TALK ],
860					'blockAllowsUTEdit' => false,
861				]
862			],
863		];
864	}
865
866	/**
867	 * @covers \MediaWiki\Permissions\PermissionManager::getUserPermissions
868	 */
869	public function testGetUserPermissions() {
870		$user = $this->getTestUser( [ 'unittesters' ] )->getUser();
871		$rights = MediaWikiServices::getInstance()->getPermissionManager()
872			->getUserPermissions( $user );
873		$this->assertContains( 'runtest', $rights );
874		$this->assertNotContains( 'writetest', $rights );
875		$this->assertNotContains( 'modifytest', $rights );
876		$this->assertNotContains( 'nukeworld', $rights );
877	}
878
879	/**
880	 * @covers \MediaWiki\Permissions\PermissionManager::getUserPermissions
881	 */
882	public function testGetUserPermissionsHooks() {
883		$user = $this->getTestUser( [ 'unittesters', 'testwriters' ] )->getUser();
884		$userWrapper = TestingAccessWrapper::newFromObject( $user );
885
886		$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
887		$rights = $permissionManager->getUserPermissions( $user );
888		$this->assertContains( 'test', $rights, 'sanity check' );
889		$this->assertContains( 'runtest', $rights, 'sanity check' );
890		$this->assertContains( 'writetest', $rights, 'sanity check' );
891		$this->assertNotContains( 'nukeworld', $rights, 'sanity check' );
892
893		// Add a hook manipluating the rights
894		$this->setTemporaryHook( 'UserGetRights', static function ( $user, &$rights ) {
895			$rights[] = 'nukeworld';
896			$rights = array_diff( $rights, [ 'writetest' ] );
897		} );
898
899		$permissionManager->invalidateUsersRightsCache( $user );
900		$rights = $permissionManager->getUserPermissions( $user );
901		$this->assertContains( 'test', $rights );
902		$this->assertContains( 'runtest', $rights );
903		$this->assertNotContains( 'writetest', $rights );
904		$this->assertContains( 'nukeworld', $rights );
905
906		// Add a Session that limits rights. We're mocking a stdClass because the Session
907		// class is final, and thus not mockable.
908		$mock = $this->getMockBuilder( stdClass::class )
909			->setMethods( [ 'getAllowedUserRights', 'deregisterSession', 'getSessionId' ] )
910			->getMock();
911		$mock->method( 'getAllowedUserRights' )->willReturn( [ 'test', 'writetest' ] );
912		$mock->method( 'getSessionId' )->willReturn(
913			new SessionId( str_repeat( 'X', 32 ) )
914		);
915		$session = TestUtils::getDummySession( $mock );
916		$mockRequest = $this->getMockBuilder( FauxRequest::class )
917			->setMethods( [ 'getSession' ] )
918			->getMock();
919		$mockRequest->method( 'getSession' )->willReturn( $session );
920		$userWrapper->mRequest = $mockRequest;
921
922		$this->resetServices();
923		$rights = MediaWikiServices::getInstance()
924			->getPermissionManager()
925			->getUserPermissions( $user );
926		$this->assertContains( 'test', $rights );
927		$this->assertNotContains( 'runtest', $rights );
928		$this->assertNotContains( 'writetest', $rights );
929		$this->assertNotContains( 'nukeworld', $rights );
930	}
931
932	/**
933	 * @covers \MediaWiki\Permissions\PermissionManager::getGroupPermissions
934	 */
935	public function testGroupPermissions() {
936		$rights = MediaWikiServices::getInstance()->getPermissionManager()
937			->getGroupPermissions( [ 'unittesters' ] );
938		$this->assertContains( 'runtest', $rights );
939		$this->assertNotContains( 'writetest', $rights );
940		$this->assertNotContains( 'modifytest', $rights );
941		$this->assertNotContains( 'nukeworld', $rights );
942
943		$rights = MediaWikiServices::getInstance()->getPermissionManager()
944			->getGroupPermissions( [ 'unittesters', 'testwriters' ] );
945		$this->assertContains( 'runtest', $rights );
946		$this->assertContains( 'writetest', $rights );
947		$this->assertContains( 'modifytest', $rights );
948		$this->assertNotContains( 'nukeworld', $rights );
949	}
950
951	/**
952	 * @covers \MediaWiki\Permissions\PermissionManager::getGroupPermissions
953	 */
954	public function testRevokePermissions() {
955		$rights = MediaWikiServices::getInstance()->getPermissionManager()
956			->getGroupPermissions( [ 'unittesters', 'formertesters' ] );
957		$this->assertNotContains( 'runtest', $rights );
958		$this->assertNotContains( 'writetest', $rights );
959		$this->assertNotContains( 'modifytest', $rights );
960		$this->assertNotContains( 'nukeworld', $rights );
961	}
962
963	/**
964	 * @dataProvider provideGetGroupsWithPermission
965	 * @covers \MediaWiki\Permissions\PermissionManager::getGroupsWithPermission
966	 */
967	public function testGetGroupsWithPermission( $expected, $right ) {
968		$result = MediaWikiServices::getInstance()->getPermissionManager()
969			->getGroupsWithPermission( $right );
970		sort( $result );
971		sort( $expected );
972
973		$this->assertEquals( $expected, $result, "Groups with permission $right" );
974	}
975
976	public static function provideGetGroupsWithPermission() {
977		return [
978			[
979				[ 'unittesters', 'testwriters' ],
980				'test'
981			],
982			[
983				[ 'unittesters' ],
984				'runtest'
985			],
986			[
987				[ 'testwriters' ],
988				'writetest'
989			],
990			[
991				[ 'testwriters' ],
992				'modifytest'
993			],
994		];
995	}
996
997	/**
998	 * @covers \MediaWiki\Permissions\PermissionManager::userHasRight
999	 */
1000	public function testUserHasRight() {
1001		$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1002
1003		$result = $permissionManager->userHasRight(
1004			$this->getTestUser( 'unittesters' )->getUser(),
1005			'test'
1006		);
1007		$this->assertTrue( $result );
1008
1009		$result = $permissionManager->userHasRight(
1010			$this->getTestUser( 'formertesters' )->getUser(),
1011			'runtest'
1012		);
1013		$this->assertFalse( $result );
1014
1015		$result = $permissionManager->userHasRight(
1016			$this->getTestUser( 'formertesters' )->getUser(),
1017			''
1018		);
1019		$this->assertTrue( $result );
1020	}
1021
1022	/**
1023	 * @covers \MediaWiki\Permissions\PermissionManager::groupHasPermission
1024	 */
1025	public function testGroupHasPermission() {
1026		$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1027
1028		$result = $permissionManager->groupHasPermission(
1029			'unittesters',
1030			'test'
1031		);
1032		$this->assertTrue( $result );
1033
1034		$result = $permissionManager->groupHasPermission(
1035			'formertesters',
1036			'runtest'
1037		);
1038		$this->assertFalse( $result );
1039	}
1040
1041	/**
1042	 * @covers \MediaWiki\Permissions\PermissionManager::isEveryoneAllowed
1043	 */
1044	public function testIsEveryoneAllowed() {
1045		$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1046
1047		$result = $permissionManager->isEveryoneAllowed( 'editmyoptions' );
1048		$this->assertTrue( $result );
1049
1050		$result = $permissionManager->isEveryoneAllowed( 'test' );
1051		$this->assertFalse( $result );
1052	}
1053
1054	/**
1055	 * @covers \MediaWiki\Permissions\PermissionManager::addTemporaryUserRights
1056	 */
1057	public function testAddTemporaryUserRights() {
1058		$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1059		$this->overrideUserPermissions( $this->user, [ 'read', 'edit' ] );
1060		// sanity checks
1061		$this->assertEquals( [ 'read', 'edit' ], $permissionManager->getUserPermissions( $this->user ) );
1062		$this->assertFalse( $permissionManager->userHasRight( $this->user, 'move' ) );
1063
1064		$scope = $permissionManager->addTemporaryUserRights( $this->user, [ 'move', 'delete' ] );
1065		$this->assertEquals( [ 'read', 'edit', 'move', 'delete' ],
1066			$permissionManager->getUserPermissions( $this->user ) );
1067		$this->assertTrue( $permissionManager->userHasRight( $this->user, 'move' ) );
1068
1069		$scope2 = $permissionManager->addTemporaryUserRights( $this->user, [ 'delete', 'upload' ] );
1070		$this->assertEquals( [ 'read', 'edit', 'move', 'delete', 'upload' ],
1071			$permissionManager->getUserPermissions( $this->user ) );
1072
1073		ScopedCallback::consume( $scope );
1074		$this->assertEquals( [ 'read', 'edit', 'delete', 'upload' ],
1075			$permissionManager->getUserPermissions( $this->user ) );
1076		ScopedCallback::consume( $scope2 );
1077		$this->assertEquals( [ 'read', 'edit' ],
1078			$permissionManager->getUserPermissions( $this->user ) );
1079		$this->assertFalse( $permissionManager->userHasRight( $this->user, 'move' ) );
1080
1081		( function () use ( $permissionManager ) {
1082			$scope = $permissionManager->addTemporaryUserRights( $this->user, 'move' );
1083			$this->assertTrue( $permissionManager->userHasRight( $this->user, 'move' ) );
1084		} )();
1085		$this->assertFalse( $permissionManager->userHasRight( $this->user, 'move' ) );
1086	}
1087
1088	/**
1089	 * Create a RevisionRecord with a single Javascript main slot.
1090	 * @param Title $title
1091	 * @param User $user
1092	 * @param string $text
1093	 * @return MutableRevisionRecord
1094	 */
1095	private function getJavascriptRevision( Title $title, User $user, $text ) {
1096		$content = ContentHandler::makeContent( $text, $title, CONTENT_MODEL_JAVASCRIPT );
1097		$revision = new MutableRevisionRecord( $title );
1098		$revision->setContent( 'main', $content );
1099		return $revision;
1100	}
1101
1102	/**
1103	 * Create a RevisionRecord with a single Javascript redirect main slot.
1104	 * @param Title $title
1105	 * @param Title $redirectTargetTitle
1106	 * @param User $user
1107	 * @return MutableRevisionRecord
1108	 */
1109	private function getJavascriptRedirectRevision(
1110		Title $title, Title $redirectTargetTitle, User $user
1111	) {
1112		$content = MediaWikiServices::getInstance()->getContentHandlerFactory()
1113			->getContentHandler( CONTENT_MODEL_JAVASCRIPT )
1114			->makeRedirectContent( $redirectTargetTitle );
1115		$revision = new MutableRevisionRecord( $title );
1116		$revision->setContent( 'main', $content );
1117		return $revision;
1118	}
1119
1120	public function provideGetRestrictionLevels() {
1121		return [
1122			'No namespace restriction' => [ [ '', 'autoconfirmed', 'sysop' ], NS_TALK ],
1123			'Restricted to autoconfirmed' => [ [ '', 'sysop' ], NS_MAIN ],
1124			'Restricted to sysop' => [ [ '' ], NS_USER ],
1125			'Restricted to someone in two groups' => [ [ '', 'sysop' ], 101 ],
1126			'No special permissions' => [
1127				[ '' ],
1128				NS_TALK,
1129				[]
1130			],
1131			'autoconfirmed' => [
1132				[ '', 'autoconfirmed' ],
1133				NS_TALK,
1134				[ 'autoconfirmed' ]
1135			],
1136			'autoconfirmed revoked' => [
1137				[ '' ],
1138				NS_TALK,
1139				[ 'autoconfirmed', 'noeditsemiprotected' ]
1140			],
1141			'sysop' => [
1142				[ '', 'autoconfirmed', 'sysop' ],
1143				NS_TALK,
1144				[ 'sysop' ]
1145			],
1146			'sysop with autoconfirmed revoked (a bit silly)' => [
1147				[ '', 'sysop' ],
1148				NS_TALK,
1149				[ 'sysop', 'noeditsemiprotected' ]
1150			],
1151		];
1152	}
1153
1154	/**
1155	 * @dataProvider provideGetRestrictionLevels
1156	 * @covers \MediaWiki\Permissions\PermissionManager::getNamespaceRestrictionLevels
1157	 */
1158	public function testGetRestrictionLevels( array $expected, $ns, array $userGroups = null ) {
1159		$this->setMwGlobals( [
1160			'wgGroupPermissions' => [
1161				'*' => [ 'edit' => true ],
1162				'autoconfirmed' => [ 'editsemiprotected' => true ],
1163				'sysop' => [
1164					'editsemiprotected' => true,
1165					'editprotected' => true,
1166				],
1167				'privileged' => [ 'privileged' => true ],
1168			],
1169			'wgRevokePermissions' => [
1170				'noeditsemiprotected' => [ 'editsemiprotected' => true ],
1171			],
1172			'wgNamespaceProtection' => [
1173				NS_MAIN => 'autoconfirmed',
1174				NS_USER => 'sysop',
1175				101 => [ 'editsemiprotected', 'privileged' ],
1176			],
1177			'wgRestrictionLevels' => [ '', 'autoconfirmed', 'sysop' ],
1178			'wgAutopromote' => []
1179		] );
1180		$user = $userGroups === null ? null : $this->getTestUser( $userGroups )->getUser();
1181		$this->assertSame( $expected, MediaWikiServices::getInstance()
1182			->getPermissionManager()
1183			->getNamespaceRestrictionLevels( $ns, $user ) );
1184	}
1185
1186	/**
1187	 * @covers \MediaWiki\Permissions\PermissionManager::getAllPermissions
1188	 */
1189	public function testGetAllPermissions() {
1190		$this->setMwGlobals( [
1191			'wgAvailableRights' => [ 'test_right' ]
1192		] );
1193		$this->resetServices();
1194		$this->assertContains(
1195			'test_right',
1196			MediaWikiServices::getInstance()
1197				->getPermissionManager()
1198				->getAllPermissions()
1199		);
1200	}
1201
1202	/**
1203	 * @covers \MediaWiki\Permissions\PermissionManager::getRightsCacheKey
1204	 * @throws \Exception
1205	 */
1206	public function testAnonPermissionsNotClash() {
1207		$user1 = User::newFromName( 'User1' );
1208		$user2 = User::newFromName( 'User2' );
1209		$pm = MediaWikiServices::getInstance()->getPermissionManager();
1210		$pm->overrideUserRightsForTesting( $user2, [] );
1211		$this->assertNotSame( $pm->getUserPermissions( $user1 ), $pm->getUserPermissions( $user2 ) );
1212	}
1213
1214	/**
1215	 * @covers \MediaWiki\Permissions\PermissionManager::getRightsCacheKey
1216	 */
1217	public function testAnonPermissionsNotClashOneRegistered() {
1218		$user1 = User::newFromName( 'User1' );
1219		$user2 = $this->getTestSysop()->getUser();
1220		$pm = MediaWikiServices::getInstance()->getPermissionManager();
1221		$this->assertNotSame( $pm->getUserPermissions( $user1 ), $pm->getUserPermissions( $user2 ) );
1222	}
1223
1224	/**
1225	 * Test delete-redirect checks for Special:MovePage
1226	 */
1227	public function testDeleteRedirect() {
1228		$this->editPage( 'ExistentRedirect3', '#REDIRECT [[Existent]]' );
1229		$page = Title::newFromText( 'ExistentRedirect3' );
1230		$pm = MediaWikiServices::getInstance()->getPermissionManager();
1231
1232		$user = $this->getMockBuilder( User::class )
1233			->setMethods( [ 'getEffectiveGroups' ] )
1234			->getMock();
1235		$user->method( 'getEffectiveGroups' )->willReturn( [ '*', 'user' ] );
1236
1237		$this->assertFalse( $pm->quickUserCan( 'delete-redirect', $user, $page ) );
1238
1239		$pm->overrideUserRightsForTesting( $user, 'delete-redirect' );
1240
1241		$this->assertTrue( $pm->quickUserCan( 'delete-redirect', $user, $page ) );
1242		$this->assertArrayEquals( [], $pm->getPermissionErrors( 'delete-redirect', $user, $page ) );
1243	}
1244
1245	/**
1246	 * Enuser normal admins can view deleted javascript, but not restore it
1247	 * See T202989
1248	 */
1249	public function testSysopInterfaceAdminRights() {
1250		$interfaceAdmin = $this->getTestUser( [ 'interface-admin', 'sysop' ] )->getUser();
1251		$admin = $this->getTestSysop()->getUser();
1252
1253		$permManager = MediaWikiServices::getInstance()->getPermissionManager();
1254		$userJs = Title::newFromText( 'Example/common.js', NS_USER );
1255
1256		$this->assertTrue( $permManager->userCan( 'delete', $admin, $userJs ) );
1257		$this->assertTrue( $permManager->userCan( 'delete', $interfaceAdmin, $userJs ) );
1258		$this->assertTrue( $permManager->userCan( 'deletedhistory', $admin, $userJs ) );
1259		$this->assertTrue( $permManager->userCan( 'deletedhistory', $interfaceAdmin, $userJs ) );
1260		$this->assertTrue( $permManager->userCan( 'deletedtext', $admin, $userJs ) );
1261		$this->assertTrue( $permManager->userCan( 'deletedtext', $interfaceAdmin, $userJs ) );
1262		$this->assertFalse( $permManager->userCan( 'undelete', $admin, $userJs ) );
1263		$this->assertTrue( $permManager->userCan( 'undelete', $interfaceAdmin, $userJs ) );
1264	}
1265}
1266