[BUGFIX] Use proper phpDoc in PSR-7 implementation
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Http / Message.php
1 <?php
2 namespace TYPO3\CMS\Core\Http;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Psr\Http\Message\MessageInterface;
18 use Psr\Http\Message\StreamInterface;
19
20 /**
21 * Default implementation for the MessageInterface of the PSR-7 standard
22 * It is the base for any request or response for PSR-7.
23 *
24 * Highly inspired by https://github.com/phly/http/
25 *
26 * @internal Note that this is not public API yet.
27 */
28 class Message implements MessageInterface
29 {
30 /**
31 * The HTTP Protocol version, defaults to 1.1
32 * @var string
33 */
34 protected $protocolVersion = '1.1';
35
36 /**
37 * Associative array containing all headers of this Message
38 * This is a mixed-case list of the headers (as due to the specification)
39 * @var array
40 */
41 protected $headers = [];
42
43 /**
44 * Lowercased version of all headers, in order to check if a header is set or not
45 * this way a lot of checks are easier to be set
46 * @var array
47 */
48 protected $lowercasedHeaderNames = [];
49
50 /**
51 * The body as a Stream object
52 * @var StreamInterface
53 */
54 protected $body;
55
56 /**
57 * Retrieves the HTTP protocol version as a string.
58 *
59 * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
60 *
61 * @return string HTTP protocol version.
62 */
63 public function getProtocolVersion()
64 {
65 return $this->protocolVersion;
66 }
67
68 /**
69 * Return an instance with the specified HTTP protocol version.
70 *
71 * The version string MUST contain only the HTTP version number (e.g.,
72 * "1.1", "1.0").
73 *
74 * This method MUST be implemented in such a way as to retain the
75 * immutability of the message, and MUST return an instance that has the
76 * new protocol version.
77 *
78 * @param string $version HTTP protocol version
79 * @return static
80 */
81 public function withProtocolVersion($version)
82 {
83 $clonedObject = clone $this;
84 $clonedObject->protocolVersion = $version;
85 return $clonedObject;
86 }
87
88 /**
89 * Retrieves all message header values.
90 *
91 * The keys represent the header name as it will be sent over the wire, and
92 * each value is an array of strings associated with the header.
93 *
94 * // Represent the headers as a string
95 * foreach ($message->getHeaders() as $name => $values) {
96 * echo $name . ": " . implode(", ", $values);
97 * }
98 *
99 * // Emit headers iteratively:
100 * foreach ($message->getHeaders() as $name => $values) {
101 * foreach ($values as $value) {
102 * header(sprintf('%s: %s', $name, $value), false);
103 * }
104 * }
105 *
106 * While header names are not case-sensitive, getHeaders() will preserve the
107 * exact case in which headers were originally specified.
108 *
109 * @return array Returns an associative array of the message's headers. Each
110 * key MUST be a header name, and each value MUST be an array of strings
111 * for that header.
112 */
113 public function getHeaders()
114 {
115 return $this->headers;
116 }
117
118 /**
119 * Checks if a header exists by the given case-insensitive name.
120 *
121 * @param string $name Case-insensitive header field name.
122 * @return bool Returns true if any header names match the given header
123 * name using a case-insensitive string comparison. Returns false if
124 * no matching header name is found in the message.
125 */
126 public function hasHeader($name)
127 {
128 return isset($this->lowercasedHeaderNames[strtolower($name)]);
129 }
130
131 /**
132 * Retrieves a message header value by the given case-insensitive name.
133 *
134 * This method returns an array of all the header values of the given
135 * case-insensitive header name.
136 *
137 * If the header does not appear in the message, this method MUST return an
138 * empty array.
139 *
140 * @param string $name Case-insensitive header field name.
141 * @return string[] An array of string values as provided for the given
142 * header. If the header does not appear in the message, this method MUST
143 * return an empty array.
144 */
145 public function getHeader($name)
146 {
147 if (!$this->hasHeader($name)) {
148 return [];
149 }
150 $header = $this->lowercasedHeaderNames[strtolower($name)];
151 $headerValue = $this->headers[$header];
152 if (is_array($headerValue)) {
153 return $headerValue;
154 }
155 return [$headerValue];
156 }
157
158 /**
159 * Retrieves a comma-separated string of the values for a single header.
160 *
161 * This method returns all of the header values of the given
162 * case-insensitive header name as a string concatenated together using
163 * a comma.
164 *
165 * NOTE: Not all header values may be appropriately represented using
166 * comma concatenation. For such headers, use getHeader() instead
167 * and supply your own delimiter when concatenating.
168 *
169 * If the header does not appear in the message, this method MUST return
170 * an empty string.
171 *
172 * @param string $name Case-insensitive header field name.
173 * @return string A string of values as provided for the given header
174 * concatenated together using a comma. If the header does not appear in
175 * the message, this method MUST return an empty string.
176 */
177 public function getHeaderLine($name)
178 {
179 $headerValue = $this->getHeader($name);
180 if (empty($headerValue)) {
181 return '';
182 }
183 return implode(',', $headerValue);
184 }
185
186 /**
187 * Return an instance with the provided value replacing the specified header.
188 *
189 * While header names are case-insensitive, the casing of the header will
190 * be preserved by this function, and returned from getHeaders().
191 *
192 * This method MUST be implemented in such a way as to retain the
193 * immutability of the message, and MUST return an instance that has the
194 * new and/or updated header and value.
195 *
196 * @param string $name Case-insensitive header field name.
197 * @param string|string[] $value Header value(s).
198 * @return static
199 * @throws \InvalidArgumentException for invalid header names or values.
200 */
201 public function withHeader($name, $value)
202 {
203 if (is_string($value)) {
204 $value = [$value];
205 }
206
207 if (!is_array($value) || !$this->arrayContainsOnlyStrings($value)) {
208 throw new \InvalidArgumentException('Invalid header value for header "' . $name . '"". The value must be a string or an array of strings.', 1436717266);
209 }
210
211 $this->validateHeaderName($name);
212 $this->validateHeaderValues($value);
213 $lowercasedHeaderName = strtolower($name);
214
215 $clonedObject = clone $this;
216 $clonedObject->headers[$name] = $value;
217 $clonedObject->lowercasedHeaderNames[$lowercasedHeaderName] = $name;
218 return $clonedObject;
219 }
220
221 /**
222 * Return an instance with the specified header appended with the given value.
223 *
224 * Existing values for the specified header will be maintained. The new
225 * value(s) will be appended to the existing list. If the header did not
226 * exist previously, it will be added.
227 *
228 * This method MUST be implemented in such a way as to retain the
229 * immutability of the message, and MUST return an instance that has the
230 * new header and/or value.
231 *
232 * @param string $name Case-insensitive header field name to add.
233 * @param string|string[] $value Header value(s).
234 * @return static
235 * @throws \InvalidArgumentException for invalid header names or values.
236 */
237 public function withAddedHeader($name, $value)
238 {
239 if (is_string($value)) {
240 $value = [$value];
241 }
242 if (!is_array($value) || !$this->arrayContainsOnlyStrings($value)) {
243 throw new \InvalidArgumentException('Invalid header value for header "' . $name . '". The header value must be a string or array of strings', 1436717267);
244 }
245 $this->validateHeaderName($name);
246 $this->validateHeaderValues($value);
247 if (!$this->hasHeader($name)) {
248 return $this->withHeader($name, $value);
249 }
250 $name = $this->lowercasedHeaderNames[strtolower($name)];
251 $clonedObject = clone $this;
252 $clonedObject->headers[$name] = array_merge($this->headers[$name], $value);
253 return $clonedObject;
254 }
255
256 /**
257 * Return an instance without the specified header.
258 *
259 * Header resolution MUST be done without case-sensitivity.
260 *
261 * This method MUST be implemented in such a way as to retain the
262 * immutability of the message, and MUST return an instance that removes
263 * the named header.
264 *
265 * @param string $name Case-insensitive header field name to remove.
266 * @return static
267 */
268 public function withoutHeader($name)
269 {
270 if (!$this->hasHeader($name)) {
271 return clone $this;
272 }
273 // fetch the original header from the lowercased version
274 $lowercasedHeader = strtolower($name);
275 $name = $this->lowercasedHeaderNames[$lowercasedHeader];
276 $clonedObject = clone $this;
277 unset($clonedObject->headers[$name], $clonedObject->lowercasedHeaderNames[$lowercasedHeader]);
278 return $clonedObject;
279 }
280
281 /**
282 * Gets the body of the message.
283 *
284 * @return \Psr\Http\Message\StreamInterface Returns the body as a stream.
285 */
286 public function getBody()
287 {
288 return $this->body;
289 }
290
291 /**
292 * Return an instance with the specified message body.
293 *
294 * The body MUST be a StreamInterface object.
295 *
296 * This method MUST be implemented in such a way as to retain the
297 * immutability of the message, and MUST return a new instance that has the
298 * new body stream.
299 *
300 * @param \Psr\Http\Message\StreamInterface $body Body.
301 * @return static
302 * @throws \InvalidArgumentException When the body is not valid.
303 */
304 public function withBody(StreamInterface $body)
305 {
306 $clonedObject = clone $this;
307 $clonedObject->body = $body;
308 return $clonedObject;
309 }
310
311 /**
312 * Ensure header names and values are valid.
313 *
314 * @param array $headers
315 * @throws \InvalidArgumentException
316 */
317 protected function assertHeaders(array $headers)
318 {
319 foreach ($headers as $name => $headerValues) {
320 $this->validateHeaderName($name);
321 // check if all values are correct
322 array_walk($headerValues, function ($value, $key, Message $messageObject) {
323 if (!$messageObject->isValidHeaderValue($value)) {
324 throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717268);
325 }
326 }, $this);
327 }
328 }
329
330 /**
331 * Filter a set of headers to ensure they are in the correct internal format.
332 *
333 * Used by message constructors to allow setting all initial headers at once.
334 *
335 * @param array $originalHeaders Headers to filter.
336 * @return array Filtered headers and names.
337 */
338 protected function filterHeaders(array $originalHeaders)
339 {
340 $headerNames = $headers = [];
341 foreach ($originalHeaders as $header => $value) {
342 if (!is_string($header) || (!is_array($value) && !is_string($value))) {
343 continue;
344 }
345 if (!is_array($value)) {
346 $value = [$value];
347 }
348 $headerNames[strtolower($header)] = $header;
349 $headers[$header] = $value;
350 }
351 return [$headerNames, $headers];
352 }
353
354 /**
355 * Helper function to test if an array contains only strings
356 *
357 * @param array $data
358 * @return bool
359 */
360 protected function arrayContainsOnlyStrings(array $data)
361 {
362 return array_reduce($data, function ($original, $item) {
363 return is_string($item) ? $original : false;
364 }, true);
365 }
366
367 /**
368 * Assert that the provided header values are valid.
369 *
370 * @see http://tools.ietf.org/html/rfc7230#section-3.2
371 * @param string[] $values
372 * @throws \InvalidArgumentException
373 */
374 protected function validateHeaderValues(array $values)
375 {
376 array_walk($values, function ($value, $key, Message $messageObject) {
377 if (!$messageObject->isValidHeaderValue($value)) {
378 throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717269);
379 }
380 }, $this);
381 }
382
383 /**
384 * Filter a header value
385 *
386 * Ensures CRLF header injection vectors are filtered.
387 *
388 * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
389 * tabs are allowed in values; header continuations MUST consist of
390 * a single CRLF sequence followed by a space or horizontal tab.
391 *
392 * This method filters any values not allowed from the string, and is
393 * lossy.
394 *
395 * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
396 * @param string $value
397 * @return string
398 */
399 public function filter($value)
400 {
401 $value = (string)$value;
402 $length = strlen($value);
403 $string = '';
404 for ($i = 0; $i < $length; $i += 1) {
405 $ascii = ord($value[$i]);
406
407 // Detect continuation sequences
408 if ($ascii === 13) {
409 $lf = ord($value[$i + 1]);
410 $ws = ord($value[$i + 2]);
411 if ($lf === 10 && in_array($ws, [9, 32], true)) {
412 $string .= $value[$i] . $value[$i + 1];
413 $i += 1;
414 }
415 continue;
416 }
417
418 // Non-visible, non-whitespace characters
419 // 9 === horizontal tab
420 // 32-126, 128-254 === visible
421 // 127 === DEL
422 // 255 === null byte
423 if (($ascii < 32 && $ascii !== 9) || $ascii === 127 || $ascii > 254) {
424 continue;
425 }
426
427 $string .= $value[$i];
428 }
429
430 return $string;
431 }
432
433 /**
434 * Check whether or not a header name is valid and throw an exception.
435 *
436 * @see http://tools.ietf.org/html/rfc7230#section-3.2
437 * @param string $name
438 * @throws \InvalidArgumentException
439 */
440 public function validateHeaderName($name)
441 {
442 if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) {
443 throw new \InvalidArgumentException('Invalid header name, given "' . $name . '"', 1436717270);
444 }
445 }
446
447 /**
448 * Checks if an HTTP header value is valid.
449 *
450 * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
451 * tabs are allowed in values; header continuations MUST consist of
452 * a single CRLF sequence followed by a space or horizontal tab.
453 *
454 * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
455 * @param string $value
456 * @return bool
457 */
458 public function isValidHeaderValue($value)
459 {
460 $value = (string)$value;
461
462 // Any occurrence of \r or \n is invalid
463 if (strpbrk($value, "\r\n") !== false) {
464 return false;
465 }
466
467 $length = strlen($value);
468 for ($i = 0; $i < $length; $i += 1) {
469 $ascii = ord($value[$i]);
470
471 // Non-visible, non-whitespace characters
472 // 9 === horizontal tab
473 // 10 === line feed
474 // 13 === carriage return
475 // 32-126, 128-254 === visible
476 // 127 === DEL
477 // 255 === null byte
478 if (($ascii < 32 && !in_array($ascii, [9, 10, 13], true)) || $ascii === 127 || $ascii > 254) {
479 return false;
480 }
481 }
482
483 return true;
484 }
485 }