Test Failed
Pull Request — master (#124)
by MusikAnimal
05:49
created

UserRepository   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 728
Duplicated Lines 8.93 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

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

20 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 31 4
B getGlobalGroups() 0 24 4
A getBlockExpiry() 12 12 1
F getPagesCreated() 0 88 11
B countEdits() 0 30 2
B countAutomatedEdits() 0 52 5
B getNonAutomatedEdits() 0 61 5
B getAutomatedCounts() 0 47 4
B getAutomatedCountsSql() 0 44 3
A getPageAndNamespaceSql() 0 12 3
B getInnerAutomatedCountsSql() 0 26 4
A getXtoolsUserInfo() 0 6 1
A maxEdits() 0 4 1
B getToolRegexAndTags() 0 22 4
A getRevTimestampConditions() 0 14 3
B executeQuery() 0 22 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();
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.
0 ignored issues
show
Documentation introduced by
Should the return type not be string|boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
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 = [
127
            'list' => 'users',
128
            'ususers' => $username,
129
            'usprop' => 'groups'
130
        ];
131
        $query = new SimpleRequest('query', $params);
132
        $result = [];
133
        $res = $api->getRequest($query);
134
        if (isset($res['batchcomplete']) && isset($res['query']['users'][0]['groups'])) {
135
            $result = $res['query']['users'][0]['groups'];
136
        }
137
138
        // Cache for 10 minutes, and return.
139
        $cacheItem = $this->cache->getItem($cacheKey)
140
            ->set($result)
141
            ->expiresAfter(new DateInterval('PT10M'));
142
        $this->cache->save($cacheItem);
143
        $this->stopwatch->stop($cacheKey);
144
145
        return $result;
146
    }
147
148
    /**
149
     * Get a user's global group membership (starting at XTools' default project if none is
150
     * provided). This requires the CentralAuth extension to be installed.
151
     * @link https://www.mediawiki.org/wiki/Extension:CentralAuth
152
     * @param string $username The username.
153
     * @param Project $project The project to query.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $project not be null|Project?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
154
     * @return string[]
155
     */
156
    public function getGlobalGroups($username, Project $project = null)
157
    {
158
        // Get the default project if not provided.
159
        if (!$project instanceof Project) {
160
            $project = ProjectRepository::getDefaultProject($this->container);
161
        }
162
163
        // Create the API query.
164
        $api = $this->getMediawikiApi($project);
165
        $params = [
166
            'meta' => 'globaluserinfo',
167
            'guiuser' => $username,
168
            'guiprop' => 'groups'
169
        ];
170
        $query = new SimpleRequest('query', $params);
171
172
        // Get the result.
173
        $res = $api->getRequest($query);
174
        $result = [];
175
        if (isset($res['batchcomplete']) && isset($res['query']['globaluserinfo']['groups'])) {
176
            $result = $res['query']['globaluserinfo']['groups'];
177
        }
178
        return $result;
179
    }
180
181
    /**
182
     * Search the ipblocks table to see if the user is currently blocked
183
     * and return the expiry if they are.
184
     * @param $databaseName The database to query.
185
     * @param $userid The ID of the user to search for.
186
     * @return bool|string Expiry of active block or false
187
     */
188 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...
189
    {
190
        $ipblocksTable = $this->getTableName($databaseName, 'ipblocks');
191
        $sql = "SELECT ipb_expiry
192
                FROM $ipblocksTable
193
                WHERE ipb_user = :userid
194
                LIMIT 1";
195
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
196
        $resultQuery->bindParam('userid', $userid);
197
        $resultQuery->execute();
198
        return $resultQuery->fetchColumn();
199
    }
200
201
    /**
202
     * Get pages created by a user
203
     * @param Project $project
204
     * @param User $user
205
     * @param string|int $namespace Namespace ID or 'all'
206
     * @param string $redirects One of 'noredirects', 'onlyredirects' or blank for both
207
     * @return string[] Result of query, see below. Includes live and deleted pages.
208
     */
209
    public function getPagesCreated(Project $project, User $user, $namespace, $redirects)
