[f9c482b] | 1 | <?php
|
---|
| 2 |
|
---|
| 3 | declare(strict_types=1);
|
---|
| 4 |
|
---|
| 5 | namespace GuzzleHttp\Psr7;
|
---|
| 6 |
|
---|
| 7 | use Psr\Http\Message\MessageInterface;
|
---|
| 8 | use Psr\Http\Message\RequestInterface;
|
---|
| 9 | use Psr\Http\Message\ResponseInterface;
|
---|
| 10 |
|
---|
| 11 | final class Message
|
---|
| 12 | {
|
---|
| 13 | /**
|
---|
| 14 | * Returns the string representation of an HTTP message.
|
---|
| 15 | *
|
---|
| 16 | * @param MessageInterface $message Message to convert to a string.
|
---|
| 17 | */
|
---|
| 18 | public static function toString(MessageInterface $message): string
|
---|
| 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 | if (is_string($name) && strtolower($name) === 'set-cookie') {
|
---|
| 37 | foreach ($values as $value) {
|
---|
| 38 | $msg .= "\r\n{$name}: ".$value;
|
---|
| 39 | }
|
---|
| 40 | } else {
|
---|
| 41 | $msg .= "\r\n{$name}: ".implode(', ', $values);
|
---|
| 42 | }
|
---|
| 43 | }
|
---|
| 44 |
|
---|
| 45 | return "{$msg}\r\n\r\n".$message->getBody();
|
---|
| 46 | }
|
---|
| 47 |
|
---|
| 48 | /**
|
---|
| 49 | * Get a short summary of the message body.
|
---|
| 50 | *
|
---|
| 51 | * Will return `null` if the response is not printable.
|
---|
| 52 | *
|
---|
| 53 | * @param MessageInterface $message The message to get the body summary
|
---|
| 54 | * @param int $truncateAt The maximum allowed size of the summary
|
---|
| 55 | */
|
---|
| 56 | public static function bodySummary(MessageInterface $message, int $truncateAt = 120): ?string
|
---|
| 57 | {
|
---|
| 58 | $body = $message->getBody();
|
---|
| 59 |
|
---|
| 60 | if (!$body->isSeekable() || !$body->isReadable()) {
|
---|
| 61 | return null;
|
---|
| 62 | }
|
---|
| 63 |
|
---|
| 64 | $size = $body->getSize();
|
---|
| 65 |
|
---|
| 66 | if ($size === 0) {
|
---|
| 67 | return null;
|
---|
| 68 | }
|
---|
| 69 |
|
---|
| 70 | $body->rewind();
|
---|
| 71 | $summary = $body->read($truncateAt);
|
---|
| 72 | $body->rewind();
|
---|
| 73 |
|
---|
| 74 | if ($size > $truncateAt) {
|
---|
| 75 | $summary .= ' (truncated...)';
|
---|
| 76 | }
|
---|
| 77 |
|
---|
| 78 | // Matches any printable character, including unicode characters:
|
---|
| 79 | // letters, marks, numbers, punctuation, spacing, and separators.
|
---|
| 80 | if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/u', $summary) !== 0) {
|
---|
| 81 | return null;
|
---|
| 82 | }
|
---|
| 83 |
|
---|
| 84 | return $summary;
|
---|
| 85 | }
|
---|
| 86 |
|
---|
| 87 | /**
|
---|
| 88 | * Attempts to rewind a message body and throws an exception on failure.
|
---|
| 89 | *
|
---|
| 90 | * The body of the message will only be rewound if a call to `tell()`
|
---|
| 91 | * returns a value other than `0`.
|
---|
| 92 | *
|
---|
| 93 | * @param MessageInterface $message Message to rewind
|
---|
| 94 | *
|
---|
| 95 | * @throws \RuntimeException
|
---|
| 96 | */
|
---|
| 97 | public static function rewindBody(MessageInterface $message): void
|
---|
| 98 | {
|
---|
| 99 | $body = $message->getBody();
|
---|
| 100 |
|
---|
| 101 | if ($body->tell()) {
|
---|
| 102 | $body->rewind();
|
---|
| 103 | }
|
---|
| 104 | }
|
---|
| 105 |
|
---|
| 106 | /**
|
---|
| 107 | * Parses an HTTP message into an associative array.
|
---|
| 108 | *
|
---|
| 109 | * The array contains the "start-line" key containing the start line of
|
---|
| 110 | * the message, "headers" key containing an associative array of header
|
---|
| 111 | * array values, and a "body" key containing the body of the message.
|
---|
| 112 | *
|
---|
| 113 | * @param string $message HTTP request or response to parse.
|
---|
| 114 | */
|
---|
| 115 | public static function parseMessage(string $message): array
|
---|
| 116 | {
|
---|
| 117 | if (!$message) {
|
---|
| 118 | throw new \InvalidArgumentException('Invalid message');
|
---|
| 119 | }
|
---|
| 120 |
|
---|
| 121 | $message = ltrim($message, "\r\n");
|
---|
| 122 |
|
---|
| 123 | $messageParts = preg_split("/\r?\n\r?\n/", $message, 2);
|
---|
| 124 |
|
---|
| 125 | if ($messageParts === false || count($messageParts) !== 2) {
|
---|
| 126 | throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
|
---|
| 127 | }
|
---|
| 128 |
|
---|
| 129 | [$rawHeaders, $body] = $messageParts;
|
---|
| 130 | $rawHeaders .= "\r\n"; // Put back the delimiter we split previously
|
---|
| 131 | $headerParts = preg_split("/\r?\n/", $rawHeaders, 2);
|
---|
| 132 |
|
---|
| 133 | if ($headerParts === false || count($headerParts) !== 2) {
|
---|
| 134 | throw new \InvalidArgumentException('Invalid message: Missing status line');
|
---|
| 135 | }
|
---|
| 136 |
|
---|
| 137 | [$startLine, $rawHeaders] = $headerParts;
|
---|
| 138 |
|
---|
| 139 | if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') {
|
---|
| 140 | // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
|
---|
| 141 | $rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders);
|
---|
| 142 | }
|
---|
| 143 |
|
---|
| 144 | /** @var array[] $headerLines */
|
---|
| 145 | $count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER);
|
---|
| 146 |
|
---|
| 147 | // If these aren't the same, then one line didn't match and there's an invalid header.
|
---|
| 148 | if ($count !== substr_count($rawHeaders, "\n")) {
|
---|
| 149 | // Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
|
---|
| 150 | if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) {
|
---|
| 151 | throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
|
---|
| 152 | }
|
---|
| 153 |
|
---|
| 154 | throw new \InvalidArgumentException('Invalid header syntax');
|
---|
| 155 | }
|
---|
| 156 |
|
---|
| 157 | $headers = [];
|
---|
| 158 |
|
---|
| 159 | foreach ($headerLines as $headerLine) {
|
---|
| 160 | $headers[$headerLine[1]][] = $headerLine[2];
|
---|
| 161 | }
|
---|
| 162 |
|
---|
| 163 | return [
|
---|
| 164 | 'start-line' => $startLine,
|
---|
| 165 | 'headers' => $headers,
|
---|
| 166 | 'body' => $body,
|
---|
| 167 | ];
|
---|
| 168 | }
|
---|
| 169 |
|
---|
| 170 | /**
|
---|
| 171 | * Constructs a URI for an HTTP request message.
|
---|
| 172 | *
|
---|
| 173 | * @param string $path Path from the start-line
|
---|
| 174 | * @param array $headers Array of headers (each value an array).
|
---|
| 175 | */
|
---|
| 176 | public static function parseRequestUri(string $path, array $headers): string
|
---|
| 177 | {
|
---|
| 178 | $hostKey = array_filter(array_keys($headers), function ($k) {
|
---|
| 179 | // Numeric array keys are converted to int by PHP.
|
---|
| 180 | $k = (string) $k;
|
---|
| 181 |
|
---|
| 182 | return strtolower($k) === 'host';
|
---|
| 183 | });
|
---|
| 184 |
|
---|
| 185 | // If no host is found, then a full URI cannot be constructed.
|
---|
| 186 | if (!$hostKey) {
|
---|
| 187 | return $path;
|
---|
| 188 | }
|
---|
| 189 |
|
---|
| 190 | $host = $headers[reset($hostKey)][0];
|
---|
| 191 | $scheme = substr($host, -4) === ':443' ? 'https' : 'http';
|
---|
| 192 |
|
---|
| 193 | return $scheme.'://'.$host.'/'.ltrim($path, '/');
|
---|
| 194 | }
|
---|
| 195 |
|
---|
| 196 | /**
|
---|
| 197 | * Parses a request message string into a request object.
|
---|
| 198 | *
|
---|
| 199 | * @param string $message Request message string.
|
---|
| 200 | */
|
---|
| 201 | public static function parseRequest(string $message): RequestInterface
|
---|
| 202 | {
|
---|
| 203 | $data = self::parseMessage($message);
|
---|
| 204 | $matches = [];
|
---|
| 205 | if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) {
|
---|
| 206 | throw new \InvalidArgumentException('Invalid request string');
|
---|
| 207 | }
|
---|
| 208 | $parts = explode(' ', $data['start-line'], 3);
|
---|
| 209 | $version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1';
|
---|
| 210 |
|
---|
| 211 | $request = new Request(
|
---|
| 212 | $parts[0],
|
---|
| 213 | $matches[1] === '/' ? self::parseRequestUri($parts[1], $data['headers']) : $parts[1],
|
---|
| 214 | $data['headers'],
|
---|
| 215 | $data['body'],
|
---|
| 216 | $version
|
---|
| 217 | );
|
---|
| 218 |
|
---|
| 219 | return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]);
|
---|
| 220 | }
|
---|
| 221 |
|
---|
| 222 | /**
|
---|
| 223 | * Parses a response message string into a response object.
|
---|
| 224 | *
|
---|
| 225 | * @param string $message Response message string.
|
---|
| 226 | */
|
---|
| 227 | public static function parseResponse(string $message): ResponseInterface
|
---|
| 228 | {
|
---|
| 229 | $data = self::parseMessage($message);
|
---|
| 230 | // According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
|
---|
| 231 | // the space between status-code and reason-phrase is required. But
|
---|
| 232 | // browsers accept responses without space and reason as well.
|
---|
| 233 | if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) {
|
---|
| 234 | throw new \InvalidArgumentException('Invalid response string: '.$data['start-line']);
|
---|
| 235 | }
|
---|
| 236 | $parts = explode(' ', $data['start-line'], 3);
|
---|
| 237 |
|
---|
| 238 | return new Response(
|
---|
| 239 | (int) $parts[1],
|
---|
| 240 | $data['headers'],
|
---|
| 241 | $data['body'],
|
---|
| 242 | explode('/', $parts[0])[1],
|
---|
| 243 | $parts[2] ?? null
|
---|
| 244 | );
|
---|
| 245 | }
|
---|
| 246 | }
|
---|