EditCounterController::generalStatsIndexAction()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace App\Controller;
6
7
use App\Model\EditCounter;
8
use App\Model\GlobalContribs;
9
use App\Model\UserRights;
10
use App\Repository\EditCounterRepository;
11
use App\Repository\EditRepository;
12
use App\Repository\GlobalContribsRepository;
13
use App\Repository\UserRightsRepository;
14
use OpenApi\Annotations as OA;
15
use Symfony\Component\HttpFoundation\Cookie;
16
use Symfony\Component\HttpFoundation\JsonResponse;
17
use Symfony\Component\HttpFoundation\RedirectResponse;
18
use Symfony\Component\HttpFoundation\RequestStack;
19
use Symfony\Component\HttpFoundation\Response;
20
use Symfony\Component\Routing\Annotation\Route;
21
22
/**
23
 * Class EditCounterController
24
 */
25
class EditCounterController extends XtoolsController
26
{
27
    /**
28
     * Available statistic sections. These can be hand-picked on the index form so that you only get the data you
29
     * want and hence speed up the tool. Keys are the i18n messages (and DOM IDs), values are the action names.
30
     */
31
    private const AVAILABLE_SECTIONS = [
32
        'general-stats' => 'EditCounterGeneralStats',
33
        'namespace-totals' => 'EditCounterNamespaceTotals',
34
        'year-counts' => 'EditCounterYearCounts',
35
        'month-counts' => 'EditCounterMonthCounts',
36
        'timecard' => 'EditCounterTimecard',
37
        'top-edited-pages' => 'TopEditsResultNamespace',
38
        'rights-changes' => 'EditCounterRightsChanges',
39
    ];
40
41
    protected EditCounter $editCounter;
42
    protected UserRights $userRights;
43
44
    /** @var string[] Which sections to show. */
45
    protected array $sections;
46
47
    /**
48
     * @inheritDoc
49
     * @codeCoverageIgnore
50
     */
51
    public function getIndexRoute(): string
52
    {
53
        return 'EditCounter';
54
    }
55
56
    /**
57
     * Causes the tool to redirect to the Simple Edit Counter if the user has too high of an edit count.
58
     * @inheritDoc
59
     * @codeCoverageIgnore
60
     */
61
    public function tooHighEditCountRoute(): string
62
    {
63
        return 'SimpleEditCounterResult';
64
    }
65
66
    /**
67
     * @inheritDoc
68
     * @codeCoverageIgnore
69
     */
70
    public function tooHighEditCountActionAllowlist(): array
71
    {
72
        return ['rightsChanges'];
73
    }
74
75
    /**
76
     * @inheritDoc
77
     * @codeCoverageIgnore
78
     */
79
    public function restrictedApiActions(): array
80
    {
81
        return ['monthCountsApi', 'timecardApi'];
82
    }
83
84
    /**
85
     * Every action in this controller (other than 'index') calls this first.
86
     * If a response is returned, the calling action is expected to return it.
87
     * @param EditCounterRepository $editCounterRepo
88
     * @param UserRightsRepository $userRightsRepo
89
     * @param RequestStack $requestStack
90
     * @codeCoverageIgnore
91
     */
92
    protected function setUpEditCounter(
93
        EditCounterRepository $editCounterRepo,
94
        UserRightsRepository $userRightsRepo,
95
        RequestStack $requestStack
96
    ): void {
97
        // Whether we're making a subrequest (the view makes a request to another action).
98
        // Subrequests to the same controller do not re-instantiate a new controller, and hence
99
        // this flag would not be set in XtoolsController::__construct(), so we must do it here as well.
100
        $this->isSubRequest = $this->request->get('htmlonly')
101
            || null !== $requestStack->getParentRequest();
102
103
        // Return the EditCounter if we already have one.
104
        if (isset($this->editCounter)) {
105
            return;
106
        }
107
108
        // Will redirect to Simple Edit Counter if they have too many edits, as defined self::construct.
109
        $this->validateUser($this->user->getUsername());
0 ignored issues
show
Bug introduced by
The method getUsername() 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

109
        $this->validateUser($this->user->/** @scrutinizer ignore-call */ getUsername());

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...
110
111
        // Store which sections of the Edit Counter they requested.
112
        $this->sections = $this->getRequestedSections();
113
114
        $this->userRights = new UserRights($userRightsRepo, $this->project, $this->user, $this->i18n);
0 ignored issues
show
Bug introduced by
It seems like $this->user can also be of type null; however, parameter $user of App\Model\UserRights::__construct() does only seem to accept App\Model\User, 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

114
        $this->userRights = new UserRights($userRightsRepo, $this->project, /** @scrutinizer ignore-type */ $this->user, $this->i18n);
Loading history...
115
116
        // Instantiate EditCounter.
117
        $this->editCounter = new EditCounter(
118
            $editCounterRepo,
119
            $this->i18n,
120
            $this->userRights,
121
            $this->project,
122
            $this->user
0 ignored issues
show
Bug introduced by
It seems like $this->user can also be of type null; however, parameter $user of App\Model\EditCounter::__construct() does only seem to accept App\Model\User, 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

122
            /** @scrutinizer ignore-type */ $this->user
Loading history...
123
        );
124
    }
125
126
    /**
127
     * The initial GET request that displays the search form.
128
     * @Route("/ec", name="EditCounter")
129
     * @Route("/ec/index.php", name="EditCounterIndexPhp")
130
     * @Route("/ec/{project}", name="EditCounterProject")
131
     * @return RedirectResponse|Response
132
     */
133
    public function indexAction()
134
    {
135
        if (isset($this->params['project']) && isset($this->params['username'])) {
136
            return $this->redirectFromSections();
137
        }
138
139
        $this->sections = $this->getRequestedSections(true);
140
141
        // Otherwise fall through.
142
        return $this->render('editCounter/index.html.twig', [
143
            'xtPageTitle' => 'tool-editcounter',
144
            'xtSubtitle' => 'tool-editcounter-desc',
145
            'xtPage' => 'EditCounter',
146
            'project' => $this->project,
147
            'sections' => $this->sections,
148
            'availableSections' => $this->getSectionNames(),
149
            'isAllSections' => $this->sections === $this->getSectionNames(),
150
        ]);
151
    }
152
153
    /**
154
     * Get the requested sections either from the URL, cookie, or the defaults (all sections).
155
     * @param bool $useCookies Whether or not to check cookies for the preferred sections.
156
     *   This option should not be true except on the index form.
157
     * @return array|string[]
158
     * @codeCoverageIgnore
159
     */
160
    private function getRequestedSections(bool $useCookies = false): array
161
    {
162
        // Happens from sub-tool index pages, e.g. see self::generalStatsIndexAction().
163
        if (isset($this->sections)) {
164
            return $this->sections;
165
        }
166
167
        // Query param for sections gets priority.
168
        $sectionsQuery = $this->request->get('sections', '');
169
170
        // If not present, try the cookie, and finally the defaults (all sections).
171
        if ($useCookies && '' == $sectionsQuery) {
172
            $sectionsQuery = $this->request->cookies->get('XtoolsEditCounterOptions', '');
173
        }
174
175
        // Either a pipe-separated string or an array.
176
        $sections = is_array($sectionsQuery) ? $sectionsQuery : explode('|', $sectionsQuery);
177
178
        // Filter out any invalid section IDs.
179
        $sections = array_filter($sections, function ($section) {
180
            return in_array($section, $this->getSectionNames());
181
        });
182
183
        // Fallback for when no valid sections were requested or provided by the cookie.
184
        if (0 === count($sections)) {
185
            $sections = $this->getSectionNames();
186
        }
187
188
        return $sections;
189
    }
190
191
    /**
192
     * Get the names of the available sections.
193
     * @return string[]
194
     * @codeCoverageIgnore
195
     */
196
    private function getSectionNames(): array
197
    {
198
        return array_keys(self::AVAILABLE_SECTIONS);
199
    }
200
201
    /**
202
     * Redirect to the appropriate action based on what sections are being requested.
203
     * @return RedirectResponse
204
     * @codeCoverageIgnore
205
     */
206
    private function redirectFromSections(): RedirectResponse
207
    {
208
        $this->sections = $this->getRequestedSections();
209
210
        if (1 === count($this->sections)) {
211
            // Redirect to dedicated route.
212
            $response = $this->redirectToRoute(self::AVAILABLE_SECTIONS[$this->sections[0]], $this->params);
213
        } elseif ($this->sections === $this->getSectionNames()) {
214
            $response = $this->redirectToRoute('EditCounterResult', $this->params);
215
        } else {
216
            // Add sections to the params, which $this->generalUrl() will append to the URL.
217
            $this->params['sections'] = implode('|', $this->sections);
218
219
            // We want a pretty URL, with pipes | instead of the encoded value %7C
220
            $url = str_replace('%7C', '|', $this->generateUrl('EditCounterResult', $this->params));
221
222
            $response = $this->redirect($url);
223
        }
224
225
        // Save the preferred sections in a cookie.
226
        $response->headers->setCookie(
227
            new Cookie('XtoolsEditCounterOptions', implode('|', $this->sections))
228
        );
229
230
        return $response;
231
    }
232
233
    /**
234
     * Display all results.
235
     * @Route(
236
     *     "/ec/{project}/{username}",
237
     *     name="EditCounterResult",
238
     *     requirements={
239
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
240
     *     }
241
     * )
242
     * @param EditCounterRepository $editCounterRepo
243
     * @param UserRightsRepository $userRightsRepo
244
     * @param RequestStack $requestStack
245
     * @return Response|RedirectResponse
246
     * @codeCoverageIgnore
247
     */
248
    public function resultAction(
249
        EditCounterRepository $editCounterRepo,
250
        UserRightsRepository $userRightsRepo,
251
        RequestStack $requestStack
252
    ) {
253
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo, $requestStack);
254
255
        if (1 === count($this->sections)) {
256
            // Redirect to dedicated route.
257
            return $this->redirectToRoute(self::AVAILABLE_SECTIONS[$this->sections[0]], $this->params);
258
        }
259
260
        $ret = [
261
            'xtTitle' => $this->user->getUsername() . ' - ' . $this->project->getTitle(),
262
            'xtPage' => 'EditCounter',
263
            'user' => $this->user,
264
            'project' => $this->project,
265
            'ec' => $this->editCounter,
266
            'sections' => $this->sections,
267
            'isAllSections' => $this->sections === $this->getSectionNames(),
268
        ];
269
270
        // Used when querying for global rights changes.
271
        if ($this->isWMF) {
272
            $ret['metaProject'] = $this->projectRepo->getProject('metawiki');
273
        }
274
275
        return $this->getFormattedResponse('editCounter/result', $ret);
276
    }
