Passed
Push — master ( 8bf10c...04141c )
by MusikAnimal
06:17
created

EditCounter   F

Complexity

Total Complexity 180

Size/Duplication

Total Lines 1136
Duplicated Lines 0 %

Test Coverage

Coverage 51.2%

Importance

Changes 0
Metric Value
eloc 353
dl 0
loc 1136
ccs 192
cts 375
cp 0.512
rs 2
c 0
b 0
f 0
wmc 180

65 Methods

Rating   Name   Duplication   Size   Complexity  
A countMinorRevisions() 0 4 2
A countBlocksSet() 0 5 2
A getFirstAndLatestActions() 0 9 2
C getLongestBlockSeconds() 0 70 15
A countDeletedPagesEdited() 0 4 2
A getBlocks() 0 17 4
A countAllPagesEdited() 0 3 1
A countUnblocksSet() 0 4 2
A countPagesMoved() 0 4 2
A countPagesCreatedDeleted() 0 4 2
A getPairData() 0 7 2
A getLogCounts() 0 7 2
A countReblocksSet() 0 5 2
A countLiveRevisions() 0 4 2
A countAllRevisions() 0 3 1
A __construct() 0 5 1
A countLivePagesEdited() 0 4 2
A countRevisionsWithoutComments() 0 3 1
A countDeletedRevisions() 0 4 2
A countCreatedPagesLive() 0 4 2
A countBlocksReceived() 0 4 1
A countBlocksLifted() 0 4 2
A countPagesCreated() 0 3 1
A countPagesDeleted() 0 4 2
A countRevisionsWithComments() 0 4 2
A countFilesUploaded() 0 4 2
A namespaceTotals() 0 9 2
A monthTotals() 0 17 5
A countAutomatedEdits() 0 10 2
A globalEditCount() 0 7 2
B timeCard() 0 39 8
A monthCounts() 0 45 5
A thanks() 0 4 2
A accountsCreated() 0 6 3
A countPagesImported() 0 7 4
A countSmallEdits() 0 4 2
A fillInMonthCounts() 0 24 5
A averageRevisionsPerDay() 0 6 2
A countRevisionsInLast() 0 4 1
A getDays() 0 16 6
A countLogsDeleted() 0 4 2
A globalEditCounts() 0 15 3
A parseBlockLogEntry() 0 29 4
A getEditSizeData() 0 7 2
A yearCountsWithNamespaces() 0 14 3
A averageEditSize() 0 7 2
A fillInMonthTotalsAndLabels() 0 26 6
A globalEditCountWithoutTopN() 0 9 2
A countRightsModified() 0 4 2
A countPagesReprotected() 0 4 2
A approvals() 0 8 4
A yearCounts() 0 16 4
A globalEditCountsTopN() 0 6 1
A yearTotals() 0 14 4
A monthCountsWithNamespaces() 0 17 4
A countPagesRestored() 0 4 2
A averageRevisionsPerPage() 0 6 2
A countLargeEdits() 0 4 2
A countPagesProtected() 0 4 2
A patrols() 0 4 2
A countLast5000() 0 3 2
A countPagesUnprotected() 0 4 2
A countEditsDeleted() 0 4 2
A countFilesUploadedCommons() 0 4 2
B globalEdits() 0 44 7

How to fix   Complexity   

Complex Class

Complex classes like EditCounter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EditCounter, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file contains only the EditCounter class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace AppBundle\Model;
9
10
use AppBundle\Helper\I18nHelper;
11
use DateInterval;
12
use DatePeriod;
13
use DateTime;
14
15
/**
16
 * An EditCounter provides statistics about a user's edits on a project.
17
 */
