Completed
Push — master ( dc7aae...2784d5 )
by MusikAnimal
02:59
created

EditCounterRepository::getYearCounts()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 32
Code Lines 26

Duplication

Lines 32
Ratio 100 %

Importance

Changes 0
Metric Value
dl 32
loc 32
rs 8.8571
c 0
b 0
f 0
cc 2
eloc 26
nc 2
nop 2
1
<?php
2
/**
3
 * This file contains only the EditCounterRepository class.
4
 */
5
6
namespace Xtools;
7
8
use DateInterval;
9
use DateTime;
10
use Mediawiki\Api\SimpleRequest;
11
12
/**
13
 * An EditCounterRepository is responsible for retrieving edit count information from the
14
 * databases and API. It doesn't do any post-processing of that information.
15
 */
16
class EditCounterRepository extends Repository
17
{
18
19
    /**
20
     * Get data about revisions, pages, etc.
21
     * @param Project $project The project.
22
     * @param User $user The user.
23
     * @returns string[] With keys: 'deleted', 'live', 'total', 'first', 'last', '24h', '7d', '30d',
24
     * '365d', 'small', 'large', 'with_comments', and 'minor_edits', ...
25
     */
26
    public function getPairData(Project $project, User $user)
27
    {
28
        // Set up cache.
29
        $cacheKey = 'pairdata.'.$project->getDatabaseName().'.'.$user->getCacheKey();
30
        if ($this->cache->hasItem($cacheKey)) {
31
            return $this->cache->getItem($cacheKey)->get();
32
        }
33
34
        // Prepare the queries and execute them.
35
        $archiveTable = $this->getTableName($project->getDatabaseName(), 'archive');
36
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
37
        $queries = "
38
39
            -- Revision counts.
40
            (SELECT 'deleted' AS `key`, COUNT(ar_id) AS val FROM $archiveTable
41
                WHERE ar_user = :userId
42
            ) UNION (
43
            SELECT 'live' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
44
                WHERE rev_user = :userId
45
            ) UNION (
46
            SELECT 'day' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
47
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 DAY)
48
            ) UNION (
49
            SELECT 'week' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
50
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 WEEK)
51
            ) UNION (
52
            SELECT 'month' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
53
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 MONTH)
54
            ) UNION (
55
            SELECT 'year' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
56
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 YEAR)
57
            ) UNION (
58
            SELECT 'with_comments' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
59
                WHERE rev_user = :userId AND rev_comment != ''
60
            ) UNION (
61
            SELECT 'minor' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
62
                WHERE rev_user = :userId AND rev_minor_edit = 1
63
64
            -- Dates.
65
            ) UNION (
66
            SELECT 'first' AS `key`, rev_timestamp AS `val` FROM $revisionTable
67
                WHERE rev_user = :userId ORDER BY rev_timestamp ASC LIMIT 1
68
            ) UNION (
69
            SELECT 'last' AS `key`, rev_timestamp AS `date` FROM $revisionTable
70
                WHERE rev_user = :userId ORDER BY rev_timestamp DESC LIMIT 1
71
72
            -- Page counts.
73
            ) UNION (
74
            SELECT 'edited-live' AS `key`, COUNT(DISTINCT rev_page) AS `val`
75
                FROM $revisionTable
76
                WHERE rev_user = :userId
77
            ) UNION (
78
            SELECT 'edited-deleted' AS `key`, COUNT(DISTINCT ar_page_id) AS `val`
79
                FROM $archiveTable
80
                WHERE ar_user = :userId
81
            ) UNION (
82
            SELECT 'created-live' AS `key`, COUNT(DISTINCT rev_page) AS `val`
83
                FROM $revisionTable
84
                WHERE rev_user = :userId AND rev_parent_id = 0
85
            ) UNION (
86
            SELECT 'created-deleted' AS `key`, COUNT(DISTINCT ar_page_id) AS `val`
87
                FROM $archiveTable
88
                WHERE ar_user = :userId AND ar_parent_id = 0
89
            )
