Completed
Push — master ( 14c233...a96712 )
by MusikAnimal
03:53
created

ApiController::articleInfo()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 62
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 62
rs 8.9167
c 0
b 0
f 0
cc 4
eloc 44
nc 4
nop 3

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file contains only the ApiController class.
4
 */
5
6
namespace AppBundle\Controller;
7
8
use Exception;
9
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
10
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
11
use Symfony\Component\HttpFoundation\Request;
12
use Symfony\Component\HttpFoundation\Response;
13
use Symfony\Component\Debug\Exception\FatalErrorException;
14
use FOS\RestBundle\Controller\Annotations as Rest;
15
use FOS\RestBundle\Controller\FOSRestController;
16
use FOS\RestBundle\View\View;
17
use Xtools\ProjectRepository;
18
use Xtools\UserRepository;
19
use Xtools\Page;
20
use Xtools\PagesRepository;
21
use Xtools\Edit;
22
use DateTime;
23
24
/**
25
 * Serves the external API of XTools.
26
 */
27
class ApiController extends FOSRestController
28
{
29
    /**
30
     * Get domain name, URL, and API URL of the given project.
31
     * @Rest\Get("/api/normalizeProject/{project}")
32
     * @Rest\Get("/api/normalize_project/{project}")
33
     * @param string $project Project database name, URL, or domain name.
34
     * @return View
35
     */
36
    public function normalizeProject($project)
37
    {
38
        $proj = ProjectRepository::getProject($project, $this->container);
39
40
        if (!$proj->exists()) {
41
            return new View(
42
                [
43
                    'error' => "$project is not a valid project",
44
                ],
45
                Response::HTTP_NOT_FOUND
46
            );
47
        }
48
49
        return new View(
50
            [
51
                'domain' => $proj->getDomain(),
52
                'url' => $proj->getUrl(),
53
                'api' => $proj->getApiUrl(),
54
                'database' => $proj->getDatabaseName(),
55
            ],
56
            Response::HTTP_OK
57
        );
58
    }
59
60
    /**
61
     * Get all namespaces of the given project.
62
     * @Rest\Get("/api/namespaces/{project}")
63
     * @param string $project The project name.
64
     * @return View
65
     */
66
    public function namespaces($project)
67
    {
68
        $proj = ProjectRepository::getProject($project, $this->container);
69
70
        if (!$proj->exists()) {
71
            return new View(
72
                [
73
                    'error' => "$project is not a valid project",
74
                ],
75
                Response::HTTP_NOT_FOUND
76
            );
77
        }
78
79
        return new View(
80
            [
81
                'api' => $proj->getApiUrl(),
82
                'namespaces' => $proj->getNamespaces(),
83
            ],
84
            Response::HTTP_OK
85
        );
86
    }
87
88
    /**
89
     * Get non-automated edits for the given user.
90
     * @Rest\Get(
91
     *   "/api/nonautomated_edits/{project}/{username}/{namespace}/{start}/{end}/{offset}",
92
     *   requirements={"start" = "|\d{4}-\d{2}-\d{2}", "end" = "|\d{4}-\d{2}-\d{2}"}
93
     * )
94
     * @param Request $request The HTTP request.
95
     * @param string $project
96
     * @param string $username
97
     * @param int|string $namespace ID of the namespace, or 'all' for all namespaces
98
     * @param string $start In the format YYYY-MM-DD
99
     * @param string $end In the format YYYY-MM-DD
100
     * @param int $offset For pagination, offset results by N edits
101
     * @return View
102
     */
103
    public function nonautomatedEdits(
104
        Request $request,
105
        $project,
106
        $username,
107
        $namespace,
108
        $start = '',
109
        $end = '',
110
        $offset = 0
111
    ) {
112
        $twig = $this->container->get('twig');
0 ignored issues
show
Unused Code introduced by
$twig 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...
113
        $project = ProjectRepository::getProject($project, $this->container);
114
        $user = UserRepository::getUser($username, $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...
115
        $data = $user->getNonautomatedEdits($project, $namespace, $start, $end, $offset);
116
117
        $view = View::create()->setStatusCode(Response::HTTP_OK);
118
119
        if ($request->query->get('format') === 'html') {
120
            $edits = array_map(function ($attrs) use ($project, $username) {
121
                $nsName = '';
122
                if ($attrs['page_namespace']) {
123
                    $nsName = $project->getNamespaces()[$attrs['page_namespace']];
124
                }
125
                $page = $project->getRepository()
126
                    ->getPage($project, $nsName . ':' . $attrs['page_title']);
127
                $attrs['id'] = $attrs['rev_id'];
128
                $attrs['username'] = $username;
129
                return new Edit($page, $attrs);
130
            }, $data);
131
132
            $twig = $this->container->get('twig');
0 ignored issues
show
Unused Code introduced by
$twig 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...
133
            $view->setTemplate('api/nonautomated_edits.html.twig');
134
            $view->setTemplateData([
135
                'edits' => $edits,
136
                'project' => $project,
137
            ]);
138
            $view->setFormat('html');
139
        } else {
140
            $view->setData(['data' => $data])
141
                ->setFormat('json');
142
        }
143
144
        return $view;
145
    }
146
147
    /**
148
     * Get basic info on a given article.
149
     * @Rest\Get("/api/articleinfo/{project}/{article}", requirements={"article"=".+"})
150
     * @param Request $request The HTTP request.
151
     * @param string $project
152
     * @param string $article
153
     * @return View
154
     */
155
    public function articleInfo(Request $request, $project, $article)
156
    {
157
        /** @var integer Number of days to query for pageviews */
158
        $pageviewsOffset = 30;
159
160
        $project = ProjectRepository::getProject($project, $this->container);
161
        if (!$project->exists()) {
162
            return new View(
163
                ['error' => "$project is not a valid project"],
164
                Response::HTTP_NOT_FOUND
165
            );
166
        }
167
168
        $page = new Page($project, $article);
169
        $pageRepo = new PagesRepository();
170
        $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...
171
        $page->setRepository($pageRepo);
172
173
        if (!$page->exists()) {
174
            return new View(
175
                ['error' => "$article was not found"],
176
                Response::HTTP_NOT_FOUND
177
            );
178
        }
179
180
        $info = $page->getBasicEditingInfo();
181
        $creationDateTime = DateTime::createFromFormat('YmdHis', $info['created_at']);
182
        $modifiedDateTime = DateTime::createFromFormat('YmdHis', $info['modified_at']);
183
        $secsSinceLastEdit = $modifiedDateTime->getTimestamp() - $creationDateTime->getTimestamp();
184
185
        $data = [
186
            'revisions' => (int) $info['num_edits'],
187
            'editors' => (int) $info['num_editors'],
188
            'author' => $info['author'],
189
            'author_editcount' => (int) $info['author_editcount'],
190
            'created_at' => $creationDateTime->format('Y-m-d H:i'),
191
            'modified_at' => $modifiedDateTime->format('Y-m-d H:i'),
192
            'secs_since_last_edit' => $secsSinceLastEdit,
193
            'last_edit_id' => (int) $info['modified_rev_id'],
194
            'watchers' => (int) $page->getWatchers(),
195
            'pageviews' => $page->getLastPageviews($pageviewsOffset),
196
            'pageviews_offset' => $pageviewsOffset,
197
        ];
198
199
        $view = View::create()->setStatusCode(Response::HTTP_OK);
200
201
        if ($request->query->get('format') === 'html') {
202
            $twig = $this->container->get('twig');
0 ignored issues
show
Unused Code introduced by
$twig 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...
203
            $view->setTemplate('api/articleinfo.html.twig');
204
            $view->setTemplateData([
205
                'data' => $data,
206
                'project' => $project,
207
                'page' => $page,
208
            ]);
209
            $view->setFormat('html');
210
        } else {
211
            $view->setData(['data' => $data])
212
                ->setFormat('json');
213
        }
214
215
        return $view;
216
    }
217
218
    /**
219
     * Record usage of a particular XTools tool. This is called automatically
220
     *   in base.html.twig via JavaScript so that it is done asynchronously
221
     * @Rest\Put("/api/usage/{tool}/{project}/{token}")
222
     * @param  string $tool    Internal name of tool
223
     * @param  string $project Project domain such as en.wikipedia.org
224
     * @param  string $token   Unique token for this request, so we don't have people
225
     *                         meddling with these statistics
226
     * @return View
227
     */
228
    public function recordUsage($tool, $project, $token)
229
    {
230
        // Validate token
231
        if (!$this->isCsrfTokenValid('intention', $token)) {
232
            return new View(
233
                [],
234
                Response::HTTP_FORBIDDEN
235
            );
236
        }
237
238
        // Don't update counts for tools that aren't enabled
239
        if (!$this->container->getParameter("enable.$tool")) {
240
            return new View(
241
                [
242
                    'error' => 'This tool is disabled'
243
                ],
244
                Response::HTTP_FORBIDDEN
245
            );
246
        }
247
248
        $conn = $this->getDoctrine()->getManager('default')->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...
249
        $date =  date('Y-m-d');
250
251
        // Increment count in timeline
252
        $existsSql = "SELECT 1 FROM usage_timeline
253
                      WHERE date = '$date'
254
                      AND tool = '$tool'";
255
256 View Code Duplication
        if (count($conn->query($existsSql)->fetchAll()) === 0) {
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...
257
            $createSql = "INSERT INTO usage_timeline
258
                          VALUES(NULL, '$date', '$tool', 1)";
259
            $conn->query($createSql);
260
        } else {
261
            $updateSql = "UPDATE usage_timeline
262
                          SET count = count + 1
263
                          WHERE tool = '$tool'
264
                          AND date = '$date'";
265
            $conn->query($updateSql);
266
        }
267
268
        // Update per-project usage, if applicable
269
        if (!$this->container->getParameter('app.single_wiki')) {
270
            $existsSql = "SELECT 1 FROM usage_projects
271
                          WHERE tool = '$tool'
272
                          AND project = '$project'";
273
274 View Code Duplication
            if (count($conn->query($existsSql)->fetchAll()) === 0) {
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...
275
                $createSql = "INSERT INTO usage_projects
276
                              VALUES(NULL, '$tool', '$project', 1)";
277
                $conn->query($createSql);
278
            } else {
279
                $updateSql = "UPDATE usage_projects
280
                              SET count = count + 1
281
                              WHERE tool = '$tool'
282
                              AND project = '$project'";
283
                $conn->query($updateSql);
284
            }
285
        }
286
287
        return new View(
288
            [],
289
            Response::HTTP_NO_CONTENT
290
        );
291
    }
292
}
293