Completed
Push — master ( 9fe0d2...0e0f1f )
by Sam
03:08
created

ArticleInfoController::getLinksAndRedirects()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 33
c 0
b 0
f 0
rs 8.8571
cc 2
eloc 17
nc 2
nop 0
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\LabsHelper;
10
use AppBundle\Helper\PageviewsHelper;
11
use Doctrine\DBAL\Connection;
12
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
13
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
14
use Symfony\Component\HttpFoundation\Request;
15
use Symfony\Component\DependencyInjection\ContainerInterface;
16
use Symfony\Component\HttpFoundation\Response;
17
use Xtools\ProjectRepository;
18
use Xtools\Page;
19
use Xtools\PagesRepository;
20
use Xtools\Edit;
21
22
/**
23
 * This controller serves the search form and results for the ArticleInfo tool
24
 */
25
class ArticleInfoController extends Controller
26
{
27
    /** @var LabsHelper The Labs helper object. */
28
    private $lh;
29
    /** @var mixed[] Information about the page in question. */
30
    private $pageInfo;
31
    /** @var Edit[] All edits of the page. */
32
    private $pageHistory;
33
    /** @var string The fully-qualified name of the revision table. */
34
    private $revisionTable;
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->lh = $this->get('app.labs_helper');
67
        $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...
68
        $this->ph = $this->get('app.pageviews_helper');
69
        $this->aeh = $this->get('app.automated_edits_helper');
70
    }
71
72
    /**
73
     * The search form.
74
     * @Route("/articleinfo", name="articleinfo")
75
     * @Route("/articleinfo", name="articleInfo")
76
     * @Route("/articleinfo/", name="articleInfoSlash")
77
     * @Route("/articleinfo/index.php", name="articleInfoIndexPhp")
78
     * @Route("/articleinfo/{project}", name="ArticleInfoProject")
79
     * @param Request $request The HTTP request.
80
     * @return Response
81
     */
82
    public function indexAction(Request $request)
83
    {
84
        $projectQuery = $request->query->get('project');
85
        $article = $request->query->get('article');
86
87
        if ($projectQuery != '' && $article != '') {
88
            return $this->redirectToRoute('ArticleInfoResult', [ 'project'=>$projectQuery, 'article' => $article ]);
89
        } elseif ($article != '') {
90
            return $this->redirectToRoute('ArticleInfoProject', [ 'project'=>$projectQuery ]);
91
        }
92
93
        if ($projectQuery == '') {
94
            $projectQuery = $this->container->getParameter('default_project');
95
        }
96
97
        $project = ProjectRepository::getProject($projectQuery, $this->container);
98
99
        return $this->render('articleInfo/index.html.twig', [
100
            'xtPage' => 'articleinfo',
101
            'xtPageTitle' => 'tool-articleinfo',
102
            'xtSubtitle' => 'tool-articleinfo-desc',
103
            'project' => $project,
104
        ]);
105
    }
106
107
    /**
108
     * Display the results.
109
     * @Route("/articleinfo/{project}/{article}", name="ArticleInfoResult", requirements={"article"=".+"})
110
     * @param Request $request The HTTP request.
111
     * @return Response
112
     */
113
    public function resultAction(Request $request)
