Passed
Push — master ( 885c2d...cbeb5d )
by MusikAnimal
05:30
created

EditCounterRepository   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 529
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 529
rs 8.2608
c 0
b 0
f 0
wmc 40

12 Methods

Rating   Name   Duplication   Size   Complexity  
B getLogCounts() 0 77 6
A countAutomatedEdits() 0 5 1
B getRevisions() 0 47 4
A globalEditCountsFromDatabases() 0 19 2
A getBlocksReceived() 0 15 1
B getEditSizeData() 0 30 4
B getTimeCard() 0 26 2
B getPairData() 0 80 6
B getNamespaceTotals() 0 30 4
B globalEditCountsFromCentralAuth() 0 40 5
A globalEditCounts() 0 20 3
B getMonthCounts() 0 27 2

How to fix   Complexity   

Complex Class

Complex classes like EditCounterRepository 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.

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 EditCounterRepository, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file contains only the EditCounterRepository class.
4
 */
5
6
namespace Xtools;
7
8
use Mediawiki\Api\SimpleRequest;
9
10
/**
11
 * An EditCounterRepository is responsible for retrieving edit count information from the
12
 * databases and API. It doesn't do any post-processing of that information.
13
 * @codeCoverageIgnore
14
 */
15
class EditCounterRepository extends UserRightsRepository
16
{
17
    /**
18
     * Get data about revisions, pages, etc.
19
     * @param Project $project The project.
20
     * @param User $user The user.
21
     * @return string[] With keys: 'deleted', 'live', 'total', 'first', 'last', '24h', '7d', '30d',
22
     * '365d', 'small', 'large', 'with_comments', and 'minor_edits', ...
23
     */
24
    public function getPairData(Project $project, User $user)
25
    {
26
        // Set up cache.
27
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_pairdata');
28
        if ($this->cache->hasItem($cacheKey)) {
29
            return $this->cache->getItem($cacheKey)->get();
30
        }
31
32
        // Prepare the queries and execute them.
33
        $archiveTable = $this->getTableName($project->getDatabaseName(), 'archive');
34
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
35
36
        // For IPs we use rev_user_text, and for accounts rev_user which is slightly faster.
37
        $revUserClause = $user->isAnon() ? 'rev_user_text = :username' : 'rev_user = :userId';
38
        $arUserClause = $user->isAnon() ? 'ar_user_text = :username' : 'ar_user = :userId';
39
40
        $sql = "
41
            -- Revision counts.
42
            (SELECT 'deleted' AS `key`, COUNT(ar_id) AS val FROM $archiveTable
43
                WHERE $arUserClause
44
            ) UNION (
45
            SELECT 'live' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
46
                WHERE $revUserClause
47
            ) UNION (
48
            SELECT 'day' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
49
                WHERE $revUserClause AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 DAY)
50
            ) UNION (
51
            SELECT 'week' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
52
                WHERE $revUserClause AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 WEEK)
53
            ) UNION (
54
            SELECT 'month' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
55
                WHERE $revUserClause AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 MONTH)
56
            ) UNION (
57
            SELECT 'year' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
58
                WHERE $revUserClause AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 YEAR)
59
            ) UNION (
60
            SELECT 'with_comments' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
61
                WHERE $revUserClause AND rev_comment != ''
62
            ) UNION (
63
            SELECT 'minor' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
64
                WHERE $revUserClause AND rev_minor_edit = 1
65
66
            -- Dates.
67
            ) UNION (
68
            SELECT 'first' AS `key`, rev_timestamp AS `val` FROM $revisionTable
69
                WHERE $revUserClause ORDER BY rev_timestamp ASC LIMIT 1
70
            ) UNION (
71
            SELECT 'last' AS `key`, rev_timestamp AS `date` FROM $revisionTable
72
                WHERE $revUserClause ORDER BY rev_timestamp DESC LIMIT 1
73
74
            -- Page counts.
75
            ) UNION (
76
            SELECT 'edited-live' AS `key`, COUNT(DISTINCT rev_page) AS `val`
77
                FROM $revisionTable
78
                WHERE $revUserClause
79
            ) UNION (
80
            SELECT 'edited-deleted' AS `key`, COUNT(DISTINCT ar_page_id) AS `val`
81
                FROM $archiveTable
82
                WHERE $arUserClause
83
            ) UNION (
84
            SELECT 'created-live' AS `key`, COUNT(DISTINCT rev_page) AS `val`
85
                FROM $revisionTable
86
                WHERE $revUserClause AND rev_parent_id = 0
87
            ) UNION (
88
            SELECT 'created-deleted' AS `key`, COUNT(DISTINCT ar_page_id) AS `val`
89
                FROM $archiveTable
90
                WHERE $arUserClause AND ar_parent_id = 0
91
            )
