Test Setup Failed
Pull Request — main (#426)
by MusikAnimal
17:10 queued 11:44
created

ArticleInfo::getMonthLabels()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the ArticleInfo class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace App\Model;
9
10
use App\Helper\I18nHelper;
11
use DateTime;
12
13
/**
14
 * An ArticleInfo provides statistics about a page on a project.
15
 */
16
class ArticleInfo extends ArticleInfoApi
17
{
18
    /** @var I18nHelper For i18n and l10n. */
19
    protected $i18n;
20
21
    /** @var int Number of revisions that were actually processed. */
22
    protected $numRevisionsProcessed;
23
24
    /**
25
     * Various statistics about editors to the page. These are not User objects
26
     * so as to preserve memory.
27
     * @var mixed[]
28
     */
29
    protected $editors = [];
30
31
    /** @var mixed[] The top 10 editors to the page by number of edits. */
32
    protected $topTenEditorsByEdits;
33
34
    /** @var mixed[] The top 10 editors to the page by added text. */
35
    protected $topTenEditorsByAdded;
36
37
    /** @var int Number of edits made by the top 10 editors. */
38
    protected $topTenCount;
39
40
    /** @var mixed[] Various counts about each individual year and month of the page's history. */
41
    protected $yearMonthCounts;
42
43
    /** @var string[] Localized labels for the years, to be used in the 'Year counts' chart. */
44
    protected $yearLabels = [];
45
46
    /** @var string[] Localized labels for the months, to be used in the 'Month counts' chart. */
47
    protected $monthLabels = [];
48
49
    /** @var Edit The first edit to the page. */
50
    protected $firstEdit;
51
52
    /** @var Edit The last edit to the page. */
53
    protected $lastEdit;
54
55
    /** @var Edit Edit that made the largest addition by number of bytes. */
56
    protected $maxAddition;
57
58
    /** @var Edit Edit that made the largest deletion by number of bytes. */
59
    protected $maxDeletion;
60
61
    /**
62
     * Maximum number of edits that were created across all months. This is used as a comparison
63
     * for the bar charts in the months section.
64
     * @var int
65
     */
66
    protected $maxEditsPerMonth;
67
68
    /** @var string[][] List of (semi-)automated tools that were used to edit the page. */
69
    protected $tools;
70
71
    /**
72
     * Total number of bytes added throughout the page's history. This is used as a comparison
73
     * when computing the top 10 editors by added text.
74
     * @var int
75
     */
76
    protected $addedBytes = 0;
77
78
    /** @var int Number of days between first and last edit. */
79
    protected $totalDays;
80
81
    /** @var int Number of minor edits to the page. */
82
    protected $minorCount = 0;
83
84
    /** @var int Number of anonymous edits to the page. */
85
    protected $anonCount = 0;
86
87
    /** @var int Number of automated edits to the page. */
88
    protected $automatedCount = 0;
89
90
    /** @var int Number of edits to the page that were reverted with the subsequent edit. */
91
    protected $revertCount = 0;
92
93
    /** @var int[] The "edits per <time>" counts. */
94
    protected $countHistory = [
95
        'day' => 0,
96
        'week' => 0,
97
        'month' => 0,
98
        'year' => 0,
99
    ];
100
101
    /**
102
     * Make the I18nHelper accessible to ArticleInfo.
103
     * @param I18nHelper $i18n
104
     * @codeCoverageIgnore
105
     */
106
    public function setI18nHelper(I18nHelper $i18n): void
107
    {
108
        $this->i18n = $i18n;
109
    }
110
111
    /**
112
     * Get the day of last date we should show in the month/year sections,
113
     * based on $this->end or the current date.
114
     * @return int As Unix timestamp.
115
     */
116
    private function getLastDay(): int
117
    {
118
        if (is_int($this->end)) {
119
            return (new DateTime("@$this->end"))
120
                ->modify('last day of this month')
121
                ->getTimestamp();
122
        } else {
123
            return strtotime('last day of this month');
124
        }
125
    }
126
127
    /**
128
     * Return the start/end date values as associative array, with YYYY-MM-DD as the date format.
129
     * This is used mainly as a helper to pass to the pageviews Twig macros.
130
     * @return array
131
     */
132
    public function getDateParams(): array
133
    {
134
        if (!$this->hasDateRange()) {
135
            return [];
136
        }
137
138
        $ret = [
139
            'start' => $this->firstEdit->getTimestamp()->format('Y-m-d'),
140
            'end' => $this->lastEdit->getTimestamp()->format('Y-m-d'),
141
        ];
142
143
        if (is_int($this->start)) {
144
            $ret['start'] = date('Y-m-d', $this->start);
145
        }
146
        if (is_int($this->end)) {
147
            $ret['end'] = date('Y-m-d', $this->end);
148
        }
149
150
        return $ret;
151
    }
152
153
    /**
154
     * Get the number of revisions that are actually getting processed. This goes by the app.max_page_revisions
155
     * parameter, or the actual number of revisions, whichever is smaller.
156
     * @return int
157
     */
158
    public function getNumRevisionsProcessed(): int
159
    {
160
        if (isset($this->numRevisionsProcessed)) {
161
            return $this->numRevisionsProcessed;
162
        }
163
164
        if ($this->tooManyRevisions()) {
165
            $this->numRevisionsProcessed = $this->getMaxRevisions();
166
        } else {
167
            $this->numRevisionsProcessed = $this->getNumRevisions();
168
        }
169
170
        return $this->numRevisionsProcessed;
171
    }
172
173
    /**
174
     * Fetch and store all the data we need to show the ArticleInfo view.
175
     * @codeCoverageIgnore
176
     */
177
    public function prepareData(): void
178
    {
179
        $this->parseHistory();
180
        $this->setLogsEvents();
181
182
        // Bots need to be set before setting top 10 counts.
183
        $this->bots = $this->getBots();
184
185
        $this->doPostPrecessing();
186
    }
187
188
    /**
189
     * Get the number of editors that edited the page.
190
     * @return int
191
     */
192
    public function getNumEditors(): int
193
    {
194
        return count($this->editors);
195
    }
196
197
    /**
198
     * Get the number of days between the first and last edit.
199
     * @return int
200
     */
201
    public function getTotalDays(): int
202
    {
203
        if (isset($this->totalDays)) {
204
            return $this->totalDays;
205
        }
206
        $dateFirst = $this->firstEdit->getTimestamp();
207
        $dateLast = $this->lastEdit->getTimestamp();
208
        $interval = date_diff($dateLast, $dateFirst, true);
209
        $this->totalDays = (int)$interval->format('%a');
210
        return $this->totalDays;
211
    }
212
213
    /**
214
     * Returns length of the page.
215
     * @return int
216
     */
217
    public function getLength(): int
218
    {
219
        if ($this->hasDateRange()) {
220
            return $this->lastEdit->getLength();
221
        }
222
223
        return $this->page->getLength();
224
    }
225
226
    /**
227
     * Get the average number of days between edits to the page.
228
     * @return float
229
     */
230
    public function averageDaysPerEdit(): float
231
    {
232
        return round($this->getTotalDays() / $this->getNumRevisionsProcessed(), 1);
233
    }
234
235
    /**
236
     * Get the average number of edits per day to the page.
237
     * @return float
238
     */
239
    public function editsPerDay(): float
240
    {
241
        $editsPerDay = $this->getTotalDays()
242
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / (365 / 12 / 24))
243
            : 0;
244
        return round($editsPerDay, 1);
245
    }
246
247
    /**
248
     * Get the average number of edits per month to the page.
249
     * @return float
250
     */
251
    public function editsPerMonth(): float
252
    {
253
        $editsPerMonth = $this->getTotalDays()
254
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / (365 / 12))
255
            : 0;
256
        return min($this->getNumRevisionsProcessed(), round($editsPerMonth, 1));
257
    }
