Completed
Push — master ( e62d58...4dd6bb )
by Sam
03:44
created

ArticleInfoController   F

Complexity

Total Complexity 78

Size/Duplication

Total Lines 756
Duplicated Lines 4.5 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
wmc 78
lcom 1
cbo 16
dl 34
loc 756
rs 1.6901
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getNumWikidataItems() 0 8 1
B getBotData() 0 35 2
C getTopTenCount() 0 68 8
B getLinksAndRedirects() 0 33 2
A setContainer() 0 5 1
A containerInitialized() 0 8 1
B indexAction() 0 24 5
B resultAction() 4 89 6
C setLogsEvents() 0 44 8
A getCheckWikiErrors() 0 19 3
A getWikidataErrors() 18 58 4
A getDiffSize() 0 18 3
F parseHistory() 12 250 34

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ArticleInfoController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ArticleInfoController, and based on these observations, apply Extract Interface, too.

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
     * Override method to call ArticleInfoController::containerInitialized() when container set.
44
     * @param ContainerInterface|null $container A ContainerInterface instance or null
45
     */
46
    public function setContainer(ContainerInterface $container = null)
47
    {
48
        parent::setContainer($container);
49
        $this->containerInitialized();
50
    }
51
52
    /**
53
     * Perform some operations after controller initialized and container set.
54
     */
55
    private function containerInitialized()
56
    {
57
        $this->lh = $this->get('app.labs_helper');
58
        $this->lh->checkEnabled('articleinfo');
59
        $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...
60
        $this->ph = $this->get('app.pageviews_helper');
61
        $this->aeh = $this->get('app.automated_edits_helper');
62
    }
63
64
    /**
65
     * The search form.
66
     * @Route("/articleinfo", name="articleinfo")
67
     * @Route("/articleinfo", name="articleInfo")
68
     * @Route("/articleinfo/", name="articleInfoSlash")
69
     * @Route("/articleinfo/index.php", name="articleInfoIndexPhp")
70
     * @Route("/articleinfo/{project}", name="ArticleInfoProject")
71
     * @param Request $request The HTTP request.
72
     * @return Response
73
     */
74
    public function indexAction(Request $request)
