Passed
Pull Request — master (#125)
by MusikAnimal
03:53
created

ArticleInfo::getBugs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 6
ccs 0
cts 4
cp 0
crap 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the ArticleInfo class.
4
 */
5
6
namespace Xtools;
7
8
use Symfony\Component\DependencyInjection\Container;
9
use DateTime;
10
11
/**
12
 * An ArticleInfo provides statistics about a page on a project. This model does not
13
 * have a separate Repository because it needs to use individual SQL statements to
14
 * traverse the page's history, saving class instance variables along the way.
15
 */
16
class ArticleInfo extends Model
17
{
18
    /** @var Container The application's DI container. */
19
    protected $container;
20
21
    /** @var Page The page. */
22
    protected $page;
23
24
    /** @var int Number of revisions that belong to the page. */
25
    protected $numRevisions;
26
27
    /** @var int Maximum number of revisions to process, as configured. */
28
    protected $maxRevisions;
29
30
    /** @var int Number of revisions that were actually processed. */
31
    protected $numRevisionsProcessed;
32
33
    /**
34
     * Various statistics about editors to the page. These are not User objects
35
     * so as to preserve memory.
36
     * @var mixed[]
37
     */
38
    protected $editors;
39
40
    /** @var mixed[] The top 10 editors to the page by number of edits. */
41
    protected $topTenEditorsByEdits;
42
43
    /** @var mixed[] The top 10 editors to the page by added text. */
44
    protected $topTenEditorsByAdded;
45
46
    /** @var int Number of edits made by the top 10 editors. */
47
    protected $topTenCount;
48
49
    /** @var mixed[] Various statistics about bots that edited the page. */
50
    protected $bots;
51
52
    /** @var int Number of edits made to the page by bots. */
53
    protected $botRevisionCount;
54
55
    /** @var mixed[] Various counts about each individual year and month of the page's history. */
56
    protected $yearMonthCounts;
57
58
    /** @var Edit The first edit to the page. */
59
    protected $firstEdit;
60
61
    /** @var Edit The last edit to the page. */
62
    protected $lastEdit;
63
64
    /** @var Edit Edit that made the largest addition by number of bytes. */
65
    protected $maxAddition;
66
67
    /** @var Edit Edit that made the largest deletion by number of bytes. */
68
    protected $maxDeletion;
69
70
    /** @var int[] Number of in and outgoing links and redirects to the page. */
71
    protected $linksAndRedirects;
72
73
    /** @var string[] Assessments of the page (see Page::getAssessments). */
74
    protected $assessments;
75
76
    /**
77
     * Maximum number of edits that were created across all months. This is used as a comparison
78
     * for the bar charts in the months section.
79
     * @var int
80
     */
81
    protected $maxEditsPerMonth;
82
83
    /** @var string[] List of (semi-)automated tools that were used to edit the page. */
84
    protected $tools;
85
86
    /**
87
     * Total number of bytes added throughout the page's history. This is used as a comparison
88
     * when computing the top 10 editors by added text.
89
     * @var int
90
     */
91
    protected $addedBytes = 0;
92
93
    /** @var int Number of days between first and last edit. */
94
    protected $totalDays;
95
96
    /** @var int Number of minor edits to the page. */
97
    protected $minorCount = 0;
98
99
    /** @var int Number of anonymous edits to the page. */
100
    protected $anonCount = 0;
101
102
    /** @var int Number of automated edits to the page. */
103
    protected $automatedCount = 0;
104
105
    /** @var int Number of edits to the page that were reverted with the subsequent edit. */
106
    protected $revertCount = 0;
107
108
    /** @var int[] The "edits per <time>" counts. */
109
    protected $countHistory = [
110
        'day' => 0,
111
        'week' => 0,
112
        'month' => 0,
113
        'year' => 0
114
    ];
115
116
    /** @var string[] List of wikidata and Checkwiki errors. */
117
    protected $bugs;
118
119
    /**
120
     * ArticleInfo constructor.
121
     * @param Page $page The page to process.
122
     * @param Container $container The DI container.
123
     */
124 9
    public function __construct(Page $page, Container $container)
125
    {
126 9
        $this->page = $page;
127 9
        $this->container = $container;
128 9
    }
129
130
    /**
131
     * Shorthand to get the page's project.
132
     * @return Project
133
     * @codeCoverageIgnore
134
     */
135
    public function getProject()
136
    {
137
        return $this->page->getProject();
138
    }
139
140
    /**
141
     * Get the number of revisions belonging to the page.
142
     * @return int
143
     */
144 4
    public function getNumRevisions()
145
    {
146 4
        if (!isset($this->numRevisions)) {
147 4
            $this->numRevisions = $this->page->getNumRevisions();
148
        }
149 4
        return $this->numRevisions;
150
    }
151
152
    /**
153
     * Get the maximum number of revisions that we should process.
154
     * @return int
155
     */
156 3
    public function getMaxRevisions()
157
    {
158 3
        if (!isset($this->maxRevisions)) {
159 3
            $this->maxRevisions = (int) $this->container->getParameter('app.max_page_revisions');
160
        }
161 3
        return $this->maxRevisions;
162
    }
163
164
    /**
165
     * Get the number of revisions that are actually getting processed.
166
     * This goes by the app.max_page_revisions parameter, or the actual
167
     * number of revisions, whichever is smaller.
168
     * @return int
169
     */
170 5
    public function getNumRevisionsProcessed()
171
    {
172 5
        if (isset($this->numRevisionsProcessed)) {
173 3
            return $this->numRevisionsProcessed;
174
        }
175
176 2
        if ($this->tooManyRevisions()) {
177 1
            $this->numRevisionsProcessed = $this->getMaxRevisions();
178
        } else {
179 1
            $this->numRevisionsProcessed = $this->getNumRevisions();
180
        }
181
182 2
        return $this->numRevisionsProcessed;
183
    }
184
185
    /**
186
     * Are there more revisions than we should process, based on the config?
187
     * @return bool
188
     */
189 3
    public function tooManyRevisions()
190
    {
191 3
        return $this->getMaxRevisions() > 0 && $this->getNumRevisions() > $this->getMaxRevisions();
192
    }
193
194
    /**
195
     * Fetch and store all the data we need to show the ArticleInfo view.
196
     * @codeCoverageIgnore
197
     */
198
    public function prepareData()
199
    {
200
        $this->parseHistory();
201
        $this->setLogsEvents();
202
        $this->setTopTenCounts();
203
    }
204
205
    /**
206
     * Get the number of editors that edited the page.
207
     * @return int
208
     */
209 1
    public function getNumEditors()
210
    {
211 1
        return count($this->editors);
212
    }
213
214
    /**
215
     * Get the number of bots that edited the page.
216
     * @return int
217
     */
218
    public function getNumBots()
219
    {
220
        return count($this->getBots());
221
    }
222
223
    /**
224
     * Get the number of days between the first and last edit.
225
     * @return int
226
     */
227 1
    public function getTotalDays()
228
    {
229 1
        if (isset($this->totalDays)) {
230 1
            return $this->totalDays;
231
        }
232 1
        $dateFirst = $this->firstEdit->getTimestamp();
233 1
        $dateLast = $this->lastEdit->getTimestamp();
234 1
        $interval = date_diff($dateLast, $dateFirst, true);
235 1
        $this->totalDays = $interval->format('%a');
236 1
        return $this->totalDays;
237
    }
238
239
    /**
240
     * Get the average number of days between edits to the page.
241
     * @return double
242
     */
243 1
    public function averageDaysPerEdit()
244
    {
245 1
        return round($this->getTotalDays() / $this->getNumRevisionsProcessed(), 1);
246
    }
247
248
    /**
249
     * Get the average number of edits per day to the page.
250
     * @return double
251
     */
252 1
    public function editsPerDay()
253
    {
254 1
        $editsPerDay = $this->getTotalDays()
255 1
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / (365 / 12 / 24))
256 1
            : 0;
257 1
        return round($editsPerDay, 1);
258
    }
259
260
    /**
261
     * Get the average number of edits per month to the page.
262
     * @return double
263
     */
264 1 View Code Duplication
    public function editsPerMonth()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
265
    {
266 1
        $editsPerMonth = $this->getTotalDays()
267 1
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / (365 / 12))
268 1
            : 0;
269 1
        return min($this->getNumRevisionsProcessed(), round($editsPerMonth, 1));
270
    }
