Passed
Push — master ( 0c65d8...e43fc5 )
by MusikAnimal
18:43
created

EditCounterController::rightsChangesAction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 11
nc 2
nop 0
dl 0
loc 19
ccs 0
cts 0
cp 0
crap 6
rs 9.9
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the EditCounterController class.
4
 */
5
6
declare(strict_types=1);
7
8
namespace AppBundle\Controller;
9
10
use AppBundle\Exception\XtoolsHttpException;
11
use AppBundle\Helper\I18nHelper;
12
use AppBundle\Model\EditCounter;
13
use AppBundle\Model\GlobalContribs;
14
use AppBundle\Repository\EditCounterRepository;
15
use AppBundle\Repository\GlobalContribsRepository;
16
use AppBundle\Repository\ProjectRepository;
17
use Symfony\Component\DependencyInjection\ContainerInterface;
18
use Symfony\Component\HttpFoundation\Cookie;
19
use Symfony\Component\HttpFoundation\JsonResponse;
20
use Symfony\Component\HttpFoundation\RedirectResponse;
21
use Symfony\Component\HttpFoundation\RequestStack;
22
use Symfony\Component\HttpFoundation\Response;
23
use Symfony\Component\Routing\Annotation\Route;
24
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
25
26
/**
27
 * Class EditCounterController
28
 */