90
        ";
91
        $resultQuery = $this->getProjectsConnection()->prepare($queries);
92
        $userId = $user->getId($project);
93
        $resultQuery->bindParam("userId", $userId);
94
        $resultQuery->execute();
95
        $revisionCounts = [];
96
        while ($result = $resultQuery->fetch()) {
97
            $revisionCounts[$result['key']] = $result['val'];
98
        }
99
100
        // Cache for 10 minutes, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
101
        $cacheItem = $this->cache->getItem($cacheKey)
102
                ->set($revisionCounts)
103
                ->expiresAfter(new DateInterval('PT10M'));
104
        $this->cache->save($cacheItem);
105
106
        return $revisionCounts;
107
    }
108
109
    /**
110
     * Get log totals for a user.
111
     * @param Project $project The project.
112
     * @param User $user The user.
113
     * @return integer[] Keys are "<log>-<action>" strings, values are counts.
114
     */
115
    public function getLogCounts(Project $project, User $user)
116
    {
117
        // Set up cache.
118
        $cacheKey = 'logcounts.'.$project->getDatabaseName().'.'.$user->getCacheKey();
119
        if ($this->cache->hasItem($cacheKey)) {
120
            return $this->cache->getItem($cacheKey)->get();
121
        }
122
        $this->stopwatch->start($cacheKey, 'XTools');
123
124
        // Query.
125
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging');
126
        $sql = "
127
        (SELECT CONCAT(log_type, '-', log_action) AS source, COUNT(log_id) AS value
128
            FROM $loggingTable
129
            WHERE log_user = :userId
130
            GROUP BY log_type, log_action
131
        )";
132
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
133
        $userId = $user->getId($project);
134
        $resultQuery->bindParam('userId', $userId);
135
        $resultQuery->execute();
136
        $results = $resultQuery->fetchAll();
137
        $logCounts = array_combine(
138
            array_map(function ($e) {
139
                return $e['source'];
140
            }, $results),
141
            array_map(function ($e) {
142
                return $e['value'];
143
            }, $results)
144
        );
145
146
        // Make sure there is some value for each of the wanted counts.
147
        $requiredCounts = [
148
            'thanks-thank',
149
            'review-approve',
150
            'newusers-create2',
151
            'newusers-byemail',
152
            'patrol-patrol',
153
            'block-block',
154
            'block-reblock',
155
            'block-unblock',
156
            'protect-protect',
157
            'protect-modify',
158
            'protect-unprotect',
159
            'rights-rights',
160
            'move-move',
161
            'delete-delete',
162
            'delete-revision',
163
            'delete-restore',
164
            'import-import',
165
            'import-interwiki',
166
            'import-upload',
167
            'upload-upload',
168
            'upload-overwrite',
169
        ];
170
        foreach ($requiredCounts as $req) {
171
            if (!isset($logCounts[$req])) {
172
                $logCounts[$req] = 0;
173
            }
174
        }
175
176
        // Add Commons upload count, if applicable.
177
        $logCounts['files_uploaded_commons'] = 0;
178
        if ($this->isLabs()) {
179
            $commons = ProjectRepository::getProject('commonswiki', $this->container);
180
            $userId = $user->getId($commons);
181
            if ($userId) {
182
                $sql = "SELECT COUNT(log_id) FROM commonswiki_p.logging_userindex
183
                    WHERE log_type = 'upload' AND log_action = 'upload' AND log_user = :userId";
184
                $resultQuery = $this->getProjectsConnection()->prepare($sql);
185
                $resultQuery->bindParam('userId', $userId);
186
                $resultQuery->execute();
187
                $logCounts['files_uploaded_commons'] = $resultQuery->fetchColumn();
188
            }
189
        }
190
191
        // Cache for 10 minutes, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
192
        $cacheItem = $this->cache->getItem($cacheKey)
193
            ->set($logCounts)
194
            ->expiresAfter(new DateInterval('PT10M'));
195
        $this->cache->save($cacheItem);
196
        $this->stopwatch->stop($cacheKey);
197
198
        return $logCounts;
0 ignored issues
show
Best Practice introduced by
The expression return $logCounts; seems to be an array, but some of its elements' types (boolean) are incompatible with the return type documented by Xtools\EditCounterRepository::getLogCounts of type integer[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
199
    }