271
272
    /**
273
     * Get the average number of edits per year to the page.
274
     * @return double
275
     */
276 1 View Code Duplication
    public function editsPerYear()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
277
    {
278 1
        $editsPerYear = $this->getTotalDays()
279 1
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / 365)
280 1
            : 0;
281 1
        return min($this->getNumRevisionsProcessed(), round($editsPerYear, 1));
282
    }
283
284
    /**
285
     * Get the average number of edits per editor.
286
     * @return double
287
     */
288 1
    public function editsPerEditor()
289
    {
290 1
        return round($this->getNumRevisionsProcessed() / count($this->editors), 1);
291
    }
292
293
    /**
294
     * Get the percentage of minor edits to the page.
295
     * @return double
296
     */
297 1
    public function minorPercentage()
298
    {
299 1
        return round(
300 1
            ($this->minorCount / $this->getNumRevisionsProcessed()) * 100,
301 1
            1
302
        );
303
    }
304
305
    /**
306
     * Get the percentage of anonymous edits to the page.
307
     * @return double
308
     */
309 1
    public function anonPercentage()
310
    {
311 1
        return round(
312 1
            ($this->anonCount / $this->getNumRevisionsProcessed()) * 100,
313 1
            1
314
        );
315
    }
316
317
    /**
318
     * Get the percentage of edits made by the top 10 editors.
319
     * @return double
320
     */
