Test Failed
Pull Request — main (#442)
by MusikAnimal
07:57 queued 03:53
created

ArticleInfo::getRevertCount()   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
declare(strict_types = 1);
4
5
namespace App\Model;
6
7
use DateTime;
8
9
/**
10
 * An ArticleInfo provides statistics about a page on a project.
11
 */
12
class ArticleInfo extends ArticleInfoApi
13
{
14
    /** @var int Number of revisions that were actually processed. */
15
    protected int $numRevisionsProcessed;
16
17
    /**
18
     * Various statistics about editors to the page. These are not User objects
19
     * so as to preserve memory.
20
     * @var array
21
     */
22
    protected array $editors = [];
23
24
    /** @var array The top 10 editors to the page by number of edits. */
25
    protected array $topTenEditorsByEdits;
26
27
    /** @var array The top 10 editors to the page by added text. */
28
    protected array $topTenEditorsByAdded;
29
30
    /** @var int Number of edits made by the top 10 editors. */
31
    protected int $topTenCount;
32
33
    /** @var array Various counts about each individual year and month of the page's history. */
34
    protected array $yearMonthCounts;
35
36
    /** @var string[] Localized labels for the years, to be used in the 'Year counts' chart. */
37
    protected array $yearLabels = [];
38
39
    /** @var string[] Localized labels for the months, to be used in the 'Month counts' chart. */
40
    protected array $monthLabels = [];
41
42
    /** @var Edit|null The first edit to the page. */
43
    protected ?Edit $firstEdit = null;
44
45
    /** @var Edit|null The last edit to the page. */
46
    protected ?Edit $lastEdit = null;
47
48
    /** @var Edit|null Edit that made the largest addition by number of bytes. */
49
    protected ?Edit $maxAddition = null;
50
51
    /** @var Edit|null Edit that made the largest deletion by number of bytes. */
52
    protected ?Edit $maxDeletion = null;
53
54
    /**
55
     * Maximum number of edits that were created across all months. This is used as a comparison
56
     * for the bar charts in the months section.
57
     * @var int
58
     */
59
    protected int $maxEditsPerMonth = 0;
60
61
    /** @var string[][] List of (semi-)automated tools that were used to edit the page. */
62
    protected array $tools = [];
63
64
    /**
65
     * Total number of bytes added throughout the page's history. This is used as a comparison
66
     * when computing the top 10 editors by added text.
67
     * @var int
68
     */
69
    protected int $addedBytes = 0;
70
71
    /** @var int Number of days between first and last edit. */
72
    protected int $totalDays;
73
74
    /** @var int Number of minor edits to the page. */
75
    protected int $minorCount = 0;
76
77
    /** @var int Number of anonymous edits to the page. */
78
    protected int $anonCount = 0;
79
80
    /** @var int Number of automated edits to the page. */
81
    protected int $automatedCount = 0;
82
83
    /** @var int Number of edits to the page that were reverted with the subsequent edit. */
84
    protected int $revertCount = 0;
85
86
    /** @var int[] The "edits per <time>" counts. */
87
    protected array $countHistory = [
88
        'day' => 0,
89
        'week' => 0,
90
        'month' => 0,
91
        'year' => 0,
92
    ];
93
94
    /**
95
     * Get the day of last date we should show in the month/year sections,
96
     * based on $this->end or the current date.
97
     * @return int As Unix timestamp.
98
     */
99
    private function getLastDay(): int
100
    {
101
        if (is_int($this->end)) {
102
            return (new DateTime("@$this->end"))
103
                ->modify('last day of this month')
104
                ->getTimestamp();
105
        } else {
106
            return strtotime('last day of this month');
107
        }
108
    }
109
110
    /**
111
     * Return the start/end date values as associative array, with YYYY-MM-DD as the date format.
112
     * This is used mainly as a helper to pass to the pageviews Twig macros.
113
     * @return array
114
     */
115
    public function getDateParams(): array
116
    {
117
        if (!$this->hasDateRange()) {
118
            return [];
119
        }
120
121
        $ret = [
122
            'start' => $this->firstEdit->getTimestamp()->format('Y-m-d'),
0 ignored issues
show
Bug introduced by
The method getTimestamp() does not exist on null. ( Ignorable by Annotation )

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

122
            'start' => $this->firstEdit->/** @scrutinizer ignore-call */ getTimestamp()->format('Y-m-d'),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
123
            'end' => $this->lastEdit->getTimestamp()->format('Y-m-d'),
0 ignored issues
show
Bug introduced by
The method getTimestamp() does not exist on null. ( Ignorable by Annotation )

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

123
            'end' => $this->lastEdit->/** @scrutinizer ignore-call */ getTimestamp()->format('Y-m-d'),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
124
        ];
125
126
        if (is_int($this->start)) {
127
            $ret['start'] = date('Y-m-d', $this->start);
128
        }
129
        if (is_int($this->end)) {
130
            $ret['end'] = date('Y-m-d', $this->end);
131
        }
132
133
        return $ret;
134
    }
135
136
    /**
137
     * Get the number of revisions that are actually getting processed. This goes by the app.max_page_revisions
138
     * parameter, or the actual number of revisions, whichever is smaller.
139
     * @return int
140
     */
141
    public function getNumRevisionsProcessed(): int
142
    {
143
        if (isset($this->numRevisionsProcessed)) {
144
            return $this->numRevisionsProcessed;
145
        }
146
147
        if ($this->tooManyRevisions()) {
148
            $this->numRevisionsProcessed = $this->repository->getMaxPageRevisions();
0 ignored issues
show
Bug introduced by
The method getMaxPageRevisions() 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

148
            /** @scrutinizer ignore-call */ 
149
            $this->numRevisionsProcessed = $this->repository->getMaxPageRevisions();
Loading history...
149
        } else {
150
            $this->numRevisionsProcessed = $this->getNumRevisions();
151
        }
152
153
        return $this->numRevisionsProcessed;
154
    }
155
156
    /**
157
     * Fetch and store all the data we need to show the ArticleInfo view.
158
     * @codeCoverageIgnore
159
     */
160
    public function prepareData(): void
161
    {
162
        $this->parseHistory();
163
        $this->setLogsEvents();
164
165
        // Bots need to be set before setting top 10 counts.
166
        $this->bots = $this->getBots();
167
168
        $this->doPostPrecessing();
169
    }
170
171
    /**
172
     * Get the number of editors that edited the page.
173
     * @return int
174
     */
175
    public function getNumEditors(): int
176
    {
177
        return count($this->editors);
178
    }
179
180
    /**
181
     * Get the number of days between the first and last edit.
182
     * @return int
183
     */
184
    public function getTotalDays(): int
185
    {
186
        if (isset($this->totalDays)) {
187
            return $this->totalDays;
188
        }
189
        $dateFirst = $this->firstEdit->getTimestamp();
190
        $dateLast = $this->lastEdit->getTimestamp();
191
        $interval = date_diff($dateLast, $dateFirst, true);
192
        $this->totalDays = (int)$interval->format('%a');
193
        return $this->totalDays;
194
    }
195
196
    /**
197
     * Returns length of the page.
198
     * @return int
199
     */
200
    public function getLength(): int
201
    {
202
        if ($this->hasDateRange()) {
203
            return $this->lastEdit->getLength();
204
        }
205
206
        return $this->page->getLength();
0 ignored issues
show
Bug introduced by
The method getLength() does not exist on null. ( Ignorable by Annotation )

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

206
        return $this->page->/** @scrutinizer ignore-call */ getLength();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
207
    }
208
209
    /**
210
     * Get the average number of days between edits to the page.
211
     * @return float
212
     */
213
    public function averageDaysPerEdit(): float
214
    {
215
        return round($this->getTotalDays() / $this->getNumRevisionsProcessed(), 1);
216
    }
217
218
    /**
219
     * Get the average number of edits per day to the page.
220
     * @return float
221
     */
222
    public function editsPerDay(): float
223
    {
224
        $editsPerDay = $this->getTotalDays()
225
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / (365 / 12 / 24))
226
            : 0;
227
        return round($editsPerDay, 1);
228
    }
229
230
    /**
231
     * Get the average number of edits per month to the page.
232
     * @return float
233
     */
234
    public function editsPerMonth(): float
235
    {
236
        $editsPerMonth = $this->getTotalDays()
237
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / (365 / 12))
238
            : 0;
239
        return min($this->getNumRevisionsProcessed(), round($editsPerMonth, 1));
240
    }
