Lines
100.00%
54 / 54
Methods
100.00%
11 / 11
Classes
100.00%
1 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| 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% | 16 / 16 | 100.00% | 1 / 1 | 1 | ||
| 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 | * The input length limit is lifted so large FAQ answers stay intact when saving. | |
| 199 | */ | |
| 200 | private static function sanitizerConfig(): HtmlSanitizerConfig | |
| 201 | { | |
| 202 | return new HtmlSanitizerConfig() | |
| 203 | ->withMaxInputLength(-1) | |
| 204 | ->allowSafeElements() | |
| 205 | ->allowRelativeLinks() | |
| 206 | ->allowRelativeMedias() | |
| 207 | ->allowAttribute('class', allowedElements: '*') | |
| 208 | ->allowAttribute('id', allowedElements: '*') | |
| 209 | ->allowAttribute('dir', allowedElements: '*') | |
| 210 | ->allowAttribute('name', allowedElements: '*') | |
| 211 | ->allowAttribute('target', allowedElements: 'a') | |
| 212 | ->allowAttribute('controls', allowedElements: ['audio', 'video']) | |
| 213 | ->blockElement('form') | |
| 214 | ->blockElement('input') | |
| 215 | ->blockElement('textarea') | |
| 216 | ->blockElement('select') | |
| 217 | ->blockElement('button'); | |
| 218 | } | |
| 219 | } |