Passed
Push — master ( 4c1320...e0f84b )
by MusikAnimal
05:55
created

EditCounterController::getSectionNames()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
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
228
     * @codeCoverageIgnore
229
     */
230
    public function resultAction()
231
    {
232
        $this->setUpEditCounter();
233
234
        $ret = [
235
            'xtTitle' => $this->user->getUsername() . ' - ' . $this->project->getTitle(),
236
            'xtPage' => 'editcounter',
237
            'user' => $this->user,
238
            'project' => $this->project,
239
            'ec' => $this->editCounter,
240
            'sections' => $this->sections,
241
            'isAllSections' => $this->sections === $this->getSectionNames(),
242
        ];
243
244
        // Used when querying for global rights changes.
245
        if ((bool)$this->container->hasParameter('app.is_labs')) {
246
            $ret['metaProject'] = ProjectRepository::getProject('metawiki', $this->container);
247
        }
248
249
        $response = $this->getFormattedResponse('editCounter/result', $ret);
250
251
        return $response;
252
    }
253
254
    /**
255
     * Display the general statistics section.
256
     * @Route("/ec-generalstats/{project}/{username}", name="EditCounterGeneralStats")
257
     * @return Response
258
     * @codeCoverageIgnore
259
     */
260
    public function generalStatsAction()
261
    {
262
        $this->setUpEditCounter();
263
264
        $ret = [
265
            'xtTitle' => $this->user->getUsername(),
266
            'xtPage' => 'editcounter',
267
            'is_sub_request' => $this->isSubRequest,
268
            'user' => $this->user,
269
            'project' => $this->project,
270
            'ec' => $this->editCounter,
271
        ];
272
273
        // Output the relevant format template.
274
        return $this->getFormattedResponse('editCounter/general_stats', $ret);
275
    }
276
277
    /**
278
     * Search form for general stats.
279
     * @Route("/ec-generalstats", name="EditCounterGeneralStatsIndex")
280
     * @Route("/ec-generalstats/", name="EditCounterGeneralStatsIndexSlash")
281
     * @return Response
282
     */
283 1
    public function generalStatsIndexAction()
284
    {
285 1
        $this->sections = ['general-stats'];
286 1
        return $this->indexAction();
287
    }
288
289
    /**
290
     * Display the namespace totals section.
291
     * @Route("/ec-namespacetotals/{project}/{username}", name="EditCounterNamespaceTotals")
292
     * @return Response
293
     * @codeCoverageIgnore
294
     */
295
    public function namespaceTotalsAction()
296
    {
297
        $this->setUpEditCounter();
298
299
        $ret = [
300
            'xtTitle' => $this->user->getUsername(),
301
            'xtPage' => 'editcounter',
302
            'is_sub_request' => $this->isSubRequest,
303
            'user' => $this->user,
304
            'project' => $this->project,
305
            'ec' => $this->editCounter,
306
        ];
307
308
        // Output the relevant format template.
309
        return $this->getFormattedResponse('editCounter/namespace_totals', $ret);
310
    }
311
312
    /**
313
     * Search form for namespace totals.
314
     * @Route("/ec-namespacetotals", name="EditCounterNamespaceTotalsIndex")
315
     * @Route("/ec-namespacetotals/", name="EditCounterNamespaceTotalsIndexSlash")
316
     * @return Response
317
     */
318 1
    public function namespaceTotalsIndexAction()
319
    {
320 1
        $this->sections = ['namespace-totals'];
321 1
        return $this->indexAction();
322
    }
323
324
    /**
325
     * Display the timecard section.
326
     * @Route("/ec-timecard/{project}/{username}", name="EditCounterTimecard")
327
     * @return Response
328
     * @codeCoverageIgnore
329
     */
330
    public function timecardAction()
331
    {
332
        $this->setUpEditCounter();
333
334
        $optedInPage = $this->project
335
            ->getRepository()
336
            ->getPage($this->project, $this->project->userOptInPage($this->user));
337
338
        $ret = [
339
            'xtTitle' => $this->user->getUsername(),
340
            'xtPage' => 'editcounter',
341
            'is_sub_request' => $this->isSubRequest,
342
            'user' => $this->user,
343
            'project' => $this->project,
344
            'ec' => $this->editCounter,
345
            'opted_in_page' => $optedInPage,
346
        ];
347
348
        // Output the relevant format template.
349
        return $this->getFormattedResponse('editCounter/timecard', $ret);
350
    }
351
352
    /**
353
     * Search form for timecard.
354
     * @Route("/ec-timecard", name="EditCounterTimecardIndex")
355
     * @Route("/ec-timecard/", name="EditCounterTimecardIndexSlash")
356
     * @return Response
357
     */
358 1
    public function timecardIndexAction()
