Completed
Push — master ( cf9a22...944927 )
by Sam
06:52
created

EditCounterRepository::countAutomatedRevisions()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 41
Code Lines 29

Duplication

Lines 4
Ratio 9.76 %

Importance

Changes 0
Metric Value
dl 4
loc 41
rs 8.439
c 0
b 0
f 0
cc 5
eloc 29
nc 5
nop 2
1
<?php
2
/**
3
 * This file contains only the EditCounterRepository class.
4
 */
5
6
namespace Xtools;
7
8
use AppBundle\Helper\AutomatedEditsHelper;
9
use DateInterval;
10
use DateTime;
11
use Mediawiki\Api\SimpleRequest;
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
 */
17
class EditCounterRepository extends Repository
18
{
19
20
    /**
21
     * Get data about revisions, pages, etc.
22
     * @param Project $project The project.
23
     * @param User $user The user.
24
     * @returns string[] With keys: 'deleted', 'live', 'total', 'first', 'last', '24h', '7d', '30d',
25
     * '365d', 'small', 'large', 'with_comments', and 'minor_edits', ...
26
     */
27
    public function getPairData(Project $project, User $user)
28
    {
29
        // Set up cache.
30
        $cacheKey = 'pairdata.'.$project->getDatabaseName().'.'.$user->getCacheKey();
31
        if ($this->cache->hasItem($cacheKey)) {
32
            return $this->cache->getItem($cacheKey)->get();
33
        }
34
35
        // Prepare the queries and execute them.
36
        $archiveTable = $this->getTableName($project->getDatabaseName(), 'archive');
37
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
38
        $queries = "
39
40
            -- Revision counts.
41
            (SELECT 'deleted' AS `key`, COUNT(ar_id) AS val FROM $archiveTable
42
                WHERE ar_user = :userId
43
            ) UNION (
44
            SELECT 'live' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
45
                WHERE rev_user = :userId
46
            ) UNION (
47
            SELECT 'day' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
48
                WHERE rev_user = :userId 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_user = :userId 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_user = :userId 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_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 YEAR)
58
            ) UNION (
59
            SELECT 'with_comments' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
60
                WHERE rev_user = :userId AND rev_comment != ''
61
            ) UNION (
62
            SELECT 'minor' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
63
                WHERE rev_user = :userId AND rev_minor_edit = 1
64
65
            -- Dates.
66
            ) UNION (
67
            SELECT 'first' AS `key`, rev_timestamp AS `val` FROM $revisionTable
68
                WHERE rev_user = :userId ORDER BY rev_timestamp ASC LIMIT 1
69
            ) UNION (
70
            SELECT 'last' AS `key`, rev_timestamp AS `date` FROM $revisionTable
71
                WHERE rev_user = :userId ORDER BY rev_timestamp DESC LIMIT 1
72
73
            -- Page counts.
74
            ) UNION (
75
            SELECT 'edited-live' AS `key`, COUNT(DISTINCT rev_page) AS `val`
76
                FROM $revisionTable
77
                WHERE rev_user = :userId
78
            ) UNION (
79
            SELECT 'edited-deleted' AS `key`, COUNT(DISTINCT ar_page_id) AS `val`
80
                FROM $archiveTable
81
                WHERE ar_user = :userId
82
            ) UNION (
83
            SELECT 'created-live' AS `key`, COUNT(DISTINCT rev_page) AS `val`
84
                FROM $revisionTable
85
                WHERE rev_user = :userId AND rev_parent_id = 0
86
            ) UNION (
87
            SELECT 'created-deleted' AS `key`, COUNT(DISTINCT ar_page_id) AS `val`
88
                FROM $archiveTable
89
                WHERE ar_user = :userId AND ar_parent_id = 0
90
            )
91
        ";
92
        $resultQuery = $this->getProjectsConnection()->prepare($queries);
93
        $userId = $user->getId($project);
94
        $resultQuery->bindParam("userId", $userId);
95
        $resultQuery->execute();
96
        $revisionCounts = [];
97
        while ($result = $resultQuery->fetch()) {
98
            $revisionCounts[$result['key']] = $result['val'];
99
        }
100
101
        // 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...
102
        $cacheItem = $this->cache->getItem($cacheKey)
103
                ->set($revisionCounts)
104
                ->expiresAfter(new DateInterval('PT10M'));
105
        $this->cache->save($cacheItem);
106
107
        return $revisionCounts;
108
    }
