Test Failed
Push — master ( 28b62c...34bd96 )
by MusikAnimal
05:45
created

EditCounterRepository::getMonthCounts()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 18
nc 2
nop 2
dl 0
loc 25
rs 9.6666
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the EditCounterRepository class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace AppBundle\Repository;
9
10
use AppBundle\Model\Project;
11
use AppBundle\Model\User;
12
use Mediawiki\Api\SimpleRequest;
13
14
/**
15
 * An EditCounterRepository is responsible for retrieving edit count information from the
16
 * databases and API. It doesn't do any post-processing of that information.
17
 * @codeCoverageIgnore
18
 */
19
class EditCounterRepository extends UserRightsRepository
20
{
21
    /**
22
     * Get data about revisions, pages, etc.
23
     * @param Project $project The project.
24
     * @param User $user The user.
25
     * @return string[] With keys: 'deleted', 'live', 'total', '24h', '7d', '30d',
26
     * '365d', 'small', 'large', 'with_comments', and 'minor_edits', ...
27
     */
28
    public function getPairData(Project $project, User $user): array
29
    {
30
        // Set up cache.
31
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_pairdata');
32
        if ($this->cache->hasItem($cacheKey)) {
33
            return $this->cache->getItem($cacheKey)->get();
34
        }
35
36
        // Prepare the queries and execute them.
37
        $archiveTable = $project->getTableName('archive');
38
        $revisionTable = $project->getTableName('revision');
39
40
        $sql = "
41
            -- Revision counts.
42
            (SELECT 'deleted' AS `key`, COUNT(ar_id) AS val FROM $archiveTable
43
                WHERE ar_actor = :actorId
44
            ) UNION (
45
            SELECT 'live' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
46
                WHERE rev_actor = :actorId
47
            ) UNION (
48
            SELECT 'day' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
49
                WHERE rev_actor = :actorId 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 rev_actor = :actorId 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 rev_actor = :actorId 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 rev_actor = :actorId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 YEAR)
59
            ) UNION (
60
            SELECT 'minor' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
61
                WHERE rev_actor = :actorId AND rev_minor_edit = 1
62
63
            -- Page counts.
64
            ) UNION (
65
            SELECT 'edited-live' AS `key`, COUNT(DISTINCT rev_page) AS `val`
66
                FROM $revisionTable
67
                WHERE rev_actor = :actorId
68
            ) UNION (
69
            SELECT 'edited-deleted' AS `key`, COUNT(DISTINCT ar_page_id) AS `val`
70
                FROM $archiveTable
71
                WHERE ar_actor = :actorId
72
            ) UNION (
73
            SELECT 'created-live' AS `key`, COUNT(DISTINCT rev_page) AS `val`
74
                FROM $revisionTable
75
                WHERE rev_actor = :actorId AND rev_parent_id = 0
76
            ) UNION (
77
            SELECT 'created-deleted' AS `key`, COUNT(DISTINCT ar_page_id) AS `val`
78
                FROM $archiveTable
79
                WHERE ar_actor = :actorId AND ar_parent_id = 0
80
            )
81
        ";
82
83
        $resultQuery = $this->executeProjectsQuery($sql, [
84
            'actorId' => $user->getActorId($project),
85
        ]);
86
87
        $revisionCounts = [];
88
        while ($result = $resultQuery->fetch()) {
89
            $revisionCounts[$result['key']] = (int)$result['val'];
90
        }
91
92
        // Cache and return.
93
        return $this->setCache($cacheKey, $revisionCounts);
94
    }
95
96
    /**
97
     * Get log totals for a user.
98
     * @param Project $project The project.
99
     * @param User $user The user.
100
     * @return integer[] Keys are "<log>-<action>" strings, values are counts.
101
     */
102
    public function getLogCounts(Project $project, User $user): array
