Completed
Push — master ( ee7daf...bbf020 )
by MusikAnimal
02:28
created

ApiController::namespaces()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 24
Code Lines 13

Duplication

Lines 24
Ratio 100 %

Importance

Changes 0
Metric Value
dl 24
loc 24
rs 8.9713
c 0
b 0
f 0
cc 2
eloc 13
nc 2
nop 1
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/project/normalize/{project}")
32
     * @param string $project Project database name, URL, or domain name.
33
     * @return View
34
     */
35 View Code Duplication
    public function normalizeProject($project)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
36
    {
37
        $proj = ProjectRepository::getProject($project, $this->container);
38
39
        if (!$proj->exists()) {
40
            return new View(
41
                [
42
                    'error' => "$project is not a valid project",
43
                ],
44
                Response::HTTP_NOT_FOUND
45
            );
46
        }
47
48
        return new View(
49
            [
50
                'domain' => $proj->getDomain(),
51
                'url' => $proj->getUrl(),
52
                'api' => $proj->getApiUrl(),
53
                'database' => $proj->getDatabaseName(),
54
            ],
55
            Response::HTTP_OK
56
        );
57
    }
58
59
    /**
60
     * Get all namespaces of the given project. This endpoint also does the same thing
61
     * as the normalize_project endpoint, returning other basic info about the project.
62
     * @Rest\Get("/api/project/namespaces/{project}")
63
     * @param string $project The project name.
64
     * @return View
65
     */
66 View Code Duplication
    public function namespaces($project)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
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
                'domain' => $proj->getDomain(),
82
                'url' => $proj->getUrl(),
83
                'api' => $proj->getApiUrl(),
84
                'database' => $proj->getDatabaseName(),
85
                'namespaces' => $proj->getNamespaces(),
86
            ],
87
            Response::HTTP_OK
88
        );
89
    }
90
91
    /**
92
     * Count the number of automated edits the given user has made.
93
     * @Rest\Get(
94
     *   "/api/user/automated_editcount/{project}/{username}/{namespace}/{start}/{end}/{tools}",
95
     *   requirements={"start" = "|\d{4}-\d{2}-\d{2}", "end" = "|\d{4}-\d{2}-\d{2}"}
96
     * )
97
     * @param Request $request The HTTP request.
98
     * @param string $project
99
     * @param string $username
100
     * @param int|string $namespace ID of the namespace, or 'all' for all namespaces
101
     * @param string $start In the format YYYY-MM-DD
102
     * @param string $end In the format YYYY-MM-DD
103
     * @param string $tools Non-blank to show which tools were used and how many times.
104
     */
105
    public function automatedEditCount(
106
        Request $request,
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
107
        $project,
108
        $username,
109
        $namespace = 'all',
110
        $start = '',
111
        $end = '',
112
        $tools = ''
113
    ) {
114
        $project = ProjectRepository::getProject($project, $this->container);
115
        $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...
116
117
        $res = [
118
            'project' => $project->getDomain(),
119
            'username' => $user->getUsername(),
120
        ];
121
122
        if ($tools != '') {
123
            $tools = $user->getAutomatedCounts($project, $namespace, $start, $end);
124
            $res['automated_editcount'] = 0;
125
            foreach ($tools as $tool) {
126
                $res['automated_editcount'] += $tool['count'];
127
            }
128
            $res['automated_tools'] = $tools;
129
        } else {
130
            $res['automated_editcount'] = $user->countAutomatedEdits($project, $namespace, $start, $end);
131
        }
132
133
        $view = View::create()->setStatusCode(Response::HTTP_OK);
134
        $view->setData($res);
135
136
        return $view->setFormat('json');
137
    }