241
242
    /**
243
     * Get the average number of edits per year to the page.
244
     * @return float
245
     */
246
    public function editsPerYear(): float
247
    {
248
        $editsPerYear = $this->getTotalDays()
249
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / 365)
250
            : 0;
251
        return min($this->getNumRevisionsProcessed(), round($editsPerYear, 1));
252
    }
253
254
    /**
255
     * Get the average number of edits per editor.
256
     * @return float
257
     */
258
    public function editsPerEditor(): float
259
    {
260
        if (count($this->editors) > 0) {
261
            return round($this->getNumRevisionsProcessed() / count($this->editors), 1);
262
        }
263
264
        // To prevent division by zero error; can happen if all usernames are removed (see T303724).
265
        return 0;
266
    }
267
268
    /**
269
     * Get the percentage of minor edits to the page.
270
     * @return float
271
     */
272
    public function minorPercentage(): float
273
    {
274
        return round(
275
            ($this->minorCount / $this->getNumRevisionsProcessed()) * 100,
276
            1
277
        );
278
    }
279
280
    /**
281
     * Get the percentage of anonymous edits to the page.
282
     * @return float
283
     */
284
    public function anonPercentage(): float
285
    {
286
        return round(
287
            ($this->anonCount / $this->getNumRevisionsProcessed()) * 100,
288
            1
289
        );
290
    }
