1<?php
2/**
3 * @see       https://github.com/zendframework/zend-diactoros for the canonical source repository
4 * @copyright Copyright (c) 2015-2017 Zend Technologies USA Inc. (http://www.zend.com)
5 * @license   https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
6 */
7
8namespace Zend\Diactoros;
9
10use InvalidArgumentException;
11use Psr\Http\Message\StreamInterface;
12
13use function array_map;
14use function array_merge;
15use function get_class;
16use function gettype;
17use function implode;
18use function is_array;
19use function is_object;
20use function is_resource;
21use function is_string;
22use function preg_match;
23use function sprintf;
24use function strtolower;
25
26/**
27 * Trait implementing the various methods defined in MessageInterface.
28 *
29 * @see https://github.com/php-fig/http-message/tree/master/src/MessageInterface.php
30 */
31trait MessageTrait
32{
33    /**
34     * List of all registered headers, as key => array of values.
35     *
36     * @var array
37     */
38    protected $headers = [];
39
40    /**
41     * Map of normalized header name to original name used to register header.
42     *
43     * @var array
44     */
45    protected $headerNames = [];
46
47    /**
48     * @var string
49     */
50    private $protocol = '1.1';
51
52    /**
53     * @var StreamInterface
54     */
55    private $stream;
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->protocol;
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        $this->validateProtocolVersion($version);
85        $new = clone $this;
86        $new->protocol = $version;
87        return $new;
88    }
89
90    /**
91     * Retrieves all message headers.
92     *
93     * The keys represent the header name as it will be sent over the wire, and
94     * each value is an array of strings associated with the header.
95     *
96     *     // Represent the headers as a string
97     *     foreach ($message->getHeaders() as $name => $values) {
98     *         echo $name . ": " . implode(", ", $values);
99     *     }
100     *
101     *     // Emit headers iteratively:
102     *     foreach ($message->getHeaders() as $name => $values) {
103     *         foreach ($values as $value) {
104     *             header(sprintf('%s: %s', $name, $value), false);
105     *         }
106     *     }
107     *
108     * @return array Returns an associative array of the message's headers. Each
109     *     key MUST be a header name, and each value MUST be an array of strings.
110     */
111    public function getHeaders()
112    {
113        return $this->headers;
114    }
115
116    /**
117     * Checks if a header exists by the given case-insensitive name.
118     *
119     * @param string $header Case-insensitive header name.
120     * @return bool Returns true if any header names match the given header
121     *     name using a case-insensitive string comparison. Returns false if
122     *     no matching header name is found in the message.
123     */
124    public function hasHeader($header)
125    {
126        return isset($this->headerNames[strtolower($header)]);
127    }
128
129    /**
130     * Retrieves a message header value by the given case-insensitive name.
131     *
132     * This method returns an array of all the header values of the given
133     * case-insensitive header name.
134     *
135     * If the header does not appear in the message, this method MUST return an
136     * empty array.
137     *
138     * @param string $header Case-insensitive header field name.
139     * @return string[] An array of string values as provided for the given
140     *    header. If the header does not appear in the message, this method MUST
141     *    return an empty array.
142     */
143    public function getHeader($header)
144    {
145        if (! $this->hasHeader($header)) {
146            return [];
147        }
148
149        $header = $this->headerNames[strtolower($header)];
150
151        return $this->headers[$header];
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    {
175        $value = $this->getHeader($name);
176        if (empty($value)) {
177            return '';
178        }
179
180        return implode(',', $value);
181    }
182
183    /**
184     * Return an instance with the provided header, replacing any existing
185     * values of any headers with the same case-insensitive name.
186     *
187     * While header names are case-insensitive, the casing of the header will
188     * be preserved by this function, and returned from getHeaders().
189     *
190     * This method MUST be implemented in such a way as to retain the
191     * immutability of the message, and MUST return an instance that has the
192     * new and/or updated header and value.
193     *
194     * @param string $header Case-insensitive header field name.
195     * @param string|string[] $value Header value(s).
196     * @return static
197     * @throws \InvalidArgumentException for invalid header names or values.
198     */
199    public function withHeader($header, $value)
200    {
201        $this->assertHeader($header);
202
203        $normalized = strtolower($header);
204
205        $new = clone $this;
206        if ($new->hasHeader($header)) {
207            unset($new->headers[$new->headerNames[$normalized]]);
208        }
209
210        $value = $this->filterHeaderValue($value);
211
212        $new->headerNames[$normalized] = $header;
213        $new->headers[$header]         = $value;
214
215        return $new;
216    }
217
218    /**
219     * Return an instance with the specified header appended with the
220     * given value.
221     *
222     * Existing values for the specified header will be maintained. The new
223     * value(s) will be appended to the existing list. If the header did not
224     * exist previously, it will be added.
225     *
226     * This method MUST be implemented in such a way as to retain the
227     * immutability of the message, and MUST return an instance that has the
228     * new header and/or value.
229     *
230     * @param string $header Case-insensitive header field name to add.
231     * @param string|string[] $value Header value(s).
232     * @return static
233     * @throws \InvalidArgumentException for invalid header names or values.
234     */
235    public function withAddedHeader($header, $value)
236    {
237        $this->assertHeader($header);
238
239        if (! $this->hasHeader($header)) {
240            return $this->withHeader($header, $value);
241        }
242
243        $header = $this->headerNames[strtolower($header)];
244
245        $new = clone $this;
246        $value = $this->filterHeaderValue($value);
247        $new->headers[$header] = array_merge($this->headers[$header], $value);
248        return $new;
249    }
250
251    /**
252     * Return an instance without the specified header.
253     *
254     * Header resolution MUST be done without case-sensitivity.
255     *
256     * This method MUST be implemented in such a way as to retain the
257     * immutability of the message, and MUST return an instance that removes
258     * the named header.
259     *
260     * @param string $header Case-insensitive header field name to remove.
261     * @return static
262     */
263    public function withoutHeader($header)
264    {
265        if (! $this->hasHeader($header)) {
266            return clone $this;
267        }
268
269        $normalized = strtolower($header);
270        $original   = $this->headerNames[$normalized];
271
272        $new = clone $this;
273        unset($new->headers[$original], $new->headerNames[$normalized]);
274        return $new;
275    }
276
277    /**
278     * Gets the body of the message.
279     *
280     * @return StreamInterface Returns the body as a stream.
281     */
282    public function getBody()
283    {
284        return $this->stream;
285    }
286
287    /**
288     * Return an instance with the specified message body.
289     *
290     * The body MUST be a StreamInterface object.
291     *
292     * This method MUST be implemented in such a way as to retain the
293     * immutability of the message, and MUST return a new instance that has the
294     * new body stream.
295     *
296     * @param StreamInterface $body Body.
297     * @return static
298     * @throws \InvalidArgumentException When the body is not valid.
299     */
300    public function withBody(StreamInterface $body)
301    {
302        $new = clone $this;
303        $new->stream = $body;
304        return $new;
305    }
306
307    private function getStream($stream, $modeIfNotInstance)
308    {
309        if ($stream instanceof StreamInterface) {
310            return $stream;
311        }
312
313        if (! is_string($stream) && ! is_resource($stream)) {
314            throw new InvalidArgumentException(
315                'Stream must be a string stream resource identifier, '
316                . 'an actual stream resource, '
317                . 'or a Psr\Http\Message\StreamInterface implementation'
318            );
319        }
320
321        return new Stream($stream, $modeIfNotInstance);
322    }
323
324    /**
325     * Filter a set of headers to ensure they are in the correct internal format.
326     *
327     * Used by message constructors to allow setting all initial headers at once.
328     *
329     * @param array $originalHeaders Headers to filter.
330     */
331    private function setHeaders(array $originalHeaders)
332    {
333        $headerNames = $headers = [];
334
335        foreach ($originalHeaders as $header => $value) {
336            $value = $this->filterHeaderValue($value);
337
338            $this->assertHeader($header);
339
340            $headerNames[strtolower($header)] = $header;
341            $headers[$header] = $value;
342        }
343
344        $this->headerNames = $headerNames;
345        $this->headers = $headers;
346    }
347
348    /**
349     * Validate the HTTP protocol version
350     *
351     * @param string $version
352     * @throws InvalidArgumentException on invalid HTTP protocol version
353     */
354    private function validateProtocolVersion($version)
355    {
356        if (empty($version)) {
357            throw new InvalidArgumentException(
358                'HTTP protocol version can not be empty'
359            );
360        }
361        if (! is_string($version)) {
362            throw new InvalidArgumentException(sprintf(
363                'Unsupported HTTP protocol version; must be a string, received %s',
364                (is_object($version) ? get_class($version) : gettype($version))
365            ));
366        }
367
368        // HTTP/1 uses a "<major>.<minor>" numbering scheme to indicate
369        // versions of the protocol, while HTTP/2 does not.
370        if (! preg_match('#^(1\.[01]|2)$#', $version)) {
371            throw new InvalidArgumentException(sprintf(
372                'Unsupported HTTP protocol version "%s" provided',
373                $version
374            ));
375        }
376    }
377
378    /**
379     * @param mixed $values
380     * @return string[]
381     */
382    private function filterHeaderValue($values)
383    {
384        if (! is_array($values)) {
385            $values = [$values];
386        }
387
388        if ([] === $values) {
389            throw new InvalidArgumentException(
390                'Invalid header value: must be a string or array of strings; '
391                . 'cannot be an empty array'
392            );
393        }
394
395        return array_map(function ($value) {
396            HeaderSecurity::assertValid($value);
397
398            return (string) $value;
399        }, array_values($values));
400    }
401
402    /**
403     * Ensure header name and values are valid.
404     *
405     * @param string $name
406     *
407     * @throws InvalidArgumentException
408     */
409    private function assertHeader($name)
410    {
411        HeaderSecurity::assertValidName($name);
412    }
413}
414