Passed
Push — master ( d05aeb...0815a5 )
by MusikAnimal
01:39
created

UserRepository::getInnerAutomatedCountsSql()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 15
nc 6
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
 * @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 = [
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.
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
315
        $pageTable = $project->getTableName('page');
316
        $revisionTable = $project->getTableName('revision');
317
        $condNamespace = $namespace === 'all' ? '' : 'AND page_namespace = :namespace';
318
        $pageJoin = $namespace === 'all' ? '' : "JOIN $pageTable ON rev_page = page_id";
319
320
        $sql = "SELECT COUNT(rev_id)
321
                FROM $revisionTable
322
                $pageJoin
323
                WHERE rev_user_text = :username
324
                $condNamespace
325
                $condBegin
326
                $condEnd";
327
328
        $resultQuery = $this->executeQuery($sql, $user, $namespace, $start, $end);
329
        $result = $resultQuery->fetchColumn();
330
331
        // Cache for 10 minutes, and return.
332
        $cacheItem = $this->cache->getItem($cacheKey)
333
            ->set($result)
334
            ->expiresAfter(new DateInterval('PT10M'));
335
        $this->cache->save($cacheItem);
336
337
        return $result;
338
    }
339
340
    /**
341
     * Get the number of edits this user made using semi-automated tools.
342
     * @param Project $project
343
     * @param User $user
344
     * @param string|int $namespace Namespace ID or 'all'
345
     * @param string $start Start date in a format accepted by strtotime()
346
     * @param string $end End date in a format accepted by strtotime()
347
     * @return int Result of query, see below.
348
     */
349
    public function countAutomatedEdits(Project $project, User $user, $namespace = 'all', $start = '', $end = '')
350
    {
351
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_autoeditcount');
352
        if ($this->cache->hasItem($cacheKey)) {
353
            return $this->cache->getItem($cacheKey)->get();
354
        }
355
        $this->stopwatch->start($cacheKey, 'XTools');
356
357
        list($condBegin, $condEnd) = $this->getRevTimestampConditions($start, $end);
358
359
        // Get the combined regex and tags for the tools
360
        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...
361
362
        $pageTable = $project->getTableName('page');
363
        $revisionTable = $project->getTableName('revision');
364
        $tagTable = $project->getTableName('change_tag');
365
        $condNamespace = $namespace === 'all' ? '' : 'AND page_namespace = :namespace';
366
        $pageJoin = $namespace === 'all' ? '' : "JOIN $pageTable ON page_id = rev_page";
367
        $tagJoin = '';
368
369
        // Build SQL for detecting autoedits via regex and/or tags
370
        $condTools = [];
371
        if ($regex != '') {
372
            $condTools[] = "rev_comment REGEXP $regex";
373
        }
374
        if ($tags != '') {
375
            $tagJoin = $tags != '' ? "LEFT OUTER JOIN $tagTable ON ct_rev_id = rev_id" : '';
376
            $condTools[] = "ct_tag IN ($tags)";
377
        }
378
        $condTool = 'AND (' . implode(' OR ', $condTools) . ')';
379
380
        $sql = "SELECT COUNT(DISTINCT(rev_id))
381
                FROM $revisionTable
382
                $pageJoin
383
                $tagJoin
384
                WHERE rev_user_text = :username
385
                $condTool
386
                $condNamespace
387
                $condBegin
388
                $condEnd";
389
390
        $resultQuery = $this->executeQuery($sql, $user, $namespace, $start, $end);
391
        $result = (int) $resultQuery->fetchColumn();
392
393
        // Cache for 10 minutes, and return.
394
        $cacheItem = $this->cache->getItem($cacheKey)
395
            ->set($result)
396
            ->expiresAfter(new DateInterval('PT10M'));
397
        $this->cache->save($cacheItem);
398
        $this->stopwatch->stop($cacheKey);
399
400
        return $result;
401
    }