291
292
    /**
293
     * Get the percentage of edits made by the top 10 editors.
294
     * @return float
295
     */
296
    public function topTenPercentage(): float
297
    {
298
        return round(($this->topTenCount / $this->getNumRevisionsProcessed()) * 100, 1);
299
    }
300
301
    /**
302
     * Get the number of automated edits made to the page.
303
     * @return int
304
     */
305
    public function getAutomatedCount(): int
306
    {
307
        return $this->automatedCount;
308
    }
309
310
    /**
311
     * Get the number of edits to the page that were reverted with the subsequent edit.
312
     * @return int
313
     */
314
    public function getRevertCount(): int
315
    {
316
        return $this->revertCount;
317
    }
318
319
    /**
320
     * Get the number of edits to the page made by logged out users.
321
     * @return int
322
     */
323
    public function getAnonCount(): int
324
    {
325
        return $this->anonCount;
326
    }
327
328
    /**
329
     * Get the number of minor edits to the page.
330
     * @return int
331
     */
332
    public function getMinorCount(): int
333
    {
334
        return $this->minorCount;
335
    }
336
337
    /**
338
     * Get the number of edits to the page made in the past day, week, month and year.
339
     * @return int[] With keys 'day', 'week', 'month' and 'year'.
340
     */
341
    public function getCountHistory(): array
342
    {
343
        return $this->countHistory;
344
    }
345
346
    /**
347
     * Get the number of edits to the page made by the top 10 editors.
348
     * @return int
349
     */
350
    public function getTopTenCount(): int
351
    {
352
        return $this->topTenCount;
353
    }
354
355
    /**
356
     * Get the first edit to the page.
357
     * @return Edit
358
     */
359
    public function getFirstEdit(): Edit
360
    {
361
        return $this->firstEdit;
362
    }
363
364
    /**
365
     * Get the last edit to the page.
366
     * @return Edit
367
     */
368
    public function getLastEdit(): Edit
369
    {
370
        return $this->lastEdit;
371
    }
372
373
    /**
374
     * Get the edit that made the largest addition to the page (by number of bytes).
375
     * @return Edit|null
376
     */
377
    public function getMaxAddition(): ?Edit
378
    {
379
        return $this->maxAddition;
380
    }
381
382
    /**
383
     * Get the edit that made the largest removal to the page (by number of bytes).
384
     * @return Edit|null
385
     */
386
    public function getMaxDeletion(): ?Edit
387
    {
388
        return $this->maxDeletion;
389
    }
390
391
    /**
392
     * Get the list of editors to the page, including various statistics.
393
     * @return array
394
     */
395
    public function getEditors(): array
396
    {
397
        return $this->editors;
398
    }
399
400
    /**
401
     * Get usernames of human editors (not bots).
402
     * @param int|null $limit
403
     * @return string[]
404
     */
405
    public function getHumans(?int $limit = null): array
406
    {
407
        return array_slice(array_diff(array_keys($this->getEditors()), array_keys($this->getBots())), 0, $limit);
408
    }
409
410
    /**
411
     * Get the list of the top editors to the page (by edits), including various statistics.
412
     * @return array
413
     */
414
    public function topTenEditorsByEdits(): array
415
    {
416
        return $this->topTenEditorsByEdits;
417
    }
