Passed
Push — master ( e585a1...9de1dc )
by MusikAnimal
04:18
created

ArticleInfoController   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 267
Duplicated Lines 6.74 %

Test Coverage

Coverage 50%

Importance

Changes 0
Metric Value
dl 18
loc 267
ccs 9
cts 18
cp 0.5
rs 10
c 0
b 0
f 0
wmc 20

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getToolShortname() 0 3 1
B resultAction() 0 53 6
B gadgetAction() 0 42 3
A getApiHtmlResponse() 0 16 1
A indexAction() 16 16 3
B articleInfoApiAction() 0 69 6

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
/**
3
 * This file contains only the ArticleInfoController class.
4
 */
5
6
namespace AppBundle\Controller;
7
8
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
9
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
10
use Symfony\Component\HttpFoundation\Request;
11
use Symfony\Component\HttpFoundation\Response;
12
use Symfony\Component\HttpFoundation\JsonResponse;
13
use Symfony\Component\HttpFoundation\RedirectResponse;
14
use Symfony\Component\Process\Process;
15
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
16
use Xtools\ProjectRepository;
17
use Xtools\ArticleInfo;
18
use Xtools\Project;
19
use Xtools\Page;
20
use DateTime;
21
use Xtools\ArticleInfoRepository;
22
23
/**
24
 * This controller serves the search form and results for the ArticleInfo tool
25
 */
