Passed
Push — master ( 6fd999...b63f6f )
by MusikAnimal
04:34
created

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

841
        $firstEditTime = mktime(0, 0, 0, (int) $this->firstEdit->getMonth(), 1, /** @scrutinizer ignore-type */ $this->firstEdit->getYear());
Loading history...
842
843 3
        $this->yearMonthCounts[$editYear] = [
844
            'all' => 0,
845
            'minor' => 0,
846
            'anon' => 0,
847
            'automated' => 0,
848
            'size' => 0, // Keep track of the size by the end of the year.
849
            'events' => [],
850
            'months' => [],
851
        ];
852
853 3
        for ($i = 1; $i <= 12; $i++) {
854 3
            $timeObj = mktime(0, 0, 0, $i, 1, $editYear);
855
856
            // Don't show zeros for months before the first edit or after the current month.
857 3
            if ($timeObj < $firstEditTime || $timeObj > strtotime('last day of this month')) {
858 3
                continue;
859
            }
860
861 3
            $this->yearMonthCounts[$editYear]['months'][sprintf('%02d', $i)] = [
862
                'all' => 0,
863
                'minor' => 0,
864
                'anon' => 0,
865
                'automated' => 0,
866
            ];
867
        }
868 3
    }
869
870
    /**
871
     * Update the counts of anon and minor edits for year, month,
872
     * and user of the given edit.
873
     * @param Edit $edit
874
     */
875 3
    private function updateAnonMinorCounts(Edit $edit)
876
    {
877 3
        $editYear = $edit->getYear();
878 3
        $editMonth = $edit->getMonth();
879
880
        // If anonymous, increase counts
881 3
        if ($edit->isAnon()) {
882 3
            $this->anonCount++;
883 3
            $this->yearMonthCounts[$editYear]['anon']++;
884 3
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['anon']++;
885
        }
886
887
        // If minor edit, increase counts
888 3
        if ($edit->isMinor()) {
889 3
            $this->minorCount++;
890 3
            $this->yearMonthCounts[$editYear]['minor']++;
891 3
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['minor']++;
892
        }
893 3
    }
894
895
    /**
896
     * Update various counts for the user of the given edit.
897
     * @param Edit $edit
898
     */
899 3
    private function updateUserCounts(Edit $edit)
900
    {
901 3
        $username = $edit->getUser()->getUsername();
902
903
        // Initialize various user stats if needed.
904 3
        if (!isset($this->editors[$username])) {
905 3
            $this->editors[$username] = [
906 3
                'all' => 0,
907 3
                'minor' => 0,
908 3
                'minorPercentage' => 0,
909 3
                'first' => $edit->getTimestamp(),
910 3
                'firstId' => $edit->getId(),
911
                'last' => null,
912
                'atbe' => null,
913 3
                'added' => 0,
914
                'sizes' => [],
915
            ];
916
        }
917
918
        // Increment user counts
919 3
        $this->editors[$username]['all']++;
920 3
        $this->editors[$username]['last'] = $edit->getTimestamp();
921 3
        $this->editors[$username]['lastId'] = $edit->getId();
922
923
        // Store number of KB added with this edit
924 3
        $this->editors[$username]['sizes'][] = $edit->getLength() / 1024;
925
926
        // Increment minor counts for this user
927 3
        if ($edit->isMinor()) {
928 3
            $this->editors[$username]['minor']++;
929
        }
930 3
    }
931
932
    /**
933
     * Increment "edits per <time>" counts based on the given edit.
934
     * @param Edit $edit
935
     */
936 3
    private function updateCountHistory(Edit $edit)
937
    {
938 3
        $editTimestamp = $edit->getTimestamp();
939
940 3
        if ($editTimestamp > new DateTime('-1 day')) {
941
            $this->countHistory['day']++;
942
        }
943 3
        if ($editTimestamp > new DateTime('-1 week')) {
944
            $this->countHistory['week']++;
945
        }
946 3
        if ($editTimestamp > new DateTime('-1 month')) {
947
            $this->countHistory['month']++;
948
        }
949 3
        if ($editTimestamp > new DateTime('-1 year')) {
950
            $this->countHistory['year']++;
951
        }
952 3
    }
953
954
    /**
955
     * Get info about bots that edited the page.
956
     * @return mixed[] Contains the bot's username, edit count to the page,
957
     *   and whether or not they are currently a bot.
958
     */
959 1
    public function getBots()
960
    {
961 1
        return $this->bots;
962
    }
963
964
    /**
965
     * Set info about bots that edited the page. This is done as a private setter
966
     * because we need this information when computing the top 10 editors,
967
     * where we don't want to include bots.
968
     */
969
    private function setBots()
970
    {
971
        // Parse the botedits
972
        $bots = [];
973
        $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

973
        $botData = $this->getRepository()->/** @scrutinizer ignore-call */ getBotData($this->page);
Loading history...
974
        while ($bot = $botData->fetch()) {
975
            $bots[$bot['username']] = [
976
                'count' => (int) $bot['count'],
977
                'current' => $bot['current'] === 'bot',
978
            ];
979
        }
980
981
        // Sort by edit count.
982
        uasort($bots, function ($a, $b) {
983
            return $b['count'] - $a['count'];
984
        });
985
986
        $this->bots = $bots;
987
    }
