Passed
Push — master ( 6ef6ed...3181c4 )
by MusikAnimal
07:28
created

XtoolsController::setProperties()   B

Complexity

Conditions 7
Paths 36

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7.9295

Importance

Changes 0
Metric Value
cc 7
eloc 14
nc 36
nop 0
dl 0
loc 28
ccs 11
cts 15
cp 0.7332
crap 7.9295
rs 8.8333
c 0
b 0
f 0
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 DateTime;
19
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
20
use Symfony\Component\DependencyInjection\ContainerInterface;
21
use Symfony\Component\HttpFoundation\Cookie;
22
use Symfony\Component\HttpFoundation\JsonResponse;
23
use Symfony\Component\HttpFoundation\Request;
24
use Symfony\Component\HttpFoundation\RequestStack;
25
use Symfony\Component\HttpFoundation\Response;
26
27
/**
28
 * XtoolsController supplies a variety of methods around parsing and validating parameters, and initializing
29
 * Project/User instances. These are used in other controllers in the AppBundle\Controller namespace.
30
 * @abstract
31
 */
32
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

32
abstract class XtoolsController extends /** @scrutinizer ignore-deprecated */ Controller
Loading history...
33
{
34
    /** @var I18nHelper i18n helper. */
35
    protected $i18n;
36
37
    /** @var Request The request object. */
38
    protected $request;
39
40
    /** @var string Name of the action within the child controller that is being executed. */
41
    protected $controllerAction;
42
43
    /** @var array Hash of params parsed from the Request. */
44
    protected $params;
45
46
    /** @var bool Whether this is a request to an API action. */
47
    protected $isApi;
48
49
    /** @var Project Relevant Project parsed from the Request. */
50
    protected $project;
51
52
    /** @var User Relevant User parsed from the Request. */
53
    protected $user;
54
55
    /** @var Page Relevant Page parsed from the Request. */
56
    protected $page;
57
58
    /** @var int|false Start date parsed from the Request. */
59
    protected $start = false;
60
61
    /** @var int|false End date parsed from the Request. */
62
    protected $end = false;
63
64
    /**
65
     * Default days from current day, to use as the start date if none was provided.
66
     * If this is null and $maxDays is non-null, the latter will be used as the default.
67
     * Is public visibility evil here? I don't think so.
68
     * @var int|null
69
     */
70
    public $defaultDays = null;
71
72
    /**
73
     * Maximum number of days allowed for the given date range.
74
     * Set this in the controller's constructor to enforce the given date range to be within this range.
75
     * This will be used as the default date span unless $defaultDays is defined.
76
     * @see XtoolsController::getUTCFromDateParams()
77
     * @var int|null
78
     */
79
    public $maxDays = null;
80
81
    /** @var int|string Namespace parsed from the Request, ID as int or 'all' for all namespaces. */
82
    protected $namespace;
83
84
    /** @var int Pagination offset parsed from the Request. */
85
    protected $offset = 0;
86
87
    /** @var int Number of results to return. */
88
    protected $limit;
89
90
    /** @var bool Is the current request a subrequest? */
91
    protected $isSubRequest;
92
93
    /**
94
     * Stores user preferences such default project.
95
     * This may get altered from the Request and updated in the Response.
96
     * @var array
97
     */
98
    protected $cookies = [
99
        'XtoolsProject' => null,
100
    ];
101
102
    /**
103
     * This activates the 'too high edit count' functionality. This property represents the
104
     * action that should be redirected to if the user has too high of an edit count.
105
     * @var string
106
     */
107
    protected $tooHighEditCountAction;
108
109
    /**
110
     * @var array Actions that are exempt from edit count limitations.
111
     */
112
    protected $tooHighEditCountActionBlacklist = [];
113
114
    /**
115
     * Require the tool's index route (initial form) be defined here. This should also
116
     * be the name of the associated model, if present.
117
     * @return string
118
     */
119
    abstract protected function getIndexRoute(): string;
120
121
    /**
122
     * XtoolsController constructor.
123
     * @param RequestStack $requestStack
124
     * @param ContainerInterface $container
125
     * @param I18nHelper $i18n
126
     */
127 16
    public function __construct(RequestStack $requestStack, ContainerInterface $container, I18nHelper $i18n)
128
    {
129 16
        $this->request = $requestStack->getCurrentRequest();
130 16
        $this->container = $container;
131 16
        $this->i18n = $i18n;
132 16
        $this->params = $this->parseQueryParams();
133
134
        // Parse out the name of the controller and action.
135 16
        $pattern = "#::([a-zA-Z]*)Action#";
136 16
        $matches = [];
137
        // The blank string here only happens in the unit tests, where the request may not be made to an action.
138 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

138
        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...
139 16
        $this->controllerAction = $matches[1] ?? '';
140
141
        // Whether the action is an API action.
142 16
        $this->isApi = 'Api' === substr($this->controllerAction, -3);
143
144
        // Whether we're making a subrequest (the view makes a request to another action).
145 16
        $this->isSubRequest = $this->request->get('htmlonly')
146 16
            || null !== $this->get('request_stack')->getParentRequest();
147
148
        // Load user options from cookies.
149 16
        $this->loadCookies();
150
151
        // Set the class-level properties based on params.
152 16
        if (false !== strpos(strtolower($this->controllerAction), 'index')) {
153
            // Index pages should only set the project, and no other class properties.
154 10
            $this->setProject($this->getProjectFromQuery());
155
        } else {
156 6
            $this->setProperties(); // Includes the project.
157
        }
158 16
    }
159
160
    /***********
161
     * COOKIES *
162
     ***********/
163
164
    /**
165
     * Load user preferences from the associated cookies.
166
     */
167 16
    private function loadCookies(): void
168
    {
169
        // Not done for subrequests.
170 16
        if ($this->isSubRequest) {
171
            return;
172
        }
173
174 16
        foreach (array_keys($this->cookies) as $name) {
175 16
            $this->cookies[$name] = $this->request->cookies->get($name);
176
        }
177 16
    }
178
179
    /**
180
     * Set cookies on the given Response.
181
     * @param Response $response
182
     */
183
    private function setCookies(Response &$response): void
184
    {
185
        // Not done for subrequests.
186
        if ($this->isSubRequest) {
187
            return;
188
        }
189
190
        foreach ($this->cookies as $name => $value) {
191
            $response->headers->setCookie(
192
                new Cookie($name, $value)
193
            );
194
        }
195
    }
196
197
    /**
198
     * Sets the project, with the domain in $this->cookies['XtoolsProject'] that will
199
     * later get set on the Response headers in self::getFormattedResponse().
200
     * @param Project $project
201
     */
202 12
    private function setProject(Project $project): void
203
    {
204 12
        $this->project = $project;
205 12
        $this->cookies['XtoolsProject'] = $project->getDomain();
206 12
    }
207
208
    /****************************
209
     * SETTING CLASS PROPERTIES *
210
     ****************************/
211
212
    /**
213
     * Normalize all common parameters used by the controllers and set class properties.
214
     */
215 6
    private function setProperties(): void
216
    {
217 6
        $this->namespace = $this->params['namespace'] ?? null;
218
219
        // Offset and limit need to be ints.
220 6
        foreach (['offset', 'limit'] as $param) {
221 6
            if (isset($this->params[$param])) {
222 6
                $this->{$param} = (int)$this->params[$param];
223
            }
224
        }
225
226 6
        if (isset($this->params['project'])) {
227 2
            $this->setProject($this->validateProject($this->params['project']));
228 4
        } elseif (null !== $this->cookies['XtoolsProject']) {
229
            // Set from cookie.
230
            $this->setProject(
231
                $this->validateProject($this->cookies['XtoolsProject'])
232
            );
233
        }
234
235 6
        if (isset($this->params['username'])) {
236
            $this->user = $this->validateUser($this->params['username']);
237
        }
238 6
        if (isset($this->params['page'])) {
239
            $this->page = $this->getPageFromNsAndTitle($this->namespace, $this->params['page']);
240
        }
241
242 6
        $this->setDates();
243 6
    }
244
245
    /**
246
     * Set class properties for dates, if such params were passed in.
247
     */
248 6
    private function setDates(): void
249
    {
250 6
        $start = $this->params['start'] ?? false;
251 6
        $end = $this->params['end'] ?? false;
252 6
        if ($start || $end || null !== $this->maxDays) {
253
            [$this->start, $this->end] = $this->getUTCFromDateParams($start, $end);
254
255
            // Set $this->params accordingly too, so that for instance API responses will include it.
256
            $this->params['start'] = is_int($this->start) ? date('Y-m-d', $this->start) : false;
257
            $this->params['end'] = is_int($this->end) ? date('Y-m-d', $this->end) : false;
258
        }
259 6
    }
260
261
    /**
262
     * Construct a fully qualified page title given the namespace and title.
263
     * @param int|string $ns Namespace ID.
264
     * @param string $title Page title.
265
     * @param bool $rawTitle Return only the title (and not a Page).
266
     * @return Page|string
267
     */
268
    protected function getPageFromNsAndTitle($ns, string $title, bool $rawTitle = false)
269
    {
270
        if (0 === (int)$ns) {
271
            return $rawTitle ? $title : $this->validatePage($title);
272
        }
273
274
        // Prepend namespace and strip out duplicates.
275
        $nsName = $this->project->getNamespaces()[$ns] ?? $this->i18n->msg('unknown');
276
        $title = $nsName.':'.preg_replace('/^'.$nsName.':/', '', $title);
277
        return $rawTitle ? $title : $this->validatePage($title);
278
    }
279
280
    /**
281
     * Get a Project instance from the project string, using defaults if the given project string is invalid.
282
     * @return Project
283
     */
284 10
    public function getProjectFromQuery(): Project
285
    {
286
        // Set default project so we can populate the namespace selector on index pages.
287
        // Defaults to project stored in cookie, otherwise project specified in parameters.yml.
288 10
        if (isset($this->params['project'])) {
289 2
            $project = $this->params['project'];
290 8
        } elseif (null !== $this->cookies['XtoolsProject']) {
291
            $project = $this->cookies['XtoolsProject'];
292
        } else {
293 8
            $project = $this->container->getParameter('default_project');
294
        }
295
296 10
        $projectData = ProjectRepository::getProject($project, $this->container);
297
298
        // Revert back to defaults if we've established the given project was invalid.
299 10
        if (!$projectData->exists()) {
300
            $projectData = ProjectRepository::getProject(
301
                $this->container->getParameter('default_project'),
302
                $this->container
303
            );
304
        }
305
306 10
        return $projectData;
307
    }
308
309
    /*************************
310
     * GETTERS / VALIDATIONS *
311
     *************************/
312
313
    /**
314
     * Validate the given project, returning a Project if it is valid or false otherwise.
315
     * @param string $projectQuery Project domain or database name.
316
     * @return Project
317
     * @throws XtoolsHttpException
318
     */
319 2
    public function validateProject(string $projectQuery): Project
320
    {
321
        /** @var Project $project */
322 2
        $project = ProjectRepository::getProject($projectQuery, $this->container);
323
324 2
        if (!$project->exists()) {
325
            $this->throwXtoolsException(
326
                $this->getIndexRoute(),
327
                'invalid-project',
328
                [$this->params['project']],
329
                'project'
330
            );
331
        }
332
333 2
        return $project;
334
    }
335
336
    /**
337
     * Validate the given user, returning a User or Redirect if they don't exist.
338
     * @param string $username
339
     * @return User
340
     * @throws XtoolsHttpException
341
     */
342
    public function validateUser(string $username): User
343
    {
344
        $user = UserRepository::getUser($username, $this->container);
345
346
        // Allow querying for any IP, currently with no edit count limitation...
347
        // Once T188677 is resolved IPs will be affected by the EXPLAIN results.
348
        if ($user->isAnon()) {
349
            return $user;
350
        }
351
352
        $originalParams = $this->params;
353
354
        // Don't continue if the user doesn't exist.
355
        if (!$user->existsOnProject($this->project)) {
356
            $this->throwXtoolsException($this->getIndexRoute(), 'user-not-found', [], 'username');
357
        }
358
359
        // Reject users with a crazy high edit count.
360
        if (isset($this->tooHighEditCountAction) &&
361
            !in_array($this->controllerAction, $this->tooHighEditCountActionBlacklist) &&
362
            $user->hasTooManyEdits($this->project)
363
        ) {
364
            /** TODO: Somehow get this to use self::throwXtoolsException */
365
366
            $this->addFlashMessage('danger', 'too-many-edits', [
367
                $this->i18n->numberFormat($user->maxEdits()),
368
            ]);
369
370
            // If redirecting to a different controller, show an informative message accordingly.
371
            if ($this->tooHighEditCountAction !== $this->getIndexRoute()) {
372
                // FIXME: This is currently only done for Edit Counter, redirecting to Simple Edit Counter,
373
                // so this bit is hardcoded. We need to instead give the i18n key of the route.
374
                $this->addFlashMessage('info', 'too-many-edits-redir', [
375
                    $this->i18n->msg('tool-simpleeditcounter'),
376
                ]);
377
            } else {
378
                // Redirecting back to index, so remove username (otherwise we'd get a redirect loop).
379
                unset($this->params['username']);
380
            }
381
382
            throw new XtoolsHttpException(
383
                'User has made too many edits! Maximum '.$user->maxEdits(),
384
                $this->generateUrl($this->tooHighEditCountAction, $this->params),
385
                $originalParams,
386
                $this->isApi
387
            );
388
        }
389
390
        return $user;
391
    }
392
393
    /**
394
     * Get a Page instance from the given page title, and validate that it exists.
395
     * @param string $pageTitle
396
     * @return Page
397
     * @throws XtoolsHttpException
398
     */
399
    public function validatePage(string $pageTitle): Page
400
    {
401
        $page = new Page($this->project, $pageTitle);
402
        $pageRepo = new PageRepository();
403
        $pageRepo->setContainer($this->container);
404
        $page->setRepository($pageRepo);
405
406
        if (!$page->exists()) {
407
            $this->throwXtoolsException(
408
                $this->getIndexRoute(),
409
                'no-result',
410
                [$this->params['page'] ?? null],
411
                'page'
412
            );
413
        }
414
415
        return $page;
416
    }
417
418
    /**
419
     * Throw an XtoolsHttpException, which the given error message and redirects to specified action.
420
     * @param string $redirectAction Name of action to redirect to.
421
     * @param string $message i18n key of error message. Shown in API responses.
422
     *   If no message with this key exists, $message is shown as-is.
423
     * @param array $messageParams
424
     * @param string $invalidParam This will be removed from $this->params. Omit if you don't want this to happen.
425
     * @throws XtoolsHttpException
426
     */
427
    public function throwXtoolsException(
428
        string $redirectAction,
429
        string $message,
430
        array $messageParams = [],
431
        ?string $invalidParam = null
432
    ): void {
433
        $this->addFlashMessage('danger', $message, $messageParams);
434
        $originalParams = $this->params;
435
436
        // Remove invalid parameter if it was given.
437
        if (is_string($invalidParam)) {
438
            unset($this->params[$invalidParam]);
439
        }
440
441
        // We sometimes are redirecting to the index page, so also remove project (otherwise we'd get a redirect loop).
442
        /**
443
         * FIXME: Index pages should have a 'nosubmit' parameter to prevent submission.
444
         * Then we don't even need to remove $invalidParam.
445
         * Better, we should show the error on the results page, with no results.
446
         */
447
        unset($this->params['project']);
448
449
        // Throw exception which will redirect to $redirectAction.
450
        throw new XtoolsHttpException(
451
            $this->i18n->msgIfExists($message, $messageParams),
452
            $this->generateUrl($redirectAction, $this->params),
453
            $originalParams,
454
            $this->isApi
455
        );
456
    }
457
458
    /**
459
     * Get the first error message stored in the session's FlashBag.
460
     * @return string
461
     */
462
    public function getFlashMessage(): string
463
    {
464
        $key = $this->get('session')->getFlashBag()->get('danger')[0];
465
        $param = null;
466
467
        if (is_array($key)) {
468
            [$key, $param] = $key;
469
        }
470
471
        return $this->render('message.twig', [
472
            'key' => $key,
473
            'params' => [$param],
474
        ])->getContent();
475
    }
476
477
    /******************
478
     * PARSING PARAMS *
479
     ******************/
480
481
    /**
482
     * Get all standardized parameters from the Request, either via URL query string or routing.
483
     * @return string[]
484
     */
485 16
    public function getParams(): array
486
    {
487
        $paramsToCheck = [
488 16
            'project',
489
            'username',
490
            'namespace',
491
            'page',
492
            'categories',
493
            'group',
494
            'redirects',
495
            'deleted',
496
            'start',
497
            'end',
498
            'offset',
499
            'limit',
500
            'format',
501
502
            // Legacy parameters.
503
            'user',
504
            'name',
505
            'article',
506
            'wiki',
507
            'wikifam',
508
            'lang',
509
            'wikilang',
510
            'begin',
511
        ];
512
513
        /** @var string[] $params Each parameter that was detected along with its value. */
514 16
        $params = [];
515
516 16
        foreach ($paramsToCheck as $param) {
517
            // Pull in either from URL query string or route.
518 16
            $value = $this->request->query->get($param) ?: $this->request->get($param);
519
520
            // Only store if value is given ('namespace' or 'username' could be '0').
521 16
            if (null !== $value && '' !== $value) {
522 16
                $params[$param] = rawurldecode((string)$value);
523
            }
524
        }
525
526 16
        return $params;
527
    }
528
529
    /**
530
     * Parse out common parameters from the request. These include the 'project', 'username', 'namespace' and 'page',
531
     * along with their legacy counterparts (e.g. 'lang' and 'wiki').
532
     * @return string[] Normalized parameters (no legacy params).
533
     */
534 16
    public function parseQueryParams(): array
535
    {
536
        /** @var string[] $params Each parameter and value that was detected. */
537 16
        $params = $this->getParams();
538
539
        // Covert any legacy parameters, if present.
540 16
        $params = $this->convertLegacyParams($params);
541
542
        // Remove blank values.
543 16
        return array_filter($params, function ($param) {
544
            // 'namespace' or 'username' could be '0'.
545 4
            return null !== $param && '' !== $param;
546 16
        });
547
    }
548
549
    /**
550
     * Get UTC timestamps from given start and end string parameters. This also makes $start $maxDays before
551
     * $end if not present, and makes $end the current time if not present.
552
     * The date range will not exceed $this->maxDays days, if this public class property is set.
553
     * @param int|string|false $start Unix timestamp or string accepted by strtotime.
554
     * @param int|string|false $end Unix timestamp or string accepted by strtotime.
555
     * @return int[] Start and end date as UTC timestamps.
556
     */
557 1
    public function getUTCFromDateParams($start, $end): array
558
    {
559 1
        $today = strtotime('today midnight');
560
561
        // start time should not be in the future.
562 1
        $startTime = min(
563 1
            is_int($start) ? $start : strtotime((string)$start),
564 1
            $today
565
        );
566
567
        // end time defaults to now, and will not be in the future.
568 1
        $endTime = min(
569 1
            (is_int($end) ? $end : strtotime((string)$end)) ?: $today,
570 1
            $today
571
        );
572
573
        // Default to $this->defaultDays or $this->maxDays before end time if start is not present.
574 1
        $daysOffset = $this->defaultDays ?? $this->maxDays;
575 1
        if (false === $start && is_int($daysOffset)) {
576 1
            $startTime = strtotime("-$daysOffset days", $endTime);
577
        }
578
579
        // Default to $this->defaultDays or $this->maxDays after start time if end is not present.
580 1
        if (false === $end && is_int($daysOffset)) {
581
            $endTime = min(
582
                strtotime("+$daysOffset days", $startTime),
583
                $today
584
            );
585
        }
586
587
        // Reverse if start date is after end date.
588 1
        if ($startTime > $endTime && false !== $startTime && false !== $end) {
589 1
            $newEndTime = $startTime;
590 1
            $startTime = $endTime;
591 1
            $endTime = $newEndTime;
592
        }
593
594
        // Finally, don't let the date range exceed $this->maxDays.
595 1
        $startObj = DateTime::createFromFormat('U', (string)$startTime);
596 1
        $endObj = DateTime::createFromFormat('U', (string)$endTime);
597 1
        if (is_int($this->maxDays) && $startObj->diff($endObj)->days > $this->maxDays) {
0 ignored issues
show
Bug introduced by
It seems like $endObj can also be of type false; however, parameter $datetime2 of DateTime::diff() does only seem to accept DateTimeInterface, 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

597
        if (is_int($this->maxDays) && $startObj->diff(/** @scrutinizer ignore-type */ $endObj)->days > $this->maxDays) {
Loading history...
598
            // Show warnings that the date range was truncated.
599 1
            $this->addFlashMessage('warning', 'date-range-too-wide', [$this->maxDays]);
600
601 1
            $startTime = strtotime("-$this->maxDays days", $endTime);
602
        }
603
604 1
        return [$startTime, $endTime];
605
    }
606
607
    /**
608
     * Given the params hash, normalize any legacy parameters to their modern equivalent.
609
     * @param string[] $params
610
     * @return string[]
611
     */
612 16
    private function convertLegacyParams(array $params): array
613
    {
614
        $paramMap = [
615 16
            'user' => 'username',
616
            'name' => 'username',
617
            'article' => 'page',
618
            'begin' => 'start',
619
620
            // Copy super legacy project params to legacy so we can concatenate below.
621
            'wikifam' => 'wiki',
622
            'wikilang' => 'lang',
623
        ];
624
625
        // Copy legacy parameters to modern equivalent.
626 16
        foreach ($paramMap as $legacy => $modern) {
627 16
            if (isset($params[$legacy])) {
628
                $params[$modern] = $params[$legacy];
629 16
                unset($params[$legacy]);
630
            }
631
        }
632
633
        // Separate parameters for language and wiki.
634 16
        if (isset($params['wiki']) && isset($params['lang'])) {
635
            // 'wikifam' will be like '.wikipedia.org', vs just 'wikipedia',
636
            // so we must remove leading periods and trailing .org's.
637
            $params['project'] = rtrim(ltrim($params['wiki'], '.'), '.org').'.org';
638
639
            /** @var string[] $languagelessProjects Projects for which there is no specific language association. */
640
            $languagelessProjects = $this->container->getParameter('languageless_wikis');
641
642
            // Prepend language if applicable.
643
            if (isset($params['lang']) && !in_array($params['wiki'], $languagelessProjects)) {
644
                $params['project'] = $params['lang'].'.'.$params['project'];
645
            }
646
647
            unset($params['wiki']);
648
            unset($params['lang']);
649
        }
650
651 16
        return $params;
652
    }
653
654
    /************************
655
     * FORMATTING RESPONSES *
656
     ************************/
657
658
    /**
659
     * Get the rendered template for the requested format. This method also updates the cookies.
660
     * @param string $templatePath Path to template without format,
661
     *   such as '/editCounter/latest_global'.
662
     * @param array $ret Data that should be passed to the view.
663
     * @return Response
664
     * @codeCoverageIgnore
665
     */
666
    public function getFormattedResponse(string $templatePath, array $ret): Response
667
    {
668
        $format = $this->request->query->get('format', 'html');
669
        if ('' == $format) {
670
            // The default above doesn't work when the 'format' parameter is blank.
671
            $format = 'html';
672
        }
673
674
        // Merge in common default parameters, giving $ret (from the caller) the priority.
675
        $ret = array_merge([
676
            'project' => $this->project,
677
            'user' => $this->user,
678
            'page' => $this->page,
679
        ], $ret);
680
681
        $formatMap = [
682
            'wikitext' => 'text/plain',
683
            'csv' => 'text/csv',
684
            'tsv' => 'text/tab-separated-values',
685
            'json' => 'application/json',
686
        ];
687
688
        $response = new Response();
689
690
        // Set cookies. Note this must be done before rendering the view, as the view may invoke subrequests.
691
        $this->setCookies($response);
692
693
        // If requested format does not exist, assume HTML.
694
        if (false === $this->get('twig')->getLoader()->exists("$templatePath.$format.twig")) {
695
            $format = 'html';
696
        }
697
698
        $response = $this->render("$templatePath.$format.twig", $ret, $response);
699
700
        $contentType = $formatMap[$format] ?? 'text/html';
701
        $response->headers->set('Content-Type', $contentType);
702
703
        return $response;
704
    }
705
706
    /**
707
     * Return a JsonResponse object pre-supplied with the requested params.
708
     * @param array $data
709
     * @return JsonResponse
710
     */
711 2
    public function getFormattedApiResponse(array $data): JsonResponse
712
    {
713 2
        $response = new JsonResponse();
714 2
        $response->setEncodingOptions(JSON_NUMERIC_CHECK);
715 2
        $response->setStatusCode(Response::HTTP_OK);
716
717 2
        $elapsedTime = round(
718 2
            microtime(true) - $this->request->server->get('REQUEST_TIME_FLOAT'),
719 2
            3
720
        );
721
722
        // Any pipe-separated values should be returned as an array.
723 2
        foreach ($this->params as $param => $value) {
724 2
            if (false !== strpos($value, '|')) {
725 2
                $this->params[$param] = explode('|', $value);
726
            }
727
        }
728
729 2
        $ret = array_merge($this->params, [
730
            // In some controllers, $this->params['project'] may be overridden with a Project object.
731 2
            'project' => $this->project->getDomain(),
732 2
        ], $data, ['elapsed_time' => $elapsedTime]);
733
734
        // Merge in flash messages, putting them at the top.
735 2
        $flashes = $this->get('session')->getFlashBag()->peekAll();
736 2
        $ret = array_merge($flashes, $ret);
737
738
        // Flashes now can be cleared after merging into the response.
739 2
        $this->get('session')->getFlashBag()->clear();
740
741 2
        $response->setData($ret);
742
743 2
        return $response;
744
    }
745
746
    /*********
747
     * OTHER *
748
     *********/
749
750
    /**
751
     * Record usage of an API endpoint.
752
     * @param string $endpoint
753
     * @codeCoverageIgnore
754
     */
755
    public function recordApiUsage(string $endpoint): void
756
    {
757
        /** @var \Doctrine\DBAL\Connection $conn */
758
        $conn = $this->container->get('doctrine')
759
            ->getManager('default')
760
            ->getConnection();
761
        $date =  date('Y-m-d');
762
763
        // Increment count in timeline
764
        $existsSql = "SELECT 1 FROM usage_api_timeline
765
                      WHERE date = '$date'
766
                      AND endpoint = '$endpoint'";
767
768
        if (0 === count($conn->query($existsSql)->fetchAll())) {
769
            $createSql = "INSERT INTO usage_api_timeline
770
                          VALUES(NULL, '$date', '$endpoint', 1)";
771
            $conn->query($createSql);
772
        } else {
773
            $updateSql = "UPDATE usage_api_timeline
774
                          SET count = count + 1
775
                          WHERE endpoint = '$endpoint'
776
                          AND date = '$date'";
777
            $conn->query($updateSql);
778
        }
779
    }
780
781
    /**
782
     * Add a flash message.
783
     * @param string $type
784
     * @param string $key
785
     * @param array $vars
786
     */
787 1
    public function addFlashMessage(string $type, string $key, array $vars = []): void
788
    {
789 1
        $this->addFlash(
790 1
            $type,
791 1
            $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

791
            /** @scrutinizer ignore-type */ $this->i18n->msg($key, $vars)
Loading history...
792
        );
793 1
    }
794
}
795