Completed
Push — master ( 61d54e...4a5166 )
by Sam
03:19
created

EditCounterRepository::getRevisions()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 31
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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

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 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('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...
299
    }
300
301
    /**
302
     * Get total edit counts for the top 10 projects for this user.
303
     * @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...
304
     * @return string[] Elements are arrays with 'dbName', 'url', 'name', and 'total'.
305
     */
306
    protected function getRevisionCountsAllProjectsNoCentralAuth(
307
        User $user,
308
        Project $project,
309
        $stopwatchName
310
    ) {
311
        $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...
312
        $topEditCounts = [];
313
        foreach ($allProjects as $projectMeta) {
314
            $revisionTableName = $this->getTableName($projectMeta['dbName'], 'revision');
315
            $sql = "SELECT COUNT(rev_id) FROM $revisionTableName WHERE rev_user_text=:username";
316
            $stmt = $this->getProjectsConnection()->prepare($sql);
317
            $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...
318
            $stmt->execute();
319
            $total = (int)$stmt->fetchColumn();
320
            $project = ProjectRepository::getProject($projectMeta['dbName'], $this->container);
321
            $topEditCounts[] = [
322
                'project' => $project,
323
                'total' => $total,
324
            ];
325
            $this->stopwatch->lap($stopwatchName);
326
        }
327
        return $topEditCounts;
328
    }
329
330
    /**
331
     * Get the given user's total edit counts per namespace on the given project.
332
     * @param Project $project The project.
333
     * @param User $user The user.
334
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
335
     */
336
    public function getNamespaceTotals(Project $project, User $user)
337
    {
338
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
339
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
340
        $sql = "SELECT page_namespace, COUNT(rev_id) AS total
341
            FROM $revisionTable r JOIN $pageTable p on r.rev_page = p.page_id
342
            WHERE r.rev_user = :id
343
            GROUP BY page_namespace";
344
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
345
        $userId = $user->getId($project);
346
        $resultQuery->bindParam(":id", $userId);
347
        $resultQuery->execute();
348
        $results = $resultQuery->fetchAll();
349
        $namespaceTotals = array_combine(array_map(function ($e) {
350
            return $e['page_namespace'];
351
        }, $results), array_map(function ($e) {
352
            return $e['total'];
353
        }, $results));
354
        return $namespaceTotals;
355
    }
356
357
    /**
358
     * Get revisions by this user.
359
     * @param Project $project
360
     * @param User $user
361
     * @param DateTime $oldest
362
     * @return array|mixed
363
     */
364
    public function getRevisions(Project $project, User $user, $oldest = null)
365
    {
366
        $username = $user->getUsername();
367
        $cacheKey = "globalcontribs.{$project->getDatabaseName()}.$username";
368
        if ($this->cache->hasItem($cacheKey)) {
369
            return $this->cache->getItem($cacheKey)->get();
370
        }
371
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
372
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
373
        $whereTimestamp = ($oldest instanceof DateTime)
374
            ? ' AND rev_timestamp > '.$oldest->getTimestamp()
375
            : '';
376
        $sql = "SELECT rev_id, rev_timestamp, UNIX_TIMESTAMP(rev_timestamp) AS unix_timestamp, "
377
            ." rev_minor_edit, rev_deleted, rev_len, rev_parent_id, rev_comment, "
378
            ." page_title, page_namespace "
379
            ."FROM $revisionTable JOIN $pageTable ON (rev_page = page_id)"
380
            ." WHERE rev_user_text LIKE :username $whereTimestamp"
381
            ." ORDER BY rev_timestamp DESC";
382
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
383
        $resultQuery->bindParam(":username", $username);
384
        $resultQuery->execute();
385
        $revisions = $resultQuery->fetchAll();
386
387
        // Cache this.
388
        $cacheItem = $this->cache->getItem($cacheKey);
389
        $cacheItem->set($revisions);
390
        $cacheItem->expiresAfter(new DateInterval('PT15M'));
391
        $this->cache->save($cacheItem);
392
393
        return $revisions;
394
    }
395
396
    /**
397
     * Get this user's most recent 10 edits across all projects.
398
     * @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...
399
     * @param integer $topN The number of items to return.
400
     * @param integer $days The number of days to search from each wiki.
401
     * @return string[]
402
     */
403
    public function getRecentGlobalContribs(User $user, $topN = 10, $days = 30)
