Issues (196)

Security Analysis    6 potential vulnerabilities

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection (4)
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection (1)
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Controller/PageInfoController.php (7 issues)

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
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
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
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
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
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
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
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