418
419
    /**
420
     * Get the list of the top editors to the page (by added text), including various statistics.
421
     * @return array
422
     */
423
    public function topTenEditorsByAdded(): array
424
    {
425
        return $this->topTenEditorsByAdded;
426
    }
427
428
    /**
429
     * Get various counts about each individual year and month of the page's history.
430
     * @return array
431
     */
432
    public function getYearMonthCounts(): array
433
    {
434
        return $this->yearMonthCounts;
435
    }
436
437
    /**
438
     * Get the localized labels for the 'Year counts' chart.
439
     * @return string[]
440
     */
441
    public function getYearLabels(): array
442
    {
443
        return $this->yearLabels;
444
    }
445
446
    /**
447
     * Get the localized labels for the 'Month counts' chart.
448
     * @return string[]
449
     */
450
    public function getMonthLabels(): array
451
    {
452
        return $this->monthLabels;
453
    }
454
455
    /**
456
     * Get the maximum number of edits that were created across all months. This is used as a
457
     * comparison for the bar charts in the months section.
458
     * @return int
459
     */
460
    public function getMaxEditsPerMonth(): int
461
    {
462
        return $this->maxEditsPerMonth;
463
    }
464
465
    /**
466
     * Get a list of (semi-)automated tools that were used to edit the page, including
467
     * the number of times they were used, and a link to the tool's homepage.
468
     * @return string[]
469
     */
470
    public function getTools(): array
471
    {
472
        return $this->tools;
473
    }
474
475
    /**
476
     * Parse the revision history, collecting our core statistics.
477
     *
478
     * Untestable because it relies on getting a PDO statement. All the important
479
     * logic lives in other methods which are tested.
480
     * @codeCoverageIgnore
481
     */
482
    private function parseHistory(): void
483
    {
484
        $limit = $this->tooManyRevisions() ? $this->repository->getMaxPageRevisions() : null;
485
486
        // Third parameter is ignored if $limit is null.
487
        $revStmt = $this->page->getRevisionsStmt(
488
            null,
489
            $limit,
490
            $this->getNumRevisions(),
491
            $this->start,
492
            $this->end
493
        );
494
        $revCount = 0;
495
496
        /**
497
         * Data about previous edits so that we can use them as a basis for comparison.
498
         * @var Edit[] $prevEdits
499
         */
500
        $prevEdits = [
501
            // The previous Edit, used to discount content that was reverted.
502
            'prev' => null,
503
504
            // The SHA-1 of the edit *before* the previous edit. Used for more
505
            // accurate revert detection.
506
            'prevSha' => null,
507
508
            // The last edit deemed to be the max addition of content. This is kept track of
509
            // in case we find out the next edit was reverted (and was also a max edit),
510
            // in which case we'll want to discount it and use this one instead.
511
            'maxAddition' => null,
512
513
            // Same as with maxAddition, except the maximum amount of content deleted.
514
            // This is used to discount content that was reverted.
515
            'maxDeletion' => null,
516
        ];
517
518
        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

518
        while ($rev = $revStmt->/** @scrutinizer ignore-call */ fetchAssociative()) {
Loading history...
519
            $edit = $this->repository->getEdit($this->page, $rev);
0 ignored issues
show
Bug introduced by
The method getEdit() 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 or App\Repository\TopEditsRepository. ( Ignorable by Annotation )

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

519
            /** @scrutinizer ignore-call */ 
520
            $edit = $this->repository->getEdit($this->page, $rev);
Loading history...
520
521
            if (0 === $revCount) {
522
                $this->firstEdit = $edit;
523
            }
524
525
            // Sometimes, with old revisions (2001 era), the revisions from 2002 come before 2001
526
            if ($edit->getTimestamp() < $this->firstEdit->getTimestamp()) {
527
                $this->firstEdit = $edit;
528
            }
529
530
            $prevEdits = $this->updateCounts($edit, $prevEdits);
531
532
            $revCount++;
533
        }
534
535
        $this->numRevisionsProcessed = $revCount;
536
537
        // Various sorts
538
        arsort($this->editors);
539
        ksort($this->yearMonthCounts);
540
        if ($this->tools) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->tools of type array 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...
541
            arsort($this->tools);
542
        }
543
    }
544
545
    /**
546
     * Update various counts based on the current edit.
547
     * @param Edit $edit
548
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'
549
     * @return Edit[] Updated version of $prevEdits.
550
     */
551
    private function updateCounts(Edit $edit, array $prevEdits): array
