Skip to content

Commit 803fecd

Browse files
committed
Customizable action list
1 parent 1f516e8 commit 803fecd

File tree

3 files changed

+129
-6
lines changed

3 files changed

+129
-6
lines changed

extension.json

Lines changed: 6 additions & 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": {
@@ -23,6 +23,11 @@
2323
"SpecialPageBeforeExecute": "main"
2424
},
2525
"config": {
26+
"CrawlerProtectedActions": {
27+
"value": [
28+
"history"
29+
]
30+
},
2631
"CrawlerProtectedSpecialPages": {
2732
"value": [
2833
"mobilediff",

includes/CrawlerProtectionService.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class CrawlerProtectionService {
4040

4141
/** @var string[] List of constructor options this class accepts */
4242
public const CONSTRUCTOR_OPTIONS = [
43+
'CrawlerProtectedActions',
4344
'CrawlerProtectedSpecialPages',
4445
];
4546

@@ -89,7 +90,7 @@ public function checkPerformAction(
8990

9091
if (
9192
$type === 'revision'
92-
|| $action === 'history'
93+
|| $this->isProtectedAction( $action )
9394
|| $diffId > 0
9495
|| $oldId > 0
9596
) {
@@ -100,6 +101,29 @@ public function checkPerformAction(
100101
return true;
101102
}
102103

104+
/**
105+
* Determine whether the given action name is in the configured
106+
* list of protected actions.
107+
*
108+
* The comparison is case-insensitive so that configured values
109+
* like "History" or "HISTORY" match the action parameter.
110+
*
111+
* @param string|null $action
112+
* @return bool
113+
*/
114+
public function isProtectedAction( ?string $action ): bool {
115+
if ( $action === null ) {
116+
return false;
117+
}
118+
119+
$protectedActions = array_map(
120+
'strtolower',
121+
$this->options->get( 'CrawlerProtectedActions' )
122+
);
123+
124+
return in_array( strtolower( $action ), $protectedActions, true );
125+
}
126+
103127
/**
104128
* Check whether a special page request should be blocked.
105129
*

tests/phpunit/unit/CrawlerProtectionServiceTest.php

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,21 @@ public static function setUpBeforeClass(): void {
4141
* a mock ResponseFactory.
4242
*
4343
* @param array $protectedPages
44+
* @param array $protectedActions
4445
* @param ResponseFactory|\PHPUnit\Framework\MockObject\MockObject|null $responseFactory
4546
* @return CrawlerProtectionService
4647
*/
4748
private function buildService(
4849
array $protectedPages = [ 'recentchangeslinked', 'whatlinkshere', 'mobilediff' ],
50+
array $protectedActions = [ 'history' ],
4951
$responseFactory = null
5052
): CrawlerProtectionService {
5153
$options = new ServiceOptions(
5254
CrawlerProtectionService::CONSTRUCTOR_OPTIONS,
53-
[ 'CrawlerProtectedSpecialPages' => $protectedPages ]
55+
[
56+
'CrawlerProtectedActions' => $protectedActions,
57+
'CrawlerProtectedSpecialPages' => $protectedPages,
58+
]
5459
);
5560

5661
$responseFactory ??= $this->createMock( ResponseFactory::class );
@@ -78,7 +83,7 @@ public function testCheckPerformActionAllowsRegisteredUser() {
7883
$responseFactory = $this->createMock( ResponseFactory::class );
7984
$responseFactory->expects( $this->never() )->method( 'denyAccess' );
8085

81-
$service = $this->buildService( [], $responseFactory );
86+
$service = $this->buildService( [], [ 'history' ], $responseFactory );
8287
$this->assertTrue( $service->checkPerformAction( $output, $user, $request ) );
8388
}
8489

@@ -100,7 +105,7 @@ public function testCheckPerformActionBlocksAnonymous( array $getValMap, string
100105
$responseFactory = $this->createMock( ResponseFactory::class );
101106
$responseFactory->expects( $this->once() )->method( 'denyAccess' )->with( $output );
102107

103-
$service = $this->buildService( [], $responseFactory );
108+
$service = $this->buildService( [], [ 'history' ], $responseFactory );
104109
$this->assertFalse( $service->checkPerformAction( $output, $user, $request ), $msg );
105110
}
106111

@@ -169,10 +174,96 @@ public function testCheckPerformActionAllowsNormalAnonymousView() {
169174
$responseFactory = $this->createMock( ResponseFactory::class );
170175
$responseFactory->expects( $this->never() )->method( 'denyAccess' );
171176

172-
$service = $this->buildService( [], $responseFactory );
177+
$service = $this->buildService( [], [ 'history' ], $responseFactory );
173178
$this->assertTrue( $service->checkPerformAction( $output, $user, $request ) );
174179
}
175180

181+
/**
182+
* @covers ::checkPerformAction
183+
*/
184+
public function testCheckPerformActionBlocksConfiguredAction() {
185+
$output = $this->createMock( self::$outputPageClassName );
186+
$user = $this->createMock( self::$userClassName );
187+
$user->method( 'isRegistered' )->willReturn( false );
188+
189+
$request = $this->createMock( self::$webRequestClassName );
190+
$request->method( 'getVal' )->willReturnMap( [
191+
[ 'type', null, null ],
192+
[ 'action', null, 'edit' ],
193+
[ 'diff', null, null ],
194+
[ 'oldid', null, null ],
195+
] );
196+
197+
$responseFactory = $this->createMock( ResponseFactory::class );
198+
$responseFactory->expects( $this->once() )->method( 'denyAccess' )->with( $output );
199+
200+
$service = $this->buildService( [], [ 'edit', 'history' ], $responseFactory );
201+
$this->assertFalse( $service->checkPerformAction( $output, $user, $request ) );
202+
}
203+
204+
/**
205+
* @covers ::checkPerformAction
206+
*/
207+
public function testCheckPerformActionAllowsActionNotInConfig() {
208+
$output = $this->createMock( self::$outputPageClassName );
209+
$user = $this->createMock( self::$userClassName );
210+
$user->method( 'isRegistered' )->willReturn( false );
211+
212+
$request = $this->createMock( self::$webRequestClassName );
213+
$request->method( 'getVal' )->willReturnMap( [
214+
[ 'type', null, null ],
215+
[ 'action', null, 'history' ],
216+
[ 'diff', null, null ],
217+
[ 'oldid', null, null ],
218+
] );
219+
220+
$responseFactory = $this->createMock( ResponseFactory::class );
221+
$responseFactory->expects( $this->never() )->method( 'denyAccess' );
222+
223+
$service = $this->buildService( [], [], $responseFactory );
224+
$this->assertTrue( $service->checkPerformAction( $output, $user, $request ) );
225+
}
226+
227+
// ---------------------------------------------------------------
228+
// isProtectedAction tests
229+
// ---------------------------------------------------------------
230+
231+
/**
232+
* @covers ::isProtectedAction
233+
*/
234+
public function testIsProtectedActionReturnsTrueForConfiguredAction() {
235+
$service = $this->buildService( [], [ 'history', 'edit' ] );
236+
$this->assertTrue( $service->isProtectedAction( 'history' ) );
237+
$this->assertTrue( $service->isProtectedAction( 'edit' ) );
238+
}
239+
240+
/**
241+
* @covers ::isProtectedAction
242+
*/
243+
public function testIsProtectedActionReturnsFalseForUnconfiguredAction() {
244+
$service = $this->buildService( [], [ 'history' ] );
245+
$this->assertFalse( $service->isProtectedAction( 'view' ) );
246+
$this->assertFalse( $service->isProtectedAction( 'edit' ) );
247+
}
248+
249+
/**
250+
* @covers ::isProtectedAction
251+
*/
252+
public function testIsProtectedActionReturnsFalseForNull() {
253+
$service = $this->buildService( [], [ 'history' ] );
254+
$this->assertFalse( $service->isProtectedAction( null ) );
255+
}
256+
257+
/**
258+
* @covers ::isProtectedAction
259+
*/
260+
public function testIsProtectedActionIsCaseInsensitive() {
261+
$service = $this->buildService( [], [ 'History' ] );
262+
$this->assertTrue( $service->isProtectedAction( 'history' ) );
263+
$this->assertTrue( $service->isProtectedAction( 'HISTORY' ) );
264+
$this->assertTrue( $service->isProtectedAction( 'History' ) );
265+
}
266+
176267
// ---------------------------------------------------------------
177268
// checkSpecialPage tests
178269
// ---------------------------------------------------------------
@@ -193,6 +284,7 @@ public function testCheckSpecialPageBlocksAnonymous( string $specialPageName ) {
193284

194285
$service = $this->buildService(
195286
[ 'RecentChangesLinked', 'WhatLinksHere', 'MobileDiff' ],
287+
[],
196288
$responseFactory
197289
);
198290
$this->assertFalse( $service->checkSpecialPage( $specialPageName, $output, $user ) );
@@ -214,6 +306,7 @@ public function testCheckSpecialPageAllowsRegistered( string $specialPageName )
214306

215307
$service = $this->buildService(
216308
[ 'RecentChangesLinked', 'WhatLinksHere', 'MobileDiff' ],
309+
[],
217310
$responseFactory
218311
);
219312
$this->assertTrue( $service->checkSpecialPage( $specialPageName, $output, $user ) );
@@ -232,6 +325,7 @@ public function testCheckSpecialPageAllowsUnprotected() {
232325

233326
$service = $this->buildService(
234327
[ 'RecentChangesLinked', 'WhatLinksHere', 'MobileDiff' ],
328+
[],
235329
$responseFactory
236330
);
237331
$this->assertTrue( $service->checkSpecialPage( 'Search', $output, $user ) );

0 commit comments

Comments
 (0)