Completed
Push — master ( 3af732...89cc2d )
by MusikAnimal
13s
created

ArticleInfo::getEditors()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 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
    public function __construct(Page $page, Container $container)
125
    {
126
        $this->page = $page;
127
        $this->container = $container;
128
        $this->conn = $this->container
0 ignored issues
show
Bug introduced by
The property conn does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
129
            ->get('doctrine')
130
            ->getManager('replicas')
131
            ->getConnection();
132
    }
133
134
    /**
135
     * Shorthand to get the page's project.
136
     * @return Project
137
     */
138
    public function getProject()
139
    {
140
        return $this->page->getProject();
141
    }
142
143
    /**
144
     * Get the number of revisions belonging to the page.
145
     * @return int
146
     */
147
    public function getNumRevisions()
148
    {
149
        if (!isset($this->numRevisions)) {
150
            $this->numRevisions = $this->page->getNumRevisions();
151
        }
152
        return $this->numRevisions;
153
    }
154
155
    /**
156
     * Get the maximum number of revisions that we should process.
157
     * @return int
158
     */
159
    public function getMaxRevisions()
160
    {
161
        if (!isset($this->maxRevisions)) {
162
            $this->maxRevisions = (int) $this->container->getParameter('app.max_page_revisions');
163
        }
164
        return $this->maxRevisions;
165
    }
166
167
    /**
168
     * Get the number of revisions that are actually getting processed.
169
     * This goes by the app.max_page_revisions parameter, or the actual
170
     * number of revisions, whichever is smaller.
171
     * @return int
172
     */
173
    public function getNumRevisionsProcessed()
174
    {
175
        if (isset($this->numRevisionsProcessed)) {
176
            return $this->numRevisionsProcessed;
177
        }
178
179
        if ($this->tooManyRevisions()) {
180
            $this->numRevisionsProcessed = $this->getMaxRevisions();
181
        } else {
182
            $this->numRevisionsProcessed = $this->getNumRevisions();
183
        }
184
185
        return $this->numRevisionsProcessed;
186
    }
187
188
    /**
189
     * Are there more revisions than we should process, based on the config?
190
     * @return bool
191
     */
192
    public function tooManyRevisions()
193
    {
194
        return $this->getMaxRevisions() > 0 && $this->getNumRevisions() > $this->getMaxRevisions();
195
    }
196
197
    /**
198
     * Fetch and store all the data we need to show the ArticleInfo view.
199
     */
200
    public function prepareData()
201
    {
202
        $this->parseHistory();
203
        $this->setLogsEvents();
204
        $this->setTopTenCounts();
205
        $this->bots = $this->getBotData();
206
    }
207
208
    /**
209
     * Get the number of editors that edited the page.
210
     * @return int
211
     */
212
    public function numEditors()
213
    {
214
        return count($this->editors);
215
    }
216
217
    /**
218
     * Get the number of bots that edited the page.
219
     * @return int
220
     */
221
    public function numBots()
222
    {
223
        return count($this->bots);
224
    }
225
226
    /**
227
     * Get the number of days between the first and last edit.
228
     * @return int
229
     */
230
    public function getTotalDays()
231
    {
232
        if (isset($this->totalDays)) {
233
            return $this->totalDays;
234
        }
235
        $dateFirst = $this->firstEdit->getTimestamp();
236
        $dateLast = $this->lastEdit->getTimestamp();
237
        $interval = date_diff($dateLast, $dateFirst, true);
238
        return $interval->format('%a');
239
    }
240
241
    /**
242
     * Get the average number of days between edits to the page.
243
     * @return double
244
     */
245
    public function averageDaysPerEdit()
246
    {
247
        return round($this->getTotalDays() / $this->getNumRevisions(), 1);
248
    }
249
250
    /**
251
     * Get the average number of edits per day to the page.
252
     * @return double
253
     */
254
    public function editsPerDay()
255
    {
256
        $editsPerDay = $this->getTotalDays()
257
            ? $this->getNumRevisions() / ($this->getTotalDays() / (365 / 12 / 24))
258
            : 0;
259
        return round($editsPerDay, 1);
260
    }