402
403
    /**
404
     * Get non-automated contributions for the given user.
405
     * @param Project $project
406
     * @param User $user
407
     * @param string|int $namespace Namespace ID or 'all'
408
     * @param string $start Start date in a format accepted by strtotime()
409
     * @param string $end End date in a format accepted by strtotime()
410
     * @param int $offset Used for pagination, offset results by N edits
411
     * @return string[] Result of query, with columns 'page_title',
412
     *   'page_namespace', 'rev_id', 'timestamp', 'minor',
413
     *   'length', 'length_change', 'comment'
414
     */
415
    public function getNonAutomatedEdits(
416
        Project $project,
417
        User $user,
418
        $namespace = 'all',
419
        $start = '',
420
        $end = '',
421
        $offset = 0
422
    ) {
423
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_nonautoedits');
424
        if ($this->cache->hasItem($cacheKey)) {
425
            return $this->cache->getItem($cacheKey)->get();
426
        }
427
        $this->stopwatch->start($cacheKey, 'XTools');
428
429
        list($condBegin, $condEnd) = $this->getRevTimestampConditions($start, $end);
430
431
        // Get the combined regex and tags for the tools
432
        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...
433
434
        $pageTable = $project->getTableName('page');
435
        $revisionTable = $project->getTableName('revision');
436
        $tagTable = $project->getTableName('change_tag');
437
        $condNamespace = $namespace === 'all' ? '' : 'AND page_namespace = :namespace';
438
        $tagJoin = $tags != '' ? "LEFT OUTER JOIN $tagTable ON (ct_rev_id = revs.rev_id)" : '';
439
        $condTag = $tags != '' ? "AND (ct_tag NOT IN ($tags) OR ct_tag IS NULL)" : '';
440
        $sql = "SELECT
441
                    page_title,
442
                    page_namespace,
443
                    revs.rev_id AS rev_id,
444
                    revs.rev_timestamp AS timestamp,
445
                    revs.rev_minor_edit AS minor,
446
                    revs.rev_len AS length,
447
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
448
                    revs.rev_comment AS comment
449
                FROM $pageTable
450
                JOIN $revisionTable AS revs ON (page_id = revs.rev_page)
451
                LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
452
                $tagJoin
453
                WHERE revs.rev_user_text = :username
454
                AND revs.rev_timestamp > 0
455
                AND revs.rev_comment NOT RLIKE $regex
456
                $condTag
457
                $condBegin
458
                $condEnd
459
                $condNamespace
460
                ORDER BY revs.rev_timestamp DESC
461
                LIMIT 50
462
                OFFSET $offset";
463
464
        $resultQuery = $this->executeQuery($sql, $user, $namespace, $start, $end);
465
        $result = $resultQuery->fetchAll();
466
467
        // Cache for 10 minutes, and return.
468
        $cacheItem = $this->cache->getItem($cacheKey)
469
            ->set($result)
470
            ->expiresAfter(new DateInterval('PT10M'));
471
        $this->cache->save($cacheItem);
472
        $this->stopwatch->stop($cacheKey);
473
474
        return $result;
475
    }
476
477
    /**
478
     * Get counts of known automated tools used by the given user.
479
     * @param Project $project
480
     * @param User $user
481
     * @param string|int $namespace Namespace ID or 'all'.
482
     * @param string $start Start date in a format accepted by strtotime()
483
     * @param string $end End date in a format accepted by strtotime()
484
     * @return string[] Each tool that they used along with the count and link:
485
     *                  [
486
     *                      'Twinkle' => [
487
     *                          'count' => 50,
488
     *                          'link' => 'Wikipedia:Twinkle',
489
     *                      ],
490
     *                  ]
491
     */