210
    {
211
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_pages_created');
212
        if ($this->cache->hasItem($cacheKey)) {
213
            return $this->cache->getItem($cacheKey)->get();
214
        }
215
        $this->stopwatch->start($cacheKey, 'XTools');
216
217
        $pageTable = $project->getTableName('page');
218
        $pageAssessmentsTable = $project->getTableName('page_assessments');
219
        $revisionTable = $project->getTableName('revision');
220
        $archiveTable = $project->getTableName('archive');
221
        $logTable = $project->getTableName('logging', 'logindex');
222
223
        $username = $user->getUsername();
224
        $userId = $user->getId($project);
225
226
        $namespaceConditionArc = '';
227
        $namespaceConditionRev = '';
228
229
        if ($namespace != 'all') {
230
            $namespaceConditionRev = " AND page_namespace = '".intval($namespace)."' ";
231
            $namespaceConditionArc = " AND ar_namespace = '".intval($namespace)."' ";
232
        }
233
234
        $redirectCondition = '';
235
236
        if ($redirects == 'onlyredirects') {
237
            $redirectCondition = " AND page_is_redirect = '1' ";
238
        } elseif ($redirects == 'noredirects') {
239
            $redirectCondition = " AND page_is_redirect = '0' ";
240
        }
241
242
        if ($userId == 0) { // IP Editor or undefined username.
243
            $whereRev = " rev_user_text = '$username' AND rev_user = '0' ";
244
            $whereArc = " ar_user_text = '$username' AND ar_user = '0' ";
245
        } else {
246
            $whereRev = " rev_user = '$userId' AND rev_timestamp > 1 ";
247
            $whereArc = " ar_user = '$userId' AND ar_timestamp > 1 ";
248
        }
249
250
        $hasPageAssessments = $this->isLabs() && $project->hasPageAssessments();
251
        $paSelects = $hasPageAssessments ? ', pa_class, pa_importance, pa_page_revision' : '';
252
        $paSelectsArchive = $hasPageAssessments ?
253
            ', NULL AS pa_class, NULL AS pa_page_id, NULL AS pa_page_revision'
254
            : '';
255
        $paJoin = $hasPageAssessments ? "LEFT JOIN $pageAssessmentsTable ON rev_page = pa_page_id" : '';
256
257
        $sql = "
258
            (
259
                SELECT DISTINCT page_namespace AS namespace, 'rev' AS type, page_title AS page_title,
260
                    page_len, page_is_redirect, rev_timestamp AS rev_timestamp,
261
                    rev_user, rev_user_text AS username, rev_len, rev_id $paSelects
262
                FROM $pageTable
263
                JOIN $revisionTable ON page_id = rev_page
264
                $paJoin
265
                WHERE $whereRev AND rev_parent_id = '0' $namespaceConditionRev $redirectCondition" .
266
                ($hasPageAssessments ? 'GROUP BY rev_page' : '') . "
267
            )
268
269
            UNION
270
271
            (
272
                SELECT ar_namespace AS namespace, 'arc' AS type, ar_title AS page_title,
273
                    0 AS page_len, '0' AS page_is_redirect, MIN(ar_timestamp) AS rev_timestamp,
274
                    ar_user AS rev_user, ar_user_text AS username, ar_len AS rev_len,
275
                    ar_rev_id AS rev_id $paSelectsArchive
276
                FROM $archiveTable
277
                LEFT JOIN $logTable ON log_namespace = ar_namespace AND log_title = ar_title
278
                    AND log_user = ar_user AND (log_action = 'move' OR log_action = 'move_redir')
279
                    AND log_type = 'move'
280
                WHERE $whereArc AND ar_parent_id = '0' $namespaceConditionArc AND log_action IS NULL
281
                GROUP BY ar_namespace, ar_title
282
            )";
283
284
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
285
        $resultQuery->execute();
286
        $result = $resultQuery->fetchAll();
287
288
        // Cache for 10 minutes, and return.
289
        $cacheItem = $this->cache->getItem($cacheKey)
290
            ->set($result)
291
            ->expiresAfter(new DateInterval('PT10M'));
292
        $this->cache->save($cacheItem);
293
        $this->stopwatch->stop($cacheKey);
294
295
        return $result;
296
    }
297
298
    /**
299
     * Get edit count within given timeframe and namespace.
300
     * @param Project $project
301
     * @param User $user
302
     * @param int|string $namespace Namespace ID or 'all' for all namespaces
303
     * @param string $start Start date in a format accepted by strtotime()
304
     * @param string $end End date in a format accepted by strtotime()
305
     */