261
262
    /**
263
     * Get the average number of edits per month to the page.
264
     * @return double
265
     */
266
    public function editsPerMonth()
267
    {
268
        $editsPerMonth = $this->getTotalDays()
269
            ? $this->getNumRevisions() / ($this->getTotalDays() / (365 / 12))
270
            : 0;
271
        return round($editsPerMonth, 1);
272
    }
273
274
    /**
275
     * Get the average number of edits per year to the page.
276
     * @return double
277
     */
278
    public function editsPerYear()
279
    {
280
        $editsPerYear = $this->getTotalDays()
281
            ? $this->getNumRevisions() / ($this->getTotalDays() / 365)
282
            : 0;
283
        return round($editsPerYear, 1);
284
    }
285
286
    /**
287
     * Get the average number of edits per editor.
288
     * @return double
289
     */
290
    public function editsPerEditor()
291
    {
292
        return round($this->getNumRevisions() / count($this->editors), 1);
293
    }
294
295
    /**
296
     * Get the percentage of minor edits to the page.
297
     * @return double
298
     */
299
    public function minorPercentage()
300
    {
301
        return round(
302
            ($this->minorCount / $this->getNumRevisions()) * 100,
303
            1
304
        );
305
    }
306
307
    /**
308
     * Get the percentage of anonymous edits to the page.
309
     * @return double
310
     */
311
    public function anonPercentage()
312
    {
313
        return round(
314
            ($this->anonCount / $this->getNumRevisions()) * 100,
315
            1
316
        );
317
    }
318
319
    /**
320
     * Get the percentage of edits made by the top 10 editors.
321
     * @return double
322
     */
323
    public function topTenPercentage()
324
    {
325
        return round(($this->topTenCount / $this->getNumRevisions()) * 100, 1);
326
    }
327
328
    /**
329
     * Get the number of times the page has been viewed in the given timeframe.
330
     * @param  int $latest Last N days.
331
     * @return int
332
     */
333
    public function getPageviews($latest)
334
    {
335
        return $this->page->getLastPageviews($latest);
336
    }
337
338
    /**
339
     * Get the page assessments of the page.
340
     * @see https://www.mediawiki.org/wiki/Extension:PageAssessments
341
     * @return string[]|false False if unsupported.
342
     */
343
    public function getAssessments()
344
    {
345
        if (!is_array($this->assessments)) {
346
            $this->assessments = $this->page->getAssessments();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->page->getAssessments() can also be of type false. However, the property $assessments is declared as type array<integer,string>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
347
        }
348
        return $this->assessments;
349
    }
350
351
    /**
352
     * Get the number of automated edits made to the page.
353
     * @return int
354
     */
355
    public function getAutomatedCount()
356
    {
357
        return $this->automatedCount;
358
    }
359
360
    /**
361
     * Get the number of edits to the page that were reverted with the subsequent edit.
362
     * @return int
363
     */
364
    public function getRevertCount()
365
    {
366
        return $this->revertCount;
367
    }
368
369
    /**
370
     * Get the number of edits to the page made by logged out users.
371
     * @return int
372
     */
373
    public function getAnonCount()
374
    {
375
        return $this->anonCount;
376
    }
377
378
    /**
379
     * Get the number of minor edits to the page.
380
     * @return int
381
     */
382
    public function getMinorCount()
383
    {
384
        return $this->minorCount;
385
    }
386
387
    /**
388
     * Get the number edits to the page made by bots.
389
     * @return int
390
     */
391
    public function getBotRevisionCount()
392
    {
393
        return $this->botRevisionCount;
394
    }
395
396
    /**
397
     * Get the number of edits to the page made in the past day, week, month and year.
398
     * @return int[] With keys 'day', 'week', 'month' and 'year'.
399
     */
400
    public function getCountHistory()
401
    {
402
        return $this->countHistory;
403
    }
404
405
    /**
406
     * Get the number of edits to the page made by the top 10 editors.
407
     * @return int
408
     */
409
    public function getTopTenCount()
410
    {
411
        return $this->topTenCount;
412
    }