200
201
    /**
202
     * Get data for all blocks set on the given user.
203
     * @param Project $project
204
     * @param User $user
205
     * @return array
206
     */
207
    public function getBlocksReceived(Project $project, User $user)
208
    {
209
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging', 'logindex');
210
        $sql = "SELECT log_timestamp, log_params FROM $loggingTable
211
                WHERE log_type = 'block'
212
                AND log_action = 'block'
213
                AND log_timestamp > 0
214
                AND log_title = :username";
215
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
216
        $username = str_replace(' ', '_', $user->getUsername());
217
        $resultQuery->bindParam('username', $username);
218
        $resultQuery->execute();
219
        return $resultQuery->fetchAll();
220
    }
221
222
    /**
223
     * Get a user's total edit count on all projects.
224
     * @see EditCounterRepository::globalEditCountsFromCentralAuth()
225
     * @see EditCounterRepository::globalEditCountsFromDatabases()
226
     * @param User $user The user.
227
     * @param Project $project The project to start from.
228
     * @return mixed[] Elements are arrays with 'project' (Project), and 'total' (int).
229
     */
230
    public function globalEditCounts(User $user, Project $project)
231
    {
232
        // Get the edit counts from CentralAuth or database.
233
        $editCounts = $this->globalEditCountsFromCentralAuth($user, $project);
234
        if ($editCounts === false) {
235
            $editCounts = $this->globalEditCountsFromDatabases($user, $project);
236
        }
237
238
        // Pre-populate all projects' metadata, to prevent each project call from fetching it.
239
        $project->getRepository()->getAll();
1 ignored issue
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Xtools\Repository as the method getAll() does only exist in the following sub-classes of Xtools\Repository: Xtools\ProjectRepository. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
240
241
        // Compile the output.
242
        $out = [];
243
        foreach ($editCounts as $editCount) {
244
            $out[] = [
245
                'project' => ProjectRepository::getProject($editCount['dbName'], $this->container),
246
                'total' => $editCount['total'],
247
            ];
248
        }
249
        return $out;
250
    }
251
252
    /**
253
     * Get a user's total edit count on one or more project.
254
     * Requires the CentralAuth extension to be installed on the project.
255
     *
256
     * @param User $user The user.
257
     * @param Project $project The project to start from.
258
     * @return mixed[] Elements are arrays with 'dbName' (string), and 'total' (int).
259
     */
260
    protected function globalEditCountsFromCentralAuth(User $user, Project $project)
261
    {
262
        $this->log->debug(__METHOD__." Getting global edit counts for ".$user->getUsername());
263
        // Set up cache and stopwatch.
264
        $cacheKey = 'globalRevisionCounts.'.$user->getCacheKey();
265
        if ($this->cache->hasItem($cacheKey)) {
266
            return $this->cache->getItem($cacheKey)->get();
267
        }
268
        $this->stopwatch->start($cacheKey, 'XTools');
269
270
        // Load all projects, so it doesn't have to request metadata about each one as it goes.
271
        $project->getRepository()->getAll();
1 ignored issue
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Xtools\Repository as the method getAll() does only exist in the following sub-classes of Xtools\Repository: Xtools\ProjectRepository. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
272
273
        $api = $this->getMediawikiApi($project);
274
        $params = [
275
            'meta' => 'globaluserinfo',
276
            'guiprop' => 'editcount|merged',
277
            'guiuser' => $user->getUsername(),
278
        ];
279
        $query = new SimpleRequest('query', $params);
280
        $result = $api->getRequest($query);
281
        if (!isset($result['query']['globaluserinfo']['merged'])) {
282
            return [];
283
        }
284
        $out = [];
285
        foreach ($result['query']['globaluserinfo']['merged'] as $result) {
286
            $out[] = [
287
                'dbName' => $result['wiki'],
288
                'total' => $result['editcount'],
289
            ];
290
        }
291
292
        // Cache for 10 minutes, and return.
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
293
        $cacheItem = $this->cache->getItem($cacheKey)
294
            ->set($out)
295
            ->expiresAfter(new DateInterval('PT10M'));
296
        $this->cache->save($cacheItem);
297
        $this->stopwatch->stop($cacheKey);
298
299
        return $out;
300
    }