109
110
    /**
111
     * Get log totals for a user.
112
     * @param Project $project The project.
113
     * @param User $user The user.
114
     * @return integer[] Keys are "<log>-<action>" strings, values are counts.
115
     */
116
    public function getLogCounts(Project $project, User $user)
117
    {
118
        // Set up cache.
119
        $cacheKey = 'logcounts.'.$project->getDatabaseName().'.'.$user->getCacheKey();
120
        if ($this->cache->hasItem($cacheKey)) {
121
            return $this->cache->getItem($cacheKey)->get();
122
        }
123
        $this->stopwatch->start($cacheKey, 'XTools');
124
125
        // Query.
126
        $userNamespaceId = 2;
0 ignored issues
show
Unused Code introduced by
$userNamespaceId is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
127
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging');
128
        $sql = "
129
        (SELECT CONCAT(log_type, '-', log_action) AS source, COUNT(log_id) AS value
130
            FROM $loggingTable
131
            WHERE log_user = :userId
132
            GROUP BY log_type, log_action
133
        )";
134
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
135
        $userId = $user->getId($project);
136
        $resultQuery->bindParam('userId', $userId);
137
        $resultQuery->execute();
138
        $results = $resultQuery->fetchAll();
139
        $logCounts = array_combine(
140
            array_map(function ($e) {
141
                return $e['source'];
142
            }, $results),
143
            array_map(function ($e) {
144
                return $e['value'];
145
            }, $results)
146
        );
147
148
        // Make sure there is some value for each of the wanted counts.
149
        $requiredCounts = [
150
            'thanks-thank',
151
            'review-approve',
152
            'newusers-create2',
153
            'newusers-byemail',
154
            'patrol-patrol',
155
            'block-block',
156
            'block-reblock',
157
            'block-unblock',
158
            'protect-protect',
159
            'protect-modify',
160
            'protect-unprotect',
161
            'rights-rights',
162
            'move-move',
163
            'delete-delete',
164
            'delete-revision',
165
            'delete-restore',
166
            'import-import',
167
            'import-interwiki',
168
            'import-upload',
169
            'upload-upload',
170
            'upload-overwrite',
171
        ];
172
        foreach ($requiredCounts as $req) {
173
            if (!isset($logCounts[$req])) {
174
                $logCounts[$req] = 0;
175
            }
176
        }
177
178
        // Add Commons upload count, if applicable.
179
        $logCounts['files_uploaded_commons'] = 0;
180
        if ($this->isLabs()) {
181
            $commons = ProjectRepository::getProject('commonswiki', $this->container);
182
            $userId = $user->getId($commons);
183
            if ($userId) {
184
                $sql = "SELECT COUNT(log_id) FROM commonswiki_p.logging_userindex
185
                    WHERE log_type = 'upload' AND log_action = 'upload' AND log_user = :userId";
186
                $resultQuery = $this->getProjectsConnection()->prepare($sql);
187
                $resultQuery->bindParam('userId', $userId);
188
                $resultQuery->execute();
189
                $logCounts['files_uploaded_commons'] = $resultQuery->fetchColumn();
190
            }
191
        }
192
193
        // 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...
194
        $cacheItem = $this->cache->getItem($cacheKey)
195
            ->set($logCounts)
196
            ->expiresAfter(new DateInterval('PT10M'));
197
        $this->cache->save($cacheItem);
198
        $this->stopwatch->stop($cacheKey);
199
200
        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...
201
    }
202
203
    /**
204
     * Get data for all blocks set on the given user.
205
     * @param Project $project
206
     * @param User $user
207
     * @return array
208
     */
209
    public function getBlocksReceived(Project $project, User $user)
210
    {
211
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging', 'logindex');
212
        $sql = "SELECT log_timestamp, log_params FROM $loggingTable
213
                WHERE log_type = 'block'
214
                AND log_action = 'block'
215
                AND log_timestamp > 0
216
                AND log_title = :username";
217
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
218
        $username = str_replace(' ', '_', $user->getUsername());
219
        $resultQuery->bindParam('username', $username);
220
        $resultQuery->execute();
221
        return $resultQuery->fetchAll();
222
    }