75
    {
76
        $projectQuery = $request->query->get('project');
77
        $article = $request->query->get('article');
78
79
        if ($projectQuery != '' && $article != '') {
80
            return $this->redirectToRoute('ArticleInfoResult', [ 'project'=>$projectQuery, 'article' => $article ]);
81
        } elseif ($article != '') {
82
            return $this->redirectToRoute('ArticleInfoProject', [ 'project'=>$projectQuery ]);
83
        }
84
85
        if ($projectQuery == '') {
86
            $projectQuery = $this->container->getParameter('default_project');
87
        }
88
89
        $project = ProjectRepository::getProject($projectQuery, $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...
90
91
        return $this->render('articleInfo/index.html.twig', [
92
            'xtPage' => 'articleinfo',
93
            'xtPageTitle' => 'tool-articleinfo',
94
            'xtSubtitle' => 'tool-articleinfo-desc',
95
            'project' => $project,
96
        ]);
97
    }
98
99
    /**
100
     * Display the results.
101
     * @Route("/articleinfo/{project}/{article}", name="ArticleInfoResult", requirements={"article"=".+"})
102
     * @param Request $request The HTTP request.
103
     * @return Response
104
     */
105
    public function resultAction(Request $request)
106
    {
107
        $projectQuery = $request->attributes->get('project');
108
        $project = ProjectRepository::getProject($projectQuery, $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...
109 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...
110
            $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...
111
            return $this->redirectToRoute('articleInfo');
112
        }
113
        $projectUrl = $project->getUrl();
114
        $dbName = $project->getDatabaseName();
115
116
        $pageQuery = $request->attributes->get('article');
117
        $page = new Page($project, $pageQuery);
118
        $pageRepo = new PagesRepository();
119
        $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...
120
        $page->setRepository($pageRepo);
121
122
        if (!$page->exists()) {
123
            $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...
124
            return $this->redirectToRoute('articleInfo');
125
        }
126
127
        $this->revisionTable = $project->getRepository()->getTableName(
128
            $project->getDatabaseName(),
129
            'revision'
130
        );
131
132
        // 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...
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...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...
135
            'project' => $project,
136
            'projectUrl' => $projectUrl,
137
            'page' => $page,
138
            'dbName' => $dbName,
139
            'lang' => $project->getLang(),
140
        ];
141
142
        if ($page->getWikidataId()) {
143
            $this->pageInfo['numWikidataItems'] = $this->getNumWikidataItems();
144
        }
145
146
        // TODO: Adapted from legacy code; may be used to indicate how many dead ext links there are
147
        // 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...
148
        //     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...
149
        //         $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...
150
        //     }
151
        // }
152
153
        $this->pageHistory = $page->getRevisions();
154
        $this->pageInfo['firstEdit'] = new Edit($this->pageInfo['page'], $this->pageHistory[0]);
155
        $this->pageInfo['lastEdit'] = new Edit(
156
            $this->pageInfo['page'],
157
            $this->pageHistory[$page->getNumRevisions() - 1]
158
        );
159
160
        // NOTE: bots are fetched first in case we want to restrict some stats to humans editors only
161
        $this->pageInfo['bots'] = $this->getBotData();
162
        $this->pageInfo['general']['bot_count'] = count($this->pageInfo['bots']);
163
164
        $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...
165
        $this->pageInfo['general']['top_ten_count'] = $this->getTopTenCount();
166
        $this->pageInfo['general']['top_ten_percentage'] = round(
167
            ($this->pageInfo['general']['top_ten_count'] / $page->getNumRevisions()) * 100,
168
            1
169
        );
170
        $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...
171
        $this->pageInfo['general']['pageviews_offset'] = 60;
172
        $this->pageInfo['general']['pageviews'] = $this->ph->sumLastDays(
173
            $this->pageInfo['project']->getDomain(),
174
            $this->pageInfo['page']->getTitle(),
175
            $this->pageInfo['general']['pageviews_offset']
176
        );
177
        $api = $this->get('app.api_helper');
178
        $assessments = $api->getPageAssessments($projectQuery, $pageQuery);
179
        if ($assessments) {
180
            $this->pageInfo['assessments'] = $assessments;
181
        }
182
        $this->setLogsEvents();
183
184
        $bugs = array_merge($this->getCheckWikiErrors(), $this->getWikidataErrors());
185
        if (!empty($bugs)) {
186
            $this->pageInfo['bugs'] = $bugs;
187
        }
188
189
        $this->pageInfo['xtPage'] = 'articleinfo';
190
        $this->pageInfo['xtTitle'] = $this->pageInfo['page']->getTitle();
191
192
        return $this->render("articleInfo/result.html.twig", $this->pageInfo);
193
    }
194
195
    /**
196
     * Get number of wikidata items (not just languages of sister projects)
197
     * @return integer Number of items
198
     */
199
    private function getNumWikidataItems()
200
    {
201
        $query = "SELECT COUNT(*) AS count
202
                  FROM wikidatawiki_p.wb_items_per_site
203
                  WHERE ips_item_id = ". ltrim($this->pageInfo['page']->getWikidataId(), 'Q');
204
        $res = $this->conn->query($query)->fetchAll();
205
        return $res[0]['count'];
206
    }
207
208
    /**
209
     * Get info about bots that edited the page
210
     * This also sets $this->pageInfo['bot_revision_count'] and $this->pageInfo['bot_percentage']
211
     * @return array Associative array containing the bot's username, edit count to the page
212
     *               and whether or not they are currently a bot
213
     */
214
    private function getBotData()
215
    {
216
        $userGroupsTable = $this->lh->getTable('user_groups', $this->pageInfo['dbName']);
217
        $userFromerGroupsTable = $this->lh->getTable('user_former_groups', $this->pageInfo['dbName']);
218
        $query = "SELECT COUNT(rev_user_text) AS count, rev_user_text AS username, ug_group AS current
219
                  FROM $this->revisionTable
220
                  LEFT JOIN $userGroupsTable ON rev_user = ug_user
221
                  LEFT JOIN $userFromerGroupsTable ON rev_user = ufg_user
222
                  WHERE rev_page = " . $this->pageInfo['page']->getId() . " AND (ug_group = 'bot' OR ufg_group = 'bot')
223
                  GROUP BY rev_user_text";
224
        $res = $this->conn->query($query)->fetchAll();
225
226
        // Parse the botedits
227
        $bots = [];
228
        $sum = 0;
229
        foreach ($res as $bot) {
230
            $bots[$bot['username']] = [
231
                'count' => (int) $bot['count'],
232
                'current' => $bot['current'] === 'bot'
233
            ];
234
            $sum += $bot['count'];
235
        }
236
237
        uasort($bots, function ($a, $b) {
238
            return $b['count'] - $a['count'];
239
        });
240
241
        $this->pageInfo['general']['bot_revision_count'] = $sum;
242
        $this->pageInfo['general']['bot_percentage'] = round(
243
            ($sum / $this->pageInfo['page']->getNumRevisions()) * 100,
244
            1
245
        );
246
247
        return $bots;
248
    }
