68083ed282918d44bdc9aab7f14d63c38ef1c083
[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 Message
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 } else {
155 return [$headerValue];
156 }
157 }
158
159 /**
160 * Retrieves a comma-separated string of the values for a single header.
161 *
162 * This method returns all of the header values of the given
163 * case-insensitive header name as a string concatenated together using
164 * a comma.
165 *
166 * NOTE: Not all header values may be appropriately represented using
167 * comma concatenation. For such headers, use getHeader() instead
168 * and supply your own delimiter when concatenating.
169 *
170 * If the header does not appear in the message, this method MUST return
171 * an empty string.
172 *
173 * @param string $name Case-insensitive header field name.
174 * @return string A string of values as provided for the given header
175 * concatenated together using a comma. If the header does not appear in
176 * the message, this method MUST return an empty string.
177 */
178 public function getHeaderLine($name)
179 {
180 $headerValue = $this->getHeader($name);
181 if (empty($headerValue)) {
182 return '';
183 }
184 return implode(',', $headerValue);
185 }
186
187 /**
188 * Return an instance with the provided value replacing the specified header.
189 *
190 * While header names are case-insensitive, the casing of the header will
191 * be preserved by this function, and returned from getHeaders().
192 *
193 * This method MUST be implemented in such a way as to retain the
194 * immutability of the message, and MUST return an instance that has the
195 * new and/or updated header and value.
196 *
197 * @param string $name Case-insensitive header field name.
198 * @param string|string[] $value Header value(s).
199 * @return Message
200 * @throws \InvalidArgumentException for invalid header names or values.
201 */
202 public function withHeader($name, $value)
203 {
204 if (is_string($value)) {
205 $value = [$value];
206 }
207
208 if (!is_array($value) || !$this->arrayContainsOnlyStrings($value)) {
209 throw new \InvalidArgumentException('Invalid header value for header "' . $name . '"". The value must be a string or an array of strings.', 1436717266);
210 }
211
212 $this->validateHeaderName($name);
213 $this->validateHeaderValues($value);
214 $lowercasedHeaderName = strtolower($name);
215
216 $clonedObject = clone $this;
217 $clonedObject->headers[$name] = $value;
218 $clonedObject->lowercasedHeaderNames[$lowercasedHeaderName] = $name;
219 return $clonedObject;
220 }
221
222 /**
223 * Return an instance with the specified header appended with the given value.
224 *
225 * Existing values for the specified header will be maintained. The new
226 * value(s) will be appended to the existing list. If the header did not
227 * exist previously, it will be added.
228 *
229 * This method MUST be implemented in such a way as to retain the
230 * immutability of the message, and MUST return an instance that has the
231 * new header and/or value.
232 *
233 * @param string $name Case-insensitive header field name to add.
234 * @param string|string[] $value Header value(s).
235 * @return Message
236 * @throws \InvalidArgumentException for invalid header names or values.
237 */
238 public function withAddedHeader($name, $value)
239 {
240 if (is_string($value)) {
241 $value = [$value];
242 }
243 if (!is_array($value) || !$this->arrayContainsOnlyStrings($value)) {
244 throw new \InvalidArgumentException('Invalid header value for header "' . $name . '". The header value must be a string or array of strings', 1436717267);
245 }
246 $this->validateHeaderName($name);
247 $this->validateHeaderValues($value);
248 if (!$this->hasHeader($name)) {
249 return $this->withHeader($name, $value);
250 }
251 $name = $this->lowercasedHeaderNames[strtolower($name)];
252 $clonedObject = clone $this;
253 $clonedObject->headers[$name] = array_merge($this->headers[$name], $value);
254 return $clonedObject;
255 }
256
257 /**
258 * Return an instance without the specified header.
259 *
260 * Header resolution MUST be done without case-sensitivity.
261 *
262 * This method MUST be implemented in such a way as to retain the
263 * immutability of the message, and MUST return an instance that removes
264 * the named header.
265 *
266 * @param string $name Case-insensitive header field name to remove.
267 * @return Message
268 */
269 public function withoutHeader($name)
270 {
271 if (!$this->hasHeader($name)) {
272 return clone $this;
273 }
274 // fetch the original header from the lowercased version
275 $lowercasedHeader = strtolower($name);
276 $name = $this->lowercasedHeaderNames[$lowercasedHeader];
277 $clonedObject = clone $this;
278 unset($clonedObject->headers[$name], $clonedObject->lowercasedHeaderNames[$lowercasedHeader]);
279 return $clonedObject;
280 }
281
282 /**
283 * Gets the body of the message.
284 *
285 * @return \Psr\Http\Message\StreamInterface Returns the body as a stream.
286 */
287 public function getBody()
288 {
289 return $this->body;
290 }
291
292 /**
293 * Return an instance with the specified message body.
294 *
295 * The body MUST be a StreamInterface object.
296 *
297 * This method MUST be implemented in such a way as to retain the
298 * immutability of the message, and MUST return a new instance that has the
299 * new body stream.
300 *
301 * @param \Psr\Http\Message\StreamInterface $body Body.
302 * @return Message
303 * @throws \InvalidArgumentException When the body is not valid.
304 */
305 public function withBody(StreamInterface $body)
306 {
307 $clonedObject = clone $this;
308 $clonedObject->body = $body;
309 return $clonedObject;
310 }
311
312 /**
313 * Ensure header names and values are valid.
314 *
315 * @param array $headers
316 * @throws \InvalidArgumentException
317 */
318 protected function assertHeaders(array $headers)
319 {
320 foreach ($headers as $name => $headerValues) {
321 $this->validateHeaderName($name);
322 // check if all values are correct
323 array_walk($headerValues, function ($value, $key, Message $messageObject) {
324 if (!$messageObject->isValidHeaderValue($value)) {
325 throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717268);
326 }
327 }, $this);
328 }
329 }
330
331 /**
332 * Filter a set of headers to ensure they are in the correct internal format.
333 *
334 * Used by message constructors to allow setting all initial headers at once.
335 *
336 * @param array $originalHeaders Headers to filter.
337 * @return array Filtered headers and names.
338 */
339 protected function filterHeaders(array $originalHeaders)
340 {
341 $headerNames = $headers = [];
342 foreach ($originalHeaders as $header => $value) {
343 if (!is_string($header) || (!is_array($value) && !is_string($value))) {
344 continue;
345 }
346 if (!is_array($value)) {
347 $value = [$value];
348 }
349 $headerNames[strtolower($header)] = $header;
350 $headers[$header] = $value;
351 }
352 return [$headerNames, $headers];
353 }
354
355 /**
356 * Helper function to test if an array contains only strings
357 *
358 * @param array $data
359 * @return bool
360 */
361 protected function arrayContainsOnlyStrings(array $data)
362 {
363 return array_reduce($data, function ($original, $item) {
364 return is_string($item) ? $original : false;
365 }, true);
366 }
367
368 /**
369 * Assert that the provided header values are valid.
370 *
371 * @see http://tools.ietf.org/html/rfc7230#section-3.2
372 * @param string[] $values
373 * @throws \InvalidArgumentException
374 */
375 protected function validateHeaderValues(array $values)
376 {
377 array_walk($values, function ($value, $key, Message $messageObject) {
378 if (!$messageObject->isValidHeaderValue($value)) {
379 throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717269);
380 }
381 }, $this);
382 }
383
384 /**
385 * Filter a header value
386 *
387 * Ensures CRLF header injection vectors are filtered.
388 *
389 * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
390 * tabs are allowed in values; header continuations MUST consist of
391 * a single CRLF sequence followed by a space or horizontal tab.
392 *
393 * This method filters any values not allowed from the string, and is
394 * lossy.
395 *
396 * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
397 * @param string $value
398 * @return string
399 */
400 public function filter($value)
401 {
402 $value = (string)$value;
403 $length = strlen($value);
404 $string = '';
405 for ($i = 0; $i < $length; $i += 1) {
406 $ascii = ord($value[$i]);
407
408 // Detect continuation sequences
409 if ($ascii === 13) {
410 $lf = ord($value[$i + 1]);
411 $ws = ord($value[$i + 2]);
412 if ($lf === 10 && in_array($ws, [9, 32], true)) {
413 $string .= $value[$i] . $value[$i + 1];
414 $i += 1;
415 }
416 continue;
417 }
418
419 // Non-visible, non-whitespace characters
420 // 9 === horizontal tab
421 // 32-126, 128-254 === visible
422 // 127 === DEL
423 // 255 === null byte
424 if (($ascii < 32 && $ascii !== 9) || $ascii === 127 || $ascii > 254) {
425 continue;
426 }
427
428 $string .= $value[$i];
429 }
430
431 return $string;
432 }
433
434 /**
435 * Check whether or not a header name is valid and throw an exception.
436 *
437 * @see http://tools.ietf.org/html/rfc7230#section-3.2
438 * @param string $name
439 * @throws \InvalidArgumentException
440 */
441 public function validateHeaderName($name)
442 {
443 if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) {
444 throw new \InvalidArgumentException('Invalid header name, given "' . $name . '"', 1436717270);
445 }
446 }
447
448 /**
449 * Checks if an HTTP header value is valid.
450 *
451 * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
452 * tabs are allowed in values; header continuations MUST consist of
453 * a single CRLF sequence followed by a space or horizontal tab.
454 *
455 * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
456 * @param string $value
457 * @return bool
458 */
459 public function isValidHeaderValue($value)
460 {
461 $value = (string)$value;
462
463 // Any occurence of \r or \n is invalid
464 if (strpbrk($value, "\r\n") !== false) {
465 return false;
466 }
467
468 $length = strlen($value);
469 for ($i = 0; $i < $length; $i += 1) {
470 $ascii = ord($value[$i]);
471
472 // Non-visible, non-whitespace characters
473 // 9 === horizontal tab
474 // 10 === line feed
475 // 13 === carriage return
476 // 32-126, 128-254 === visible
477 // 127 === DEL
478 // 255 === null byte
479 if (($ascii < 32 && ! in_array($ascii, [9, 10, 13], true)) || $ascii === 127 || $ascii > 254) {
480 return false;
481 }
482 }
483
484 return true;
485 }
486 }