Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.38% covered (warning)
75.38%
49 / 65
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
CentralAuthEditCounter
75.38% covered (warning)
75.38%
49 / 65
40.00% covered (danger)
40.00%
2 / 5
18.36
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getCount
69.05% covered (warning)
69.05%
29 / 42
0.00% covered (danger)
0.00%
0 / 1
7.07
 getCountFromWikis
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 getCountFromDB
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 increment
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
1<?php
2
3namespace MediaWiki\Extension\CentralAuth;
4
5use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
6use Wikimedia\ObjectCache\WANObjectCache;
7use Wikimedia\Rdbms\IReadableDatabase;
8use Wikimedia\Rdbms\RawSQLValue;
9
10class CentralAuthEditCounter {
11
12    private CentralAuthDatabaseManager $databaseManager;
13    private WANObjectCache $wanCache;
14
15    public function __construct(
16        CentralAuthDatabaseManager $databaseManager,
17        WANObjectCache $wanCache
18    ) {
19        $this->databaseManager = $databaseManager;
20        $this->wanCache = $wanCache;
21    }
22
23    /**
24     * Get the global edit count for a user
25     *
26     * @param CentralAuthUser $centralUser
27     * @return int
28     */
29    public function getCount( CentralAuthUser $centralUser ) {
30        $userId = $centralUser->getId();
31        $dbr = $this->databaseManager->getCentralReplicaDB();
32        $count = $this->getCountFromDB( $dbr, $userId );
33        if ( $count !== false ) {
34            return $count;
35        }
36
37        if ( $this->databaseManager->isReadOnly() ) {
38            // Don't try DB_PRIMARY since that will throw an exception
39            return $this->wanCache->getWithSetCallback(
40                $this->wanCache->makeGlobalKey( 'centralauth-editcount', $centralUser->getId() ),
41                5 * $this->wanCache::TTL_MINUTE,
42                function () use ( $centralUser ) {
43                    return $this->getCountFromWikis( $centralUser );
44                }
45            );
46        }
47
48        $dbw = $this->databaseManager->getCentralPrimaryDB();
49        $count = $this->getCountFromDB( $dbw, $userId );
50        if ( $count !== false ) {
51            return $count;
52        }
53
54        $dbw->startAtomic( __METHOD__ );
55        // Lock the row
56        $dbw->newInsertQueryBuilder()
57            ->insertInto( 'global_edit_count' )
58            ->ignore()
59            ->row( [
60                'gec_user' => $userId,
61                'gec_count' => 0
62            ] )
63            ->caller( __METHOD__ )
64            ->execute();
65        if ( !$dbw->affectedRows() ) {
66            // Try one more time after the lock wait
67            $dbw->endAtomic( __METHOD__ );
68            $count = $this->getCountFromDB( $dbw, $userId );
69            if ( $count !== false ) {
70                return $count;
71            }
72            $dbw->startAtomic( __METHOD__ );
73        }
74        $count = $this->getCountFromWikis( $centralUser );
75
76        $dbw->newUpdateQueryBuilder()
77            ->update( 'global_edit_count' )
78            ->set( [ 'gec_count' => $count ] )
79            ->where( [ 'gec_user' => $userId ] )
80            ->caller( __METHOD__ )
81            ->execute();
82        $dbw->endAtomic( __METHOD__ );
83        return $count;
84    }
85
86    /**
87     * Get the count by adding the user_editcount value across all attached wikis
88     *
89     * @param CentralAuthUser $centralUser
90     * @return int
91     */
92    public function getCountFromWikis( CentralAuthUser $centralUser ) {
93        $count = 0;
94        foreach ( $centralUser->queryAttached() as $acc ) {
95            if ( isset( $acc['editCount'] ) ) {
96                $count += (int)$acc['editCount'];
97            }
98        }
99        return $count;
100    }
101
102    /**
103     * @param IReadableDatabase $dbr
104     * @param int $userId
105     * @return int|false
106     */
107    private function getCountFromDB( IReadableDatabase $dbr, int $userId ) {
108        $count = $dbr->newSelectQueryBuilder()
109            ->select( 'gec_count' )
110            ->from( 'global_edit_count' )
111            ->where( [ 'gec_user' => $userId ] )
112            ->caller( __METHOD__ )
113            ->fetchField();
114        return is_string( $count ) ? (int)$count : $count;
115    }
116
117    /**
118     * Increment a global edit count
119     *
120     * @param CentralAuthUser $centralUser
121     * @param int $increment
122     */
123    public function increment( CentralAuthUser $centralUser, $increment ) {
124        if ( !$increment || $this->databaseManager->isReadOnly() ) {
125            return;
126        }
127        $dbw = $this->databaseManager->getCentralPrimaryDB();
128        $dbw->newUpdateQueryBuilder()
129            ->update( 'global_edit_count' )
130            ->set( [ 'gec_count' => new RawSQLValue( 'gec_count + ' . (int)$increment ) ] )
131            ->where( [ 'gec_user' => $centralUser->getId() ] )
132            ->caller( __METHOD__ )
133            ->execute();
134        // No need to populate when affectedRows() = 0, we can just wait for
135        // getCount() to be called.
136    }
137}