Passed
Push — main ( 49d96d...eb753e )
by MusikAnimal
04:04
created

EditCounterController::resultAction()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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

105
        $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...
106
107
        // Store which sections of the Edit Counter they requested.
108
        $this->sections = $this->getRequestedSections();
109
110
        $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

110
        $this->userRights = new UserRights($userRightsRepo, $this->project, /** @scrutinizer ignore-type */ $this->user, $this->i18n);
Loading history...
111
112
        // Instantiate EditCounter.
113
        $this->editCounter = new EditCounter(
114
            $editCounterRepo,
115
            $this->i18n,
116
            $this->userRights,
117
            $this->project,
118
            $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

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

299
            /** @scrutinizer ignore-type */ $this->user
Loading history...
300
        );
301
        $ret = [
302
            'xtTitle' => $this->user->getUsername(),
303
            'xtPage' => 'EditCounter',
304
            'subtool_msg_key' => 'general-stats',
305
            'is_sub_request' => $this->isSubRequest,
306
            'user' => $this->user,
307
            'project' => $this->project,
308
            'ec' => $this->editCounter,
309
            'gc' => $globalContribs,
310
        ];
311
312
        // Output the relevant format template.
313
        return $this->getFormattedResponse('editCounter/general_stats', $ret);
314
    }
315
316
    /**
317
     * Search form for general stats.
318
     * @Route(
319
     *     "/ec-generalstats",
320
     *     name="EditCounterGeneralStatsIndex",
321
     *     requirements={
322
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
323
     *     }
324
     * )
325
     * @return Response
326
     */
327
    public function generalStatsIndexAction(): Response
328
    {
329
        $this->sections = ['general-stats'];
330
        return $this->indexAction();
331
    }
332
333
    /**
334
     * Display the namespace totals section.
335
     * @Route(
336
     *     "/ec-namespacetotals/{project}/{username}",
337
     *     name="EditCounterNamespaceTotals",
338
     *     requirements={
339
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
340
     *     }
341
     * )
342
     * @param EditCounterRepository $editCounterRepo
343
     * @param UserRightsRepository $userRightsRepo
344
     * @return Response
345
     * @codeCoverageIgnore
346
     */
347
    public function namespaceTotalsAction(
348
        EditCounterRepository $editCounterRepo,
349
        UserRightsRepository $userRightsRepo
350
    ): Response {
351
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo);
352
353
        $ret = [
354
            'xtTitle' => $this->user->getUsername(),
355
            'xtPage' => 'EditCounter',
356
            'subtool_msg_key' => 'namespace-totals',
357
            'is_sub_request' => $this->isSubRequest,
358
            'user' => $this->user,
359
            'project' => $this->project,
360
            'ec' => $this->editCounter,
361
        ];
362
363
        // Output the relevant format template.
364
        return $this->getFormattedResponse('editCounter/namespace_totals', $ret);
365
    }
366
367
    /**
368
     * Search form for namespace totals.
369
     * @Route("/ec-namespacetotals", name="EditCounterNamespaceTotalsIndex")
370
     * @return Response
371
     */
372
    public function namespaceTotalsIndexAction(): Response
373
    {
374
        $this->sections = ['namespace-totals'];
375
        return $this->indexAction();
376
    }
377
378
    /**
379
     * Display the timecard section.
380
     * @Route(
381
     *     "/ec-timecard/{project}/{username}",
382
     *     name="EditCounterTimecard",
383
     *     requirements={
384
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
385
     *     }
386
     * )
387
     * @param EditCounterRepository $editCounterRepo
388
     * @param UserRightsRepository $userRightsRepo
389
     * @return Response
390
     * @codeCoverageIgnore
391
     */
392
    public function timecardAction(
393
        EditCounterRepository $editCounterRepo,
394
        UserRightsRepository $userRightsRepo
395
    ): Response {
396
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo);
397
398
        $ret = [
399
            'xtTitle' => $this->user->getUsername(),
400
            'xtPage' => 'EditCounter',
401
            'subtool_msg_key' => 'timecard',
402
            'is_sub_request' => $this->isSubRequest,
403
            'user' => $this->user,
404
            'project' => $this->project,
405
            'ec' => $this->editCounter,
406
            'opted_in_page' => $this->getOptedInPage(),
407
        ];
