diff --git a/includes/Hooks.php b/includes/Hooks.php index 436d7e0..c13430a 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -84,6 +84,7 @@ public function onMediaWikiPerformAction( /** * Block Special:RecentChangesLinked and Special:WhatLinksHere for anonymous users. + * Also blocks any special page access with ?oldid parameter. * * @param SpecialPage $special * @param string|null $subPage @@ -103,6 +104,15 @@ public function onSpecialPageBeforeExecute( $special, $subPage ) { return false; } + // Also block if oldid parameter is present + $request = $special->getContext()->getRequest(); + $oldId = (int)$request->getVal( 'oldid' ); + if ( $oldId > 0 ) { + $out = $special->getContext()->getOutput(); + $this->denyAccess( $out ); + return false; + } + return true; } diff --git a/tests/phpunit/unit/HooksTest.php b/tests/phpunit/unit/HooksTest.php index eec20b8..c68f922 100644 --- a/tests/phpunit/unit/HooksTest.php +++ b/tests/phpunit/unit/HooksTest.php @@ -27,6 +27,12 @@ class HooksTest extends TestCase { /** @var string */ private static string $webRequestClassName; + /** @var string */ + private static string $specialPageClassName; + + /** @var string */ + private static string $contextClassName; + public static function setUpBeforeClass(): void { self::$actionEntryPointClassName = class_exists( '\MediaWiki\Actions\ActionEntryPoint' ) ? '\MediaWiki\Actions\ActionEntryPoint' @@ -51,6 +57,14 @@ public static function setUpBeforeClass(): void { self::$webRequestClassName = class_exists( '\MediaWiki\Request\WebRequest' ) ? '\MediaWiki\Request\WebRequest' : '\WebRequest'; + + self::$specialPageClassName = class_exists( '\MediaWiki\SpecialPage\SpecialPage' ) + ? '\MediaWiki\SpecialPage\SpecialPage' + : '\SpecialPage'; + + self::$contextClassName = class_exists( '\MediaWiki\Context\RequestContext' ) + ? '\MediaWiki\Context\RequestContext' + : '\RequestContext'; } /** @@ -133,4 +147,150 @@ public function testNonRevisionTypeAlwaysAllowed() { $result = $runner->onMediaWikiPerformAction( $output, $article, $title, $user, $request, $wiki ); $this->assertTrue( $result ); } + + /** + * @covers ::onMediaWikiPerformAction + */ + public function testOldIdBlocksAnonymous() { + $output = $this->createMock( self::$outputPageClassName ); + + $request = $this->createMock( self::$webRequestClassName ); + $request->method( 'getVal' )->willReturnMap( [ + [ 'oldid', null, '1234' ], + ] ); + + $user = $this->createMock( self::$userClassName ); + $user->method( 'isRegistered' )->willReturn( false ); + + $article = $this->createMock( self::$articleClassName ); + $title = $this->createMock( self::$titleClassName ); + $wiki = $this->createMock( self::$actionEntryPointClassName ); + + $runner = $this->getMockBuilder( Hooks::class ) + ->onlyMethods( [ 'denyAccess' ] ) + ->getMock(); + $runner->expects( $this->once() )->method( 'denyAccess' ); + + $result = $runner->onMediaWikiPerformAction( $output, $article, $title, $user, $request, $wiki ); + $this->assertFalse( $result ); + } + + /** + * @covers ::onMediaWikiPerformAction + */ + public function testOldIdAllowsLoggedIn() { + $output = $this->createMock( self::$outputPageClassName ); + + $request = $this->createMock( self::$webRequestClassName ); + $request->method( 'getVal' )->willReturnMap( [ + [ 'oldid', null, '1234' ], + ] ); + + $user = $this->createMock( self::$userClassName ); + $user->method( 'isRegistered' )->willReturn( true ); + + $article = $this->createMock( self::$articleClassName ); + $title = $this->createMock( self::$titleClassName ); + $wiki = $this->createMock( self::$actionEntryPointClassName ); + + $runner = $this->getMockBuilder( Hooks::class ) + ->onlyMethods( [ 'denyAccess' ] ) + ->getMock(); + $runner->expects( $this->never() )->method( 'denyAccess' ); + + $result = $runner->onMediaWikiPerformAction( $output, $article, $title, $user, $request, $wiki ); + $this->assertTrue( $result ); + } + + /** + * @covers ::onSpecialPageBeforeExecute + */ + public function testSpecialPageWithOldIdBlocksAnonymous() { + $output = $this->createMock( self::$outputPageClassName ); + + $request = $this->createMock( self::$webRequestClassName ); + $request->method( 'getVal' )->willReturnMap( [ + [ 'oldid', null, '4463' ], + ] ); + + $user = $this->createMock( self::$userClassName ); + $user->method( 'isRegistered' )->willReturn( false ); + + $context = $this->createMock( self::$contextClassName ); + $context->method( 'getUser' )->willReturn( $user ); + $context->method( 'getOutput' )->willReturn( $output ); + $context->method( 'getRequest' )->willReturn( $request ); + + $special = $this->createMock( self::$specialPageClassName ); + $special->method( 'getContext' )->willReturn( $context ); + $special->method( 'getName' )->willReturn( 'Login' ); + + $runner = $this->getMockBuilder( Hooks::class ) + ->onlyMethods( [ 'denyAccess' ] ) + ->getMock(); + $runner->expects( $this->once() )->method( 'denyAccess' ); + + $result = $runner->onSpecialPageBeforeExecute( $special, null ); + $this->assertFalse( $result ); + } + + /** + * @covers ::onSpecialPageBeforeExecute + */ + public function testSpecialPageWithOldIdAllowsLoggedIn() { + $output = $this->createMock( self::$outputPageClassName ); + + $request = $this->createMock( self::$webRequestClassName ); + $request->method( 'getVal' )->willReturnMap( [ + [ 'oldid', null, '4463' ], + ] ); + + $user = $this->createMock( self::$userClassName ); + $user->method( 'isRegistered' )->willReturn( true ); + + $context = $this->createMock( self::$contextClassName ); + $context->method( 'getUser' )->willReturn( $user ); + $context->method( 'getRequest' )->willReturn( $request ); + + $special = $this->createMock( self::$specialPageClassName ); + $special->method( 'getContext' )->willReturn( $context ); + $special->method( 'getName' )->willReturn( 'Login' ); + + $runner = $this->getMockBuilder( Hooks::class ) + ->onlyMethods( [ 'denyAccess' ] ) + ->getMock(); + $runner->expects( $this->never() )->method( 'denyAccess' ); + + $result = $runner->onSpecialPageBeforeExecute( $special, null ); + $this->assertTrue( $result ); + } + + /** + * @covers ::onSpecialPageBeforeExecute + */ + public function testSpecialPageWithoutOldIdAllowsAnonymous() { + $request = $this->createMock( self::$webRequestClassName ); + $request->method( 'getVal' )->willReturnMap( [ + [ 'oldid', null, null ], + ] ); + + $user = $this->createMock( self::$userClassName ); + $user->method( 'isRegistered' )->willReturn( false ); + + $context = $this->createMock( self::$contextClassName ); + $context->method( 'getUser' )->willReturn( $user ); + $context->method( 'getRequest' )->willReturn( $request ); + + $special = $this->createMock( self::$specialPageClassName ); + $special->method( 'getContext' )->willReturn( $context ); + $special->method( 'getName' )->willReturn( 'Login' ); + + $runner = $this->getMockBuilder( Hooks::class ) + ->onlyMethods( [ 'denyAccess' ] ) + ->getMock(); + $runner->expects( $this->never() )->method( 'denyAccess' ); + + $result = $runner->onSpecialPageBeforeExecute( $special, null ); + $this->assertTrue( $result ); + } }