v2: Updates

* Simplifies & beautifies everything
* Introduces a new Class system.
* Errors are defaulted to AWS's handler.
* New function names & more efficient handling.
* Should fix a majority of the errors.

Please read the README for more!
This commit is contained in:
Devang Srivastava 2020-09-28 15:32:51 +05:30
parent ad0726e41e
commit e6d7753dc8
1095 changed files with 45088 additions and 2911 deletions

View file

@ -16,7 +16,6 @@ class AppendStream implements StreamInterface
private $seekable = true;
private $current = 0;
private $pos = 0;
private $detached = false;
/**
* @param StreamInterface[] $streams Streams to decorate. Each stream must
@ -73,6 +72,7 @@ class AppendStream implements StreamInterface
public function close()
{
$this->pos = $this->current = 0;
$this->seekable = true;
foreach ($this->streams as $stream) {
$stream->close();
@ -82,14 +82,22 @@ class AppendStream implements StreamInterface
}
/**
* Detaches each attached stream
* Detaches each attached stream.
*
* Returns null as it's not clear which underlying stream resource to return.
*
* {@inheritdoc}
*/
public function detach()
{
$this->close();
$this->detached = true;
$this->pos = $this->current = 0;
$this->seekable = true;
foreach ($this->streams as $stream) {
$stream->detach();
}
$this->streams = [];
}
public function tell()

View file