103
    {
104
        // Set up cache.
105
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_logcounts');
106
        if ($this->cache->hasItem($cacheKey)) {
107
            return $this->cache->getItem($cacheKey)->get();
108
        }
109
110
        // Query.
111
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging');
112
        $sql = "
113
        (SELECT CONCAT(log_type, '-', log_action) AS source, COUNT(log_id) AS value
114
            FROM $loggingTable
115
            WHERE log_actor = :actorId
116
            GROUP BY log_type, log_action
117
        )";
118
119
        $results = $this->executeProjectsQuery($sql, [
120
            'actorId' => $user->getActorId($project),
121
        ])->fetchAll();
122
123
        $logCounts = array_combine(
124
            array_map(function ($e) {
125
                return $e['source'];
126
            }, $results),
127
            array_map(function ($e) {
128
                return (int)$e['value'];
129
            }, $results)
130
        );
131
132
        // Make sure there is some value for each of the wanted counts.
133
        $requiredCounts = [
134
            'thanks-thank',
135
            'review-approve',
136
            'newusers-create2',
137
            'newusers-byemail',
138
            'patrol-patrol',
139
            'block-block',
140
            'block-reblock',
141
            'block-unblock',
142
            'protect-protect',
143
            'protect-modify',
144
            'protect-unprotect',
145
            'rights-rights',
146
            'move-move',
147
            'delete-delete',
148
            'delete-revision',
149
            'delete-restore',
150
            'import-import',
151
            'import-interwiki',
152
            'import-upload',
153
            'upload-upload',
154
            'upload-overwrite',
155
            'abusefilter-modify',
156
            'merge-merge',
157
        ];
158
        foreach ($requiredCounts as $req) {
159
            if (!isset($logCounts[$req])) {
160
                $logCounts[$req] = 0;
161
            }
162
        }
163
164
        // Add Commons upload count, if applicable.
165
        $logCounts['files_uploaded_commons'] = 0;
166
        if ($this->isLabs() && !$user->isAnon()) {
167
            $sql = "SELECT COUNT(log_id)
168
                    FROM commonswiki_p.logging_userindex
169
                    JOIN commonswiki_p.actor ON actor_id = log_actor
170
                    WHERE log_type = 'upload' AND log_action = 'upload'
171
                        AND actor_name = :username";
172
            $resultQuery = $this->executeProjectsQuery($sql, [
173
                'username' => $user->getUsername(),
174
            ]);
175
            $logCounts['files_uploaded_commons'] = (int)$resultQuery->fetchColumn();
176
        }
177
178
        // Cache and return.
179
        return $this->setCache($cacheKey, $logCounts);
180
    }
181
182
    /**
183
     * Get the IDs and timestamps of the latest edit and logged action by the given user.
184
     * @param Project $project
185
     * @param User $user
186
     * @return string[] With keys 'rev_first', 'rev_latest', 'log_latest'.
187
     */
188
    public function getFirstAndLatestActions(Project $project, User $user): array
189
    {
190
        $loggingTable = $project->getTableName('logging', 'userindex');
191
        $revisionTable = $project->getTableName('revision');
192
193
        $sql = "(
194
                    SELECT 'rev_first' AS `key`, rev_id AS `id`,
195
                        rev_timestamp AS `timestamp`, NULL as `type`
196
                    FROM $revisionTable
197
                    WHERE rev_actor = :actorId
198
                    LIMIT 1
199
                ) UNION (
200
                    SELECT 'rev_latest' AS `key`, rev_id AS `id`,
201
                        rev_timestamp AS `timestamp`, NULL as `type`
202
                    FROM $revisionTable
203
                    WHERE rev_actor = :actorId
204
                    ORDER BY rev_timestamp DESC LIMIT 1
205
                ) UNION (
206
                    SELECT 'log_latest' AS `key`, log_id AS `id`,
207
                        log_timestamp AS `timestamp`, log_type AS `type`
208
                    FROM $loggingTable
209
                    WHERE log_actor = :actorId
210
                    ORDER BY log_timestamp DESC LIMIT 1
211
                )";
212
213
        $resultQuery = $this->executeProjectsQuery($sql, [
214
            'actorId' => $user->getActorId($project),
215
        ]);
216
217
        $actions = [];
218
        while ($result = $resultQuery->fetch()) {
219
            $actions[$result['key']] = [
220
                'id' => $result['id'],
221
                'timestamp' => $result['timestamp'],
222
                'type' => $result['type'],
223
            ];
224
        }
225
226
        return $actions;
227
    }
228
229
    /**
230
     * Get data for all blocks set on the given user.
231
     * @param Project $project
232
     * @param User $user
233
     * @return array
234
     */
235
    public function getBlocksReceived(Project $project, User $user): array
236
    {
237
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging', 'logindex');
238
        $sql = "SELECT log_action, log_timestamp, log_params FROM $loggingTable
239
                WHERE log_type = 'block'
240
                AND log_action IN ('block', 'reblock', 'unblock')
241
                AND log_timestamp > 0
242
                AND log_title = :username
243
                AND log_namespace = 2
244
                ORDER BY log_timestamp ASC";
245
        $username = str_replace(' ', '_', $user->getUsername());
246
247
        return $this->executeProjectsQuery($sql, [
248
            'username' => $username,
249
        ])->fetchAll();
250
    }