552
    {
553
        // Update the counts for the year and month of the current edit.
554
        $this->updateYearMonthCounts($edit);
555
556
        // Update counts for the user who made the edit.
557
        $this->updateUserCounts($edit);
558
559
        // Update the year/month/user counts of anon and minor edits.
560
        $this->updateAnonMinorCounts($edit);
561
562
        // Update counts for automated tool usage, if applicable.
563
        $this->updateToolCounts($edit);
564
565
        // Increment "edits per <time>" counts
566
        $this->updateCountHistory($edit);
567
568
        // Update figures regarding content addition/removal, and the revert count.
569
        $prevEdits = $this->updateContentSizes($edit, $prevEdits);
570
571
        // Now that we've updated all the counts, we can reset
572
        // the prev and last edits, which are used for tracking.
573
        // But first, let's copy over the SHA of the actual previous edit
574
        // and put it in our $prevEdits['prev'], so that we'll know
575
        // that content added after $prevEdit['prev'] was reverted.
576
        if (null !== $prevEdits['prev']) {
577
            $prevEdits['prevSha'] = $prevEdits['prev']->getSha();
578
        }
579
        $prevEdits['prev'] = $edit;
580
        $this->lastEdit = $edit;
581
582
        return $prevEdits;
583
    }
584
585
    /**
586
     * Update various figures about content sizes based on the given edit.
587
     * @param Edit $edit
588
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
589
     * @return Edit[] Updated version of $prevEdits.
590
     */
591
    private function updateContentSizes(Edit $edit, array $prevEdits): array
592
    {
593
        // Check if it was a revert
594
        if ($this->isRevert($edit, $prevEdits)) {
595
            $edit->setReverted(true);
596
            return $this->updateContentSizesRevert($prevEdits);
597
        } else {
598
            return $this->updateContentSizesNonRevert($edit, $prevEdits);
599
        }
600
    }
601
602
    /**
603
     * Is the given Edit a revert?
604
     * @param Edit $edit
605
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
606
     * @return bool
607
     */
608
    private function isRevert(Edit $edit, array $prevEdits): bool
609
    {
610
        return $edit->getSha() === $prevEdits['prevSha'] || $edit->isRevert();
611
    }
612
613
    /**
614
     * Updates the figures on content sizes assuming the given edit was a revert of the previous one.
615
     * In such a case, we don't want to treat the previous edit as legit content addition or removal.
616
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
617
     * @return Edit[] Updated version of $prevEdits, for tracking.
618
     */
619
    private function updateContentSizesRevert(array $prevEdits): array
620
    {
621
        $this->revertCount++;
622
623
        // Adjust addedBytes given this edit was a revert of the previous one.
624
        if ($prevEdits['prev'] && !$prevEdits['prev']->isReverted() && $prevEdits['prev']->getSize() > 0) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $prevEdits['prev']->isReverted() of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
625
            $this->addedBytes -= $prevEdits['prev']->getSize();
626
627
            // Also deduct from the user's individual added byte count.
628
            // We don't do this if the previous edit was reverted, since that would make the net bytes zero.
629
            if ($prevEdits['prev']->getUser()) {
630
                $username = $prevEdits['prev']->getUser()->getUsername();
631
                $this->editors[$username]['added'] -= $prevEdits['prev']->getSize();
632
            }
633
        }
634
635
        // @TODO: Test this against an edit war (use your sandbox).
636
        // Also remove as max added or deleted, if applicable.
637
        if ($this->maxAddition && $prevEdits['prev']->getId() === $this->maxAddition->getId()) {
638
            $this->maxAddition = $prevEdits['maxAddition'];
639
            $prevEdits['maxAddition'] = $prevEdits['prev']; // In the event of edit wars.
640
        } elseif ($this->maxDeletion && $prevEdits['prev']->getId() === $this->maxDeletion->getId()) {
641
            $this->maxDeletion = $prevEdits['maxDeletion'];
642
            $prevEdits['maxDeletion'] = $prevEdits['prev']; // In the event of edit wars.
643
        }
644
645
        return $prevEdits;
646
    }
647
648
    /**
649
     * Updates the figures on content sizes assuming the given edit was NOT a revert of the previous edit.
650
     * @param Edit $edit
651
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
652
     * @return Edit[] Updated version of $prevEdits, for tracking.
653
     */
654
    private function updateContentSizesNonRevert(Edit $edit, array $prevEdits): array
