Passed
Pull Request — master (#115)
by MusikAnimal
03:31
created

ArticleInfo::getBugs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 0
cts 4
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 0
crap 6
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
        $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 9
            ->get('doctrine')
130 9
            ->getManager('replicas')
131 9
            ->getConnection();
132 9
    }
133
134
    /**
135
     * Shorthand to get the page's project.
136
     * @return Project
137
     * @codeCoverageIgnore
138
     */
139
    public function getProject()
140
    {
141
        return $this->page->getProject();
142
    }
143
144
    /**
145
     * Get the number of revisions belonging to the page.
146
     * @return int
147
     */
148 4
    public function getNumRevisions()
149
    {
150 4
        if (!isset($this->numRevisions)) {
151 4
            $this->numRevisions = $this->page->getNumRevisions();
152
        }
153 4
        return $this->numRevisions;
154
    }
155
156
    /**
157
     * Get the maximum number of revisions that we should process.
158
     * @return int
159
     */
160 3
    public function getMaxRevisions()
161
    {
162 3
        if (!isset($this->maxRevisions)) {
163 3
            $this->maxRevisions = (int) $this->container->getParameter('app.max_page_revisions');
164
        }
165 3
        return $this->maxRevisions;
166
    }
167
168
    /**
169
     * Get the number of revisions that are actually getting processed.
170
     * This goes by the app.max_page_revisions parameter, or the actual
171
     * number of revisions, whichever is smaller.
172
     * @return int
173
     */
174 5
    public function getNumRevisionsProcessed()
175
    {
176 5
        if (isset($this->numRevisionsProcessed)) {
177 3
            return $this->numRevisionsProcessed;
178
        }
179
180 2
        if ($this->tooManyRevisions()) {
181 1
            $this->numRevisionsProcessed = $this->getMaxRevisions();
182
        } else {
183 1
            $this->numRevisionsProcessed = $this->getNumRevisions();
184
        }
185
186 2
        return $this->numRevisionsProcessed;
187
    }
188
189
    /**
190
     * Are there more revisions than we should process, based on the config?
191
     * @return bool
192
     */
193 3
    public function tooManyRevisions()
194
    {
195 3
        return $this->getMaxRevisions() > 0 && $this->getNumRevisions() > $this->getMaxRevisions();
196
    }
197
198
    /**
199
     * Fetch and store all the data we need to show the ArticleInfo view.
200
     * @codeCoverageIgnore
201
     */
202
    public function prepareData()
203
    {
204
        $this->parseHistory();
205
        $this->setLogsEvents();
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();
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...
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();
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...
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);
0 ignored issues
show
Documentation introduced by
$prevEdits is of type array<string,null|object<Xtools\Edit>>, but the function expects a array<integer,object<Xtools\Edit>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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
702
        // @TODO: Test this against an edit war (use your sandbox).
703
        // Also remove as max added or deleted, if applicable.
704 3
        if ($this->maxAddition && $prevEdits['prev']->getId() === $this->maxAddition->getId()) {
705
            $this->maxAddition = $prevEdits['maxAddition'];
706
            $prevEdits['maxAddition'] = $prevEdits['prev']; // in the event of edit wars
707 3
        } elseif ($this->maxDeletion && $prevEdits['prev']->getId() === $this->maxDeletion->getId()) {
708 3
            $this->maxDeletion = $prevEdits['maxDeletion'];
709 3
            $prevEdits['maxDeletion'] = $prevEdits['prev']; // in the event of edit wars
710
        }
711
712 3
        return $prevEdits;
713
    }
714
715
    /**
716
     * Updates the figures on content sizes assuming the given edit
717
     * was NOT a revert of the previous edit.
718
     * @param  Edit   $edit
719
     * @param  Edit[] $prevEdits With 'prev', 'maxAddition' and 'maxDeletion'.
720
     * @return Edit[] Updated version of $prevEdits, for tracking.
721
     */
722 3
    private function updateContentSizesNonRevert(Edit $edit, $prevEdits)
723
    {
724 3
        $editSize = $this->getEditSize($edit, $prevEdits);
725
726
        // Edit was not a revert, so treat size > 0 as content added.
727 3
        if ($editSize > 0) {
728 3
            $this->addedBytes += $editSize;
729 3
            $this->editors[$edit->getUser()->getUsername()]['added'] += $editSize;
730
731
            // Keep track of edit with max addition.
732 3
            if (!$this->maxAddition || $editSize > $this->maxAddition->getSize()) {
733
                // Keep track of old maxAddition in case we find out the next $edit was reverted
734
                // (and was also a max edit), in which case we'll want to use this one ($edit).
735 3
                $prevEdits['maxAddition'] = $this->maxAddition;
736
737 3
                $this->maxAddition = $edit;
738
            }
739 3
        } elseif ($editSize < 0 && (!$this->maxDeletion || $editSize < $this->maxDeletion->getSize())) {
740
            // Keep track of old maxDeletion in case we find out the next edit was reverted
741
            // (and was also a max deletion), in which case we'll want to use this one.
742 3
            $prevEdits['maxDeletion'] = $this->maxDeletion;
743
744 3
            $this->maxDeletion = $edit;
745
        }
746
747 3
        return $prevEdits;
748
    }