92
        ";
93
94
        $params = $user->isAnon() ? ['username' => $user->getUsername()] : ['userId' => $user->getId($project)];
95
        $resultQuery = $this->executeProjectsQuery($sql, $params);
96
97
        $revisionCounts = [];
98
        while ($result = $resultQuery->fetch()) {
99
            $revisionCounts[$result['key']] = $result['val'];
100
        }
101
102
        // Cache and return.
103
        return $this->setCache($cacheKey, $revisionCounts);
104
    }
105
106
    /**
107
     * Get log totals for a user.
108
     * @param Project $project The project.
109
     * @param User $user The user.
110
     * @return integer[] Keys are "<log>-<action>" strings, values are counts.
111
     */
112
    public function getLogCounts(Project $project, User $user)
113
    {
114
        // Set up cache.
115
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_logcounts');
116
        if ($this->cache->hasItem($cacheKey)) {
117
            return $this->cache->getItem($cacheKey)->get();
118
        }
119
        $this->stopwatch->start($cacheKey, 'XTools');
120
121
        // Query.
122
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging');
123
        $sql = "
124
        (SELECT CONCAT(log_type, '-', log_action) AS source, COUNT(log_id) AS value
125
            FROM $loggingTable
126
            WHERE log_user_text = :username
127
            GROUP BY log_type, log_action
128
        )";
129
130
        $results = $this->executeProjectsQuery($sql, [
131
            'username' => $user->getUsername(),
132
        ])->fetchAll();
133
134
        $logCounts = array_combine(
135
            array_map(function ($e) {
136
                return $e['source'];
137
            }, $results),
138
            array_map(function ($e) {
139
                return $e['value'];
140
            }, $results)
141
        );
142
143
        // Make sure there is some value for each of the wanted counts.
144
        $requiredCounts = [
145
            'thanks-thank',
146
            'review-approve',
147
            'newusers-create2',
148
            'newusers-byemail',
149
            'patrol-patrol',
150
            'block-block',
151
            'block-reblock',
152
            'block-unblock',
153
            'protect-protect',
154
            'protect-modify',
155
            'protect-unprotect',
156
            'rights-rights',
157
            'move-move',
158
            'delete-delete',
159
            'delete-revision',
160
            'delete-restore',
161
            'import-import',
162
            'import-interwiki',
163
            'import-upload',
164
            'upload-upload',
165
            'upload-overwrite',
166
        ];
167
        foreach ($requiredCounts as $req) {
168
            if (!isset($logCounts[$req])) {
169
                $logCounts[$req] = 0;
170
            }
171
        }
172
173
        // Add Commons upload count, if applicable.
174
        $logCounts['files_uploaded_commons'] = 0;
175
        if ($this->isLabs()) {
176
            $commons = ProjectRepository::getProject('commonswiki', $this->container);
177
            $userId = $user->getId($commons);
178
            if ($userId) {
179
                $sql = "SELECT COUNT(log_id) FROM commonswiki_p.logging_userindex
180
                    WHERE log_type = 'upload' AND log_action = 'upload' AND log_user = :userId";
181
                $resultQuery = $this->executeProjectsQuery($sql, ['userId' => $userId]);
182
                $logCounts['files_uploaded_commons'] = $resultQuery->fetchColumn();
183
            }
184
        }
185
186
        // Cache and return.
187
        $this->stopwatch->stop($cacheKey);
188
        return $this->setCache($cacheKey, $logCounts);
189
    }
190
191
    /**
192
     * Get data for all blocks set on the given user.
193
     * @param Project $project
194
     * @param User $user
195
     * @return array
196
     */
197
    public function getBlocksReceived(Project $project, User $user)
198
    {
199
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging', 'logindex');
200
        $sql = "SELECT log_action, log_timestamp, log_params FROM $loggingTable
201
                WHERE log_type = 'block'
202
                AND log_action IN ('block', 'reblock', 'unblock')
203
                AND log_timestamp > 0
204
                AND log_title = :username
205
                AND log_namespace = 2
206
                ORDER BY log_timestamp ASC";
207
        $username = str_replace(' ', '_', $user->getUsername());
208
209
        return $this->executeProjectsQuery($sql, [
210
            'username' => $username
211
        ])->fetchAll();
212
    }
