Passed
Pull Request — master (#376)
by MusikAnimal
10:04 queued 21s
created

ArticleInfo::getMaxEditsPerMonth()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
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
use Symfony\Component\DependencyInjection\ContainerInterface;
13
14
/**
15
 * An ArticleInfo provides statistics about a page on a project.
16
 */
17
class ArticleInfo extends ArticleInfoApi
18
{
19
    /** @var I18nHelper For i18n and l10n. */
20
    protected $i18n;
21
22
    /** @var int Maximum number of revisions to process, as configured. */
23
    protected $maxRevisions;
24
25
    /** @var int Number of revisions that were actually processed. */
26
    protected $numRevisionsProcessed;
27
28
    /**
29
     * Various statistics about editors to the page. These are not User objects
30
     * so as to preserve memory.
31
     * @var mixed[]
32
     */
33
    protected $editors = [];
34
35
    /** @var mixed[] The top 10 editors to the page by number of edits. */
36
    protected $topTenEditorsByEdits;
37
38
    /** @var mixed[] The top 10 editors to the page by added text. */
39
    protected $topTenEditorsByAdded;
40
41
    /** @var int Number of edits made by the top 10 editors. */
42
    protected $topTenCount;
43
44
    /** @var mixed[] Various counts about each individual year and month of the page's history. */
45
    protected $yearMonthCounts;
46
47
    /** @var string[] Localized labels for the years, to be used in the 'Year counts' chart. */
48
    protected $yearLabels = [];
49
50
    /** @var string[] Localized labels for the months, to be used in the 'Month counts' chart. */
51
    protected $monthLabels = [];
52
53
    /** @var Edit The first edit to the page. */
54
    protected $firstEdit;
55
56
    /** @var Edit The last edit to the page. */
57
    protected $lastEdit;
58
59
    /** @var Edit Edit that made the largest addition by number of bytes. */
60
    protected $maxAddition;
61
62
    /** @var Edit Edit that made the largest deletion by number of bytes. */
63
    protected $maxDeletion;
64
65
    /**
66
     * Maximum number of edits that were created across all months. This is used as a comparison
67
     * for the bar charts in the months section.
68
     * @var int
69
     */
70
    protected $maxEditsPerMonth;
71
72
    /** @var string[][] List of (semi-)automated tools that were used to edit the page. */
73
    protected $tools;
74
75
    /**
76
     * Total number of bytes added throughout the page's history. This is used as a comparison
77
     * when computing the top 10 editors by added text.
78
     * @var int
79
     */
80
    protected $addedBytes = 0;
81
82
    /** @var int Number of days between first and last edit. */
83
    protected $totalDays;
84
85
    /** @var int Number of minor edits to the page. */
86
    protected $minorCount = 0;
87
88
    /** @var int Number of anonymous edits to the page. */
89
    protected $anonCount = 0;
90
91
    /** @var int Number of automated edits to the page. */
92
    protected $automatedCount = 0;
93
94
    /** @var int Number of edits to the page that were reverted with the subsequent edit. */
95
    protected $revertCount = 0;
96
97
    /** @var int[] The "edits per <time>" counts. */
98
    protected $countHistory = [
99
        'day' => 0,
100
        'week' => 0,
101
        'month' => 0,
102
        'year' => 0,
103
    ];
104
105
    /**
106
     * ArticleInfo constructor.
107
     * @param Page $page The page to process.
108
     * @param ContainerInterface $container The DI container.
109
     * @param false|int $start From what date to obtain records.
110
     * @param false|int $end To what date to obtain records.
111
     */
112 12
    public function __construct(Page $page, ContainerInterface $container, $start = false, $end = false)
113
    {
114 12
        parent::__construct($page, $container, $start, $end);
115 12
    }
116
117
    /**
118
     * Make the I18nHelper accessible to ArticleInfo.
119
     * @param I18nHelper $i18n
120
     * @codeCoverageIgnore
121
     */
122
    public function setI18nHelper(I18nHelper $i18n): void
123
    {
124
        $this->i18n = $i18n;
125
    }
126
127
    /**
128
     * Get the day of last date we should show in the month/year sections,
129
     * based on $this->end or the current date.
130
     * @return int As Unix timestamp.
131
     */
132 4
    private function getLastDay(): int
133
    {
134 4
        if (false !== $this->end) {
135
            return (new DateTime('@'.$this->end))
0 ignored issues
show
Bug introduced by
Are you sure $this->end of type integer|string|true can be used in concatenation? ( Ignorable by Annotation )

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

135
            return (new DateTime('@'./** @scrutinizer ignore-type */ $this->end))
Loading history...
136
                ->modify('last day of this month')
137
                ->getTimestamp();
138
        } else {
139 4
            return strtotime('last day of this month');
140
        }
141
    }
142
143
    /**
144
     * Return the start/end date values as associative array, with YYYY-MM-DD as the date format.
145
     * This is used mainly as a helper to pass to the pageviews Twig macros.
146
     * @return array
147
     */
148 1
    public function getDateParams(): array
149
    {
150 1
        if (!$this->hasDateRange()) {
151
            return [];
152
        }
153
154
        $ret = [
155 1
            'start' => $this->firstEdit->getTimestamp()->format('Y-m-d'),
156 1
            'end' => $this->lastEdit->getTimestamp()->format('Y-m-d'),
157
        ];
158
159 1
        if (false !== $this->start) {
160 1
            $ret['start'] = date('Y-m-d', $this->start);
0 ignored issues
show
Bug introduced by
It seems like $this->start can also be of type string and true; however, parameter $timestamp of date() does only seem to accept integer|null, 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

160
            $ret['start'] = date('Y-m-d', /** @scrutinizer ignore-type */ $this->start);
Loading history...
161
        }
162 1
        if (false !== $this->end) {
163 1
            $ret['end'] = date('Y-m-d', $this->end);
164
        }
165
166 1
        return $ret;
167
    }
168
169
    /**
170
     * Get the maximum number of revisions that we should process.
171
     * @return int
172
     */
173 3
    public function getMaxRevisions(): int
174
    {
175 3
        if (!isset($this->maxRevisions)) {
176 3
            $this->maxRevisions = (int) $this->container->getParameter('app.max_page_revisions');
177
        }
178 3
        return $this->maxRevisions;
179
    }
180
181
    /**
182
     * Get the number of revisions that are actually getting processed. This goes by the app.max_page_revisions
183
     * parameter, or the actual number of revisions, whichever is smaller.
184
     * @return int
185
     */
186 3
    public function getNumRevisionsProcessed(): int
187
    {
188 3
        if (isset($this->numRevisionsProcessed)) {
189 1
            return $this->numRevisionsProcessed;
190
        }
191
192 2
        if ($this->tooManyRevisions()) {
193 1
            $this->numRevisionsProcessed = $this->getMaxRevisions();
194
        } else {
195 1
            $this->numRevisionsProcessed = $this->getNumRevisions();
196
        }
197
198 2
        return $this->numRevisionsProcessed;
199
    }
200
201
    /**
202
     * Are there more revisions than we should process, based on the config?
203
     * @return bool
204
     */
205 3
    public function tooManyRevisions(): bool
206
    {
207 3
        return $this->getMaxRevisions() > 0 && $this->getNumRevisions() > $this->getMaxRevisions();
208
    }
209
210
    /**
211
     * Fetch and store all the data we need to show the ArticleInfo view.
212
     * @codeCoverageIgnore
213
     */
214
    public function prepareData(): void
215
    {
216
        $this->parseHistory();
217
        $this->setLogsEvents();
218
219
        // Bots need to be set before setting top 10 counts.
220
        $this->bots = $this->getBots();
221
222
        $this->doPostPrecessing();
223
    }
224
225
    /**
226
     * Get the number of editors that edited the page.
227
     * @return int
228
     */
229 1
    public function getNumEditors(): int
230
    {
231 1
        return count($this->editors);
232
    }
233
234
    /**
235
     * Get the number of days between the first and last edit.
236
     * @return int
237
     */
238 1
    public function getTotalDays(): int
239
    {
240 1
        if (isset($this->totalDays)) {
241 1
            return $this->totalDays;
242
        }
243 1
        $dateFirst = $this->firstEdit->getTimestamp();
244 1
        $dateLast = $this->lastEdit->getTimestamp();
245 1
        $interval = date_diff($dateLast, $dateFirst, true);
246 1
        $this->totalDays = (int)$interval->format('%a');
247 1
        return $this->totalDays;
248
    }
249
250
    /**
251
     * Returns length of the page.
252
     * @return int
253
     */
254 1
    public function getLength(): int
255
    {
256 1
        if ($this->hasDateRange()) {
257 1
            return $this->lastEdit->getLength();
258
        }
259
260
        return $this->page->getLength();
261
    }
262
263
    /**
264
     * Get the average number of days between edits to the page.
265
     * @return float
266
     */
267 1
    public function averageDaysPerEdit(): float
268
    {
269 1
        return round($this->getTotalDays() / $this->getNumRevisionsProcessed(), 1);
270
    }
271
272
    /**
273
     * Get the average number of edits per day to the page.
274
     * @return float
275
     */
276 1
    public function editsPerDay(): float
277
    {
278 1
        $editsPerDay = $this->getTotalDays()
279 1
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / (365 / 12 / 24))
280 1
            : 0;
281 1
        return round($editsPerDay, 1);
282
    }
283
284
    /**
285
     * Get the average number of edits per month to the page.
286
     * @return float
287
     */
288 1
    public function editsPerMonth(): float
289
    {
290 1
        $editsPerMonth = $this->getTotalDays()
291 1
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / (365 / 12))
292 1
            : 0;
293 1
        return min($this->getNumRevisionsProcessed(), round($editsPerMonth, 1));
294
    }
