Completed
Pull Request — master (#143)
by Albert
11:10 queued 06:06
created

getNamespaceRedirectAndDeletedPagesConditions()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

527
            $start = date('Ymd000000', /** @scrutinizer ignore-type */ strtotime($start));
Loading history...
528
            $resultQuery->bindParam('start', $start);
529
        }
530
        if (!empty($end)) {
531
            $end = date('Ymd235959', strtotime($end));
532
            $resultQuery->bindParam('end', $end);
533
        }
534
        if ($namespace !== 'all') {
535
            $resultQuery->bindParam('namespace', $namespace);
536
        }
537
538
        $resultQuery->execute();
539
540
        return $resultQuery;
541
    }
542
}
543