@ -52,6 +52,15 @@ class FnStream implements StreamInterface
}
}
/**
* An unserialize would allow the __destruct to run when the unserialized value goes out of scope.
* @throws \LogicException
*/
public function __wakeup()
{
throw new \LogicException('FnStream should never be unserialized');
}
/**
* Adds custom functionality to an underlying stream by intercepting
* specific method calls.

View file

@ -27,7 +27,7 @@ class InflateStream implements StreamInterface
$stream = new LimitStream($stream, -1, 10 + $filenameHeaderLength);
$resource = StreamWrapper::getResource($stream);
stream_filter_append($resource, 'zlib.inflate', STREAM_FILTER_READ);
$this->stream = new Stream($resource);
$this->stream = $stream->isSeekable() ? new Stream($resource) : new NoSeekStream(new Stream($resource));
}
/**

View file

@ -72,7 +72,7 @@ class LimitStream implements StreamInterface
{
if ($whence !== SEEK_SET || $offset < 0) {
throw new \RuntimeException(sprintf(
'Cannot seek to offset % with whence %s',
'Cannot seek to offset %s with whence %s',
$offset,
$whence
));

View file

@ -66,11 +66,8 @@ trait MessageTrait
public function withHeader($header, $value)
{
if (!is_array($value)) {
$value = [$value];
}
$value = $this->trimHeaderValues($value);
$this->assertHeader($header);
$value = $this->normalizeHeaderValue($value);
$normalized = strtolower($header);
$new = clone $this;
@ -85,11 +82,8 @@ trait MessageTrait
public function withAddedHeader($header, $value)
{
if (!is_array($value)) {
$value = [$value];
}
$value = $this->trimHeaderValues($value);
$this->assertHeader($header);
$value = $this->normalizeHeaderValue($value);
$normalized = strtolower($header);
$new = clone $this;
@ -144,11 +138,13 @@ trait MessageTrait
{
$this->headerNames = $this->headers = [];
foreach ($headers as $header => $value) {
if (!is_array($value)) {
$value = [$value];
if (is_int($header)) {
// Numeric array keys are converted to int by PHP but having a header name '123' is not forbidden by the spec
// and also allowed in withHeader(). So we need to cast it to string again for the following assertion to pass.
$header = (string) $header;
}
$value = $this->trimHeaderValues($value);
$this->assertHeader($header);
$value = $this->normalizeHeaderValue($value);
$normalized = strtolower($header);
if (isset($this->headerNames[$normalized])) {
$header = $this->headerNames[$normalized];
@ -160,6 +156,19 @@ trait MessageTrait
}
}
private function normalizeHeaderValue($value)
{
if (!is_array($value)) {
return $this->trimHeaderValues([$value]);
}
if (count($value) === 0) {
throw new \InvalidArgumentException('Header value can not be an empty array.');
}
return $this->trimHeaderValues($value);
}
/**
* Trims whitespace from the header values.
*
@ -177,7 +186,28 @@ trait MessageTrait
private function trimHeaderValues(array $values)
{
return array_map(function ($value) {
return trim($value, " \t");
if (!is_scalar($value) && null !== $value) {
throw new \InvalidArgumentException(sprintf(
'Header value must be scalar or null but %s provided.',
is_object($value) ? get_class($value) : gettype($value)
));
}
return trim((string) $value, " \t");
}, $values);
}
private function assertHeader($header)
{
if (!is_string($header)) {
throw new \InvalidArgumentException(sprintf(
'Header name must be a string but %s provided.',
is_object($header) ? get_class($header) : gettype($header)
));
}
if ($header === '') {
throw new \InvalidArgumentException('Header name can not be empty.');
}
}
}

View file

@ -36,6 +36,7 @@ class Request implements RequestInterface
$body = null,
$version = '1.1'
) {
$this->assertMethod($method);
if (!($uri instanceof UriInterface)) {
$uri = new Uri($uri);
}
@ -45,7 +46,7 @@ class Request implements RequestInterface
$this->setHeaders($headers);
$this->protocol = $version;
if (!$this->hasHeader('Host')) {
if (!isset($this->headerNames['host'])) {
$this->updateHostFromUri();
}
@ -91,6 +92,7 @@ class Request implements RequestInterface
public function withMethod($method)
{
$this->assertMethod($method);
$new = clone $this;
$new->method = strtoupper($method);
return $new;
@ -110,7 +112,7 @@ class Request implements RequestInterface
$new = clone $this;
$new->uri = $uri;
if (!$preserveHost) {
if (!$preserveHost || !isset($this->headerNames['host'])) {
$new->updateHostFromUri();
}
@ -139,4 +141,11 @@ class Request implements RequestInterface
// See: http://tools.ietf.org/html/rfc7230#section-5.4
$this->headers = [$header => [$host]] + $this->headers;
}
private function assertMethod($method)
{
if (!is_string($method) || $method === '') {
throw new \InvalidArgumentException('Method must be a non-empty string.');
}
}
}

View file

@ -93,7 +93,11 @@ class Response implements ResponseInterface
$version = '1.1',
$reason = null
) {
$this->statusCode = (int) $status;
$this->assertStatusCodeIsInteger($status);
$status = (int) $status;
$this->assertStatusCodeRange($status);
$this->statusCode = $status;
if ($body !== '' && $body !== null) {
$this->stream = stream_for($body);
@ -121,12 +125,30 @@ class Response implements ResponseInterface
public function withStatus($code, $reasonPhrase = '')
{
$this->assertStatusCodeIsInteger($code);
$code = (int) $code;
$this->assertStatusCodeRange($code);
$new = clone $this;
$new->statusCode = (int) $code;
$new->statusCode = $code;
if ($reasonPhrase == '' && isset(self::$phrases[$new->statusCode])) {
$reasonPhrase = self::$phrases[$new->statusCode];
}
$new->reasonPhrase = $reasonPhrase;
return $new;
}
private function assertStatusCodeIsInteger($statusCode)
{
if (filter_var($statusCode, FILTER_VALIDATE_INT) === false) {
throw new \InvalidArgumentException('Status code must be an integer value.');
}
}
private function assertStatusCodeRange($statusCode)
{
if ($statusCode < 100 || $statusCode >= 600) {
throw new \InvalidArgumentException('Status code must be an integer value between 1xx and 5xx.');
}
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace GuzzleHttp\Psr7;
final class Rfc7230
{
/**
* Header related regular expressions (copied from amphp/http package)
* (Note: once we require PHP 7.x we could just depend on the upstream package)
*
* Note: header delimiter (\r\n) is modified to \r?\n to accept line feed only delimiters for BC reasons.
*
* @link https://github.com/amphp/http/blob/v1.0.1/src/Rfc7230.php#L12-L15
* @license https://github.com/amphp/http/blob/v1.0.1/LICENSE
*/
const HEADER_REGEX = "(^([^()<>@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m";
const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)";
}

