Passed
Push — master ( 4e5b74...9a5c5d )
by MusikAnimal
05:54
created

EditCounterRepository::countAutomatedEdits()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 5
rs 10
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
13
/**
14
 * An EditCounterRepository is responsible for retrieving edit count information from the
15
 * databases and API. It doesn't do any post-processing of that information.
16
 * @codeCoverageIgnore
17
 */
18
class EditCounterRepository extends UserRightsRepository
19
{
20
    /**
21
     * Get data about revisions, pages, etc.
22
     * @param Project $project The project.
23
     * @param User $user The user.
24
     * @return string[] With keys: 'deleted', 'live', 'total', '24h', '7d', '30d',
25
     * '365d', 'small', 'large', 'with_comments', and 'minor_edits', ...
26
     */
27
    public function getPairData(Project $project, User $user): array
28
    {
29
        // Set up cache.
30
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_pairdata');
31
        if ($this->cache->hasItem($cacheKey)) {
32
            return $this->cache->getItem($cacheKey)->get();
33
        }
34
35
        // Prepare the queries and execute them.
36
        $archiveTable = $project->getTableName('archive');
37
        $revisionTable = $project->getTableName('revision');
38
39
        $sql = "
40
            -- Revision counts.
41
            (SELECT 'deleted' AS `key`, COUNT(ar_id) AS val FROM $archiveTable
42
                WHERE ar_actor = :actorId
43
            ) UNION (
44
            SELECT 'live' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
45
                WHERE rev_actor = :actorId
46
            ) UNION (
47
            SELECT 'day' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
48
                WHERE rev_actor = :actorId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 DAY)
49
            ) UNION (
50
            SELECT 'week' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
51
                WHERE rev_actor = :actorId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 WEEK)
52
            ) UNION (
53
            SELECT 'month' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
54
                WHERE rev_actor = :actorId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 MONTH)
55
            ) UNION (
56
            SELECT 'year' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
57
                WHERE rev_actor = :actorId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 YEAR)
58
            ) UNION (
59
            SELECT 'minor' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
60
                WHERE rev_actor = :actorId AND rev_minor_edit = 1
61
62
            -- Page counts.
63
            ) UNION (
64
            SELECT 'edited-live' AS `key`, COUNT(DISTINCT rev_page) AS `val`
65
                FROM $revisionTable
66
                WHERE rev_actor = :actorId
67
            ) UNION (
68
            SELECT 'edited-deleted' AS `key`, COUNT(DISTINCT ar_page_id) AS `val`
69
                FROM $archiveTable
70
                WHERE ar_actor = :actorId
71
            ) UNION (
72
            SELECT 'created-live' AS `key`, COUNT(DISTINCT rev_page) AS `val`
73
                FROM $revisionTable
74
                WHERE rev_actor = :actorId AND rev_parent_id = 0
75
            ) UNION (
76
            SELECT 'created-deleted' AS `key`, COUNT(DISTINCT ar_page_id) AS `val`
77
                FROM $archiveTable
78
                WHERE ar_actor = :actorId AND ar_parent_id = 0
79
            )";
80
81
        $resultQuery = $this->executeProjectsQuery($sql, [
82
            'actorId' => $user->getActorId($project),
83
        ]);
84
85
        $revisionCounts = [];
86
        while ($result = $resultQuery->fetch()) {
87
            $revisionCounts[$result['key']] = (int)$result['val'];
88
        }
89
90
        // Cache and return.
91
        return $this->setCache($cacheKey, $revisionCounts);
92
    }
93
94
    /**
95
     * Get log totals for a user.
96
     * @param Project $project The project.
97
     * @param User $user The user.
98
     * @return integer[] Keys are "<log>-<action>" strings, values are counts.
99
     */
100
    public function getLogCounts(Project $project, User $user): array
