Completed
Push — master ( dc7aae...2784d5 )
by MusikAnimal
02:59
created

UserRepository::getRegistrationDate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23
Code Lines 16

Duplication

Lines 23
Ratio 100 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 23
loc 23
rs 9.0856
cc 2
eloc 16
nc 2
nop 2
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
 */
17
class UserRepository extends Repository
18
{
19
20
    /**
21
     * Convenience method to get a new User object.
22
     * @param string $username The username.
23
     * @param Container $container The DI container.
24
     * @return User
25
     */
26
    public static function getUser($username, Container $container)
27
    {
28
        $user = new User($username);
29
        $userRepo = new UserRepository();
30
        $userRepo->setContainer($container);
31
        $user->setRepository($userRepo);
32
        return $user;
33
    }
34
35
    /**
36
     * Get the user's ID.
37
     * @param string $databaseName The database to query.
38
     * @param string $username The username to find.
39
     * @return int
40
     */
41 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...
42
    {
43
        // Use md5 to ensure the key does not contain reserved characters.
44
        $cacheKey = 'user_id.'.$databaseName.'.'.md5($username);
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
        // Use md5 to ensure the key does not contain reserved characters.
74
        $cacheKey = 'user_registration.'.$databaseName.'.'.md5($username);
75
        if ($this->cache->hasItem($cacheKey)) {
76
            return $this->cache->getItem($cacheKey)->get();
77
        }
78
79
        $userTable = $this->getTableName($databaseName, 'user');
80
        $sql = "SELECT user_registration FROM $userTable WHERE user_name = :username LIMIT 1";
81
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
82
        $resultQuery->bindParam('username', $username);
83
        $resultQuery->execute();
84
        $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 92 which is incompatible with the return type documented by Xtools\UserRepository::getRegistrationDate of type string|null.
Loading history...
85
86
        // Cache for 10 minutes.
87
        $cacheItem = $this->cache
88
            ->getItem($cacheKey)
89
            ->set($registrationDate)
90
            ->expiresAfter(new DateInterval('PT10M'));
91
        $this->cache->save($cacheItem);
92
        return $registrationDate;
93
    }
94
95
    /**
96
     * Get group names of the given user.
97
     * @param Project $project The project.
98
     * @param string $username The username.
99
     * @return string[]
100
     */
101
    public function getGroups(Project $project, $username)
102
    {
103
        // Use md5 to ensure the key does not contain reserved characters.
104
        $cacheKey = 'usergroups.'.$project->getDatabaseName().'.'.md5($username);
105
        if ($this->cache->hasItem($cacheKey)) {
106
            return $this->cache->getItem($cacheKey)->get();
107
        }
108
109
        $this->stopwatch->start($cacheKey, 'XTools');
110
        $api = $this->getMediawikiApi($project);
111
        $params = [ "list"=>"users", "ususers"=>$username, "usprop"=>"groups" ];
112
        $query = new SimpleRequest('query', $params);
113
        $result = [];
114
        $res = $api->getRequest($query);
115
        if (isset($res["batchcomplete"]) && isset($res["query"]["users"][0]["groups"])) {
116
            $result = $res["query"]["users"][0]["groups"];
117
        }
118
119
        // Cache for 10 minutes, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
120
        $cacheItem = $this->cache->getItem($cacheKey)
121
            ->set($result)
122
            ->expiresAfter(new DateInterval('PT10M'));
123
        $this->cache->save($cacheItem);
124
        $this->stopwatch->stop($cacheKey);
125
126
        return $result;
127
    }
128
129
    /**
130
     * Get a user's global group membership (starting at XTools' default project if none is
131
     * provided). This requires the CentralAuth extension to be installed.
132
     * @link https://www.mediawiki.org/wiki/Extension:CentralAuth
133
     * @param string $username The username.
134
     * @param Project $project The project to query.
135
     * @return string[]
136
     */
137
    public function getGlobalGroups($username, Project $project = null)
138
    {
139
        // Get the default project if not provided.
140
        if (!$project instanceof Project) {
141
            $project = ProjectRepository::getDefaultProject($this->container);
142
        }
143
144
        // Create the API query.
145
        $api = $this->getMediawikiApi($project);
146
        $params = [ "meta"=>"globaluserinfo", "guiuser"=>$username, "guiprop"=>"groups" ];
147
        $query = new SimpleRequest('query', $params);
148
149
        // Get the result.
150
        $res = $api->getRequest($query);
151
        $result = [];
152
        if (isset($res["batchcomplete"]) && isset($res["query"]["globaluserinfo"]["groups"])) {
153
            $result = $res["query"]["globaluserinfo"]["groups"];
154
        }
155
        return $result;
156
    }
157
158
    /**
159
     * Search the ipblocks table to see if the user is currently blocked
160
     *   and return the expiry if they are
161
     * @param $databaseName The database to query.
162
     * @param $userid The ID of the user to search for.
163
     * @return bool|string Expiry of active block or false
164
     */
165
    public function getBlockExpiry($databaseName, $userid)
166
    {
167
        $ipblocksTable = $this->getTableName($databaseName, 'ipblocks');
168
        $sql = "SELECT ipb_expiry
169
                FROM $ipblocksTable
170
                WHERE ipb_user = :userid
171
                LIMIT 1";
172
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
173
        $resultQuery->bindParam('userid', $userid);
174
        $resultQuery->execute();
175
        return $resultQuery->fetchColumn();
176
    }
177
178
    /**
179
     * Get pages created by a user
180
     * @param Project $project
181
     * @param User $user
182
     * @param string|int $namespace Namespace ID or 'all'
183
     * @param string $redirects One of 'noredirects', 'onlyredirects' or blank for both
184
     * @return string[] Result of query, see below. Includes live and deleted pages.
185
     */
186
    public function getPagesCreated(Project $project, User $user, $namespace, $redirects)
187
    {
188
        $username = $user->getUsername();
189
190
        $cacheKey = 'pages.' . $project->getDatabaseName() . '.'
191
            . $user->getCacheKey() . '.' . $namespace . '.' . $redirects;
192
        if ($this->cache->hasItem($cacheKey)) {
193
            return $this->cache->getItem($cacheKey)->get();
194
        }
195
        $this->stopwatch->start($cacheKey, 'XTools');
196
197
        $dbName = $project->getDatabaseName();
198
        $projectRepo = $project->getRepository();
199
200
        $pageTable = $projectRepo->getTableName($dbName, 'page');
201
        $pageAssessmentsTable = $projectRepo->getTableName($dbName, 'page_assessments');
202
        $revisionTable = $projectRepo->getTableName($dbName, 'revision');
203
        $archiveTable = $projectRepo->getTableName($dbName, 'archive');
204
        $logTable = $projectRepo->getTableName($dbName, 'logging', 'userindex');
205
206
        $userId = $user->getId($project);
207
208
        $namespaceConditionArc = '';
209
        $namespaceConditionRev = '';
210
211
        if ($namespace != 'all') {
212
            $namespaceConditionRev = " AND page_namespace = '".intval($namespace)."' ";
213
            $namespaceConditionArc = " AND ar_namespace = '".intval($namespace)."' ";
214
        }
215
216
        $redirectCondition = '';
217
218
        if ($redirects == 'onlyredirects') {
219
            $redirectCondition = " AND page_is_redirect = '1' ";
220
        } elseif ($redirects == 'noredirects') {
221
            $redirectCondition = " AND page_is_redirect = '0' ";
222
        }
223
224
        if ($userId == 0) { // IP Editor or undefined username.
225
            $whereRev = " rev_user_text = '$username' AND rev_user = '0' ";
226
            $whereArc = " ar_user_text = '$username' AND ar_user = '0' ";
227
            $having = " rev_user_text = '$username' ";
228
        } else {
229
            $whereRev = " rev_user = '$userId' AND rev_timestamp > 1 ";
230
            $whereArc = " ar_user = '$userId' AND ar_timestamp > 1 ";
231
            $having = " rev_user = '$userId' ";
232
        }
233
234
        $hasPageAssessments = $this->isLabs() && $project->hasPageAssessments();
235
        $paSelects = $hasPageAssessments ? ', pa_class, pa_importance, pa_page_revision' : '';
236
        $paSelectsArchive = $hasPageAssessments ?
237
            ', NULL AS pa_class, NULL AS pa_page_id, NULL AS pa_page_revision'
238
            : '';
239
        $paJoin = $hasPageAssessments ? "LEFT JOIN $pageAssessmentsTable ON rev_page = pa_page_id" : '';
240
241
        $sql = "
242
            (SELECT DISTINCT page_namespace AS namespace, 'rev' AS type, page_title AS page_title,
243
                page_len, page_is_redirect, rev_timestamp AS rev_timestamp,
244
                rev_user, rev_user_text AS username, rev_len, rev_id $paSelects
245
            FROM $pageTable
246
            JOIN $revisionTable ON page_id = rev_page
247
            $paJoin
248
            WHERE $whereRev AND rev_parent_id = '0' $namespaceConditionRev $redirectCondition
249
            " . ($hasPageAssessments ? 'GROUP BY rev_page' : '') . "
250
            )
251
252
            UNION
253
254
            (SELECT a.ar_namespace AS namespace, 'arc' AS type, a.ar_title AS page_title,
255
                0 AS page_len, '0' AS page_is_redirect, MIN(a.ar_timestamp) AS rev_timestamp,
256
                a.ar_user AS rev_user, a.ar_user_text AS username, a.ar_len AS rev_len,
257
                a.ar_rev_id AS rev_id $paSelectsArchive
258
            FROM $archiveTable a
259
            JOIN
260
            (
261
                SELECT b.ar_namespace, b.ar_title
262
                FROM $archiveTable AS b
263
                LEFT JOIN $logTable ON log_namespace = b.ar_namespace AND log_title = b.ar_title
264
                    AND log_user = b.ar_user AND (log_action = 'move' OR log_action = 'move_redir')
265
                WHERE $whereArc AND b.ar_parent_id = '0' $namespaceConditionArc AND log_action IS NULL
266
            ) AS c ON c.ar_namespace= a.ar_namespace AND c.ar_title = a.ar_title
267
            GROUP BY a.ar_namespace, a.ar_title
268
            HAVING $having
269
            )
270
            ";
271
272
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
273
        $resultQuery->execute();
274
        $result = $resultQuery->fetchAll();
275
276
        // Cache for 10 minutes, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
277
        $cacheItem = $this->cache->getItem($cacheKey)
278
            ->set($result)
279
            ->expiresAfter(new DateInterval('PT10M'));
280
        $this->cache->save($cacheItem);
281
        $this->stopwatch->stop($cacheKey);
282
283
        return $result;
284
    }
285
286
    /**
287
     * Get edit count within given timeframe and namespace
288
     * @param Project $project
289
     * @param User $user
290
     * @param int|string [$namespace] Namespace ID or 'all' for all namespaces
291
     * @param string [$start] Start date in a format accepted by strtotime()
292
     * @param string [$end] End date in a format accepted by strtotime()
293
     */
294
    public function countEdits(Project $project, User $user, $namespace = 'all', $start = '', $end = '')
295
    {
296
        $cacheKey = 'editcount.' . $project->getDatabaseName() . '.'
297
            . $user->getCacheKey() . '.' . $namespace;
298
299
        $condBegin = '';
300
        $condEnd = '';
301
302 View Code Duplication
        if (!empty($start)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
303
            $cacheKey .= '.' . $start;
304
305
            // For the query
306
            $start = date('Ymd000000', strtotime($start));
307
            $condBegin = 'AND rev_timestamp >= :start ';
308
        }
309 View Code Duplication
        if (!empty($end)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
310
            $cacheKey .= '.' . $end;
311
312
            // For the query
313
            $end = date('Ymd235959', strtotime($end));
314
            $condEnd = 'AND rev_timestamp <= :end ';
315
        }
316
317
        if ($this->cache->hasItem($cacheKey)) {
318
            return $this->cache->getItem($cacheKey)->get();
319
        }
320
321
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
322
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
323
        $condNamespace = $namespace === 'all' ? '' : 'AND page_namespace = :namespace';
324
        $pageJoin = $namespace === 'all' ? '' : "JOIN $pageTable ON rev_page = page_id";
325
326
        $sql = "SELECT COUNT(rev_id)
327
                FROM $revisionTable
328
                $pageJoin
329
                WHERE rev_user_text = :username
330
                $condNamespace
331
                $condBegin
332
                $condEnd";
333
334
        $username = $user->getUsername();
335
        $conn = $this->getProjectsConnection();
336
        $resultQuery = $conn->prepare($sql);
337
        $resultQuery->bindParam('username', $username);
338
        if (!empty($start)) {
339
            $resultQuery->bindParam('start', $start);
340
        }
341
        if (!empty($end)) {
342
            $resultQuery->bindParam('end', $end);
343
        }
344
        if ($namespace !== 'all') {
345
            $resultQuery->bindParam('namespace', $namespace);
346
        }
347
        $resultQuery->execute();
348
        $result = $resultQuery->fetchColumn();
349
350
        // Cache for 10 minutes, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
351
        $cacheItem = $this->cache->getItem($cacheKey)
352
            ->set($result)
353
            ->expiresAfter(new DateInterval('PT10M'));
354
        $this->cache->save($cacheItem);
355
356
        return $result;
357
    }
358
359
    /**
360
     * Get the number of edits this user made using semi-automated tools.
361
     * @param Project $project
362
     * @param User $user
363
     * @param string|int [$namespace] Namespace ID or 'all'
364
     * @param string [$start] Start date in a format accepted by strtotime()
365
     * @param string [$end] End date in a format accepted by strtotime()
366
     * @return int Result of query, see below.
367
     */
368
    public function countAutomatedEdits(Project $project, User $user, $namespace = 'all', $start = '', $end = '')
369
    {
370
        $cacheKey = 'autoeditcount.' . $project->getDatabaseName() . '.'
371
            . $user->getCacheKey() . '.' . $namespace;
372
373
        $condBegin = '';
374
        $condEnd = '';
375
376 View Code Duplication
        if (!empty($start)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
377
            $cacheKey .= '.' . $start;
378
379
            // For the query
380
            $start = date('Ymd000000', strtotime($start));
381
            $condBegin = 'AND rev_timestamp >= :start ';
382
        }
383 View Code Duplication
        if (!empty($end)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
384
            $cacheKey .= '.' . $end;
385
386
            // For the query
387
            $end = date('Ymd235959', strtotime($end));
388
            $condEnd = 'AND rev_timestamp <= :end ';
389
        }
390
391
        if ($this->cache->hasItem($cacheKey)) {
392
            return $this->cache->getItem($cacheKey)->get();
393
        }
394
        $this->stopwatch->start($cacheKey, 'XTools');
395
396
        // Get the combined regex and tags for the tools
397
        $conn = $this->getProjectsConnection();
398
        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...
399
400
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
401
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
402
        $tagTable = $this->getTableName($project->getDatabaseName(), 'change_tag');
403
        $condNamespace = $namespace === 'all' ? '' : 'AND page_namespace = :namespace';
404
        $pageJoin = $namespace === 'all' ? '' : "JOIN $pageTable ON page_id = rev_page";
405
        $tagJoin = '';
406
407
        // Build SQL for detecting autoedits via regex and/or tags
408
        $condTools = [];
409
        if ($regex != '') {
410
            $condTools[] = "rev_comment REGEXP $regex";
411
        }
412
        if ($tags != '') {
413
            $tagJoin = $tags != '' ? "LEFT OUTER JOIN $tagTable ON ct_rev_id = rev_id" : '';
414
            $condTools[] = "ct_tag IN ($tags)";
415
        }
416
        $condTool = 'AND (' . implode(' OR ', $condTools) . ')';
417
418
        $sql = "SELECT COUNT(DISTINCT(rev_id))
419
                FROM $revisionTable
420
                $pageJoin
421
                $tagJoin
422
                WHERE rev_user_text = :username
423
                $condTool
424
                $condNamespace
425
                $condBegin
426
                $condEnd";
427
428
        $username = $user->getUsername();
429
        $resultQuery = $conn->prepare($sql);
430
        $resultQuery->bindParam('username', $username);
431
        if (!empty($start)) {
432
            $resultQuery->bindParam('start', $start);
433
        }
434
        if (!empty($end)) {
435
            $resultQuery->bindParam('end', $end);
436
        }
437
        if ($namespace !== 'all') {
438
            $resultQuery->bindParam('namespace', $namespace);
439
        }
440
        $resultQuery->execute();
441
        $result = (int) $resultQuery->fetchColumn();
442
443
        // Cache for 10 minutes, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
444
        $cacheItem = $this->cache->getItem($cacheKey)
445
            ->set($result)
446
            ->expiresAfter(new DateInterval('PT10M'));
447
        $this->cache->save($cacheItem);
448
        $this->stopwatch->stop($cacheKey);
449
450
        return $result;
451
    }
452
453
    /**
454
     * Get non-automated contributions for the given user.
455
     * @param Project $project
456
     * @param User $user
457
     * @param string|int [$namespace] Namespace ID or 'all'
458
     * @param string [$start] Start date in a format accepted by strtotime()
459
     * @param string [$end] End date in a format accepted by strtotime()
460
     * @param int [$offset] Used for pagination, offset results by N edits
461
     * @return string[] Result of query, with columns 'page_title',
462
     *   'page_namespace', 'rev_id', 'timestamp', 'minor',
463
     *   'length', 'length_change', 'comment'
464
     */
465
    public function getNonAutomatedEdits(
466
        Project $project,
467
        User $user,
468
        $namespace = 'all',
469
        $start = '',
470
        $end = '',
471
        $offset = 0
472
    ) {
473
        $cacheKey = 'nonautoedits.' . $project->getDatabaseName() . '.'
474
            . $user->getCacheKey() . '.' . $namespace . '.' . $offset;
475
476
        $condBegin = '';
477
        $condEnd = '';
478
479 View Code Duplication
        if (!empty($start)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
480
            $cacheKey .= '.' . $start;
481
482
            // For the query
483
            $start = date('Ymd000000', strtotime($start));
484
            $condBegin = 'AND revs.rev_timestamp >= :start ';
485
        }
486 View Code Duplication
        if (!empty($end)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
487
            $cacheKey .= '.' . $end;
488
489
            // For the query
490
            $end = date('Ymd235959', strtotime($end));
491
            $condEnd = 'AND revs.rev_timestamp <= :end ';
492
        }
493
494
        if ($this->cache->hasItem($cacheKey)) {
495
            return $this->cache->getItem($cacheKey)->get();
496
        }
497
        $this->stopwatch->start($cacheKey, 'XTools');
498
499
        // Get the combined regex and tags for the tools
500
        $conn = $this->getProjectsConnection();
501
        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...
502
503
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
504
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
505
        $tagTable = $this->getTableName($project->getDatabaseName(), 'change_tag');
506
        $condNamespace = $namespace === 'all' ? '' : 'AND page_namespace = :namespace';
507
        $tagJoin = $tags != '' ? "LEFT OUTER JOIN $tagTable ON (ct_rev_id = revs.rev_id)" : '';
508
        $condTag = $tags != '' ? "AND (ct_tag NOT IN ($tags) OR ct_tag IS NULL)" : '';
509
        $sql = "SELECT
510
                    page_title,
511
                    page_namespace,
512
                    revs.rev_id AS rev_id,
513
                    revs.rev_timestamp AS timestamp,
514
                    revs.rev_minor_edit AS minor,
515
                    revs.rev_len AS length,
516
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
517
                    revs.rev_comment AS comment
518
                FROM $pageTable
519
                JOIN $revisionTable AS revs ON (page_id = revs.rev_page)
520
                LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
521
                $tagJoin
522
                WHERE revs.rev_user_text = :username
523
                AND revs.rev_timestamp > 0
524
                AND revs.rev_comment NOT RLIKE $regex
525
                $condTag
526
                $condBegin
527
                $condEnd
528
                $condNamespace
529
                ORDER BY revs.rev_timestamp DESC
530
                LIMIT 50
531
                OFFSET $offset";
532
533
        $username = $user->getUsername();
534
        $resultQuery = $conn->prepare($sql);
535
        $resultQuery->bindParam('username', $username);
536
        if (!empty($start)) {
537
            $resultQuery->bindParam('start', $start);
538
        }
539
        if (!empty($end)) {
540
            $resultQuery->bindParam('end', $end);
541
        }
542
        if ($namespace !== 'all') {
543
            $resultQuery->bindParam('namespace', $namespace);
544
        }
545
        $resultQuery->execute();
546
        $result = $resultQuery->fetchAll();
547
548
        // Cache for 10 minutes, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
549
        $cacheItem = $this->cache->getItem($cacheKey)
550
            ->set($result)
551
            ->expiresAfter(new DateInterval('PT10M'));
552
        $this->cache->save($cacheItem);
553
        $this->stopwatch->stop($cacheKey);
554
555
        return $result;
556
    }
557
558
    /**
559
     * Get non-automated contributions for the given user.
560
     * @param Project $project
561
     * @param User $user
562
     * @param string|int [$namespace] Namespace ID or 'all'
563
     * @param string [$start] Start date in a format accepted by strtotime()
564
     * @param string [$end] End date in a format accepted by strtotime()
565
     * @return string[] Each tool that they used along with the count and link:
566
     *                  [
567
     *                      'Twinkle' => [
568
     *                          'count' => 50,
569
     *                          'link' => 'Wikipedia:Twinkle',
570
     *                      ],
571
     *                  ]
572
     */
573
    public function getAutomatedCounts(
574
        Project $project,
575
        User $user,
576
        $namespace = 'all',
577
        $start = '',
578
        $end = ''
579
    ) {
580
        $cacheKey = 'autotoolcounts.' . $project->getDatabaseName() . '.'
581
            . $user->getCacheKey() . '.' . $namespace;
582
583
        $condBegin = '';
584
        $condEnd = '';
585
586 View Code Duplication
        if (!empty($start)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
587
            $cacheKey .= '.' . $start;
588
589
            // For the query
590
            $start = date('Ymd000000', strtotime($start));
591
            $condBegin = 'AND rev_timestamp >= :start ';
592
        }
593 View Code Duplication
        if (!empty($end)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
594
            $cacheKey .= '.' . $end;
595
596
            // For the query
597
            $end = date('Ymd235959', strtotime($end));
598
            $condEnd = 'AND rev_timestamp <= :end ';
599
        }
600
601
        if ($this->cache->hasItem($cacheKey)) {
602
            return $this->cache->getItem($cacheKey)->get();
603
        }
604
        $this->stopwatch->start($cacheKey, 'XTools');
605
606
        $conn = $this->getProjectsConnection();
607
608
        // Load the semi-automated edit types.
609
        $automatedEditsHelper = $this->container->get('app.automated_edits_helper');
610
        $tools = $automatedEditsHelper->getTools($project->getDomain());
611
612
        // Create a collection of queries that we're going to run.
613
        $queries = [];
614
615
        $revisionTable = $project->getRepository()->getTableName($project->getDatabaseName(), 'revision');
616
        $pageTable = $project->getRepository()->getTableName($project->getDatabaseName(), 'page');
617
        $tagTable = $project->getRepository()->getTableName($project->getDatabaseName(), 'change_tag');
618
619
        $pageJoin = $namespace !== 'all' ? "LEFT JOIN $pageTable ON rev_page = page_id" : null;
620
        $condNamespace = $namespace !== 'all' ? "AND page_namespace = :namespace" : null;
621
622
        foreach ($tools as $toolname => $values) {
623
            $tagJoin = '';
624
            $condTool = '';
625
            $toolname = $conn->quote($toolname, \PDO::PARAM_STR);
626
627
            if (isset($values['regex'])) {
628
                $regex = $conn->quote($values['regex'], \PDO::PARAM_STR);
629
                $condTool = "rev_comment REGEXP $regex";
630
            }
631
            if (isset($values['tag'])) {
632
                $tagJoin = "LEFT OUTER JOIN $tagTable ON ct_rev_id = rev_id";
633
                $tag = $conn->quote($values['tag'], \PDO::PARAM_STR);
634
635
                // Append to regex clause if already present.
636
                // Tags are more reliable but may not be present for edits made with
637
                //   older versions of the tool, before it started adding tags.
638
                if ($condTool === '') {
639
                    $condTool = "ct_tag = $tag";
640
                } else {
641
                    $condTool = '(' . $condTool . " OR ct_tag = $tag)";
642
                }
643
            }
644
645
            // Developer error, no regex or tag provided for this tool.
646
            if ($condTool === '') {
647
                throw new Exception("No regex or tag found for the tool $toolname. " .
648
                    "Please verify this entry in semi_automated.yml");
649
            }
650
651
            $queries[] .= "
652
                SELECT $toolname AS toolname, COUNT(rev_id) AS count
653
                FROM $revisionTable
654
                $pageJoin
655
                $tagJoin
656
                WHERE rev_user_text = :username
657
                AND $condTool
658
                $condNamespace
659
                $condBegin
660
                $condEnd";
661
        }
662
663
        // Create a big query and execute.
664
        $sql = implode(' UNION ', $queries);
665
666
        $resultQuery = $conn->prepare($sql);
667
668
        $username = $user->getUsername(); // use normalized user name
669
        $resultQuery->bindParam('username', $username);
670
        if (!empty($start)) {
671
            $startParam = date('Ymd000000', strtotime($start));
672
            $resultQuery->bindParam('start', $startParam);
673
        }
674
        if (!empty($end)) {
675
            $endParam = date('Ymd235959', strtotime($end));
676
            $resultQuery->bindParam('end', $endParam);
677
        }
678
        if ($namespace !== 'all') {
679
            $resultQuery->bindParam('namespace', $namespace);
680
        }
681
682
        $resultQuery->execute();
683
684
        // handling results
685
        $results = [];
686
687
        while ($row = $resultQuery->fetch()) {
688
            // Only track tools that they've used at least once
689
            $tool = $row['toolname'];
690
            if ($row['count'] > 0) {
691
                $results[$tool] = [
692
                    'link' => $tools[$tool]['link'],
693
                    'count' => $row['count'],
694
                ];
695
            }
696
        }
697
698
        // Sort the array by count
699
        uasort($results, function ($a, $b) {
700
            return $b['count'] - $a['count'];
701
        });
702
703
        // Cache for 10 minutes, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
704
        $cacheItem = $this->cache->getItem($cacheKey)
705
            ->set($results)
706
            ->expiresAfter(new DateInterval('PT10M'));
707
        $this->cache->save($cacheItem);
708
        $this->stopwatch->stop($cacheKey);
709
710
        return $results;
711
    }
712
713
    /**
714
     * Get information about the currently-logged in user.
715
     * @return array
716
     */
717
    public function getXtoolsUserInfo()
718
    {
719
        /** @var Session $session */
720
        $session = $this->container->get('session');
721
        return $session->get('logged_in_user');
722
    }
723
724
    /**
725
     * Get the combined regex and tags for all semi-automated tools,
726
     *   ready to be used in a query.
727
     * @param string $projectDomain Such as en.wikipedia.org
728
     * @param $conn Doctrine\DBAL\Connection Used for proper escaping
729
     * @return string[] In the format:
730
     *    ['combined|regex', 'combined,tags']
731
     */
732
    private function getToolRegexAndTags($projectDomain, $conn)
733
    {
734
        $automatedEditsHelper = $this->container->get('app.automated_edits_helper');
735
        $tools = $automatedEditsHelper->getTools($projectDomain);
736
        $regexes = [];
737
        $tags = [];
738
        foreach ($tools as $tool => $values) {
739
            if (isset($values['regex'])) {
740
                $regexes[] = $values['regex'];
741
            }
742
            if (isset($values['tag'])) {
743
                $tags[] = $conn->quote($values['tag'], \PDO::PARAM_STR);
744
            }
745
        }
746
747
        return [
748
            $conn->quote(implode('|', $regexes), \PDO::PARAM_STR),
749
            implode(',', $tags),
750
        ];
751
    }
752
}
753