Test Failed
Push — master ( e0f84b...2e2660 )
by MusikAnimal
04:53
created

EditCounterController   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 598
Duplicated Lines 0 %

Test Coverage

Coverage 97.37%

Importance

Changes 0
Metric Value
eloc 189
dl 0
loc 598
ccs 37
cts 38
cp 0.9737
rs 8.96
c 0
b 0
f 0
wmc 43

27 Methods

Rating   Name   Duplication   Size   Complexity  
A getIndexRoute() 0 3 1
A getRequestedSections() 0 29 6
A getSectionNames() 0 3 1
A indexAction() 0 17 3
A redirectFromSections() 0 25 3
A setUpEditCounter() 0 33 5
A __construct() 0 9 1
A monthCountsApiAction() 0 7 1
A timecardIndexAction() 0 4 1
A latestGlobalIndexAction() 0 4 1
A namespaceTotalsApiAction() 0 7 1
A pairDataApiAction() 0 7 1
A generalStatsAction() 0 15 1
A monthCountsIndexAction() 0 4 1
A namespaceTotalsAction() 0 15 1
A namespaceTotalsIndexAction() 0 4 1
A latestGlobalAction() 0 13 1
A timecardAction() 0 20 1
A monthCountsAction() 0 19 1
A generalStatsIndexAction() 0 4 1
A logCountsApiAction() 0 7 1
A yearCountsIndexAction() 0 4 1
A editSizesApiAction() 0 7 1
A yearCountsAction() 0 15 1
A rightsChangesAction() 0 19 2
A rightsChangesIndexAction() 0 4 1
A resultAction() 0 27 3

How to fix   Complexity   

Complex Class

Complex classes like EditCounterController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EditCounterController, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file contains only the EditCounterController class.
4
 */
5
6
namespace AppBundle\Controller;
7
8
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
9
use Symfony\Component\DependencyInjection\ContainerInterface;
10
use Symfony\Component\HttpFoundation\Cookie;
11
use Symfony\Component\HttpFoundation\JsonResponse;
12
use Symfony\Component\HttpFoundation\RedirectResponse;
13
use Symfony\Component\HttpFoundation\RequestStack;
14
use Symfony\Component\HttpFoundation\Response;
15
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
16
use Xtools\EditCounter;
17
use Xtools\EditCounterRepository;
18
use Xtools\ProjectRepository;
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
    const AVAILABLE_SECTIONS = [
30
        'general-stats' => 'EditCounterGeneralStats',
31
        'namespace-totals' => 'EditCounterNamespaceTotals',
32
        'year-counts' => 'EditCounterYearCounts',
33
        'month-counts' => 'EditCounterMonthCounts',
34
        'timecard' => 'EditCounterRightsChanges',
35
        'top-edited-pages' => 'TopEditsResult',
36
        'rights-changes' => 'EditCounterRightsChanges',
37
        'latest-global-edits' => 'EditCounterLatestGlobalContribs',
38
    ];
39
40
    /** @var EditCounter The edit-counter, that does all the work. */
41
    protected $editCounter;
42
43
    /** @var string[] Which sections to show. */
44
    protected $sections;
45
46
    /**
47
     * Get the name of the tool's index route. This is also the name of the associated model.
48
     * @return string
49
     * @codeCoverageIgnore
50
     */
51
    public function getIndexRoute()
52
    {
53
        return 'EditCounter';
54
    }
55
56
    /**
57
     * EditCounterController constructor.
58
     * @param RequestStack $requestStack
59
     * @param ContainerInterface $container
60
     */
61 2
    public function __construct(RequestStack $requestStack, ContainerInterface $container)
62
    {
63
        // Causes the tool to redirect to the Simple Edit Counter if the user has too high of an edit count.
64 2
        $this->tooHighEditCountAction = 'SimpleEditCounterResult';
65
66
        // The rightsChanges action is exempt from the edit count limitation.
67 2
        $this->tooHighEditCountActionBlacklist = ['rightsChanges'];
68
69 2
        parent::__construct($requestStack, $container);
70 2
    }