321 1
    public function topTenPercentage()
322
    {
323 1
        return round(($this->topTenCount / $this->getNumRevisionsProcessed()) * 100, 1);
324
    }
325
326
    /**
327
     * Get the number of times the page has been viewed in the given timeframe.
328
     * @param  int $latest Last N days.
329
     * @return int
330
     */
331
    public function getPageviews($latest)
332
    {
333
        return $this->page->getLastPageviews($latest);
334
    }
335
336
    /**
337
     * Get the page assessments of the page.
338
     * @see https://www.mediawiki.org/wiki/Extension:PageAssessments
339
     * @return string[]|false False if unsupported.
340
     * @codeCoverageIgnore
341
     */
342
    public function getAssessments()
343
    {
344
        if (!is_array($this->assessments)) {
345
            $this->assessments = $this->page->getAssessments();
346
        }
347
        return $this->assessments;
348
    }
349
350
    /**
351
     * Get the number of automated edits made to the page.
352
     * @return int
353
     */
354 1
    public function getAutomatedCount()
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()
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()
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()
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()
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()
400
    {
401 1
        return $this->topTenCount;
402
    }
403
404
    /**
405
     * Get the first edit to the page.
406
     * @return Edit
407
     */
408
    public function getFirstEdit()
409
    {
410
        return $this->firstEdit;
411
    }
412
413
    /**
414
     * Get the last edit to the page.
415
     * @return Edit
416
     */
417 1
    public function getLastEdit()
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
425
     */
426 1
    public function getMaxAddition()
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
434
     */
435 1
    public function getMaxDeletion()
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()
445
    {
446 1
        return $this->editors;
447
    }
448
449
    /**
450
     * Get the list of the top editors to the page (by edits), including various statistics.
451
     * @return mixed[]
452
     */
453 1
    public function topTenEditorsByEdits()
454
    {
455 1
        return $this->topTenEditorsByEdits;
456
    }
457
458
    /**
459
     * Get the list of the top editors to the page (by added text), including various statistics.
460
     * @return mixed[]
461
     */
462 1
    public function topTenEditorsByAdded()
463
    {
464 1
        return $this->topTenEditorsByAdded;
465
    }
466
467
    /**
468
     * Get various counts about each individual year and month of the page's history.
469
     * @return mixed[]
470
     */
471 2
    public function getYearMonthCounts()
472
    {
473 2
        return $this->yearMonthCounts;
474
    }
475
476
    /**
477
     * Get the maximum number of edits that were created across all months. This is used as a
478
     * comparison for the bar charts in the months section.
479
     * @return int
480
     */
481 1
    public function getMaxEditsPerMonth()
482
    {
483 1
        return $this->maxEditsPerMonth;
484
    }
485
486
    /**
487
     * Get a list of (semi-)automated tools that were used to edit the page, including
488
     * the number of times they were used, and a link to the tool's homepage.
489
     * @return mixed[]
490
     */
491 1
    public function getTools()
492
    {
493 1
        return $this->tools;
494
    }
495
496
    /**
497
     * Get the list of page's wikidata and Checkwiki errors.
498
     * @see Page::getErrors()
499
     * @return string[]
500
     */
501
    public function getBugs()
502
    {
503
        if (!is_array($this->bugs)) {
504
            $this->bugs = $this->page->getErrors();
505
        }
506
        return $this->bugs;
507
    }
508
509
    /**
510
     * Get the number of wikidata nad CheckWiki errors.
511
     * @return int
512
     */
513
    public function numBugs()
514
    {
515
        return count($this->getBugs());
516
    }
517
518
    /**
519
     * Get the number of external links on the page.
520
     * @return int
521
     */
522 1
    public function linksExtCount()
523
    {
524 1
        return $this->getLinksAndRedirects()['links_ext_count'];
525
    }
526
527
    /**
528
     * Get the number of incoming links to the page.
529
     * @return int
530
     */
531 1
    public function linksInCount()
532
    {
533 1
        return $this->getLinksAndRedirects()['links_in_count'];
534
    }
