Completed
Push — master ( d0838b...120b58 )
by MusikAnimal
07:49 queued 02:25
created

ArticleInfo::topTenPercentage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
277 1
        return $this->totalDays;
278
    }
279
280
    /**
281
     * Returns length of the page.
282
     * @return int
283
     */
284
    public function getLength()
285
    {
286
        if ($this->hasDateRange()) {
287
            return $this->lastEdit->getLength();
288
        }
289
290
        return $this->page->getLength();
291
    }
292
293
    /**
294
     * Get the average number of days between edits to the page.
295
     * @return double
296
     */
297 1
    public function averageDaysPerEdit()
298
    {
299 1
        return round($this->getTotalDays() / $this->getNumRevisionsProcessed(), 1);
300
    }
301
302
    /**
303
     * Get the average number of edits per day to the page.
304
     * @return double
305
     */
306 1
    public function editsPerDay()
307
    {
308 1
        $editsPerDay = $this->getTotalDays()
309 1
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / (365 / 12 / 24))
310 1
            : 0;
311 1
        return round($editsPerDay, 1);
312
    }
313
314
    /**
315
     * Get the average number of edits per month to the page.
316
     * @return double
317
     */
318 1
    public function editsPerMonth()
319
    {
320 1
        $editsPerMonth = $this->getTotalDays()
321 1
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / (365 / 12))
322 1
            : 0;
323 1
        return min($this->getNumRevisionsProcessed(), round($editsPerMonth, 1));
324
    }
325
326
    /**
327
     * Get the average number of edits per year to the page.
328
     * @return double
329
     */
330 1
    public function editsPerYear()
331
    {
332 1
        $editsPerYear = $this->getTotalDays()
333 1
            ? $this->getNumRevisionsProcessed() / ($this->getTotalDays() / 365)
334 1
            : 0;
335 1
        return min($this->getNumRevisionsProcessed(), round($editsPerYear, 1));
336
    }
337
338
    /**
339
     * Get the average number of edits per editor.
340
     * @return double
341
     */
342 1
    public function editsPerEditor()
343
    {
344 1
        return round($this->getNumRevisionsProcessed() / count($this->editors), 1);
345
    }
346
347
    /**
348
     * Get the percentage of minor edits to the page.
349
     * @return double
350
     */
351 1
    public function minorPercentage()
352
    {
353 1
        return round(
354 1
            ($this->minorCount / $this->getNumRevisionsProcessed()) * 100,
355 1
            1
356
        );
357
    }
358
359
    /**
360
     * Get the percentage of anonymous edits to the page.
361
     * @return double
362
     */
363 1
    public function anonPercentage()
364
    {
365 1
        return round(
366 1
            ($this->anonCount / $this->getNumRevisionsProcessed()) * 100,
367 1
            1
368
        );
369
    }
370
371
    /**
372
     * Get the percentage of edits made by the top 10 editors.
373
     * @return double
374
     */
375 1
    public function topTenPercentage()
376
    {
377 1
        return round(($this->topTenCount / $this->getNumRevisionsProcessed()) * 100, 1);
378
    }
379
380
    /**
381
     * Get the number of times the page has been viewed in the given timeframe.
382
     * @param  int $latest Last N days.
383
     * @return int
384
     */
385
    public function getPageviews($latest)
386
    {
387
        if (false === $this->startDate && false === $this->endDate) {
388
            return $this->page->getLastPageviews($latest);
389
        }
390
391
        list($start, $end) = $this->translateDatesToYYYYMMDD($this->startDate, $this->endDate);
392
        list($start, $end) = $this->applyDatesDefaults($start, $end);
393
394
        return $this->page->getPageviews($start, $end);
395
    }
396
397
    /**
398
     * "Translate" dates to YYYYMMDD format.
399
     *
400
     * @param false|string $start
401
     * @param false|string $end
402
     * @return array
403
     */
404
    private function translateDatesToYYYYMMDD($start, $end)
405
    {
406
        if (false !== $start) {
407
            $start = date('Ymd', $start);
0 ignored issues
show
Bug introduced by
$start of type string is incompatible with the type integer expected by parameter $timestamp of date(). ( Ignorable by Annotation )

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

407
            $start = date('Ymd', /** @scrutinizer ignore-type */ $start);
Loading history...
408
        }
409
        if (false !== $end) {
410
            $end = date('Ymd', $end);
411
        }
412
413
        return [$start, $end];
414
    }