251
252
    /**
253
     * Get a user's total edit count on all projects.
254
     * @see EditCounterRepository::globalEditCountsFromCentralAuth()
255
     * @see EditCounterRepository::globalEditCountsFromDatabases()
256
     * @param User $user The user.
257
     * @param Project $project The project to start from.
258
     * @return mixed[] Elements are arrays with 'project' (Project), and 'total' (int).
259
     */
260
    public function globalEditCounts(User $user, Project $project): array
261
    {
262
        // Get the edit counts from CentralAuth or database.
263
        $editCounts = $this->globalEditCountsFromCentralAuth($user, $project);
264
        if (false === $editCounts) {
265
            $editCounts = $this->globalEditCountsFromDatabases($user, $project);
266
        }
267
268
        // Pre-populate all projects' metadata, to prevent each project call from fetching it.
269
        $project->getRepository()->getAll();
0 ignored issues
show
Bug introduced by
The method getAll() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\ProjectRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

269
        $project->getRepository()->/** @scrutinizer ignore-call */ getAll();
Loading history...
270
271
        // Compile the output.
272
        $out = [];
273
        foreach ($editCounts as $editCount) {
274
            $out[] = [
275
                'project' => ProjectRepository::getProject($editCount['dbName'], $this->container),
276
                'total' => $editCount['total'],
277
            ];
278
        }
279
        return $out;
280
    }
281
282
    /**
283
     * Get a user's total edit count on one or more project.
284
     * Requires the CentralAuth extension to be installed on the project.
285
     *
286
     * @param User $user The user.
287
     * @param Project $project The project to start from.
288
     * @return mixed[]|false Elements are arrays with 'dbName' (string), and 'total' (int). False for logged out users.
289
     */
290
    protected function globalEditCountsFromCentralAuth(User $user, Project $project)
291
    {
292
        if (true === $user->isAnon()) {
293
            return false;
294
        }
295
296
        // Set up cache.
297
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_globaleditcounts');
298
        if ($this->cache->hasItem($cacheKey)) {
299
            return $this->cache->getItem($cacheKey)->get();
300
        }
301
302
        $this->log->debug(__METHOD__." Getting global edit counts from for ".$user->getUsername());
303
304
        // Load all projects, so it doesn't have to request metadata about each one as it goes.
305
        $project->getRepository()->getAll();
306
307
        $api = $this->getMediawikiApi($project);
308
        $params = [
309
            'meta' => 'globaluserinfo',
310
            'guiprop' => 'editcount|merged',
311
            'guiuser' => $user->getUsername(),
312
        ];
313
        $query = new SimpleRequest('query', $params);
314
        $result = $api->getRequest($query);
315
        if (!isset($result['query']['globaluserinfo']['merged'])) {
316
            return [];
317
        }
318
        $out = [];
319
        foreach ($result['query']['globaluserinfo']['merged'] as $result) {
320
            $out[] = [
321
                'dbName' => $result['wiki'],
322
                'total' => $result['editcount'],
323
            ];
324
        }
325
326
        // Cache and return.
327
        return $this->setCache($cacheKey, $out);
328
    }
329
330
    /**
331
     * Get total edit counts from all projects for this user.
332
     * @see EditCounterRepository::globalEditCountsFromCentralAuth()
333
     * @param User $user The user.
334
     * @param Project $project The project to start from.
335
     * @return mixed[] Elements are arrays with 'dbName' (string), and 'total' (int).
336
     */
337
    protected function globalEditCountsFromDatabases(User $user, Project $project): array
338
    {
339
        $this->log->debug(__METHOD__." Getting global edit counts for ".$user->getUsername());
340
        $allProjects = $project->getRepository()->getAll();
341
        $topEditCounts = [];
342
        foreach ($allProjects as $projectMeta) {
343
            $revisionTableName = $this->getTableName($projectMeta['dbName'], 'revision');
344
            $sql = "SELECT COUNT(rev_id) FROM $revisionTableName WHERE rev_actor = :actorId";
345
346
            $resultQuery = $this->executeProjectsQuery($sql, [
347
                'actorId' => $user->getActorId($project),
348
            ]);
349
            $total = (int)$resultQuery->fetchColumn();
350
            $topEditCounts[] = [
351
                'dbName' => $projectMeta['dbName'],
352
                'total' => $total,
353
            ];
354
        }
355
        return $topEditCounts;
356
    }