71
72
    /**
73
     * Every action in this controller (other than 'index') calls this first.
74
     * If a response is returned, the calling action is expected to return it.
75
     * @param string $key API key, as given in the request. Omit this for actions
76
     *   that are public (only /api/ec actions should pass this in).
77
     * @return RedirectResponse|null
78
     * @throws AccessDeniedException If attempting to access internal endpoint.
79
     * @codeCoverageIgnore
80
     */
81
    protected function setUpEditCounter($key = null)
82
    {
83
        // Whether we're making a subrequest (the view makes a request to another action).
84
        // Subrequests to the same controller do not re-instantiate a new controller, and hence
85
        // this flag would not be set in XtoolsController::__construct(), so we must do it here as well.
86
        $this->isSubRequest = $this->request->get('htmlonly')
87
            || $this->get('request_stack')->getParentRequest() !== null;
88
89
        // Return the EditCounter if we already have one.
90
        if ($this->editCounter instanceof EditCounter) {
0 ignored issues
show
introduced by
$this->editCounter is always a sub-type of Xtools\EditCounter. If $this->editCounter can have other possible types, add them to src/AppBundle/Controller/EditCounterController.php:40.
Loading history...
91
            return null;
92
        }
93
94
        // Validate key if attempted to make internal API request.
95
        if ($key && (string)$key !== (string)$this->container->getParameter('secret')) {
96
            throw $this->createAccessDeniedException('This endpoint is for internal use only.');
97
        }
98
99
        // Will redirect to Simple Edit Counter if they have too many edits, as defined self::construct.
100
        $this->validateUser($this->user->getUsername());
101
102
        // Store which sections of the Edit Counter they requested.
103
        $this->sections = $this->getRequestedSections();
104
105
        // Instantiate EditCounter.
106
        $editCounterRepo = new EditCounterRepository();
107
        $editCounterRepo->setContainer($this->container);
108
        $this->editCounter = new EditCounter(
109
            $this->project,
110
            $this->user,
111
            $this->container->get('app.i18n_helper')
112
        );
113
        $this->editCounter->setRepository($editCounterRepo);
114
    }
115
116
    /**
117
     * The initial GET request that displays the search form.
118
     * @Route("/ec", name="EditCounter")
119
     * @Route("/ec/", name="EditCounterSlash")
120
     * @Route("/ec/index.php", name="EditCounterIndexPhp")
121
     * @Route("/ec/{project}", name="EditCounterProject")
122
     * @return RedirectResponse|Response
123
     */
124 2
    public function indexAction()
125
    {
126 2
        if (isset($this->params['project']) && isset($this->params['username'])) {
127
            return $this->redirectFromSections();
128
        }
129
130 2
        $this->sections = $this->getRequestedSections(true);
131
132
        // Otherwise fall through.
133 2
        return $this->render('editCounter/index.html.twig', [
134 2
            'xtPageTitle' => 'tool-editcounter',
135 2
            'xtSubtitle' => 'tool-editcounter-desc',
136 2
            'xtPage' => 'editcounter',
137 2
            'project' => $this->project,
138 2
            'sections' => $this->sections,
139 2
            'availableSections' => $this->getSectionNames(),
140 2
            'isAllSections' => $this->sections === $this->getSectionNames(),
141
        ]);
142
    }
143
144
    /**
145
     * Get the requested sections either from the URL, cookie, or the defaults (all sections).
146
     * @param bool $useCookies Whether or not to check cookies for the preferred sections.
147
     *   This option should not be true except on the index form.
148
     * @return array|mixed|string[]
149
     * @codeCoverageIgnore
150
     */
151
    private function getRequestedSections($useCookies = false)
