Passed
Push — master ( 4ccd3c...a7eee1 )
by MusikAnimal
05:26
created

EditCounterController::indexAction()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.009

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 2
nop 0
dl 0
loc 14
ccs 9
cts 10
cp 0.9
crap 3.009
rs 9.9666
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
71
        // Now that we have the request object, parse out the requested sections from the URL or cookie.
72
        // This could be a pipe-separated string, or an array.
73
        // $this->sections might already be set from a cookie. See self::getCookieMap().
74 2
        if (!isset($this->sections)) {
75 2
            $cookieValue = $this->request->cookies->get('XtoolsEditCounterOptions');
76 2
            $sectionsQuery = isset($cookieValue)
77
                ? $cookieValue
78 2
                : $this->request->get('sections', array_keys(self::AVAILABLE_SECTIONS));
79 2
            $this->sections = is_array($sectionsQuery) ? $sectionsQuery : explode('|', $sectionsQuery);
80
        }
81 2
    }
82
83
    /**
84
     * Every action in this controller (other than 'index') calls this first.
85
     * If a response is returned, the calling action is expected to return it.
86
     * @param string $key API key, as given in the request. Omit this for actions
87
     *   that are public (only /api/ec actions should pass this in).
88
     * @return RedirectResponse|null
89
     * @throws AccessDeniedException If attempting to access internal endpoint.
90
     * @codeCoverageIgnore
91
     */
92
    protected function setUpEditCounter($key = null)
93
    {
94
        // Whether we're making a subrequest (the view makes a request to another action).
95
        // Subrequests to the same controller do not re-instantiate a new controller, and hence
96
        // this flag would not be set in XtoolsController::__construct(), so we must do it here as well.
97
        $this->isSubRequest = $this->request->get('htmlonly')
98
            || $this->get('request_stack')->getParentRequest() !== null;
99
100
        // Return the EditCounter if we already have one.
101
        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...
102
            return null;
103
        }
104
105
        // Validate key if attempted to make internal API request.
106
        if ($key && (string)$key !== (string)$this->container->getParameter('secret')) {
107
            throw $this->createAccessDeniedException('This endpoint is for internal use only.');
108
        }
109
110
        // Will redirect to Simple Edit Counter if they have too many edits, as defined self::construct.
111
        $this->validateUser($this->user->getUsername());
112
113
        // Instantiate EditCounter.
114
        $editCounterRepo = new EditCounterRepository();
115
        $editCounterRepo->setContainer($this->container);
116
        $this->editCounter = new EditCounter(
117
            $this->project,
118
            $this->user,
119
            $this->container->get('app.i18n_helper')
120
        );
121
        $this->editCounter->setRepository($editCounterRepo);
122
    }
123
124
    /**
125
     * The initial GET request that displays the search form.
126
     * @Route("/ec", name="EditCounter")
127
     * @Route("/ec/", name="EditCounterSlash")
128
     * @Route("/ec/index.php", name="EditCounterIndexPhp")
129
     * @Route("/ec/{project}", name="EditCounterProject")
130
     * @return RedirectResponse|Response
131
     */
132 2
    public function indexAction()
133
    {
134 2
        if (isset($this->params['project']) && isset($this->params['username'])) {
135
            return $this->redirectFromSections();
136
        }
137
138
        // Otherwise fall through.
139 2
        return $this->render('editCounter/index.html.twig', [
140 2
            'xtPageTitle' => 'tool-editcounter',
141 2
            'xtSubtitle' => 'tool-editcounter-desc',
142 2
            'xtPage' => 'editcounter',
143 2
            'project' => $this->project,
144 2
            'sections' => $this->sections,
145 2
            'availableSections' => array_keys(self::AVAILABLE_SECTIONS),
146
        ]);
147
    }
148
149
    /**
150
     * Redirect to the appropriate action based on what sections are being requested.
151
     * @return RedirectResponse
152
     */
153
    private function redirectFromSections()
154
    {
155
        if (count($this->sections) === 1) {
156
            // Redirect to dedicated route.
157
            return $this->redirectToRoute(self::AVAILABLE_SECTIONS[$this->sections[0]], $this->params);
158
        } elseif ($this->sections === array_keys(self::AVAILABLE_SECTIONS)) {
159
            return $this->redirectToRoute('EditCounterResult', $this->params);
160
        }
161
162
        // Add sections to the params, which $this->generalUrl() will append to the URL.
163
        $this->params['sections'] = implode('|', $this->sections);
164
165
        // We want a pretty URL, with pipes | instead of the encoded value %7C
166
        $url = str_replace('%7C', '|', $this->generateUrl('EditCounterResult', $this->params));
167
168
        return $this->redirect($url);
169
    }
