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 +114 -14 27.9 → trunk View file →
@@ -15,9 +15,9 @@
15 15 */
16 16 public static function toString(\YoastSEO_Vendor\Psr\Http\Message\MessageInterface $message) : string
17 17 {
18 18 if ($message instanceof \YoastSEO_Vendor\Psr\Http\Message\RequestInterface) {
19 - $msg = \trim($message->getMethod() . ' ' . $message->getRequestTarget()) . ' HTTP/' . $message->getProtocolVersion();
19 + $msg = \trim($message->getMethod() . ' ' . $message->getRequestTarget(), " \n\r\t\x00\v") . ' HTTP/' . $message->getProtocolVersion();
20 20 if (!$message->hasHeader('host')) {
21 21 $msg .= "\r\nHost: " . $message->getUri()->getHost();
22 22 }
23 23 } elseif ($message instanceof \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface) {
@@ -25,9 +25,9 @@
25 25 } else {
26 26 throw new \InvalidArgumentException('Unknown message type');
27 27 }
28 28 foreach ($message->getHeaders() as $name => $values) {
29 - if (\is_string($name) && \strtolower($name) === 'set-cookie') {
29 + if (\is_string($name) && \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower($name) === 'set-cookie') {
30 30 foreach ($values as $value) {
31 31 $msg .= "\r\n{$name}: " . $value;
32 32 }
33 33 } else {
@@ -55,12 +55,15 @@
55 55 return null;
56 56 }
57 57 $body->rewind();
58 58 $summary = $body->read($truncateAt);
59 - $body->rewind();
60 59 if ($size > $truncateAt) {
60 + if (\preg_match('//u', $summary) !== 1) {
61 + $summary = self::trimTrailingIncompleteUtf8Character($summary, $body->read(3));
62 + }
61 63 $summary .= ' (truncated...)';
62 64 }
65 + $body->rewind();
63 66 // Matches any printable character, including unicode characters:
64 67 // letters, marks, numbers, punctuation, spacing, and separators.
65 68 if (\preg_match('/[^\\pL\\pM\\pN\\pP\\pS\\pZ\\n\\r\\t]/u', $summary) !== 0) {
66 69 return null;
@@ -67,8 +70,48 @@
67 70 }
68 71 return $summary;
69 72 }
70 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 + /**
71 114 * Attempts to rewind a message body and throws an exception on failure.
72 115 *
73 116 * The body of the message will only be rewound if a call to `tell()`
74 117 * returns a value other than `0`.
@@ -99,9 +142,12 @@
99 142 throw new \InvalidArgumentException('Invalid message');
100 143 }
101 144 $message = \ltrim($message, "\r\n");
102 145 $messageParts = \preg_split("/\r?\n\r?\n/", $message, 2);
103 - 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) {
104 150 throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
105 151 }
106 152 [$rawHeaders, $body] = $messageParts;
107 153 $rawHeaders .= "\r\n";
@@ -106,22 +152,39 @@
106 152 [$rawHeaders, $body] = $messageParts;
107 153 $rawHeaders .= "\r\n";
108 154 // Put back the delimiter we split previously
109 155 $headerParts = \preg_split("/\r?\n/", $rawHeaders, 2);
110 - 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) {
111 160 throw new \InvalidArgumentException('Invalid message: Missing status line');
112 161 }
113 162 [$startLine, $rawHeaders] = $headerParts;
114 - if (\preg_match("/(?:^HTTP\\/|^[A-Z]+ \\S+ HTTP\\/)(\\d+(?:\\.\\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') {
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') {
115 168 // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
116 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 + }
117 173 }
118 174 /** @var array[] $headerLines */
119 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 + }
120 179 // If these aren't the same, then one line didn't match and there's an invalid header.
121 180 if ($count !== \substr_count($rawHeaders, "\n")) {
122 181 // Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
123 - if (\preg_match(\YoastSEO_Vendor\GuzzleHttp\Psr7\Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) {
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) {
124 187 throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
125 188 }
126 189 throw new \InvalidArgumentException('Invalid header syntax');
127 190 }
@@ -138,20 +201,43 @@
138 201 * @param array $headers Array of headers (each value an array).
139 202 */
140 203 public static function parseRequestUri(string $path, array $headers) : string
141 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 + {
142 227 $hostKey = \array_filter(\array_keys($headers), function ($k) {
143 228 // Numeric array keys are converted to int by PHP.
144 229 $k = (string) $k;
145 - return \strtolower($k) === 'host';
230 + return \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::asciiToLower($k) === 'host';
146 231 });
147 - // If no host is found, then a full URI cannot be constructed.
148 232 if (!$hostKey) {
149 - return $path;
233 + return null;
150 234 }
151 235 $host = $headers[\reset($hostKey)][0];
152 - $scheme = \substr($host, -4) === ':443' ? 'https' : 'http';
153 - 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;
154 240 }
155 241 /**
156 242 * Parses a request message string into a request object.
157 243 *
@@ -159,10 +245,17 @@
159 245 */
160 246 public static function parseRequest(string $message) : \YoastSEO_Vendor\Psr\Http\Message\RequestInterface
161 247 {
162 248 $data = self::parseMessage($message);
249 + if (\strpbrk($data['start-line'], "\r\n") !== \false) {
250 + throw new \InvalidArgumentException('Invalid request string');
251 + }
163 252 $matches = [];
164 - 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) {
165 258 throw new \InvalidArgumentException('Invalid request string');
166 259 }
167 260 $parts = \explode(' ', $data['start-line'], 3);
168 261 $version = isset($parts[2]) ? \explode('/', $parts[2])[1] : '1.1';
@@ -176,12 +269,19 @@
176 269 */
177 270 public static function parseResponse(string $message) : \YoastSEO_Vendor\Psr\Http\Message\ResponseInterface
178 271 {
179 272 $data = self::parseMessage($message);
273 + if (\strpbrk($data['start-line'], "\r\n") !== \false) {
274 + throw new \InvalidArgumentException('Invalid response string');
275 + }
180 276 // According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
181 277 // the space between status-code and reason-phrase is required. But
182 278 // browsers accept responses without space and reason as well.
183 - if (!\preg_match('/^HTTP\\/.* [0-9]{3}( .*|$)/', $data['start-line'])) {
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) {
184 284 throw new \InvalidArgumentException('Invalid response string: ' . $data['start-line']);
185 285 }
186 286 $parts = \explode(' ', $data['start-line'], 3);
187 287 return new \YoastSEO_Vendor\GuzzleHttp\Psr7\Response((int) $parts[1], $data['headers'], $data['body'], \explode('/', $parts[0])[1], $parts[2] ?? null);