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