408
409
        // Output the relevant format template.
410
        return $this->getFormattedResponse('editCounter/timecard', $ret);
411
    }
412
413
    /**
414
     * Search form for timecard.
415
     * @Route("/ec-timecard", name="EditCounterTimecardIndex")
416
     * @return Response
417
     */
418
    public function timecardIndexAction(): Response
419
    {
420
        $this->sections = ['timecard'];
421
        return $this->indexAction();
422
    }
423
424
    /**
425
     * Display the year counts section.
426
     * @Route(
427
     *     "/ec-yearcounts/{project}/{username}",
428
     *     name="EditCounterYearCounts",
429
     *     requirements={
430
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
431
     *     }
432
     * )
433
     * @param EditCounterRepository $editCounterRepo
434
     * @param UserRightsRepository $userRightsRepo
435
     * @return Response
436
     * @codeCoverageIgnore
437
     */
438
    public function yearCountsAction(
439
        EditCounterRepository $editCounterRepo,
440
        UserRightsRepository $userRightsRepo
441
    ): Response {
442
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo);
443
444
        $ret = [
445
            'xtTitle' => $this->user->getUsername(),
446
            'xtPage' => 'EditCounter',
447
            'subtool_msg_key' => 'year-counts',
448
            'is_sub_request' => $this->isSubRequest,
449
            'user' => $this->user,
450
            'project' => $this->project,
451
            'ec' => $this->editCounter,
452
        ];
453
454
        // Output the relevant format template.
455
        return $this->getFormattedResponse('editCounter/yearcounts', $ret);
456
    }
457
458
    /**
459
     * Search form for year counts.
460
     * @Route("/ec-yearcounts", name="EditCounterYearCountsIndex")
461
     * @return Response
462
     */
463
    public function yearCountsIndexAction(): Response
464
    {
465
        $this->sections = ['year-counts'];
466
        return $this->indexAction();
467
    }
468
469
    /**
470
     * Display the month counts section.
471
     * @Route(
472
     *     "/ec-monthcounts/{project}/{username}",
473
     *     name="EditCounterMonthCounts",
474
     *     requirements={
475
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
476
     *     }
477
     * )
478
     * @param EditCounterRepository $editCounterRepo
479
     * @param UserRightsRepository $userRightsRepo
480
     * @return Response
481
     * @codeCoverageIgnore
482
     */
483
    public function monthCountsAction(
484
        EditCounterRepository $editCounterRepo,
485
        UserRightsRepository $userRightsRepo
486
    ): Response {
487
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo);
488
489
        $ret = [
490
            'xtTitle' => $this->user->getUsername(),
491
            'xtPage' => 'EditCounter',
492
            'subtool_msg_key' => 'month-counts',
493
            'is_sub_request' => $this->isSubRequest,
494
            'user' => $this->user,
495
            'project' => $this->project,
496
            'ec' => $this->editCounter,
497
            'opted_in_page' => $this->getOptedInPage(),
498
        ];
499
500
        // Output the relevant format template.
501
        return $this->getFormattedResponse('editCounter/monthcounts', $ret);
502
    }
503
504
    /**
505
     * Search form for month counts.
506
     * @Route("/ec-monthcounts", name="EditCounterMonthCountsIndex")
507
     * @return Response
508
     */
509
    public function monthCountsIndexAction(): Response
510
    {
511
        $this->sections = ['month-counts'];
512
        return $this->indexAction();
513
    }
514
515
    /**
516
     * Display the user rights changes section.
517
     * @Route(
518
     *     "/ec-rightschanges/{project}/{username}",
519
     *     name="EditCounterRightsChanges",
520
     *     requirements={
521
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
522
     *     }
523
     * )
524
     * @param EditCounterRepository $editCounterRepo
525
     * @param UserRightsRepository $userRightsRepo
526
     * @return Response
527
     * @codeCoverageIgnore
528
     */
529
    public function rightsChangesAction(
530
        EditCounterRepository $editCounterRepo,
531
        UserRightsRepository $userRightsRepo
532
    ): Response {
533
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo);
534
535
        $ret = [
536
            'xtTitle' => $this->user->getUsername(),
537
            'xtPage' => 'EditCounter',
538
            'is_sub_request' => $this->isSubRequest,
539
            'user' => $this->user,
540
            'project' => $this->project,
541
            'ec' => $this->editCounter,
542
        ];
