Completed
Push — master ( e4c3c6...3f33e1 )
by Sam
03:51
created

ArticleInfoController::setLogsEvents()   C

Complexity

Conditions 8
Paths 12

Size

Total Lines 44
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 44
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 31
nc 12
nop 0
1
<?php
2
/**
3
 * This file contains only the ArticleInfoController class.
4
 */
5
6
namespace AppBundle\Controller;
7
8
use AppBundle\Helper\LabsHelper;
9
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
10
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
11
use Symfony\Component\HttpFoundation\Request;
12
use Symfony\Component\DependencyInjection\ContainerInterface;
13
use Symfony\Component\HttpFoundation\Response;
14
use Xtools\ProjectRepository;
15
use Xtools\Page;
16
use Xtools\PagesRepository;
17
use Xtools\Edit;
18
19
/**
20
 * This controller serves the search form and results for the ArticleInfo tool
21
 */
22
class ArticleInfoController extends Controller
23
{
24
    /** @var LabsHelper The Labs helper object. */
25
    private $lh;
26
    /** @var string Information about the page in question. */
27
    private $pageInfo;
28
    /** @var Edit[] All edits of the page. */
29
    private $pageHistory;
30
    /** @var string The fully-qualified name of the revision table. */
31
    private $revisionTable;
32
33
    /**
34
     * Override method to call ArticleInfoController::containerInitialized() when container set.
35
     * @param ContainerInterface|null $container A ContainerInterface instance or null
36
     */
37
    public function setContainer(ContainerInterface $container = null)
38
    {
39
        parent::setContainer($container);
40
        $this->containerInitialized();
41
    }
42
43
    /**
44
     * Perform some operations after controller initialized and container set.
45
     */
46
    private function containerInitialized()
47
    {
48
        $this->lh = $this->get('app.labs_helper');
49
        $this->lh->checkEnabled('articleinfo');
50
        $this->conn = $this->getDoctrine()->getManager('replicas')->getConnection();
0 ignored issues
show
Bug introduced by
The property conn does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
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...
51
        $this->ph = $this->get('app.pageviews_helper');
0 ignored issues
show
Bug introduced by
The property ph does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
52
        $this->aeh = $this->get('app.automated_edits_helper');
0 ignored issues
show
Bug introduced by
The property aeh does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
53
    }
54
55
    /**
56
     * The search form.
57
     * @Route("/articleinfo", name="articleinfo")
58
     * @Route("/articleinfo", name="articleInfo")
59
     * @Route("/articleinfo/", name="articleInfoSlash")
60
     * @Route("/articleinfo/index.php", name="articleInfoIndexPhp")
61
     * @Route("/articleinfo/{project}", name="ArticleInfoProject")
62
     * @param Request $request The HTTP request.
63
     * @return Response
64
     */
65
    public function indexAction(Request $request)
66
    {
67
        $projectQuery = $request->query->get('project');
68
        $article = $request->query->get('article');
69
70
        if ($projectQuery != '' && $article != '') {
71
            return $this->redirectToRoute('ArticleInfoResult', [ 'project'=>$projectQuery, 'article' => $article ]);
72
        } elseif ($article != '') {
73
            return $this->redirectToRoute('ArticleInfoProject', [ 'project'=>$projectQuery ]);
74
        }
75
76
        if ($projectQuery == '') {
77
            $projectQuery = $this->container->getParameter('default_project');
78
        }
79
80
        $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...
81
82
        return $this->render('articleInfo/index.html.twig', [
83
            'xtPage' => 'articleinfo',
84
            'xtPageTitle' => 'tool-articleinfo',
85
            'xtSubtitle' => 'tool-articleinfo-desc',
86
            'project' => $project,
87
        ]);
88
    }
89
90
    /**
91
     * Display the results.
92
     * @Route("/articleinfo/{project}/{article}", name="ArticleInfoResult", requirements={"article"=".+"})
93
     * @param Request $request The HTTP request.
94
     * @return Response
95
     */
96
    public function resultAction(Request $request)
97
    {
98
        $projectQuery = $request->attributes->get('project');
99
        $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...
100 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...
101
            $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...
102
            return $this->redirectToRoute('articleInfo');
103
        }
104
        $projectUrl = $project->getUrl();
105
        $dbName = $project->getDatabaseName();
106
107
        $pageQuery = $request->attributes->get('article');
108
        $page = new Page($project, $pageQuery);
109
        $pageRepo = new PagesRepository();
110
        $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...
111
        $page->setRepository($pageRepo);
112
113
        if (!$page->exists()) {
114
            $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...
115
            return $this->redirectToRoute('articleInfo');
116
        }
117
118
        $this->revisionTable = $this->lh->getTable('revision', $dbName);
119
120
        // 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...
121
122
        $this->pageInfo = [
0 ignored issues
show
Documentation Bug introduced by
It seems like array('project' => $proj...=> $project->getLang()) of type array<string,object<Xtoo...ring","lang":"string"}> is incompatible with the declared type string 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...
123
            'project' => $project,
124
            'projectUrl' => $projectUrl,
125
            'page' => $page,
126
            'dbName' => $dbName,
127
            'lang' => $project->getLang(),
128
        ];
129
130
        if ($page->getWikidataId()) {
131
            $this->pageInfo['numWikidataItems'] = $this->getNumWikidataItems();
132
        }
133
134
        // TODO: Adapted from legacy code; may be used to indicate how many dead ext links there are
135
        // 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...
136
        //     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...
137
        //         $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...
138
        //     }
139
        // }
140
141
        $this->pageHistory = $page->getRevisions();
142
        $this->pageInfo['firstEdit'] = new Edit($this->pageInfo['page'], $this->pageHistory[0]);
143
        $this->pageInfo['lastEdit'] = new Edit(
144
            $this->pageInfo['page'],
145
            $this->pageHistory[$page->getNumRevisions() - 1]
146
        );
147
148
        // NOTE: bots are fetched first in case we want to restrict some stats to humans editors only
149
        $this->pageInfo['bots'] = $this->getBotData();
150
        $this->pageInfo['general']['bot_count'] = count($this->pageInfo['bots']);
151
152
        $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 string 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...
153
        $this->pageInfo['general']['top_ten_count'] = $this->getTopTenCount();
154
        $this->pageInfo['general']['top_ten_percentage'] = round(
155
            ($this->pageInfo['general']['top_ten_count'] / $page->getNumRevisions()) * 100,
156
            1
157
        );
158
        $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 string 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...
159
        $this->pageInfo['general']['pageviews_offset'] = 60;
160
        $this->pageInfo['general']['pageviews'] = $this->ph->sumLastDays(
161
            $this->pageInfo['project']->getDomain(),
162
            $this->pageInfo['page']->getTitle(),
163
            $this->pageInfo['general']['pageviews_offset']
164
        );
165
        $api = $this->get('app.api_helper');
166
        $assessments = $api->getPageAssessments($projectQuery, $pageQuery);
167
        if ($assessments) {
168
            $this->pageInfo['assessments'] = $assessments;
169
        }
170
        $this->setLogsEvents();
171
172
        $bugs = array_merge($this->getCheckWikiErrors(), $this->getWikidataErrors());
173
        if (!empty($bugs)) {
174
            $this->pageInfo['bugs'] = $bugs;
175
        }
176
177
        $this->pageInfo['xtPage'] = 'articleinfo';
178
        $this->pageInfo['xtTitle'] = $this->pageInfo['page']->getTitle();
179
180
        return $this->render("articleInfo/result.html.twig", $this->pageInfo);
181
    }
