Skip to content

Commit 1f7c032

Browse files
Copilotjeffw16
andcommitted
Add configurable action protection with $wgCrawlerProtectedActions
Co-authored-by: jeffw16 <11380894+jeffw16@users.noreply.github.com>
1 parent c3eca22 commit 1f7c032

File tree

5 files changed

+138
-2
lines changed

5 files changed

+138
-2
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ intensive.
66

77
# Configuration
88

9+
* `$wgCrawlerProtectedActions` - array of actions to protect (default: `[ 'history' ]`).
10+
Actions specified in this array will be denied for anonymous users.
11+
Set to an empty array `[]` to allow all actions for anonymous users.
912
* `$wgCrawlerProtectedSpecialPages` - array of special pages to protect
1013
(default: `[ 'mobilediff', 'recentchangeslinked', 'whatlinkshere' ]`).
1114
Supported values are special page names or their aliases regardless of case.

extension.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
"SpecialPageBeforeExecute": "main"
2222
},
2323
"config": {
24+
"CrawlerProtectedActions": {
25+
"value": [
26+
"history"
27+
]
28+
},
2429
"CrawlerProtectedSpecialPages": {
2530
"value": [
2631
"mobilediff",

includes/Hooks.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class Hooks implements MediaWikiPerformActionHook, SpecialPageBeforeExecuteHook
4343
* Block sensitive page views for anonymous users via MediaWikiPerformAction.
4444
* Handles:
4545
* - ?type=revision
46-
* - ?action=history
46+
* - ?action=<configurable actions>
4747
* - ?diff=1234
4848
* - ?oldid=1234
4949
*
@@ -70,11 +70,14 @@ public function onMediaWikiPerformAction(
7070
$diffId = (int)$request->getVal( 'diff' );
7171
$oldId = (int)$request->getVal( 'oldid' );
7272

73+
$config = MediaWikiServices::getInstance()->getMainConfig();
74+
$protectedActions = $config->get( 'CrawlerProtectedActions' );
75+
7376
if (
7477
!$user->isRegistered()
7578
&& (
7679
$type === 'revision'
77-
|| $action === 'history'
80+
|| in_array( $action, $protectedActions, true )
7881
|| $diffId > 0
7982
|| $oldId > 0
8083
)

tests/phpunit/namespaced-stubs.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ public function getMainConfig() {
130130
* @return mixed
131131
*/
132132
public function get( $name ) {
133+
if ( $name === 'CrawlerProtectedActions' ) {
134+
return [ 'history' ];
135+
}
133136
if ( $name === 'CrawlerProtectedSpecialPages' ) {
134137
return [
135138
'RecentChangesLinked',

tests/phpunit/unit/HooksTest.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,128 @@ public function testNonRevisionTypeAlwaysAllowed() {
160160
$this->assertTrue( $result );
161161
}
162162

163+
/**
164+
* @covers ::onMediaWikiPerformAction
165+
*/
166+
public function testHistoryActionBlocksAnonymous() {
167+
// Skip this test in MediaWiki environment - it requires service container
168+
if ( !property_exists( '\MediaWiki\MediaWikiServices', 'testUse418' ) ) {
169+
$this->markTestSkipped(
170+
'Test requires stub MediaWikiServices. Skipped in MediaWiki unit test environment.'
171+
);
172+
}
173+
174+
$output = $this->createMock( self::$outputPageClassName );
175+
176+
$request = $this->createMock( self::$webRequestClassName );
177+
$request->method( 'getVal' )->willReturnMap( [
178+
[ 'action', null, 'history' ],
179+
] );
180+
181+
$user = $this->createMock( self::$userClassName );
182+
$user->method( 'isRegistered' )->willReturn( false );
183+
184+
$article = $this->createMock( self::$articleClassName );
185+
$title = $this->createMock( self::$titleClassName );
186+
$wiki = $this->createMock( self::$actionEntryPointClassName );
187+
188+
$runner = $this->getMockBuilder( Hooks::class )
189+
->onlyMethods( [ 'denyAccess' ] )
190+
->getMock();
191+
$runner->expects( $this->once() )->method( 'denyAccess' );
192+
193+
$result = $runner->onMediaWikiPerformAction( $output, $article, $title, $user, $request, $wiki );
194+
$this->assertFalse( $result );
195+
}
196+
197+
/**
198+
* @covers ::onMediaWikiPerformAction
199+
*/
200+
public function testHistoryActionAllowsLoggedIn() {
201+
// Skip this test in MediaWiki environment - it requires service container
202+
if ( !property_exists( '\MediaWiki\MediaWikiServices', 'testUse418' ) ) {
203+
$this->markTestSkipped(
204+
'Test requires stub MediaWikiServices. Skipped in MediaWiki unit test environment.'
205+
);
206+
}
207+
208+
$output = $this->createMock( self::$outputPageClassName );
209+
210+
$request = $this->createMock( self::$webRequestClassName );
211+
$request->method( 'getVal' )->willReturnMap( [
212+
[ 'action', null, 'history' ],
213+
] );
214+
215+
$user = $this->createMock( self::$userClassName );
216+
$user->method( 'isRegistered' )->willReturn( true );
217+
218+
$article = $this->createMock( self::$articleClassName );
219+
$title = $this->createMock( self::$titleClassName );
220+
$wiki = $this->createMock( self::$actionEntryPointClassName );
221+
222+
$runner = $this->getMockBuilder( Hooks::class )
223+
->onlyMethods( [ 'denyAccess' ] )
224+
->getMock();
225+
$runner->expects( $this->never() )->method( 'denyAccess' );
226+
227+
$result = $runner->onMediaWikiPerformAction( $output, $article, $title, $user, $request, $wiki );
228+
$this->assertTrue( $result );
229+
}
230+
231+
/**
232+
* @covers ::onMediaWikiPerformAction
233+
*/
234+
public function testDiffParameterBlocksAnonymous() {
235+
$output = $this->createMock( self::$outputPageClassName );
236+
237+
$request = $this->createMock( self::$webRequestClassName );
238+
$request->method( 'getVal' )->willReturnMap( [
239+
[ 'diff', null, '1234' ],
240+
] );
241+
242+
$user = $this->createMock( self::$userClassName );
243+
$user->method( 'isRegistered' )->willReturn( false );
244+
245+
$article = $this->createMock( self::$articleClassName );
246+
$title = $this->createMock( self::$titleClassName );
247+
$wiki = $this->createMock( self::$actionEntryPointClassName );
248+
249+
$runner = $this->getMockBuilder( Hooks::class )
250+
->onlyMethods( [ 'denyAccess' ] )
251+
->getMock();
252+
$runner->expects( $this->once() )->method( 'denyAccess' );
253+
254+
$result = $runner->onMediaWikiPerformAction( $output, $article, $title, $user, $request, $wiki );
255+
$this->assertFalse( $result );
256+
}
257+
258+
/**
259+
* @covers ::onMediaWikiPerformAction
260+
*/
261+
public function testOldidParameterBlocksAnonymous() {
262+
$output = $this->createMock( self::$outputPageClassName );
263+
264+
$request = $this->createMock( self::$webRequestClassName );
265+
$request->method( 'getVal' )->willReturnMap( [
266+
[ 'oldid', null, '5678' ],
267+
] );
268+
269+
$user = $this->createMock( self::$userClassName );
270+
$user->method( 'isRegistered' )->willReturn( false );
271+
272+
$article = $this->createMock( self::$articleClassName );
273+
$title = $this->createMock( self::$titleClassName );
274+
$wiki = $this->createMock( self::$actionEntryPointClassName );
275+
276+
$runner = $this->getMockBuilder( Hooks::class )
277+
->onlyMethods( [ 'denyAccess' ] )
278+
->getMock();
279+
$runner->expects( $this->once() )->method( 'denyAccess' );
280+
281+
$result = $runner->onMediaWikiPerformAction( $output, $article, $title, $user, $request, $wiki );
282+
$this->assertFalse( $result );
283+
}
284+
163285
/**
164286
* @covers ::onSpecialPageBeforeExecute
165287
* @dataProvider provideBlockedSpecialPages

0 commit comments

Comments
 (0)