Completed
Push — master ( 1a7130...a16686 )
by Sam
03:53
created

globalEditCountsFromCentralAuth()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 40
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 40
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 26
nc 4
nop 2
1
<?php
2
3
namespace Xtools;
4
5
use AppBundle\Helper\AutomatedEditsHelper;
6
use DateInterval;
7
use DateTime;
8
use Mediawiki\Api\SimpleRequest;
9
use Mediawiki\Api\UsageException;
10
use MediaWiki\OAuthClient\Exception;
11
use Symfony\Component\VarDumper\VarDumper;
12
13
class EditCounterRepository extends Repository
14
{
15
16
    /**
17
     * Get revision counts for the given user.
18
     * @param User $user The user.
19
     * @returns string[] With keys: 'deleted', 'live', 'total', 'first', 'last', '24h', '7d', '30d',
20
     * '365d', 'small', 'large', 'with_comments', and 'minor_edits'.
21
     */
22
    public function getRevisionCounts(Project $project, User $user)
23
    {
24
        // Set up cache.
25
        $cacheKey = 'revisioncounts.' . $project->getDatabaseName() . '.' . $user->getUsername();
26
        if ($this->cache->hasItem($cacheKey)) {
27
            return $this->cache->getItem($cacheKey)->get();
28
        }
29
30
        // Prepare the queries and execute them.
31
        $archiveTable = $this->getTableName($project->getDatabaseName(), 'archive');
32
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
33
        $queries = [
34
            'deleted' => "SELECT COUNT(ar_id) FROM $archiveTable
35
                WHERE ar_user = :userId",
36
            'live' => "SELECT COUNT(rev_id) FROM $revisionTable
37
                WHERE rev_user = :userId",
38
            'day' => "SELECT COUNT(rev_id) FROM $revisionTable
39
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 DAY)",
40
            'week' => "SELECT COUNT(rev_id) FROM $revisionTable
41
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 WEEK)",
42
            'month' => "SELECT COUNT(rev_id) FROM $revisionTable
43
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 MONTH)",
44
            'year' => "SELECT COUNT(rev_id) FROM $revisionTable
45
                WHERE rev_user = :userId AND rev_timestamp >= DATE_SUB(NOW(), INTERVAL 1 YEAR)",
46
            'small' => "SELECT COUNT(rev_id) FROM $revisionTable
47
                WHERE rev_user = :userId AND rev_len < 20",
48
            'large' => "SELECT COUNT(rev_id) FROM $revisionTable
49
                WHERE rev_user = :userId AND rev_len > 1000",
50
            'with_comments' => "SELECT COUNT(rev_id) FROM $revisionTable
51
                WHERE rev_user = :userId AND rev_comment = ''",
52
            'minor' => "SELECT COUNT(rev_id) FROM $revisionTable
53
                WHERE rev_user = :userId AND rev_minor_edit = 1",
54
            'average_size' => "SELECT AVG(rev_len) FROM $revisionTable
55
                WHERE rev_user = :userId",
56
        ];
57
        $this->stopwatch->start($cacheKey);
58
        $revisionCounts = [];
59
        foreach ($queries as $varName => $query) {
60
            $resultQuery = $this->getProjectsConnection()->prepare($query);
61
            $userId = $user->getId($project);
62
            $resultQuery->bindParam("userId", $userId);
63
            $resultQuery->execute();
64
            $val = $resultQuery->fetchColumn();
65
            $revisionCounts[$varName] = $val ?: 0;
66
            $this->stopwatch->lap($cacheKey);
67
        }
68
69
        // 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...
70
        $this->stopwatch->stop($cacheKey);
71
        $cacheItem = $this->cache->getItem($cacheKey)
72
                ->set($revisionCounts)
73
                ->expiresAfter(new DateInterval('PT10M'));
74
        $this->cache->save($cacheItem);
75
76
        return $revisionCounts;
77
    }
78
79
    /**
80
     * Get the first and last revision dates (in MySQL YYYYMMDDHHMMSS format).
81
     * @return string[] With keys 'first' and 'last'.
82
     */
83
    public function getRevisionDates(Project $project, User $user)