114
    {
115
        $projectQuery = $request->attributes->get('project');
116
        $project = ProjectRepository::getProject($projectQuery, $this->container);
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
        $projectUrl = $project->getUrl();
122
        $dbName = $project->getDatabaseName();
123
124
        $pageQuery = $request->attributes->get('article');
125
        $page = new Page($project, $pageQuery);
126
        $pageRepo = new PagesRepository();
127
        $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...
128
        $page->setRepository($pageRepo);
129
130
        if (!$page->exists()) {
131
            $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...
132
            return $this->redirectToRoute('articleInfo');
133
        }
134
135
        $this->revisionTable = $project->getRepository()->getTableName(
136
            $project->getDatabaseName(),
137
            'revision'
138
        );
139
140
        // TODO: throw error if $basicInfo['missing'] is set
1 ignored issue
show
Unused Code Comprehensibility introduced by
36% 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...
141
142
        $this->pageInfo = [
0 ignored issues
show
Documentation Bug introduced by
It seems like array('project' => $proj...=> $project->getLang()) of type array<string,object<Xtoo...bject<string>|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...
143
            'project' => $project,
144
            'projectUrl' => $projectUrl,
145
            'page' => $page,
146
            'dbName' => $dbName,
147
            'lang' => $project->getLang(),
148
        ];
149
150
        if ($page->getWikidataId()) {
151
            $this->pageInfo['numWikidataItems'] = $this->getNumWikidataItems();
152
        }
153
154
        // TODO: Adapted from legacy code; may be used to indicate how many dead ext links there are
155
        // 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...
156
        //     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...
157
        //         $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...
158
        //     }
159
        // }
160
161
        $this->pageHistory = $page->getRevisions();
162
        $this->pageInfo['firstEdit'] = new Edit($this->pageInfo['page'], $this->pageHistory[0]);
163
        $this->pageInfo['lastEdit'] = new Edit(
164
            $this->pageInfo['page'],
165
            $this->pageHistory[$page->getNumRevisions() - 1]
166
        );
167
168
        // NOTE: bots are fetched first in case we want to restrict some stats to humans editors only
169
        $this->pageInfo['bots'] = $this->getBotData();
170
        $this->pageInfo['general']['bot_count'] = count($this->pageInfo['bots']);
171
172
        $this->pageInfo = array_merge($this->pageInfo, $this->parseHistory());
0 ignored issues
show
Documentation Bug introduced by
It seems like array_merge($this->pageI... $this->parseHistory()) of type array<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...
173
        $this->pageInfo['general']['top_ten_count'] = $this->getTopTenCount();
174
        $this->pageInfo['general']['top_ten_percentage'] = round(
175
            ($this->pageInfo['general']['top_ten_count'] / $page->getNumRevisions()) * 100,
176
            1
177
        );
178
        $this->pageInfo = array_merge($this->pageInfo, $this->getLinksAndRedirects());
0 ignored issues
show
Documentation Bug introduced by
It seems like array_merge($this->pageI...getLinksAndRedirects()) 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...
179
        $this->pageInfo['general']['pageviews_offset'] = 60;
180
        $this->pageInfo['general']['pageviews'] = $this->ph->sumLastDays(
181
            $this->pageInfo['project']->getDomain(),
182
            $this->pageInfo['page']->getTitle(),
183
            $this->pageInfo['general']['pageviews_offset']
184
        );
185
        $api = $this->get('app.api_helper');
186
        $assessments = $api->getPageAssessments($projectQuery, $pageQuery);
187
        if ($assessments) {
188
            $this->pageInfo['assessments'] = $assessments;
189
        }
190
        $this->setLogsEvents();
191
192
        $bugs = array_merge($this->getCheckWikiErrors(), $this->getWikidataErrors());
193
        if (!empty($bugs)) {
194
            $this->pageInfo['bugs'] = $bugs;
195
        }
196
197
        $this->pageInfo['xtPage'] = 'articleinfo';
198
        $this->pageInfo['xtTitle'] = $this->pageInfo['page']->getTitle();
199
200
        return $this->render("articleInfo/result.html.twig", $this->pageInfo);
201
    }
202
203
    /**
204
     * Get number of wikidata items (not just languages of sister projects)
205
     * @return integer Number of items
206
     */
207
    private function getNumWikidataItems()
208
    {
209
        $query = "SELECT COUNT(*) AS count
210
                  FROM wikidatawiki_p.wb_items_per_site
211
                  WHERE ips_item_id = ". ltrim($this->pageInfo['page']->getWikidataId(), 'Q');
212
        $res = $this->conn->query($query)->fetchAll();
213
        return $res[0]['count'];
214
    }
215
216
    /**
217
     * Get info about bots that edited the page
218
     * This also sets $this->pageInfo['bot_revision_count'] and $this->pageInfo['bot_percentage']
219
     * @return array Associative array containing the bot's username, edit count to the page
220
     *               and whether or not they are currently a bot
221
     */