357
358
    /**
359
     * Get the given user's total edit counts per namespace on the given project.
360
     * @param Project $project The project.
361
     * @param User $user The user.
362
     * @return array Array keys are namespace IDs, values are the edit counts.
363
     */
364
    public function getNamespaceTotals(Project $project, User $user): array
365
    {
366
        // Cache?
367
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_namespacetotals');
368
        if ($this->cache->hasItem($cacheKey)) {
369
            return $this->cache->getItem($cacheKey)->get();
370
        }
371
372
        // Query.
373
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
374
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
375
        $sql = "SELECT page_namespace, COUNT(rev_id) AS total
376
            FROM $pageTable p JOIN $revisionTable r ON (r.rev_page = p.page_id)
377
            WHERE r.rev_actor = :actorId
378
            GROUP BY page_namespace";
379
380
        $results = $this->executeProjectsQuery($sql, [
381
            'actorId' => $user->getActorId($project),
382
        ])->fetchAll();
383
384
        $namespaceTotals = array_combine(array_map(function ($e) {
385
            return $e['page_namespace'];
386
        }, $results), array_map(function ($e) {
387
            return (int)$e['total'];
388
        }, $results));
389
390
        // Cache and return.
391
        return $this->setCache($cacheKey, $namespaceTotals);
392
    }
393
394
    /**
395
     * Get revisions by this user.
396
     * @param Project[] $projects The projects.
397
     * @param User $user The user.
398
     * @param int $limit The maximum number of revisions to fetch from each project.
399
     * @param int $offset Offset results by this number of rows.
400
     * @return array|mixed
401
     */
402
    public function getRevisions(array $projects, User $user, int $limit = 30, int $offset = 0)
403
    {
404
        // Check cache.
405
        $cacheKey = $this->getCacheKey('ec_globalcontribs.'.$user->getCacheKey().'.'.$limit.'.'.$offset);
406
        if ($this->cache->hasItem($cacheKey)) {
407
            return $this->cache->getItem($cacheKey)->get();
408
        }
409
410
        $username = $this->getProjectsConnection()->quote($user->getUsername(), \PDO::PARAM_STR);
411
412
        // Assemble queries.
413
        $queries = [];
414
        foreach ($projects as $project) {
415
            $revisionTable = $project->getTableName('revision');
416
            $pageTable = $project->getTableName('page');
417
            $commentTable = $project->getTableName('comment');
418
            $actorId = $user->getActorId($project);
419
            $sql = "SELECT
420
                    '".$project->getDatabaseName()."' AS project_name,
421
                    revs.rev_id AS id,
422
                    revs.rev_timestamp AS timestamp,
423
                    UNIX_TIMESTAMP(revs.rev_timestamp) AS unix_timestamp,
424
                    revs.rev_minor_edit AS minor,
425
                    revs.rev_deleted AS deleted,
426
                    revs.rev_len AS length,
427
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
428
                    revs.rev_parent_id AS parent_id,
429
                    $username AS username,
430
                    page.page_title,
431
                    page.page_namespace,
432
                    comment_text AS `comment`
433
                FROM $revisionTable AS revs
434
                    JOIN $pageTable AS page ON (rev_page = page_id)
435
                    LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
436
                    LEFT OUTER JOIN $commentTable ON revs.rev_comment_id = comment_id
437
                WHERE revs.rev_actor = $actorId";
438
            $queries[] = $sql;
439
        }
440
        $sql = "SELECT * FROM ((\n" . join("\n) UNION (\n", $queries) . ")) a ORDER BY timestamp DESC LIMIT $limit";
441
442
        if (is_numeric($offset)) {
0 ignored issues
show
introduced by
The condition is_numeric($offset) is always true.
Loading history...
443
            $sql .= " OFFSET $offset";
444
        }
445
446
        $revisions = $this->executeProjectsQuery($sql)->fetchAll();
447
448
        // Cache and return.
449
        return $this->setCache($cacheKey, $revisions);
450
    }
451
452
    /**
453
     * Get data for a bar chart of monthly edit totals per namespace.
454
     * @param Project $project The project.
455
     * @param User $user The user.
456
     * @return string[] [
457
     *                      [
458
     *                          'year' => <year>,
459
     *                          'month' => <month>,
460
     *                          'page_namespace' => <namespace>,
461
     *                          'count' => <count>,
462
     *                      ],
463
     *                      ...
464
     *                  ]
465
     */