213
214
    /**
215
     * Get a user's total edit count on all projects.
216
     * @see EditCounterRepository::globalEditCountsFromCentralAuth()
217
     * @see EditCounterRepository::globalEditCountsFromDatabases()
218
     * @param User $user The user.
219
     * @param Project $project The project to start from.
220
     * @return mixed[] Elements are arrays with 'project' (Project), and 'total' (int).
221
     */
222
    public function globalEditCounts(User $user, Project $project)
223
    {
224
        // Get the edit counts from CentralAuth or database.
225
        $editCounts = $this->globalEditCountsFromCentralAuth($user, $project);
226
        if ($editCounts === false) {
0 ignored issues
show
introduced by
The condition $editCounts === false is always false.
Loading history...
227
            $editCounts = $this->globalEditCountsFromDatabases($user, $project);
228
        }
229
230
        // Pre-populate all projects' metadata, to prevent each project call from fetching it.
231
        $project->getRepository()->getAll();
232
233
        // Compile the output.
234
        $out = [];
235
        foreach ($editCounts as $editCount) {
236
            $out[] = [
237
                'project' => ProjectRepository::getProject($editCount['dbName'], $this->container),
238
                'total' => $editCount['total'],
239
            ];
240
        }
241
        return $out;
242
    }
243
244
    /**
245
     * Get a user's total edit count on one or more project.
246
     * Requires the CentralAuth extension to be installed on the project.
247
     *
248
     * @param User $user The user.
249
     * @param Project $project The project to start from.
250
     * @return mixed[] Elements are arrays with 'dbName' (string), and 'total' (int).
251
     */
252
    protected function globalEditCountsFromCentralAuth(User $user, Project $project)
253
    {
254
        if ($user->isAnon() === true) {
255
            return false;
256
        }
257
258
        // Set up cache and stopwatch.
259
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_globaleditcounts');
260
        if ($this->cache->hasItem($cacheKey)) {
261
            return $this->cache->getItem($cacheKey)->get();
262
        }
263
        $this->stopwatch->start($cacheKey, 'XTools');
264
265
        $this->log->debug(__METHOD__." Getting global edit counts from for ".$user->getUsername());
266
267
        // Load all projects, so it doesn't have to request metadata about each one as it goes.
268
        $project->getRepository()->getAll();
269
270
        $api = $this->getMediawikiApi($project);
271
        $params = [
272
            'meta' => 'globaluserinfo',
273
            'guiprop' => 'editcount|merged',
274
            'guiuser' => $user->getUsername(),
275
        ];
276
        $query = new SimpleRequest('query', $params);
277
        $result = $api->getRequest($query);
278
        if (!isset($result['query']['globaluserinfo']['merged'])) {
279
            return [];
280
        }
281
        $out = [];
282
        foreach ($result['query']['globaluserinfo']['merged'] as $result) {
283
            $out[] = [
284
                'dbName' => $result['wiki'],
285
                'total' => $result['editcount'],
286
            ];
287
        }
288
289
        // Cache and return.
290
        $this->stopwatch->stop($cacheKey);
291
        return $this->setCache($cacheKey, $out);
292
    }
293
294
    /**
295
     * Get total edit counts from all projects for this user.
296
     * @see EditCounterRepository::globalEditCountsFromCentralAuth()
297
     * @param User $user The user.
298
     * @param Project $project The project to start from.
299
     * @return mixed[] Elements are arrays with 'dbName' (string), and 'total' (int).
300
     */
301
    protected function globalEditCountsFromDatabases(User $user, Project $project)
302
    {
303
        $this->log->debug(__METHOD__." Getting global edit counts for ".$user->getUsername());
304
        $allProjects = $project->getRepository()->getAll();
305
        $topEditCounts = [];
306
        foreach ($allProjects as $projectMeta) {
307
            $revisionTableName = $this->getTableName($projectMeta['dbName'], 'revision');
308
            $sql = "SELECT COUNT(rev_id) FROM $revisionTableName WHERE rev_user_text = :username";
309
310
            $resultQuery = $this->executeProjectsQuery($sql, [
311
                'username' => $user->getUsername()
312
            ]);
313
            $total = (int)$resultQuery->fetchColumn();
314
            $topEditCounts[] = [
315
                'dbName' => $projectMeta['dbName'],
316
                'total' => $total,
317
            ];
318
        }
319
        return $topEditCounts;
320
    }