101
    {
102
        // Set up cache.
103
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_logcounts');
104
        if ($this->cache->hasItem($cacheKey)) {
105
            return $this->cache->getItem($cacheKey)->get();
106
        }
107
108
        // Query.
109
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging');
110
        $sql = "
111
        (SELECT CONCAT(log_type, '-', log_action) AS source, COUNT(log_id) AS value
112
            FROM $loggingTable
113
            WHERE log_actor = :actorId
114
            GROUP BY log_type, log_action
115
        )";
116
117
        $results = $this->executeProjectsQuery($sql, [
118
            'actorId' => $user->getActorId($project),
119
        ])->fetchAll();
120
121
        $logCounts = array_combine(
122
            array_map(function ($e) {
123
                return $e['source'];
124
            }, $results),
125
            array_map(function ($e) {
126
                return (int)$e['value'];
127
            }, $results)
128
        );
129
130
        // Make sure there is some value for each of the wanted counts.
131
        $requiredCounts = [
132
            'thanks-thank',
133
            'review-approve',
134
            'newusers-create2',
135
            'newusers-byemail',
136
            'patrol-patrol',
137
            'block-block',
138
            'block-reblock',
139
            'block-unblock',
140
            'protect-protect',
141
            'protect-modify',
142
            'protect-unprotect',
143
            'rights-rights',
144
            'move-move',
145
            'delete-delete',
146
            'delete-revision',
147
            'delete-restore',
148
            'import-import',
149
            'import-interwiki',
150
            'import-upload',
151
            'upload-upload',
152
            'upload-overwrite',
153
            'abusefilter-modify',
154
            'merge-merge',
155
        ];
156
        foreach ($requiredCounts as $req) {
157
            if (!isset($logCounts[$req])) {
158
                $logCounts[$req] = 0;
159
            }
160
        }
161
162
        // Cache and return.
163
        return $this->setCache($cacheKey, $logCounts);
164
    }
165
166
    /**
167
     * Get counts of files moved, and files moved/uploaded on Commons.
168
     * Local file uploads are counted in getLogCounts() since we're querying the same rows anyway.
169
     * @param Project $project
170
     * @param User $user
171
     * @return array
172
     */
173
    public function getFileCounts(Project $project, User $user): array
174
    {
175
        // Anons can't upload or move files.
176
        if ($user->isAnon()) {
177
            return [];
178
        }
179
180
        // Set up cache.
181
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_filecounts');
182
        if ($this->cache->hasItem($cacheKey)) {
183
            return $this->cache->getItem($cacheKey)->get();
184
        }
185
186
        $loggingTable = $project->getTableName('logging');
187
188
        $sqlParts = [
189
            "SELECT 'files_moved' AS `key`, COUNT(log_id) AS `val`
190
             FROM $loggingTable
191
             WHERE log_actor = :actorId
192
               AND log_type = 'move'
193
               AND log_action = 'move'
194
               AND log_namespace = 6",
195
        ];
196
197
        $bindings = ['actorId' => $user->getActorId($project)];
198
199
        if ($this->isLabs() && 'commons.wikimedia.org' !== $project->getDomain()) {
200
            $commonsProject = ProjectRepository::getProject('commonswiki', $this->container);
201
            $loggingTableCommons = $commonsProject->getTableName('logging');
202
            $sqlParts[] = "SELECT 'files_moved_commons' AS `key`, COUNT(log_id) AS `val`
203
                           FROM $loggingTableCommons
204
                           WHERE log_actor = :actorId2 AND log_type = 'move'
205
                               AND log_action = 'move' AND log_namespace = 6";
206
            $sqlParts[] = "SELECT 'files_uploaded_commons' AS `key`, COUNT(log_id) AS `val`
207
                           FROM $loggingTableCommons
208
                           WHERE log_actor = :actorId2 AND log_type = 'upload' AND log_action = 'upload'";
209
            $bindings['actorId2'] = $user->getActorId($commonsProject);
210
        }
211
212
        $sql = '('.implode("\n) UNION (\n", $sqlParts).')';
213
214
        $results = $this->executeProjectsQuery($sql, $bindings)->fetchAll();
215
216
        $counts = array_combine(
217
            array_map(function ($e) {
218
                return $e['key'];
219
            }, $results),
220
            array_map(function ($e) {
221
                return (int)$e['val'];
222
            }, $results)
223
        );
224
225
        // Cache and return.
226
        return $this->setCache($cacheKey, $counts);
227
    }
228
229
    /**
230
     * Get the IDs and timestamps of the latest edit and logged action by the given user.
231
     * @param Project $project
232
     * @param User $user
233
     * @return string[] With keys 'rev_first', 'rev_latest', 'log_latest'.
234
     */
235
    public function getFirstAndLatestActions(Project $project, User $user): array