222
    private function getBotData()
223
    {
224
        $userGroupsTable = $this->lh->getTable('user_groups', $this->pageInfo['dbName']);
225
        $userFromerGroupsTable = $this->lh->getTable('user_former_groups', $this->pageInfo['dbName']);
226
        $query = "SELECT COUNT(rev_user_text) AS count, rev_user_text AS username, ug_group AS current
227
                  FROM $this->revisionTable
228
                  LEFT JOIN $userGroupsTable ON rev_user = ug_user
229
                  LEFT JOIN $userFromerGroupsTable ON rev_user = ufg_user
230
                  WHERE rev_page = " . $this->pageInfo['page']->getId() . " AND (ug_group = 'bot' OR ufg_group = 'bot')
231
                  GROUP BY rev_user_text";
232
        $res = $this->conn->query($query)->fetchAll();
233
234
        // Parse the botedits
235
        $bots = [];
236
        $sum = 0;
237
        foreach ($res as $bot) {
238
            $bots[$bot['username']] = [
239
                'count' => (int) $bot['count'],
240
                'current' => $bot['current'] === 'bot'
241
            ];
242
            $sum += $bot['count'];
243
        }
244
245
        uasort($bots, function ($a, $b) {
246
            return $b['count'] - $a['count'];
247
        });
248
249
        $this->pageInfo['general']['bot_revision_count'] = $sum;
250
        $this->pageInfo['general']['bot_percentage'] = round(
251
            ($sum / $this->pageInfo['page']->getNumRevisions()) * 100,
252
            1
253
        );
254
255
        return $bots;
256
    }
257
258
    /**
259
     * Get the number of edits made to the page by the top 10% of editors
260
     * This is ran *after* parseHistory() since we need the grand totals first.
261
     * Various stats are also set for each editor in $this->pageInfo['editors']
262
     *   and top ten editors are stored in $this->pageInfo['general']['top_ten']
263
     *   to be used in the charts
264
     * @return integer Number of edits
265
     */
266
    private function getTopTenCount()
267
    {
268
        $topTenCount = $counter = 0;
269
        $topTenEditors = [];
270
271
        foreach ($this->pageInfo['editors'] as $editor => $info) {
272
            // Count how many users are in the top 10% by number of edits
273
            if ($counter < 10) {
274
                $topTenCount += $info['all'];
275
                $counter++;
276
277
                // To be used in the Top Ten charts
278
                $topTenEditors[] = [
279
                    'label' => $editor,
280
                    'value' => $info['all'],
281
                    'percentage' => (
282
                        100 * ($info['all'] / $this->pageInfo['page']->getNumRevisions())
283
                    )
284
                ];
285
            }
286
287
            // Compute the percentage of minor edits the user made
288
            $this->pageInfo['editors'][$editor]['minor_percentage'] = $info['all']
289
                ? ($info['minor'] / $info['all']) * 100
290
                : 0;
291
292
            if ($info['all'] > 1) {
293
                // Number of seconds between first and last edit
294
                $secs = intval(strtotime($info['last']) - strtotime($info['first']) / $info['all']);
295
296
                // Average time between edits (in days)
297
                $this->pageInfo['editors'][$editor]['atbe'] = $secs / ( 60 * 60 * 24 );
298
            }
299
300
            if (count($info['sizes'])) {
301
                // Average Total KB divided by number of stored sizes (user's edit count to this page)
302
                $this->pageInfo['editors'][$editor]['size'] = array_sum($info['sizes']) / count($info['sizes']);
303
            } else {
304
                $this->pageInfo['editors'][$editor]['size'] = 0;
305
            }
306
        }
307
308
        $this->pageInfo['topTenEditors'] = $topTenEditors;
309
310
        // First sort editors array by the amount of text they added
311
        $topTenEditorsByAdded = $this->pageInfo['editors'];
312
        uasort($topTenEditorsByAdded, function ($a, $b) {
313
            if ($a['added'] === $b['added']) {
314
                return 0;
315
            }
316
            return $a['added'] > $b['added'] ? -1 : 1;
317
        });
318
319
        // Then build a new array of top 10 editors by added text,
320
        //   in the data structure needed for the chart
321
        $this->pageInfo['topTenEditorsByAdded'] = array_map(function ($editor) {
322
            $added = $this->pageInfo['editors'][$editor]['added'];
323
            return [
324
                'label' => $editor,
325
                'value' => $added,
326
                'percentage' => (
327
                    100 * ($added / $this->pageInfo['general']['added'])
328
                )
329
            ];
330
        }, array_keys(array_slice($topTenEditorsByAdded, 0, 10)));
331
332
        return $topTenCount;
333
    }