277
278
    /**
279
     * Display the general statistics section.
280
     * @Route(
281
     *     "/ec-generalstats/{project}/{username}",
282
     *     name="EditCounterGeneralStats",
283
     *     requirements={
284
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
285
     *     }
286
     * )
287
     * @param EditCounterRepository $editCounterRepo
288
     * @param UserRightsRepository $userRightsRepo
289
     * @param GlobalContribsRepository $globalContribsRepo
290
     * @param EditRepository $editRepo
291
     * @param RequestStack $requestStack
292
     * @return Response
293
     * @codeCoverageIgnore
294
     */
295
    public function generalStatsAction(
296
        EditCounterRepository $editCounterRepo,
297
        UserRightsRepository $userRightsRepo,
298
        GlobalContribsRepository $globalContribsRepo,
299
        EditRepository $editRepo,
300
        RequestStack $requestStack
301
    ): Response {
302
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo, $requestStack);
303
304
        $globalContribs = new GlobalContribs(
305
            $globalContribsRepo,
306
            $this->pageRepo,
307
            $this->userRepo,
308
            $editRepo,
309
            $this->user
0 ignored issues
show
Bug introduced by
It seems like $this->user can also be of type null; however, parameter $user of App\Model\GlobalContribs::__construct() does only seem to accept App\Model\User, 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

309
            /** @scrutinizer ignore-type */ $this->user
Loading history...
310
        );
