Passed
Pull Request — main (#415)
by MusikAnimal
04:17
created

EditCounter::getThanksReceived()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 6
ccs 4
cts 5
cp 0.8
crap 2.032
rs 10
c 0
b 0
f 0
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 lot totals. */
27
    protected $logCounts;
28
29
    /** @var mixed[] Total numbers of edits per month */
30
    protected $monthCounts;
31
32
    /** @var mixed[] Total numbers of edits per year */
33
    protected $yearCounts;
34
35
    /** @var array Block data, with keys 'set' and 'received'. */
36
    protected $blocks;
37
38
    /** @var integer[] Array keys are namespace IDs, values are the edit counts. */
39
    protected $namespaceTotals;
40
41
    /** @var int Number of semi-automated edits. */
42
    protected $autoEditCount;
43
44
    /** @var string[] Data needed for time card chart. */
45
    protected $timeCardData;
46
47
    /**
48
     * Revision size data, with keys 'average_size', 'large_edits' and 'small_edits'.
49
     * @var string[] As returned by the DB, unconverted to int or float
50
     */
51
    protected $editSizeData;
52
53
    /**
54
     * Duration of the longest block in seconds; -1 if indefinite,
55
     *   or false if could not be parsed from log params
56
     * @var int|bool
57
     */
58
    protected $longestBlockSeconds;
59
60
    /** @var int Number of times the user has been thanked. */
61
    protected $thanksReceived;
62
63
    /**
64
     * EditCounter constructor.
65
     * @param Project $project The base project to count edits
66 18
     * @param User $user
67
     * @param I18nHelper $i18n
68 18
     */
69 18
    public function __construct(Project $project, User $user, I18nHelper $i18n)
70 18
    {
71 18
        $this->project = $project;
72
        $this->user = $user;
73
        $this->i18n = $i18n;
74
    }
75
76
    /**
77 2
     * Get revision and page counts etc.
78
     * @return int[]
79 2
     */
80 2
    public function getPairData(): array
81 2
    {
82
        if (!is_array($this->pairData)) {
0 ignored issues
show
introduced by
The condition is_array($this->pairData) is always true.
Loading history...
83 2
            $this->pairData = $this->getRepository()
84
                ->getPairData($this->project, $this->user);
85
        }
86
        return $this->pairData;
87
    }
88
89
    /**
90
     * Get revision dates.
91
     * @return array
92
     */
93
    public function getLogCounts(): array
94
    {
95
        if (!is_array($this->logCounts)) {
0 ignored issues
show
introduced by
The condition is_array($this->logCounts) is always true.
Loading history...
96
            $this->logCounts = $this->getRepository()
97
                ->getLogCounts($this->project, $this->user);
98
        }
99
        return $this->logCounts;
100
    }
101
102
    /**
103 1
     * Get the IDs and timestamps of the latest edit and logged action.
104
     * @return string[] With keys 'rev_first', 'rev_latest', 'log_latest', each with 'id' and 'timestamp'.
105 1
     */
106 1
    public function getFirstAndLatestActions(): array
107 1
    {
108 1
        if (!isset($this->firstAndLatestActions)) {
109
            $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

109
            $this->firstAndLatestActions = $this->getRepository()->/** @scrutinizer ignore-call */ getFirstAndLatestActions(
Loading history...
110
                $this->project,
111 1
                $this->user
112
            );
113
        }
114
        return $this->firstAndLatestActions;
115
    }
116
117
    /**
118
     * Get the number of times the user was thanked.
119
     * @return int
120 6
     * @codeCoverageIgnore Simply returns the result of an SQL query.
121
     */
122 6
    public function getThanksReceived(): int
123
    {
124
        if (!isset($this->thanksReceived)) {
125 6
            $this->thanksReceived = $this->getRepository()->getThanksReceived($this->project, $this->user);
0 ignored issues
show
Bug introduced by
The method getThanksReceived() 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

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

564
        $this->autoEditCount = $this->getRepository()->/** @scrutinizer ignore-call */ countAutomatedEdits(
Loading history...
565
            $this->project,
566
            $this->user
567
        );
568
        return $this->autoEditCount;
569
    }
570
571 1
    /**
572
     * Get the count of (non-deleted) edits made in the given timeframe to now.
573 1
     * @param string $time One of 'day', 'week', 'month', or 'year'.
574 1
     * @return int The total number of live edits.
575 1
     */
576 1
    public function countRevisionsInLast(string $time): int
577 1
    {
578 1
        $revCounts = $this->getPairData();
579
        return $revCounts[$time] ?? 0;
580 1
    }
581
582
    /**
583
     * Get the number of days between the first and last edits.
584 1
     * If there's only one edit, this is counted as one day.
585
     * @return int
586 1
     */
587
    public function getDays(): int
588
    {
589
        $first = isset($this->getFirstAndLatestActions()['rev_first']['timestamp'])
590
            ? new DateTime($this->getFirstAndLatestActions()['rev_first']['timestamp'])
591
            : false;
592
        $latest = isset($this->getFirstAndLatestActions()['rev_latest']['timestamp'])
593
            ? new DateTime($this->getFirstAndLatestActions()['rev_latest']['timestamp'])
594
            : false;
595
596
        if (false === $first || false === $latest) {
597
            return 0;
598
        }
599
600
        $days = $latest->diff($first)->days;
0 ignored issues
show
Documentation Bug introduced by
It seems like $latest->diff($first)->days can also be of type boolean. However, the property $days is declared as type false|integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
601
602
        return $days > 0 ? $days : 1;
603
    }
604
605
    /**
606
     * Get the total number of files uploaded (including those now deleted).
607
     * @return int
608
     */
609
    public function countFilesUploaded(): int
610
    {
611
        $logCounts = $this->getLogCounts();
612
        return $logCounts['upload-upload'] ?: 0;
613
    }
614
615
    /**
616
     * Get the total number of files uploaded to Commons (including those now deleted).
617
     * This is only applicable for WMF labs installations.
618
     * @return int
619
     */
620
    public function countFilesUploadedCommons(): int
621
    {
622
        $fileCounts = $this->getRepository()->getFileCounts($this->project, $this->user);
0 ignored issues
show
Bug introduced by
The method getFileCounts() 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

622
        $fileCounts = $this->getRepository()->/** @scrutinizer ignore-call */ getFileCounts($this->project, $this->user);
Loading history...
623
        return $fileCounts['files_uploaded_commons'] ?? 0;
624
    }
625
626
    /**
627
     * Get the total number of files that were renamed (including those now deleted).
628
     */
629
    public function countFilesMoved(): int
630
    {
631
        $fileCounts = $this->getRepository()->getFileCounts($this->project, $this->user);
632
        return $fileCounts['files_moved'] ?? 0;
633
    }
634
635
    /**
636
     * Get the total number of files that were renamed on Commons (including those now deleted).
637
     */
638
    public function countFilesMovedCommons(): int
639
    {
640
        $fileCounts = $this->getRepository()->getFileCounts($this->project, $this->user);
641
        return $fileCounts['files_moved_commons'] ?? 0;
642
    }
643
644
    /**
645
     * Get the total number of revisions the user has sent thanks for.
646
     * @return int
647
     */
648
    public function thanks(): int
649
    {
650
        $logCounts = $this->getLogCounts();
651
        return $logCounts['thanks-thank'] ?: 0;
652
    }
653
654
    /**
655
     * Get the total number of approvals
656
     * @return int
657
     */
658
    public function approvals(): int
659
    {
660
        $logCounts = $this->getLogCounts();
661
        $total = (!empty($logCounts['review-approve']) ? $logCounts['review-approve'] : 0) +
662
            (!empty($logCounts['review-approve2']) ? $logCounts['review-approve2'] : 0) +
663
            (!empty($logCounts['review-approve-i']) ? $logCounts['review-approve-i'] : 0) +
664
            (!empty($logCounts['review-approve2-i']) ? $logCounts['review2-approve-i'] : 0);
665
        return $total;
666
    }
667
668
    /**
669
     * Get the total number of patrols performed by the user.
670
     * @return int
671
     */
672
    public function patrols(): int
673
    {
674
        $logCounts = $this->getLogCounts();
675
        return $logCounts['patrol-patrol'] ?: 0;
676
    }
677
678
    /**
679
     * Get the total number of accounts created by the user.
680
     * @return int
681
     */
682
    public function accountsCreated(): int
683
    {
684
        $logCounts = $this->getLogCounts();
685
        $create2 = $logCounts['newusers-create2'] ?: 0;
686
        $byemail = $logCounts['newusers-byemail'] ?: 0;
687
        return $create2 + $byemail;
688 1
    }
689
690 1
    /**
691
     * Get the number of history merges performed by the user.
692
     * @return int
693 1
     */
694 1
    public function merges(): int
695 1
    {
696 1
        $logCounts = $this->getLogCounts();
697
        return $logCounts['merge-merge'];
698
    }
699
700
    /**
701
     * Get the given user's total edit counts per namespace.
702
     * @return array Array keys are namespace IDs, values are the edit counts.
703
     */
704
    public function namespaceTotals(): array
705
    {
706
        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...
707
            return $this->namespaceTotals;
708
        }
709
        $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

709
        $counts = $this->getRepository()->/** @scrutinizer ignore-call */ getNamespaceTotals($this->project, $this->user);
Loading history...
710
        arsort($counts);
711
        $this->namespaceTotals = $counts;
712
        return $counts;
713
    }
714
715
    /**
716
     * Get the total number of live edits by summing the namespace totals.
717
     * This is used in the view for namespace totals so we don't unnecessarily run the self::getPairData() query.
718
     * @return int
719
     */
720
    public function liveRevisionsFromNamespaces(): int
721
    {
722
        return array_sum($this->namespaceTotals());
723
    }
724
725
    /**
726
     * Get a summary of the times of day and the days of the week that the user has edited.
727
     * @return string[]
728
     */
729
    public function timeCard(): array
730
    {
731
        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...
732
            return $this->timeCardData;
733
        }
734
        $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

734
        $totals = $this->getRepository()->/** @scrutinizer ignore-call */ getTimeCard($this->project, $this->user);
Loading history...
735
736
        // Scale the radii: get the max, then scale each radius.
737
        // This looks inefficient, but there's a max of 72 elements in this array.
738
        $max = 0;
739
        foreach ($totals as $total) {
740
            $max = max($max, $total['value']);
741
        }
742
        foreach ($totals as &$total) {
743
            $total['scale'] = round(($total['value'] / $max) * 20);
744
        }
745
746
        // Fill in zeros for timeslots that have no values.
747
        $sortedTotals = [];
748
        $index = 0;
749
        $sortedIndex = 0;
750
        foreach (range(1, 7) as $day) {
751
            foreach (range(0, 23) as $hour) {
752
                if (isset($totals[$index]) && (int)$totals[$index]['hour'] === $hour) {
753
                    $sortedTotals[$sortedIndex] = $totals[$index];
754
                    $index++;
755
                } else {
756
                    $sortedTotals[$sortedIndex] = [
757
                        'day_of_week' => $day,
758
                        'hour' => $hour,
759
                        'value' => 0,
760 2
                    ];
761
                }
762 2
                $sortedIndex++;
763 1
            }
764
        }
765
766
        $this->timeCardData = $sortedTotals;
767 2
        return $sortedTotals;
768
    }