View file

@ -166,9 +166,9 @@ class ServerRequest extends Request implements ServerRequestInterface
public static function fromGlobals()
{
$method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
$headers = function_exists('getallheaders') ? getallheaders() : [];
$headers = getallheaders();
$uri = self::getUriFromGlobals();
$body = new LazyOpenStream('php://input', 'r+');
$body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
$protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1';
$serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER);
@ -180,23 +180,41 @@ class ServerRequest extends Request implements ServerRequestInterface
->withUploadedFiles(self::normalizeFiles($_FILES));
}
private static function extractHostAndPortFromAuthority($authority)
{
$uri = 'http://'.$authority;
$parts = parse_url($uri);
if (false === $parts) {
return [null, null];
}
$host = isset($parts['host']) ? $parts['host'] : null;
$port = isset($parts['port']) ? $parts['port'] : null;
return [$host, $port];
}
/**
* Get a Uri populated with values from $_SERVER.
*
* @return UriInterface
*/
public static function getUriFromGlobals() {
public static function getUriFromGlobals()
{
$uri = new Uri('');
$uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http');
$hasPort = false;
if (isset($_SERVER['HTTP_HOST'])) {
$hostHeaderParts = explode(':', $_SERVER['HTTP_HOST']);
$uri = $uri->withHost($hostHeaderParts[0]);
if (isset($hostHeaderParts[1])) {
list($host, $port) = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']);
if ($host !== null) {
$uri = $uri->withHost($host);
}
if ($port !== null) {
$hasPort = true;
$uri = $uri->withPort($hostHeaderParts[1]);
$uri = $uri->withPort($port);
}
} elseif (isset($_SERVER['SERVER_NAME'])) {
$uri = $uri->withHost($_SERVER['SERVER_NAME']);
@ -210,7 +228,7 @@ class ServerRequest extends Request implements ServerRequestInterface
$hasQuery = false;
if (isset($_SERVER['REQUEST_URI'])) {
$requestUriParts = explode('?', $_SERVER['REQUEST_URI']);
$requestUriParts = explode('?', $_SERVER['REQUEST_URI'], 2);
$uri = $uri->withPath($requestUriParts[0]);
if (isset($requestUriParts[1])) {
$hasQuery = true;

View file

@ -10,6 +10,17 @@ use Psr\Http\Message\StreamInterface;
*/
class Stream implements StreamInterface
{
/**
* Resource modes.
*
* @var string
*
* @see http://php.net/manual/function.fopen.php
* @see http://php.net/manual/en/function.gzopen.php
*/
const READABLE_MODES = '/r|a\+|ab\+|w\+|wb\+|x\+|xb\+|c\+|cb\+/';
const WRITABLE_MODES = '/a|w|r\+|rb\+|rw|x|c/';
private $stream;
private $size;
private $seekable;
@ -18,22 +29,6 @@ class Stream implements StreamInterface
private $uri;
private $customMetadata;
/** @var array Hash of readable and writable stream types */
private static $readWriteHash = [
'read' => [
'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
'x+t' => true, 'c+t' => true, 'a+' => true
],
'write' => [
'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true,
'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true
]
];
/**
* This constructor accepts an associative array of options.
*
@ -65,20 +60,11 @@ class Stream implements StreamInterface
$this->stream = $stream;
$meta = stream_get_meta_data($this->stream);
$this->seekable = $meta['seekable'];
$this->readable = isset(self::$readWriteHash['read'][$meta['mode']]);
$this->writable = isset(self::$readWriteHash['write'][$meta['mode']]);
$this->readable = (bool)preg_match(self::READABLE_MODES, $meta['mode']);
$this->writable = (bool)preg_match(self::WRITABLE_MODES, $meta['mode']);
$this->uri = $this->getMetadata('uri');
}
public function __get($name)
{
if ($name == 'stream') {
throw new \RuntimeException('The stream is detached');
}
throw new \BadMethodCallException('No value for ' . $name);
}
/**
* Closes the stream when the destructed
*/
@ -99,6 +85,10 @@ class Stream implements StreamInterface
public function getContents()
{
if (!isset($this->stream)) {
throw new \RuntimeException('Stream is detached');
}
$contents = stream_get_contents($this->stream);
if ($contents === false) {
@ -173,11 +163,19 @@ class Stream implements StreamInterface
public function eof()
{
return !$this->stream || feof($this->stream);
if (!isset($this->stream)) {
throw new \RuntimeException('Stream is detached');
}
return feof($this->stream);
}
public function tell()
{
if (!isset($this->stream)) {
throw new \RuntimeException('Stream is detached');
}
$result = ftell($this->stream);
if ($result === false) {
@ -194,9 +192,15 @@ class Stream implements StreamInterface
public function seek($offset, $whence = SEEK_SET)
{
$whence = (int) $whence;
if (!isset($this->stream)) {
throw new \RuntimeException('Stream is detached');
}
if (!$this->seekable) {
throw new \RuntimeException('Stream is not seekable');
} elseif (fseek($this->stream, $offset, $whence) === -1) {
}
if (fseek($this->stream, $offset, $whence) === -1) {
throw new \RuntimeException('Unable to seek to stream position '
. $offset . ' with whence ' . var_export($whence, true));
}
@ -204,6 +208,9 @@ class Stream implements StreamInterface
public function read($length)
{
if (!isset($this->stream)) {
throw new \RuntimeException('Stream is detached');
}
if (!$this->readable) {
throw new \RuntimeException('Cannot read from non-readable stream');
}
@ -225,6 +232,9 @@ class Stream implements StreamInterface
public function write($string)
{
if (!isset($this->stream)) {
throw new \RuntimeException('Stream is detached');
}
if (!$this->writable) {
throw new \RuntimeException('Cannot write to a non-writable stream');
}

View file

@ -38,9 +38,21 @@ class StreamWrapper
. 'writable, or both.');
}
return fopen('guzzle://stream', $mode, null, stream_context_create([
return fopen('guzzle://stream', $mode, null, self::createStreamContext($stream));
}
/**
* Creates a stream context that can be used to open a stream as a php stream resource.
*
* @param StreamInterface $stream
*
* @return resource
*/
public static function createStreamContext(StreamInterface $stream)
{
return stream_context_create([
'guzzle' => ['stream' => $stream]
]));
]);
}
/**
@ -94,12 +106,21 @@ class StreamWrapper
return true;
}
public function stream_cast($cast_as)
{
$stream = clone($this->stream);
return $stream->detach();
}
public function stream_stat()
{
static $modeMap = [
'r' => 33060,
'rb' => 33060,
'r+' => 33206,
'w' => 33188
'w' => 33188,
'wb' => 33188
];
return [
@ -118,4 +139,23 @@ class StreamWrapper
'blocks' => 0
];
}
public function url_stat($path, $flags)
{
return [
'dev' => 0,
'ino' => 0,
'mode' => 0,
'nlink' => 0,
'uid' => 0,
'gid' => 0,
'rdev' => 0,
'size' => 0,
'atime' => 0,
'mtime' => 0,
'ctime' => 0,
'blksize' => 0,
'blocks' => 0
];
}
}

View file

@ -301,15 +301,7 @@ class Uri implements UriInterface
*/
public static function withoutQueryValue(UriInterface $uri, $key)
{
$current = $uri->getQuery();
if ($current === '') {
return $uri;
}
$decodedKey = rawurldecode($key);
$result = array_filter(explode('&', $current), function ($part) use ($decodedKey) {
return rawurldecode(explode('=', $part)[0]) !== $decodedKey;
});
$result = self::getFilteredQueryString($uri, [$key]);
return $uri->withQuery(implode('&', $result));
}
@ -331,26 +323,29 @@ class Uri implements UriInterface
*/
public static function withQueryValue(UriInterface $uri, $key, $value)
{
$current = $uri->getQuery();
$result = self::getFilteredQueryString($uri, [$key]);
if ($current === '') {
$result = [];
} else {
$decodedKey = rawurldecode($key);
$result = array_filter(explode('&', $current), function ($part) use ($decodedKey) {
return rawurldecode(explode('=', $part)[0]) !== $decodedKey;
});
}
$result[] = self::generateQueryString($key, $value);
// Query string separators ("=", "&") within the key or value need to be encoded
// (while preventing double-encoding) before setting the query string. All other
// chars that need percent-encoding will be encoded by withQuery().
$key = strtr($key, self::$replaceQuery);
return $uri->withQuery(implode('&', $result));
}
if ($value !== null) {
$result[] = $key . '=' . strtr($value, self::$replaceQuery);
} else {
$result[] = $key;
/**
* Creates a new URI with multiple specific query string values.
*
* It has the same behavior as withQueryValue() but for an associative array of key => value.
*
* @param UriInterface $uri URI to use as a base.
* @param array $keyValueArray Associative array of key and values
*
* @return UriInterface
*/
public static function withQueryValues(UriInterface $uri, array $keyValueArray)
{
$result = self::getFilteredQueryString($uri, array_keys($keyValueArray));
foreach ($keyValueArray as $key => $value) {
$result[] = self::generateQueryString($key, $value);
}
return $uri->withQuery(implode('&', $result));
@ -442,9 +437,9 @@ class Uri implements UriInterface
public function withUserInfo($user, $password = null)
{
$info = $user;
if ($password != '') {
$info .= ':' . $password;
$info = $this->filterUserInfoComponent($user);
if ($password !== null) {
$info .= ':' . $this->filterUserInfoComponent($password);
}
if ($this->userInfo === $info) {
@ -542,7 +537,9 @@ class Uri implements UriInterface
$this->scheme = isset($parts['scheme'])
? $this->filterScheme($parts['scheme'])
: '';
$this->userInfo = isset($parts['user']) ? $parts['user'] : '';
$this->userInfo = isset($parts['user'])
? $this->filterUserInfoComponent($parts['user'])
: '';
$this->host = isset($parts['host'])
? $this->filterHost($parts['host'])
: '';
@ -559,7 +556,7 @@ class Uri implements UriInterface
? $this->filterQueryAndFragment($parts['fragment'])
: '';
if (isset($parts['pass'])) {
$this->userInfo .= ':' . $parts['pass'];
$this->userInfo .= ':' . $this->filterUserInfoComponent($parts['pass']);
}
$this->removeDefaultPort();
@ -581,6 +578,26 @@ class Uri implements UriInterface
return strtolower($scheme);
}
/**
* @param string $component
*
* @return string
*
* @throws \InvalidArgumentException If the user info is invalid.
*/
private function filterUserInfoComponent($component)
{
if (!is_string($component)) {
throw new \InvalidArgumentException('User info must be a string');
}
return preg_replace_callback(
'/(?:[^%' . self::$charUnreserved . self::$charSubDelims . ']+|%(?![A-Fa-f0-9]{2}))/',
[$this, 'rawurlencodeMatchZero'],
$component
);
}
/**
* @param string $host
*
@ -611,15 +628,56 @@ class Uri implements UriInterface
}
$port = (int) $port;
if (1 > $port || 0xffff < $port) {
if (0 > $port || 0xffff < $port) {
throw new \InvalidArgumentException(
sprintf('Invalid port: %d. Must be between 1 and 65535', $port)
sprintf('Invalid port: %d. Must be between 0 and 65535', $port)
);
}
return $port;
}
/**
* @param UriInterface $uri
* @param array $keys
*
* @return array
*/
private static function getFilteredQueryString(UriInterface $uri, array $keys)
{
$current = $uri->getQuery();
if ($current === '') {
return [];
}
$decodedKeys = array_map('rawurldecode', $keys);
return array_filter(explode('&', $current), function ($part) use ($decodedKeys) {
return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true);
});
}
/**
* @param string $key
* @param string|null $value
*
* @return string
*/
private static function generateQueryString($key, $value)
{
// Query string separators ("=", "&") within the key or value need to be encoded
// (while preventing double-encoding) before setting the query string. All other
// chars that need percent-encoding will be encoded by withQuery().
$queryString = strtr($key, self::$replaceQuery);
if ($value !== null) {
$queryString .= '=' . strtr($value, self::$replaceQuery);
}
return $queryString;
}
private function removeDefaultPort()
{
if ($this->port !== null && self::isDefaultPort($this)) {

View file

@ -69,10 +69,10 @@ function uri_for($uri)
* - metadata: Array of custom metadata.
* - size: Size of the stream.
*
* @param resource|string|null|int|float|bool|StreamInterface|callable $resource Entity body data
* @param array $options Additional options
* @param resource|string|null|int|float|bool|StreamInterface|callable|\Iterator $resource Entity body data
* @param array $options Additional options
*
* @return Stream
* @return StreamInterface
* @throws \InvalidArgumentException if the $resource arg is not valid.
*/
function stream_for($resource = '', array $options = [])
@ -238,7 +238,7 @@ function modify_request(RequestInterface $request, array $changes)
}
if ($request instanceof ServerRequestInterface) {
return new ServerRequest(
return (new ServerRequest(
isset($changes['method']) ? $changes['method'] : $request->getMethod(),
$uri,
$headers,
@ -247,7 +247,11 @@ function modify_request(RequestInterface $request, array $changes)
? $changes['version']
: $request->getProtocolVersion(),
$request->getServerParams()
);
))
->withParsedBody($request->getParsedBody())
->withQueryParams($request->getQueryParams())
->withCookieParams($request->getCookieParams())
->withUploadedFiles($request->getUploadedFiles());
}
return new Request(
@ -431,7 +435,7 @@ function hash(
* @param StreamInterface $stream Stream to read from
* @param int $maxLength Maximum buffer length
*
* @return string|bool
* @return string
*/
function readline(StreamInterface $stream, $maxLength = null)
{
@ -495,7 +499,7 @@ function parse_response($message)
// between status-code and reason-phrase is required. But browsers accept
// responses without space and reason as well.
if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) {
throw new \InvalidArgumentException('Invalid response string');
throw new \InvalidArgumentException('Invalid response string: ' . $data['start-line']);
}
$parts = explode(' ', $data['start-line'], 3);
@ -516,8 +520,8 @@ function parse_response($message)
* PHP style arrays into an associative array (e.g., foo[a]=1&foo[b]=2 will
* be parsed into ['foo[a]' => '1', 'foo[b]' => '2']).
*
* @param string $str Query string to parse
* @param bool|string $urlEncoding How the query string is encoded
* @param string $str Query string to parse
* @param int|bool $urlEncoding How the query string is encoded
*
* @return array
*/
@ -533,9 +537,9 @@ function parse_query($str, $urlEncoding = true)
$decoder = function ($value) {
return rawurldecode(str_replace('+', ' ', $value));
};
} elseif ($urlEncoding == PHP_QUERY_RFC3986) {
} elseif ($urlEncoding === PHP_QUERY_RFC3986) {
$decoder = 'rawurldecode';
} elseif ($urlEncoding == PHP_QUERY_RFC1738) {
} elseif ($urlEncoding === PHP_QUERY_RFC1738) {
$decoder = 'urldecode';
} else {
$decoder = function ($str) { return $str; };
@ -633,6 +637,7 @@ function mimetype_from_filename($filename)
function mimetype_from_extension($extension)
{
static $mimetypes = [
'3gp' => 'video/3gpp',
'7z' => 'application/x-7z-compressed',
'aac' => 'audio/x-aac',
'ai' => 'application/postscript',
@ -680,6 +685,7 @@ function mimetype_from_extension($extension)
'mid' => 'audio/midi',
'midi' => 'audio/midi',
'mov' => 'video/quicktime',
'mkv' => 'video/x-matroska',
'mp3' => 'audio/mpeg',
'mp4' => 'video/mp4',
'mp4a' => 'audio/mp4',
@ -718,6 +724,7 @@ function mimetype_from_extension($extension)
'txt' => 'text/plain',
'wav' => 'audio/x-wav',
'webm' => 'video/webm',
'webp' => 'image/webp',
'wma' => 'audio/x-ms-wma',
'wmv' => 'video/x-ms-wmv',
'woff' => 'application/x-font-woff',
@ -758,29 +765,53 @@ function _parse_message($message)
throw new \InvalidArgumentException('Invalid message');
}
// Iterate over each line in the message, accounting for line endings
$lines = preg_split('/(\\r?\\n)/', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
$result = ['start-line' => array_shift($lines), 'headers' => [], 'body' => ''];
array_shift($lines);
$message = ltrim($message, "\r\n");
for ($i = 0, $totalLines = count($lines); $i < $totalLines; $i += 2) {
$line = $lines[$i];
// If two line breaks were encountered, then this is the end of body
if (empty($line)) {
if ($i < $totalLines - 1) {
$result['body'] = implode('', array_slice($lines, $i + 2));
}
break;
}
if (strpos($line, ':')) {
$parts = explode(':', $line, 2);
$key = trim($parts[0]);
$value = isset($parts[1]) ? trim($parts[1]) : '';
$result['headers'][$key][] = $value;
}
$messageParts = preg_split("/\r?\n\r?\n/", $message, 2);
if ($messageParts === false || count($messageParts) !== 2) {
throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
}
return $result;
list($rawHeaders, $body) = $messageParts;
$rawHeaders .= "\r\n"; // Put back the delimiter we split previously
$headerParts = preg_split("/\r?\n/", $rawHeaders, 2);
if ($headerParts === false || count($headerParts) !== 2) {
throw new \InvalidArgumentException('Invalid message: Missing status line');
}
list($startLine, $rawHeaders) = $headerParts;
if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') {
// Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
$rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders);
}
/** @var array[] $headerLines */
$count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER);
// If these aren't the same, then one line didn't match and there's an invalid header.
if ($count !== substr_count($rawHeaders, "\n")) {
// Folding is deprecated, see https://tools.ietf.org/html/rfc7230#section-3.2.4
if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) {
throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
}
throw new \InvalidArgumentException('Invalid header syntax');
}
$headers = [];
foreach ($headerLines as $headerLine) {
$headers[$headerLine[1]][] = $headerLine[2];
}
return [
'start-line' => $startLine,
'headers' => $headers,
'body' => $body,
];
}
/**
@ -809,6 +840,46 @@ function _parse_request_uri($path, array $headers)
return $scheme . '://' . $host . '/' . ltrim($path, '/');
}
/**
* Get a short summary of the message body
*
* Will return `null` if the response is not printable.
*
* @param MessageInterface $message The message to get the body summary
* @param int $truncateAt The maximum allowed size of the summary
*
* @return null|string
*/
function get_message_body_summary(MessageInterface $message, $truncateAt = 120)
{
$body = $message->getBody();
if (!$body->isSeekable() || !$body->isReadable()) {
return null;
}
$size = $body->getSize();
if ($size === 0) {
return null;
}
$summary = $body->read($truncateAt);
$body->rewind();
if ($size > $truncateAt) {
$summary .= ' (truncated...)';
}
// Matches any printable character, including unicode characters:
// letters, marks, numbers, punctuation, spacing, and separators.
if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/', $summary)) {
return null;
}
return $summary;
}
/** @internal */
function _caseless_remove($keys, array $data)
{