306
    public function countEdits(Project $project, User $user, $namespace = 'all', $start = '', $end = '')
307
    {
308
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_editcount');
309
        if ($this->cache->hasItem($cacheKey)) {
310
            return $this->cache->getItem($cacheKey)->get();
311
        }
312
313
        list($condBegin, $condEnd) = $this->getRevTimestampConditions($start, $end);
314
        list($pageJoin, $condNamespace) = $this->getPageAndNamespaceSql($project, $namespace);
315
        $revisionTable = $project->getTableName('revision');
316
317
        $sql = "SELECT COUNT(rev_id)
318
                FROM $revisionTable
319
                $pageJoin
320
                WHERE rev_user_text = :username
321
                $condNamespace
322
                $condBegin
323
                $condEnd";
324
325
        $resultQuery = $this->executeQuery($sql, $user, $namespace, $start, $end);
326
        $result = $resultQuery->fetchColumn();
327
328
        // Cache for 10 minutes, and return.
329
        $cacheItem = $this->cache->getItem($cacheKey)
330
            ->set($result)
331
            ->expiresAfter(new DateInterval('PT10M'));
332
        $this->cache->save($cacheItem);
333
334
        return $result;
335
    }
336
337
    /**
338
     * Get the number of edits this user made using semi-automated tools.
339
     * @param Project $project
340
     * @param User $user
341
     * @param string|int $namespace Namespace ID or 'all'
342
     * @param string $start Start date in a format accepted by strtotime()
343
     * @param string $end End date in a format accepted by strtotime()
344
     * @return int Result of query, see below.
345
     */
346
    public function countAutomatedEdits(Project $project, User $user, $namespace = 'all', $start = '', $end = '')
347
    {
348
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_autoeditcount');
349
        if ($this->cache->hasItem($cacheKey)) {
350
            return $this->cache->getItem($cacheKey)->get();
351
        }
352
        $this->stopwatch->start($cacheKey, 'XTools');
353
354
        list($condBegin, $condEnd) = $this->getRevTimestampConditions($start, $end);
355
356
        // Get the combined regex and tags for the tools
357
        list($regex, $tags) = $this->getToolRegexAndTags($project->getDomain());
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...
358
359
        list($pageJoin, $condNamespace) = $this->getPageAndNamespaceSql($project, $namespace);
360
361
        $revisionTable = $project->getTableName('revision');
362
        $tagTable = $project->getTableName('change_tag');
363
        $tagJoin = '';
364
365
        // Build SQL for detecting autoedits via regex and/or tags
366
        $condTools = [];
367
        if ($regex != '') {
368
            $condTools[] = "rev_comment REGEXP $regex";
369
        }
370
        if ($tags != '') {
371
            $tagJoin = $tags != '' ? "LEFT OUTER JOIN $tagTable ON ct_rev_id = rev_id" : '';
372
            $condTools[] = "ct_tag IN ($tags)";
373
        }
374
        $condTool = 'AND (' . implode(' OR ', $condTools) . ')';
375
376
        $sql = "SELECT COUNT(DISTINCT(rev_id))
377
                FROM $revisionTable
378
                $pageJoin
379
                $tagJoin
380
                WHERE rev_user_text = :username
381
                $condTool
382
                $condNamespace
383
                $condBegin
384
                $condEnd";
385
386
        $resultQuery = $this->executeQuery($sql, $user, $namespace, $start, $end);
387
        $result = (int) $resultQuery->fetchColumn();
388
389
        // Cache for 10 minutes, and return.
390
        $cacheItem = $this->cache->getItem($cacheKey)
391
            ->set($result)
392
            ->expiresAfter(new DateInterval('PT10M'));
393
        $this->cache->save($cacheItem);
394
        $this->stopwatch->stop($cacheKey);
395
396
        return $result;
397
    }
398
399
    /**
400
     * Get non-automated contributions for the given user.
401
     * @param Project $project
402
     * @param User $user
403
     * @param string|int $namespace Namespace ID or 'all'
404
     * @param string $start Start date in a format accepted by strtotime()
405
     * @param string $end End date in a format accepted by strtotime()
406
     * @param int $offset Used for pagination, offset results by N edits
407
     * @return string[] Result of query, with columns 'page_title',
408
     *   'page_namespace', 'rev_id', 'timestamp', 'minor',
409
     *   'length', 'length_change', 'comment'
410
     */