84
    {
85
        // Set up cache.
86
        $cacheKey = 'revisiondates.' . $project->getDatabaseName() . '.' . $user->getUsername();
87
        if ($this->cache->hasItem($cacheKey)) {
88
            return $this->cache->getItem($cacheKey)->get();
89
        }
90
91
        $this->stopwatch->start($cacheKey);
92
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
93
        $query = "(SELECT 'first' AS `key`, rev_timestamp AS `date` FROM $revisionTable
94
            WHERE rev_user = :userId ORDER BY rev_timestamp ASC LIMIT 1)
95
            UNION
96
            (SELECT 'last' AS `key`, rev_timestamp AS `date` FROM $revisionTable
97
            WHERE rev_user = :userId ORDER BY rev_timestamp DESC LIMIT 1)";
98
        $resultQuery = $this->getProjectsConnection()->prepare($query);
99
        $userId = $user->getId($project);
100
        $resultQuery->bindParam("userId", $userId);
101
        $resultQuery->execute();
102
        $result = $resultQuery->fetchAll();
103
        $revisionDates = [];
104
        foreach ($result as $res) {
105
            $revisionDates[$res['key']] = $res['date'];
106
        }
107
108
        // 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...
109
        $cacheItem = $this->cache->getItem($cacheKey)
110
                ->set($revisionDates)
111
                ->expiresAfter(new DateInterval('PT10M'));
112
        $this->cache->save($cacheItem);
113
        $this->stopwatch->stop($cacheKey);
114
        return $revisionDates;
115
    }
116
117
    /**
118
     * Get page counts for the given user, both for live and deleted pages/revisions.
119
     * @param Project $project The project.
120
     * @param User $user The user.
121
     * @return int[] With keys: edited-live, edited-deleted, created-live, created-deleted.
122
     */
123
    public function getPageCounts(Project $project, User $user)
