Passed
Push — flex ( 91a41f )
by MusikAnimal
07:11
created

ArticleInfoController::assessmentsApiAction()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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

77
            /** @scrutinizer ignore-type */ $this->page,
Loading history...
78
            $this->start,
79
            $this->end
80
        );
81
    }
82
83
    /**
84
     * Generate ArticleInfo gadget script for use on-wiki. This automatically points the
85
     * script to this installation's API.
86
     *
87
     * @Route("/articleinfo-gadget.js", name="ArticleInfoGadget")
88
     * @link https://www.mediawiki.org/wiki/XTools/ArticleInfo_gadget
89
     *
90
     * @return Response
91
     * @codeCoverageIgnore
92
     */
93
    public function gadgetAction(): Response
94
    {
95
        $rendered = $this->renderView('articleInfo/articleinfo.js.twig');
96
        $response = new Response($rendered);
97
        $response->headers->set('Content-Type', 'text/javascript');
98
        return $response;
99
    }
100
101
    /**
102
     * Display the results in given date range.
103
     * @Route(
104
     *    "/articleinfo/{project}/{page}/{start}/{end}", name="ArticleInfoResult",
105
     *     requirements={
106
     *         "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?)?$",
107
     *         "start"="|\d{4}-\d{2}-\d{2}",
108
     *         "end"="|\d{4}-\d{2}-\d{2}",
109
     *     },
110
     *     defaults={
111
     *         "start"=false,
112
     *         "end"=false,
113
     *     }
114
     * )
115
     * @param ArticleInfoRepository $articleInfoRepo
116
     * @param AutomatedEditsHelper $autoEditsHelper
117
     * @return Response
118
     * @codeCoverageIgnore
119
     */