249
250
    /**
251
     * Get the number of edits made to the page by the top 10% of editors
252
     * This is ran *after* parseHistory() since we need the grand totals first.
253
     * Various stats are also set for each editor in $this->pageInfo['editors']
254
     *   and top ten editors are stored in $this->pageInfo['general']['top_ten']
255
     *   to be used in the charts
256
     * @return integer Number of edits
257
     */
258
    private function getTopTenCount()
259
    {
260
        $topTenCount = $counter = 0;
261
        $topTenEditors = [];
262
263
        foreach ($this->pageInfo['editors'] as $editor => $info) {
264
            // Count how many users are in the top 10% by number of edits
265
            if ($counter < 10) {
266
                $topTenCount += $info['all'];
267
                $counter++;
268
269
                // To be used in the Top Ten charts
270
                $topTenEditors[] = [
271
                    'label' => $editor,
272
                    'value' => $info['all'],
273
                    'percentage' => (
274
                        100 * ($info['all'] / $this->pageInfo['page']->getNumRevisions())
275
                    )
276
                ];
277
            }
278
279
            // Compute the percentage of minor edits the user made
280
            $this->pageInfo['editors'][$editor]['minor_percentage'] = $info['all']
281
                ? ($info['minor'] / $info['all']) * 100
282
                : 0;
283
284
            if ($info['all'] > 1) {
285
                // Number of seconds between first and last edit
286
                $secs = intval(strtotime($info['last']) - strtotime($info['first']) / $info['all']);
287
288
                // Average time between edits (in days)
289
                $this->pageInfo['editors'][$editor]['atbe'] = $secs / ( 60 * 60 * 24 );
290
            }
291
292
            if (count($info['sizes'])) {
293
                // Average Total KB divided by number of stored sizes (user's edit count to this page)
294
                $this->pageInfo['editors'][$editor]['size'] = array_sum($info['sizes']) / count($info['sizes']);
295
            } else {
296
                $this->pageInfo['editors'][$editor]['size'] = 0;
297
            }
298
        }
299
300
        $this->pageInfo['topTenEditors'] = $topTenEditors;
301
302
        // First sort editors array by the amount of text they added
303
        $topTenEditorsByAdded = $this->pageInfo['editors'];
304
        uasort($topTenEditorsByAdded, function ($a, $b) {
305
            if ($a['added'] === $b['added']) {
306
                return 0;
307
            }
308
            return $a['added'] > $b['added'] ? -1 : 1;
309
        });
310
311
        // Then build a new array of top 10 editors by added text,
312
        //   in the data structure needed for the chart
313
        $this->pageInfo['topTenEditorsByAdded'] = array_map(function ($editor) {
314
            $added = $this->pageInfo['editors'][$editor]['added'];
315
            return [
316
                'label' => $editor,
317
                'value' => $added,
318
                'percentage' => (
319
                    100 * ($added / $this->pageInfo['general']['added'])
320
                )
321
            ];
322
        }, array_keys(array_slice($topTenEditorsByAdded, 0, 10)));
323
324
        return $topTenCount;
325
    }
326
327
    /**
328
     * Get number of in and outgoing links and redirects to the page
329
     * @return array Associative array containing counts
330
     */
331
    private function getLinksAndRedirects()
