Passed
Push — master ( 5f527e...bcf4fb )
by MusikAnimal
04:22
created

UserRepository::getGroups()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 17
nc 3
nop 2
dl 0
loc 27
rs 8.5806
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the UserRepository class.
4
 */
5
6
namespace Xtools;
7
8
use Exception;
9
use Mediawiki\Api\SimpleRequest;
10
use Symfony\Component\DependencyInjection\Container;
11
use Symfony\Component\HttpFoundation\Session\Session;
12
13
/**
14
 * This class provides data for the User class.
15
 * @codeCoverageIgnore
16
 */
17
class UserRepository extends Repository
18
{
19
    /**
20
     * Convenience method to get a new User object.
21
     * @param string $username The username.
22
     * @param Container $container The DI container.
23
     * @return User
24
     */
25
    public static function getUser($username, Container $container)
26
    {
27
        $user = new User($username);
28
        $userRepo = new UserRepository();
29
        $userRepo->setContainer($container);
30
        $user->setRepository($userRepo);
31
        return $user;
32
    }
33
34
    /**
35
     * Get the user's ID.
36
     * @param string $databaseName The database to query.
37
     * @param string $username The username to find.
38
     * @return int
39
     */
40 View Code Duplication
    public function getId($databaseName, $username)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
41
    {
42
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_id');
43
        if ($this->cache->hasItem($cacheKey)) {
44
            return $this->cache->getItem($cacheKey)->get();
45
        }
46
47
        $userTable = $this->getTableName($databaseName, 'user');
48
        $sql = "SELECT user_id FROM $userTable WHERE user_name = :username LIMIT 1";
49
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
50
        $resultQuery->bindParam('username', $username);
51
        $resultQuery->execute();
52
        $userId = (int)$resultQuery->fetchColumn();
53
54
        // Cache for 10 minutes and return.
55
        $this->setCache($cacheKey, $userId);
56
        return $userId;
57
    }
58
59
    /**
60
     * Get the user's registration date.
61
     * @param string $databaseName The database to query.
62
     * @param string $username The username to find.
63
     * @return string|null As returned by the database.
64
     */
65 View Code Duplication
    public function getRegistrationDate($databaseName, $username)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
66
    {
67
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_registration');
68
        if ($this->cache->hasItem($cacheKey)) {
69
            return $this->cache->getItem($cacheKey)->get();
70
        }
71
72
        $userTable = $this->getTableName($databaseName, 'user');
73
        $sql = "SELECT user_registration FROM $userTable WHERE user_name = :username LIMIT 1";
74
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
75
        $resultQuery->bindParam('username', $username);
76
        $resultQuery->execute();
77
        $registrationDate = $resultQuery->fetchColumn();
78
79
        // Cache and return.
80
        $this->setCache($cacheKey, $registrationDate);
81
        return $registrationDate;
82
    }
83
84
    /**
85
     * Get the user's (system) edit count.
86
     * @param string $databaseName The database to query.
87
     * @param string $username The username to find.
88
     * @return int|null As returned by the database.
89
     */
90 View Code Duplication
    public function getEditCount($databaseName, $username)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
91
    {
92
        $userTable = $this->getTableName($databaseName, 'user');
93
        $sql = "SELECT user_editcount FROM $userTable WHERE user_name = :username LIMIT 1";
94
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
95
        $resultQuery->bindParam('username', $username);
96
        $resultQuery->execute();
97
        return $resultQuery->fetchColumn();
98
    }
99
100
    /**
101
     * Get group names of the given user.
102
     * @param Project $project The project.
103
     * @param string $username The username.
104
     * @return string[]
105
     */
106
    public function getGroups(Project $project, $username)
107
    {
108
        // Use md5 to ensure the key does not contain reserved characters.
109
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_groups');
110
        if ($this->cache->hasItem($cacheKey)) {
111
            return $this->cache->getItem($cacheKey)->get();
112
        }
113
114
        $this->stopwatch->start($cacheKey, 'XTools');
115
        $api = $this->getMediawikiApi($project);
116
        $params = [
117
            'list' => 'users',
118
            'ususers' => $username,
119
            'usprop' => 'groups'
120
        ];
121
        $query = new SimpleRequest('query', $params);
122
        $result = [];
123
        $res = $api->getRequest($query);
124
        if (isset($res['batchcomplete']) && isset($res['query']['users'][0]['groups'])) {
125
            $result = $res['query']['users'][0]['groups'];
126
        }
127
128
        // Cache for 10 minutes, and return.
129
        $this->setCache($cacheKey, $result);
130
        $this->stopwatch->stop($cacheKey);
131
132
        return $result;
133
    }
134
135
    /**
136
     * Get a user's global group membership (starting at XTools' default project if none is
137
     * provided). This requires the CentralAuth extension to be installed.
138
     * @link https://www.mediawiki.org/wiki/Extension:CentralAuth
139
     * @param string $username The username.
140
     * @param Project $project The project to query.
141
     * @return string[]
142
     */
143
    public function getGlobalGroups($username, Project $project = null)
144
    {
145
        // Get the default project if not provided.
146
        if (!$project instanceof Project) {
147
            $project = ProjectRepository::getDefaultProject($this->container);
148
        }
149
150
        // Create the API query.
151
        $api = $this->getMediawikiApi($project);
152
        $params = [
153
            'meta' => 'globaluserinfo',
154
            'guiuser' => $username,
155
            'guiprop' => 'groups'
156
        ];
157
        $query = new SimpleRequest('query', $params);
158
159
        // Get the result.
160
        $res = $api->getRequest($query);
161
        $result = [];
162
        if (isset($res['batchcomplete']) && isset($res['query']['globaluserinfo']['groups'])) {
163
            $result = $res['query']['globaluserinfo']['groups'];
164
        }
165
        return $result;
166
    }
167
168
    /**
169
     * Search the ipblocks table to see if the user is currently blocked
170
     * and return the expiry if they are.
171
     * @param $databaseName The database to query.
0 ignored issues
show
Bug introduced by
The type Xtools\The was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
172
     * @param $userid The ID of the user to search for.
173
     * @return bool|string Expiry of active block or false
174
     */
175 View Code Duplication
    public function getBlockExpiry($databaseName, $userid)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
176
    {
177
        $ipblocksTable = $this->getTableName($databaseName, 'ipblocks');
178
        $sql = "SELECT ipb_expiry
179
                FROM $ipblocksTable
180
                WHERE ipb_user = :userid
181
                LIMIT 1";
182
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
183
        $resultQuery->bindParam('userid', $userid);
184
        $resultQuery->execute();
185
        return $resultQuery->fetchColumn();
186
    }
187
188
    /**
189
     * Count the number of pages created by a user.
190
     * @param Project $project
191
     * @param User $user
192
     * @param string|int $namespace Namespace ID or 'all'.
193
     * @param string $redirects     One of 'noredirects', 'onlyredirects' or blank for both.
194
     * @return string[] Result of query, see below. Includes live and deleted pages.
195
     */
196
    public function countPagesCreated(
197
        Project $project,
198
        User $user,
199
        $namespace,
200
        $redirects
201
    ) {
202
        $cacheKey = $this->getCacheKey(func_get_args(), 'num_user_pages_created');
203
        if ($this->cache->hasItem($cacheKey)) {
204
            return $this->cache->getItem($cacheKey)->get();
205
        }
206
207
        $conditions = [
208
            'paSelects' => '',
209
            'paSelectsArchive' => '',
210
            'paJoin' => '',
211
            'revPageGroupBy' => '',
212
        ];
213
        $conditions = array_merge(
214
            $conditions,
215
            $this->getNamespaceAndRedirectConditions($namespace, $redirects),
216
            $this->getUserConditions($project, $user)
217
        );
218
219
        $sql = "SELECT namespace,
220
                    COUNT(page_title) AS count,
221
                    SUM(CASE WHEN type = 'arc' THEN 1 ELSE 0 END) AS deleted,
222
                    SUM(page_is_redirect) AS redirects
223
                FROM (".
224
                    $this->getPagesCreatedInnerSql($project, $conditions)."
225
                ) a
226
                GROUP BY namespace";
227
228
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
229
        $resultQuery->execute();
230
        $result = $resultQuery->fetchAll();
231
232
        // Cache for 10 minutes, and return.
233
        $this->setCache($cacheKey, $result);
234
235
        return $result;
236
    }
237
238
    /**
239
     * Get pages created by a user.
240
     * @param Project $project
241
     * @param User $user
242
     * @param string|int $namespace Namespace ID or 'all'.
243
     * @param string $redirects     One of 'noredirects', 'onlyredirects' or blank for both.
244
     * @param int|null $limit       Number of results to return, or blank to return all.
245
     * @param int $offset           Number of pages past the initial dataset. Used for pagination.
246
     * @return string[] Result of query, see below. Includes live and deleted pages.
247
     */
248
    public function getPagesCreated(
249
        Project $project,
250
        User $user,
251
        $namespace,
252
        $redirects,
253
        $limit = 1000,
254
        $offset = 0
255
    ) {
256
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_pages_created');
257
        if ($this->cache->hasItem($cacheKey)) {
258
            return $this->cache->getItem($cacheKey)->get();
259
        }
260
        $this->stopwatch->start($cacheKey, 'XTools');
261
262
        $conditions = [
263
            'paSelects' => '',
264
            'paSelectsArchive' => '',
265
            'paJoin' => '',
266
            'revPageGroupBy' => '',
267
        ];
268
269
        $conditions = array_merge(
270
            $conditions,
271
            $this->getNamespaceAndRedirectConditions($namespace, $redirects),
272
            $this->getUserConditions($project, $user)
273
        );
274
275
        $pageAssessmentsTable = $project->getTableName('page_assessments');
276
277
        $hasPageAssessments = $this->isLabs() && $project->hasPageAssessments();
278
        if ($hasPageAssessments) {
279
            $conditions['paSelects'] = ', pa_class, pa_importance, pa_page_revision';
280
            $conditions['paSelectsArchive'] = ', NULL AS pa_class, NULL AS pa_page_id, '.
281
                'NULL AS pa_page_revision';
282
            $conditions['paJoin'] = "LEFT JOIN $pageAssessmentsTable ON rev_page = pa_page_id";
283
            $conditions['revPageGroupBy'] = 'GROUP BY rev_page';
284
        }
285
286
        $sql = "SELECT * FROM (".
287
                    $this->getPagesCreatedInnerSql($project, $conditions)."
288
                ) a ".(!empty($limit) ? "LIMIT $limit OFFSET $offset" : '');
289
290
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
291
        $resultQuery->execute();
292
        $result = $resultQuery->fetchAll();
293
294
        // Cache for 10 minutes, and return.
295
        $this->setCache($cacheKey, $result);
296
        $this->stopwatch->stop($cacheKey);
297
298
        return $result;
299
    }