258
259
    /**
260
     * Get the average number of edits per year to the page.
261
     * @return float
262
     */
263
    public function editsPerYear(): float
264
    {
265
        $editsPerYear = $this->getTotalDays()
266
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / 365)
267
            : 0;
268
        return min($this->getNumRevisionsProcessed(), round($editsPerYear, 1));
269
    }
270
271
    /**
272
     * Get the average number of edits per editor.
273
     * @return float
274
     */
275
    public function editsPerEditor(): float
276
    {
277
        if (count($this->editors) > 0) {
278
            return round($this->getNumRevisionsProcessed() / count($this->editors), 1);
279
        }
280
281
        // To prevent division by zero error; can happen if all usernames are removed (see T303724).
282
        return 0;
283
    }
284
285
    /**
286
     * Get the percentage of minor edits to the page.
287
     * @return float
288
     */
289
    public function minorPercentage(): float
290
    {
291
        return round(
292
            ($this->minorCount / $this->getNumRevisionsProcessed()) * 100,
293
            1
294
        );
295
    }
296
297
    /**
298
     * Get the percentage of anonymous edits to the page.
299
     * @return float
300
     */
301
    public function anonPercentage(): float
302
    {
303
        return round(
304
            ($this->anonCount / $this->getNumRevisionsProcessed()) * 100,
305
            1
306
        );
307
    }
