Completed
Push — master ( fd3d21...1cb40b )
by Sam
02:55
created

EditCounterRepository::getPairData()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 91
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 91
rs 8.518
cc 3
eloc 36
nc 3
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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->getUsername();
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 'small' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
60
                WHERE rev_user = :userId AND rev_len < 20
61
            ) UNION (
62
            SELECT 'large' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
63
                WHERE rev_user = :userId AND rev_len > 1000
64
            ) UNION (
65
            SELECT 'with_comments' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
66
                WHERE rev_user = :userId AND rev_comment = ''
67
            ) UNION (
68
            SELECT 'minor' AS `key`, COUNT(rev_id) AS val FROM $revisionTable
69
                WHERE rev_user = :userId AND rev_minor_edit = 1
70
            ) UNION (
71
            SELECT 'average_size' AS `key`, AVG(rev_len) AS val FROM $revisionTable
72
                WHERE rev_user = :userId
73
74
            -- Dates.
75
            ) UNION (
76
            SELECT 'first' AS `key`, rev_timestamp AS `val` FROM $revisionTable
77
                WHERE rev_user = :userId ORDER BY rev_timestamp ASC LIMIT 1
78
            ) UNION (
79
            SELECT 'last' AS `key`, rev_timestamp AS `date` FROM $revisionTable
80
                WHERE rev_user = :userId ORDER BY rev_timestamp DESC LIMIT 1
81
82
            -- Page counts.
83
            ) UNION (
84
            SELECT 'edited-live' AS `key`, COUNT(DISTINCT rev_page) AS `val`
85
                FROM $revisionTable
86
                WHERE rev_user = :userId
87
            ) UNION (
88
            SELECT 'edited-deleted' AS `key`, COUNT(DISTINCT ar_page_id) AS `val`
89
                FROM $archiveTable
90
                WHERE ar_user = :userId
91
            ) UNION (
92
            SELECT 'created-live' AS `key`, COUNT(DISTINCT rev_page) AS `val`
93
                FROM $revisionTable
94
                WHERE rev_user = :userId AND rev_parent_id=0
95
            ) UNION (
96
            SELECT 'created-deleted' AS `key`, COUNT(DISTINCT ar_page_id) AS `val`
97
                FROM $archiveTable
98
                WHERE ar_user = :userId AND ar_parent_id=0
99
            )
100
        ";
101
        $resultQuery = $this->getProjectsConnection()->prepare($queries);
102
        $userId = $user->getId($project);
103
        $resultQuery->bindParam("userId", $userId);
104
        $resultQuery->execute();
105
        $revisionCounts = [];
106
        while ($result = $resultQuery->fetch()) {
107
            $revisionCounts[$result['key']] = $result['val'];
108
        }
109
110
        // 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...
111
        $cacheItem = $this->cache->getItem($cacheKey)
112
                ->set($revisionCounts)
113
                ->expiresAfter(new DateInterval('PT10M'));
114
        $this->cache->save($cacheItem);
115
116
        return $revisionCounts;
117
    }
118
119
    /**
120
     * Get log totals for a user.
121
     * @param Project $project The project.
122
     * @param User $user The user.
123
     * @return integer[] Keys are "<log>-<action>" strings, values are counts.
124
     */
125
    public function getLogCounts(Project $project, User $user)