301
302
    /**
303
     * Get total edit counts from all projects for this user.
304
     * @see EditCounterRepository::globalEditCountsFromCentralAuth()
305
     * @param User $user The user.
306
     * @param Project $project The project to start from.
307
     * @return mixed[] Elements are arrays with 'dbName' (string), and 'total' (int).
308
     */
309
    protected function globalEditCountsFromDatabases(User $user, Project $project)
310
    {
311
        $stopwatchName = 'globalRevisionCounts.'.$user->getUsername();
312
        $allProjects = $project->getRepository()->getAll();
1 ignored issue
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Xtools\Repository as the method getAll() does only exist in the following sub-classes of Xtools\Repository: Xtools\ProjectRepository. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
313
        $topEditCounts = [];
314
        $username = $user->getUsername();
315
        foreach ($allProjects as $projectMeta) {
316
            $revisionTableName = $this->getTableName($projectMeta['dbName'], 'revision');
317
            $sql = "SELECT COUNT(rev_id) FROM $revisionTableName WHERE rev_user_text=:username";
318
            $stmt = $this->getProjectsConnection()->prepare($sql);
319
            $stmt->bindParam('username', $username);
320
            $stmt->execute();
321
            $total = (int)$stmt->fetchColumn();
322
            $topEditCounts[] = [
323
                'dbName' => $projectMeta['dbName'],
324
                'total' => $total,
325
            ];
326
            $this->stopwatch->lap($stopwatchName);
327
        }
328
        return $topEditCounts;
329
    }
330
331
    /**
332
     * Get the given user's total edit counts per namespace on the given project.
333
     * @param Project $project The project.
334
     * @param User $user The user.
335
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
336
     */
337
    public function getNamespaceTotals(Project $project, User $user)
338
    {
339
        // Cache?
340
        $userId = $user->getId($project);
341
        $cacheKey = "ec.namespacetotals.{$project->getDatabaseName()}.$userId";
342
        $this->stopwatch->start($cacheKey, 'XTools');
343
        if ($this->cache->hasItem($cacheKey)) {
344
            return $this->cache->getItem($cacheKey)->get();
345
        }
346
347
        // Query.
348
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
349
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
350
        $sql = "SELECT page_namespace, COUNT(rev_id) AS total
351
            FROM $pageTable p JOIN $revisionTable r ON (r.rev_page = p.page_id)
352
            WHERE r.rev_user = :id
353
            GROUP BY page_namespace";
354
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
355
        $resultQuery->bindParam(":id", $userId);
356
        $resultQuery->execute();
357
        $results = $resultQuery->fetchAll();
358
        $namespaceTotals = array_combine(array_map(function ($e) {
359
            return $e['page_namespace'];
360
        }, $results), array_map(function ($e) {
361
            return $e['total'];
362
        }, $results));
363
364
        // Cache and return.
365
        $cacheItem = $this->cache->getItem($cacheKey);
366
        $cacheItem->set($namespaceTotals);
367
        $cacheItem->expiresAfter(new DateInterval('PT15M'));
368
        $this->cache->save($cacheItem);
369
        $this->stopwatch->stop($cacheKey);
370
        return $namespaceTotals;
371
    }
372
373
    /**
374
     * Get revisions by this user.
375
     * @param Project[] $projects The projects.
376
     * @param User $user The user.
377
     * @param int $lim The maximum number of revisions to fetch from each project.
378
     * @return array|mixed
379
     */