308
309
    /**
310
     * Get the percentage of edits made by the top 10 editors.
311
     * @return float
312
     */
313
    public function topTenPercentage(): float
314
    {
315
        return round(($this->topTenCount / $this->getNumRevisionsProcessed()) * 100, 1);
316
    }
317
318
    /**
319
     * Get the number of automated edits made to the page.
320
     * @return int
321
     */
322
    public function getAutomatedCount(): int
323
    {
324
        return $this->automatedCount;
325
    }
326
327
    /**
328
     * Get the number of edits to the page that were reverted with the subsequent edit.
329
     * @return int
330
     */
331
    public function getRevertCount(): int
332
    {
333
        return $this->revertCount;
334
    }
335
336
    /**
337
     * Get the number of edits to the page made by logged out users.
338
     * @return int
339
     */
340
    public function getAnonCount(): int
341
    {
342
        return $this->anonCount;
343
    }
344
345
    /**
346
     * Get the number of minor edits to the page.
347
     * @return int
348
     */
349
    public function getMinorCount(): int
350
    {
351
        return $this->minorCount;
352
    }
353
354
    /**
355
     * Get the number of edits to the page made in the past day, week, month and year.
356
     * @return int[] With keys 'day', 'week', 'month' and 'year'.
357
     */
358
    public function getCountHistory(): array
359
    {
360
        return $this->countHistory;
361
    }
362
363
    /**
364
     * Get the number of edits to the page made by the top 10 editors.
365
     * @return int
366
     */
367
    public function getTopTenCount(): int
368
    {
369
        return $this->topTenCount;
370
    }
371
372
    /**
373
     * Get the first edit to the page.
374
     * @return Edit
375
     */
376
    public function getFirstEdit(): Edit
377
    {
378
        return $this->firstEdit;
379
    }
380
381
    /**
382
     * Get the last edit to the page.
383
     * @return Edit
384
     */
385
    public function getLastEdit(): Edit
386
    {
387
        return $this->lastEdit;
388
    }
389
390
    /**
391
     * Get the edit that made the largest addition to the page (by number of bytes).
392
     * @return Edit|null
393
     */
394
    public function getMaxAddition(): ?Edit
395
    {
396
        return $this->maxAddition;
397
    }
398
399
    /**
400
     * Get the edit that made the largest removal to the page (by number of bytes).
401
     * @return Edit|null
402
     */
403
    public function getMaxDeletion(): ?Edit
404
    {
405
        return $this->maxDeletion;
406
    }
407
408
    /**
409
     * Get the list of editors to the page, including various statistics.
410
     * @return mixed[]
411
     */
412
    public function getEditors(): array
413
    {
414
        return $this->editors;
415
    }
416
417
    /**
418
     * Get usernames of human editors (not bots).
419
     * @param int|null $limit
420
     * @return string[]
421
     */
422
    public function getHumans(?int $limit = null): array
423
    {
424
        return array_slice(array_diff(array_keys($this->getEditors()), array_keys($this->getBots())), 0, $limit);
425
    }
426
427
    /**
428
     * Get the list of the top editors to the page (by edits), including various statistics.
429
     * @return mixed[]
430
     */
431
    public function topTenEditorsByEdits(): array
432
    {
433
        return $this->topTenEditorsByEdits;
434
    }
435
436
    /**
437
     * Get the list of the top editors to the page (by added text), including various statistics.
438
     * @return mixed[]
439
     */
440
    public function topTenEditorsByAdded(): array
441
    {
442
        return $this->topTenEditorsByAdded;
443
    }
444
445
    /**
446
     * Get various counts about each individual year and month of the page's history.
447
     * @return mixed[]
448
     */
449
    public function getYearMonthCounts(): array
450
    {
451
        return $this->yearMonthCounts;
452
    }
453
454
    /**
455
     * Get the localized labels for the 'Year counts' chart.
456
     * @return string[]
457
     */
458
    public function getYearLabels(): array
459
    {
460
        return $this->yearLabels;
461
    }
462
463
    /**
464
     * Get the localized labels for the 'Month counts' chart.
465
     * @return string[]
466
     */