492
    public function getAutomatedCounts(
493
        Project $project,
494
        User $user,
495
        $namespace = 'all',
496
        $start = '',
497
        $end = ''
498
    ) {
499
        $cacheKey = $this->getCacheKey(func_get_args(), 'user_autotoolcounts');
500
        if ($this->cache->hasItem($cacheKey)) {
501
            return $this->cache->getItem($cacheKey)->get();
502
        }
503
        $this->stopwatch->start($cacheKey, 'XTools');
504
505
        $sql = $this->getAutomatedCountsSql($project, $namespace, $start, $end);
506
        $resultQuery = $this->executeQuery($sql, $user, $namespace, $start, $end);
507
508
        $automatedEditsHelper = $this->container->get('app.automated_edits_helper');
509
        $tools = $automatedEditsHelper->getTools($project->getDomain());
510
511
        // handling results
512
        $results = [];
513
514
        while ($row = $resultQuery->fetch()) {
515
            // Only track tools that they've used at least once
516
            $tool = $row['toolname'];
517
            if ($row['count'] > 0) {
518
                $results[$tool] = [
519
                    'link' => $tools[$tool]['link'],
520
                    'count' => $row['count'],
521
                ];
522
            }
523
        }
524
525
        // Sort the array by count
526
        uasort($results, function ($a, $b) {
527
            return $b['count'] - $a['count'];
528
        });
529
530
        // Cache for 10 minutes, and return.
531
        $cacheItem = $this->cache->getItem($cacheKey)
532
            ->set($results)
533
            ->expiresAfter(new DateInterval('PT10M'));
534
        $this->cache->save($cacheItem);
535
        $this->stopwatch->stop($cacheKey);
536
537
        return $results;
538
    }
539
540
    /**
541
     * Get SQL for getting counts of known automated tools used by the given user.
542
     * @see self::getAutomatedCounts()
543
     * @param Project $project
544
     * @param string|int $namespace Namespace ID or 'all'.
545
     * @param string $start Start date in a format accepted by strtotime()
546
     * @param string $end End date in a format accepted by strtotime()
547
     * @return string The SQL.
548
     */
549
    private function getAutomatedCountsSql(Project $project, $namespace, $start, $end)
550
    {
551
        list($condBegin, $condEnd) = $this->getRevTimestampConditions($start, $end);
552
553
        // Load the semi-automated edit types.
554
        $automatedEditsHelper = $this->container->get('app.automated_edits_helper');
555
        $tools = $automatedEditsHelper->getTools($project->getDomain());
556
557
        // Create a collection of queries that we're going to run.
558
        $queries = [];
559
560
        $revisionTable = $project->getTableName('revision');
561
        $pageTable = $project->getTableName('page');
562
        $tagTable = $project->getTableName('change_tag');
563
564
        $pageJoin = $namespace !== 'all' ? "LEFT JOIN $pageTable ON rev_page = page_id" : null;
565
        $condNamespace = $namespace !== 'all' ? "AND page_namespace = :namespace" : null;
566
567
        $conn = $this->getProjectsConnection();
568
569
        foreach ($tools as $toolname => $values) {
570
            list($condTool, $tagJoin) = $this->getInnerAutomatedCountsSql($tagTable, $values);
571
572
            $toolname = $conn->quote($toolname, \PDO::PARAM_STR);
573
574
            // Developer error, no regex or tag provided for this tool.
575
            if ($condTool === '') {
576
                throw new Exception("No regex or tag found for the tool $toolname. " .
577
                    "Please verify this entry in semi_automated.yml");
578
            }
579
580
            $queries[] .= "
581
                SELECT $toolname AS toolname, COUNT(rev_id) AS count
582
                FROM $revisionTable
583
                $pageJoin
584
                $tagJoin
585
                WHERE rev_user_text = :username
586
                AND $condTool
587
                $condNamespace
588
                $condBegin
589
                $condEnd";
590
        }
591
592
        // Combine to one big query.
593
        return implode(' UNION ', $queries);
594
    }
595
596
    /**
597
     * Get some of the inner SQL for self::getAutomatedCountsSql().
598
     * @param  string $tagTable Name of the `change_tag` table.
599
     * @param  string[] $values Values as defined in semi_automated.yml
600
     * @return string[] [Equality clause, JOIN clause]
601
     */
602
    private function getInnerAutomatedCountsSql($tagTable, $values)