223
224
    /**
225
     * Get a user's total edit count on all projects.
226
     * @see EditCounterRepository::globalEditCountsFromCentralAuth()
227
     * @see EditCounterRepository::globalEditCountsFromDatabases()
228
     * @param User $user The user.
229
     * @param Project $project The project to start from.
230
     * @return mixed[] Elements are arrays with 'project' (Project), and 'total' (int).
231
     */
232
    public function globalEditCounts(User $user, Project $project)
233
    {
234
        // Get the edit counts from CentralAuth or database.
235
        $editCounts = $this->globalEditCountsFromCentralAuth($user, $project);
236
        if ($editCounts === false) {
237
            $editCounts = $this->globalEditCountsFromDatabases($user, $project);
238
        }
239
240
        // Pre-populate all projects' metadata, to prevent each project call from fetching it.
241
        $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...
242
243
        // Compile the output.
244
        $out = [];
245
        foreach ($editCounts as $editCount) {
246
            $out[] = [
247
                'project' => ProjectRepository::getProject($editCount['dbName'], $this->container),
248
                'total' => $editCount['total'],
249
            ];
250
        }
251
        return $out;
252
    }
253
254
    /**
255
     * Get a user's total edit count on one or more project.
256
     * Requires the CentralAuth extension to be installed on the project.
257
     *
258
     * @param User $user The user.
259
     * @param Project $project The project to start from.
260
     * @return mixed[] Elements are arrays with 'dbName' (string), and 'total' (int).
261
     */
262
    protected function globalEditCountsFromCentralAuth(User $user, Project $project)
263
    {
264
        $this->log->debug(__METHOD__." Getting global edit counts for ".$user->getUsername());
265
        // Set up cache and stopwatch.
266
        $cacheKey = 'globalRevisionCounts.'.$user->getCacheKey();
267
        if ($this->cache->hasItem($cacheKey)) {
268
            return $this->cache->getItem($cacheKey)->get();
269
        }
270
        $this->stopwatch->start($cacheKey, 'XTools');
271
272
        // Load all projects, so it doesn't have to request metadata about each one as it goes.
273
        $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...
274
275
        $api = $this->getMediawikiApi($project);
276
        $params = [
277
            'meta' => 'globaluserinfo',
278
            'guiprop' => 'editcount|merged',
279
            'guiuser' => $user->getUsername(),
280
        ];
281
        $query = new SimpleRequest('query', $params);
282
        $result = $api->getRequest($query);
283
        if (!isset($result['query']['globaluserinfo']['merged'])) {
284
            return [];
285
        }
286
        $out = [];
287
        foreach ($result['query']['globaluserinfo']['merged'] as $result) {
288
            $out[] = [
289
                'dbName' => $result['wiki'],
290
                'total' => $result['editcount'],
291
            ];
292
        }
293
294
        // 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...
295
        $cacheItem = $this->cache->getItem($cacheKey)
296
            ->set($out)
297
            ->expiresAfter(new DateInterval('PT10M'));
298
        $this->cache->save($cacheItem);
299
        $this->stopwatch->stop($cacheKey);
300
301
        return $out;
302
    }
303
304
    /**
305
     * Get total edit counts from all projects for this user.
306
     * @see EditCounterRepository::globalEditCountsFromCentralAuth()
307
     * @param User $user The user.
308
     * @param Project $project The project to start from.
309
     * @return mixed[] Elements are arrays with 'dbName' (string), and 'total' (int).
310
     */
311
    protected function globalEditCountsFromDatabases(User $user, Project $project)
312
    {
313
        $stopwatchName = 'globalRevisionCounts.'.$user->getUsername();
314
        $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...
315
        $topEditCounts = [];
316
        $username = $user->getUsername();
317
        foreach ($allProjects as $projectMeta) {
318
            $revisionTableName = $this->getTableName($projectMeta['dbName'], 'revision');
319
            $sql = "SELECT COUNT(rev_id) FROM $revisionTableName WHERE rev_user_text=:username";
320
            $stmt = $this->getProjectsConnection()->prepare($sql);
321
            $stmt->bindParam('username', $username);
322
            $stmt->execute();
323
            $total = (int)$stmt->fetchColumn();
324
            $topEditCounts[] = [
325
                'dbName' => $projectMeta['dbName'],
326
                'total' => $total,
327
            ];
328
            $this->stopwatch->lap($stopwatchName);
329
        }
330
        return $topEditCounts;
331
    }