126
    {
127
        // Set up cache.
128
        $cacheKey = 'logcounts.'.$project->getDatabaseName().'.'.$user->getUsername();
129
        if ($this->cache->hasItem($cacheKey)) {
130
            return $this->cache->getItem($cacheKey)->get();
131
        }
132
        $this->stopwatch->start($cacheKey, 'XTools');
133
134
        // Query.
135
        $userNamespaceId = 2;
136
        $loggingTable = $this->getTableName($project->getDatabaseName(), 'logging');
137
        $sql = "
138
        (SELECT CONCAT(log_type, '-', log_action) AS source, COUNT(log_id) AS value
139
            FROM $loggingTable
140
            WHERE log_user = :userId
141
            GROUP BY log_type, log_action
142
        ) UNION
143
        (SELECT 'users-unblocked' AS source, COUNT(DISTINCT log_title) AS value
144
            FROM $loggingTable
145
            WHERE log_user = :userId
146
                AND log_type = 'block'
147
                AND log_action = 'unblock'
148
                AND log_namespace = $userNamespaceId
149
        )";
150
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
151
        $userId = $user->getId($project);
152
        $resultQuery->bindParam('userId', $userId);
153
        $resultQuery->execute();
154
        $results = $resultQuery->fetchAll();
155
        $logCounts = array_combine(
156
            array_map(function ($e) {
157
                return $e['source'];
158
            }, $results),
159
            array_map(function ($e) {
160
                return $e['value'];
161
            }, $results)
162
        );
163
164
        // Make sure there is some value for each of the wanted counts.
165
        $requiredCounts = [
166
            'thanks-thank',
167
            'review-approve',
168
            'patrol-patrol',
169
            'block-block',
170
            'block-reblock',
171
            'block-unblock',
172
            'users-unblocked', // Second query above.
173
            'protect-protect',
174
            'protect-unprotect',
175
            'move-move',
176
            'delete-delete',
177
            'delete-revision',
178
            'delete-restore',
179
            'import-import',
180
            'import-interwiki',
181
            'import-upload',
182
            'upload-upload',
183
            'upload-overwrite',
184
        ];
185
        foreach ($requiredCounts as $req) {
186
            if (!isset($logCounts[$req])) {
187
                $logCounts[$req] = 0;
188
            }
189
        }
190
191
        // Add Commons upload count, if applicable.
192
        $logCounts['files_uploaded_commons'] = 0;
193
        if ($this->isLabs()) {
194
            $sql = "SELECT COUNT(log_id) FROM commonswiki_p.logging_userindex
195
                WHERE log_type = 'upload' AND log_action = 'upload' AND log_user = :userId";
196
            $resultQuery = $this->getProjectsConnection()->prepare($sql);
197
            $resultQuery->bindParam('userId', $userId);
198
            $resultQuery->execute();
199
            $logCounts['files_uploaded_commons'] = $resultQuery->fetchColumn();
200
        }
201
202
        // 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...
203
        $cacheItem = $this->cache->getItem($cacheKey)
204
            ->set($logCounts)
205
            ->expiresAfter(new DateInterval('PT10M'));
206
        $this->cache->save($cacheItem);
207
        $this->stopwatch->stop($cacheKey);
208
209
        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...
210
    }
211
212
    /**
213
     * Get data for all blocks set by the given user.
214
     * @param Project $project
215
     * @param User $user
216
     * @return array
217
     */