769
770
    /**
771 2
     * Get the total numbers of edits per month.
772
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING* so we can mock the current DateTime.
773 2
     * @return mixed[] With keys 'yearLabels', 'monthLabels' and 'totals',
774
     *   the latter keyed by namespace, year and then month.
775
     */
776
    public function monthCounts(?DateTime $currentTime = null): array
777
    {
778
        if (isset($this->monthCounts)) {
779 2
            return $this->monthCounts;
780
        }
781 2
782
        // Set to current month if we're not unit-testing
783 2
        if (!($currentTime instanceof DateTime)) {
784 2
            $currentTime = new DateTime('last day of this month');
785 2
        }
786 2
787
        $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

787
        $totals = $this->getRepository()->/** @scrutinizer ignore-call */ getMonthCounts($this->project, $this->user);
Loading history...
788
        $out = [
789 2
            'yearLabels' => [],  // labels for years
790
            'monthLabels' => [], // labels for months
791
            'totals' => [], // actual totals, grouped by namespace, year and then month
792 2
        ];
793 2
794
        /** @var DateTime $firstEdit Keep track of the date of their first edit. */
795 2
        $firstEdit = new DateTime();
796 2
797
        [$out, $firstEdit] = $this->fillInMonthCounts($out, $totals, $firstEdit);
798
799
        $dateRange = new DatePeriod(
800
            $firstEdit,
801 2
            new DateInterval('P1M'),
802
            $currentTime->modify('first day of this month')
803 2
        );
804 2
805
        $out = $this->fillInMonthTotalsAndLabels($out, $dateRange);
806
807
        // One more set of loops to sort by year/month
808
        foreach (array_keys($out['totals']) as $nsId) {
809
            ksort($out['totals'][$nsId]);
810
811
            foreach ($out['totals'][$nsId] as &$yearData) {
812
                ksort($yearData);
813
            }
814
        }
815 1
816
        // Finally, sort the namespaces
817 1
        ksort($out['totals']);
818 1
819 1
        $this->monthCounts = $out;
820
        return $out;
821
    }