467
    public function getMonthLabels(): array
468
    {
469
        return $this->monthLabels;
470
    }
471
472
    /**
473
     * Get the maximum number of edits that were created across all months. This is used as a
474
     * comparison for the bar charts in the months section.
475
     * @return int
476
     */
477
    public function getMaxEditsPerMonth(): int
478
    {
479
        return $this->maxEditsPerMonth;
480
    }
481
482
    /**
483
     * Get a list of (semi-)automated tools that were used to edit the page, including
484
     * the number of times they were used, and a link to the tool's homepage.
485
     * @return string[]
486
     */
487
    public function getTools(): array
488
    {
489
        return $this->tools;
490
    }
491
492
    /**
493
     * Parse the revision history, collecting our core statistics.
494
     *
495
     * Untestable because it relies on getting a PDO statement. All the important
496
     * logic lives in other methods which are tested.
497
     * @codeCoverageIgnore
498
     */
499
    private function parseHistory(): void
500
    {
501
        $limit = $this->tooManyRevisions() ? $this->getMaxRevisions() : null;
502
503
        // Third parameter is ignored if $limit is null.
504
        $revStmt = $this->page->getRevisionsStmt(
505
            null,
506
            $limit,
507
            $this->getNumRevisions(),
508
            $this->start,
509
            $this->end
510
        );
511
        $revCount = 0;
512
513
        /**
514
         * Data about previous edits so that we can use them as a basis for comparison.
515
         * @var Edit[]
516
         */
517
        $prevEdits = [
518
            // The previous Edit, used to discount content that was reverted.
519
            'prev' => null,
520
521
            // The SHA-1 of the edit *before* the previous edit. Used for more
522
            // accurate revert detection.
523
            'prevSha' => null,
524
525
            // The last edit deemed to be the max addition of content. This is kept track of
526
            // in case we find out the next edit was reverted (and was also a max edit),
527
            // in which case we'll want to discount it and use this one instead.
528
            'maxAddition' => null,
529
530
            // Same as with maxAddition, except the maximum amount of content deleted.
531
            // This is used to discount content that was reverted.
532
            'maxDeletion' => null,
533
        ];
534
535
        while ($rev = $revStmt->fetchAssociative()) {
0 ignored issues
show
Bug introduced by
The method fetchAssociative() does not exist on Doctrine\DBAL\Driver\ResultStatement. It seems like you code against a sub-type of said class. However, the method does not exist in Doctrine\DBAL\Driver\Statement. Are you sure you never get one of those? ( Ignorable by Annotation )

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

535
        while ($rev = $revStmt->/** @scrutinizer ignore-call */ fetchAssociative()) {
Loading history...
536
            $edit = new Edit($this->page, $rev);
537
538
            if (0 === $revCount) {
539
                $this->firstEdit = $edit;
540
            }
541
542
            // Sometimes, with old revisions (2001 era), the revisions from 2002 come before 2001
543
            if ($edit->getTimestamp() < $this->firstEdit->getTimestamp()) {
544
                $this->firstEdit = $edit;
545
            }
546
547
            $prevEdits = $this->updateCounts($edit, $prevEdits);
548
549
            $revCount++;
550
        }
551
552
        $this->numRevisionsProcessed = $revCount;
553
554
        // Various sorts
555
        arsort($this->editors);
556
        ksort($this->yearMonthCounts);
557
        if ($this->tools) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->tools of type array<mixed,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...
558
            arsort($this->tools);
559
        }
560
    }
561
562
    /**
563
     * Update various counts based on the current edit.
564
     * @param Edit $edit
565
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'
566
     * @return Edit[] Updated version of $prevEdits.
567
     */
568
    private function updateCounts(Edit $edit, array $prevEdits): array
569
    {
570
        // Update the counts for the year and month of the current edit.
571
        $this->updateYearMonthCounts($edit);
572
573
        // Update counts for the user who made the edit.
574
        $this->updateUserCounts($edit);
575
576
        // Update the year/month/user counts of anon and minor edits.
577
        $this->updateAnonMinorCounts($edit);
578
579
        // Update counts for automated tool usage, if applicable.
580
        $this->updateToolCounts($edit);
581
582
        // Increment "edits per <time>" counts
583
        $this->updateCountHistory($edit);
584
585
        // Update figures regarding content addition/removal, and the revert count.
586
        $prevEdits = $this->updateContentSizes($edit, $prevEdits);
587
588
        // Now that we've updated all the counts, we can reset
589
        // the prev and last edits, which are used for tracking.
590
        // But first, let's copy over the SHA of the actual previous edit
591
        // and put it in our $prevEdits['prev'], so that we'll know
592
        // that content added after $prevEdit['prev'] was reverted.
593
        if (null !== $prevEdits['prev']) {
594
            $prevEdits['prevSha'] = $prevEdits['prev']->getSha();
595
        }
596
        $prevEdits['prev'] = $edit;
597
        $this->lastEdit = $edit;
598
599
        return $prevEdits;
600
    }
