Passed
Push — master ( b84e1a...526c52 )
by MusikAnimal
05:07
created

EditCounter::globalEditCountsTopN()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 6
ccs 3
cts 3
cp 1
crap 1
rs 9.4285
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
use Exception;
13
use GuzzleHttp;
14
use GuzzleHttp\Promise\Promise;
15
use Xtools\Edit;
16
17
/**
18
 * An EditCounter provides statistics about a user's edits on a project.
19
 */
20
class EditCounter extends Model
21
{
22
23
    /** @var Project The project. */
24
    protected $project;
25
26
    /** @var User The user. */
27
    protected $user;
28
29
    /** @var I18nHelper For i18n and l10n. */
30
    protected $i18n;
31
32
    /** @var int[] Revision and page counts etc. */
33
    protected $pairData;
34
35
    /** @var string[] The start and end dates of revisions. */
36
    protected $revisionDates;
37
38
    /** @var int[] The total page counts. */
39
    protected $pageCounts;
40
41
    /** @var int[] The lot totals. */
42
    protected $logCounts;
43
44
    /** @var mixed[] Total numbers of edits per month */
45
    protected $monthCounts;
46
47
    /** @var mixed[] Total numbers of edits per year */
48
    protected $yearCounts;
49
50
    /** @var string[] Rights changes, keyed by timestamp then 'added' and 'removed'. */
51
    protected $rightsChanges;
52
53
    /** @var string[] Global rights changes, keyed by timestamp then 'added' and 'removed'. */
54
    protected $globalRightsChanges;
55
56
    /** @var int[] Keys are project DB names. */
57
    protected $globalEditCounts;
58
59
    /** @var array Block data, with keys 'set' and 'received'. */
60
    protected $blocks;
61
62
    /** @var integer[] Array keys are namespace IDs, values are the edit counts. */
63
    protected $namespaceTotals;
64
65
    /** @var int Number of semi-automated edits. */
66
    protected $autoEditCount;
67
68
    /** @var string[] Data needed for time card chart. */
69
    protected $timeCardData;
70
71
    /** @var array Most recent revisions across all projects. */
72
    protected $globalEdits;
73
74
    /**
75
     * Revision size data, with keys 'average_size', 'large_edits' and 'small_edits'.
76
     * @var string[] As returned by the DB, unconverted to int or float
77
     */
78
    protected $editSizeData;
79
80
    /**
81
     * Duration of the longest block in seconds; -1 if indefinite,
82
     *   or false if could not be parsed from log params
83
     * @var int|bool
84
     */
85
    protected $longestBlockSeconds;
86
87
    /**
88
     * EditCounter constructor.
89
     * @param Project $project The base project to count edits
90
     * @param User $user
91
     */
92 20
    public function __construct(Project $project, User $user)
93
    {
94 20
        $this->project = $project;
95 20
        $this->user = $user;
96 20
    }
97
98
    /**
99
     * Make the I18nHelper accessible to EditCounter.
100
     * @param I18nHelper $i18n
101
     */
102 20
    public function setI18nHelper(I18nHelper $i18n)
103
    {
104 20
        $this->i18n = $i18n;
105 20
    }
106
107
    /**
108
     * This method asynchronously fetches all the expensive data, waits
109
     * for each request to finish, and copies the values to the class instance.
110
     * @return null
111
     */
112
    public function prepareData()
113
    {
114
        $project = $this->project->getDomain();
115
        $username = $this->user->getUsername();
116
117
        /**
118
         * The URL of each endpoint, keyed by the name of the corresponding class-level
119
         * instance variable.
120
         * @var array[]
121
         */
122
        $endpoints = [
123
            "pairData" => "ec/pairdata/$project/$username",
124
            "logCounts" => "ec/logcounts/$project/$username",
125
            "namespaceTotals" => "ec/namespacetotals/$project/$username",
126
            "editSizeData" => "ec/editsizes/$project/$username",
127
            "monthCounts" => "ec/monthcounts/$project/$username",
128
            // "globalEditCounts" => "ec-globaleditcounts/$project/$username",
129
            "autoEditCount" => "user/automated_editcount/$project/$username",
130
        ];
131
132
        /**
133
         * Keep track of all promises so we can wait for all of them to complete.
134
         * @var GuzzleHttp\Promise\Promise[]
135
         */
136
        $promises = [];
137
138
        foreach ($endpoints as $key => $endpoint) {
139
            $promise = $this->getRepository()->queryXToolsApi($endpoint, true);
140
            $promises[] = $promise;
141
142
            // Handle response of $promise asynchronously.
143
            $promise->then(function ($response) use ($key, $endpoint) {
144
                $result = (array) json_decode($response->getBody()->getContents());
145
146
                $this->getRepository()
147
                    ->getLog()
148
                    ->debug("$key promise resolved successfully.");
149
150
                if (isset($result)) {
151
                    // Copy result to the class class instance. From here any subsequent
152
                    // calls to the getters (e.g. getPairData()) will return these cached values.
153
                    $this->{$key} = $result;
154
                } else {
155
                    // The API should *always* return something, so if $result is not set,
156
                    // something went wrong, so we simply won't set it and the getters will in
157
                    // turn re-attempt to get the data synchronously.
158
                    // We'll log this to see how often it happens.
159
                    $this->getRepository()
160
                        ->getLog()
161
                        ->error("Failed to fetch data for $endpoint via async, " .
162
                            "re-attempting synchoronously.");
163
                }
164
            });
165
        }
166
167
        // Wait for all promises to complete, even if some of them fail.
168
        GuzzleHttp\Promise\settle($promises)->wait();
0 ignored issues
show
Bug introduced by
The function settle was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

168
        /** @scrutinizer ignore-call */ 
169
        GuzzleHttp\Promise\settle($promises)->wait();
Loading history...
169
170
        // Everything we need now lives on the class instance, so we're done.
171
        return;
172
    }
173
174
    /**
175
     * Get revision and page counts etc.
176
     * @return int[]
177
     */
178 4
    public function getPairData()
179
    {
180 4
        if (!is_array($this->pairData)) {
0 ignored issues
show
introduced by
The condition is_array($this->pairData) is always true.
Loading history...
181 4
            $this->pairData = $this->getRepository()
182 4
                ->getPairData($this->project, $this->user);
183
        }
184 4
        return $this->pairData;
185
    }
186
187
    /**
188
     * Get revision dates.
189
     * @return int[]
190
     */
191
    public function getLogCounts()
192
    {
193
        if (!is_array($this->logCounts)) {
0 ignored issues
show
introduced by
The condition is_array($this->logCounts) is always true.
Loading history...
194
            $this->logCounts = $this->getRepository()
195
                ->getLogCounts($this->project, $this->user);
196
        }
197
        return $this->logCounts;
198
    }
199
200
    /**
201
     * Get block data.
202
     * @param string $type Either 'set', 'received'
203
     * @param bool $blocksOnly Whether to include only blocks, and not reblocks and unblocks.
204
     * @return array
205
     */
206 6
    protected function getBlocks($type, $blocksOnly = true)
207
    {
208 6
        if (isset($this->blocks[$type]) && is_array($this->blocks[$type])) {
209
            return $this->blocks[$type];
210
        }
211 6
        $method = "getBlocks".ucfirst($type);
212 6
        $blocks = $this->getRepository()->$method($this->project, $this->user);
213 6
        $this->blocks[$type] = $blocks;
214
215
        // Filter out unblocks unless requested.
216 6
        if ($blocksOnly) {
217
            $blocks = array_filter($blocks, function ($block) {
218
                return $block['log_action'] === 'block';
219
            });
220
        }
221
222 6
        return $blocks;
223
    }
224
225
    /**
226
     * Get user rights changes of the given user.
227
     * @param Project $project
228
     * @param User $user
229
     * @return string[] Keyed by timestamp then 'added' and 'removed'.
230
     */
231 1
    public function getRightsChanges()
232
    {
233 1
        if (isset($this->rightsChanges)) {
234 1
            return $this->rightsChanges;
235
        }
236
237 1
        $logData = $this->getRepository()
238 1
            ->getRightsChanges($this->project, $this->user);
239
240 1
        $this->rightsChanges = $this->processRightsChanges($logData);
241
242 1
        return $this->rightsChanges;
243
    }
244
245
    /**
246
     * Checks the user rights log to see whether the user is an admin
247
     * or used to be one.
248
     * @return string|false One of false (never an admin), 'current' or 'former'.
249
     */
250
    public function getAdminStatus()
251
    {
252
        $rightsStates = $this->getRightsStates();
253
254
        if (in_array('sysop', $rightsStates['current'])) {
255
            return 'current';
256
        } elseif (in_array('sysop', $rightsStates['former'])) {
257
            return 'former';
258
        } else {
259
            return false;
260
        }
261
    }
262
263
    /**
264
     * Get a list of the current and former rights of the user.
265
     * @return array With keys 'current' and 'former'.
266
     */
267 1
    public function getRightsStates()
268
    {
269 1
        $current = [];
270 1
        $former = [];
271
272 1
        foreach (array_reverse($this->getRightsChanges()) as $change) {
273 1
            $current = array_diff(
274 1
                array_unique(array_merge($current, $change['added'])),
275 1
                $change['removed']
276
            );
277 1
            $former = array_diff(
278 1
                array_unique(array_merge($former, $change['removed'])),
279 1
                $change['added']
280
            );
281
        }
282
283
        return [
284 1
            'current' => $current,
285 1
            'former' => $former,
286
        ];
287
    }
288
289
    /**
290
     * Get global user rights changes of the given user.
291
     * @param Project $project
292
     * @param User $user
293
     * @return string[] Keyed by timestamp then 'added' and 'removed'.
294
     */
295 1
    public function getGlobalRightsChanges()
296
    {
297 1
        if (isset($this->globalRightsChanges)) {
298
            return $this->globalRightsChanges;
299
        }
300
301 1
        $logData = $this->getRepository()
302 1
            ->getGlobalRightsChanges($this->project, $this->user);
303
304 1
        $this->globalRightsChanges = $this->processRightsChanges($logData);
305
306 1
        return $this->globalRightsChanges;
307
    }
308
309
    /**
310
     * Process the given rights changes, sorting an putting in a human-readable format.
311
     * @param  array $logData As fetched with EditCounterRepository::getRightsChanges.
312
     * @return array
313
     */
314 1
    private function processRightsChanges($logData)
315
    {
316 1
        $rightsChanges = [];
317
318 1
        foreach ($logData as $row) {
319 1
            $unserialized = @unserialize($row['log_params']);
320 1
            if ($unserialized !== false) {
321 1
                $old = $unserialized['4::oldgroups'];
322 1
                $new = $unserialized['5::newgroups'];
323 1
                $added = array_diff($new, $old);
324 1
                $removed = array_diff($old, $new);
325
326 1
                $rightsChanges = $this->setAutoRemovals($rightsChanges, $row, $unserialized, $added);
327
            } else {
328
                // This is the old school format the most likely contains
329
                // the list of rights additions in as a comma-separated list.
330
                try {
331 1
                    list($old, $new) = explode("\n", $row['log_params']);
332 1
                    $old = array_filter(array_map('trim', explode(',', $old)));
333 1
                    $new = array_filter(array_map('trim', explode(',', $new)));
334 1
                    $added = array_diff($new, $old);
335 1
                    $removed = array_diff($old, $new);
336
                } catch (Exception $e) {
337
                    // Really really old school format that may be missing metadata
338
                    // altogether. Here we'll just leave $added and $removed blank.
339
                    $added = [];
340
                    $removed = [];
341
                }
342
            }
343
344
            // Remove '(none)'.
345 1
            if (in_array('(none)', $added)) {
346
                array_splice($added, array_search('(none)', $added), 1);
0 ignored issues
show
Bug introduced by
It seems like array_search('(none)', $added) can also be of type string and false; however, parameter $offset of array_splice() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

346
                array_splice($added, /** @scrutinizer ignore-type */ array_search('(none)', $added), 1);
Loading history...
347
            }
348 1
            if (in_array('(none)', $removed)) {
349
                array_splice($removed, array_search('(none)', $removed), 1);
350
            }
351
352 1
            $rightsChanges[$row['log_timestamp']] = [
353 1
                'logId' => $row['log_id'],
354 1
                'admin' => $row['log_user_text'],
355 1
                'comment' => $row['log_comment'],
356 1
                'added' => array_values($added),
357 1
                'removed' => array_values($removed),
358 1
                'automatic' => $row['log_action'] === 'autopromote',
359 1
                'type' => $row['type'],
360
            ];
361
        }
362
363 1
        krsort($rightsChanges);
364
365 1
        return $rightsChanges;
366
    }
367
368
    /**
369
     * Check the given log entry for rights changes that are set to automatically expire,
370
     * and add entries to $rightsChanges accordingly.
371
     * @param array $rightsChanges
372
     * @param array $row Log entry row from database.
373
     * @param array $params Unserialized log params.
374
     * @param string[] $added List of added user rights.
375
     * @return array Modified $rightsChanges.
376
     */
377 1
    private function setAutoRemovals($rightsChanges, $row, $params, $added)
378
    {
379 1
        foreach ($added as $index => $entry) {
380 1
            if (!isset($params['newmetadata'][$index]) ||
381 1
                !array_key_exists('expiry', $params['newmetadata'][$index]) ||
382 1
                empty($params['newmetadata'][$index]['expiry'])
383
            ) {
384 1
                continue;
385
            }
386
387 1
            $expiry = $params['newmetadata'][$index]['expiry'];
388
389 1
            if (isset($rightsChanges[$expiry]) && !in_array($entry, $rightsChanges[$expiry]['removed'])) {
390 1
                $rightsChanges[$expiry]['removed'][] = $entry;
391
            } else {
392 1
                $rightsChanges[$expiry] = [
393 1
                    'logId' => $row['log_id'],
394 1
                    'admin' => $row['log_user_text'],
395
                    'comment' => null,
396
                    'added' => [],
397 1
                    'removed' => [$entry],
398
                    'automatic' => true,
399 1
                    'type' => $row['type'],
400
                ];
401
            }
402
        }
403
404 1
        return $rightsChanges;
405
    }
406
407
    /**
408
     * Get the total number of currently-live revisions.
409
     * @return int
410
     */
411 1
    public function countLiveRevisions()
412
    {
413 1
        $revCounts = $this->getPairData();
414 1
        return isset($revCounts['live']) ? (int)$revCounts['live'] : 0;
415
    }
416
417
    /**
418
     * Get the total number of the user's revisions that have been deleted.
419
     * @return int
420
     */
421 1
    public function countDeletedRevisions()
422
    {
423 1
        $revCounts = $this->getPairData();
424 1
        return isset($revCounts['deleted']) ? (int)$revCounts['deleted'] : 0;
425
    }
426
427
    /**
428
     * Get the total edit count (live + deleted).
429
     * @return int
430
     */
431 1
    public function countAllRevisions()
432
    {
433 1
        return $this->countLiveRevisions() + $this->countDeletedRevisions();
434
    }
435
436
    /**
437
     * Get the total number of live revisions with comments.
438
     * @return int
439
     */
440 1
    public function countRevisionsWithComments()
441
    {
442 1
        $revCounts = $this->getPairData();
443 1
        return isset($revCounts['with_comments']) ? (int)$revCounts['with_comments'] : 0;
444
    }
445
446
    /**
447
     * Get the total number of live revisions without comments.
448
     * @return int
449
     */
450 1
    public function countRevisionsWithoutComments()
451
    {
452 1
        return $this->countLiveRevisions() - $this->countRevisionsWithComments();
453
    }
454
455
    /**
456
     * Get the total number of revisions marked as 'minor' by the user.
457
     * @return int
458
     */
459
    public function countMinorRevisions()
460
    {
461
        $revCounts = $this->getPairData();
462
        return isset($revCounts['minor']) ? (int)$revCounts['minor'] : 0;
463
    }
464
465
    /**
466
     * Get the total number of non-deleted pages edited by the user.
467
     * @return int
468
     */
469 1
    public function countLivePagesEdited()
470
    {
471 1
        $pageCounts = $this->getPairData();
472 1
        return isset($pageCounts['edited-live']) ? (int)$pageCounts['edited-live'] : 0;
473
    }
474
475
    /**
476
     * Get the total number of deleted pages ever edited by the user.
477
     * @return int
478
     */
479 1
    public function countDeletedPagesEdited()
480
    {
481 1
        $pageCounts = $this->getPairData();
482 1
        return isset($pageCounts['edited-deleted']) ? (int)$pageCounts['edited-deleted'] : 0;
483
    }
484
485
    /**
486
     * Get the total number of pages ever edited by this user (both live and deleted).
487
     * @return int
488
     */
489 1
    public function countAllPagesEdited()
490
    {
491 1
        return $this->countLivePagesEdited() + $this->countDeletedPagesEdited();
492
    }
493
494
    /**
495
     * Get the total number of pages (both still live and those that have been deleted) created
496
     * by the user.
497
     * @return int
498
     */
499 1
    public function countPagesCreated()
500
    {
501 1
        return $this->countCreatedPagesLive() + $this->countPagesCreatedDeleted();
502
    }
503
504
    /**
505
     * Get the total number of pages created by the user, that have not been deleted.
506
     * @return int
507
     */
508 1
    public function countCreatedPagesLive()
509
    {
510 1
        $pageCounts = $this->getPairData();
511 1
        return isset($pageCounts['created-live']) ? (int)$pageCounts['created-live'] : 0;
512
    }
513
514
    /**
515
     * Get the total number of pages created by the user, that have since been deleted.
516
     * @return int
517
     */
518 1
    public function countPagesCreatedDeleted()
519
    {
520 1
        $pageCounts = $this->getPairData();
521 1
        return isset($pageCounts['created-deleted']) ? (int)$pageCounts['created-deleted'] : 0;
522
    }
523
524
    /**
525
     * Get the total number of pages that have been deleted by the user.
526
     * @return int
527
     */
528
    public function countPagesDeleted()
529
    {
530
        $logCounts = $this->getLogCounts();
531
        return isset($logCounts['delete-delete']) ? (int)$logCounts['delete-delete'] : 0;
532
    }
533
534
    /**
535
     * Get the total number of pages moved by the user.
536
     * @return int
537
     */
538
    public function countPagesMoved()
539
    {
540
        $logCounts = $this->getLogCounts();
541
        return isset($logCounts['move-move']) ? (int)$logCounts['move-move'] : 0;
542
    }
543
544
    /**
545
     * Get the total number of times the user has blocked a user.
546
     * @return int
547
     */
548
    public function countBlocksSet()
549
    {
550
        $logCounts = $this->getLogCounts();
551
        $reBlock = isset($logCounts['block-block']) ? (int)$logCounts['block-block'] : 0;
552
        return $reBlock;
553
    }
554
555
    /**
556
     * Get the total number of times the user has re-blocked a user.
557
     * @return int
558
     */
559
    public function countReblocksSet()
560
    {
561
        $logCounts = $this->getLogCounts();
562
        $reBlock = isset($logCounts['block-reblock']) ? (int)$logCounts['block-reblock'] : 0;
563
        return $reBlock;
564
    }
565
566
    /**
567
     * Get the total number of times the user has unblocked a user.
568
     * @return int
569
     */
570
    public function countUnblocksSet()
571
    {
572
        $logCounts = $this->getLogCounts();
573
        return isset($logCounts['block-unblock']) ? (int)$logCounts['block-unblock'] : 0;
574
    }
575
576
    /**
577
     * Get the total number of blocks that have been lifted (i.e. unblocks) by this user.
578
     * @return int
579
     */
580
    public function countBlocksLifted()
581
    {
582
        $logCounts = $this->getLogCounts();
583
        return isset($logCounts['block-unblock']) ? (int)$logCounts['block-unblock'] : 0;
584
    }
585
586
    /**
587
     * Get the total number of times the user has been blocked.
588
     * @return int
589
     */
590
    public function countBlocksReceived()
591
    {
592
        $blocks = $this->getBlocks('received');
593
        return count($blocks);
594
    }
595
596
    /**
597
     * Get the length of the longest block the user received, in seconds.
598
     * @return int Number of seconds or false if it could not be determined.
599
     *   If the user is blocked, the time since the block is returned. If the block is
600
     *   indefinite, -1 is returned. 0 if there was never a block.
601
     */
602 6
    public function getLongestBlockSeconds()
603
    {
604 6
        if (isset($this->longestBlockSeconds)) {
605
            return $this->longestBlockSeconds;
606
        }
607
608 6
        $blocks = $this->getBlocks('received', false);
609 6
        $this->longestBlockSeconds = false;
610
611
        // If there was never a block, the longest was zero seconds.
612 6
        if (empty($blocks)) {
613
            return 0;
614
        }
615
616
        /**
617
         * Keep track of the last block so we can determine the duration
618
         * if the current block in the loop is an unblock.
619
         * @var int[] [
620
         *              Unix timestamp,
621
         *              Duration in seconds (-1 if indefinite)
622
         *            ]
623
         */
624 6
        $lastBlock = [null, null];
625
626 6
        foreach ($blocks as $index => $block) {
627 6
            list($timestamp, $duration) = $this->parseBlockLogEntry($block);
628
629 6
            if ($block['log_action'] === 'block') {
630
                // This is a new block, so first see if the duration of the last
631
                // block exceeded our longest duration. -1 duration means indefinite.
632 6
                if ($lastBlock[1] > $this->longestBlockSeconds || $lastBlock[1] === -1) {
633 2
                    $this->longestBlockSeconds = $lastBlock[1];
634
                }
635
636
                // Now set this as the last block.
637 6
                $lastBlock = [$timestamp, $duration];
638 3
            } elseif ($block['log_action'] === 'unblock') {
639
                // The last block was lifted. So the duration will be the time from when the
640
                // last block was set to the time of the unblock.
641 1
                $timeSinceLastBlock = $timestamp - $lastBlock[0];
642 1
                if ($timeSinceLastBlock > $this->longestBlockSeconds) {
643 1
                    $this->longestBlockSeconds = $timeSinceLastBlock;
644
645
                    // Reset the last block, as it has now been accounted for.
646 1
                    $lastBlock = null;
647
                }
648 2
            } elseif ($block['log_action'] === 'reblock' && $lastBlock[1] !== -1) {
649
                // The last block was modified. So we will adjust $lastBlock to include
650
                // the difference of the duration of the new reblock, and time since the last block.
651
                // $lastBlock is left unchanged if its duration was indefinite.
652 1
                $timeSinceLastBlock = $timestamp - $lastBlock[0];
653 6
                $lastBlock[1] = $timeSinceLastBlock + $duration;
654
            }
655
        }
656
657
        // If the last block was indefinite, we'll return that as the longest duration.
658 6
        if ($lastBlock[1] === -1) {
659 2
            return -1;
660
        }
661
662
        // Test if the last block is still active, and if so use the expiry as the duration.
663 4
        $lastBlockExpiry = $lastBlock[0] + $lastBlock[1];
664 4
        if ($lastBlockExpiry > time() && $lastBlockExpiry > $this->longestBlockSeconds) {
665 1
            $this->longestBlockSeconds = $lastBlock[1];
666
        // Otherwise, test if the duration of the last block is now the longest overall.
667 3
        } elseif ($lastBlock[1] > $this->longestBlockSeconds) {
668 2
            $this->longestBlockSeconds = $lastBlock[1];
669
        }
670
671 4
        return $this->longestBlockSeconds;
672
    }
673
674
    /**
675
     * Given a block log entry from the database, get the timestamp and duration in seconds.
676
     * @param  mixed[] $block Block log entry as fetched via self::getBlocks()
677
     * @return int[] [
678
     *                 Unix timestamp,
679
     *                 Duration in seconds (-1 if indefinite, null if unparsable or unblock)
680
     *               ]
681
     */
682 11
    public function parseBlockLogEntry($block)
683
    {
684 11
        $timestamp = strtotime($block['log_timestamp']);
685 11
        $duration = null;
686
687
        // First check if the string is serialized, and if so parse it to get the block duration.
688 11
        if (@unserialize($block['log_params']) !== false) {
689 8
            $parsedParams = unserialize($block['log_params']);
690 8
            $durationStr = isset($parsedParams['5::duration']) ? $parsedParams['5::duration'] : null;
691
        } else {
692
            // Old format, the duration in English + block options separated by new lines.
693 4
            $durationStr = explode("\n", $block['log_params'])[0];
694
        }
695
696 11
        if (in_array($durationStr, ['indefinite', 'infinity', 'infinite'])) {
697 3
            $duration = -1;
698
        }
699
700
        // Make sure $durationStr is valid just in case it is in an older, unpredictable format.
701
        // If invalid, $duration is left as null.
702 11
        if (strtotime($durationStr)) {
703 8
            $expiry = strtotime($durationStr, $timestamp);
704 8
            $duration = $expiry - $timestamp;
705
        }
706
707 11
        return [$timestamp, $duration];
708
    }
709
710
    /**
711
     * Get the total number of pages protected by the user.
712
     * @return int
713
     */
714
    public function countPagesProtected()
715
    {
716
        $logCounts = $this->getLogCounts();
717
        return isset($logCounts['protect-protect']) ? (int)$logCounts['protect-protect'] : 0;
718
    }
719
720
    /**
721
     * Get the total number of pages reprotected by the user.
722
     * @return int
723
     */
724
    public function countPagesReprotected()
725
    {
726
        $logCounts = $this->getLogCounts();
727
        return isset($logCounts['protect-modify']) ? (int)$logCounts['protect-modify'] : 0;
728
    }
729
730
    /**
731
     * Get the total number of pages unprotected by the user.
732
     * @return int
733
     */
734
    public function countPagesUnprotected()
735
    {
736
        $logCounts = $this->getLogCounts();
737
        return isset($logCounts['protect-unprotect']) ? (int)$logCounts['protect-unprotect'] : 0;
738
    }
739
740
    /**
741
     * Get the total number of edits deleted by the user.
742
     * @return int
743
     */
744
    public function countEditsDeleted()
745
    {
746
        $logCounts = $this->getLogCounts();
747
        return isset($logCounts['delete-revision']) ? (int)$logCounts['delete-revision'] : 0;
748
    }
749
750
    /**
751
     * Get the total number of pages restored by the user.
752
     * @return int
753
     */
754
    public function countPagesRestored()
755
    {
756
        $logCounts = $this->getLogCounts();
757
        return isset($logCounts['delete-restore']) ? (int)$logCounts['delete-restore'] : 0;
758
    }
759
760
    /**
761
     * Get the total number of times the user has modified the rights of a user.
762
     * @return int
763
     */
764
    public function countRightsModified()
765
    {
766
        $logCounts = $this->getLogCounts();
767
        return isset($logCounts['rights-rights']) ? (int)$logCounts['rights-rights'] : 0;
768
    }
769
770
    /**
771
     * Get the total number of pages imported by the user (through any import mechanism:
772
     * interwiki, or XML upload).
773
     * @return int
774
     */
775
    public function countPagesImported()
776
    {
777
        $logCounts = $this->getLogCounts();
778
        $import = isset($logCounts['import-import']) ? (int)$logCounts['import-import'] : 0;
779
        $interwiki = isset($logCounts['import-interwiki']) ? (int)$logCounts['import-interwiki'] : 0;
780
        $upload = isset($logCounts['import-upload']) ? (int)$logCounts['import-upload'] : 0;
781
        return $import + $interwiki + $upload;
782
    }
783
784
    /**
785
     * Get the average number of edits per page (including deleted revisions and pages).
786
     * @return float
787
     */
788
    public function averageRevisionsPerPage()
789
    {
790
        if ($this->countAllPagesEdited() == 0) {
791
            return 0;
792
        }
793
        return round($this->countAllRevisions() / $this->countAllPagesEdited(), 3);
794
    }
795
796
    /**
797
     * Average number of edits made per day.
798
     * @return float
799
     */
800
    public function averageRevisionsPerDay()
801
    {
802
        if ($this->getDays() == 0) {
803
            return 0;
804
        }
805
        return round($this->countAllRevisions() / $this->getDays(), 3);
806
    }
807
808
    /**
809
     * Get the total number of edits made by the user with semi-automating tools.
810
     */
811
    public function countAutomatedEdits()
812
    {
813
        if ($this->autoEditCount) {
814
            return $this->autoEditCount;
815
        }
816
        $this->autoEditCount = $this->getRepository()->countAutomatedEdits(
817
            $this->project,
818
            $this->user
819
        );
820
        return $this->autoEditCount;
821
    }
822
823
    /**
824
     * Get the count of (non-deleted) edits made in the given timeframe to now.
825
     * @param string $time One of 'day', 'week', 'month', or 'year'.
826
     * @return int The total number of live edits.
827
     */
828
    public function countRevisionsInLast($time)
829
    {
830
        $revCounts = $this->getPairData();
831
        return isset($revCounts[$time]) ? $revCounts[$time] : 0;
832
    }
833
834
    /**
835
     * Get the date and time of the user's first edit.
836
     * @return DateTime|bool The time of the first revision, or false.
837
     */
838 2
    public function datetimeFirstRevision()
839
    {
840 2
        $revDates = $this->getPairData();
841 2
        return isset($revDates['first']) ? new DateTime($revDates['first']) : false;
842
    }
843
844
    /**
845
     * Get the date and time of the user's first edit.
846
     * @return DateTime|bool The time of the last revision, or false.
847
     */
848 2
    public function datetimeLastRevision()
849
    {
850 2
        $revDates = $this->getPairData();
851 2
        return isset($revDates['last']) ? new DateTime($revDates['last']) : false;
852
    }
853
854
    /**
855
     * Get the number of days between the first and last edits.
856
     * If there's only one edit, this is counted as one day.
857
     * @return int
858
     */
859 2
    public function getDays()
860
    {
861 2
        $first = $this->datetimeFirstRevision();
862 2
        $last = $this->datetimeLastRevision();
863 2
        if ($first === false || $last === false) {
864
            return 0;
865
        }
866 2
        $days = $last->diff($first)->days;
867 2
        return $days > 0 ? $days : 1;
868
    }
869
870
    /**
871
     * Get the total number of files uploaded (including those now deleted).
872
     * @return int
873
     */
874
    public function countFilesUploaded()
875
    {
876
        $logCounts = $this->getLogCounts();
877
        return $logCounts['upload-upload'] ?: 0;
878
    }
879
880
    /**
881
     * Get the total number of files uploaded to Commons (including those now deleted).
882
     * This is only applicable for WMF labs installations.
883
     * @return int
884
     */
885
    public function countFilesUploadedCommons()
886
    {
887
        $logCounts = $this->getLogCounts();
888
        return $logCounts['files_uploaded_commons'] ?: 0;
889
    }
890
891
    /**
892
     * Get the total number of revisions the user has sent thanks for.
893
     * @return int
894
     */
895
    public function thanks()
896
    {
897
        $logCounts = $this->getLogCounts();
898
        return $logCounts['thanks-thank'] ?: 0;
899
    }
900
901
    /**
902
     * Get the total number of approvals
903
     * @return int
904
     */
905
    public function approvals()
906
    {
907
        $logCounts = $this->getLogCounts();
908
        $total = $logCounts['review-approve'] +
909
        (!empty($logCounts['review-approve-a']) ? $logCounts['review-approve-a'] : 0) +
910
        (!empty($logCounts['review-approve-i']) ? $logCounts['review-approve-i'] : 0) +
911
        (!empty($logCounts['review-approve-ia']) ? $logCounts['review-approve-ia'] : 0);
912
        return $total;
913
    }
914
915
    /**
916
     * Get the total number of patrols performed by the user.
917
     * @return int
918
     */
919
    public function patrols()
920
    {
921
        $logCounts = $this->getLogCounts();
922
        return $logCounts['patrol-patrol'] ?: 0;
923
    }
924
925
    /**
926
     * Get the total number of accounts created by the user.
927
     * @return int
928
     */
929
    public function accountsCreated()
930
    {
931
        $logCounts = $this->getLogCounts();
932
        $create2 = $logCounts['newusers-create2'] ?: 0;
933
        $byemail = $logCounts['newusers-byemail'] ?: 0;
934
        return $create2 + $byemail;
935
    }
936
937
    /**
938
     * Get the given user's total edit counts per namespace.
939
     * @return integer[] Array keys are namespace IDs, values are the edit counts.
940
     */
941 1
    public function namespaceTotals()
942
    {
943 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...
944
            return $this->namespaceTotals;
945
        }
946 1
        $counts = $this->getRepository()->getNamespaceTotals($this->project, $this->user);
947 1
        arsort($counts);
948 1
        $this->namespaceTotals = $counts;
949 1
        return $counts;
950
    }