822 1
823 1
    /**
824 1
     * Get the counts keyed by month and then namespace.
825 1
     * Basically the opposite of self::monthCounts()['totals'].
826 1
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING*
827
     *   so we can mock the current DateTime.
828
     * @return array Months as keys, values are counts keyed by namesapce.
829
     * @fixme Create API for this!
830
     */
831 1
    public function monthCountsWithNamespaces(?DateTime $currentTime = null): array
832
    {
833
        $countsMonthNamespace = array_fill_keys(
834
            array_keys($this->monthTotals($currentTime)),
835
            []
836
        );
837
838
        foreach ($this->monthCounts($currentTime)['totals'] as $ns => $years) {
839
            foreach ($years as $year => $months) {
840
                foreach ($months as $month => $count) {
841
                    $monthKey = $year.'-'.sprintf('%02d', $month);
842
                    $countsMonthNamespace[$monthKey][$ns] = $count;
843
                }
844
            }
845
        }
846
847
        return $countsMonthNamespace;
848
    }
849
850
    /**
851
     * Loop through the database results and fill in the values
852
     * for the months that we have data for.
853
     * @param array $out
854
     * @param array $totals
855
     * @param DateTime $firstEdit
856
     * @return array [
857
     *           string[] - Modified $out filled with month stats,
858
     *           DateTime - timestamp of first edit
859
     *         ]
860
     * Tests covered in self::monthCounts().
861
     * @codeCoverageIgnore
862
     */