601
602
    /**
603
     * Update various figures about content sizes based on the given edit.
604
     * @param Edit $edit
605
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
606
     * @return Edit[] Updated version of $prevEdits.
607
     */
608
    private function updateContentSizes(Edit $edit, array $prevEdits): array
609
    {
610
        // Check if it was a revert
611
        if ($this->isRevert($edit, $prevEdits)) {
612
            $edit->setReverted(true);
613
            return $this->updateContentSizesRevert($prevEdits);
614
        } else {
615
            return $this->updateContentSizesNonRevert($edit, $prevEdits);
616
        }
617
    }
618
619
    /**
620
     * Is the given Edit a revert?
621
     * @param Edit $edit
622
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
623
     * @return bool
624
     */
625
    private function isRevert(Edit $edit, array $prevEdits): bool
626
    {
627
        return $edit->getSha() === $prevEdits['prevSha'] || $edit->isRevert($this->container);
628
    }
629
630
    /**
631
     * Updates the figures on content sizes assuming the given edit was a revert of the previous one.
632
     * In such a case, we don't want to treat the previous edit as legit content addition or removal.
633
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
634
     * @return Edit[] Updated version of $prevEdits, for tracking.
635
     */
636
    private function updateContentSizesRevert(array $prevEdits): array
637
    {
638
        $this->revertCount++;
639
640
        // Adjust addedBytes given this edit was a revert of the previous one.
641
        if ($prevEdits['prev'] && !$prevEdits['prev']->isReverted() && $prevEdits['prev']->getSize() > 0) {
642
            $this->addedBytes -= $prevEdits['prev']->getSize();
643
644
            // Also deduct from the user's individual added byte count.
645
            // We don't do this if the previous edit was reverted, since that would make the net bytes zero.
646
            if ($prevEdits['prev']->getUser()) {
647
                $username = $prevEdits['prev']->getUser()->getUsername();
648
                $this->editors[$username]['added'] -= $prevEdits['prev']->getSize();
649
            }
650
        }
651
652
        // @TODO: Test this against an edit war (use your sandbox).
653
        // Also remove as max added or deleted, if applicable.
654
        if ($this->maxAddition && $prevEdits['prev']->getId() === $this->maxAddition->getId()) {
655
            $this->maxAddition = $prevEdits['maxAddition'];
656
            $prevEdits['maxAddition'] = $prevEdits['prev']; // In the event of edit wars.
657
        } elseif ($this->maxDeletion && $prevEdits['prev']->getId() === $this->maxDeletion->getId()) {
658
            $this->maxDeletion = $prevEdits['maxDeletion'];
659
            $prevEdits['maxDeletion'] = $prevEdits['prev']; // In the event of edit wars.
660
        }
661
662
        return $prevEdits;
663
    }
664
665
    /**
666
     * Updates the figures on content sizes assuming the given edit was NOT a revert of the previous edit.
667
     * @param Edit $edit
668
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
669
     * @return Edit[] Updated version of $prevEdits, for tracking.
670
     */
671
    private function updateContentSizesNonRevert(Edit $edit, array $prevEdits): array
672
    {
673
        $editSize = $this->getEditSize($edit, $prevEdits);
674
675
        // Edit was not a revert, so treat size > 0 as content added.
676
        if ($editSize > 0) {
677
            $this->addedBytes += $editSize;
678
679
            if ($edit->getUser()) {
680
                $this->editors[$edit->getUser()->getUsername()]['added'] += $editSize;
681
            }
682
683
            // Keep track of edit with max addition.
684
            if (!$this->maxAddition || $editSize > $this->maxAddition->getSize()) {
685
                // Keep track of old maxAddition in case we find out the next $edit was reverted
686
                // (and was also a max edit), in which case we'll want to use this one ($edit).
687
                $prevEdits['maxAddition'] = $this->maxAddition;
688
689
                $this->maxAddition = $edit;
690
            }
691
        } elseif ($editSize < 0 && (!$this->maxDeletion || $editSize < $this->maxDeletion->getSize())) {
692
            // Keep track of old maxDeletion in case we find out the next edit was reverted
693
            // (and was also a max deletion), in which case we'll want to use this one.
694
            $prevEdits['maxDeletion'] = $this->maxDeletion;
695
696
            $this->maxDeletion = $edit;
697
        }
698
699
        return $prevEdits;
700
    }