332
333
    /**
334
     * Get the given user's total edit counts per namespace on the given project.
335
     * @param Project $project The project.
336
     * @param User $user The user.
337
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
338
     */
339
    public function getNamespaceTotals(Project $project, User $user)
340
    {
341
        // Cache?
342
        $userId = $user->getId($project);
343
        $cacheKey = "ec.namespacetotals.{$project->getDatabaseName()}.$userId";
344
        $this->stopwatch->start($cacheKey, 'XTools');
345
        if ($this->cache->hasItem($cacheKey)) {
346
            return $this->cache->getItem($cacheKey)->get();
347
        }
348
349
        // Query.
350
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
351
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
352
        $sql = "SELECT page_namespace, COUNT(rev_id) AS total
353
            FROM $pageTable p JOIN $revisionTable r ON (r.rev_page = p.page_id)
354
            WHERE r.rev_user = :id
355
            GROUP BY page_namespace";
356
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
357
        $resultQuery->bindParam(":id", $userId);
358
        $resultQuery->execute();
359
        $results = $resultQuery->fetchAll();
360
        $namespaceTotals = array_combine(array_map(function ($e) {
361
            return $e['page_namespace'];
362
        }, $results), array_map(function ($e) {
363
            return $e['total'];
364
        }, $results));
365
366
        // Cache and return.
367
        $cacheItem = $this->cache->getItem($cacheKey);
368
        $cacheItem->set($namespaceTotals);
369
        $cacheItem->expiresAfter(new DateInterval('PT15M'));
370
        $this->cache->save($cacheItem);
371
        $this->stopwatch->stop($cacheKey);
372
        return $namespaceTotals;
373
    }
374
375
    /**
376
     * Get revisions by this user.
377
     * @param Project[] $projects The projects.
378
     * @param User $user The user.
379
     * @param int $lim The maximum number of revisions to fetch from each project.
380
     * @return array|mixed
381
     */
382
    public function getRevisions($projects, User $user, $lim = 40)
383
    {
384
        // Check cache.
385
        $cacheKey = "globalcontribs.".$user->getCacheKey();
386
        $this->stopwatch->start($cacheKey, 'XTools');
387
        if ($this->cache->hasItem($cacheKey)) {
388
            return $this->cache->getItem($cacheKey)->get();
389
        }
390
391
        // Assemble queries.
392
        $username = $user->getUsername();
393
        $queries = [];
394
        foreach ($projects as $project) {
395
            $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
396
            $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
397
            $sql = "SELECT
398
                    '".$project->getDatabaseName()."' AS project_name,
399
                    revs.rev_id AS id,
400
                    revs.rev_timestamp AS timestamp,
401
                    UNIX_TIMESTAMP(revs.rev_timestamp) AS unix_timestamp,
402
                    revs.rev_minor_edit AS minor,
403
                    revs.rev_deleted AS deleted,
404
                    revs.rev_len AS length,
405
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
406
                    revs.rev_parent_id AS parent_id,
407
                    revs.rev_comment AS comment,
408
                    revs.rev_user_text AS username,
409
                    page.page_title,
410
                    page.page_namespace
411
                FROM $revisionTable AS revs
412
                    JOIN $pageTable AS page ON (rev_page = page_id)
413
                    LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
414
                WHERE revs.rev_user_text = :username
415
                ORDER BY revs.rev_timestamp DESC";
416
            if (is_numeric($lim)) {
417
                $sql .= " LIMIT $lim";
418
            }
419
            $queries[] = $sql;
420
        }
421
        $sql = "(\n" . join("\n) UNION (\n", $queries) . ")\n";
422
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
423
        $resultQuery->bindParam(":username", $username);
424
        $resultQuery->execute();
425
        $revisions = $resultQuery->fetchAll();
426
427
        // Cache this.
428
        $cacheItem = $this->cache->getItem($cacheKey);
429
        $cacheItem->set($revisions);
430
        $cacheItem->expiresAfter(new DateInterval('PT15M'));
431
        $this->cache->save($cacheItem);
432
433
        $this->stopwatch->stop($cacheKey);
434
        return $revisions;
435
    }
436
437
    /**
438
     * Get data for a bar chart of monthly edit totals per namespace.
439
     * @param Project $project The project.
440
     * @param User $user The user.
441
     * @return string[]
442
     */
443 View Code Duplication
    public function getMonthCounts(Project $project, User $user)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