332
    {
333
        $pageId = $this->pageInfo['page']->getId();
334
        $namespace = $this->pageInfo['page']->getNamespace();
335
        $title = str_replace(' ', '_', $this->pageInfo['page']->getTitle());
336
        $externalLinksTable = $this->lh->getTable('externallinks', $this->pageInfo['dbName']);
337
        $pageLinksTable = $this->lh->getTable('pagelinks', $this->pageInfo['dbName']);
338
        $redirectTable = $this->lh->getTable('redirect', $this->pageInfo['dbName']);
339
340
        // FIXME: Probably need to make the $title mysql-safe or whatever
341
        $query = "SELECT COUNT(*) AS value, 'links_ext' AS type
342
                  FROM $externalLinksTable WHERE el_from = $pageId
343
                  UNION
344
                  SELECT COUNT(*) AS value, 'links_out' AS type
345
                  FROM $pageLinksTable WHERE pl_from = $pageId
346
                  UNION
347
                  SELECT COUNT(*) AS value, 'links_in' AS type
348
                  FROM $pageLinksTable WHERE pl_namespace = $namespace AND pl_title = \"$title\"
349
                  UNION
350
                  SELECT COUNT(*) AS value, 'redirects' AS type
351
                  FROM $redirectTable WHERE rd_namespace = $namespace AND rd_title = \"$title\"";
352
353
        $res = $this->conn->query($query)->fetchAll();
354
355
        $data = [];
356
357
        // Transform to associative array by 'type'
358
        foreach ($res as $row) {
359
            $data[$row['type'] . '_count'] = $row['value'];
360
        }
361
362
        return $data;
363
    }
364
365
    /**
366
     * Query for log events during each year of the article's history,
367
     *   and set the results in $this->pageInfo['year_count']
368
     */
369
    private function setLogsEvents()
370
    {
371
        $loggingTable = $this->lh->getTable('logging', $this->pageInfo['dbName'], 'logindex');
372
        $title = str_replace(' ', '_', $this->pageInfo['page']->getTitle());
373
        $query = "SELECT log_action, log_type, log_timestamp AS timestamp
374
                  FROM $loggingTable
375
                  WHERE log_namespace = '" . $this->pageInfo['page']->getNamespace() . "'
376
                  AND log_title = '$title' AND log_timestamp > 1
377
                  AND log_type IN ('delete', 'move', 'protect', 'stable')";
378
        $events = $this->conn->query($query)->fetchAll();
379
380
        foreach ($events as $event) {
381
            $time = strtotime($event['timestamp']);
382
            $year = date('Y', $time);
383
            if (isset($this->pageInfo['year_count'][$year])) {
384
                $yearEvents = $this->pageInfo['year_count'][$year]['events'];
385
386
                // Convert log type value to i18n key
387
                switch ($event['log_type']) {
388
                    case 'protect':
389
                        $action = 'protections';
390
                        break;
391
                    case 'delete':
392
                        $action = 'deletions';
393
                        break;
394
                    case 'move':
395
                        $action = 'moves';
396
                        break;
397
                    // count pending-changes protections along with normal protections
398
                    case 'stable':
399
                        $action = 'protections';
400
                        break;
401
                }
402
403
                if (empty($yearEvents[$action])) {
404
                    $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...
405
                } else {
406
                    $yearEvents[$action]++;
407
                }
408
409
                $this->pageInfo['year_count'][$year]['events'] = $yearEvents;
410
            }
411
        }
412
    }
413
414
    /**
415
     * Get any CheckWiki errors
416
     * @return array Results from query
417
     */
418
    private function getCheckWikiErrors()
419
    {
420
        if ($this->pageInfo['page']->getNamespace() !== 0 || !$this->container->getParameter('app.is_labs')) {
421
            return [];
422
        }
423
        $title = $this->pageInfo['page']->getTitle(); // no underscores
424
        $dbName = preg_replace('/_p$/', '', $this->pageInfo['dbName']); // remove _p if present
425
426
        $query = "SELECT error, notice, found, name_trans AS name, prio, text_trans AS explanation
427
                  FROM s51080__checkwiki_p.cw_error a
428
                  JOIN s51080__checkwiki_p.cw_overview_errors b
429
                  WHERE a.project = b.project AND a.project = '$dbName'
430
                  AND a.title = '$title' AND a.error = b.id
431
                  AND b.done IS NULL";
432
433
        $conn = $this->container->get('doctrine')->getManager('toolsdb')->getConnection();
434
        $res = $conn->query($query)->fetchAll();
435
        return $res;
436
    }
437
438
    /**
439
     * Get basic wikidata on the page: label and description.
440
     * Reported as "bugs" if they are missing.
441
     * @return array Label and description, if present
442
     */
443
    private function getWikidataErrors()