535
536
    /**
537
     * Get the number of outgoing links from the page.
538
     * @return int
539
     */
540 1
    public function linksOutCount()
541
    {
542 1
        return $this->getLinksAndRedirects()['links_out_count'];
543
    }
544
545
    /**
546
     * Get the number of redirects to the page.
547
     * @return int
548
     */
549 1
    public function redirectsCount()
550
    {
551 1
        return $this->getLinksAndRedirects()['redirects_count'];
552
    }
553
554
    /**
555
     * Get the number of external, incoming and outgoing links, along with
556
     * the number of redirects to the page.
557
     * @return int
558
     * @codeCoverageIgnore
559
     */
560
    private function getLinksAndRedirects()
561
    {
562
        if (!is_array($this->linksAndRedirects)) {
563
            $this->linksAndRedirects = $this->page->countLinksAndRedirects();
564
        }
565
        return $this->linksAndRedirects;
566
    }
567
568
    /**
569
     * Parse the revision history, collecting our core statistics.
570
     * @return mixed[] Associative "master" array of metadata about the page.
571
     *
572
     * Untestable because it relies on getting a PDO statement. All the important
573
     * logic lives in other methods which are tested.
574
     * @codeCoverageIgnore
575
     */
576
    private function parseHistory()
577
    {
578
        if ($this->tooManyRevisions()) {
579
            $limit = $this->getMaxRevisions();
580
        } else {
581
            $limit = null;
582
        }
583
584
        // Third parameter is ignored if $limit is null.
585
        $revStmt = $this->page->getRevisionsStmt(null, $limit, $this->getNumRevisions());
586
        $revCount = 0;
587
588
        /**
589
         * Data about previous edits so that we can use them as a basis for comparison.
590
         * @var Edit[]
591
         */
592
        $prevEdits = [
593
            // The previous Edit, used to discount content that was reverted.
594
            'prev' => null,
595
596
            // The last edit deemed to be the max addition of content. This is kept track of
597
            // in case we find out the next edit was reverted (and was also a max edit),
598
            // in which case we'll want to discount it and use this one instead.
599
            'maxAddition' => null,
600
601
            // Same as with maxAddition, except the maximum amount of content deleted.
602
            // This is used to discount content that was reverted.
603
            'maxDeletion' => null,
604
        ];
605
606
        while ($rev = $revStmt->fetch()) {
607
            $edit = new Edit($this->page, $rev);
608
609
            if ($revCount === 0) {
610
                $this->firstEdit = $edit;
611
            }
612
613
            // Sometimes, with old revisions (2001 era), the revisions from 2002 come before 2001
614
            if ($edit->getTimestamp() < $this->firstEdit->getTimestamp()) {
615
                $this->firstEdit = $edit;
616
            }
617
618
            $prevEdits = $this->updateCounts($edit, $prevEdits);
619
620
            $revCount++;
621
        }
622
623
        $this->numRevisionsProcessed = $revCount;
624
625
        // Various sorts
626
        arsort($this->editors);
627
        ksort($this->yearMonthCounts);
628
        if ($this->tools) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->tools of type 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...
629
            arsort($this->tools);
630
        }
631
    }
632
633
    /**
634
     * Update various counts based on the current edit.
635
     * @param  Edit   $edit
636
     * @param  Edit[] $prevEdits With 'prev', 'maxAddition' and 'maxDeletion'
637
     * @return Edit[] Updated version of $prevEdits.
638
     */
639 3
    private function updateCounts(Edit $edit, $prevEdits)
640
    {
641
        // Update the counts for the year and month of the current edit.
642 3
        $this->updateYearMonthCounts($edit);
643
644
        // Update counts for the user who made the edit.
645 3
        $this->updateUserCounts($edit);
646
647
        // Update the year/month/user counts of anon and minor edits.
648 3
        $this->updateAnonMinorCounts($edit);
649
650
        // Update counts for automated tool usage, if applicable.
651 3
        $this->updateToolCounts($edit);
652
653
        // Increment "edits per <time>" counts
654 3
        $this->updateCountHistory($edit);
655
656
        // Update figures regarding content addition/removal, and the revert count.
657 3
        $prevEdits = $this->updateContentSizes($edit, $prevEdits);
658
659
        // Now that we've updated all the counts, we can reset
660
        // the prev and last edits, which are used for tracking.
661 3
        $prevEdits['prev'] = $edit;
662 3
        $this->lastEdit = $edit;
663
664 3
        return $prevEdits;
665
    }
666
667
    /**
668
     * Update various figures about content sizes based on the given edit.
669
     * @param  Edit   $edit
670
     * @param  Edit[] $prevEdits With 'prev', 'maxAddition' and 'maxDeletion'
671
     * @return Edit[] Updated version of $prevEdits.
672
     */