701
702
    /**
703
     * Get the size of the given edit, based on the previous edit (if present).
704
     * We also don't return the actual edit size if last revision had a length of null.
705
     * This happens when the edit follows other edits that were revision-deleted.
706
     * @see T148857 for more information.
707
     * @todo Remove once T101631 is resolved.
708
     * @param Edit $edit
709
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
710
     * @return int
711
     */
712
    private function getEditSize(Edit $edit, array $prevEdits): int
713
    {
714
        if ($prevEdits['prev'] && null === $prevEdits['prev']->getLength()) {
0 ignored issues
show
introduced by
The condition null === $prevEdits['prev']->getLength() is always false.
Loading history...
715
            return 0;
716
        } else {
717
            return $edit->getSize();
718
        }
719
    }
720
721
    /**
722
     * Update counts of automated tool usage for the given edit.
723
     * @param Edit $edit
724
     */
725
    private function updateToolCounts(Edit $edit): void
726
    {
727
        $automatedTool = $edit->getTool($this->container);
728
729
        if (false === $automatedTool) {
730
            // Nothing to do.
731
            return;
732
        }
733
734
        $editYear = $edit->getYear();
735
        $editMonth = $edit->getMonth();
736
737
        $this->automatedCount++;
738
        $this->yearMonthCounts[$editYear]['automated']++;
739
        $this->yearMonthCounts[$editYear]['months'][$editMonth]['automated']++;
740
741
        if (!isset($this->tools[$automatedTool['name']])) {
742
            $this->tools[$automatedTool['name']] = [
743
                'count' => 1,
744
                'link' => $automatedTool['link'],
745
            ];
746
        } else {
747
            $this->tools[$automatedTool['name']]['count']++;
748
        }
749
    }
750
751
    /**
752
     * Update various counts for the year and month of the given edit.
753
     * @param Edit $edit
754
     */
755
    private function updateYearMonthCounts(Edit $edit): void
756
    {
757
        $editYear = $edit->getYear();
758
        $editMonth = $edit->getMonth();
759
760
        // Fill in the blank arrays for the year and 12 months if needed.
761
        if (!isset($this->yearMonthCounts[$editYear])) {
762
            $this->addYearMonthCountEntry($edit);
763
        }
764
765
        // Increment year and month counts for all edits
766
        $this->yearMonthCounts[$editYear]['all']++;
767
        $this->yearMonthCounts[$editYear]['months'][$editMonth]['all']++;
768
        // This will ultimately be the size of the page by the end of the year
769
        $this->yearMonthCounts[$editYear]['size'] = $edit->getLength();
770
771
        // Keep track of which month had the most edits
772
        $editsThisMonth = $this->yearMonthCounts[$editYear]['months'][$editMonth]['all'];
773
        if ($editsThisMonth > $this->maxEditsPerMonth) {
774
            $this->maxEditsPerMonth = $editsThisMonth;
775
        }
776
    }
777
778
    /**
779
     * Add a new entry to $this->yearMonthCounts for the given year,
780
     * with blank values for each month. This called during self::parseHistory().
781
     * @param Edit $edit
782
     */
783
    private function addYearMonthCountEntry(Edit $edit): void
784
    {
785
        $this->yearLabels[] = $this->i18n->dateFormat($edit->getTimestamp(), 'yyyy');
786
        $editYear = $edit->getYear();
787
788
        // Beginning of the month at 00:00:00.
789
        $firstEditTime = mktime(0, 0, 0, (int)$this->firstEdit->getMonth(), 1, (int)$this->firstEdit->getYear());
790
791
        $this->yearMonthCounts[$editYear] = [
792
            'all' => 0,
793
            'minor' => 0,
794
            'anon' => 0,
795
            'automated' => 0,
796
            'size' => 0, // Keep track of the size by the end of the year.
797
            'events' => [],
798
            'months' => [],
799
        ];
800
801
        for ($i = 1; $i <= 12; $i++) {
802
            $timeObj = mktime(0, 0, 0, $i, 1, (int)$editYear);
803
804
            // Don't show zeros for months before the first edit or after the current month.
805
            if ($timeObj < $firstEditTime || $timeObj > $this->getLastDay()) {
806
                continue;
807
            }
808
809
            $this->monthLabels[] = $this->i18n->dateFormat($timeObj, 'yyyy-MM');
810
            $this->yearMonthCounts[$editYear]['months'][sprintf('%02d', $i)] = [
811
                'all' => 0,
812
                'minor' => 0,
813
                'anon' => 0,
814
                'automated' => 0,
815
            ];
816
        }
817
    }