413
414
    /**
415
     * Get the first edit to the page.
416
     * @return Edit
417
     */
418
    public function getFirstEdit()
419
    {
420
        return $this->firstEdit;
421
    }
422
423
    /**
424
     * Get the last edit to the page.
425
     * @return Edit
426
     */
427
    public function getLastEdit()
428
    {
429
        return $this->lastEdit;
430
    }
431
432
    /**
433
     * Get the edit that made the largest addition to the page (by number of bytes).
434
     * @return Edit
435
     */
436
    public function getMaxAddition()
437
    {
438
        return $this->maxAddition;
439
    }
440
441
    /**
442
     * Get the edit that made the largest removal to the page (by number of bytes).
443
     * @return Edit
444
     */
445
    public function getMaxDeletion()
446
    {
447
        return $this->maxDeletion;
448
    }
449
450
    /**
451
     * Get the list of editors to the page, including various statistics.
452
     * @return mixed[]
453
     */
454
    public function getEditors()
455
    {
456
        return $this->editors;
457
    }
458
459
    /**
460
     * Get the list of the top editors to the page (by edits), including various statistics.
461
     * @return mixed[]
462
     */
463
    public function topTenEditorsByEdits()
464
    {
465
        return $this->topTenEditorsByEdits;
466
    }
467
468
    /**
469
     * Get the list of the top editors to the page (by added text), including various statistics.
470
     * @return mixed[]
471
     */
472
    public function topTenEditorsByAdded()
473
    {
474
        return $this->topTenEditorsByAdded;
475
    }
476
477
    /**
478
     * Get the list of bots that edited the page, including various statistics.
479
     * @return mixed[]
480
     */
481
    public function getBots()
482
    {
483
        return $this->bots;
484
    }
485
486
    /**
487
     * Get various counts about each individual year and month of the page's history.
488
     * @return mixed[]
489
     */
490
    public function getYearMonthCounts()
491
    {
492
        return $this->yearMonthCounts;
493
    }
494
495
    /**
496
     * Get the maximum number of edits that were created across all months. This is used as a
497
     * comparison for the bar charts in the months section.
498
     * @return int
499
     */
500
    public function getMaxEditsPerMonth()
501
    {
502
        return $this->maxEditsPerMonth;
503
    }
504
505
    /**
506
     * Get a list of (semi-)automated tools that were used to edit the page, including
507
     * the number of times they were used, and a link to the tool's homepage.
508
     * @return mixed[]
509
     */
510
    public function getTools()
511
    {
512
        return $this->tools;
513
    }
514
515
    /**
516
     * Get the list of page's wikidata and Checkwiki errors.
517
     * @see Page::getErrors()
518
     * @return string[]
519
     */
520
    public function getBugs()
521
    {
522
        if (!is_array($this->bugs)) {
523
            $this->bugs = $this->page->getErrors();
524
        }
525
        return $this->bugs;
526
    }
527
528
    /**
529
     * Get the number of wikidata nad CheckWiki errors.
530
     * @return int
531
     */
532
    public function numBugs()
533
    {
534
        return count($this->getBugs());
535
    }
536
537
    /**
538
     * Get the number of external links on the page.
539
     * @return int
540
     */
541
    public function linksExtCount()
542
    {
543
        return $this->getLinksAndRedirects()['links_ext_count'];
544
    }
545
546
    /**
547
     * Get the number of incoming links to the page.
548
     * @return int
549
     */
550
    public function linksInCount()
551
    {
552
        return $this->getLinksAndRedirects()['links_in_count'];
553
    }
554
555
    /**
556
     * Get the number of outgoing links from the page.
557
     * @return int
558
     */
559
    public function linksOutCount()
560
    {
561
        return $this->getLinksAndRedirects()['links_out_count'];
562
    }
563
564
    /**
565
     * Get the number of redirects to the page.
566
     * @return int
567
     */
568
    public function redirectsCount()
569
    {
570
        return $this->getLinksAndRedirects()['redirects_count'];
571
    }
572
573
    /**
574
     * Get the number of external, incoming and outgoing links, along with
575
     * the number of redirects to the page.
576
     * @return int
577
     */