415
416
    /**
417
     * Apply defaults, that is $defaultDays days back for $start and current date for $end.
418
     *
419
     * @param false|string $start
420
     * @param false|string $end
421
     * @return array
422
     */
423
    private function applyDatesDefaults($start, $end)
424
    {
425
        if (false === $start && false === $end) {
426
            // [false, false] basically
427
            return [$start, $end];
428
        }
429
430
        if (false === $start) {
431
            // Remember, YYYYMMDD format.
432
            $start = date('Ymd', 0);
433
        }
434
        if (false === $end) {
435
            $end = date('Ymd', time());
436
        }
437
438
        return [$start, $end];
439
    }
440
441
    /**
442
     * Get the page assessments of the page.
443
     * @see https://www.mediawiki.org/wiki/Extension:PageAssessments
444
     * @return string[]|false False if unsupported.
445
     * @codeCoverageIgnore
446
     */
447
    public function getAssessments()
448
    {
449
        if (!is_array($this->assessments)) {
450
            $this->assessments = $this->page->getAssessments();
451
        }
452
        return $this->assessments;
453
    }
454
455
    /**
456
     * Get the number of automated edits made to the page.
457
     * @return int
458
     */
459 1
    public function getAutomatedCount()
460
    {
461 1
        return $this->automatedCount;
462
    }
463
464
    /**
465
     * Get the number of edits to the page that were reverted with the subsequent edit.
466
     * @return int
467
     */
468 1
    public function getRevertCount()
469
    {
470 1
        return $this->revertCount;
471
    }
472
473
    /**
474
     * Get the number of edits to the page made by logged out users.
475
     * @return int
476
     */
477 1
    public function getAnonCount()
478
    {
479 1
        return $this->anonCount;
480
    }
481
482
    /**
483
     * Get the number of minor edits to the page.
484
     * @return int
485
     */
486 1
    public function getMinorCount()
487
    {
488 1
        return $this->minorCount;
489
    }
490
491
    /**
492
     * Get the number of edits to the page made in the past day, week, month and year.
493
     * @return int[] With keys 'day', 'week', 'month' and 'year'.
494
     */
495
    public function getCountHistory()
496
    {
497
        return $this->countHistory;
498
    }
499
500
    /**
501
     * Get the number of edits to the page made by the top 10 editors.
502
     * @return int
503
     */
504 1
    public function getTopTenCount()
505
    {
506 1
        return $this->topTenCount;
507
    }
508
509
    /**
510
     * Get the first edit to the page.
511
     * @return Edit
512
     */
513
    public function getFirstEdit()
514
    {
515
        return $this->firstEdit;
516
    }
517
518
    /**
519
     * Get the last edit to the page.
520
     * @return Edit
521
     */
522 1
    public function getLastEdit()
523
    {
524 1
        return $this->lastEdit;
525
    }
526
527
    /**
528
     * Get the edit that made the largest addition to the page (by number of bytes).
529
     * @return Edit
530
     */
531 1
    public function getMaxAddition()
532
    {
533 1
        return $this->maxAddition;
534
    }
535
536
    /**
537
     * Get the edit that made the largest removal to the page (by number of bytes).
538
     * @return Edit
539
     */
540 1
    public function getMaxDeletion()
541
    {
542 1
        return $this->maxDeletion;
543
    }
544
545
    /**
546
     * Get the list of editors to the page, including various statistics.
547
     * @return mixed[]
548
     */
549 1
    public function getEditors()
550
    {
551 1
        return $this->editors;
552
    }
553
554
    /**
555
     * Get the list of the top editors to the page (by edits), including various statistics.
556
     * @return mixed[]
557
     */
558 1
    public function topTenEditorsByEdits()
559
    {
560 1
        return $this->topTenEditorsByEdits;
561
    }
562
563
    /**
564
     * Get the list of the top editors to the page (by added text), including various statistics.
565
     * @return mixed[]
566
     */
567 1
    public function topTenEditorsByAdded()
568
    {
569 1
        return $this->topTenEditorsByAdded;
570
    }
571
572
    /**
573
     * Get various counts about each individual year and month of the page's history.
574
     * @return mixed[]
575
     */