236
    {
237
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_first_latest_actions');
238
        if ($this->cache->hasItem($cacheKey)) {
239
            return $this->cache->getItem($cacheKey)->get();
240
        }
241
242
        $loggingTable = $project->getTableName('logging', 'userindex');
243
        $revisionTable = $project->getTableName('revision');
244
245
        $sql = "(
246
                    SELECT 'rev_first' AS `key`, rev_id AS `id`,
247
                        rev_timestamp AS `timestamp`, NULL as `type`
248
                    FROM $revisionTable
249
                    WHERE rev_actor = :actorId
250
                    LIMIT 1
251
                ) UNION (
252
                    SELECT 'rev_latest' AS `key`, rev_id AS `id`,
253
                        rev_timestamp AS `timestamp`, NULL as `type`
254
                    FROM $revisionTable
255
                    WHERE rev_actor = :actorId
256
                    ORDER BY rev_timestamp DESC LIMIT 1
257
                ) UNION (
258
                    SELECT 'log_latest' AS `key`, log_id AS `id`,
259
                        log_timestamp AS `timestamp`, log_type AS `type`
260
                    FROM $loggingTable
261
                    WHERE log_actor = :actorId
262
                    ORDER BY log_timestamp DESC LIMIT 1
263
                )";
264
265
        $resultQuery = $this->executeProjectsQuery($sql, [
266
            'actorId' => $user->getActorId($project),
267
        ]);
268
269
        $actions = [];
270
        while ($result = $resultQuery->fetch()) {
271
            $actions[$result['key']] = [
272
                'id' => $result['id'],
273
                'timestamp' => $result['timestamp'],
274
                'type' => $result['type'],
275
            ];
276
        }
277
278
        return $this->setCache($cacheKey, $actions);
279
    }
280
281
    /**
282
     * Get data for all blocks set on the given user.
283
     * @param Project $project
284
     * @param User $user
285
     * @return array
286
     */
287
    public function getBlocksReceived(Project $project, User $user): array
288
    {
289
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging', 'logindex');
290
        $sql = "SELECT log_action, log_timestamp, log_params FROM $loggingTable
291
                WHERE log_type = 'block'
292
                AND log_action IN ('block', 'reblock', 'unblock')
293
                AND log_timestamp > 0
294
                AND log_title = :username
295
                AND log_namespace = 2
296
                ORDER BY log_timestamp ASC";
297
        $username = str_replace(' ', '_', $user->getUsername());
298
299
        return $this->executeProjectsQuery($sql, [
300
            'username' => $username,
301
        ])->fetchAll();
302
    }
303
304
    /**
305
     * Get the given user's total edit counts per namespace on the given project.
306
     * @param Project $project The project.
307
     * @param User $user The user.
308
     * @return array Array keys are namespace IDs, values are the edit counts.
309
     */
310
    public function getNamespaceTotals(Project $project, User $user): array
311
    {
312
        // Cache?
313
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_namespacetotals');
314
        if ($this->cache->hasItem($cacheKey)) {
315
            return $this->cache->getItem($cacheKey)->get();
316
        }
317
318
        // Query.
319
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
320
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
321
        $sql = "SELECT page_namespace, COUNT(rev_id) AS total
322
            FROM $pageTable p JOIN $revisionTable r ON (r.rev_page = p.page_id)
323
            WHERE r.rev_actor = :actorId
324
            GROUP BY page_namespace";
325
326
        $results = $this->executeProjectsQuery($sql, [
327
            'actorId' => $user->getActorId($project),
328
        ])->fetchAll();
329
330
        $namespaceTotals = array_combine(array_map(function ($e) {
331
            return $e['page_namespace'];
332
        }, $results), array_map(function ($e) {
333
            return (int)$e['total'];
334
        }, $results));
335
336
        // Cache and return.
337
        return $this->setCache($cacheKey, $namespaceTotals);
338
    }
339
340
    /**
341
     * Get data for a bar chart of monthly edit totals per namespace.
342
     * @param Project $project The project.
343
     * @param User $user The user.
344
     * @return string[] [
345
     *                      [
346
     *                          'year' => <year>,
347
     *                          'month' => <month>,
348
     *                          'page_namespace' => <namespace>,
349
     *                          'count' => <count>,
350
     *                      ],
351
     *                      ...
352
     *                  ]
353
     */
