Passed
Push — articleinfo-bot-percentages ( 0613da )
by MusikAnimal
09:12
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 AppBundle\Model;
9
10
use AppBundle\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 Maximum number of revisions to process, as configured. */
22
    protected $maxRevisions;
23
24
    /** @var int Number of revisions that were actually processed. */
25
    protected $numRevisionsProcessed;
26
27
    /**
28
     * Various statistics about editors to the page. These are not User objects
29
     * so as to preserve memory.
30
     * @var mixed[]
31
     */
32
    protected $editors = [];
33
34
    /** @var mixed[] The top 10 editors to the page by number of edits. */
35
    protected $topTenEditorsByEdits;
36
37
    /** @var mixed[] The top 10 editors to the page by added text. */
38
    protected $topTenEditorsByAdded;
39
40
    /** @var int Number of edits made by the top 10 editors. */
41
    protected $topTenCount;
42
43
    /** @var mixed[] Various counts about each individual year and month of the page's history. */
44
    protected $yearMonthCounts;
45
46
    /** @var string[] Localized labels for the years, to be used in the 'Year counts' chart. */
47
    protected $yearLabels = [];
48
49
    /** @var string[] Localized labels for the months, to be used in the 'Month counts' chart. */
50
    protected $monthLabels = [];
51
52
    /** @var Edit The first edit to the page. */
53
    protected $firstEdit;
54
55
    /** @var Edit The last edit to the page. */
56
    protected $lastEdit;
57
58
    /** @var Edit Edit that made the largest addition by number of bytes. */
59
    protected $maxAddition;
60
61
    /** @var Edit Edit that made the largest deletion by number of bytes. */
62
    protected $maxDeletion;
63
64
    /**
65
     * Maximum number of edits that were created across all months. This is used as a comparison
66
     * for the bar charts in the months section.
67
     * @var int
68
     */
69
    protected $maxEditsPerMonth;
70
71
    /** @var string[][] List of (semi-)automated tools that were used to edit the page. */
72
    protected $tools;
73
74
    /**
75
     * Total number of bytes added throughout the page's history. This is used as a comparison
76
     * when computing the top 10 editors by added text.
77
     * @var int
78
     */
79
    protected $addedBytes = 0;
80
81
    /** @var int Number of days between first and last edit. */
82
    protected $totalDays;
83
84
    /** @var int Number of minor edits to the page. */
85
    protected $minorCount = 0;
86
87
    /** @var int Number of anonymous edits to the page. */
88
    protected $anonCount = 0;
89
90
    /** @var int Number of automated edits to the page. */
91
    protected $automatedCount = 0;
92
93
    /** @var int Number of edits to the page that were reverted with the subsequent edit. */
94
    protected $revertCount = 0;
95
96
    /** @var int[] The "edits per <time>" counts. */
97
    protected $countHistory = [
98
        'day' => 0,
99
        'week' => 0,
100
        'month' => 0,
101
        'year' => 0,
102
    ];
103
104
    /**
105
     * Make the I18nHelper accessible to ArticleInfo.
106
     * @param I18nHelper $i18n
107
     * @codeCoverageIgnore
108
     */
109
    public function setI18nHelper(I18nHelper $i18n): void
110
    {
111
        $this->i18n = $i18n;
112
    }
113
114
    /**
115
     * Get the day of last date we should show in the month/year sections,
116
     * based on $this->end or the current date.
117
     * @return int As Unix timestamp.
118
     */
119
    private function getLastDay(): int
120
    {
121
        if (is_int($this->end)) {
122
            return (new DateTime("@{$this->end}"))
123
                ->modify('last day of this month')
124
                ->getTimestamp();
125
        } else {
126
            return strtotime('last day of this month');
127
        }
128
    }
129
130
    /**
131
     * Return the start/end date values as associative array, with YYYY-MM-DD as the date format.
132
     * This is used mainly as a helper to pass to the pageviews Twig macros.
133
     * @return array
134
     */
135
    public function getDateParams(): array
