Passed
Push — master ( e98754...9567d9 )
by MusikAnimal
08:10
created

XtoolsController   F

Complexity

Total Complexity 77

Size/Duplication

Total Lines 706
Duplicated Lines 0 %

Test Coverage

Coverage 51.79%

Importance

Changes 0
Metric Value
eloc 254
dl 0
loc 706
ccs 101
cts 195
cp 0.5179
rs 2.24
c 0
b 0
f 0
wmc 77

21 Methods

Rating   Name   Duplication   Size   Complexity  
A validateProject() 0 15 2
A setCookies() 0 10 3
B validateUser() 0 49 7
A getProjectFromQuery() 0 23 4
A validatePage() 0 17 2
A __construct() 0 30 3
A loadCookies() 0 9 3
A setDates() 0 6 3
A getPageFromNsAndTitle() 0 10 4
B setProperties() 0 28 7
A getFlashMessage() 0 13 2
A throwXtoolsException() 0 28 2
A setProject() 0 4 1
B convertLegacyParams() 0 40 7
A parseQueryParams() 0 12 2
A getParams() 0 42 5
A getFormattedResponse() 0 38 3
B getUTCFromDateParams() 0 23 11
A recordApiUsage() 0 23 2
A getFormattedApiResponse() 0 24 3
A addFlashMessage() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like XtoolsController 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 XtoolsController, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file contains the abstract XtoolsController, which all other controllers will extend.
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\Page;
13
use AppBundle\Model\Project;
14
use AppBundle\Model\User;
15
use AppBundle\Repository\PageRepository;
16
use AppBundle\Repository\ProjectRepository;
17
use AppBundle\Repository\UserRepository;
18
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
19
use Symfony\Component\DependencyInjection\ContainerInterface;
20
use Symfony\Component\HttpFoundation\Cookie;
21
use Symfony\Component\HttpFoundation\JsonResponse;
22
use Symfony\Component\HttpFoundation\Request;
23
use Symfony\Component\HttpFoundation\RequestStack;
24
use Symfony\Component\HttpFoundation\Response;
25
26
/**
27
 * XtoolsController supplies a variety of methods around parsing and validating parameters, and initializing
28
 * Project/User instances. These are used in other controllers in the AppBundle\Controller namespace.
29
 * @abstract
30
 */
31
abstract class XtoolsController extends Controller
0 ignored issues
show
Deprecated Code introduced by
The class Symfony\Bundle\Framework...e\Controller\Controller has been deprecated: since Symfony 4.2, use {@see AbstractController} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