300
301
    /**
302
     * Get SQL fragments for the namespace and redirects,
303
     * to be used in self::getPagesCreatedInnerSql().
304
     * @param  string|int $namespace Namespace ID or 'all'.
305
     * @param  string $redirects     One of 'noredirects', 'onlyredirects' or blank for both.
306
     * @return string[] With keys 'namespaceRev', 'namespaceArc' and 'redirects'
307
     */
308
    private function getNamespaceAndRedirectConditions($namespace, $redirects)
309
    {
310
        $conditions = [
311
            'namespaceArc' => '',
312
            'namespaceRev' => '',
313
            'redirects' => '',
314
        ];
315
316
        if ($namespace !== 'all') {
317
            $conditions['namespaceRev'] = " AND page_namespace = '".intval($namespace)."' ";
318
            $conditions['namespaceArc'] = " AND ar_namespace = '".intval($namespace)."' ";
319
        }
320
321
        if ($redirects == 'onlyredirects') {
322
            $conditions['redirects'] = " AND page_is_redirect = '1' ";
323
        } elseif ($redirects == 'noredirects') {
324
            $conditions['redirects'] = " AND page_is_redirect = '0' ";
325
        }
326
327
        return $conditions;
328
    }
329
330
    /**
331
     * Get SQL fragments for rev_user or rev_user_text, depending on if the user is logged out.
332
     * Used in self::getPagesCreatedInnerSql().
333
     * @param  Project $project
334
     * @param  User $user
335
     * @return string[] Keys 'whereRev' and 'whereArc'.
336
     */