136
    {
137
        if (!$this->hasDateRange()) {
138
            return [];
139
        }
140
141
        $ret = [
142
            'start' => $this->firstEdit->getTimestamp()->format('Y-m-d'),
143
            'end' => $this->lastEdit->getTimestamp()->format('Y-m-d'),
144
        ];
145
146
        if (is_int($this->start)) {
147
            $ret['start'] = date('Y-m-d', $this->start);
148
        }
149
        if (is_int($this->end)) {
150
            $ret['end'] = date('Y-m-d', $this->end);
151
        }
152
153
        return $ret;
154
    }
155
156
    /**
157
     * Get the number of revisions that are actually getting processed. This goes by the app.max_page_revisions
158
     * parameter, or the actual number of revisions, whichever is smaller.
159
     * @return int
160
     */
161
    public function getNumRevisionsProcessed(): int
162
    {
163
        if (isset($this->numRevisionsProcessed)) {
164
            return $this->numRevisionsProcessed;
165
        }
166
167
        if ($this->tooManyRevisions()) {
168
            $this->numRevisionsProcessed = $this->getMaxRevisions();
169
        } else {
170
            $this->numRevisionsProcessed = $this->getNumRevisions();
171
        }
172
173
        return $this->numRevisionsProcessed;
174
    }
175
176
    /**
177
     * Fetch and store all the data we need to show the ArticleInfo view.
178
     * @codeCoverageIgnore
179
     */
180
    public function prepareData(): void
181
    {
182
        $this->parseHistory();
183
        $this->setLogsEvents();
184
185
        // Bots need to be set before setting top 10 counts.
186
        $this->bots = $this->getBots();
187
188
        $this->doPostPrecessing();
189
    }
190
191
    /**
192
     * Get the number of editors that edited the page.
193
     * @return int
194
     */
195
    public function getNumEditors(): int
196
    {
197
        return count($this->editors);
198
    }
199
200
    /**
201
     * Get the number of days between the first and last edit.
202
     * @return int
203
     */
204
    public function getTotalDays(): int
205
    {
206
        if (isset($this->totalDays)) {
207
            return $this->totalDays;
208
        }
209
        $dateFirst = $this->firstEdit->getTimestamp();
210
        $dateLast = $this->lastEdit->getTimestamp();
211
        $interval = date_diff($dateLast, $dateFirst, true);
212
        $this->totalDays = (int)$interval->format('%a');
213
        return $this->totalDays;
214
    }
215
216
    /**
217
     * Returns length of the page.
218
     * @return int
219
     */
220
    public function getLength(): int
221
    {
222
        if ($this->hasDateRange()) {
223
            return $this->lastEdit->getLength();
224
        }
225
226
        return $this->page->getLength();
227
    }
228
229
    /**
230
     * Get the average number of days between edits to the page.
231
     * @return float
232
     */
233
    public function averageDaysPerEdit(): float
234
    {
235
        return round($this->getTotalDays() / $this->getNumRevisionsProcessed(), 1);
236
    }
237
238
    /**
239
     * Get the average number of edits per day to the page.
240
     * @return float
241
     */
242
    public function editsPerDay(): float
243
    {
244
        $editsPerDay = $this->getTotalDays()
245
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / (365 / 12 / 24))
246
            : 0;
247
        return round($editsPerDay, 1);
248
    }
249
250
    /**
251
     * Get the average number of edits per month to the page.
252
     * @return float
253
     */
254
    public function editsPerMonth(): float
255
    {
256
        $editsPerMonth = $this->getTotalDays()
257
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / (365 / 12))
258
            : 0;
259
        return min($this->getNumRevisionsProcessed(), round($editsPerMonth, 1));
260
    }
261
262
    /**
263
     * Get the average number of edits per year to the page.
264
     * @return float
265
     */
266
    public function editsPerYear(): float
267
    {
268
        $editsPerYear = $this->getTotalDays()
269
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / 365)
270
            : 0;
271
        return min($this->getNumRevisionsProcessed(), round($editsPerYear, 1));
272
    }
273
274
    /**
275
     * Get the average number of edits per editor.
276
     * @return float
277
     */
278
    public function editsPerEditor(): float
279
    {
280
        if (count($this->editors) > 0) {
281
            return round($this->getNumRevisionsProcessed() / count($this->editors), 1);
282
        }
283
284
        // To prevent division by zero error; can happen if all usernames are removed (see T303724).
285
        return 0;
286
    }
