Passed
Push — master ( 0c65d8...e43fc5 )
by MusikAnimal
18:43
created

EditCounterRepository::getProjectsWithEdits()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 16
nc 4
nop 1
dl 0
loc 27
rs 9.7333
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
82
        $resultQuery = $this->executeProjectsQuery($sql, [
83
            'actorId' => $user->getActorId($project),
84
        ]);
85
86
        $revisionCounts = [];
87
        while ($result = $resultQuery->fetch()) {
88
            $revisionCounts[$result['key']] = (int)$result['val'];
89
        }
90
91
        // Cache and return.
92
        return $this->setCache($cacheKey, $revisionCounts);
93
    }
94
95
    /**
96
     * Get log totals for a user.
97
     * @param Project $project The project.
98
     * @param User $user The user.
99
     * @return integer[] Keys are "<log>-<action>" strings, values are counts.
100
     */
101
    public function getLogCounts(Project $project, User $user): array
102
    {
103
        // Set up cache.
104
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_logcounts');
105
        if ($this->cache->hasItem($cacheKey)) {
106
            return $this->cache->getItem($cacheKey)->get();
107
        }
108
109
        // Query.
110
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging');
111
        $sql = "
112
        (SELECT CONCAT(log_type, '-', log_action) AS source, COUNT(log_id) AS value
113
            FROM $loggingTable
114
            WHERE log_actor = :actorId
115
            GROUP BY log_type, log_action
116
        )";
117
118
        $results = $this->executeProjectsQuery($sql, [
119
            'actorId' => $user->getActorId($project),
120
        ])->fetchAll();
121
122
        $logCounts = array_combine(
123
            array_map(function ($e) {
124
                return $e['source'];
125
            }, $results),
126
            array_map(function ($e) {
127
                return (int)$e['value'];
128
            }, $results)
129
        );
130
131
        // Make sure there is some value for each of the wanted counts.
132
        $requiredCounts = [
133
            'thanks-thank',
134
            'review-approve',
135
            'newusers-create2',
136
            'newusers-byemail',
137
            'patrol-patrol',
138
            'block-block',
139
            'block-reblock',
140
            'block-unblock',
141
            'protect-protect',
142
            'protect-modify',
143
            'protect-unprotect',
144
            'rights-rights',
145
            'move-move',
146
            'delete-delete',
147
            'delete-revision',
148
            'delete-restore',
149
            'import-import',
150
            'import-interwiki',
151
            'import-upload',
152
            'upload-upload',
153
            'upload-overwrite',
154
            'abusefilter-modify',
155
            'merge-merge',
156
        ];
157
        foreach ($requiredCounts as $req) {
158
            if (!isset($logCounts[$req])) {
159
                $logCounts[$req] = 0;
160
            }
161
        }
162
163
        // Add Commons upload count, if applicable.
164
        $logCounts['files_uploaded_commons'] = 0;
165
        if ($this->isLabs() && !$user->isAnon()) {
166
            $sql = "SELECT COUNT(log_id)
167
                    FROM commonswiki_p.logging_userindex
168
                    JOIN commonswiki_p.actor ON actor_id = log_actor
169
                    WHERE log_type = 'upload' AND log_action = 'upload'
170
                        AND actor_name = :username";
171
            $resultQuery = $this->executeProjectsQuery($sql, [
172
                'username' => $user->getUsername(),
173
            ]);
174
            $logCounts['files_uploaded_commons'] = (int)$resultQuery->fetchColumn();
175
        }
176
177
        // Cache and return.
178
        return $this->setCache($cacheKey, $logCounts);
179
    }
180
181
    /**
182
     * Get the IDs and timestamps of the latest edit and logged action by the given user.
183
     * @param Project $project
184
     * @param User $user
185
     * @return string[] With keys 'rev_first', 'rev_latest', 'log_latest'.
186
     */
187
    public function getFirstAndLatestActions(Project $project, User $user): array
