source: vendor/guzzlehttp/psr7/src/Message.php@ f9c482b

Last change on this file since f9c482b was f9c482b, checked in by Vlado 222039 <vlado.popovski@…>, 7 days ago

Upload new project files

  • Property mode set to 100644
File size: 8.1 KB
Line 
1<?php
2
3declare(strict_types=1);
4
5namespace GuzzleHttp\Psr7;
6
7use Psr\Http\Message\MessageInterface;
8use Psr\Http\Message\RequestInterface;
9use Psr\Http\Message\ResponseInterface;
10
11final 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}
Note: See TracBrowser for help on using the repository browser.