29
class EditCounterController extends XtoolsController
30
{
31
    /**
32
     * Available statistic sections. These can be hand-picked on the index form so that you only get the data you
33
     * want and hence speed up the tool. Keys are the i18n messages (and DOM IDs), values are the action names.
34
     */
35
    private const AVAILABLE_SECTIONS = [
36
        'general-stats' => 'EditCounterGeneralStats',
37
        'namespace-totals' => 'EditCounterNamespaceTotals',
38
        'year-counts' => 'EditCounterYearCounts',
39
        'month-counts' => 'EditCounterMonthCounts',
40
        'timecard' => 'EditCounterTimecard',
41
        'top-edited-pages' => 'TopEditsResultNamespace',
42
        'rights-changes' => 'EditCounterRightsChanges',
43
        'latest-global-edits' => 'GlobalContribsResult',
44
    ];
45
46
    /** @var EditCounter The edit-counter, that does all the work. */
47
    protected $editCounter;
48
49
    /** @var string[] Which sections to show. */
50
    protected $sections;
51
52
    /**
53
     * Get the name of the tool's index route. This is also the name of the associated model.
54
     * @return string
55
     * @codeCoverageIgnore
56
     */
57
    public function getIndexRoute(): string
58
    {
59
        return 'EditCounter';
60
    }
61
62
    /**
63
     * EditCounterController constructor.
64
     * @param RequestStack $requestStack
65
     * @param ContainerInterface $container
66
     * @param I18nHelper $i18n
67
     */
68 2
    public function __construct(RequestStack $requestStack, ContainerInterface $container, I18nHelper $i18n)
69
    {
70
        // Causes the tool to redirect to the Simple Edit Counter if the user has too high of an edit count.
71 2
        $this->tooHighEditCountAction = 'SimpleEditCounterResult';
72
73
        // The rightsChanges action is exempt from the edit count limitation.
74 2
        $this->tooHighEditCountActionBlacklist = ['rightsChanges'];
75
76 2
        $this->restrictedActions = ['monthCountsApi', 'timecardApi'];
77
78 2
        parent::__construct($requestStack, $container, $i18n);
79 2
    }
80
81
    /**
82
     * Every action in this controller (other than 'index') calls this first.
83
     * If a response is returned, the calling action is expected to return it.
84
     * @return null
85
     * @throws AccessDeniedException If attempting to access internal endpoint.
86
     * @throws XtoolsHttpException If an API request to restricted endpoint when user has not opted in.
87
     * @codeCoverageIgnore
88
     */
89
    protected function setUpEditCounter()
90
    {
91
        // Whether we're making a subrequest (the view makes a request to another action).
92
        // Subrequests to the same controller do not re-instantiate a new controller, and hence
93
        // this flag would not be set in XtoolsController::__construct(), so we must do it here as well.
94
        $this->isSubRequest = $this->request->get('htmlonly')
95
            || null !== $this->get('request_stack')->getParentRequest();
96
97
        // Return the EditCounter if we already have one.
98
        if (isset($this->editCounter)) {
99
            return null;
100
        }
101
102
        // Will redirect to Simple Edit Counter if they have too many edits, as defined self::construct.
103
        $this->validateUser($this->user->getUsername());
104
105
        // Store which sections of the Edit Counter they requested.
106
        $this->sections = $this->getRequestedSections();
107
108
        // Instantiate EditCounter.
109
        $editCounterRepo = new EditCounterRepository();
110
        $editCounterRepo->setContainer($this->container);
111
        $this->editCounter = new EditCounter(
112
            $this->project,
113
            $this->user,
114
            $this->container->get('app.i18n_helper')
115
        );
116
        $this->editCounter->setRepository($editCounterRepo);
117
    }
118
119
    /**
120
     * The initial GET request that displays the search form.
121
     * @Route("/ec", name="EditCounter")
122
     * @Route("/ec/index.php", name="EditCounterIndexPhp")
123
     * @Route("/ec/{project}", name="EditCounterProject")
124
     * @return RedirectResponse|Response
125
     */
126 2
    public function indexAction()
127
    {
128 2
        if (isset($this->params['project']) && isset($this->params['username'])) {
129
            return $this->redirectFromSections();
130
        }
131
132 2
        $this->sections = $this->getRequestedSections(true);
133
134
        // Otherwise fall through.
135 2
        return $this->render('editCounter/index.html.twig', [
136 2
            'xtPageTitle' => 'tool-editcounter',
137 2
            'xtSubtitle' => 'tool-editcounter-desc',
138 2
            'xtPage' => 'EditCounter',
139 2
            'project' => $this->project,
140 2
            'sections' => $this->sections,
141 2
            'availableSections' => $this->getSectionNames(),
142 2
            'isAllSections' => $this->sections === $this->getSectionNames(),
143
        ]);
144
    }
145
146
    /**
147
     * Get the requested sections either from the URL, cookie, or the defaults (all sections).
148
     * @param bool $useCookies Whether or not to check cookies for the preferred sections.
149
     *   This option should not be true except on the index form.
150
     * @return array|mixed|string[]
151
     * @codeCoverageIgnore
152
     */
153
    private function getRequestedSections(bool $useCookies = false)
154
    {
155
        // Happens from sub-tool index pages, e.g. see self::generalStatsIndexAction().
156
        if (isset($this->sections)) {
157
            return $this->sections;
158
        }
159
160
        // Query param for sections gets priority.
161
        $sectionsQuery = $this->request->get('sections', '');
162
163
        // If not present, try the cookie, and finally the defaults (all sections).
164
        if ($useCookies && '' == $sectionsQuery) {
165
            $sectionsQuery = $this->request->cookies->get('XtoolsEditCounterOptions', '');
166
        }
167
168
        // Either a pipe-separated string or an array.
169
        $sections = is_array($sectionsQuery) ? $sectionsQuery : explode('|', $sectionsQuery);
170
171
        // Filter out any invalid section IDs.
172
        $sections = array_filter($sections, function ($section) {
173
            return in_array($section, $this->getSectionNames());
174
        });
175
176
        // Fallback for when no valid sections were requested or provided by the cookie.
177
        if (0 === count($sections)) {
178
            $sections = $this->getSectionNames();
179
        }
180
181
        return $sections;
182
    }
183
184
    /**
185
     * Get the names of the available sections.
186
     * @return string[]
187
     * @codeCoverageIgnore
188
     */
189
    private function getSectionNames(): array
190
    {
191
        return array_keys(self::AVAILABLE_SECTIONS);
192
    }
193
194
    /**
195
     * Redirect to the appropriate action based on what sections are being requested.
196
     * @return RedirectResponse
197
     * @codeCoverageIgnore
198
     */
199
    private function redirectFromSections(): RedirectResponse
200
    {
201
        $this->sections = $this->getRequestedSections();
202
203
        if (1 === count($this->sections)) {
204
            // Redirect to dedicated route.
205
            $response = $this->redirectToRoute(self::AVAILABLE_SECTIONS[$this->sections[0]], $this->params);
206
        } elseif ($this->sections === $this->getSectionNames()) {
207
            $response = $this->redirectToRoute('EditCounterResult', $this->params);
208
        } else {
209
            // Add sections to the params, which $this->generalUrl() will append to the URL.
210
            $this->params['sections'] = implode('|', $this->sections);
211
212
            // We want a pretty URL, with pipes | instead of the encoded value %7C
213
            $url = str_replace('%7C', '|', $this->generateUrl('EditCounterResult', $this->params));
214
215
            $response = $this->redirect($url);
216
        }
217
218
        // Save the preferred sections in a cookie.
219
        $response->headers->setCookie(
220
            new Cookie('XtoolsEditCounterOptions', implode('|', $this->sections))
221
        );
222
223
        return $response;
224
    }
225
226
    /**
227
     * Display all results.
228
     * @Route("/ec/{project}/{username}", name="EditCounterResult")
229
     * @return Response|RedirectResponse
230
     * @codeCoverageIgnore
231
     */
232
    public function resultAction()
233
    {
234
        $this->setUpEditCounter();
235
236
        if (1 === count($this->sections)) {
237
            // Redirect to dedicated route.
238
            return $this->redirectToRoute(self::AVAILABLE_SECTIONS[$this->sections[0]], $this->params);
239
        }
240
241
        $ret = [
242
            'xtTitle' => $this->user->getUsername() . ' - ' . $this->project->getTitle(),
243
            'xtPage' => 'EditCounter',
244
            'user' => $this->user,
245
            'project' => $this->project,
246
            'ec' => $this->editCounter,
247
            'sections' => $this->sections,
248
            'isAllSections' => $this->sections === $this->getSectionNames(),
249
        ];
250
251
        // Used when querying for global rights changes.
252
        if ((bool)$this->container->hasParameter('app.is_labs')) {
253
            $ret['metaProject'] = ProjectRepository::getProject('metawiki', $this->container);
254
        }
255
256
        $response = $this->getFormattedResponse('editCounter/result', $ret);
257
258
        return $response;
259
    }
260
261
    /**
262
     * Display the general statistics section.
263
     * @Route("/ec-generalstats/{project}/{username}", name="EditCounterGeneralStats")
264
     * @return Response
265
     * @codeCoverageIgnore
266
     */
267
    public function generalStatsAction(): Response
268
    {
269
        $this->setUpEditCounter();
270
271
        $globalContribsRepo = new GlobalContribsRepository();
272
        $globalContribsRepo->setContainer($this->container);
273
        $globalContribs = new GlobalContribs($this->user);
274
        $globalContribs->setRepository($globalContribsRepo);
275
276
        $ret = [
277
            'xtTitle' => $this->user->getUsername(),
278
            'xtPage' => 'EditCounter',
279
            'is_sub_request' => $this->isSubRequest,
280
            'user' => $this->user,
281
            'project' => $this->project,
282
            'ec' => $this->editCounter,
283
            'gc' => $globalContribs,
284
        ];
285
286
        // Output the relevant format template.
287
        return $this->getFormattedResponse('editCounter/general_stats', $ret);
288
    }
289
290
    /**
291
     * Search form for general stats.
292
     * @Route("/ec-generalstats", name="EditCounterGeneralStatsIndex")
293
     * @return Response
294
     */
295 1
    public function generalStatsIndexAction(): Response
296
    {
297 1
        $this->sections = ['general-stats'];
298 1
        return $this->indexAction();
299
    }
300
301
    /**
302
     * Display the namespace totals section.
303
     * @Route("/ec-namespacetotals/{project}/{username}", name="EditCounterNamespaceTotals")
304
     * @return Response
305
     * @codeCoverageIgnore
306
     */
307
    public function namespaceTotalsAction(): Response
308
    {
309
        $this->setUpEditCounter();
310
311
        $ret = [
312
            'xtTitle' => $this->user->getUsername(),
313
            'xtPage' => 'EditCounter',
314
            'is_sub_request' => $this->isSubRequest,
315
            'user' => $this->user,
316
            'project' => $this->project,
317
            'ec' => $this->editCounter,
318
        ];
319
320
        // Output the relevant format template.
321
        return $this->getFormattedResponse('editCounter/namespace_totals', $ret);
322
    }
323
324
    /**
325
     * Search form for namespace totals.
326
     * @Route("/ec-namespacetotals", name="EditCounterNamespaceTotalsIndex")
327
     * @return Response
328
     */
329 1
    public function namespaceTotalsIndexAction(): Response
330
    {
331 1
        $this->sections = ['namespace-totals'];
332 1
        return $this->indexAction();
333
    }
334
335
    /**
336
     * Display the timecard section.
337
     * @Route("/ec-timecard/{project}/{username}", name="EditCounterTimecard")
338
     * @return Response
339
     * @codeCoverageIgnore
340
     */
341
    public function timecardAction(): Response
342
    {
343
        $this->setUpEditCounter();
344
345
        $ret = [
346
            'xtTitle' => $this->user->getUsername(),
347
            'xtPage' => 'EditCounter',
348
            'is_sub_request' => $this->isSubRequest,
349
            'user' => $this->user,
350
            'project' => $this->project,
351
            'ec' => $this->editCounter,
352
            'opted_in_page' => $this->getOptedInPage(),
353
        ];
354
355
        // Output the relevant format template.
356
        return $this->getFormattedResponse('editCounter/timecard', $ret);
357
    }
358
359
    /**
360
     * Search form for timecard.
361
     * @Route("/ec-timecard", name="EditCounterTimecardIndex")
362
     * @return Response
363
     */
364 1
    public function timecardIndexAction(): Response
365
    {
366 1
        $this->sections = ['timecard'];
367 1
        return $this->indexAction();
368
    }
369
370
    /**
371
     * Display the year counts section.
372
     * @Route("/ec-yearcounts/{project}/{username}", name="EditCounterYearCounts")
373
     * @return Response
374
     * @codeCoverageIgnore
375
     */
376
    public function yearCountsAction(): Response
377
    {
378
        $this->setUpEditCounter();
379
380
        $ret = [
381
            'xtTitle' => $this->user->getUsername(),
382
            'xtPage' => 'EditCounter',
383
            'is_sub_request' => $this->isSubRequest,
384
            'user' => $this->user,
385
            'project' => $this->project,
386
            'ec' => $this->editCounter,
387
        ];
388
389
        // Output the relevant format template.
390
        return $this->getFormattedResponse('editCounter/yearcounts', $ret);
391
    }
392
393
    /**
394
     * Search form for year counts.
395
     * @Route("/ec-yearcounts", name="EditCounterYearCountsIndex")
396
     * @return Response
397
     */
398 1
    public function yearCountsIndexAction(): Response
399
    {
400 1
        $this->sections = ['year-counts'];
401 1
        return $this->indexAction();
402
    }
403
404
    /**
405
     * Display the month counts section.
406
     * @Route("/ec-monthcounts/{project}/{username}", name="EditCounterMonthCounts")
407
     * @return Response
408
     * @codeCoverageIgnore
409
     */
410
    public function monthCountsAction(): Response
411
    {
412
        $this->setUpEditCounter();
413
414
        $ret = [
415
            'xtTitle' => $this->user->getUsername(),
416
            'xtPage' => 'EditCounter',
417
            'is_sub_request' => $this->isSubRequest,
418
            'user' => $this->user,
419
            'project' => $this->project,
420
            'ec' => $this->editCounter,
421
            'opted_in_page' => $this->getOptedInPage(),
422
        ];
423
424
        // Output the relevant format template.
425
        return $this->getFormattedResponse('editCounter/monthcounts', $ret);
426
    }
427
428
    /**
429
     * Search form for month counts.
430
     * @Route("/ec-monthcounts", name="EditCounterMonthCountsIndex")
431
     * @return Response
432
     */
433 1
    public function monthCountsIndexAction(): Response
434
    {
435 1
        $this->sections = ['month-counts'];
436 1
        return $this->indexAction();
437
    }
438
439
    /**
440
     * Display the user rights changes section.
441
     * @Route("/ec-rightschanges/{project}/{username}", name="EditCounterRightsChanges")
442
     * @return Response
443
     * @codeCoverageIgnore
444
     */
445
    public function rightsChangesAction(): Response
446
    {
447
        $this->setUpEditCounter();
448
449
        $ret = [
450
            'xtTitle' => $this->user->getUsername(),
451
            'xtPage' => 'EditCounter',
452
            'is_sub_request' => $this->isSubRequest,
453
            'user' => $this->user,
454
            'project' => $this->project,
455
            'ec' => $this->editCounter,
456
        ];
457
458
        if ((bool)$this->container->hasParameter('app.is_labs')) {
459
            $ret['metaProject'] = ProjectRepository::getProject('metawiki', $this->container);
460
        }
461
462
        // Output the relevant format template.
463
        return $this->getFormattedResponse('editCounter/rights_changes', $ret);
464
    }
465
466
    /**
467
     * Search form for rights changes.
468
     * @Route("/ec-rightschanges", name="EditCounterRightsChangesIndex")
469
     * @return Response
470
     */
471 1
    public function rightsChangesIndexAction(): Response
472
    {
473 1
        $this->sections = ['rights-changes'];
474 1
        return $this->indexAction();
475
    }
476
477
    /************************ API endpoints ************************/
478
479
    /**
480
     * Get various log counts for the user as JSON.
481
     * @Route("/api/user/log_counts/{project}/{username}", name="UserApiLogCounts")
482
     * @return JsonResponse
483
     * @codeCoverageIgnore
484
     */
485
    public function logCountsApiAction(): JsonResponse
486
    {
487
        $this->setUpEditCounter();
488
489
        return $this->getFormattedApiResponse([
490
            'log_counts' => $this->editCounter->getLogCounts(),
491
        ]);
492
    }
493
494
    /**
495
     * Get the namespace totals for the user as JSON.
496
     * @Route("/api/user/namespace_totals/{project}/{username}", name="UserApiNamespaceTotals")
497
     * @return JsonResponse
498
     * @codeCoverageIgnore
499
     */
500
    public function namespaceTotalsApiAction(): JsonResponse
501
    {
502
        $this->setUpEditCounter();
503
504
        return $this->getFormattedApiResponse([
505
            'namespace_totals' => $this->editCounter->namespaceTotals(),
506
        ]);
507
    }
508
509
    /**
510
     * Get the month counts for the user as JSON.
511
     * @Route("/api/user/month_counts/{project}/{username}", name="UserApiMonthCounts")
512
     * @return JsonResponse
513
     * @codeCoverageIgnore
514
     */
515
    public function monthCountsApiAction(): JsonResponse
516
    {
517
        $this->setUpEditCounter();
518
519
        $ret = $this->editCounter->monthCounts();
520
521
        // Remove labels that are only needed by Twig views, and not consumers of the API.
522
        unset($ret['yearLabels']);
523
        unset($ret['monthLabels']);
524
525
        return $this->getFormattedApiResponse($ret);
526
    }
527
528
    /**
529
     * Get the timecard data as JSON.
530
     * @Route("/api/user/timecard/{project}/{username}", name="UserApiTimeCard")
531
     * @return JsonResponse
532
     * @codeCoverageIgnore
533
     */
534
    public function timecardApiAction(): JsonResponse
535
    {
536
        $this->setUpEditCounter();
537
538
        return $this->getFormattedApiResponse([
539
            'timecard' => $this->editCounter->timeCard(),
540
        ]);
541
    }
542
}
543