Passed
Push — main ( d49740...410c99 )
by MusikAnimal
04:06
created

UserRepository::getEditCount()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 2
dl 0
loc 12
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Repository;
6
7
use App\Model\Project;
8
use App\Model\User;
9
use Doctrine\DBAL\Driver\ResultStatement;
10
use Doctrine\Persistence\ManagerRegistry;
11
use GuzzleHttp\Client;
12
use Psr\Cache\CacheItemPoolInterface;
13
use Psr\Log\LoggerInterface;
14
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
15
use Symfony\Component\HttpFoundation\RequestStack;
16
use Wikimedia\IPUtils;
17
18
/**
19
 * This class provides data for the User class.
20
 * @codeCoverageIgnore
21
 */
22
class UserRepository extends Repository
23
{
24
    protected ProjectRepository $projectRepo;
25
    protected RequestStack $requestStack;
26
27
    /**
28
     * @param ManagerRegistry $managerRegistry
29
     * @param CacheItemPoolInterface $cache
30
     * @param Client $guzzle
31
     * @param LoggerInterface $logger
32
     * @param ParameterBagInterface $parameterBag
33
     * @param bool $isWMF
34
     * @param int $queryTimeout
35
     * @param ProjectRepository $projectRepo
36
     * @param RequestStack $requestStack
37
     */
38
    public function __construct(
39
        ManagerRegistry $managerRegistry,
40
        CacheItemPoolInterface $cache,
41
        Client $guzzle,
42
        LoggerInterface $logger,
43
        ParameterBagInterface $parameterBag,
44
        bool $isWMF,
45
        int $queryTimeout,
46
        ProjectRepository $projectRepo,
47
        RequestStack $requestStack
48
    ) {
49
        $this->projectRepo = $projectRepo;
50
        $this->requestStack = $requestStack;
51
        parent::__construct($managerRegistry, $cache, $guzzle, $logger, $parameterBag, $isWMF, $queryTimeout);
52
    }
53
54
    /**
55
     * Get the user's ID and registration date.
56
     * @param string $databaseName The database to query.
57
     * @param string $username The username to find.
58
     * @return array|false With keys 'userId' and regDate'. false if user not found.
59
     */
60
    public function getIdAndRegistration(string $databaseName, string $username)
61
    {
62
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_id_reg');
63
        if ($this->cache->hasItem($cacheKey)) {
64
            return $this->cache->getItem($cacheKey)->get();
65
        }
66
67
        $userTable = $this->getTableName($databaseName, 'user');
68
        $sql = "SELECT user_id AS userId, user_registration AS regDate
69
                FROM $userTable
70
                WHERE user_name = :username
71
                LIMIT 1";
72
        $resultQuery = $this->executeProjectsQuery($databaseName, $sql, ['username' => $username]);
73
74
        // Cache and return.
75
        return $this->setCache($cacheKey, $resultQuery->fetchAssociative());
76
    }
77
78
    /**
79
     * Get the user's actor ID.
80
     * @param string $databaseName
81
     * @param string $username
82
     * @return int|null
83
     */
84
    public function getActorId(string $databaseName, string $username): ?int
85
    {
86
        if (IPUtils::isValidRange($username)) {
87
            return null;
88
        }
89
90
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_actor_id');
91
        if ($this->cache->hasItem($cacheKey)) {
92
            return (int)$this->cache->getItem($cacheKey)->get();
93
        }
94
95
        $actorTable = $this->getTableName($databaseName, 'actor');
96
97
        $sql = "SELECT actor_id
98
                FROM $actorTable
99
                WHERE actor_name = :username
100
                LIMIT 1";
101
        $resultQuery = $this->executeProjectsQuery($databaseName, $sql, ['username' => $username]);
102
103
        // Cache and return.
104
        return (int)$this->setCache($cacheKey, $resultQuery->fetchOne());
105
    }
106
107
    /**
108
     * Get the user's (system) edit count.
109
     * @param string $databaseName The database to query.
110
     * @param string $username The username to find.
111
     * @return int As returned by the database.
112
     */
113
    public function getEditCount(string $databaseName, string $username): int
114
    {
115
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_edit_count');
116
        if ($this->cache->hasItem($cacheKey)) {
117
            return (int)$this->cache->getItem($cacheKey)->get();
118
        }
119
120
        $userTable = $this->getTableName($databaseName, 'user');
121
        $sql = "SELECT user_editcount FROM $userTable WHERE user_name = :username LIMIT 1";
122
        $resultQuery = $this->executeProjectsQuery($databaseName, $sql, ['username' => $username]);
123
124
        return (int)$this->setCache($cacheKey, $resultQuery->fetchColumn());
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\ForwardCom...y\Result::fetchColumn() has been deprecated: Use fetchOne() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

124
        return (int)$this->setCache($cacheKey, /** @scrutinizer ignore-deprecated */ $resultQuery->fetchColumn());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
125
    }
126
127
    /**
128
     * Search the ipblocks table to see if the user is currently blocked and return the expiry if they are.
129
     * @param string $databaseName The database to query.
130
     * @param string $username The username of the user to search for.
131
     * @return bool|string Expiry of active block or false
132
     */
133
    public function getBlockExpiry(string $databaseName, string $username)
134
    {
135
        $ipblocksTable = $this->getTableName($databaseName, 'ipblocks', 'ipindex');
136
        $sql = "SELECT ipb_expiry
137
                FROM $ipblocksTable
138
                WHERE ipb_address = :username
139
                LIMIT 1";
140
        $resultQuery = $this->executeProjectsQuery($databaseName, $sql, ['username' => $username]);
141
        return $resultQuery->fetchOne();
142
    }
143
144
    /**
145
     * Get edit count within given timeframe and namespace.
146
     * @param Project $project
147
     * @param User $user
148
     * @param int|string $namespace Namespace ID or 'all' for all namespaces
149
     * @param int|false $start Start date as Unix timestamp.
150
     * @param int|false $end End date as Unix timestamp.
151
     * @return int
152
     */
153
    public function countEdits(Project $project, User $user, $namespace = 'all', $start = false, $end = false): int
154
    {
155
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_editcount');
156
        if ($this->cache->hasItem($cacheKey)) {
157
            return (int)$this->cache->getItem($cacheKey)->get();
158
        }
159
160
        $revDateConditions = $this->getDateConditions($start, $end);
161
        [$pageJoin, $condNamespace] = $this->getPageAndNamespaceSql($project, $namespace);
162
        $revisionTable = $project->getTableName('revision');
163
        $params = [];
164
165
        if ($user->isAnon()) {
166
            [$params['startIp'], $params['endIp']] = IPUtils::parseRange($user->getUsername());
167
            $ipcTable = $project->getTableName('ip_changes');
168
            $sql = "SELECT COUNT(ipc_rev_id)
169
                    FROM $ipcTable
170
                    JOIN $revisionTable ON ipc_rev_id = rev_id
171
                    $pageJoin
172
                    WHERE ipc_hex BETWEEN :startIp AND :endIp
173
                    $condNamespace
174
                    $revDateConditions";
175
        } else {
176
            $sql = "SELECT COUNT(rev_id)
177
                FROM $revisionTable
178
                $pageJoin
179
                WHERE rev_actor = :actorId
180
                $condNamespace
181
                $revDateConditions";
182
        }
183
184
        $resultQuery = $this->executeQuery($sql, $project, $user, $namespace, $params);
185
        $result = (int)$resultQuery->fetchOne();
186
187
        // Cache and return.
188
        return $this->setCache($cacheKey, $result);
189
    }
190
191
    /**
192
     * Get information about the currently-logged in user.
193
     * @return array|object|null null if not logged in.
194
     */
195
    public function getXtoolsUserInfo()
196
    {
197
        return $this->requestStack->getSession()->get('logged_in_user');
198
    }
199
200
    /**
201
     * Number of edits which if exceeded, will require the user to log in.
202
     * @return int
203
     */
204
    public function numEditsRequiringLogin(): int
205
    {
206
        return (int)$this->parameterBag->get('app.num_edits_requiring_login');
207
    }
208
209
    /**
210
     * Maximum number of edits to process, based on configuration.
211
     * @return int
212
     */
213
    public function maxEdits(): int
214
    {
215
        return (int)$this->parameterBag->get('app.max_user_edits');
216
    }
217
218
    /**
219
     * Get SQL clauses for joining on `page` and restricting to a namespace.
220
     * @param Project $project
221
     * @param int|string $namespace Namespace ID or 'all' for all namespaces.
222
     * @return array [page join clause, page namespace clause]
223
     */
224
    protected function getPageAndNamespaceSql(Project $project, $namespace): array
225
    {
226
        if ('all' === $namespace) {
227
            return [null, null];
228
        }
229
230
        $pageTable = $project->getTableName('page');
231
        $pageJoin = "LEFT JOIN $pageTable ON rev_page = page_id";
232
        $condNamespace = 'AND page_namespace = :namespace';
233
234
        return [$pageJoin, $condNamespace];
235
    }
236
237
    /**
238
     * Get SQL fragments for filtering by user.
239
     * Used in self::getPagesCreatedInnerSql().
240
     * @param bool $dateFiltering Whether the query you're working with has date filtering.
241
     *   If false, a clause to check timestamp > 1 is added to force use of the timestamp index.
242
     * @return string[] Keys 'whereRev' and 'whereArc'.
243
     */
244
    public function getUserConditions(bool $dateFiltering = false): array
245
    {
246
        return [
247
            'whereRev' => " rev_actor = :actorId ".($dateFiltering ? '' : "AND rev_timestamp > 1 "),
248
            'whereArc' => " ar_actor = :actorId ".($dateFiltering ? '' : "AND ar_timestamp > 1 "),
249
        ];
250
    }
251
252
    /**
253
     * Prepare the given SQL, bind the given parameters, and execute the Doctrine Statement.
254
     * @param string $sql
255
     * @param Project $project
256
     * @param User $user
257
     * @param int|string|null $namespace Namespace ID, or 'all'/null for all namespaces.
258
     * @param array $extraParams Will get merged in the params array used for binding values.
259
     * @return ResultStatement
260
     */
261
    protected function executeQuery(
262
        string $sql,
263
        Project $project,
264
        User $user,
265
        $namespace = 'all',
266
        array $extraParams = []
267
    ): ResultStatement {
268
        $params = ['actorId' => $user->getActorId($project)];
269
270
        if ('all' !== $namespace) {
271
            $params['namespace'] = $namespace;
272
        }
273
274
        return $this->executeProjectsQuery($project, $sql, array_merge($params, $extraParams));
275
    }
276
277
    /**
278
     * Check if a user exists globally.
279
     * @param User $user
280
     * @return bool
281
     */
282
    public function existsGlobally(User $user): bool
283
    {
284
        if ($user->isAnon()) {
285
            return true;
286
        }
287
288
        return (bool)$this->executeProjectsQuery(
289
            'centralauth',
290
            'SELECT 1 FROM centralauth_p.globaluser WHERE gu_name = :username',
291
            ['username' => $user->getUsername()]
292
        )->fetchFirstColumn();
293
    }
294
295
    /**
296
     * Get a user's local user rights on the given Project.
297
     * @param Project $project
298
     * @param User $user
299
     * @return string[]
300
     */
301
    public function getUserRights(Project $project, User $user): array
302
    {
303
        if ($user->isAnon()) {
304
            return [];
305
        }
306
307
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_rights');
308
        if ($this->cache->hasItem($cacheKey)) {
309
            return $this->cache->getItem($cacheKey)->get();
310
        }
311
312
        $userGroupsTable = $project->getTableName('user_groups');
313
        $userTable = $project->getTableName('user');
314
315
        $sql = "SELECT ug_group
316
                FROM $userGroupsTable
317
                JOIN $userTable ON user_id = ug_user
318
                WHERE user_name = :username
319
                AND (ug_expiry IS NULL OR ug_expiry > CURRENT_TIMESTAMP)";
320
321
        $ret = $this->executeProjectsQuery($project, $sql, [
322
            'username' => $user->getUsername(),
323
        ])->fetchFirstColumn();
324
325
        // Cache and return.
326
        return $this->setCache($cacheKey, $ret);
327
    }
328
329
    /**
330
     * Get a user's global group membership (starting at XTools' default project if none is
331
     * provided). This requires the CentralAuth extension to be installed.
332
     * @link https://www.mediawiki.org/wiki/Extension:CentralAuth
333
     * @param string $username The username.
334
     * @param Project|null $project The project to query.
335
     * @return string[]
336
     */
337
    public function getGlobalUserRights(string $username, ?Project $project = null): array
338
    {
339
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_global_groups');
340
        if ($this->cache->hasItem($cacheKey)) {
341
            return $this->cache->getItem($cacheKey)->get();
342
        }
343
344
        // Get the default project if not provided.
345
        if (!$project instanceof Project) {
346
            $project = $this->projectRepo->getDefaultProject();
347
        }
348
349
        $params = [
350
            'meta' => 'globaluserinfo',
351
            'guiuser' => $username,
352
            'guiprop' => 'groups',
353
        ];
354
355
        $res = $this->executeApiRequest($project, $params);
356
        $result = [];
357
        if (isset($res['batchcomplete']) && isset($res['query']['globaluserinfo']['groups'])) {
358
            $result = $res['query']['globaluserinfo']['groups'];
359
        }
360
361
        // Cache and return.
362
        return $this->setCache($cacheKey, $result);
363
    }
364
}
365