951
952
    /**
953
     * Get a summary of the times of day and the days of the week that the user has edited.
954
     * @return string[]
955
     */
956
    public function timeCard()
957
    {
958
        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...
959
            return $this->timeCardData;
960
        }
961
        $totals = $this->getRepository()->getTimeCard($this->project, $this->user);
962
963
        // Scale the radii: get the max, then scale each radius.
964
        // This looks inefficient, but there's a max of 72 elements in this array.
965
        $max = 0;
966
        foreach ($totals as $total) {
967
            $max = max($max, $total['value']);
968
        }
969
        foreach ($totals as &$total) {
970
            $total['value'] = round($total['value'] / $max * 100);
971
        }
972
973
        // Fill in zeros for timeslots that have no values.
974
        $sortedTotals = [];
975
        $index = 0;
976
        $sortedIndex = 0;
977
        foreach (range(1, 7) as $day) {
978
            foreach (range(0, 24, 2) as $hour) {
979
                if (isset($totals[$index]) && (int)$totals[$index]['x'] === $hour) {
980
                    $sortedTotals[$sortedIndex] = $totals[$index];
981
                    $index++;
982
                } else {
983
                    $sortedTotals[$sortedIndex] = [
984
                        'y' => $day,
985
                        'x' => $hour,
986
                        'value' => 0,
987
                    ];
988
                }
989
                $sortedIndex++;
990
            }
991
        }