576 2
    public function getYearMonthCounts()
577
    {
578 2
        return $this->yearMonthCounts;
579
    }
580
581
    /**
582
     * Get the maximum number of edits that were created across all months. This is used as a
583
     * comparison for the bar charts in the months section.
584
     * @return int
585
     */
586 1
    public function getMaxEditsPerMonth()
587
    {
588 1
        return $this->maxEditsPerMonth;
589
    }
590
591
    /**
592
     * Get a list of (semi-)automated tools that were used to edit the page, including
593
     * the number of times they were used, and a link to the tool's homepage.
594
     * @return mixed[]
595
     */
596 1
    public function getTools()
597
    {
598 1
        return $this->tools;
599
    }
600
601
    /**
602
     * Get the list of page's wikidata and Checkwiki errors.
603
     * @see Page::getErrors()
604
     * @return string[]
605
     */
606
    public function getBugs()
607
    {
608
        if (!is_array($this->bugs)) {
609
            $this->bugs = $this->page->getErrors();
610
        }
611
        return $this->bugs;
612
    }
613
614
    /**
615
     * Get the number of wikidata nad CheckWiki errors.
616
     * @return int
617
     */
618
    public function numBugs()
619
    {
620
        return count($this->getBugs());
621
    }
622
623
    /**
624
     * Get the number of external links on the page.
625
     * @return int
626
     */
627 1
    public function linksExtCount()
628
    {
629 1
        return $this->getLinksAndRedirects()['links_ext_count'];
630
    }
631
632
    /**
633
     * Get the number of incoming links to the page.
634
     * @return int
635
     */
636 1
    public function linksInCount()
637
    {
638 1
        return $this->getLinksAndRedirects()['links_in_count'];
639
    }
640
641
    /**
642
     * Get the number of outgoing links from the page.
643
     * @return int
644
     */
645 1
    public function linksOutCount()
646
    {
647 1
        return $this->getLinksAndRedirects()['links_out_count'];
648
    }
649
650
    /**
651
     * Get the number of redirects to the page.
652
     * @return int
653
     */
654 1
    public function redirectsCount()
655
    {
656 1
        return $this->getLinksAndRedirects()['redirects_count'];
657
    }
658
659
    /**
660
     * Get the number of external, incoming and outgoing links, along with
661
     * the number of redirects to the page.
662
     * @return int
663
     * @codeCoverageIgnore
664
     */
665
    private function getLinksAndRedirects()
666
    {
667
        if (!is_array($this->linksAndRedirects)) {
668
            $this->linksAndRedirects = $this->page->countLinksAndRedirects();
669
        }
670
        return $this->linksAndRedirects;
671
    }
672
673
    /**
674
     * Parse the revision history, collecting our core statistics.
675
     * @return mixed[] Associative "master" array of metadata about the page.
676
     *
677
     * Untestable because it relies on getting a PDO statement. All the important
678
     * logic lives in other methods which are tested.
679
     * @codeCoverageIgnore
680
     */
681
    private function parseHistory()
682
    {
683
        if ($this->tooManyRevisions()) {
684
            $limit = $this->getMaxRevisions();
685
        } else {
686
            $limit = null;
687
        }
688
689
        // Third parameter is ignored if $limit is null.
690
        $revStmt = $this->page->getRevisionsStmt(
691
            null,
692
            $limit,
693
            $this->getNumRevisions(),
694
            $this->startDate,
695
            $this->endDate
696
        );
697
        $revCount = 0;
698
699
        /**
700
         * Data about previous edits so that we can use them as a basis for comparison.
701
         * @var Edit[]
702
         */
703
        $prevEdits = [
704
            // The previous Edit, used to discount content that was reverted.
705
            'prev' => null,
706
707
            // The last edit deemed to be the max addition of content. This is kept track of
708
            // in case we find out the next edit was reverted (and was also a max edit),
709
            // in which case we'll want to discount it and use this one instead.
710
            'maxAddition' => null,
711
712
            // Same as with maxAddition, except the maximum amount of content deleted.
713
            // This is used to discount content that was reverted.
714
            'maxDeletion' => null,
715
        ];
716
717
        while ($rev = $revStmt->fetch()) {
718
            $edit = new Edit($this->page, $rev);
719
720
            if ($revCount === 0) {
721
                $this->firstEdit = $edit;
722
            }
723
724
            // Sometimes, with old revisions (2001 era), the revisions from 2002 come before 2001
725
            if ($edit->getTimestamp() < $this->firstEdit->getTimestamp()) {
726
                $this->firstEdit = $edit;
727
            }
728
729
            $prevEdits = $this->updateCounts($edit, $prevEdits);
730
731
            $revCount++;
732
        }
733
734
        $this->numRevisionsProcessed = $revCount;
735
736
        // Various sorts
737
        arsort($this->editors);
738
        ksort($this->yearMonthCounts);
739
        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...
740
            arsort($this->tools);
741
        }