380
    public function getRevisions($projects, User $user, $lim = 40)
381
    {
382
        // Check cache.
383
        $cacheKey = "globalcontribs.".$user->getCacheKey();
384
        $this->stopwatch->start($cacheKey, 'XTools');
385
        if ($this->cache->hasItem($cacheKey)) {
386
            return $this->cache->getItem($cacheKey)->get();
387
        }
388
389
        // Assemble queries.
390
        $username = $user->getUsername();
391
        $queries = [];
392
        foreach ($projects as $project) {
393
            $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
394
            $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
395
            $sql = "SELECT
396
                    '".$project->getDatabaseName()."' AS project_name,
397
                    revs.rev_id AS id,
398
                    revs.rev_timestamp AS timestamp,
399
                    UNIX_TIMESTAMP(revs.rev_timestamp) AS unix_timestamp,
400
                    revs.rev_minor_edit AS minor,
401
                    revs.rev_deleted AS deleted,
402
                    revs.rev_len AS length,
403
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
404
                    revs.rev_parent_id AS parent_id,
405
                    revs.rev_comment AS comment,
406
                    revs.rev_user_text AS username,
407
                    page.page_title,
408
                    page.page_namespace
409
                FROM $revisionTable AS revs
410
                    JOIN $pageTable AS page ON (rev_page = page_id)
411
                    LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
412
                WHERE revs.rev_user_text = :username
413
                ORDER BY revs.rev_timestamp DESC";
414
            if (is_numeric($lim)) {
415
                $sql .= " LIMIT $lim";
416
            }
417
            $queries[] = $sql;
418
        }
419
        $sql = "(\n" . join("\n) UNION (\n", $queries) . ")\n";
420
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
421
        $resultQuery->bindParam(":username", $username);
422
        $resultQuery->execute();
423
        $revisions = $resultQuery->fetchAll();
424
425
        // Cache this.
426
        $cacheItem = $this->cache->getItem($cacheKey);
427
        $cacheItem->set($revisions);
428
        $cacheItem->expiresAfter(new DateInterval('PT15M'));
429
        $this->cache->save($cacheItem);
430
431
        $this->stopwatch->stop($cacheKey);
432
        return $revisions;
433
    }
434
435
    /**
436
     * Get data for a bar chart of monthly edit totals per namespace.
437
     * @param Project $project The project.
438
     * @param User $user The user.
439
     * @return string[] [
440
     *                      [
441
     *                          'year' => <year>,
442
     *                          'month' => <month>,
443
     *                          'page_namespace' => <namespace>,
444
     *                          'count' => <count>,
445
     *                      ],
446
     *                      ...
447
     *                  ]
448
     */
449
    public function getMonthCounts(Project $project, User $user)
450
    {
451
        $cacheKey = "monthcounts.".$user->getCacheKey();
452
        $this->stopwatch->start($cacheKey, 'XTools');
453
        if ($this->cache->hasItem($cacheKey)) {
454
            return $this->cache->getItem($cacheKey)->get();
455
        }
456
457
        $username = $user->getUsername();
458
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
459
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
460
        $sql =
461
            "SELECT "
462
            . "     YEAR(rev_timestamp) AS `year`,"
463
            . "     MONTH(rev_timestamp) AS `month`,"
464
            . "     page_namespace,"
465
            . "     COUNT(rev_id) AS `count` "
466
            .  " FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
467
            . " WHERE rev_user_text = :username"
468
            . " GROUP BY YEAR(rev_timestamp), MONTH(rev_timestamp), page_namespace "
469
            . " ORDER BY rev_timestamp DESC";
470
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
471
        $resultQuery->bindParam(":username", $username);
472
        $resultQuery->execute();
473
        $totals = $resultQuery->fetchAll();
474
475
        $cacheItem = $this->cache->getItem($cacheKey);
476
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
477
        $cacheItem->set($totals);
478
        $this->cache->save($cacheItem);
479
480
        $this->stopwatch->stop($cacheKey);
481
        return $totals;
482
    }
