WebController   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 571
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 1
Metric Value
wmc 67
eloc 314
dl 0
loc 571
rs 3.04
c 7
b 0
f 1

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 50 3
B listStyle() 0 11 8
B guessLanguage() 0 32 8
A findCustomTemplates() 0 16 2
C sendFeedback() 0 45 10
A invokeGenericErrorPage() 0 12 1
A createFeedbackHeaders() 0 12 3
B invokeFeedbackForm() 0 31 6
A invokeAboutPage() 0 11 1
A parseVocabularyLanguageOrder() 0 14 3
A invokeLandingPage() 0 23 1
A invokeVocabularyConcept() 0 40 3
B invokeGlobalSearch() 0 67 10
B invokeVocabularySearch() 0 68 7
A invokeVocabularyHome() 0 24 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
/**
4
 * Importing the dependencies.
5
 */
6
use Punic\Language;
7
use Symfony\Bridge\Twig\Extension\TranslationExtension;
8
9
/**
10
 * WebController is an extension of the Controller that handles all
11
 * the requests originating from the view of the website.
12
 */
13
class WebController extends Controller
14
{
15
    /**
16
     * Provides access to the templating engine.
17
     * @property object $twig the twig templating engine.
18
     */
19
    public $twig;
20
    public $honeypot;
21
    public $translator;
22
23
    /**
24
     * Constructor for the WebController.
25
     * @param Model $model
26
     */
27
    public function __construct($model)
28
    {
29
        parent::__construct($model);
30
31
        // initialize Twig templates
32
        $tmpDir = $model->getConfig()->getTemplateCache();
33
34
        // check if the cache pointed by config.ttl exists, if not we create it.
35
        if (!file_exists($tmpDir)) {
36
            mkdir($tmpDir);
37
        }
38
39
        // specify where to look for templates and cache
40
        $loader = new \Twig\Loader\FilesystemLoader([__DIR__ . '/../../custom-templates', __DIR__ . '/../view']);
41
        // initialize Twig environment
42
        $this->twig = new \Twig\Environment($loader, array('cache' => $tmpDir,'auto_reload' => true));
43
        // used for setting the base href for the relative urls
44
        $this->twig->addGlobal("BaseHref", $this->getBaseHref());
45
46
        // pass the GlobalConfig object to templates so they can access configuration
47
        $this->twig->addGlobal("GlobalConfig", $this->model->getConfig());
48
49
        // setting the list of properties to be displayed in the search results
50
        $this->twig->addGlobal("PreferredProperties", array('skos:prefLabel', 'skos:narrower', 'skos:broader', 'skosmos:memberOf', 'skos:altLabel', 'skos:related'));
51
52
        // register a Twig filter for generating URLs for global pages (landing, about, feedback, vocab-home..)
53
        $this->twig->addExtension(new GlobalUrlExtension());
54
55
        // register a Twig filter for generating URLs for vocabulary resources (concepts and groups)
56
        $this->twig->addExtension(new LinkUrlExtension($model));
57
58
        // register a Twig filter for generating strings from language codes with CLDR
59
        $langFilter = new \Twig\TwigFilter('lang_name', function ($langcode, $lang) {
60
            return Language::getName($langcode, $lang);
61
        });
62
        $this->twig->addFilter($langFilter);
63
64
        $this->translator = $model->getTranslator();
65
        $this->twig->addExtension(new TranslationExtension($this->translator));
66
67
        // create the honeypot
68
        $this->honeypot = new \Honeypot();
69
        if (!$this->model->getConfig()->getHoneypotEnabled()) {
70
            $this->honeypot->disable();
71
        }
72
        $this->twig->addGlobal('honeypot', $this->honeypot);
73
74
        // populate the customizable content slots from custom templates
75
        $customTemplates = $this->findCustomTemplates("../custom-templates");
76
        $this->twig->addGlobal('customTemplates', $customTemplates);
77
    }
78
79
    /**
80
     * Find any custom templates from the given directory and return them as an array.
81
     * @param string $dir path of custom templates directory
82
     * @return array array of custom template filenames, keyed by slot
83
     */
84
    public function findCustomTemplates($dir)
85
    {
86
        $customTemplateSubDirs = glob($dir . '/*', GLOB_ONLYDIR);
87
        $customTemplates = [];
88
89
        foreach ($customTemplateSubDirs as $slotDir) {
90
            $slotName = basename($slotDir);
91
            $files = glob($slotDir . '/*.twig');
92
            // Strip the directory part to make the file paths relative to the directory.
93
            // The "custom-templates" directory is registered to the Twig FilesystemLoader.
94
            $customTemplates[$slotName] = array_map(function ($file) use ($dir) {
95
                return str_replace($dir . '/', '', $file);
96
            }, $files);
97
        }
98
99
        return $customTemplates;
100
    }
101
102
    /**
103
     * Guess the language of the user. Return a language string that is one
104
     * of the supported languages defined in the $LANGUAGES setting, e.g. "fi".
105
     * @param Request $request HTTP request
106
     * @param string $vocid identifier for the vocabulary eg. 'yso'.
107
     * @return string returns the language choice as a numeric string value
108
     */
109
    public function guessLanguage($request, $vocid = null)
110
    {
111
        // 1. select language based on SKOSMOS_LANGUAGE cookie
112
        $languageCookie = $request->getCookie('SKOSMOS_LANGUAGE');
113
        if ($languageCookie) {
114
            return $languageCookie;
115
        }
116
117
        // 2. if vocabulary given, select based on the default language of the vocabulary
118
        if ($vocid !== null && $vocid !== '') {
119
            try {
120
                $vocab = $this->model->getVocabulary($vocid);
121
                return $vocab->getConfig()->getDefaultLanguage();
122
            } catch (Exception $e) {
123
                // vocabulary id not found, move on to the next selection method
124
            }
125
        }
126
127
        // 3. select language based on Accept-Language header
128
        header('Vary: Accept-Language'); // inform caches that a decision was made based on Accept header
129
        $this->negotiator = new \Negotiation\LanguageNegotiator();
130
        $langcodes = array_keys($this->languages);
131
        // using a random language from the configured UI languages when there is no accept language header set
132
        $acceptLanguage = $request->getServerConstant('HTTP_ACCEPT_LANGUAGE') ? $request->getServerConstant('HTTP_ACCEPT_LANGUAGE') : $langcodes[0];
133
134
        $bestLang = $this->negotiator->getBest($acceptLanguage, $langcodes);
135
        if (isset($bestLang) && in_array($bestLang->getValue(), $langcodes)) {
136
            return $bestLang->getValue();
137
        }
138
139
        // show default site or prompt for language
140
        return $langcodes[0];
141
    }
142
143
    /**
144
     * Determines a css class that controls width and positioning of the vocabulary list element.
145
     * The layout is wider if the left/right box templates have not been provided.
146
     * @return string css class for the container eg. 'voclist-wide' or 'voclist-right'
147
     */
148
    private function listStyle()
149
    {
150
        $left = file_exists('view/left.inc');
151
        $right = file_exists('view/right.inc');
152
        $ret = 'voclist';
153
        if (!$left && !$right) {
154
            $ret .= '-wide';
155
        } elseif (!($left && $right) && ($right || $left)) {
156
            $ret .= ($right) ? '-left' : '-right';
157
        }
158
        return $ret;
159
    }
160
161
    /**
162
    * Renders the list of supported languages from vocabulary config in order.
163
    * The ordering is done according to the language order parameter in vocabulary config if such exists
164
    * @param Vocabulary $vocab the vocabulary object
165
    * @return array with language codes as keys and language labels as values
166
    */
167
    public function parseVocabularyLanguageOrder($vocab)
168
    {
169
        $vocabContentLanguages = array_flip($vocab->getConfig()->getLanguages());
170
        $languageOrder = $vocab->getConfig()->getLanguageOrder();
171
172
        $tmpList = [];
173
174
        foreach ($languageOrder as $vocLang) {
175
            if (isset($vocabContentLanguages[$vocLang])) {
176
                $tmpList[$vocLang] = $vocabContentLanguages[$vocLang];
177
                unset($vocabContentLanguages[$vocLang]);
178
            }
179
        }
180
        return $tmpList + $vocabContentLanguages;
181
    }
182
183
184
    /**
185
     * Loads and renders the landing page view.
186
     * @param Request $request
187
     */
188
    public function invokeLandingPage($request)
189
    {
190
        $this->model->setLocale($request->getLang());
191
        // load template
192
        $template = $this->twig->load('landing.twig');
193
        // set template variables
194
        $categoryLabel = $this->model->getClassificationLabel($request->getLang());
195
        $sortedVocabs = $this->model->getVocabularyList(false, true);
196
        $contentLanguages = array_flip($this->model->getLanguages($request->getLang()));
197
        $listStyle = $this->listStyle();
198
199
        $vocabTypes = $this->model->getTypes(null, $request->getLang());
200
201
        // render template
202
        echo $template->render(
203
            array(
204
                'sorted_vocabs' => $sortedVocabs,
205
                'category_label' => $categoryLabel,
206
                'languages' => $this->languages,
207
                'content_languages' => $contentLanguages,
208
                'request' => $request,
209
                'types' => $vocabTypes,
210
                'list_style' => $listStyle
211
            )
212
        );
213
    }
214
215
    /**
216
     * Invokes the concept page of a single concept in a specific vocabulary.
217
     *
218
     * @param Request $request
219
     */
220
    public function invokeVocabularyConcept(Request $request)
221
    {
222
        $lang = $request->getLang();
0 ignored issues
show
Unused Code introduced by
The assignment to $lang is dead and can be removed.
Loading history...
223
        $this->model->setLocale($request->getLang());
224
        $vocab = $request->getVocab();
225
226
        $langcodes = $vocab->getConfig()->getShowLangCodes();
227
        $uri = $vocab->getConceptURI($request->getUri()); // make sure it's a full URI
228
229
        $concept = $vocab->getConceptInfo($uri, $request->getContentLang());
230
        if (empty($concept)) {
231
            $this->invokeGenericErrorPage($request);
232
            return;
233
        }
234
        if ($this->notModified($concept)) {
235
            return;
236
        }
237
        $customLabels = $vocab->getConfig()->getPropertyLabelOverrides();
238
239
        $pluginParameters = json_encode($vocab->getConfig()->getPluginParameters());
240
        $template = $this->twig->load('concept.twig');
241
242
        $crumbs = $vocab->getBreadCrumbs($request->getContentLang(), $uri);
243
244
        $vocabTypes = $this->model->getTypes($request->getVocabid(), $request->getLang());
245
246
        echo $template->render(
247
            array(
248
            'concept' => $concept,
249
            'vocab' => $vocab,
250
            'concept_uri' => $uri,
251
            'languages' => $this->languages,
252
            'content_languages' => $this->parseVocabularyLanguageOrder($vocab),
253
            'explicit_langcodes' => $langcodes,
254
            'visible_breadcrumbs' => $crumbs['breadcrumbs'],
255
            'hidden_breadcrumbs' => $crumbs['combined'],
256
            'request' => $request,
257
            'types' => $vocabTypes,
258
            'plugin_params' => $pluginParameters,
259
            'custom_labels' => $customLabels)
260
        );
261
    }
262
263
    /**
264
     * Invokes the feedback page with information of the users current vocabulary.
265
     */
266
    public function invokeFeedbackForm($request)
267
    {
268
        $template = $this->twig->load('feedback.twig');
269
        $this->model->setLocale($request->getLang());
270
        $vocabList = $this->model->getVocabularyList(false);
271
        $vocab = $request->getVocab();
272
273
        $feedbackSent = false;
274
        if ($request->getQueryParamPOST('message')) {
275
            $feedbackSent = true;
276
            $feedbackMsg = $request->getQueryParamPOST('message');
277
            $feedbackName = $request->getQueryParamPOST('name');
278
            $feedbackEmail = $request->getQueryParamPOST('email');
279
            $msgSubject = $request->getQueryParamPOST('msgsubject');
280
            $feedbackVocab = $request->getQueryParamPOST('vocab');
281
            $feedbackVocabEmail = ($feedbackVocab !== null && $feedbackVocab !== '') ?
282
                $this->model->getVocabulary($feedbackVocab)->getConfig()->getFeedbackRecipient() : null;
283
            // if the hidden field has been set a value we have found a spam bot
284
            // and we do not actually send the message.
285
            if ($this->honeypot->validateHoneypot($request->getQueryParamPOST('item-description')) &&
286
                $this->honeypot->validateHoneytime($request->getQueryParamPOST('user-captcha'), $this->model->getConfig()->getHoneypotTime())) {
287
                $this->sendFeedback($request, $feedbackMsg, $msgSubject, $feedbackName, $feedbackEmail, $feedbackVocab, $feedbackVocabEmail);
288
            }
289
        }
290
        echo $template->render(
291
            array(
292
                'languages' => $this->languages,
293
                'vocab' => $vocab,
294
                'vocabList' => $vocabList,
295
                'feedback_sent' => $feedbackSent,
296
                'request' => $request,
297
            )
298
        );
299
    }
300
301
    private function createFeedbackHeaders($fromName, $fromEmail, $toMail, $sender)
302
    {
303
        $headers = "MIME-Version: 1.0" . "\r\n";
304
        $headers .= "Content-type: text/html; charset=UTF-8" . "\r\n";
305
        if (!empty($toMail)) {
306
            $headers .= "Cc: " . $this->model->getConfig()->getFeedbackAddress() . "\r\n";
307
        }
308
        if (!empty($fromEmail)) {
309
            $headers .= "Reply-To: $fromName <$fromEmail>\r\n";
310
        }
311
        $service = $this->model->getConfig()->getServiceName();
312
        return $headers . "From: $fromName via $service <$sender>";
313
    }
314
315
    /**
316
     * Sends the user entered message through the php's mailer.
317
     * @param string $message content given by user.
318
     * @param string $messageSubject subject line given by user.
319
     * @param string $fromName senders own name.
320
     * @param string $fromEmail senders email address.
321
     * @param string $fromVocab which vocabulary is the feedback related to.
322
     */
323
    public function sendFeedback($request, $message, $messageSubject, $fromName = null, $fromEmail = null, $fromVocab = null, $toMail = null)
324
    {
325
        $toAddress = ($toMail) ? $toMail : $this->model->getConfig()->getFeedbackAddress();
326
        $messageSubject = "[" . $this->model->getConfig()->getServiceName() . "] " . $messageSubject;
327
        if ($fromVocab !== null && $fromVocab !== '') {
328
            $message = 'Feedback from vocab: ' . strtoupper($fromVocab) . "<br />" . $message;
329
        }
330
        $envelopeSender = $this->model->getConfig()->getFeedbackEnvelopeSender();
331
        // determine the sender address of the message
332
        $sender = $this->model->getConfig()->getFeedbackSender();
333
        if (empty($sender)) {
334
            $sender = $envelopeSender;
335
        }
336
        if (empty($sender)) {
337
            $sender = $this->model->getConfig()->getFeedbackAddress();
338
        }
339
340
        // determine sender name - default to "anonymous user" if not given by user
341
        if (empty($fromName)) {
342
            $fromName = "anonymous user";
343
        }
344
        $headers = $this->createFeedbackHeaders($fromName, $fromEmail, $toMail, $sender);
345
        $params = empty($envelopeSender) ? '' : "-f $envelopeSender";
346
        // adding some information about the user for debugging purposes.
347
        $message = $message . "<br /><br /> Debugging information:"
348
            . "<br />Timestamp: " . date(DATE_RFC2822)
349
            . "<br />User agent: " . $request->getServerConstant('HTTP_USER_AGENT')
350
            . "<br />Referer: " . $request->getServerConstant('HTTP_REFERER');
351
352
        try {
353
            mail($toAddress, $messageSubject, $message, $headers, $params);
354
        } catch (Exception $e) {
355
            header("HTTP/1.0 404 Not Found");
356
            $template = $this->twig->load('error.twig');
357
            if ($this->model->getConfig()->getLogCaughtExceptions()) {
358
                error_log('Caught exception: ' . $e->getMessage());
359
            }
360
361
            echo $template->render(
362
                array(
363
                    'languages' => $this->languages,
364
                )
365
            );
366
367
            return;
368
        }
369
    }
370
371
    /**
372
     * Invokes the about page for the Skosmos service.
373
     */
374
    public function invokeAboutPage($request)
375
    {
376
        $template = $this->twig->load('about.twig');
377
        $this->model->setLocale($request->getLang());
378
        $url = $request->getServerConstant('HTTP_HOST');
379
380
        echo $template->render(
381
            array(
382
                'languages' => $this->languages,
383
                'server_instance' => $url,
384
                'request' => $request,
385
            )
386
        );
387
    }
388
389
    /**
390
     * Invokes the search for concepts in all the available ontologies.
391
     */
392
    public function invokeGlobalSearch($request)
393
    {
394
        $lang = $request->getLang();
395
        $template = $this->twig->load('global-search.twig');
396
        $this->model->setLocale($request->getLang());
397
398
        $parameters = new ConceptSearchParameters($request, $this->model->getConfig());
399
400
        $vocabs = $request->getQueryParam('vocabs'); # optional
401
        // convert to vocids array to support multi-vocabulary search
402
        $vocids = ($vocabs !== null && $vocabs !== '') ? explode(' ', $vocabs) : null;
403
        $vocabObjects = array();
404
        if ($vocids) {
405
            foreach ($vocids as $vocid) {
406
                try {
407
                    $vocabObjects[] = $this->model->getVocabulary($vocid);
408
                } catch (ValueError $e) {
409
                    // fail fast with an error page if the vocabulary cannot be found
410
                    if ($this->model->getConfig()->getLogCaughtExceptions()) {
411
                        error_log('Caught exception: ' . $e->getMessage());
412
                    }
413
                    header("HTTP/1.0 400 Bad Request");
414
                    $this->invokeGenericErrorPage($request, $e->getMessage());
415
                    return;
416
                }
417
            }
418
        }
419
        $parameters->setVocabularies($vocabObjects);
420
421
        $counts = null;
422
        $searchResults = null;
423
        $errored = false;
424
425
        try {
426
            $countAndResults = $this->model->searchConceptsAndInfo($parameters);
427
            $counts = $countAndResults['count'];
428
            $searchResults = $countAndResults['results'];
429
            $vocabTypes = $this->model->getTypes(null, $request->getLang());
430
        } catch (Exception $e) {
431
            $errored = true;
432
            header("HTTP/1.0 500 Internal Server Error");
433
            if ($this->model->getConfig()->getLogCaughtExceptions()) {
434
                error_log('Caught exception: ' . $e->getMessage());
435
            }
436
        }
437
        $vocabList = $this->model->getVocabularyList();
438
        $sortedVocabs = $this->model->getVocabularyList(false, true);
439
        $langList = $this->model->getLanguages($lang);
440
        $contentLanguages = array_flip($this->model->getLanguages($request->getLang()));
441
442
        echo $template->render(
443
            array(
444
                'search_count' => $counts,
445
                'languages' => $this->languages,
446
                'content_languages' => $contentLanguages,
447
                'search_results' => $searchResults,
448
                'rest' => $parameters->getOffset() > 0,
449
                'global_search' => true,
450
                'search_failed' => $errored,
451
                'term' => $request->getQueryParamRaw('q'),
452
                'lang_list' => $langList,
453
                'vocabs' => isset($vocabs) ? str_replace(' ', '+', $vocabs) : null,
454
                'vocab_list' => $vocabList,
455
                'sorted_vocabs' => $sortedVocabs,
456
                'request' => $request,
457
                'types' => $vocabTypes,
458
                'parameters' => $parameters
459
            )
460
        );
461
    }
462
463
    /**
464
     * Invokes the search for a single vocabs concepts.
465
     */
466
    public function invokeVocabularySearch($request)
467
    {
468
        $template = $this->twig->load('vocab-search.twig');
469
        $this->model->setLocale($request->getLang());
470
        $vocab = $request->getVocab();
471
        $searchResults = null;
472
        try {
473
            $vocabTypes = $this->model->getTypes($request->getVocabid(), $request->getLang());
474
        } catch (Exception $e) {
475
            header("HTTP/1.0 500 Internal Server Error");
476
            if ($this->model->getConfig()->getLogCaughtExceptions()) {
477
                error_log('Caught exception: ' . $e->getMessage());
478
            }
479
480
            echo $template->render(
481
                array(
482
                    'languages' => $this->languages,
483
                    'vocab' => $vocab,
484
                    'request' => $request,
485
                    'content_languages' => $this->parseVocabularyLanguageOrder($vocab),
486
                    'search_results' => $searchResults
487
                )
488
            );
489
490
            return;
491
        }
492
493
        $langcodes = $vocab->getConfig()->getShowLangCodes();
494
        $parameters = new ConceptSearchParameters($request, $this->model->getConfig());
495
496
        try {
497
            $countAndResults = $this->model->searchConceptsAndInfo($parameters);
498
            $counts = $countAndResults['count'];
499
            $searchResults = $countAndResults['results'];
500
        } catch (Exception $e) {
501
            header("HTTP/1.0 404 Not Found");
502
            if ($this->model->getConfig()->getLogCaughtExceptions()) {
503
                error_log('Caught exception: ' . $e->getMessage());
504
            }
505
506
            echo $template->render(
507
                array(
508
                    'languages' => $this->languages,
509
                    'vocab' => $vocab,
510
                    'term' => $request->getQueryParam('q'),
511
                    'search_results' => $searchResults
512
                )
513
            );
514
            return;
515
        }
516
        echo $template->render(
517
            array(
518
                'languages' => $this->languages,
519
                'content_languages' => $this->parseVocabularyLanguageOrder($vocab),
520
                'vocab' => $vocab,
521
                'search_results' => $searchResults,
522
                'search_count' => $counts,
523
                'rest' => $parameters->getOffset() > 0,
524
                'limit_parent' => $parameters->getParentLimit(),
525
                'limit_type' =>  $request->getQueryParam('type') ? explode('+', $request->getQueryParam('type')) : null,
526
                'limit_group' => $parameters->getGroupLimit(),
527
                'limit_scheme' =>  $request->getQueryParam('scheme') ? explode('+', $request->getQueryParam('scheme')) : null,
528
                'group_index' => $vocab->listConceptGroups($request->getContentLang()),
529
                'parameters' => $parameters,
530
                'term' => $request->getQueryParamRaw('q'),
531
                'types' => $vocabTypes,
532
                'explicit_langcodes' => $langcodes,
533
                'request' => $request,
534
            )
535
        );
536
    }
537
538
    /**
539
     * Loads and renders the view containing a specific vocabulary.
540
     */
541
    public function invokeVocabularyHome($request)
542
    {
543
        $lang = $request->getLang();
0 ignored issues
show
Unused Code introduced by
The assignment to $lang is dead and can be removed.
Loading history...
544
        $this->model->setLocale($request->getLang());
545
        $vocab = $request->getVocab();
546
547
        $defaultView = $vocab->getConfig()->getDefaultSidebarView();
548
549
        $pluginParameters = json_encode($vocab->getConfig()->getPluginParameters());
550
551
        $template = $this->twig->load('vocab-home.twig');
552
553
        $vocabTypes = $this->model->getTypes($request->getVocabid(), $request->getLang());
554
555
        echo $template->render(
556
            array(
557
                'languages' => $this->languages,
558
                'vocab' => $vocab,
559
                'content_languages' => $this->parseVocabularyLanguageOrder($vocab),
560
                'search_letter' => 'A',
561
                'active_tab' => $defaultView,
562
                'request' => $request,
563
                'types' => $vocabTypes,
564
                'plugin_params' => $pluginParameters
565
            )
566
        );
567
    }
568
569
    /**
570
     * Invokes a very generic errorpage.
571
     */
572
    public function invokeGenericErrorPage($request, $message = null)
573
    {
574
        $this->model->setLocale($request->getLang());
575
        header("HTTP/1.0 404 Not Found");
576
        $template = $this->twig->load('error.twig');
577
        echo $template->render(
578
            array(
579
                'languages' => $this->languages,
580
                'request' => $request,
581
                'vocab' => $request->getVocab(),
582
                'message' => $message,
583
                'requested_page' => filter_input(INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
0 ignored issues
show
Bug introduced by
The constant FILTER_SANITIZE_FULL_SPECIAL_CHARS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
584
            )
585
        );
586
    }
587
}
588