742
    }
743
744
    /**
745
     * Update various counts based on the current edit.
746
     * @param  Edit   $edit
747
     * @param  Edit[] $prevEdits With 'prev', 'maxAddition' and 'maxDeletion'
748
     * @return Edit[] Updated version of $prevEdits.
749
     */
750 3
    private function updateCounts(Edit $edit, $prevEdits)
751
    {
752
        // Update the counts for the year and month of the current edit.
753 3
        $this->updateYearMonthCounts($edit);
754
755
        // Update counts for the user who made the edit.
756 3
        $this->updateUserCounts($edit);
757
758
        // Update the year/month/user counts of anon and minor edits.
759 3
        $this->updateAnonMinorCounts($edit);
760
761
        // Update counts for automated tool usage, if applicable.
762 3
        $this->updateToolCounts($edit);
763
764
        // Increment "edits per <time>" counts
765 3
        $this->updateCountHistory($edit);
766
767
        // Update figures regarding content addition/removal, and the revert count.
768 3
        $prevEdits = $this->updateContentSizes($edit, $prevEdits);
769
770
        // Now that we've updated all the counts, we can reset
771
        // the prev and last edits, which are used for tracking.
772 3
        $prevEdits['prev'] = $edit;
773 3
        $this->lastEdit = $edit;
774
775 3
        return $prevEdits;
776
    }
777
778
    /**
779
     * Update various figures about content sizes based on the given edit.
780
     * @param  Edit   $edit
781
     * @param  Edit[] $prevEdits With 'prev', 'maxAddition' and 'maxDeletion'
782
     * @return Edit[] Updated version of $prevEdits.
783
     */
784 3
    private function updateContentSizes(Edit $edit, $prevEdits)
785
    {
786
        // Check if it was a revert
787 3
        if ($edit->isRevert($this->container)) {
788 3
            return $this->updateContentSizesRevert($prevEdits);
789
        } else {
790 3
            return $this->updateContentSizesNonRevert($edit, $prevEdits);
791
        }
792
    }
793
794
    /**
795
     * Updates the figures on content sizes assuming the given edit was a revert of the previous one.
796
     * In such a case, we don't want to treat the previous edit as legit content addition or removal.
797
     * @param  Edit[] $prevEdits With 'prev', 'maxAddition' and 'maxDeletion'.
798
     * @return Edit[] Updated version of $prevEdits, for tracking.
799
     */
800 3
    private function updateContentSizesRevert($prevEdits)
801
    {
802 3
        $this->revertCount++;
803
804
        // Adjust addedBytes given this edit was a revert of the previous one.
805 3
        if ($prevEdits['prev'] && $prevEdits['prev']->getSize() > 0) {
806
            $this->addedBytes -= $prevEdits['prev']->getSize();
807
808
            // Also deduct from the user's individual added byte count.
809
            $username = $prevEdits['prev']->getUser()->getUsername();
810
            $this->editors[$username]['added'] -= $prevEdits['prev']->getSize();
811
        }
812
813
        // @TODO: Test this against an edit war (use your sandbox).
814
        // Also remove as max added or deleted, if applicable.
815 3
        if ($this->maxAddition && $prevEdits['prev']->getId() === $this->maxAddition->getId()) {
816
            // $this->editors[$prevEdits->getUser()->getUsername()]['sizes'] = $edit->getLength() / 1024;
817
            $this->maxAddition = $prevEdits['maxAddition'];
818
            $prevEdits['maxAddition'] = $prevEdits['prev']; // In the event of edit wars.
819 3
        } elseif ($this->maxDeletion && $prevEdits['prev']->getId() === $this->maxDeletion->getId()) {
820 3
            $this->maxDeletion = $prevEdits['maxDeletion'];
821 3
            $prevEdits['maxDeletion'] = $prevEdits['prev']; // In the event of edit wars.
822
        }
823
824 3
        return $prevEdits;
825
    }
