PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
← All changes | vendor_prefixed/guzzlehttp/psr7/src/Message.php +133 -41 18.5 → trunk View file →
@@ -1,6 +1,7 @@
1 1 <?php
2 2
3 +declare (strict_types=1);
3 4 namespace YoastSEO_Vendor\GuzzleHttp\Psr7;
4 5
5 6 use YoastSEO_Vendor\Psr\Http\Message\MessageInterface;
6 7 use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
@@ -10,15 +11,13 @@
10 11 /**
11 12 * Returns the string representation of an HTTP message.
12 13 *
13 14 * @param MessageInterface $message Message to convert to a string.
14 - *
15 - * @return string
16 15 */
17 - public static function toString(\YoastSEO_Vendor\Psr\Http\Message\MessageInterface $message)
16 + public static function toString(\YoastSEO_Vendor\Psr\Http\Message\MessageInterface $message) : string
18 17 {
19 18 if ($message instanceof \YoastSEO_Vendor\Psr\Http\Message\RequestInterface) {
20 - $msg = \trim($message->getMethod() . ' ' . $message->getRequestTarget()) . ' HTTP/' . $message->getProtocolVersion();
19 + $msg = \trim($message->getMethod() . ' ' . $message->getRequestTarget(), " \n\r\t\x00\v") . ' HTTP/' . $message->getProtocolVersion();
21 20 if (!$message->hasHeader('host')) {
22 21 $msg .= "\r\nHost: " . $message->getUri()->getHost();
23 22 }
24 23 } elseif ($message instanceof \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface) {
@@ -26,9 +25,9 @@
26 25 } else {
27 26 throw new \InvalidArgumentException('Unknown message type');
28 27 }
29 28 foreach ($message->getHeaders() as $name => $values) {
30 - if (\strtolower($name) === 'set-cookie') {
29 + if (\is_string($name) && \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower($name) === 'set-cookie') {
31 30 foreach ($values as $value) {
32 31 $msg .= "\r\n{$name}: " . $value;
33 32 }
34 33 } else {
@@ -43,12 +42,10 @@
43 42 * Will return `null` if the response is not printable.
44 43 *
45 44 * @param MessageInterface $message The message to get the body summary
46 45 * @param int $truncateAt The maximum allowed size of the summary
47 - *
48 - * @return string|null
49 46 */
50 - public static function bodySummary(\YoastSEO_Vendor\Psr\Http\Message\MessageInterface $message, $truncateAt = 120)
47 + public static function bodySummary(\YoastSEO_Vendor\Psr\Http\Message\MessageInterface $message, int $truncateAt = 120) : ?string
51 48 {
52 49 $body = $message->getBody();
53 50 if (!$body->isSeekable() || !$body->isReadable()) {
54 51 return null;
@@ -56,21 +53,65 @@
56 53 $size = $body->getSize();
57 54 if ($size === 0) {
58 55 return null;
59 56 }
57 + $body->rewind();
60 58 $summary = $body->read($truncateAt);
61 - $body->rewind();
62 59 if ($size > $truncateAt) {
60 + if (\preg_match('//u', $summary) !== 1) {
61 + $summary = self::trimTrailingIncompleteUtf8Character($summary, $body->read(3));
62 + }
63 63 $summary .= ' (truncated...)';
64 64 }
65 + $body->rewind();
65 66 // Matches any printable character, including unicode characters:
66 67 // letters, marks, numbers, punctuation, spacing, and separators.
67 - if (\preg_match('/[^\\pL\\pM\\pN\\pP\\pS\\pZ\\n\\r\\t]/u', $summary)) {
68 + if (\preg_match('/[^\\pL\\pM\\pN\\pP\\pS\\pZ\\n\\r\\t]/u', $summary) !== 0) {
68 69 return null;
69 70 }
70 71 return $summary;
71 72 }
72 73 /**
74 + * Trims a partial UTF-8 character from the end of a truncated string.
75 + */
76 + private static function trimTrailingIncompleteUtf8Character(string $summary, string $lookahead) : string
77 + {
78 + $length = \strlen($summary);
79 + if ($length === 0) {
80 + return $summary;
81 + }
82 + $start = $length - 1;
83 + while ($start >= 0) {
84 + $byte = \ord($summary[$start]);
85 + if ($byte < 0x80 || $byte > 0xbf) {
86 + break;
87 + }
88 + --$start;
89 + }
90 + if ($start < 0) {
91 + return $summary;
92 + }
93 + $lead = \ord($summary[$start]);
94 + if ($lead >= 0xc2 && $lead <= 0xdf) {
95 + $expectedLength = 2;
96 + } elseif ($lead >= 0xe0 && $lead <= 0xef) {
97 + $expectedLength = 3;
98 + } elseif ($lead >= 0xf0 && $lead <= 0xf4) {
99 + $expectedLength = 4;
100 + } else {
101 + return $summary;
102 + }
103 + $availableLength = $length - $start;
104 + if ($availableLength >= $expectedLength) {
105 + return $summary;
106 + }
107 + $sequence = \substr($summary, $start) . \substr($lookahead, 0, $expectedLength - $availableLength);
108 + if (\strlen($sequence) !== $expectedLength || \preg_match('//u', $sequence) !== 1) {
109 + return $summary;
110 + }
111 + return \substr($summary, 0, $start);
112 + }
113 + /**
73 114 * Attempts to rewind a message body and throws an exception on failure.
74 115 *
75 116 * The body of the message will only be rewound if a call to `tell()`
76 117 * returns a value other than `0`.
@@ -78,9 +119,9 @@
78 119 * @param MessageInterface $message Message to rewind
79 120 *
80 121 * @throws \RuntimeException
81 122 */
82 - public static function rewindBody(\YoastSEO_Vendor\Psr\Http\Message\MessageInterface $message)
123 + public static function rewindBody(\YoastSEO_Vendor\Psr\Http\Message\MessageInterface $message) : void
83 124 {
84 125 $body = $message->getBody();
85 126 if ($body->tell()) {
86 127 $body->rewind();
@@ -93,12 +134,10 @@
93 134 * the message, "headers" key containing an associative array of header
94 135 * array values, and a "body" key containing the body of the message.
95 136 *
96 137 * @param string $message HTTP request or response to parse.
97 - *
98 - * @return array
99 138 */
100 - public static function parseMessage($message)
139 + public static function parseMessage(string $message) : array
101 140 {
102 141 if (!$message) {
103 142 throw new \InvalidArgumentException('Invalid message');
104 143 }
@@ -103,29 +142,49 @@
103 142 throw new \InvalidArgumentException('Invalid message');
104 143 }
105 144 $message = \ltrim($message, "\r\n");
106 145 $messageParts = \preg_split("/\r?\n\r?\n/", $message, 2);
107 - if ($messageParts === \false || \count($messageParts) !== 2) {
146 + if ($messageParts === \false) {
147 + throw new \RuntimeException('Unable to split HTTP message: ' . \preg_last_error_msg());
148 + }
149 + if (\count($messageParts) !== 2) {
108 150 throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
109 151 }
110 - list($rawHeaders, $body) = $messageParts;
152 + [$rawHeaders, $body] = $messageParts;
111 153 $rawHeaders .= "\r\n";
112 154 // Put back the delimiter we split previously
113 155 $headerParts = \preg_split("/\r?\n/", $rawHeaders, 2);
114 - if ($headerParts === \false || \count($headerParts) !== 2) {
156 + if ($headerParts === \false) {
157 + throw new \RuntimeException('Unable to split HTTP message headers: ' . \preg_last_error_msg());
158 + }
159 + if (\count($headerParts) !== 2) {
115 160 throw new \InvalidArgumentException('Invalid message: Missing status line');
116 161 }
117 - list($startLine, $rawHeaders) = $headerParts;
118 - if (\preg_match("/(?:^HTTP\\/|^[A-Z]+ \\S+ HTTP\\/)(\\d+(?:\\.\\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') {
162 + [$startLine, $rawHeaders] = $headerParts;
163 + $versionMatch = \preg_match("/(?:^HTTP\\/|^[A-Z]+ \\S+ HTTP\\/)(\\d+(?:\\.\\d+)?)/i", $startLine, $matches);
164 + if ($versionMatch === \false) {
165 + throw new \RuntimeException('Unable to parse HTTP start line: ' . \preg_last_error_msg());
166 + }
167 + if ($versionMatch === 1 && $matches[1] === '1.0') {
119 168 // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
120 169 $rawHeaders = \preg_replace(\YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders);
170 + if ($rawHeaders === null) {
171 + throw new \RuntimeException('Unable to unfold HTTP headers: ' . \preg_last_error_msg());
172 + }
121 173 }
122 174 /** @var array[] $headerLines */
123 175 $count = \preg_match_all(\YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, \PREG_SET_ORDER);
176 + if ($count === \false) {
177 + throw new \RuntimeException('Unable to parse HTTP headers: ' . \preg_last_error_msg());
178 + }
124 179 // If these aren't the same, then one line didn't match and there's an invalid header.
125 180 if ($count !== \substr_count($rawHeaders, "\n")) {
126 - // Folding is deprecated, see https://tools.ietf.org/html/rfc7230#section-3.2.4
127 - if (\preg_match(\YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) {
181 + // Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
182 + $hasFoldedHeader = \preg_match(\YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc7230::HEADER_FOLD_REGEX, $rawHeaders);
183 + if ($hasFoldedHeader === \false) {
184 + throw new \RuntimeException('Unable to inspect HTTP header folding: ' . \preg_last_error_msg());
185 + }
186 + if ($hasFoldedHeader === 1) {
128 187 throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
129 188 }
130 189 throw new \InvalidArgumentException('Invalid header syntax');
131 190 }
@@ -139,36 +198,64 @@
139 198 * Constructs a URI for an HTTP request message.
140 199 *
141 200 * @param string $path Path from the start-line
142 201 * @param array $headers Array of headers (each value an array).
143 - *
144 - * @return string
145 202 */
146 - public static function parseRequestUri($path, array $headers)
203 + public static function parseRequestUri(string $path, array $headers) : string
147 204 {
205 + $host = self::getHostFromHeaders($headers);
206 + // If no host is found, then a full URI cannot be constructed.
207 + // Collapse leading slashes so an origin-form target cannot be
208 + // parsed as a network-path reference with its own authority.
209 + if ($host === null) {
210 + return self::normalizePathForOriginForm($path);
211 + }
212 + $scheme = \substr($host, -4) === ':443' ? 'https' : 'http';
213 + return $scheme . '://' . $host . '/' . \ltrim($path, '/');
214 + }
215 + private static function normalizePathForOriginForm(string $path) : string
216 + {
217 + if (0 === \strpos($path, '//')) {
218 + return '/' . \ltrim($path, '/');
219 + }
220 + return $path;
221 + }
222 + /**
223 + * @param array $headers Array of headers (each value an array).
224 + */
225 + private static function getHostFromHeaders(array $headers) : ?string
226 + {
148 227 $hostKey = \array_filter(\array_keys($headers), function ($k) {
149 - return \strtolower($k) === 'host';
228 + // Numeric array keys are converted to int by PHP.
229 + $k = (string) $k;
230 + return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower($k) === 'host';
150 231 });
151 - // If no host is found, then a full URI cannot be constructed.
152 232 if (!$hostKey) {
153 - return $path;
233 + return null;
154 234 }
155 235 $host = $headers[\reset($hostKey)][0];
156 - $scheme = \substr($host, -4) === ':443' ? 'https' : 'http';
157 - return $scheme . '://' . $host . '/' . \ltrim($path, '/');
236 + if (!\is_string($host) || \YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc7230::parseHostHeader($host) === null) {
237 + throw new \InvalidArgumentException('Invalid request string');
238 + }
239 + return $host;
158 240 }
159 241 /**
160 242 * Parses a request message string into a request object.
161 243 *
162 244 * @param string $message Request message string.
163 - *
164 - * @return Request
165 245 */
166 - public static function parseRequest($message)
246 + public static function parseRequest(string $message) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface
167 247 {
168 248 $data = self::parseMessage($message);
249 + if (\strpbrk($data['start-line'], "\r\n") !== \false) {
250 + throw new \InvalidArgumentException('Invalid request string');
251 + }
169 252 $matches = [];
170 - if (!\preg_match('/^[\\S]+\\s+([a-zA-Z]+:\\/\\/|\\/).*/', $data['start-line'], $matches)) {
253 + $requestStartLineMatch = \preg_match('/^[\\S]+\\s+([a-zA-Z]+:\\/\\/|\\/).*/', $data['start-line'], $matches);
254 + if ($requestStartLineMatch === \false) {
255 + throw new \RuntimeException('Unable to parse request start line: ' . \preg_last_error_msg());
256 + }
257 + if ($requestStartLineMatch === 0) {
171 258 throw new \InvalidArgumentException('Invalid request string');
172 259 }
173 260 $parts = \explode(' ', $data['start-line'], 3);
174 261 $version = isset($parts[2]) ? \explode('/', $parts[2])[1] : '1.1';
@@ -178,20 +265,25 @@
178 265 /**
179 266 * Parses a response message string into a response object.
180 267 *
181 268 * @param string $message Response message string.
182 - *
183 - * @return Response
184 269 */
185 - public static function parseResponse($message)
270 + public static function parseResponse(string $message) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface
186 271 {
187 272 $data = self::parseMessage($message);
188 - // According to https://tools.ietf.org/html/rfc7230#section-3.1.2 the space
189 - // between status-code and reason-phrase is required. But browsers accept
190 - // responses without space and reason as well.
191 - if (!\preg_match('/^HTTP\\/.* [0-9]{3}( .*|$)/', $data['start-line'])) {
273 + if (\strpbrk($data['start-line'], "\r\n") !== \false) {
274 + throw new \InvalidArgumentException('Invalid response string');
275 + }
276 + // According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
277 + // the space between status-code and reason-phrase is required. But
278 + // browsers accept responses without space and reason as well.
279 + $responseStartLineMatch = \preg_match('/^HTTP\\/.* [0-9]{3}( .*|$)/D', $data['start-line']);
280 + if ($responseStartLineMatch === \false) {
281 + throw new \RuntimeException('Unable to parse response start line: ' . \preg_last_error_msg());
282 + }
283 + if ($responseStartLineMatch === 0) {
192 284 throw new \InvalidArgumentException('Invalid response string: ' . $data['start-line']);
193 285 }
194 286 $parts = \explode(' ', $data['start-line'], 3);
195 - return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Response((int) $parts[1], $data['headers'], $data['body'], \explode('/', $parts[0])[1], isset($parts[2]) ? $parts[2] : null);
287 + return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Response((int) $parts[1], $data['headers'], $data['body'], \explode('/', $parts[0])[1], $parts[2] ?? null);
196 288 }
197 289 }