444
    {
445
        if (empty($this->pageInfo['wikidataId'])) {
446
            return [];
447
        }
448
449
        $wikidataId = ltrim($this->pageInfo['wikidataId'], 'Q');
450
        $lang = $this->pageInfo['lang'];
451
452
        $query = "SELECT IF(term_type = 'label', 'label', 'description') AS term, term_text
453
                  FROM wikidatawiki_p.wb_entity_per_page
454
                  JOIN wikidatawiki_p.page ON epp_page_id = page_id
455
                  JOIN wikidatawiki_p.wb_terms ON term_entity_id = epp_entity_id
456
                    AND term_language = '$lang' AND term_type IN ('label', 'description')
457
                  WHERE epp_entity_id = $wikidataId
458
                  UNION
459
                  SELECT pl_title AS term, wb_terms.term_text
460
                  FROM wikidatawiki_p.pagelinks
461
                  JOIN wikidatawiki_p.wb_terms ON term_entity_id = SUBSTRING(pl_title, 2)
462
                    AND term_entity_type = (IF(SUBSTRING(pl_title, 1, 1) = 'Q', 'item', 'property'))
463
                    AND term_language = '$lang'
464
                    AND term_type = 'label'
465
                  WHERE pl_namespace IN (0,120 )
466
                  AND pl_from = (
467
                    SELECT page_id FROM page
468
                    WHERE page_namespace = 0 AND page_title = 'Q$wikidataId'
469
                  )";
470
471
        $conn = $this->container->get('doctrine')->getManager('replicas')->getConnection();
472
        $res = $conn->query($query)->fetchAll();
473
474
        $terms = array_map(function ($entry) {
475
            return $entry['term'];
476
        }, $res);
477
478
        $errors = [];
479
480 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...
481
            $errors[] = [
482
                'prio' => 2,
483
                'name' => 'Wikidata',
484
                'notice' => "Label for language <em>$lang</em> is missing", // FIXME: i18n
485
                'explanation' => "See: <a target='_blank' " .
486
                    "href='//www.wikidata.org/wiki/Help:Label'>Help:Label</a>",
487
            ];
488
        }
489 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...
490
            $errors[] = [
491
                'prio' => 3,
492
                'name' => 'Wikidata',
493
                'notice' => "Description for language <em>$lang</em> is missing", // FIXME: i18n
494
                'explanation' => "See: <a target='_blank' " .
495
                    "href='//www.wikidata.org/wiki/Help:Description'>Help:Description</a>",
496
            ];
497
        }
498
499
        return $errors;
500
    }
501
502
    /**
503
     * Get the size of the diff.
504
     * @param  int $revIndex The index of the revision within $this->pageHistory
505
     * @return int Size of the diff
506
     */
507
    private function getDiffSize($revIndex)
508
    {
509
        $rev = $this->pageHistory[$revIndex];
510
511
        if ($revIndex === 0) {
512
            return $rev['length'];
513
        }
514
515
        $lastRev = $this->pageHistory[$revIndex - 1];
516
517
        // TODO: Remove once T101631 is resolved
518
        // Treat as zero change in size if length of previous edit is missing
519
        if ($lastRev['length'] === null) {
520
            return 0;
521
        } else {
522
            return $rev['length'] - $lastRev['length'];
523
        }
524
    }
525
526
    /**
527
     * Parse the revision history, which should be at $this->pageHistory
528
     * @return array Associative "master" array of metadata about the page
529
     */
530
    private function parseHistory()