404
    {
405
        $defaultProject =
0 ignored issues
show
Unused Code introduced by
$defaultProject 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...
406
        $projects = $this->getRevisionCountsAllProjects($user, $project);
0 ignored issues
show
Bug introduced by
The variable $project seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
407
        $username = $user->getUsername();
408
        $allRevisions = [];
409
        foreach ($projects as $project) {
0 ignored issues
show
Bug introduced by
The expression $projects of type array<integer,*>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
410
            $cacheKey = "globalcontribs.{$project['dbname']}.$username";
411
            if ($this->cache->hasItem($cacheKey)) {
412
                $revisions = $this->cache->getItem($cacheKey)->get();
413
            } else {
414
                $sql =
415
                    "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 " . " FROM " .
418
                    $this->getTableName($project['dbname'], 'revision') . "    JOIN " .
419
                    $this->getTableName($project['dbname'], 'page') .
420
                    "    ON (rev_page = page_id)" .
421
                    " WHERE rev_timestamp > NOW() - INTERVAL $days DAY AND rev_user_text LIKE :username" .
422
                    " ORDER BY rev_timestamp DESC" . " LIMIT 10";
423
                $resultQuery = $this->getProjectsConnection()->prepare($sql);
424
                $resultQuery->bindParam(":username", $username);
425
                $resultQuery->execute();
426
                $revisions = $resultQuery->fetchAll();
427
428
                // Cache this.
429
                $cacheItem = $this->cache->getItem($cacheKey);
430
                $cacheItem->set($revisions);
431
                $cacheItem->expiresAfter(new DateInterval('PT15M'));
432
                $this->cache->save($cacheItem);
433
            }
434
            if (count($revisions) === 0) {
435
                continue;
436
            }
437
            $revsWithProject = array_map(function (&$item) use ($project) {
438
                $item['project_name'] = $project['wikiName'];
439
                $item['project_url'] = $project['url'];
440
                $item['project_db_name'] = $project['dbName'];
441
                $item['rev_time_formatted'] = date('Y-m-d H:i', $item['unix_timestamp']);
442
443
                return $item;
444
            }, $revisions);
445
            $allRevisions = array_merge($allRevisions, $revsWithProject);
446
        }
447
        usort($allRevisions, function ($a, $b) {
448
            return $b['rev_timestamp'] - $a['rev_timestamp'];
449
        });
450
451
        return array_slice($allRevisions, 0, $topN);
452
    }
453
454
    /**
455
     * Get data for a bar chart of monthly edit totals per namespace.
456
     * @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...
457
     * @return string[]
458
     */
459 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...
460
    {
461
        $username = $user->getUsername();
462
        $cacheKey = "monthcounts.$username";
463
        if ($this->cache->hasItem($cacheKey)) {
464
            return $this->cache->getItem($cacheKey)->get();
465
        }
466
467
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
468
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
469
        $sql =
470
            "SELECT " . "     YEAR(rev_timestamp) AS `year`," .
471
            "     MONTH(rev_timestamp) AS `month`," . "     page_namespace," .
472
            "     COUNT(rev_id) AS `count` "
473
            . " FROM $revisionTable    JOIN $pageTable ON (rev_page = page_id)" .
474
            " WHERE rev_user_text = :username" .
475
            " GROUP BY YEAR(rev_timestamp), MONTH(rev_timestamp), page_namespace " .
476
            " ORDER BY rev_timestamp DESC";
477
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
478
        $resultQuery->bindParam(":username", $username);
479
        $resultQuery->execute();
480
        $totals = $resultQuery->fetchAll();
481
        
482
        $cacheItem = $this->cache->getItem($cacheKey);
483
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
484
        $cacheItem->set($totals);
485
        $this->cache->save($cacheItem);
486
487
        return $totals;
488
    }
489
490
    /**
491
     * Get yearly edit totals for this user, grouped by namespace.
492
     * @param string $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...
493
     * @return string[] ['<namespace>' => ['<year>' => 'total', ... ], ... ]
494
     */
495 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...
496
    {
497
        $username = $user->getUsername();
498
        $cacheKey = "yearcounts.$username";
499
        if ($this->cache->hasItem($cacheKey)) {
500
            return $this->cache->getItem($cacheKey)->get();
501
        }
502
503
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
504
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
505
        $sql = "SELECT "
506
            . "     SUBSTR(CAST(rev_timestamp AS CHAR(4)), 1, 4) AS `year`,"
507
            . "     page_namespace,"
508
            . "     COUNT(rev_id) AS `count` "
509
            . " FROM $revisionTable    JOIN $pageTable ON (rev_page = page_id)"
510
            . " WHERE rev_user_text = :username"
511
            . " GROUP BY SUBSTR(CAST(rev_timestamp AS CHAR(4)), 1, 4), page_namespace "
512
            . " ORDER BY rev_timestamp DESC ";
513
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
514
        $resultQuery->bindParam(":username", $username);
515
        $resultQuery->execute();
516
        $totals = $resultQuery->fetchAll();
517
518
        $cacheItem = $this->cache->getItem($cacheKey);
519
        $cacheItem->set($totals);
520
        $cacheItem->expiresAfter(new DateInterval('P10M'));
521
        $this->cache->save($cacheItem);
522
523
        return $totals;
524
    }