170
171
    /**
172
     * Display all results.
173
     * @Route("/ec/{project}/{username}", name="EditCounterResult")
174
     * @return Response
175
     * @codeCoverageIgnore
176
     */
177
    public function resultAction()
178
    {
179
        $this->setUpEditCounter();
180
181
        $ret = [
182
            'xtTitle' => $this->user->getUsername() . ' - ' . $this->project->getTitle(),
183
            'xtPage' => 'editcounter',
184
            'user' => $this->user,
185
            'project' => $this->project,
186
            'ec' => $this->editCounter,
187
            'sections' => $this->sections,
188
        ];
189
190
        // Used when querying for global rights changes.
191
        if ((bool)$this->container->hasParameter('app.is_labs')) {
192
            $ret['metaProject'] = ProjectRepository::getProject('metawiki', $this->container);
193
        }
194
195
        $response = $this->getFormattedResponse('editCounter/result', $ret);
196
197
        // Save the preferred sections in a cookie.
198
        $response->headers->setCookie(
199
            new Cookie('XtoolsEditCounterOptions', implode('|', $this->sections))
200
        );
201
202
        return $response;
203
    }
204
205
    /**
206
     * Display the general statistics section.
207
     * @Route("/ec-generalstats/{project}/{username}", name="EditCounterGeneralStats")
208
     * @return Response
209
     * @codeCoverageIgnore
210
     */
211
    public function generalStatsAction()
212
    {
213
        $this->setUpEditCounter();
214
215
        $ret = [
216
            'xtTitle' => $this->user->getUsername(),
217
            'xtPage' => 'editcounter',
218
            'is_sub_request' => $this->isSubRequest,
219
            'user' => $this->user,
220
            'project' => $this->project,
221
            'ec' => $this->editCounter,
222
        ];
223
224
        // Output the relevant format template.
225
        return $this->getFormattedResponse('editCounter/general_stats', $ret);
226
    }
227
228
    /**
229
     * Search form for general stats.
230
     * @Route("/ec-generalstats", name="EditCounterGeneralStatsIndex")
231
     * @Route("/ec-generalstats/", name="EditCounterGeneralStatsIndexSlash")
232
     * @return Response
233
     */
234 1
    public function generalStatsIndexAction()
235
    {
236 1
        $this->sections = ['general-stats'];
237 1
        return $this->indexAction();
238
    }
239
240
    /**
241
     * Display the namespace totals section.
242
     * @Route("/ec-namespacetotals/{project}/{username}", name="EditCounterNamespaceTotals")
243
     * @return Response
244
     * @codeCoverageIgnore
245
     */
246
    public function namespaceTotalsAction()
247
    {
248
        $this->setUpEditCounter();
249
250
        $ret = [
251
            'xtTitle' => $this->user->getUsername(),
252
            'xtPage' => 'editcounter',
253
            'is_sub_request' => $this->isSubRequest,
254
            'user' => $this->user,
255
            'project' => $this->project,
256
            'ec' => $this->editCounter,
257
        ];
258
259
        // Output the relevant format template.
260
        return $this->getFormattedResponse('editCounter/namespace_totals', $ret);
261
    }
262
263
    /**
264
     * Search form for namespace totals.
265
     * @Route("/ec-namespacetotals", name="EditCounterNamespaceTotalsIndex")
266
     * @Route("/ec-namespacetotals/", name="EditCounterNamespaceTotalsIndexSlash")
267
     * @return Response
268
     */
269 1
    public function namespaceTotalsIndexAction()
270
    {
271 1
        $this->sections = ['namespace-totals'];
272 1
        return $this->indexAction();
273
    }
274
275
    /**
276
     * Display the timecard section.
277
     * @Route("/ec-timecard/{project}/{username}", name="EditCounterTimecard")
278
     * @return Response
279
     * @codeCoverageIgnore
280
     */
281
    public function timecardAction()
282
    {
283
        $this->setUpEditCounter();
284
285
        $optedInPage = $this->project
286
            ->getRepository()
287
            ->getPage($this->project, $this->project->userOptInPage($this->user));
288
289
        $ret = [
290
            'xtTitle' => $this->user->getUsername(),
291
            'xtPage' => 'editcounter',
292
            'is_sub_request' => $this->isSubRequest,
293
            'user' => $this->user,
294
            'project' => $this->project,
295
            'ec' => $this->editCounter,
296
            'opted_in_page' => $optedInPage,
297
        ];
298
299
        // Output the relevant format template.
300
        return $this->getFormattedResponse('editCounter/timecard', $ret);
301
    }
