Completed
Push — master ( ac3fb6...2d8d7f )
by
unknown
02:49
created

ArticleInfoController::getDiffSize()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 9
nc 3
nop 1
1
<?php
2
/**
3
 * This file contains only the ArticleInfoController class.
4
 */
5
6
namespace AppBundle\Controller;
7
8
use AppBundle\Helper\AutomatedEditsHelper;
9
use AppBundle\Helper\PageviewsHelper;
10
use Doctrine\DBAL\Connection;
11
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
12
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
13
use Symfony\Component\HttpFoundation\Request;
14
use Symfony\Component\DependencyInjection\ContainerInterface;
15
use Symfony\Component\HttpFoundation\Response;
16
use Xtools\ProjectRepository;
17
use Xtools\Page;
18
use Xtools\PagesRepository;
19
use Xtools\Edit;
20
use DateTime;
21
22
/**
23
 * This controller serves the search form and results for the ArticleInfo tool
24
 */
25
class ArticleInfoController extends Controller
26
{
27
    /** @var mixed[] Information about the page in question. */
28
    private $pageInfo;
29
    /** @var Edit[] All edits of the page. */
30
    private $pageHistory;
0 ignored issues
show
Unused Code introduced by
The property $pageHistory is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
31
    /** @var ProjectRepository Shared Project repository for use of getting table names, etc. */
32
    private $projectRepo;
33
    /** @var string Database name, for us of getting table names, etc. */
34
    private $dbName;
35
    /** @var Connection The projects' database connection. */
36
    protected $conn;
37
    /** @var AutomatedEditsHelper The semi-automated edits helper. */
38
    protected $aeh;
39
    /** @var PageviewsHelper The page-views helper. */
40
    protected $ph;
41
42
    /**
43
     * Get the tool's shortname.
44
     * @return string
45
     */
46
    public function getToolShortname()
47
    {
48
        return 'articleinfo';
49
    }
50
51
    /**
52
     * Override method to call ArticleInfoController::containerInitialized() when container set.
53
     * @param ContainerInterface|null $container A ContainerInterface instance or null
54
     */
55
    public function setContainer(ContainerInterface $container = null)
56
    {
57
        parent::setContainer($container);
58
        $this->containerInitialized();
59
    }
60
61
    /**
62
     * Perform some operations after controller initialized and container set.
63
     */
64
    private function containerInitialized()
65
    {
66
        $this->conn = $this->getDoctrine()->getManager('replicas')->getConnection();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Doctrine\Common\Persistence\ObjectManager as the method getConnection() does only exist in the following implementations of said interface: Doctrine\ORM\Decorator\EntityManagerDecorator, Doctrine\ORM\EntityManager.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
67
        $this->ph = $this->get('app.pageviews_helper');
68
        $this->aeh = $this->get('app.automated_edits_helper');
69
    }
70
71
    /**
72
     * The search form.
73
     * @Route("/articleinfo", name="articleinfo")
74
     * @Route("/articleinfo", name="articleInfo")
75
     * @Route("/articleinfo/", name="articleInfoSlash")
76
     * @Route("/articleinfo/index.php", name="articleInfoIndexPhp")
77
     * @Route("/articleinfo/{project}", name="ArticleInfoProject")
78
     * @param Request $request The HTTP request.
79
     * @return Response
80
     */
81
    public function indexAction(Request $request)
82
    {
83
        $projectQuery = $request->query->get('project');
84
        $article = $request->query->get('article');
85
86
        if ($projectQuery != '' && $article != '') {
87
            return $this->redirectToRoute('ArticleInfoResult', [ 'project'=>$projectQuery, 'article' => $article ]);
88
        } elseif ($article != '') {
89
            return $this->redirectToRoute('ArticleInfoProject', [ 'project'=>$projectQuery ]);
90
        }
91
92
        if ($projectQuery == '') {
93
            $projectQuery = $this->container->getParameter('default_project');
94
        }
95
96
        $project = ProjectRepository::getProject($projectQuery, $this->container);
97
98
        return $this->render('articleInfo/index.html.twig', [
99
            'xtPage' => 'articleinfo',
100
            'xtPageTitle' => 'tool-articleinfo',
101
            'xtSubtitle' => 'tool-articleinfo-desc',
102
            'project' => $project,
103
        ]);
104
    }
105
106
    /**
107
     * Display the results.
108
     * @Route("/articleinfo/{project}/{article}", name="ArticleInfoResult", requirements={"article"=".+"})
109
     * @param Request $request The HTTP request.
110
     * @return Response
111
     */
112
    public function resultAction(Request $request)
113
    {
114
        $projectQuery = $request->attributes->get('project');
115
        $project = ProjectRepository::getProject($projectQuery, $this->container);
116
        $this->projectRepo = $project->getRepository();
117 View Code Duplication
        if (!$project->exists()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
118
            $this->addFlash('notice', ['invalid-project', $projectQuery]);
0 ignored issues
show
Documentation introduced by
array('invalid-project', $projectQuery) is of type array<integer,*,{"0":"string","1":"*"}>, but the function expects a string.

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...
119
            return $this->redirectToRoute('articleInfo');
120
        }
121
        $this->dbName = $project->getDatabaseName();
122
123
        $pageQuery = $request->attributes->get('article');
124
        $page = new Page($project, $pageQuery);
125
        $pageRepo = new PagesRepository();
126
        $pageRepo->setContainer($this->container);
1 ignored issue
show
Compatibility introduced by
$this->container of type object<Symfony\Component...ion\ContainerInterface> is not a sub-type of object<Symfony\Component...ncyInjection\Container>. It seems like you assume a concrete implementation of the interface Symfony\Component\Depend...tion\ContainerInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
127
        $page->setRepository($pageRepo);
128
129
        if (!$page->exists()) {
130
            $this->addFlash('notice', ['no-exist', $pageQuery]);
0 ignored issues
show
Documentation introduced by
array('no-exist', $pageQuery) is of type array<integer,*,{"0":"string","1":"*"}>, but the function expects a string.

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...
131
            return $this->redirectToRoute('articleInfo');
132
        }
133
134
        $this->pageInfo = [
0 ignored issues
show
Documentation Bug introduced by
It seems like array('project' => $proj...=> $project->getLang()) of type array<string,object<Xtoo...age>","lang":"string"}> is incompatible with the declared type array<integer,*> of property $pageInfo.

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...
135
            'project' => $project,
136
            'page' => $page,
137
            'lang' => $project->getLang(),
138
        ];
139
140
        // TODO: Adapted from legacy code; may be used to indicate how many dead ext links there are
141
        // if ( isset( $basicInfo->extlinks ) ){
1 ignored issue
show
Unused Code Comprehensibility introduced by
57% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
142
        //     foreach ( $basicInfo->extlinks as $i => $link ){
1 ignored issue
show
Unused Code Comprehensibility introduced by
53% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
143
        //         $this->extLinks[] = array("link" => $link->{'*'}, "status" => "unchecked" );
1 ignored issue
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
144
        //     }
145
        // }
146
147
        $this->pageInfo = array_merge($this->pageInfo, $this->parseHistory($page));
0 ignored issues
show
Documentation Bug introduced by
It seems like array_merge($this->pageI...s->parseHistory($page)) of type array is incompatible with the declared type array<integer,*> of property $pageInfo.

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...
148
        $this->pageInfo['bots'] = $this->getBotData();
149
        $this->pageInfo['general']['bot_count'] = count($this->pageInfo['bots']);
150
        $this->pageInfo['general']['top_ten_count'] = $this->getTopTenCount();
151
        $this->pageInfo['general']['top_ten_percentage'] = round(
152
            ($this->pageInfo['general']['top_ten_count'] / $page->getNumRevisions()) * 100,
153
            1
154
        );
155
        $this->pageInfo = array_merge($this->pageInfo, $page->countLinksAndRedirects());
0 ignored issues
show
Documentation Bug introduced by
It seems like array_merge($this->pageI...untLinksAndRedirects()) of type array is incompatible with the declared type array<integer,*> of property $pageInfo.

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...
156
        $this->pageInfo['general']['pageviews_offset'] = 60;
157
        $this->pageInfo['general']['pageviews'] = $this->ph->sumLastDays(
158
            $this->pageInfo['project']->getDomain(),
159
            $this->pageInfo['page']->getTitle(),
160
            $this->pageInfo['general']['pageviews_offset']
161
        );
162
163
        $assessments = $page->getAssessments();
164
        if ($assessments) {
165
            $this->pageInfo['assessments'] = $assessments;
166
        }
167
        $this->setLogsEvents();
168
169
        $bugs = $page->getErrors();
170
        if (!empty($bugs)) {
171
            $this->pageInfo['bugs'] = $bugs;
172
        }
173
174
        $this->pageInfo['xtPage'] = 'articleinfo';
175
        $this->pageInfo['xtTitle'] = $page->getTitle();
176
177
        return $this->render('articleInfo/result.html.twig', $this->pageInfo);
178
    }
179
180
    /**
181
     * Get info about bots that edited the page
182
     * This also sets $this->pageInfo['bot_revision_count'] and $this->pageInfo['bot_percentage']
183
     * @return array Associative array containing the bot's username, edit count to the page
184
     *               and whether or not they are currently a bot
185
     */
186
    private function getBotData()
187
    {
188
        $userGroupsTable = $this->projectRepo->getTableName($this->dbName, 'user_groups');
189
        $userFromerGroupsTable = $this->projectRepo->getTableName($this->dbName, 'user_former_groups');
190
        $query = "SELECT COUNT(rev_user_text) AS count, rev_user_text AS username, ug_group AS current
191
                  FROM " . $this->projectRepo->getTableName($this->dbName, 'revision') . "
192
                  LEFT JOIN $userGroupsTable ON rev_user = ug_user
193
                  LEFT JOIN $userFromerGroupsTable ON rev_user = ufg_user
194
                  WHERE rev_page = " . $this->pageInfo['page']->getId() . " AND (ug_group = 'bot' OR ufg_group = 'bot')
195
                  GROUP BY rev_user_text";
196
        $res = $this->conn->query($query)->fetchAll();
197
198
        // Parse the botedits
199
        $bots = [];
200
        $sum = 0;
201
        foreach ($res as $bot) {
202
            $bots[$bot['username']] = [
203
                'count' => (int) $bot['count'],
204
                'current' => $bot['current'] === 'bot'
205
            ];
206
            $sum += $bot['count'];
207
        }
208
209
        uasort($bots, function ($a, $b) {
210
            return $b['count'] - $a['count'];
211
        });
212
213
        $this->pageInfo['general']['bot_revision_count'] = $sum;
214
        $this->pageInfo['general']['bot_percentage'] = round(
215
            ($sum / $this->pageInfo['page']->getNumRevisions()) * 100,
216
            1
217
        );
218
219
        return $bots;
220
    }
221
222
    /**
223
     * Get the number of edits made to the page by the top 10% of editors
224
     * This is ran *after* parseHistory() since we need the grand totals first.
225
     * Various stats are also set for each editor in $this->pageInfo['editors']
226
     *   and top ten editors are stored in $this->pageInfo['general']['top_ten']
227
     *   to be used in the charts
228
     * @return integer Number of edits
229
     */
230
    private function getTopTenCount()
231
    {
232
        $topTenCount = $counter = 0;
233
        $topTenEditors = [];
234
235
        foreach ($this->pageInfo['editors'] as $editor => $info) {
236
            // Count how many users are in the top 10% by number of edits
237
            if ($counter < 10) {
238
                $topTenCount += $info['all'];
239
                $counter++;
240
241
                // To be used in the Top Ten charts
242
                $topTenEditors[] = [
243
                    'label' => $editor,
244
                    'value' => $info['all'],
245
                    'percentage' => (
246
                        100 * ($info['all'] / $this->pageInfo['page']->getNumRevisions())
247
                    )
248
                ];
249
            }
250
251
            // Compute the percentage of minor edits the user made
252
            $this->pageInfo['editors'][$editor]['minor_percentage'] = $info['all']
253
                ? ($info['minor'] / $info['all']) * 100
254
                : 0;
255
256
            if ($info['all'] > 1) {
257
                // Number of seconds between first and last edit
258
                $secs = $info['last']->getTimestamp() - $info['first']->getTimestamp();
259
260
                // Average time between edits (in days)
261
                $this->pageInfo['editors'][$editor]['atbe'] = $secs / ( 60 * 60 * 24 );
262
            }
263
264
            if (count($info['sizes'])) {
265
                // Average Total KB divided by number of stored sizes (user's edit count to this page)
266
                $this->pageInfo['editors'][$editor]['size'] = array_sum($info['sizes']) / count($info['sizes']);
267
            } else {
268
                $this->pageInfo['editors'][$editor]['size'] = 0;
269
            }
270
        }
271
272
        $this->pageInfo['topTenEditors'] = $topTenEditors;
273
274
        // First sort editors array by the amount of text they added
275
        $topTenEditorsByAdded = $this->pageInfo['editors'];
276
        uasort($topTenEditorsByAdded, function ($a, $b) {
277
            if ($a['added'] === $b['added']) {
278
                return 0;
279
            }
280
            return $a['added'] > $b['added'] ? -1 : 1;
281
        });
282
283
        // Then build a new array of top 10 editors by added text,
284
        //   in the data structure needed for the chart
285
        $this->pageInfo['topTenEditorsByAdded'] = array_map(function ($editor) {
286
            $added = $this->pageInfo['editors'][$editor]['added'];
287
            return [
288
                'label' => $editor,
289
                'value' => $added,
290
                'percentage' => (
291
                    100 * ($added / $this->pageInfo['general']['added'])
292
                )
293
            ];
294
        }, array_keys(array_slice($topTenEditorsByAdded, 0, 10)));
295
296
        return $topTenCount;
297
    }
298
299
    /**
300
     * Query for log events during each year of the article's history,
301
     *   and set the results in $this->pageInfo['year_count']
302
     */
303
    private function setLogsEvents()
304
    {
305
        $loggingTable = $this->projectRepo->getTableName($this->dbName, 'logging', 'logindex');
306
        $title = str_replace(' ', '_', $this->pageInfo['page']->getTitle());
307
        $query = "SELECT log_action, log_type, log_timestamp AS timestamp
308
                  FROM $loggingTable
309
                  WHERE log_namespace = '" . $this->pageInfo['page']->getNamespace() . "'
310
                  AND log_title = '$title' AND log_timestamp > 1
311
                  AND log_type IN ('delete', 'move', 'protect', 'stable')";
312
        $events = $this->conn->query($query)->fetchAll();
313
314
        foreach ($events as $event) {
315
            $time = strtotime($event['timestamp']);
316
            $year = date('Y', $time);
317
            if (isset($this->pageInfo['year_count'][$year])) {
318
                $yearEvents = $this->pageInfo['year_count'][$year]['events'];
319
320
                // Convert log type value to i18n key
321
                switch ($event['log_type']) {
322
                    case 'protect':
323
                        $action = 'protections';
324
                        break;
325
                    case 'delete':
326
                        $action = 'deletions';
327
                        break;
328
                    case 'move':
329
                        $action = 'moves';
330
                        break;
331
                    // count pending-changes protections along with normal protections
332
                    case 'stable':
333
                        $action = 'protections';
334
                        break;
335
                }
336
337
                if (empty($yearEvents[$action])) {
338
                    $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...
339
                } else {
340
                    $yearEvents[$action]++;
341
                }
342
343
                $this->pageInfo['year_count'][$year]['events'] = $yearEvents;
344
            }
345
        }
346
    }
347
348
    /**
349
     * Parse the revision history. This also sets some $this->pageInfo vars
350
     *   like 'firstEdit' and 'lastEdit'
351
     * @param Page $page Page to parse
352
     * @return array Associative "master" array of metadata about the page
353
     */
354
    private function parseHistory(Page $page)
355
    {
356
        $revStmt = $page->getRevisionsStmt();
357
        $revCount = 0;
358
359
        /** @var string[] Master array containing all the data we need */
360
        $data = [
361
            'general' => [
362
                'max_add' => null, // Edit
363
                'max_del' => null, // Edit
364
                'editor_count' => 0,
365
                'anon_count' => 0,
366
                'minor_count' => 0,
367
                'count_history' => ['day' => 0, 'week' => 0, 'month' => 0, 'year' => 0],
368
                'current_size' => null,
369
                'textshares' => [],
370
                'textshare_total' => 0,
371
                'automated_count' => 0,
372
                'revert_count' => 0,
373
                'added' => 0,
374
            ],
375
            'max_edits_per_month' => 0, // for bar chart in "Month counts" section
376
            'editors' => [],
377
            'anons' => [],
378
            'year_count' => [],
379
            'tools' => [],
380
        ];
381
382
        /** @var Edit|null */
383
        $firstEdit = null;
384
385
        /** @var Edit|null The previous edit, used to discount content that was reverted */
386
        $prevEdit = null;
387
388
        /**
389
         * The edit previously deemed as having the maximum amount of content added.
390
         * This is used to discount content that was reverted.
391
         * @var Edit|null
392
        */
393
        $prevMaxAddEdit = null;
394
395
        /**
396
         * The edit previously deemed as having the maximum amount of content deleted.
397
         * This is used to discount content that was reverted
398
         * @var Edit|null
399
         */
400
        $prevMaxDelEdit = null;
401
402
        /** @var Time|null Time of first revision, used as a comparison for month counts */
403
        $firstEditMonth = null;
404
405
        while ($rev = $revStmt->fetch()) {
406
            $edit = new Edit($this->pageInfo['page'], $rev);
407
            // $edit->setContainer($this->container);
1 ignored issue
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
408
409
            // Some shorthands
410
            $editYear = $edit->getYear();
411
            $editMonth = $edit->getMonth();
412
            $editTimestamp = $edit->getTimestamp();
413
414
            // Don't return actual edit size if last revision had a length of null.
415
            // This happens when the edit follows other edits that were revision-deleted.
416
            // See T148857 for more information.
417
            // @TODO: Remove once T101631 is resolved
418
            if ($prevEdit && $prevEdit->getLength() === null) {
419
                $editSize = 0;
420
            } else {
421
                $editSize = $edit->getSize();
422
            }
423
424
            if ($revCount === 0) {
425
                $firstEdit = $edit;
426
                $firstEditMonth = mktime(0, 0, 0, (int) $firstEdit->getMonth(), 1, $firstEdit->getYear());
427
            }
428
429
            $username = $edit->getUser()->getUsername();
430
431
            // Sometimes, with old revisions (2001 era), the revisions from 2002 come before 2001
432
            if ($editTimestamp < $firstEdit->getTimestamp()) {
433
                $firstEdit = $edit;
434
            }
435
436
            // Fill in the blank arrays for the year and 12 months
437
            if (!isset($data['year_count'][$editYear])) {
438
                $data['year_count'][$editYear] = [
439
                    'all' => 0,
440
                    'minor' => 0,
441
                    'anon' => 0,
442
                    'automated' => 0,
443
                    'size' => 0, // keep track of the size by the end of the year
444
                    'events' => [],
445
                    'months' => [],
446
                ];
447
448
                for ($i = 1; $i <= 12; $i++) {
449
                    $timeObj = mktime(0, 0, 0, $i, 1, $editYear);
450
451
                    // don't show zeros for months before the first edit or after the current month
452
                    if ($timeObj < $firstEditMonth || $timeObj > strtotime('last day of this month')) {
453
                        continue;
454
                    }
455
456
                    $data['year_count'][$editYear]['months'][sprintf('%02d', $i)] = [
457
                        'all' => 0,
458
                        'minor' => 0,
459
                        'anon' => 0,
460
                        'automated' => 0,
461
                    ];
462
                }
463
            }
464
465
            // Increment year and month counts for all edits
466
            $data['year_count'][$editYear]['all']++;
467
            $data['year_count'][$editYear]['months'][$editMonth]['all']++;
468
            // This will ultimately be the size of the page by the end of the year
469
            $data['year_count'][$editYear]['size'] = $edit->getLength();
470
471
            // Keep track of which month had the most edits
472
            $editsThisMonth = $data['year_count'][$editYear]['months'][$editMonth]['all'];
473
            if ($editsThisMonth > $data['max_edits_per_month']) {
474
                $data['max_edits_per_month'] = $editsThisMonth;
475
            }
476
477
            // Initialize various user stats
478
            if (!isset($data['editors'][$username])) {
479
                $data['general']['editor_count']++;
480
                $data['editors'][$username] = [
481
                    'all' => 0,
482
                    'minor' => 0,
483
                    'minor_percentage' => 0,
484
                    'first' => $editTimestamp,
485
                    'first_id' => $edit->getId(),
486
                    'last' => null,
487
                    'atbe' => null,
488
                    'added' => 0,
489
                    'sizes' => [],
490
                ];
491
            }
492
493
            // Increment user counts
494
            $data['editors'][$username]['all']++;
495
            $data['editors'][$username]['last'] = $editTimestamp;
496
            $data['editors'][$username]['last_id'] = $edit->getId();
497
498
            // Store number of KB added with this edit
499
            $data['editors'][$username]['sizes'][] = $edit->getLength() / 1024;
500
501
            // Check if it was a revert
502
            if ($this->aeh->isRevert($edit->getComment())) {
503
                $data['general']['revert_count']++;
504
505
                // Since this was a revert, we don't want to treat the previous
506
                //   edit as legit content addition or removal
507
                if ($prevEdit && $prevEdit->getSize() > 0) {
508
                    $data['general']['added'] -= $prevEdit->getSize();
509
                }
510
511
                // @TODO: Test this against an edit war (use your sandbox)
512
                // Also remove as max added or deleted, if applicable
513
                if ($prevEdit->getId() === $data['general']['max_add']->getId()) {
514
                    $data['general']['max_add'] = $prevMaxAddEdit;
515
                    $prevMaxAddEdit = $prevEdit; // in the event of edit wars
516
                } elseif ($prevEdit->getId() === $data['general']['max_del']->getId()) {
517
                    $data['general']['max_del'] = $prevMaxDelEdit;
518
                    $prevMaxDelEdit = $prevEdit; // in the event of edit wars
519
                }
520
            } else {
521
                // Edit was not a revert, so treat size > 0 as content added
522
                if ($editSize > 0) {
523
                    $data['general']['added'] += $editSize;
524
                    $data['editors'][$username]['added'] += $editSize;
525
526
                    // Keep track of edit with max addition
527
                    if (!$data['general']['max_add'] || $editSize > $data['general']['max_add']->getSize()) {
528
                        // Keep track of old max_add in case we find out the next $edit was reverted
529
                        //   (and was also a max edit), in which case we'll want to use this one ($edit)
530
                        $prevMaxAddEdit = $data['general']['max_add'];
531
532
                        $data['general']['max_add'] = $edit;
533
                    }
534
                } elseif ($editSize < 0 && (
535
                    !$data['general']['max_del'] || $editSize < $data['general']['max_del']->getSize()
536
                )) {
537
                    $data['general']['max_del'] = $edit;
538
                }
539
            }
540
541
            // If anonymous, increase counts
542
            if ($edit->isAnon()) {
543
                $data['general']['anon_count']++;
544
                $data['year_count'][$editYear]['anon']++;
545
                $data['year_count'][$editYear]['months'][$editMonth]['anon']++;
546
            }
547
548
            // If minor edit, increase counts
549
            if ($edit->isMinor()) {
550
                $data['general']['minor_count']++;
551
                $data['year_count'][$editYear]['minor']++;
552
                $data['year_count'][$editYear]['months'][$editMonth]['minor']++;
553
554
                // Increment minor counts for this user
555
                $data['editors'][$username]['minor']++;
556
            }
557
558
            $automatedTool = $this->aeh->getTool($edit->getComment());
559
            if ($automatedTool) {
560
                $data['general']['automated_count']++;
561
                $data['year_count'][$editYear]['automated']++;
562
                $data['year_count'][$editYear]['months'][$editMonth]['automated']++;
563
564
                if (!isset($data['tools'][$automatedTool])) {
565
                    $data['tools'][$automatedTool] = [
566
                        'count' => 1,
567
                        'link' => $this->aeh->getTools()[$automatedTool]['link'],
568
                    ];
569
                } else {
570
                    $data['tools'][$automatedTool]['count']++;
571
                }
572
            }
573
574
            // Increment "edits per <time>" counts
575
            if ($editTimestamp > new DateTime('-1 day')) {
576
                $data['general']['count_history']['day']++;
577
            }
578
            if ($editTimestamp > new DateTime('-1 week')) {
579
                $data['general']['count_history']['week']++;
580
            }
581
            if ($editTimestamp > new DateTime('-1 month')) {
582
                $data['general']['count_history']['month']++;
583
            }
584
            if ($editTimestamp > new DateTime('-1 year')) {
585
                $data['general']['count_history']['year']++;
586
            }
587
588
            $revCount++;
589
            $prevEdit = $edit;
590
            $lastEdit = $edit;
591
        }
592
593
        // add percentages
594
        $data['general']['minor_percentage'] = round(
595
            ($data['general']['minor_count'] / $revCount) * 100,
596
            1
597
        );
598
        $data['general']['anon_percentage'] = round(
599
            ($data['general']['anon_count'] / $revCount) * 100,
600
            1
601
        );
602
603
        // other general statistics
604
        $dateFirst = $firstEdit->getTimestamp();
605
        $dateLast = $lastEdit->getTimestamp();
0 ignored issues
show
Bug introduced by
The variable $lastEdit 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...
606
        $data['general']['datetime_first_edit'] = $dateFirst;
607
        $data['general']['datetime_last_edit'] = $dateLast;
608
        $interval = date_diff($dateLast, $dateFirst, true);
609
610
        $data['totaldays'] = $interval->format('%a');
611
        $data['general']['average_days_per_edit'] = round($data['totaldays'] / $revCount, 1);
612
        $editsPerDay = $data['totaldays']
613
            ? $revCount / ($data['totaldays'] / (365 / 12 / 24))
614
            : 0;
615
        $data['general']['edits_per_day'] = round($editsPerDay, 1);
616
        $editsPerMonth = $data['totaldays']
617
            ? $revCount / ($data['totaldays'] / (365 / 12))
618
            : 0;
619
        $data['general']['edits_per_month'] = round($editsPerMonth, 1);
620
        $editsPerYear = $data['totaldays']
621
            ? $revCount / ($data['totaldays'] / 365)
622
            : 0;
623
        $data['general']['edits_per_year'] = round($editsPerYear, 1);
624
        $data['general']['edits_per_editor'] = round($revCount / count($data['editors']), 1);
625
626
        $data['firstEdit'] = $firstEdit;
627
        $data['lastEdit'] = $lastEdit;
628
629
        // Various sorts
630
        arsort($data['editors']);
631
        arsort($data['tools']);
632
        ksort($data['year_count']);
633
634
        return $data;
635
    }
636
}
637