359
    {
360 1
        $this->sections = ['timecard'];
361 1
        return $this->indexAction();
362
    }
363
364
    /**
365
     * Display the year counts section.
366
     * @Route("/ec-yearcounts/{project}/{username}", name="EditCounterYearCounts")
367
     * @return Response
368
     * @codeCoverageIgnore
369
     */
370
    public function yearCountsAction()
371
    {
372
        $this->setUpEditCounter();
373
374
        $ret = [
375
            'xtTitle' => $this->user->getUsername(),
376
            'xtPage' => 'editcounter',
377
            'is_sub_request' => $this->isSubRequest,
378
            'user' => $this->user,
379
            'project' => $this->project,
380
            'ec' => $this->editCounter,
381
        ];
382
383
        // Output the relevant format template.
384
        return $this->getFormattedResponse('editCounter/yearcounts', $ret);
385
    }
386
387
    /**
388
     * Search form for year counts.
389
     * @Route("/ec-yearcounts", name="EditCounterYearCountsIndex")
390
     * @Route("/ec-yearcounts/", name="EditCounterYearCountsIndexSlash")
391
     * @return Response
392
     */
393 1
    public function yearCountsIndexAction()
394
    {
395 1
        $this->sections = ['year-counts'];
396 1
        return $this->indexAction();
397
    }
398
399
    /**
400
     * Display the month counts section.
401
     * @Route("/ec-monthcounts/{project}/{username}", name="EditCounterMonthCounts")
402
     * @return Response
403
     * @codeCoverageIgnore
404
     */
405
    public function monthCountsAction()
406
    {
407
        $this->setUpEditCounter();
408
409
        $optedInPage = $this->project
410
            ->getRepository()
411
            ->getPage($this->project, $this->project->userOptInPage($this->user));
412
        $ret = [
413
            'xtTitle' => $this->user->getUsername(),
414
            'xtPage' => 'editcounter',
415
            'is_sub_request' => $this->isSubRequest,
416
            'user' => $this->user,
417
            'project' => $this->project,
418
            'ec' => $this->editCounter,
419
            'opted_in_page' => $optedInPage,
420
        ];
421
422
        // Output the relevant format template.
423
        return $this->getFormattedResponse('editCounter/monthcounts', $ret);
424
    }
425
426
    /**
427
     * Search form for month counts.
428
     * @Route("/ec-monthcounts", name="EditCounterMonthCountsIndex")
429
     * @Route("/ec-monthcounts/", name="EditCounterMonthCountsIndexSlash")
430
     * @return Response
431
     */
432 1
    public function monthCountsIndexAction()
433
    {
434 1
        $this->sections = ['month-counts'];
435 1
        return $this->indexAction();
436
    }
437
438
    /**
439
     * Display the user rights changes section.
440
     * @Route("/ec-rightschanges/{project}/{username}", name="EditCounterRightsChanges")
441
     * @return Response
442
     * @codeCoverageIgnore
443
     */
444
    public function rightsChangesAction()
445
    {
446
        $this->setUpEditCounter();
447
448
        $ret = [
449
            'xtTitle' => $this->user->getUsername(),
450
            'xtPage' => 'editcounter',
451
            'is_sub_request' => $this->isSubRequest,
452
            'user' => $this->user,
453
            'project' => $this->project,
454
            'ec' => $this->editCounter,
455
        ];
456
457
        if ((bool)$this->container->hasParameter('app.is_labs')) {
458
            $ret['metaProject'] = ProjectRepository::getProject('metawiki', $this->container);
459
        }
460
461
        // Output the relevant format template.
462
        return $this->getFormattedResponse('editCounter/rights_changes', $ret);
463
    }
464
465
    /**
466
     * Search form for rights changes.
467
     * @Route("/ec-rightschanges", name="EditCounterRightsChangesIndex")
468
     * @Route("/ec-rightschanges/", name="EditCounterRightsChangesIndexSlash")
469
     * @return Response
470
     */
471 1
    public function rightsChangesIndexAction()
472
    {
473 1
        $this->sections = ['rights-changes'];
474 1
        return $this->indexAction();
475
    }
476
477
    /**
478
     * Display the latest global edits section.
479
     * @Route(
480
     *     "/ec-latestglobal-contributions/{project}/{username}/{offset}",
481
     *     name="EditCounterLatestGlobalContribs",
482
     *     requirements={"offset" = "|\d*"},
483
     *     defaults={"offset" = 0}
484
     * )
485
     * @Route(
486
     *     "/ec-latestglobal/{project}/{username}/{offset}",
487
     *     name="EditCounterLatestGlobal",
488
     *     requirements={"offset" = "|\d*"},
489
     *     defaults={"offset" = 0}
490
     * ),
491
     * @return Response
492
     * @codeCoverageIgnore
493
     */