466
    public function getMonthCounts(Project $project, User $user): array
467
    {
468
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_monthcounts');
469
        if ($this->cache->hasItem($cacheKey)) {
470
            return $this->cache->getItem($cacheKey)->get();
471
        }
472
473
        $revisionTable = $project->getTableName('revision');
474
        $pageTable = $project->getTableName('page');
475
        $sql =
476
            "SELECT "
477
            . "     YEAR(rev_timestamp) AS `year`,"
478
            . "     MONTH(rev_timestamp) AS `month`,"
479
            . "     page_namespace,"
480
            . "     COUNT(rev_id) AS `count` "
481
            .  " FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
482
            . " WHERE rev_actor = :actorId"
483
            . " GROUP BY YEAR(rev_timestamp), MONTH(rev_timestamp), page_namespace";
484
485
        $totals = $this->executeProjectsQuery($sql, [
486
            'actorId' => $user->getActorId($project),
487
        ])->fetchAll();
488
489
        // Cache and return.
490
        return $this->setCache($cacheKey, $totals);
491
    }
492
493
    /**
494
     * Get data for the timecard chart, with totals grouped by day and to the nearest two-hours.
495
     * @param Project $project
496
     * @param User $user
497
     * @return string[]
498
     */
499
    public function getTimeCard(Project $project, User $user): array
500
    {
501
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_timecard');
502
        if ($this->cache->hasItem($cacheKey)) {
503
            return $this->cache->getItem($cacheKey)->get();
504
        }
505
506
        $hourInterval = 2;
507
        $xCalc = "ROUND(HOUR(rev_timestamp)/$hourInterval) * $hourInterval";
508
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
509
        $sql = "SELECT "
510
            . "     DAYOFWEEK(rev_timestamp) AS `y`, "
511
            . "     $xCalc AS `x`, "
512
            . "     COUNT(rev_id) AS `value` "
513
            . " FROM $revisionTable"
514
            . " WHERE rev_actor = :actorId"
515
            . " GROUP BY DAYOFWEEK(rev_timestamp), $xCalc ";
516
517
        $totals = $this->executeProjectsQuery($sql, [
518
            'actorId' => $user->getActorId($project),
519
        ])->fetchAll();
520
521
        // Cache and return.
522
        return $this->setCache($cacheKey, $totals);
523
    }
524
525
    /**
526
     * Get various data about edit sizes of the past 5,000 edits.
527
     * Will cache the result for 10 minutes.
528
     * @param Project $project The project.
529
     * @param User $user The user.
530
     * @return string[] Values with for keys 'average_size',
531
     *                  'small_edits' and 'large_edits'
532
     */
533
    public function getEditSizeData(Project $project, User $user): array
534
    {
535
        // Set up cache.
536
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_editsizes');
537
        if ($this->cache->hasItem($cacheKey)) {
538
            return $this->cache->getItem($cacheKey)->get();
539
        }
540
541
        // Prepare the queries and execute them.
542
        $revisionTable = $project->getTableName('revision');
543
        $sql = "SELECT AVG(sizes.size) AS average_size,
544
                COUNT(CASE WHEN sizes.size < 20 THEN 1 END) AS small_edits,
545
                COUNT(CASE WHEN sizes.size > 1000 THEN 1 END) AS large_edits
546
                FROM (
547
                    SELECT (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS size
548
                    FROM $revisionTable AS revs
549
                    LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
550
                    WHERE revs.rev_actor = :actorId
551
                    ORDER BY revs.rev_timestamp DESC
552
                    LIMIT 5000
553
                ) sizes";
554
        $results = $this->executeProjectsQuery($sql, [
555
            'actorId' => $user->getActorId($project),
556
        ])->fetch();
557
558
        // Cache and return.
559
        return $this->setCache($cacheKey, $results);
560
    }
561
562
    /**
563
     * Get the number of edits this user made using semi-automated tools.
564
     * @param Project $project
565
     * @param User $user
566
     * @return int Result of query, see below.
567
     */
568
    public function countAutomatedEdits(Project $project, User $user): int
569
    {
570
        $autoEditsRepo = new AutoEditsRepository();
571
        $autoEditsRepo->setContainer($this->container);
572
        return $autoEditsRepo->countAutomatedEdits($project, $user);
573
    }
574
}
575