1<?php
2
3use MediaWiki\Block\BlockRestrictionStore;
4use MediaWiki\Block\DatabaseBlock;
5use MediaWiki\Block\Restriction\ActionRestriction;
6use MediaWiki\Block\Restriction\NamespaceRestriction;
7use MediaWiki\Block\Restriction\PageRestriction;
8use MediaWiki\MediaWikiServices;
9use Wikimedia\Rdbms\LoadBalancer;
10use Wikimedia\TestingAccessWrapper;
11
12/**
13 * @group Blocking
14 * @group Database
15 * @coversDefaultClass SpecialBlock
16 */
17class SpecialBlockTest extends SpecialPageTestBase {
18	/**
19	 * @inheritDoc
20	 */
21	protected function newSpecialPage() {
22		$services = MediaWikiServices::getInstance();
23		return new SpecialBlock(
24			$services->getBlockUtils(),
25			$services->getBlockPermissionCheckerFactory(),
26			$services->getBlockUserFactory(),
27			$services->getUserNameUtils(),
28			$services->getUserNamePrefixSearch(),
29			$services->getBlockActionInfo(),
30			$services->getTitleFormatter(),
31			$services->getNamespaceInfo()
32		);
33	}
34
35	protected function tearDown(): void {
36		$this->resetTables();
37		parent::tearDown();
38	}
39
40	/**
41	 * @covers ::getFormFields()
42	 */
43	public function testGetFormFields() {
44		$this->setMwGlobals( [
45			'wgBlockAllowsUTEdit' => true,
46			'wgEnablePartialActionBlocks' => true,
47		] );
48		$page = $this->newSpecialPage();
49		$wrappedPage = TestingAccessWrapper::newFromObject( $page );
50		$fields = $wrappedPage->getFormFields();
51		$this->assertIsArray( $fields );
52		$this->assertArrayHasKey( 'Target', $fields );
53		$this->assertArrayHasKey( 'Expiry', $fields );
54		$this->assertArrayHasKey( 'Reason', $fields );
55		$this->assertArrayHasKey( 'CreateAccount', $fields );
56		$this->assertArrayHasKey( 'DisableUTEdit', $fields );
57		$this->assertArrayHasKey( 'AutoBlock', $fields );
58		$this->assertArrayHasKey( 'HardBlock', $fields );
59		$this->assertArrayHasKey( 'PreviousTarget', $fields );
60		$this->assertArrayHasKey( 'Confirm', $fields );
61		$this->assertArrayHasKey( 'EditingRestriction', $fields );
62		$this->assertArrayHasKey( 'PageRestrictions', $fields );
63		$this->assertArrayHasKey( 'NamespaceRestrictions', $fields );
64		$this->assertArrayHasKey( 'ActionRestrictions', $fields );
65	}
66
67	/**
68	 * @covers ::getFormFields()
69	 */
70	public function testGetFormFieldsActionRestrictionDisabled() {
71		$this->setMwGlobals( [
72			'wgEnablePartialActionBlocks' => false,
73		] );
74		$page = $this->newSpecialPage();
75		$wrappedPage = TestingAccessWrapper::newFromObject( $page );
76		$fields = $wrappedPage->getFormFields();
77		$this->assertArrayNotHasKey( 'ActionRestrictions', $fields );
78	}
79
80	/**
81	 * @covers ::maybeAlterFormDefaults()
82	 */
83	public function testMaybeAlterFormDefaults() {
84		$this->setMwGlobals( [
85			'wgBlockAllowsUTEdit' => true,
86		] );
87
88		$block = $this->insertBlock();
89
90		// Refresh the block from the database.
91		$block = DatabaseBlock::newFromTarget( $block->getTargetUserIdentity() );
92
93		$page = $this->newSpecialPage();
94
95		$wrappedPage = TestingAccessWrapper::newFromObject( $page );
96		$wrappedPage->target = $block->getTargetUserIdentity();
97		$fields = $wrappedPage->getFormFields();
98
99		$this->assertSame( $block->getTargetName(), $fields['Target']['default'] );
100		$this->assertSame( $block->isHardblock(), $fields['HardBlock']['default'] );
101		$this->assertSame( $block->isCreateAccountBlocked(), $fields['CreateAccount']['default'] );
102		$this->assertSame( $block->isAutoblocking(), $fields['AutoBlock']['default'] );
103		$this->assertSame( !$block->isUsertalkEditAllowed(), $fields['DisableUTEdit']['default'] );
104		$this->assertSame( $block->getReasonComment()->text, $fields['Reason']['default'] );
105		$this->assertSame( 'infinite', $fields['Expiry']['default'] );
106	}
107
108	/**
109	 * @covers ::maybeAlterFormDefaults()
110	 */
111	public function testMaybeAlterFormDefaultsPartial() {
112		$this->setMwGlobals( [
113			'wgEnablePartialActionBlocks' => true,
114		] );
115		$badActor = $this->getTestUser()->getUser();
116		$sysop = $this->getTestSysop()->getUser();
117		$pageSaturn = $this->getExistingTestPage( 'Saturn' );
118		$pageMars = $this->getExistingTestPage( 'Mars' );
119		$actionId = 100;
120
121		$block = new DatabaseBlock( [
122			'address' => $badActor->getName(),
123			'user' => $badActor->getId(),
124			'by' => $sysop,
125			'expiry' => 'infinity',
126			'sitewide' => 0,
127			'enableAutoblock' => true,
128		] );
129
130		$block->setRestrictions( [
131			new PageRestriction( 0, $pageSaturn->getId() ),
132			new PageRestriction( 0, $pageMars->getId() ),
133			new NamespaceRestriction( 0, NS_TALK ),
134			// Deleted page.
135			new PageRestriction( 0, 999999 ),
136			new ActionRestriction( 0, $actionId ),
137		] );
138
139		MediaWikiServices::getInstance()->getDatabaseBlockStore()->insertBlock( $block );
140
141		// Refresh the block from the database.
142		$block = DatabaseBlock::newFromTarget( $block->getTargetUserIdentity() );
143
144		$page = $this->newSpecialPage();
145
146		$wrappedPage = TestingAccessWrapper::newFromObject( $page );
147		$wrappedPage->target = $block->getTargetUserIdentity();
148		$fields = $wrappedPage->getFormFields();
149
150		$titles = [
151			$pageMars->getTitle()->getPrefixedText(),
152			$pageSaturn->getTitle()->getPrefixedText(),
153		];
154
155		$this->assertSame( $block->getTargetName(), $fields['Target']['default'] );
156		$this->assertSame( 'partial', $fields['EditingRestriction']['default'] );
157		$this->assertSame( implode( "\n", $titles ), $fields['PageRestrictions']['default'] );
158		$this->assertSame( [ $actionId ], $fields['ActionRestrictions']['default'] );
159	}
160
161	/**
162	 * @covers ::processForm()
163	 */
164	public function testProcessForm() {
165		$badActor = $this->getTestUser()->getUserIdentity();
166		$context = RequestContext::getMain();
167		$context->setUser( $this->getTestSysop()->getUser() );
168
169		$page = $this->newSpecialPage();
170		$reason = 'test';
171		$expiry = 'infinity';
172		$data = [
173			'Target' => (string)$badActor,
174			'Expiry' => 'infinity',
175			'Reason' => [
176				$reason,
177			],
178			'Confirm' => '1',
179			'CreateAccount' => '0',
180			'DisableUTEdit' => '0',
181			'DisableEmail' => '0',
182			'HardBlock' => '0',
183			'AutoBlock' => '1',
184			'HideUser' => '0',
185			'Watch' => '0',
186		];
187		$result = $page->processForm( $data, $context );
188
189		$this->assertTrue( $result );
190
191		$block = DatabaseBlock::newFromTarget( $badActor );
192		$this->assertSame( $reason, $block->getReasonComment()->text );
193		$this->assertSame( $expiry, $block->getExpiry() );
194	}
195
196	/**
197	 * @covers ::processForm()
198	 */
199	public function testProcessFormExisting() {
200		$badActor = $this->getTestUser()->getUser();
201		$sysop = $this->getTestSysop()->getUser();
202		$context = RequestContext::getMain();
203		$context->setUser( $sysop );
204
205		// Create a block that will be updated.
206		$block = new DatabaseBlock( [
207			'address' => $badActor->getName(),
208			'user' => $badActor->getId(),
209			'by' => $sysop,
210			'expiry' => 'infinity',
211			'sitewide' => 0,
212			'enableAutoblock' => false,
213		] );
214		MediaWikiServices::getInstance()->getDatabaseBlockStore()->insertBlock( $block );
215
216		$page = $this->newSpecialPage();
217		$reason = 'test';
218		$expiry = 'infinity';
219		$data = [
220			'Target' => (string)$badActor,
221			'Expiry' => 'infinity',
222			'Reason' => [
223				$reason,
224			],
225			'Confirm' => '1',
226			'CreateAccount' => '0',
227			'DisableUTEdit' => '0',
228			'DisableEmail' => '0',
229			'HardBlock' => '0',
230			'AutoBlock' => '1',
231			'HideUser' => '0',
232			'Watch' => '0',
233		];
234		$result = $page->processForm( $data, $context );
235
236		$this->assertTrue( $result );
237
238		$block = DatabaseBlock::newFromTarget( $badActor );
239		$this->assertSame( $reason, $block->getReasonComment()->text );
240		$this->assertSame( $expiry, $block->getExpiry() );
241		$this->assertTrue( $block->isAutoblocking() );
242	}
243
244	/**
245	 * @covers ::processForm()
246	 */
247	public function testProcessFormRestrictions() {
248		$this->setMwGlobals( [
249			'wgEnablePartialActionBlocks' => true,
250		] );
251
252		$badActor = $this->getTestUser()->getUser();
253		$context = RequestContext::getMain();
254		$context->setUser( $this->getTestSysop()->getUser() );
255
256		$pageSaturn = $this->getExistingTestPage( 'Saturn' );
257		$pageMars = $this->getExistingTestPage( 'Mars' );
258		$actionId = 100;
259
260		$titles = [
261			$pageSaturn->getTitle()->getText(),
262			$pageMars->getTitle()->getText(),
263		];
264
265		$page = $this->newSpecialPage();
266		$reason = 'test';
267		$expiry = 'infinity';
268		$data = [
269			'Target' => (string)$badActor,
270			'Expiry' => 'infinity',
271			'Reason' => [
272				$reason,
273			],
274			'Confirm' => '1',
275			'CreateAccount' => '0',
276			'DisableUTEdit' => '0',
277			'DisableEmail' => '0',
278			'HardBlock' => '0',
279			'AutoBlock' => '1',
280			'HideUser' => '0',
281			'Watch' => '0',
282			'EditingRestriction' => 'partial',
283			'PageRestrictions' => implode( "\n", $titles ),
284			'NamespaceRestrictions' => '',
285			'ActionRestrictions' => [ $actionId ],
286		];
287		$result = $page->processForm( $data, $context );
288
289		$this->assertTrue( $result );
290
291		$block = DatabaseBlock::newFromTarget( $badActor );
292		$this->assertSame( $reason, $block->getReasonComment()->text );
293		$this->assertSame( $expiry, $block->getExpiry() );
294		$this->assertCount( 3, $block->getRestrictions() );
295		$this->assertTrue( $this->getBlockRestrictionStore()->equals( $block->getRestrictions(), [
296			new PageRestriction( $block->getId(), $pageMars->getId() ),
297			new PageRestriction( $block->getId(), $pageSaturn->getId() ),
298			new ActionRestriction( $block->getId(), $actionId ),
299		] ) );
300	}
301
302	/**
303	 * @covers ::processForm()
304	 */
305	public function testProcessFormRestrictionsChange() {
306		$badActor = $this->getTestUser()->getUser();
307		$context = RequestContext::getMain();
308		$context->setUser( $this->getTestSysop()->getUser() );
309
310		$pageSaturn = $this->getExistingTestPage( 'Saturn' );
311		$pageMars = $this->getExistingTestPage( 'Mars' );
312
313		$titles = [
314			$pageSaturn->getTitle()->getText(),
315			$pageMars->getTitle()->getText(),
316		];
317
318		// Create a partial block.
319		$page = $this->newSpecialPage();
320		$reason = 'test';
321		$expiry = 'infinity';
322		$data = [
323			'Target' => (string)$badActor,
324			'Expiry' => 'infinity',
325			'Reason' => [
326				$reason,
327			],
328			'Confirm' => '1',
329			'CreateAccount' => '1',
330			'DisableUTEdit' => '0',
331			'DisableEmail' => '0',
332			'HardBlock' => '0',
333			'AutoBlock' => '1',
334			'HideUser' => '0',
335			'Watch' => '0',
336			'EditingRestriction' => 'partial',
337			'PageRestrictions' => implode( "\n", $titles ),
338			'NamespaceRestrictions' => '',
339		];
340		$result = $page->processForm( $data, $context );
341
342		$this->assertTrue( $result );
343
344		$block = DatabaseBlock::newFromTarget( $badActor );
345		$this->assertSame( $reason, $block->getReasonComment()->text );
346		$this->assertSame( $expiry, $block->getExpiry() );
347		$this->assertFalse( $block->isSitewide() );
348		$this->assertTrue( $block->isCreateAccountBlocked() );
349		$this->assertCount( 2, $block->getRestrictions() );
350		$this->assertTrue( $this->getBlockRestrictionStore()->equals( $block->getRestrictions(), [
351			new PageRestriction( $block->getId(), $pageMars->getId() ),
352			new PageRestriction( $block->getId(), $pageSaturn->getId() ),
353		] ) );
354
355		// Remove a page from the partial block.
356		$data['PageRestrictions'] = $pageMars->getTitle()->getText();
357		$result = $page->processForm( $data, $context );
358
359		$this->assertTrue( $result );
360
361		$block = DatabaseBlock::newFromTarget( $badActor );
362		$this->assertSame( $reason, $block->getReasonComment()->text );
363		$this->assertSame( $expiry, $block->getExpiry() );
364		$this->assertFalse( $block->isSitewide() );
365		$this->assertTrue( $block->isCreateAccountBlocked() );
366		$this->assertCount( 1, $block->getRestrictions() );
367		$this->assertTrue( $this->getBlockRestrictionStore()->equals( $block->getRestrictions(), [
368			new PageRestriction( $block->getId(), $pageMars->getId() ),
369		] ) );
370
371		// Remove the last page from the block.
372		$data['PageRestrictions'] = '';
373		$result = $page->processForm( $data, $context );
374
375		$this->assertTrue( $result );
376
377		$block = DatabaseBlock::newFromTarget( $badActor );
378		$this->assertSame( $reason, $block->getReasonComment()->text );
379		$this->assertSame( $expiry, $block->getExpiry() );
380		$this->assertFalse( $block->isSitewide() );
381		$this->assertTrue( $block->isCreateAccountBlocked() );
382		$this->assertSame( [], $block->getRestrictions() );
383
384		// Change to sitewide.
385		$data['EditingRestriction'] = 'sitewide';
386		$result = $page->processForm( $data, $context );
387
388		$this->assertTrue( $result );
389
390		$block = DatabaseBlock::newFromTarget( $badActor );
391		$this->assertSame( $reason, $block->getReasonComment()->text );
392		$this->assertSame( $expiry, $block->getExpiry() );
393		$this->assertTrue( $block->isSitewide() );
394		$this->assertSame( [], $block->getRestrictions() );
395
396		// Ensure that there are no restrictions where the blockId is 0.
397		$count = $this->db->selectRowCount(
398			'ipblocks_restrictions',
399			'*',
400			[ 'ir_ipb_id' => 0 ],
401			__METHOD__
402		);
403		$this->assertSame( 0, $count );
404	}
405
406	/**
407	 * @dataProvider provideProcessFormUserTalkEditFlag
408	 * @covers ::processForm()
409	 */
410	public function testProcessFormUserTalkEditFlag( $options, $expected ) {
411		$this->setMwGlobals( [
412			'wgBlockAllowsUTEdit' => $options['configAllowsUserTalkEdit'],
413		] );
414
415		$performer = $this->getTestSysop()->getUser();
416		$target = $this->getTestUser()->getUser();
417
418		$context = new DerivativeContext( RequestContext::getMain() );
419		$context->setUser( $performer );
420
421		$data = [
422			'Target' => $target,
423			'PreviousTarget' => $target,
424			'Expiry' => 'infinity',
425			'CreateAccount' => '1',
426			'DisableEmail' => '0',
427			'HardBlock' => '0',
428			'AutoBlock' => '0',
429			'Watch' => '0',
430			'Confirm' => '1',
431			'DisableUTEdit' => $options['optionBlocksUserTalkEdit'],
432		];
433
434		if ( !$options['userTalkNamespaceBlocked'] ) {
435			$data['EditingRestriction'] = 'partial';
436			$data['PageRestrictions'] = '';
437			$data['NamespaceRestrictions'] = '';
438		}
439
440		$result = $this->newSpecialPage()->processForm(
441			$data,
442			$context
443		);
444
445		if ( $expected === 'ipb-prevent-user-talk-edit' ) {
446			$this->assertSame( $expected, $result->getErrorsArray()[0][0] );
447		} else {
448			$block = DatabaseBlock::newFromTarget( $target );
449			$this->assertSame( $expected, $block->isUsertalkEditAllowed() );
450		}
451	}
452
453	/**
454	 * Test cases for whether own user talk edit is allowed, with different combinations of:
455	 * - whether user talk namespace blocked
456	 * - config BlockAllowsUTEdit true/false
457	 * - block option specifying whether to block own user talk edit
458	 * For more about the desired behaviour, see T252892.
459	 *
460	 * @return array
461	 */
462	public function provideProcessFormUserTalkEditFlag() {
463		return [
464			'Always allowed if user talk namespace not blocked' => [
465				[
466					'userTalkNamespaceBlocked' => false,
467					'configAllowsUserTalkEdit' => true,
468					'optionBlocksUserTalkEdit' => false,
469				],
470				true,
471			],
472			'Always allowed if user talk namespace not blocked (config is false)' => [
473				[
474					'userTalkNamespaceBlocked' => false,
475					'configAllowsUserTalkEdit' => false,
476					'optionBlocksUserTalkEdit' => false,
477				],
478				true,
479			],
480			'Error if user talk namespace not blocked, but option blocks user talk edit' => [
481				[
482					'userTalkNamespaceBlocked' => false,
483					'configAllowsUserTalkEdit' => true,
484					'optionBlocksUserTalkEdit' => true,
485				],
486				'ipb-prevent-user-talk-edit',
487			],
488			'Always blocked if user talk namespace blocked and wgBlockAllowsUTEdit is false' => [
489				[
490					'userTalkNamespaceBlocked' => true,
491					'configAllowsUserTalkEdit' => false,
492					'optionBlocksUserTalkEdit' => false,
493				],
494				false,
495			],
496			'Option used if user talk namespace blocked and config is true (blocked)' => [
497				[
498					'userTalkNamespaceBlocked' => true,
499					'configAllowsUserTalkEdit' => true,
500					'optionBlocksUserTalkEdit' => true,
501				],
502				false,
503			],
504			'Option used if user talk namespace blocked and config is true (not blocked)' => [
505				[
506					'userTalkNamespaceBlocked' => true,
507					'configAllowsUserTalkEdit' => true,
508					'optionBlocksUserTalkEdit' => false,
509				],
510				true,
511			],
512		];
513	}
514
515	/**
516	 * @dataProvider provideProcessFormErrors
517	 * @covers ::processForm()
518	 */
519	public function testProcessFormErrors( $data, $expected, $options = [] ) {
520		$this->setMwGlobals( [
521			'wgBlockAllowsUTEdit' => true,
522		] );
523
524		$performer = $this->getTestSysop()->getUser();
525		$target = !empty( $options['blockingSelf'] ) ? $performer : '1.2.3.4';
526		$defaultData = [
527			'Target' => $target,
528			'PreviousTarget' => $target,
529			'Expiry' => 'infinity',
530			'CreateAccount' => '0',
531			'DisableUTEdit' => '0',
532			'DisableEmail' => '0',
533			'HardBlock' => '0',
534			'AutoBlock' => '0',
535			'Confirm' => '0',
536			'Watch' => '0',
537		];
538
539		$context = new DerivativeContext( RequestContext::getMain() );
540		$context->setUser( $performer );
541
542		$result = $this->newSpecialPage()->processForm(
543			array_merge( $defaultData, $data ),
544			$context
545		);
546
547		if ( $result instanceof Status ) {
548			$result = $result->getErrorsArray();
549		}
550		$error = is_array( $result[0] ) ? $result[0][0] : $result[0];
551		$this->assertEquals( $expected, $error );
552	}
553
554	public function provideProcessFormErrors() {
555		return [
556			'Invalid expiry' => [
557				[
558					'Expiry' => 'invalid',
559				],
560				'ipb_expiry_invalid',
561			],
562			'Expiry is in the past' => [
563				[
564					'Expiry' => 'yesterday',
565				],
566				'ipb_expiry_old',
567			],
568			'Bad ip address' => [
569				[
570					'Target' => '1.2.3.4/1234',
571				],
572				'badipaddress',
573			],
574			'Edit user talk page invalid with no restrictions' => [
575				[
576					'EditingRestriction' => 'partial',
577					'DisableUTEdit' => '1',
578					'PageRestrictions' => '',
579					'NamespaceRestrictions' => '',
580				],
581				'ipb-prevent-user-talk-edit',
582			],
583			'Edit user talk page invalid with namespace restriction !== NS_USER_TALK ' => [
584				[
585					'EditingRestriction' => 'partial',
586					'DisableUTEdit' => '1',
587					'PageRestrictions' => '',
588					'NamespaceRestrictions' => NS_USER,
589				],
590				'ipb-prevent-user-talk-edit',
591			],
592			'Blocking self and target changed' => [
593				[
594					'PreviousTarget' => 'other',
595					'Confirm' => '1',
596				],
597				'ipb-blockingself',
598				[
599					'blockingSelf' => true,
600				],
601			],
602			'Blocking self and no confirm' => [
603				[],
604				'ipb-blockingself',
605				[
606					'blockingSelf' => true,
607				],
608			],
609			'Empty expiry' => [
610				[
611					'Expiry' => '',
612				],
613				'ipb_expiry_invalid',
614			],
615			'Expiry valid but longer than 50 chars' => [
616				[
617					'Expiry' => '30th September 9999 19:19:19.532453 Europe/Amsterdam',
618				],
619				'ipb_expiry_invalid',
620			],
621		];
622	}
623
624	/**
625	 * @dataProvider provideProcessFormErrorsReblock
626	 * @covers ::processForm()
627	 */
628	public function testProcessFormErrorsReblock( $data, $permissions, $expected ) {
629		$this->setMwGlobals( [
630			'wgBlockAllowsUTEdit' => true,
631		] );
632
633		$performer = $this->getTestSysop()->getUser();
634		$this->overrideUserPermissions( $performer, $permissions );
635		$blockedUser = $this->getTestUser()->getUser();
636
637		$block = new DatabaseBlock( [
638			'address' => $blockedUser,
639			'by' => $performer,
640			'hideName' => true,
641		] );
642		MediaWikiServices::getInstance()->getDatabaseBlockStore()->insertBlock( $block );
643
644		// Matches the existing block
645		$defaultData = [
646			'Target' => $blockedUser->getName(),
647			'PreviousTarget' => $blockedUser->getName(),
648			'Expiry' => 'infinity',
649			'DisableUTEdit' => '1',
650			'CreateAccount' => '0',
651			'DisableEmail' => '0',
652			'HardBlock' => '0',
653			'AutoBlock' => '0',
654			'HideUser' => '1',
655			'Confirm' => '1',
656			'Watch' => '0',
657		];
658
659		$context = new DerivativeContext( RequestContext::getMain() );
660		$context->setUser( $performer );
661
662		$result = $this->newSpecialPage()->processForm(
663			array_merge( $defaultData, $data ),
664			$context
665		);
666
667		if ( $result instanceof Status ) {
668			$result = $result->getErrorsArray();
669		}
670		$error = is_array( $result[0] ) ? $result[0][0] : $result[0];
671		$this->assertEquals( $expected, $error );
672	}
673
674	public function provideProcessFormErrorsReblock() {
675		return [
676			'Reblock user with Confirm false' => [
677				[
678					// Avoid error for hiding user with confirm false
679					'HideUser' => '0',
680					'Confirm' => '0',
681				],
682				[ 'block', 'hideuser' ],
683				'ipb_already_blocked',
684			],
685			'Reblock user with Reblock false' => [
686				[ 'Reblock' => '0' ],
687				[ 'block', 'hideuser' ],
688				'ipb_already_blocked',
689			],
690			'Reblock with confirm True but target has changed' => [
691				[ 'PreviousTarget' => '1.2.3.4' ],
692				[ 'block', 'hideuser' ],
693				'ipb_already_blocked',
694			],
695			'Reblock with same block' => [
696				[ 'HideUser' => '1' ],
697				[ 'block', 'hideuser' ],
698				'ipb_already_blocked',
699			],
700			'Reblock hidden user with wrong permissions' => [
701				[ 'HideUser' => '0' ],
702				[ 'block', 'hideuser' => false ],
703				'cant-see-hidden-user',
704			],
705		];
706	}
707
708	/**
709	 * @dataProvider provideProcessFormErrorsHideUser
710	 * @covers ::processForm()
711	 */
712	public function testProcessFormErrorsHideUser( $data, $permissions, $expected ) {
713		$performer = $this->getTestSysop()->getUser();
714		$this->overrideUserPermissions( $performer, array_merge( $permissions, [ 'block' ] ) );
715
716		$defaultData = [
717			'Target' => $this->getTestUser()->getUser(),
718			'HideUser' => '1',
719			'Expiry' => 'infinity',
720			'Confirm' => '1',
721			'CreateAccount' => '0',
722			'DisableUTEdit' => '0',
723			'DisableEmail' => '0',
724			'HardBlock' => '0',
725			'AutoBlock' => '0',
726			'Watch' => '0',
727		];
728
729		$context = new DerivativeContext( RequestContext::getMain() );
730		$context->setUser( $performer );
731
732		$result = $this->newSpecialPage()->processForm(
733			array_merge( $defaultData, $data ),
734			$context
735		);
736
737		if ( $result instanceof Status ) {
738			$result = $result->getErrorsArray();
739		}
740		$error = is_array( $result[0] ) ? $result[0][0] : $result[0];
741		$this->assertEquals( $expected, $error );
742	}
743
744	public function provideProcessFormErrorsHideUser() {
745		return [
746			'HideUser with wrong permissions' => [
747				[],
748				[ 'hideuser' => '0' ],
749				'badaccess-group0',
750			],
751			'Hideuser with partial block' => [
752				[ 'EditingRestriction' => 'partial' ],
753				[ 'hideuser' ],
754				'ipb_hide_partial',
755			],
756			'Hideuser with finite expiry' => [
757				[ 'Expiry' => '1 hour' ],
758				[ 'hideuser' ],
759				'ipb_expiry_temp',
760			],
761			'Hideuser with no confirm' => [
762				[ 'Confirm' => '0' ],
763				[ 'hideuser' ],
764				'ipb-confirmhideuser',
765			],
766		];
767	}
768
769	/**
770	 * @covers ::processForm()
771	 */
772	public function testProcessFormErrorsHideUserProlific() {
773		$this->setMwGlobals( [ 'wgHideUserContribLimit' => 0 ] );
774
775		$performer = $this->getTestSysop()->getUser();
776		$this->overrideUserPermissions( $performer, [ 'block', 'hideuser' ] );
777
778		$userToBlock = $this->getTestUser()->getUser();
779		$pageSaturn = $this->getExistingTestPage( 'Saturn' );
780		$pageSaturn->doUserEditContent(
781			ContentHandler::makeContent( 'content', $pageSaturn->getTitle() ),
782			$userToBlock,
783			'summary'
784		);
785
786		$context = new DerivativeContext( RequestContext::getMain() );
787		$context->setUser( $performer );
788
789		$result = $this->newSpecialPage()->processForm(
790			[
791				'Target' => $userToBlock,
792				'CreateAccount' => '1',
793				'HideUser' => '1',
794				'Expiry' => 'infinity',
795				'Confirm' => '1',
796				'DisableUTEdit' => '0',
797				'DisableEmail' => '0',
798				'HardBlock' => '0',
799				'AutoBlock' => '0',
800				'Watch' => '0',
801			],
802			$context
803		);
804
805		if ( $result instanceof Status ) {
806			$result = $result->getErrorsArray();
807		}
808		$error = is_array( $result[0] ) ? $result[0][0] : $result[0];
809		$this->assertEquals( 'ipb_hide_invalid', $error );
810	}
811
812	/**
813	 * TODO: Move to BlockPermissionCheckerTest
814	 *
815	 * @dataProvider provideCheckUnblockSelf
816	 * @covers ::checkUnblockSelf
817	 */
818	public function testCheckUnblockSelf(
819		$blockedUser,
820		$blockPerformer,
821		$adjustPerformer,
822		$adjustTarget,
823		$sitewide,
824		$expectedResult,
825		$reason
826	) {
827		$this->hideDeprecated( 'SpecialBlock::checkUnblockSelf' );
828
829		$this->setMwGlobals( [
830			'wgBlockDisablesLogin' => false,
831		] );
832		$this->setGroupPermissions( 'sysop', 'unblockself', true );
833		$this->setGroupPermissions( 'user', 'block', true );
834		// Getting errors about creating users in db in provider.
835		// Need to use mutable to ensure different named testusers.
836		$users = [
837			'u1' => $this->getMutableTestUser( 'sysop' )->getUser(),
838			'u2' => $this->getMutableTestUser( 'sysop' )->getUser(),
839			'u3' => $this->getMutableTestUser( 'sysop' )->getUser(),
840			'u4' => $this->getMutableTestUser( 'sysop' )->getUser(),
841			'nonsysop' => $this->getTestUser()->getUser()
842		];
843		foreach ( [ 'blockedUser', 'blockPerformer', 'adjustPerformer', 'adjustTarget' ] as $var ) {
844			$$var = $users[$$var];
845		}
846
847		$block = new DatabaseBlock( [
848			'address' => $blockedUser->getName(),
849			'user' => $blockedUser->getId(),
850			'by' => $blockPerformer,
851			'expiry' => 'infinity',
852			'sitewide' => $sitewide,
853			'enableAutoblock' => true,
854		] );
855
856		MediaWikiServices::getInstance()->getDatabaseBlockStore()->insertBlock( $block );
857
858		$this->assertSame(
859			SpecialBlock::checkUnblockSelf( $adjustTarget, $adjustPerformer ),
860			$expectedResult,
861			$reason
862		);
863	}
864
865	public function provideCheckUnblockSelf() {
866		// 'blockedUser', 'blockPerformer', 'adjustPerformer', 'adjustTarget'
867		return [
868			[ 'u1', 'u2', 'u3', 'u4', 1, true, 'Unrelated users' ],
869			[ 'u1', 'u2', 'u1', 'u4', 1, 'ipbblocked', 'Block unrelated while blocked' ],
870			[ 'u1', 'u2', 'u1', 'u4', 0, true, 'Block unrelated while partial blocked' ],
871			[ 'u1', 'u2', 'u1', 'u1', 1, true, 'Has unblockself' ],
872			[ 'nonsysop', 'u2', 'nonsysop', 'nonsysop', 1, 'ipbnounblockself', 'no unblockself' ],
873			[ 'nonsysop', 'nonsysop', 'nonsysop', 'nonsysop', 1, true,
874				'no unblockself but can de-selfblock'
875			],
876			[ 'u1', 'u2', 'u1', 'u2', 1, true, 'Can block user who blocked' ],
877		];
878	}
879
880	/**
881	 * @dataProvider provideGetTargetAndType
882	 * @covers ::getTargetAndType
883	 */
884	public function testGetTargetAndType( $par, $requestData, $expectedTarget ) {
885		$request = $requestData ? new FauxRequest( $requestData ) : null;
886		$page = $this->newSpecialPage();
887		list( $target, $type ) = $page->getTargetAndType( $par, $request );
888		$this->assertSame( $target, $expectedTarget );
889	}
890
891	public function provideGetTargetAndType() {
892		$invalidTarget = '';
893		return [
894			'Choose \'wpTarget\' parameter first' => [
895				'2.2.2.0/24',
896				[
897					'wpTarget' => '1.1.1.0/24',
898					'ip' => '3.3.3.0/24',
899					'wpBlockAddress' => '4.4.4.0/24',
900				],
901				'1.1.1.0/24',
902			],
903			'Choose subpage parameter second' => [
904				'2.2.2.0/24',
905				[
906					'wpTarget' => $invalidTarget,
907					'ip' => '3.3.3.0/24',
908					'wpBlockAddress' => '4.4.4.0/24',
909				],
910				'2.2.2.0/24',
911			],
912			'Choose \'ip\' parameter third' => [
913				$invalidTarget,
914				[
915					'wpTarget' => $invalidTarget,
916					'ip' => '3.3.3.0/24',
917					'wpBlockAddress' => '4.4.4.0/24',
918				],
919				'3.3.3.0/24',
920			],
921			'Choose \'wpBlockAddress\' parameter fourth' => [
922				$invalidTarget,
923				[
924					'wpTarget' => $invalidTarget,
925					'ip' => $invalidTarget,
926					'wpBlockAddress' => '4.4.4.0/24',
927				],
928				'4.4.4.0/24',
929			],
930			'No web request' => [
931				'2.2.2.0/24',
932				false,
933				'2.2.2.0/24',
934			],
935			'No valid request data or subpage parameter' => [
936				null,
937				[],
938				null,
939			],
940		];
941	}
942
943	protected function insertBlock() {
944		$badActor = $this->getTestUser()->getUser();
945		$sysop = $this->getTestSysop()->getUser();
946
947		$block = new DatabaseBlock( [
948			'address' => $badActor->getName(),
949			'user' => $badActor->getId(),
950			'by' => $sysop,
951			'expiry' => 'infinity',
952			'sitewide' => 1,
953			'enableAutoblock' => true,
954		] );
955
956		MediaWikiServices::getInstance()->getDatabaseBlockStore()->insertBlock( $block );
957
958		return $block;
959	}
960
961	protected function resetTables() {
962		$this->db->delete( 'ipblocks', '*', __METHOD__ );
963		$this->db->delete( 'ipblocks_restrictions', '*', __METHOD__ );
964	}
965
966	/**
967	 * Get a BlockRestrictionStore instance
968	 *
969	 * @return BlockRestrictionStore
970	 */
971	private function getBlockRestrictionStore(): BlockRestrictionStore {
972		$loadBalancer = $this->getMockBuilder( LoadBalancer::class )
973			->disableOriginalConstructor()
974			->getMock();
975
976		return new BlockRestrictionStore( $loadBalancer );
977	}
978}
979