444
    {
445
        $cacheKey = "monthcounts.".$user->getCacheKey();
446
        $this->stopwatch->start($cacheKey, 'XTools');
447
        if ($this->cache->hasItem($cacheKey)) {
448
            return $this->cache->getItem($cacheKey)->get();
449
        }
450
451
        $username = $user->getUsername();
452
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
453
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
454
        $sql =
455
            "SELECT "
456
            . "     YEAR(rev_timestamp) AS `year`,"
457
            . "     MONTH(rev_timestamp) AS `month`,"
458
            . "     page_namespace,"
459
            . "     COUNT(rev_id) AS `count` "
460
            .  " FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
461
            . " WHERE rev_user_text = :username"
462
            . " GROUP BY YEAR(rev_timestamp), MONTH(rev_timestamp), page_namespace "
463
            . " ORDER BY rev_timestamp DESC";
464
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
465
        $resultQuery->bindParam(":username", $username);
466
        $resultQuery->execute();
467
        $totals = $resultQuery->fetchAll();
468
469
        $cacheItem = $this->cache->getItem($cacheKey);
470
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
471
        $cacheItem->set($totals);
472
        $this->cache->save($cacheItem);
473
474
        $this->stopwatch->stop($cacheKey);
475
        return $totals;
476
    }
477
478
    /**
479
     * Get yearly edit totals for this user, grouped by namespace.
480
     * @param Project $project The project.
481
     * @param User $user The user.
482
     * @return string[] ['<namespace>' => ['<year>' => 'total', ... ], ... ]
483
     */
484 View Code Duplication
    public function getYearCounts(Project $project, User $user)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
485
    {
486
        $cacheKey = "yearcounts.".$user->getCacheKey();
487
        $this->stopwatch->start($cacheKey, 'XTools');
488
        if ($this->cache->hasItem($cacheKey)) {
489
            return $this->cache->getItem($cacheKey)->get();
490
        }
491
492
        $username = $user->getUsername();
493
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
494
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
495
        $sql = "SELECT "
496
            . "     YEAR(rev_timestamp) AS `year`,"
497
            . "     page_namespace,"
498
            . "     COUNT(rev_id) AS `count` "
499
            . " FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
500
            . " WHERE rev_user_text = :username"
501
            . " GROUP BY YEAR(rev_timestamp), page_namespace "
502
            . " ORDER BY rev_timestamp ASC ";
503
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
504
        $resultQuery->bindParam(":username", $username);
505
        $resultQuery->execute();
506
        $totals = $resultQuery->fetchAll();
507
508
        $cacheItem = $this->cache->getItem($cacheKey);
509
        $cacheItem->set($totals);
510
        $cacheItem->expiresAfter(new DateInterval('P10M'));
511
        $this->cache->save($cacheItem);
512
513
        $this->stopwatch->stop($cacheKey);
514
        return $totals;
515
    }
516
517
    /**
518
     * Get data for the timecard chart, with totals grouped by day and to the nearest two-hours.
519
     * @param Project $project
520
     * @param User $user
521
     * @return string[]
522
     */
523
    public function getTimeCard(Project $project, User $user)
524
    {
525
        $cacheKey = "timecard.".$user->getCacheKey();
526
        $this->stopwatch->start($cacheKey, 'XTools');
527
        if ($this->cache->hasItem($cacheKey)) {
528
            return $this->cache->getItem($cacheKey)->get();
529
        }
530
531
        $username = $user->getUsername();
532
        $hourInterval = 2;
533
        $xCalc = "ROUND(HOUR(rev_timestamp)/$hourInterval) * $hourInterval";
534
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
535
        $sql = "SELECT "
536
            . "     DAYOFWEEK(rev_timestamp) AS `y`, "
537
            . "     $xCalc AS `x`, "
538
            . "     COUNT(rev_id) AS `r` "
539
            . " FROM $revisionTable"
540
            . " WHERE rev_user_text = :username"
541
            . " GROUP BY DAYOFWEEK(rev_timestamp), $xCalc ";
542
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
543
        $resultQuery->bindParam(":username", $username);
544
        $resultQuery->execute();
545
        $totals = $resultQuery->fetchAll();
546
        // Scale the radii: get the max, then scale each radius.
547
        // This looks inefficient, but there's a max of 72 elements in this array.
548
        $max = 0;
549
        foreach ($totals as $total) {
550
            $max = max($max, $total['r']);
551
        }
552
        foreach ($totals as &$total) {
553
            $total['r'] = round($total['r'] / $max * 100);
554
        }
555
        $cacheItem = $this->cache->getItem($cacheKey);
556
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
557
        $cacheItem->set($totals);
558
        $this->cache->save($cacheItem);
559
560
        $this->stopwatch->stop($cacheKey);
561
        return $totals;
562
    }