988
989
    /**
990
     * Number of edits made to the page by current or former bots.
991
     * @param string[] $bots Used only in unit tests, where we
992
     *   supply mock data for the bots that will get processed.
993
     * @return int
994
     */
995 2
    public function getBotRevisionCount($bots = null)
996
    {
997 2
        if (isset($this->botRevisionCount)) {
998
            return $this->botRevisionCount;
999
        }
1000
1001 2
        if ($bots === null) {
1002 1
            $bots = $this->getBots();
1003
        }
1004
1005 2
        $count = 0;
1006
1007 2
        foreach ($bots as $username => $data) {
1008 2
            $count += $data['count'];
1009
        }
1010
1011 2
        $this->botRevisionCount = $count;
1012 2
        return $count;
1013
    }
1014
1015
    /**
1016
     * Query for log events during each year of the article's history,
1017
     *   and set the results in $this->yearMonthCounts.
1018
     */
1019 1
    private function setLogsEvents()
1020
    {
1021 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

1021
        $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...
1022
1023 1
        foreach ($logData as $event) {
1024 1
            $time = strtotime($event['timestamp']);
1025 1
            $year = date('Y', $time);
1026
1027 1
            if (!isset($this->yearMonthCounts[$year])) {
1028
                break;
1029
            }
1030
1031 1
            $yearEvents = $this->yearMonthCounts[$year]['events'];
1032
1033
            // Convert log type value to i18n key.
1034 1
            switch ($event['log_type']) {
1035 1
                case 'protect':
1036 1
                    $action = 'protections';
1037 1
                    break;
1038 1
                case 'delete':
1039 1
                    $action = 'deletions';
1040 1
                    break;
1041
                case 'move':
1042
                    $action = 'moves';
1043
                    break;
1044
                // count pending-changes protections along with normal protections.
1045
                case 'stable':
1046
                    $action = 'protections';
1047
                    break;
1048
            }
1049
1050 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...
1051 1
                $yearEvents[$action] = 1;
1052
            } else {
1053
                $yearEvents[$action]++;
1054
            }
1055
1056 1
            $this->yearMonthCounts[$year]['events'] = $yearEvents;
1057
        }
1058 1
    }
1059
1060
    /**
1061
     * Set statistics about the top 10 editors by added text and number of edits.
1062
     * This is ran *after* parseHistory() since we need the grand totals first.
1063
     * Various stats are also set for each editor in $this->editors to be used in the charts.
1064
     * @return integer Number of edits
1065
     */
1066 3
    private function setTopTenCounts()
1067
    {
1068 3
        $topTenCount = $counter = 0;
1069 3
        $topTenEditors = [];
1070
1071 3
        foreach ($this->editors as $editor => $info) {
1072
            // Count how many users are in the top 10% by number of edits, excluding bots.
1073 3
            if ($counter < 10 && !in_array($editor, array_keys($this->bots))) {
1074 3
                $topTenCount += $info['all'];
1075 3
                $counter++;
1076
1077
                // To be used in the Top Ten charts.
1078 3
                $topTenEditors[] = [
1079 3
                    'label' => $editor,
1080 3
                    'value' => $info['all'],
1081
                    'percentage' => (
1082 3
                        100 * ($info['all'] / $this->getNumRevisionsProcessed())
1083
                    )
1084
                ];
1085
            }
1086
1087
            // Compute the percentage of minor edits the user made.
1088 3
            $this->editors[$editor]['minorPercentage'] = $info['all']
1089 3
                ? ($info['minor'] / $info['all']) * 100
1090
                : 0;
1091
1092 3
            if ($info['all'] > 1) {
1093
                // Number of seconds/days between first and last edit.
1094 3
                $secs = $info['last']->getTimestamp() - $info['first']->getTimestamp();
1095 3
                $days = $secs / (60 * 60 * 24);
1096
1097
                // Average time between edits (in days).
1098 3
                $this->editors[$editor]['atbe'] = $days / $info['all'];
1099
            }
1100
1101 3
            if (count($info['sizes'])) {
1102
                // Average Total KB divided by number of stored sizes (usually the user's edit count to this page).
1103 3
                $this->editors[$editor]['size'] = array_sum($info['sizes']) / count($info['sizes']);
1104
            } else {
1105 3
                $this->editors[$editor]['size'] = 0;
1106
            }
1107
        }
1108
1109 3
        $this->topTenEditorsByEdits = $topTenEditors;
1110
1111
        // First sort editors array by the amount of text they added.
1112 3
        $topTenEditorsByAdded = $this->editors;
1113 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...
1114 3
            if ($a['added'] === $b['added']) {
1115 3
                return 0;
1116
            }
1117 3
            return $a['added'] > $b['added'] ? -1 : 1;
1118 3
        });
1119
1120
        // Then build a new array of top 10 editors by added text,
1121
        // in the data structure needed for the chart.
1122 3
        $this->topTenEditorsByAdded = array_map(function ($editor) {
1123 3
            $added = $this->editors[$editor]['added'];
1124
            return [
1125 3
                'label' => $editor,
1126 3
                'value' => $added,
1127
                'percentage' => (
1128 3
                    100 * ($added / $this->addedBytes)
1129
                )
1130
            ];
1131 3
        }, array_keys(array_slice($topTenEditorsByAdded, 0, 10)));
1132
1133 3
        $this->topTenCount = $topTenCount;
1134 3
    }
1135
}
1136