18
class EditCounter extends UserRights
19
{
20
    /** @var int[] Revision and page counts etc. */
21
    protected $pairData;
22
23
    /** @var string[] The IDs and timestamps of first/latest edit and logged action. */
24
    protected $firstAndLatestActions;
25
26
    /** @var int[] The total page counts. */
27
    protected $pageCounts;
28
29
    /** @var int[] The lot totals. */
30
    protected $logCounts;
31
32
    /** @var mixed[] Total numbers of edits per month */
33
    protected $monthCounts;
34
35
    /** @var mixed[] Total numbers of edits per year */
36
    protected $yearCounts;
37
38
    /** @var int[] Keys are project DB names. */
39
    protected $globalEditCounts;
40
41
    /** @var array Block data, with keys 'set' and 'received'. */
42
    protected $blocks;
43
44
    /** @var integer[] Array keys are namespace IDs, values are the edit counts. */
45
    protected $namespaceTotals;
46
47
    /** @var int Number of semi-automated edits. */
48
    protected $autoEditCount;
49
50
    /** @var string[] Data needed for time card chart. */
51
    protected $timeCardData;
52
53
    /** @var array Most recent revisions across all projects. */
54
    protected $globalEdits;
55
56
    /**
57
     * Revision size data, with keys 'average_size', 'large_edits' and 'small_edits'.
58
     * @var string[] As returned by the DB, unconverted to int or float
59
     */
60
    protected $editSizeData;
61
62
    /**
63
     * Duration of the longest block in seconds; -1 if indefinite,
64
     *   or false if could not be parsed from log params
65
     * @var int|bool
66
     */
67
    protected $longestBlockSeconds;
68
69
    /**
70
     * EditCounter constructor.
71
     * @param Project $project The base project to count edits
72
     * @param User $user
73
     * @param I18nHelper $i18n
74
     */
75 19
    public function __construct(Project $project, User $user, I18nHelper $i18n)
76
    {
77 19
        $this->project = $project;
78 19
        $this->user = $user;
79 19
        $this->i18n = $i18n;
80 19
    }
81
82
    /**
83
     * Get revision and page counts etc.
84
     * @return int[]
85
     */
86 2
    public function getPairData(): array
87
    {
88 2
        if (!is_array($this->pairData)) {
0 ignored issues
show
introduced by
The condition is_array($this->pairData) is always true.
Loading history...
89 2
            $this->pairData = $this->getRepository()
90 2
                ->getPairData($this->project, $this->user);
91
        }
92 2
        return $this->pairData;
93
    }
94
95
    /**
96
     * Get revision dates.
97
     * @return array
98
     */
99
    public function getLogCounts(): array
100
    {
101
        if (!is_array($this->logCounts)) {
0 ignored issues
show
introduced by
The condition is_array($this->logCounts) is always true.
Loading history...
102
            $this->logCounts = $this->getRepository()
103
                ->getLogCounts($this->project, $this->user);
104
        }
105
        return $this->logCounts;
106
    }
107
108
    /**
109
     * Get the IDs and timestamps of the latest edit and logged action.
110
     * @return string[] With keys 'rev_first', 'rev_latest', 'log_latest', each with 'id' and 'timestamp'.
111
     */
112 1
    public function getFirstAndLatestActions(): array
113
    {
114 1
        if (!isset($this->firstAndLatestActions)) {
115 1
            $this->firstAndLatestActions = $this->getRepository()->getFirstAndLatestActions(
0 ignored issues
show
Bug introduced by
The method getFirstAndLatestActions() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\EditCounterRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

115
            $this->firstAndLatestActions = $this->getRepository()->/** @scrutinizer ignore-call */ getFirstAndLatestActions(
Loading history...
116 1
                $this->project,
117 1
                $this->user
118
            );
119
        }
120 1
        return $this->firstAndLatestActions;
121
    }
122
123
    /**
124
     * Get block data.
125
     * @param string $type Either 'set', 'received'
126
     * @param bool $blocksOnly Whether to include only blocks, and not reblocks and unblocks.
127
     * @return array
128
     */
129 6
    protected function getBlocks(string $type, bool $blocksOnly = true): array
130
    {
131 6
        if (isset($this->blocks[$type]) && is_array($this->blocks[$type])) {
132
            return $this->blocks[$type];
133
        }
134 6
        $method = "getBlocks".ucfirst($type);
135 6
        $blocks = $this->getRepository()->$method($this->project, $this->user);
136 6
        $this->blocks[$type] = $blocks;
137
138
        // Filter out unblocks unless requested.
139 6
        if ($blocksOnly) {
140
            $blocks = array_filter($blocks, function ($block) {
141
                return 'block' === $block['log_action'];
142
            });
143
        }
144
145 6
        return $blocks;
146
    }
147
148
    /**
149
     * Get the total number of currently-live revisions.
150
     * @return int
151
     */
152 1
    public function countLiveRevisions(): int
153
    {
154 1
        $revCounts = $this->getPairData();
155 1
        return isset($revCounts['live']) ? (int)$revCounts['live'] : 0;
156
    }
157
158
    /**
159
     * Get the total number of the user's revisions that have been deleted.
160
     * @return int
161
     */
162 1
    public function countDeletedRevisions(): int
163
    {
164 1
        $revCounts = $this->getPairData();
165 1
        return isset($revCounts['deleted']) ? (int)$revCounts['deleted'] : 0;
166
    }
167
168
    /**
169
     * Get the total edit count (live + deleted).
170
     * @return int
171
     */
172 1
    public function countAllRevisions(): int
173
    {
174 1
        return $this->countLiveRevisions() + $this->countDeletedRevisions();
175
    }
176
177
    /**
178
     * Get the total number of live revisions with comments.
179
     * @return int
180
     */
181 1
    public function countRevisionsWithComments(): int
182
    {
183 1
        $revCounts = $this->getPairData();
184 1
        return isset($revCounts['with_comments']) ? (int)$revCounts['with_comments'] : 0;
185
    }
186
187
    /**
188
     * Get the total number of live revisions without comments.
189
     * @return int
190
     */
191 1
    public function countRevisionsWithoutComments(): int
192
    {
193 1
        return $this->countLiveRevisions() - $this->countRevisionsWithComments();
194
    }
195
196
    /**
197
     * Get the total number of revisions marked as 'minor' by the user.
198
     * @return int
199
     */
200
    public function countMinorRevisions(): int
201
    {
202
        $revCounts = $this->getPairData();
203
        return isset($revCounts['minor']) ? (int)$revCounts['minor'] : 0;
204
    }
205
206
    /**
207
     * Get the total number of non-deleted pages edited by the user.
208
     * @return int
209
     */
210 1
    public function countLivePagesEdited(): int
211
    {
212 1
        $pageCounts = $this->getPairData();
213 1
        return isset($pageCounts['edited-live']) ? (int)$pageCounts['edited-live'] : 0;
214
    }
215
216
    /**
217
     * Get the total number of deleted pages ever edited by the user.
218
     * @return int
219
     */
220 1
    public function countDeletedPagesEdited(): int
221
    {
222 1
        $pageCounts = $this->getPairData();
223 1
        return isset($pageCounts['edited-deleted']) ? (int)$pageCounts['edited-deleted'] : 0;
224
    }
225
226
    /**
227
     * Get the total number of pages ever edited by this user (both live and deleted).
228
     * @return int
229
     */
230 1
    public function countAllPagesEdited(): int
231
    {
232 1
        return $this->countLivePagesEdited() + $this->countDeletedPagesEdited();
233
    }
234
235
    /**
236
     * Get the total number of pages (both still live and those that have been deleted) created
237
     * by the user.
238
     * @return int
239
     */
240 1
    public function countPagesCreated(): int
241
    {
242 1
        return $this->countCreatedPagesLive() + $this->countPagesCreatedDeleted();
243
    }
244
245
    /**
246
     * Get the total number of pages created by the user, that have not been deleted.
247
     * @return int
248
     */
249 1
    public function countCreatedPagesLive(): int
250
    {
251 1
        $pageCounts = $this->getPairData();
252 1
        return isset($pageCounts['created-live']) ? (int)$pageCounts['created-live'] : 0;
253
    }
254
255
    /**
256
     * Get the total number of pages created by the user, that have since been deleted.
257
     * @return int
258
     */
259 1
    public function countPagesCreatedDeleted(): int
260
    {
261 1
        $pageCounts = $this->getPairData();
262 1
        return isset($pageCounts['created-deleted']) ? (int)$pageCounts['created-deleted'] : 0;
263
    }
264
265
    /**
266
     * Get the total number of pages that have been deleted by the user.
267
     * @return int
268
     */
269
    public function countPagesDeleted(): int
270
    {
271
        $logCounts = $this->getLogCounts();
272
        return isset($logCounts['delete-delete']) ? (int)$logCounts['delete-delete'] : 0;
273
    }
274
275
    /**
276
     * Get the total number of pages moved by the user.
277
     * @return int
278
     */
279
    public function countPagesMoved(): int
280
    {
281
        $logCounts = $this->getLogCounts();
282
        return isset($logCounts['move-move']) ? (int)$logCounts['move-move'] : 0;
283
    }
284
285
    /**
286
     * Get the total number of times the user has blocked a user.
287
     * @return int
288
     */
289
    public function countBlocksSet(): int
290
    {
291
        $logCounts = $this->getLogCounts();
292
        $reBlock = isset($logCounts['block-block']) ? (int)$logCounts['block-block'] : 0;
293
        return $reBlock;
294
    }
295
296
    /**
297
     * Get the total number of times the user has re-blocked a user.
298
     * @return int
299
     */
300
    public function countReblocksSet(): int
301
    {
302
        $logCounts = $this->getLogCounts();
303
        $reBlock = isset($logCounts['block-reblock']) ? (int)$logCounts['block-reblock'] : 0;
304
        return $reBlock;
305
    }
306
307
    /**
308
     * Get the total number of times the user has unblocked a user.
309
     * @return int
310
     */
311
    public function countUnblocksSet(): int
312
    {
313
        $logCounts = $this->getLogCounts();
314
        return isset($logCounts['block-unblock']) ? (int)$logCounts['block-unblock'] : 0;
315
    }
316
317
    /**
318
     * Get the total number of blocks that have been lifted (i.e. unblocks) by this user.
319
     * @return int
320
     */
321
    public function countBlocksLifted(): int
322
    {
323
        $logCounts = $this->getLogCounts();
324
        return isset($logCounts['block-unblock']) ? (int)$logCounts['block-unblock'] : 0;
325
    }
326
327
    /**
328
     * Get the total number of times the user has been blocked.
329
     * @return int
330
     */
331
    public function countBlocksReceived(): int
332
    {
333
        $blocks = $this->getBlocks('received');
334
        return count($blocks);
335
    }
336
337
    /**
338
     * Get the length of the longest block the user received, in seconds.
339
     * If the user is blocked, the time since the block is returned. If the block is
340
     * indefinite, -1 is returned. 0 if there was never a block.
341
     * @return int|false Number of seconds or false if it could not be determined.
342
     */
343 6
    public function getLongestBlockSeconds()
344
    {
345 6
        if (isset($this->longestBlockSeconds)) {
346
            return $this->longestBlockSeconds;
347
        }
348
349 6
        $blocks = $this->getBlocks('received', false);
350 6
        $this->longestBlockSeconds = false;
351
352
        // If there was never a block, the longest was zero seconds.
353 6
        if (empty($blocks)) {
354
            return 0;
355
        }
356
357
        /**
358
         * Keep track of the last block so we can determine the duration
359
         * if the current block in the loop is an unblock.
360
         * @var int[] [
361
         *              Unix timestamp,
362
         *              Duration in seconds (-1 if indefinite)
363
         *            ]
364
         */
365 6
        $lastBlock = [null, null];
366
367 6
        foreach (array_values($blocks) as $block) {
368 6
            [$timestamp, $duration] = $this->parseBlockLogEntry($block);
369
370 6
            if ('block' === $block['log_action']) {
371
                // This is a new block, so first see if the duration of the last
372
                // block exceeded our longest duration. -1 duration means indefinite.
373 6
                if ($lastBlock[1] > $this->longestBlockSeconds || -1 === $lastBlock[1]) {
374 2
                    $this->longestBlockSeconds = $lastBlock[1];
375
                }
376
377
                // Now set this as the last block.
378 6
                $lastBlock = [$timestamp, $duration];
379 3
            } elseif ('unblock' === $block['log_action']) {
380
                // The last block was lifted. So the duration will be the time from when the
381
                // last block was set to the time of the unblock.
382 1
                $timeSinceLastBlock = $timestamp - $lastBlock[0];
383 1
                if ($timeSinceLastBlock > $this->longestBlockSeconds) {
384 1
                    $this->longestBlockSeconds = $timeSinceLastBlock;
385
386
                    // Reset the last block, as it has now been accounted for.
387 1
                    $lastBlock = null;
388
                }
389 2
            } elseif ('reblock' === $block['log_action'] && -1 !== $lastBlock[1]) {
390
                // The last block was modified. So we will adjust $lastBlock to include
391
                // the difference of the duration of the new reblock, and time since the last block.
392
                // $lastBlock is left unchanged if its duration was indefinite.
393 1
                $timeSinceLastBlock = $timestamp - $lastBlock[0];
394 6
                $lastBlock[1] = $timeSinceLastBlock + $duration;
395
            }
396
        }
397
398
        // If the last block was indefinite, we'll return that as the longest duration.
399 6
        if (-1 === $lastBlock[1]) {
400 2
            return -1;
401
        }
402
403
        // Test if the last block is still active, and if so use the expiry as the duration.
404 4
        $lastBlockExpiry = $lastBlock[0] + $lastBlock[1];
405 4
        if ($lastBlockExpiry > time() && $lastBlockExpiry > $this->longestBlockSeconds) {
406 1
            $this->longestBlockSeconds = $lastBlock[1];
407
        // Otherwise, test if the duration of the last block is now the longest overall.
408 3
        } elseif ($lastBlock[1] > $this->longestBlockSeconds) {
409 2
            $this->longestBlockSeconds = $lastBlock[1];
410
        }
411
412 4
        return $this->longestBlockSeconds;
413
    }
414
415
    /**
416
     * Given a block log entry from the database, get the timestamp and duration in seconds.
417
     * @param  mixed[] $block Block log entry as fetched via self::getBlocks()
418
     * @return int[] [
419
     *                 Unix timestamp,
420
     *                 Duration in seconds (-1 if indefinite, null if unparsable or unblock)
421
     *               ]
422
     */
423 11
    public function parseBlockLogEntry(array $block): array
424
    {
425 11
        $timestamp = strtotime($block['log_timestamp']);
426 11
        $duration = null;
427
428
        // log_params may be null, but we need to treat it like a string.
429 11
        $block['log_params'] = (string)$block['log_params'];
430
431
        // First check if the string is serialized, and if so parse it to get the block duration.
432 11
        if (false !== @unserialize($block['log_params'])) {
433 8
            $parsedParams = unserialize($block['log_params']);
434 8
            $durationStr = $parsedParams['5::duration'] ?? '';
435
        } else {
436
            // Old format, the duration in English + block options separated by new lines.
437 4
            $durationStr = explode("\n", $block['log_params'])[0];
438
        }
439
440 11
        if (in_array($durationStr, ['indefinite', 'infinity', 'infinite'])) {
441 3
            $duration = -1;
442
        }
443
444
        // Make sure $durationStr is valid just in case it is in an older, unpredictable format.
445
        // If invalid, $duration is left as null.
446 11
        if (strtotime($durationStr)) {
447 8
            $expiry = strtotime($durationStr, $timestamp);
448 8
            $duration = $expiry - $timestamp;
449
        }
450
451 11
        return [$timestamp, $duration];
452
    }
453
454
    /**
455
     * Get the total number of pages protected by the user.
456
     * @return int
457
     */
458
    public function countPagesProtected(): int
459
    {
460
        $logCounts = $this->getLogCounts();
461
        return isset($logCounts['protect-protect']) ? (int)$logCounts['protect-protect'] : 0;
462
    }
463
464
    /**
465
     * Get the total number of pages reprotected by the user.
466
     * @return int
467
     */
468
    public function countPagesReprotected(): int
469
    {
470
        $logCounts = $this->getLogCounts();
471
        return isset($logCounts['protect-modify']) ? (int)$logCounts['protect-modify'] : 0;
472
    }
473
474
    /**
475
     * Get the total number of pages unprotected by the user.
476
     * @return int
477
     */
478
    public function countPagesUnprotected(): int
479
    {
480
        $logCounts = $this->getLogCounts();
481
        return isset($logCounts['protect-unprotect']) ? (int)$logCounts['protect-unprotect'] : 0;
482
    }
483
484
    /**
485
     * Get the total number of edits deleted by the user.
486
     * @return int
487
     */
488
    public function countEditsDeleted(): int
489
    {
490
        $logCounts = $this->getLogCounts();
491
        return isset($logCounts['delete-revision']) ? (int)$logCounts['delete-revision'] : 0;
492
    }
493
494
    /**
495
     * Get the total number of log entries deleted by the user.
496
     * @return int
497
     */
498
    public function countLogsDeleted(): int
499
    {
500
        $revCounts = $this->getLogCounts();
501
        return isset($revCounts['delete-event']) ? (int)$revCounts['delete-event'] : 0;
502
    }
503
504
    /**
505
     * Get the total number of pages restored by the user.
506
     * @return int
507
     */
508
    public function countPagesRestored(): int
509
    {
510
        $logCounts = $this->getLogCounts();
511
        return isset($logCounts['delete-restore']) ? (int)$logCounts['delete-restore'] : 0;
512
    }
513
514
    /**
515
     * Get the total number of times the user has modified the rights of a user.
516
     * @return int
517
     */
518
    public function countRightsModified(): int
519
    {
520
        $logCounts = $this->getLogCounts();
521
        return isset($logCounts['rights-rights']) ? (int)$logCounts['rights-rights'] : 0;
522
    }
523
524
    /**
525
     * Get the total number of pages imported by the user (through any import mechanism:
526
     * interwiki, or XML upload).
527
     * @return int
528
     */
529
    public function countPagesImported(): int
530
    {
531
        $logCounts = $this->getLogCounts();
532
        $import = isset($logCounts['import-import']) ? (int)$logCounts['import-import'] : 0;
533
        $interwiki = isset($logCounts['import-interwiki']) ? (int)$logCounts['import-interwiki'] : 0;
534
        $upload = isset($logCounts['import-upload']) ? (int)$logCounts['import-upload'] : 0;
535
        return $import + $interwiki + $upload;
536
    }
537
538
    /**
539
     * Get the average number of edits per page (including deleted revisions and pages).
540
     * @return float
541
     */
542
    public function averageRevisionsPerPage(): float
543
    {
544
        if (0 == $this->countAllPagesEdited()) {
545
            return 0;
546
        }
547
        return round($this->countAllRevisions() / $this->countAllPagesEdited(), 3);
548
    }
549
550
    /**
551
     * Average number of edits made per day.
552
     * @return float
553
     */
554
    public function averageRevisionsPerDay(): float
555
    {
556
        if (0 == $this->getDays()) {
557
            return 0;
558
        }
559
        return round($this->countAllRevisions() / $this->getDays(), 3);
560
    }
561
562
    /**
563
     * Get the total number of edits made by the user with semi-automating tools.
564
     */
565
    public function countAutomatedEdits(): int
566
    {
567
        if ($this->autoEditCount) {
568
            return $this->autoEditCount;
569
        }
570
        $this->autoEditCount = $this->getRepository()->countAutomatedEdits(
0 ignored issues
show
Bug introduced by
The method countAutomatedEdits() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\AutoEditsRepository or AppBundle\Repository\EditCounterRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

570
        $this->autoEditCount = $this->getRepository()->/** @scrutinizer ignore-call */ countAutomatedEdits(
Loading history...
571
            $this->project,
572
            $this->user
573
        );
574
        return $this->autoEditCount;
575
    }
576
577
    /**
578
     * Get the count of (non-deleted) edits made in the given timeframe to now.
579
     * @param string $time One of 'day', 'week', 'month', or 'year'.
580
     * @return int The total number of live edits.
581
     */
582
    public function countRevisionsInLast(string $time): int
583
    {
584
        $revCounts = $this->getPairData();
585
        return $revCounts[$time] ?? 0;
586
    }
587
588
    /**
589
     * Get the number of days between the first and last edits.
590
     * If there's only one edit, this is counted as one day.
591
     * @return int
592
     */
593 1
    public function getDays(): int
594
    {
595 1
        $first = isset($this->getFirstAndLatestActions()['rev_first']['timestamp'])
596 1
            ? new DateTime($this->getFirstAndLatestActions()['rev_first']['timestamp'])
597 1
            : false;
598 1
        $latest = isset($this->getFirstAndLatestActions()['rev_latest']['timestamp'])
599 1
            ? new DateTime($this->getFirstAndLatestActions()['rev_latest']['timestamp'])
600 1
            : false;
601
602 1
        if (false === $first || false === $latest) {
603
            return 0;
604
        }
605
606 1
        $days = $latest->diff($first)->days;
607
608 1
        return $days > 0 ? $days : 1;
609
    }
610
611
    /**
612
     * Get the total number of files uploaded (including those now deleted).
613
     * @return int
614
     */
615
    public function countFilesUploaded(): int
616
    {
617
        $logCounts = $this->getLogCounts();
618
        return $logCounts['upload-upload'] ?: 0;
619
    }
620
621
    /**
622
     * Get the total number of files uploaded to Commons (including those now deleted).
623
     * This is only applicable for WMF labs installations.
624
     * @return int
625
     */
626
    public function countFilesUploadedCommons(): int
627
    {
628
        $logCounts = $this->getLogCounts();
629
        return $logCounts['files_uploaded_commons'] ?: 0;
630
    }
631
632
    /**
633
     * Get the total number of revisions the user has sent thanks for.
634
     * @return int
635
     */
636
    public function thanks(): int
637
    {
638
        $logCounts = $this->getLogCounts();
639
        return $logCounts['thanks-thank'] ?: 0;
640
    }
641
642
    /**
643
     * Get the total number of approvals
644
     * @return int
645
     */
646
    public function approvals(): int
647
    {
648
        $logCounts = $this->getLogCounts();
649
        $total = $logCounts['review-approve'] +
650
        (!empty($logCounts['review-approve-a']) ? $logCounts['review-approve-a'] : 0) +
651
        (!empty($logCounts['review-approve-i']) ? $logCounts['review-approve-i'] : 0) +
652
        (!empty($logCounts['review-approve-ia']) ? $logCounts['review-approve-ia'] : 0);
653
        return $total;
654
    }
655
656
    /**
657
     * Get the total number of patrols performed by the user.
658
     * @return int
659
     */
660
    public function patrols(): int
661
    {
662
        $logCounts = $this->getLogCounts();
663
        return $logCounts['patrol-patrol'] ?: 0;
664
    }
665
666
    /**
667
     * Get the total number of accounts created by the user.
668
     * @return int
669
     */
670
    public function accountsCreated(): int
671
    {
672
        $logCounts = $this->getLogCounts();
673
        $create2 = $logCounts['newusers-create2'] ?: 0;
674
        $byemail = $logCounts['newusers-byemail'] ?: 0;
675
        return $create2 + $byemail;
676
    }
677
678
    /**
679
     * Get the given user's total edit counts per namespace.
680
     * @return array Array keys are namespace IDs, values are the edit counts.
681
     */
682 1
    public function namespaceTotals(): array
683
    {
684 1
        if ($this->namespaceTotals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->namespaceTotals of type integer[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
685
            return $this->namespaceTotals;
686
        }
687 1
        $counts = $this->getRepository()->getNamespaceTotals($this->project, $this->user);
0 ignored issues
show
Bug introduced by
The method getNamespaceTotals() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\EditCounterRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

687
        $counts = $this->getRepository()->/** @scrutinizer ignore-call */ getNamespaceTotals($this->project, $this->user);
Loading history...
688 1
        arsort($counts);
689 1
        $this->namespaceTotals = $counts;
690 1
        return $counts;
691
    }
692
693
    /**
694
     * Get a summary of the times of day and the days of the week that the user has edited.
695
     * @return string[]
696
     */
697
    public function timeCard(): array
698
    {
699
        if ($this->timeCardData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->timeCardData of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
700
            return $this->timeCardData;
701
        }
702
        $totals = $this->getRepository()->getTimeCard($this->project, $this->user);
0 ignored issues
show
Bug introduced by
The method getTimeCard() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\EditCounterRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

702
        $totals = $this->getRepository()->/** @scrutinizer ignore-call */ getTimeCard($this->project, $this->user);
Loading history...
703
704
        // Scale the radii: get the max, then scale each radius.
705
        // This looks inefficient, but there's a max of 72 elements in this array.
706
        $max = 0;
707
        foreach ($totals as $total) {
708
            $max = max($max, $total['value']);
709
        }
710
        foreach ($totals as &$total) {
711
            $total['value'] = round($total['value'] / $max * 100);
712
        }
713
714
        // Fill in zeros for timeslots that have no values.
715
        $sortedTotals = [];
716
        $index = 0;
717
        $sortedIndex = 0;
718
        foreach (range(1, 7) as $day) {
719
            foreach (range(0, 24, 2) as $hour) {
720
                if (isset($totals[$index]) && (int)$totals[$index]['x'] === $hour) {
721
                    $sortedTotals[$sortedIndex] = $totals[$index];
722
                    $index++;
723
                } else {
724
                    $sortedTotals[$sortedIndex] = [
725
                        'y' => $day,
726
                        'x' => $hour,
727
                        'value' => 0,
728
                    ];
729
                }
730
                $sortedIndex++;
731
            }
732
        }
733
734
        $this->timeCardData = $sortedTotals;
735
        return $sortedTotals;
736
    }
737
738
    /**
739
     * Get the total numbers of edits per month.
740
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING* so we can mock the current DateTime.
741
     * @return mixed[] With keys 'yearLabels', 'monthLabels' and 'totals',
742
     *   the latter keyed by namespace, year and then month.
743
     */
744 2
    public function monthCounts(?DateTime $currentTime = null): array
745
    {
746 2
        if (isset($this->monthCounts)) {
747 1
            return $this->monthCounts;
748
        }
749
750
        // Set to current month if we're not unit-testing
751 2
        if (!($currentTime instanceof DateTime)) {
752
            $currentTime = new DateTime('last day of this month');
753
        }
754
755 2
        $totals = $this->getRepository()->getMonthCounts($this->project, $this->user);
0 ignored issues
show
Bug introduced by
The method getMonthCounts() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\EditCounterRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

755
        $totals = $this->getRepository()->/** @scrutinizer ignore-call */ getMonthCounts($this->project, $this->user);
Loading history...
756
        $out = [
757 2
            'yearLabels' => [],  // labels for years
758
            'monthLabels' => [], // labels for months
759
            'totals' => [], // actual totals, grouped by namespace, year and then month
760
        ];
761
762
        /** @var DateTime $firstEdit Keep track of the date of their first edit. */
763 2
        $firstEdit = new DateTime();
764
765 2
        [$out, $firstEdit] = $this->fillInMonthCounts($out, $totals, $firstEdit);
766
767 2
        $dateRange = new DatePeriod(
768 2
            $firstEdit,
769 2
            new DateInterval('P1M'),
770 2
            $currentTime->modify('first day of this month')
771
        );
772
773 2
        $out = $this->fillInMonthTotalsAndLabels($out, $dateRange);
774
775
        // One more set of loops to sort by year/month
776 2
        foreach (array_keys($out['totals']) as $nsId) {
777 2
            ksort($out['totals'][$nsId]);
778
779 2
            foreach ($out['totals'][$nsId] as &$yearData) {
780 2
                ksort($yearData);
781
            }
782
        }
783
784
        // Finally, sort the namespaces
785 2
        ksort($out['totals']);
786
787 2
        $this->monthCounts = $out;
788 2
        return $out;
789
    }
790
791
    /**
792
     * Get the counts keyed by month and then namespace.
793
     * Basically the opposite of self::monthCounts()['totals'].
794
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING*
795
     *   so we can mock the current DateTime.
796
     * @return array Months as keys, values are counts keyed by namesapce.
797
     * @fixme Create API for this!
798
     */
799 1
    public function monthCountsWithNamespaces(?DateTime $currentTime = null): array
800
    {
801 1
        $countsMonthNamespace = array_fill_keys(
802 1
            array_keys($this->monthTotals($currentTime)),
803 1
            []
804
        );
805
806 1
        foreach ($this->monthCounts($currentTime)['totals'] as $ns => $years) {
807 1
            foreach ($years as $year => $months) {
808 1
                foreach ($months as $month => $count) {
809 1
                    $monthKey = $year.'-'.sprintf('%02d', $month);
810 1
                    $countsMonthNamespace[$monthKey][$ns] = $count;
811
                }
812
            }
813
        }
814
815 1
        return $countsMonthNamespace;
816
    }
817
818
    /**
819
     * Loop through the database results and fill in the values
820
     * for the months that we have data for.
821
     * @param array $out
822
     * @param array $totals
823
     * @param DateTime $firstEdit
824
     * @return array [
825
     *           string[] - Modified $out filled with month stats,
826
     *           DateTime - timestamp of first edit
827
     *         ]
828
     * Tests covered in self::monthCounts().
829
     * @codeCoverageIgnore
830
     */
831
    private function fillInMonthCounts(array $out, array $totals, DateTime $firstEdit): array
832
    {
833
        foreach ($totals as $total) {
834
            // Keep track of first edit
835
            $date = new DateTime($total['year'].'-'.$total['month'].'-01');
836
            if ($date < $firstEdit) {
837
                $firstEdit = $date;
838
            }
839
840
            // Collate the counts by namespace, and then year and month.
841
            $ns = $total['page_namespace'];
842
            if (!isset($out['totals'][$ns])) {
843
                $out['totals'][$ns] = [];
844
            }
845
846
            // Start array for this year if not already present.
847
            if (!isset($out['totals'][$ns][$total['year']])) {
848
                $out['totals'][$ns][$total['year']] = [];
849
            }
850
851
            $out['totals'][$ns][$total['year']][$total['month']] = (int) $total['count'];
852
        }
853
854
        return [$out, $firstEdit];
855
    }
856
857
    /**
858
     * Given the output array, fill each month's totals and labels.
859
     * @param array $out
860
     * @param DatePeriod $dateRange From first edit to present.
861
     * @return array Modified $out filled with month stats.
862
     * Tests covered in self::monthCounts().
863
     * @codeCoverageIgnore
864
     */
865
    private function fillInMonthTotalsAndLabels(array $out, DatePeriod $dateRange): array
866
    {
867
        foreach ($dateRange as $monthObj) {
868
            $year = (int) $monthObj->format('Y');
869
            $yearLabel = $this->i18n->dateFormat($monthObj, 'yyyy');
870
            $month = (int) $monthObj->format('n');
871
            $monthLabel = $this->i18n->dateFormat($monthObj, 'yyyy-MM');
872
873
            // Fill in labels
874
            $out['monthLabels'][] = $monthLabel;
875
            if (!in_array($yearLabel, $out['yearLabels'])) {
876
                $out['yearLabels'][] = $yearLabel;
877
            }
878
879
            foreach (array_keys($out['totals']) as $nsId) {
880
                if (!isset($out['totals'][$nsId][$year])) {
881
                    $out['totals'][$nsId][$year] = [];
882
                }
883
884
                if (!isset($out['totals'][$nsId][$year][$month])) {
885
                    $out['totals'][$nsId][$year][$month] = 0;
886
                }
887
            }
888
        }
889
890
        return $out;
891
    }
892
893
    /**
894
     * Get total edits for each month. Used in wikitext export.
895
     * @param null|DateTime $currentTime *USED ONLY FOR UNIT TESTING*
896
     * @return array With the months as the keys, counts as the values.
897
     */
898 1
    public function monthTotals(?DateTime $currentTime = null): array
899
    {
900 1
        $months = [];
901
902 1
        foreach (array_values($this->monthCounts($currentTime)['totals']) as $nsData) {
903 1
            foreach ($nsData as $year => $monthData) {
904 1
                foreach ($monthData as $month => $count) {
905 1
                    $monthLabel = $year.'-'.sprintf('%02d', $month);
906 1
                    if (!isset($months[$monthLabel])) {
907 1
                        $months[$monthLabel] = 0;
908
                    }
909 1
                    $months[$monthLabel] += $count;
910
                }
911
            }
912
        }
913
914 1
        return $months;
915
    }
916
917
    /**
918
     * Get the total numbers of edits per year.
919
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING* so we can mock the current DateTime.
920
     * @return mixed[] With keys 'yearLabels' and 'totals', the latter keyed by namespace then year.
921
     */
922 2
    public function yearCounts(?DateTime $currentTime = null): array
923
    {
924 2
        if (isset($this->yearCounts)) {
925
            return $this->yearCounts;
926
        }
927
928 2
        $out = $this->monthCounts($currentTime);
929
930 2
        foreach ($out['totals'] as $nsId => $years) {
931 2
            foreach ($years as $year => $months) {
932 2
                $out['totals'][$nsId][$year] = array_sum(array_values($months));
933
            }
934
        }
935
936 2
        $this->yearCounts = $out;
937 2
        return $out;
938
    }
939
940
    /**
941
     * Get the counts keyed by year and then namespace.
942
     * Basically the opposite of self::yearCounts()['totals'].
943
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING*
944
     *   so we can mock the current DateTime.
945
     * @return array Years as keys, values are counts keyed by namesapce.
946
     */
947
    public function yearCountsWithNamespaces(?DateTime $currentTime = null): array
948
    {
949
        $countsYearNamespace = array_fill_keys(
950
            array_keys($this->yearTotals($currentTime)),
951
            []
952
        );
953
954
        foreach ($this->yearCounts($currentTime)['totals'] as $ns => $years) {
955
            foreach ($years as $year => $count) {
956
                $countsYearNamespace[$year][$ns] = $count;
957
            }
958
        }
959
960
        return $countsYearNamespace;
961
    }
962
963
    /**
964
     * Get total edits for each year. Used in wikitext export.
965
     * @param null|DateTime $currentTime *USED ONLY FOR UNIT TESTING*
966
     * @return array With the years as the keys, counts as the values.
967
     */
968 1
    public function yearTotals(?DateTime $currentTime = null): array
969
    {
970 1
        $years = [];
971
972 1
        foreach (array_values($this->yearCounts($currentTime)['totals']) as $nsData) {
973 1
            foreach ($nsData as $year => $count) {
974 1
                if (!isset($years[$year])) {
975 1
                    $years[$year] = 0;
976
                }
977 1
                $years[$year] += $count;
978
            }
979
        }
980
981 1
        return $years;
982
    }
983
984
    /**
985
     * Get the total edit counts for the top n projects of this user.
986
     * @param int $numProjects
987
     * @return mixed[] Each element has 'total' and 'project' keys.
988
     */
989 1
    public function globalEditCountsTopN(int $numProjects = 10): array
990
    {
991
        // Get counts.
992 1
        $editCounts = $this->globalEditCounts(true);
993
        // Truncate, and return.
994 1
        return array_slice($editCounts, 0, $numProjects);
995
    }
996
997
    /**
998
     * Get the total number of edits excluding the top n.
999
     * @param int $numProjects
1000
     * @return int
1001
     */
1002 1
    public function globalEditCountWithoutTopN(int $numProjects = 10): int
1003
    {
1004 1
        $editCounts = $this->globalEditCounts(true);
1005 1
        $bottomM = array_slice($editCounts, $numProjects);
1006 1
        $total = 0;
1007 1
        foreach ($bottomM as $editCount) {
1008 1
            $total += $editCount['total'];
1009
        }
1010 1
        return $total;
1011
    }
1012
1013
    /**
1014
     * Get the grand total of all edits on all projects.
1015
     * @return int
1016
     */
1017 1
    public function globalEditCount(): int
1018
    {
1019 1
        $total = 0;
1020 1
        foreach ($this->globalEditCounts() as $editCount) {
1021 1
            $total += $editCount['total'];
1022
        }
1023 1
        return $total;
1024
    }
1025
1026
    /**
1027
     * Get the total revision counts for all projects for this user.
1028
     * @param bool $sorted Whether to sort the list by total, or not.
1029
     * @return mixed[] Each element has 'total' and 'project' keys.
1030
     */
1031 1
    public function globalEditCounts(bool $sorted = false): array
1032
    {
1033 1
        if (empty($this->globalEditCounts)) {
1034 1
            $this->globalEditCounts = $this->getRepository()
1035 1
                ->globalEditCounts($this->user, $this->project);
0 ignored issues
show
Bug introduced by
The method globalEditCounts() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\EditCounterRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1035
                ->/** @scrutinizer ignore-call */ globalEditCounts($this->user, $this->project);
Loading history...
1036
        }
1037
1038 1
        if ($sorted) {
1039
            // Sort.
1040 1
            uasort($this->globalEditCounts, function ($a, $b) {
1041 1
                return $b['total'] - $a['total'];
1042 1
            });
1043
        }
1044
1045 1
        return $this->globalEditCounts;
1046
    }
1047
1048
    /**
1049
     * Get the most recent n revisions across all projects.
1050
     * @param int $max The maximum number of revisions to return.
1051
     * @param int $offset Offset results by this number of revisions.
1052
     * @return Edit[]
1053
     */
1054
    public function globalEdits(int $max, int $offset = 0): array
1055
    {
1056
        if (is_array($this->globalEdits)) {
0 ignored issues
show
introduced by
The condition is_array($this->globalEdits) is always true.
Loading history...
1057
            return $this->globalEdits;
1058
        }
1059
1060
        // Collect all projects with any edits.
1061
        $projects = [];
1062
        foreach ($this->globalEditCounts() as $editCount) {
1063
            // Don't query revisions if there aren't any.
1064
            if (0 == $editCount['total']) {
1065
                continue;
1066
            }
1067
            $projects[$editCount['project']->getDatabaseName()] = $editCount['project'];
1068
        }
1069
1070
        if (0 === count($projects)) {
1071
            return [];
1072
        }
1073
1074
        // Get all revisions for those projects.
1075
        $globalRevisionsData = $this->getRepository()
1076
            ->getRevisions($projects, $this->user, $max, $offset);
1077
        $globalEdits = [];
1078
        foreach ($globalRevisionsData as $revision) {
1079
            /** @var Project $project */
1080
            $project = $projects[$revision['project_name']];
1081
1082
            $nsName = '';
1083
            if ($revision['page_namespace']) {
1084
                $nsName = $project->getNamespaces()[$revision['page_namespace']];
1085
            }
1086
1087
            $page = $project->getRepository()
1088
                ->getPage($project, $nsName.':'.$revision['page_title']);
1089
            $edit = new Edit($page, $revision);
1090
            $globalEdits[$edit->getTimestamp()->getTimestamp().'-'.$edit->getId()] = $edit;
1091
        }
1092
1093
        // Sort and prune, before adding more.
1094
        krsort($globalEdits);
1095
        $this->globalEdits = array_slice($globalEdits, 0, $max);
1096
1097
        return $this->globalEdits;
1098
    }
1099
1100
    /**
1101
     * Get average edit size, and number of large and small edits.
1102
     * @return int[]
1103
     */
1104
    public function getEditSizeData(): array
1105
    {
1106
        if (!is_array($this->editSizeData)) {
0 ignored issues
show
introduced by
The condition is_array($this->editSizeData) is always true.
Loading history...
1107
            $this->editSizeData = $this->getRepository()
1108
                ->getEditSizeData($this->project, $this->user);
1109
        }
1110
        return $this->editSizeData;
1111
    }
1112
1113
    /**
1114
     * Get the total edit count of this user or 5,000 if they've made more than 5,000 edits.
1115
     * This is used to ensure percentages of small and large edits are computed properly.
1116
     * @return int
1117
     */
1118 1
    public function countLast5000(): int
1119
    {
1120 1
        return $this->countLiveRevisions() > 5000 ? 5000 : $this->countLiveRevisions();
1121
    }
1122
1123
    /**
1124
     * Get the number of edits under 20 bytes of the user's past 5000 edits.
1125
     * @return int
1126
     */
1127
    public function countSmallEdits(): int
1128
    {
1129
        $editSizeData = $this->getEditSizeData();
1130
        return isset($editSizeData['small_edits']) ? (int) $editSizeData['small_edits'] : 0;
1131
    }
1132
1133
    /**
1134
     * Get the total number of edits over 1000 bytes of the user's past 5000 edits.
1135
     * @return int
1136
     */
1137
    public function countLargeEdits(): int
1138
    {
1139
        $editSizeData = $this->getEditSizeData();
1140
        return isset($editSizeData['large_edits']) ? (int) $editSizeData['large_edits'] : 0;
1141
    }
1142
1143
    /**
1144
     * Get the average size of the user's past 5000 edits.
1145
     * @return float Size in bytes.
1146
     */
1147
    public function averageEditSize(): float
1148
    {
1149
        $editSizeData = $this->getEditSizeData();
1150
        if (isset($editSizeData['average_size'])) {
1151
            return round($editSizeData['average_size'], 3);
0 ignored issues
show
Bug introduced by
$editSizeData['average_size'] of type string is incompatible with the type double expected by parameter $val of round(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1151
            return round(/** @scrutinizer ignore-type */ $editSizeData['average_size'], 3);
Loading history...
1152
        } else {
1153
            return 0;
1154
        }
1155
    }
1156
}
1157