152
    {
153
        // Happens from sub-tool index pages, e.g. see self::generalStatsIndexAction().
154
        if (isset($this->sections)) {
155
            return $this->sections;
156
        }
157
158
        // Query param for sections gets priority.
159
        $sectionsQuery = $this->request->get('sections', '');
160
161
        // If not present, try the cookie, and finally the defaults (all sections).
162
        if ($useCookies && $sectionsQuery == '') {
163
            $sectionsQuery = $this->request->cookies->get('XtoolsEditCounterOptions');
164
        }
165
166
        // Either a pipe-separated string or an array.
167
        $sections = is_array($sectionsQuery) ? $sectionsQuery : explode('|', $sectionsQuery);
168
169
        // Filter out any invalid section IDs.
170
        $sections = array_filter($sections, function ($section) {
171
            return in_array($section, $this->getSectionNames());
172
        });
173
174
        // Fallback for when no valid sections were requested or provided by the cookie.
175
        if (count($sections) === 0) {
176
            $sections = $this->getSectionNames();
177
        }
178
179
        return $sections;
180
    }
181
182
    /**
183
     * Get the names of the available sections.
184
     * @return string[]
185
     * @codeCoverageIgnore
186
     */
187
    private function getSectionNames()
188
    {
189
        return array_keys(self::AVAILABLE_SECTIONS);
190
    }
191
192
    /**
193
     * Redirect to the appropriate action based on what sections are being requested.
194
     * @return RedirectResponse
195
     * @codeCoverageIgnore
196
     */
197
    private function redirectFromSections()
198
    {
199
        $this->sections = $this->getRequestedSections();
200
201
        if (count($this->sections) === 1) {
202
            // Redirect to dedicated route.
203
            $response = $this->redirectToRoute(self::AVAILABLE_SECTIONS[$this->sections[0]], $this->params);
204
        } elseif ($this->sections === $this->getSectionNames()) {
205
            $response = $this->redirectToRoute('EditCounterResult', $this->params);
206
        } else {
207
            // Add sections to the params, which $this->generalUrl() will append to the URL.
208
            $this->params['sections'] = implode('|', $this->sections);
209
210
            // We want a pretty URL, with pipes | instead of the encoded value %7C
211
            $url = str_replace('%7C', '|', $this->generateUrl('EditCounterResult', $this->params));
212
213
            $response = $this->redirect($url);
214
        }
215
216
        // Save the preferred sections in a cookie.
217
        $response->headers->setCookie(
218
            new Cookie('XtoolsEditCounterOptions', implode('|', $this->sections))
219
        );
220
221
        return $response;
222
    }
223
224
    /**
225
     * Display all results.
226
     * @Route("/ec/{project}/{username}", name="EditCounterResult")
227
     * @return Response|RedirectResponse
228
     * @codeCoverageIgnore
229
     */
230
    public function resultAction()
231
    {
232
        $this->setUpEditCounter();
233
234
        if (count($this->sections) === 1) {
235
            // Redirect to dedicated route.
236
            return $this->redirectToRoute(self::AVAILABLE_SECTIONS[$this->sections[0]], $this->params);
237
        }
238
239
        $ret = [
240
            'xtTitle' => $this->user->getUsername() . ' - ' . $this->project->getTitle(),
241
            'xtPage' => 'editcounter',
242
            'user' => $this->user,
243
            'project' => $this->project,
244
            'ec' => $this->editCounter,
245
            'sections' => $this->sections,
246
            'isAllSections' => $this->sections === $this->getSectionNames(),
247
        ];
248
249
        // Used when querying for global rights changes.
250
        if ((bool)$this->container->hasParameter('app.is_labs')) {
251
            $ret['metaProject'] = ProjectRepository::getProject('metawiki', $this->container);
252
        }
253
254
        $response = $this->getFormattedResponse('editCounter/result', $ret);
255
256
        return $response;
257
    }
258
259
    /**
260
     * Display the general statistics section.
261
     * @Route("/ec-generalstats/{project}/{username}", name="EditCounterGeneralStats")
262
     * @return Response
263
     * @codeCoverageIgnore
264
     */
265
    public function generalStatsAction()
266
    {
267
        $this->setUpEditCounter();
268
269
        $ret = [
270
            'xtTitle' => $this->user->getUsername(),
271
            'xtPage' => 'editcounter',
272
            'is_sub_request' => $this->isSubRequest,
273
            'user' => $this->user,
274
            'project' => $this->project,
275
            'ec' => $this->editCounter,
276
        ];
277
278
        // Output the relevant format template.
279
        return $this->getFormattedResponse('editCounter/general_stats', $ret);
280
    }
