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