334
335
    /**
336
     * Get number of in and outgoing links and redirects to the page
337
     * @return array Associative array containing counts
338
     */
339
    private function getLinksAndRedirects()
340
    {
341
        $pageId = $this->pageInfo['page']->getId();
342
        $namespace = $this->pageInfo['page']->getNamespace();
343
        $title = str_replace(' ', '_', $this->pageInfo['page']->getTitle());
344
        $externalLinksTable = $this->lh->getTable('externallinks', $this->pageInfo['dbName']);
345
        $pageLinksTable = $this->lh->getTable('pagelinks', $this->pageInfo['dbName']);
346
        $redirectTable = $this->lh->getTable('redirect', $this->pageInfo['dbName']);
347
348
        // FIXME: Probably need to make the $title mysql-safe or whatever
349
        $query = "SELECT COUNT(*) AS value, 'links_ext' AS type
350
                  FROM $externalLinksTable WHERE el_from = $pageId
351
                  UNION
352
                  SELECT COUNT(*) AS value, 'links_out' AS type
353
                  FROM $pageLinksTable WHERE pl_from = $pageId
354
                  UNION
355
                  SELECT COUNT(*) AS value, 'links_in' AS type
356
                  FROM $pageLinksTable WHERE pl_namespace = $namespace AND pl_title = \"$title\"
357
                  UNION
358
                  SELECT COUNT(*) AS value, 'redirects' AS type
359
                  FROM $redirectTable WHERE rd_namespace = $namespace AND rd_title = \"$title\"";
360
361
        $res = $this->conn->query($query)->fetchAll();
362
363
        $data = [];
364
365
        // Transform to associative array by 'type'
366
        foreach ($res as $row) {
367
            $data[$row['type'] . '_count'] = $row['value'];
368
        }
369
370
        return $data;
371
    }
372
373
    /**
374
     * Query for log events during each year of the article's history,
375
     *   and set the results in $this->pageInfo['year_count']
376
     */
377
    private function setLogsEvents()
378
    {
379
        $loggingTable = $this->lh->getTable('logging', $this->pageInfo['dbName'], 'logindex');
380
        $title = str_replace(' ', '_', $this->pageInfo['page']->getTitle());
381
        $query = "SELECT log_action, log_type, log_timestamp AS timestamp
382
                  FROM $loggingTable
383
                  WHERE log_namespace = '" . $this->pageInfo['page']->getNamespace() . "'
384
                  AND log_title = '$title' AND log_timestamp > 1
385
                  AND log_type IN ('delete', 'move', 'protect', 'stable')";
386
        $events = $this->conn->query($query)->fetchAll();
387
388
        foreach ($events as $event) {
389
            $time = strtotime($event['timestamp']);
390
            $year = date('Y', $time);
391
            if (isset($this->pageInfo['year_count'][$year])) {
392
                $yearEvents = $this->pageInfo['year_count'][$year]['events'];
393
394
                // Convert log type value to i18n key
395
                switch ($event['log_type']) {
396
                    case 'protect':
397
                        $action = 'protections';
398
                        break;
399
                    case 'delete':
400
                        $action = 'deletions';
401
                        break;
402
                    case 'move':
403
                        $action = 'moves';
404
                        break;
405
                    // count pending-changes protections along with normal protections
406
                    case 'stable':
407
                        $action = 'protections';
408
                        break;
409
                }
410
411
                if (empty($yearEvents[$action])) {
412
                    $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...
413
                } else {
414
                    $yearEvents[$action]++;
415
                }
416
417
                $this->pageInfo['year_count'][$year]['events'] = $yearEvents;
418
            }
419
        }
420
    }