673 3
    private function updateContentSizes(Edit $edit, $prevEdits)
674
    {
675
        // Check if it was a revert
676 3
        if ($edit->isRevert($this->container)) {
677 3
            return $this->updateContentSizesRevert($prevEdits);
678
        } else {
679 3
            return $this->updateContentSizesNonRevert($edit, $prevEdits);
680
        }
681
    }
682
683
    /**
684
     * Updates the figures on content sizes assuming the given edit was a revert of the previous one.
685
     * In such a case, we don't want to treat the previous edit as legit content addition or removal.
686
     * @param  Edit[] $prevEdits With 'prev', 'maxAddition' and 'maxDeletion'.
687
     * @return Edit[] Updated version of $prevEdits, for tracking.
688
     */
689 3
    private function updateContentSizesRevert($prevEdits)
690
    {
691 3
        $this->revertCount++;
692
693
        // Adjust addedBytes given this edit was a revert of the previous one.
694 3
        if ($prevEdits['prev'] && $prevEdits['prev']->getSize() > 0) {
695
            $this->addedBytes -= $prevEdits['prev']->getSize();
696
        }
697
698
        // @TODO: Test this against an edit war (use your sandbox).
699
        // Also remove as max added or deleted, if applicable.
700 3
        if ($this->maxAddition && $prevEdits['prev']->getId() === $this->maxAddition->getId()) {
701
            $this->maxAddition = $prevEdits['maxAddition'];
702
            $prevEdits['maxAddition'] = $prevEdits['prev']; // in the event of edit wars
703 3
        } elseif ($this->maxDeletion && $prevEdits['prev']->getId() === $this->maxDeletion->getId()) {
704 3
            $this->maxDeletion = $prevEdits['maxDeletion'];
705 3
            $prevEdits['maxDeletion'] = $prevEdits['prev']; // in the event of edit wars
706
        }
707
708 3
        return $prevEdits;
709
    }
710
711
    /**
712
     * Updates the figures on content sizes assuming the given edit
713
     * was NOT a revert of the previous edit.
714
     * @param  Edit   $edit
715
     * @param  Edit[] $prevEdits With 'prev', 'maxAddition' and 'maxDeletion'.
716
     * @return Edit[] Updated version of $prevEdits, for tracking.
717
     */
718 3
    private function updateContentSizesNonRevert(Edit $edit, $prevEdits)
719
    {
720 3
        $editSize = $this->getEditSize($edit, $prevEdits);
721
722
        // Edit was not a revert, so treat size > 0 as content added.
723 3
        if ($editSize > 0) {
724 3
            $this->addedBytes += $editSize;
725 3
            $this->editors[$edit->getUser()->getUsername()]['added'] += $editSize;
726
727
            // Keep track of edit with max addition.
728 3
            if (!$this->maxAddition || $editSize > $this->maxAddition->getSize()) {
729
                // Keep track of old maxAddition in case we find out the next $edit was reverted
730
                // (and was also a max edit), in which case we'll want to use this one ($edit).
731 3
                $prevEdits['maxAddition'] = $this->maxAddition;
732
733 3
                $this->maxAddition = $edit;
734
            }
735 3
        } elseif ($editSize < 0 && (!$this->maxDeletion || $editSize < $this->maxDeletion->getSize())) {
736
            // Keep track of old maxDeletion in case we find out the next edit was reverted
737
            // (and was also a max deletion), in which case we'll want to use this one.
738 3
            $prevEdits['maxDeletion'] = $this->maxDeletion;
739
740 3
            $this->maxDeletion = $edit;
741
        }
742
743 3
        return $prevEdits;
744
    }
745
746
    /**
747
     * Get the size of the given edit, based on the previous edit (if present).
748
     * We also don't return the actual edit size if last revision had a length of null.
749
     * This happens when the edit follows other edits that were revision-deleted.
750
     * @see T148857 for more information.
751
     * @todo Remove once T101631 is resolved.
752
     * @param  Edit   $edit
753
     * @param  Edit[] $prevEdits With 'prev', 'maxAddition' and 'maxDeletion'.
754
     * @return Edit[] Updated version of $prevEdits, for tracking.
755
     */
756 3
    private function getEditSize(Edit $edit, $prevEdits)
757
    {
758 3
        if ($prevEdits['prev'] && $prevEdits['prev']->getLength() === null) {
759
            return 0;
760
        } else {
761 3
            return $edit->getSize();
762
        }
763
    }
764
765
    /**
766
     * Update counts of automated tool usage for the given edit.
767
     * @param Edit $edit
768
     */