531
    {
532
        $revisionCount = $this->pageInfo['page']->getNumRevisions();
533
        if ($revisionCount == 0) {
534
            // $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...
535
            return;
536
        }
537
538
        $firstEdit = $this->pageInfo['firstEdit'];
539
540
        // Get UNIX timestamp of the first day of the month of the first edit
541
        // This is used as a comparison when building our array of per-month stats
542
        $firstEditMonth = mktime(0, 0, 0, (int) $firstEdit->getMonth(), 1, $firstEdit->getYear());
543
544
        $lastEdit = $this->pageInfo['lastEdit'];
545
        $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...
546
547
        // Now we can start our master array. This one will be HUGE!
548
        $data = [
549
            'general' => [
550
                'max_add' => $firstEdit,
551
                'max_del' => $firstEdit,
552
                'editor_count' => 0,
553
                'anon_count' => 0,
554
                'minor_count' => 0,
555
                'count_history' => ['day' => 0, 'week' => 0, 'month' => 0, 'year' => 0],
556
                'current_size' => $this->pageHistory[$revisionCount-1]['length'],
557
                'textshares' => [],
558
                'textshare_total' => 0,
559
                'automated_count' => 0,
560
                'revert_count' => 0,
561
                'added' => 0,
562
            ],
563
            'max_edits_per_month' => 0, // for bar chart in "Month counts" section
564
            'editors' => [],
565
            'anons' => [],
566
            'year_count' => [],
567
            'tools' => [],
568
        ];
569
570
        // restore existing general data
571
        $data['general'] = array_merge($data['general'], $this->pageInfo['general']);
572
573
        // And now comes the logic for filling said master array
574
        foreach ($this->pageHistory as $i => $rev) {
575
            $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...
576
            $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...
577
            $username = htmlspecialchars($rev['username']);
578
579
            // Sometimes, with old revisions (2001 era), the revisions from 2002 come before 2001
580
            if ($edit->getTimestamp() < $firstEdit->getTimestamp()) {
581
                $firstEdit = $edit;
582
            }
583
584
            // Fill in the blank arrays for the year and 12 months
585
            if (!isset($data['year_count'][$edit->getYear()])) {
586
                $data['year_count'][$edit->getYear()] = [
587
                    'all' => 0,
588
                    'minor' => 0,
589
                    'anon' => 0,
590
                    'automated' => 0,
591
                    'size' => 0, // keep track of the size by the end of the year
592
                    'events' => [],
593
                    'months' => [],
594
                ];
595
596
                for ($i = 1; $i <= 12; $i++) {
597
                    $timeObj = mktime(0, 0, 0, $i, 1, $edit->getYear());
598
599
                    // don't show zeros for months before the first edit or after the current month
600
                    if ($timeObj < $firstEditMonth || $timeObj > strtotime('last day of this month')) {
601
                        continue;
602
                    }
603
604
                    $data['year_count'][$edit->getYear()]['months'][sprintf('%02d', $i)] = [
605
                        'all' => 0,
606
                        'minor' => 0,
607
                        'anon' => 0,
608
                        'automated' => 0,
609
                    ];
610
                }
611
            }
612
613
            // Increment year and month counts for all edits
614
            $data['year_count'][$edit->getYear()]['all']++;
615
            $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['all']++;
616
            $data['year_count'][$edit->getYear()]['size'] = (int) $rev['length'];
617
618
            $editsThisMonth = $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['all'];
619
            if ($editsThisMonth > $data['max_edits_per_month']) {
620
                $data['max_edits_per_month'] = $editsThisMonth;
621
            }
622
623
            // Fill in various user stats
624
            if (!isset($data['editors'][$username])) {
625
                $data['general']['editor_count']++;
626
                $data['editors'][$username] = [
627
                    'all' => 0,
628
                    'minor' => 0,
629
                    'minor_percentage' => 0,
630
                    'first' => date('Y-m-d, H:i', strtotime($rev['timestamp'])),
631
                    'first_id' => $rev['id'],
632
                    'last' => null,
633
                    'atbe' => null,
634
                    'added' => 0,
635
                    'sizes' => [],
636
                    'urlencoded' => rawurlencode($rev['username']),
637
                ];
638
            }
639
640
            // Increment user counts
641
            $data['editors'][$username]['all']++;
642
            $data['editors'][$username]['last'] = date('Y-m-d, H:i', strtotime($rev['timestamp']));
643
            $data['editors'][$username]['last_id'] = $rev['id'];
644
645
            // Store number of KB added with this edit
646
            $data['editors'][$username]['sizes'][] = $rev['length'] / 1024;
647
648
            // check if it was a revert
649
            if ($this->aeh->isRevert($rev['comment'])) {
650
                $data['general']['revert_count']++;
651
            } else {
652
                // edit was NOT a revert
653
654
                if ($edit->getSize() > 0) {
655
                    $data['general']['added'] += $edit->getSize();
656
                    $data['editors'][$username]['added'] += $edit->getSize();
657
                }
658
659
                // determine if the next revision was a revert
660
                $nextRevision = isset($this->pageHistory[$i + 1]) ? $this->pageHistory[$i + 1] : null;
661
                $nextRevisionIsRevert = $nextRevision &&
662
                    $this->getDiffSize($i + 1) === -$edit->getSize() &&
663
                    $this->aeh->isRevert($nextRevision['comment']);
664
665
                // don't count this edit as content removal if the next edit reverted it
666
                if (!$nextRevisionIsRevert && $edit->getSize() < $data['general']['max_del']->getSize()) {
667
                    $data['general']['max_del'] = $edit;
668
                }
669
670
                // FIXME: possibly remove this
671
                if ($edit->getLength() > 0) {
672
                    // keep track of added content
673
                    $data['general']['textshare_total'] += $edit->getLength();
674
                    if (!isset($data['textshares'][$username]['all'])) {
675
                        $data['textshares'][$username]['all'] = 0;
676
                    }
677
                    $data['textshares'][$username]['all'] += $edit->getLength();
678
                }
679
680
                if ($edit->getSize() > $data['general']['max_add']->getSize()) {
681
                    $data['general']['max_add'] = $edit;
682
                }
683
            }
684
685
            if ($edit->isAnon()) {
686
                if (!isset($rev['rev_user']['anons'][$username])) {
687
                    $data['general']['anon_count']++;
688
                }
689
                // Anonymous, increase counts
690
                $data['anons'][] = $username;
691
                $data['year_count'][$edit->getYear()]['anon']++;
692
                $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['anon']++;
693
            }
694
695
            if ($edit->isMinor()) {
696
                // Logged in, increase counts
697
                $data['general']['minor_count']++;
698
                $data['year_count'][$edit->getYear()]['minor']++;
699
                $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['minor']++;
700
                $data['editors'][$username]['minor']++;
701
            }
702
703
            $automatedTool = $this->aeh->getTool($rev['comment']);
704
            if ($automatedTool) {
705
                $data['general']['automated_count']++;
706
                $data['year_count'][$edit->getYear()]['automated']++;
707
                $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['automated']++;
708
709
                if (!isset($data['tools'][$automatedTool])) {
710
                    $data['tools'][$automatedTool] = [
711
                        'count' => 1,
712
                        'link' => $this->aeh->getTools()[$automatedTool]['link'],
713
                    ];
714
                } else {
715
                    $data['tools'][$automatedTool]['count']++;
716
                }
717
            }
718
719
            // Increment "edits per <time>" counts
720 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...
721
                $data['general']['count_history']['day']++;
722
            }
723 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...
724
                $data['general']['count_history']['week']++;
725
            }