826
827
    /**
828
     * Updates the figures on content sizes assuming the given edit
829
     * was NOT a revert of the previous edit.
830
     * @param  Edit   $edit
831
     * @param  Edit[] $prevEdits With 'prev', 'maxAddition' and 'maxDeletion'.
832
     * @return Edit[] Updated version of $prevEdits, for tracking.
833
     */
834 3
    private function updateContentSizesNonRevert(Edit $edit, $prevEdits)
835
    {
836 3
        $editSize = $this->getEditSize($edit, $prevEdits);
837
838
        // Edit was not a revert, so treat size > 0 as content added.
839 3
        if ($editSize > 0) {
840 3
            $this->addedBytes += $editSize;
841 3
            $this->editors[$edit->getUser()->getUsername()]['added'] += $editSize;
842
843
            // Keep track of edit with max addition.
844 3
            if (!$this->maxAddition || $editSize > $this->maxAddition->getSize()) {
845
                // Keep track of old maxAddition in case we find out the next $edit was reverted
846
                // (and was also a max edit), in which case we'll want to use this one ($edit).
847 3
                $prevEdits['maxAddition'] = $this->maxAddition;
848
849 3
                $this->maxAddition = $edit;
850
            }
851 3
        } elseif ($editSize < 0 && (!$this->maxDeletion || $editSize < $this->maxDeletion->getSize())) {
852
            // Keep track of old maxDeletion in case we find out the next edit was reverted
853
            // (and was also a max deletion), in which case we'll want to use this one.
854 3
            $prevEdits['maxDeletion'] = $this->maxDeletion;
855
856 3
            $this->maxDeletion = $edit;
857
        }
858
859 3
        return $prevEdits;
860
    }
861
862
    /**
863
     * Get the size of the given edit, based on the previous edit (if present).
864
     * We also don't return the actual edit size if last revision had a length of null.
865
     * This happens when the edit follows other edits that were revision-deleted.
866
     * @see T148857 for more information.
867
     * @todo Remove once T101631 is resolved.
868
     * @param  Edit   $edit
869
     * @param  Edit[] $prevEdits With 'prev', 'maxAddition' and 'maxDeletion'.
870
     * @return Edit[] Updated version of $prevEdits, for tracking.
871
     */
872 3
    private function getEditSize(Edit $edit, $prevEdits)
873
    {
874 3
        if ($prevEdits['prev'] && $prevEdits['prev']->getLength() === null) {
875
            return 0;
876
        } else {
877 3
            return $edit->getSize();
878
        }
879
    }
880
881
    /**
882
     * Update counts of automated tool usage for the given edit.
883
     * @param Edit $edit
884
     */
885 3
    private function updateToolCounts(Edit $edit)
886
    {
887 3
        $automatedTool = $edit->getTool($this->container);
888
889 3
        if ($automatedTool === false) {
890
            // Nothing to do.
891 3
            return;
892
        }
893
894 3
        $editYear = $edit->getYear();
895 3
        $editMonth = $edit->getMonth();
896
897 3
        $this->automatedCount++;
898 3
        $this->yearMonthCounts[$editYear]['automated']++;
899 3
        $this->yearMonthCounts[$editYear]['months'][$editMonth]['automated']++;
900
901 3
        if (!isset($this->tools[$automatedTool['name']])) {
902 3
            $this->tools[$automatedTool['name']] = [
903 3
                'count' => 1,
904 3
                'link' => $automatedTool['link'],
905
            ];
906
        } else {
907
            $this->tools[$automatedTool['name']]['count']++;
908
        }
909 3
    }
910
911
    /**
912
     * Update various counts for the year and month of the given edit.
913
     * @param Edit $edit
914
     */
915 3
    private function updateYearMonthCounts(Edit $edit)