421
422
    /**
423
     * Get any CheckWiki errors
424
     * @return array Results from query
425
     */
426
    private function getCheckWikiErrors()
427
    {
428
        if ($this->pageInfo['page']->getNamespace() !== 0 || !$this->container->getParameter('app.is_labs')) {
429
            return [];
430
        }
431
        $title = $this->pageInfo['page']->getTitle(); // no underscores
432
        $dbName = preg_replace('/_p$/', '', $this->pageInfo['dbName']); // remove _p if present
433
434
        $query = "SELECT error, notice, found, name_trans AS name, prio, text_trans AS explanation
435
                  FROM s51080__checkwiki_p.cw_error a
436
                  JOIN s51080__checkwiki_p.cw_overview_errors b
437
                  WHERE a.project = b.project AND a.project = '$dbName'
438
                  AND a.title = '$title' AND a.error = b.id
439
                  AND b.done IS NULL";
440
441
        $conn = $this->container->get('doctrine')->getManager('toolsdb')->getConnection();
442
        $res = $conn->query($query)->fetchAll();
443
        return $res;
444
    }
445
446
    /**
447
     * Get basic wikidata on the page: label and description.
448
     * Reported as "bugs" if they are missing.
449
     * @return array Label and description, if present
450
     */
451
    private function getWikidataErrors()
452
    {
453
        if (empty($this->pageInfo['wikidataId'])) {
454
            return [];
455
        }
456
457
        $wikidataId = ltrim($this->pageInfo['wikidataId'], 'Q');
458
        $lang = $this->pageInfo['lang'];
459
460
        $query = "SELECT IF(term_type = 'label', 'label', 'description') AS term, term_text
461
                  FROM wikidatawiki_p.wb_entity_per_page
462
                  JOIN wikidatawiki_p.page ON epp_page_id = page_id
463
                  JOIN wikidatawiki_p.wb_terms ON term_entity_id = epp_entity_id
464
                    AND term_language = '$lang' AND term_type IN ('label', 'description')
465
                  WHERE epp_entity_id = $wikidataId
466
                  UNION
467
                  SELECT pl_title AS term, wb_terms.term_text
468
                  FROM wikidatawiki_p.pagelinks
469
                  JOIN wikidatawiki_p.wb_terms ON term_entity_id = SUBSTRING(pl_title, 2)
470
                    AND term_entity_type = (IF(SUBSTRING(pl_title, 1, 1) = 'Q', 'item', 'property'))
471
                    AND term_language = '$lang'
472
                    AND term_type = 'label'
473
                  WHERE pl_namespace IN (0,120 )
474
                  AND pl_from = (
475
                    SELECT page_id FROM page
476
                    WHERE page_namespace = 0 AND page_title = 'Q$wikidataId'
477
                  )";
478
479
        $conn = $this->container->get('doctrine')->getManager('replicas')->getConnection();
480
        $res = $conn->query($query)->fetchAll();
481
482
        $terms = array_map(function ($entry) {
483
            return $entry['term'];
484
        }, $res);
485
486
        $errors = [];
487
488 View Code Duplication
        if (!in_array('label', $terms)) {
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...
489
            $errors[] = [
490
                'prio' => 2,
491
                'name' => 'Wikidata',
492
                'notice' => "Label for language <em>$lang</em> is missing", // FIXME: i18n
493
                'explanation' => "See: <a target='_blank' " .
494
                    "href='//www.wikidata.org/wiki/Help:Label'>Help:Label</a>",
495
            ];
496
        }
497 View Code Duplication
        if (!in_array('description', $terms)) {
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...
498
            $errors[] = [
499
                'prio' => 3,
500
                'name' => 'Wikidata',
501
                'notice' => "Description for language <em>$lang</em> is missing", // FIXME: i18n
502
                'explanation' => "See: <a target='_blank' " .
503
                    "href='//www.wikidata.org/wiki/Help:Description'>Help:Description</a>",
504
            ];
505
        }
506
507
        return $errors;
508
    }