992
993
        $this->timeCardData = $sortedTotals;
0 ignored issues
show
Documentation Bug introduced by
$sortedTotals is of type array<mixed,array<string,mixed|integer>|mixed>, but the property $timeCardData was declared to be of type string[]. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof 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 given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
994
        return $sortedTotals;
995
    }
996
997
    /**
998
     * Get the total numbers of edits per month.
999
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING*
1000
     *   so we can mock the current DateTime.
1001
     * @return mixed[] With keys 'yearLabels', 'monthLabels' and 'totals',
1002
     *   the latter keyed by namespace, year and then month.
1003
     */
1004 2
    public function monthCounts($currentTime = null)
1005
    {
1006 2
        if (isset($this->monthCounts)) {
1007 1
            return $this->monthCounts;
1008
        }
1009
1010
        // Set to current month if we're not unit-testing
1011 2
        if (!($currentTime instanceof DateTime)) {
1012
            $currentTime = new DateTime('last day of this month');
1013
        }
1014
1015 2
        $totals = $this->getRepository()->getMonthCounts($this->project, $this->user);
1016
        $out = [
1017 2
            'yearLabels' => [],  // labels for years
1018
            'monthLabels' => [], // labels for months
1019
            'totals' => [], // actual totals, grouped by namespace, year and then month
1020
        ];
1021
1022
        /** @var DateTime Keep track of the date of their first edit. */
1023 2
        $firstEdit = new DateTime();
1024
1025 2
        list($out, $firstEdit) = $this->fillInMonthCounts($out, $totals, $firstEdit);
1026
1027 2
        $dateRange = new DatePeriod(
1028 2
            $firstEdit,
1029 2
            new DateInterval('P1M'),
1030 2
            $currentTime->modify('first day of this month')
1031
        );
1032
1033 2
        $out = $this->fillInMonthTotalsAndLabels($out, $dateRange);
1034
1035
        // One more set of loops to sort by year/month
1036 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

1036
        foreach (array_keys(/** @scrutinizer ignore-type */ $out['totals']) as $nsId) {
Loading history...
1037 2
            ksort($out['totals'][$nsId]);
1038
1039 2
            foreach ($out['totals'][$nsId] as &$yearData) {
1040 2
                ksort($yearData);
1041
            }
1042
        }
1043
1044
        // Finally, sort the namespaces
1045 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

1045
        ksort(/** @scrutinizer ignore-type */ $out['totals']);
Loading history...
1046
1047 2
        $this->monthCounts = $out;
1048 2
        return $out;
1049
    }