769 3
    private function updateToolCounts(Edit $edit)
770
    {
771 3
        $automatedTool = $edit->getTool($this->container);
772
773 3
        if ($automatedTool === false) {
774
            // Nothing to do.
775 3
            return;
776
        }
777
778 3
        $editYear = $edit->getYear();
779 3
        $editMonth = $edit->getMonth();
780
781 3
        $this->automatedCount++;
782 3
        $this->yearMonthCounts[$editYear]['automated']++;
783 3
        $this->yearMonthCounts[$editYear]['months'][$editMonth]['automated']++;
784
785 3
        if (!isset($this->tools[$automatedTool['name']])) {
786 3
            $this->tools[$automatedTool['name']] = [
787 3
                'count' => 1,
788 3
                'link' => $automatedTool['link'],
789
            ];
790
        } else {
791
            $this->tools[$automatedTool['name']]['count']++;
792
        }
793 3
    }
794
795
    /**
796
     * Update various counts for the year and month of the given edit.
797
     * @param Edit $edit
798
     */
799 3
    private function updateYearMonthCounts(Edit $edit)
800
    {
801 3
        $editYear = $edit->getYear();
802 3
        $editMonth = $edit->getMonth();
803
804
        // Fill in the blank arrays for the year and 12 months if needed.
805 3
        if (!isset($this->yearMonthCounts[$editYear])) {
806 3
            $this->addYearMonthCountEntry($edit);
807
        }
808
809
        // Increment year and month counts for all edits
810 3
        $this->yearMonthCounts[$editYear]['all']++;
811 3
        $this->yearMonthCounts[$editYear]['months'][$editMonth]['all']++;
812
        // This will ultimately be the size of the page by the end of the year
813 3
        $this->yearMonthCounts[$editYear]['size'] = (int) $edit->getLength();
814
815
        // Keep track of which month had the most edits
816 3
        $editsThisMonth = $this->yearMonthCounts[$editYear]['months'][$editMonth]['all'];
817 3
        if ($editsThisMonth > $this->maxEditsPerMonth) {
818 3
            $this->maxEditsPerMonth = $editsThisMonth;
819
        }
820 3
    }
821
822
    /**
823
     * Add a new entry to $this->yearMonthCounts for the given year,
824
     * with blank values for each month. This called during self::parseHistory().
825
     * @param Edit $edit
826
     */
827 3
    private function addYearMonthCountEntry(Edit $edit)
828
    {
829 3
        $editYear = $edit->getYear();
830
831
        // Beginning of the month at 00:00:00.
832 3
        $firstEditTime = mktime(0, 0, 0, (int) $this->firstEdit->getMonth(), 1, $this->firstEdit->getYear());
0 ignored issues
show
Bug introduced by
$this->firstEdit->getYear() of type string is incompatible with the type integer expected by parameter $year of mktime(). ( Ignorable by Annotation )

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

832
        $firstEditTime = mktime(0, 0, 0, (int) $this->firstEdit->getMonth(), 1, /** @scrutinizer ignore-type */ $this->firstEdit->getYear());
Loading history...
833
834 3
        $this->yearMonthCounts[$editYear] = [
835
            'all' => 0,
836
            'minor' => 0,
837
            'anon' => 0,
838
            'automated' => 0,
839
            'size' => 0, // Keep track of the size by the end of the year.
840
            'events' => [],
841
            'months' => [],
842
        ];
843
844 3
        for ($i = 1; $i <= 12; $i++) {
845 3
            $timeObj = mktime(0, 0, 0, $i, 1, $editYear);
846
847
            // Don't show zeros for months before the first edit or after the current month.
848 3
            if ($timeObj < $firstEditTime || $timeObj > strtotime('last day of this month')) {
849 3
                continue;
850
            }
851
852 3
            $this->yearMonthCounts[$editYear]['months'][sprintf('%02d', $i)] = [
853
                'all' => 0,
854
                'minor' => 0,
855
                'anon' => 0,
856
                'automated' => 0,
857
            ];
858
        }
859 3
    }
860
861
    /**
862
     * Update the counts of anon and minor edits for year, month,
863
     * and user of the given edit.
864
     * @param Edit $edit
865
     */
866 3
    private function updateAnonMinorCounts(Edit $edit)
867
    {
868 3
        $editYear = $edit->getYear();
869 3
        $editMonth = $edit->getMonth();
870
871
        // If anonymous, increase counts
872 3
        if ($edit->isAnon()) {
873 3
            $this->anonCount++;
874 3
            $this->yearMonthCounts[$editYear]['anon']++;
875 3
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['anon']++;
876
        }
877
878
        // If minor edit, increase counts
879 3
        if ($edit->isMinor()) {
880 3
            $this->minorCount++;
881 3
            $this->yearMonthCounts[$editYear]['minor']++;
882 3
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['minor']++;
883
        }
884 3
    }
