Passed
Push — master ( ed4035...f2dd38 )
by MusikAnimal
10:32
created

EditCounter::averageEditSize()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 0
dl 0
loc 7
ccs 0
cts 5
cp 0
crap 6
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
    /**
61
     * EditCounter constructor.
62
     * @param Project $project The base project to count edits
63
     * @param User $user
64
     * @param I18nHelper $i18n
65
     */
66 18
    public function __construct(Project $project, User $user, I18nHelper $i18n)
67
    {
68 18
        $this->project = $project;
69 18
        $this->user = $user;
70 18
        $this->i18n = $i18n;
71 18
    }
72
73
    /**
74
     * Get revision and page counts etc.
75
     * @return int[]
76
     */
77 2
    public function getPairData(): array
78
    {
79 2
        if (!is_array($this->pairData)) {
0 ignored issues
show
introduced by
The condition is_array($this->pairData) is always true.
Loading history...
80 2
            $this->pairData = $this->getRepository()
81 2
                ->getPairData($this->project, $this->user);
82
        }
83 2
        return $this->pairData;
84
    }
85
86
    /**
87
     * Get revision dates.
88
     * @return array
89
     */
90
    public function getLogCounts(): array
91
    {
92
        if (!is_array($this->logCounts)) {
0 ignored issues
show
introduced by
The condition is_array($this->logCounts) is always true.
Loading history...
93
            $this->logCounts = $this->getRepository()
94
                ->getLogCounts($this->project, $this->user);
95
        }
96
        return $this->logCounts;
97
    }
98
99
    /**
100
     * Get the IDs and timestamps of the latest edit and logged action.
101
     * @return string[] With keys 'rev_first', 'rev_latest', 'log_latest', each with 'id' and 'timestamp'.
102
     */
103 1
    public function getFirstAndLatestActions(): array
104
    {
105 1
        if (!isset($this->firstAndLatestActions)) {
106 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

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

548
        $this->autoEditCount = $this->getRepository()->/** @scrutinizer ignore-call */ countAutomatedEdits(
Loading history...
549
            $this->project,
550
            $this->user
551
        );
552
        return $this->autoEditCount;
553
    }
554
555
    /**
556
     * Get the count of (non-deleted) edits made in the given timeframe to now.
557
     * @param string $time One of 'day', 'week', 'month', or 'year'.
558
     * @return int The total number of live edits.
559
     */
560
    public function countRevisionsInLast(string $time): int
561
    {
562
        $revCounts = $this->getPairData();
563
        return $revCounts[$time] ?? 0;
564
    }
565
566
    /**
567
     * Get the number of days between the first and last edits.
568
     * If there's only one edit, this is counted as one day.
569
     * @return int
570
     */
571 1
    public function getDays(): int
572
    {
573 1
        $first = isset($this->getFirstAndLatestActions()['rev_first']['timestamp'])
574 1
            ? new DateTime($this->getFirstAndLatestActions()['rev_first']['timestamp'])
575 1
            : false;
576 1
        $latest = isset($this->getFirstAndLatestActions()['rev_latest']['timestamp'])
577 1
            ? new DateTime($this->getFirstAndLatestActions()['rev_latest']['timestamp'])
578 1
            : false;
579
580 1
        if (false === $first || false === $latest) {
581
            return 0;
582
        }
583
584 1
        $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...
585
586 1
        return $days > 0 ? $days : 1;
587
    }
588
589
    /**
590
     * Get the total number of files uploaded (including those now deleted).
591
     * @return int
592
     */
593
    public function countFilesUploaded(): int
594
    {
595
        $logCounts = $this->getLogCounts();
596
        return $logCounts['upload-upload'] ?: 0;
597
    }
598
599
    /**
600
     * Get the total number of files uploaded to Commons (including those now deleted).
601
     * This is only applicable for WMF labs installations.
602
     * @return int
603
     */
604
    public function countFilesUploadedCommons(): int
605
    {
606
        $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

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

693
        $counts = $this->getRepository()->/** @scrutinizer ignore-call */ getNamespaceTotals($this->project, $this->user);
Loading history...
694 1
        arsort($counts);
695 1
        $this->namespaceTotals = $counts;
696 1
        return $counts;
697
    }
698
699
    /**
700
     * Get the total number of live edits by summing the namespace totals.
701
     * This is used in the view for namespace totals so we don't unnecessarily run the self::getPairData() query.
702
     * @return int
703
     */
704
    public function liveRevisionsFromNamespaces(): int
705
    {
706
        return array_sum($this->namespaceTotals());
707
    }
708
709
    /**
710
     * Get a summary of the times of day and the days of the week that the user has edited.
711
     * @return string[]
712
     */
713
    public function timeCard(): array
714
    {
715
        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...
716
            return $this->timeCardData;
717
        }
718
        $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

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

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

1051
            return round(/** @scrutinizer ignore-type */ $editSizeData['average_size'], 3);
Loading history...
1052
        } else {
1053
            return 0;
1054
        }
1055
    }
1056
}
1057