818
819
    /**
820
     * Update the counts of anon and minor edits for year, month, and user of the given edit.
821
     * @param Edit $edit
822
     */
823
    private function updateAnonMinorCounts(Edit $edit): void
824
    {
825
        $editYear = $edit->getYear();
826
        $editMonth = $edit->getMonth();
827
828
        // If anonymous, increase counts
829
        if ($edit->isAnon()) {
830
            $this->anonCount++;
831
            $this->yearMonthCounts[$editYear]['anon']++;
832
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['anon']++;
833
        }
834
835
        // If minor edit, increase counts
836
        if ($edit->isMinor()) {
837
            $this->minorCount++;
838
            $this->yearMonthCounts[$editYear]['minor']++;
839
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['minor']++;
840
        }
841
    }
842
843
    /**
844
     * Update various counts for the user of the given edit.
845
     * @param Edit $edit
846
     */
847
    private function updateUserCounts(Edit $edit): void
848
    {
849
        if (!$edit->getUser()) {
850
            return;
851
        }
852
853
        $username = $edit->getUser()->getUsername();
854
855
        // Initialize various user stats if needed.
856
        if (!isset($this->editors[$username])) {
857
            $this->editors[$username] = [
858
                'all' => 0,
859
                'minor' => 0,
860
                'minorPercentage' => 0,
861
                'first' => $edit->getTimestamp(),
862
                'firstId' => $edit->getId(),
863
                'last' => null,
864
                'atbe' => null,
865
                'added' => 0,
866
            ];
867
        }
868
869
        // Increment user counts
870
        $this->editors[$username]['all']++;
871
        $this->editors[$username]['last'] = $edit->getTimestamp();
872
        $this->editors[$username]['lastId'] = $edit->getId();
873
874
        // Increment minor counts for this user
875
        if ($edit->isMinor()) {
876
            $this->editors[$username]['minor']++;
877
        }
878
    }
879
880
    /**
881
     * Increment "edits per <time>" counts based on the given edit.
882
     * @param Edit $edit
883
     */
884
    private function updateCountHistory(Edit $edit): void
885
    {
886
        $editTimestamp = $edit->getTimestamp();
887
888
        if ($editTimestamp > new DateTime('-1 day')) {
889
            $this->countHistory['day']++;
890
        }
891
        if ($editTimestamp > new DateTime('-1 week')) {
892
            $this->countHistory['week']++;
893
        }
894
        if ($editTimestamp > new DateTime('-1 month')) {
895
            $this->countHistory['month']++;
896
        }
897
        if ($editTimestamp > new DateTime('-1 year')) {
898
            $this->countHistory['year']++;
899
        }
900
    }
901
902
    /**
903
     * Query for log events during each year of the article's history, and set the results in $this->yearMonthCounts.
904
     */
905
    private function setLogsEvents(): void
906
    {
907
        $logData = $this->getRepository()->getLogEvents(
0 ignored issues
show
Bug introduced by
The method getLogEvents() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\ArticleInfoRepository. ( Ignorable by Annotation )

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

907
        $logData = $this->getRepository()->/** @scrutinizer ignore-call */ getLogEvents(
Loading history...
908
            $this->page,
909
            $this->start,
910
            $this->end
911
        );
912
913
        foreach ($logData as $event) {
914
            $time = strtotime($event['timestamp']);
915
            $year = date('Y', $time);
916
917
            if (!isset($this->yearMonthCounts[$year])) {
918
                break;
919
            }
920
921
            $yearEvents = $this->yearMonthCounts[$year]['events'];
922
923
            // Convert log type value to i18n key.
924
            switch ($event['log_type']) {
925
                // count pending-changes protections along with normal protections.
926
                case 'stable':
927
                case 'protect':
928
                    $action = 'protections';
929
                    break;
930
                case 'delete':
931
                    $action = 'deletions';
932
                    break;
933
                case 'move':
934
                    $action = 'moves';
935
                    break;
936
            }
937
938
            if (empty($yearEvents[$action])) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $action does not seem to be defined for all execution paths leading up to this point.
Loading history...
939
                $yearEvents[$action] = 1;
940
            } else {
941
                $yearEvents[$action]++;
942
            }
943
944
            $this->yearMonthCounts[$year]['events'] = $yearEvents;
945
        }
946
    }