411
    public function getNonAutomatedEdits(
412
        Project $project,
413
        User $user,
414
        $namespace = 'all',
415
        $start = '',
416
        $end = '',
417
        $offset = 0
418
    ) {
419
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_nonautoedits');
420
        if ($this->cache->hasItem($cacheKey)) {
421
            return $this->cache->getItem($cacheKey)->get();
422
        }
423
        $this->stopwatch->start($cacheKey, 'XTools');
424
425
        list($condBegin, $condEnd) = $this->getRevTimestampConditions($start, $end);
426
427
        // Get the combined regex and tags for the tools
428
        list($regex, $tags) = $this->getToolRegexAndTags($project->getDomain());
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...
429
430
        $pageTable = $project->getTableName('page');
431
        $revisionTable = $project->getTableName('revision');
432
        $tagTable = $project->getTableName('change_tag');
433
        $condNamespace = $namespace === 'all' ? '' : 'AND page_namespace = :namespace';
434
        $tagJoin = $tags != '' ? "LEFT OUTER JOIN $tagTable ON (ct_rev_id = revs.rev_id)" : '';
435
        $condTag = $tags != '' ? "AND (ct_tag NOT IN ($tags) OR ct_tag IS NULL)" : '';
436
        $sql = "SELECT
437
                    page_title,
438
                    page_namespace,
439
                    revs.rev_id AS rev_id,
440
                    revs.rev_timestamp AS timestamp,
441
                    revs.rev_minor_edit AS minor,
442
                    revs.rev_len AS length,
443
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
444
                    revs.rev_comment AS comment
445
                FROM $pageTable
446
                JOIN $revisionTable AS revs ON (page_id = revs.rev_page)
447
                LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
448
                $tagJoin
449
                WHERE revs.rev_user_text = :username
450
                AND revs.rev_timestamp > 0
451
                AND revs.rev_comment NOT RLIKE $regex
452
                $condTag
453
                $condBegin
454
                $condEnd
455
                $condNamespace
456
                ORDER BY revs.rev_timestamp DESC
457
                LIMIT 50
458
                OFFSET $offset";
459
460
        $resultQuery = $this->executeQuery($sql, $user, $namespace, $start, $end);
461
        $result = $resultQuery->fetchAll();
462
463
        // Cache for 10 minutes, and return.
464
        $cacheItem = $this->cache->getItem($cacheKey)
465
            ->set($result)
466
            ->expiresAfter(new DateInterval('PT10M'));
467
        $this->cache->save($cacheItem);
468
        $this->stopwatch->stop($cacheKey);
469
470
        return $result;
471
    }
472
473
    /**
474
     * Get counts of known automated tools used by the given user.
475
     * @param Project $project
476
     * @param User $user
477
     * @param string|int $namespace Namespace ID or 'all'.
478
     * @param string $start Start date in a format accepted by strtotime()
479
     * @param string $end End date in a format accepted by strtotime()
480
     * @return string[] Each tool that they used along with the count and link:
481
     *                  [
482
     *                      'Twinkle' => [
483
     *                          'count' => 50,
484
     *                          'link' => 'Wikipedia:Twinkle',
485
     *                      ],
486
     *                  ]
487
     */
488
    public function getAutomatedCounts(
489
        Project $project,
490
        User $user,
491
        $namespace = 'all',
492
        $start = '',
493
        $end = ''
494
    ) {
495
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_autotoolcounts');
496
        if ($this->cache->hasItem($cacheKey)) {
497
            return $this->cache->getItem($cacheKey)->get();
498
        }
499
        $this->stopwatch->start($cacheKey, 'XTools');
500
501
        $sql = $this->getAutomatedCountsSql($project, $namespace, $start, $end);
502
        $resultQuery = $this->executeQuery($sql, $user, $namespace, $start, $end);
503
504
        $automatedEditsHelper = $this->container->get('app.automated_edits_helper');
505
        $tools = $automatedEditsHelper->getTools($project->getDomain());
506
507
        // handling results
508
        $results = [];
509
510
        while ($row = $resultQuery->fetch()) {
511
            // Only track tools that they've used at least once
512
            $tool = $row['toolname'];
513
            if ($row['count'] > 0) {
514
                $results[$tool] = [
515
                    'link' => $tools[$tool]['link'],
516
                    'count' => $row['count'],
517
                ];
518
            }
519
        }
520
521
        // Sort the array by count
522
        uasort($results, function ($a, $b) {
523
            return $b['count'] - $a['count'];
524
        });
525
526
        // Cache for 10 minutes, and return.
527
        $cacheItem = $this->cache->getItem($cacheKey)
528
            ->set($results)
529
            ->expiresAfter(new DateInterval('PT10M'));
530
        $this->cache->save($cacheItem);
531
        $this->stopwatch->stop($cacheKey);
532
533
        return $results;
534
    }
