Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
11 / 11
CRAP
100.00% covered (success)
100.00%
1 / 1
Filter
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
11 / 11
24
100.00% covered (success)
100.00%
1 / 1
 filterInput
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 filterInputArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 filterVar
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 filterEmail
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 filterArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFilteredQueryString
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 sanitizeQueryValue
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 filterSanitizeString
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 filterHtml
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 removeAttributes
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 sanitizerConfig
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * ext/filter wrapper class.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2009-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2009-01-28
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ;
21
22use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
23use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
24use Symfony\Component\HttpFoundation\Request;
25
26/**
27 * Class Filter
28 *
29 * @package phpMyFAQ
30 */
31class Filter
32{
33    /**
34     * Static wrapper method for filter_input().
35     *
36     * @param int        $type Filter type
37     * @param string     $variableName Variable name
38     * @param int        $filter Filter
39     * @param mixed|null $default Default value
40     */
41    public static function filterInput(int $type, string $variableName, int $filter, mixed $default = null): mixed
42    {
43        $return = $filter === FILTER_SANITIZE_SPECIAL_CHARS ? filter_input($type, $variableName, FILTER_CALLBACK, [
44                'options' => new Filter()->filterSanitizeString(...),
45            ]) : filter_input($type, $variableName, $filter);
46
47        return is_null($return) || $return === false ? $default : $return;
48    }
49
50    /**
51     * Static wrapper method for filter_input_array.
52     *
53     * @param int   $type Filter type
54     * @param array $definition Definition
55     */
56    public static function filterInputArray(int $type, array $definition): array|bool|null
57    {
58        return filter_input_array($type, $definition);
59    }
60
61    /**
62     * Static wrapper method for filter_var().
63     *
64     * The conditional return type narrows the result by filter so callers stop
65     * receiving `mixed`. Literal filter values are used because mago currently
66     * resolves named constants in conditional types as class names; the values
67     * map to:
68     *   515 = FILTER_SANITIZE_SPECIAL_CHARS, 257 = FILTER_VALIDATE_INT,
69     *   258 = FILTER_VALIDATE_BOOLEAN.
70     * On failure the method returns `$default`, so each branch is unioned with
71     * the `TDefault` template (which resolves to `null` when no default is given,
72     * or e.g. `string`/`array` when a typed default is passed).
73     *
74     * Interim workaround: mago already infers native `filter_var()` return types
75     * from validation flags, but not through this wrapper (the `$filter` argument
76     * is a runtime variable here). Migrate hot call sites to native `filter_var()`
77     * once that inference is richer. See https://github.com/carthage-software/mago/issues/1117
78     *
79     * @template TDefault
80     *
81     * @param mixed    $variable Variable
82     * @param int      $filter Filter
83     * @param TDefault $default Default value
84     *
85     * @return ($filter is 515 ? string|TDefault : ($filter is 257 ? int|TDefault : ($filter is 258 ? bool|TDefault : mixed)))
86     */
87    public static function filterVar(mixed $variable, int $filter, mixed $default = null): mixed
88    {
89        $return = $filter === FILTER_SANITIZE_SPECIAL_CHARS
90            ? filter_var($variable, FILTER_CALLBACK, ['options' => new Filter()->filterSanitizeString(...)])
91            : filter_var($variable, $filter);
92
93        return $return === false || $return === null ? $default : $return;
94    }
95
96    /**
97     * Validates an email address and sanitizes it for safe output.
98     */
99    public static function filterEmail(mixed $variable, mixed $default = null): mixed
100    {
101        $validated = self::filterVar($variable, FILTER_VALIDATE_EMAIL, $default);
102        if ($validated !== null && $validated !== false && $validated !== $default) {
103            return self::filterVar($validated, FILTER_SANITIZE_SPECIAL_CHARS);
104        }
105
106        return $validated;
107    }
108
109    /**
110     * Static wrapper method for filter_var_array().
111     */
112    public static function filterArray(array $array, array|int $options = FILTER_UNSAFE_RAW): bool|array|null
113    {
114        return filter_var_array($array, $options);
115    }
116
117    /**
118     * Filters a query string.
119     */
120    public static function getFilteredQueryString(): string
121    {
122        $urlData = [];
123        $cleanUrlData = [];
124
125        $request = Request::createFromGlobals();
126        $queryString = $request->getQueryString();
127
128        if ($queryString === null) {
129            return '';
130        }
131
132        parse_str($queryString, $urlData);
133
134        foreach ($urlData as $key => $urlPart) {
135            $cleanKey = strip_tags($key);
136            $cleanUrlData[$cleanKey] = self::sanitizeQueryValue($urlPart);
137        }
138
139        return http_build_query($cleanUrlData, arg_separator: '&', encoding_type: PHP_QUERY_RFC3986);
140    }
141
142    /**
143     * Recursively sanitizes query string values by stripping tags.
144     */
145    private static function sanitizeQueryValue(mixed $value): string|array
146    {
147        if (is_array($value)) {
148            return array_map(self::sanitizeQueryValue(...), $value);
149        }
150
151        return strip_tags((string) $value);
152    }
153
154    /**
155     * This method is a polyfill for FILTER_SANITIZE_STRING, deprecated since PHP 8.1.
156     */
157    public function filterSanitizeString(string $string): string
158    {
159        $string = str_replace("\x00", replace: '', subject: $string);
160        $string = strip_tags($string);
161        return str_replace(["'", '"'], ['&apos;', '&quot;'], $string);
162    }
163
164    /**
165     * Filters a variable containing HTML: removes unsafe elements and attributes via
166     * Symfony's HtmlSanitizer, so safe HTML markup is kept intact. Unlike
167     * removeAttributes(), inline style attributes are preserved.
168     *
169     * @param mixed      $variable Variable
170     * @param mixed|null $default Default value
171     */
172    public static function filterHtml(mixed $variable, mixed $default = null): mixed
173    {
174        if (!is_string($variable)) {
175            return $default ?? '';
176        }
177
178        $config = self::sanitizerConfig()->allowAttribute('style', allowedElements: '*');
179
180        return new HtmlSanitizer($config)->sanitize(str_replace(search: '&#13;', replace: '', subject: $variable));
181    }
182
183    /**
184     * Sanitizes HTML by allowing safe elements and attributes via Symfony's HtmlSanitizer.
185     */
186    public static function removeAttributes(string $html = ''): string
187    {
188        // remove broken stuff
189        $html = str_replace(search: '&#13;', replace: '', subject: $html);
190
191        $sanitizer = new HtmlSanitizer(self::sanitizerConfig());
192
193        return $sanitizer->sanitize($html);
194    }
195
196    /**
197     * Shared HtmlSanitizer configuration: safe elements only, no form controls.
198     */
199    private static function sanitizerConfig(): HtmlSanitizerConfig
200    {
201        return new HtmlSanitizerConfig()
202            ->allowSafeElements()
203            ->allowRelativeLinks()
204            ->allowRelativeMedias()
205            ->allowAttribute('class', allowedElements: '*')
206            ->allowAttribute('id', allowedElements: '*')
207            ->allowAttribute('dir', allowedElements: '*')
208            ->allowAttribute('name', allowedElements: '*')
209            ->allowAttribute('target', allowedElements: 'a')
210            ->allowAttribute('controls', allowedElements: ['audio', 'video'])
211            ->blockElement('form')
212            ->blockElement('input')
213            ->blockElement('textarea')
214            ->blockElement('select')
215            ->blockElement('button');
216    }
217}