295
296
    /**
297
     * Get the average number of edits per year to the page.
298
     * @return float
299
     */
300 1
    public function editsPerYear(): float
301
    {
302 1
        $editsPerYear = $this->getTotalDays()
303 1
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / 365)
304 1
            : 0;
305 1
        return min($this->getNumRevisionsProcessed(), round($editsPerYear, 1));
306
    }
307
308
    /**
309
     * Get the average number of edits per editor.
310
     * @return float
311
     */
312 1
    public function editsPerEditor(): float
313
    {
314 1
        return round($this->getNumRevisionsProcessed() / count($this->editors), 1);
315
    }
316
317
    /**
318
     * Get the percentage of minor edits to the page.
319
     * @return float
320
     */
321 1
    public function minorPercentage(): float
322
    {
323 1
        return round(
324 1
            ($this->minorCount / $this->getNumRevisionsProcessed()) * 100,
325 1
            1
326
        );
327
    }
328
329
    /**
330
     * Get the percentage of anonymous edits to the page.
331
     * @return float
332
     */
333 1
    public function anonPercentage(): float
334
    {
335 1
        return round(
336 1
            ($this->anonCount / $this->getNumRevisionsProcessed()) * 100,
337 1
            1
338
        );
339
    }
340
341
    /**
342
     * Get the percentage of edits made by the top 10 editors.
343
     * @return float
344
     */