535
536
    /**
537
     * Get SQL for getting counts of known automated tools used by the given user.
538
     * @see self::getAutomatedCounts()
539
     * @param Project $project
540
     * @param string|int $namespace Namespace ID or 'all'.
541
     * @param string $start Start date in a format accepted by strtotime()
542
     * @param string $end End date in a format accepted by strtotime()
543
     * @return string The SQL.
544
     */
545
    private function getAutomatedCountsSql(Project $project, $namespace, $start, $end)
546
    {
547
        list($condBegin, $condEnd) = $this->getRevTimestampConditions($start, $end);
548
549
        // Load the semi-automated edit types.
550
        $automatedEditsHelper = $this->container->get('app.automated_edits_helper');
551
        $tools = $automatedEditsHelper->getTools($project->getDomain());
552
553
        // Create a collection of queries that we're going to run.
554
        $queries = [];
555
556
        $revisionTable = $project->getTableName('revision');
557
        $tagTable = $project->getTableName('change_tag');
558
559
        list($pageJoin, $condNamespace) = $this->getPageAndNamespaceSql($project, $namespace);
560
561
        $conn = $this->getProjectsConnection();
562
563
        foreach ($tools as $toolname => $values) {
564
            list($condTool, $tagJoin) = $this->getInnerAutomatedCountsSql($tagTable, $values);
565
566
            $toolname = $conn->quote($toolname, \PDO::PARAM_STR);
567
568
            // Developer error, no regex or tag provided for this tool.
569
            if ($condTool === '') {
570
                throw new Exception("No regex or tag found for the tool $toolname. " .
571
                    "Please verify this entry in semi_automated.yml");
572
            }
573
574
            $queries[] .= "
575
                SELECT $toolname AS toolname, COUNT(rev_id) AS count
576
                FROM $revisionTable
577
                $pageJoin
578
                $tagJoin
579
                WHERE rev_user_text = :username
580
                AND $condTool
581
                $condNamespace
582
                $condBegin
583
                $condEnd";
584
        }
585
586
        // Combine to one big query.
587
        return implode(' UNION ', $queries);
588
    }
589
590
    /**
591
     * Get SQL clauses for joining on `page` and restricting to a namespace.
592
     * @param  Project $project
593
     * @param  int|string $namespace Namespace ID or 'all' for all namespaces.
594
     * @return array [page join clause, page namespace clause]
595
     */
596
    private function getPageAndNamespaceSql(Project $project, $namespace)
597
    {
598
        if ($namespace === 'all') {
599
            return [null, null];
600
        }
601
602
        $pageTable = $project->getTableName('page');
603
        $pageJoin = $namespace !== 'all' ? "LEFT JOIN $pageTable ON rev_page = page_id" : null;
604
        $condNamespace = 'AND page_namespace = :namespace';
605
606
        return [$pageJoin, $condNamespace];
607
    }
608
609
    /**
610
     * Get some of the inner SQL for self::getAutomatedCountsSql().
611
     * @param  string $tagTable Name of the `change_tag` table.
612
     * @param  string[] $values Values as defined in semi_automated.yml
613
     * @return string[] [Equality clause, JOIN clause]
614
     */
615
    private function getInnerAutomatedCountsSql($tagTable, $values)