578
    private function getLinksAndRedirects()
579
    {
580
        if (!is_array($this->linksAndRedirects)) {
581
            $this->linksAndRedirects = $this->page->countLinksAndRedirects();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->page->countLinksAndRedirects() of type array<integer,string> is incompatible with the declared type array<integer,integer> of property $linksAndRedirects.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
582
        }
583
        return $this->linksAndRedirects;
584
    }
585
586
    /**
587
     * Parse the revision history, collecting our core statistics.
588
     * @todo Break this out into separate functions, perhaps with the loop body
589
     *   as a separate class.
590
     * @return mixed[] Associative "master" array of metadata about the page.
591
     */
592
    private function parseHistory()
593
    {
594
        if ($this->tooManyRevisions()) {
595
            $limit = $this->getMaxRevisions();
596
        } else {
597
            $limit = null;
598
        }
599
        // Third parameter is ignored if $limit is null.
600
        $revStmt = $this->page->getRevisionsStmt(null, $limit, $this->getNumRevisions());
601
        $revCount = 0;
602
603
        /** @var Edit|null The previous edit, used to discount content that was reverted */
604
        $prevEdit = null;
605
606
        /**
607
         * The edit previously deemed as having the maximum amount of content added.
608
         * This is used to discount content that was reverted.
609
         * @var Edit|null
610
        */
611
        $prevMaxAddEdit = null;
612
613
        /**
614
         * The edit previously deemed as having the maximum amount of content deleted.
615
         * This is used to discount content that was reverted
616
         * @var Edit|null
617
         */
618
        $prevMaxDelEdit = null;
619
620
        /** @var Time|null Time of first revision, used as a comparison for month counts */
621
        $firstEditMonth = null;
622
623
        while ($rev = $revStmt->fetch()) {
624
            $edit = new Edit($this->page, $rev);
625
626
            // Some shorthands
627
            $editYear = $edit->getYear();
628
            $editMonth = $edit->getMonth();
629
            $editTimestamp = $edit->getTimestamp();
630
631
            // Don't return actual edit size if last revision had a length of null.
632
            // This happens when the edit follows other edits that were revision-deleted.
633
            // See T148857 for more information.
634
            // @TODO: Remove once T101631 is resolved
635
            if ($prevEdit && $prevEdit->getLength() === null) {
636
                $editSize = 0;
637
            } else {
638
                $editSize = $edit->getSize();
639
            }
640
641
            if ($revCount === 0) {
642
                $this->firstEdit = $edit;
643
                $firstEditMonth = mktime(0, 0, 0, (int) $this->firstEdit->getMonth(), 1, $this->firstEdit->getYear());
644
            }
645
646
            $username = $edit->getUser()->getUsername();
647
648
            // Sometimes, with old revisions (2001 era), the revisions from 2002 come before 2001
649
            if ($editTimestamp < $this->firstEdit->getTimestamp()) {
650
                $this->firstEdit = $edit;
651
            }
652
653
            // Fill in the blank arrays for the year and 12 months
654
            if (!isset($this->yearMonthCounts[$editYear])) {
655
                $this->yearMonthCounts[$editYear] = [
656
                    'all' => 0,
657
                    'minor' => 0,
658
                    'anon' => 0,
659
                    'automated' => 0,
660
                    'size' => 0, // keep track of the size by the end of the year
661
                    'events' => [],
662
                    'months' => [],
663
                ];
664
665
                for ($i = 1; $i <= 12; $i++) {
666
                    $timeObj = mktime(0, 0, 0, $i, 1, $editYear);
667
668
                    // don't show zeros for months before the first edit or after the current month
669
                    if ($timeObj < $firstEditMonth || $timeObj > strtotime('last day of this month')) {
670
                        continue;
671
                    }
672
673
                    $this->yearMonthCounts[$editYear]['months'][sprintf('%02d', $i)] = [
674
                        'all' => 0,
675
                        'minor' => 0,
676
                        'anon' => 0,
677
                        'automated' => 0,
678
                    ];
679
                }
680
            }
681
682
            // Increment year and month counts for all edits
683
            $this->yearMonthCounts[$editYear]['all']++;
684
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['all']++;
685
            // This will ultimately be the size of the page by the end of the year
686
            $this->yearMonthCounts[$editYear]['size'] = $edit->getLength();
687
688
            // Keep track of which month had the most edits
689
            $editsThisMonth = $this->yearMonthCounts[$editYear]['months'][$editMonth]['all'];
690
            if ($editsThisMonth > $this->maxEditsPerMonth) {
691
                $this->maxEditsPerMonth = $editsThisMonth;
692
            }
693
694
            // Initialize various user stats
695
            if (!isset($this->editors[$username])) {
696
                $this->editors[$username] = [
697
                    'all' => 0,
698
                    'minor' => 0,
699
                    'minorPercentage' => 0,
700
                    'first' => $editTimestamp,
701
                    'firstId' => $edit->getId(),
702
                    'last' => null,
703
                    'atbe' => null,
704
                    'added' => 0,
705
                    'sizes' => [],
706
                ];
707
            }
708
709
            // Increment user counts
710
            $this->editors[$username]['all']++;
711
            $this->editors[$username]['last'] = $editTimestamp;
712
            $this->editors[$username]['lastId'] = $edit->getId();
713
714
            // Store number of KB added with this edit
715
            $this->editors[$username]['sizes'][] = $edit->getLength() / 1024;
716
717
            // Check if it was a revert
718
            if ($edit->isRevert($this->container)) {
719
                $this->revertCount++;
720
721
                // Since this was a revert, we don't want to treat the previous
722
                //   edit as legit content addition or removal
723
                if ($prevEdit && $prevEdit->getSize() > 0) {
724
                    $this->addedBytes -= $prevEdit->getSize();
725
                }
726
727
                // @TODO: Test this against an edit war (use your sandbox)
728
                // Also remove as max added or deleted, if applicable
729
                if ($this->maxAddition && $prevEdit->getId() === $this->maxAddition->getId()) {
730
                    $this->maxAddition = $prevMaxAddEdit;
731
                    $prevMaxAddEdit = $prevEdit; // in the event of edit wars
732
                } elseif ($this->maxDeletion &&
733
                    $prevEdit->getId() === $this->maxDeletion->getId()
734
                ) {
735
                    $this->maxDeletion = $prevMaxDelEdit;
736
                    $prevMaxDelEdit = $prevEdit; // in the event of edit wars
737
                }
738
            } else {
739
                // Edit was not a revert, so treat size > 0 as content added
740
                if ($editSize > 0) {
741
                    $this->addedBytes += $editSize;
742
                    $this->editors[$username]['added'] += $editSize;
743
744
                    // Keep track of edit with max addition
745
                    if (!$this->maxAddition || $editSize > $this->maxAddition->getSize()) {
746
                        // Keep track of old maxAddition in case we find out the next $edit was reverted
747
                        //   (and was also a max edit), in which case we'll want to use this one ($edit)
748
                        $prevMaxAddEdit = $this->maxAddition;
749
750
                        $this->maxAddition = $edit;
751
                    }
752
                } elseif ($editSize < 0 && (
753
                    !$this->maxDeletion || $editSize < $this->maxDeletion->getSize()
754
                )) {
755
                    $this->maxDeletion = $edit;
756
                }
757
            }
758
759
            // If anonymous, increase counts
760
            if ($edit->isAnon()) {
761
                $this->anonCount++;
762
                $this->yearMonthCounts[$editYear]['anon']++;
763
                $this->yearMonthCounts[$editYear]['months'][$editMonth]['anon']++;
764
            }
765
766
            // If minor edit, increase counts
767
            if ($edit->isMinor()) {
768
                $this->minorCount++;
769
                $this->yearMonthCounts[$editYear]['minor']++;
770
                $this->yearMonthCounts[$editYear]['months'][$editMonth]['minor']++;
771
772
                // Increment minor counts for this user
773
                $this->editors[$username]['minor']++;
774
            }
775
776
            $automatedTool = $edit->getTool($this->container);
777
            if ($automatedTool !== false) {
778
                $this->automatedCount++;
779
                $this->yearMonthCounts[$editYear]['automated']++;
780
                $this->yearMonthCounts[$editYear]['months'][$editMonth]['automated']++;
781
782
                if (!isset($this->tools[$automatedTool['name']])) {
783
                    $this->tools[$automatedTool['name']] = [
784
                        'count' => 1,
785
                        'link' => $automatedTool['link'],
786
                    ];
787
                } else {
788
                    $this->tools[$automatedTool['name']]['count']++;
789
                }
790
            }
791
792
            // Increment "edits per <time>" counts
793
            if ($editTimestamp > new DateTime('-1 day')) {
794
                $this->countHistory['day']++;
795
            }
796
            if ($editTimestamp > new DateTime('-1 week')) {
797
                $this->countHistory['week']++;
798
            }
799
            if ($editTimestamp > new DateTime('-1 month')) {
800
                $this->countHistory['month']++;
801
            }
802
            if ($editTimestamp > new DateTime('-1 year')) {
803
                $this->countHistory['year']++;
804
            }
805
806
            $revCount++;
807
            $prevEdit = $edit;
808
            $this->lastEdit = $edit;
809
        }
810
811
        // Various sorts
812
        arsort($this->editors);
813
        ksort($this->yearMonthCounts);
814
        if ($this->tools) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->tools of type array<*,string|array<string,integer|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...
815
            arsort($this->tools);
816
        }