726 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...
727
                $data['general']['count_history']['month']++;
728
            }
729 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...
730
                $data['general']['count_history']['year']++;
731
            }
732
        }
733
734
        // add percentages
735
        $data['general']['minor_percentage'] = round(
736
            ($data['general']['minor_count'] / $revisionCount) * 100,
737
            1
738
        );
739
        $data['general']['anon_percentage'] = round(
740
            ($data['general']['anon_count'] / $revisionCount) * 100,
741
            1
742
        );
743
744
        // other general statistics
745
        $dateFirst = $firstEdit->getTimestamp();
746
        $dateLast = $lastEdit->getTimestamp();
747
        $data['general']['datetime_first_edit'] = $dateFirst;
748
        $data['general']['datetime_last_edit'] = $dateLast;
749
        $interval = date_diff($dateLast, $dateFirst, true);
750
751
        $data['totaldays'] = $interval->format('%a');
752
        $data['general']['average_days_per_edit'] = round($data['totaldays'] / $revisionCount, 1);
753
        $editsPerDay = $data['totaldays']
754
            ? $revisionCount / ($data['totaldays'] / (365 / 12 / 24))
755
            : 0;
756
        $data['general']['edits_per_day'] = round($editsPerDay, 1);
757
        $editsPerMonth = $data['totaldays']
758
            ? $revisionCount / ($data['totaldays'] / (365 / 12))
759
            : 0;
760
        $data['general']['edits_per_month'] = round($editsPerMonth, 1);
761
        $editsPerYear = $data['totaldays']
762
            ? $revisionCount / ($data['totaldays'] / 365)
763
            : 0;
764
        $data['general']['edits_per_year'] = round($editsPerYear, 1);
765
        $data['general']['edits_per_editor'] = round($revisionCount / count($data['editors']), 1);
766
767
        // If after processing max_del is positive, no edit actually removed text, so unset this value
768
        if ($data['general']['max_del']->getSize() > 0) {
769
            unset($data['general']['max_del']);
770
        }
771
772
        // Various sorts
773
        arsort($data['editors']);
774
        arsort($data['textshares']);
775
        arsort($data['tools']);
776
        ksort($data['year_count']);
777
778
        return $data;
779
    }
780
}
781