863
    private function fillInMonthCounts(array $out, array $totals, DateTime $firstEdit): array
864
    {
865
        foreach ($totals as $total) {
866
            // Keep track of first edit
867
            $date = new DateTime($total['year'].'-'.$total['month'].'-01');
868
            if ($date < $firstEdit) {
869
                $firstEdit = $date;
870
            }
871
872
            // Collate the counts by namespace, and then year and month.
873
            $ns = $total['page_namespace'];
874
            if (!isset($out['totals'][$ns])) {
875
                $out['totals'][$ns] = [];
876
            }
877
878
            // Start array for this year if not already present.
879
            if (!isset($out['totals'][$ns][$total['year']])) {
880
                $out['totals'][$ns][$total['year']] = [];
881
            }
882
883
            $out['totals'][$ns][$total['year']][$total['month']] = (int) $total['count'];
884
        }
885
886
        return [$out, $firstEdit];
887
    }
888
889
    /**
890
     * Given the output array, fill each month's totals and labels.
891
     * @param array $out
892
     * @param DatePeriod $dateRange From first edit to present.
893
     * @return array Modified $out filled with month stats.
894
     * Tests covered in self::monthCounts().
895
     * @codeCoverageIgnore
896
     */
897
    private function fillInMonthTotalsAndLabels(array $out, DatePeriod $dateRange): array
898
    {
899
        foreach ($dateRange as $monthObj) {
900
            $year = (int) $monthObj->format('Y');
901
            $yearLabel = $this->i18n->dateFormat($monthObj, 'yyyy');
902
            $month = (int) $monthObj->format('n');
903
            $monthLabel = $this->i18n->dateFormat($monthObj, 'yyyy-MM');
904
905
            // Fill in labels
906
            $out['monthLabels'][] = $monthLabel;
907
            if (!in_array($yearLabel, $out['yearLabels'])) {
908
                $out['yearLabels'][] = $yearLabel;
909
            }
910
911
            foreach (array_keys($out['totals']) as $nsId) {
912
                if (!isset($out['totals'][$nsId][$year])) {
913
                    $out['totals'][$nsId][$year] = [];
914 1
                }
915
916 1
                if (!isset($out['totals'][$nsId][$year][$month])) {
917
                    $out['totals'][$nsId][$year][$month] = 0;
918 1
                }
919 1
            }
920 1
        }
921 1
922 1
        return $out;
923 1
    }
924
925 1
    /**
926
     * Get total edits for each month. Used in wikitext export.
927
     * @param null|DateTime $currentTime *USED ONLY FOR UNIT TESTING*
928
     * @return array With the months as the keys, counts as the values.
929
     */
930 1
    public function monthTotals(?DateTime $currentTime = null): array
931
    {
932
        $months = [];
933
934
        foreach (array_values($this->monthCounts($currentTime)['totals']) as $nsData) {
935
            foreach ($nsData as $year => $monthData) {
936
                foreach ($monthData as $month => $count) {
937
                    $monthLabel = $year.'-'.sprintf('%02d', $month);
938 2
                    if (!isset($months[$monthLabel])) {
939
                        $months[$monthLabel] = 0;
940 2
                    }
941
                    $months[$monthLabel] += $count;
942
                }
943
            }
944 2
        }
945
946 2
        return $months;
947 2
    }