885
886
    /**
887
     * Update various counts for the user of the given edit.
888
     * @param Edit $edit
889
     */
890 3
    private function updateUserCounts(Edit $edit)
891
    {
892 3
        $username = $edit->getUser()->getUsername();
893
894
        // Initialize various user stats if needed.
895 3
        if (!isset($this->editors[$username])) {
896 3
            $this->editors[$username] = [
897 3
                'all' => 0,
898 3
                'minor' => 0,
899 3
                'minorPercentage' => 0,
900 3
                'first' => $edit->getTimestamp(),
901 3
                'firstId' => $edit->getId(),
902
                'last' => null,
903
                'atbe' => null,
904 3
                'added' => 0,
905
                'sizes' => [],
906
            ];
907
        }
908
909
        // Increment user counts
910 3
        $this->editors[$username]['all']++;
911 3
        $this->editors[$username]['last'] = $edit->getTimestamp();
912 3
        $this->editors[$username]['lastId'] = $edit->getId();
913
914
        // Store number of KB added with this edit
915 3
        $this->editors[$username]['sizes'][] = $edit->getLength() / 1024;
916
917
        // Increment minor counts for this user
918 3
        if ($edit->isMinor()) {
919 3
            $this->editors[$username]['minor']++;
920
        }
921 3
    }
922
923
    /**
924
     * Increment "edits per <time>" counts based on the given edit.
925
     * @param Edit $edit
926
     */
927 3
    private function updateCountHistory(Edit $edit)
928
    {
929 3
        $editTimestamp = $edit->getTimestamp();
930
931 3
        if ($editTimestamp > new DateTime('-1 day')) {
932
            $this->countHistory['day']++;
933
        }
934 3
        if ($editTimestamp > new DateTime('-1 week')) {
935
            $this->countHistory['week']++;
936
        }
937 3
        if ($editTimestamp > new DateTime('-1 month')) {
938
            $this->countHistory['month']++;
939
        }
940 3
        if ($editTimestamp > new DateTime('-1 year')) {
941
            $this->countHistory['year']++;
942
        }
943 3
    }
944
945
    /**
946
     * Get info about bots that edited the page.
947
     * @return mixed[] Contains the bot's username, edit count to the page,
948
     *   and whether or not they are currently a bot.
949
     */
950
    public function getBots()
951
    {
952
        if (isset($this->bots)) {
953
            return $this->bots;
954
        }
955
956
        // Parse the botedits
957
        $bots = [];
958
        $botData = $this->getRepository()->getBotData($this->page);
1 ignored issue
show
Bug introduced by
The method getBotData() does not exist on Xtools\Repository. It seems like you code against a sub-type of Xtools\Repository such as Xtools\ArticleInfoRepository. ( Ignorable by Annotation )

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

958
        $botData = $this->getRepository()->/** @scrutinizer ignore-call */ getBotData($this->page);
Loading history...
959
        while ($bot = $botData->fetch()) {
960
            $bots[$bot['username']] = [
961
                'count' => (int) $bot['count'],
962
                'current' => $bot['current'] === 'bot',
963
            ];
964
        }
965
966
        // Sort by edit count.
967
        uasort($bots, function ($a, $b) {
968
            return $b['count'] - $a['count'];
969
        });
970
971
        $this->bots = $bots;
972
        return $bots;
973
    }
974
975
    /**
976
     * Number of edits made to the page by current or former bots.
977
     * @param string[] $bots Used only in unit tests, where we
978
     *   supply mock data for the bots that will get processed.
979
     * @return int
980
     */
981 1
    public function getBotRevisionCount($bots = null)
982
    {
983 1
        if (isset($this->botRevisionCount)) {
984
            return $this->botRevisionCount;
985
        }
986
987 1
        if ($bots === null) {
988
            $bots = $this->getBots();
989
        }
990
991 1
        $count = 0;
992
993 1
        foreach ($bots as $username => $data) {
994 1
            $count += $data['count'];
995
        }
996
997 1
        $this->botRevisionCount = $count;
998 1
        return $count;
999
    }
1000
1001
    /**
1002
     * Query for log events during each year of the article's history,
1003
     *   and set the results in $this->yearMonthCounts.
1004
     */
1005 1
    private function setLogsEvents()