337
    private function getUserConditions(Project $project, User $user)
338
    {
339
        $username = $user->getUsername();
340
        $userId = $user->getId($project);
341
342
        if ($userId == 0) { // IP Editor or undefined username.
343
            return [
344
                'whereRev' => " rev_user_text = '$username' AND rev_user = '0' ",
345
                'whereArc' => " ar_user_text = '$username' AND ar_user = '0' ",
346
            ];
347
        } else {
348
            return [
349
                'whereRev' => " rev_user = '$userId' AND rev_timestamp > 1 ",
350
                'whereArc' => " ar_user = '$userId' AND ar_timestamp > 1 ",
351
            ];
352
        }
353
    }
354
355
    /**
356
     * Inner SQL for getting or counting pages created by the user.
357
     * @param  Project  $project
358
     * @param  string[] $conditions Conditions for the SQL, must include 'paSelects',
359
     *     'paSelectsArchive', 'paJoin', 'whereRev', 'whereArc', 'namespaceRev', 'namespaceArc',
360
     *     'redirects' and 'revPageGroupBy'.
361
     * @return string Raw SQL.
362
     */
363
    private function getPagesCreatedInnerSql(Project $project, $conditions)
364
    {
365
        $pageTable = $project->getTableName('page');
366
        $revisionTable = $project->getTableName('revision');
367
        $archiveTable = $project->getTableName('archive');
368
        $logTable = $project->getTableName('logging', 'logindex');
369
370
        return "
371
            (
372
                SELECT DISTINCT page_namespace AS namespace, 'rev' AS type, page_title AS page_title,
373
                    page_len, page_is_redirect, rev_timestamp AS rev_timestamp,
374
                    rev_user, rev_user_text AS username, rev_len, rev_id ".$conditions['paSelects']."
375
                FROM $pageTable
376
                JOIN $revisionTable ON page_id = rev_page ".
377
                $conditions['paJoin']."
378
                WHERE ".$conditions['whereRev']."
379
                    AND rev_parent_id = '0'".
380
                    $conditions['namespaceRev'].
381
                    $conditions['redirects'].
382
                $conditions['revPageGroupBy']."
383
            )
384
385
            UNION
386
387
            (
388
                SELECT ar_namespace AS namespace, 'arc' AS type, ar_title AS page_title,
389
                    0 AS page_len, '0' AS page_is_redirect, MIN(ar_timestamp) AS rev_timestamp,
390
                    ar_user AS rev_user, ar_user_text AS username, ar_len AS rev_len,
391
                    ar_rev_id AS rev_id ".$conditions['paSelectsArchive']."
392
                FROM $archiveTable
393
                LEFT JOIN $logTable ON log_namespace = ar_namespace AND log_title = ar_title
394
                    AND log_user = ar_user AND (log_action = 'move' OR log_action = 'move_redir')
395
                    AND log_type = 'move'
396
                WHERE ".$conditions['whereArc']."
397
                    AND ar_parent_id = '0' ".
398
                    $conditions['namespaceArc']."
399
                    AND log_action IS NULL
400
                GROUP BY ar_namespace, ar_title
401
            )
402
        ";
403
    }
