PageInfoController::indexAction()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 2
nop 0
dl 0
loc 16
rs 9.9332
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\Authorship;
10
use App\Model\Page;
11
use App\Model\PageInfo;
12
use App\Model\Project;
13
use App\Repository\PageInfoRepository;
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 PageInfo tool
23
 */
24
class PageInfoController extends XtoolsController
25
{
26
    protected PageInfo $pageInfo;
27
28
    /**
29
     * @inheritDoc
30
     * @codeCoverageIgnore
31
     */
32
    public function getIndexRoute(): string
33
    {
34
        return 'PageInfo';
35
    }
36
37
    /**
38
     * The search form.
39
     * @Route("/pageinfo", name="PageInfo")
40
     * @Route("/pageinfo/{project}", name="PageInfoProject")
41
     * @Route("/articleinfo", name="PageInfoLegacy")
42
     * @Route("/articleinfo/index.php", name="PageInfoLegacyPhp")
43
     * @return Response
44
     */
45
    public function indexAction(): Response
46
    {
47
        if (isset($this->params['project']) && isset($this->params['page'])) {
48
            return $this->redirectToRoute('PageInfoResult', $this->params);
49
        }
50
51
        return $this->render('pageInfo/index.html.twig', array_merge([
52
            'xtPage' => 'PageInfo',
53
            'xtPageTitle' => 'tool-pageinfo',
54
            'xtSubtitle' => 'tool-pageinfo-desc',
55
56
            // Defaults that will get overridden if in $params.
57
            'start' => '',
58
            'end' => '',
59
            'page' => '',
60
        ], $this->params, ['project' => $this->project]));
61
    }
62
63
    /**
64
     * Setup the PageInfo instance and its Repository.
65
     * @param PageInfoRepository $pageInfoRepo
66
     * @param AutomatedEditsHelper $autoEditsHelper
67
     * @codeCoverageIgnore
68
     */
69
    private function setupPageInfo(
70
        PageInfoRepository $pageInfoRepo,
71
        AutomatedEditsHelper $autoEditsHelper
72
    ): void {
73
        if (isset($this->pageInfo)) {
74
            return;
75
        }
76
77
        $this->pageInfo = new PageInfo(
78
            $pageInfoRepo,
79
            $this->i18n,
80
            $autoEditsHelper,
81
            $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\PageInfo::__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

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

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

185
            '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...
186
            'project' => $this->project,
187
            'editorlimit' => (int)$this->request->query->get('editorlimit', 20),
188
            'botlimit' => $this->request->query->get('botlimit', 10),
189
            'pageviewsOffset' => 60,
190
            'ai' => $this->pageInfo,
191
            'showAuthorship' => Authorship::isSupportedPage($this->page) && $this->pageInfo->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

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

281
            $data = $this->pageInfo->getPageInfoApiData($this->project, /** @scrutinizer ignore-type */ $this->page);
Loading history...
282
        } catch (ServerException $e) {
283
            // The Wikimedia action API can fail for any number of reasons. To our users
284
            // any ServerException means the data could not be fetched, so we capture it here
285
            // to avoid the flood of automated emails when the API goes down, etc.
286
            $data['error'] = $this->i18n->msg('api-error', [$this->project->getDomain()]);
287
        }
288
289
        if ('html' === $this->request->query->get('format')) {
290
            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\PageInfoC...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

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