749
750
    /**
751
     * Get the size of the given edit, based on the previous edit (if present).
752
     * We also don't return the actual edit size if last revision had a length of null.
753
     * This happens when the edit follows other edits that were revision-deleted.
754
     * @see T148857 for more information.
755
     * @todo Remove once T101631 is resolved.
756
     * @param  Edit   $edit
757
     * @param  Edit[] $prevEdits With 'prev', 'maxAddition' and 'maxDeletion'.
758
     * @return Edit[] Updated version of $prevEdits, for tracking.
759
     */
760 3
    private function getEditSize(Edit $edit, $prevEdits)
761
    {
762 3
        if ($prevEdits['prev'] && $prevEdits['prev']->getLength() === null) {
763
            return 0;
764
        } else {
765 3
            return $edit->getSize();
766
        }
767
    }
768
769
    /**
770
     * Update counts of automated tool usage for the given edit.
771
     * @param Edit $edit
772
     */
773 3
    private function updateToolCounts(Edit $edit)
774
    {
775 3
        $automatedTool = $edit->getTool($this->container);
776
777 3
        if ($automatedTool === false) {
778
            // Nothing to do.
779 3
            return;
780
        }
781
782 3
        $editYear = $edit->getYear();
783 3
        $editMonth = $edit->getMonth();
784
785 3
        $this->automatedCount++;
786 3
        $this->yearMonthCounts[$editYear]['automated']++;
787 3
        $this->yearMonthCounts[$editYear]['months'][$editMonth]['automated']++;
788
789 3
        if (!isset($this->tools[$automatedTool['name']])) {
790 3
            $this->tools[$automatedTool['name']] = [
791 3
                'count' => 1,
792 3
                'link' => $automatedTool['link'],
793
            ];
794
        } else {
795
            $this->tools[$automatedTool['name']]['count']++;
796
        }
797 3
    }
798
799
    /**
800
     * Update various counts for the year and month of the given edit.
801
     * @param Edit $edit
802
     */
803 3
    private function updateYearMonthCounts(Edit $edit)
804
    {
805 3
        $editYear = $edit->getYear();
806 3
        $editMonth = $edit->getMonth();
807
808
        // Fill in the blank arrays for the year and 12 months if needed.
809 3
        if (!isset($this->yearMonthCounts[$editYear])) {
810 3
            $this->addYearMonthCountEntry($edit);
811
        }
812
813
        // Increment year and month counts for all edits
814 3
        $this->yearMonthCounts[$editYear]['all']++;
815 3
        $this->yearMonthCounts[$editYear]['months'][$editMonth]['all']++;
816
        // This will ultimately be the size of the page by the end of the year
817 3
        $this->yearMonthCounts[$editYear]['size'] = (int) $edit->getLength();
818
819
        // Keep track of which month had the most edits
820 3
        $editsThisMonth = $this->yearMonthCounts[$editYear]['months'][$editMonth]['all'];
821 3
        if ($editsThisMonth > $this->maxEditsPerMonth) {
822 3
            $this->maxEditsPerMonth = $editsThisMonth;
823
        }
824 3
    }
825
826
    /**
827
     * Add a new entry to $this->yearMonthCounts for the given year,
828
     * with blank values for each month. This called during self::parseHistory().
829
     * @param Edit $edit
830
     */
831 3
    private function addYearMonthCountEntry(Edit $edit)
832
    {
833 3
        $editYear = $edit->getYear();
834
835
        // Beginning of the month at 00:00:00.
836 3
        $firstEditTime = mktime(0, 0, 0, (int) $this->firstEdit->getMonth(), 1, $this->firstEdit->getYear());
837
838 3
        $this->yearMonthCounts[$editYear] = [
839
            'all' => 0,
840
            'minor' => 0,
841
            'anon' => 0,
842
            'automated' => 0,
843
            'size' => 0, // Keep track of the size by the end of the year.
844
            'events' => [],
845
            'months' => [],
846
        ];
847
848 3
        for ($i = 1; $i <= 12; $i++) {
849 3
            $timeObj = mktime(0, 0, 0, $i, 1, $editYear);
850
851
            // Don't show zeros for months before the first edit or after the current month.
852 3
            if ($timeObj < $firstEditTime || $timeObj > strtotime('last day of this month')) {
853 3
                continue;
854
            }
855
856 3
            $this->yearMonthCounts[$editYear]['months'][sprintf('%02d', $i)] = [
857
                'all' => 0,
858
                'minor' => 0,
859
                'anon' => 0,
860
                'automated' => 0,
861
            ];
862
        }
863 3
    }