287
288
    /**
289
     * Get the percentage of minor edits to the page.
290
     * @return float
291
     */
292
    public function minorPercentage(): float
293
    {
294
        return round(
295
            ($this->minorCount / $this->getNumRevisionsProcessed()) * 100,
296
            1
297
        );
298
    }
299
300
    /**
301
     * Get the percentage of anonymous edits to the page.
302
     * @return float
303
     */
304
    public function anonPercentage(): float
305
    {
306
        return round(
307
            ($this->anonCount / $this->getNumRevisionsProcessed()) * 100,
308
            1
309
        );
310
    }
311
312
    /**
313
     * Get the percentage of edits made by the top 10 editors.
314
     * @return float
315
     */
316
    public function topTenPercentage(): float
317
    {
318
        return round(($this->topTenCount / $this->getNumRevisionsProcessed()) * 100, 1);
319
    }
320
321
    /**
322
     * Get the number of automated edits made to the page.
323
     * @return int
324
     */
325
    public function getAutomatedCount(): int
326
    {
327
        return $this->automatedCount;
328
    }
329
330
    /**
331
     * Get the number of edits to the page that were reverted with the subsequent edit.
332
     * @return int
333
     */
334
    public function getRevertCount(): int
335
    {
336
        return $this->revertCount;
337
    }
338
339
    /**
340
     * Get the number of edits to the page made by logged out users.
341
     * @return int
342
     */
343
    public function getAnonCount(): int
344
    {
345
        return $this->anonCount;
346
    }
347
348
    /**
349
     * Get the number of minor edits to the page.
350
     * @return int
351
     */
352
    public function getMinorCount(): int
353
    {
354
        return $this->minorCount;
355
    }
356
357
    /**
358
     * Get the number of edits to the page made in the past day, week, month and year.
359
     * @return int[] With keys 'day', 'week', 'month' and 'year'.
360
     */
361
    public function getCountHistory(): array
362
    {
363
        return $this->countHistory;
364
    }
365
366
    /**
367
     * Get the number of edits to the page made by the top 10 editors.
368
     * @return int
369
     */
370
    public function getTopTenCount(): int
371
    {
372
        return $this->topTenCount;
373
    }
374
375
    /**
376
     * Get the first edit to the page.
377
     * @return Edit
378
     */
379
    public function getFirstEdit(): Edit
380
    {
381
        return $this->firstEdit;
382
    }
383
384
    /**
385
     * Get the last edit to the page.
386
     * @return Edit
387
     */
388
    public function getLastEdit(): Edit
389
    {
390
        return $this->lastEdit;
391
    }
392
393
    /**
394
     * Get the edit that made the largest addition to the page (by number of bytes).
395
     * @return Edit|null
396
     */
397
    public function getMaxAddition(): ?Edit
398
    {
399
        return $this->maxAddition;
400
    }
401
402
    /**
403
     * Get the edit that made the largest removal to the page (by number of bytes).
404
     * @return Edit|null
405
     */
406
    public function getMaxDeletion(): ?Edit
407
    {
408
        return $this->maxDeletion;
409
    }
410
411
    /**
412
     * Get the list of editors to the page, including various statistics.
413
     * @return mixed[]
414
     */
415
    public function getEditors(): array
416
    {
417
        return $this->editors;
418
    }
419
420
    /**
421
     * Get usernames of human editors (not bots).
422
     * @param int|null $limit
423
     * @return string[]
424
     */
425
    public function getHumans(?int $limit = null): array
426
    {
427
        return array_slice(array_diff(array_keys($this->getEditors()), array_keys($this->getBots())), 0, $limit);
428
    }
429
430
    /**
431
     * Get the list of the top editors to the page (by edits), including various statistics.
432
     * @return mixed[]
433
     */
434
    public function topTenEditorsByEdits(): array
435
    {
436
        return $this->topTenEditorsByEdits;
437
    }
438
439
    /**
440
     * Get the list of the top editors to the page (by added text), including various statistics.
441
     * @return mixed[]
442
     */
443
    public function topTenEditorsByAdded(): array
444
    {
445
        return $this->topTenEditorsByAdded;
446
    }
447
448
    /**
449
     * Get various counts about each individual year and month of the page's history.
450
     * @return mixed[]
451
     */