1050
1051
    /**
1052
     * Loop through the database results and fill in the values
1053
     * for the months that we have data for.
1054
     * @param array $out
1055
     * @param string[] $totals
1056
     * @param DateTime $firstEdit
1057
     * @return array [
1058
     *           string[] - Modified $out filled with month stats,
1059
     *           DateTime - timestamp of first edit
1060
     *         ]
1061
     * Tests covered in self::monthCounts().
1062
     * @codeCoverageIgnore
1063
     */
1064
    private function fillInMonthCounts($out, $totals, $firstEdit)
1065
    {
1066
        foreach ($totals as $total) {
1067
            // Keep track of first edit
1068
            $date = new DateTime($total['year'].'-'.$total['month'].'-01');
1069
            if ($date < $firstEdit) {
1070
                $firstEdit = $date;
1071
            }
1072
1073
            // Collate the counts by namespace, and then year and month.
1074
            $ns = $total['page_namespace'];
1075
            if (!isset($out['totals'][$ns])) {
1076
                $out['totals'][$ns] = [];
1077
            }
1078
1079
            // Start array for this year if not already present.
1080
            if (!isset($out['totals'][$ns][$total['year']])) {
1081
                $out['totals'][$ns][$total['year']] = [];
1082
            }
1083
1084
            $out['totals'][$ns][$total['year']][$total['month']] = (int) $total['count'];
1085
        }
1086
1087
        return [$out, $firstEdit];
1088
    }