655
    {
656
        $editSize = $this->getEditSize($edit, $prevEdits);
657
658
        // Edit was not a revert, so treat size > 0 as content added.
659
        if ($editSize > 0) {
660
            $this->addedBytes += $editSize;
661
662
            if ($edit->getUser()) {
663
                $this->editors[$edit->getUser()->getUsername()]['added'] += $editSize;
664
            }
665
666
            // Keep track of edit with max addition.
667
            if (!$this->maxAddition || $editSize > $this->maxAddition->getSize()) {
668
                // Keep track of old maxAddition in case we find out the next $edit was reverted
669
                // (and was also a max edit), in which case we'll want to use this one ($edit).
670
                $prevEdits['maxAddition'] = $this->maxAddition;
671
672
                $this->maxAddition = $edit;
673
            }
674
        } elseif ($editSize < 0 && (!$this->maxDeletion || $editSize < $this->maxDeletion->getSize())) {
675
            // Keep track of old maxDeletion in case we find out the next edit was reverted
676
            // (and was also a max deletion), in which case we'll want to use this one.
677
            $prevEdits['maxDeletion'] = $this->maxDeletion;
678
679
            $this->maxDeletion = $edit;
680
        }
681
682
        return $prevEdits;
683
    }
684
685
    /**
686
     * Get the size of the given edit, based on the previous edit (if present).
687
     * We also don't return the actual edit size if last revision had a length of null.
688
     * This happens when the edit follows other edits that were revision-deleted.
689
     * @see T148857 for more information.
690
     * @todo Remove once T101631 is resolved.
691
     * @param Edit $edit
692
     * @param Edit[] $prevEdits With 'prev', 'prevSha', 'maxAddition' and 'maxDeletion'.
693
     * @return int
694
     */
695
    private function getEditSize(Edit $edit, array $prevEdits): int
696
    {
697
        if ($prevEdits['prev'] && null === $prevEdits['prev']->getLength()) {
0 ignored issues
show
introduced by
The condition null === $prevEdits['prev']->getLength() is always false.
Loading history...
698
            return 0;
699
        } else {
700
            return $edit->getSize();
701
        }
702
    }
703
704
    /**
705
     * Update counts of automated tool usage for the given edit.
706
     * @param Edit $edit
707
     */
708
    private function updateToolCounts(Edit $edit): void
709
    {
710
        $automatedTool = $edit->getTool();
711
712
        if (!$automatedTool) {
713
            // Nothing to do.
714
            return;
715
        }
716
717
        $editYear = $edit->getYear();
718
        $editMonth = $edit->getMonth();
719
720
        $this->automatedCount++;
721
        $this->yearMonthCounts[$editYear]['automated']++;
722
        $this->yearMonthCounts[$editYear]['months'][$editMonth]['automated']++;
723
724
        if (!isset($this->tools[$automatedTool['name']])) {
725
            $this->tools[$automatedTool['name']] = [
726
                'count' => 1,
727
                'link' => $automatedTool['link'],
728
            ];
729
        } else {
730
            $this->tools[$automatedTool['name']]['count']++;
731
        }
732
    }
733
734
    /**
735
     * Update various counts for the year and month of the given edit.
736
     * @param Edit $edit
737
     */
738
    private function updateYearMonthCounts(Edit $edit): void
739
    {
740
        $editYear = $edit->getYear();
741
        $editMonth = $edit->getMonth();
742
743
        // Fill in the blank arrays for the year and 12 months if needed.
744
        if (!isset($this->yearMonthCounts[$editYear])) {
745
            $this->addYearMonthCountEntry($edit);
746
        }
747
748
        // Increment year and month counts for all edits
749
        $this->yearMonthCounts[$editYear]['all']++;
750
        $this->yearMonthCounts[$editYear]['months'][$editMonth]['all']++;
751
        // This will ultimately be the size of the page by the end of the year
752
        $this->yearMonthCounts[$editYear]['size'] = $edit->getLength();
753
754
        // Keep track of which month had the most edits
755
        $editsThisMonth = $this->yearMonthCounts[$editYear]['months'][$editMonth]['all'];
756
        if ($editsThisMonth > $this->maxEditsPerMonth) {
757
            $this->maxEditsPerMonth = $editsThisMonth;
758
        }
759
    }
760
761
    /**
762
     * Add a new entry to $this->yearMonthCounts for the given year,
763
     * with blank values for each month. This called during self::parseHistory().
764
     * @param Edit $edit
765
     */