120
    public function resultAction(
121
        ArticleInfoRepository $articleInfoRepo,
122
        AutomatedEditsHelper $autoEditsHelper
123
    ): Response {
124
        if (!$this->isDateRangeValid($this->page, $this->start, $this->end)) {
0 ignored issues
show
Bug introduced by
It seems like $this->page can also be of type null; however, parameter $page of App\Controller\ArticleIn...ler::isDateRangeValid() does only seem to accept App\Model\Page, 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

124
        if (!$this->isDateRangeValid(/** @scrutinizer ignore-type */ $this->page, $this->start, $this->end)) {
Loading history...
125
            $this->addFlashMessage('notice', 'date-range-outside-revisions');
126
127
            return $this->redirectToRoute('ArticleInfo', [
128
                'project' => $this->request->get('project'),
129
            ]);
130
        }
131
132
        $this->setupArticleInfo($articleInfoRepo, $autoEditsHelper);
133
        $this->articleInfo->prepareData();
134
135
        $maxRevisions = $this->getParameter('app.max_page_revisions');
136
137
        // Show message if we hit the max revisions.
138
        if ($this->articleInfo->tooManyRevisions()) {
139
            $this->addFlashMessage('notice', 'too-many-revisions', [
140
                $this->i18n->numberFormat($maxRevisions),
141
                $maxRevisions,
142
            ]);
143
        }
144
145
        // For when there is very old data (2001 era) which may cause miscalculations.
146
        if ($this->articleInfo->getFirstEdit()->getYear() < 2003) {
147
            $this->addFlashMessage('warning', 'old-page-notice');
148
        }
149
150
        // When all username info has been hidden (see T303724).
151
        if (0 === $this->articleInfo->getNumEditors()) {
152
            $this->addFlashMessage('warning', 'error-usernames-missing');
153
        }
154
155
        $ret = [
156
            'xtPage' => 'ArticleInfo',
157
            'xtTitle' => $this->page->getTitle(),
0 ignored issues
show
Bug introduced by
The method getTitle() does not exist on null. ( Ignorable by Annotation )

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

157
            'xtTitle' => $this->page->/** @scrutinizer ignore-call */ getTitle(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
158
            'project' => $this->project,
159
            'editorlimit' => (int)$this->request->query->get('editorlimit', 20),
160
            'botlimit' => $this->request->query->get('botlimit', 10),
161
            'pageviewsOffset' => 60,
162
            'ai' => $this->articleInfo,
163
            'showAuthorship' => Authorship::isSupportedPage($this->page) && $this->articleInfo->getNumEditors() > 0,
0 ignored issues
show
Bug introduced by
It seems like $this->page can also be of type null; however, parameter $page of App\Model\Authorship::isSupportedPage() does only seem to accept App\Model\Page, 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

163
            'showAuthorship' => Authorship::isSupportedPage(/** @scrutinizer ignore-type */ $this->page) && $this->articleInfo->getNumEditors() > 0,
Loading history...
164
        ];
165
166
        // Output the relevant format template.
167
        return $this->getFormattedResponse('articleInfo/result', $ret);
168
    }
169
170
    /**
171
     * Check if there were any revisions of given page in given date range.
172
     * @param Page $page
173
     * @param false|int $start
174
     * @param false|int $end
175
     * @return bool
176
     */
177
    private function isDateRangeValid(Page $page, $start, $end): bool
178
    {
179
        return $page->getNumRevisions(null, $start, $end) > 0;
180
    }
181
182
    /************************ API endpoints ************************/
183
184
    /**
185
     * Get basic info on a given article.
186
     * @Route(
187
     *     "/api/articleinfo/{project}/{page}",
188
     *     name="ArticleInfoApiAction",
189
     *     requirements={"page"=".+"}
190
     * )
191
     * @Route("/api/page/articleinfo/{project}/{page}", requirements={"page"=".+"})
192
     * @param ArticleInfoRepository $articleInfoRepo
193
     * @param AutomatedEditsHelper $autoEditsHelper
194
     * @return Response|JsonResponse
195
     * See ArticleInfoControllerTest::testArticleInfoApi()
196
     * @codeCoverageIgnore
197
     */
198
    public function articleInfoApiAction(
199
        ArticleInfoRepository $articleInfoRepo,
200
        AutomatedEditsHelper $autoEditsHelper
201
    ): Response {
202
        $this->recordApiUsage('page/articleinfo');
203
204
        $this->setupArticleInfo($articleInfoRepo, $autoEditsHelper);
205
        $data = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
206
207
        try {
208
            $data = $this->articleInfo->getArticleInfoApiData($this->project, $this->page);
0 ignored issues
show
Bug introduced by
It seems like $this->page can also be of type null; however, parameter $page of App\Model\ArticleInfoApi::getArticleInfoApiData() does only seem to accept App\Model\Page, 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

208
            $data = $this->articleInfo->getArticleInfoApiData($this->project, /** @scrutinizer ignore-type */ $this->page);
Loading history...
209
        } catch (ServerException $e) {
210
            // The Wikimedia action API can fail for any number of reasons. To our users
211
            // any ServerException means the data could not be fetched, so we capture it here
212
            // to avoid the flood of automated emails when the API goes down, etc.
213
            $data['error'] = $this->i18n->msg('api-error', [$this->project->getDomain()]);
214
        }
215
216
        if ('html' === $this->request->query->get('format')) {
217
            return $this->getApiHtmlResponse($this->project, $this->page, $data);
0 ignored issues
show
Bug introduced by
It seems like $this->page can also be of type null; however, parameter $page of App\Controller\ArticleIn...r::getApiHtmlResponse() does only seem to accept App\Model\Page, 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

217
            return $this->getApiHtmlResponse($this->project, /** @scrutinizer ignore-type */ $this->page, $data);
Loading history...
218
        }
219
220
        return $this->getFormattedApiResponse($data);
221
    }
222
223
    /**
224
     * Get the Response for the HTML output of the ArticleInfo API action.
225
     * @param Project $project
226
     * @param Page $page
227
     * @param string[] $data The pre-fetched data.
228
     * @return Response
229
     * @codeCoverageIgnore
230
     */
231
    private function getApiHtmlResponse(Project $project, Page $page, array $data): Response
232
    {
233
        $response = $this->render('articleInfo/api.html.twig', [
234
            'project' => $project,
235
            'page' => $page,
236
            'data' => $data,
237
        ]);
238
239
        // All /api routes by default respond with a JSON content type.
240
        $response->headers->set('Content-Type', 'text/html');
241
242
        // This endpoint is hit constantly and user could be browsing the same page over
243
        // and over (popular noticeboard, for instance), so offload brief caching to browser.
244
        $response->setClientTtl(350);
245
246
        return $response;
247
    }
248
249
    /**
250
     * Get prose statistics for the given article.
251
     * @Route(
252
     *     "/api/page/prose/{project}/{page}",
253
     *     name="PageApiProse",
254
     *     requirements={"page"=".+"}
255
     * )
256
     * @param ArticleInfoRepository $articleInfoRepo
257
     * @param AutomatedEditsHelper $autoEditsHelper
258
     * @return JsonResponse
259
     * @codeCoverageIgnore
260
     */
261
    public function proseStatsApiAction(
262
        ArticleInfoRepository $articleInfoRepo,
263
        AutomatedEditsHelper $autoEditsHelper
264
    ): JsonResponse {
265
        $this->recordApiUsage('page/prose');
266
        $this->setupArticleInfo($articleInfoRepo, $autoEditsHelper);
267
        return $this->getFormattedApiResponse($this->articleInfo->getProseStats());
268
    }
269
270
    /**
271
     * Get the page assessments of one or more pages, along with various related metadata.
272
     * @Route(
273
     *     "/api/page/assessments/{project}/{pages}",
274
     *     name="PageApiAssessments",
275
     *     requirements={"pages"=".+"}
276
     * )
277
     * @param string $pages May be multiple pages separated by pipes, e.g. Foo|Bar|Baz
278
     * @return JsonResponse
279
     * @codeCoverageIgnore
280
     */
281
    public function assessmentsApiAction(string $pages): JsonResponse
282
    {
283
        $this->recordApiUsage('page/assessments');
284
285
        $pages = explode('|', $pages);
286
        $out = [];
287
288
        foreach ($pages as $pageTitle) {
289
            try {
290
                $page = $this->validatePage($pageTitle);
291
                $assessments = $page->getProject()
292
                    ->getPageAssessments()
293
                    ->getAssessments($page);
294
295
                $out[$page->getTitle()] = $this->request->get('classonly')
296
                    ? $assessments['assessment']
297
                    : $assessments;
298
            } catch (XtoolsHttpException $e) {
299
                $out[$pageTitle] = false;
300
            }
301
        }
302
303
        return $this->getFormattedApiResponse($out);
304
    }
305
306
    /**
307
     * Get number of in and outgoing links and redirects to the given page.
308
     * @Route(
309
     *     "/api/page/links/{project}/{page}",
310
     *     name="PageApiLinks",
311
     *     requirements={"page"=".+"}
312
     * )
313
     * @return JsonResponse
314
     * @codeCoverageIgnore
315
     */
316
    public function linksApiAction(): JsonResponse
317
    {
318
        $this->recordApiUsage('page/links');
319
        return $this->getFormattedApiResponse($this->page->countLinksAndRedirects());
320
    }
321
322
    /**
323
     * Get the top editors to a page.
324
     * @Route(
325
     *     "/api/page/top_editors/{project}/{page}/{start}/{end}/{limit}", name="PageApiTopEditors",
326
     *     requirements={
327
     *         "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?(?:\/(\d+))?)?$",
328
     *         "start"="|\d{4}-\d{2}-\d{2}",
329
     *         "end"="|\d{4}-\d{2}-\d{2}",
330
     *         "limit"="|\d+"
331
     *     },
332
     *     defaults={
333
     *         "start"=false,
334
     *         "end"=false,
335
     *         "limit"=20,
336
     *     }
337
     * )
338
     * @param ArticleInfoRepository $articleInfoRepo
339
     * @param AutomatedEditsHelper $autoEditsHelper
340
     * @return JsonResponse
341
     * @codeCoverageIgnore
342
     */
343
    public function topEditorsApiAction(
344
        ArticleInfoRepository $articleInfoRepo,
345
        AutomatedEditsHelper $autoEditsHelper
346
    ): JsonResponse {
347
        $this->recordApiUsage('page/top_editors');
348
349
        $this->setupArticleInfo($articleInfoRepo, $autoEditsHelper);
350
        $topEditors = $this->articleInfo->getTopEditorsByEditCount(
351
            (int)$this->limit,
352
            '' != $this->request->query->get('nobots')
353
        );
354
355
        return $this->getFormattedApiResponse([
356
            'top_editors' => $topEditors,
357
        ]);
358
    }
359
}
360