311
        $ret = [
312
            'xtTitle' => $this->user->getUsername(),
313
            'xtPage' => 'EditCounter',
314
            'subtool_msg_key' => 'general-stats',
315
            'is_sub_request' => $this->isSubRequest,
316
            'user' => $this->user,
317
            'project' => $this->project,
318
            'ec' => $this->editCounter,
319
            'gc' => $globalContribs,
320
        ];
321
322
        // Output the relevant format template.
323
        return $this->getFormattedResponse('editCounter/general_stats', $ret);
324
    }
325
326
    /**
327
     * Search form for general stats.
328
     * @Route(
329
     *     "/ec-generalstats",
330
     *     name="EditCounterGeneralStatsIndex",
331
     *     requirements={
332
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
333
     *     }
334
     * )
335
     * @return Response
336
     */
337
    public function generalStatsIndexAction(): Response
338
    {
339
        $this->sections = ['general-stats'];
340
        return $this->indexAction();
341
    }
342
343
    /**
344
     * Display the namespace totals section.
345
     * @Route(
346
     *     "/ec-namespacetotals/{project}/{username}",
347
     *     name="EditCounterNamespaceTotals",
348
     *     requirements={
349
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
350
     *     }
351
     * )
352
     * @param EditCounterRepository $editCounterRepo
353
     * @param UserRightsRepository $userRightsRepo
354
     * @param RequestStack $requestStack
355
     * @return Response
356
     * @codeCoverageIgnore
357
     */
