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