543
544
        if ($this->isWMF) {
545
            $ret['metaProject'] = $this->projectRepo->getProject('metawiki');
546
        }
547
548
        // Output the relevant format template.
549
        return $this->getFormattedResponse('editCounter/rights_changes', $ret);
550
    }
551
552
    /**
553
     * Search form for rights changes.
554
     * @Route("/ec-rightschanges", name="EditCounterRightsChangesIndex")
555
     * @return Response
556
     */
557
    public function rightsChangesIndexAction(): Response
558
    {
559
        $this->sections = ['rights-changes'];
560
        return $this->indexAction();
561
    }
562
563
    /************************ API endpoints ************************/
564
565
    /**
566
     * Get various log counts for the user as JSON.
567
     * @Route(
568
     *     "/api/user/log_counts/{project}/{username}",
569
     *     name="UserApiLogCounts",
570
     *     requirements={
571
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
572
     *     }
573
     * )
574
     * @param EditCounterRepository $editCounterRepo
575
     * @param UserRightsRepository $userRightsRepo
576
     * @return JsonResponse
577
     * @codeCoverageIgnore
578
     */
579
    public function logCountsApiAction(
580
        EditCounterRepository $editCounterRepo,
581
        UserRightsRepository $userRightsRepo
582
    ): JsonResponse {
583
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo);
584
585
        return $this->getFormattedApiResponse([
586
            'log_counts' => $this->editCounter->getLogCounts(),
587
        ]);
588
    }
589
590
    /**
591
     * Get the namespace totals for the user as JSON.
592
     * @Route(
593
     *     "/api/user/namespace_totals/{project}/{username}",
594
     *     name="UserApiNamespaceTotals",
595
     *     requirements={
596
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
597
     *     }
598
     * )
599
     * @param EditCounterRepository $editCounterRepo
600
     * @param UserRightsRepository $userRightsRepo
601
     * @return JsonResponse
602
     * @codeCoverageIgnore
603
     */
604
    public function namespaceTotalsApiAction(
605
        EditCounterRepository $editCounterRepo,
606
        UserRightsRepository $userRightsRepo
607
    ): JsonResponse {
608
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo);
609
610
        return $this->getFormattedApiResponse([
611
            'namespace_totals' => (object)$this->editCounter->namespaceTotals(),
612
        ]);
613
    }
614
615
    /**
616
     * Get the month counts for the user as JSON.
617
     * @Route(
618
     *     "/api/user/month_counts/{project}/{username}",
619
     *     name="UserApiMonthCounts",
620
     *     requirements={
621
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
622
     *     }
623
     * )
624
     * @param EditCounterRepository $editCounterRepo
625
     * @param UserRightsRepository $userRightsRepo
626
     * @return JsonResponse
627
     * @codeCoverageIgnore
628
     */
629
    public function monthCountsApiAction(
630
        EditCounterRepository $editCounterRepo,
631
        UserRightsRepository $userRightsRepo
632
    ): JsonResponse {
633
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo);
634
635
        $ret = $this->editCounter->monthCounts();
636
637
        // Remove labels that are only needed by Twig views, and not consumers of the API.
638
        unset($ret['yearLabels']);
639
        unset($ret['monthLabels']);
640
641
        return $this->getFormattedApiResponse($ret);
642
    }
643
644
    /**
645
     * Get the timecard data as JSON.
646
     * @Route(
647
     *     "/api/user/timecard/{project}/{username}",
648
     *     name="UserApiTimeCard",
649
     *     requirements={
650
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
651
     *     }
652
     * )
653
     * @param EditCounterRepository $editCounterRepo
654
     * @param UserRightsRepository $userRightsRepo
655
     * @return JsonResponse
656
     * @codeCoverageIgnore
657
     */
658
    public function timecardApiAction(
659
        EditCounterRepository $editCounterRepo,
660
        UserRightsRepository $userRightsRepo
661
    ): JsonResponse {
662
        $this->setUpEditCounter($editCounterRepo, $userRightsRepo);
663
664
        return $this->getFormattedApiResponse([
665
            'timecard' => $this->editCounter->timeCard(),
666
        ]);
667
    }
668
}
669