138
139
    /**
140
     * Get non-automated edits for the given user.
141
     * @Rest\Get(
142
     *   "/api/user/nonautomated_edits/{project}/{username}/{namespace}/{start}/{end}/{offset}",
143
     *   requirements={"start" = "|\d{4}-\d{2}-\d{2}", "end" = "|\d{4}-\d{2}-\d{2}"}
144
     * )
145
     * @param Request $request The HTTP request.
146
     * @param string $project
147
     * @param string $username
148
     * @param int|string $namespace ID of the namespace, or 'all' for all namespaces
149
     * @param string $start In the format YYYY-MM-DD
150
     * @param string $end In the format YYYY-MM-DD
151
     * @param int $offset For pagination, offset results by N edits
152
     * @return View
153
     */
154
    public function nonautomatedEdits(
155
        Request $request,
156
        $project,
157
        $username,
158
        $namespace,
159
        $start = '',
160
        $end = '',
161
        $offset = 0
162
    ) {
163
        $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...
164
        $project = ProjectRepository::getProject($project, $this->container);
165
        $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...
166
        $data = $user->getNonautomatedEdits($project, $namespace, $start, $end, $offset);
167
168
        $view = View::create()->setStatusCode(Response::HTTP_OK);
169
170
        if ($request->query->get('format') === 'html') {
171
            $edits = array_map(function ($attrs) use ($project, $username) {
172
                $nsName = '';
173
                if ($attrs['page_namespace']) {
174
                    $nsName = $project->getNamespaces()[$attrs['page_namespace']];
175
                }
176
                $page = $project->getRepository()
177
                    ->getPage($project, $nsName . ':' . $attrs['page_title']);
178
                $attrs['id'] = $attrs['rev_id'];
179
                $attrs['username'] = $username;
180
                return new Edit($page, $attrs);
181
            }, $data);
182
183
            $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...
184
            $view->setTemplate('api/nonautomated_edits.html.twig');
185
            $view->setTemplateData([
186
                'edits' => $edits,
187
                'project' => $project,
188
            ]);
189
            $view->setFormat('html');
190
        } else {
191
            $res = [
192
                'project' => $project->getDomain(),
193
                'username' => $user->getUsername(),
194
            ];
195
            if ($namespace != '' && $namespace !== 'all') {
196
                $res['namespace'] = $namespace;
197
            }
198
            if ($start != '') {
199
                $res['start'] = $start;
200
            }
201
            if ($end != '') {
202
                $res['end'] = $end;
203
            }
204
            $res['offset'] = $offset;
205
            $res['nonautomated_edits'] = $data;
206
207
            $view->setData($res)->setFormat('json');
208
        }
209
210
        return $view;
211
    }
212
213
    /**
214
     * Get basic info on a given article.
215
     * @Rest\Get("/api/articleinfo/{project}/{article}", requirements={"article"=".+"})
216
     * @Rest\Get("/api/page/articleinfo/{project}/{article}", requirements={"article"=".+"})
217
     * @param Request $request The HTTP request.
218
     * @param string $project
219
     * @param string $article
220
     * @return View
221
     */
222
    public function articleInfo(Request $request, $project, $article)