218 View Code Duplication
    public function getBlocksSet(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...
219
    {
220
        $ipblocksTable = $this->getTableName($project->getDatabaseName(), 'ipblocks');
221
        $sql = "SELECT * FROM $ipblocksTable WHERE ipb_by = :userId";
222
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
223
        $userId = $user->getId($project);
224
        $resultQuery->bindParam('userId', $userId);
225
        $resultQuery->execute();
226
        return $resultQuery->fetchAll();
227
    }
228
229
    /**
230
     * Get data for all blocks set on the given user.
231
     * @param Project $project
232
     * @param User $user
233
     * @return array
234
     */
235 View Code Duplication
    public function getBlocksReceived(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...
236
    {
237
        $ipblocksTable = $this->getTableName($project->getDatabaseName(), 'ipblocks');
238
        $sql = "SELECT * FROM $ipblocksTable WHERE ipb_user = :userId";
239
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
240
        $userId = $user->getId($project);
241
        $resultQuery->bindParam('userId', $userId);
242
        $resultQuery->execute();
243
        return $resultQuery->fetchAll();
244
    }
245
246
    /**
247
     * Get a user's total edit count on all projects.
248
     * @see EditCounterRepository::globalEditCountsFromCentralAuth()
249
     * @see EditCounterRepository::globalEditCountsFromDatabases()
250
     * @param User $user The user.
251
     * @param Project $project The project to start from.
252
     * @return mixed[] Elements are arrays with 'project' (Project), and 'total' (int).
253
     */
254
    public function globalEditCounts(User $user, Project $project)
255
    {
256
        // Get the edit counts from CentralAuth or database.
257
        $editCounts = $this->globalEditCountsFromCentralAuth($user, $project);
258
        if ($editCounts === false) {
259
            $editCounts = $this->globalEditCountsFromDatabases($user, $project);
260
        }
261
262
        // Pre-populate all projects' metadata, to prevent each project call from fetching it.
263
        $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...
264
265
        // Compile the output.
266
        $out = [];
267
        foreach ($editCounts as $editCount) {
268
            $out[] = [
269
                'project' => ProjectRepository::getProject($editCount['dbname'], $this->container),
270
                'total' => $editCount['total'],
271
            ];
272
        }
273
        return $out;
274
    }
275
276
    /**
277
     * Get a user's total edit count on one or more project.
278
     * Requires the CentralAuth extension to be installed on the project.
279
     *
280
     * @param User $user The user.
281
     * @param Project $project The project to start from.
282
     * @return mixed[] Elements are arrays with 'dbname' (string), and 'total' (int).
283
     */
284
    protected function globalEditCountsFromCentralAuth(User $user, Project $project)
285
    {
286
        $this->log->debug(__METHOD__." Getting global edit counts for ".$user->getUsername());
287
        // Set up cache and stopwatch.
288
        $cacheKey = 'globalRevisionCounts.'.$user->getUsername();
289
        if ($this->cache->hasItem($cacheKey)) {
290
            return $this->cache->getItem($cacheKey)->get();
291
        }
292
        $this->stopwatch->start($cacheKey, 'XTools');
293
294
        // Load all projects, so it doesn't have to request metadata about each one as it goes.
295
        $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...
296
297
        $api = $this->getMediawikiApi($project);
298
        $params = [
299
            'meta' => 'globaluserinfo',
300
            'guiprop' => 'editcount|merged',
301
            'guiuser' => $user->getUsername(),
302
        ];
303
        $query = new SimpleRequest('query', $params);
304
        $result = $api->getRequest($query);
305
        if (!isset($result['query']['globaluserinfo']['merged'])) {
306
            return [];
307
        }
308
        $out = [];
309
        foreach ($result['query']['globaluserinfo']['merged'] as $result) {
310
            $out[] = [
311
                'dbname' => $result['wiki'],
312
                'total' => $result['editcount'],
313
            ];
314
        }
315
316
        // 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...
317
        $cacheItem = $this->cache->getItem($cacheKey)
318
            ->set($out)
319
            ->expiresAfter(new DateInterval('PT10M'));
320
        $this->cache->save($cacheItem);
321
        $this->stopwatch->stop($cacheKey);
322
323
        return $out;
324
    }
325
326
    /**
327
     * Get total edit counts from all projects for this user.
328
     * @see EditCounterRepository::globalEditCountsFromCentralAuth()
329
     * @param User $user The user.
330
     * @param Project $project The project to start from.
331
     * @return mixed[] Elements are arrays with 'dbname' (string), and 'total' (int).
332
     */
333
    protected function globalEditCountsFromDatabases(User $user, Project $project)
334
    {
335
        $stopwatchName = 'globalRevisionCounts.'.$user->getUsername();
336
        $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...
337
        $topEditCounts = [];
338
        foreach ($allProjects as $projectMeta) {
339
            $revisionTableName = $this->getTableName($projectMeta['dbName'], 'revision');
340
            $sql = "SELECT COUNT(rev_id) FROM $revisionTableName WHERE rev_user_text=:username";
341
            $stmt = $this->getProjectsConnection()->prepare($sql);
342
            $stmt->bindParam("username", $user->getUsername());
0 ignored issues
show
Bug introduced by
$user->getUsername() cannot be passed to bindparam() as the parameter $variable expects a reference.
Loading history...
343
            $stmt->execute();
344
            $total = (int)$stmt->fetchColumn();
345
            $topEditCounts[] = [
346
                'dbname' => $projectMeta['dbName'],
347
                'total' => $total,
348
            ];
349
            $this->stopwatch->lap($stopwatchName);
350
        }
351
        return $topEditCounts;
352
    }
353
354
    /**
355
     * Get the given user's total edit counts per namespace on the given project.
356
     * @param Project $project The project.
357
     * @param User $user The user.
358
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
359
     */
360
    public function getNamespaceTotals(Project $project, User $user)
361
    {
362
        // Cache?
363
        $userId = $user->getId($project);
364
        $cacheKey = "ec.namespacetotals.{$project->getDatabaseName()}.$userId";
365
        $this->stopwatch->start($cacheKey, 'XTools');
366
        if ($this->cache->hasItem($cacheKey)) {
367
            return $this->cache->getItem($cacheKey)->get();
368
        }
369
370
        // Query.
371
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
372
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
373
        $sql = "SELECT page_namespace, COUNT(rev_id) AS total
374
            FROM $pageTable p JOIN $revisionTable r ON (r.rev_page = p.page_id)
375
            WHERE r.rev_user = :id
376
            GROUP BY page_namespace";
377
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
378
        $resultQuery->bindParam(":id", $userId);
379
        $resultQuery->execute();
380
        $results = $resultQuery->fetchAll();
381
        $namespaceTotals = array_combine(array_map(function ($e) {
382
            return $e['page_namespace'];
383
        }, $results), array_map(function ($e) {
384
            return $e['total'];
385
        }, $results));
386
387
        // Cache and return.
388
        $cacheItem = $this->cache->getItem($cacheKey);
389
        $cacheItem->set($namespaceTotals);
390
        $cacheItem->expiresAfter(new DateInterval('PT15M'));
391
        $this->cache->save($cacheItem);
392
        $this->stopwatch->stop($cacheKey);
393
        return $namespaceTotals;
394
    }
395
396
    /**
397
     * Get revisions by this user.
398
     * @param Project[] $projects The projects.
399
     * @param User $user The user.
400
     * @param int $lim The maximum number of revisions to fetch from each project.
401
     * @return array|mixed
402
     */
403
    public function getRevisions($projects, User $user, $lim = 40)
404
    {
405
        // Check cache.
406
        $username = $user->getUsername();
407
        $cacheKey = "globalcontribs.$username";
408
        $this->stopwatch->start($cacheKey, 'XTools');
409
        if ($this->cache->hasItem($cacheKey)) {
410
            return $this->cache->getItem($cacheKey)->get();
411
        }
412
413
        // Assemble queries.
414
        $queries = [];
415
        foreach ($projects as $project) {
416
            $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
417
            $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
418
            $sql = "SELECT
419
                    '".$project->getDatabaseName()."' AS project_name,
420
                    revs.rev_id AS id,
421
                    revs.rev_timestamp AS timestamp,
422
                    UNIX_TIMESTAMP(revs.rev_timestamp) AS unix_timestamp,
423
                    revs.rev_minor_edit AS minor,
424
                    revs.rev_deleted AS deleted,
425
                    revs.rev_len AS length,
426
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
427
                    revs.rev_parent_id AS parent_id,
428
                    revs.rev_comment AS comment,
429
                    revs.rev_user_text AS username,
430
                    page.page_title,
431
                    page.page_namespace
432
                FROM $revisionTable AS revs
433
                    JOIN $pageTable AS page ON (rev_page = page_id)
434
                    LEFT JOIN $revisionTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
435
                WHERE revs.rev_user_text = :username
436
                ORDER BY revs.rev_timestamp DESC";
437
            if (is_numeric($lim)) {
438
                $sql .= " LIMIT $lim";
439
            }
440
            $queries[] = $sql;
441
        }
442
        $sql = "(\n" . join("\n) UNION (\n", $queries) . ")\n";
443
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
444
        $resultQuery->bindParam(":username", $username);
445
        $resultQuery->execute();
446
        $revisions = $resultQuery->fetchAll();
447
448
        // Cache this.
449
        $cacheItem = $this->cache->getItem($cacheKey);
450
        $cacheItem->set($revisions);
451
        $cacheItem->expiresAfter(new DateInterval('PT15M'));
452
        $this->cache->save($cacheItem);
453
454
        $this->stopwatch->stop($cacheKey);
455
        return $revisions;
456
    }
457
458
    /**
459
     * Get data for a bar chart of monthly edit totals per namespace.
460
     * @param Project $project The project.
461
     * @param User $user The user.
462
     * @return string[]
463
     */
464 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...
465
    {
466
        $username = $user->getUsername();
467
        $cacheKey = "monthcounts.$username";
468
        $this->stopwatch->start($cacheKey, 'XTools');
469
        if ($this->cache->hasItem($cacheKey)) {
470
            return $this->cache->getItem($cacheKey)->get();
471
        }
472
473
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
474
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
475
        $sql =
476
            "SELECT "
477
            . "     YEAR(rev_timestamp) AS `year`,"
478
            . "     MONTH(rev_timestamp) AS `month`,"
479
            . "     page_namespace,"
480
            . "     COUNT(rev_id) AS `count` "
481
            .  " FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
482
            . " WHERE rev_user_text = :username"
483
            . " GROUP BY YEAR(rev_timestamp), MONTH(rev_timestamp), page_namespace "
484
            . " ORDER BY rev_timestamp DESC";
485
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
486
        $resultQuery->bindParam(":username", $username);
487
        $resultQuery->execute();
488
        $totals = $resultQuery->fetchAll();
489
        
490
        $cacheItem = $this->cache->getItem($cacheKey);
491
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
492
        $cacheItem->set($totals);
493
        $this->cache->save($cacheItem);
494
495
        $this->stopwatch->stop($cacheKey);
496
        return $totals;
497
    }
498
499
    /**
500
     * Get yearly edit totals for this user, grouped by namespace.
501
     * @param Project $project The project.
502
     * @param User $user The user.
503
     * @return string[] ['<namespace>' => ['<year>' => 'total', ... ], ... ]
504
     */
505 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...
506
    {
507
        $username = $user->getUsername();
508
        $cacheKey = "yearcounts.$username";
509
        $this->stopwatch->start($cacheKey, 'XTools');
510
        if ($this->cache->hasItem($cacheKey)) {
511
            return $this->cache->getItem($cacheKey)->get();
512
        }
513
514
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
515
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
516
        $sql = "SELECT "
517
            . "     YEAR(rev_timestamp) AS `year`,"
518
            . "     page_namespace,"
519
            . "     COUNT(rev_id) AS `count` "
520
            . " FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
521
            . " WHERE rev_user_text = :username"
522
            . " GROUP BY YEAR(rev_timestamp), page_namespace "
523
            . " ORDER BY rev_timestamp DESC ";
524
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
525
        $resultQuery->bindParam(":username", $username);
526
        $resultQuery->execute();
527
        $totals = $resultQuery->fetchAll();
528
529
        $cacheItem = $this->cache->getItem($cacheKey);
530
        $cacheItem->set($totals);
531
        $cacheItem->expiresAfter(new DateInterval('P10M'));
532
        $this->cache->save($cacheItem);
533
534
        $this->stopwatch->stop($cacheKey);
535
        return $totals;
536
    }
537
538
    /**
539
     * Get data for the timecard chart, with totals grouped by day and to the nearest two-hours.
540
     * @param Project $project
541
     * @param User $user
542
     * @return string[]
543
     */
544
    public function getTimeCard(Project $project, User $user)
545
    {
546
        $username = $user->getUsername();
547
        $cacheKey = "timecard.".$username;
548
        $this->stopwatch->start($cacheKey, 'XTools');
549
        if ($this->cache->hasItem($cacheKey)) {
550
            return $this->cache->getItem($cacheKey)->get();
551
        }
552
553
        $hourInterval = 2;
554
        $xCalc = "ROUND(HOUR(rev_timestamp)/$hourInterval) * $hourInterval";
555
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
556
        $sql = "SELECT "
557
            . "     DAYOFWEEK(rev_timestamp) AS `y`, "
558
            . "     $xCalc AS `x`, "
559
            . "     COUNT(rev_id) AS `r` "
560
            . " FROM $revisionTable"
561
            . " WHERE rev_user_text = :username"
562
            . " GROUP BY DAYOFWEEK(rev_timestamp), $xCalc ";
563
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
564
        $resultQuery->bindParam(":username", $username);
565
        $resultQuery->execute();
566
        $totals = $resultQuery->fetchAll();
567
        // Scale the radii: get the max, then scale each radius.
568
        // This looks inefficient, but there's a max of 72 elements in this array.
569
        $max = 0;
570
        foreach ($totals as $total) {
571
            $max = max($max, $total['r']);
572
        }
573
        foreach ($totals as &$total) {
574
            $total['r'] = round($total['r'] / $max * 100);
575
        }
576
        $cacheItem = $this->cache->getItem($cacheKey);
577
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
578
        $cacheItem->set($totals);
579
        $this->cache->save($cacheItem);
580
581
        $this->stopwatch->stop($cacheKey);
582
        return $totals;
583
    }
584
585
    /**
586
     * Get a summary of automated edits made by the given user in their last 1000 edits.
587
     * Will cache the result for 10 minutes.
588
     * @param Project $project The project.
589
     * @param User $user The user.
590
     * @return integer[] Array of edit counts, keyed by all tool names from
591
     * app/config/semi_automated.yml
592
     * @TODO This currently uses AutomatedEditsHelper but that could probably be refactored.
593
     */
594
    public function countAutomatedRevisions(Project $project, User $user)
595
    {
596
        $userId = $user->getId($project);
597
        $cacheKey = "automatedEdits.".$project->getDatabaseName().'.'.$userId;
598
        $this->stopwatch->start($cacheKey, 'XTools');
599
        if ($this->cache->hasItem($cacheKey)) {
600
            $this->log->debug("Using cache for $cacheKey");
601
            return $this->cache->getItem($cacheKey)->get();
602
        }
603
604
        /** @var AutomatedEditsHelper $automatedEditsHelper */
605
        $automatedEditsHelper = $this->container->get("app.automated_edits_helper");
606
607
        // Get the most recent 1000 edit summaries.
608
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
609
        $sql = "SELECT rev_comment FROM $revisionTable
610
            WHERE rev_user=:userId ORDER BY rev_timestamp DESC LIMIT 1000";
611
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
612
        $resultQuery->bindParam("userId", $userId);
613
        $resultQuery->execute();
614
        $results = $resultQuery->fetchAll();
615
        $editCounts = [];
616
        foreach ($results as $result) {
617
            $toolName = $automatedEditsHelper->getTool($result['rev_comment']);
618
            if ($toolName) {
619
                if (!isset($editCounts[$toolName])) {
620
                    $editCounts[$toolName] = 0;
621
                }
622
                $editCounts[$toolName]++;
623
            }
624
        }
625
        arsort($editCounts);
626
627
        // Cache for 10 minutes.
628
        $this->log->debug("Saving $cacheKey to cache", [$editCounts]);
629
        $cacheItem = $this->cache->getItem($cacheKey);
630
        $cacheItem->set($editCounts);
631
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
632
        $this->cache->save($cacheItem);
633
634
        $this->stopwatch->stop($cacheKey);
635
        return $editCounts;
636
    }
637
}
638