817
    }
818
819
    /**
820
     * Get info about bots that edited the page.
821
     * This also sets $this->botRevisionCount and $this->botPercentage.
822
     * @return mixed[] Contains the bot's username, edit count to the page,
823
     *   and whether or not they are currently a bot.
824
     */
825
    private function getBotData()
826
    {
827
        $userGroupsTable = $this->getProject()->getTableName('user_groups');
828
        $userFromerGroupsTable = $this->getProject()->getTableName('user_former_groups');
829
        $sql = "SELECT COUNT(rev_user_text) AS count, rev_user_text AS username, ug_group AS current
830
                FROM " . $this->getProject()->getTableName('revision') . "
831
                LEFT JOIN $userGroupsTable ON rev_user = ug_user
832
                LEFT JOIN $userFromerGroupsTable ON rev_user = ufg_user
833
                WHERE rev_page = :pageId AND (ug_group = 'bot' OR ufg_group = 'bot')
834
                GROUP BY rev_user_text";
835
        $resultQuery = $this->conn->prepare($sql);
836
        $pageId = $this->page->getId();
837
        $resultQuery->bindParam('pageId', $pageId);
838
        $resultQuery->execute();
839
840
        // Parse the botedits
841
        $bots = [];
842
        $sum = 0;
843
        while ($bot = $resultQuery->fetch()) {
844
            $bots[$bot['username']] = [
845
                'count' => (int) $bot['count'],
846
                'current' => $bot['current'] === 'bot'
847
            ];
848
            $sum += $bot['count'];
849
        }
850
851
        uasort($bots, function ($a, $b) {
852
            return $b['count'] - $a['count'];
853
        });
854
855
        $this->botRevisionCount = $sum;
856
        $this->botPercentage = round(
0 ignored issues
show
Bug introduced by
The property botPercentage does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
857
            // FIXME: should be processed revisions
858
            ($sum / $this->getNumRevisions()) * 100,
859
            1
860
        );
861
862
        return $bots;
863
    }