358
    public function namespaceTotalsAction(
359
        EditCounterRepository $editCounterRepo,
360
        UserRightsRepository $userRightsRepo,
361
        RequestStack $requestStack
362
    ): Response {
363
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo, $requestStack);
364
365
        $ret = [
366
            'xtTitle' => $this->user->getUsername(),
367
            'xtPage' => 'EditCounter',
368
            'subtool_msg_key' => 'namespace-totals',
369
            'is_sub_request' => $this->isSubRequest,
370
            'user' => $this->user,
371
            'project' => $this->project,
372
            'ec' => $this->editCounter,
373
        ];
374
375
        // Output the relevant format template.
376
        return $this->getFormattedResponse('editCounter/namespace_totals', $ret);
377
    }
378
379
    /**
380
     * Search form for namespace totals.
381
     * @Route("/ec-namespacetotals", name="EditCounterNamespaceTotalsIndex")
382
     * @return Response
383
     */
384
    public function namespaceTotalsIndexAction(): Response
385
    {
386
        $this->sections = ['namespace-totals'];
387
        return $this->indexAction();
388
    }
389
390
    /**
391
     * Display the timecard section.
392
     * @Route(
393
     *     "/ec-timecard/{project}/{username}",
394
     *     name="EditCounterTimecard",
395
     *     requirements={
396
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
397
     *     }
398
     * )
399
     * @param EditCounterRepository $editCounterRepo
400
     * @param UserRightsRepository $userRightsRepo
401
     * @param RequestStack $requestStack
402
     * @return Response
403
     * @codeCoverageIgnore
404
     */
405
    public function timecardAction(
406
        EditCounterRepository $editCounterRepo,
407
        UserRightsRepository $userRightsRepo,
408
        RequestStack $requestStack
409
    ): Response {
410
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo, $requestStack);
411
412
        $ret = [
413
            'xtTitle' => $this->user->getUsername(),
414
            'xtPage' => 'EditCounter',
415
            'subtool_msg_key' => 'timecard',
416
            'is_sub_request' => $this->isSubRequest,
417
            'user' => $this->user,
418
            'project' => $this->project,
419
            'ec' => $this->editCounter,
420
            'opted_in_page' => $this->getOptedInPage(),
421
        ];
422
423
        // Output the relevant format template.
424
        return $this->getFormattedResponse('editCounter/timecard', $ret);
425
    }
426
427
    /**
428
     * Search form for timecard.
429
     * @Route("/ec-timecard", name="EditCounterTimecardIndex")
430
     * @return Response
431
     */
432
    public function timecardIndexAction(): Response
433
    {
434
        $this->sections = ['timecard'];
435
        return $this->indexAction();
436
    }
437
438
    /**
439
     * Display the year counts section.
440
     * @Route(
441
     *     "/ec-yearcounts/{project}/{username}",
442
     *     name="EditCounterYearCounts",
443
     *     requirements={
444
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
445
     *     }
446
     * )
447
     * @param EditCounterRepository $editCounterRepo
448
     * @param UserRightsRepository $userRightsRepo
449
     * @param RequestStack $requestStack
450
     * @return Response
451
     * @codeCoverageIgnore
452
     */
453
    public function yearCountsAction(
454
        EditCounterRepository $editCounterRepo,
455
        UserRightsRepository $userRightsRepo,
456
        RequestStack $requestStack
457
    ): Response {
458
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo, $requestStack);
459
460
        $ret = [
461
            'xtTitle' => $this->user->getUsername(),
462
            'xtPage' => 'EditCounter',
463
            'subtool_msg_key' => 'year-counts',
464
            'is_sub_request' => $this->isSubRequest,
465
            'user' => $this->user,
466
            'project' => $this->project,
467
            'ec' => $this->editCounter,
468
        ];