948 2
949
    /**
950
     * Get the total numbers of edits per year.
951
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING* so we can mock the current DateTime.
952 2
     * @return mixed[] With keys 'yearLabels' and 'totals', the latter keyed by namespace then year.
953 2
     */
954
    public function yearCounts(?DateTime $currentTime = null): array
955
    {
956
        if (isset($this->yearCounts)) {
957
            return $this->yearCounts;
958
        }
959
960
        $out = $this->monthCounts($currentTime);
961
962
        foreach ($out['totals'] as $nsId => $years) {
963
            foreach ($years as $year => $months) {
964
                $out['totals'][$nsId][$year] = array_sum(array_values($months));
965
            }
966
        }
967
968
        $this->yearCounts = $out;
969
        return $out;
970
    }
971
972
    /**
973
     * Get the counts keyed by year and then namespace.
974
     * Basically the opposite of self::yearCounts()['totals'].
975
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING*
976
     *   so we can mock the current DateTime.
977
     * @return array Years as keys, values are counts keyed by namesapce.
978
     */
979
    public function yearCountsWithNamespaces(?DateTime $currentTime = null): array
980
    {
981
        $countsYearNamespace = array_fill_keys(
982
            array_keys($this->yearTotals($currentTime)),
983
            []
984 1
        );
985
986 1
        foreach ($this->yearCounts($currentTime)['totals'] as $ns => $years) {
987
            foreach ($years as $year => $count) {
988 1
                $countsYearNamespace[$year][$ns] = $count;
989 1
            }
990 1
        }
991 1
992
        return $countsYearNamespace;
993 1
    }
994
995
    /**
996
     * Get total edits for each year. Used in wikitext export.
997 1
     * @param null|DateTime $currentTime *USED ONLY FOR UNIT TESTING*
998
     * @return array With the years as the keys, counts as the values.
999
     */
1000
    public function yearTotals(?DateTime $currentTime = null): array
1001
    {
1002
        $years = [];
1003
1004
        foreach (array_values($this->yearCounts($currentTime)['totals']) as $nsData) {
1005
            foreach ($nsData as $year => $count) {
1006
                if (!isset($years[$year])) {
1007
                    $years[$year] = 0;
1008
                }
1009
                $years[$year] += $count;
1010
            }
1011
        }
1012
1013
        return $years;
1014
    }
1015
1016
    /**
1017
     * Get average edit size, and number of large and small edits.
1018 1
     * @return int[]
1019
     */
1020 1
    public function getEditSizeData(): array
1021
    {
1022
        if (!is_array($this->editSizeData)) {
0 ignored issues
show
introduced by
The condition is_array($this->editSizeData) is always true.
Loading history...
1023
            $this->editSizeData = $this->getRepository()
1024
                ->getEditSizeData($this->project, $this->user);
1025
        }
1026
        return $this->editSizeData;
1027
    }
1028
1029
    /**
1030
     * Get the total edit count of this user or 5,000 if they've made more than 5,000 edits.
1031
     * This is used to ensure percentages of small and large edits are computed properly.
1032
     * @return int
1033
     */
1034
    public function countLast5000(): int
1035
    {
1036
        return $this->countLiveRevisions() > 5000 ? 5000 : $this->countLiveRevisions();
1037
    }
1038
1039
    /**
1040
     * Get the number of edits under 20 bytes of the user's past 5000 edits.
1041
     * @return int
1042
     */
1043
    public function countSmallEdits(): int
1044
    {
1045
        $editSizeData = $this->getEditSizeData();
1046
        return isset($editSizeData['small_edits']) ? (int) $editSizeData['small_edits'] : 0;
1047
    }
1048
1049
    /**
1050
     * Get the total number of edits over 1000 bytes of the user's past 5000 edits.
1051
     * @return int
1052
     */
1053
    public function countLargeEdits(): int
1054
    {
1055
        $editSizeData = $this->getEditSizeData();
1056
        return isset($editSizeData['large_edits']) ? (int) $editSizeData['large_edits'] : 0;
1057
    }
1058
1059
    /**
1060
     * Get the average size of the user's past 5000 edits.
1061
     * @return float Size in bytes.
1062
     */
1063
    public function averageEditSize(): float
1064
    {
1065
        $editSizeData = $this->getEditSizeData();
1066
        if (isset($editSizeData['average_size'])) {
1067
            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|integer expected by parameter $num 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

1067
            return round(/** @scrutinizer ignore-type */ $editSizeData['average_size'], 3);
Loading history...
1068
        } else {
1069
            return 0;
1070
        }
1071
    }
1072
}
1073