345 1
    public function topTenPercentage(): float
346
    {
347 1
        return round(($this->topTenCount / $this->getNumRevisionsProcessed()) * 100, 1);
348
    }
349
350
    /**
351
     * Get the number of automated edits made to the page.
352
     * @return int
353
     */
354 1
    public function getAutomatedCount(): int
355
    {
356 1
        return $this->automatedCount;
357
    }
358
359
    /**
360
     * Get the number of edits to the page that were reverted with the subsequent edit.
361
     * @return int
362
     */
363 1
    public function getRevertCount(): int
364
    {
365 1
        return $this->revertCount;
366
    }
367
368
    /**
369
     * Get the number of edits to the page made by logged out users.
370
     * @return int
371
     */
372 1
    public function getAnonCount(): int
373
    {
374 1
        return $this->anonCount;
375
    }
376
377
    /**
378
     * Get the number of minor edits to the page.
379
     * @return int
380
     */
381 1
    public function getMinorCount(): int
382
    {
383 1
        return $this->minorCount;
384
    }
385
386
    /**
387
     * Get the number of edits to the page made in the past day, week, month and year.
388
     * @return int[] With keys 'day', 'week', 'month' and 'year'.
389
     */
390
    public function getCountHistory(): array
391
    {
392
        return $this->countHistory;
393
    }
394
395
    /**
396
     * Get the number of edits to the page made by the top 10 editors.
397
     * @return int
398
     */
399 1
    public function getTopTenCount(): int
400
    {
401 1
        return $this->topTenCount;
402
    }
403
404
    /**
405
     * Get the first edit to the page.
406
     * @return Edit
407
     */
408 1
    public function getFirstEdit(): Edit