469
470
        // Output the relevant format template.
471
        return $this->getFormattedResponse('editCounter/yearcounts', $ret);
472
    }
473
474
    /**
475
     * Search form for year counts.
476
     * @Route("/ec-yearcounts", name="EditCounterYearCountsIndex")
477
     * @return Response
478
     */
479
    public function yearCountsIndexAction(): Response
480
    {
481
        $this->sections = ['year-counts'];
482
        return $this->indexAction();
483
    }
484
485
    /**
486
     * Display the month counts section.
487
     * @Route(
488
     *     "/ec-monthcounts/{project}/{username}",
489
     *     name="EditCounterMonthCounts",
490
     *     requirements={
491
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
492
     *     }
493
     * )
494
     * @param EditCounterRepository $editCounterRepo
495
     * @param UserRightsRepository $userRightsRepo
496
     * @param RequestStack $requestStack
497
     * @return Response
498
     * @codeCoverageIgnore
499
     */
500
    public function monthCountsAction(
501
        EditCounterRepository $editCounterRepo,
502
        UserRightsRepository $userRightsRepo,
503
        RequestStack $requestStack
504
    ): Response {
505
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo, $requestStack);
506
507
        $ret = [
508
            'xtTitle' => $this->user->getUsername(),
509
            'xtPage' => 'EditCounter',
510
            'subtool_msg_key' => 'month-counts',
511
            'is_sub_request' => $this->isSubRequest,
512
            'user' => $this->user,
513
            'project' => $this->project,
514
            'ec' => $this->editCounter,
515
            'opted_in_page' => $this->getOptedInPage(),
516
        ];
517
518
        // Output the relevant format template.
519
        return $this->getFormattedResponse('editCounter/monthcounts', $ret);
520
    }
521
522
    /**
523
     * Search form for month counts.
524
     * @Route("/ec-monthcounts", name="EditCounterMonthCountsIndex")
525
     * @return Response
526
     */
527
    public function monthCountsIndexAction(): Response
528
    {
529
        $this->sections = ['month-counts'];
530
        return $this->indexAction();
531
    }
532
533
    /**
534
     * Display the user rights changes section.
535
     * @Route(
536
     *     "/ec-rightschanges/{project}/{username}",
537
     *     name="EditCounterRightsChanges",
538
     *     requirements={
539
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
540
     *     }
541
     * )
542
     * @param EditCounterRepository $editCounterRepo
543
     * @param UserRightsRepository $userRightsRepo
544
     * @param RequestStack $requestStack
545
     * @return Response
546
     * @codeCoverageIgnore
547
     */
548
    public function rightsChangesAction(
549
        EditCounterRepository $editCounterRepo,
550
        UserRightsRepository $userRightsRepo,
551
        RequestStack $requestStack
552
    ): Response {
553
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo, $requestStack);
554
555
        $ret = [
556
            'xtTitle' => $this->user->getUsername(),
557
            'xtPage' => 'EditCounter',
558
            'is_sub_request' => $this->isSubRequest,
559
            'user' => $this->user,
560
            'project' => $this->project,
561
            'ec' => $this->editCounter,
562
        ];
563
564
        if ($this->isWMF) {
565
            $ret['metaProject'] = $this->projectRepo->getProject('metawiki');
566
        }
567
568
        // Output the relevant format template.
569
        return $this->getFormattedResponse('editCounter/rights_changes', $ret);
570
    }
571
572
    /**
573
     * Search form for rights changes.
574
     * @Route("/ec-rightschanges", name="EditCounterRightsChangesIndex")
575
     * @return Response
576
     */
577
    public function rightsChangesIndexAction(): Response
578
    {
579
        $this->sections = ['rights-changes'];
580
        return $this->indexAction();
581
    }
582
583
    /************************ API endpoints ************************/
