1<?php
2namespace 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
17use Psr\Http\Message\MessageInterface;
18use 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 */
28class 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}
486