1089
1090
    /**
1091
     * Given the output array, fill each month's totals and labels.
1092
     * @param array $out
1093
     * @param DatePeriod $dateRange From first edit to present.
1094
     * @return string[] - Modified $out filled with month stats.
1095
     * Tests covered in self::monthCounts().
1096
     * @codeCoverageIgnore
1097
     */
1098
    private function fillInMonthTotalsAndLabels($out, DatePeriod $dateRange)
1099
    {
1100
        foreach ($dateRange as $monthObj) {
1101
            $year = (int) $monthObj->format('Y');
1102
            $yearLabel = $this->i18n->dateFormat($monthObj, 'yyyy');
1103
            $month = (int) $monthObj->format('n');
1104
            $monthLabel = $this->i18n->dateFormat($monthObj, 'yyyy-MM');
1105
1106
            // Fill in labels
1107
            $out['monthLabels'][] = $monthLabel;
1108
            if (!in_array($yearLabel, $out['yearLabels'])) {
1109
                $out['yearLabels'][] = $yearLabel;
1110
            }
1111
1112
            foreach (array_keys($out['totals']) as $nsId) {
1113
                if (!isset($out['totals'][$nsId][$year])) {
1114
                    $out['totals'][$nsId][$year] = [];
1115
                }
1116
1117
                if (!isset($out['totals'][$nsId][$year][$month])) {
1118
                    $out['totals'][$nsId][$year][$month] = 0;
1119
                }
1120
            }
1121
        }
1122
1123
        return $out;
1124
    }
