| @@ -32,13 +32,13 @@ | ||
| 32 | 32 | * Splits an HTTP header by one or more separators. |
| 33 | 33 | * |
| 34 | 34 | * Example: |
| 35 | 35 | * |
| 36 | - * HeaderUtils::split("da, en-gb;q=0.8", ",;") | |
| 36 | + * HeaderUtils::split('da, en-gb;q=0.8', ',;') | |
| 37 | 37 | * // => ['da'], ['en-gb', 'q=0.8']] |
| 38 | 38 | * |
| 39 | 39 | * @param string $separators List of characters to split on, ordered by |
| 40 | - * precedence, e.g. ",", ";=", or ",;=" | |
| 40 | + * precedence, e.g. ',', ';=', or ',;=' | |
| 41 | 41 | * |
| 42 | 42 | * @return array Nested array with as many levels as there are characters in |
| 43 | 43 | * $separators |
| 44 | 44 | */ |
| @@ -43,8 +43,12 @@ | ||
| 43 | 43 | * $separators |
| 44 | 44 | */ |
| 45 | 45 | public static function split(string $header, string $separators): array |
| 46 | 46 | { |
| 47 | + if ('' === $separators) { | |
| 48 | + throw new \InvalidArgumentException('At least one separator must be specified.'); | |
| 49 | + } | |
| 50 | + | |
| 47 | 51 | $quotedSeparators = preg_quote($separators, '/'); |
| 48 | 52 | |
| 49 | 53 | preg_match_all(' |
| 50 | 54 | / |
| @@ -76,10 +80,10 @@ | ||
| 76 | 80 | * element. Array keys are lowercased. |
| 77 | 81 | * |
| 78 | 82 | * Example: |
| 79 | 83 | * |
| 80 | - * HeaderUtils::combine([["foo", "abc"], ["bar"]]) | |
| 81 | - * // => ["foo" => "abc", "bar" => true] | |
| 84 | + * HeaderUtils::combine([['foo', 'abc'], ['bar']]) | |
| 85 | + * // => ['foo' => 'abc', 'bar' => true] | |
| 82 | 86 | */ |
| 83 | 87 | public static function combine(array $parts): array |
| 84 | 88 | { |
| 85 | 89 | $assoc = []; |
| @@ -94,15 +98,15 @@ | ||
| 94 | 98 | |
| 95 | 99 | /** |
| 96 | 100 | * Joins an associative array into a string for use in an HTTP header. |
| 97 | 101 | * |
| 98 | - * The key and value of each entry are joined with "=", and all entries | |
| 102 | + * The key and value of each entry are joined with '=', and all entries | |
| 99 | 103 | * are joined with the specified separator and an additional space (for |
| 100 | 104 | * readability). Values are quoted if necessary. |
| 101 | 105 | * |
| 102 | 106 | * Example: |
| 103 | 107 | * |
| 104 | - * HeaderUtils::toString(["foo" => "abc", "bar" => true, "baz" => "a b c"], ",") | |
| 108 | + * HeaderUtils::toString(['foo' => 'abc', 'bar' => true, 'baz' => 'a b c'], ',') | |
| 105 | 109 | * // => 'foo=abc, bar, baz="a b c"' |
| 106 | 110 | */ |
| 107 | 111 | public static function toString(array $assoc, string $separator): string |
| 108 | 112 | { |
| @@ -137,9 +141,9 @@ | ||
| 137 | 141 | /** |
| 138 | 142 | * Decodes a quoted string. |
| 139 | 143 | * |
| 140 | 144 | * If passed an unquoted string that matches the "token" construct (as |
| 141 | - * defined in the HTTP specification), it is passed through verbatimly. | |
| 145 | + * defined in the HTTP specification), it is passed through verbatim. | |
| 142 | 146 | */ |
| 143 | 147 | public static function unquote(string $s): string |
| 144 | 148 | { |
| 145 | 149 | return preg_replace('/\\\\(.)|"/', '$1', $s); |
| @@ -153,10 +157,8 @@ | ||
| 153 | 157 | * @param string $filenameFallback A string containing only ASCII characters that |
| 154 | 158 | * is semantically equivalent to $filename. If the filename is already ASCII, |
| 155 | 159 | * it can be omitted, or just copied from $filename |
| 156 | 160 | * |
| 157 | - * @return string A string suitable for use as a Content-Disposition field-value | |
| 158 | - * | |
| 159 | 161 | * @throws \InvalidArgumentException |
| 160 | 162 | * |
| 161 | 163 | * @see RFC 6266 |
| 162 | 164 | */ |
| @@ -192,44 +194,103 @@ | ||
| 192 | 194 | |
| 193 | 195 | return $disposition.'; '.self::toString($params, ';'); |
| 194 | 196 | } |
| 195 | 197 | |
| 198 | + /** | |
| 199 | + * Like parse_str(), but preserves dots in variable names. | |
| 200 | + */ | |
| 201 | + public static function parseQuery(string $query, bool $ignoreBrackets = false, string $separator = '&'): array | |
| 202 | + { | |
| 203 | + $q = []; | |
| 204 | + | |
| 205 | + foreach (explode($separator, $query) as $v) { | |
| 206 | + if (false !== $i = strpos($v, "\0")) { | |
| 207 | + $v = substr($v, 0, $i); | |
| 208 | + } | |
| 209 | + | |
| 210 | + if (false === $i = strpos($v, '=')) { | |
| 211 | + $k = urldecode($v); | |
| 212 | + $v = ''; | |
| 213 | + } else { | |
| 214 | + $k = urldecode(substr($v, 0, $i)); | |
| 215 | + $v = substr($v, $i); | |
| 216 | + } | |
| 217 | + | |
| 218 | + if (false !== $i = strpos($k, "\0")) { | |
| 219 | + $k = substr($k, 0, $i); | |
| 220 | + } | |
| 221 | + | |
| 222 | + $k = ltrim($k, ' '); | |
| 223 | + | |
| 224 | + if ($ignoreBrackets) { | |
| 225 | + $q[$k][] = urldecode(substr($v, 1)); | |
| 226 | + | |
| 227 | + continue; | |
| 228 | + } | |
| 229 | + | |
| 230 | + if (false === $i = strpos($k, '[')) { | |
| 231 | + $q[] = bin2hex($k).$v; | |
| 232 | + } else { | |
| 233 | + $q[] = bin2hex(substr($k, 0, $i)).rawurlencode(substr($k, $i)).$v; | |
| 234 | + } | |
| 235 | + } | |
| 236 | + | |
| 237 | + if ($ignoreBrackets) { | |
| 238 | + return $q; | |
| 239 | + } | |
| 240 | + | |
| 241 | + parse_str(implode('&', $q), $q); | |
| 242 | + | |
| 243 | + $query = []; | |
| 244 | + | |
| 245 | + foreach ($q as $k => $v) { | |
| 246 | + if (false !== $i = strpos($k, '_')) { | |
| 247 | + $query[substr_replace($k, hex2bin(substr($k, 0, $i)).'[', 0, 1 + $i)] = $v; | |
| 248 | + } else { | |
| 249 | + $query[hex2bin($k)] = $v; | |
| 250 | + } | |
| 251 | + } | |
| 252 | + | |
| 253 | + return $query; | |
| 254 | + } | |
| 255 | + | |
| 196 | 256 | private static function groupParts(array $matches, string $separators, bool $first = true): array |
| 197 | 257 | { |
| 198 | 258 | $separator = $separators[0]; |
| 199 | - $partSeparators = substr($separators, 1); | |
| 259 | + $separators = substr($separators, 1) ?: ''; | |
| 260 | + $i = 0; | |
| 200 | 261 | |
| 201 | - $i = 0; | |
| 262 | + if ('' === $separators && !$first) { | |
| 263 | + $parts = ['']; | |
| 264 | + | |
| 265 | + foreach ($matches as $match) { | |
| 266 | + if (!$i && isset($match['separator'])) { | |
| 267 | + $i = 1; | |
| 268 | + $parts[1] = ''; | |
| 269 | + } else { | |
| 270 | + $parts[$i] .= self::unquote($match[0]); | |
| 271 | + } | |
| 272 | + } | |
| 273 | + | |
| 274 | + return $parts; | |
| 275 | + } | |
| 276 | + | |
| 277 | + $parts = []; | |
| 202 | 278 | $partMatches = []; |
| 203 | - $previousMatchWasSeparator = false; | |
| 279 | + | |
| 204 | 280 | foreach ($matches as $match) { |
| 205 | - if (!$first && $previousMatchWasSeparator && isset($match['separator']) && $match['separator'] === $separator) { | |
| 206 | - $previousMatchWasSeparator = true; | |
| 207 | - $partMatches[$i][] = $match; | |
| 208 | - } elseif (isset($match['separator']) && $match['separator'] === $separator) { | |
| 209 | - $previousMatchWasSeparator = true; | |
| 281 | + if (($match['separator'] ?? null) === $separator) { | |
| 210 | 282 | ++$i; |
| 211 | 283 | } else { |
| 212 | - $previousMatchWasSeparator = false; | |
| 213 | 284 | $partMatches[$i][] = $match; |
| 214 | 285 | } |
| 215 | 286 | } |
| 216 | 287 | |
| 217 | - $parts = []; | |
| 218 | - if ($partSeparators) { | |
| 219 | - foreach ($partMatches as $matches) { | |
| 220 | - $parts[] = self::groupParts($matches, $partSeparators, false); | |
| 221 | - } | |
| 222 | - } else { | |
| 223 | - foreach ($partMatches as $matches) { | |
| 224 | - $parts[] = self::unquote($matches[0][0]); | |
| 225 | - } | |
| 226 | - | |
| 227 | - if (!$first && 2 < \count($parts)) { | |
| 228 | - $parts = [ | |
| 229 | - $parts[0], | |
| 230 | - implode($separator, \array_slice($parts, 1)), | |
| 231 | - ]; | |
| 288 | + foreach ($partMatches as $matches) { | |
| 289 | + if ('' === $separators && '' !== $unquoted = self::unquote($matches[0][0])) { | |
| 290 | + $parts[] = $unquoted; | |
| 291 | + } elseif ($groupedParts = self::groupParts($matches, $separators, false)) { | |
| 292 | + $parts[] = $groupedParts; | |
| 232 | 293 | } |
| 233 | 294 | } |
| 234 | 295 | |
| 235 | 296 | return $parts; |