916
    {
917 3
        $editYear = $edit->getYear();
918 3
        $editMonth = $edit->getMonth();
919
920
        // Fill in the blank arrays for the year and 12 months if needed.
921 3
        if (!isset($this->yearMonthCounts[$editYear])) {
922 3
            $this->addYearMonthCountEntry($edit);
923
        }
924
925
        // Increment year and month counts for all edits
926 3
        $this->yearMonthCounts[$editYear]['all']++;
927 3
        $this->yearMonthCounts[$editYear]['months'][$editMonth]['all']++;
928
        // This will ultimately be the size of the page by the end of the year
929 3
        $this->yearMonthCounts[$editYear]['size'] = (int) $edit->getLength();
930
931
        // Keep track of which month had the most edits
932 3
        $editsThisMonth = $this->yearMonthCounts[$editYear]['months'][$editMonth]['all'];
933 3
        if ($editsThisMonth > $this->maxEditsPerMonth) {
934 3
            $this->maxEditsPerMonth = $editsThisMonth;
935
        }
936 3
    }
937
938
    /**
939
     * Add a new entry to $this->yearMonthCounts for the given year,
940
     * with blank values for each month. This called during self::parseHistory().
941
     * @param Edit $edit
942
     */
943 3
    private function addYearMonthCountEntry(Edit $edit)
944
    {
945 3
        $editYear = $edit->getYear();
946
947
        // Beginning of the month at 00:00:00.
948 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

948
        $firstEditTime = mktime(0, 0, 0, (int) $this->firstEdit->getMonth(), 1, /** @scrutinizer ignore-type */ $this->firstEdit->getYear());
Loading history...
949
950 3
        $this->yearMonthCounts[$editYear] = [
951
            'all' => 0,
952
            'minor' => 0,
953
            'anon' => 0,
954
            'automated' => 0,
955
            'size' => 0, // Keep track of the size by the end of the year.
956
            'events' => [],
957
            'months' => [],
958
        ];
959
960 3
        for ($i = 1; $i <= 12; $i++) {
961 3
            $timeObj = mktime(0, 0, 0, $i, 1, $editYear);
962
963 3
            $date = $editYear . sprintf('%02d', $i) . '01';
964 3
            if (false !== $this->startDate && $date < date('Ymd', $this->startDate)
965 3
                || false !== $this->endDate && $date > date('Ymd', $this->endDate)) {
966
                continue;
967
            }
968
969
            // Don't show zeros for months before the first edit or after the current month.
970 3
            if ($timeObj < $firstEditTime || $timeObj > strtotime('last day of this month')) {
971 3
                continue;
972
            }
973
974 3
            $this->yearMonthCounts[$editYear]['months'][sprintf('%02d', $i)] = [
975
                'all' => 0,
976
                'minor' => 0,
977
                'anon' => 0,
978
                'automated' => 0,
979
            ];
980
        }
981 3
    }
982
983
    /**
984
     * Update the counts of anon and minor edits for year, month,
985
     * and user of the given edit.
986
     * @param Edit $edit
987
     */
988 3
    private function updateAnonMinorCounts(Edit $edit)
989
    {
990 3
        $editYear = $edit->getYear();
991 3
        $editMonth = $edit->getMonth();
992
993
        // If anonymous, increase counts
994 3
        if ($edit->isAnon()) {
995 3
            $this->anonCount++;
996 3
            $this->yearMonthCounts[$editYear]['anon']++;
997 3
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['anon']++;
998
        }
999
1000
        // If minor edit, increase counts
1001 3
        if ($edit->isMinor()) {
1002 3
            $this->minorCount++;
1003 3
            $this->yearMonthCounts[$editYear]['minor']++;
1004 3
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['minor']++;
1005
        }
1006 3
    }
1007
1008
    /**
1009
     * Update various counts for the user of the given edit.
1010
     * @param Edit $edit
1011
     */
1012 3
    private function updateUserCounts(Edit $edit)