182
183
    /**
184
     * Get number of wikidata items (not just languages of sister projects)
185
     * @return integer Number of items
186
     */
187
    private function getNumWikidataItems()
188
    {
189
        $query = "SELECT COUNT(*) AS count
190
                  FROM wikidatawiki_p.wb_items_per_site
191
                  WHERE ips_item_id = ". ltrim($this->pageInfo['page']->getWikidataId(), 'Q');
0 ignored issues
show
Bug introduced by
The method getWikidataId cannot be called on $this->pageInfo['page'] (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
192
        $res = $this->conn->query($query)->fetchAll();
193
        return $res[0]['count'];
194
    }
195
196
    /**
197
     * Get info about bots that edited the page
198
     * This also sets $this->pageInfo['bot_revision_count'] and $this->pageInfo['bot_percentage']
199
     * @return array Associative array containing the bot's username, edit count to the page
200
     *               and whether or not they are currently a bot
201
     */
202
    private function getBotData()
203
    {
204
        $userGroupsTable = $this->lh->getTable('user_groups', $this->pageInfo['dbName']);
205
        $userFromerGroupsTable = $this->lh->getTable('user_former_groups', $this->pageInfo['dbName']);
206
        $query = "SELECT COUNT(rev_user_text) AS count, rev_user_text AS username, ug_group AS current
207
                  FROM $this->revisionTable
208
                  LEFT JOIN $userGroupsTable ON rev_user = ug_user
209
                  LEFT JOIN $userFromerGroupsTable ON rev_user = ufg_user
210
                  WHERE rev_page = " . $this->pageInfo['page']->getId() . " AND (ug_group = 'bot' OR ufg_group = 'bot')
0 ignored issues
show
Bug introduced by
The method getId cannot be called on $this->pageInfo['page'] (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
211
                  GROUP BY rev_user_text";
212
        $res = $this->conn->query($query)->fetchAll();
213
214
        // Parse the botedits
215
        $bots = [];
216
        $sum = 0;
217
        foreach ($res as $bot) {
218
            $bots[$bot['username']] = [
219
                'count' => (int) $bot['count'],
220
                'current' => $bot['current'] === 'bot'
221
            ];
222
            $sum += $bot['count'];
223
        }
224
225
        uasort($bots, function ($a, $b) {
226
            return $b['count'] - $a['count'];
227
        });
228
229
        $this->pageInfo['general']['bot_revision_count'] = $sum;
230
        $this->pageInfo['general']['bot_percentage'] = round(
231
            ($sum / $this->pageInfo['page']->getNumRevisions()) * 100,
0 ignored issues
show
Bug introduced by
The method getNumRevisions cannot be called on $this->pageInfo['page'] (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
232
            1
233
        );
234
235
        return $bots;
236
    }
237
238
    /**
239
     * Get the number of edits made to the page by the top 10% of editors
240
     * This is ran *after* parseHistory() since we need the grand totals first.
241
     * Various stats are also set for each editor in $this->pageInfo['editors']
242
     *   and top ten editors are stored in $this->pageInfo['general']['top_ten']
243
     *   to be used in the charts
244
     * @return integer Number of edits
245
     */
246
    private function getTopTenCount()
247
    {
248
        $topTenCount = $counter = 0;
249
        $topTenEditors = [];
250
251
        foreach ($this->pageInfo['editors'] as $editor => $info) {
0 ignored issues
show
Bug introduced by
The expression $this->pageInfo['editors'] of type string is not traversable.
Loading history...
252
            // Count how many users are in the top 10% by number of edits
253
            if ($counter < 10) {
254
                $topTenCount += $info['all'];
255
                $counter++;
256
257
                // To be used in the Top Ten charts
258
                $topTenEditors[] = [
259
                    'label' => $editor,
260
                    'value' => $info['all'],
261
                    'percentage' => (
262
                        100 * ($info['all'] / $this->pageInfo['page']->getNumRevisions())
0 ignored issues
show
Bug introduced by
The method getNumRevisions cannot be called on $this->pageInfo['page'] (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
263
                    )
264
                ];
265
            }
266
267
            // Compute the percentage of minor edits the user made
268
            $this->pageInfo['editors'][$editor]['minor_percentage'] = $info['all']
269
                ? ($info['minor'] / $info['all']) * 100
270
                : 0;
271
272
            if ($info['all'] > 1) {
273
                // Number of seconds between first and last edit
274
                $secs = intval(strtotime($info['last']) - strtotime($info['first']) / $info['all']);
275
276
                // Average time between edits (in days)
277
                $this->pageInfo['editors'][$editor]['atbe'] = $secs / ( 60 * 60 * 24 );
278
            }
279
280
            if (count($info['sizes'])) {
281
                // Average Total KB divided by number of stored sizes (user's edit count to this page)
282
                $this->pageInfo['editors'][$editor]['size'] = array_sum($info['sizes']) / count($info['sizes']);
283
            } else {
284
                $this->pageInfo['editors'][$editor]['size'] = 0;
285
            }
286
        }
287
288
        $this->pageInfo['topTenEditors'] = $topTenEditors;
289
290
        // First sort editors array by the amount of text they added
291
        $topTenEditorsByAdded = $this->pageInfo['editors'];
292
        uasort($topTenEditorsByAdded, function ($a, $b) {
293
            if ($a['added'] === $b['added']) {
294
                return 0;
295
            }
296
            return $a['added'] > $b['added'] ? -1 : 1;
297
        });
298
299
        // Then build a new array of top 10 editors by added text,
300
        //   in the data structure needed for the chart
301
        $this->pageInfo['topTenEditorsByAdded'] = array_map(function ($editor) {
302
            $added = $this->pageInfo['editors'][$editor]['added'];
303
            return [
304
                'label' => $editor,
305
                'value' => $added,
306
                'percentage' => (
307
                    100 * ($added / $this->pageInfo['general']['added'])
308
                )
309
            ];
310
        }, array_keys(array_slice($topTenEditorsByAdded, 0, 10)));
311
312
        return $topTenCount;
313
    }
314
315
    /**
316
     * Get number of in and outgoing links and redirects to the page
317
     * @return array Associative array containing counts
318
     */
319
    private function getLinksAndRedirects()
320
    {
321
        $pageId = $this->pageInfo['page']->getId();
0 ignored issues
show
Bug introduced by
The method getId cannot be called on $this->pageInfo['page'] (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
322
        $namespace = $this->pageInfo['page']->getNamespace();
0 ignored issues
show
Bug introduced by
The method getNamespace cannot be called on $this->pageInfo['page'] (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
323
        $title = str_replace(' ', '_', $this->pageInfo['page']->getTitle());
0 ignored issues
show
Bug introduced by
The method getTitle cannot be called on $this->pageInfo['page'] (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
324
        $externalLinksTable = $this->lh->getTable('externallinks', $this->pageInfo['dbName']);
325
        $pageLinksTable = $this->lh->getTable('pagelinks', $this->pageInfo['dbName']);
326
        $redirectTable = $this->lh->getTable('redirect', $this->pageInfo['dbName']);
327
328
        // FIXME: Probably need to make the $title mysql-safe or whatever
329
        $query = "SELECT COUNT(*) AS value, 'links_ext' AS type
330
                  FROM $externalLinksTable WHERE el_from = $pageId
331
                  UNION
332
                  SELECT COUNT(*) AS value, 'links_out' AS type
333
                  FROM $pageLinksTable WHERE pl_from = $pageId
334
                  UNION
335
                  SELECT COUNT(*) AS value, 'links_in' AS type
336
                  FROM $pageLinksTable WHERE pl_namespace = $namespace AND pl_title = \"$title\"
337
                  UNION
338
                  SELECT COUNT(*) AS value, 'redirects' AS type
339
                  FROM $redirectTable WHERE rd_namespace = $namespace AND rd_title = \"$title\"";
340
341
        $res = $this->conn->query($query)->fetchAll();
342
343
        $data = [];
344
345
        // Transform to associative array by 'type'
346
        foreach ($res as $row) {
347
            $data[$row['type'] . '_count'] = $row['value'];
348
        }
349
350
        return $data;
351
    }
352
353
    /**
354
     * Query for log events during each year of the article's history,
355
     *   and set the results in $this->pageInfo['year_count']
356
     */
357
    private function setLogsEvents()
358
    {
359
        $loggingTable = $this->lh->getTable('logging', $this->pageInfo['dbName'], 'logindex');
360
        $title = str_replace(' ', '_', $this->pageInfo['page']->getTitle());
0 ignored issues
show
Bug introduced by
The method getTitle cannot be called on $this->pageInfo['page'] (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
361
        $query = "SELECT log_action, log_type, log_timestamp AS timestamp
362
                  FROM $loggingTable
363
                  WHERE log_namespace = '" . $this->pageInfo['page']->getNamespace() . "'
0 ignored issues
show
Bug introduced by
The method getNamespace cannot be called on $this->pageInfo['page'] (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
364
                  AND log_title = '$title' AND log_timestamp > 1
365
                  AND log_type IN ('delete', 'move', 'protect', 'stable')";
366
        $events = $this->conn->query($query)->fetchAll();
367
368
        foreach ($events as $event) {
369
            $time = strtotime($event['timestamp']);
370
            $year = date('Y', $time);
371
            if (isset($this->pageInfo['year_count'][$year])) {
372
                $yearEvents = $this->pageInfo['year_count'][$year]['events'];
373
374
                // Convert log type value to i18n key
375
                switch ($event['log_type']) {
376
                    case 'protect':
377
                        $action = 'protections';
378
                        break;
379
                    case 'delete':
380
                        $action = 'deletions';
381
                        break;
382
                    case 'move':
383
                        $action = 'moves';
384
                        break;
385
                    // count pending-changes protections along with normal protections
386
                    case 'stable':
387
                        $action = 'protections';
388
                        break;
389
                }
390
391
                if (empty($yearEvents[$action])) {
392
                    $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...
393
                } else {
394
                    $yearEvents[$action]++;
395
                }
396
397
                $this->pageInfo['year_count'][$year]['events'] = $yearEvents;
398
            }
399
        }
400
    }
401
402
    /**
403
     * Get any CheckWiki errors
404
     * @return array Results from query
405
     */
406
    private function getCheckWikiErrors()
407
    {
408
        if ($this->pageInfo['page']->getNamespace() !== 0 || !$this->container->getParameter('app.is_labs')) {
0 ignored issues
show
Bug introduced by
The method getNamespace cannot be called on $this->pageInfo['page'] (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
409
            return [];
410
        }
411
        $title = $this->pageInfo['page']->getTitle(); // no underscores
0 ignored issues
show
Bug introduced by
The method getTitle cannot be called on $this->pageInfo['page'] (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
412
        $dbName = preg_replace('/_p$/', '', $this->pageInfo['dbName']); // remove _p if present
413
414
        $query = "SELECT error, notice, found, name_trans AS name, prio, text_trans AS explanation
415
                  FROM s51080__checkwiki_p.cw_error a
416
                  JOIN s51080__checkwiki_p.cw_overview_errors b
417
                  WHERE a.project = b.project AND a.project = '$dbName'
418
                  AND a.title = '$title' AND a.error = b.id
419
                  AND b.done IS NULL";
420
421
        $conn = $this->container->get('doctrine')->getManager('toolsdb')->getConnection();
422
        $res = $conn->query($query)->fetchAll();
423
        return $res;
424
    }
425
426
    /**
427
     * Get basic wikidata on the page: label and description.
428
     * Reported as "bugs" if they are missing.
429
     * @return array Label and description, if present
430
     */
431
    private function getWikidataErrors()
432
    {
433
        if (empty($this->pageInfo['wikidataId'])) {
434
            return [];
435
        }
436
437
        $wikidataId = ltrim($this->pageInfo['wikidataId'], 'Q');
438
        $lang = $this->pageInfo['lang'];
439
440
        $query = "SELECT IF(term_type = 'label', 'label', 'description') AS term, term_text
441
                  FROM wikidatawiki_p.wb_entity_per_page
442
                  JOIN wikidatawiki_p.page ON epp_page_id = page_id
443
                  JOIN wikidatawiki_p.wb_terms ON term_entity_id = epp_entity_id
444
                    AND term_language = '$lang' AND term_type IN ('label', 'description')
445
                  WHERE epp_entity_id = $wikidataId
446
                  UNION
447
                  SELECT pl_title AS term, wb_terms.term_text
448
                  FROM wikidatawiki_p.pagelinks
449
                  JOIN wikidatawiki_p.wb_terms ON term_entity_id = SUBSTRING(pl_title, 2)
450
                    AND term_entity_type = (IF(SUBSTRING(pl_title, 1, 1) = 'Q', 'item', 'property'))
451
                    AND term_language = '$lang'
452
                    AND term_type = 'label'
453
                  WHERE pl_namespace IN (0,120 )
454
                  AND pl_from = (
455
                    SELECT page_id FROM page
456
                    WHERE page_namespace = 0 AND page_title = 'Q$wikidataId'
457
                  )";
458
459
        $conn = $this->container->get('doctrine')->getManager('replicas')->getConnection();
460
        $res = $conn->query($query)->fetchAll();
461
462
        $terms = array_map(function ($entry) {
463
            return $entry['term'];
464
        }, $res);
465
466
        $errors = [];
467
468 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...
469
            $errors[] = [
470
                'prio' => 2,
471
                'name' => 'Wikidata',
472
                'notice' => "Label for language <em>$lang</em> is missing", // FIXME: i18n
473
                'explanation' => "See: <a target='_blank' " .
474
                    "href='//www.wikidata.org/wiki/Help:Label'>Help:Label</a>",
475
            ];
476
        }
477 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...
478
            $errors[] = [
479
                'prio' => 3,
480
                'name' => 'Wikidata',
481
                'notice' => "Description for language <em>$lang</em> is missing", // FIXME: i18n
482
                'explanation' => "See: <a target='_blank' " .
483
                    "href='//www.wikidata.org/wiki/Help:Description'>Help:Description</a>",
484
            ];
485
        }
486
487
        return $errors;
488
    }
489
490
    /**
491
     * Get the size of the diff.
492
     * @param  int $revIndex The index of the revision within $this->pageHistory
493
     * @return int Size of the diff
494
     */
495
    private function getDiffSize($revIndex)
496
    {
497
        $rev = $this->pageHistory[$revIndex];
498
499
        if ($revIndex === 0) {
500
            return $rev['length'];
501
        }
502
503
        $lastRev = $this->pageHistory[$revIndex - 1];
504
505
        // TODO: Remove once T101631 is resolved
506
        // Treat as zero change in size if length of previous edit is missing
507
        if ($lastRev['length'] === null) {
508
            return 0;
509
        } else {
510
            return $rev['length'] - $lastRev['length'];
511
        }
512
    }
513
514
    /**
515
     * Parse the revision history, which should be at $this->pageHistory
516
     * @return array Associative "master" array of metadata about the page
517
     */
518
    private function parseHistory()
519
    {
520
        $revisionCount = $this->pageInfo['page']->getNumRevisions();
0 ignored issues
show
Bug introduced by
The method getNumRevisions cannot be called on $this->pageInfo['page'] (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
521
        if ($revisionCount == 0) {
522
            // $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...
523
            return;
524
        }
525
526
        $firstEdit = $this->pageInfo['firstEdit'];
527
528
        // Get UNIX timestamp of the first day of the month of the first edit
529
        // This is used as a comparison when building our array of per-month stats
530
        $firstEditMonth = mktime(0, 0, 0, (int) $firstEdit->getMonth(), 1, $firstEdit->getYear());
0 ignored issues
show
Bug introduced by
The method getMonth cannot be called on $firstEdit (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
Bug introduced by
The method getYear cannot be called on $firstEdit (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
531
532
        $lastEdit = $this->pageInfo['lastEdit'];
533
        $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...
534
535
        // Now we can start our master array. This one will be HUGE!
536
        $data = [
537
            'general' => [
538
                'max_add' => $firstEdit,
539
                'max_del' => $firstEdit,
540
                'editor_count' => 0,
541
                'anon_count' => 0,
542
                'minor_count' => 0,
543
                'count_history' => ['day' => 0, 'week' => 0, 'month' => 0, 'year' => 0],
544
                'current_size' => $this->pageHistory[$revisionCount-1]['length'],
545
                'textshares' => [],
546
                'textshare_total' => 0,
547
                'automated_count' => 0,
548
                'revert_count' => 0,
549
                'added' => 0,
550
            ],
551
            'max_edits_per_month' => 0, // for bar chart in "Month counts" section
552
            'editors' => [],
553
            'anons' => [],
554
            'year_count' => [],
555
            'tools' => [],
556
        ];
557
558
        // restore existing general data
559
        $data['general'] = array_merge($data['general'], $this->pageInfo['general']);
560
561
        // And now comes the logic for filling said master array
562
        foreach ($this->pageHistory as $i => $rev) {
563
            $edit = new Edit($this->pageInfo['page'], $rev);
0 ignored issues
show
Documentation introduced by
$this->pageInfo['page'] is of type string, but the function expects a object<Xtools\Page>.

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...
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...
564
            $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...
565
            $username = htmlspecialchars($rev['username']);
566
567
            // Sometimes, with old revisions (2001 era), the revisions from 2002 come before 2001
568
            if ($edit->getTimestamp() < $firstEdit->getTimestamp()) {
569
                $firstEdit = $edit;
570
            }
571
572
            // Fill in the blank arrays for the year and 12 months
573
            if (!isset($data['year_count'][$edit->getYear()])) {
574
                $data['year_count'][$edit->getYear()] = [
575
                    'all' => 0,
576
                    'minor' => 0,
577
                    'anon' => 0,
578
                    'automated' => 0,
579
                    'size' => 0, // keep track of the size by the end of the year
580
                    'events' => [],
581
                    'months' => [],
582
                ];
583
584
                for ($i = 1; $i <= 12; $i++) {
585
                    $timeObj = mktime(0, 0, 0, $i, 1, $edit->getYear());
586
587
                    // don't show zeros for months before the first edit or after the current month
588
                    if ($timeObj < $firstEditMonth || $timeObj > strtotime('last day of this month')) {
589
                        continue;
590
                    }
591
592
                    $data['year_count'][$edit->getYear()]['months'][sprintf('%02d', $i)] = [
593
                        'all' => 0,
594
                        'minor' => 0,
595
                        'anon' => 0,
596
                        'automated' => 0,
597
                    ];
598
                }
599
            }
600
601
            // Increment year and month counts for all edits
602
            $data['year_count'][$edit->getYear()]['all']++;
603
            $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['all']++;
604
            $data['year_count'][$edit->getYear()]['size'] = (int) $rev['length'];
605
606
            $editsThisMonth = $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['all'];
607
            if ($editsThisMonth > $data['max_edits_per_month']) {
608
                $data['max_edits_per_month'] = $editsThisMonth;
609
            }
610
611
            // Fill in various user stats
612
            if (!isset($data['editors'][$username])) {
613
                $data['general']['editor_count']++;
614
                $data['editors'][$username] = [
615
                    'all' => 0,
616
                    'minor' => 0,
617
                    'minor_percentage' => 0,
618
                    'first' => date('Y-m-d, H:i', strtotime($rev['timestamp'])),
619
                    'first_id' => $rev['id'],
620
                    'last' => null,
621
                    'atbe' => null,
622
                    'added' => 0,
623
                    'sizes' => [],
624
                    'urlencoded' => rawurlencode($rev['username']),
625
                ];
626
            }
627
628
            // Increment user counts
629
            $data['editors'][$username]['all']++;
630
            $data['editors'][$username]['last'] = date('Y-m-d, H:i', strtotime($rev['timestamp']));
631
            $data['editors'][$username]['last_id'] = $rev['id'];
632
633
            // Store number of KB added with this edit
634
            $data['editors'][$username]['sizes'][] = $rev['length'] / 1024;
635
636
            // check if it was a revert
637
            if ($this->aeh->isRevert($rev['comment'])) {
638
                $data['general']['revert_count']++;
639
            } else {
640
                // edit was NOT a revert
641
642
                if ($edit->getSize() > 0) {
643
                    $data['general']['added'] += $edit->getSize();
644
                    $data['editors'][$username]['added'] += $edit->getSize();
645
                }
646
647
                // determine if the next revision was a revert
648
                $nextRevision = isset($this->pageHistory[$i + 1]) ? $this->pageHistory[$i + 1] : null;
649
                $nextRevisionIsRevert = $nextRevision &&
650
                    $this->getDiffSize($i + 1) === -$edit->getSize() &&
651
                    $this->aeh->isRevert($nextRevision['comment']);
652
653
                // don't count this edit as content removal if the next edit reverted it
654
                if (!$nextRevisionIsRevert && $edit->getSize() < $data['general']['max_del']->getSize()) {
655
                    $data['general']['max_del'] = $edit;
656
                }
657
658
                // FIXME: possibly remove this
659
                if ($edit->getLength() > 0) {
660
                    // keep track of added content
661
                    $data['general']['textshare_total'] += $edit->getLength();
662
                    if (!isset($data['textshares'][$username]['all'])) {
663
                        $data['textshares'][$username]['all'] = 0;
664
                    }
665
                    $data['textshares'][$username]['all'] += $edit->getLength();
666
                }
667
668
                if ($edit->getSize() > $data['general']['max_add']->getSize()) {
669
                    $data['general']['max_add'] = $edit;
670
                }
671
            }
672
673
            if ($edit->isAnon()) {
674
                if (!isset($rev['rev_user']['anons'][$username])) {
675
                    $data['general']['anon_count']++;
676
                }
677
                // Anonymous, increase counts
678
                $data['anons'][] = $username;
679
                $data['year_count'][$edit->getYear()]['anon']++;
680
                $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['anon']++;
681
            }
682
683
            if ($edit->isMinor()) {
684
                // Logged in, increase counts
685
                $data['general']['minor_count']++;
686
                $data['year_count'][$edit->getYear()]['minor']++;
687
                $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['minor']++;
688
                $data['editors'][$username]['minor']++;
689
            }
690
691
            $automatedTool = $this->aeh->getTool($rev['comment']);
692
            if ($automatedTool) {
693
                $data['general']['automated_count']++;
694
                $data['year_count'][$edit->getYear()]['automated']++;
695
                $data['year_count'][$edit->getYear()]['months'][$edit->getMonth()]['automated']++;
696
697
                if (!isset($data['tools'][$automatedTool])) {
698
                    $data['tools'][$automatedTool] = [
699
                        'count' => 1,
700
                        'link' => $this->aeh->getTools()[$automatedTool]['link'],
701
                    ];
702
                } else {
703
                    $data['tools'][$automatedTool]['count']++;
704
                }
705
            }
706
707
            // Increment "edits per <time>" counts
708 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...
709
                $data['general']['count_history']['day']++;
710
            }
711 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...
712
                $data['general']['count_history']['week']++;
713
            }
714 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...
715
                $data['general']['count_history']['month']++;
716
            }
717 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...
718
                $data['general']['count_history']['year']++;
719
            }
720
        }
721
722
        // add percentages
723
        $data['general']['minor_percentage'] = round(
724
            ($data['general']['minor_count'] / $revisionCount) * 100,
725
            1
726
        );
727
        $data['general']['anon_percentage'] = round(
728
            ($data['general']['anon_count'] / $revisionCount) * 100,
729
            1
730
        );
731
732
        // other general statistics
733
        $dateFirst = $firstEdit->getTimestamp();
734
        $dateLast = $lastEdit->getTimestamp();
0 ignored issues
show
Bug introduced by
The method getTimestamp cannot be called on $lastEdit (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
735
        $data['general']['datetime_first_edit'] = $dateFirst;
736
        $data['general']['datetime_last_edit'] = $dateLast;
737
        $interval = date_diff($dateLast, $dateFirst, true);
738
739
        $data['totaldays'] = $interval->format('%a');
740
        $data['general']['average_days_per_edit'] = round($data['totaldays'] / $revisionCount, 1);
741
        $editsPerDay = $data['totaldays']
742
            ? $revisionCount / ($data['totaldays'] / (365 / 12 / 24))
743
            : 0;
744
        $data['general']['edits_per_day'] = round($editsPerDay, 1);
745
        $editsPerMonth = $data['totaldays']
746
            ? $revisionCount / ($data['totaldays'] / (365 / 12))
747
            : 0;
748
        $data['general']['edits_per_month'] = round($editsPerMonth, 1);
749
        $editsPerYear = $data['totaldays']
750
            ? $revisionCount / ($data['totaldays'] / 365)
751
            : 0;
752
        $data['general']['edits_per_year'] = round($editsPerYear, 1);
753
        $data['general']['edits_per_editor'] = round($revisionCount / count($data['editors']), 1);
754
755
        // If after processing max_del is positive, no edit actually removed text, so unset this value
756
        if ($data['general']['max_del']->getSize() > 0) {
757
            unset($data['general']['max_del']);
758
        }
759
760
        // Various sorts
761
        arsort($data['editors']);
762
        arsort($data['textshares']);
763
        arsort($data['tools']);
764
        ksort($data['year_count']);
765
766
        return $data;
767
    }
768
}
769