124
    {
125
        // Set up cache.
126
        $cacheKey = 'pagecounts.'.$project->getDatabaseName().'.'.$user->getUsername();
127
        if ($this->cache->hasItem($cacheKey)) {
128
            return $this->cache->getItem($cacheKey)->get();
129
        }
130
        $this->stopwatch->start($cacheKey, 'XTools');
131
132
        // Build and execute query.
133
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
134
        $archiveTable = $this->getTableName($project->getDatabaseName(), 'archive');
135
        $resultQuery = $this->getProjectsConnection()->prepare("
136
            (SELECT 'edited-live' AS source, COUNT(DISTINCT rev_page) AS value
137
                FROM $revisionTable
138
                WHERE rev_user_text=:username)
139
            UNION
140
            (SELECT 'edited-deleted' AS source, COUNT(DISTINCT ar_page_id) AS value
141
                FROM $archiveTable
142
                WHERE ar_user_text=:username)
143
            UNION
144
            (SELECT 'created-live' AS source, COUNT(DISTINCT rev_page) AS value
145
                FROM $revisionTable
146
                WHERE rev_user_text=:username AND rev_parent_id=0)
147
            UNION
148
            (SELECT 'created-deleted' AS source, COUNT(DISTINCT ar_page_id) AS value
149
                FROM $archiveTable
150
                WHERE ar_user_text=:username AND ar_parent_id=0)
151
            ");
152
        $username = $user->getUsername();
153
        $resultQuery->bindParam("username", $username);
154
        $resultQuery->execute();
155
        $results = $resultQuery->fetchAll();
156
157
        $pageCounts = array_combine(
158
            array_map(function ($e) {
159
                return $e['source'];
160
            }, $results),
161
            array_map(function ($e) {
162
                return $e['value'];
163
            }, $results)
164
        );
165
166
        // 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...
167
        $cacheItem = $this->cache->getItem($cacheKey)
168
            ->set($pageCounts)
169
            ->expiresAfter(new DateInterval('PT10M'));
170
        $this->cache->save($cacheItem);
171
        $this->stopwatch->stop($cacheKey);
172
        return $pageCounts;
173
    }
174
175
    /**
176
     * Get log totals for a user.
177
     * @param Project $project The project.
178
     * @param User $user The user.
179
     * @return integer[] Keys are "<log>-<action>" strings, values are counts.
180
     */
181
    public function getLogCounts(Project $project, User $user)
182
    {
183
        // Set up cache.
184
        $cacheKey = 'logcounts.'.$project->getDatabaseName().'.'.$user->getUsername();
185
        if ($this->cache->hasItem($cacheKey)) {
186
            return $this->cache->getItem($cacheKey)->get();
187
        }
188
        $this->stopwatch->start($cacheKey, 'XTools');
189
190
        // Query.
191
        $sql = "SELECT CONCAT(log_type, '-', log_action) AS source, COUNT(log_id) AS value
192
            FROM " . $this->getTableName($project->getDatabaseName(), 'logging') . "
193
            WHERE log_user = :userId
194
            GROUP BY log_type, log_action";
195
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
196
        $userId = $user->getId($project);
197
        $resultQuery->bindParam('userId', $userId);
198
        $resultQuery->execute();
199
        $results = $resultQuery->fetchAll();
200
        $logCounts = array_combine(
201
            array_map(function ($e) {
202
                return $e['source'];
203
            }, $results),
204
            array_map(function ($e) {
205
                return $e['value'];
206
            }, $results)
207
        );
208
209
        // Make sure there is some value for each of the wanted counts.
210
        $requiredCounts = [
211
            'thanks-thank',
212
            'review-approve',
213
            'patrol-patrol',
214
            'block-block',
215
            'block-unblock',
216
            'protect-protect',
217
            'protect-unprotect',
218
            'move-move',
219
            'delete-delete',
220
            'delete-revision',
221
            'delete-restore',
222
            'import-import',
223
            'upload-upload',
224
            'upload-overwrite',
225
        ];
226
        foreach ($requiredCounts as $req) {
227
            if (!isset($logCounts[$req])) {
228
                $logCounts[$req] = 0;
229
            }
230
        }
231
232
        // Add Commons upload count, if applicable.
233
        $logCounts['files_uploaded_commons'] = 0;
234
        if ($this->isLabs()) {
235
            $sql = "SELECT COUNT(log_id) FROM commonswiki_p.logging_userindex
236
                WHERE log_type = 'upload' AND log_action = 'upload' AND log_user = :userId";
237
            $resultQuery = $this->getProjectsConnection()->prepare($sql);
238
            $resultQuery->bindParam('userId', $userId);
239
            $resultQuery->execute();
240
            $logCounts['files_uploaded_commons'] = $resultQuery->fetchColumn();
241
        }
242
243
        // 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...
244
        $cacheItem = $this->cache->getItem($cacheKey)
245
            ->set($logCounts)
246
            ->expiresAfter(new DateInterval('PT10M'));
247
        $this->cache->save($cacheItem);
248
        $this->stopwatch->stop($cacheKey);
249
250
        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...
251
    }
252
253
    /*
254
     * Get a user's total edit count on all projects.
255
     *
256
     * @see EditCounterRepository::globalEditCountsFromCentralAuth()
257
     * @see EditCounterRepository::globalEditCountsFromDatabases()
258
     *
259
     * @param string $username The username.
0 ignored issues
show
Bug introduced by
There is no parameter named $username. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
260
     * @param Project $project The project to start from.
261
     *
262
     * @return mixed[] Elements are arrays with 'project' (Project), and 'total' (int).
263
     */
264
    public function globalEditCounts(User $user, Project $project) {
265
        $editCounts = $this->globalEditCountsFromCentralAuth($user, $project);
266
        if ($editCounts === false) {
267
            $editCounts = $this->globalEditCountsFromDatabases($user, $project);
268
        }
269
        $out = [];
270
        foreach ($editCounts as $editCount) {
271
            $out[] = [
272
                'project' => ProjectRepository::getProject($editCount['dbname'], $this->container),
273
                'total' => $editCount['total'],
274
            ];
275
        }
276
        return $out;
277
    }
278
279
    /**
280
     * Get a user's total edit count on one or more project.
281
     * Requires the CentralAuth extension to be installed on the project.
282
     *
283
     * @param User $user The user.
284
     * @param Project $project The project to start from.
285
     * @return mixed[] Elements are arrays with 'dbname' (string), and 'total' (int).
286
     */
287
    protected function globalEditCountsFromCentralAuth(User $user, Project $project)
288
    {
289
        // Set up cache and stopwatch.
290
        $cacheKey = 'globalRevisionCounts.'.$user->getUsername();
291
        if ($this->cache->hasItem($cacheKey)) {
292
            return $this->cache->getItem($cacheKey)->get();
293
        }
294
        $this->stopwatch->start($cacheKey, 'XTools');
295
296
        // Load all projects, so it doesn't have to request metadata about each one as it goes.
297
        $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...
298
299
        $api = $this->getMediawikiApi($project);
300
        $params = [
301
            'meta' => 'globaluserinfo',
302
            'guiprop' => 'editcount|merged',
303
            'guiuser' => $user->getUsername(),
304
        ];
305
        $query = new SimpleRequest('query', $params);
306
        $result = $api->getRequest($query);
307
        if (!isset($result['query']['globaluserinfo']['merged'])) {
308
            return [];
309
        }
310
        $out = [];
311
        foreach ($result['query']['globaluserinfo']['merged'] as $result) {
312
            $out[] = [
313
                'dbname' => $result['wiki'],
314
                'total' => $result['editcount'],
315
            ];
316
        }
317
318
        // 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...
319
        $cacheItem = $this->cache->getItem($cacheKey)
320
            ->set($out)
321
            ->expiresAfter(new DateInterval('PT10M'));
322
        $this->cache->save($cacheItem);
323
        $this->stopwatch->stop($cacheKey);
324
325
        return $out;
326
    }
327
328
    /**
329
     * Get total edit counts from all projects for this user.
330
     * @see EditCounterRepository::globalEditCountsFromCentralAuth()
331
     * @return mixed[] Elements are arrays with 'dbname' (string), and 'total' (int).
332
     */
333
    protected function globalEditCountsFromDatabases(User $user, Project $project) {
334
        $stopwatchName = 'globalRevisionCounts.'.$user->getUsername();
335
        $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...
336
        $topEditCounts = [];
337
        foreach ($allProjects as $projectMeta) {
338
            $revisionTableName = $this->getTableName($projectMeta['dbName'], 'revision');
339
            $sql = "SELECT COUNT(rev_id) FROM $revisionTableName WHERE rev_user_text=:username";
340
            $stmt = $this->getProjectsConnection()->prepare($sql);
341
            $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...
342
            $stmt->execute();
343
            $total = (int)$stmt->fetchColumn();
344
            $topEditCounts[] = [
345
                'dbname' => $projectMeta['dbName'],
346
                'total' => $total,
347
            ];
348
            $this->stopwatch->lap($stopwatchName);
349
        }
350
        return $topEditCounts;
351
    }
352
353
    /**
354
     * Get the given user's total edit counts per namespace on the given project.
355
     * @param Project $project The project.
356
     * @param User $user The user.
357
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
358
     */
359
    public function getNamespaceTotals(Project $project, User $user)
360
    {
361
        // Cache?
362
        $userId = $user->getId($project);
363
        $cacheKey = "ec.namespacetotals.{$project->getDatabaseName()}.$userId";
364
        $this->stopwatch->start($cacheKey, 'XTools');
365
        if ($this->cache->hasItem($cacheKey)) {
366
            return $this->cache->getItem($cacheKey)->get();
367
        }
368
369
        // Query.
370
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
371
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
372
        $sql = "SELECT page_namespace, COUNT(rev_id) AS total
373
            FROM $revisionTable r JOIN $pageTable p on r.rev_page = p.page_id
374
            WHERE r.rev_user = :id
375
            GROUP BY page_namespace";
376
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
377
        $resultQuery->bindParam(":id", $userId);
378
        $resultQuery->execute();
379
        $results = $resultQuery->fetchAll();
380
        $namespaceTotals = array_combine(array_map(function ($e) {
381
            return $e['page_namespace'];
382
        }, $results), array_map(function ($e) {
383
            return $e['total'];
384
        }, $results));
385
386
        // Cache and return.
387
        $cacheItem = $this->cache->getItem($cacheKey);
388
        $cacheItem->set($namespaceTotals);
389
        $cacheItem->expiresAfter(new DateInterval('PT15M'));
390
        $this->cache->save($cacheItem);
391
        $this->stopwatch->stop($cacheKey);
392
        return $namespaceTotals;
393
    }
394
395
    /**
396
     * Get revisions by this user.
397
     * @param Project $project
398
     * @param User $user
399
     * @param DateTime $oldest
400
     * @return array|mixed
401
     */
402
    public function getRevisions(Project $project, User $user, $oldest = null)
403
    {
404
        $username = $user->getUsername();
405
        $cacheKey = "globalcontribs.{$project->getDatabaseName()}.$username";
406
        $this->stopwatch->start($cacheKey, 'XTools');
407
        if ($this->cache->hasItem($cacheKey)) {
408
            return $this->cache->getItem($cacheKey)->get();
409
        }
410
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
411
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
412
        $whereTimestamp = ($oldest instanceof DateTime)
413
            ? ' AND rev_timestamp > '.$oldest->getTimestamp()
414
            : '';
415
        $sql = "SELECT rev_id, rev_timestamp, UNIX_TIMESTAMP(rev_timestamp) AS unix_timestamp, "
416
            ." rev_minor_edit, rev_deleted, rev_len, rev_parent_id, rev_comment, "
417
            ." page_title, page_namespace "
418
            ."FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
419
            ." WHERE rev_user_text LIKE :username $whereTimestamp"
420
            ." ORDER BY rev_timestamp DESC";
421
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
422
        $resultQuery->bindParam(":username", $username);
423
        $resultQuery->execute();
424
        $revisions = $resultQuery->fetchAll();
425
426
        // Cache this.
427
        $cacheItem = $this->cache->getItem($cacheKey);
428
        $cacheItem->set($revisions);
429
        $cacheItem->expiresAfter(new DateInterval('PT15M'));
430
        $this->cache->save($cacheItem);
431
432
        $this->stopwatch->stop($cacheKey);
433
        return $revisions;
434
    }
435
436
    /**
437
     * Get data for a bar chart of monthly edit totals per namespace.
438
     * @param string $username The username.
0 ignored issues
show
Bug introduced by
There is no parameter named $username. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
439
     * @return string[]
440
     */
441 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...
442
    {
443
        $username = $user->getUsername();
444
        $cacheKey = "monthcounts.$username";
445
        $this->stopwatch->start($cacheKey, 'XTools');
446
        if ($this->cache->hasItem($cacheKey)) {
447
            return $this->cache->getItem($cacheKey)->get();
448
        }
449
450
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
451
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
452
        $sql =
453
            "SELECT "
454
            . "     YEAR(rev_timestamp) AS `year`,"
455
            . "     MONTH(rev_timestamp) AS `month`,"
456
            . "     page_namespace,"
457
            . "     COUNT(rev_id) AS `count` "
458
            .  " FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
459
            . " WHERE rev_user_text = :username"
460
            . " GROUP BY YEAR(rev_timestamp), MONTH(rev_timestamp), page_namespace "
461
            . " ORDER BY rev_timestamp DESC";
462
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
463
        $resultQuery->bindParam(":username", $username);
464
        $resultQuery->execute();
465
        $totals = $resultQuery->fetchAll();
466
        
467
        $cacheItem = $this->cache->getItem($cacheKey);
468
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
469
        $cacheItem->set($totals);
470
        $this->cache->save($cacheItem);
471
472
        $this->stopwatch->stop($cacheKey);
473
        return $totals;
474
    }
475
476
    /**
477
     * Get yearly edit totals for this user, grouped by namespace.
478
     * @param Project $project
479
     * @param User $user
480
     * @return string[] ['<namespace>' => ['<year>' => 'total', ... ], ... ]
481
     */
482 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...
483
    {
484
        $username = $user->getUsername();
485
        $cacheKey = "yearcounts.$username";
486
        $this->stopwatch->start($cacheKey, 'XTools');
487
        if ($this->cache->hasItem($cacheKey)) {
488
            return $this->cache->getItem($cacheKey)->get();
489
        }
490
491
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
492
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
493
        $sql = "SELECT "
494
            . "     YEAR(rev_timestamp) AS `year`,"
495
            . "     page_namespace,"
496
            . "     COUNT(rev_id) AS `count` "
497
            . " FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
498
            . " WHERE rev_user_text = :username"
499
            . " GROUP BY YEAR(rev_timestamp), page_namespace "
500
            . " ORDER BY rev_timestamp DESC ";
501
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
502
        $resultQuery->bindParam(":username", $username);
503
        $resultQuery->execute();
504
        $totals = $resultQuery->fetchAll();
505
506
        $cacheItem = $this->cache->getItem($cacheKey);
507
        $cacheItem->set($totals);
508
        $cacheItem->expiresAfter(new DateInterval('P10M'));
509
        $this->cache->save($cacheItem);
510
511
        $this->stopwatch->stop($cacheKey);
512
        return $totals;
513
    }
514
515
    /**
516
     * Get data for the timecard chart, with totals grouped by day and to the nearest two-hours.
517
     * @param Project $project
518
     * @param User $user
519
     * @return string[]
520
     */
521
    public function getTimeCard(Project $project, User $user)
522
    {
523
        $username = $user->getUsername();
524
        $cacheKey = "timecard.".$username;
525
        $this->stopwatch->start($cacheKey, 'XTools');
526
        if ($this->cache->hasItem($cacheKey)) {
527
            return $this->cache->getItem($cacheKey)->get();
528
        }
529
530
        $hourInterval = 2;
531
        $xCalc = "ROUND(HOUR(rev_timestamp)/$hourInterval) * $hourInterval";
532
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
533
        $sql = "SELECT "
534
            . "     DAYOFWEEK(rev_timestamp) AS `y`, "
535
            . "     $xCalc AS `x`, "
536
            . "     COUNT(rev_id) AS `r` "
537
            . " FROM $revisionTable"
538
            . " WHERE rev_user_text = :username"
539
            . " GROUP BY DAYOFWEEK(rev_timestamp), $xCalc ";
540
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
541
        $resultQuery->bindParam(":username", $username);
542
        $resultQuery->execute();
543
        $totals = $resultQuery->fetchAll();
544
        // Scale the radii: get the max, then scale each radius.
545
        // This looks inefficient, but there's a max of 72 elements in this array.
546
        $max = 0;
547
        foreach ($totals as $total) {
548
            $max = max($max, $total['r']);
549
        }
550
        foreach ($totals as &$total) {
551
            $total['r'] = round($total['r'] / $max * 100);
552
        }
553
        $cacheItem = $this->cache->getItem($cacheKey);
554
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
555
        $cacheItem->set($totals);
556
        $this->cache->save($cacheItem);
557
558
        $this->stopwatch->stop($cacheKey);
559
        return $totals;
560
    }
561
562
    /**
563
     * Get a summary of automated edits made by the given user in their last 1000 edits.
564
     * Will cache the result for 10 minutes.
565
     * @param User $user The user.
566
     * @return integer[] Array of edit counts, keyed by all tool names from
567
     * app/config/semi_automated.yml
568
     * @TODO This currently uses AutomatedEditsHelper but that could probably be refactored.
569
     */
570
    public function countAutomatedRevisions(Project $project, User $user)
571
    {
572
        $userId = $user->getId($project);
573
        $cacheKey = "automatedEdits.".$project->getDatabaseName().'.'.$userId;
574
        $this->stopwatch->start($cacheKey, 'XTools');
575
        if ($this->cache->hasItem($cacheKey)) {
576
            $this->log->debug("Using cache for $cacheKey");
577
            return $this->cache->getItem($cacheKey)->get();
578
        }
579
580
        /** @var AutomatedEditsHelper $automatedEditsHelper */
581
        $automatedEditsHelper = $this->container->get("app.automated_edits_helper");
582
583
        // Get the most recent 1000 edit summaries.
584
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
585
        $sql = "SELECT rev_comment FROM $revisionTable
586
            WHERE rev_user=:userId ORDER BY rev_timestamp DESC LIMIT 1000";
587
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
588
        $resultQuery->bindParam("userId", $userId);
589
        $resultQuery->execute();
590
        $results = $resultQuery->fetchAll();
591
        $editCounts = [];
592
        foreach ($results as $result) {
593
            $toolName = $automatedEditsHelper->getTool($result['rev_comment']);
594
            if ($toolName) {
595
                if (!isset($editCounts[$toolName])) {
596
                    $editCounts[$toolName] = 0;
597
                }
598
                $editCounts[$toolName]++;
599
            }
600
        }
601
        arsort($editCounts);
602
603
        // Cache for 10 minutes.
604
        $this->log->debug("Saving $cacheKey to cache", [$editCounts]);
605
        $cacheItem = $this->cache->getItem($cacheKey);
606
        $cacheItem->set($editCounts);
607
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
608
        $this->cache->save($cacheItem);
609
610
        $this->stopwatch->stop($cacheKey);
611
        return $editCounts;
612
    }
613
}
614