281
282
    /**
283 1
     * Search form for general stats.
284
     * @Route("/ec-generalstats", name="EditCounterGeneralStatsIndex")
285 1
     * @Route("/ec-generalstats/", name="EditCounterGeneralStatsIndexSlash")
286 1
     * @return Response
287
     */
288
    public function generalStatsIndexAction()
289
    {
290
        $this->sections = ['general-stats'];
291
        return $this->indexAction();
292
    }
293
294
    /**
295
     * Display the namespace totals section.
296
     * @Route("/ec-namespacetotals/{project}/{username}", name="EditCounterNamespaceTotals")
297
     * @return Response
298
     * @codeCoverageIgnore
299
     */
300
    public function namespaceTotalsAction()
301
    {
302
        $this->setUpEditCounter();
303
304
        $ret = [
305
            'xtTitle' => $this->user->getUsername(),
306
            'xtPage' => 'editcounter',
307
            'is_sub_request' => $this->isSubRequest,
308
            'user' => $this->user,
309
            'project' => $this->project,
310
            'ec' => $this->editCounter,
311
        ];
312
313
        // Output the relevant format template.
314
        return $this->getFormattedResponse('editCounter/namespace_totals', $ret);
315
    }
316
317
    /**
318 1
     * Search form for namespace totals.
319
     * @Route("/ec-namespacetotals", name="EditCounterNamespaceTotalsIndex")
320 1
     * @Route("/ec-namespacetotals/", name="EditCounterNamespaceTotalsIndexSlash")
321 1
     * @return Response
322
     */
323
    public function namespaceTotalsIndexAction()
324
    {
325
        $this->sections = ['namespace-totals'];
326
        return $this->indexAction();
327
    }
328
329
    /**
330
     * Display the timecard section.
331
     * @Route("/ec-timecard/{project}/{username}", name="EditCounterTimecard")
332
     * @return Response
333
     * @codeCoverageIgnore
334
     */
335
    public function timecardAction()
336
    {
337
        $this->setUpEditCounter();
338
339
        $optedInPage = $this->project
340
            ->getRepository()
341
            ->getPage($this->project, $this->project->userOptInPage($this->user));
342
343
        $ret = [
344
            'xtTitle' => $this->user->getUsername(),
345
            'xtPage' => 'editcounter',
346
            'is_sub_request' => $this->isSubRequest,
347
            'user' => $this->user,
348
            'project' => $this->project,
349
            'ec' => $this->editCounter,
350
            'opted_in_page' => $optedInPage,
351
        ];
352
353
        // Output the relevant format template.
354
        return $this->getFormattedResponse('editCounter/timecard', $ret);
355
    }
356
357
    /**
358 1
     * Search form for timecard.
359
     * @Route("/ec-timecard", name="EditCounterTimecardIndex")
360 1
     * @Route("/ec-timecard/", name="EditCounterTimecardIndexSlash")
361 1
     * @return Response
362
     */
363
    public function timecardIndexAction()
364
    {
365
        $this->sections = ['timecard'];
366
        return $this->indexAction();
367
    }
368
369
    /**
370
     * Display the year counts section.
371
     * @Route("/ec-yearcounts/{project}/{username}", name="EditCounterYearCounts")
372
     * @return Response
373
     * @codeCoverageIgnore
374
     */
375
    public function yearCountsAction()
376
    {
377
        $this->setUpEditCounter();
378
379
        $ret = [
380
            'xtTitle' => $this->user->getUsername(),
381
            'xtPage' => 'editcounter',
382
            'is_sub_request' => $this->isSubRequest,
383
            'user' => $this->user,
384
            'project' => $this->project,
385
            'ec' => $this->editCounter,
386
        ];
387
388
        // Output the relevant format template.
389
        return $this->getFormattedResponse('editCounter/yearcounts', $ret);
390
    }