321
322
    /**
323
     * Get the given user's total edit counts per namespace on the given project.
324
     * @param Project $project The project.
325
     * @param User $user The user.
326
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
327
     */
328
    public function getNamespaceTotals(Project $project, User $user)
329
    {
330
        // Cache?
331
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_namespacetotals');
332
        $this->stopwatch->start($cacheKey, 'XTools');
333
        if ($this->cache->hasItem($cacheKey)) {
334
            return $this->cache->getItem($cacheKey)->get();
335
        }
336
337
        // Query.
338
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
339
        $revUserClause = $user->isAnon() ? 'r.rev_user_text = :username' : 'r.rev_user = :userId';
340
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
341
        $sql = "SELECT page_namespace, COUNT(rev_id) AS total
342
            FROM $pageTable p JOIN $revisionTable r ON (r.rev_page = p.page_id)
343
            WHERE $revUserClause
344
            GROUP BY page_namespace";
345
346
        $params = $user->isAnon() ? ['username' => $user->getUsername()] : ['userId' => $user->getId($project)];
347
        $results = $this->executeProjectsQuery($sql, $params)->fetchAll();
348
349
        $namespaceTotals = array_combine(array_map(function ($e) {
350
            return $e['page_namespace'];
351
        }, $results), array_map(function ($e) {
352
            return $e['total'];
353
        }, $results));
354
355
        // Cache and return.
356
        $this->stopwatch->stop($cacheKey);
357
        return $this->setCache($cacheKey, $namespaceTotals);
358
    }
359
360
    /**
361
     * Get revisions by this user.
362
     * @param Project[] $projects The projects.
363
     * @param User $user The user.
364
     * @param int $lim The maximum number of revisions to fetch from each project.
365
     * @return array|mixed
366
     */
367
    public function getRevisions($projects, User $user, $lim = 40)
368
    {
369
        // Check cache.
370
        $cacheKey = $this->getCacheKey('ec_globalcontribs.'.$user->getCacheKey().'.'.$lim);
371
        $this->stopwatch->start($cacheKey, 'XTools');
372
        if ($this->cache->hasItem($cacheKey)) {
373
            return $this->cache->getItem($cacheKey)->get();
374
        }
375
376
        // Assemble queries.
377
        $queries = [];
378
        foreach ($projects as $project) {
379
            $revisionTable = $project->getTableName('revision');
380
            $pageTable = $project->getTableName('page');
381
            $sql = "SELECT
382
                    '".$project->getDatabaseName()."' AS project_name,
383
                    revs.rev_id AS id,
384
                    revs.rev_timestamp AS timestamp,
385
                    UNIX_TIMESTAMP(revs.rev_timestamp) AS unix_timestamp,
386
                    revs.rev_minor_edit AS minor,
387
                    revs.rev_deleted AS deleted,
388
                    revs.rev_len AS length,
389
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
390
                    revs.rev_parent_id AS parent_id,
391
                    revs.rev_comment AS comment,
392
                    revs.rev_user_text AS username,
393
                    page.page_title,
394
                    page.page_namespace
395
                FROM $revisionTable AS revs
396
                    JOIN $pageTable AS page ON (rev_page = page_id)
397
                    LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
398
                WHERE revs.rev_user_text = :username
399
                ORDER BY revs.rev_timestamp DESC";
400
            if (is_numeric($lim)) {
401
                $sql .= " LIMIT $lim";
402
            }
403
            $queries[] = $sql;
404
        }
405
        $sql = "(\n" . join("\n) UNION (\n", $queries) . ")\n";
406
407
        $revisions = $this->executeProjectsQuery($sql, [
408
            'username' => $user->getUsername(),
409
        ])->fetchAll();
410
411
        // Cache and return.
412
        $this->stopwatch->stop($cacheKey);
413
        return $this->setCache($cacheKey, $revisions);
414
    }
415
416
    /**
417
     * Get data for a bar chart of monthly edit totals per namespace.
418
     * @param Project $project The project.
419
     * @param User $user The user.
420
     * @return string[] [
421
     *                      [
422
     *                          'year' => <year>,
423
     *                          'month' => <month>,
424
     *                          'page_namespace' => <namespace>,
425
     *                          'count' => <count>,
426
     *                      ],
427
     *                      ...
428
     *                  ]
429
     */
430
    public function getMonthCounts(Project $project, User $user)
