Passed
Push — master ( 8b9089...a51f69 )
by MusikAnimal
01:23
created

UserRepository   F

Complexity

Total Complexity 79

Size/Duplication

Total Lines 733
Duplicated Lines 8.87 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
wmc 79
lcom 1
cbo 13
dl 65
loc 733
rs 2.2857
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getUser() 0 8 1
A getId() 22 22 2
A getRegistrationDate() 22 22 2
A getEditCount() 9 9 1
B getGroups() 0 27 4
B getGlobalGroups() 0 24 4
A getBlockExpiry() 12 12 1
F getPagesCreated() 0 88 11
C countEdits() 0 58 9
F countAutomatedEdits() 0 83 12
F getNonAutomatedEdits() 0 86 10
F getAutomatedCounts() 0 133 16
A getXtoolsUserInfo() 0 6 1
A maxEdits() 0 4 1
A getToolRegexAndTags() 0 20 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like UserRepository often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UserRepository, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file contains only the UserRepository class.
4
 */
5
6
namespace Xtools;
7
8
use Exception;
9
use DateInterval;
10
use Mediawiki\Api\SimpleRequest;
11
use Symfony\Component\DependencyInjection\Container;
12
use Symfony\Component\HttpFoundation\Session\Session;
13
14
/**
15
 * This class provides data for the User class.
16
 * @codeCoverageIgnore
17
 */