525
526
    /**
527
     * Get data for the timecard chart, with totals grouped by day and to the nearest two-hours.
528
     * @param Project $project
529
     * @param User $user
530
     * @return string[]
531
     */
532
    public function getTimeCard(Project $project, User $user)
533
    {
534
        $username = $user->getUsername();
535
        $cacheKey = "timecard.".$username;
536
        if ($this->cache->hasItem($cacheKey)) {
537
            return $this->cache->getItem($cacheKey)->get();
538
        }
539
540
        $hourInterval = 2;
541
        $xCalc = "ROUND(HOUR(rev_timestamp)/$hourInterval) * $hourInterval";
542
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
543
        $sql = "SELECT "
544
            . "     DAYOFWEEK(rev_timestamp) AS `y`, "
545
            . "     $xCalc AS `x`, "
546
            . "     COUNT(rev_id) AS `r` "
547
            . " FROM $revisionTable"
548
            . " WHERE rev_user_text = :username"
549
            . " GROUP BY DAYOFWEEK(rev_timestamp), $xCalc ";
550
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
551
        $resultQuery->bindParam(":username", $username);
552
        $resultQuery->execute();
553
        $totals = $resultQuery->fetchAll();
554
        // Scale the radii: get the max, then scale each radius.
555
        // This looks inefficient, but there's a max of 72 elements in this array.
556
        $max = 0;
557
        foreach ($totals as $total) {
558
            $max = max($max, $total['r']);
559
        }
560
        foreach ($totals as &$total) {
561
            $total['r'] = round($total['r'] / $max * 100);
562
        }
563
        $cacheItem = $this->cache->getItem($cacheKey);
564
        $cacheItem->expiresAfter(new DateInterval('PT10M'));
565
        $cacheItem->set($totals);
566
        $this->cache->save($cacheItem);
567
568
        return $totals;
569
    }
570
571
    /**
572
     * Get a summary of automated edits made by the given user in their last 1000 edits.
573
     * Will cache the result for 10 minutes.
574
     * @param User $user The user.
575
     * @return integer[] Array of edit counts, keyed by all tool names from
576
     * app/config/semi_automated.yml
577
     * @TODO this is broke
578
     */
579
    public function countAutomatedRevisions(Project $project, User $user)
580
    {
581
        $userId = $user->getId($project);
582
        $cacheKey = "automatedEdits.".$project->getDatabaseName().'.'.$userId;
583
        if ($this->cache->hasItem($cacheKey)) {
584
            $this->log->debug("Using cache for $cacheKey");
585
            return $this->cache->getItem($cacheKey)->get();
586
        }
587
588
        // Get the most recent 1000 edit summaries.
589
        $revisionTable = $this->getTableName($project->getDatabaseName(), 'revision');
590
        $sql = "SELECT rev_comment FROM $revisionTable
591
            WHERE rev_user=:userId ORDER BY rev_timestamp DESC LIMIT 1000";
592
        $resultQuery = $this->getProjectsConnection()->prepare($sql);
593
        $resultQuery->bindParam("userId", $userId);
594
        $resultQuery->execute();
595
        $results = $resultQuery->fetchAll();
596
        $out = [];
597
        foreach ($results as $result) {
598
            $toolName = $this->getTool($result['rev_comment']);
0 ignored issues
show
Bug introduced by
The method getTool() does not exist on Xtools\EditCounterRepository. Did you maybe mean getToolsConnection()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
599
            if ($toolName) {
600
                if (!isset($out[$toolName])) {
601
                    $out[$toolName] = 0;
602
                }
603
                $out[$toolName]++;
604
            }
605
        }
606
        arsort($out);
607
608
        // Cache for 10 minutes.
609
        $this->log->debug("Saving $cacheKey to cache", [$out]);
610
        $this->cacheSave($cacheKey, $out, 'PT10M');
0 ignored issues
show
Bug introduced by
The method cacheSave() does not seem to exist on object<Xtools\EditCounterRepository>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
611
612
        return $out;
613
    }
614
}
615