Passed
Pull Request — main (#414)
by MusikAnimal
31:26 queued 08:10
created

ArticleInfoController::resultAction()   B

Complexity

Conditions 6
Paths 17

Size

Total Lines 46
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
eloc 25
nc 17
nop 1
dl 0
loc 46
ccs 0
cts 0
cp 0
crap 42
rs 8.8977
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the ArticleInfoController class.
4
 */
5
6
declare(strict_types=1);
7
8
namespace AppBundle\Controller;
9
10
use AppBundle\Exception\XtoolsHttpException;
11
use AppBundle\Helper\I18nHelper;
12
use AppBundle\Model\ArticleInfo;
13
use AppBundle\Model\Authorship;
14
use AppBundle\Model\Page;
15
use AppBundle\Model\Project;
16
use AppBundle\Repository\ArticleInfoRepository;
17
use GuzzleHttp\Exception\ServerException;
18
use Symfony\Component\HttpFoundation\JsonResponse;
19
use Symfony\Component\HttpFoundation\Request;
20
use Symfony\Component\HttpFoundation\Response;
21
use Symfony\Component\Routing\Annotation\Route;
22
23
/**
24
 * This controller serves the search form and results for the ArticleInfo tool
25
 */
26
class ArticleInfoController extends XtoolsController
27
{
28
    /** @var ArticleInfo The ArticleInfo class that does all the work. */
29
    protected $articleInfo;
30
31
    /**
32
     * Get the name of the tool's index route. This is also the name of the associated model.
33
     * @return string
34
     * @codeCoverageIgnore
35
     */
36
    public function getIndexRoute(): string
37
    {
38
        return 'ArticleInfo';
39
    }
40
41
    /**
42
     * The search form.
43
     * @Route("/articleinfo", name="ArticleInfo")
44
     * @Route("/articleinfo/index.php", name="articleInfoIndexPhp")
45
     * @Route("/articleinfo/{project}", name="ArticleInfoProject")
46
     * @return Response
47
     */
48
    public function indexAction(): Response
49 1
    {
50
        if (isset($this->params['project']) && isset($this->params['page'])) {
51 1
            return $this->redirectToRoute('ArticleInfoResult', $this->params);
52
        }
53
54
        return $this->render('articleInfo/index.html.twig', array_merge([
55 1
            'xtPage' => 'ArticleInfo',
56 1
            'xtPageTitle' => 'tool-articleinfo',
57 1
            'xtSubtitle' => 'tool-articleinfo-desc',
58 1
            'project' => $this->project,
59 1
60
            // Defaults that will get overridden if in $params.
61
            'start' => '',
62 1
            'end' => '',
63 1
            'page' => '',
64 1
        ], $this->params, ['project' => $this->project]));
65 1
    }
66
67
    /**
68
     * Setup the ArticleInfo instance and its Repository.
69
     */
70
    private function setupArticleInfo(): void
71
    {
72
        if (isset($this->articleInfo)) {
73
            return;
74
        }
75
76
        $articleInfoRepo = new ArticleInfoRepository();
77
        $articleInfoRepo->setContainer($this->container);
78
        $this->articleInfo = new ArticleInfo($this->page, $this->container, $this->start, $this->end);
79
        $this->articleInfo->setRepository($articleInfoRepo);
80
        $this->articleInfo->setI18nHelper($this->container->get('app.i18n_helper'));
0 ignored issues
show
Bug introduced by
It seems like $this->container->get('app.i18n_helper') can also be of type null; however, parameter $i18n of AppBundle\Model\ArticleInfo::setI18nHelper() does only seem to accept AppBundle\Helper\I18nHelper, maybe add an additional type check? ( Ignorable by Annotation )

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

80
        $this->articleInfo->setI18nHelper(/** @scrutinizer ignore-type */ $this->container->get('app.i18n_helper'));
Loading history...
81
    }
82
83
    /**
84
     * Generate ArticleInfo gadget script for use on-wiki. This automatically points the
85
     * script to this installation's API. Pass ?uglify=1 to uglify the code.
86
     *
87
     * @Route("/articleinfo-gadget.js", name="ArticleInfoGadget")
88
     * @link https://www.mediawiki.org/wiki/XTools/ArticleInfo_gadget
89
     *
90
     * @param Request $request The HTTP request
91
     * @return Response
92
     * @codeCoverageIgnore
93
     */
94
    public function gadgetAction(Request $request): Response
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

94
    public function gadgetAction(/** @scrutinizer ignore-unused */ Request $request): Response

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

Loading history...
95
    {
96
        $rendered = $this->renderView('articleInfo/articleinfo.js.twig');
97
        $response = new Response($rendered);
98
        $response->headers->set('Content-Type', 'text/javascript');
99
        return $response;
100
    }
101
102
    /**
103
     * Display the results in given date range.
104
     * @Route(
105
     *    "/articleinfo/{project}/{page}/{start}/{end}", name="ArticleInfoResult",
106
     *     requirements={
107
     *         "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?)?$",
108
     *         "start"="|\d{4}-\d{2}-\d{2}",
109
     *         "end"="|\d{4}-\d{2}-\d{2}",
110
     *     },
111
     *     defaults={
112
     *         "start"=false,
113
     *         "end"=false,
114
     *     }
115
     * )
116
     * @param I18nHelper $i18n
117
     * @return Response
118
     * @codeCoverageIgnore
119
     */
120
    public function resultAction(I18nHelper $i18n): Response
121
    {
122
        if (!$this->isDateRangeValid($this->page, $this->start, $this->end)) {
123
            $this->addFlashMessage('notice', 'date-range-outside-revisions');
124
125
            return $this->redirectToRoute('ArticleInfo', [
126
                'project' => $this->request->get('project'),
127
            ]);
128
        }
129
130
        $this->setupArticleInfo();
131
        $this->articleInfo->prepareData();
132
133
        $maxRevisions = $this->container->getParameter('app.max_page_revisions');
134
135
        // Show message if we hit the max revisions.
136
        if ($this->articleInfo->tooManyRevisions()) {
137
            $this->addFlashMessage('notice', 'too-many-revisions', [
138
                $i18n->numberFormat($maxRevisions),
139
                $maxRevisions,
140
            ]);
141
        }
142
143
        // For when there is very old data (2001 era) which may cause miscalculations.
144
        if ($this->articleInfo->getFirstEdit()->getYear() < 2003) {
145
            $this->addFlashMessage('warning', 'old-page-notice');
146
        }
147
148
        // When all username info has been hidden (see T303724).
149
        if (0 === $this->articleInfo->getNumEditors()) {
150
            $this->addFlashMessage('warning', 'error-usernames-missing');
151
        }
152
153
        $ret = [
154
            'xtPage' => 'ArticleInfo',
155
            'xtTitle' => $this->page->getTitle(),
156
            'project' => $this->project,
157
            'editorlimit' => $this->request->query->get('editorlimit', 20),
158
            'botlimit' => $this->request->query->get('botlimit', 10),
159
            'pageviewsOffset' => 60,
160
            'ai' => $this->articleInfo,
161
            'showAuthorship' => Authorship::isSupportedPage($this->page) && $this->articleInfo->getNumEditors() > 0,
162
        ];
163
164
        // Output the relevant format template.
165
        return $this->getFormattedResponse('articleInfo/result', $ret);
166
    }
167
168
    /**
169
     * Check if there were any revisions of given page in given date range.
170
     * @param Page $page
171
     * @param false|int $start
172
     * @param false|int $end
173
     * @return bool
174
     */
175
    private function isDateRangeValid(Page $page, $start, $end): bool
176
    {
177
        return $page->getNumRevisions(null, $start, $end) > 0;
178
    }
179
180
    /************************ API endpoints ************************/
181
182
    /**
183
     * Get basic info on a given article.
184
     * @Route(
185
     *     "/api/articleinfo/{project}/{page}",
186
     *     name="ArticleInfoApiAction",
187
     *     requirements={"page"=".+"}
188
     * )
189
     * @Route("/api/page/articleinfo/{project}/{page}", requirements={"page"=".+"})
190
     * @return Response|JsonResponse
191
     * See ArticleInfoControllerTest::testArticleInfoApi()
192
     * @codeCoverageIgnore
193
     */
194
    public function articleInfoApiAction(): Response
195
    {
196
        $this->recordApiUsage('page/articleinfo');
197
198
        $this->setupArticleInfo();
199
        $data = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
200
201
        try {
202
            $data = $this->articleInfo->getArticleInfoApiData($this->project, $this->page);
203
        } catch (ServerException $e) {
204
            // The Wikimedia action API can fail for any number of reasons. To our users
205
            // any ServerException means the data could not be fetched, so we capture it here
206
            // to avoid the flood of automated emails when the API goes down, etc.
207
            $data['error'] = $this->i18n->msg('api-error', [$this->project->getDomain()]);
208
        }
209
210
        if ('html' === $this->request->query->get('format')) {
211
            return $this->getApiHtmlResponse($this->project, $this->page, $data);
212
        }
213
214
        return $this->getFormattedApiResponse($data);
215
    }
216
217
    /**
218
     * Get the Response for the HTML output of the ArticleInfo API action.
219
     * @param Project $project
220
     * @param Page $page
221
     * @param string[] $data The pre-fetched data.
222
     * @return Response
223
     * @codeCoverageIgnore
224
     */
225
    private function getApiHtmlResponse(Project $project, Page $page, array $data): Response
226
    {
227
        $response = $this->render('articleInfo/api.html.twig', [
228
            'project' => $project,
229
            'page' => $page,
230
            'data' => $data,
231
        ]);
232
233
        // All /api routes by default respond with a JSON content type.
234
        $response->headers->set('Content-Type', 'text/html');
235
236
        // This endpoint is hit constantly and user could be browsing the same page over
237
        // and over (popular noticeboard, for instance), so offload brief caching to browser.
238
        $response->setClientTtl(350);
239
240
        return $response;
241
    }
242
243
    /**
244
     * Get prose statistics for the given article.
245
     * @Route(
246
     *     "/api/page/prose/{project}/{page}",
247
     *     name="PageApiProse",
248
     *     requirements={"page"=".+"}
249
     * )
250
     * @return JsonResponse
251
     * @codeCoverageIgnore
252
     */
253
    public function proseStatsApiAction(): JsonResponse
254
    {
255
        $this->recordApiUsage('page/prose');
256
257
        $this->setupArticleInfo();
258
        return $this->getFormattedApiResponse($this->articleInfo->getProseStats());
259
    }
260
261
    /**
262
     * Get the page assessments of one or more pages, along with various related metadata.
263
     * @Route(
264
     *     "/api/page/assessments/{project}/{pages}",
265
     *     name="PageApiAssessments",
266
     *     requirements={"pages"=".+"}
267
     * )
268
     * @param string $pages May be multiple pages separated by pipes, e.g. Foo|Bar|Baz
269
     * @return JsonResponse
270
     * @codeCoverageIgnore
271
     */
272
    public function assessmentsApiAction(string $pages): JsonResponse
273
    {
274
        $this->recordApiUsage('page/assessments');
275
276
        $pages = explode('|', $pages);
277
        $out = [];
278
279
        foreach ($pages as $pageTitle) {
280
            try {
281
                $page = $this->validatePage($pageTitle);
282
                $assessments = $page->getProject()
283
                    ->getPageAssessments()
284
                    ->getAssessments($page);
285
286
                $out[$page->getTitle()] = $this->request->get('classonly')
287
                    ? $assessments['assessment']
288
                    : $assessments;
289
            } catch (XtoolsHttpException $e) {
290
                $out[$pageTitle] = false;
291
            }
292
        }
293
294
        return $this->getFormattedApiResponse($out);
295
    }
296
297
    /**
298
     * Get number of in and outgoing links and redirects to the given page.
299
     * @Route(
300
     *     "/api/page/links/{project}/{page}",
301
     *     name="PageApiLinks",
302
     *     requirements={"page"=".+"}
303
     * )
304
     * @return JsonResponse
305
     * @codeCoverageIgnore
306
     */
307
    public function linksApiAction(): JsonResponse
308
    {
309
        $this->recordApiUsage('page/links');
310
        return $this->getFormattedApiResponse($this->page->countLinksAndRedirects());
311
    }
312
313
    /**
314
     * Get the top editors to a page.
315
     * @Route(
316
     *     "/api/page/top_editors/{project}/{page}/{start}/{end}/{limit}", name="PageApiTopEditors",
317
     *     requirements={
318
     *         "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?(?:\/(\d+))?)?$",
319
     *         "start"="|\d{4}-\d{2}-\d{2}",
320
     *         "end"="|\d{4}-\d{2}-\d{2}",
321
     *         "limit"="|\d+"
322
     *     },
323
     *     defaults={
324
     *         "start"=false,
325
     *         "end"=false,
326
     *         "limit"=20,
327
     *     }
328
     * )
329
     * @return JsonResponse
330
     * @codeCoverageIgnore
331
     */
332
    public function topEditorsApiAction(): JsonResponse
333
    {
334
        $this->recordApiUsage('page/top_editors');
335
336
        $this->setupArticleInfo();
337
        $topEditors = $this->articleInfo->getTopEditorsByEditCount(
338
            (int)$this->limit,
339
            '' != $this->request->query->get('nobots')
340
        );
341
342
        return $this->getFormattedApiResponse([
343
            'top_editors' => $topEditors,
344
        ]);
345
    }
346
}
347