302
303
    /**
304
     * Search form for timecard.
305
     * @Route("/ec-timecard", name="EditCounterTimecardIndex")
306
     * @Route("/ec-timecard/", name="EditCounterTimecardIndexSlash")
307
     * @return Response
308
     */
309 1
    public function timecardIndexAction()
310
    {
311 1
        $this->sections = ['timecard'];
312 1
        return $this->indexAction();
313
    }
314
315
    /**
316
     * Display the year counts section.
317
     * @Route("/ec-yearcounts/{project}/{username}", name="EditCounterYearCounts")
318
     * @return Response
319
     * @codeCoverageIgnore
320
     */
321
    public function yearCountsAction()
322
    {
323
        $this->setUpEditCounter();
324
325
        $ret = [
326
            'xtTitle' => $this->user->getUsername(),
327
            'xtPage' => 'editcounter',
328
            'is_sub_request' => $this->isSubRequest,
329
            'user' => $this->user,
330
            'project' => $this->project,
331
            'ec' => $this->editCounter,
332
        ];
333
334
        // Output the relevant format template.
335
        return $this->getFormattedResponse('editCounter/yearcounts', $ret);
336
    }
337
338
    /**
339
     * Search form for year counts.
340
     * @Route("/ec-yearcounts", name="EditCounterYearCountsIndex")
341
     * @Route("/ec-yearcounts/", name="EditCounterYearCountsIndexSlash")
342
     * @return Response
343
     */
344 1
    public function yearCountsIndexAction()
345
    {
346 1
        $this->sections = ['year-counts'];
347 1
        return $this->indexAction();
348
    }
349
350
    /**
351
     * Display the month counts section.
352
     * @Route("/ec-monthcounts/{project}/{username}", name="EditCounterMonthCounts")
353
     * @return Response
354
     * @codeCoverageIgnore
355
     */
356
    public function monthCountsAction()
357
    {
358
        $this->setUpEditCounter();
359
360
        $optedInPage = $this->project
361
            ->getRepository()
362
            ->getPage($this->project, $this->project->userOptInPage($this->user));
363
        $ret = [
364
            'xtTitle' => $this->user->getUsername(),
365
            'xtPage' => 'editcounter',
366
            'is_sub_request' => $this->isSubRequest,
367
            'user' => $this->user,
368
            'project' => $this->project,
369
            'ec' => $this->editCounter,
370
            'opted_in_page' => $optedInPage,
371
        ];
372
373
        // Output the relevant format template.
374
        return $this->getFormattedResponse('editCounter/monthcounts', $ret);
375
    }
376
377
    /**
378
     * Search form for month counts.
379
     * @Route("/ec-monthcounts", name="EditCounterMonthCountsIndex")
380
     * @Route("/ec-monthcounts/", name="EditCounterMonthCountsIndexSlash")
381
     * @return Response
382
     */
383 1
    public function monthCountsIndexAction()
384
    {
385 1
        $this->sections = ['month-counts'];
386 1
        return $this->indexAction();
387
    }
388
389
    /**
390
     * Display the user rights changes section.
391
     * @Route("/ec-rightschanges/{project}/{username}", name="EditCounterRightsChanges")
392
     * @return Response
393
     * @codeCoverageIgnore
394
     */
395
    public function rightsChangesAction()
396
    {
397
        $this->setUpEditCounter();
398
399
        $ret = [
400
            'xtTitle' => $this->user->getUsername(),
401
            'xtPage' => 'editcounter',
402
            'is_sub_request' => $this->isSubRequest,
403
            'user' => $this->user,
404
            'project' => $this->project,
405
            'ec' => $this->editCounter,
406
        ];
407
408
        if ((bool)$this->container->hasParameter('app.is_labs')) {
409
            $ret['metaProject'] = ProjectRepository::getProject('metawiki', $this->container);
410
        }
411
412
        // Output the relevant format template.
413
        return $this->getFormattedResponse('editCounter/rights_changes', $ret);
414
    }
415
416
    /**
417
     * Search form for rights changes.
418
     * @Route("/ec-rightschanges", name="EditCounterRightsChangesIndex")
419
     * @Route("/ec-rightschanges/", name="EditCounterRightsChangesIndexSlash")
420
     * @return Response
421
     */
422 1
    public function rightsChangesIndexAction()
423
    {
424 1
        $this->sections = ['rights-changes'];
425 1
        return $this->indexAction();
426
    }
427
428
    /**
429
     * Display the latest global edits section.
430
     * @Route(
431
     *     "/ec-latestglobal-contributions/{project}/{username}/{offset}",
432
     *     name="EditCounterLatestGlobalContribs",
433
     *     requirements={"offset" = "|\d*"},
434
     *     defaults={"offset" = 0}
435
     * )
436
     * @Route(
437
     *     "/ec-latestglobal/{project}/{username}/{offset}",
438
     *     name="EditCounterLatestGlobal",
439
     *     requirements={"offset" = "|\d*"},
440
     *     defaults={"offset" = 0}
441
     * ),
442
     * @return Response
443
     * @codeCoverageIgnore
444
     */