452
    public function getYearMonthCounts(): array
453
    {
454
        return $this->yearMonthCounts;
455
    }
456
457
    /**
458
     * Get the localized labels for the 'Year counts' chart.
459
     * @return string[]
460
     */
461
    public function getYearLabels(): array
462
    {
463
        return $this->yearLabels;
464
    }
465
466
    /**
467
     * Get the localized labels for the 'Month counts' chart.
468
     * @return string[]
469
     */
470
    public function getMonthLabels(): array
471
    {
472
        return $this->monthLabels;
473
    }
474
475
    /**
476
     * Get the maximum number of edits that were created across all months. This is used as a
477
     * comparison for the bar charts in the months section.
478
     * @return int
479
     */
480
    public function getMaxEditsPerMonth(): int
481
    {
482
        return $this->maxEditsPerMonth;
483
    }
484
485
    /**
486
     * Get a list of (semi-)automated tools that were used to edit the page, including
487
     * the number of times they were used, and a link to the tool's homepage.
488
     * @return string[]
489
     */
490
    public function getTools(): array
491
    {
492
        return $this->tools;
493
    }
494
495
    /**
496
     * Parse the revision history, collecting our core statistics.
497
     *
498
     * Untestable because it relies on getting a PDO statement. All the important
499
     * logic lives in other methods which are tested.
500
     * @codeCoverageIgnore
501
     */
502
    private function parseHistory(): void
503
    {
504
        $limit = $this->tooManyRevisions() ? $this->getMaxRevisions() : null;
505
506
        // Third parameter is ignored if $limit is null.
507
        $revStmt = $this->page->getRevisionsStmt(
508
            null,
509
            $limit,
510
            $this->getNumRevisions(),
511
            $this->start,
512
            $this->end
513
        );
514
        $revCount = 0;
515
516
        /**
517
         * Data about previous edits so that we can use them as a basis for comparison.
518
         * @var Edit[]
519
         */
520
        $prevEdits = [
521
            // The previous Edit, used to discount content that was reverted.
522
            'prev' => null,
523
524
            // The SHA-1 of the edit *before* the previous edit. Used for more
525
            // accurate revert detection.
526
            'prevSha' => null,
527
528
            // The last edit deemed to be the max addition of content. This is kept track of
529
            // in case we find out the next edit was reverted (and was also a max edit),
530
            // in which case we'll want to discount it and use this one instead.
531
            'maxAddition' => null,
532
533
            // Same as with maxAddition, except the maximum amount of content deleted.
534
            // This is used to discount content that was reverted.
535
            'maxDeletion' => null,
536
        ];
537
538
        while ($rev = $revStmt->fetch()) {
539
            $edit = new Edit($this->page, $rev);
540
541
            if (0 === $revCount) {
542
                $this->firstEdit = $edit;
543
            }
544
545
            // Sometimes, with old revisions (2001 era), the revisions from 2002 come before 2001
546
            if ($edit->getTimestamp() < $this->firstEdit->getTimestamp()) {
547
                $this->firstEdit = $edit;
548
            }
549
550
            $prevEdits = $this->updateCounts($edit, $prevEdits);
551
552
            $revCount++;
553
        }
554
555
        $this->numRevisionsProcessed = $revCount;
556
557
        // Various sorts
558
        arsort($this->editors);
559
        ksort($this->yearMonthCounts);
560
        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...
561
            arsort($this->tools);
562
        }
563
    }
564
565
    /**
566
     * Update various counts based on the current edit.
567
     * @param Edit $edit
568
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'
569
     * @return Edit[] Updated version of $prevEdits.
570
     */
571
    private function updateCounts(Edit $edit, array $prevEdits): array
