Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
LoginAttemptCounter
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 7
420
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 increaseBadLoginCounter
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 resetBadLoginCounter
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 isBadLoginTriggered
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 isBadLoginPerUserTriggered
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 badLoginKey
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 badLoginPerUserKey
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace MediaWiki\Extension\ConfirmEdit\Auth;
4
5use MediaWiki\Extension\ConfirmEdit\CaptchaTriggers;
6use MediaWiki\Extension\ConfirmEdit\SimpleCaptcha\SimpleCaptcha;
7use MediaWiki\MediaWikiServices;
8use MediaWiki\User\User;
9use MediaWiki\User\UserNameUtils;
10use Wikimedia\ObjectCache\BagOStuff;
11
12/**
13 * Helper to count login attempts per IP and per username.
14 *
15 * @internal
16 */
17class LoginAttemptCounter {
18    private SimpleCaptcha $captcha;
19
20    public function __construct( SimpleCaptcha $captcha ) {
21        $this->captcha = $captcha;
22    }
23
24    /**
25     * Increase bad login counter after a failed login.
26     * The user might be required to solve a captcha if the count is high.
27     * @param string $username
28     * TODO use Throttler
29     */
30    public function increaseBadLoginCounter( $username ) {
31        global $wgCaptchaBadLoginExpiration, $wgCaptchaBadLoginPerUserExpiration;
32
33        $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getLocalClusterInstance();
34
35        if ( $this->captcha->triggersCaptcha( CaptchaTriggers::BAD_LOGIN ) ) {
36            $key = $this->badLoginKey( $cache );
37            $cache->incrWithInit( $key, $wgCaptchaBadLoginExpiration );
38
39            // Longer period of time
40            $key = $this->badLoginKey( $cache, true );
41            $cache->incrWithInit( $key, $wgCaptchaBadLoginExpiration * 300 );
42        }
43
44        if ( $this->captcha->triggersCaptcha( CaptchaTriggers::BAD_LOGIN_PER_USER ) && $username ) {
45            $key = $this->badLoginPerUserKey( $username, $cache );
46            $cache->incrWithInit( $key, $wgCaptchaBadLoginPerUserExpiration );
47
48            $key = $this->badLoginPerUserKey( $username, $cache, true );
49            $cache->incrWithInit( $key, $wgCaptchaBadLoginPerUserExpiration * 300 );
50        }
51    }
52
53    /**
54     * Reset bad login counter after a successful login.
55     * @param string $username
56     */
57    public function resetBadLoginCounter( $username ) {
58        if ( $this->captcha->triggersCaptcha( CaptchaTriggers::BAD_LOGIN_PER_USER ) && $username ) {
59            $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getLocalClusterInstance();
60            $cache->delete( $this->badLoginPerUserKey( $username, $cache ) );
61            $cache->delete( $this->badLoginPerUserKey( $username, $cache, true ) );
62        }
63    }
64
65    /**
66     * Check if a bad login has already been registered for this
67     * IP address. If so, require a captcha.
68     * @return bool
69     */
70    public function isBadLoginTriggered() {
71        global $wgCaptchaBadLoginAttempts;
72
73        $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getLocalClusterInstance();
74        return $this->captcha->triggersCaptcha( CaptchaTriggers::BAD_LOGIN )
75            && (
76                (int)$cache->get( $this->badLoginKey( $cache ) ) >= $wgCaptchaBadLoginAttempts ||
77                (int)$cache->get( $this->badLoginKey( $cache, true ) ) >= ( $wgCaptchaBadLoginAttempts * 30 )
78            );
79    }
80
81    /**
82     * Is the per-user captcha triggered?
83     *
84     * @param User|string $u User object, or name
85     * @return bool
86     */
87    public function isBadLoginPerUserTriggered( $u ) {
88        global $wgCaptchaBadLoginPerUserAttempts;
89
90        $cache = MediaWikiServices::getInstance()->getObjectCacheFactory()->getLocalClusterInstance();
91
92        if ( is_object( $u ) ) {
93            $u = $u->getName();
94        }
95        return $this->captcha->triggersCaptcha( CaptchaTriggers::BAD_LOGIN_PER_USER )
96            && (
97                (int)$cache->get( $this->badLoginPerUserKey( $u, $cache ) ) >= $wgCaptchaBadLoginPerUserAttempts ||
98                (int)$cache->get( $this->badLoginPerUserKey( $u, $cache, true ) )
99                    >= ( $wgCaptchaBadLoginPerUserAttempts * 30 )
100            );
101    }
102
103    /**
104     * Internal cache key for badlogin checks.
105     * @param BagOStuff $cache
106     * @param bool $long
107     * @return string
108     */
109    private function badLoginKey( BagOStuff $cache, $long = false ) {
110        global $wgRequest;
111        $ip = $wgRequest->getIP();
112        if ( !$long ) {
113            return $cache->makeGlobalKey( 'captcha', 'badlogin', 'ip', $ip );
114        }
115        return $cache->makeGlobalKey( 'captcha', 'badlogin', 'ip', 'long', $ip );
116    }
117
118    /**
119     * Cache key for badloginPerUser checks.
120     * @param string $username
121     * @param BagOStuff $cache
122     * @param bool $long
123     * @return string
124     */
125    private function badLoginPerUserKey( $username, BagOStuff $cache, $long = false ) {
126        $userNameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
127        $username = $userNameUtils->getCanonical( $username, UserNameUtils::RIGOR_USABLE ) ?: $username;
128        if ( !$long ) {
129            return $cache->makeGlobalKey(
130                'captcha', 'badlogin', 'user', md5( $username )
131            );
132        }
133        return $cache->makeGlobalKey(
134            'captcha', 'badlogin', 'user', 'long', md5( $username )
135        );
136    }
137}