391
392
    /**
393 1
     * Search form for year counts.
394
     * @Route("/ec-yearcounts", name="EditCounterYearCountsIndex")
395 1
     * @Route("/ec-yearcounts/", name="EditCounterYearCountsIndexSlash")
396 1
     * @return Response
397
     */
398
    public function yearCountsIndexAction()
399
    {
400
        $this->sections = ['year-counts'];
401
        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()
411
    {
412
        $this->setUpEditCounter();
413
414
        $optedInPage = $this->project
415
            ->getRepository()
416
            ->getPage($this->project, $this->project->userOptInPage($this->user));
417
        $ret = [
418
            'xtTitle' => $this->user->getUsername(),
419
            'xtPage' => 'editcounter',
420
            'is_sub_request' => $this->isSubRequest,
421
            'user' => $this->user,
422
            'project' => $this->project,
423
            'ec' => $this->editCounter,
424
            'opted_in_page' => $optedInPage,
425
        ];
426
427
        // Output the relevant format template.
428
        return $this->getFormattedResponse('editCounter/monthcounts', $ret);
429
    }
430
431
    /**
432 1
     * Search form for month counts.
433
     * @Route("/ec-monthcounts", name="EditCounterMonthCountsIndex")
434 1
     * @Route("/ec-monthcounts/", name="EditCounterMonthCountsIndexSlash")
435 1
     * @return Response
436
     */
437
    public function monthCountsIndexAction()
438
    {
439
        $this->sections = ['month-counts'];
440
        return $this->indexAction();
441
    }
442
443
    /**
444
     * Display the user rights changes section.
445
     * @Route("/ec-rightschanges/{project}/{username}", name="EditCounterRightsChanges")
446
     * @return Response
447
     * @codeCoverageIgnore
448
     */
449
    public function rightsChangesAction()
450
    {
451
        $this->setUpEditCounter();
452
453
        $ret = [
454
            'xtTitle' => $this->user->getUsername(),
455
            'xtPage' => 'editcounter',
456
            'is_sub_request' => $this->isSubRequest,
457
            'user' => $this->user,
458
            'project' => $this->project,
459
            'ec' => $this->editCounter,
460
        ];
461
462
        if ((bool)$this->container->hasParameter('app.is_labs')) {
463
            $ret['metaProject'] = ProjectRepository::getProject('metawiki', $this->container);
464
        }
465
466
        // Output the relevant format template.
467
        return $this->getFormattedResponse('editCounter/rights_changes', $ret);
468
    }
469
470
    /**
471 1
     * Search form for rights changes.
472
     * @Route("/ec-rightschanges", name="EditCounterRightsChangesIndex")
473 1
     * @Route("/ec-rightschanges/", name="EditCounterRightsChangesIndexSlash")
474 1
     * @return Response
475
     */
476
    public function rightsChangesIndexAction()
477
    {
478
        $this->sections = ['rights-changes'];
479
        return $this->indexAction();
480
    }
481
482
    /**
483
     * Display the latest global edits section.
484
     * @Route(
485
     *     "/ec-latestglobal-contributions/{project}/{username}/{offset}",
486
     *     name="EditCounterLatestGlobalContribs",
487
     *     requirements={"offset" = "|\d*"},
488
     *     defaults={"offset" = 0}
489
     * )
490
     * @Route(
491
     *     "/ec-latestglobal/{project}/{username}/{offset}",
492
     *     name="EditCounterLatestGlobal",
493
     *     requirements={"offset" = "|\d*"},
494
     *     defaults={"offset" = 0}
495
     * ),
496
     * @return Response
497
     * @codeCoverageIgnore
498
     */
499
    public function latestGlobalAction()
500
    {
501
        $this->setUpEditCounter();
502
503
        return $this->render('editCounter/latest_global.html.twig', [
504
            'xtTitle' => $this->user->getUsername(),
505
            'xtPage' => 'editcounter',
506
            'is_sub_request' => $this->isSubRequest,
507
            'user' => $this->user,
508
            'project' => $this->project,
509
            'ec' => $this->editCounter,
510
            'offset' => $this->request->get('offset'),
511
            'pageSize' => $this->request->get('pagesize'),
512
        ]);
513
    }
514
515
    /**
516
     * Search form for latest global edits.
517
     * @Route("/ec-latestglobal-contributions", name="EditCounterLatestGlobalContribsIndex")
518
     * @Route("/ec-latestglobal-contributions/", name="EditCounterLatestGlobalContribsIndexSlash")
519
     * @Route("/ec-latestglobal", name="EditCounterLatestGlobalIndex")
520 1
     * @Route("/ec-latestglobal/", name="EditCounterLatestGlobalIndexSlash")
521
     * @Route("/ec-latestglobaledits", name="EditCounterLatestGlobalEditsIndex")
522 1
     * @Route("/ec-latestglobaledits/", name="EditCounterLatestGlobalEditsIndexSlash")
523 1
     * @return Response
524
     */
525
    public function latestGlobalIndexAction()
526
    {
527
        $this->sections = ['latest-global-edits'];
528
        return $this->indexAction();
529
    }
530
531
532
    /**
533
     * Below are internal API endpoints for the Edit Counter.
534
     * All only respond with JSON and only to requests passing in the value
535
     * of the 'secret' parameter. This should not be used in JavaScript or clientside
536
     * applications, rather only used internally.
537
     */
538
539
    /**
540
     * Get (most) of the general statistics as JSON.
541
     * @Route("/api/ec/pairdata/{project}/{username}/{key}", name="EditCounterApiPairData")
542
     * @param string $key API key.
543
     * @return JsonResponse|RedirectResponse
544
     * @codeCoverageIgnore
545
     */
546
    public function pairDataApiAction($key)
547
    {
548
        $this->setUpEditCounter($key);
549
550
        return new JsonResponse(
551
            $this->editCounter->getPairData(),
552
            Response::HTTP_OK
553
        );
554
    }
555
556
    /**
557
     * Get various log counts for the user as JSON.
558
     * @Route("/api/ec/logcounts/{project}/{username}/{key}", name="EditCounterApiLogCounts")
559
     * @param string $key API key.
560
     * @return JsonResponse|RedirectResponse
561
     * @codeCoverageIgnore
562
     */
563
    public function logCountsApiAction($key)
564
    {
565
        $this->setUpEditCounter($key);
566
567
        return new JsonResponse(
568
            $this->editCounter->getLogCounts(),
569
            Response::HTTP_OK
570
        );
571
    }
572
573
    /**
574
     * Get edit sizes for the user as JSON.
575
     * @Route("/api/ec/editsizes/{project}/{username}/{key}", name="EditCounterApiEditSizes")
576
     * @param string $key API key.
577
     * @return JsonResponse|RedirectResponse
578
     * @codeCoverageIgnore
579
     */
580
    public function editSizesApiAction($key)
581
    {
582
        $this->setUpEditCounter($key);
583
584
        return new JsonResponse(
585
            $this->editCounter->getEditSizeData(),
586
            Response::HTTP_OK
587
        );
588
    }
589
590
    /**
591
     * Get the namespace totals for the user as JSON.
592
     * @Route("/api/ec/namespacetotals/{project}/{username}/{key}", name="EditCounterApiNamespaceTotals")
593
     * @param string $key API key.
594
     * @return Response|RedirectResponse
595
     * @codeCoverageIgnore
596
     */
597
    public function namespaceTotalsApiAction($key)
598
    {
599
        $this->setUpEditCounter($key);
600
601
        return new JsonResponse(
602
            $this->editCounter->namespaceTotals(),
603
            Response::HTTP_OK
604
        );
605
    }
606
607
    /**
608
     * Display or fetch the month counts for the user.
609
     * @Route("/api/ec/monthcounts/{project}/{username}/{key}", name="EditCounterApiMonthCounts")
610
     * @param string $key API key.
611
     * @return Response
612
     * @codeCoverageIgnore
613
     */
614
    public function monthCountsApiAction($key)
615
    {
616
        $this->setUpEditCounter($key);
617
618
        return new JsonResponse(
619
            $this->editCounter->monthCounts(),
620
            Response::HTTP_OK
621
        );
622
    }
623
}
624