| 1 |
<?php |
| 2 |
|
| 3 |
|
| 4 |
if (!defined('ABSPATH')) { |
| 5 |
exit; |
| 6 |
} |
| 7 |
|
| 8 |
/** |
| 9 |
* String, URL, and character-set sanitization service. |
| 10 |
* |
| 11 |
* Responsibilities: |
| 12 |
* - sanitize_text_field_recursive: array/scalar recursive WP sanitize_text_field |
| 13 |
* - escapeForXSS: strip control characters and HTML-significant punctuation |
| 14 |
* - normalizeUrlString: trim + optional rawurldecode + strip invalid UTF-8/control bytes |
| 15 |
* - sanitizeUrlComponent: strip invalid UTF-8/control bytes from a URL component |
| 16 |
* - containsUtf8mb4Characters: detect 4-byte UTF-8 sequences (utf8mb4-only codepoints) |
| 17 |
* |
| 18 |
* Extracted from ABJ_404_Solution_Functions per design-audit-2026-06-02 |
| 19 |
* M201 (Functions.php grab-bag split, parent task i802). This is a |
| 20 |
* sanitization concern distinct from the polymorphic mbstring/preg |
| 21 |
* adapter and from URL percent-encoding (UrlEncoder). |
| 22 |
* |
| 23 |
* Depends on ABJ_404_Solution_MbStringAdapter for sanitizeInvalidUTF8(). |
| 24 |
* Injected via constructor - Sanitizer does not need the rest of the |
| 25 |
* Functions utility surface, so it depends on the smaller adapter |
| 26 |
* interface directly (sibling task i825 extracted the adapter). |
| 27 |
*/ |
| 28 |
class ABJ_404_Solution_Sanitizer { |
| 29 |
|
| 30 |
/** @var ABJ_404_Solution_MbStringAdapter */ |
| 31 |
private $mbAdapter; |
| 32 |
|
| 33 |
/** |
| 34 |
* Accepts either an MbStringAdapter (the focused dependency, preferred |
| 35 |
* for new callers) or the legacy ABJ_404_Solution_Functions kitchen |
| 36 |
* sink (which carries an MbStringAdapter internally). The Functions |
| 37 |
* variant is kept for backward compatibility with test fixtures and |
| 38 |
* older wiring that has not yet migrated. |
| 39 |
* |
| 40 |
* @param ABJ_404_Solution_MbStringAdapter|ABJ_404_Solution_Functions $adapter |
| 41 |
*/ |
| 42 |
public function __construct($adapter) { |
| 43 |
if ($adapter instanceof ABJ_404_Solution_MbStringAdapter) { |
| 44 |
$this->mbAdapter = $adapter; |
| 45 |
} else if ($adapter instanceof ABJ_404_Solution_Functions) { |
| 46 |
$this->mbAdapter = $adapter->getMbStringAdapter(); |
| 47 |
} else { |
| 48 |
throw new InvalidArgumentException( |
| 49 |
'ABJ_404_Solution_Sanitizer requires an MbStringAdapter or Functions instance; got ' |
| 50 |
. (is_object($adapter) ? get_class($adapter) : gettype($adapter)) |
| 51 |
); |
| 52 |
} |
| 53 |
} |
| 54 |
|
| 55 |
/** |
| 56 |
* Recursively applies `sanitize_text_field` to strings in an array or other data structure. |
| 57 |
* @param mixed $data The data to sanitize. If an array, will recursively |
| 58 |
* apply this function to all elements. |
| 59 |
* @return mixed The sanitized data. |
| 60 |
*/ |
| 61 |
public function sanitize_text_field_recursive($data) { |
| 62 |
if (is_array($data)) { |
| 63 |
return array_map([$this, 'sanitize_text_field_recursive'], $data); |
| 64 |
} |
| 65 |
|
| 66 |
return sanitize_text_field(is_string($data) ? $data : (is_scalar($data) ? (string)$data : '')); |
| 67 |
} |
| 68 |
|
| 69 |
/** Escape a string to avoid Cross Site Scripting (XSS) attacks by encoding unsafe HTML characters. |
| 70 |
* @param string|null $value The string to be escaped. |
| 71 |
* @return string The escaped string. |
| 72 |
*/ |
| 73 |
public function escapeForXSS(?string $value): string { |
| 74 |
if ($value === null) { |
| 75 |
return ''; |
| 76 |
} |
| 77 |
// Remove control characters and other unsafe characters |
| 78 |
$value = preg_replace('/[\x00-\x1F\x7F]/u', '', $value) ?? ''; |
| 79 |
// Remove any other characters you consider unsafe |
| 80 |
$value = preg_replace('/[<>"\'`{}()]/u', '', $value) ?? ''; |
| 81 |
|
| 82 |
return $value; |
| 83 |
} |
| 84 |
|
| 85 |
/** |
| 86 |
* Normalize a URL string for storage or matching. |
| 87 |
* - Optionally decode percent-encoded octets |
| 88 |
* - Strip invalid UTF-8/control bytes |
| 89 |
* |
| 90 |
* Accepts `mixed` (matching the original Functions::normalizeUrlString |
| 91 |
* signature it was extracted from) because callers reach for it with |
| 92 |
* raw `$_SERVER`/`$_COOKIE`/option-row values whose static type is |
| 93 |
* `mixed`. Non-string scalars are coerced; null/empty short-circuit |
| 94 |
* to ''. |
| 95 |
* |
| 96 |
* @param mixed $url |
| 97 |
* @param array<string, bool> $options Supported keys: decode (bool) |
| 98 |
* @return string |
| 99 |
*/ |
| 100 |
public function normalizeUrlString($url, array $options = array()) { |
| 101 |
$options = array_merge(array('decode' => true), $options); |
| 102 |
|
| 103 |
if ($url === null || $url === '') { |
| 104 |
return ''; |
| 105 |
} |
| 106 |
|
| 107 |
if (!is_string($url)) { |
| 108 |
$url = is_scalar($url) ? strval($url) : ''; |
| 109 |
} |
| 110 |
|
| 111 |
$url = trim($url); |
| 112 |
if ($options['decode']) { |
| 113 |
$url = rawurldecode($url); |
| 114 |
} |
| 115 |
|
| 116 |
$url = $this->mbAdapter->sanitizeInvalidUTF8($url); |
| 117 |
// Remove remaining control characters (keep whitespace) |
| 118 |
$url = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $url) ?? $url; |
| 119 |
|
| 120 |
return $url; |
| 121 |
} |
| 122 |
|
| 123 |
/** |
| 124 |
* Sanitize URL components without stripping reserved characters. |
| 125 |
* Keeps characters like ()[]{} for matching but removes invalid UTF-8/control bytes. |
| 126 |
* |
| 127 |
* @param mixed $value |
| 128 |
* @return mixed |
| 129 |
*/ |
| 130 |
public function sanitizeUrlComponent($value) { |
| 131 |
if (is_array($value)) { |
| 132 |
return array_map([$this, 'sanitizeUrlComponent'], $value); |
| 133 |
} |
| 134 |
|
| 135 |
if ($value === null || $value === '') { |
| 136 |
return ''; |
| 137 |
} |
| 138 |
|
| 139 |
if (!is_string($value)) { |
| 140 |
$value = is_scalar($value) ? strval($value) : ''; |
| 141 |
} |
| 142 |
|
| 143 |
$value = $this->mbAdapter->sanitizeInvalidUTF8($value); |
| 144 |
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $value); |
| 145 |
|
| 146 |
return $value; |
| 147 |
} |
| 148 |
|
| 149 |
/** |
| 150 |
* Check whether a string contains any UTF-8 4-byte characters (codepoints > U+FFFF). |
| 151 |
* These characters require utf8mb4 storage; they cannot exist in a utf8mb3 or latin1 column. |
| 152 |
* |
| 153 |
* @param string $string |
| 154 |
* @return bool true if the string contains at least one 4-byte UTF-8 character |
| 155 |
*/ |
| 156 |
public function containsUtf8mb4Characters(string $string): bool { |
| 157 |
if ($string === '') { |
| 158 |
return false; |
| 159 |
} |
| 160 |
// 4-byte UTF-8 sequences start with a byte in the range F0-F4 |
| 161 |
// followed by three continuation bytes (80-BF). |
| 162 |
return (bool) preg_match('/[\xF0-\xF4][\x80-\xBF]{3}/', $string); |
| 163 |
} |
| 164 |
} |
| 165 |
|