445
    public function latestGlobalAction()
446
    {
447
        $this->setUpEditCounter();
448
449
        return $this->render('editCounter/latest_global.html.twig', [
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
            'offset' => $this->request->get('offset'),
457
            'pageSize' => $this->request->get('pagesize'),
458
        ]);
459
    }
460
461
    /**
462
     * Search form for latest global edits.
463
     * @Route("/ec-latestglobal-contributions", name="EditCounterLatestGlobalContribsIndex")
464
     * @Route("/ec-latestglobal-contributions/", name="EditCounterLatestGlobalContribsIndexSlash")
465
     * @Route("/ec-latestglobal", name="EditCounterLatestGlobalIndex")
466
     * @Route("/ec-latestglobal/", name="EditCounterLatestGlobalIndexSlash")
467
     * @Route("/ec-latestglobaledits", name="EditCounterLatestGlobalEditsIndex")
468
     * @Route("/ec-latestglobaledits/", name="EditCounterLatestGlobalEditsIndexSlash")
469
     * @return Response
470
     */
471 1
    public function latestGlobalIndexAction()
472
    {
473 1
        $this->sections = ['latest-global-edits'];
474 1
        return $this->indexAction();
475
    }
476
477
478
    /**
479
     * Below are internal API endpoints for the Edit Counter.
480
     * All only respond with JSON and only to requests passing in the value
481
     * of the 'secret' parameter. This should not be used in JavaScript or clientside
482
     * applications, rather only used internally.
483
     */
484
485
    /**
486
     * Get (most) of the general statistics as JSON.
487
     * @Route("/api/ec/pairdata/{project}/{username}/{key}", name="EditCounterApiPairData")
488
     * @param string $key API key.
489
     * @return JsonResponse|RedirectResponse
490
     * @codeCoverageIgnore
491
     */
492
    public function pairDataApiAction($key)
493
    {
494
        $this->setUpEditCounter($key);
495
496
        return new JsonResponse(
497
            $this->editCounter->getPairData(),
498
            Response::HTTP_OK
499
        );
500
    }
501
502
    /**
503
     * Get various log counts for the user as JSON.
504
     * @Route("/api/ec/logcounts/{project}/{username}/{key}", name="EditCounterApiLogCounts")
505
     * @param string $key API key.
506
     * @return JsonResponse|RedirectResponse
507
     * @codeCoverageIgnore
508
     */
509
    public function logCountsApiAction($key)
510
    {
511
        $this->setUpEditCounter($key);
512
513
        return new JsonResponse(
514
            $this->editCounter->getLogCounts(),
515
            Response::HTTP_OK
516
        );
517
    }
518
519
    /**
520
     * Get edit sizes for the user as JSON.
521
     * @Route("/api/ec/editsizes/{project}/{username}/{key}", name="EditCounterApiEditSizes")
522
     * @param string $key API key.
523
     * @return JsonResponse|RedirectResponse
524
     * @codeCoverageIgnore
525
     */
526
    public function editSizesApiAction($key)
527
    {
528
        $this->setUpEditCounter($key);
529
530
        return new JsonResponse(
531
            $this->editCounter->getEditSizeData(),
532
            Response::HTTP_OK
533
        );
534
    }
535
536
    /**
537
     * Get the namespace totals for the user as JSON.
538
     * @Route("/api/ec/namespacetotals/{project}/{username}/{key}", name="EditCounterApiNamespaceTotals")
539
     * @param string $key API key.
540
     * @return Response|RedirectResponse
541
     * @codeCoverageIgnore
542
     */
543
    public function namespaceTotalsApiAction($key)
544
    {
545
        $this->setUpEditCounter($key);
546
547
        return new JsonResponse(
548
            $this->editCounter->namespaceTotals(),
549
            Response::HTTP_OK
550
        );
551
    }
552
553
    /**
554
     * Display or fetch the month counts for the user.
555
     * @Route("/api/ec/monthcounts/{project}/{username}/{key}", name="EditCounterApiMonthCounts")
556
     * @param string $key API key.
557
     * @return Response
558
     * @codeCoverageIgnore
559
     */
560
    public function monthCountsApiAction($key)
561
    {
562
        $this->setUpEditCounter($key);
563
564
        return new JsonResponse(
565
            $this->editCounter->monthCounts(),
566
            Response::HTTP_OK
567
        );
568
    }
569
}
570