603
    {
604
        $conn = $this->getProjectsConnection();
605
        $tagJoin = '';
606
        $condTool = '';
607
608
        if (isset($values['regex'])) {
609
            $regex = $conn->quote($values['regex'], \PDO::PARAM_STR);
610
            $condTool = "rev_comment REGEXP $regex";
611
        }
612
        if (isset($values['tag'])) {
613
            $tagJoin = "LEFT OUTER JOIN $tagTable ON ct_rev_id = rev_id";
614
            $tag = $conn->quote($values['tag'], \PDO::PARAM_STR);
615
616
            // Append to regex clause if already present.
617
            // Tags are more reliable but may not be present for edits made with
618
            //   older versions of the tool, before it started adding tags.
619
            if ($condTool === '') {
620
                $condTool = "ct_tag = $tag";
621
            } else {
622
                $condTool = '(' . $condTool . " OR ct_tag = $tag)";
623
            }
624
        }
625
626
        return [$condTool, $tagJoin];
627
    }
628
629
    /**
630
     * Get information about the currently-logged in user.
631
     * @return array
632
     */
633
    public function getXtoolsUserInfo()
634
    {
635
        /** @var Session $session */
636
        $session = $this->container->get('session');
637
        return $session->get('logged_in_user');
638
    }
639
640
    /**
641
     * Maximum number of edits to process, based on configuration.
642
     * @return int
643
     */
644
    public function maxEdits()
645
    {
646
        return $this->container->getParameter('app.max_user_edits');
647
    }
648
649
    /**
650
     * Get the combined regex and tags for all semi-automated tools,
651
     *   ready to be used in a query.
652
     * @param string $projectDomain Such as en.wikipedia.org
653
     * @return string[] In the format:
654
     *    ['combined|regex', 'combined,tags']
655
     */
656
    private function getToolRegexAndTags($projectDomain)
657
    {
658
        $conn = $this->getProjectsConnection();
659
        $automatedEditsHelper = $this->container->get('app.automated_edits_helper');
660
        $tools = $automatedEditsHelper->getTools($projectDomain);
661
        $regexes = [];
662
        $tags = [];
663
664
        foreach ($tools as $tool => $values) {
665
            if (isset($values['regex'])) {
666
                $regexes[] = $values['regex'];
667
            }
668
            if (isset($values['tag'])) {
669
                $tags[] = $conn->quote($values['tag'], \PDO::PARAM_STR);
670
            }
671
        }
672
673
        return [
674
            $conn->quote(implode('|', $regexes), \PDO::PARAM_STR),
675
            implode(',', $tags),
676
        ];
677
    }
678
679
    /**
680
     * Get SQL clauses for rev_timestamp, based on whether values for
681
     * the given start and end parameters exist.
682
     * @param  string $start
683
     * @param  string $end
684
     * @return string[] Clauses for start and end timestamps.
685
     */
686
    private function getRevTimestampConditions($start, $end)
687
    {
688
        $condBegin = '';
689
        $condEnd = '';
690
691
        if (!empty($start)) {
692
            $condBegin = 'AND rev_timestamp >= :start ';
693
        }
694
        if (!empty($end)) {
695
            $condEnd = 'AND rev_timestamp <= :end ';
696
        }
697
698
        return [$condBegin, $condEnd];
699
    }
700
701
    /**
702
     * Prepare the given SQL, bind the given parameters, and execute the Doctrine Statement.
703
     * @param  string $sql
704
     * @param  User   $user
705
     * @param  string $namespace
706
     * @param  string $start
707
     * @param  string $end
708
     * @return Doctrine\DBAL\Statement
709
     */
710
    private function executeQuery($sql, User $user, $namespace = 'all', $start = '', $end = '')
711
    {
712
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
713
        $username = $user->getUsername();
714
        $resultQuery->bindParam('username', $username);
715
716
        if (!empty($start)) {
717
            $start = date('Ymd000000', strtotime($start));
718
            $resultQuery->bindParam('start', $start);
719
        }
720
        if (!empty($end)) {
721
            $end = date('Ymd235959', strtotime($end));
722
            $resultQuery->bindParam('end', $end);
723
        }
724
        if ($namespace !== 'all') {
725
            $resultQuery->bindParam('namespace', $namespace);
726
        }
727
728
        $resultQuery->execute();
729
730
        return $resultQuery;
731
    }
732
}
733