188
    {
189
        $loggingTable = $project->getTableName('logging', 'userindex');
190
        $revisionTable = $project->getTableName('revision');
191
192
        $sql = "(
193
                    SELECT 'rev_first' AS `key`, rev_id AS `id`,
194
                        rev_timestamp AS `timestamp`, NULL as `type`
195
                    FROM $revisionTable
196
                    WHERE rev_actor = :actorId
197
                    LIMIT 1
198
                ) UNION (
199
                    SELECT 'rev_latest' AS `key`, rev_id AS `id`,
200
                        rev_timestamp AS `timestamp`, NULL as `type`
201
                    FROM $revisionTable
202
                    WHERE rev_actor = :actorId
203
                    ORDER BY rev_timestamp DESC LIMIT 1
204
                ) UNION (
205
                    SELECT 'log_latest' AS `key`, log_id AS `id`,
206
                        log_timestamp AS `timestamp`, log_type AS `type`
207
                    FROM $loggingTable
208
                    WHERE log_actor = :actorId
209
                    ORDER BY log_timestamp DESC LIMIT 1
210
                )";
211
212
        $resultQuery = $this->executeProjectsQuery($sql, [
213
            'actorId' => $user->getActorId($project),
214
        ]);
215
216
        $actions = [];
217
        while ($result = $resultQuery->fetch()) {
218
            $actions[$result['key']] = [
219
                'id' => $result['id'],
220
                'timestamp' => $result['timestamp'],
221
                'type' => $result['type'],
222
            ];
223
        }
224
225
        return $actions;
226
    }
227
228
    /**
229
     * Get data for all blocks set on the given user.
230
     * @param Project $project
231
     * @param User $user
232
     * @return array
233
     */
234
    public function getBlocksReceived(Project $project, User $user): array
235
    {
236
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging', 'logindex');
237
        $sql = "SELECT log_action, log_timestamp, log_params FROM $loggingTable
238
                WHERE log_type = 'block'
239
                AND log_action IN ('block', 'reblock', 'unblock')
240
                AND log_timestamp > 0
241
                AND log_title = :username
242
                AND log_namespace = 2
243
                ORDER BY log_timestamp ASC";
244
        $username = str_replace(' ', '_', $user->getUsername());
245
246
        return $this->executeProjectsQuery($sql, [
247
            'username' => $username,
248
        ])->fetchAll();
249
    }
250
251
    /**
252
     * Get the given user's total edit counts per namespace on the given project.
253
     * @param Project $project The project.
254
     * @param User $user The user.
255
     * @return array Array keys are namespace IDs, values are the edit counts.
256
     */
257
    public function getNamespaceTotals(Project $project, User $user): array
258
    {
259
        // Cache?
260
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_namespacetotals');
261
        if ($this->cache->hasItem($cacheKey)) {
262
            return $this->cache->getItem($cacheKey)->get();
263
        }
264
265
        // Query.
266
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
267
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
268
        $sql = "SELECT page_namespace, COUNT(rev_id) AS total
269
            FROM $pageTable p JOIN $revisionTable r ON (r.rev_page = p.page_id)
270
            WHERE r.rev_actor = :actorId
271
            GROUP BY page_namespace";
272
273
        $results = $this->executeProjectsQuery($sql, [
274
            'actorId' => $user->getActorId($project),
275
        ])->fetchAll();
276
277
        $namespaceTotals = array_combine(array_map(function ($e) {
278
            return $e['page_namespace'];
279
        }, $results), array_map(function ($e) {
280
            return (int)$e['total'];
281
        }, $results));
282
283
        // Cache and return.
284
        return $this->setCache($cacheKey, $namespaceTotals);
285
    }
286
287
    /**
288
     * Get data for a bar chart of monthly edit totals per namespace.
289
     * @param Project $project The project.
290
     * @param User $user The user.
291
     * @return string[] [
292
     *                      [
293
     *                          'year' => <year>,
294
     *                          'month' => <month>,
295
     *                          'page_namespace' => <namespace>,
296
     *                          'count' => <count>,
297
     *                      ],
298
     *                      ...
299
     *                  ]
300
     */
301
    public function getMonthCounts(Project $project, User $user): array
