Skip to content

Commit 925f145

Browse files
authored
Version 1.3.0
Version 1.3.0 (Oct 11, 2025) * Add oldid parameter blocking to special pages and comprehensive tests
1 parent 1bf3fd5 commit 925f145

4 files changed

Lines changed: 171 additions & 1 deletion

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,7 @@ intensive.
3434
1.2.0 (Aug 13, 2025)
3535

3636
* Fix 403 handling and add login links to prevent nginx 404 fallthrough
37+
38+
1.3.0 (Oct 11, 2025)
39+
40+
* Add oldid parameter blocking to special pages and comprehensive tests

extension.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "CrawlerProtection",
33
"author": "MyWikis LLC",
4-
"version": "1.2.0",
4+
"version": "1.3.0",
55
"description": "Suite of protective measures to protect wikis from crawlers.",
66
"type": "hook",
77
"requires": {

includes/Hooks.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ public function onMediaWikiPerformAction(
9595

9696
/**
9797
* Block Special:RecentChangesLinked, Special:WhatLinksHere, and Special:MobileDiff for anonymous users.
98+
* Also blocks any special page access with ?oldid parameter.
9899
*
99100
* @param SpecialPage $special
100101
* @param string|null $subPage
@@ -129,6 +130,18 @@ public function onSpecialPageBeforeExecute( $special, $subPage ) {
129130
return false;
130131
}
131132

133+
// Also block if oldid parameter is present
134+
$request = $special->getContext()->getRequest();
135+
$oldId = (int)$request->getVal( 'oldid' );
136+
if ( $oldId > 0 ) {
137+
if ( $denyFast ) {
138+
$this->denyAccessWith418();
139+
}
140+
$out = $special->getContext()->getOutput();
141+
$this->denyAccess( $out );
142+
return false;
143+
}
144+
132145
return true;
133146
}
134147

tests/phpunit/unit/HooksTest.php

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ class HooksTest extends TestCase {
3030
/** @var string */
3131
private static string $specialPageClassName;
3232

33+
/** @var string */
34+
private static string $contextClassName;
35+
3336
public static function setUpBeforeClass(): void {
3437
parent::setUpBeforeClass();
3538

@@ -60,6 +63,10 @@ public static function setUpBeforeClass(): void {
6063
self::$webRequestClassName = class_exists( '\MediaWiki\Request\WebRequest' )
6164
? '\MediaWiki\Request\WebRequest'
6265
: '\WebRequest';
66+
67+
self::$contextClassName = class_exists( '\MediaWiki\Context\RequestContext' )
68+
? '\MediaWiki\Context\RequestContext'
69+
: '\RequestContext';
6370
}
6471

6572
/**
@@ -368,4 +375,150 @@ public function provideBlockedSpecialPages() {
368375
'MobileDiff mixed case' => [ 'MoBiLeDiFf' ],
369376
];
370377
}
378+
379+
/**
380+
* @covers ::onMediaWikiPerformAction
381+
*/
382+
public function testOldIdBlocksAnonymous() {
383+
$output = $this->createMock( self::$outputPageClassName );
384+
385+
$request = $this->createMock( self::$webRequestClassName );
386+
$request->method( 'getVal' )->willReturnMap( [
387+
[ 'oldid', null, '1234' ],
388+
] );
389+
390+
$user = $this->createMock( self::$userClassName );
391+
$user->method( 'isRegistered' )->willReturn( false );
392+
393+
$article = $this->createMock( self::$articleClassName );
394+
$title = $this->createMock( self::$titleClassName );
395+
$wiki = $this->createMock( self::$actionEntryPointClassName );
396+
397+
$runner = $this->getMockBuilder( Hooks::class )
398+
->onlyMethods( [ 'denyAccess' ] )
399+
->getMock();
400+
$runner->expects( $this->once() )->method( 'denyAccess' );
401+
402+
$result = $runner->onMediaWikiPerformAction( $output, $article, $title, $user, $request, $wiki );
403+
$this->assertFalse( $result );
404+
}
405+
406+
/**
407+
* @covers ::onMediaWikiPerformAction
408+
*/
409+
public function testOldIdAllowsLoggedIn() {
410+
$output = $this->createMock( self::$outputPageClassName );
411+
412+
$request = $this->createMock( self::$webRequestClassName );
413+
$request->method( 'getVal' )->willReturnMap( [
414+
[ 'oldid', null, '1234' ],
415+
] );
416+
417+
$user = $this->createMock( self::$userClassName );
418+
$user->method( 'isRegistered' )->willReturn( true );
419+
420+
$article = $this->createMock( self::$articleClassName );
421+
$title = $this->createMock( self::$titleClassName );
422+
$wiki = $this->createMock( self::$actionEntryPointClassName );
423+
424+
$runner = $this->getMockBuilder( Hooks::class )
425+
->onlyMethods( [ 'denyAccess' ] )
426+
->getMock();
427+
$runner->expects( $this->never() )->method( 'denyAccess' );
428+
429+
$result = $runner->onMediaWikiPerformAction( $output, $article, $title, $user, $request, $wiki );
430+
$this->assertTrue( $result );
431+
}
432+
433+
/**
434+
* @covers ::onSpecialPageBeforeExecute
435+
*/
436+
public function testSpecialPageWithOldIdBlocksAnonymous() {
437+
$output = $this->createMock( self::$outputPageClassName );
438+
439+
$request = $this->createMock( self::$webRequestClassName );
440+
$request->method( 'getVal' )->willReturnMap( [
441+
[ 'oldid', null, '4463' ],
442+
] );
443+
444+
$user = $this->createMock( self::$userClassName );
445+
$user->method( 'isRegistered' )->willReturn( false );
446+
447+
$context = $this->createMock( self::$contextClassName );
448+
$context->method( 'getUser' )->willReturn( $user );
449+
$context->method( 'getOutput' )->willReturn( $output );
450+
$context->method( 'getRequest' )->willReturn( $request );
451+
452+
$special = $this->createMock( self::$specialPageClassName );
453+
$special->method( 'getContext' )->willReturn( $context );
454+
$special->method( 'getName' )->willReturn( 'Login' );
455+
456+
$runner = $this->getMockBuilder( Hooks::class )
457+
->onlyMethods( [ 'denyAccess' ] )
458+
->getMock();
459+
$runner->expects( $this->once() )->method( 'denyAccess' );
460+
461+
$result = $runner->onSpecialPageBeforeExecute( $special, null );
462+
$this->assertFalse( $result );
463+
}
464+
465+
/**
466+
* @covers ::onSpecialPageBeforeExecute
467+
*/
468+
public function testSpecialPageWithOldIdAllowsLoggedIn() {
469+
$output = $this->createMock( self::$outputPageClassName );
470+
471+
$request = $this->createMock( self::$webRequestClassName );
472+
$request->method( 'getVal' )->willReturnMap( [
473+
[ 'oldid', null, '4463' ],
474+
] );
475+
476+
$user = $this->createMock( self::$userClassName );
477+
$user->method( 'isRegistered' )->willReturn( true );
478+
479+
$context = $this->createMock( self::$contextClassName );
480+
$context->method( 'getUser' )->willReturn( $user );
481+
$context->method( 'getRequest' )->willReturn( $request );
482+
483+
$special = $this->createMock( self::$specialPageClassName );
484+
$special->method( 'getContext' )->willReturn( $context );
485+
$special->method( 'getName' )->willReturn( 'Login' );
486+
487+
$runner = $this->getMockBuilder( Hooks::class )
488+
->onlyMethods( [ 'denyAccess' ] )
489+
->getMock();
490+
$runner->expects( $this->never() )->method( 'denyAccess' );
491+
492+
$result = $runner->onSpecialPageBeforeExecute( $special, null );
493+
$this->assertTrue( $result );
494+
}
495+
496+
/**
497+
* @covers ::onSpecialPageBeforeExecute
498+
*/
499+
public function testSpecialPageWithoutOldIdAllowsAnonymous() {
500+
$request = $this->createMock( self::$webRequestClassName );
501+
$request->method( 'getVal' )->willReturnMap( [
502+
[ 'oldid', null, null ],
503+
] );
504+
505+
$user = $this->createMock( self::$userClassName );
506+
$user->method( 'isRegistered' )->willReturn( false );
507+
508+
$context = $this->createMock( self::$contextClassName );
509+
$context->method( 'getUser' )->willReturn( $user );
510+
$context->method( 'getRequest' )->willReturn( $request );
511+
512+
$special = $this->createMock( self::$specialPageClassName );
513+
$special->method( 'getContext' )->willReturn( $context );
514+
$special->method( 'getName' )->willReturn( 'Login' );
515+
516+
$runner = $this->getMockBuilder( Hooks::class )
517+
->onlyMethods( [ 'denyAccess' ] )
518+
->getMock();
519+
$runner->expects( $this->never() )->method( 'denyAccess' );
520+
521+
$result = $runner->onSpecialPageBeforeExecute( $special, null );
522+
$this->assertTrue( $result );
523+
}
371524
}

0 commit comments

Comments
 (0)