Passed
Push — master ( 09c743...c121a7 )
by MusikAnimal
04:34
created

EditCounter::countLogsDeleted()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 0
dl 0
loc 4
ccs 0
cts 3
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
namespace Xtools;
7
8
use AppBundle\Helper\I18nHelper;
9
use DateInterval;
10
use DatePeriod;
11
use DateTime;
12
13
/**
14
 * An EditCounter provides statistics about a user's edits on a project.
15
 */
16
class EditCounter extends UserRights
17
{
18
    /** @var int[] Revision and page counts etc. */
19
    protected $pairData;
20
21
    /** @var string[] The IDs and timestamps of first/latest edit and logged action. */
22
    protected $firstAndLatestActions;
23
24
    /** @var int[] The total page counts. */
25
    protected $pageCounts;
26
27
    /** @var int[] The lot totals. */
28
    protected $logCounts;
29
30
    /** @var mixed[] Total numbers of edits per month */
31
    protected $monthCounts;
32
33
    /** @var mixed[] Total numbers of edits per year */
34
    protected $yearCounts;
35
36
    /** @var int[] Keys are project DB names. */
37
    protected $globalEditCounts;
38
39
    /** @var array Block data, with keys 'set' and 'received'. */
40
    protected $blocks;
41
42
    /** @var integer[] Array keys are namespace IDs, values are the edit counts. */
43
    protected $namespaceTotals;
44
45
    /** @var int Number of semi-automated edits. */
46
    protected $autoEditCount;
47
48
    /** @var string[] Data needed for time card chart. */
49
    protected $timeCardData;
50
51
    /** @var array Most recent revisions across all projects. */
52
    protected $globalEdits;
53
54
    /**
55
     * Revision size data, with keys 'average_size', 'large_edits' and 'small_edits'.
56
     * @var string[] As returned by the DB, unconverted to int or float
57
     */
58
    protected $editSizeData;
59
60
    /**
61
     * Duration of the longest block in seconds; -1 if indefinite,
62
     *   or false if could not be parsed from log params
63
     * @var int|bool
64
     */
65
    protected $longestBlockSeconds;
66
67
    /**
68
     * EditCounter constructor.
69
     * @param Project $project The base project to count edits
70
     * @param User $user
71
     * @param I18nHelper $i18n
72
     */
73 19
    public function __construct(Project $project, User $user, I18nHelper $i18n)
74
    {
75 19
        $this->project = $project;
76 19
        $this->user = $user;
77 19
        $this->i18n = $i18n;
78 19
    }
79
80
    /**
81
     * Get revision and page counts etc.
82
     * @return int[]
83
     */
84 2
    public function getPairData()
85
    {
86 2
        if (!is_array($this->pairData)) {
0 ignored issues
show
introduced by
The condition is_array($this->pairData) is always true.
Loading history...
87 2
            $this->pairData = $this->getRepository()
88 2
                ->getPairData($this->project, $this->user);
89
        }
90 2
        return $this->pairData;
91
    }
92
93
    /**
94
     * Get revision dates.
95
     * @return int[]
96
     */
97
    public function getLogCounts()
98
    {
99
        if (!is_array($this->logCounts)) {
0 ignored issues
show
introduced by
The condition is_array($this->logCounts) is always true.
Loading history...
100
            $this->logCounts = $this->getRepository()
101
                ->getLogCounts($this->project, $this->user);
102
        }
103
        return $this->logCounts;
104
    }
105
106
    /**
107
     * Get the IDs and timestamps of the latest edit and logged action.
108
     * @return string[] With keys 'rev_first', 'rev_latest', 'log_latest',
109
     *   each with 'id' and 'timestamp'.
110
     */
111 1
    public function getFirstAndLatestActions()
112
    {
113 1
        if (!isset($this->firstAndLatestActions)) {
114 1
            $this->firstAndLatestActions = $this->getRepository()->getFirstAndLatestActions(
115 1
                $this->project,
116 1
                $this->user
117
            );
118
        }
119 1
        return $this->firstAndLatestActions;
120
    }
121
122
    /**
123
     * Get block data.
124
     * @param string $type Either 'set', 'received'
125
     * @param bool $blocksOnly Whether to include only blocks, and not reblocks and unblocks.
126
     * @return array
127
     */
128 6
    protected function getBlocks($type, $blocksOnly = true)
129
    {
130 6
        if (isset($this->blocks[$type]) && is_array($this->blocks[$type])) {
131
            return $this->blocks[$type];
132
        }
133 6
        $method = "getBlocks".ucfirst($type);
134 6
        $blocks = $this->getRepository()->$method($this->project, $this->user);
135 6
        $this->blocks[$type] = $blocks;
136
137
        // Filter out unblocks unless requested.
138 6
        if ($blocksOnly) {
139
            $blocks = array_filter($blocks, function ($block) {
140
                return $block['log_action'] === 'block';
141
            });
142
        }
143
144 6
        return $blocks;
145
    }
146
147
    /**
148
     * Get the total number of currently-live revisions.
149
     * @return int
150
     */
151 1
    public function countLiveRevisions()
152
    {
153 1
        $revCounts = $this->getPairData();
154 1
        return isset($revCounts['live']) ? (int)$revCounts['live'] : 0;
155
    }
156
157
    /**
158
     * Get the total number of the user's revisions that have been deleted.
159
     * @return int
160
     */
161 1
    public function countDeletedRevisions()
162
    {
163 1
        $revCounts = $this->getPairData();
164 1
        return isset($revCounts['deleted']) ? (int)$revCounts['deleted'] : 0;
165
    }
166
167
    /**
168
     * Get the total edit count (live + deleted).
169
     * @return int
170
     */
171 1
    public function countAllRevisions()
172
    {
173 1
        return $this->countLiveRevisions() + $this->countDeletedRevisions();
174
    }
175
176
    /**
177
     * Get the total number of live revisions with comments.
178
     * @return int
179
     */
180 1
    public function countRevisionsWithComments()
181
    {
182 1
        $revCounts = $this->getPairData();
183 1
        return isset($revCounts['with_comments']) ? (int)$revCounts['with_comments'] : 0;
184
    }
185
186
    /**
187
     * Get the total number of live revisions without comments.
188
     * @return int
189
     */
190 1
    public function countRevisionsWithoutComments()
191
    {
192 1
        return $this->countLiveRevisions() - $this->countRevisionsWithComments();
193
    }
194
195
    /**
196
     * Get the total number of revisions marked as 'minor' by the user.
197
     * @return int
198
     */
199
    public function countMinorRevisions()
200
    {
201
        $revCounts = $this->getPairData();
202
        return isset($revCounts['minor']) ? (int)$revCounts['minor'] : 0;
203
    }
204
205
    /**
206
     * Get the total number of non-deleted pages edited by the user.
207
     * @return int
208
     */
209 1
    public function countLivePagesEdited()
210
    {
211 1
        $pageCounts = $this->getPairData();
212 1
        return isset($pageCounts['edited-live']) ? (int)$pageCounts['edited-live'] : 0;
213
    }
214
215
    /**
216
     * Get the total number of deleted pages ever edited by the user.
217
     * @return int
218
     */
219 1
    public function countDeletedPagesEdited()
220
    {
221 1
        $pageCounts = $this->getPairData();
222 1
        return isset($pageCounts['edited-deleted']) ? (int)$pageCounts['edited-deleted'] : 0;
223
    }
224
225
    /**
226
     * Get the total number of pages ever edited by this user (both live and deleted).
227
     * @return int
228
     */
229 1
    public function countAllPagesEdited()
230
    {
231 1
        return $this->countLivePagesEdited() + $this->countDeletedPagesEdited();
232
    }
233
234
    /**
235
     * Get the total number of pages (both still live and those that have been deleted) created
236
     * by the user.
237
     * @return int
238
     */
239 1
    public function countPagesCreated()
240
    {
241 1
        return $this->countCreatedPagesLive() + $this->countPagesCreatedDeleted();
242
    }
243
244
    /**
245
     * Get the total number of pages created by the user, that have not been deleted.
246
     * @return int
247
     */
248 1
    public function countCreatedPagesLive()
249
    {
250 1
        $pageCounts = $this->getPairData();
251 1
        return isset($pageCounts['created-live']) ? (int)$pageCounts['created-live'] : 0;
252
    }
253
254
    /**
255
     * Get the total number of pages created by the user, that have since been deleted.
256
     * @return int
257
     */
258 1
    public function countPagesCreatedDeleted()
259
    {
260 1
        $pageCounts = $this->getPairData();
261 1
        return isset($pageCounts['created-deleted']) ? (int)$pageCounts['created-deleted'] : 0;
262
    }
263
264
    /**
265
     * Get the total number of pages that have been deleted by the user.
266
     * @return int
267
     */
268
    public function countPagesDeleted()
269
    {
270
        $logCounts = $this->getLogCounts();
271
        return isset($logCounts['delete-delete']) ? (int)$logCounts['delete-delete'] : 0;
272
    }
273
274
    /**
275
     * Get the total number of pages moved by the user.
276
     * @return int
277
     */
278
    public function countPagesMoved()
279
    {
280
        $logCounts = $this->getLogCounts();
281
        return isset($logCounts['move-move']) ? (int)$logCounts['move-move'] : 0;
282
    }
283
284
    /**
285
     * Get the total number of times the user has blocked a user.
286
     * @return int
287
     */
288
    public function countBlocksSet()
289
    {
290
        $logCounts = $this->getLogCounts();
291
        $reBlock = isset($logCounts['block-block']) ? (int)$logCounts['block-block'] : 0;
292
        return $reBlock;
293
    }
294
295
    /**
296
     * Get the total number of times the user has re-blocked a user.
297
     * @return int
298
     */
299
    public function countReblocksSet()
300
    {
301
        $logCounts = $this->getLogCounts();
302
        $reBlock = isset($logCounts['block-reblock']) ? (int)$logCounts['block-reblock'] : 0;
303
        return $reBlock;
304
    }
305
306
    /**
307
     * Get the total number of times the user has unblocked a user.
308
     * @return int
309
     */
310
    public function countUnblocksSet()
311
    {
312
        $logCounts = $this->getLogCounts();
313
        return isset($logCounts['block-unblock']) ? (int)$logCounts['block-unblock'] : 0;
314
    }
315
316
    /**
317
     * Get the total number of blocks that have been lifted (i.e. unblocks) by this user.
318
     * @return int
319
     */
320
    public function countBlocksLifted()
321
    {
322
        $logCounts = $this->getLogCounts();
323
        return isset($logCounts['block-unblock']) ? (int)$logCounts['block-unblock'] : 0;
324
    }
325
326
    /**
327
     * Get the total number of times the user has been blocked.
328
     * @return int
329
     */
330
    public function countBlocksReceived()
331
    {
332
        $blocks = $this->getBlocks('received');
333
        return count($blocks);
334
    }
335
336
    /**
337
     * Get the length of the longest block the user received, in seconds.
338
     * @return int Number of seconds or false if it could not be determined.
339
     *   If the user is blocked, the time since the block is returned. If the block is
340
     *   indefinite, -1 is returned. 0 if there was never a block.
341
     */
342 6
    public function getLongestBlockSeconds()
343
    {
344 6
        if (isset($this->longestBlockSeconds)) {
345
            return $this->longestBlockSeconds;
346
        }
347
348 6
        $blocks = $this->getBlocks('received', false);
349 6
        $this->longestBlockSeconds = false;
350
351
        // If there was never a block, the longest was zero seconds.
352 6
        if (empty($blocks)) {
353
            return 0;
354
        }
355
356
        /**
357
         * Keep track of the last block so we can determine the duration
358
         * if the current block in the loop is an unblock.
359
         * @var int[] [
360
         *              Unix timestamp,
361
         *              Duration in seconds (-1 if indefinite)
362
         *            ]
363
         */
364 6
        $lastBlock = [null, null];
365
366 6
        foreach ($blocks as $index => $block) {
367 6
            list($timestamp, $duration) = $this->parseBlockLogEntry($block);
368
369 6
            if ($block['log_action'] === 'block') {
370
                // This is a new block, so first see if the duration of the last
371
                // block exceeded our longest duration. -1 duration means indefinite.
372 6
                if ($lastBlock[1] > $this->longestBlockSeconds || $lastBlock[1] === -1) {
373 2
                    $this->longestBlockSeconds = $lastBlock[1];
374
                }
375
376
                // Now set this as the last block.
377 6
                $lastBlock = [$timestamp, $duration];
378 3
            } elseif ($block['log_action'] === 'unblock') {
379
                // The last block was lifted. So the duration will be the time from when the
380
                // last block was set to the time of the unblock.
381 1
                $timeSinceLastBlock = $timestamp - $lastBlock[0];
382 1
                if ($timeSinceLastBlock > $this->longestBlockSeconds) {
383 1
                    $this->longestBlockSeconds = $timeSinceLastBlock;
384
385
                    // Reset the last block, as it has now been accounted for.
386 1
                    $lastBlock = null;
387
                }
388 2
            } elseif ($block['log_action'] === 'reblock' && $lastBlock[1] !== -1) {
389
                // The last block was modified. So we will adjust $lastBlock to include
390
                // the difference of the duration of the new reblock, and time since the last block.
391
                // $lastBlock is left unchanged if its duration was indefinite.
392 1
                $timeSinceLastBlock = $timestamp - $lastBlock[0];
393 6
                $lastBlock[1] = $timeSinceLastBlock + $duration;
394
            }
395
        }
396
397
        // If the last block was indefinite, we'll return that as the longest duration.
398 6
        if ($lastBlock[1] === -1) {
399 2
            return -1;
400
        }
401
402
        // Test if the last block is still active, and if so use the expiry as the duration.
403 4
        $lastBlockExpiry = $lastBlock[0] + $lastBlock[1];
404 4
        if ($lastBlockExpiry > time() && $lastBlockExpiry > $this->longestBlockSeconds) {
405 1
            $this->longestBlockSeconds = $lastBlock[1];
406
        // Otherwise, test if the duration of the last block is now the longest overall.
407 3
        } elseif ($lastBlock[1] > $this->longestBlockSeconds) {
408 2
            $this->longestBlockSeconds = $lastBlock[1];
409
        }
410
411 4
        return $this->longestBlockSeconds;
412
    }
413
414
    /**
415
     * Given a block log entry from the database, get the timestamp and duration in seconds.
416
     * @param  mixed[] $block Block log entry as fetched via self::getBlocks()
417
     * @return int[] [
418
     *                 Unix timestamp,
419
     *                 Duration in seconds (-1 if indefinite, null if unparsable or unblock)
420
     *               ]
421
     */
422 11
    public function parseBlockLogEntry($block)
423
    {
424 11
        $timestamp = strtotime($block['log_timestamp']);
425 11
        $duration = null;
426
427
        // First check if the string is serialized, and if so parse it to get the block duration.
428 11
        if (@unserialize($block['log_params']) !== false) {
429 8
            $parsedParams = unserialize($block['log_params']);
430 8
            $durationStr = isset($parsedParams['5::duration']) ? $parsedParams['5::duration'] : null;
431
        } else {
432
            // Old format, the duration in English + block options separated by new lines.
433 4
            $durationStr = explode("\n", $block['log_params'])[0];
434
        }
435
436 11
        if (in_array($durationStr, ['indefinite', 'infinity', 'infinite'])) {
437 3
            $duration = -1;
438
        }
439
440
        // Make sure $durationStr is valid just in case it is in an older, unpredictable format.
441
        // If invalid, $duration is left as null.
442 11
        if (strtotime($durationStr)) {
443 8
            $expiry = strtotime($durationStr, $timestamp);
444 8
            $duration = $expiry - $timestamp;
445
        }
446
447 11
        return [$timestamp, $duration];
448
    }
449
450
    /**
451
     * Get the total number of pages protected by the user.
452
     * @return int
453
     */
454
    public function countPagesProtected()
455
    {
456
        $logCounts = $this->getLogCounts();
457
        return isset($logCounts['protect-protect']) ? (int)$logCounts['protect-protect'] : 0;
458
    }
459
460
    /**
461
     * Get the total number of pages reprotected by the user.
462
     * @return int
463
     */
464
    public function countPagesReprotected()
465
    {
466
        $logCounts = $this->getLogCounts();
467
        return isset($logCounts['protect-modify']) ? (int)$logCounts['protect-modify'] : 0;
468
    }
469
470
    /**
471
     * Get the total number of pages unprotected by the user.
472
     * @return int
473
     */
474
    public function countPagesUnprotected()
475
    {
476
        $logCounts = $this->getLogCounts();
477
        return isset($logCounts['protect-unprotect']) ? (int)$logCounts['protect-unprotect'] : 0;
478
    }
479
480
    /**
481
     * Get the total number of edits deleted by the user.
482
     * @return int
483
     */
484
    public function countEditsDeleted()
485
    {
486
        $logCounts = $this->getLogCounts();
487
        return isset($logCounts['delete-revision']) ? (int)$logCounts['delete-revision'] : 0;
488
    }
489
490
    /**
491
     * Get the total number of log entries deleted by the user.
492
     * @return int
493
     */
494
    public function countLogsDeleted()
495
    {
496
        $revCounts = $this->getLogCounts();
497
        return isset($revCounts['delete-event']) ? (int)$revCounts['delete-event'] : 0;
498
    }
499
500
    /**
501
     * Get the total number of pages restored by the user.
502
     * @return int
503
     */
504
    public function countPagesRestored()
505
    {
506
        $logCounts = $this->getLogCounts();
507
        return isset($logCounts['delete-restore']) ? (int)$logCounts['delete-restore'] : 0;
508
    }
509
510
    /**
511
     * Get the total number of times the user has modified the rights of a user.
512
     * @return int
513
     */
514
    public function countRightsModified()
515
    {
516
        $logCounts = $this->getLogCounts();
517
        return isset($logCounts['rights-rights']) ? (int)$logCounts['rights-rights'] : 0;
518
    }
519
520
    /**
521
     * Get the total number of pages imported by the user (through any import mechanism:
522
     * interwiki, or XML upload).
523
     * @return int
524
     */
525
    public function countPagesImported()
526
    {
527
        $logCounts = $this->getLogCounts();
528
        $import = isset($logCounts['import-import']) ? (int)$logCounts['import-import'] : 0;
529
        $interwiki = isset($logCounts['import-interwiki']) ? (int)$logCounts['import-interwiki'] : 0;
530
        $upload = isset($logCounts['import-upload']) ? (int)$logCounts['import-upload'] : 0;
531
        return $import + $interwiki + $upload;
532
    }
533
534
    /**
535
     * Get the average number of edits per page (including deleted revisions and pages).
536
     * @return float
537
     */
538
    public function averageRevisionsPerPage()
539
    {
540
        if ($this->countAllPagesEdited() == 0) {
541
            return 0;
542
        }
543
        return round($this->countAllRevisions() / $this->countAllPagesEdited(), 3);
544
    }
545
546
    /**
547
     * Average number of edits made per day.
548
     * @return float
549
     */
550
    public function averageRevisionsPerDay()
551
    {
552
        if ($this->getDays() == 0) {
553
            return 0;
554
        }
555
        return round($this->countAllRevisions() / $this->getDays(), 3);
556
    }
557
558
    /**
559
     * Get the total number of edits made by the user with semi-automating tools.
560
     */
561
    public function countAutomatedEdits()
562
    {
563
        if ($this->autoEditCount) {
564
            return $this->autoEditCount;
565
        }
566
        $this->autoEditCount = $this->getRepository()->countAutomatedEdits(
567
            $this->project,
568
            $this->user
569
        );
570
        return $this->autoEditCount;
571
    }
572
573
    /**
574
     * Get the count of (non-deleted) edits made in the given timeframe to now.
575
     * @param string $time One of 'day', 'week', 'month', or 'year'.
576
     * @return int The total number of live edits.
577
     */
578
    public function countRevisionsInLast($time)
579
    {
580
        $revCounts = $this->getPairData();
581
        return isset($revCounts[$time]) ? $revCounts[$time] : 0;
582
    }
583
584
    /**
585
     * Get the number of days between the first and last edits.
586
     * If there's only one edit, this is counted as one day.
587
     * @return int
588
     */
589 1
    public function getDays()
590
    {
591 1
        $first = isset($this->getFirstAndLatestActions()['rev_first']['timestamp'])
592 1
            ? new DateTime($this->getFirstAndLatestActions()['rev_first']['timestamp'])
593 1
            : false;
594 1
        $latest = isset($this->getFirstAndLatestActions()['rev_latest']['timestamp'])
595 1
            ? new DateTime($this->getFirstAndLatestActions()['rev_latest']['timestamp'])
596 1
            : false;
597
598 1
        if ($first === false || $latest === false) {
599
            return 0;
600
        }
601
602 1
        $days = $latest->diff($first)->days;
603
604 1
        return $days > 0 ? $days : 1;
605
    }
606
607
    /**
608
     * Get the total number of files uploaded (including those now deleted).
609
     * @return int
610
     */
611
    public function countFilesUploaded()
612
    {
613
        $logCounts = $this->getLogCounts();
614
        return $logCounts['upload-upload'] ?: 0;
615
    }
616
617
    /**
618
     * Get the total number of files uploaded to Commons (including those now deleted).
619
     * This is only applicable for WMF labs installations.
620
     * @return int
621
     */
622
    public function countFilesUploadedCommons()
623
    {
624
        $logCounts = $this->getLogCounts();
625
        return $logCounts['files_uploaded_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()
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()
643
    {
644
        $logCounts = $this->getLogCounts();
645
        $total = $logCounts['review-approve'] +
646
        (!empty($logCounts['review-approve-a']) ? $logCounts['review-approve-a'] : 0) +
647
        (!empty($logCounts['review-approve-i']) ? $logCounts['review-approve-i'] : 0) +
648
        (!empty($logCounts['review-approve-ia']) ? $logCounts['review-approve-ia'] : 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()
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()
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 given user's total edit counts per namespace.
676
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
677
     */
678 1
    public function namespaceTotals()
679
    {
680 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...
681
            return $this->namespaceTotals;
682
        }
683 1
        $counts = $this->getRepository()->getNamespaceTotals($this->project, $this->user);
684 1
        arsort($counts);
685 1
        $this->namespaceTotals = $counts;
686 1
        return $counts;
687
    }
688
689
    /**
690
     * Get a summary of the times of day and the days of the week that the user has edited.
691
     * @return string[]
692
     */
693
    public function timeCard()
694
    {
695
        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...
696
            return $this->timeCardData;
697
        }
698
        $totals = $this->getRepository()->getTimeCard($this->project, $this->user);
699
700
        // Scale the radii: get the max, then scale each radius.
701
        // This looks inefficient, but there's a max of 72 elements in this array.
702
        $max = 0;
703
        foreach ($totals as $total) {
704
            $max = max($max, $total['value']);
705
        }
706
        foreach ($totals as &$total) {
707
            $total['value'] = round($total['value'] / $max * 100);
708
        }
709
710
        // Fill in zeros for timeslots that have no values.
711
        $sortedTotals = [];
712
        $index = 0;
713
        $sortedIndex = 0;
714
        foreach (range(1, 7) as $day) {
715
            foreach (range(0, 24, 2) as $hour) {
716
                if (isset($totals[$index]) && (int)$totals[$index]['x'] === $hour) {
717
                    $sortedTotals[$sortedIndex] = $totals[$index];
718
                    $index++;
719
                } else {
720
                    $sortedTotals[$sortedIndex] = [
721
                        'y' => $day,
722
                        'x' => $hour,
723
                        'value' => 0,
724
                    ];
725
                }
726
                $sortedIndex++;
727
            }
728
        }
729
730
        $this->timeCardData = $sortedTotals;
731
        return $sortedTotals;
732
    }
733
734
    /**
735
     * Get the total numbers of edits per month.
736
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING*
737
     *   so we can mock the current DateTime.
738
     * @return mixed[] With keys 'yearLabels', 'monthLabels' and 'totals',
739
     *   the latter keyed by namespace, year and then month.
740
     */
741 2
    public function monthCounts($currentTime = null)
742
    {
743 2
        if (isset($this->monthCounts)) {
744 1
            return $this->monthCounts;
745
        }
746
747
        // Set to current month if we're not unit-testing
748 2
        if (!($currentTime instanceof DateTime)) {
749
            $currentTime = new DateTime('last day of this month');
750
        }
751
752 2
        $totals = $this->getRepository()->getMonthCounts($this->project, $this->user);
753
        $out = [
754 2
            'yearLabels' => [],  // labels for years
755
            'monthLabels' => [], // labels for months
756
            'totals' => [], // actual totals, grouped by namespace, year and then month
757
        ];
758
759
        /** @var DateTime Keep track of the date of their first edit. */
760 2
        $firstEdit = new DateTime();
761
762 2
        list($out, $firstEdit) = $this->fillInMonthCounts($out, $totals, $firstEdit);
763
764 2
        $dateRange = new DatePeriod(
765 2
            $firstEdit,
766 2
            new DateInterval('P1M'),
767 2
            $currentTime->modify('first day of this month')
768
        );
769
770 2
        $out = $this->fillInMonthTotalsAndLabels($out, $dateRange);
771
772
        // One more set of loops to sort by year/month
773 2
        foreach (array_keys($out['totals']) as $nsId) {
0 ignored issues
show
Bug introduced by
$out['totals'] of type string is incompatible with the type array expected by parameter $input of array_keys(). ( Ignorable by Annotation )

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

773
        foreach (array_keys(/** @scrutinizer ignore-type */ $out['totals']) as $nsId) {
Loading history...
774 2
            ksort($out['totals'][$nsId]);
775
776 2
            foreach ($out['totals'][$nsId] as &$yearData) {
777 2
                ksort($yearData);
778
            }
779
        }
780
781
        // Finally, sort the namespaces
782 2
        ksort($out['totals']);
0 ignored issues
show
Bug introduced by
$out['totals'] of type string is incompatible with the type array expected by parameter $array of ksort(). ( Ignorable by Annotation )

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

782
        ksort(/** @scrutinizer ignore-type */ $out['totals']);
Loading history...
783
784 2
        $this->monthCounts = $out;
785 2
        return $out;
786
    }
787
788
    /**
789
     * Get the counts keyed by month and then namespace.
790
     * Basically the opposite of self::monthCounts()['totals'].
791
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING*
792
     *   so we can mock the current DateTime.
793
     * @return array Months as keys, values are counts keyed by namesapce.
794
     * @fixme Create API for this!
795
     */
796 1
    public function monthCountsWithNamespaces($currentTime = null)
797
    {
798 1
        $countsMonthNamespace = array_fill_keys(
799 1
            array_keys($this->monthTotals($currentTime)),
800 1
            []
801
        );
802
803 1
        foreach ($this->monthCounts($currentTime)['totals'] as $ns => $years) {
804 1
            foreach ($years as $year => $months) {
805 1
                foreach ($months as $month => $count) {
806 1
                    $monthKey = $year.'-'.sprintf('%02d', $month);
807 1
                    $countsMonthNamespace[$monthKey][$ns] = $count;
808
                }
809
            }
810
        }
811
812 1
        return $countsMonthNamespace;
813
    }
814
815
    /**
816
     * Loop through the database results and fill in the values
817
     * for the months that we have data for.
818
     * @param array $out
819
     * @param string[] $totals
820
     * @param DateTime $firstEdit
821
     * @return array [
822
     *           string[] - Modified $out filled with month stats,
823
     *           DateTime - timestamp of first edit
824
     *         ]
825
     * Tests covered in self::monthCounts().
826
     * @codeCoverageIgnore
827
     */
828
    private function fillInMonthCounts($out, $totals, $firstEdit)
829
    {
830
        foreach ($totals as $total) {
831
            // Keep track of first edit
832
            $date = new DateTime($total['year'].'-'.$total['month'].'-01');
833
            if ($date < $firstEdit) {
834
                $firstEdit = $date;
835
            }
836
837
            // Collate the counts by namespace, and then year and month.
838
            $ns = $total['page_namespace'];
839
            if (!isset($out['totals'][$ns])) {
840
                $out['totals'][$ns] = [];
841
            }
842
843
            // Start array for this year if not already present.
844
            if (!isset($out['totals'][$ns][$total['year']])) {
845
                $out['totals'][$ns][$total['year']] = [];
846
            }
847
848
            $out['totals'][$ns][$total['year']][$total['month']] = (int) $total['count'];
849
        }
850
851
        return [$out, $firstEdit];
852
    }
853
854
    /**
855
     * Given the output array, fill each month's totals and labels.
856
     * @param array $out
857
     * @param DatePeriod $dateRange From first edit to present.
858
     * @return string[] - Modified $out filled with month stats.
859
     * Tests covered in self::monthCounts().
860
     * @codeCoverageIgnore
861
     */
862
    private function fillInMonthTotalsAndLabels($out, DatePeriod $dateRange)
863
    {
864
        foreach ($dateRange as $monthObj) {
865
            $year = (int) $monthObj->format('Y');
866
            $yearLabel = $this->i18n->dateFormat($monthObj, 'yyyy');
867
            $month = (int) $monthObj->format('n');
868
            $monthLabel = $this->i18n->dateFormat($monthObj, 'yyyy-MM');
869
870
            // Fill in labels
871
            $out['monthLabels'][] = $monthLabel;
872
            if (!in_array($yearLabel, $out['yearLabels'])) {
873
                $out['yearLabels'][] = $yearLabel;
874
            }
875
876
            foreach (array_keys($out['totals']) as $nsId) {
877
                if (!isset($out['totals'][$nsId][$year])) {
878
                    $out['totals'][$nsId][$year] = [];
879
                }
880
881
                if (!isset($out['totals'][$nsId][$year][$month])) {
882
                    $out['totals'][$nsId][$year][$month] = 0;
883
                }
884
            }
885
        }
886
887
        return $out;
888
    }
889
890
    /**
891
     * Get total edits for each month. Used in wikitext export.
892
     * @param null|DateTime $currentTime *USED ONLY FOR UNIT TESTING*
893
     * @return array With the months as the keys, counts as the values.
894
     */
895 1
    public function monthTotals($currentTime = null)
896
    {
897 1
        $months = [];
898
899 1
        foreach ($this->monthCounts($currentTime)['totals'] as $nsId => $nsData) {
900 1
            foreach ($nsData as $year => $monthData) {
901 1
                foreach ($monthData as $month => $count) {
902 1
                    $monthLabel = $year.'-'.sprintf('%02d', $month);
903 1
                    if (!isset($months[$monthLabel])) {
904 1
                        $months[$monthLabel] = 0;
905
                    }
906 1
                    $months[$monthLabel] += $count;
907
                }
908
            }
909
        }
910
911 1
        return $months;
912
    }
913
914
    /**
915
     * Get the total numbers of edits per year.
916
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING*
917
     *   so we can mock the current DateTime.
918
     * @return mixed[] With keys 'yearLabels' and 'totals', the latter
919
     *   keyed by namespace then year.
920
     */
921 2
    public function yearCounts($currentTime = null)
922
    {
923 2
        if (isset($this->yearCounts)) {
924
            return $this->yearCounts;
925
        }
926
927 2
        $out = $this->monthCounts($currentTime);
928
929 2
        foreach ($out['totals'] as $nsId => $years) {
930 2
            foreach ($years as $year => $months) {
931 2
                $out['totals'][$nsId][$year] = array_sum(array_values($months));
932
            }
933
        }
934
935 2
        $this->yearCounts = $out;
936 2
        return $out;
937
    }
938
939
    /**
940
     * Get the counts keyed by year and then namespace.
941
     * Basically the opposite of self::yearCounts()['totals'].
942
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING*
943
     *   so we can mock the current DateTime.
944
     * @return array Years as keys, values are counts keyed by namesapce.
945
     */
946
    public function yearCountsWithNamespaces($currentTime = null)
947
    {
948
        $countsYearNamespace = array_fill_keys(
949
            array_keys($this->yearTotals($currentTime)),
950
            []
951
        );
952
953
        foreach ($this->yearCounts($currentTime)['totals'] as $ns => $years) {
954
            foreach ($years as $year => $count) {
955
                $countsYearNamespace[$year][$ns] = $count;
956
            }
957
        }
958
959
        return $countsYearNamespace;
960
    }
961
962
    /**
963
     * Get total edits for each year. Used in wikitext export.
964
     * @param null|DateTime $currentTime *USED ONLY FOR UNIT TESTING*
965
     * @return array With the years as the keys, counts as the values.
966
     */
967 1
    public function yearTotals($currentTime = null)
968
    {
969 1
        $years = [];
970
971 1
        foreach ($this->yearCounts($currentTime)['totals'] as $nsId => $nsData) {
972 1
            foreach ($nsData as $year => $count) {
973 1
                if (!isset($years[$year])) {
974 1
                    $years[$year] = 0;
975
                }
976 1
                $years[$year] += $count;
977
            }
978
        }
979
980 1
        return $years;
981
    }
982
983
    /**
984
     * Get the total edit counts for the top n projects of this user.
985
     * @param int $numProjects
986
     * @return mixed[] Each element has 'total' and 'project' keys.
987
     */
988 1
    public function globalEditCountsTopN($numProjects = 10)
989
    {
990
        // Get counts.
991 1
        $editCounts = $this->globalEditCounts(true);
992
        // Truncate, and return.
993 1
        return array_slice($editCounts, 0, $numProjects);
994
    }
995
996
    /**
997
     * Get the total number of edits excluding the top n.
998
     * @param int $numProjects
999
     * @return int
1000
     */
1001 1
    public function globalEditCountWithoutTopN($numProjects = 10)
1002
    {
1003 1
        $editCounts = $this->globalEditCounts(true);
1004 1
        $bottomM = array_slice($editCounts, $numProjects);
1005 1
        $total = 0;
1006 1
        foreach ($bottomM as $editCount) {
1007 1
            $total += $editCount['total'];
1008
        }
1009 1
        return $total;
1010
    }
1011
1012
    /**
1013
     * Get the grand total of all edits on all projects.
1014
     * @return int
1015
     */
1016 1
    public function globalEditCount()
1017
    {
1018 1
        $total = 0;
1019 1
        foreach ($this->globalEditCounts() as $editCount) {
1020 1
            $total += $editCount['total'];
1021
        }
1022 1
        return $total;
1023
    }
1024
1025
    /**
1026
     * Get the total revision counts for all projects for this user.
1027
     * @param bool $sorted Whether to sort the list by total, or not.
1028
     * @return mixed[] Each element has 'total' and 'project' keys.
1029
     */
1030 1
    public function globalEditCounts($sorted = false)
1031
    {
1032 1
        if (empty($this->globalEditCounts)) {
1033 1
            $this->globalEditCounts = $this->getRepository()
1034 1
                ->globalEditCounts($this->user, $this->project);
1035
        }
1036
1037 1
        if ($sorted) {
1038
            // Sort.
1039 1
            uasort($this->globalEditCounts, function ($a, $b) {
1040 1
                return $b['total'] - $a['total'];
1041 1
            });
1042
        }
1043
1044 1
        return $this->globalEditCounts;
1045
    }
1046
1047
    /**
1048
     * Get the most recent n revisions across all projects.
1049
     * @param int $max The maximum number of revisions to return.
1050
     * @param int $offset Offset results by this number of revisions.
1051
     * @return Edit[]
1052
     */
1053
    public function globalEdits($max, $offset = 0)
1054
    {
1055
        if (is_array($this->globalEdits)) {
0 ignored issues
show
introduced by
The condition is_array($this->globalEdits) is always true.
Loading history...
1056
            return $this->globalEdits;
1057
        }
1058
1059
        // Collect all projects with any edits.
1060
        $projects = [];
1061
        foreach ($this->globalEditCounts() as $editCount) {
1062
            // Don't query revisions if there aren't any.
1063
            if ($editCount['total'] == 0) {
1064
                continue;
1065
            }
1066
            $projects[$editCount['project']->getDatabaseName()] = $editCount['project'];
1067
        }
1068
1069
        if (count($projects) === 0) {
1070
            return [];
1071
        }
1072
1073
        // Get all revisions for those projects.
1074
        $globalRevisionsData = $this->getRepository()
1075
            ->getRevisions($projects, $this->user, $max, $offset);
1076
        $globalEdits = [];
1077
        foreach ($globalRevisionsData as $revision) {
1078
            /** @var Project $project */
1079
            $project = $projects[$revision['project_name']];
1080
1081
            $nsName = '';
1082
            if ($revision['page_namespace']) {
1083
                $nsName = $project->getNamespaces()[$revision['page_namespace']];
1084
            }
1085
1086
            $page = $project->getRepository()
1087
                ->getPage($project, $nsName.':'.$revision['page_title']);
1088
            $edit = new Edit($page, $revision);
1089
            $globalEdits[$edit->getTimestamp()->getTimestamp().'-'.$edit->getId()] = $edit;
1090
        }
1091
1092
        // Sort and prune, before adding more.
1093
        krsort($globalEdits);
1094
        $this->globalEdits = array_slice($globalEdits, 0, $max);
1095
1096
        return $this->globalEdits;
1097
    }
1098
1099
    /**
1100
     * Get average edit size, and number of large and small edits.
1101
     * @return int[]
1102
     */
1103
    public function getEditSizeData()
1104
    {
1105
        if (!is_array($this->editSizeData)) {
0 ignored issues
show
introduced by
The condition is_array($this->editSizeData) is always true.
Loading history...
1106
            $this->editSizeData = $this->getRepository()
1107
                ->getEditSizeData($this->project, $this->user);
1108
        }
1109
        return $this->editSizeData;
1110
    }
1111
1112
    /**
1113
     * Get the total edit count of this user or 5,000 if they've made more than 5,000 edits.
1114
     * This is used to ensure percentages of small and large edits are computed properly.
1115
     * @return int
1116
     */
1117 1
    public function countLast5000()
1118
    {
1119 1
        return $this->countLiveRevisions() > 5000 ? 5000 : $this->countLiveRevisions();
1120
    }
1121
1122
    /**
1123
     * Get the number of edits under 20 bytes of the user's past 5000 edits.
1124
     * @return int
1125
     */
1126
    public function countSmallEdits()
1127
    {
1128
        $editSizeData = $this->getEditSizeData();
1129
        return isset($editSizeData['small_edits']) ? (int) $editSizeData['small_edits'] : 0;
1130
    }
1131
1132
    /**
1133
     * Get the total number of edits over 1000 bytes of the user's past 5000 edits.
1134
     * @return int
1135
     */
1136
    public function countLargeEdits()
1137
    {
1138
        $editSizeData = $this->getEditSizeData();
1139
        return isset($editSizeData['large_edits']) ? (int) $editSizeData['large_edits'] : 0;
1140
    }
1141
1142
    /**
1143
     * Get the average size of the user's past 5000 edits.
1144
     * @return float Size in bytes.
1145
     */
1146
    public function averageEditSize()
1147
    {
1148
        $editSizeData = $this->getEditSizeData();
1149
        if (isset($editSizeData['average_size'])) {
1150
            return round($editSizeData['average_size'], 3);
1151
        } else {
1152
            return 0;
1153
        }
1154
    }
1155
}
1156