Completed
Push — master ( fcf66f...4020b1 )
by Sam
02:59
created

TopEditsController::resultAction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 15
rs 9.4285
cc 2
eloc 9
nc 2
nop 4
1
<?php
2
3
namespace AppBundle\Controller;
4
5
use AppBundle\Helper\ApiHelper;
6
use AppBundle\Helper\LabsHelper;
7
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
8
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
9
use Symfony\Component\HttpFoundation\Request;
10
use Xtools\Project;
11
use Xtools\ProjectRepository;
12
use Xtools\User;
13
14
class TopEditsController extends Controller
15
{
16
17
    /** @var LabsHelper */
18
    private $lh;
19
20
    /**
21
     * Get the Project.
22
     * @todo This can probably be made more common.
23
     * @param string $projectIdent The domain name, database name, or URL of a project.
24
     * @return Project
25
     */
26
    protected function getProject($projectIdent)
27
    {
28
        $project = new Project($projectIdent);
29
        $projectRepo = new ProjectRepository();
30
        $projectRepo->setMetaConnection($this->get('doctrine')->getManager("meta")->getConnection());
31
        $projectRepo->setCache($this->get('cache.app'));
32
        if ($this->container->getParameter('app.single_wiki')) {
33
            $projectRepo->setSingleMetadata([
34
                'url' => $this->container->getParameter('wiki_url'),
35
                'dbname' => $this->container->getParameter('database_replica_name'),
36
            ]);
37
        }
38
        $project->setRepository($projectRepo);
39
        return $project;
40
    }
41
42
    /**
43
     * @Route("/topedits", name="topedits")
44
     * @Route("/topedits", name="topEdits")
45
     * @Route("/topedits/", name="topEditsSlash")
46
     * @Route("/topedits/index.php", name="topEditsIndex")
47
     */
48
    public function indexAction(Request $request)
49
    {
50
        $this->lh = $this->get("app.labs_helper");
51
        $this->lh->checkEnabled("topedits");
52
53
        $project = $request->query->get('project');
54
        $username = $request->query->get('username');
55
        $namespace = $request->query->get('namespace');
56
        $article = $request->query->get('article');
57
58
        if ($project != "" && $username != "" && $namespace != "" && $article != "") {
59
            return $this->redirectToRoute("TopEditsResults", [
60
                'project'=>$project,
61
                'username' => $username,
62
                'namespace'=>$namespace,
63
                'article'=>$article,
64
            ]);
65
        } elseif ($project != "" && $username != "" && $namespace != "") {
66
            return $this->redirectToRoute("TopEditsResults", [
67
                'project'=>$project,
68
                'username' => $username,
69
                'namespace'=>$namespace,
70
            ]);
71
        } elseif ($project != "" && $username != "") {
72
            return $this->redirectToRoute("TopEditsResults", [
73
                'project' => $project,
74
                'username' => $username,
75
            ]);
76
        } elseif ($project != "") {
77
            return $this->redirectToRoute("TopEditsResults", [ 'project'=>$project ]);
78
        }
79
80
        // set default wiki so we can populate the namespace selector
81
        if (!$project) {
82
            $project = $this->container->getParameter('default_project');
83
        }
84
85
        $project = $this->getProject($project);
86
87
        return $this->render('topedits/index.html.twig', [
88
            'xtPageTitle' => 'tool-topedits',
89
            'xtSubtitle' => 'tool-topedits-desc',
90
            'xtPage' => 'topedits',
91
            'project' => $project,
92
        ]);
93
    }
94
95
    /**
96
     * @Route("/topedits/{project}/{username}/{namespace}/{article}", name="TopEditsResults")
97
     */
98
    public function resultAction($project, $username, $namespace = 0, $article = "")
99
    {
100
        /** @var LabsHelper $lh */
101
        $this->lh = $this->get('app.labs_helper');
102
        $this->lh->checkEnabled('topedits');
103
104
        $project = $this->getProject($project);
105
        $user = new User($username);
106
107
        if ($article === "") {
108
            return $this->namespaceTopEdits($user, $project, $namespace);
109
        } else {
110
            return $this->singlePageTopEdits($user, $project, $article);
111
        }
112
    }
113
114
    /**
115
     * List top edits by this user for all pages in a particular namespace.
116
     * @param User $user The User.
117
     * @param Project $project The project.
118
     * @param integer|string $namespace The namespace ID or 'all'
119
     * @return \Symfony\Component\HttpFoundation\Response
120
     */
121
    protected function namespaceTopEdits(User $user, Project $project, $namespace)
122
    {
123
        // Get list of namespaces.
124
        $namespaces = $project->getNamespaces();
125
126
        // Get the basic data about the pages edited by this user.
127
        $params = ['username'=>$user->getUsername()];
128
        $nsClause = '';
129
        $namespaceMsg = 'namespaces_all';
130
        if (is_numeric($namespace)) {
131
            $nsClause = 'AND page_namespace = :namespace';
132
            $params['namespace'] = $namespace;
133
            $namespaceMsg = str_replace(' ', '_', strtolower($namespaces[$namespace]));
134
        }
135
        $revTable = $this->lh->getTable('revision', $project->getDatabaseName());
136
        $pageTable = $this->lh->getTable('page', $project->getDatabaseName());
137
        $query = "SELECT page_namespace, page_title, page_is_redirect, COUNT(page_title) AS count
138
                FROM $pageTable JOIN $revTable ON page_id = rev_page
139
                WHERE rev_user_text = :username $nsClause
140
                GROUP BY page_namespace, page_title
141
                ORDER BY count DESC
142
                LIMIT 100";
143
        $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...
144
        $editData = $conn->executeQuery($query, $params)->fetchAll();
145
146
        // Inform user if no revisions found.
147
        if (count($editData) === 0) {
148
            $this->addFlash("notice", ["nocontribs"]);
0 ignored issues
show
Documentation introduced by
array('nocontribs') is of type array<integer,string,{"0":"string"}>, 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...
149
        }
150
151
        // Get page info about these 100 pages, so we can use their display title.
152
        $titles = array_map(function ($e) {
153
            return $e['page_title'];
154
        }, $editData);
155
        /** @var ApiHelper $apiHelper */
156
        $apiHelper = $this->get('app.api_helper');
157
        $displayTitles = $apiHelper->displayTitles($project->getDomain(), $titles);
0 ignored issues
show
Security Bug introduced by
It seems like $project->getDomain() targeting Xtools\Project::getDomain() can also be of type false; however, AppBundle\Helper\ApiHelper::displayTitles() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
158
159
        // Put all together, and return the view.
160
        $edits = [];
161
        foreach ($editData as $editDatum) {
162
            $pageTitle = $editDatum['page_title'];
163
            // If 'all' namespaces, prepend namespace to display title.
164
            $nsTitle = !is_numeric($namespace) ? $namespaces[$editDatum['page_namespace']].':' : '';
165
            $editDatum['displaytitle'] = $nsTitle.$displayTitles[$pageTitle];
166
            $edits[] = $editDatum;
167
        }
168
        return $this->render('topedits/result_namespace.html.twig', [
169
            'xtPage' => 'topedits',
170
            'project' => $project,
171
            'user' => $user,
172
            'namespace' => $namespace,
173
            'edits' => $edits,
174
            'content_title' => $namespaceMsg,
175
        ]);
176
    }
177
178
    /**
179
     * List top edits by this user for a particular page.
180
     * @param User $user
181
     * @param Project $project
182
     * @param string $article
183
     * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
184
     */
185
    protected function singlePageTopEdits(User $user, Project $project, $article)
186
    {
187
        /** @var ApiHelper $apiHelper */
188
        $apiHelper = $this->get("app.api_helper");
189
        $pageInfo = $apiHelper->getBasicPageInfo($project->getDomain(), $article, true);
0 ignored issues
show
Security Bug introduced by
It seems like $project->getDomain() targeting Xtools\Project::getDomain() can also be of type false; however, AppBundle\Helper\ApiHelper::getBasicPageInfo() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
190
        if (isset($pageInfo['missing']) && $pageInfo['missing']) {
191
            // Redirect if the page doesn't exist.
192
            $this->addFlash("notice", ["noresult", $article]);
0 ignored issues
show
Documentation introduced by
array('noresult', $article) is of type array<integer,string,{"0":"string","1":"string"}>, 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...
193
            return $this->redirectToRoute("topedits");
194
        }
195
196
        // Get all revisions of this page by this user.
197
        $revTable = $this->lh->getTable('revision', $project->getDatabaseName());
198
        $query = "SELECT
199
                    revs.rev_id AS id,
200
                    revs.rev_timestamp AS timestamp,
201
                    (CAST(revs.rev_len AS SIGNED) - IFNULL(parentrevs.rev_len, 0)) AS length_change,
202
                    revs.rev_comment AS comment
203
                FROM $revTable AS revs
204
                    LEFT JOIN $revTable AS parentrevs ON (revs.rev_parent_id = parentrevs.rev_id)
205
                WHERE revs.rev_user_text in (:username) AND revs.rev_page = :pageid
206
                ORDER BY revs.rev_timestamp DESC
207
            ";
208
        $params = ['username' => $user->getUsername(), 'pageid' => $pageInfo['pageid']];
209
        $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...
210
        $revisionsData = $conn->executeQuery($query, $params)->fetchAll();
211
212
        // Loop through all revisions and format dates, find totals, etc.
213
        $totalAdded = 0;
214
        $totalRemoved = 0;
215
        $revisions = [];
216
        foreach ($revisionsData as $revision) {
217
            if ($revision['length_change'] > 0) {
218
                $totalAdded += $revision['length_change'];
219
            } else {
220
                $totalRemoved += $revision['length_change'];
221
            }
222
            $time = strtotime($revision['timestamp']);
223
            $revision['timestamp'] = $time; // formatted via Twig helper
224
            $revision['year'] = date('Y', $time);
225
            $revision['month'] = date('m', $time);
226
            $revisions[] = $revision;
227
        }
228
229
        // Send all to the template.
230
        return $this->render('topedits/result_article.html.twig', [
231
            'xtPage' => 'topedits',
232
            'project' => $project,
233
            'user' => $user,
234
            'article' => $pageInfo,
235
            'total_added' => $totalAdded,
236
            'total_removed' => $totalRemoved,
237
            'revisions' => $revisions,
238
            'revision_count' => count($revisions),
239
        ]);
240
    }
241
}
242