584
585
    /**
586
     * Get counts of various log actions made by the user.
587
     * @Route(
588
     *     "/api/user/log_counts/{project}/{username}",
589
     *     name="UserApiLogCounts",
590
     *     requirements={
591
     *         "username"="(ipr-.+\/\d+[^\/])|([^\/]+)",
592
     *     },
593
     *     methods={"GET"}
594
     * )
595
     * @OA\Tag(name="User API")
596
     * @OA\Get(description="Get counts of various logged actions made by a user. The keys of the returned `log_counts`
597
           property describe the log type and log action in the form of _type-action_.
598
           See also the [logevents API](https://www.mediawiki.org/wiki/Special:MyLanguage/API:Logevents).")
599
     * @OA\ExternalDocumentation(url="https://www.mediawiki.org/wiki/Manual:Log_actions")
600
     * @OA\Parameter(ref="#/components/parameters/Project")
601
     * @OA\Parameter(ref="#/components/parameters/UsernameOrIp")
602
     * @OA\Response(
603
     *     response=200,
604
     *     description="Counts of logged actions",
605
     *     @OA\JsonContent(
606
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
607
     *         @OA\Property(property="username", ref="#/components/parameters/UsernameOrIp/schema"),
608
     *         @OA\Property(property="log_counts", type="object", example={
609
     *             "block-block": 0,
610
     *             "block-unblock": 0,
611
     *             "protect-protect": 0,
612
     *             "protect-unprotect": 0,
613
     *             "move-move": 0,
614
     *             "move-move_redir": 0
615
     *         })
616
     *     )
617
     * )
618
     * @OA\Response(response=404, ref="#/components/responses/404")
619
     * @OA\Response(response=501, ref="#/components/responses/501")
620
     * @OA\Response(response=503, ref="#/components/responses/503")
621
     * @OA\Response(response=504, ref="#/components/responses/504")
622
     * @param EditCounterRepository $editCounterRepo
623
     * @param UserRightsRepository $userRightsRepo
624
     * @param RequestStack $requestStack
625
     * @return JsonResponse
626
     * @codeCoverageIgnore
627
     */
628
    public function logCountsApiAction(
629
        EditCounterRepository $editCounterRepo,
630
        UserRightsRepository $userRightsRepo,
631
        RequestStack $requestStack
632
    ): JsonResponse {
633
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo, $requestStack);
634
635
        return $this->getFormattedApiResponse([
636
            'log_counts' => $this->editCounter->getLogCounts(),
637
        ]);
638
    }
639
640
    /**
641
     * Get the number of edits made by the user to each namespace.
642
     * @Route(
643
     *     "/api/user/namespace_totals/{project}/{username}",
644
     *     name="UserApiNamespaceTotals",
645
     *     requirements={
646
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
647
     *     },
648
     *     methods={"GET"}
649
     * )
650
     * @OA\Tag(name="User API")
651
     * @OA\Get(description="Get edit counts of a user broken down by [namespace](https://w.wiki/6oKq).")
652
     * @OA\Parameter(ref="#/components/parameters/Project")
653
     * @OA\Parameter(ref="#/components/parameters/UsernameOrIp")
654
     * @OA\Response(
655
     *     response=200,
656
     *     description="Namepsace totals",
657
     *     @OA\JsonContent(
658
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
659
     *         @OA\Property(property="username", ref="#/components/parameters/UsernameOrIp/schema"),
660
     *         @OA\Property(property="namespace_totals", type="object", example={"0": 50, "2": 10, "3": 100},
661
     *             description="Keys are namespace IDs, values are edit counts.")
662
     *     )
663
     * )
664
     * @OA\Response(response=404, ref="#/components/responses/404")
665
     * @OA\Response(response=501, ref="#/components/responses/501")
666
     * @OA\Response(response=503, ref="#/components/responses/503")
667
     * @OA\Response(response=504, ref="#/components/responses/504")
668
     * @param EditCounterRepository $editCounterRepo
669
     * @param UserRightsRepository $userRightsRepo
670
     * @param RequestStack $requestStack
671
     * @return JsonResponse
672
     * @codeCoverageIgnore
673
     */
674
    public function namespaceTotalsApiAction(
675
        EditCounterRepository $editCounterRepo,
676
        UserRightsRepository $userRightsRepo,
677
        RequestStack $requestStack
678
    ): JsonResponse {
679
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo, $requestStack);
680
681
        return $this->getFormattedApiResponse([
682
            'namespace_totals' => (object)$this->editCounter->namespaceTotals(),
683
        ]);
684
    }