354
    public function getMonthCounts(Project $project, User $user): array
355
    {
356
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_monthcounts');
357
        if ($this->cache->hasItem($cacheKey)) {
358
            return $this->cache->getItem($cacheKey)->get();
359
        }
360
361
        $revisionTable = $project->getTableName('revision');
362
        $pageTable = $project->getTableName('page');
363
        $sql =
364
            "SELECT "
365
            . "     YEAR(rev_timestamp) AS `year`,"
366
            . "     MONTH(rev_timestamp) AS `month`,"
367
            . "     page_namespace,"
368
            . "     COUNT(rev_id) AS `count` "
369
            .  " FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
370
            . " WHERE rev_actor = :actorId"
371
            . " GROUP BY YEAR(rev_timestamp), MONTH(rev_timestamp), page_namespace";
372
373
        $totals = $this->executeProjectsQuery($sql, [
374
            'actorId' => $user->getActorId($project),
375
        ])->fetchAll();
376
377
        // Cache and return.
378
        return $this->setCache($cacheKey, $totals);
379
    }
380
381
    /**
382
     * Get data for the timecard chart, with totals grouped by day and to the nearest two-hours.
383
     * @param Project $project
384
     * @param User $user
385
     * @return string[]
386
     */
387
    public function getTimeCard(Project $project, User $user): array
388
    {
389
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_timecard');
390
        if ($this->cache->hasItem($cacheKey)) {
391
            return $this->cache->getItem($cacheKey)->get();
392
        }
393
394
        $hourInterval = 2;
395
        $xCalc = "ROUND(HOUR(rev_timestamp)/$hourInterval) * $hourInterval";
396
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
397
        $sql = "SELECT "
398
            . "     DAYOFWEEK(rev_timestamp) AS `day_of_week`, "
399
            . "     $xCalc AS `hour`, "
400
            . "     COUNT(rev_id) AS `value` "
401
            . " FROM $revisionTable"
402
            . " WHERE rev_actor = :actorId"
403
            . " GROUP BY DAYOFWEEK(rev_timestamp), $xCalc ";
404
405
        $totals = $this->executeProjectsQuery($sql, [
406
            'actorId' => $user->getActorId($project),
407
        ])->fetchAll();
408
409
        // Cache and return.
410
        return $this->setCache($cacheKey, $totals);
411
    }
412
413
    /**
414
     * Get various data about edit sizes of the past 5,000 edits.
415
     * Will cache the result for 10 minutes.
416
     * @param Project $project The project.
417
     * @param User $user The user.
418
     * @return string[] Values with for keys 'average_size',
419
     *                  'small_edits' and 'large_edits'
420
     */
421
    public function getEditSizeData(Project $project, User $user): array
422
    {
423
        // Set up cache.
424
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_editsizes');
425
        if ($this->cache->hasItem($cacheKey)) {
426
            return $this->cache->getItem($cacheKey)->get();
427
        }
428
429
        // Prepare the queries and execute them.
430
        $revisionTable = $project->getTableName('revision');
431
        $sql = "SELECT AVG(sizes.size) AS average_size,
432
                COUNT(CASE WHEN sizes.size < 20 THEN 1 END) AS small_edits,
433
                COUNT(CASE WHEN sizes.size > 1000 THEN 1 END) AS large_edits
434
                FROM (
435
                    SELECT (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS size
436
                    FROM $revisionTable AS revs
437
                    LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
438
                    WHERE revs.rev_actor = :actorId
439
                    ORDER BY revs.rev_timestamp DESC
440
                    LIMIT 5000
441
                ) sizes";
442
        $results = $this->executeProjectsQuery($sql, [
443
            'actorId' => $user->getActorId($project),
444
        ])->fetch();
445
446
        // Cache and return.
447
        return $this->setCache($cacheKey, $results);
448
    }
449
450
    /**
451
     * Get the number of edits this user made using semi-automated tools.
452
     * @param Project $project
453
     * @param User $user
454
     * @return int Result of query, see below.
455
     */
456
    public function countAutomatedEdits(Project $project, User $user): int
457
    {
458
        $autoEditsRepo = new AutoEditsRepository();
459
        $autoEditsRepo->setContainer($this->container);
460
        return $autoEditsRepo->countAutomatedEdits($project, $user);
461
    }
462
}
463