Passed
Push — main ( ec4ebd...49d96d )
by MusikAnimal
04:21
created

ArticleInfoController::getApiHtmlResponse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 3
dl 0
loc 16
rs 10
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\Helper\I18nHelper;
10
use App\Model\ArticleInfo;
11
use App\Model\Authorship;
12
use App\Model\Page;
13
use App\Model\Project;
14
use App\Repository\ArticleInfoRepository;
15
use App\Repository\PageRepository;
16
use App\Repository\ProjectRepository;
17
use App\Repository\UserRepository;
18
use GuzzleHttp\Client;
19
use GuzzleHttp\Exception\ServerException;
20
use Psr\Cache\CacheItemPoolInterface;
21
use Psr\Container\ContainerInterface;
22
use Symfony\Component\HttpFoundation\JsonResponse;
23
use Symfony\Component\HttpFoundation\RequestStack;
24
use Symfony\Component\HttpFoundation\Response;
25
use Symfony\Component\Routing\Annotation\Route;
26
27
/**
28
 * This controller serves the search form and results for the ArticleInfo tool
29
 */
30
class ArticleInfoController extends XtoolsController
31
{
32
    protected ArticleInfo $articleInfo;
33
    protected ArticleInfoRepository $articleInfoRepo;
34
    protected AutomatedEditsHelper $autoEditsHelper;
35
36
    /**
37
     * @inheritDoc
38
     * @codeCoverageIgnore
39
     */
40
    public function getIndexRoute(): string
41
    {
42
        return 'ArticleInfo';
43
    }
44
45
    /**
46
     * @param RequestStack $requestStack
47
     * @param ContainerInterface $container
48
     * @param CacheItemPoolInterface $cache
49
     * @param Client $guzzle
50
     * @param I18nHelper $i18n
51
     * @param ProjectRepository $projectRepo
52
     * @param UserRepository $userRepo
53
     * @param PageRepository $pageRepo
54
     * @param ArticleInfoRepository $articleInfoRepo
55
     * @param AutomatedEditsHelper $autoEditsHelper
56
     */
57
    public function __construct(
58
        RequestStack $requestStack,
59
        ContainerInterface $container,
60
        CacheItemPoolInterface $cache,
61
        Client $guzzle,
62
        I18nHelper $i18n,
63
        ProjectRepository $projectRepo,
64
        UserRepository $userRepo,
65
        PageRepository $pageRepo,
66
        ArticleInfoRepository $articleInfoRepo,
67
        AutomatedEditsHelper $autoEditsHelper
68
    ) {
69
        $this->articleInfoRepo = $articleInfoRepo;
70
        $this->autoEditsHelper = $autoEditsHelper;
71
        parent::__construct($requestStack, $container, $cache, $guzzle, $i18n, $projectRepo, $userRepo, $pageRepo);
72
    }
73
74
    /**
75
     * The search form.
76
     * @Route("/articleinfo", name="ArticleInfo")
77
     * @Route("/articleinfo/index.php", name="articleInfoIndexPhp")
78
     * @Route("/articleinfo/{project}", name="ArticleInfoProject")
79
     * @return Response
80
     */
81
    public function indexAction(): Response
82
    {
83
        if (isset($this->params['project']) && isset($this->params['page'])) {
84
            return $this->redirectToRoute('ArticleInfoResult', $this->params);
85
        }
86
87
        return $this->render('articleInfo/index.html.twig', array_merge([
88
            'xtPage' => 'ArticleInfo',
89
            'xtPageTitle' => 'tool-articleinfo',
90
            'xtSubtitle' => 'tool-articleinfo-desc',
91
92
            // Defaults that will get overridden if in $params.
93
            'start' => '',
94
            'end' => '',
95
            'page' => '',
96
        ], $this->params, ['project' => $this->project]));
97
    }
98
99
    /**
100
     * Setup the ArticleInfo instance and its Repository.
101
     */
102
    private function setupArticleInfo(): void
103
    {
104
        if (isset($this->articleInfo)) {
105
            return;
106
        }
107
108
        $this->articleInfo = new ArticleInfo(
109
            $this->articleInfoRepo,
110
            $this->i18n,
111
            $this->autoEditsHelper,
112
            $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

112
            /** @scrutinizer ignore-type */ $this->page,
Loading history...
113
            $this->start,
114
            $this->end
115
        );
116
    }
117
118
    /**
119
     * Generate ArticleInfo gadget script for use on-wiki. This automatically points the
120
     * script to this installation's API. Pass ?uglify=1 to uglify the code.
121
     *
122
     * @Route("/articleinfo-gadget.js", name="ArticleInfoGadget")
123
     * @link https://www.mediawiki.org/wiki/XTools/ArticleInfo_gadget
124
     *
125
     * @return Response
126
     * @codeCoverageIgnore
127
     */
128
    public function gadgetAction(): Response
129
    {
130
        $rendered = $this->renderView('articleInfo/articleinfo.js.twig');
131
        $response = new Response($rendered);
132
        $response->headers->set('Content-Type', 'text/javascript');
133
        return $response;
134
    }
135
136
    /**
137
     * Display the results in given date range.
138
     * @Route(
139
     *    "/articleinfo/{project}/{page}/{start}/{end}", name="ArticleInfoResult",
140
     *     requirements={
141
     *         "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?)?$",
142
     *         "start"="|\d{4}-\d{2}-\d{2}",
143
     *         "end"="|\d{4}-\d{2}-\d{2}",
144
     *     },
145
     *     defaults={
146
     *         "start"=false,
147
     *         "end"=false,
148
     *     }
149
     * )
150
     * @return Response
151
     * @codeCoverageIgnore
152
     */
153
    public function resultAction(): Response
154
    {
155
        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

155
        if (!$this->isDateRangeValid(/** @scrutinizer ignore-type */ $this->page, $this->start, $this->end)) {
Loading history...
156
            $this->addFlashMessage('notice', 'date-range-outside-revisions');
157
158
            return $this->redirectToRoute('ArticleInfo', [
159
                'project' => $this->request->get('project'),
160
            ]);
161
        }
162
163
        $this->setupArticleInfo();
164
        $this->articleInfo->prepareData();
165
166
        $maxRevisions = $this->getParameter('app.max_page_revisions');
167
168
        // Show message if we hit the max revisions.
169
        if ($this->articleInfo->tooManyRevisions()) {
170
            $this->addFlashMessage('notice', 'too-many-revisions', [
171
                $this->i18n->numberFormat($maxRevisions),
172
                $maxRevisions,
173
            ]);
174
        }
175
176
        // For when there is very old data (2001 era) which may cause miscalculations.
177
        if ($this->articleInfo->getFirstEdit()->getYear() < 2003) {
178
            $this->addFlashMessage('warning', 'old-page-notice');
179
        }
180
181
        // When all username info has been hidden (see T303724).
182
        if (0 === $this->articleInfo->getNumEditors()) {
183
            $this->addFlashMessage('warning', 'error-usernames-missing');
184
        }
185
186
        $ret = [
187
            'xtPage' => 'ArticleInfo',
188
            '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

188
            '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...
189
            'project' => $this->project,
190
            'editorlimit' => (int)$this->request->query->get('editorlimit', 20),
191
            'botlimit' => $this->request->query->get('botlimit', 10),
192
            'pageviewsOffset' => 60,
193
            'ai' => $this->articleInfo,
194
            '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

194
            'showAuthorship' => Authorship::isSupportedPage(/** @scrutinizer ignore-type */ $this->page) && $this->articleInfo->getNumEditors() > 0,
Loading history...
195
        ];
196
197
        // Output the relevant format template.
198
        return $this->getFormattedResponse('articleInfo/result', $ret);
199
    }
200
201
    /**
202
     * Check if there were any revisions of given page in given date range.
203
     * @param Page $page
204
     * @param false|int $start
205
     * @param false|int $end
206
     * @return bool
207
     */
208
    private function isDateRangeValid(Page $page, $start, $end): bool
209
    {
210
        return $page->getNumRevisions(null, $start, $end) > 0;
211
    }
212
213
    /************************ API endpoints ************************/
214
215
    /**
216
     * Get basic info on a given article.
217
     * @Route(
218
     *     "/api/articleinfo/{project}/{page}",
219
     *     name="ArticleInfoApiAction",
220
     *     requirements={"page"=".+"}
221
     * )
222
     * @Route("/api/page/articleinfo/{project}/{page}", requirements={"page"=".+"})
223
     * @return Response|JsonResponse
224
     * See ArticleInfoControllerTest::testArticleInfoApi()
225
     * @codeCoverageIgnore
226
     */
227
    public function articleInfoApiAction(): Response
228
    {
229
        $this->recordApiUsage('page/articleinfo');
230
231
        $this->setupArticleInfo();
232
        $data = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
233
234
        try {
235
            $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

235
            $data = $this->articleInfo->getArticleInfoApiData($this->project, /** @scrutinizer ignore-type */ $this->page);
Loading history...
236
        } catch (ServerException $e) {
237
            // The Wikimedia action API can fail for any number of reasons. To our users
238
            // any ServerException means the data could not be fetched, so we capture it here
239
            // to avoid the flood of automated emails when the API goes down, etc.
240
            $data['error'] = $this->i18n->msg('api-error', [$this->project->getDomain()]);
241
        }
242
243
        if ('html' === $this->request->query->get('format')) {
244
            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

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