685
686
    /**
687
     * Get the number of edits made by the user for each month, grouped by namespace.
688
     * @Route(
689
     *     "/api/user/month_counts/{project}/{username}",
690
     *     name="UserApiMonthCounts",
691
     *     requirements={
692
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
693
     *     },
694
     *     methods={"GET"}
695
     * )
696
     * @OA\Tag(name="User API")
697
     * @OA\Get(description="Get the number of edits a user has made grouped by namespace and month.")
698
     * @OA\Parameter(ref="#/components/parameters/Project")
699
     * @OA\Parameter(ref="#/components/parameters/UsernameOrIp")
700
     * @OA\Response(
701
     *     response=200,
702
     *     description="Month counts",
703
     *     @OA\JsonContent(
704
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
705
     *         @OA\Property(property="username", ref="#/components/parameters/UsernameOrIp/schema"),
706
     *         @OA\Property(property="totals", type="object", example={
707
     *             "0": {
708
     *                 "2020-11": 40,
709
     *                 "2020-12": 50,
710
     *                 "2021-01": 5
711
     *             },
712
     *             "3": {
713
     *                 "2020-11": 0,
714
     *                 "2020-12": 10,
715
     *                 "2021-01": 0
716
     *             }
717
     *         })
718
     *     )
719
     * )
720
     * @OA\Response(response=404, ref="#/components/responses/404")
721
     * @OA\Response(response=501, ref="#/components/responses/501")
722
     * @OA\Response(response=503, ref="#/components/responses/503")
723
     * @OA\Response(response=504, ref="#/components/responses/504")
724
     * @param EditCounterRepository $editCounterRepo
725
     * @param UserRightsRepository $userRightsRepo
726
     * @param RequestStack $requestStack
727
     * @return JsonResponse
728
     * @codeCoverageIgnore
729
     */
730
    public function monthCountsApiAction(
731
        EditCounterRepository $editCounterRepo,
732
        UserRightsRepository $userRightsRepo,
733
        RequestStack $requestStack
734
    ): JsonResponse {
735
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo, $requestStack);
736
737
        $ret = $this->editCounter->monthCounts();
738
739
        // Remove labels that are only needed by Twig views, and not consumers of the API.
740
        unset($ret['yearLabels']);
741
        unset($ret['monthLabels']);
742
743
        // Ensure 'totals' keys are strings, see T292031.
744
        $ret['totals'] = (object)$ret['totals'];
745
746
        return $this->getFormattedApiResponse($ret);
747
    }
748
749
    /**
750
     * Get the total number of edits made by a user during each hour of day and day of week.
751
     * @Route(
752
     *     "/api/user/timecard/{project}/{username}",
753
     *     name="UserApiTimeCard",
754
     *     requirements={
755
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
756
     *     },
757
     *     methods={"GET"}
758
     * )
759
     * @OA\Tag(name="User API")
760
     * @OA\Get(description="Get the raw number of edits made by a user during each hour of day and day of week. The
761
            `scale` is a value that indicates the number of edits made relative to other hours and days of the week.")
762
     * @OA\Parameter(ref="#/components/parameters/Project")
763
     * @OA\Parameter(ref="#/components/parameters/UsernameOrIp")
764
     * @OA\Response(
765
     *     response=200,
766
     *     description="Timecard",
767
     *     @OA\JsonContent(
768
     *         @OA\Property(property="project", ref="#/components/parameters/Project/schema"),
769
     *         @OA\Property(property="username", ref="#/components/parameters/UsernameOrIp/schema"),
770
     *         @OA\Property(property="timecard", type="array", @OA\Items(type="object"), example={
771
     *             {
772
     *                 "day_of_week": 1,
773
     *                 "hour": 0,
774
     *                 "value": 50,
775
     *                 "scale": 5
776
     *             }
777
     *         })
778
     *     )
779
     * )
780
     * @OA\Response(response=404, ref="#/components/responses/404")
781
     * @OA\Response(response=501, ref="#/components/responses/501")
782
     * @OA\Response(response=503, ref="#/components/responses/503")
783
     * @OA\Response(response=504, ref="#/components/responses/504")
784
     * @param EditCounterRepository $editCounterRepo
785
     * @param UserRightsRepository $userRightsRepo
786
     * @param RequestStack $requestStack
787
     * @return JsonResponse
788
     * @codeCoverageIgnore
789
     */
790
    public function timecardApiAction(
791
        EditCounterRepository $editCounterRepo,
792
        UserRightsRepository $userRightsRepo,
793
        RequestStack $requestStack
794
    ): JsonResponse {
795
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo, $requestStack);
796
797
        return $this->getFormattedApiResponse([
798
            'timecard' => $this->editCounter->timeCard(),
799
        ]);
800
    }
801
}
802