1125
1126
    /**
1127
     * Get total edits for each month. Used in wikitext export.
1128
     * @param  null|DateTime $currentTime *USED ONLY FOR UNIT TESTING*
1129
     * @return array With the months as the keys, counts as the values.
1130
     */
1131 1
    public function monthTotals($currentTime = null)
1132
    {
1133 1
        $months = [];
1134
1135 1
        foreach ($this->monthCounts($currentTime)['totals'] as $nsId => $nsData) {
1136 1
            foreach ($nsData as $year => $monthData) {
1137 1
                foreach ($monthData as $month => $count) {
1138 1
                    $monthLabel = $year.'-'.sprintf('%02d', $month);
1139 1
                    if (!isset($months[$monthLabel])) {
1140 1
                        $months[$monthLabel] = 0;
1141
                    }
1142 1
                    $months[$monthLabel] += $count;
1143
                }
1144
            }
1145
        }
1146
1147 1
        return $months;
1148
    }
1149
1150
    /**
1151
     * Get the total numbers of edits per year.
1152
     * @param null|DateTime $currentTime - *USED ONLY FOR UNIT TESTING*
1153
     *   so we can mock the current DateTime.
1154
     * @return mixed[] With keys 'yearLabels' and 'totals', the latter
1155
     *   keyed by namespace then year.
1156
     */
