annotate vendor/guzzlehttp/psr7/src/functions.php @ 19:fa3358dc1485 tip

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