572
    {
573
        // Update the counts for the year and month of the current edit.
574
        $this->updateYearMonthCounts($edit);
575
576
        // Update counts for the user who made the edit.
577
        $this->updateUserCounts($edit);
578
579
        // Update the year/month/user counts of anon and minor edits.
580
        $this->updateAnonMinorCounts($edit);
581
582
        // Update counts for automated tool usage, if applicable.
583
        $this->updateToolCounts($edit);
584
585
        // Increment "edits per <time>" counts
586
        $this->updateCountHistory($edit);
587
588
        // Update figures regarding content addition/removal, and the revert count.
589
        $prevEdits = $this->updateContentSizes($edit, $prevEdits);
590
591
        // Now that we've updated all the counts, we can reset
592
        // the prev and last edits, which are used for tracking.
593
        // But first, let's copy over the SHA of the actual previous edit
594
        // and put it in our $prevEdits['prev'], so that we'll know
595
        // that content added after $prevEdit['prev'] was reverted.
596
        if (null !== $prevEdits['prev']) {
597
            $prevEdits['prevSha'] = $prevEdits['prev']->getSha();
598
        }
599
        $prevEdits['prev'] = $edit;
600
        $this->lastEdit = $edit;
601
602
        return $prevEdits;
603
    }
604
605
    /**
606
     * Update various figures about content sizes based on the given edit.
607
     * @param Edit $edit
608
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
609
     * @return Edit[] Updated version of $prevEdits.
610
     */
611
    private function updateContentSizes(Edit &$edit, array $prevEdits): array
612
    {
613
        // Check if it was a revert
614
        if ($this->isRevert($edit, $prevEdits)) {
615
            $edit->setReverted(true);
616
            return $this->updateContentSizesRevert($prevEdits);
617
        } else {
618
            return $this->updateContentSizesNonRevert($edit, $prevEdits);
619
        }
620
    }
621
622
    /**
623
     * Is the given Edit a revert?
624
     * @param Edit $edit
625
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
626
     * @return bool
627
     */
628
    private function isRevert(Edit $edit, array $prevEdits): bool
629
    {
630
        return $edit->getSha() === $prevEdits['prevSha'] || $edit->isRevert($this->container);
631
    }
632
633
    /**
634
     * Updates the figures on content sizes assuming the given edit was a revert of the previous one.
635
     * In such a case, we don't want to treat the previous edit as legit content addition or removal.
636
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
637
     * @return Edit[] Updated version of $prevEdits, for tracking.
638
     */
639
    private function updateContentSizesRevert(array $prevEdits): array
640
    {
641
        $this->revertCount++;
642
643
        // Adjust addedBytes given this edit was a revert of the previous one.
644
        if ($prevEdits['prev'] && !$prevEdits['prev']->isReverted() && $prevEdits['prev']->getSize() > 0) {
645
            $this->addedBytes -= $prevEdits['prev']->getSize();
646
647
            // Also deduct from the user's individual added byte count.
648
            // We don't do this if the previous edit was reverted, since that would make the net bytes zero.
649
            if ($prevEdits['prev']->getUser()) {
650
                $username = $prevEdits['prev']->getUser()->getUsername();
651
                $this->editors[$username]['added'] -= $prevEdits['prev']->getSize();
652
            }
653
        }
654
655
        // @TODO: Test this against an edit war (use your sandbox).
656
        // Also remove as max added or deleted, if applicable.
657
        if ($this->maxAddition && $prevEdits['prev']->getId() === $this->maxAddition->getId()) {
658
            $this->maxAddition = $prevEdits['maxAddition'];
659
            $prevEdits['maxAddition'] = $prevEdits['prev']; // In the event of edit wars.
660
        } elseif ($this->maxDeletion && $prevEdits['prev']->getId() === $this->maxDeletion->getId()) {
661
            $this->maxDeletion = $prevEdits['maxDeletion'];
662
            $prevEdits['maxDeletion'] = $prevEdits['prev']; // In the event of edit wars.
663
        }
664
665
        return $prevEdits;
666
    }
667
668
    /**
669
     * Updates the figures on content sizes assuming the given edit was NOT a revert of the previous edit.
670
     * @param Edit $edit
671
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
672
     * @return Edit[] Updated version of $prevEdits, for tracking.
673
     */
674
    private function updateContentSizesNonRevert(Edit $edit, array $prevEdits): array