18
class UserRepository extends Repository
19
{
20
21
    /**
22
     * Convenience method to get a new User object.
23
     * @param string $username The username.
24
     * @param Container $container The DI container.
25
     * @return User
26
     */
27
    public static function getUser($username, Container $container)
28
    {
29
        $user = new User($username);
30
        $userRepo = new UserRepository();
31
        $userRepo->setContainer($container);
32
        $user->setRepository($userRepo);
33
        return $user;
34
    }
35
36
    /**
37
     * Get the user's ID.
38
     * @param string $databaseName The database to query.
39
     * @param string $username The username to find.
40
     * @return int
41
     */
42 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...
43
    {
44
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_id');
45
        if ($this->cache->hasItem($cacheKey)) {
46
            return $this->cache->getItem($cacheKey)->get();
47
        }
48
49
        $userTable = $this->getTableName($databaseName, 'user');
50
        $sql = "SELECT user_id FROM $userTable WHERE user_name = :username LIMIT 1";
51
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
52
        $resultQuery->bindParam("username", $username);
53
        $resultQuery->execute();
54
        $userId = (int)$resultQuery->fetchColumn();
55
56
        // Cache for 10 minutes.
57
        $cacheItem = $this->cache
58
            ->getItem($cacheKey)
59
            ->set($userId)
60
            ->expiresAfter(new DateInterval('PT10M'));
61
        $this->cache->save($cacheItem);
62
        return $userId;
63
    }
64
65
    /**
66
     * Get the user's registration date.
67
     * @param string $databaseName The database to query.
68
     * @param string $username The username to find.
69
     * @return string|null As returned by the database.
70
     */
71 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...
72
    {
73
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_registration');
74
        if ($this->cache->hasItem($cacheKey)) {
75
            return $this->cache->getItem($cacheKey)->get();
76
        }
77
78
        $userTable = $this->getTableName($databaseName, 'user');
79
        $sql = "SELECT user_registration FROM $userTable WHERE user_name = :username LIMIT 1";
80
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
81
        $resultQuery->bindParam('username', $username);
82
        $resultQuery->execute();
83
        $registrationDate = $resultQuery->fetchColumn();
0 ignored issues
show
Bug Compatibility introduced by
The expression $resultQuery->fetchColumn(); of type string|boolean adds the type boolean to the return on line 91 which is incompatible with the return type documented by Xtools\UserRepository::getRegistrationDate of type string|null.
Loading history...
84
85
        // Cache for 10 minutes.
86
        $cacheItem = $this->cache
87
            ->getItem($cacheKey)
88
            ->set($registrationDate)
89
            ->expiresAfter(new DateInterval('PT10M'));
90
        $this->cache->save($cacheItem);
91
        return $registrationDate;
92
    }
93
94
    /**
95
     * Get the user's (system) edit count.
96
     * @param string $databaseName The database to query.
97
     * @param string $username The username to find.
98
     * @return int|null As returned by the database.
99
     */
100 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...
101
    {
102
        $userTable = $this->getTableName($databaseName, 'user');
103
        $sql = "SELECT user_editcount FROM $userTable WHERE user_name = :username LIMIT 1";
104
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
105
        $resultQuery->bindParam('username', $username);
106
        $resultQuery->execute();
107
        return $resultQuery->fetchColumn();
108
    }
109
110
    /**
111
     * Get group names of the given user.
112
     * @param Project $project The project.
113
     * @param string $username The username.
114
     * @return string[]
115
     */
116
    public function getGroups(Project $project, $username)
117
    {
118
        // Use md5 to ensure the key does not contain reserved characters.
119
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_groups');
120
        if ($this->cache->hasItem($cacheKey)) {
121
            return $this->cache->getItem($cacheKey)->get();
122
        }
123
124
        $this->stopwatch->start($cacheKey, 'XTools');
125
        $api = $this->getMediawikiApi($project);
126
        $params = [ "list"=>"users", "ususers"=>$username, "usprop"=>"groups" ];
127
        $query = new SimpleRequest('query', $params);
128
        $result = [];
129
        $res = $api->getRequest($query);
130
        if (isset($res["batchcomplete"]) && isset($res["query"]["users"][0]["groups"])) {
131
            $result = $res["query"]["users"][0]["groups"];
132
        }
133
134
        // Cache for 10 minutes, and return.
135
        $cacheItem = $this->cache->getItem($cacheKey)
136
            ->set($result)
137
            ->expiresAfter(new DateInterval('PT10M'));
138
        $this->cache->save($cacheItem);
139
        $this->stopwatch->stop($cacheKey);
140
141
        return $result;
142
    }
143
144
    /**
145
     * Get a user's global group membership (starting at XTools' default project if none is
146
     * provided). This requires the CentralAuth extension to be installed.
147
     * @link https://www.mediawiki.org/wiki/Extension:CentralAuth
148
     * @param string $username The username.
149
     * @param Project $project The project to query.
150
     * @return string[]
151
     */
152
    public function getGlobalGroups($username, Project $project = null)
153
    {
154
        // Get the default project if not provided.
155
        if (!$project instanceof Project) {
156
            $project = ProjectRepository::getDefaultProject($this->container);
157
        }
158
159
        // Create the API query.
160
        $api = $this->getMediawikiApi($project);
161
        $params = [
162
            'meta' => 'globaluserinfo',
163
            'guiuser' => $username,
164
            'guiprop' => 'groups'
165
        ];
166
        $query = new SimpleRequest('query', $params);
167
168
        // Get the result.
169
        $res = $api->getRequest($query);
170
        $result = [];
171
        if (isset($res['batchcomplete']) && isset($res['query']['globaluserinfo']['groups'])) {
172
            $result = $res['query']['globaluserinfo']['groups'];
173
        }
174
        return $result;
175
    }
176
177
    /**
178
     * Search the ipblocks table to see if the user is currently blocked
179
     * and return the expiry if they are.
180
     * @param $databaseName The database to query.
181
     * @param $userid The ID of the user to search for.
182
     * @return bool|string Expiry of active block or false
183
     */
184 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...
185
    {
186
        $ipblocksTable = $this->getTableName($databaseName, 'ipblocks');
187
        $sql = "SELECT ipb_expiry
188
                FROM $ipblocksTable
189
                WHERE ipb_user = :userid
190
                LIMIT 1";
191
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
192
        $resultQuery->bindParam('userid', $userid);
193
        $resultQuery->execute();
194
        return $resultQuery->fetchColumn();
195
    }
196
197
    /**
198
     * Get pages created by a user
199
     * @param Project $project
200
     * @param User $user
201
     * @param string|int $namespace Namespace ID or 'all'
202
     * @param string $redirects One of 'noredirects', 'onlyredirects' or blank for both
203
     * @return string[] Result of query, see below. Includes live and deleted pages.
204
     */
205
    public function getPagesCreated(Project $project, User $user, $namespace, $redirects)
206
    {
207
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_pages_created');
208
        if ($this->cache->hasItem($cacheKey)) {
209
            return $this->cache->getItem($cacheKey)->get();
210
        }
211
        $this->stopwatch->start($cacheKey, 'XTools');
212
213
        $pageTable = $project->getTableName('page');
214
        $pageAssessmentsTable = $project->getTableName('page_assessments');
215
        $revisionTable = $project->getTableName('revision');
216
        $archiveTable = $project->getTableName('archive');
217
        $logTable = $project->getTableName('logging', 'logindex');
218
219
        $username = $user->getUsername();
220
        $userId = $user->getId($project);
221
222
        $namespaceConditionArc = '';
223
        $namespaceConditionRev = '';
224
225
        if ($namespace != 'all') {
226
            $namespaceConditionRev = " AND page_namespace = '".intval($namespace)."' ";
227
            $namespaceConditionArc = " AND ar_namespace = '".intval($namespace)."' ";
228
        }
229
230
        $redirectCondition = '';
231
232
        if ($redirects == 'onlyredirects') {
233
            $redirectCondition = " AND page_is_redirect = '1' ";
234
        } elseif ($redirects == 'noredirects') {
235
            $redirectCondition = " AND page_is_redirect = '0' ";
236
        }
237
238
        if ($userId == 0) { // IP Editor or undefined username.
239
            $whereRev = " rev_user_text = '$username' AND rev_user = '0' ";
240
            $whereArc = " ar_user_text = '$username' AND ar_user = '0' ";
241
        } else {
242
            $whereRev = " rev_user = '$userId' AND rev_timestamp > 1 ";
243
            $whereArc = " ar_user = '$userId' AND ar_timestamp > 1 ";
244
        }
245
246
        $hasPageAssessments = $this->isLabs() && $project->hasPageAssessments();
247
        $paSelects = $hasPageAssessments ? ', pa_class, pa_importance, pa_page_revision' : '';
248
        $paSelectsArchive = $hasPageAssessments ?
249
            ', NULL AS pa_class, NULL AS pa_page_id, NULL AS pa_page_revision'
250
            : '';
251
        $paJoin = $hasPageAssessments ? "LEFT JOIN $pageAssessmentsTable ON rev_page = pa_page_id" : '';
252
253
        $sql = "
254
            (
255
                SELECT DISTINCT page_namespace AS namespace, 'rev' AS type, page_title AS page_title,
256
                    page_len, page_is_redirect, rev_timestamp AS rev_timestamp,
257
                    rev_user, rev_user_text AS username, rev_len, rev_id $paSelects
258
                FROM $pageTable
259
                JOIN $revisionTable ON page_id = rev_page
260
                $paJoin
261
                WHERE $whereRev AND rev_parent_id = '0' $namespaceConditionRev $redirectCondition" .
262
                ($hasPageAssessments ? 'GROUP BY rev_page' : '') . "
263
            )
264
265
            UNION
266
267
            (
268
                SELECT ar_namespace AS namespace, 'arc' AS type, ar_title AS page_title,
269
                    0 AS page_len, '0' AS page_is_redirect, MIN(ar_timestamp) AS rev_timestamp,
270
                    ar_user AS rev_user, ar_user_text AS username, ar_len AS rev_len,
271
                    ar_rev_id AS rev_id $paSelectsArchive
272
                FROM $archiveTable
273
                LEFT JOIN $logTable ON log_namespace = ar_namespace AND log_title = ar_title
274
                    AND log_user = ar_user AND (log_action = 'move' OR log_action = 'move_redir')
275
                    AND log_type = 'move'
276
                WHERE $whereArc AND ar_parent_id = '0' $namespaceConditionArc AND log_action IS NULL
277
                GROUP BY ar_namespace, ar_title
278
            )";
279
280
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
281
        $resultQuery->execute();
282
        $result = $resultQuery->fetchAll();
283
284
        // Cache for 10 minutes, and return.
285
        $cacheItem = $this->cache->getItem($cacheKey)
286
            ->set($result)
287
            ->expiresAfter(new DateInterval('PT10M'));
288
        $this->cache->save($cacheItem);
289
        $this->stopwatch->stop($cacheKey);
290
291
        return $result;
292
    }
293
294
    /**
295
     * Get edit count within given timeframe and namespace.
296
     * @param Project $project
297
     * @param User $user
298
     * @param int|string $namespace Namespace ID or 'all' for all namespaces
299
     * @param string $start Start date in a format accepted by strtotime()
300
     * @param string $end End date in a format accepted by strtotime()
301
     */
302
    public function countEdits(Project $project, User $user, $namespace = 'all', $start = '', $end = '')
303
    {
304
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_editcount');
305
        if ($this->cache->hasItem($cacheKey)) {
306
            return $this->cache->getItem($cacheKey)->get();
307
        }
308
309
        $condBegin = '';
310
        $condEnd = '';
311
312
        if (!empty($start)) {
313
            // For the query
314
            $start = date('Ymd000000', strtotime($start));
315
            $condBegin = 'AND rev_timestamp >= :start ';
316
        }
317
        if (!empty($end)) {
318
            // For the query
319
            $end = date('Ymd235959', strtotime($end));
320
            $condEnd = 'AND rev_timestamp <= :end ';
321
        }
322
323
        $pageTable = $project->getTableName('page');
324
        $revisionTable = $project->getTableName('revision');
325
        $condNamespace = $namespace === 'all' ? '' : 'AND page_namespace = :namespace';
326
        $pageJoin = $namespace === 'all' ? '' : "JOIN $pageTable ON rev_page = page_id";
327
328
        $sql = "SELECT COUNT(rev_id)
329
                FROM $revisionTable
330
                $pageJoin
331
                WHERE rev_user_text = :username
332
                $condNamespace
333
                $condBegin
334
                $condEnd";
335
336
        $username = $user->getUsername();
337
        $conn = $this->getProjectsConnection();
338
        $resultQuery = $conn->prepare($sql);
339
        $resultQuery->bindParam('username', $username);
340
        if (!empty($start)) {
341
            $resultQuery->bindParam('start', $start);
342
        }
343
        if (!empty($end)) {
344
            $resultQuery->bindParam('end', $end);
345
        }
346
        if ($namespace !== 'all') {
347
            $resultQuery->bindParam('namespace', $namespace);
348
        }
349
        $resultQuery->execute();
350
        $result = $resultQuery->fetchColumn();
351
352
        // Cache for 10 minutes, and return.
353
        $cacheItem = $this->cache->getItem($cacheKey)
354
            ->set($result)
355
            ->expiresAfter(new DateInterval('PT10M'));
356
        $this->cache->save($cacheItem);
357
358
        return $result;
359
    }
360
361
    /**
362
     * Get the number of edits this user made using semi-automated tools.
363
     * @param Project $project
364
     * @param User $user
365
     * @param string|int $namespace Namespace ID or 'all'
366
     * @param string $start Start date in a format accepted by strtotime()
367
     * @param string $end End date in a format accepted by strtotime()
368
     * @return int Result of query, see below.
369
     */
370
    public function countAutomatedEdits(Project $project, User $user, $namespace = 'all', $start = '', $end = '')
371
    {
372
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_autoeditcount');
373
        if ($this->cache->hasItem($cacheKey)) {
374
            return $this->cache->getItem($cacheKey)->get();
375
        }
376
377
        $condBegin = '';
378
        $condEnd = '';
379
380
        if (!empty($start)) {
381
            $cacheKey .= '.' . $start;
382
383
            // For the query
384
            $start = date('Ymd000000', strtotime($start));
385
            $condBegin = 'AND rev_timestamp >= :start ';
386
        }
387
        if (!empty($end)) {
388
            $cacheKey .= '.' . $end;
389
390
            // For the query
391
            $end = date('Ymd235959', strtotime($end));
392
            $condEnd = 'AND rev_timestamp <= :end ';
393
        }
394
395
        $this->stopwatch->start($cacheKey, 'XTools');
396
397
        // Get the combined regex and tags for the tools
398
        $conn = $this->getProjectsConnection();
399
        list($regex, $tags) = $this->getToolRegexAndTags($project->getDomain(), $conn);
0 ignored issues
show
Security Bug introduced by
It seems like $project->getDomain() targeting Xtools\Project::getDomain() can also be of type false; however, Xtools\UserRepository::getToolRegexAndTags() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
400
401
        $pageTable = $project->getTableName('page');
402
        $revisionTable = $project->getTableName('revision');
403
        $tagTable = $project->getTableName('change_tag');
404
        $condNamespace = $namespace === 'all' ? '' : 'AND page_namespace = :namespace';
405
        $pageJoin = $namespace === 'all' ? '' : "JOIN $pageTable ON page_id = rev_page";
406
        $tagJoin = '';
407
408
        // Build SQL for detecting autoedits via regex and/or tags
409
        $condTools = [];
410
        if ($regex != '') {
411
            $condTools[] = "rev_comment REGEXP $regex";
412
        }
413
        if ($tags != '') {
414
            $tagJoin = $tags != '' ? "LEFT OUTER JOIN $tagTable ON ct_rev_id = rev_id" : '';
415
            $condTools[] = "ct_tag IN ($tags)";
416
        }
417
        $condTool = 'AND (' . implode(' OR ', $condTools) . ')';
418
419
        $sql = "SELECT COUNT(DISTINCT(rev_id))
420
                FROM $revisionTable
421
                $pageJoin
422
                $tagJoin
423
                WHERE rev_user_text = :username
424
                $condTool
425
                $condNamespace
426
                $condBegin
427
                $condEnd";
428
429
        $username = $user->getUsername();
430
        $resultQuery = $conn->prepare($sql);
431
        $resultQuery->bindParam('username', $username);
432
        if (!empty($start)) {
433
            $resultQuery->bindParam('start', $start);
434
        }
435
        if (!empty($end)) {
436
            $resultQuery->bindParam('end', $end);
437
        }
438
        if ($namespace !== 'all') {
439
            $resultQuery->bindParam('namespace', $namespace);
440
        }
441
        $resultQuery->execute();
442
        $result = (int) $resultQuery->fetchColumn();
443
444
        // Cache for 10 minutes, and return.
445
        $cacheItem = $this->cache->getItem($cacheKey)
446
            ->set($result)
447
            ->expiresAfter(new DateInterval('PT10M'));
448
        $this->cache->save($cacheItem);
449
        $this->stopwatch->stop($cacheKey);
450
451
        return $result;
452
    }
453
454
    /**
455
     * Get non-automated contributions for the given user.
456
     * @param Project $project
457
     * @param User $user
458
     * @param string|int $namespace Namespace ID or 'all'
459
     * @param string $start Start date in a format accepted by strtotime()
460
     * @param string $end End date in a format accepted by strtotime()
461
     * @param int $offset Used for pagination, offset results by N edits
462
     * @return string[] Result of query, with columns 'page_title',
463
     *   'page_namespace', 'rev_id', 'timestamp', 'minor',
464
     *   'length', 'length_change', 'comment'
465
     */
466
    public function getNonAutomatedEdits(
467
        Project $project,
468
        User $user,
469
        $namespace = 'all',
470
        $start = '',
471
        $end = '',
472
        $offset = 0
473
    ) {
474
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_nonautoedits');
475
        if ($this->cache->hasItem($cacheKey)) {
476
            return $this->cache->getItem($cacheKey)->get();
477
        }
478
        $this->stopwatch->start($cacheKey, 'XTools');
479
480
        $condBegin = '';
481
        $condEnd = '';
482
483
        if (!empty($start)) {
484
            // For the query
485
            $start = date('Ymd000000', strtotime($start));
486
            $condBegin = 'AND revs.rev_timestamp >= :start ';
487
        }
488
        if (!empty($end)) {
489
            // For the query
490
            $end = date('Ymd235959', strtotime($end));
491
            $condEnd = 'AND revs.rev_timestamp <= :end ';
492
        }
493
494
        // Get the combined regex and tags for the tools
495
        $conn = $this->getProjectsConnection();
496
        list($regex, $tags) = $this->getToolRegexAndTags($project->getDomain(), $conn);
0 ignored issues
show
Security Bug introduced by
It seems like $project->getDomain() targeting Xtools\Project::getDomain() can also be of type false; however, Xtools\UserRepository::getToolRegexAndTags() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
497
498
        $pageTable = $project->getTableName('page');
499
        $revisionTable = $project->getTableName('revision');
500
        $tagTable = $project->getTableName('change_tag');
501
        $condNamespace = $namespace === 'all' ? '' : 'AND page_namespace = :namespace';
502
        $tagJoin = $tags != '' ? "LEFT OUTER JOIN $tagTable ON (ct_rev_id = revs.rev_id)" : '';
503
        $condTag = $tags != '' ? "AND (ct_tag NOT IN ($tags) OR ct_tag IS NULL)" : '';
504
        $sql = "SELECT
505
                    page_title,
506
                    page_namespace,
507
                    revs.rev_id AS rev_id,
508
                    revs.rev_timestamp AS timestamp,
509
                    revs.rev_minor_edit AS minor,
510
                    revs.rev_len AS length,
511
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
512
                    revs.rev_comment AS comment
513
                FROM $pageTable
514
                JOIN $revisionTable AS revs ON (page_id = revs.rev_page)
515
                LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
516
                $tagJoin
517
                WHERE revs.rev_user_text = :username
518
                AND revs.rev_timestamp > 0
519
                AND revs.rev_comment NOT RLIKE $regex
520
                $condTag
521
                $condBegin
522
                $condEnd
523
                $condNamespace
524
                ORDER BY revs.rev_timestamp DESC
525
                LIMIT 50
526
                OFFSET $offset";
527
528
        $username = $user->getUsername();
529
        $resultQuery = $conn->prepare($sql);
530
        $resultQuery->bindParam('username', $username);
531
        if (!empty($start)) {
532
            $resultQuery->bindParam('start', $start);
533
        }
534
        if (!empty($end)) {
535
            $resultQuery->bindParam('end', $end);
536
        }
537
        if ($namespace !== 'all') {
538
            $resultQuery->bindParam('namespace', $namespace);
539
        }
540
        $resultQuery->execute();
541
        $result = $resultQuery->fetchAll();
542
543
        // Cache for 10 minutes, and return.
544
        $cacheItem = $this->cache->getItem($cacheKey)
545
            ->set($result)
546
            ->expiresAfter(new DateInterval('PT10M'));
547
        $this->cache->save($cacheItem);
548
        $this->stopwatch->stop($cacheKey);
549
550
        return $result;
551
    }
552
553
    /**
554
     * Get non-automated contributions for the given user.
555
     * @param Project $project
556
     * @param User $user
557
     * @param string|int $namespace Namespace ID or 'all'.
558
     * @param string $start Start date in a format accepted by strtotime()
559
     * @param string $end End date in a format accepted by strtotime()
560
     * @return string[] Each tool that they used along with the count and link:
561
     *                  [
562
     *                      'Twinkle' => [
563
     *                          'count' => 50,
564
     *                          'link' => 'Wikipedia:Twinkle',
565
     *                      ],
566
     *                  ]
567
     */
568
    public function getAutomatedCounts(
569
        Project $project,
570
        User $user,
571
        $namespace = 'all',
572
        $start = '',
573
        $end = ''
574
    ) {
575
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_autotoolcounts');
576
        if ($this->cache->hasItem($cacheKey)) {
577
            return $this->cache->getItem($cacheKey)->get();
578
        }
579
        $this->stopwatch->start($cacheKey, 'XTools');
580
581
        $condBegin = '';
582
        $condEnd = '';
583
584
        if (!empty($start)) {
585
            // For the query
586
            $start = date('Ymd000000', strtotime($start));
587
            $condBegin = 'AND rev_timestamp >= :start ';
588
        }
589
        if (!empty($end)) {
590
            // For the query
591
            $end = date('Ymd235959', strtotime($end));
592
            $condEnd = 'AND rev_timestamp <= :end ';
593
        }
594
595
        $conn = $this->getProjectsConnection();
596
597
        // Load the semi-automated edit types.
598
        $automatedEditsHelper = $this->container->get('app.automated_edits_helper');
599
        $tools = $automatedEditsHelper->getTools($project->getDomain());
600
601
        // Create a collection of queries that we're going to run.
602
        $queries = [];
603
604
        $revisionTable = $project->getRepository()->getTableName($project->getDatabaseName(), 'revision');
605
        $pageTable = $project->getRepository()->getTableName($project->getDatabaseName(), 'page');
606
        $tagTable = $project->getRepository()->getTableName($project->getDatabaseName(), 'change_tag');
607
608
        $pageJoin = $namespace !== 'all' ? "LEFT JOIN $pageTable ON rev_page = page_id" : null;
609
        $condNamespace = $namespace !== 'all' ? "AND page_namespace = :namespace" : null;
610
611
        foreach ($tools as $toolname => $values) {
612
            $tagJoin = '';
613
            $condTool = '';
614
            $toolname = $conn->quote($toolname, \PDO::PARAM_STR);
615
616
            if (isset($values['regex'])) {
617
                $regex = $conn->quote($values['regex'], \PDO::PARAM_STR);
618
                $condTool = "rev_comment REGEXP $regex";
619
            }
620
            if (isset($values['tag'])) {
621
                $tagJoin = "LEFT OUTER JOIN $tagTable ON ct_rev_id = rev_id";
622
                $tag = $conn->quote($values['tag'], \PDO::PARAM_STR);
623
624
                // Append to regex clause if already present.
625
                // Tags are more reliable but may not be present for edits made with
626
                //   older versions of the tool, before it started adding tags.
627
                if ($condTool === '') {
628
                    $condTool = "ct_tag = $tag";
629
                } else {
630
                    $condTool = '(' . $condTool . " OR ct_tag = $tag)";
631
                }
632
            }
633
634
            // Developer error, no regex or tag provided for this tool.
635
            if ($condTool === '') {
636
                throw new Exception("No regex or tag found for the tool $toolname. " .
637
                    "Please verify this entry in semi_automated.yml");
638
            }
639
640
            $queries[] .= "
641
                SELECT $toolname AS toolname, COUNT(rev_id) AS count
642
                FROM $revisionTable
643
                $pageJoin
644
                $tagJoin
645
                WHERE rev_user_text = :username
646
                AND $condTool
647
                $condNamespace
648
                $condBegin
649
                $condEnd";
650
        }
651
652
        // Create a big query and execute.
653
        $sql = implode(' UNION ', $queries);
654
655
        $resultQuery = $conn->prepare($sql);
656
657
        $username = $user->getUsername(); // use normalized user name
658
        $resultQuery->bindParam('username', $username);
659
        if (!empty($start)) {
660
            $startParam = date('Ymd000000', strtotime($start));
661
            $resultQuery->bindParam('start', $startParam);
662
        }
663
        if (!empty($end)) {
664
            $endParam = date('Ymd235959', strtotime($end));
665
            $resultQuery->bindParam('end', $endParam);
666
        }
667
        if ($namespace !== 'all') {
668
            $resultQuery->bindParam('namespace', $namespace);
669
        }
670
671
        $resultQuery->execute();
672
673
        // handling results
674
        $results = [];
675
676
        while ($row = $resultQuery->fetch()) {
677
            // Only track tools that they've used at least once
678
            $tool = $row['toolname'];
679
            if ($row['count'] > 0) {
680
                $results[$tool] = [
681
                    'link' => $tools[$tool]['link'],
682
                    'count' => $row['count'],
683
                ];
684
            }
685
        }
686
687
        // Sort the array by count
688
        uasort($results, function ($a, $b) {
689
            return $b['count'] - $a['count'];
690
        });
691
692
        // Cache for 10 minutes, and return.
693
        $cacheItem = $this->cache->getItem($cacheKey)
694
            ->set($results)
695
            ->expiresAfter(new DateInterval('PT10M'));
696
        $this->cache->save($cacheItem);
697
        $this->stopwatch->stop($cacheKey);
698
699
        return $results;
700
    }
701
702
    /**
703
     * Get information about the currently-logged in user.
704
     * @return array
705
     */
706
    public function getXtoolsUserInfo()
707
    {
708
        /** @var Session $session */
709
        $session = $this->container->get('session');
710
        return $session->get('logged_in_user');
711
    }
712
713
    /**
714
     * Maximum number of edits to process, based on configuration.
715
     * @return int
716
     */
717
    public function maxEdits()
718
    {
719
        return $this->container->getParameter('app.max_user_edits');
720
    }
721
722
    /**
723
     * Get the combined regex and tags for all semi-automated tools,
724
     *   ready to be used in a query.
725
     * @param string $projectDomain Such as en.wikipedia.org
726
     * @param $conn Doctrine\DBAL\Connection Used for proper escaping
727
     * @return string[] In the format:
728
     *    ['combined|regex', 'combined,tags']
729
     */
730
    private function getToolRegexAndTags($projectDomain, $conn)
731
    {
732
        $automatedEditsHelper = $this->container->get('app.automated_edits_helper');
733
        $tools = $automatedEditsHelper->getTools($projectDomain);
734
        $regexes = [];
735
        $tags = [];
736
        foreach ($tools as $tool => $values) {
737
            if (isset($values['regex'])) {
738
                $regexes[] = $values['regex'];
739
            }
740
            if (isset($values['tag'])) {
741
                $tags[] = $conn->quote($values['tag'], \PDO::PARAM_STR);
742
            }
743
        }
744
745
        return [
746
            $conn->quote(implode('|', $regexes), \PDO::PARAM_STR),
747
            implode(',', $tags),
748
        ];
749
    }
750
}
751