404
405
    /**
406
     * Get edit count within given timeframe and namespace.
407
     * @param Project $project
408
     * @param User $user
409
     * @param int|string $namespace Namespace ID or 'all' for all namespaces
410
     * @param string $start Start date in a format accepted by strtotime()
411
     * @param string $end End date in a format accepted by strtotime()
412
     */
413
    public function countEdits(Project $project, User $user, $namespace = 'all', $start = '', $end = '')
414
    {
415
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_editcount');
416
        if ($this->cache->hasItem($cacheKey)) {
417
            return $this->cache->getItem($cacheKey)->get();
418
        }
419
420
        list($condBegin, $condEnd) = $this->getRevTimestampConditions($start, $end);
421
        list($pageJoin, $condNamespace) = $this->getPageAndNamespaceSql($project, $namespace);
422
        $revisionTable = $project->getTableName('revision');
423
424
        $sql = "SELECT COUNT(rev_id)
425
                FROM $revisionTable
426
                $pageJoin
427
                WHERE rev_user_text = :username
428
                $condNamespace
429
                $condBegin
430
                $condEnd";
431
432
        $resultQuery = $this->executeQuery($sql, $user, $namespace, $start, $end);
433
        $result = $resultQuery->fetchColumn();
434
435
        // Cache for 10 minutes, and return.
436
        $this->setCache($cacheKey, $result);
437
438
        return $result;
439
    }