409
    {
410 1
        return $this->firstEdit;
411
    }
412
413
    /**
414
     * Get the last edit to the page.
415
     * @return Edit
416
     */
417 1
    public function getLastEdit(): Edit
418
    {
419 1
        return $this->lastEdit;
420
    }
421
422
    /**
423
     * Get the edit that made the largest addition to the page (by number of bytes).
424
     * @return Edit|null
425
     */
426 1
    public function getMaxAddition(): ?Edit
427
    {
428 1
        return $this->maxAddition;
429
    }
430
431
    /**
432
     * Get the edit that made the largest removal to the page (by number of bytes).
433
     * @return Edit|null
434
     */
435 1
    public function getMaxDeletion(): ?Edit
436
    {
437 1
        return $this->maxDeletion;
438
    }
439
440
    /**
441
     * Get the list of editors to the page, including various statistics.
442
     * @return mixed[]
443
     */
444 1
    public function getEditors(): array
445
    {
446 1
        return $this->editors;
447
    }
448
449
    /**
450
     * Get usernames of human editors (not bots).
451
     * @param int|null $limit
452
     * @return string[]
453
     */
454
    public function getHumans(?int $limit = null): array
455
    {
456
        return array_slice(array_diff(array_keys($this->getEditors()), array_keys($this->getBots())), 0, $limit);
457
    }
458
459
    /**
460
     * Get the list of the top editors to the page (by edits), including various statistics.
461
     * @return mixed[]
462
     */
463 1
    public function topTenEditorsByEdits(): array
464
    {
465 1
        return $this->topTenEditorsByEdits;
466
    }
467
468
    /**
469
     * Get the list of the top editors to the page (by added text), including various statistics.
470
     * @return mixed[]
471
     */
472 1
    public function topTenEditorsByAdded(): array
473
    {
474 1
        return $this->topTenEditorsByAdded;
475
    }
476
477
    /**
478
     * Get various counts about each individual year and month of the page's history.
479
     * @return mixed[]
480
     */
481 2
    public function getYearMonthCounts(): array
482
    {
483 2
        return $this->yearMonthCounts;
484
    }
485
486
    /**
487
     * Get the localized labels for the 'Year counts' chart.
488
     * @return string[]
489
     */
490
    public function getYearLabels(): array
491
    {
492
        return $this->yearLabels;
493
    }
494
495
    /**
496
     * Get the localized labels for the 'Month counts' chart.
497
     * @return string[]
498
     */
499
    public function getMonthLabels(): array
500
    {
501
        return $this->monthLabels;
502
    }
503
504
    /**
505
     * Get the maximum number of edits that were created across all months. This is used as a
506
     * comparison for the bar charts in the months section.
507
     * @return int
508
     */
509 1
    public function getMaxEditsPerMonth(): int
510
    {
511 1
        return $this->maxEditsPerMonth;
512
    }
513
514
    /**
515
     * Get a list of (semi-)automated tools that were used to edit the page, including
516
     * the number of times they were used, and a link to the tool's homepage.
517
     * @return string[]
518
     */
519 1
    public function getTools(): array
520
    {
521 1
        return $this->tools;
522
    }
523
524
    /**
525
     * Parse the revision history, collecting our core statistics.
526
     *
527
     * Untestable because it relies on getting a PDO statement. All the important
528
     * logic lives in other methods which are tested.
529
     * @codeCoverageIgnore
530
     */
531
    private function parseHistory(): void
532
    {
533
        if ($this->tooManyRevisions()) {
534
            $limit = $this->getMaxRevisions();
535
        } else {
536
            $limit = null;
537
        }
538
539
        // Third parameter is ignored if $limit is null.
540
        $revStmt = $this->page->getRevisionsStmt(
541
            null,
542
            $limit,
543
            $this->getNumRevisions(),
544
            $this->start,
0 ignored issues
show
Bug introduced by
It seems like $this->start can also be of type string; however, parameter $start of AppBundle\Model\Page::getRevisionsStmt() does only seem to accept false|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

544
            /** @scrutinizer ignore-type */ $this->start,
Loading history...
545
            $this->end
0 ignored issues
show
Bug introduced by
It seems like $this->end can also be of type string; however, parameter $end of AppBundle\Model\Page::getRevisionsStmt() does only seem to accept false|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

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

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