31
abstract class XtoolsController extends /** @scrutinizer ignore-deprecated */ Controller
Loading history...
32
{
33
    /** @var I18nHelper i18n helper. */
34
    protected $i18n;
35
36
    /** @var Request The request object. */
37
    protected $request;
38
39
    /** @var string Name of the action within the child controller that is being executed. */
40
    protected $controllerAction;
41
42
    /** @var array Hash of params parsed from the Request. */
43
    protected $params;
44
45
    /** @var bool Whether this is a request to an API action. */
46
    protected $isApi;
47
48
    /** @var Project Relevant Project parsed from the Request. */
49
    protected $project;
50
51
    /** @var User Relevant User parsed from the Request. */
52
    protected $user;
53
54
    /** @var Page Relevant Page parsed from the Request. */
55
    protected $page;
56
57
    /** @var int|false Start date parsed from the Request. */
58
    protected $start = false;
59
60
    /** @var int|false End date parsed from the Request. */
61
    protected $end = false;
62
63
    /** @var int|string Namespace parsed from the Request, ID as int or 'all' for all namespaces. */
64
    protected $namespace;
65
66
    /** @var int Pagination offset parsed from the Request. */
67
    protected $offset = 0;
68
69
    /** @var int Number of results to return. */
70
    protected $limit;
71
72
    /** @var bool Is the current request a subrequest? */
73
    protected $isSubRequest;
74
75
    /**
76
     * Stores user preferences such default project.
77
     * This may get altered from the Request and updated in the Response.
78
     * @var array
79
     */
80
    protected $cookies = [
81
        'XtoolsProject' => null,
82
    ];
83
84
    /**
85
     * This activates the 'too high edit count' functionality. This property represents the
86
     * action that should be redirected to if the user has too high of an edit count.
87
     * @var string
88
     */
89
    protected $tooHighEditCountAction;
90
91
    /**
92
     * @var array Actions that are exempt from edit count limitations.
93
     */
94
    protected $tooHighEditCountActionBlacklist = [];
95
96
    /**
97
     * Require the tool's index route (initial form) be defined here. This should also
98
     * be the name of the associated model, if present.
99
     * @return string
100
     */
101
    abstract protected function getIndexRoute(): string;
102
103
    /**
104
     * XtoolsController constructor.
105
     * @param RequestStack $requestStack
106
     * @param ContainerInterface $container
107
     * @param I18nHelper $i18n
108
     */
109 16
    public function __construct(RequestStack $requestStack, ContainerInterface $container, I18nHelper $i18n)
110
    {
111 16
        $this->request = $requestStack->getCurrentRequest();
112 16
        $this->container = $container;
113 16
        $this->i18n = $i18n;
114 16
        $this->params = $this->parseQueryParams();
115
116
        // Parse out the name of the controller and action.
117 16
        $pattern = "#::([a-zA-Z]*)Action#";
118 16
        $matches = [];
119
        // The blank string here only happens in the unit tests, where the request may not be made to an action.
120 16
        preg_match($pattern, $this->request->get('_controller') ?? '', $matches);
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

120
        preg_match($pattern, $this->request->/** @scrutinizer ignore-call */ get('_controller') ?? '', $matches);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
121 16
        $this->controllerAction = $matches[1] ?? '';
122
123
        // Whether the action is an API action.
124 16
        $this->isApi = 'Api' === substr($this->controllerAction, -3);
125
126
        // Whether we're making a subrequest (the view makes a request to another action).
127 16
        $this->isSubRequest = $this->request->get('htmlonly')
128 16
            || null !== $this->get('request_stack')->getParentRequest();
129
130
        // Load user options from cookies.
131 16
        $this->loadCookies();
132
133
        // Set the class-level properties based on params.
134 16
        if (false !== strpos(strtolower($this->controllerAction), 'index')) {
135
            // Index pages should only set the project, and no other class properties.
136 10
            $this->setProject($this->getProjectFromQuery());
137
        } else {
138 6
            $this->setProperties(); // Includes the project.
139
        }
140 16
    }
141
142
    /***********
143
     * COOKIES *
144
     ***********/
145
146
    /**
147
     * Load user preferences from the associated cookies.
148
     */
149 16
    private function loadCookies(): void
150
    {
151
        // Not done for subrequests.
152 16
        if ($this->isSubRequest) {
153
            return;
154
        }
155
156 16
        foreach (array_keys($this->cookies) as $name) {
157 16
            $this->cookies[$name] = $this->request->cookies->get($name);
158
        }
159 16
    }
160
161
    /**
162
     * Set cookies on the given Response.
163
     * @param Response $response
164
     */
165
    private function setCookies(Response &$response): void
166
    {
167
        // Not done for subrequests.
168
        if ($this->isSubRequest) {
169
            return;
170
        }
171
172
        foreach ($this->cookies as $name => $value) {
173
            $response->headers->setCookie(
174
                new Cookie($name, $value)
175
            );
176
        }
177
    }
178
179
    /**
180
     * Sets the project, with the domain in $this->cookies['XtoolsProject'] that will
181
     * later get set on the Response headers in self::getFormattedResponse().
182
     * @param Project $project
183
     */
184 12
    private function setProject(Project $project): void
185
    {
186 12
        $this->project = $project;
187 12
        $this->cookies['XtoolsProject'] = $project->getDomain();
188 12
    }
189
190
    /****************************
191
     * SETTING CLASS PROPERTIES *
192
     ****************************/
193
194
    /**
195
     * Normalize all common parameters used by the controllers and set class properties.
196
     */
197 6
    private function setProperties(): void
198
    {
199 6
        $this->namespace = $this->params['namespace'] ?? null;
200
201
        // Offset and limit need to be ints.
202 6
        foreach (['offset', 'limit'] as $param) {
203 6
            if (isset($this->params[$param])) {
204 6
                $this->{$param} = (int)$this->params[$param];
205
            }
206
        }
207
208 6
        if (isset($this->params['project'])) {
209 2
            $this->setProject($this->validateProject($this->params['project']));
210 4
        } elseif (null !== $this->cookies['XtoolsProject']) {
211
            // Set from cookie.
212
            $this->setProject(
213
                $this->validateProject($this->cookies['XtoolsProject'])
214
            );
215
        }
216
217 6
        if (isset($this->params['username'])) {
218
            $this->user = $this->validateUser($this->params['username']);
219
        }
220 6
        if (isset($this->params['page'])) {
221
            $this->page = $this->getPageFromNsAndTitle($this->namespace, $this->params['page']);
222
        }
223
224 6
        $this->setDates();
225 6
    }
226
227
    /**
228
     * Set class properties for dates, if such params were passed in.
229
     */
230 6
    private function setDates(): void
231
    {
232 6
        $start = $this->params['start'] ?? false;
233 6
        $end = $this->params['end'] ?? false;
234 6
        if ($start || $end) {
235
            [$this->start, $this->end] = $this->getUTCFromDateParams($start, $end);
236
        }
237 6
    }
238
239
    /**
240
     * Construct a fully qualified page title given the namespace and title.
241
     * @param int|string $ns Namespace ID.
242
     * @param string $title Page title.
243
     * @param bool $rawTitle Return only the title (and not a Page).
244
     * @return Page|string
245
     */
246
    protected function getPageFromNsAndTitle($ns, string $title, bool $rawTitle = false)
247
    {
248
        if (0 === (int)$ns) {
249
            return $rawTitle ? $title : $this->validatePage($title);
250
        }
251
252
        // Prepend namespace and strip out duplicates.
253
        $nsName = $this->project->getNamespaces()[$ns] ?? $this->i18n->msg('unknown');
254
        $title = $nsName.':'.preg_replace('/^'.$nsName.':/', '', $title);
255
        return $rawTitle ? $title : $this->validatePage($title);
256
    }
257
258
    /**
259
     * Get a Project instance from the project string, using defaults if the given project string is invalid.
260
     * @return Project
261
     */
262 10
    public function getProjectFromQuery(): Project
263
    {
264
        // Set default project so we can populate the namespace selector on index pages.
265
        // Defaults to project stored in cookie, otherwise project specified in parameters.yml.
266 10
        if (isset($this->params['project'])) {
267 2
            $project = $this->params['project'];
268 8
        } elseif (null !== $this->cookies['XtoolsProject']) {
269
            $project = $this->cookies['XtoolsProject'];
270
        } else {
271 8
            $project = $this->container->getParameter('default_project');
272
        }
273
274 10
        $projectData = ProjectRepository::getProject($project, $this->container);
275
276
        // Revert back to defaults if we've established the given project was invalid.
277 10
        if (!$projectData->exists()) {
278
            $projectData = ProjectRepository::getProject(
279
                $this->container->getParameter('default_project'),
280
                $this->container
281
            );
282
        }
283
284 10
        return $projectData;
285
    }
286
287
    /*************************
288
     * GETTERS / VALIDATIONS *
289
     *************************/
290
291
    /**
292
     * Validate the given project, returning a Project if it is valid or false otherwise.
293
     * @param string $projectQuery Project domain or database name.
294
     * @return Project
295
     * @throws XtoolsHttpException
296
     */
297 2
    public function validateProject(string $projectQuery): Project
298
    {
299
        /** @var Project $project */
300 2
        $project = ProjectRepository::getProject($projectQuery, $this->container);
301
302 2
        if (!$project->exists()) {
303
            $this->throwXtoolsException(
304
                $this->getIndexRoute(),
305
                'invalid-project',
306
                [$this->params['project']],
307
                'project'
308
            );
309
        }
310
311 2
        return $project;
312
    }
313
314
    /**
315
     * Validate the given user, returning a User or Redirect if they don't exist.
316
     * @param string $username
317
     * @return User
318
     * @throws XtoolsHttpException
319
     */
320
    public function validateUser(string $username): User
321
    {
322
        $user = UserRepository::getUser($username, $this->container);
323
324
        // Allow querying for any IP, currently with no edit count limitation...
325
        // Once T188677 is resolved IPs will be affected by the EXPLAIN results.
326
        if ($user->isAnon()) {
327
            return $user;
328
        }
329
330
        $originalParams = $this->params;
331
332
        // Don't continue if the user doesn't exist.
333
        if (!$user->existsOnProject($this->project)) {
334
            $this->throwXtoolsException($this->getIndexRoute(), 'user-not-found', [], 'username');
335
        }
336
337
        // Reject users with a crazy high edit count.
338
        if (isset($this->tooHighEditCountAction) &&
339
            !in_array($this->controllerAction, $this->tooHighEditCountActionBlacklist) &&
340
            $user->hasTooManyEdits($this->project)
341
        ) {
342
            /** TODO: Somehow get this to use self::throwXtoolsException */
343
344
            $this->addFlashMessage('danger', 'too-many-edits', [
345
                $this->i18n->numberFormat($user->maxEdits()),
346
            ]);
347
348
            // If redirecting to a different controller, show an informative message accordingly.
349
            if ($this->tooHighEditCountAction !== $this->getIndexRoute()) {
350
                // FIXME: This is currently only done for Edit Counter, redirecting to Simple Edit Counter,
351
                // so this bit is hardcoded. We need to instead give the i18n key of the route.
352
                $this->addFlashMessage('info', 'too-many-edits-redir', [
353
                    $this->i18n->msg('tool-simpleeditcounter'),
354
                ]);
355
            } else {
356
                // Redirecting back to index, so remove username (otherwise we'd get a redirect loop).
357
                unset($this->params['username']);
358
            }
359
360
            throw new XtoolsHttpException(
361
                'User has made too many edits! Maximum '.$user->maxEdits(),
362
                $this->generateUrl($this->tooHighEditCountAction, $this->params),
363
                $originalParams,
364
                $this->isApi
365
            );
366
        }
367
368
        return $user;
369
    }
370
371
    /**
372
     * Get a Page instance from the given page title, and validate that it exists.
373
     * @param string $pageTitle
374
     * @return Page
375
     * @throws XtoolsHttpException
376
     */
377
    public function validatePage(string $pageTitle): Page
378
    {
379
        $page = new Page($this->project, $pageTitle);
380
        $pageRepo = new PageRepository();
381
        $pageRepo->setContainer($this->container);
382
        $page->setRepository($pageRepo);
383
384
        if (!$page->exists()) {
385
            $this->throwXtoolsException(
386
                $this->getIndexRoute(),
387
                'no-result',
388
                [$this->params['page'] ?? null],
389
                'page'
390
            );
391
        }
392
393
        return $page;
394
    }
395
396
    /**
397
     * Throw an XtoolsHttpException, which the given error message and redirects to specified action.
398
     * @param string $redirectAction Name of action to redirect to.
399
     * @param string $message i18n key of error message. Shown in API responses.
400
     *   If no message with this key exists, $message is shown as-is.
401
     * @param array $messageParams
402
     * @param string $invalidParam This will be removed from $this->params. Omit if you don't want this to happen.
403
     * @throws XtoolsHttpException
404
     */
405
    public function throwXtoolsException(
406
        string $redirectAction,
407
        string $message,
408
        array $messageParams = [],
409
        ?string $invalidParam = null
410
    ): void {
411
        $this->addFlashMessage('danger', $message, $messageParams);
412
        $originalParams = $this->params;
413
414
        // Remove invalid parameter if it was given.
415
        if (is_string($invalidParam)) {
416
            unset($this->params[$invalidParam]);
417
        }
418
419
        // We sometimes are redirecting to the index page, so also remove project (otherwise we'd get a redirect loop).
420
        /**
421
         * FIXME: Index pages should have a 'nosubmit' parameter to prevent submission.
422
         * Then we don't even need to remove $invalidParam.
423
         * Better, we should show the error on the results page, with no results.
424
         */
425
        unset($this->params['project']);
426
427
        // Throw exception which will redirect to $redirectAction.
428
        throw new XtoolsHttpException(
429
            $this->i18n->msgIfExists($message, $messageParams),
430
            $this->generateUrl($redirectAction, $this->params),
431
            $originalParams,
432
            $this->isApi
433
        );
434
    }
435
436
    /**
437
     * Get the first error message stored in the session's FlashBag.
438
     * @return string
439
     */
440
    public function getFlashMessage(): string
441
    {
442
        $key = $this->get('session')->getFlashBag()->get('danger')[0];
443
        $param = null;
444
445
        if (is_array($key)) {
446
            [$key, $param] = $key;
447
        }
448
449
        return $this->render('message.twig', [
450
            'key' => $key,
451
            'params' => [$param],
452
        ])->getContent();
453
    }
454
455
    /******************
456
     * PARSING PARAMS *
457
     ******************/
458
459
    /**
460
     * Get all standardized parameters from the Request, either via URL query string or routing.
461
     * @return string[]
462
     */
463 16
    public function getParams(): array
464
    {
465
        $paramsToCheck = [
466 16
            'project',
467
            'username',
468
            'namespace',
469
            'page',
470
            'categories',
471
            'group',
472
            'redirects',
473
            'deleted',
474
            'start',
475
            'end',
476
            'offset',
477
            'limit',
478
            'format',
479
480
            // Legacy parameters.
481
            'user',
482
            'name',
483
            'article',
484
            'wiki',
485
            'wikifam',
486
            'lang',
487
            'wikilang',
488
            'begin',
489
        ];
490
491
        /** @var string[] $params Each parameter that was detected along with its value. */
492 16
        $params = [];
493
494 16
        foreach ($paramsToCheck as $param) {
495
            // Pull in either from URL query string or route.
496 16
            $value = $this->request->query->get($param) ?: $this->request->get($param);
497
498
            // Only store if value is given ('namespace' or 'username' could be '0').
499 16
            if (null !== $value && '' !== $value) {
500 16
                $params[$param] = rawurldecode((string)$value);
501
            }
502
        }
503
504 16
        return $params;
505
    }
506
507
    /**
508
     * Parse out common parameters from the request. These include the 'project', 'username', 'namespace' and 'page',
509
     * along with their legacy counterparts (e.g. 'lang' and 'wiki').
510
     * @return string[] Normalized parameters (no legacy params).
511
     */
512 16
    public function parseQueryParams(): array
513
    {
514
        /** @var string[] $params Each parameter and value that was detected. */
515 16
        $params = $this->getParams();
516
517
        // Covert any legacy parameters, if present.
518 16
        $params = $this->convertLegacyParams($params);
519
520
        // Remove blank values.
521 16
        return array_filter($params, function ($param) {
522
            // 'namespace' or 'username' could be '0'.
523 4
            return null !== $param && '' !== $param;
524 16
        });
525
    }
526
527
    /**
528
     * Get UTC timestamps from given start and end string parameters. This also makes $start on month before
529
     * $end if not present, and makes $end the current time if not present.
530
     * @param int|string|false $start Unix timestamp or string accepted by strtotime.
531
     * @param int|string|false $end Unix timestamp or string accepted by strtotime.
532
     * @param bool $useDefaults Whether to use defaults if the values are blank. The start date is set to one month
533
     *   before the end date, and the end date is set to the present.
534
     * @return mixed[] Start and end date as UTC timestamps or 'false' if empty.
535
     */
536 1
    public function getUTCFromDateParams($start, $end, $useDefaults = false): array
537
    {
538 1
        $start = is_int($start) ? $start : strtotime((string)$start);
539 1
        $end = is_int($end) ? $end : strtotime((string)$end);
540
541
        // Use current time if end is not present (and is required), or if it exceeds the current time.
542 1
        if (($useDefaults && false === $end) || $end > time()) {
0 ignored issues
show
introduced by
The condition false === $end is always false.
Loading history...
543
            $end = time();
544
        }
545
546
        // Default to one month before end time if start is not present, as is not optional.
547 1
        if ($useDefaults && false === $start) {
0 ignored issues
show
introduced by
The condition false === $start is always false.
Loading history...
548 1
            $start = strtotime('-1 month', $end);
549
        }
550
551
        // Reverse if start date is after end date.
552 1
        if ($start > $end && false !== $start && false !== $end) {
553 1
            $newEnd = $start;
554 1
            $start = $end;
555 1
            $end = $newEnd;
556
        }
557
558 1
        return [$start, $end];
559
    }
560
561
    /**
562
     * Given the params hash, normalize any legacy parameters to their modern equivalent.
563
     * @param string[] $params
564
     * @return string[]
565
     */
566 16
    private function convertLegacyParams(array $params): array
567
    {
568
        $paramMap = [
569 16
            'user' => 'username',
570
            'name' => 'username',
571
            'article' => 'page',
572
            'begin' => 'start',
573
574
            // Copy super legacy project params to legacy so we can concatenate below.
575
            'wikifam' => 'wiki',
576
            'wikilang' => 'lang',
577
        ];
578
579
        // Copy legacy parameters to modern equivalent.
580 16
        foreach ($paramMap as $legacy => $modern) {
581 16
            if (isset($params[$legacy])) {
582
                $params[$modern] = $params[$legacy];
583 16
                unset($params[$legacy]);
584
            }
585
        }
586
587
        // Separate parameters for language and wiki.
588 16
        if (isset($params['wiki']) && isset($params['lang'])) {
589
            // 'wikifam' will be like '.wikipedia.org', vs just 'wikipedia',
590
            // so we must remove leading periods and trailing .org's.
591
            $params['project'] = rtrim(ltrim($params['wiki'], '.'), '.org').'.org';
592
593
            /** @var string[] $languagelessProjects Projects for which there is no specific language association. */
594
            $languagelessProjects = $this->container->getParameter('languageless_wikis');
595
596
            // Prepend language if applicable.
597
            if (isset($params['lang']) && !in_array($params['wiki'], $languagelessProjects)) {
598
                $params['project'] = $params['lang'].'.'.$params['project'];
599
            }
600
601
            unset($params['wiki']);
602
            unset($params['lang']);
603
        }
604
605 16
        return $params;
606
    }
607
608
    /************************
609
     * FORMATTING RESPONSES *
610
     ************************/
611
612
    /**
613
     * Get the rendered template for the requested format. This method also updates the cookies.
614
     * @param string $templatePath Path to template without format,
615
     *   such as '/editCounter/latest_global'.
616
     * @param array $ret Data that should be passed to the view.
617
     * @return Response
618
     * @codeCoverageIgnore
619
     */
620
    public function getFormattedResponse(string $templatePath, array $ret): Response
621
    {
622
        $format = $this->request->query->get('format', 'html');
623
        if ('' == $format) {
624
            // The default above doesn't work when the 'format' parameter is blank.
625
            $format = 'html';
626
        }
627
628
        // Merge in common default parameters, giving $ret (from the caller) the priority.
629
        $ret = array_merge([
630
            'project' => $this->project,
631
            'user' => $this->user,
632
            'page' => $this->page,
633
        ], $ret);
634
635
        $formatMap = [
636
            'wikitext' => 'text/plain',
637
            'csv' => 'text/csv',
638
            'tsv' => 'text/tab-separated-values',
639
            'json' => 'application/json',
640
        ];
641
642
        $response = new Response();
643
644
        // Set cookies. Note this must be done before rendering the view, as the view may invoke subrequests.
645
        $this->setCookies($response);
646
647
        // If requested format does not exist, assume HTML.
648
        if (false === $this->get('twig')->getLoader()->exists("$templatePath.$format.twig")) {
649
            $format = 'html';
650
        }
651
652
        $response = $this->render("$templatePath.$format.twig", $ret, $response);
653
654
        $contentType = $formatMap[$format] ?? 'text/html';
655
        $response->headers->set('Content-Type', $contentType);
656
657
        return $response;
658
    }
659
660
    /**
661
     * Return a JsonResponse object pre-supplied with the requested params.
662
     * @param array $data
663
     * @return JsonResponse
664
     */
665 2
    public function getFormattedApiResponse(array $data): JsonResponse
666
    {
667 2
        $response = new JsonResponse();
668 2
        $response->setEncodingOptions(JSON_NUMERIC_CHECK);
669 2
        $response->setStatusCode(Response::HTTP_OK);
670
671 2
        $elapsedTime = round(
672 2
            microtime(true) - $this->request->server->get('REQUEST_TIME_FLOAT'),
673 2
            3
674
        );
675
676
        // Any pipe-separated values should be returned as an array.
677 2
        foreach ($this->params as $param => $value) {
678 2
            if (false !== strpos($value, '|')) {
679 2
                $this->params[$param] = explode('|', $value);
680
            }
681
        }
682
683 2
        $response->setData(array_merge($this->params, [
684
            // In some controllers, $this->params['project'] may be overridden with a Project object.
685 2
            'project' => $this->project->getDomain(),
686 2
        ], $data, ['elapsed_time' => $elapsedTime]));
687
688 2
        return $response;
689
    }
690
691
    /*********
692
     * OTHER *
693
     *********/
694
695
    /**
696
     * Record usage of an API endpoint.
697
     * @param string $endpoint
698
     * @codeCoverageIgnore
699
     */
700
    public function recordApiUsage(string $endpoint): void
701
    {
702
        /** @var \Doctrine\DBAL\Connection $conn */
703
        $conn = $this->container->get('doctrine')
704
            ->getManager('default')
705
            ->getConnection();
706
        $date =  date('Y-m-d');
707
708
        // Increment count in timeline
709
        $existsSql = "SELECT 1 FROM usage_api_timeline
710
                      WHERE date = '$date'
711
                      AND endpoint = '$endpoint'";
712
713
        if (0 === count($conn->query($existsSql)->fetchAll())) {
714
            $createSql = "INSERT INTO usage_api_timeline
715
                          VALUES(NULL, '$date', '$endpoint', 1)";
716
            $conn->query($createSql);
717
        } else {
718
            $updateSql = "UPDATE usage_api_timeline
719
                          SET count = count + 1
720
                          WHERE endpoint = '$endpoint'
721
                          AND date = '$date'";
722
            $conn->query($updateSql);
723
        }
724
    }
725
726
    /**
727
     * Add a flash message.
728
     * @param string $type
729
     * @param string $key
730
     * @param array $vars
731
     */
732
    public function addFlashMessage(string $type, string $key, array $vars = []): void
733
    {
734
        $this->addFlash(
735
            $type,
736
            $this->i18n->msg($key, $vars)
0 ignored issues
show
Bug introduced by
It seems like $this->i18n->msg($key, $vars) can also be of type null; however, parameter $message of Symfony\Bundle\Framework...\Controller::addFlash() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

736
            /** @scrutinizer ignore-type */ $this->i18n->msg($key, $vars)
Loading history...
737
        );
738
    }
739
}
740