223
    {
224
        /** @var integer Number of days to query for pageviews */
225
        $pageviewsOffset = 30;
226
227
        $project = ProjectRepository::getProject($project, $this->container);
228
        if (!$project->exists()) {
229
            return new View(
230
                ['error' => "$project is not a valid project"],
231
                Response::HTTP_NOT_FOUND
232
            );
233
        }
234
235
        $page = new Page($project, $article);
236
        $pageRepo = new PagesRepository();
237
        $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...
238
        $page->setRepository($pageRepo);
239
240
        if (!$page->exists()) {
241
            return new View(
242
                ['error' => "$article was not found"],
243
                Response::HTTP_NOT_FOUND
244
            );
245
        }
246
247
        $info = $page->getBasicEditingInfo();
248
        $creationDateTime = DateTime::createFromFormat('YmdHis', $info['created_at']);
249
        $modifiedDateTime = DateTime::createFromFormat('YmdHis', $info['modified_at']);
250
        $secsSinceLastEdit = (new DateTime)->getTimestamp() - $modifiedDateTime->getTimestamp();
251
252
        $data = [
253
            'revisions' => (int) $info['num_edits'],
254
            'editors' => (int) $info['num_editors'],
255
            'author' => $info['author'],
256
            'author_editcount' => (int) $info['author_editcount'],
257
            'created_at' => $creationDateTime->format('Y-m-d'),
258
            'created_rev_id' => $info['created_rev_id'],
259
            'modified_at' => $modifiedDateTime->format('Y-m-d H:i'),
260
            'secs_since_last_edit' => $secsSinceLastEdit,
261
            'last_edit_id' => (int) $info['modified_rev_id'],
262
            'watchers' => (int) $page->getWatchers(),
263
            'pageviews' => $page->getLastPageviews($pageviewsOffset),
264
            'pageviews_offset' => $pageviewsOffset,
265
        ];
266
267
        $view = View::create()->setStatusCode(Response::HTTP_OK);
268
269
        if ($request->query->get('format') === 'html') {
270
            $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...
271
            $view->setTemplate('api/articleinfo.html.twig');
272
            $view->setTemplateData([
273
                'data' => $data,
274
                'project' => $project,
275
                'page' => $page,
276
            ]);
277
            $view->setFormat('html');
278
        } else {
279
            $res = array_merge([
280
                'project' => $project->getDomain(),
281
                'page' => $page->getTitle(),
282
            ], $data);
283
            $view->setData($res)->setFormat('json');
284
        }
285
286
        return $view;
287
    }
288
289
    /**
290
     * Record usage of a particular XTools tool. This is called automatically
291
     *   in base.html.twig via JavaScript so that it is done asynchronously
292
     * @Rest\Put("/api/usage/{tool}/{project}/{token}")
293
     * @param  string $tool    Internal name of tool
294
     * @param  string $project Project domain such as en.wikipedia.org
295
     * @param  string $token   Unique token for this request, so we don't have people
296
     *                         meddling with these statistics
297
     * @return View
298
     */
299
    public function recordUsage($tool, $project, $token)
300
    {
301
        // Validate token
302
        if (!$this->isCsrfTokenValid('intention', $token)) {
303
            return new View(
304
                [],
305
                Response::HTTP_FORBIDDEN
306
            );
307
        }
308
309
        // Don't update counts for tools that aren't enabled
310
        if (!$this->container->getParameter("enable.$tool")) {
311
            return new View(
312
                [
313
                    'error' => 'This tool is disabled'
314
                ],
315
                Response::HTTP_FORBIDDEN
316
            );
317
        }
318
319
        $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...
320
        $date =  date('Y-m-d');
321
322
        // Increment count in timeline
323
        $existsSql = "SELECT 1 FROM usage_timeline
324
                      WHERE date = '$date'
325
                      AND tool = '$tool'";
326
327 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...
328
            $createSql = "INSERT INTO usage_timeline
329
                          VALUES(NULL, '$date', '$tool', 1)";
330
            $conn->query($createSql);
331
        } else {
332
            $updateSql = "UPDATE usage_timeline
333
                          SET count = count + 1
334
                          WHERE tool = '$tool'
335
                          AND date = '$date'";
336
            $conn->query($updateSql);
337
        }
338
339
        // Update per-project usage, if applicable
340
        if (!$this->container->getParameter('app.single_wiki')) {
341
            $existsSql = "SELECT 1 FROM usage_projects
342
                          WHERE tool = '$tool'
343
                          AND project = '$project'";
344
345 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...
346
                $createSql = "INSERT INTO usage_projects
347
                              VALUES(NULL, '$tool', '$project', 1)";
348
                $conn->query($createSql);
349
            } else {
350
                $updateSql = "UPDATE usage_projects
351
                              SET count = count + 1
352
                              WHERE tool = '$tool'
353
                              AND project = '$project'";
354
                $conn->query($updateSql);
355
            }
356
        }
357
358
        return new View(
359
            [],
360
            Response::HTTP_NO_CONTENT
361
        );
362
    }
363
}
364