Passed
Push — pageinfo ( 7c1380 )
by MusikAnimal
06:18
created

PageInfoController   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 652
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 130
dl 0
loc 652
rs 10
c 0
b 0
f 0
wmc 29

14 Methods

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