675
    {
676
        $editSize = $this->getEditSize($edit, $prevEdits);
677
678
        // Edit was not a revert, so treat size > 0 as content added.
679
        if ($editSize > 0) {
680
            $this->addedBytes += $editSize;
681
682
            if ($edit->getUser()) {
683
                $this->editors[$edit->getUser()->getUsername()]['added'] += $editSize;
684
            }
685
686
            // Keep track of edit with max addition.
687
            if (!$this->maxAddition || $editSize > $this->maxAddition->getSize()) {
688
                // Keep track of old maxAddition in case we find out the next $edit was reverted
689
                // (and was also a max edit), in which case we'll want to use this one ($edit).
690
                $prevEdits['maxAddition'] = $this->maxAddition;
691
692
                $this->maxAddition = $edit;
693
            }
694
        } elseif ($editSize < 0 && (!$this->maxDeletion || $editSize < $this->maxDeletion->getSize())) {
695
            // Keep track of old maxDeletion in case we find out the next edit was reverted
696
            // (and was also a max deletion), in which case we'll want to use this one.
697
            $prevEdits['maxDeletion'] = $this->maxDeletion;
698
699
            $this->maxDeletion = $edit;
700
        }
701
702
        return $prevEdits;
703
    }
704
705
    /**
706
     * Get the size of the given edit, based on the previous edit (if present).
707
     * We also don't return the actual edit size if last revision had a length of null.
708
     * This happens when the edit follows other edits that were revision-deleted.
709
     * @see T148857 for more information.
710
     * @todo Remove once T101631 is resolved.
711
     * @param Edit $edit
712
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
713
     * @return int
714
     */
715
    private function getEditSize(Edit $edit, array $prevEdits): int
716
    {
717
        if ($prevEdits['prev'] && null === $prevEdits['prev']->getLength()) {
0 ignored issues
show
introduced by
The condition null === $prevEdits['prev']->getLength() is always false.
Loading history...
718
            return 0;
719
        } else {
720
            return $edit->getSize();
721
        }
722
    }
723
724
    /**
725
     * Update counts of automated tool usage for the given edit.
726
     * @param Edit $edit
727
     */
728
    private function updateToolCounts(Edit $edit): void
729
    {
730
        $automatedTool = $edit->getTool($this->container);
731
732
        if (false === $automatedTool) {
733
            // Nothing to do.
734
            return;
735
        }
736
737
        $editYear = $edit->getYear();
738
        $editMonth = $edit->getMonth();
739
740
        $this->automatedCount++;
741
        $this->yearMonthCounts[$editYear]['automated']++;
742
        $this->yearMonthCounts[$editYear]['months'][$editMonth]['automated']++;
743
744
        if (!isset($this->tools[$automatedTool['name']])) {
745
            $this->tools[$automatedTool['name']] = [
746
                'count' => 1,
747
                'link' => $automatedTool['link'],
748
            ];
749
        } else {
750
            $this->tools[$automatedTool['name']]['count']++;
751
        }
752
    }
753
754
    /**
755
     * Update various counts for the year and month of the given edit.
756
     * @param Edit $edit
757
     */
758
    private function updateYearMonthCounts(Edit $edit): void
759
    {
760
        $editYear = $edit->getYear();
761
        $editMonth = $edit->getMonth();
762
763
        // Fill in the blank arrays for the year and 12 months if needed.
764
        if (!isset($this->yearMonthCounts[$editYear])) {
765
            $this->addYearMonthCountEntry($edit);
766
        }
767
768
        // Increment year and month counts for all edits
769
        $this->yearMonthCounts[$editYear]['all']++;
770
        $this->yearMonthCounts[$editYear]['months'][$editMonth]['all']++;
771
        // This will ultimately be the size of the page by the end of the year
772
        $this->yearMonthCounts[$editYear]['size'] = (int) $edit->getLength();
773
774
        // Keep track of which month had the most edits
775
        $editsThisMonth = $this->yearMonthCounts[$editYear]['months'][$editMonth]['all'];
776
        if ($editsThisMonth > $this->maxEditsPerMonth) {
777
            $this->maxEditsPerMonth = $editsThisMonth;
778
        }
779
    }
780
781
    /**
782
     * Add a new entry to $this->yearMonthCounts for the given year,
783
     * with blank values for each month. This called during self::parseHistory().
784
     * @param Edit $edit
785
     */
786
    private function addYearMonthCountEntry(Edit $edit): void
