Passed
Push — master ( 249321...cafa97 )
by MusikAnimal
05:17
created

UserRightsRepository::getRawRightsNames()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 11
nc 4
nop 1
dl 0
loc 22
rs 9.2
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the UserRightsRepository class.
4
 */
5
6
namespace Xtools;
7
8
use GuzzleHttp;
9
use Mediawiki\Api\SimpleRequest;
10
11
/**
12
 * An UserRightsRepository is responsible for retrieving information around a user's
13
 * rights on a given wiki. It doesn't do any post-processing of that information.
14
 * @codeCoverageIgnore
15
 */
16
class UserRightsRepository extends Repository
17
{
18
    /**
19
     * Get user rights changes of the given user, including those made on Meta.
20
     * @param Project $project
21
     * @param User $user
22
     * @return array
23
     */
24
    public function getRightsChanges(Project $project, User $user)
25
    {
26
        $changes = $this->queryRightsChanges($project, $user);
27
28
        if ((bool)$this->container->hasParameter('app.is_labs')) {
29
            $changes = array_merge(
30
                $changes,
31
                $this->queryRightsChanges($project, $user, 'meta')
32
            );
33
        }
34
35
        return $changes;
36
    }
37
38
    /**
39
     * Get global user rights changes of the given user.
40
     * @param Project $project Global rights are always on Meta, so this
41
     *     Project instance is re-used if it is already Meta, otherwise
42
     *     a new Project instance is created.
43
     * @param User $user
44
     * @return array
45
     */
46
    public function getGlobalRightsChanges(Project $project, User $user)
47
    {
48
        return $this->queryRightsChanges($project, $user, 'global');
49
    }
50
51
    /**
52
     * User rights changes for given project, optionally fetched from Meta.
53
     * @param Project $project Global rights and Meta-changed rights will
54
     *     automatically use the Meta Project. This Project instance is re-used
55
     *     if it is already Meta, otherwise a new Project instance is created.
56
     * @param User $user
57
     * @param string $type One of 'local' - query the local rights log,
58
     *     'meta' - query for username@dbname for local rights changes made on Meta, or
59
     *     'global' - query for global rights changes.
60
     * @return array
61
     */
62
    private function queryRightsChanges(Project $project, User $user, $type = 'local')
63
    {
64
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_rightschanges');
65
        if ($this->cache->hasItem($cacheKey)) {
66
            return $this->cache->getItem($cacheKey)->get();
67
        }
68
69
        $dbName = $project->getDatabaseName();
70
71
        // Global rights and Meta-changed rights should use a Meta Project.
72
        if ($type !== 'local') {
73
            $dbName = 'metawiki';
74
        }
75
76
        $loggingTable = $this->getTableName($dbName, 'logging', 'logindex');
77
        $userTable = $this->getTableName($dbName, 'user');
78
        $username = str_replace(' ', '_', $user->getUsername());
79
80
        if ($type === 'meta') {
81
            // Reference the original Project.
82
            $username = $username.'@'.$project->getDatabaseName();
83
        }
84
85
        // Way back when it was possible to have usernames with lowercase characters.
86
        // Some log entires are caught unless we look for both variations.
87
        $usernameLower = lcfirst($username);
88
89
        $logType = $type == 'global' ? 'gblrights' : 'rights';
90
91
        $sql = "SELECT log_id, log_timestamp, log_comment, log_params, log_action,
92
                    IF(log_user_text != '', log_user_text, (
93
                        SELECT user_name
94
                        FROM $userTable
95
                        WHERE user_id = log_user
96
                    )) AS log_user_text,
97
                    '$type' AS type
98
                FROM $loggingTable
99
                WHERE log_type = '$logType'
100
                AND log_namespace = 2
101
                AND log_title IN (:username, :username2)
102
                ORDER BY log_timestamp DESC";
103
104
        $ret = $this->executeProjectsQuery($sql, [
105
            'username' => $username,
106
            'username2' => $usernameLower,
107
        ])->fetchAll();
108
109
        // Cache and return.
110
        return $this->setCache($cacheKey, $ret);
111
    }
112
113
    /**
114
     * Get the localized names for all user groups on given Project (and global),
115
     * fetched from on-wiki system messages.
116
     * @param Project $project
117
     * @param string $lang Language code to pass in.
118
     * @return string[] Localized names keyed by database value.
119
     */
120
    public function getRightsNames(Project $project, $lang)
121
    {
122
        $cacheKey = $this->getCacheKey(func_get_args(), 'project_rights_names');
123
        if ($this->cache->hasItem($cacheKey)) {
124
            return $this->cache->getItem($cacheKey)->get();
125
        }
126
127
        $rightsPaths = array_map(function ($right) {
128
            return "Group-$right-member";
129
        }, $this->getRawRightsNames($project));
130
131
        $rightsNames = [];
132
133
        for ($i = 0; $i < count($rightsPaths); $i += 50) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
134
            $rightsSlice = array_slice($rightsPaths, $i, 50);
135
            $params = [
136
                'action' => 'query',
137
                'meta' => 'allmessages',
138
                'ammessages' => implode('|', $rightsSlice),
139
                'amlang' => $lang,
140
                'amenableparser' => 1,
141
                'formatversion' => 2,
142
            ];
143
            $api = $this->getMediawikiApi($project);
144
            $query = new SimpleRequest('query', $params);
145
            $result = $api->getRequest($query)['query']['allmessages'];
146
147
            foreach ($result as $msg) {
148
                $normalized = preg_replace('/^group-|-member$/', '', $msg['normalizedname']);
149
                $rightsNames[$normalized] = isset($msg['content']) ? $msg['content'] : $normalized;
150
            }
151
        }
152
153
        // Cache for one day and return.
154
        return $this->setCache($cacheKey, $rightsNames, 'P1D');
155
    }