431
    {
432
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_monthcounts');
433
        $this->stopwatch->start($cacheKey, 'XTools');
434
        if ($this->cache->hasItem($cacheKey)) {
435
            return $this->cache->getItem($cacheKey)->get();
436
        }
437
438
        $revisionTable = $project->getTableName('revision');
439
        $pageTable = $project->getTableName('page');
440
        $sql =
441
            "SELECT "
442
            . "     YEAR(rev_timestamp) AS `year`,"
443
            . "     MONTH(rev_timestamp) AS `month`,"
444
            . "     page_namespace,"
445
            . "     COUNT(rev_id) AS `count` "
446
            .  " FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
447
            . " WHERE rev_user_text = :username"
448
            . " GROUP BY YEAR(rev_timestamp), MONTH(rev_timestamp), page_namespace";
449
450
        $totals = $this->executeProjectsQuery($sql, [
451
            'username' => $user->getUsername(),
452
        ])->fetchAll();
453
454
        // Cache and return.
455
        $this->stopwatch->stop($cacheKey);
456
        return $this->setCache($cacheKey, $totals);
457
    }
458
459
    /**
460
     * Get data for the timecard chart, with totals grouped by day and to the nearest two-hours.
461
     * @param Project $project
462
     * @param User $user
463
     * @return string[]
464
     */
465
    public function getTimeCard(Project $project, User $user)
466
    {
467
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_timecard');
468
        $this->stopwatch->start($cacheKey, 'XTools');
469
        if ($this->cache->hasItem($cacheKey)) {
470
            return $this->cache->getItem($cacheKey)->get();
471
        }
472
473
        $hourInterval = 2;
474
        $xCalc = "ROUND(HOUR(rev_timestamp)/$hourInterval) * $hourInterval";
475
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
476
        $sql = "SELECT "
477
            . "     DAYOFWEEK(rev_timestamp) AS `y`, "
478
            . "     $xCalc AS `x`, "
479
            . "     COUNT(rev_id) AS `value` "
480
            . " FROM $revisionTable"
481
            . " WHERE rev_user_text = :username"
482
            . " GROUP BY DAYOFWEEK(rev_timestamp), $xCalc ";
483
484
        $totals = $this->executeProjectsQuery($sql, [
485
            'username' => $user->getUsername(),
486
        ])->fetchAll();
487
488
        // Cache and return.
489
        $this->stopwatch->stop($cacheKey);
490
        return $this->setCache($cacheKey, $totals);
491
    }
492
493
    /**
494
     * Get various data about edit sizes of the past 5,000 edits.
495
     * Will cache the result for 10 minutes.
496
     * @param Project $project The project.
497
     * @param User $user The user.
498
     * @return string[] Values with for keys 'average_size',
499
     *                  'small_edits' and 'large_edits'
500
     */
501
    public function getEditSizeData(Project $project, User $user)
502
    {
503
        // Set up cache.
504
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_editsizes');
505
        $this->stopwatch->start($cacheKey, 'XTools');
506
        if ($this->cache->hasItem($cacheKey)) {
507
            return $this->cache->getItem($cacheKey)->get();
508
        }
509
510
        // Prepare the queries and execute them.
511
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
512
        $revUserClause = $user->isAnon() ? 'revs.rev_user_text = :username' : 'revs.rev_user = :userId';
513
        $sql = "SELECT AVG(sizes.size) AS average_size,
514
                COUNT(CASE WHEN sizes.size < 20 THEN 1 END) AS small_edits,
515
                COUNT(CASE WHEN sizes.size > 1000 THEN 1 END) AS large_edits
516
                FROM (
517
                    SELECT (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS size
518
                    FROM $revisionTable AS revs
519
                    LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
520
                    WHERE $revUserClause
521
                    ORDER BY revs.rev_timestamp DESC
522
                    LIMIT 5000
523
                ) sizes";
524
525
        $params = $user->isAnon() ? ['username' => $user->getUsername()] : ['userId' => $user->getId($project)];
526
        $results = $this->executeProjectsQuery($sql, $params)->fetch();
527
528
        // Cache and return.
529
        $this->stopwatch->stop($cacheKey);
530
        return $this->setCache($cacheKey, $results);
531
    }
532
533
    /**
534
     * Get the number of edits this user made using semi-automated tools.
535
     * @param Project $project
536
     * @param User $user
537
     * @return int Result of query, see below.
538
     */
539
    public function countAutomatedEdits(Project $project, User $user)
540
    {
541
        $autoEditsRepo = new AutoEditsRepository();
542
        $autoEditsRepo->setContainer($this->container);
543
        return $autoEditsRepo->countAutomatedEdits($project, $user);
544
    }
545
}
546