Completed
Push — master ( 77d109...dc7aae )
by Sam
05:53 queued 03:04
created

ApiController::articleInfo()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 50
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 50
rs 9.3333
c 0
b 0
f 0
cc 3
eloc 35
nc 3
nop 3
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}/{format}",
92
     *   requirements={"start" = "|\d{4}-\d{2}-\d{2}", "end" = "|\d{4}-\d{2}-\d{2}"}
93
     * )
94
     * @param string $project
95
     * @param string $username
96
     * @param int|string $namespace ID of the namespace, or 'all' for all namespaces
97
     * @param string [$start] In the format YYYY-MM-DD
98
     * @param string [$end] In the format YYYY-MM-DD
99
     * @param int $offset For pagination, offset results by N edits
100
     * @param string $format 'json' or 'html'
101
     * @return View
102
     */
103
    public function nonautomatedEdits(
104
        $project,
105
        $username,
106
        $namespace,
107
        $start = '',
108
        $end = '',
109
        $offset = 0,
110
        $format = 'json'
111
    ) {
112
        $twig = $this->container->get('twig');
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
        if ($format === 'html') {
118
            $edits = array_map(function ($attrs) use ($project, $username) {
119
                $nsName = '';
120
                if ($attrs['page_namespace']) {
121
                    $nsName = $project->getNamespaces()[$attrs['page_namespace']];
122
                }
123
                $page = $project->getRepository()
124
                    ->getPage($project, $nsName . ':' . $attrs['page_title']);
125
                $attrs['id'] = $attrs['rev_id'];
126
                $attrs['username'] = $username;
127
                return new Edit($page, $attrs);
128
            }, $data);
129
130
            $data = $twig->render('api/automated_edits.html.twig', [
131
                'edits' => $edits,
132
                'project' => $project,
133
            ]);
134
        }
135
136
        return new View(
137
            ['data' => $data],
138
            Response::HTTP_OK
139
        );
140
    }
141
142
    /**
143
     * Get basic info on a given article.
144
     * @Rest\Get("/api/articleinfo/{project}/{article}/{format}")
145
     * @param string $project
146
     * @param string $article
147
     * @param string $format 'json' or 'wikitext'
148
     * @return View
149
     */
150
    public function articleInfo($project, $article, $format = 'json')
151
    {
152
        /** @var integer Number of days to query for pageviews */
153
        $pageviewsOffset = 30;
154
155
        $project = ProjectRepository::getProject($project, $this->container);
156
        if (!$project->exists()) {
157
            return new View(
158
                ['error' => "$project is not a valid project"],
159
                Response::HTTP_NOT_FOUND
160
            );
161
        }
162
163
        $page = new Page($project, $article);
164
        $pageRepo = new PagesRepository();
165
        $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...
166
        $page->setRepository($pageRepo);
167
168
        $info = $page->getBasicEditingInfo();
169
        $creationDateTime = DateTime::createFromFormat('YmdHis', $info['created_at']);
170
        $modifiedDateTime = DateTime::createFromFormat('YmdHis', $info['modified_at']);
171
        $secsSinceLastEdit = $modifiedDateTime->getTimestamp() - $creationDateTime->getTimestamp();
172
173
        $data = [
174
            'revisions' => (int) $info['num_edits'],
175
            'editors' => (int) $info['num_editors'],
176
            'author' => $info['author'],
177
            'author_editcount' => (int) $info['author_editcount'],
178
            'created_at' => $creationDateTime->format('Y-m-d H:i'),
179
            'modified_at' => $modifiedDateTime->format('Y-m-d H:i'),
180
            'secs_since_last_edit' => $secsSinceLastEdit,
181
            'watchers' => (int) $page->getWatchers(),
182
            'pageviews' => $page->getLastPageviews($pageviewsOffset),
183
            'pageviews_offset' => $pageviewsOffset,
184
        ];
185
186
        if ($format === 'wikitext') {
187
            $twig = $this->container->get('twig');
188
            $data = $twig->render('api/gadget.wikitext.twig', [
189
                'data' => $data,
190
                'project' => $project,
191
                'page' => $page,
192
            ]);
193
        }
194
195
        return new View(
196
            ['data' => $data],
197
            Response::HTTP_OK
198
        );
199
    }
200
201
    /**
202
     * Record usage of a particular XTools tool. This is called automatically
203
     *   in base.html.twig via JavaScript so that it is done asynchronously
204
     * @Rest\Put("/api/usage/{tool}/{project}/{token}")
205
     * @param  string $tool    Internal name of tool
206
     * @param  string $project Project domain such as en.wikipedia.org
207
     * @param  string $token   Unique token for this request, so we don't have people
208
     *                         meddling with these statistics
209
     * @return View
210
     */
211
    public function recordUsage($tool, $project, $token)
212
    {
213
        // Validate token
214
        if (!$this->isCsrfTokenValid('intention', $token)) {
215
            return new View(
216
                [],
217
                Response::HTTP_FORBIDDEN
218
            );
219
        }
220
221
        // Don't update counts for tools that aren't enabled
222
        if (!$this->container->getParameter("enable.$tool")) {
223
            return new View(
224
                [
225
                    'error' => 'This tool is disabled'
226
                ],
227
                Response::HTTP_FORBIDDEN
228
            );
229
        }
230
231
        $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...
232
        $date =  date('Y-m-d');
233
234
        // Increment count in timeline
235
        $existsSql = "SELECT 1 FROM usage_timeline
236
                      WHERE date = '$date'
237
                      AND tool = '$tool'";
238
239 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...
240
            $createSql = "INSERT INTO usage_timeline
241
                          VALUES(NULL, '$date', '$tool', 1)";
242
            $conn->query($createSql);
243
        } else {
244
            $updateSql = "UPDATE usage_timeline
245
                          SET count = count + 1
246
                          WHERE tool = '$tool'
247
                          AND date = '$date'";
248
            $conn->query($updateSql);
249
        }
250
251
        // Update per-project usage, if applicable
252
        if (!$this->container->getParameter('app.single_wiki')) {
253
            $existsSql = "SELECT 1 FROM usage_projects
254
                          WHERE tool = '$tool'
255
                          AND project = '$project'";
256
257 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...
258
                $createSql = "INSERT INTO usage_projects
259
                              VALUES(NULL, '$tool', '$project', 1)";
260
                $conn->query($createSql);
261
            } else {
262
                $updateSql = "UPDATE usage_projects
263
                              SET count = count + 1
264
                              WHERE tool = '$tool'
265
                              AND project = '$project'";
266
                $conn->query($updateSql);
267
            }
268
        }
269
270
        return new View(
271
            [],
272
            Response::HTTP_NO_CONTENT
273
        );
274
    }
275
}
276