156
157
    /**
158
     * Get the names of all the possible local and global user groups.
159
     * @param Project $project
160
     * @return string[]
161
     */
162
    private function getRawRightsNames(Project $project)
163
    {
164
        $ugTable = $project->getTableName('user_groups');
165
        $ufgTable = $project->getTableName('user_former_groups');
166
        $sql = "SELECT DISTINCT(ug_group)
167
                FROM $ugTable
168
                UNION
169
                SELECT DISTINCT(ufg_group)
170
                FROM $ufgTable";
171
        if ($this->isLabs()) {
172
            $sql .= "UNION SELECT DISTINCT(gug_group)
173
                     FROM centralauth_p.global_user_groups";
174
        }
175
176
        $groups = $this->executeProjectsQuery($sql)->fetchAll(\PDO::FETCH_COLUMN);
177
178
        // WMF installations have a special 'autoconfirmed' user group.
179
        if ($this->isLabs()) {
180
            $groups[] = 'autoconfirmed';
181
        }
182
183
        return array_unique($groups);
184
    }
185
186
    /**
187
     * Get the threshold values to become autoconfirmed for the given Project.
188
     * Yes, eval is bad, but here we're validating only mathematical expressions are ran.
189
     * @param Project $project
190
     * @return array With keys 'wgAutoConfirmAge' and 'wgAutoConfirmCount'.
191
     */
192
    public function getAutoconfirmedAgeAndCount(Project $project)
193
    {
194
        if (!$this->isLabs()) {
195
            return null;
196
        }
197
198
        // Set up cache.
199
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_rightschanges_autoconfirmed');
200
        if ($this->cache->hasItem($cacheKey)) {
201
            return $this->cache->getItem($cacheKey)->get();
202
        }
203
204
        $client = new GuzzleHttp\Client();
205
        $url = 'https://noc.wikimedia.org/conf/InitialiseSettings.php.txt';
206
207
        $contents = $client->request('GET', $url)
208
            ->getBody()
209
            ->getContents();
210
211
        $dbname = $project->getDatabaseName();
212
        $out = [];
213
214
        foreach (['wgAutoConfirmAge', 'wgAutoConfirmCount'] as $type) {
215
            // Extract the text of the file that contains the rules we're looking for.
216
            $typeRegex = "/\'$type.*?\]/s";
217
            $matches = [];
218
            if (1 === preg_match($typeRegex, $contents, $matches)) {
219
                $group = $matches[0];
220
221
                // Find the autoconfirmed expression for the $type and $dbname.
222
                $matches = [];
223
                $regex = "/\'$dbname\'\s*\=\>\s*([\d\*\s]+)/s";
224
                if (1 === preg_match($regex, $group, $matches)) {
225
                    $out[$type] = (int)eval('return('.$matches[1].');');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
226
                    continue;
227
                }
228
229
                // Find the autoconfirmed expression for the 'default' and $dbname.
230
                $matches = [];
231
                $regex = "/\'default\'\s*\=\>\s*([\d\*\s]+)/s";
232
                if (1 === preg_match($regex, $group, $matches)) {
233
                    $out[$type] = (int)eval('return('.$matches[1].');');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
234
                    continue;
235
                }
236
            } else {
237
                return null;
238
            }
239
        }
240
241
        // Cache for one day and return.
242
        return $this->setCache($cacheKey, $out, 'P1D');
243
    }
244
245
    /**
246
     * Get the timestamp of the nth edit made by the given user.
247
     * @param Project $project
248
     * @param User $user
249
     * @param string $acDate Date in YYYYMMDDHHSS format.
250
     * @param int $edits Offset of rows to look for (edit threshold for autoconfirmed).
251
     * @return string Timestamp in YYYYMMDDHHSS format.
252
     */
253
    public function getNthEditTimestamp(Project $project, User $user, $acDate, $edits)
254
    {
255
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_rightschanges_nthtimestamp');
256
        if ($this->cache->hasItem($cacheKey)) {
257
            return $this->cache->getItem($cacheKey)->get();
258
        }
259
260
        $revisionTable = $project->getTableName('revision');
261
        $sql = "SELECT rev_timestamp
262
                FROM $revisionTable
263
                WHERE rev_user = :userId
264
                AND rev_timestamp >= $acDate
265
                LIMIT 1 OFFSET ".($edits - 1);
266
267
        $ret = $this->executeProjectsQuery($sql, [
268
            'userId' => $user->getId($project),
269
        ])->fetchColumn();
270
271
        // Cache and return.
272
        return $this->setCache($cacheKey, $ret);
273
    }
274
275
    /**
276
     * Get the number of edits the user has made as of the given timestamp.
277
     * @param Project $project
278
     * @param User $user
279
     * @param string $timestamp In YYYYMMDDHHSS format.
280
     * @return int
281
     */
282
    public function getNumEditsByTimestamp(Project $project, User $user, $timestamp)
283
    {
284
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_rightschanges_editstimestamp');
285
        if ($this->cache->hasItem($cacheKey)) {
286
            return $this->cache->getItem($cacheKey)->get();
287
        }
288
289
        $revisionTable = $project->getTableName('revision');
290
        $sql = "SELECT COUNT(rev_id)
291
                FROM $revisionTable
292
                WHERE rev_user = :userId
293
                AND rev_timestamp <= $timestamp";
294
295
        $ret = (int)$this->executeProjectsQuery($sql, [
296
            'userId' => $user->getId($project),
297
        ])->fetchColumn();
298
299
        // Cache and return.
300
        return $this->setCache($cacheKey, $ret);
301
    }
302
}
303