Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
53 / 53 |
|
100.00% |
11 / 11 |
CRAP | |
100.00% |
1 / 1 |
| Filter | |
100.00% |
53 / 53 |
|
100.00% |
11 / 11 |
24 | |
100.00% |
1 / 1 |
| filterInput | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
| filterInputArray | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| filterVar | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
| filterEmail | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
| filterArray | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getFilteredQueryString | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
| sanitizeQueryValue | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| filterSanitizeString | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| filterHtml | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| removeAttributes | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| sanitizerConfig | |
100.00% |
15 / 15 |
|
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 | |
| 18 | declare(strict_types=1); |
| 19 | |
| 20 | namespace phpMyFAQ; |
| 21 | |
| 22 | use Symfony\Component\HtmlSanitizer\HtmlSanitizer; |
| 23 | use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; |
| 24 | use Symfony\Component\HttpFoundation\Request; |
| 25 | |
| 26 | /** |
| 27 | * Class Filter |
| 28 | * |
| 29 | * @package phpMyFAQ |
| 30 | */ |
| 31 | class 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(["'", '"'], [''', '"'], $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: ' ', 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: ' ', 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 | } |