1013
    {
1014 3
        $username = $edit->getUser()->getUsername();
1015
1016
        // Initialize various user stats if needed.
1017 3
        if (!isset($this->editors[$username])) {
1018 3
            $this->editors[$username] = [
1019 3
                'all' => 0,
1020 3
                'minor' => 0,
1021 3
                'minorPercentage' => 0,
1022 3
                'first' => $edit->getTimestamp(),
1023 3
                'firstId' => $edit->getId(),
1024
                'last' => null,
1025
                'atbe' => null,
1026 3
                'added' => 0,
1027
                'sizes' => [],
1028
            ];
1029
        }
1030
1031
        // Increment user counts
1032 3
        $this->editors[$username]['all']++;
1033 3
        $this->editors[$username]['last'] = $edit->getTimestamp();
1034 3
        $this->editors[$username]['lastId'] = $edit->getId();
1035
1036
        // Store number of KB added with this edit
1037 3
        $this->editors[$username]['sizes'][] = $edit->getLength() / 1024;
1038
1039
        // Increment minor counts for this user
1040 3
        if ($edit->isMinor()) {
1041 3
            $this->editors[$username]['minor']++;
1042
        }
1043 3
    }
1044
1045
    /**
1046
     * Increment "edits per <time>" counts based on the given edit.
1047
     * @param Edit $edit
1048
     */
1049 3
    private function updateCountHistory(Edit $edit)
1050
    {
1051 3
        $editTimestamp = $edit->getTimestamp();
1052
1053 3
        if ($editTimestamp > new DateTime('-1 day')) {
1054
            $this->countHistory['day']++;
1055
        }
1056 3
        if ($editTimestamp > new DateTime('-1 week')) {
1057
            $this->countHistory['week']++;
1058
        }
1059 3
        if ($editTimestamp > new DateTime('-1 month')) {
1060
            $this->countHistory['month']++;
1061
        }
1062 3
        if ($editTimestamp > new DateTime('-1 year')) {
1063
            $this->countHistory['year']++;
1064
        }
1065 3
    }
1066
1067
    /**
1068
     * Get info about bots that edited the page.
1069
     * @return mixed[] Contains the bot's username, edit count to the page,
1070
     *   and whether or not they are currently a bot.
1071
     */
1072 1
    public function getBots()
1073
    {
1074 1
        return $this->bots;
1075
    }
1076
1077
    /**
1078
     * Set info about bots that edited the page. This is done as a private setter
1079
     * because we need this information when computing the top 10 editors,
1080
     * where we don't want to include bots.
1081
     */
1082
    private function setBots()
1083
    {
1084
        // Parse the botedits
1085
        $bots = [];
1086
        $botData = $this->getRepository()->getBotData($this->page, $this->startDate, $this->endDate);
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

1086
        $botData = $this->getRepository()->/** @scrutinizer ignore-call */ getBotData($this->page, $this->startDate, $this->endDate);
Loading history...
1087
        while ($bot = $botData->fetch()) {
1088
            $bots[$bot['username']] = [
1089
                'count' => (int) $bot['count'],
1090
                'current' => $bot['current'] === 'bot',
1091
            ];
1092
        }
1093
1094
        // Sort by edit count.
1095
        uasort($bots, function ($a, $b) {
1096
            return $b['count'] - $a['count'];
1097
        });
1098
1099
        $this->bots = $bots;
1100
    }
1101
1102
    /**
1103
     * Number of edits made to the page by current or former bots.
1104
     * @param string[] $bots Used only in unit tests, where we
1105
     *   supply mock data for the bots that will get processed.
1106
     * @return int
1107
     */
1108 2
    public function getBotRevisionCount($bots = null)
1109
    {
1110 2
        if (isset($this->botRevisionCount)) {
1111
            return $this->botRevisionCount;
1112
        }
1113
1114 2
        if ($bots === null) {
1115 1
            $bots = $this->getBots();
1116
        }
1117
1118 2
        $count = 0;
1119
1120 2
        foreach ($bots as $username => $data) {
1121 2
            $count += $data['count'];
1122
        }
1123
1124 2
        $this->botRevisionCount = $count;
1125 2
        return $count;
1126
    }
1127
1128
    /**
1129
     * Query for log events during each year of the article's history,
1130
     *   and set the results in $this->yearMonthCounts.
1131
     */
1132 1
    private function setLogsEvents()