1157 2
    public function yearCounts($currentTime = null)
1158
    {
1159 2
        if (isset($this->yearCounts)) {
1160
            return $this->yearCounts;
1161
        }
1162
1163 2
        $out = $this->monthCounts($currentTime);
1164
1165 2
        foreach ($out['totals'] as $nsId => $years) {
1166 2
            foreach ($years as $year => $months) {
1167 2
                $out['totals'][$nsId][$year] = array_sum(array_values($months));
1168
            }
1169
        }
1170
1171 2
        $this->yearCounts = $out;
1172 2
        return $out;
1173
    }
1174
1175
    /**
1176
     * Get total edits for each year. Used in wikitext export.
1177
     * @param  null|DateTime $currentTime *USED ONLY FOR UNIT TESTING*
1178
     * @return array With the years as the keys, counts as the values.
1179
     */
1180 1
    public function yearTotals($currentTime = null)
1181
    {
1182 1
        $years = [];
1183
1184 1
        foreach ($this->yearCounts($currentTime)['totals'] as $nsId => $nsData) {
1185 1
            foreach ($nsData as $year => $count) {
1186 1
                if (!isset($years[$year])) {
1187 1
                    $years[$year] = 0;
1188
                }
1189 1
                $years[$year] += $count;
1190
            }
1191
        }
1192
1193 1
        return $years;
1194
    }
1195
1196
    /**
1197
     * Get the total edit counts for the top n projects of this user.
1198
     * @param int $numProjects
1199
     * @return mixed[] Each element has 'total' and 'project' keys.
1200
     */
