1<?php
2
3/*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16namespace TYPO3\CMS\Core\Http;
17
18use Psr\Http\Message\MessageInterface;
19use Psr\Http\Message\StreamInterface;
20
21/**
22 * Default implementation for the MessageInterface of the PSR-7 standard
23 * It is the base for any request or response for PSR-7.
24 *
25 * Highly inspired by https://github.com/phly/http/
26 *
27 * @internal Note that this is not public API yet.
28 */
29class Message implements MessageInterface
30{
31    /**
32     * The HTTP Protocol version, defaults to 1.1
33     * @var string
34     */
35    protected $protocolVersion = '1.1';
36
37    /**
38     * Associative array containing all headers of this Message
39     * This is a mixed-case list of the headers (as due to the specification)
40     * @var array
41     */
42    protected $headers = [];
43
44    /**
45     * Lowercased version of all headers, in order to check if a header is set or not
46     * this way a lot of checks are easier to be set
47     * @var array
48     */
49    protected $lowercasedHeaderNames = [];
50
51    /**
52     * The body as a Stream object
53     * @var StreamInterface|null
54     */
55    protected $body;
56
57    /**
58     * Retrieves the HTTP protocol version as a string.
59     *
60     * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
61     *
62     * @return string HTTP protocol version.
63     */
64    public function getProtocolVersion()
65    {
66        return $this->protocolVersion;
67    }
68
69    /**
70     * Return an instance with the specified HTTP protocol version.
71     *
72     * The version string MUST contain only the HTTP version number (e.g.,
73     * "1.1", "1.0").
74     *
75     * This method MUST be implemented in such a way as to retain the
76     * immutability of the message, and MUST return an instance that has the
77     * new protocol version.
78     *
79     * @param string $version HTTP protocol version
80     * @return static
81     */
82    public function withProtocolVersion($version)
83    {
84        $clonedObject = clone $this;
85        $clonedObject->protocolVersion = $version;
86        return $clonedObject;
87    }
88
89    /**
90     * Retrieves all message header values.
91     *
92     * The keys represent the header name as it will be sent over the wire, and
93     * each value is an array of strings associated with the header.
94     *
95     *     // Represent the headers as a string
96     *     foreach ($message->getHeaders() as $name => $values) {
97     *         echo $name . ": " . implode(", ", $values);
98     *     }
99     *
100     *     // Emit headers iteratively:
101     *     foreach ($message->getHeaders() as $name => $values) {
102     *         foreach ($values as $value) {
103     *             header(sprintf('%s: %s', $name, $value), false);
104     *         }
105     *     }
106     *
107     * While header names are not case-sensitive, getHeaders() will preserve the
108     * exact case in which headers were originally specified.
109     *
110     * @return array Returns an associative array of the message's headers. Each
111     *     key MUST be a header name, and each value MUST be an array of strings
112     *     for that header.
113     */
114    public function getHeaders()
115    {
116        return $this->headers;
117    }
118
119    /**
120     * Checks if a header exists by the given case-insensitive name.
121     *
122     * @param string $name Case-insensitive header field name.
123     * @return bool Returns true if any header names match the given header
124     *     name using a case-insensitive string comparison. Returns false if
125     *     no matching header name is found in the message.
126     */
127    public function hasHeader($name)
128    {
129        return isset($this->lowercasedHeaderNames[strtolower($name)]);
130    }
131
132    /**
133     * Retrieves a message header value by the given case-insensitive name.
134     *
135     * This method returns an array of all the header values of the given
136     * case-insensitive header name.
137     *
138     * If the header does not appear in the message, this method MUST return an
139     * empty array.
140     *
141     * @param string $name Case-insensitive header field name.
142     * @return string[] An array of string values as provided for the given
143     *    header. If the header does not appear in the message, this method MUST
144     *    return an empty array.
145     */
146    public function getHeader($name)
147    {
148        if (!$this->hasHeader($name)) {
149            return [];
150        }
151        $header = $this->lowercasedHeaderNames[strtolower($name)];
152        $headerValue = $this->headers[$header];
153        if (is_array($headerValue)) {
154            return $headerValue;
155        }
156        return [$headerValue];
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 static
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 static
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 static
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        if ($this->body === null) {
290            $this->body = new Stream('php://temp', 'r+');
291        }
292        return $this->body;
293    }
294
295    /**
296     * Return an instance with the specified message body.
297     *
298     * The body MUST be a StreamInterface object.
299     *
300     * This method MUST be implemented in such a way as to retain the
301     * immutability of the message, and MUST return a new instance that has the
302     * new body stream.
303     *
304     * @param \Psr\Http\Message\StreamInterface $body Body.
305     * @return static
306     * @throws \InvalidArgumentException When the body is not valid.
307     */
308    public function withBody(StreamInterface $body)
309    {
310        $clonedObject = clone $this;
311        $clonedObject->body = $body;
312        return $clonedObject;
313    }
314
315    /**
316     * Ensure header names and values are valid.
317     *
318     * @param array $headers
319     * @throws \InvalidArgumentException
320     */
321    protected function assertHeaders(array $headers)
322    {
323        foreach ($headers as $name => $headerValues) {
324            $this->validateHeaderName($name);
325            // check if all values are correct
326            array_walk($headerValues, static function ($value, $key, Message $messageObject) {
327                if (!$messageObject->isValidHeaderValue($value)) {
328                    throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717268);
329                }
330            }, $this);
331        }
332    }
333
334    /**
335     * Filter a set of headers to ensure they are in the correct internal format.
336     *
337     * Used by message constructors to allow setting all initial headers at once.
338     *
339     * @param array $originalHeaders Headers to filter.
340     * @return array Filtered headers and names.
341     */
342    protected function filterHeaders(array $originalHeaders)
343    {
344        $headerNames = $headers = [];
345        foreach ($originalHeaders as $header => $value) {
346            if (!is_string($header) || (!is_array($value) && !is_string($value))) {
347                continue;
348            }
349            if (!is_array($value)) {
350                $value = [$value];
351            }
352            $headerNames[strtolower($header)] = $header;
353            $headers[$header] = $value;
354        }
355        return [$headerNames, $headers];
356    }
357
358    /**
359     * Helper function to test if an array contains only strings
360     *
361     * @param array $data
362     * @return bool
363     */
364    protected function arrayContainsOnlyStrings(array $data)
365    {
366        return array_reduce($data, static function ($original, $item) {
367            return is_string($item) ? $original : false;
368        }, true);
369    }
370
371    /**
372     * Assert that the provided header values are valid.
373     *
374     * @see https://tools.ietf.org/html/rfc7230#section-3.2
375     * @param string[] $values
376     * @throws \InvalidArgumentException
377     */
378    protected function validateHeaderValues(array $values)
379    {
380        array_walk($values, static function ($value, $key, Message $messageObject) {
381            if (!$messageObject->isValidHeaderValue($value)) {
382                throw new \InvalidArgumentException('Invalid header value for header "' . $key . '"', 1436717269);
383            }
384        }, $this);
385    }
386
387    /**
388     * Filter a header value
389     *
390     * Ensures CRLF header injection vectors are filtered.
391     *
392     * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
393     * tabs are allowed in values; header continuations MUST consist of
394     * a single CRLF sequence followed by a space or horizontal tab.
395     *
396     * This method filters any values not allowed from the string, and is
397     * lossy.
398     *
399     * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
400     * @param string $value
401     * @return string
402     */
403    public function filter($value)
404    {
405        $value  = (string)$value;
406        $length = strlen($value);
407        $string = '';
408        for ($i = 0; $i < $length; $i += 1) {
409            $ascii = ord($value[$i]);
410
411            // Detect continuation sequences
412            if ($ascii === 13) {
413                $lf = ord($value[$i + 1]);
414                $ws = ord($value[$i + 2]);
415                if ($lf === 10 && in_array($ws, [9, 32], true)) {
416                    $string .= $value[$i] . $value[$i + 1];
417                    $i += 1;
418                }
419                continue;
420            }
421
422            // Non-visible, non-whitespace characters
423            // 9 === horizontal tab
424            // 32-126, 128-254 === visible
425            // 127 === DEL
426            // 255 === null byte
427            if (($ascii < 32 && $ascii !== 9) || $ascii === 127 || $ascii > 254) {
428                continue;
429            }
430
431            $string .= $value[$i];
432        }
433
434        return $string;
435    }
436
437    /**
438     * Check whether or not a header name is valid and throw an exception.
439     *
440     * @see https://tools.ietf.org/html/rfc7230#section-3.2
441     * @param string $name
442     * @throws \InvalidArgumentException
443     */
444    public function validateHeaderName($name)
445    {
446        if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) {
447            throw new \InvalidArgumentException('Invalid header name, given "' . $name . '"', 1436717270);
448        }
449    }
450
451    /**
452     * Checks if an HTTP header value is valid.
453     *
454     * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
455     * tabs are allowed in values; header continuations MUST consist of
456     * a single CRLF sequence followed by a space or horizontal tab.
457     *
458     * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
459     * @param string $value
460     * @return bool
461     */
462    public function isValidHeaderValue($value)
463    {
464        $value = (string)$value;
465
466        // Any occurrence of \r or \n is invalid
467        if (strpbrk($value, "\r\n") !== false) {
468            return false;
469        }
470
471        foreach (unpack('C*', $value) as $ascii) {
472
473            // Non-visible, non-whitespace characters
474            // 9 === horizontal tab
475            // 32-126, 128-254 === visible
476            // 127 === DEL
477            // 255 === null byte
478            if (($ascii < 32 && $ascii !== 9) || $ascii === 127 || $ascii > 254) {
479                return false;
480            }
481        }
482
483        return true;
484    }
485}
486