616
    {
617
        $conn = $this->getProjectsConnection();
618
        $tagJoin = '';
619
        $condTool = '';
620
621
        if (isset($values['regex'])) {
622
            $regex = $conn->quote($values['regex'], \PDO::PARAM_STR);
623
            $condTool = "rev_comment REGEXP $regex";
624
        }
625
        if (isset($values['tag'])) {
626
            $tagJoin = "LEFT OUTER JOIN $tagTable ON ct_rev_id = rev_id";
627
            $tag = $conn->quote($values['tag'], \PDO::PARAM_STR);
628
629
            // Append to regex clause if already present.
630
            // Tags are more reliable but may not be present for edits made with
631
            //   older versions of the tool, before it started adding tags.
632
            if ($condTool === '') {
633
                $condTool = "ct_tag = $tag";
634
            } else {
635
                $condTool = '(' . $condTool . " OR ct_tag = $tag)";
636
            }
637
        }
638
639
        return [$condTool, $tagJoin];
640
    }
641
642
    /**
643
     * Get information about the currently-logged in user.
644
     * @return array
645
     */
646
    public function getXtoolsUserInfo()
647
    {
648
        /** @var Session $session */
649
        $session = $this->container->get('session');
650
        return $session->get('logged_in_user');
651
    }
652
653
    /**
654
     * Maximum number of edits to process, based on configuration.
655
     * @return int
656
     */
657
    public function maxEdits()
658
    {
659
        return $this->container->getParameter('app.max_user_edits');
660
    }
661
662
    /**
663
     * Get the combined regex and tags for all semi-automated tools,
664
     *   ready to be used in a query.
665
     * @param string $projectDomain Such as en.wikipedia.org
666
     * @return string[] In the format:
667
     *    ['combined|regex', 'combined,tags']
668
     */
669
    private function getToolRegexAndTags($projectDomain)
670
    {
671
        $conn = $this->getProjectsConnection();
672
        $automatedEditsHelper = $this->container->get('app.automated_edits_helper');
673
        $tools = $automatedEditsHelper->getTools($projectDomain);
674
        $regexes = [];
675
        $tags = [];
676
677
        foreach ($tools as $tool => $values) {
678
            if (isset($values['regex'])) {
679
                $regexes[] = $values['regex'];
680
            }
681
            if (isset($values['tag'])) {
682
                $tags[] = $conn->quote($values['tag'], \PDO::PARAM_STR);
683
            }
684
        }
685
686
        return [
687
            $conn->quote(implode('|', $regexes), \PDO::PARAM_STR),
688
            implode(',', $tags),
689
        ];
690
    }
691
692
    /**
693
     * Get SQL clauses for rev_timestamp, based on whether values for
694
     * the given start and end parameters exist.
695
     * @param  string $start
696
     * @param  string $end
697
     * @return string[] Clauses for start and end timestamps.
698
     */
699
    private function getRevTimestampConditions($start, $end)
700
    {
701
        $condBegin = '';
702
        $condEnd = '';
703
704
        if (!empty($start)) {
705
            $condBegin = 'AND rev_timestamp >= :start ';
706
        }
707
        if (!empty($end)) {
708
            $condEnd = 'AND rev_timestamp <= :end ';
709
        }
710
711
        return [$condBegin, $condEnd];
712
    }
713
714
    /**
715
     * Prepare the given SQL, bind the given parameters, and execute the Doctrine Statement.
716
     * @param  string $sql
717
     * @param  User   $user
718
     * @param  string $namespace
719
     * @param  string $start
720
     * @param  string $end
721
     * @return Doctrine\DBAL\Statement
0 ignored issues
show
Documentation introduced by
Should the return type not be \Doctrine\DBAL\Driver\Statement?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
722
     */
723
    private function executeQuery($sql, User $user, $namespace = 'all', $start = '', $end = '')
724
    {
725
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
726
        $username = $user->getUsername();
727
        $resultQuery->bindParam('username', $username);
728
729
        if (!empty($start)) {
730
            $start = date('Ymd000000', strtotime($start));
731
            $resultQuery->bindParam('start', $start);
732
        }
733
        if (!empty($end)) {
734
            $end = date('Ymd235959', strtotime($end));
735
            $resultQuery->bindParam('end', $end);
736
        }
737
        if ($namespace !== 'all') {
738
            $resultQuery->bindParam('namespace', $namespace);
739
        }
740
741
        $resultQuery->execute();
742
743
        return $resultQuery;
744
    }
745
}
746