Passed
Push — swagger-docs ( 6837d7...7bc867 )
by MusikAnimal
11:22
created

ArticleInfoController::getAutoEdits()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 9
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\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 OpenApi\Annotations as OA;
16
use Symfony\Component\HttpFoundation\JsonResponse;
17
use Symfony\Component\HttpFoundation\Response;
18
use Symfony\Component\Routing\Annotation\Route;
19
use Twig\Markup;
20
21
/**
22
 * This controller serves the search form and results for the ArticleInfo tool
23
 */
24
class ArticleInfoController extends XtoolsController
25
{
26
    protected ArticleInfo $articleInfo;
27
28
    /**
29
     * @inheritDoc
30
     * @codeCoverageIgnore
31
     */
32
    public function getIndexRoute(): string
33
    {
34
        return 'ArticleInfo';
35
    }
36
37
    /**
38
     * The search form.
39
     * @Route("/articleinfo", name="ArticleInfo")
40
     * @Route("/articleinfo/index.php", name="articleInfoIndexPhp")
41
     * @Route("/articleinfo/{project}", name="ArticleInfoProject")
42
     * @return Response
43
     */
44
    public function indexAction(): Response
45
    {
46
        if (isset($this->params['project']) && isset($this->params['page'])) {
47
            return $this->redirectToRoute('ArticleInfoResult', $this->params);
48
        }
49
50
        return $this->render('articleInfo/index.html.twig', array_merge([
51
            'xtPage' => 'ArticleInfo',
52
            'xtPageTitle' => 'tool-articleinfo',
53
            'xtSubtitle' => 'tool-articleinfo-desc',
54
55
            // Defaults that will get overridden if in $params.
56
            'start' => '',
57
            'end' => '',
58
            'page' => '',
59
        ], $this->params, ['project' => $this->project]));
60
    }
61
62
    /**
63
     * Setup the ArticleInfo instance and its Repository.
64
     * @param ArticleInfoRepository $articleInfoRepo
65
     * @param AutomatedEditsHelper $autoEditsHelper
66
     * @codeCoverageIgnore
67
     */
68
    private function setupArticleInfo(
69
        ArticleInfoRepository $articleInfoRepo,
70
        AutomatedEditsHelper $autoEditsHelper
71
    ): void {
72
        if (isset($this->articleInfo)) {
73
            return;
74
        }
75
76
        $this->articleInfo = new ArticleInfo(
77
            $articleInfoRepo,
78
            $this->i18n,
79
            $autoEditsHelper,
80
            $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

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

127
        if (!$this->isDateRangeValid(/** @scrutinizer ignore-type */ $this->page, $this->start, $this->end)) {
Loading history...
128
            $this->addFlashMessage('notice', 'date-range-outside-revisions');
129
130
            return $this->redirectToRoute('ArticleInfo', [
131
                'project' => $this->request->get('project'),
132
            ]);
133
        }
134
135
        $this->setupArticleInfo($articleInfoRepo, $autoEditsHelper);
136
        $this->articleInfo->prepareData();
137
138
        $maxRevisions = $this->getParameter('app.max_page_revisions');
139
140
        // Show message if we hit the max revisions.
141
        if ($this->articleInfo->tooManyRevisions()) {
142
            $this->addFlashMessage('notice', 'too-many-revisions', [
143
                $this->i18n->numberFormat($maxRevisions),
144
                $maxRevisions,
145
            ]);
146
        }
147
148
        // For when there is very old data (2001 era) which may cause miscalculations.
149
        if ($this->articleInfo->getFirstEdit()->getYear() < 2003) {
150
            $this->addFlashMessage('warning', 'old-page-notice');
151
        }
152
153
        // When all username info has been hidden (see T303724).
154
        if (0 === $this->articleInfo->getNumEditors()) {
155
            $this->addFlashMessage('warning', 'error-usernames-missing');
156
        } elseif ($this->articleInfo->numDeletedRevisions()) {
157
            $link = new Markup(
158
                $this->renderView('flashes/deleted_data.html.twig', [
159
                    'numRevs' => $this->articleInfo->numDeletedRevisions(),
160
                ]),
161
                'UTF-8'
162
            );
163
            $this->addFlashMessage(
164
                'warning',
165
                $link,
166
                [$this->articleInfo->numDeletedRevisions(), $link]
167
            );
168
        }
169
170
        $ret = [
171
            'xtPage' => 'ArticleInfo',
172
            '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

172
            '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...
173
            'project' => $this->project,
174
            'editorlimit' => (int)$this->request->query->get('editorlimit', 20),
175
            'botlimit' => $this->request->query->get('botlimit', 10),
176
            'pageviewsOffset' => 60,
177
            'ai' => $this->articleInfo,
178
            '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

178
            'showAuthorship' => Authorship::isSupportedPage(/** @scrutinizer ignore-type */ $this->page) && $this->articleInfo->getNumEditors() > 0,
Loading history...
179
        ];
180
181
        // Output the relevant format template.
182
        return $this->getFormattedResponse('articleInfo/result', $ret);
183
    }
184
185
    /**
186
     * Check if there were any revisions of given page in given date range.
187
     * @param Page $page
188
     * @param false|int $start
189
     * @param false|int $end
190
     * @return bool
191
     */
192
    private function isDateRangeValid(Page $page, $start, $end): bool
193
    {
194
        return $page->getNumRevisions(null, $start, $end) > 0;
195
    }
196
197
    /************************ API endpoints ************************/
198
199
    /**
200
     * Get basic information about a page.
201
     * @Route(
202
     *     "/api/page/articleinfo/{project}/{page}",
203
     *     name="PageApiArticleInfo",
204
     *     requirements={"page"=".+"},
205
     *     methods={"GET"}
206
     * )
207
     * @OA\Get(description="Get basic information about the history of a page.
208
            See also the [pageviews](https://w.wiki/6o9k) and [edit data](https://w.wiki/6o9m) REST APIs.")
209
     * @OA\Tag(name="Page API")
210
     * @OA\ExternalDocumentation(url="https://www.mediawiki.org/wiki/XTools/API/Page#Article_info")
211
     * @OA\Parameter(ref="#/components/parameters/Project")
212
     * @OA\Parameter(ref="#/components/parameters/Page")
213
     * @OA\Parameter(name="format", in="query", @OA\Schema(default="json", type="string", enum={"json","html"}))
214
     * @OA\Response(
215
     *     response=200,
216
     *     description="Basic information about the page.",
217
     *     @OA\JsonContent(
218
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
219
     *         @OA\Property(property="page", ref="#/components/parameters/Page/schema"),
220
     *         @OA\Property(property="watchers", type="integer"),
221
     *         @OA\Property(property="pageviews", type="integer"),
222
     *         @OA\Property(property="pageviews_offset", type="integer"),
223
     *         @OA\Property(property="revisions", type="integer"),
224
     *         @OA\Property(property="editors", type="integer"),
225
     *         @OA\Property(property="minor_edits", type="integer"),
226
     *         @OA\Property(property="author", type="string", example="Jimbo Wales"),
227
     *         @OA\Property(property="author_editcount", type="integer"),
228
     *         @OA\Property(property="created_at", type="date"),
229
     *         @OA\Property(property="created_rev_id", type="integer"),
230
     *         @OA\Property(property="modified_at", type="date"),
231
     *         @OA\Property(property="secs_since_last_edit", type="integer"),
232
     *         @OA\Property(property="last_edit_id", type="integer"),
233
     *         @OA\Property(property="assessment", type="object", example={
234
     *             "value":"FA",
235
     *             "color": "#9CBDFF",
236
     *             "category": "Category:FA-Class articles",
237
     *             "badge": "https://upload.wikimedia.org/wikipedia/commons/b/bc/Featured_article_star.svg"
238
     *         }),
239
     *         @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time")
240
     *     ),
241
     *     @OA\XmlContent(format="text/html")
242
     * )
243
     * @OA\Response(response=404, ref="#/components/responses/404")
244
     * @OA\Response(response=503, ref="#/components/responses/503")
245
     * @OA\Response(response=504, ref="#/components/responses/504")
246
     * @param ArticleInfoRepository $articleInfoRepo
247
     * @param AutomatedEditsHelper $autoEditsHelper
248
     * @return Response|JsonResponse
249
     * See ArticleInfoControllerTest::testArticleInfoApi()
250
     * @codeCoverageIgnore
251
     */
252
    public function articleInfoApiAction(
253
        ArticleInfoRepository $articleInfoRepo,
254
        AutomatedEditsHelper $autoEditsHelper
255
    ): Response {
256
        $this->recordApiUsage('page/articleinfo');
257
258
        $this->setupArticleInfo($articleInfoRepo, $autoEditsHelper);
259
        $data = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
260
261
        try {
262
            $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

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

271
            return $this->getApiHtmlResponse($this->project, /** @scrutinizer ignore-type */ $this->page, $data);
Loading history...
272
        }
273
274
        return $this->getFormattedApiResponse($data);
275
    }
276
277
    /**
278
     * Get the Response for the HTML output of the ArticleInfo API action.
279
     * @param Project $project
280
     * @param Page $page
281
     * @param string[] $data The pre-fetched data.
282
     * @return Response
283
     * @codeCoverageIgnore
284
     */
285
    private function getApiHtmlResponse(Project $project, Page $page, array $data): Response
286
    {
287
        $response = $this->render('articleInfo/api.html.twig', [
288
            'project' => $project,
289
            'page' => $page,
290
            'data' => $data,
291
        ]);
292
293
        // All /api routes by default respond with a JSON content type.
294
        $response->headers->set('Content-Type', 'text/html');
295
296
        // This endpoint is hit constantly and user could be browsing the same page over
297
        // and over (popular noticeboard, for instance), so offload brief caching to browser.
298
        $response->setClientTtl(350);
299
300
        return $response;
301
    }
302
303
    /**
304
     * Get prose statistics for the given article.
305
     * @Route(
306
     *     "/api/page/prose/{project}/{page}",
307
     *     name="PageApiProse",
308
     *     requirements={"page"=".+"},
309
     *     methods={"GET"}
310
     * )
311
     * @OA\Tag(name="Page API")
312
     * @OA\ExternalDocumentation(url="https://www.mediawiki.org/wiki/XTools/Page_History#Prose")
313
     * @OA\Get(description="Get statistics about the [prose](https://en.wiktionary.org/wiki/prose) (characters,
314
            word count, etc.) and referencing of a page. ([more info](https://w.wiki/6oAF))")
315
     * @OA\Parameter(ref="#/components/parameters/Project")
316
     * @OA\Parameter(ref="#/components/parameters/Page", @OA\Schema(example="Metallica"))
317
     * @OA\Response(
318
     *     response=200,
319
     *     description="Prose stats",
320
     *     @OA\JsonContent(
321
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
322
     *         @OA\Property(property="page", ref="#/components/parameters/Page/schema"),
323
     *         @OA\Property(property="bytes", type="integer"),
324
     *         @OA\Property(property="characters", type="integer"),
325
     *         @OA\Property(property="words", type="integer"),
326
     *         @OA\Property(property="references", type="integer"),
327
     *         @OA\Property(property="unique_references", type="integer"),
328
     *         @OA\Property(property="sections", type="integer"),
329
     *         @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time")
330
     *     )
331
     * )
332
     * @OA\Response(response=404, ref="#/components/responses/404")
333
     * @param ArticleInfoRepository $articleInfoRepo
334
     * @param AutomatedEditsHelper $autoEditsHelper
335
     * @return JsonResponse
336
     * @codeCoverageIgnore
337
     */
338
    public function proseStatsApiAction(
339
        ArticleInfoRepository $articleInfoRepo,
340
        AutomatedEditsHelper $autoEditsHelper
341
    ): JsonResponse {
342
        $responseCode = Response::HTTP_OK;
343
        $this->recordApiUsage('page/prose');
344
        $this->setupArticleInfo($articleInfoRepo, $autoEditsHelper);
345
        $this->addFlash('info', 'The algorithm used by this API has recently changed. ' .
346
            'See https://www.mediawiki.org/wiki/XTools/Page_History#Prose for details.');
347
        $ret = $this->articleInfo->getProseStats();
348
        if (null === $ret) {
349
            $this->addFlashMessage('error', 'api-error-wikimedia');
350
            $responseCode = Response::HTTP_BAD_GATEWAY;
351
            $ret = [];
352
        }
353
        return $this->getFormattedApiResponse($ret, $responseCode);
354
    }
355
356
    /**
357
     * Get the page assessments of one or more pages, along with various related metadata.
358
     * @Route(
359
     *     "/api/page/assessments/{project}/{pages}",
360
     *     name="PageApiAssessments",
361
     *     requirements={"pages"=".+"},
362
     *     methods={"GET"}
363
     * )
364
     * @OA\Tag(name="Page API")
365
     * @OA\Get(description="Get [assessment data](https://w.wiki/6oAM) of the given pages, including the overall
366
       quality classifications, along with a list of the WikiProjects and their classifications and importance levels.")
367
     * @OA\Parameter(ref="#/components/parameters/Project")
368
     * @OA\Parameter(ref="#/components/parameters/Pages")
369
     * @OA\Parameter(name="classonly", in="query", @OA\Schema(type="boolean"),
370
     *     description="Return only the overall quality assessment instead of for each applicable WikiProject."
371
     * )
372
     * @OA\Response(
373
     *     response=200,
374
     *     description="Assessmnet data",
375
     *     @OA\JsonContent(
376
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
377
     *         @OA\Property(property="pages", type="object",
378
     *             @OA\Property(property="Page title", type="object",
379
     *                 @OA\Property(property="assessment", ref="#/components/schemas/PageAssessment"),
380
     *                 @OA\Property(property="wikiprojects", type="object",
381
     *                     @OA\Property(property="name of WikiProject",
382
     *                         ref="#/components/schemas/PageAssessmentWikiProject"
383
     *                     )
384
     *                 )
385
     *             )
386
     *         ),
387
     *         @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time")
388
     *     )
389
     * )
390
     * @OA\Response(response=404, ref="#/components/responses/404")
391
     * @param string $pages May be multiple pages separated by pipes, e.g. Foo|Bar|Baz
392
     * @return JsonResponse
393
     * @codeCoverageIgnore
394
     */
395
    public function assessmentsApiAction(string $pages): JsonResponse
396
    {
397
        $this->recordApiUsage('page/assessments');
398
399
        $pages = explode('|', $pages);
400
        $out = [
401
            'pages' => [],
402
        ];
403
404
        foreach ($pages as $pageTitle) {
405
            try {
406
                $page = $this->validatePage($pageTitle);
407
                $assessments = $page->getProject()
408
                    ->getPageAssessments()
409
                    ->getAssessments($page);
410
411
                $out['pages'][$page->getTitle()] = $this->getBoolVal('classonly')
412
                    ? $assessments['assessment']
413
                    : $assessments;
414
            } catch (XtoolsHttpException $e) {
415
                $out['pages'][$pageTitle] = false;
416
            }
417
        }
418
419
        return $this->getFormattedApiResponse($out);
420
    }
421
422
    /**
423
     * Get number of in and outgoing links, external links, and redirects to the given page.
424
     * @Route(
425
     *     "/api/page/links/{project}/{page}",
426
     *     name="PageApiLinks",
427
     *     requirements={"page"=".+"},
428
     *     methods={"GET"}
429
     * )
430
     * @OA\Tag(name="Page API")
431
     * @OA\Parameter(ref="#/components/parameters/Project")
432
     * @OA\Parameter(ref="#/components/parameters/Page")
433
     * @OA\Response(
434
     *     response=200,
435
     *     description="Counts of in and outgoing links, external links, and redirects.",
436
     *     @OA\JsonContent(
437
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
438
     *         @OA\Property(property="page", ref="#/components/parameters/Page/schema"),
439
     *         @OA\Property(property="links_ext_count", type="integer"),
440
     *         @OA\Property(property="links_out_count", type="integer"),
441
     *         @OA\Property(property="links_in_count", type="integer"),
442
     *         @OA\Property(property="redirects_count", type="integer"),
443
     *         @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time")
444
     *     )
445
     * )
446
     * @OA\Response(response=404, ref="#/components/responses/404")
447
     * @OA\Response(response=503, ref="#/components/responses/503")
448
     * @OA\Response(response=504, ref="#/components/responses/504")
449
     * @return JsonResponse
450
     * @codeCoverageIgnore
451
     */
452
    public function linksApiAction(): JsonResponse
453
    {
454
        $this->recordApiUsage('page/links');
455
        return $this->getFormattedApiResponse($this->page->countLinksAndRedirects());
456
    }
457
458
    /**
459
     * Get the top editors (by number of edits) of a page.
460
     * @Route(
461
     *     "/api/page/top_editors/{project}/{page}/{start}/{end}/{limit}", name="PageApiTopEditors",
462
     *     requirements={
463
     *         "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?(?:\/(\d+))?)?$",
464
     *         "start"="|\d{4}-\d{2}-\d{2}",
465
     *         "end"="|\d{4}-\d{2}-\d{2}",
466
     *         "limit"="\d+"
467
     *     },
468
     *     defaults={
469
     *         "start"=false,
470
     *         "end"=false,
471
     *         "limit"=20,
472
     *     },
473
     *     methods={"GET"}
474
     * )
475
     * @OA\Tag(name="Page API")
476
     * @OA\Parameter(ref="#/components/parameters/Project")
477
     * @OA\Parameter(ref="#/components/parameters/Page")
478
     * @OA\Parameter(ref="#/components/parameters/Start")
479
     * @OA\Parameter(ref="#/components/parameters/End")
480
     * @OA\Parameter(ref="#/components/parameters/Limit")
481
     * @OA\Parameter(name="nobots", in="query",
482
     *     description="Exclude bots from the results.", @OA\Schema(type="boolean")
483
     * )
484
     * @OA\Response(
485
     *     response=200,
486
     *     description="List of the top editors, sorted by how many edits they've made to the page.",
487
     *     @OA\JsonContent(
488
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
489
     *         @OA\Property(property="page", ref="#/components/parameters/Page/schema"),
490
     *         @OA\Property(property="start", ref="#/components/parameters/Start/schema"),
491
     *         @OA\Property(property="end", ref="#/components/parameters/End/schema"),
492
     *         @OA\Property(property="limit", ref="#/components/parameters/Limit/schema"),
493
     *         @OA\Property(property="top_editors", type="array", @OA\Items(type="object"), example={
494
     *             {
495
     *                 "rank": 1,
496
     *                 "username": "Jimbo Wales",
497
     *                 "count": 50,
498
     *                 "minor": 15,
499
     *                 "first_edit": {
500
     *                     "id": 12345,
501
     *                     "timestamp": 20200101125959
502
     *                 },
503
     *                 "last_edit": {
504
     *                     "id": 54321,
505
     *                     "timestamp": 20200120125959
506
     *                 }
507
     *             }
508
     *         }),
509
     *         @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time")
510
     *     )
511
     * )
512
     * @OA\Response(response=404, ref="#/components/responses/404")
513
     * @OA\Response(response=503, ref="#/components/responses/503")
514
     * @OA\Response(response=504, ref="#/components/responses/504")
515
     * @param ArticleInfoRepository $articleInfoRepo
516
     * @param AutomatedEditsHelper $autoEditsHelper
517
     * @return JsonResponse
518
     * @codeCoverageIgnore
519
     */
520
    public function topEditorsApiAction(
521
        ArticleInfoRepository $articleInfoRepo,
522
        AutomatedEditsHelper $autoEditsHelper
523
    ): JsonResponse {
524
        $this->recordApiUsage('page/top_editors');
525
526
        $this->setupArticleInfo($articleInfoRepo, $autoEditsHelper);
527
        $topEditors = $this->articleInfo->getTopEditorsByEditCount(
528
            (int)$this->limit,
529
            $this->getBoolVal('nobots')
530
        );
531
532
        return $this->getFormattedApiResponse([
533
            'top_editors' => $topEditors,
534
        ]);
535
    }
536
537
    /**
538
     * Get data about bots that have edited a page.
539
     * @Route(
540
     *     "/api/page/bot_data/{project}/{page}/{start}/{end}", name="PageApiBotData",
541
     *     requirements={
542
     *         "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?)?$",
543
     *         "start"="|\d{4}-\d{2}-\d{2}",
544
     *         "end"="|\d{4}-\d{2}-\d{2}",
545
     *     },
546
     *     defaults={
547
     *         "start"=false,
548
     *         "end"=false,
549
     *     },
550
     *     methods={"GET"}
551
     * )
552
     * @OA\Tag(name="Page API")
553
     * @OA\Get(description="List bots that have edited a page, with edit counts and whether the account
554
           is still in the `bot` user group.")
555
     * @OA\Parameter(ref="#/components/parameters/Project")
556
     * @OA\Parameter(ref="#/components/parameters/Page")
557
     * @OA\Parameter(ref="#/components/parameters/Start")
558
     * @OA\Parameter(ref="#/components/parameters/End")
559
     * @OA\Response(
560
     *     response=200,
561
     *     description="List of bots",
562
     *     @OA\JsonContent(
563
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
564
     *         @OA\Property(property="page", ref="#/components/parameters/Page/schema"),
565
     *         @OA\Property(property="start", ref="#/components/parameters/Start/schema"),
566
     *         @OA\Property(property="end", ref="#/components/parameters/End/schema"),
567
     *         @OA\Property(property="bots", type="object",
568
     *             @OA\Property(property="Page title", type="object",
569
     *                 @OA\Property(property="count", type="integer", description="Number of edits to the page."),
570
     *                 @OA\Property(property="current", type="boolean",
571
     *                     description="Whether the account currently has the bot flag"
572
     *                 )
573
     *             )
574
     *         ),
575
     *         @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time")
576
     *     )
577
     * )
578
     * @OA\Response(response=404, ref="#/components/responses/404")
579
     * @OA\Response(response=503, ref="#/components/responses/503")
580
     * @OA\Response(response=504, ref="#/components/responses/504")
581
     * @param ArticleInfoRepository $articleInfoRepo
582
     * @param AutomatedEditsHelper $autoEditsHelper
583
     * @return JsonResponse
584
     * @codeCoverageIgnore
585
     */
586
    public function botDataApiAction(
587
        ArticleInfoRepository $articleInfoRepo,
588
        AutomatedEditsHelper $autoEditsHelper
589
    ): JsonResponse {
590
        $this->recordApiUsage('page/bot_data');
591
592
        $this->setupArticleInfo($articleInfoRepo, $autoEditsHelper);
593
        $bots = $this->articleInfo->getBots();
594
595
        return $this->getFormattedApiResponse([
596
            'bots' => $bots,
597
        ]);
598
    }
599
600
    /**
601
     * Get counts of (semi-)automated tools that were used to edit the page.
602
     * @Route(
603
     *     "/api/page/automated_edits/{project}/{page}/{start}/{end}", name="PageApiAutoEdits",
604
     *     requirements={
605
     *         "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?)?$",
606
     *         "start"="|\d{4}-\d{2}-\d{2}",
607
     *         "end"="|\d{4}-\d{2}-\d{2}",
608
     *     },
609
     *     defaults={
610
     *         "start"=false,
611
     *         "end"=false,
612
     *     },
613
     *     methods={"GET"}
614
     * )
615
     * @OA\Tag(name="Page API")
616
     * @OA\Get(description="Get counts of the number of times known (semi-)automated tools were used to edit the page.")
617
     * @OA\Parameter(ref="#/components/parameters/Project")
618
     * @OA\Parameter(ref="#/components/parameters/Page")
619
     * @OA\Parameter(ref="#/components/parameters/Start")
620
     * @OA\Parameter(ref="#/components/parameters/End")
621
     * @OA\Response(
622
     *     response=200,
623
     *     description="List of tools",
624
     *     @OA\JsonContent(
625
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
626
     *         @OA\Property(property="page", ref="#/components/parameters/Page/schema"),
627
     *         @OA\Property(property="start", ref="#/components/parameters/Start/schema"),
628
     *         @OA\Property(property="end", ref="#/components/parameters/End/schema"),
629
     *         @OA\Property(property="automated_tools", ref="#/components/schemas/AutomatedTools"),
630
     *         @OA\Property(property="elapsed_time", ref="#/components/schemas/elapsed_time")
631
     *     )
632
     * )
633
     * @OA\Response(response=404, ref="#/components/responses/404")
634
     * @OA\Response(response=503, ref="#/components/responses/503")
635
     * @OA\Response(response=504, ref="#/components/responses/504")
636
     * @param ArticleInfoRepository $articleInfoRepo
637
     * @param AutomatedEditsHelper $autoEditsHelper
638
     * @return JsonResponse
639
     * @codeCoverageIgnore
640
     */
641
    public function getAutoEdits(
642
        ArticleInfoRepository $articleInfoRepo,
643
        AutomatedEditsHelper $autoEditsHelper
644
    ): JsonResponse {
645
        $this->recordApiUsage('page/auto_edits');
646
647
        $this->setupArticleInfo($articleInfoRepo, $autoEditsHelper);
648
        return $this->getFormattedApiResponse([
649
            'automated_tools' => $this->articleInfo->getAutoEditsCounts(),
650
        ]);
651
    }
652
}
653