440
441
    /**
442
     * Get information about the currently-logged in user.
443
     * @return array
444
     */
445
    public function getXtoolsUserInfo()
446
    {
447
        /** @var Session $session */
448
        $session = $this->container->get('session');
449
        return $session->get('logged_in_user');
450
    }
451
452
    /**
453
     * Maximum number of edits to process, based on configuration.
454
     * @return int
455
     */
456
    public function maxEdits()
457
    {
458
        return $this->container->getParameter('app.max_user_edits');
459
    }
460
461
    /**
462
     * Get SQL clauses for joining on `page` and restricting to a namespace.
463
     * @param  Project $project
464
     * @param  int|string $namespace Namespace ID or 'all' for all namespaces.
465
     * @return array [page join clause, page namespace clause]
466
     */
467
    protected function getPageAndNamespaceSql(Project $project, $namespace)
468
    {
469
        if ($namespace === 'all') {
470
            return [null, null];
471
        }
472
473
        $pageTable = $project->getTableName('page');
474
        $pageJoin = $namespace !== 'all' ? "LEFT JOIN $pageTable ON rev_page = page_id" : null;
475
        $condNamespace = 'AND page_namespace = :namespace';
476
477
        return [$pageJoin, $condNamespace];
478
    }
479
480
    /**
481
     * Get SQL clauses for rev_timestamp, based on whether values for
482
     * the given start and end parameters exist.
483
     * @param  string $start
484
     * @param  string $end
485
     * @return string[] Clauses for start and end timestamps.
486
     */
487
    protected function getRevTimestampConditions($start, $end)
488
    {
489
        $condBegin = '';
490
        $condEnd = '';
491
492
        if (!empty($start)) {
493
            $condBegin = 'AND rev_timestamp >= :start ';
494
        }
495
        if (!empty($end)) {
496
            $condEnd = 'AND rev_timestamp <= :end ';
497
        }
498
499
        return [$condBegin, $condEnd];
500
    }
501
502
    /**
503
     * Prepare the given SQL, bind the given parameters, and execute the Doctrine Statement.
504
     * @param  string $sql
505
     * @param  User   $user
506
     * @param  string $namespace
507
     * @param  string $start
508
     * @param  string $end
509
     * @return \Doctrine\DBAL\Statement
510
     */
511
    protected function executeQuery($sql, User $user, $namespace = 'all', $start = '', $end = '')
512
    {
513
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
514
        $username = $user->getUsername();
515
        $resultQuery->bindParam('username', $username);
516
517
        if (!empty($start)) {
518
            $start = date('Ymd000000', strtotime($start));
0 ignored issues
show
Bug introduced by
It seems like strtotime($start) can also be of type false; however, parameter $timestamp of date() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

518
            $start = date('Ymd000000', /** @scrutinizer ignore-type */ strtotime($start));
Loading history...
519
            $resultQuery->bindParam('start', $start);
520
        }
521
        if (!empty($end)) {
522
            $end = date('Ymd235959', strtotime($end));
523
            $resultQuery->bindParam('end', $end);
524
        }
525
        if ($namespace !== 'all') {
526
            $resultQuery->bindParam('namespace', $namespace);
527
        }
528
529
        $resultQuery->execute();
530
531
        return $resultQuery;
532
    }
533
}
534