Passed
Pull Request — main (#426)
by MusikAnimal
08:27 queued 04:14
created

ArticleInfo   F

Complexity

Total Complexity 121

Size/Duplication

Total Lines 1031
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 325
dl 0
loc 1031
rs 2
c 0
b 0
f 0
wmc 121

52 Methods

Rating   Name   Duplication   Size   Complexity  
A topTenEditorsByEdits() 0 3 1
A getYearLabels() 0 3 1
A updateUserCounts() 0 30 4
A getPageviews() 0 8 2
A getCountHistory() 0 3 1
A editsPerYear() 0 6 2
A getNumEditors() 0 3 1
A getAutomatedCount() 0 3 1
A updateContentSizes() 0 8 2
A getTools() 0 3 1
A updateToolCounts() 0 23 3
A getLength() 0 7 2
A getMaxAddition() 0 3 1
A getNumRevisionsProcessed() 0 13 3
A editsPerDay() 0 6 2
A getHumans() 0 3 1
A isRevert() 0 3 2
A setI18nHelper() 0 3 1
A getYearMonthCounts() 0 3 1
A getMonthLabels() 0 3 1
A anonPercentage() 0 5 1
A prepareData() 0 9 1
A addYearMonthCountEntry() 0 32 4
B doPostPrecessing() 0 42 6
A getEditors() 0 3 1
A getLastEdit() 0 3 1
A averageDaysPerEdit() 0 3 1
A editsPerMonth() 0 6 2
B updateContentSizesNonRevert() 0 29 8
A getEditSize() 0 6 3
A getRevertCount() 0 3 1
A editsPerEditor() 0 8 2
A updateYearMonthCounts() 0 20 3
A getMaxDeletion() 0 3 1
A topTenEditorsByAdded() 0 3 1
A getTopTenCount() 0 3 1
A getDateParams() 0 19 4
A getMinorCount() 0 3 1
A minorPercentage() 0 5 1
A updateCountHistory() 0 15 5
B updateContentSizesRevert() 0 27 9
B parseHistory() 0 60 6
A topTenPercentage() 0 3 1
A getFirstEdit() 0 3 1
B setLogsEvents() 0 40 8
A getMaxEditsPerMonth() 0 3 1
A getTopTenByAdded() 0 30 4
A updateAnonMinorCounts() 0 17 3
A getLastDay() 0 8 2
A updateCounts() 0 32 2
A getTotalDays() 0 10 2
A getAnonCount() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like ArticleInfo often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ArticleInfo, and based on these observations, apply Extract Interface, too.

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

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

535
        while ($rev = /** @scrutinizer ignore-deprecated */ $revStmt->fetch()) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
536
            $edit = new Edit($this->page, $rev);
537
538
            if (0 === $revCount) {
539
                $this->firstEdit = $edit;
540
            }
541
542
            // Sometimes, with old revisions (2001 era), the revisions from 2002 come before 2001
543
            if ($edit->getTimestamp() < $this->firstEdit->getTimestamp()) {
544
                $this->firstEdit = $edit;
545
            }
546
547
            $prevEdits = $this->updateCounts($edit, $prevEdits);
548
549
            $revCount++;
550
        }
551
552
        $this->numRevisionsProcessed = $revCount;
553
554
        // Various sorts
555
        arsort($this->editors);
556
        ksort($this->yearMonthCounts);
557
        if ($this->tools) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->tools of type array<mixed,string[]> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

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

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

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