509
510
    /**
511
     * Get the size of the diff.
512
     * @param  int $revIndex The index of the revision within $this->pageHistory
513
     * @return int Size of the diff
514
     */
515
    private function getDiffSize($revIndex)
516
    {
517
        $rev = $this->pageHistory[$revIndex];
518
519
        if ($revIndex === 0) {
520
            return $rev['length'];
521
        }
522
523
        $lastRev = $this->pageHistory[$revIndex - 1];
524
525
        // TODO: Remove once T101631 is resolved
526
        // Treat as zero change in size if length of previous edit is missing
527
        if ($lastRev['length'] === null) {
528
            return 0;
529
        } else {
530
            return $rev['length'] - $lastRev['length'];
531
        }
532
    }
533
534
    /**
535
     * Parse the revision history, which should be at $this->pageHistory
536
     * @return array Associative "master" array of metadata about the page
537
     */
538
    private function parseHistory()
539
    {
540
        $revisionCount = $this->pageInfo['page']->getNumRevisions();
541
        if ($revisionCount == 0) {
542
            // $this->error = "no records";
1 ignored issue
show
Unused Code Comprehensibility introduced by
45% 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...
543
            return;
544
        }
545
546
        $firstEdit = $this->pageInfo['firstEdit'];
547
548
        // Get UNIX timestamp of the first day of the month of the first edit
549
        // This is used as a comparison when building our array of per-month stats
550
        $firstEditMonth = mktime(0, 0, 0, (int) $firstEdit->getMonth(), 1, $firstEdit->getYear());
551
552
        $lastEdit = $this->pageInfo['lastEdit'];
553
        $secondLastEdit = $revisionCount === 1 ? $lastEdit : $this->pageHistory[ $revisionCount - 2 ];
0 ignored issues
show
Unused Code introduced by
$secondLastEdit is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
554
555
        // Now we can start our master array. This one will be HUGE!
556
        $data = [
557
            'general' => [
558
                'max_add' => $firstEdit,
559
                'max_del' => $firstEdit,
560
                'editor_count' => 0,
561
                'anon_count' => 0,
562
                'minor_count' => 0,
563
                'count_history' => ['day' => 0, 'week' => 0, 'month' => 0, 'year' => 0],
564
                'current_size' => $this->pageHistory[$revisionCount-1]['length'],
565
                'textshares' => [],
566
                'textshare_total' => 0,
567
                'automated_count' => 0,
568
                'revert_count' => 0,
569
                'added' => 0,
570
            ],
571
            'max_edits_per_month' => 0, // for bar chart in "Month counts" section
572
            'editors' => [],
573
            'anons' => [],
574
            'year_count' => [],
575
            'tools' => [],
576
        ];
577
578
        // restore existing general data
579
        $data['general'] = array_merge($data['general'], $this->pageInfo['general']);
580
581
        // And now comes the logic for filling said master array
582
        foreach ($this->pageHistory as $i => $rev) {
583
            $edit = new Edit($this->pageInfo['page'], $rev);
0 ignored issues
show
Documentation introduced by
$rev is of type object<Xtools\Edit>, but the function expects a array<integer,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...
584
            $diffSize = $this->getDiffSize($i);
0 ignored issues
show
Unused Code introduced by
$diffSize is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
585
            $username = htmlspecialchars($rev['username']);
586
587
            // Sometimes, with old revisions (2001 era), the revisions from 2002 come before 2001
588
            if ($edit->getTimestamp() < $firstEdit->getTimestamp()) {
589
                $firstEdit = $edit;
590
            }
591
592
            // Fill in the blank arrays for the year and 12 months
593
            if (!isset($data['year_count'][$edit->getYear()])) {
594
                $data['year_count'][$edit->getYear()] = [
595
                    'all' => 0,
596
                    'minor' => 0,
597
                    'anon' => 0,
598
                    'automated' => 0,
599
                    'size' => 0, // keep track of the size by the end of the year
600
                    'events' => [],
601
                    'months' => [],
602
                ];
603
604
                for ($i = 1; $i <= 12; $i++) {
605
                    $timeObj = mktime(0, 0, 0, $i, 1, $edit->getYear());
606
607
                    // don't show zeros for months before the first edit or after the current month
608
                    if ($timeObj < $firstEditMonth || $timeObj > strtotime('last day of this month')) {
609
                        continue;
610
                    }
611
612
                    $data['year_count'][$edit->getYear()]['months'][sprintf('%02d', $i)] = [
613
                        'all' => 0,
614
                        'minor' => 0,
615
                        'anon' => 0,
616
                        'automated' => 0,
617
                    ];
618
                }
619
            }
620
621
            // Increment year and month counts for all edits
622
            $data['year_count'][$edit->getYear()]['all']++;
623
            $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['all']++;
624
            $data['year_count'][$edit->getYear()]['size'] = (int) $rev['length'];
625
626
            $editsThisMonth = $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['all'];
627
            if ($editsThisMonth > $data['max_edits_per_month']) {
628
                $data['max_edits_per_month'] = $editsThisMonth;
629
            }
630
631
            // Fill in various user stats
632
            if (!isset($data['editors'][$username])) {
633
                $data['general']['editor_count']++;
634
                $data['editors'][$username] = [
635
                    'all' => 0,
636
                    'minor' => 0,
637
                    'minor_percentage' => 0,
638
                    'first' => date('Y-m-d, H:i', strtotime($rev['timestamp'])),
639
                    'first_id' => $rev['id'],
640
                    'last' => null,
641
                    'atbe' => null,
642
                    'added' => 0,
643
                    'sizes' => [],
644
                    'urlencoded' => rawurlencode($rev['username']),
645
                ];
646
            }
647
648
            // Increment user counts
649
            $data['editors'][$username]['all']++;
650
            $data['editors'][$username]['last'] = date('Y-m-d, H:i', strtotime($rev['timestamp']));
651
            $data['editors'][$username]['last_id'] = $rev['id'];
652
653
            // Store number of KB added with this edit
654
            $data['editors'][$username]['sizes'][] = $rev['length'] / 1024;
655
656
            // check if it was a revert
657
            if ($this->aeh->isRevert($rev['comment'])) {
658
                $data['general']['revert_count']++;
659
            } else {
660
                // edit was NOT a revert
661
662
                if ($edit->getSize() > 0) {
663
                    $data['general']['added'] += $edit->getSize();
664
                    $data['editors'][$username]['added'] += $edit->getSize();
665
                }
666
667
                // determine if the next revision was a revert
668
                $nextRevision = isset($this->pageHistory[$i + 1]) ? $this->pageHistory[$i + 1] : null;
669
                $nextRevisionIsRevert = $nextRevision &&
670
                    $this->getDiffSize($i + 1) === -$edit->getSize() &&
671
                    $this->aeh->isRevert($nextRevision['comment']);
672
673
                // don't count this edit as content removal if the next edit reverted it
674
                if (!$nextRevisionIsRevert && $edit->getSize() < $data['general']['max_del']->getSize()) {
675
                    $data['general']['max_del'] = $edit;
676
                }
677
678
                // FIXME: possibly remove this
679
                if ($edit->getLength() > 0) {
680
                    // keep track of added content
681
                    $data['general']['textshare_total'] += $edit->getLength();
682
                    if (!isset($data['textshares'][$username]['all'])) {
683
                        $data['textshares'][$username]['all'] = 0;
684
                    }
685
                    $data['textshares'][$username]['all'] += $edit->getLength();
686
                }
687
688
                if ($edit->getSize() > $data['general']['max_add']->getSize()) {
689
                    $data['general']['max_add'] = $edit;
690
                }
691
            }
692
693
            if ($edit->isAnon()) {
694
                if (!isset($rev['rev_user']['anons'][$username])) {
695
                    $data['general']['anon_count']++;
696
                }
697
                // Anonymous, increase counts
698
                $data['anons'][] = $username;
699
                $data['year_count'][$edit->getYear()]['anon']++;
700
                $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['anon']++;
701
            }
702
703
            if ($edit->isMinor()) {
704
                // Logged in, increase counts
705
                $data['general']['minor_count']++;
706
                $data['year_count'][$edit->getYear()]['minor']++;
707
                $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['minor']++;
708
                $data['editors'][$username]['minor']++;
709
            }
710
711
            $automatedTool = $this->aeh->getTool($rev['comment']);
712
            if ($automatedTool) {
713
                $data['general']['automated_count']++;
714
                $data['year_count'][$edit->getYear()]['automated']++;
715
                $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['automated']++;
716
717
                if (!isset($data['tools'][$automatedTool])) {
718
                    $data['tools'][$automatedTool] = [
719
                        'count' => 1,
720
                        'link' => $this->aeh->getTools()[$automatedTool]['link'],
721
                    ];
722
                } else {
723
                    $data['tools'][$automatedTool]['count']++;
724
                }
725
            }
726
727
            // Increment "edits per <time>" counts
728 View Code Duplication
            if (strtotime($rev['timestamp']) > strtotime('-1 day')) {
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...
729
                $data['general']['count_history']['day']++;
730
            }
731 View Code Duplication
            if (strtotime($rev['timestamp']) > strtotime('-1 week')) {
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...
732
                $data['general']['count_history']['week']++;
733
            }
734 View Code Duplication
            if (strtotime($rev['timestamp']) > strtotime('-1 month')) {
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...
735
                $data['general']['count_history']['month']++;
736
            }
737 View Code Duplication
            if (strtotime($rev['timestamp']) > strtotime('-1 year')) {
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...
738
                $data['general']['count_history']['year']++;
739
            }
740
        }
741
742
        // add percentages
743
        $data['general']['minor_percentage'] = round(
744
            ($data['general']['minor_count'] / $revisionCount) * 100,
745
            1
746
        );
747
        $data['general']['anon_percentage'] = round(
748
            ($data['general']['anon_count'] / $revisionCount) * 100,
749
            1
750
        );
751
752
        // other general statistics
753
        $dateFirst = $firstEdit->getTimestamp();
754
        $dateLast = $lastEdit->getTimestamp();
755
        $data['general']['datetime_first_edit'] = $dateFirst;
756
        $data['general']['datetime_last_edit'] = $dateLast;
757
        $interval = date_diff($dateLast, $dateFirst, true);
758
759
        $data['totaldays'] = $interval->format('%a');
760
        $data['general']['average_days_per_edit'] = round($data['totaldays'] / $revisionCount, 1);
761
        $editsPerDay = $data['totaldays']
762
            ? $revisionCount / ($data['totaldays'] / (365 / 12 / 24))
763
            : 0;
764
        $data['general']['edits_per_day'] = round($editsPerDay, 1);
765
        $editsPerMonth = $data['totaldays']
766
            ? $revisionCount / ($data['totaldays'] / (365 / 12))
767
            : 0;
768
        $data['general']['edits_per_month'] = round($editsPerMonth, 1);
769
        $editsPerYear = $data['totaldays']
770
            ? $revisionCount / ($data['totaldays'] / 365)
771
            : 0;
772
        $data['general']['edits_per_year'] = round($editsPerYear, 1);
773
        $data['general']['edits_per_editor'] = round($revisionCount / count($data['editors']), 1);
774
775
        // If after processing max_del is positive, no edit actually removed text, so unset this value
776
        if ($data['general']['max_del']->getSize() > 0) {
777
            unset($data['general']['max_del']);
778
        }
779
780
        // Various sorts
781
        arsort($data['editors']);
782
        arsort($data['textshares']);
783
        arsort($data['tools']);
784
        ksort($data['year_count']);
785
786
        return $data;
787
    }
788
}
789