766
    private function addYearMonthCountEntry(Edit $edit): void
767
    {
768
        $this->yearLabels[] = $this->i18n->dateFormat($edit->getTimestamp(), 'yyyy');
769
        $editYear = $edit->getYear();
770
771
        // Beginning of the month at 00:00:00.
772
        $firstEditTime = mktime(0, 0, 0, (int)$this->firstEdit->getMonth(), 1, (int)$this->firstEdit->getYear());
773
774
        $this->yearMonthCounts[$editYear] = [
775
            'all' => 0,
776
            'minor' => 0,
777
            'anon' => 0,
778
            'automated' => 0,
779
            'size' => 0, // Keep track of the size by the end of the year.
780
            'events' => [],
781
            'months' => [],
782
        ];
783
784
        for ($i = 1; $i <= 12; $i++) {
785
            $timeObj = mktime(0, 0, 0, $i, 1, (int)$editYear);
786
787
            // Don't show zeros for months before the first edit or after the current month.
788
            if ($timeObj < $firstEditTime || $timeObj > $this->getLastDay()) {
789
                continue;
790
            }
791
792
            $this->monthLabels[] = $this->i18n->dateFormat($timeObj, 'yyyy-MM');
793
            $this->yearMonthCounts[$editYear]['months'][sprintf('%02d', $i)] = [
794
                'all' => 0,
795
                'minor' => 0,
796
                'anon' => 0,
797
                'automated' => 0,
798
            ];
799
        }
800
    }
801
802
    /**
803
     * Update the counts of anon and minor edits for year, month, and user of the given edit.
804
     * @param Edit $edit
805
     */
806
    private function updateAnonMinorCounts(Edit $edit): void
807
    {
808
        $editYear = $edit->getYear();
809
        $editMonth = $edit->getMonth();
810
811
        // If anonymous, increase counts
812
        if ($edit->isAnon()) {
813
            $this->anonCount++;
814
            $this->yearMonthCounts[$editYear]['anon']++;
815
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['anon']++;
816
        }
817
818
        // If minor edit, increase counts
819
        if ($edit->isMinor()) {
820
            $this->minorCount++;
821
            $this->yearMonthCounts[$editYear]['minor']++;
822
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['minor']++;
823
        }
824
    }
825
826
    /**
827
     * Update various counts for the user of the given edit.
828
     * @param Edit $edit
829
     */
830
    private function updateUserCounts(Edit $edit): void
831
    {
832
        if (!$edit->getUser()) {
833
            return;
834
        }
835
836
        $username = $edit->getUser()->getUsername();
837
838
        // Initialize various user stats if needed.
839
        if (!isset($this->editors[$username])) {
840
            $this->editors[$username] = [
841
                'all' => 0,
842
                'minor' => 0,
843
                'minorPercentage' => 0,
844
                'first' => $edit->getTimestamp(),
845
                'firstId' => $edit->getId(),
846
                'last' => null,
847
                'atbe' => null,
848
                'added' => 0,
849
            ];
850
        }
851
852
        // Increment user counts
853
        $this->editors[$username]['all']++;
854
        $this->editors[$username]['last'] = $edit->getTimestamp();
855
        $this->editors[$username]['lastId'] = $edit->getId();
856
857
        // Increment minor counts for this user
858
        if ($edit->isMinor()) {
859
            $this->editors[$username]['minor']++;
860
        }
861
    }
862
863
    /**
864
     * Increment "edits per <time>" counts based on the given edit.
865
     * @param Edit $edit
866
     */
867
    private function updateCountHistory(Edit $edit): void
868
    {
869
        $editTimestamp = $edit->getTimestamp();
870
871
        if ($editTimestamp > new DateTime('-1 day')) {
872
            $this->countHistory['day']++;
873
        }
874
        if ($editTimestamp > new DateTime('-1 week')) {
875
            $this->countHistory['week']++;
876
        }
877
        if ($editTimestamp > new DateTime('-1 month')) {
878
            $this->countHistory['month']++;
879
        }
880
        if ($editTimestamp > new DateTime('-1 year')) {
881
            $this->countHistory['year']++;
882
        }
883
    }
884
885
    /**
886
     * Query for log events during each year of the article's history, and set the results in $this->yearMonthCounts.
887
     */
888
    private function setLogsEvents(): void
889
    {
890
        $logData = $this->repository->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

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