1201 1
    public function globalEditCountsTopN($numProjects = 10)
1202
    {
1203
        // Get counts.
1204 1
        $editCounts = $this->globalEditCounts(true);
1205
        // Truncate, and return.
1206 1
        return array_slice($editCounts, 0, $numProjects);
1207
    }
1208
1209
    /**
1210
     * Get the total number of edits excluding the top n.
1211
     * @param int $numProjects
1212
     * @return int
1213
     */
1214 1
    public function globalEditCountWithoutTopN($numProjects = 10)
1215
    {
1216 1
        $editCounts = $this->globalEditCounts(true);
1217 1
        $bottomM = array_slice($editCounts, $numProjects);
1218 1
        $total = 0;
1219 1
        foreach ($bottomM as $editCount) {
1220 1
            $total += $editCount['total'];
1221
        }
1222 1
        return $total;
1223
    }
1224
1225
    /**
1226
     * Get the grand total of all edits on all projects.
1227
     * @return int
1228
     */
1229 1
    public function globalEditCount()
1230
    {
1231 1
        $total = 0;
1232 1
        foreach ($this->globalEditCounts() as $editCount) {
1233 1
            $total += $editCount['total'];
1234
        }
1235 1
        return $total;
1236
    }
1237
1238
    /**
1239
     * Get the total revision counts for all projects for this user.
1240
     * @param bool $sorted Whether to sort the list by total, or not.
1241
     * @return mixed[] Each element has 'total' and 'project' keys.
1242
     */
1243 1
    public function globalEditCounts($sorted = false)
1244
    {
1245 1
        if (empty($this->globalEditCounts)) {
1246 1
            $this->globalEditCounts = $this->getRepository()
1247 1
                ->globalEditCounts($this->user, $this->project);
1248
        }
1249
1250 1
        if ($sorted) {
1251
            // Sort.
1252 1
            uasort($this->globalEditCounts, function ($a, $b) {
1253 1
                return $b['total'] - $a['total'];
1254 1
            });
1255
        }
1256
1257 1
        return $this->globalEditCounts;
1258
    }
1259
1260
    /**
1261
     * Get the most recent n revisions across all projects.
1262
     * @param int $max The maximum number of revisions to return.
1263
     * @return Edit[]
1264
     */
1265
    public function globalEdits($max)
1266
    {
1267
        if (is_array($this->globalEdits)) {
0 ignored issues
show
introduced by
The condition is_array($this->globalEdits) is always true.
Loading history...
1268
            return $this->globalEdits;
1269
        }
1270
1271
        // Collect all projects with any edits.
1272
        $projects = [];
1273
        foreach ($this->globalEditCounts() as $editCount) {
1274
            // Don't query revisions if there aren't any.
1275
            if ($editCount['total'] == 0) {
1276
                continue;
1277
            }
1278
            $projects[$editCount['project']->getDatabaseName()] = $editCount['project'];
1279
        }
1280
1281
        if (count($projects) === 0) {
1282
            return [];
1283
        }
1284
1285
        // Get all revisions for those projects.
1286
        $globalRevisionsData = $this->getRepository()
1287
            ->getRevisions($projects, $this->user, $max);
1288
        $globalEdits = [];
1289
        foreach ($globalRevisionsData as $revision) {
1290
            /** @var Project $project */
1291
            $project = $projects[$revision['project_name']];
1292
            $nsName = '';
1293
            if ($revision['page_namespace']) {
1294
                $nsName = $project->getNamespaces()[$revision['page_namespace']];
1295
            }
1296
            $page = $project->getRepository()
1297
                ->getPage($project, $nsName . ':' . $revision['page_title']);
1298
            $edit = new Edit($page, $revision);
1299
            $globalEdits[$edit->getTimestamp()->getTimestamp().'-'.$edit->getId()] = $edit;
1300
        }
1301
1302
        // Sort and prune, before adding more.
1303
        krsort($globalEdits);
1304
        $this->globalEdits = array_slice($globalEdits, 0, $max);
1305
1306
        return $this->globalEdits;
1307
    }
1308
1309
    /**
1310
     * Get average edit size, and number of large and small edits.
1311
     * @return int[]
1312
     */
1313
    public function getEditSizeData()
1314
    {
1315
        if (!is_array($this->editSizeData)) {
0 ignored issues
show
introduced by
The condition is_array($this->editSizeData) is always true.
Loading history...
1316
            $this->editSizeData = $this->getRepository()
1317
                ->getEditSizeData($this->project, $this->user);
1318
        }
1319
        return $this->editSizeData;
1320
    }
1321
1322
    /**
1323
     * Get the total edit count of this user or 5,000 if they've made more than 5,000 edits.
1324
     * This is used to ensure percentages of small and large edits are computed properly.
1325
     * @return int
1326
     */
1327 1
    public function countLast5000()
1328
    {
1329 1
        return $this->countLiveRevisions() > 5000 ? 5000 : $this->countLiveRevisions();
1330
    }
1331
1332
    /**
1333
     * Get the number of edits under 20 bytes of the user's past 5000 edits.
1334
     * @return int
1335
     */
1336
    public function countSmallEdits()
1337
    {
1338
        $editSizeData = $this->getEditSizeData();
1339
        return isset($editSizeData['small_edits']) ? (int) $editSizeData['small_edits'] : 0;
1340
    }
1341
1342
    /**
1343
     * Get the total number of edits over 1000 bytes of the user's past 5000 edits.
1344
     * @return int
1345
     */
1346
    public function countLargeEdits()
1347
    {
1348
        $editSizeData = $this->getEditSizeData();
1349
        return isset($editSizeData['large_edits']) ? (int) $editSizeData['large_edits'] : 0;
1350
    }
1351
1352
    /**
1353
     * Get the average size of the user's past 5000 edits.
1354
     * @return float Size in bytes.
1355
     */
1356
    public function averageEditSize()
1357
    {
1358
        $editSizeData = $this->getEditSizeData();
1359
        if (isset($editSizeData['average_size'])) {
1360
            return round($editSizeData['average_size'], 3);
1361
        } else {
1362
            return 0;
1363
        }
1364
    }
1365
}
1366