| 1 |
<?php |
| 2 |
namespace GuzzleHttp\Psr7; |
| 3 |
|
| 4 |
use Psr\Http\Message\MessageInterface; |
| 5 |
use Psr\Http\Message\RequestInterface; |
| 6 |
use Psr\Http\Message\ResponseInterface; |
| 7 |
use Psr\Http\Message\ServerRequestInterface; |
| 8 |
use Psr\Http\Message\StreamInterface; |
| 9 |
use Psr\Http\Message\UriInterface; |
| 10 |
|
| 11 |
/** |
| 12 |
* Returns the string representation of an HTTP message. |
| 13 |
* |
| 14 |
* @param MessageInterface $message Message to convert to a string. |
| 15 |
* |
| 16 |
* @return string |
| 17 |
*/ |
| 18 |
function str(MessageInterface $message) |
| 19 |
{ |
| 20 |
if ($message instanceof RequestInterface) { |
| 21 |
$msg = trim($message->getMethod() . ' ' |
| 22 |
. $message->getRequestTarget()) |
| 23 |
. ' HTTP/' . $message->getProtocolVersion(); |
| 24 |
if (!$message->hasHeader('host')) { |
| 25 |
$msg .= "\r\nHost: " . $message->getUri()->getHost(); |
| 26 |
} |
| 27 |
} elseif ($message instanceof ResponseInterface) { |
| 28 |
$msg = 'HTTP/' . $message->getProtocolVersion() . ' ' |
| 29 |
. $message->getStatusCode() . ' ' |
| 30 |
. $message->getReasonPhrase(); |
| 31 |
} else { |
| 32 |
throw new \InvalidArgumentException('Unknown message type'); |
| 33 |
} |
| 34 |
|
| 35 |
foreach ($message->getHeaders() as $name => $values) { |
| 36 |
$msg .= "\r\n{$name}: " . implode(', ', $values); |
| 37 |
} |
| 38 |
|
| 39 |
return "{$msg}\r\n\r\n" . $message->getBody(); |
| 40 |
} |
| 41 |
|
| 42 |
/** |
| 43 |
* Returns a UriInterface for the given value. |
| 44 |
* |
| 45 |
* This function accepts a string or {@see Psr\Http\Message\UriInterface} and |
| 46 |
* returns a UriInterface for the given value. If the value is already a |
| 47 |
* `UriInterface`, it is returned as-is. |
| 48 |
* |
| 49 |
* @param string|UriInterface $uri |
| 50 |
* |
| 51 |
* @return UriInterface |
| 52 |
* @throws \InvalidArgumentException |
| 53 |
*/ |
| 54 |
function uri_for($uri) |
| 55 |
{ |
| 56 |
if ($uri instanceof UriInterface) { |
| 57 |
return $uri; |
| 58 |
} elseif (is_string($uri)) { |
| 59 |
return new Uri($uri); |
| 60 |
} |
| 61 |
|
| 62 |
throw new \InvalidArgumentException('URI must be a string or UriInterface'); |
| 63 |
} |
| 64 |
|
| 65 |
/** |
| 66 |
* Create a new stream based on the input type. |
| 67 |
* |
| 68 |
* Options is an associative array that can contain the following keys: |
| 69 |
* - metadata: Array of custom metadata. |
| 70 |
* - size: Size of the stream. |
| 71 |
* |
| 72 |
* @param resource|string|null|int|float|bool|StreamInterface|callable|\Iterator $resource Entity body data |
| 73 |
* @param array $options Additional options |
| 74 |
* |
| 75 |
* @return StreamInterface |
| 76 |
* @throws \InvalidArgumentException if the $resource arg is not valid. |
| 77 |
*/ |
| 78 |
function stream_for($resource = '', array $options = []) |
| 79 |
{ |
| 80 |
if (is_scalar($resource)) { |
| 81 |
$stream = fopen('php://temp', 'r+'); |
| 82 |
if ($resource !== '') { |
| 83 |
fwrite($stream, $resource); |
| 84 |
fseek($stream, 0); |
| 85 |
} |
| 86 |
return new Stream($stream, $options); |
| 87 |
} |
| 88 |
|
| 89 |
switch (gettype($resource)) { |
| 90 |
case 'resource': |
| 91 |
return new Stream($resource, $options); |
| 92 |
case 'object': |
| 93 |
if ($resource instanceof StreamInterface) { |
| 94 |
return $resource; |
| 95 |
} elseif ($resource instanceof \Iterator) { |
| 96 |
return new PumpStream(function () use ($resource) { |
| 97 |
if (!$resource->valid()) { |
| 98 |
return false; |
| 99 |
} |
| 100 |
$result = $resource->current(); |
| 101 |
$resource->next(); |
| 102 |
return $result; |
| 103 |
}, $options); |
| 104 |
} elseif (method_exists($resource, '__toString')) { |
| 105 |
return stream_for((string) $resource, $options); |
| 106 |
} |
| 107 |
break; |
| 108 |
case 'NULL': |
| 109 |
return new Stream(fopen('php://temp', 'r+'), $options); |
| 110 |
} |
| 111 |
|
| 112 |
if (is_callable($resource)) { |
| 113 |
return new PumpStream($resource, $options); |
| 114 |
} |
| 115 |
|
| 116 |
throw new \InvalidArgumentException('Invalid resource type: ' . gettype($resource)); |
| 117 |
} |
| 118 |
|
| 119 |
/** |
| 120 |
* Parse an array of header values containing ";" separated data into an |
| 121 |
* array of associative arrays representing the header key value pair |
| 122 |
* data of the header. When a parameter does not contain a value, but just |
| 123 |
* contains a key, this function will inject a key with a '' string value. |
| 124 |
* |
| 125 |
* @param string|array $header Header to parse into components. |
| 126 |
* |
| 127 |
* @return array Returns the parsed header values. |
| 128 |
*/ |
| 129 |
function parse_header($header) |
| 130 |
{ |
| 131 |
static $trimmed = "\"' \n\t\r"; |
| 132 |
$params = $matches = []; |
| 133 |
|
| 134 |
foreach (normalize_header($header) as $val) { |
| 135 |
$part = []; |
| 136 |
foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) as $kvp) { |
| 137 |
if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) { |
| 138 |
$m = $matches[0]; |
| 139 |
if (isset($m[1])) { |
| 140 |
$part[trim($m[0], $trimmed)] = trim($m[1], $trimmed); |
| 141 |
} else { |
| 142 |
$part[] = trim($m[0], $trimmed); |
| 143 |
} |
| 144 |
} |
| 145 |
} |
| 146 |
if ($part) { |
| 147 |
$params[] = $part; |
| 148 |
} |
| 149 |
} |
| 150 |
|
| 151 |
return $params; |
| 152 |
} |
| 153 |
|
| 154 |
/** |
| 155 |
* Converts an array of header values that may contain comma separated |
| 156 |
* headers into an array of headers with no comma separated values. |
| 157 |
* |
| 158 |
* @param string|array $header Header to normalize. |
| 159 |
* |
| 160 |
* @return array Returns the normalized header field values. |
| 161 |
*/ |
| 162 |
function normalize_header($header) |
| 163 |
{ |
| 164 |
if (!is_array($header)) { |
| 165 |
return array_map('trim', explode(',', $header)); |
| 166 |
} |
| 167 |
|
| 168 |
$result = []; |
| 169 |
foreach ($header as $value) { |
| 170 |
foreach ((array) $value as $v) { |
| 171 |
if (strpos($v, ',') === false) { |
| 172 |
$result[] = $v; |
| 173 |
continue; |
| 174 |
} |
| 175 |
foreach (preg_split('/,(?=([^"]*"[^"]*")*[^"]*$)/', $v) as $vv) { |
| 176 |
$result[] = trim($vv); |
| 177 |
} |
| 178 |
} |
| 179 |
} |
| 180 |
|
| 181 |
return $result; |
| 182 |
} |
| 183 |
|
| 184 |
/** |
| 185 |
* Clone and modify a request with the given changes. |
| 186 |
* |
| 187 |
* The changes can be one of: |
| 188 |
* - method: (string) Changes the HTTP method. |
| 189 |
* - set_headers: (array) Sets the given headers. |
| 190 |
* - remove_headers: (array) Remove the given headers. |
| 191 |
* - body: (mixed) Sets the given body. |
| 192 |
* - uri: (UriInterface) Set the URI. |
| 193 |
* - query: (string) Set the query string value of the URI. |
| 194 |
* - version: (string) Set the protocol version. |
| 195 |
* |
| 196 |
* @param RequestInterface $request Request to clone and modify. |
| 197 |
* @param array $changes Changes to apply. |
| 198 |
* |
| 199 |
* @return RequestInterface |
| 200 |
*/ |
| 201 |
function modify_request(RequestInterface $request, array $changes) |
| 202 |
{ |
| 203 |
if (!$changes) { |
| 204 |
return $request; |
| 205 |
} |
| 206 |
|
| 207 |
$headers = $request->getHeaders(); |
| 208 |
|
| 209 |
if (!isset($changes['uri'])) { |
| 210 |
$uri = $request->getUri(); |
| 211 |
} else { |
| 212 |
// Remove the host header if one is on the URI |
| 213 |
if ($host = $changes['uri']->getHost()) { |
| 214 |
$changes['set_headers']['Host'] = $host; |
| 215 |
|
| 216 |
if ($port = $changes['uri']->getPort()) { |
| 217 |
$standardPorts = ['http' => 80, 'https' => 443]; |
| 218 |
$scheme = $changes['uri']->getScheme(); |
| 219 |
if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) { |
| 220 |
$changes['set_headers']['Host'] .= ':'.$port; |
| 221 |
} |
| 222 |
} |
| 223 |
} |
| 224 |
$uri = $changes['uri']; |
| 225 |
} |
| 226 |
|
| 227 |
if (!empty($changes['remove_headers'])) { |
| 228 |
$headers = _caseless_remove($changes['remove_headers'], $headers); |
| 229 |
} |
| 230 |
|
| 231 |
if (!empty($changes['set_headers'])) { |
| 232 |
$headers = _caseless_remove(array_keys($changes['set_headers']), $headers); |
| 233 |
$headers = $changes['set_headers'] + $headers; |
| 234 |
} |
| 235 |
|
| 236 |
if (isset($changes['query'])) { |
| 237 |
$uri = $uri->withQuery($changes['query']); |
| 238 |
} |
| 239 |
|
| 240 |
if ($request instanceof ServerRequestInterface) { |
| 241 |
return (new ServerRequest( |
| 242 |
isset($changes['method']) ? $changes['method'] : $request->getMethod(), |
| 243 |
$uri, |
| 244 |
$headers, |
| 245 |
isset($changes['body']) ? $changes['body'] : $request->getBody(), |
| 246 |
isset($changes['version']) |
| 247 |
? $changes['version'] |
| 248 |
: $request->getProtocolVersion(), |
| 249 |
$request->getServerParams() |
| 250 |
)) |
| 251 |
->withParsedBody($request->getParsedBody()) |
| 252 |
->withQueryParams($request->getQueryParams()) |
| 253 |
->withCookieParams($request->getCookieParams()) |
| 254 |
->withUploadedFiles($request->getUploadedFiles()); |
| 255 |
} |
| 256 |
|
| 257 |
return new Request( |
| 258 |
isset($changes['method']) ? $changes['method'] : $request->getMethod(), |
| 259 |
$uri, |
| 260 |
$headers, |
| 261 |
isset($changes['body']) ? $changes['body'] : $request->getBody(), |
| 262 |
isset($changes['version']) |
| 263 |
? $changes['version'] |
| 264 |
: $request->getProtocolVersion() |
| 265 |
); |
| 266 |
} |
| 267 |
|
| 268 |
/** |
| 269 |
* Attempts to rewind a message body and throws an exception on failure. |
| 270 |
* |
| 271 |
* The body of the message will only be rewound if a call to `tell()` returns a |
| 272 |
* value other than `0`. |
| 273 |
* |
| 274 |
* @param MessageInterface $message Message to rewind |
| 275 |
* |
| 276 |
* @throws \RuntimeException |
| 277 |
*/ |
| 278 |
function rewind_body(MessageInterface $message) |
| 279 |
{ |
| 280 |
$body = $message->getBody(); |
| 281 |
|
| 282 |
if ($body->tell()) { |
| 283 |
$body->rewind(); |
| 284 |
} |
| 285 |
} |
| 286 |
|
| 287 |
/** |
| 288 |
* Safely opens a PHP stream resource using a filename. |
| 289 |
* |
| 290 |
* When fopen fails, PHP normally raises a warning. This function adds an |
| 291 |
* error handler that checks for errors and throws an exception instead. |
| 292 |
* |
| 293 |
* @param string $filename File to open |
| 294 |
* @param string $mode Mode used to open the file |
| 295 |
* |
| 296 |
* @return resource |
| 297 |
* @throws \RuntimeException if the file cannot be opened |
| 298 |
*/ |
| 299 |
function try_fopen($filename, $mode) |
| 300 |
{ |
| 301 |
$ex = null; |
| 302 |
set_error_handler(function () use ($filename, $mode, &$ex) { |
| 303 |
$ex = new \RuntimeException(sprintf( |
| 304 |
'Unable to open %s using mode %s: %s', |
| 305 |
$filename, |
| 306 |
$mode, |
| 307 |
func_get_args()[1] |
| 308 |
)); |
| 309 |
}); |
| 310 |
|
| 311 |
$handle = fopen($filename, $mode); |
| 312 |
restore_error_handler(); |
| 313 |
|
| 314 |
if ($ex) { |
| 315 |
/** @var $ex \RuntimeException */ |
| 316 |
throw $ex; |
| 317 |
} |
| 318 |
|
| 319 |
return $handle; |
| 320 |
} |
| 321 |
|
| 322 |
/** |
| 323 |
* Copy the contents of a stream into a string until the given number of |
| 324 |
* bytes have been read. |
| 325 |
* |
| 326 |
* @param StreamInterface $stream Stream to read |
| 327 |
* @param int $maxLen Maximum number of bytes to read. Pass -1 |
| 328 |
* to read the entire stream. |
| 329 |
* @return string |
| 330 |
* @throws \RuntimeException on error. |
| 331 |
*/ |
| 332 |
function copy_to_string(StreamInterface $stream, $maxLen = -1) |
| 333 |
{ |
| 334 |
$buffer = ''; |
| 335 |
|
| 336 |
if ($maxLen === -1) { |
| 337 |
while (!$stream->eof()) { |
| 338 |
$buf = $stream->read(1048576); |
| 339 |
// Using a loose equality here to match on '' and false. |
| 340 |
if ($buf == null) { |
| 341 |
break; |
| 342 |
} |
| 343 |
$buffer .= $buf; |
| 344 |
} |
| 345 |
return $buffer; |
| 346 |
} |
| 347 |
|
| 348 |
$len = 0; |
| 349 |
while (!$stream->eof() && $len < $maxLen) { |
| 350 |
$buf = $stream->read($maxLen - $len); |
| 351 |
// Using a loose equality here to match on '' and false. |
| 352 |
if ($buf == null) { |
| 353 |
break; |
| 354 |
} |
| 355 |
$buffer .= $buf; |
| 356 |
$len = strlen($buffer); |
| 357 |
} |
| 358 |
|
| 359 |
return $buffer; |
| 360 |
} |
| 361 |
|
| 362 |
/** |
| 363 |
* Copy the contents of a stream into another stream until the given number |
| 364 |
* of bytes have been read. |
| 365 |
* |
| 366 |
* @param StreamInterface $source Stream to read from |
| 367 |
* @param StreamInterface $dest Stream to write to |
| 368 |
* @param int $maxLen Maximum number of bytes to read. Pass -1 |
| 369 |
* to read the entire stream. |
| 370 |
* |
| 371 |
* @throws \RuntimeException on error. |
| 372 |
*/ |
| 373 |
function copy_to_stream( |
| 374 |
StreamInterface $source, |
| 375 |
StreamInterface $dest, |
| 376 |
$maxLen = -1 |
| 377 |
) { |
| 378 |
$bufferSize = 8192; |
| 379 |
|
| 380 |
if ($maxLen === -1) { |
| 381 |
while (!$source->eof()) { |
| 382 |
if (!$dest->write($source->read($bufferSize))) { |
| 383 |
break; |
| 384 |
} |
| 385 |
} |
| 386 |
} else { |
| 387 |
$remaining = $maxLen; |
| 388 |
while ($remaining > 0 && !$source->eof()) { |
| 389 |
$buf = $source->read(min($bufferSize, $remaining)); |
| 390 |
$len = strlen($buf); |
| 391 |
if (!$len) { |
| 392 |
break; |
| 393 |
} |
| 394 |
$remaining -= $len; |
| 395 |
$dest->write($buf); |
| 396 |
} |
| 397 |
} |
| 398 |
} |
| 399 |
|
| 400 |
/** |
| 401 |
* Calculate a hash of a Stream |
| 402 |
* |
| 403 |
* @param StreamInterface $stream Stream to calculate the hash for |
| 404 |
* @param string $algo Hash algorithm (e.g. md5, crc32, etc) |
| 405 |
* @param bool $rawOutput Whether or not to use raw output |
| 406 |
* |
| 407 |
* @return string Returns the hash of the stream |
| 408 |
* @throws \RuntimeException on error. |
| 409 |
*/ |
| 410 |
function hash( |
| 411 |
StreamInterface $stream, |
| 412 |
$algo, |
| 413 |
$rawOutput = false |
| 414 |
) { |
| 415 |
$pos = $stream->tell(); |
| 416 |
|
| 417 |
if ($pos > 0) { |
| 418 |
$stream->rewind(); |
| 419 |
} |
| 420 |
|
| 421 |
$ctx = hash_init($algo); |
| 422 |
while (!$stream->eof()) { |
| 423 |
hash_update($ctx, $stream->read(1048576)); |
| 424 |
} |
| 425 |
|
| 426 |
$out = hash_final($ctx, (bool) $rawOutput); |
| 427 |
$stream->seek($pos); |
| 428 |
|
| 429 |
return $out; |
| 430 |
} |
| 431 |
|
| 432 |
/** |
| 433 |
* Read a line from the stream up to the maximum allowed buffer length |
| 434 |
* |
| 435 |
* @param StreamInterface $stream Stream to read from |
| 436 |
* @param int $maxLength Maximum buffer length |
| 437 |
* |
| 438 |
* @return string |
| 439 |
*/ |
| 440 |
function readline(StreamInterface $stream, $maxLength = null) |
| 441 |
{ |
| 442 |
$buffer = ''; |
| 443 |
$size = 0; |
| 444 |
|
| 445 |
while (!$stream->eof()) { |
| 446 |
// Using a loose equality here to match on '' and false. |
| 447 |
if (null == ($byte = $stream->read(1))) { |
| 448 |
return $buffer; |
| 449 |
} |
| 450 |
$buffer .= $byte; |
| 451 |
// Break when a new line is found or the max length - 1 is reached |
| 452 |
if ($byte === "\n" || ++$size === $maxLength - 1) { |
| 453 |
break; |
| 454 |
} |
| 455 |
} |
| 456 |
|
| 457 |
return $buffer; |
| 458 |
} |
| 459 |
|
| 460 |
/** |
| 461 |
* Parses a request message string into a request object. |
| 462 |
* |
| 463 |
* @param string $message Request message string. |
| 464 |
* |
| 465 |
* @return Request |
| 466 |
*/ |
| 467 |
function parse_request($message) |
| 468 |
{ |
| 469 |
$data = _parse_message($message); |
| 470 |
$matches = []; |
| 471 |
if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) { |
| 472 |
throw new \InvalidArgumentException('Invalid request string'); |
| 473 |
} |
| 474 |
$parts = explode(' ', $data['start-line'], 3); |
| 475 |
$version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1'; |
| 476 |
|
| 477 |
$request = new Request( |
| 478 |
$parts[0], |
| 479 |
$matches[1] === '/' ? _parse_request_uri($parts[1], $data['headers']) : $parts[1], |
| 480 |
$data['headers'], |
| 481 |
$data['body'], |
| 482 |
$version |
| 483 |
); |
| 484 |
|
| 485 |
return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]); |
| 486 |
} |
| 487 |
|
| 488 |
/** |
| 489 |
* Parses a response message string into a response object. |
| 490 |
* |
| 491 |
* @param string $message Response message string. |
| 492 |
* |
| 493 |
* @return Response |
| 494 |
*/ |
| 495 |
function parse_response($message) |
| 496 |
{ |
| 497 |
$data = _parse_message($message); |
| 498 |
// According to https://tools.ietf.org/html/rfc7230#section-3.1.2 the space |
| 499 |
// between status-code and reason-phrase is required. But browsers accept |
| 500 |
// responses without space and reason as well. |
| 501 |
if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) { |
| 502 |
throw new \InvalidArgumentException('Invalid response string: ' . $data['start-line']); |
| 503 |
} |
| 504 |
$parts = explode(' ', $data['start-line'], 3); |
| 505 |
|
| 506 |
return new Response( |
| 507 |
$parts[1], |
| 508 |
$data['headers'], |
| 509 |
$data['body'], |
| 510 |
explode('/', $parts[0])[1], |
| 511 |
isset($parts[2]) ? $parts[2] : null |
| 512 |
); |
| 513 |
} |
| 514 |
|
| 515 |
/** |
| 516 |
* Parse a query string into an associative array. |
| 517 |
* |
| 518 |
* If multiple values are found for the same key, the value of that key |
| 519 |
* value pair will become an array. This function does not parse nested |
| 520 |
* PHP style arrays into an associative array (e.g., foo[a]=1&foo[b]=2 will |
| 521 |
* be parsed into ['foo[a]' => '1', 'foo[b]' => '2']). |
| 522 |
* |
| 523 |
* @param string $str Query string to parse |
| 524 |
* @param int|bool $urlEncoding How the query string is encoded |
| 525 |
* |
| 526 |
* @return array |
| 527 |
*/ |
| 528 |
function parse_query($str, $urlEncoding = true) |
| 529 |
{ |
| 530 |
$result = []; |
| 531 |
|
| 532 |
if ($str === '') { |
| 533 |
return $result; |
| 534 |
} |
| 535 |
|
| 536 |
if ($urlEncoding === true) { |
| 537 |
$decoder = function ($value) { |
| 538 |
return rawurldecode(str_replace('+', ' ', $value)); |
| 539 |
}; |
| 540 |
} elseif ($urlEncoding === PHP_QUERY_RFC3986) { |
| 541 |
$decoder = 'rawurldecode'; |
| 542 |
} elseif ($urlEncoding === PHP_QUERY_RFC1738) { |
| 543 |
$decoder = 'urldecode'; |
| 544 |
} else { |
| 545 |
$decoder = function ($str) { return $str; }; |
| 546 |
} |
| 547 |
|
| 548 |
foreach (explode('&', $str) as $kvp) { |
| 549 |
$parts = explode('=', $kvp, 2); |
| 550 |
$key = $decoder($parts[0]); |
| 551 |
$value = isset($parts[1]) ? $decoder($parts[1]) : null; |
| 552 |
if (!isset($result[$key])) { |
| 553 |
$result[$key] = $value; |
| 554 |
} else { |
| 555 |
if (!is_array($result[$key])) { |
| 556 |
$result[$key] = [$result[$key]]; |
| 557 |
} |
| 558 |
$result[$key][] = $value; |
| 559 |
} |
| 560 |
} |
| 561 |
|
| 562 |
return $result; |
| 563 |
} |
| 564 |
|
| 565 |
/** |
| 566 |
* Build a query string from an array of key value pairs. |
| 567 |
* |
| 568 |
* This function can use the return value of parse_query() to build a query |
| 569 |
* string. This function does not modify the provided keys when an array is |
| 570 |
* encountered (like http_build_query would). |
| 571 |
* |
| 572 |
* @param array $params Query string parameters. |
| 573 |
* @param int|false $encoding Set to false to not encode, PHP_QUERY_RFC3986 |
| 574 |
* to encode using RFC3986, or PHP_QUERY_RFC1738 |
| 575 |
* to encode using RFC1738. |
| 576 |
* @return string |
| 577 |
*/ |
| 578 |
function build_query(array $params, $encoding = PHP_QUERY_RFC3986) |
| 579 |
{ |
| 580 |
if (!$params) { |
| 581 |
return ''; |
| 582 |
} |
| 583 |
|
| 584 |
if ($encoding === false) { |
| 585 |
$encoder = function ($str) { return $str; }; |
| 586 |
} elseif ($encoding === PHP_QUERY_RFC3986) { |
| 587 |
$encoder = 'rawurlencode'; |
| 588 |
} elseif ($encoding === PHP_QUERY_RFC1738) { |
| 589 |
$encoder = 'urlencode'; |
| 590 |
} else { |
| 591 |
throw new \InvalidArgumentException('Invalid type'); |
| 592 |
} |
| 593 |
|
| 594 |
$qs = ''; |
| 595 |
foreach ($params as $k => $v) { |
| 596 |
$k = $encoder($k); |
| 597 |
if (!is_array($v)) { |
| 598 |
$qs .= $k; |
| 599 |
if ($v !== null) { |
| 600 |
$qs .= '=' . $encoder($v); |
| 601 |
} |
| 602 |
$qs .= '&'; |
| 603 |
} else { |
| 604 |
foreach ($v as $vv) { |
| 605 |
$qs .= $k; |
| 606 |
if ($vv !== null) { |
| 607 |
$qs .= '=' . $encoder($vv); |
| 608 |
} |
| 609 |
$qs .= '&'; |
| 610 |
} |
| 611 |
} |
| 612 |
} |
| 613 |
|
| 614 |
return $qs ? (string) substr($qs, 0, -1) : ''; |
| 615 |
} |
| 616 |
|
| 617 |
/** |
| 618 |
* Determines the mimetype of a file by looking at its extension. |
| 619 |
* |
| 620 |
* @param $filename |
| 621 |
* |
| 622 |
* @return null|string |
| 623 |
*/ |
| 624 |
function mimetype_from_filename($filename) |
| 625 |
{ |
| 626 |
return mimetype_from_extension(pathinfo($filename, PATHINFO_EXTENSION)); |
| 627 |
} |
| 628 |
|
| 629 |
/** |
| 630 |
* Maps a file extensions to a mimetype. |
| 631 |
* |
| 632 |
* @param $extension string The file extension. |
| 633 |
* |
| 634 |
* @return string|null |
| 635 |
* @link http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types |
| 636 |
*/ |
| 637 |
function mimetype_from_extension($extension) |
| 638 |
{ |
| 639 |
static $mimetypes = [ |
| 640 |
'3gp' => 'video/3gpp', |
| 641 |
'7z' => 'application/x-7z-compressed', |
| 642 |
'aac' => 'audio/x-aac', |
| 643 |
'ai' => 'application/postscript', |
| 644 |
'aif' => 'audio/x-aiff', |
| 645 |
'asc' => 'text/plain', |
| 646 |
'asf' => 'video/x-ms-asf', |
| 647 |
'atom' => 'application/atom+xml', |
| 648 |
'avi' => 'video/x-msvideo', |
| 649 |
'bmp' => 'image/bmp', |
| 650 |
'bz2' => 'application/x-bzip2', |
| 651 |
'cer' => 'application/pkix-cert', |
| 652 |
'crl' => 'application/pkix-crl', |
| 653 |
'crt' => 'application/x-x509-ca-cert', |
| 654 |
'css' => 'text/css', |
| 655 |
'csv' => 'text/csv', |
| 656 |
'cu' => 'application/cu-seeme', |
| 657 |
'deb' => 'application/x-debian-package', |
| 658 |
'doc' => 'application/msword', |
| 659 |
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', |
| 660 |
'dvi' => 'application/x-dvi', |
| 661 |
'eot' => 'application/vnd.ms-fontobject', |
| 662 |
'eps' => 'application/postscript', |
| 663 |
'epub' => 'application/epub+zip', |
| 664 |
'etx' => 'text/x-setext', |
| 665 |
'flac' => 'audio/flac', |
| 666 |
'flv' => 'video/x-flv', |
| 667 |
'gif' => 'image/gif', |
| 668 |
'gz' => 'application/gzip', |
| 669 |
'htm' => 'text/html', |
| 670 |
'html' => 'text/html', |
| 671 |
'ico' => 'image/x-icon', |
| 672 |
'ics' => 'text/calendar', |
| 673 |
'ini' => 'text/plain', |
| 674 |
'iso' => 'application/x-iso9660-image', |
| 675 |
'jar' => 'application/java-archive', |
| 676 |
'jpe' => 'image/jpeg', |
| 677 |
'jpeg' => 'image/jpeg', |
| 678 |
'jpg' => 'image/jpeg', |
| 679 |
'js' => 'text/javascript', |
| 680 |
'json' => 'application/json', |
| 681 |
'latex' => 'application/x-latex', |
| 682 |
'log' => 'text/plain', |
| 683 |
'm4a' => 'audio/mp4', |
| 684 |
'm4v' => 'video/mp4', |
| 685 |
'mid' => 'audio/midi', |
| 686 |
'midi' => 'audio/midi', |
| 687 |
'mov' => 'video/quicktime', |
| 688 |
'mkv' => 'video/x-matroska', |
| 689 |
'mp3' => 'audio/mpeg', |
| 690 |
'mp4' => 'video/mp4', |
| 691 |
'mp4a' => 'audio/mp4', |
| 692 |
'mp4v' => 'video/mp4', |
| 693 |
'mpe' => 'video/mpeg', |
| 694 |
'mpeg' => 'video/mpeg', |
| 695 |
'mpg' => 'video/mpeg', |
| 696 |
'mpg4' => 'video/mp4', |
| 697 |
'oga' => 'audio/ogg', |
| 698 |
'ogg' => 'audio/ogg', |
| 699 |
'ogv' => 'video/ogg', |
| 700 |
'ogx' => 'application/ogg', |
| 701 |
'pbm' => 'image/x-portable-bitmap', |
| 702 |
'pdf' => 'application/pdf', |
| 703 |
'pgm' => 'image/x-portable-graymap', |
| 704 |
'png' => 'image/png', |
| 705 |
'pnm' => 'image/x-portable-anymap', |
| 706 |
'ppm' => 'image/x-portable-pixmap', |
| 707 |
'ppt' => 'application/vnd.ms-powerpoint', |
| 708 |
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', |
| 709 |
'ps' => 'application/postscript', |
| 710 |
'qt' => 'video/quicktime', |
| 711 |
'rar' => 'application/x-rar-compressed', |
| 712 |
'ras' => 'image/x-cmu-raster', |
| 713 |
'rss' => 'application/rss+xml', |
| 714 |
'rtf' => 'application/rtf', |
| 715 |
'sgm' => 'text/sgml', |
| 716 |
'sgml' => 'text/sgml', |
| 717 |
'svg' => 'image/svg+xml', |
| 718 |
'swf' => 'application/x-shockwave-flash', |
| 719 |
'tar' => 'application/x-tar', |
| 720 |
'tif' => 'image/tiff', |
| 721 |
'tiff' => 'image/tiff', |
| 722 |
'torrent' => 'application/x-bittorrent', |
| 723 |
'ttf' => 'application/x-font-ttf', |
| 724 |
'txt' => 'text/plain', |
| 725 |
'wav' => 'audio/x-wav', |
| 726 |
'webm' => 'video/webm', |
| 727 |
'webp' => 'image/webp', |
| 728 |
'wma' => 'audio/x-ms-wma', |
| 729 |
'wmv' => 'video/x-ms-wmv', |
| 730 |
'woff' => 'application/x-font-woff', |
| 731 |
'wsdl' => 'application/wsdl+xml', |
| 732 |
'xbm' => 'image/x-xbitmap', |
| 733 |
'xls' => 'application/vnd.ms-excel', |
| 734 |
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', |
| 735 |
'xml' => 'application/xml', |
| 736 |
'xpm' => 'image/x-xpixmap', |
| 737 |
'xwd' => 'image/x-xwindowdump', |
| 738 |
'yaml' => 'text/yaml', |
| 739 |
'yml' => 'text/yaml', |
| 740 |
'zip' => 'application/zip', |
| 741 |
]; |
| 742 |
|
| 743 |
$extension = strtolower($extension); |
| 744 |
|
| 745 |
return isset($mimetypes[$extension]) |
| 746 |
? $mimetypes[$extension] |
| 747 |
: null; |
| 748 |
} |
| 749 |
|
| 750 |
/** |
| 751 |
* Parses an HTTP message into an associative array. |
| 752 |
* |
| 753 |
* The array contains the "start-line" key containing the start line of |
| 754 |
* the message, "headers" key containing an associative array of header |
| 755 |
* array values, and a "body" key containing the body of the message. |
| 756 |
* |
| 757 |
* @param string $message HTTP request or response to parse. |
| 758 |
* |
| 759 |
* @return array |
| 760 |
* @internal |
| 761 |
*/ |
| 762 |
function _parse_message($message) |
| 763 |
{ |
| 764 |
if (!$message) { |
| 765 |
throw new \InvalidArgumentException('Invalid message'); |
| 766 |
} |
| 767 |
|
| 768 |
$message = ltrim($message, "\r\n"); |
| 769 |
|
| 770 |
$messageParts = preg_split("/\r?\n\r?\n/", $message, 2); |
| 771 |
|
| 772 |
if ($messageParts === false || count($messageParts) !== 2) { |
| 773 |
throw new \InvalidArgumentException('Invalid message: Missing header delimiter'); |
| 774 |
} |
| 775 |
|
| 776 |
list($rawHeaders, $body) = $messageParts; |
| 777 |
$rawHeaders .= "\r\n"; // Put back the delimiter we split previously |
| 778 |
$headerParts = preg_split("/\r?\n/", $rawHeaders, 2); |
| 779 |
|
| 780 |
if ($headerParts === false || count($headerParts) !== 2) { |
| 781 |
throw new \InvalidArgumentException('Invalid message: Missing status line'); |
| 782 |
} |
| 783 |
|
| 784 |
list($startLine, $rawHeaders) = $headerParts; |
| 785 |
|
| 786 |
if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') { |
| 787 |
// Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0 |
| 788 |
$rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders); |
| 789 |
} |
| 790 |
|
| 791 |
/** @var array[] $headerLines */ |
| 792 |
$count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER); |
| 793 |
|
| 794 |
// If these aren't the same, then one line didn't match and there's an invalid header. |
| 795 |
if ($count !== substr_count($rawHeaders, "\n")) { |
| 796 |
// Folding is deprecated, see https://tools.ietf.org/html/rfc7230#section-3.2.4 |
| 797 |
if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) { |
| 798 |
throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding'); |
| 799 |
} |
| 800 |
|
| 801 |
throw new \InvalidArgumentException('Invalid header syntax'); |
| 802 |
} |
| 803 |
|
| 804 |
$headers = []; |
| 805 |
|
| 806 |
foreach ($headerLines as $headerLine) { |
| 807 |
$headers[$headerLine[1]][] = $headerLine[2]; |
| 808 |
} |
| 809 |
|
| 810 |
return [ |
| 811 |
'start-line' => $startLine, |
| 812 |
'headers' => $headers, |
| 813 |
'body' => $body, |
| 814 |
]; |
| 815 |
} |
| 816 |
|
| 817 |
/** |
| 818 |
* Constructs a URI for an HTTP request message. |
| 819 |
* |
| 820 |
* @param string $path Path from the start-line |
| 821 |
* @param array $headers Array of headers (each value an array). |
| 822 |
* |
| 823 |
* @return string |
| 824 |
* @internal |
| 825 |
*/ |
| 826 |
function _parse_request_uri($path, array $headers) |
| 827 |
{ |
| 828 |
$hostKey = array_filter(array_keys($headers), function ($k) { |
| 829 |
return strtolower($k) === 'host'; |
| 830 |
}); |
| 831 |
|
| 832 |
// If no host is found, then a full URI cannot be constructed. |
| 833 |
if (!$hostKey) { |
| 834 |
return $path; |
| 835 |
} |
| 836 |
|
| 837 |
$host = $headers[reset($hostKey)][0]; |
| 838 |
$scheme = substr($host, -4) === ':443' ? 'https' : 'http'; |
| 839 |
|
| 840 |
return $scheme . '://' . $host . '/' . ltrim($path, '/'); |
| 841 |
} |
| 842 |
|
| 843 |
/** |
| 844 |
* Get a short summary of the message body |
| 845 |
* |
| 846 |
* Will return `null` if the response is not printable. |
| 847 |
* |
| 848 |
* @param MessageInterface $message The message to get the body summary |
| 849 |
* @param int $truncateAt The maximum allowed size of the summary |
| 850 |
* |
| 851 |
* @return null|string |
| 852 |
*/ |
| 853 |
function get_message_body_summary(MessageInterface $message, $truncateAt = 120) |
| 854 |
{ |
| 855 |
$body = $message->getBody(); |
| 856 |
|
| 857 |
if (!$body->isSeekable() || !$body->isReadable()) { |
| 858 |
return null; |
| 859 |
} |
| 860 |
|
| 861 |
$size = $body->getSize(); |
| 862 |
|
| 863 |
if ($size === 0) { |
| 864 |
return null; |
| 865 |
} |
| 866 |
|
| 867 |
$summary = $body->read($truncateAt); |
| 868 |
$body->rewind(); |
| 869 |
|
| 870 |
if ($size > $truncateAt) { |
| 871 |
$summary .= ' (truncated...)'; |
| 872 |
} |
| 873 |
|
| 874 |
// Matches any printable character, including unicode characters: |
| 875 |
// letters, marks, numbers, punctuation, spacing, and separators. |
| 876 |
if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/', $summary)) { |
| 877 |
return null; |
| 878 |
} |
| 879 |
|
| 880 |
return $summary; |
| 881 |
} |
| 882 |
|
| 883 |
/** @internal */ |
| 884 |
function _caseless_remove($keys, array $data) |
| 885 |
{ |
| 886 |
$result = []; |
| 887 |
|
| 888 |
foreach ($keys as &$key) { |
| 889 |
$key = strtolower($key); |
| 890 |
} |
| 891 |
|
| 892 |
foreach ($data as $k => $v) { |
| 893 |
if (!in_array(strtolower($k), $keys)) { |
| 894 |
$result[$k] = $v; |
| 895 |
} |
| 896 |
} |
| 897 |
|
| 898 |
return $result; |
| 899 |
} |
| 900 |
|