947
948
    /**
949
     * Set statistics about the top 10 editors by added text and number of edits.
950
     * This is ran *after* parseHistory() since we need the grand totals first.
951
     * Various stats are also set for each editor in $this->editors to be used in the charts.
952
     */
953
    private function doPostPrecessing(): void
954
    {
955
        $topTenCount = $counter = 0;
956
        $topTenEditorsByEdits = [];
957
958
        foreach ($this->editors as $editor => $info) {
959
            // Count how many users are in the top 10% by number of edits, excluding bots.
960
            if ($counter < 10 && !in_array($editor, array_keys($this->bots))) {
961
                $topTenCount += $info['all'];
962
                $counter++;
963
964
                // To be used in the Top Ten charts.
965
                $topTenEditorsByEdits[] = [
966
                    'label' => $editor,
967
                    'value' => $info['all'],
968
                ];
969
            }
970
971
            // Compute the percentage of minor edits the user made.
972
            $this->editors[$editor]['minorPercentage'] = $info['all']
973
                ? ($info['minor'] / $info['all']) * 100
974
                : 0;
975
976
            if ($info['all'] > 1) {
977
                // Number of seconds/days between first and last edit.
978
                $secs = $info['last']->getTimestamp() - $info['first']->getTimestamp();
979
                $days = $secs / (60 * 60 * 24);
980
981
                // Average time between edits (in days).
982
                $this->editors[$editor]['atbe'] = round($days / $info['all'], 1);
983
            }
984
        }
985
986
        // Loop through again and add percentages.
987
        $this->topTenEditorsByEdits = array_map(function ($editor) use ($topTenCount) {
988
            $editor['percentage'] = 100 * ($editor['value'] / $topTenCount);
989
            return $editor;
990
        }, $topTenEditorsByEdits);
991
992
        $this->topTenEditorsByAdded = $this->getTopTenByAdded();
993
994
        $this->topTenCount = $topTenCount;
995
    }
996
997
    /**
998
     * Get the top ten editors by added text.
999
     * @return array With keys 'label', 'value' and 'percentage', ready to be used by the pieChart Twig helper.
1000
     */
1001
    private function getTopTenByAdded(): array
1002
    {
1003
        // First sort editors array by the amount of text they added.
1004
        $topTenEditorsByAdded = $this->editors;
1005
        uasort($topTenEditorsByAdded, function ($a, $b) {
1006
            if ($a['added'] === $b['added']) {
1007
                return 0;
1008
            }
1009
            return $a['added'] > $b['added'] ? -1 : 1;
1010
        });
1011
1012
        // Slice to the top 10.
1013
        $topTenEditorsByAdded = array_keys(array_slice($topTenEditorsByAdded, 0, 10, true));
1014
1015
         // Get the sum of added text so that we can add in percentages.
1016
         $topTenTotalAdded = array_sum(array_map(function ($editor) {
1017
             return $this->editors[$editor]['added'];
1018
         }, $topTenEditorsByAdded));
1019
1020
        // Then build a new array of top 10 editors by added text in the data structure needed for the chart.
1021
        return array_map(function ($editor) use ($topTenTotalAdded) {
1022
            $added = $this->editors[$editor]['added'];
1023
            return [
1024
                'label' => $editor,
1025
                'value' => $added,
1026
                'percentage' => 0 === $this->addedBytes
1027
                    ? 0
1028
                    : 100 * ($added / $topTenTotalAdded),
1029
            ];
1030
        }, $topTenEditorsByAdded);
1031
    }
1032
1033
    /**
1034
     * Get the number of times the page has been viewed in the given timeframe. If the ArticleInfo instance has a
1035
     * date range, it is used instead of the value of the $latest parameter.
1036
     * @param  int $latest Last N days.
1037
     * @return int
1038
     */
1039
    public function getPageviews(int $latest): int
1040
    {
1041
        if (!$this->hasDateRange()) {
1042
            return $this->page->getLastPageviews($latest);
1043
        }
1044
1045
        $daterange = $this->getDateParams();
1046
        return $this->page->getPageviews($daterange['start'], $daterange['end']);
1047
    }
1048
}
1049