864
865
    /**
866
     * Update the counts of anon and minor edits for year, month,
867
     * and user of the given edit.
868
     * @param Edit $edit
869
     */
870 3
    private function updateAnonMinorCounts(Edit $edit)
871
    {
872 3
        $editYear = $edit->getYear();
873 3
        $editMonth = $edit->getMonth();
874
875
        // If anonymous, increase counts
876 3
        if ($edit->isAnon()) {
877 3
            $this->anonCount++;
878 3
            $this->yearMonthCounts[$editYear]['anon']++;
879 3
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['anon']++;
880
        }
881
882
        // If minor edit, increase counts
883 3
        if ($edit->isMinor()) {
884 3
            $this->minorCount++;
885 3
            $this->yearMonthCounts[$editYear]['minor']++;
886 3
            $this->yearMonthCounts[$editYear]['months'][$editMonth]['minor']++;
887
        }
888 3
    }
889
890
    /**
891
     * Update various counts for the user of the given edit.
892
     * @param Edit $edit
893
     */
894 3
    private function updateUserCounts(Edit $edit)
895
    {
896 3
        $username = $edit->getUser()->getUsername();
897
898
        // Initialize various user stats if needed.
899 3
        if (!isset($this->editors[$username])) {
900 3
            $this->editors[$username] = [
901 3
                'all' => 0,
902 3
                'minor' => 0,
903 3
                'minorPercentage' => 0,
904 3
                'first' => $edit->getTimestamp(),
905 3
                'firstId' => $edit->getId(),
906
                'last' => null,
907
                'atbe' => null,
908 3
                'added' => 0,
909
                'sizes' => [],
910
            ];
911
        }
912
913
        // Increment user counts
914 3
        $this->editors[$username]['all']++;
915 3
        $this->editors[$username]['last'] = $edit->getTimestamp();
916 3
        $this->editors[$username]['lastId'] = $edit->getId();
917
918
        // Store number of KB added with this edit
919 3
        $this->editors[$username]['sizes'][] = $edit->getLength() / 1024;
920
921
        // Increment minor counts for this user
922 3
        if ($edit->isMinor()) {
923 3
            $this->editors[$username]['minor']++;
924
        }
925 3
    }
926
927
    /**
928
     * Increment "edits per <time>" counts based on the given edit.
929
     * @param Edit $edit
930
     */
931 3
    private function updateCountHistory(Edit $edit)
932
    {
933 3
        $editTimestamp = $edit->getTimestamp();
934
935 3
        if ($editTimestamp > new DateTime('-1 day')) {
936
            $this->countHistory['day']++;
937
        }
938 3
        if ($editTimestamp > new DateTime('-1 week')) {
939
            $this->countHistory['week']++;
940
        }
941 3
        if ($editTimestamp > new DateTime('-1 month')) {
942
            $this->countHistory['month']++;
943
        }
944 3
        if ($editTimestamp > new DateTime('-1 year')) {
945
            $this->countHistory['year']++;
946
        }
947 3
    }
948
949
    /**
950
     * Get info about bots that edited the page.
951
     * @return mixed[] Contains the bot's username, edit count to the page,
952
     *   and whether or not they are currently a bot.
953
     */
954
    public function getBots()
955
    {
956
        if (isset($this->bots)) {
957
            return $this->bots;
958
        }
959
960
        // Parse the botedits
961
        $bots = [];
962
        $botData = $this->getRepository()->getBotData($this->page);
963
        while ($bot = $botData->fetch()) {
964
            $bots[$bot['username']] = [
965
                'count' => (int) $bot['count'],
966
                'current' => $bot['current'] === 'bot',
967
            ];
968
        }
969
970
        // Sort by edit count.
971
        uasort($bots, function ($a, $b) {
972
            return $b['count'] - $a['count'];
973
        });
974
975
        $this->bots = $bots;
976
        return $bots;
977
    }
978
979
    /**
980
     * Number of edits made to the page by current or former bots.
981
     * @param string[] $bots Used only in unit tests, where we
982
     *   supply mock data for the bots that will get processed.
983
     * @return int
984
     */
985 1
    public function getBotRevisionCount($bots = null)
986
    {
987 1
        if (isset($this->botRevisionCount)) {
988
            return $this->botRevisionCount;
989
        }
990
991 1
        if ($bots === null) {
992
            $bots = $this->getBots();
993
        }
994
995 1
        $count = 0;
996
997 1
        foreach ($bots as $username => $data) {
998 1
            $count += $data['count'];
999
        }
1000
1001 1
        $this->botRevisionCount = $count;
1002 1
        return $count;
1003
    }
1004
1005
    /**
1006
     * Query for log events during each year of the article's history,
1007
     *   and set the results in $this->yearMonthCounts.
1008
     */
1009 1
    private function setLogsEvents()
1010
    {
1011 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()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

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