Completed
Push — master ( 7c7725...4cc8fe )
by MusikAnimal
02:21
created

ApiController::recordUsage()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 64
Code Lines 37

Duplication

Lines 22
Ratio 34.38 %

Importance

Changes 0
Metric Value
dl 22
loc 64
rs 8.6346
c 0
b 0
f 0
cc 6
eloc 37
nc 8
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/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 /project/normalize 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
            'total_editcount' => $user->countEdits($project, $namespace, $start, $end),
121
        ];
122
123
        if ($tools != '') {
124
            $tools = $user->getAutomatedCounts($project, $namespace, $start, $end);
125
            $res['automated_editcount'] = 0;
126
            foreach ($tools as $tool) {
127
                $res['automated_editcount'] += $tool['count'];
128
            }
129
            $res['automated_tools'] = $tools;
130
        } else {
131
            $res['automated_editcount'] = $user->countAutomatedEdits($project, $namespace, $start, $end);
132
        }
133
134
        $res['nonautomated_editcount'] = $res['total_editcount'] - $res['automated_editcount'];
135
136
        $view = View::create()->setStatusCode(Response::HTTP_OK);
137
        $view->setData($res);
138
139
        return $view->setFormat('json');
140
    }
141
142
    /**
143
     * Get non-automated edits for the given user.
144
     * @Rest\Get(
145
     *   "/api/user/nonautomated_edits/{project}/{username}/{namespace}/{start}/{end}/{offset}",
146
     *   requirements={"start" = "|\d{4}-\d{2}-\d{2}", "end" = "|\d{4}-\d{2}-\d{2}"}
147
     * )
148
     * @param Request $request The HTTP request.
149
     * @param string $project
150
     * @param string $username
151
     * @param int|string $namespace ID of the namespace, or 'all' for all namespaces
152
     * @param string $start In the format YYYY-MM-DD
153
     * @param string $end In the format YYYY-MM-DD
154
     * @param int $offset For pagination, offset results by N edits
155
     * @return View
156
     */
157
    public function nonautomatedEdits(
158
        Request $request,
159
        $project,
160
        $username,
161
        $namespace,
162
        $start = '',
163
        $end = '',
164
        $offset = 0
165
    ) {
166
        $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...
167
        $project = ProjectRepository::getProject($project, $this->container);
168
        $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...
169
170
        // Reject if they've made too many edits.
171
        if ($user->hasTooManyEdits($project)) {
172
            if ($request->query->get('format') !== 'html') {
173
                return new View(
174
                    [
175
                        'error' => 'Unable to show any data. User has made over ' .
176
                            $user->maxEdits() . ' edits.',
177
                    ],
178
                    Response::HTTP_FORBIDDEN
179
                );
180
            }
181
182
            $data = false;
183
            $edits = false;
184
        } else {
185
            $data = $user->getNonautomatedEdits($project, $namespace, $start, $end, $offset);
186
        }
187
188
        $view = View::create()->setStatusCode(Response::HTTP_OK);
189
190
        if ($request->query->get('format') === 'html') {
191
            if ($data) {
192
                $edits = array_map(function ($attrs) use ($project, $username) {
193
                    $page = $project->getRepository()
194
                        ->getPage($project, $attrs['full_page_title']);
195
                    $pageTitles[] = $attrs['full_page_title'];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$pageTitles was never initialized. Although not strictly required by PHP, it is generally a good practice to add $pageTitles = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
196
                    $attrs['id'] = $attrs['rev_id'];
197
                    $attrs['username'] = $username;
198
                    return new Edit($page, $attrs);
199
                }, $data);
200
            }
201
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/nonautomated_edits.html.twig');
204
            $view->setTemplateData([
205
                'edits' => $edits,
0 ignored issues
show
Bug introduced by
The variable $edits 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...
206
                'project' => $project,
207
                'maxEdits' => $user->maxEdits(),
208
            ]);
209
            $view->setFormat('html');
210
        } else {
211
            $res = [
212
                'project' => $project->getDomain(),
213
                'username' => $user->getUsername(),
214
            ];
215
            if ($namespace != '' && $namespace !== 'all') {
216
                $res['namespace'] = $namespace;
217
            }
218
            if ($start != '') {
219
                $res['start'] = $start;
220
            }
221
            if ($end != '') {
222
                $res['end'] = $end;
223
            }
224
            $res['offset'] = $offset;
225
            $res['nonautomated_edits'] = $data;
226
227
            $view->setData($res)->setFormat('json');
228
        }
229
230
        return $view;
231
    }
232
233
    /**
234
     * Get basic info on a given article.
235
     * @Rest\Get("/api/articleinfo/{project}/{article}", requirements={"article"=".+"})
236
     * @Rest\Get("/api/page/articleinfo/{project}/{article}", requirements={"article"=".+"})
237
     * @param Request $request The HTTP request.
238
     * @param string $project
239
     * @param string $article
240
     * @return View
241
     */
242
    public function articleInfo(Request $request, $project, $article)
243
    {
244
        /** @var integer Number of days to query for pageviews */
245
        $pageviewsOffset = 30;
246
247
        $project = ProjectRepository::getProject($project, $this->container);
248
        if (!$project->exists()) {
249
            return new View(
250
                ['error' => "$project is not a valid project"],
251
                Response::HTTP_NOT_FOUND
252
            );
253
        }
254
255
        $page = new Page($project, $article);
256
        $pageRepo = new PagesRepository();
257
        $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...
258
        $page->setRepository($pageRepo);
259
260
        if (!$page->exists()) {
261
            return new View(
262
                ['error' => "$article was not found"],
263
                Response::HTTP_NOT_FOUND
264
            );
265
        }
266
267
        $info = $page->getBasicEditingInfo();
268
        $creationDateTime = DateTime::createFromFormat('YmdHis', $info['created_at']);
269
        $modifiedDateTime = DateTime::createFromFormat('YmdHis', $info['modified_at']);
270
        $secsSinceLastEdit = (new DateTime)->getTimestamp() - $modifiedDateTime->getTimestamp();
271
272
        $data = [
273
            'revisions' => (int) $info['num_edits'],
274
            'editors' => (int) $info['num_editors'],
275
            'author' => $info['author'],
276
            'author_editcount' => (int) $info['author_editcount'],
277
            'created_at' => $creationDateTime->format('Y-m-d'),
278
            'created_rev_id' => $info['created_rev_id'],
279
            'modified_at' => $modifiedDateTime->format('Y-m-d H:i'),
280
            'secs_since_last_edit' => $secsSinceLastEdit,
281
            'last_edit_id' => (int) $info['modified_rev_id'],
282
            'watchers' => (int) $page->getWatchers(),
283
            'pageviews' => $page->getLastPageviews($pageviewsOffset),
284
            'pageviews_offset' => $pageviewsOffset,
285
        ];
286
287
        $view = View::create()->setStatusCode(Response::HTTP_OK);
288
289
        if ($request->query->get('format') === 'html') {
290
            $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...
291
            $view->setTemplate('api/articleinfo.html.twig');
292
            $view->setTemplateData([
293
                'data' => $data,
294
                'project' => $project,
295
                'page' => $page,
296
            ]);
297
            $view->setFormat('html');
298
        } else {
299
            $res = array_merge([
300
                'project' => $project->getDomain(),
301
                'page' => $page->getTitle(),
302
            ], $data);
303
            $view->setData($res)->setFormat('json');
304
        }
305
306
        return $view;
307
    }
308
}
309