302
    {
303
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_monthcounts');
304
        if ($this->cache->hasItem($cacheKey)) {
305
            return $this->cache->getItem($cacheKey)->get();
306
        }
307
308
        $revisionTable = $project->getTableName('revision');
309
        $pageTable = $project->getTableName('page');
310
        $sql =
311
            "SELECT "
312
            . "     YEAR(rev_timestamp) AS `year`,"
313
            . "     MONTH(rev_timestamp) AS `month`,"
314
            . "     page_namespace,"
315
            . "     COUNT(rev_id) AS `count` "
316
            .  " FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
317
            . " WHERE rev_actor = :actorId"
318
            . " GROUP BY YEAR(rev_timestamp), MONTH(rev_timestamp), page_namespace";
319
320
        $totals = $this->executeProjectsQuery($sql, [
321
            'actorId' => $user->getActorId($project),
322
        ])->fetchAll();
323
324
        // Cache and return.
325
        return $this->setCache($cacheKey, $totals);
326
    }
327
328
    /**
329
     * Get data for the timecard chart, with totals grouped by day and to the nearest two-hours.
330
     * @param Project $project
331
     * @param User $user
332
     * @return string[]
333
     */
334
    public function getTimeCard(Project $project, User $user): array
335
    {
336
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_timecard');
337
        if ($this->cache->hasItem($cacheKey)) {
338
            return $this->cache->getItem($cacheKey)->get();
339
        }
340
341
        $hourInterval = 2;
342
        $xCalc = "ROUND(HOUR(rev_timestamp)/$hourInterval) * $hourInterval";
343
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
344
        $sql = "SELECT "
345
            . "     DAYOFWEEK(rev_timestamp) AS `day_of_week`, "
346
            . "     $xCalc AS `hour`, "
347
            . "     COUNT(rev_id) AS `value` "
348
            . " FROM $revisionTable"
349
            . " WHERE rev_actor = :actorId"
350
            . " GROUP BY DAYOFWEEK(rev_timestamp), $xCalc ";
351
352
        $totals = $this->executeProjectsQuery($sql, [
353
            'actorId' => $user->getActorId($project),
354
        ])->fetchAll();
355
356
        // Cache and return.
357
        return $this->setCache($cacheKey, $totals);
358
    }
359
360
    /**
361
     * Get various data about edit sizes of the past 5,000 edits.
362
     * Will cache the result for 10 minutes.
363
     * @param Project $project The project.
364
     * @param User $user The user.
365
     * @return string[] Values with for keys 'average_size',
366
     *                  'small_edits' and 'large_edits'
367
     */
368
    public function getEditSizeData(Project $project, User $user): array
369
    {
370
        // Set up cache.
371
        $cacheKey = $this->getCacheKey(func_get_args(), 'ec_editsizes');
372
        if ($this->cache->hasItem($cacheKey)) {
373
            return $this->cache->getItem($cacheKey)->get();
374
        }
375
376
        // Prepare the queries and execute them.
377
        $revisionTable = $project->getTableName('revision');
378
        $sql = "SELECT AVG(sizes.size) AS average_size,
379
                COUNT(CASE WHEN sizes.size < 20 THEN 1 END) AS small_edits,
380
                COUNT(CASE WHEN sizes.size > 1000 THEN 1 END) AS large_edits
381
                FROM (
382
                    SELECT (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS size
383
                    FROM $revisionTable AS revs
384
                    LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
385
                    WHERE revs.rev_actor = :actorId
386
                    ORDER BY revs.rev_timestamp DESC
387
                    LIMIT 5000
388
                ) sizes";
389
        $results = $this->executeProjectsQuery($sql, [
390
            'actorId' => $user->getActorId($project),
391
        ])->fetch();
392
393
        // Cache and return.
394
        return $this->setCache($cacheKey, $results);
395
    }
396
397
    /**
398
     * Get the number of edits this user made using semi-automated tools.
399
     * @param Project $project
400
     * @param User $user
401
     * @return int Result of query, see below.
402
     */
403
    public function countAutomatedEdits(Project $project, User $user): int
404
    {
405
        $autoEditsRepo = new AutoEditsRepository();
406
        $autoEditsRepo->setContainer($this->container);
407
        return $autoEditsRepo->countAutomatedEdits($project, $user);
408
    }
409
}
410