Passed
Pull Request — main (#442)
by MusikAnimal
08:21 queued 04:15
created

ArticleInfoController::topEditorsApiAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 0
dl 0
loc 12
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
     * Get the name of the tool's index route. This is also the name of the associated model.
38
     * @return string
39
     * @codeCoverageIgnore
40
     */
41
    public function getIndexRoute(): string
42
    {
43
        return 'ArticleInfo';
44
    }
45
46
    /**
47
     * @param RequestStack $requestStack
48
     * @param ContainerInterface $container
49
     * @param CacheItemPoolInterface $cache
50
     * @param Client $guzzle
51
     * @param I18nHelper $i18n
52
     * @param ProjectRepository $projectRepo
53
     * @param UserRepository $userRepo
54
     * @param PageRepository $pageRepo
55
     * @param ArticleInfoRepository $articleInfoRepo
56
     * @param AutomatedEditsHelper $autoEditsHelper
57
     */
58
    public function __construct(
59
        RequestStack $requestStack,
60
        ContainerInterface $container,
61
        CacheItemPoolInterface $cache,
62
        Client $guzzle,
63
        I18nHelper $i18n,
64
        ProjectRepository $projectRepo,
65
        UserRepository $userRepo,
66
        PageRepository $pageRepo,
67
        ArticleInfoRepository $articleInfoRepo,
68
        AutomatedEditsHelper $autoEditsHelper
69
    ) {
70
        $this->articleInfoRepo = $articleInfoRepo;
71
        $this->autoEditsHelper = $autoEditsHelper;
72
        parent::__construct($requestStack, $container, $cache, $guzzle, $i18n, $projectRepo, $userRepo, $pageRepo);
73
    }
74
75
    /**
76
     * The search form.
77
     * @Route("/articleinfo", name="ArticleInfo")
78
     * @Route("/articleinfo/index.php", name="articleInfoIndexPhp")
79
     * @Route("/articleinfo/{project}", name="ArticleInfoProject")
80
     * @return Response
81
     */
82
    public function indexAction(): Response
83
    {
84
        if (isset($this->params['project']) && isset($this->params['page'])) {
85
            return $this->redirectToRoute('ArticleInfoResult', $this->params);
86
        }
87
88
        return $this->render('articleInfo/index.html.twig', array_merge([
89
            'xtPage' => 'ArticleInfo',
90
            'xtPageTitle' => 'tool-articleinfo',
91
            'xtSubtitle' => 'tool-articleinfo-desc',
92
93
            // Defaults that will get overridden if in $params.
94
            'start' => '',
95
            'end' => '',
96
            'page' => '',
97
        ], $this->params, ['project' => $this->project]));
98
    }
99
100
    /**
101
     * Setup the ArticleInfo instance and its Repository.
102
     */
103
    private function setupArticleInfo(): void
104
    {
105
        if (isset($this->articleInfo)) {
106
            return;
107
        }
108
109
        $this->articleInfo = new ArticleInfo(
110
            $this->articleInfoRepo,
111
            $this->i18n,
112
            $this->autoEditsHelper,
113
            $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

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

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

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

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

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

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