787
    {
788
        $this->yearLabels[] = $this->i18n->dateFormat($edit->getTimestamp(), 'yyyy');
789
        $editYear = $edit->getYear();
790
791
        // Beginning of the month at 00:00:00.
792
        $firstEditTime = mktime(0, 0, 0, (int)$this->firstEdit->getMonth(), 1, (int)$this->firstEdit->getYear());
793
794
        $this->yearMonthCounts[$editYear] = [
795
            'all' => 0,
796
            'minor' => 0,
797
            'anon' => 0,
798
            'automated' => 0,
799
            'size' => 0, // Keep track of the size by the end of the year.
800
            'events' => [],
801
            'months' => [],
802
        ];
803
804
        for ($i = 1; $i <= 12; $i++) {
805
            $timeObj = mktime(0, 0, 0, $i, 1, (int)$editYear);
806
807
            // Don't show zeros for months before the first edit or after the current month.
808
            if ($timeObj < $firstEditTime || $timeObj > $this->getLastDay()) {
809
                continue;
810
            }
811
812
            $this->monthLabels[] = $this->i18n->dateFormat($timeObj, 'yyyy-MM');
813
            $this->yearMonthCounts[$editYear]['months'][sprintf('%02d', $i)] = [
814
                'all' => 0,
815
                'minor' => 0,
816
                'anon' => 0,
817
                'automated' => 0,
818
            ];
819
        }
820
    }
821
822
    /**
823
     * Update the counts of anon and minor edits for year, month, and user of the given edit.
824
     * @param Edit $edit
825
     */
826
    private function updateAnonMinorCounts(Edit $edit): void
827
    {
828
        $editYear = $edit->getYear();
829
        $editMonth = $edit->getMonth();
830
831
        // If anonymous, increase counts
832
        if ($edit->isAnon()) {
833
            $this->anonCount++;
834
            $this->yearMonthCounts[$editYear]['anon']++;
835
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['anon']++;
836
        }
837
838
        // If minor edit, increase counts
839
        if ($edit->isMinor()) {
840
            $this->minorCount++;
841
            $this->yearMonthCounts[$editYear]['minor']++;
842
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['minor']++;
843
        }
844
    }
845
846
    /**
847
     * Update various counts for the user of the given edit.
848
     * @param Edit $edit
849
     */
850
    private function updateUserCounts(Edit $edit): void
851
    {
852
        if (!$edit->getUser()) {
853
            return;
854
        }
855
856
        $username = $edit->getUser()->getUsername();
857
858
        // Initialize various user stats if needed.
859
        if (!isset($this->editors[$username])) {
860
            $this->editors[$username] = [
861
                'all' => 0,
862
                'minor' => 0,
863
                'minorPercentage' => 0,
864
                'first' => $edit->getTimestamp(),
865
                'firstId' => $edit->getId(),
866
                'last' => null,
867
                'atbe' => null,
868
                'added' => 0,
869
            ];
870
        }
871
872
        // Increment user counts
873
        $this->editors[$username]['all']++;
874
        $this->editors[$username]['last'] = $edit->getTimestamp();
875
        $this->editors[$username]['lastId'] = $edit->getId();
876
877
        // Increment minor counts for this user
878
        if ($edit->isMinor()) {
879
            $this->editors[$username]['minor']++;
880
        }
881
    }
882
883
    /**
884
     * Increment "edits per <time>" counts based on the given edit.
885
     * @param Edit $edit
886
     */
887
    private function updateCountHistory(Edit $edit): void
888
    {
889
        $editTimestamp = $edit->getTimestamp();
890
891
        if ($editTimestamp > new DateTime('-1 day')) {
892
            $this->countHistory['day']++;
893
        }
894
        if ($editTimestamp > new DateTime('-1 week')) {
895
            $this->countHistory['week']++;
896
        }
897
        if ($editTimestamp > new DateTime('-1 month')) {
898
            $this->countHistory['month']++;
899
        }
900
        if ($editTimestamp > new DateTime('-1 year')) {
901
            $this->countHistory['year']++;
902
        }
903
    }
904
905
    /**
906
     * Query for log events during each year of the article's history, and set the results in $this->yearMonthCounts.
907
     */
908
    private function setLogsEvents(): void
909
    {
910
        $logData = $this->getRepository()->getLogEvents(
0 ignored issues
show
Bug introduced by
The method getLogEvents() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\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

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