1133
    {
1134 1
        $logData = $this->getRepository()->getLogEvents(
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

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

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...
1135 1
            $this->page,
1136 1
            $this->startDate,
1137 1
            $this->endDate
1138
        );
1139
1140 1
        foreach ($logData as $event) {
1141 1
            $time = strtotime($event['timestamp']);
1142 1
            $year = date('Y', $time);
1143
1144 1
            if (!isset($this->yearMonthCounts[$year])) {
1145
                break;
1146
            }
1147
1148 1
            $yearEvents = $this->yearMonthCounts[$year]['events'];
1149
1150
            // Convert log type value to i18n key.
1151 1
            switch ($event['log_type']) {
1152 1
                case 'protect':
1153 1
                    $action = 'protections';
1154 1
                    break;
1155 1
                case 'delete':
1156 1
                    $action = 'deletions';
1157 1
                    break;
1158
                case 'move':
1159
                    $action = 'moves';
1160
                    break;
1161
                // count pending-changes protections along with normal protections.
1162
                case 'stable':
1163
                    $action = 'protections';
1164
                    break;
1165
            }
1166
1167 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...
1168 1
                $yearEvents[$action] = 1;
1169
            } else {
1170
                $yearEvents[$action]++;
1171
            }
1172
1173 1
            $this->yearMonthCounts[$year]['events'] = $yearEvents;
1174
        }
1175 1
    }
1176
1177
    /**
1178
     * Set statistics about the top 10 editors by added text and number of edits.
1179
     * This is ran *after* parseHistory() since we need the grand totals first.
1180
     * Various stats are also set for each editor in $this->editors to be used in the charts.
1181
     * @return integer Number of edits
1182
     */
1183 3
    private function setTopTenCounts()
1184
    {
1185 3
        $topTenCount = $counter = 0;
1186 3
        $topTenEditors = [];
1187
1188 3
        foreach ($this->editors as $editor => $info) {
1189
            // Count how many users are in the top 10% by number of edits, excluding bots.
1190 3
            if ($counter < 10 && !in_array($editor, array_keys($this->bots))) {
1191 3
                $topTenCount += $info['all'];
1192 3
                $counter++;
1193
1194
                // To be used in the Top Ten charts.
1195 3
                $topTenEditors[] = [
1196 3
                    'label' => $editor,
1197 3
                    'value' => $info['all'],
1198
                    'percentage' => (
1199 3
                        100 * ($info['all'] / $this->getNumRevisionsProcessed())
1200
                    )
1201
                ];
1202
            }
1203
1204
            // Compute the percentage of minor edits the user made.
1205 3
            $this->editors[$editor]['minorPercentage'] = $info['all']
1206 3
                ? ($info['minor'] / $info['all']) * 100
1207
                : 0;
1208
1209 3
            if ($info['all'] > 1) {
1210
                // Number of seconds/days between first and last edit.
1211 3
                $secs = $info['last']->getTimestamp() - $info['first']->getTimestamp();
1212 3
                $days = $secs / (60 * 60 * 24);
1213
1214
                // Average time between edits (in days).
1215 3
                $this->editors[$editor]['atbe'] = $days / $info['all'];
1216
            }
1217
1218 3
            if (count($info['sizes'])) {
1219
                // Average Total KB divided by number of stored sizes (usually the user's edit count to this page).
1220 3
                $this->editors[$editor]['size'] = array_sum($info['sizes']) / count($info['sizes']);
1221
            } else {
1222 3
                $this->editors[$editor]['size'] = 0;
1223
            }
1224
        }
1225
1226 3
        $this->topTenEditorsByEdits = $topTenEditors;
1227
1228
        // First sort editors array by the amount of text they added.
1229 3
        $topTenEditorsByAdded = $this->editors;
1230
        uasort($topTenEditorsByAdded, function ($a, $b) {
1231 3
            if ($a['added'] === $b['added']) {
1232 3
                return 0;
1233
            }
1234 3
            return $a['added'] > $b['added'] ? -1 : 1;
1235 3
        });
1236
1237
        // Then build a new array of top 10 editors by added text,
1238
        // in the data structure needed for the chart.
1239 3
        $this->topTenEditorsByAdded = array_map(function ($editor) {
1240 3
            $added = $this->editors[$editor]['added'];
1241
            return [
1242 3
                'label' => $editor,
1243 3
                'value' => $added,
1244
                'percentage' => (
1245 3
                    100 * ($added / $this->addedBytes)
1246
                )
1247
            ];
1248 3
        }, array_keys(array_slice($topTenEditorsByAdded, 0, 10)));
1249
1250 3
        $this->topTenCount = $topTenCount;
1251 3
    }
1252
}
1253