563
564
    /**
565
     * Get a summary of automated edits made by the given user
566
     * Will cache the result for 10 minutes.
567
     * @param Project $project The project.
568
     * @param User $user The user.
569
     * @return integer[] Array of edit counts, keyed by all tool names from
570
     * app/config/semi_automated.yml
571
     * @todo Load from AutoEditsController via AJAX
572
     */
573
    public function countAutomatedRevisions(Project $project, User $user)
574
    {
575
        $userId = $user->getId($project);
576
        $cacheKey = "automatedEdits.".$project->getDatabaseName().'.'.$userId;
577
        $this->stopwatch->start($cacheKey, 'XTools');
578 View Code Duplication
        if ($this->cache->hasItem($cacheKey)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
579
            $this->log->debug("Using cache for $cacheKey");
580
            return $this->cache->getItem($cacheKey)->get();
581
        }
582
583
        /** @var AutomatedEditsHelper $automatedEditsHelper */
584
        $automatedEditsHelper = $this->container->get("app.automated_edits_helper");
585
586
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
587
        $sql = "SELECT rev_comment FROM $revisionTable WHERE rev_user = :userId";
588
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
589
        $resultQuery->bindParam("userId", $userId);
590
        $resultQuery->execute();
591
        $results = $resultQuery->fetchAll();
592
        $editCounts = [];
593
        foreach ($results as $result) {
594
            $toolName = $automatedEditsHelper->getTool($result['rev_comment']);
595
            if ($toolName) {
596
                if (!isset($editCounts[$toolName])) {
597
                    $editCounts[$toolName] = 0;
598
                }
599
                $editCounts[$toolName]++;
600
            }
601
        }
602
        arsort($editCounts);
603
604
        // Cache for 10 minutes.
605
        $this->log->debug("Saving $cacheKey to cache", [$editCounts]);
606
        $cacheItem = $this->cache->getItem($cacheKey);
607
        $cacheItem->set($editCounts);
608
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
609
        $this->cache->save($cacheItem);
610
611
        $this->stopwatch->stop($cacheKey);
612
        return $editCounts;
613
    }
614
615
    /**
616
     * Get various data about edit sizes of the past 5,000 edits.
617
     * Will cache the result for 10 minutes.
618
     * @param Project $project The project.
619
     * @param User $user The user.
620
     * @return string[] Values with for keys 'average_size',
621
     *                  'small_edits' and 'large_edits'
622
     */
623
    public function getEditSizeData(Project $project, User $user)
624
    {
625
        // Set up cache.
626
        $cacheKey = 'editsizedata.'.$project->getDatabaseName().'.'.$user->getCacheKey();
627
        $this->stopwatch->start($cacheKey, 'XTools');
628
        if ($this->cache->hasItem($cacheKey)) {
629
            return $this->cache->getItem($cacheKey)->get();
630
        }
631
632
        // Prepare the queries and execute them.
633
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
634
        $userId = $user->getId($project);
635
        $sql = "SELECT AVG(sizes.size) AS average_size,
636
                COUNT(CASE WHEN sizes.size < 20 THEN 1 END) AS small_edits,
637
                COUNT(CASE WHEN sizes.size > 1000 THEN 1 END) AS large_edits
638
                FROM (
639
                    SELECT (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS size
640
                    FROM $revisionTable AS revs
641
                    LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
642
                    WHERE revs.rev_user = :userId
643
                    ORDER BY revs.rev_timestamp DESC
644
                    LIMIT 5000
645
                ) sizes";
646
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
647
        $resultQuery->bindParam('userId', $userId);
648
        $resultQuery->execute();
649
        $results = $resultQuery->fetchAll()[0];
650
651
        // Cache for 10 minutes.
652
        $cacheItem = $this->cache->getItem($cacheKey);
653
        $cacheItem->set($results);
654
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
655
        $this->cache->save($cacheItem);
656
657
        $this->stopwatch->stop($cacheKey);
658
        return $results;
659
    }
660
}
661