864
865
    /**
866
     * Query for log events during each year of the article's history,
867
     *   and set the results in $this->yearMonthCounts.
868
     */
869
    private function setLogsEvents()
870
    {
871
        $loggingTable = $this->getProject()->getTableName('logging', 'logindex');
872
        $title = str_replace(' ', '_', $this->page->getTitle());
873
        $sql = "SELECT log_action, log_type, log_timestamp AS timestamp
874
                FROM $loggingTable
875
                WHERE log_namespace = '" . $this->page->getNamespace() . "'
876
                AND log_title = :title AND log_timestamp > 1
877
                AND log_type IN ('delete', 'move', 'protect', 'stable')";
878
        $resultQuery = $this->conn->prepare($sql);
879
        $resultQuery->bindParam(':title', $title);
880
        $resultQuery->execute();
881
882
        while ($event = $resultQuery->fetch()) {
883
            $time = strtotime($event['timestamp']);
884
            $year = date('Y', $time);
885
            if (isset($this->yearMonthCounts[$year])) {
886
                $yearEvents = $this->yearMonthCounts[$year]['events'];
887
888
                // Convert log type value to i18n key
889
                switch ($event['log_type']) {
890
                    case 'protect':
891
                        $action = 'protections';
892
                        break;
893
                    case 'delete':
894
                        $action = 'deletions';
895
                        break;
896
                    case 'move':
897
                        $action = 'moves';
898
                        break;
899
                    // count pending-changes protections along with normal protections
900
                    case 'stable':
901
                        $action = 'protections';
902
                        break;
903
                }
904
905
                if (empty($yearEvents[$action])) {
906
                    $yearEvents[$action] = 1;
0 ignored issues
show
Bug introduced by
The variable $action does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
907
                } else {
908
                    $yearEvents[$action]++;
909
                }
910
911
                $this->yearMonthCounts[$year]['events'] = $yearEvents;
912
            }
913
        }
914
    }
