MediaWiki REL1_34
CaptchaPreAuthenticationProviderTest.php
Go to the documentation of this file.
1<?php
2
5use Wikimedia\TestingAccessWrapper;
6
11class CaptchaPreAuthenticationProviderTest extends MediaWikiTestCase {
12 public function setUp() {
13 parent::setUp();
14 $this->setMwGlobals( [
15 'wgCaptchaClass' => SimpleCaptcha::class,
16 'wgCaptchaBadLoginAttempts' => 1,
17 'wgCaptchaBadLoginPerUserAttempts' => 1,
18 'wgCaptchaStorageClass' => CaptchaHashStore::class,
19 'wgMainCacheType' => __METHOD__,
20 ] );
22 CaptchaStore::get()->clearAll();
23 $services = \MediaWiki\MediaWikiServices::getInstance();
24 if ( method_exists( $services, 'getLocalClusterObjectCache' ) ) {
25 $this->setService( 'LocalClusterObjectCache', new HashBagOStuff() );
26 } else {
27 ObjectCache::$instances[__METHOD__] = new HashBagOStuff();
28 }
29 }
30
31 public function tearDown() {
32 parent::tearDown();
33 // make sure $wgCaptcha resets between tests
34 TestingAccessWrapper::newFromClass( ConfirmEditHooks::class )->instanceCreated = false;
35 }
36
41 $action, $username, $triggers, $needsCaptcha, $preTestCallback = null
42 ) {
43 $this->setTriggers( $triggers );
44 if ( $preTestCallback ) {
45 $fn = array_shift( $preTestCallback );
46 call_user_func_array( [ $this, $fn ], $preTestCallback );
47 }
48
50 $request = RequestContext::getMain()->getRequest();
51 $request->setCookie( 'UserName', $username );
52
53 $provider = new CaptchaPreAuthenticationProvider();
54 $provider->setManager( AuthManager::singleton() );
55 $reqs = $provider->getAuthenticationRequests( $action, [ 'username' => $username ] );
56 if ( $needsCaptcha ) {
57 $this->assertCount( 1, $reqs );
58 $this->assertInstanceOf( CaptchaAuthenticationRequest::class, $reqs[0] );
59 } else {
60 $this->assertEmpty( $reqs );
61 }
62 }
63
65 return [
66 [ AuthManager::ACTION_LOGIN, null, [], false ],
67 [ AuthManager::ACTION_LOGIN, null, [ 'badlogin' ], false ],
68 [ AuthManager::ACTION_LOGIN, null, [ 'badlogin' ], true, [ 'blockLogin', 'Foo' ] ],
69 [ AuthManager::ACTION_LOGIN, null, [ 'badloginperuser' ], false, [ 'blockLogin', 'Foo' ] ],
70 [ AuthManager::ACTION_LOGIN, 'Foo', [ 'badloginperuser' ], false, [ 'blockLogin', 'Bar' ] ],
71 [ AuthManager::ACTION_LOGIN, 'Foo', [ 'badloginperuser' ], true, [ 'blockLogin', 'Foo' ] ],
72 [ AuthManager::ACTION_LOGIN, null, [ 'badloginperuser' ], true, [ 'flagSession' ] ],
73 [ AuthManager::ACTION_CREATE, null, [], false ],
74 [ AuthManager::ACTION_CREATE, null, [ 'createaccount' ], true ],
75 [ AuthManager::ACTION_CREATE, 'UTSysop', [ 'createaccount' ], false ],
76 [ AuthManager::ACTION_LINK, null, [], false ],
77 [ AuthManager::ACTION_CHANGE, null, [], false ],
78 [ AuthManager::ACTION_REMOVE, null, [], false ],
79 ];
80 }
81
83 $this->setTriggers( [ 'createaccount' ] );
84 $captcha = new SimpleCaptcha();
85 $provider = new CaptchaPreAuthenticationProvider();
86 $provider->setManager( AuthManager::singleton() );
87
88 $reqs = $provider->getAuthenticationRequests( AuthManager::ACTION_CREATE,
89 [ 'username' => 'Foo' ] );
90
91 $this->assertCount( 1, $reqs );
92 $this->assertInstanceOf( CaptchaAuthenticationRequest::class, $reqs[0] );
93
94 $id = $reqs[0]->captchaId;
95 $data = TestingAccessWrapper::newFromObject( $reqs[0] )->captchaData;
96 $this->assertEquals( $captcha->retrieveCaptcha( $id ), $data + [ 'index' => $id ] );
97 }
98
102 public function testTestForAuthentication( $req, $isBadLoginTriggered,
103 $isBadLoginPerUserTriggered, $result
104 ) {
105 $this->setMwHook( 'PingLimiter', function ( $user, $action, &$result ) {
106 $result = false;
107 return false;
108 } );
109 CaptchaStore::get()->store( '345', [ 'question' => '2+2', 'answer' => '4' ] );
110 $captcha = $this->getMock( SimpleCaptcha::class,
111 [ 'isBadLoginTriggered', 'isBadLoginPerUserTriggered' ] );
112 $captcha->expects( $this->any() )->method( 'isBadLoginTriggered' )
113 ->willReturn( $isBadLoginTriggered );
114 $captcha->expects( $this->any() )->method( 'isBadLoginPerUserTriggered' )
115 ->willReturn( $isBadLoginPerUserTriggered );
116 $this->setMwGlobals( 'wgCaptcha', $captcha );
117 TestingAccessWrapper::newFromClass( ConfirmEditHooks::class )->instanceCreated = true;
118 $provider = new CaptchaPreAuthenticationProvider();
119 $provider->setManager( AuthManager::singleton() );
120
121 $status = $provider->testForAuthentication( $req ? [ $req ] : [] );
122 $this->assertEquals( $result, $status->isGood() );
123 }
124
127 $fallback->username = 'Foo';
128 return [
129 // [ auth request, bad login?, bad login per user?, result ]
130 'no need to check' => [ $fallback, false, false, true ],
131 'badlogin' => [ $fallback, true, false, false ],
132 'badloginperuser, no username' => [ null, false, true, true ],
133 'badloginperuser' => [ $fallback, false, true, false ],
134 'non-existent captcha' => [ $this->getCaptchaRequest( '123', '4' ), true, true, false ],
135 'wrong captcha' => [ $this->getCaptchaRequest( '345', '6' ), true, true, false ],
136 'correct captcha' => [ $this->getCaptchaRequest( '345', '4' ), true, true, true ],
137 ];
138 }
139
143 public function testTestForAccountCreation( $req, $creator, $result, $disableTrigger = false ) {
144 $this->setMwHook( 'PingLimiter', function ( &$user, $action, &$result ) {
145 $result = false;
146 return false;
147 } );
148 $this->setTriggers( $disableTrigger ? [] : [ 'createaccount' ] );
149 CaptchaStore::get()->store( '345', [ 'question' => '2+2', 'answer' => '4' ] );
150 $user = User::newFromName( 'Foo' );
151 $provider = new CaptchaPreAuthenticationProvider();
152 $provider->setManager( AuthManager::singleton() );
153
154 $status = $provider->testForAccountCreation( $user, $creator, $req ? [ $req ] : [] );
155 $this->assertEquals( $result, $status->isGood() );
156 }
157
159 $user = User::newFromName( 'Bar' );
160 $sysop = User::newFromName( 'UTSysop' );
161 return [
162 // [ auth request, creator, result, disable trigger? ]
163 'no captcha' => [ null, $user, false ],
164 'non-existent captcha' => [ $this->getCaptchaRequest( '123', '4' ), $user, false ],
165 'wrong captcha' => [ $this->getCaptchaRequest( '345', '6' ), $user, false ],
166 'correct captcha' => [ $this->getCaptchaRequest( '345', '4' ), $user, true ],
167 'user is exempt' => [ null, $sysop, true ],
168 'disabled' => [ null, $user, true, 'disable' ],
169 ];
170 }
171
172 public function testPostAuthentication() {
173 $this->setTriggers( [ 'badlogin', 'badloginperuser' ] );
174 $captcha = new SimpleCaptcha();
175 $user = User::newFromName( 'Foo' );
176 $anotherUser = User::newFromName( 'Bar' );
177 $provider = new CaptchaPreAuthenticationProvider();
178 $provider->setManager( AuthManager::singleton() );
179
180 $this->assertFalse( $captcha->isBadLoginTriggered() );
181 $this->assertFalse( $captcha->isBadLoginPerUserTriggered( $user ) );
182
183 $provider->postAuthentication( $user, \MediaWiki\Auth\AuthenticationResponse::newFail(
184 wfMessage( '?' ) ) );
185
186 $this->assertTrue( $captcha->isBadLoginTriggered() );
187 $this->assertTrue( $captcha->isBadLoginPerUserTriggered( $user ) );
188 $this->assertFalse( $captcha->isBadLoginPerUserTriggered( $anotherUser ) );
189
190 $provider->postAuthentication( $user, \MediaWiki\Auth\AuthenticationResponse::newPass( 'Foo' ) );
191
192 $this->assertFalse( $captcha->isBadLoginPerUserTriggered( $user ) );
193 }
194
196 $this->setTriggers( [] );
197 $captcha = new SimpleCaptcha();
198 $user = User::newFromName( 'Foo' );
199 $provider = new CaptchaPreAuthenticationProvider();
200 $provider->setManager( AuthManager::singleton() );
201
202 $this->assertFalse( $captcha->isBadLoginTriggered() );
203 $this->assertFalse( $captcha->isBadLoginPerUserTriggered( $user ) );
204
205 $provider->postAuthentication( $user, \MediaWiki\Auth\AuthenticationResponse::newFail(
206 wfMessage( '?' ) ) );
207
208 $this->assertFalse( $captcha->isBadLoginTriggered() );
209 $this->assertFalse( $captcha->isBadLoginPerUserTriggered( $user ) );
210 }
211
215 public function testPingLimiter( array $attempts ) {
216 $this->mergeMwGlobalArrayValue(
217 'wgRateLimits',
218 [
219 'badcaptcha' => [
220 'user' => [ 1, 1 ],
221 ],
222 ]
223 );
224 $provider = new CaptchaPreAuthenticationProvider();
225 $provider->setManager( AuthManager::singleton() );
226 $providerAccess = TestingAccessWrapper::newFromObject( $provider );
227
228 foreach ( $attempts as $attempt ) {
229 if ( !empty( $attempts[3] ) ) {
230 $this->setMwHook( 'PingLimiter', function ( &$user, $action, &$result ) {
231 $result = false;
232 return false;
233 } );
234 } else {
235 $this->setMwHook( 'PingLimiter', function () {
236 } );
237 }
238
239 $captcha = new SimpleCaptcha();
240 CaptchaStore::get()->store( '345', [ 'question' => '7+7', 'answer' => '14' ] );
241 $success = $providerAccess->verifyCaptcha( $captcha, [ $attempts[0] ], $attempts[1] );
242 $this->assertEquals( $attempts[2], $success );
243 }
244 }
245
246 public function providePingLimiter() {
247 $sysop = User::newFromName( 'UTSysop' );
248 return [
249 // sequence of [ auth request, user, result, disable ping limiter? ]
250 'no failure' => [
251 [ $this->getCaptchaRequest( '345', '14' ), new User(), true ],
252 [ $this->getCaptchaRequest( '345', '14' ), new User(), true ],
253 ],
254 'limited' => [
255 [ $this->getCaptchaRequest( '345', '33' ), new User(), false ],
256 [ $this->getCaptchaRequest( '345', '14' ), new User(), false ],
257 ],
258 'exempt user' => [
259 [ $this->getCaptchaRequest( '345', '33' ), $sysop, false ],
260 [ $this->getCaptchaRequest( '345', '14' ), $sysop, true ],
261 ],
262 'pinglimiter disabled' => [
263 [ $this->getCaptchaRequest( '345', '33' ), new User(), false, 'disable' ],
264 [ $this->getCaptchaRequest( '345', '14' ), new User(), true, 'disable' ],
265 ],
266 ];
267 }
268
269 protected function getCaptchaRequest( $id, $word, $username = null ) {
270 $req = new CaptchaAuthenticationRequest( $id, [ 'question' => '?', 'answer' => $word ] );
271 $req->captchaWord = $word;
272 $req->username = $username;
273 return $req;
274 }
275
276 protected function blockLogin( $username ) {
277 $captcha = new SimpleCaptcha();
278 $captcha->increaseBadLoginCounter( $username );
279 }
280
281 protected function flagSession() {
282 RequestContext::getMain()->getRequest()->getSession()
283 ->set( 'ConfirmEdit:loginCaptchaPerUserTriggered', true );
284 }
285
286 protected function setTriggers( $triggers ) {
287 $types = [ 'edit', 'create', 'sendemail', 'addurl', 'createaccount', 'badlogin',
288 'badloginperuser' ];
289 $captchaTriggers = array_combine( $types, array_map( function ( $type ) use ( $triggers ) {
290 return in_array( $type, $triggers, true );
291 }, $types ) );
292 $this->setMwGlobals( 'wgCaptchaTriggers', $captchaTriggers );
293 }
294
301 protected function setMwHook( $hook, callable $callback ) {
302 $this->mergeMwGlobalArrayValue( 'wgHooks', [ $hook => $callback ] );
303 }
304}
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
$fallback
Generic captcha authentication request class.
@covers CaptchaPreAuthenticationProvider @group Database
testTestForAuthentication( $req, $isBadLoginTriggered, $isBadLoginPerUserTriggered, $result)
@dataProvider provideTestForAuthentication
testTestForAccountCreation( $req, $creator, $result, $disableTrigger=false)
@dataProvider provideTestForAccountCreation
setMwHook( $hook, callable $callback)
Set a $wgHooks handler for a given hook and remove all other handlers (though not ones set via Hooks:...
testPingLimiter(array $attempts)
@dataProvider providePingLimiter
testGetAuthenticationRequests( $action, $username, $triggers, $needsCaptcha, $preTestCallback=null)
@dataProvider provideGetAuthenticationRequests
static unsetInstanceForTests()
static get()
Get somewhere to store captcha data that will persist between requests.
Simple store for keeping values in an associative array for the current process.
This serves as the entry point to the authentication system.
AuthenticationRequest to ensure something with a username is present.
Demo CAPTCHA (not for production usage) and base class for real CAPTCHAs.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
This class serves as a utility class for this extension.
return true
Definition router.php:94