1006
    {
1007 1
        $logData = $this->getRepository()->getLogEvents($this->page);
0 ignored issues
show
Bug introduced by
The method getLogEvents() does not exist on Xtools\Repository. Did you maybe mean getLog()? ( Ignorable by Annotation )

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

1007
        $logData = $this->getRepository()->/** @scrutinizer ignore-call */ getLogEvents($this->page);

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...
1008
1009 1
        foreach ($logData as $event) {
1010 1
            $time = strtotime($event['timestamp']);
1011 1
            $year = date('Y', $time);
1012
1013 1
            if (!isset($this->yearMonthCounts[$year])) {
1014
                break;
1015
            }
1016
1017 1
            $yearEvents = $this->yearMonthCounts[$year]['events'];
1018
1019
            // Convert log type value to i18n key
1020 1
            switch ($event['log_type']) {
1021 1
                case 'protect':
1022 1
                    $action = 'protections';
1023 1
                    break;
1024 1
                case 'delete':
1025 1
                    $action = 'deletions';
1026 1
                    break;
1027
                case 'move':
1028
                    $action = 'moves';
1029
                    break;
1030
                // count pending-changes protections along with normal protections
1031
                case 'stable':
1032
                    $action = 'protections';
1033
                    break;
1034
            }
1035
1036 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...
1037 1
                $yearEvents[$action] = 1;
1038
            } else {
1039
                $yearEvents[$action]++;
1040
            }
1041
1042 1
            $this->yearMonthCounts[$year]['events'] = $yearEvents;
1043
        }
1044 1
    }
1045
1046
    /**
1047
     * Set statistics about the top 10 editors by added text and number of edits.
1048
     * This is ran *after* parseHistory() since we need the grand totals first.
1049
     * Various stats are also set for each editor in $this->editors to be used in the charts.
1050
     * @return integer Number of edits
1051
     */
1052 3
    private function setTopTenCounts()
1053
    {
1054 3
        $topTenCount = $counter = 0;
1055 3
        $topTenEditors = [];
1056
1057 3
        foreach ($this->editors as $editor => $info) {
1058
            // Count how many users are in the top 10% by number of edits
1059 3
            if ($counter < 10) {
1060 3
                $topTenCount += $info['all'];
1061 3
                $counter++;
1062
1063
                // To be used in the Top Ten charts
1064 3
                $topTenEditors[] = [
1065 3
                    'label' => $editor,
1066 3
                    'value' => $info['all'],
1067
                    'percentage' => (
1068 3
                        100 * ($info['all'] / $this->getNumRevisionsProcessed())
1069
                    )
1070
                ];
1071
            }
1072
1073
            // Compute the percentage of minor edits the user made
1074 3
            $this->editors[$editor]['minorPercentage'] = $info['all']
1075 3
                ? ($info['minor'] / $info['all']) * 100
1076
                : 0;
1077
1078 3
            if ($info['all'] > 1) {
1079
                // Number of seconds/days between first and last edit
1080 3
                $secs = $info['last']->getTimestamp() - $info['first']->getTimestamp();
1081 3
                $days = $secs / (60 * 60 * 24);
1082
1083
                // Average time between edits (in days)
1084 3
                $this->editors[$editor]['atbe'] = $days / $info['all'];
1085
            }
1086
1087 3
            if (count($info['sizes'])) {
1088
                // Average Total KB divided by number of stored sizes (user's edit count to this page)
1089 3
                $this->editors[$editor]['size'] = array_sum($info['sizes']) / count($info['sizes']);
1090
            } else {
1091 3
                $this->editors[$editor]['size'] = 0;
1092
            }
1093
        }
1094
1095 3
        $this->topTenEditorsByEdits = $topTenEditors;
1096
1097
        // First sort editors array by the amount of text they added
1098 3
        $topTenEditorsByAdded = $this->editors;
1099 View Code Duplication
        uasort($topTenEditorsByAdded, function ($a, $b) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1100 3
            if ($a['added'] === $b['added']) {
1101 3
                return 0;
1102
            }
1103 3
            return $a['added'] > $b['added'] ? -1 : 1;
1104 3
        });
1105
1106
        // Then build a new array of top 10 editors by added text,
1107
        //   in the data structure needed for the chart
1108 3
        $this->topTenEditorsByAdded = array_map(function ($editor) {
1109 3
            $added = $this->editors[$editor]['added'];
1110
            return [
1111 3
                'label' => $editor,
1112 3
                'value' => $added,
1113
                'percentage' => (
1114 3
                    100 * ($added / $this->addedBytes)
1115
                )
1116
            ];
1117 3
        }, array_keys(array_slice($topTenEditorsByAdded, 0, 10)));
1118
1119 3
        $this->topTenCount = $topTenCount;
1120 3
    }
1121
}
1122