26
class ArticleInfoController extends XtoolsController
27
{
28
    /**
29
     * Get the tool's shortname.
30
     * @return string
31
     * @codeCoverageIgnore
32
     */
33
    public function getToolShortname()
34
    {
35
        return 'articleinfo';
36
    }
37
38
    /**
39
     * The search form.
40
     * @Route("/articleinfo", name="articleinfo")
41
     * @Route("/articleinfo", name="articleInfo")
42
     * @Route("/articleinfo/", name="articleInfoSlash")
43
     * @Route("/articleinfo/index.php", name="articleInfoIndexPhp")
44
     * @Route("/articleinfo/{project}", name="ArticleInfoProject")
45
     * @param Request $request The HTTP request.
46
     * @return Response
47
     */
48 1 View Code Duplication
    public function indexAction(Request $request)
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...
49
    {
50 1
        $params = $this->parseQueryParams($request);
51
52 1
        if (isset($params['project']) && isset($params['article'])) {
53
            return $this->redirectToRoute('ArticleInfoResult', $params);
54
        }
55
56
        // Convert the given project (or default project) into a Project instance.
57 1
        $params['project'] = $this->getProjectFromQuery($params);
58
59 1
        return $this->render('articleInfo/index.html.twig', [
60 1
            'xtPage' => 'articleinfo',
61 1
            'xtPageTitle' => 'tool-articleinfo',
62 1
            'xtSubtitle' => 'tool-articleinfo-desc',
63 1
            'project' => $params['project'],
64
        ]);
65
    }
66
67
    /**
68
     * Generate ArticleInfo gadget script for use on-wiki. This automatically points the
69
     * script to this installation's API. Pass ?uglify=1 to uglify the code.
70
     *
71
     * @Route("/articleinfo-gadget.js", name="ArticleInfoGadget")
72
     * @link https://www.mediawiki.org/wiki/XTools#ArticleInfo_gadget
73
     *
74
     * @param Request $request The HTTP request
75
     * @return Response
76
     * @codeCoverageIgnore
77
     */
78
    public function gadgetAction(Request $request)
79
    {
80
        $rendered = $this->renderView('articleInfo/articleinfo.js.twig');
81
82
        // SUPER hacky, but it works and is safe.
83
        if ($request->query->get('uglify') != '') {
84
            // $ and " need to be escaped.
85
            $rendered = str_replace('$', '\$', trim($rendered));
86
            $rendered = str_replace('"', '\"', trim($rendered));
87
88
            // Uglify temporary file.
89
            $tmpFile = sys_get_temp_dir() . '/xtools_articleinfo_gadget.js';
90
            $script = "echo \"$rendered\" | tee $tmpFile >/dev/null && ";
91
            $script .= $this->get('kernel')->getRootDir() .
92
                "/Resources/node_modules/uglify-es/bin/uglifyjs $tmpFile --mangle " .
93
                "&& rm $tmpFile >/dev/null";
94
            $process = new Process($script);
95
            $process->run();
96
97
            // Check for errors.
98
            $errorOutput = $process->getErrorOutput();
99
            if ($errorOutput != '') {
100
                $response = new \Symfony\Component\HttpFoundation\Response(
101
                    "Error generating uglified JS. The server said:\n\n$errorOutput"
102
                );
103
                return $response;
104
            }
105
106
            // Remove escaping.
107
            $rendered = str_replace('\$', '$', trim($process->getOutput()));
108
            $rendered = str_replace('\"', '"', trim($rendered));
109
110
            // Add comment after uglifying since it removes comments.
111
            $rendered = "/**\n * This code was automatically generated and should not " .
112
                "be manually edited.\n * For updates, please copy and paste from " .
113
                $this->generateUrl('ArticleInfoGadget', ['uglify' => 1], UrlGeneratorInterface::ABSOLUTE_URL) .
114
                "\n * Released under GPL v3 license.\n */\n" . $rendered;
115
        }
116
117
        $response = new \Symfony\Component\HttpFoundation\Response($rendered);
118
        $response->headers->set('Content-Type', 'text/javascript');
119
        return $response;
120
    }
121
122
    /**
123
     * Display the results.
124
     * @Route("/articleinfo/{project}/{article}", name="ArticleInfoResult", requirements={"article"=".+"})
125
     * @param Request $request The HTTP request.
126
     * @param string $article
127
     * @return Response
128
     * @codeCoverageIgnore
129
     */
130
    public function resultAction(Request $request, $article)
131
    {
132
        // In this case only the project is validated.
133
        $ret = $this->validateProjectAndUser($request);
134
        if ($ret instanceof RedirectResponse) {
135
            return $ret;
136
        } else {
137
            $project = $ret[0];
138
        }
139
140
        $page = $this->getAndValidatePage($project, $article);
141
        if ($page instanceof RedirectResponse) {
142
            return $page;
143
        }
144
145
        $articleInfoRepo = new ArticleInfoRepository();
146
        $articleInfoRepo->setContainer($this->container);
147
        $articleInfo = new ArticleInfo($page, $this->container);
148
        $articleInfo->setRepository($articleInfoRepo);
149
150
        $articleInfo->prepareData();
151
152
        $maxRevisions = $this->container->getParameter('app.max_page_revisions');
153
154
        // Show message if we hit the max revisions.
155
        if ($articleInfo->tooManyRevisions()) {
156
            // FIXME: i18n number_format?
157
            $this->addFlash('notice', ['too-many-revisions', number_format($maxRevisions), $maxRevisions]);
0 ignored issues
show
Bug introduced by
array('too-many-revision...isions), $maxRevisions) of type array<integer,string|mixed> is incompatible with the type string expected by parameter $message of Symfony\Bundle\Framework...\Controller::addFlash(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

157
            $this->addFlash('notice', /** @scrutinizer ignore-type */ ['too-many-revisions', number_format($maxRevisions), $maxRevisions]);
Loading history...
158
        }
159
160
        $ret = [
161
            'xtPage' => 'articleinfo',
162
            'xtTitle' => $page->getTitle(),
163
            'project' => $project,
164
            'editorlimit' => $request->query->get('editorlimit', 20),
165
            'botlimit' => $request->query->get('botlimit', 10),
166
            'pageviewsOffset' => 60,
167
            'ai' => $articleInfo,
168
            'page' => $page,
169
        ];
170
171
        // Output the relevant format template.
172
        $format = $request->query->get('format', 'html');
173
        if ($format == '') {
174
            // The default above doesn't work when the 'format' parameter is blank.
175
            $format = 'html';
176
        }
177
        $response = $this->render("articleInfo/result.$format.twig", $ret);
178
        if ($format == 'wikitext') {
179
            $response->headers->set('Content-Type', 'text/plain');
180
        }
181
182
        return $response;
183
    }
184
185
    /************************ API endpoints ************************/
186
187
    /**
188
     * Get basic info on a given article.
189
     * @Route("/api/articleinfo/{project}/{article}", requirements={"article"=".+"})
190
     * @Route("/api/page/articleinfo/{project}/{article}", requirements={"article"=".+"})
191
     * @param Request $request The HTTP request.
192
     * @param string $project
193
     * @param string $article
194
     * @return View
0 ignored issues
show
Bug introduced by
The type AppBundle\Controller\View was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
195
     * See ArticleInfoControllerTest::testArticleInfoApi()
196
     * @codeCoverageIgnore
197
     */
198
    public function articleInfoApiAction(Request $request, $project, $article)
199
    {
200
        /** @var integer Number of days to query for pageviews */
201
        $pageviewsOffset = 30;
202
203
        $project = ProjectRepository::getProject($project, $this->container);
204
        if (!$project->exists()) {
205
            return new JsonResponse(
206
                ['error' => "$project is not a valid project"],
207
                Response::HTTP_NOT_FOUND
208
            );
209
        }
210
211
        $page = $this->getAndValidatePage($project, $article);
212
        if ($page instanceof RedirectResponse) {
213
            return new JsonResponse(
214
                ['error' => "$article was not found"],
215
                Response::HTTP_NOT_FOUND
216
            );
217
        }
218
219
        $data = [
220
            'project' => $project->getDomain(),
221
            'page' => $page->getTitle(),
222
            'watchers' => (int) $page->getWatchers(),
223
            'pageviews' => $page->getLastPageviews($pageviewsOffset),
224
            'pageviews_offset' => $pageviewsOffset,
225
        ];
226
227
        try {
228
            $info = $page->getBasicEditingInfo();
229
        } catch (\Doctrine\DBAL\Exception\DriverException $e) {
230
            /**
231
             * The query most likely exceeded the maximum query time,
232
             * so we'll abort and give only info retrived by the API.
233
             */
234
            $data['error'] = 'Unable to fetch revision data. The query may have timed out.';
235
        }
236
237
        if (isset($info)) {
238
            $creationDateTime = DateTime::createFromFormat('YmdHis', $info['created_at']);
239
            $modifiedDateTime = DateTime::createFromFormat('YmdHis', $info['modified_at']);
240
            $secsSinceLastEdit = (new DateTime)->getTimestamp() - $modifiedDateTime->getTimestamp();
241
242
            $data = array_merge($data, [
243
                'revisions' => (int) $info['num_edits'],
244
                'editors' => (int) $info['num_editors'],
245
                'author' => $info['author'],
246
                'author_editcount' => (int) $info['author_editcount'],
247
                'created_at' => $creationDateTime->format('Y-m-d'),
248
                'created_rev_id' => $info['created_rev_id'],
249
                'modified_at' => $modifiedDateTime->format('Y-m-d H:i'),
250
                'secs_since_last_edit' => $secsSinceLastEdit,
251
                'last_edit_id' => (int) $info['modified_rev_id'],
252
            ]);
253
        }
254
255
        if ($request->query->get('format') === 'html') {
256
            return $this->getApiHtmlResponse($project, $page, $data);
257
        }
258
259
        $body = array_merge([
260
            'project' => $project->getDomain(),
261
            'page' => $page->getTitle(),
262
        ], $data);
263
264
        return new JsonResponse(
265
            $body,
266
            Response::HTTP_OK
267
        );
268
    }
269
270
    /**
271
     * Get the Response for the HTML output of the ArticleInfo API action.
272
     * @param  Project  $project
273
     * @param  Page     $page
274
     * @param  string[] $data The pre-fetched data.
275
     * @return Response
276
     */
277
    private function getApiHtmlResponse(Project $project, Page $page, $data)
278
    {
279
        $response = $this->render('articleInfo/api.html.twig', [
280
            'data' => $data,
281
            'project' => $project,
282
            'page' => $page,
283
        ]);
284
285
        // All /api routes by default respond with a JSON content type.
286
        $response->headers->set('Content-Type', 'text/html');
287
288
        // This endpoint is hit constantly and user could be browsing the same page over
289
        // and over (popular noticeboard, for instance), so offload brief caching to browser.
290
        $response->setClientTtl(350);
291
292
        return $response;
293
    }
294
}
295