1<?php
2
3use MediaWiki\Block\AbstractBlock;
4use MediaWiki\Block\BlockPermissionChecker;
5use MediaWiki\Block\BlockUtils;
6use MediaWiki\Block\DatabaseBlock;
7use MediaWiki\Config\ServiceOptions;
8use MediaWiki\Permissions\Authority;
9use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
10use MediaWiki\User\UserIdentity;
11use MediaWiki\User\UserIdentityValue;
12use PHPUnit\Framework\MockObject\MockObject;
13
14/**
15 * @group Blocking
16 * @coversDefaultClass \MediaWiki\Block\BlockPermissionChecker
17 * @author DannyS712
18 */
19class BlockPermissionCheckerTest extends MediaWikiUnitTestCase {
20	use MockAuthorityTrait;
21
22	/**
23	 * @param bool $enableUserEmail
24	 * @param array $targetAndType
25	 * @param Authority $performer
26	 * @return BlockPermissionChecker
27	 */
28	private function getBlockPermissionChecker(
29		bool $enableUserEmail,
30		array $targetAndType,
31		Authority $performer
32	) {
33		$options = new ServiceOptions(
34			BlockPermissionChecker::CONSTRUCTOR_OPTIONS,
35			[ 'EnableUserEmail' => $enableUserEmail ]
36		);
37
38		// We don't care about how BlockUtils::parseBlockTarget actually works, just
39		// override with whatever. Only used for a single call in the constructor
40		// for getting target and type
41		$blockUtils = $this->createNoOpMock( BlockUtils::class, [ 'parseBlockTarget' ] );
42		$blockUtils->expects( $this->once() )
43			->method( 'parseBlockTarget' )
44			->willReturn( $targetAndType );
45
46		return new BlockPermissionChecker(
47			$options,
48			$blockUtils,
49			'foo', // input to BlockUtils::parseBlockTarget, not used
50			$performer
51		);
52	}
53
54	/**
55	 * @param bool $isSitewide
56	 * @param string $byName
57	 * @return DatabaseBlock|MockObject
58	 */
59	private function getBlock( bool $isSitewide, string $byName ) {
60		// Mock DatabaseBlock instead of AbstractBlock because its easier
61		$block = $this->createNoOpMock(
62			DatabaseBlock::class,
63			[ 'isSitewide', 'getBlocker' ]
64		);
65		$block->method( 'isSitewide' )->willReturn( $isSitewide );
66		$block->method( 'getBlocker' )->willReturn( new UserIdentityValue( 7, $byName ) );
67		return $block;
68	}
69
70	public function provideCheckBasePermissions() {
71		// $rights, $checkHideuser, $expect
72		yield 'need block' => [ [], false, 'badaccess-group0' ];
73		yield 'block enough for not hiding' => [ [ 'block' ], false, true ];
74		yield 'need hideuser' => [ [ 'block' ], true, 'unblock-hideuser' ];
75		yield 'can hideuser' => [ [ 'block', 'hideuser' ], true, true ];
76	}
77
78	/**
79	 * @covers ::checkBasePermissions
80	 * @dataProvider provideCheckBasePermissions
81	 */
82	public function testCheckBasePermissions( $rights, $checkHideuser, $expect ) {
83		$performer = $this->mockRegisteredAuthorityWithPermissions( $rights );
84		$blockPermissionChecker = $this->getBlockPermissionChecker(
85			true, // $enableUserEmail, irrelevant
86			[ null, null ], // $targetAndType, irrelevant
87			$performer
88		);
89		$this->assertSame(
90			$expect,
91			$blockPermissionChecker->checkBasePermissions( $checkHideuser )
92		);
93	}
94
95	/**
96	 * @covers ::checkBlockPermissions
97	 */
98	public function testNotBlockedPerformer() {
99		// checkBlockPermissions has an early return true if the performer has no block
100		$blockPermissionChecker = $this->getBlockPermissionChecker(
101			true, // $enableUserEmail, irrelevant
102			[ null, null ], // $targetAndType, irrelevant
103			$this->mockRegisteredAuthorityWithoutPermissions( [] )
104		);
105		$this->assertTrue(
106			$blockPermissionChecker->checkBlockPermissions()
107		);
108	}
109
110	/**
111	 * @covers ::checkBlockPermissions
112	 */
113	public function testPartialBlockedPerformer() {
114		// checkBlockPermissions has an early return true if the performer is not sitewide blocked
115		$blocker = new UserIdentityValue( 1, 'blocker', UserIdentity::LOCAL );
116		$performer = $this->mockUserAuthorityWithBlock( $blocker, $this->getBlock( false, '' ) );
117
118		$blockPermissionChecker = $this->getBlockPermissionChecker(
119			true, // $enableUserEmail, irrelevant
120			[ null, null ], // $targetAndType, irrelevant
121			$performer
122		);
123		$this->assertTrue(
124			$blockPermissionChecker->checkBlockPermissions()
125		);
126	}
127
128	public function provideCheckBlockPermissions() {
129		// Blocked admin changing own block
130		yield 'Self blocked' => [ 'blocker', 1, 'blocker', false, true ];
131		yield 'unblockself' => [ 'another admin', 1, 'blocker', true, true ];
132		yield 'ipbnounblockself' => [ 'another admin', 1, 'blocker', false, 'ipbnounblockself' ];
133
134		// Blocked admins can always block the admin who blocked them
135		yield 'Block my blocker' => [ 'another admin', 2, 'another admin', false, true ];
136
137		// Blocked admin blocking a third party
138		yield 'Block someone else' => [ 'another admin', 3, 'someone else', false, 'ipbblocked' ];
139	}
140
141	/**
142	 * @covers ::checkBlockPermissions
143	 * @dataProvider provideCheckBlockPermissions
144	 */
145	public function testCheckBlockPermissions(
146		string $blockedBy,
147		int $targetUserId,
148		string $targetUserName,
149		bool $unblockSelf,
150		$expect
151	) {
152		// cases for the target being a UserIdentity
153		// performing user has id 1 and the name 'blocker'
154		$block = $this->getBlock(
155			true, // sitewide
156			$blockedBy
157		);
158		$rights = $unblockSelf ? [ 'unblockself' ] : [];
159
160		$blocker = new UserIdentityValue( 1, 'blocker', UserIdentity::LOCAL );
161		$performer = $this->mockUserAuthorityWithBlock( $blocker, $block, $rights );
162
163		$target = new UserIdentityValue( $targetUserId, $targetUserName );
164
165		$blockPermissionChecker = $this->getBlockPermissionChecker(
166			true, // $enableUserEmail, irrelevant
167			[ $target, AbstractBlock::TYPE_USER ], // $targetAndType, irrelevant
168			$performer
169		);
170		$this->assertSame(
171			$expect,
172			$blockPermissionChecker->checkBlockPermissions()
173		);
174	}
175
176	public function provideCheckEmailPermissions() {
177		// $enableEmail, $rights, $expect
178		yield 'Email not enabled, without permissions' => [ false, [], false ];
179		yield 'Email not enabled, with permissions' => [ false, [ 'blockemail' ], false ];
180		yield 'Email enabled, without permissions' => [ true, [], false ];
181		yield 'Email enabled, with permissions' => [ true, [ 'blockemail' ], true ];
182	}
183
184	/**
185	 * @covers ::checkEmailPermissions
186	 * @dataProvider provideCheckEmailPermissions
187	 */
188	public function testCheckEmailPermissionOkay( $enableEmail, $rights, $expect ) {
189		$performer = $this->mockRegisteredAuthorityWithPermissions( $rights );
190		$blockPermissionChecker = $this->getBlockPermissionChecker(
191			$enableEmail,
192			[ null, null ], // $targetAndType, irrelevant
193			$performer
194		);
195		$this->assertSame( $expect, $blockPermissionChecker->checkEmailPermissions() );
196	}
197}
198