483
484
    /**
485
     * Get data for the timecard chart, with totals grouped by day and to the nearest two-hours.
486
     * @param Project $project
487
     * @param User $user
488
     * @return string[]
489
     */
490
    public function getTimeCard(Project $project, User $user)
491
    {
492
        $cacheKey = "timecard.".$user->getCacheKey();
493
        $this->stopwatch->start($cacheKey, 'XTools');
494
        if ($this->cache->hasItem($cacheKey)) {
495
            return $this->cache->getItem($cacheKey)->get();
496
        }
497
498
        $username = $user->getUsername();
499
        $hourInterval = 2;
500
        $xCalc = "ROUND(HOUR(rev_timestamp)/$hourInterval) * $hourInterval";
501
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
502
        $sql = "SELECT "
503
            . "     DAYOFWEEK(rev_timestamp) AS `y`, "
504
            . "     $xCalc AS `x`, "
505
            . "     COUNT(rev_id) AS `r` "
506
            . " FROM $revisionTable"
507
            . " WHERE rev_user_text = :username"
508
            . " GROUP BY DAYOFWEEK(rev_timestamp), $xCalc ";
509
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
510
        $resultQuery->bindParam(":username", $username);
511
        $resultQuery->execute();
512
        $totals = $resultQuery->fetchAll();
513
        // Scale the radii: get the max, then scale each radius.
514
        // This looks inefficient, but there's a max of 72 elements in this array.
515
        $max = 0;
516
        foreach ($totals as $total) {
517
            $max = max($max, $total['r']);
518
        }
519
        foreach ($totals as &$total) {
520
            $total['r'] = round($total['r'] / $max * 100);
521
        }
522
        $cacheItem = $this->cache->getItem($cacheKey);
523
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
524
        $cacheItem->set($totals);
525
        $this->cache->save($cacheItem);
526
527
        $this->stopwatch->stop($cacheKey);
528
        return $totals;
529
    }
530
531
    /**
532
     * Get various data about edit sizes of the past 5,000 edits.
533
     * Will cache the result for 10 minutes.
534
     * @param Project $project The project.
535
     * @param User $user The user.
536
     * @return string[] Values with for keys 'average_size',
537
     *                  'small_edits' and 'large_edits'
538
     */
539
    public function getEditSizeData(Project $project, User $user)
540
    {
541
        // Set up cache.
542
        $cacheKey = 'editsizedata.'.$project->getDatabaseName().'.'.$user->getCacheKey();
543
        $this->stopwatch->start($cacheKey, 'XTools');
544
        if ($this->cache->hasItem($cacheKey)) {
545
            return $this->cache->getItem($cacheKey)->get();
546
        }
547
548
        // Prepare the queries and execute them.
549
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
550
        $userId = $user->getId($project);
551
        $sql = "SELECT AVG(sizes.size) AS average_size,
552
                COUNT(CASE WHEN sizes.size < 20 THEN 1 END) AS small_edits,
553
                COUNT(CASE WHEN sizes.size > 1000 THEN 1 END) AS large_edits
554
                FROM (
555
                    SELECT (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS size
556
                    FROM $revisionTable AS revs
557
                    LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
558
                    WHERE revs.rev_user = :userId
559
                    ORDER BY revs.rev_timestamp DESC
560
                    LIMIT 5000
561
                ) sizes";
562
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
563
        $resultQuery->bindParam('userId', $userId);
564
        $resultQuery->execute();
565
        $results = $resultQuery->fetchAll()[0];
566
567
        // Cache for 10 minutes.
568
        $cacheItem = $this->cache->getItem($cacheKey);
569
        $cacheItem->set($results);
570
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
571
        $this->cache->save($cacheItem);
572
573
        $this->stopwatch->stop($cacheKey);
574
        return $results;
575
    }
576
}
577