915
916
    /**
917
     * Set statistics about the top 10 editors by added text and number of edits.
918
     * This is ran *after* parseHistory() since we need the grand totals first.
919
     * Various stats are also set for each editor in $this->editors to be used in the charts.
920
     * @return integer Number of edits
921
     */
922
    private function setTopTenCounts()
923
    {
924
        $topTenCount = $counter = 0;
925
        $topTenEditors = [];
926
927
        foreach ($this->editors as $editor => $info) {
928
            // Count how many users are in the top 10% by number of edits
929
            if ($counter < 10) {
930
                $topTenCount += $info['all'];
931
                $counter++;
932
933
                // To be used in the Top Ten charts
934
                $topTenEditors[] = [
935
                    'label' => $editor,
936
                    'value' => $info['all'],
937
                    'percentage' => (
938
                        100 * ($info['all'] / $this->getNumRevisions())
939
                    )
940
                ];
941
            }
942
943
            // Compute the percentage of minor edits the user made
944
            $this->editors[$editor]['minorPercentage'] = $info['all']
945
                ? ($info['minor'] / $info['all']) * 100
946
                : 0;
947
948
            if ($info['all'] > 1) {
949
                // Number of seconds/days between first and last edit
950
                $secs = $info['last']->getTimestamp() - $info['first']->getTimestamp();
951
                $days = $secs / (60 * 60 * 24);
952
953
                // Average time between edits (in days)
954
                $this->editors[$editor]['atbe'] = $days / $info['all'];
955
            }
956
957
            if (count($info['sizes'])) {
958
                // Average Total KB divided by number of stored sizes (user's edit count to this page)
959
                $this->editors[$editor]['size'] = array_sum($info['sizes']) / count($info['sizes']);
960
            } else {
961
                $this->editors[$editor]['size'] = 0;
962
            }
963
        }
964
965
        $this->topTenEditorsByEdits = $topTenEditors;
966
967
        // First sort editors array by the amount of text they added
968
        $topTenEditorsByAdded = $this->editors;
969
        uasort($topTenEditorsByAdded, function ($a, $b) {
970
            if ($a['added'] === $b['added']) {
971
                return 0;
972
            }
973
            return $a['added'] > $b['added'] ? -1 : 1;
974
        });
975
976
        // Then build a new array of top 10 editors by added text,
977
        //   in the data structure needed for the chart
978
        $this->topTenEditorsByAdded = array_map(function ($editor) {
979
            $added = $this->editors[$editor]['added'];
980
            return [
981
                'label' => $editor,
982
                'value' => $added,
983
                'percentage' => (
984
                    100 * ($added / $this->addedBytes)
985
                )
986
            ];
987
        }, array_keys(array_slice($topTenEditorsByAdded, 0, 10)));
988
989
        $this->topTenCount = $topTenCount;
990
    }
991
}
992