494
    public function latestGlobalAction()
495
    {
496
        $this->setUpEditCounter();
497
498
        return $this->render('editCounter/latest_global.html.twig', [
499
            'xtTitle' => $this->user->getUsername(),
500
            'xtPage' => 'editcounter',
501
            'is_sub_request' => $this->isSubRequest,
502
            'user' => $this->user,
503
            'project' => $this->project,
504
            'ec' => $this->editCounter,
505
            'offset' => $this->request->get('offset'),
506
            'pageSize' => $this->request->get('pagesize'),
507
        ]);
508
    }
509
510
    /**
511
     * Search form for latest global edits.
512
     * @Route("/ec-latestglobal-contributions", name="EditCounterLatestGlobalContribsIndex")
513
     * @Route("/ec-latestglobal-contributions/", name="EditCounterLatestGlobalContribsIndexSlash")
514
     * @Route("/ec-latestglobal", name="EditCounterLatestGlobalIndex")
515
     * @Route("/ec-latestglobal/", name="EditCounterLatestGlobalIndexSlash")
516
     * @Route("/ec-latestglobaledits", name="EditCounterLatestGlobalEditsIndex")
517
     * @Route("/ec-latestglobaledits/", name="EditCounterLatestGlobalEditsIndexSlash")
518
     * @return Response
519
     */
520 1
    public function latestGlobalIndexAction()
521
    {
522 1
        $this->sections = ['latest-global-edits'];
523 1
        return $this->indexAction();
524
    }
525
526
527
    /**
528
     * Below are internal API endpoints for the Edit Counter.
529
     * All only respond with JSON and only to requests passing in the value
530
     * of the 'secret' parameter. This should not be used in JavaScript or clientside
531
     * applications, rather only used internally.
532
     */
533
534
    /**
535
     * Get (most) of the general statistics as JSON.
536
     * @Route("/api/ec/pairdata/{project}/{username}/{key}", name="EditCounterApiPairData")
537
     * @param string $key API key.
538
     * @return JsonResponse|RedirectResponse
539
     * @codeCoverageIgnore
540
     */
541
    public function pairDataApiAction($key)
542
    {
543
        $this->setUpEditCounter($key);
544
545
        return new JsonResponse(
546
            $this->editCounter->getPairData(),
547
            Response::HTTP_OK
548
        );
549
    }
550
551
    /**
552
     * Get various log counts for the user as JSON.
553
     * @Route("/api/ec/logcounts/{project}/{username}/{key}", name="EditCounterApiLogCounts")
554
     * @param string $key API key.
555
     * @return JsonResponse|RedirectResponse
556
     * @codeCoverageIgnore
557
     */
558
    public function logCountsApiAction($key)
559
    {
560
        $this->setUpEditCounter($key);
561
562
        return new JsonResponse(
563
            $this->editCounter->getLogCounts(),
564
            Response::HTTP_OK
565
        );
566
    }
567
568
    /**
569
     * Get edit sizes for the user as JSON.
570
     * @Route("/api/ec/editsizes/{project}/{username}/{key}", name="EditCounterApiEditSizes")
571
     * @param string $key API key.
572
     * @return JsonResponse|RedirectResponse
573
     * @codeCoverageIgnore
574
     */
575
    public function editSizesApiAction($key)
576
    {
577
        $this->setUpEditCounter($key);
578
579
        return new JsonResponse(
580
            $this->editCounter->getEditSizeData(),
581
            Response::HTTP_OK
582
        );
583
    }
584
585
    /**
586
     * Get the namespace totals for the user as JSON.
587
     * @Route("/api/ec/namespacetotals/{project}/{username}/{key}", name="EditCounterApiNamespaceTotals")
588
     * @param string $key API key.
589
     * @return Response|RedirectResponse
590
     * @codeCoverageIgnore
591
     */
592
    public function namespaceTotalsApiAction($key)
593
    {
594
        $this->setUpEditCounter($key);
595
596
        return new JsonResponse(
597
            $this->editCounter->namespaceTotals(),
598
            Response::HTTP_OK
599
        );
600
    }
601
602
    /**
603
     * Display or fetch the month counts for the user.
604
     * @Route("/api/ec/monthcounts/{project}/{username}/{key}", name="EditCounterApiMonthCounts")
605
     * @param string $key API key.
606
     * @return Response
607
     * @codeCoverageIgnore
608
     */
609
    public function monthCountsApiAction($key)
610
    {
611
        $this->setUpEditCounter($key);
612
613
        return new JsonResponse(
614
            $this->editCounter->monthCounts(),
615
            Response::HTTP_OK
616
        );
617
    }
618
}
619