MessageTrait::checkHeaderValue()   A
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6.2017

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 13
ccs 7
cts 11
cp 0.6364
rs 9.6111
c 0
b 0
f 0
cc 5
nc 3
nop 1
crap 6.2017
1
<?php
2
3
/**
4
 * This file is part of the Phalcon Framework.
5
 *
6
 * (c) Phalcon Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Phalcon\Http\Message\Traits;
15
16
use Phalcon\Collection;
17
use Phalcon\Http\Message\Exception\InvalidArgumentException;
18
use Phalcon\Http\Message\Stream;
19
use Psr\Http\Message\StreamInterface;
20
use Psr\Http\Message\UriInterface;
21
22
use function array_merge;
23
use function implode;
24
use function is_array;
25
use function is_numeric;
26
use function is_resource;
27
use function is_string;
28
use function preg_match;
29
30
/**
31
 * Message methods
32
 *
33
 * @property StreamInterface $body
34
 * @property Collection      $headers
35
 * @property string          $protocolVersion
36
 * @property UriInterface    $uri
37
 */
38
trait MessageTrait
39
{
40
    /**
41
     * Gets the body of the message.
42
     *
43
     * @var StreamInterface
44
     */
45
    private $body;
46
47
    /**
48
     * @var Collection
49
     */
50
    private $headers;
51
52
    /**
53
     * Retrieves the HTTP protocol version as a string.
54
     *
55
     * The string MUST contain only the HTTP version number (e.g., '1.1',
56
     * '1.0').
57
     *
58
     * @var string HTTP protocol version.
59
     */
60
    private $protocolVersion = '1.1';
61
62
    /**
63
     * Retrieves the URI instance.
64
     *
65
     * This method MUST return a UriInterface instance.
66
     *
67
     * @see http://tools.ietf.org/html/rfc3986#section-4.3
68
     *
69
     * @var UriInterface
70
     */
71
    private $uri;
72
73
    /**
74
     * Return the body of the request
75
     *
76
     * @return StreamInterface
77
     */
78 9
    public function getBody(): StreamInterface
79
    {
80 9
        return $this->body;
81
    }
82
83
    /**
84
     * Retrieves a message header value by the given case-insensitive name.
85
     *
86
     * This method returns an array of all the header values of the given
87
     * case-insensitive header name.
88
     *
89
     * If the header does not appear in the message, this method MUST return an
90
     * empty array.
91
     *
92
     * @param string $name
93
     *
94
     * @return array
95
     */
96 13
    public function getHeader($name): array
97
    {
98 13
        $name = (string) $name;
99
100 13
        return $this->headers->get($name, []);
101
    }
102
103
    /**
104
     * Retrieves a comma-separated string of the values for a single header.
105
     *
106
     * This method returns all of the header values of the given
107
     * case-insensitive header name as a string concatenated together using
108
     * a comma.
109
     *
110
     * NOTE: Not all header values may be appropriately represented using
111
     * comma concatenation. For such headers, use getHeader() instead
112
     * and supply your own delimiter when concatenating.
113
     *
114
     * If the header does not appear in the message, this method MUST return
115
     * an empty string.
116
     *
117
     * @param string $name
118
     *
119
     * @return string
120
     */
121 6
    public function getHeaderLine($name): string
122
    {
123 6
        $header = $this->getHeader($name);
124
125 6
        return implode(',', $header);
126
    }
127
128
    /**
129
     * Retrieves all message header values.
130
     *
131
     * The keys represent the header name as it will be sent over the wire, and
132
     * each value is an array of strings associated with the header.
133
     *
134
     *     // Represent the headers as a string
135
     *     foreach ($message->getHeaders() as $name => $values) {
136
     *         echo $name . ': ' . implode(', ', $values);
137
     *     }
138
     *
139
     *     // Emit headers iteratively:
140
     *     foreach ($message->getHeaders() as $name => $values) {
141
     *         foreach ($values as $value) {
142
     *             header(sprintf('%s: %s', $name, $value), false);
143
     *         }
144
     *     }
145
     *
146
     * While header names are not case-sensitive, getHeaders() will preserve the
147
     * exact case in which headers were originally specified.
148
     *
149
     * @return array
150
     */
151 24
    public function getHeaders(): array
152
    {
153 24
        return $this->headers->toArray();
154
    }
155
156
    /**
157
     * Return the protocol version
158
     *
159
     * @return string
160
     */
161 6
    public function getProtocolVersion(): string
162
    {
163 6
        return $this->protocolVersion;
164
    }
165
166
    /**
167
     * Checks if a header exists by the given case-insensitive name.
168
     *
169
     * @param string $name
170
     *
171
     * @return bool
172
     */
173 6
    public function hasHeader($name): bool
174
    {
175 6
        return $this->headers->has($name);
176
    }
177
178
    /**
179
     * Return an instance with the specified header appended with the given
180
     * value.
181
     *
182
     * Existing values for the specified header will be maintained. The new
183
     * value(s) will be appended to the existing list. If the header did not
184
     * exist previously, it will be added.
185
     *
186
     * This method MUST be implemented in such a way as to retain the
187
     * immutability of the message, and MUST return an instance that has the
188
     * new header and/or value.
189
     *
190
     * @param string          $name
191
     * @param string|string[] $value
192
     *
193
     * @return self
194
     */
195 8
    public function withAddedHeader($name, $value): self
196
    {
197 8
        $this->checkHeaderName($name);
198
199 8
        $headers  = clone $this->headers;
200 8
        $existing = $headers->get($name, []);
201 8
        $value    = $this->getHeaderValue($value);
202 7
        $value    = array_merge($existing, $value);
203
204 7
        $headers->set($name, $value);
205
206 7
        return $this->cloneInstance($headers, 'headers');
207
    }
208
209
    /**
210
     * Return an instance with the specified message body.
211
     *
212
     * The body MUST be a StreamInterface object.
213
     *
214
     * This method MUST be implemented in such a way as to retain the
215
     * immutability of the message, and MUST return a new instance that has the
216
     * new body stream.
217
     *
218
     * @param StreamInterface $body
219
     *
220
     * @return self
221
     * @throws InvalidArgumentException When the body is not valid.
222
     *
223
     */
224 3
    public function withBody(StreamInterface $body): self
225
    {
226 3
        $newBody = $this->processBody($body, 'w+b');
227
228 3
        return $this->cloneInstance($newBody, 'body');
229
    }
230
231
    /**
232
     * Return an instance with the provided value replacing the specified
233
     * header.
234
     *
235
     * While header names are case-insensitive, the casing of the header will
236
     * be preserved by this function, and returned from getHeaders().
237
     *
238
     * This method MUST be implemented in such a way as to retain the
239
     * immutability of the message, and MUST return an instance that has the
240
     * new and/or updated header and value.
241
     *
242
     * @param string          $name
243
     * @param string|string[] $value
244
     *
245
     * @return self
246
     * @throws InvalidArgumentException for invalid header names or values.
247
     *
248
     */
249 5
    public function withHeader($name, $value): self
250
    {
251 5
        $this->checkHeaderName($name);
252
253 4
        $headers = clone $this->headers;
254 4
        $value   = $this->getHeaderValue($value);
255
256 3
        $headers->set($name, $value);
257
258 3
        return $this->cloneInstance($headers, 'headers');
259
    }
260
261
    /**
262
     * Return an instance with the specified HTTP protocol version.
263
     *
264
     * The version string MUST contain only the HTTP version number (e.g.,
265
     * '1.1', '1.0').
266
     *
267
     * This method MUST be implemented in such a way as to retain the
268
     * immutability of the message, and MUST return an instance that has the
269
     * new protocol version.
270
     *
271
     * @param string $version
272
     *
273
     * @return self
274
     */
275 6
    public function withProtocolVersion($version): self
276
    {
277 6
        $this->processProtocol($version);
278
279 3
        return $this->cloneInstance($version, 'protocolVersion');
280
    }
281
282
    /**
283
     * Return an instance without the specified header.
284
     *
285
     * Header resolution MUST be done without case-sensitivity.
286
     *
287
     * This method MUST be implemented in such a way as to retain the
288
     * immutability of the message, and MUST return an instance that removes
289
     * the named header.
290
     *
291
     * @param string $name
292
     *
293
     * @return self
294
     */
295 3
    public function withoutHeader($name): self
296
    {
297 3
        $headers = clone $this->headers;
298 3
        $headers->remove($name);
299
300 3
        return $this->cloneInstance($headers, 'headers');
301
    }
302
303
    /**
304
     * Ensure Host is the first header.
305
     *
306
     * @see: http://tools.ietf.org/html/rfc7230#section-5.4
307
     *
308
     * @param Collection $collection
309
     *
310
     * @return Collection
311
     */
312 124
    protected function checkHeaderHost(Collection $collection): Collection
313
    {
314
        if (
315 124
            $collection->has('host') &&
316 124
            true !== empty($this->uri) &&
317 124
            '' !== $this->uri->getHost()
318
        ) {
319 1
            $host = $this->getUriHost($this->uri);
320
321 1
            $collection->set('Host', [$host]);
322
323 1
            $data   = $collection->toArray();
324 1
            $header = $data['Host'];
325 1
            unset($data['Host']);
326 1
            $data = ['Host' => $header] + $data;
327 1
            $collection->clear();
328 1
            $collection->init($data);
329
        }
330
331 124
        return $collection;
332
    }
333
334
    /**
335
     * Check the name of the header. Throw exception if not valid
336
     *
337
     * @see http://tools.ietf.org/html/rfc7230#section-3.2
338
     *
339
     * @param mixed $name
340
     */
341 35
    private function checkHeaderName($name): void
342
    {
343
        if (
344 35
            !is_string($name) ||
345 35
            !preg_match("/^[a-zA-Z0-9'`#$%&*+.^_|~!-]+$/", $name)
346
        ) {
347 1
            throw new InvalidArgumentException(
348 1
                'Invalid header name ' . $name
349
            );
350
        }
351 34
    }
352
353
    /**
354
     * Validates a header value
355
     *
356
     * Most HTTP header field values are defined using common syntax
357
     * components (token, quoted-string, and comment) separated by
358
     * whitespace or specific delimiting characters.  Delimiters are chosen
359
     * from the set of US-ASCII visual characters not allowed in a token
360
     * (DQUOTE and '(),/:;<=>?@[\]{}').
361
     *
362
     *     token          = 1*tchar
363
     *
364
     *     tchar          = '!' / '#' / '$' / '%' / '&' / ''' / '*'
365
     *                    / '+' / '-' / '.' / '^' / '_' / '`' / '|' / '~'
366
     *                    / DIGIT / ALPHA
367
     *                    ; any VCHAR, except delimiters
368
     *
369
     * A string of text is parsed as a single value if it is quoted using
370
     * double-quote marks.
371
     *
372
     *     quoted-string  = DQUOTE *( qdtext / quoted-pair ) DQUOTE
373
     *     qdtext         = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
374
     *     obs-text       = %x80-FF
375
     *
376
     * Comments can be included in some HTTP header fields by surrounding
377
     * the comment text with parentheses.  Comments are only allowed in
378
     * fields containing 'comment' as part of their field value definition.
379
     *
380
     *     comment        = '(' *( ctext / quoted-pair / comment ) ')'
381
     *     ctext          = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
382
     *
383
     * The backslash octet ('\') can be used as a single-octet quoting
384
     * mechanism within quoted-string and comment constructs.  Recipients
385
     * that process the value of a quoted-string MUST handle a quoted-pair
386
     * as if it were replaced by the octet following the backslash.
387
     *
388
     *     quoted-pair    = '\' ( HTAB / SP / VCHAR / obs-text )
389
     *
390
     * A sender SHOULD NOT generate a quoted-pair in a quoted-string except
391
     * where necessary to quote DQUOTE and backslash octets occurring within
392
     * that string.  A sender SHOULD NOT generate a quoted-pair in a comment
393
     * except where necessary to quote parentheses ['(' and ')'] and
394
     * backslash octets occurring within that comment.
395
     *
396
     * @see https://tools.ietf.org/html/rfc7230#section-3.2.6
397
     *
398
     * @param mixed $value
399
     */
400 33
    private function checkHeaderValue($value): void
401
    {
402 33
        if (!is_string($value) && !is_numeric($value)) {
403 1
            throw new InvalidArgumentException('Invalid header value');
404
        }
405
406 33
        $value = (string) $value;
407
408
        if (
409 33
            preg_match("#(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))#", $value) ||
410 33
            preg_match("/[^\x09\x0a\x0d\x20-\x7E\x80-\xFE]/", $value)
411
        ) {
412 1
            throw new InvalidArgumentException('Invalid header value');
413
        }
414 32
    }
415
416
    abstract protected function cloneInstance($element, string $property);
417
418
    /**
419
     * Returns the header values checked for validity
420
     *
421
     * @param mixed $values
422
     *
423
     * @return array
424
     */
425 34
    private function getHeaderValue($values): array
426
    {
427 34
        if (!is_array($values)) {
428 5
            $values = [$values];
429
        }
430
431 34
        if (empty($values)) {
432 1
            throw new InvalidArgumentException(
433
                'Invalid header value: must be a string or ' .
434 1
                'array of strings; cannot be an empty array'
435
            );
436
        }
437
438 33
        $valueData = [];
439
440 33
        foreach ($values as $value) {
441 33
            $this->checkHeaderValue($value);
442
443 32
            $valueData[] = (string) $value;
444
        }
445
446 32
        return $valueData;
447
    }
448
449
    /**
450
     * Return the host and if applicable the port
451
     *
452
     * @param UriInterface $uri
453
     *
454
     * @return string
455
     */
456 1
    private function getUriHost(UriInterface $uri): string
457
    {
458 1
        $host = $uri->getHost();
459
460 1
        if (null !== $uri->getPort()) {
461 1
            $host .= ':' . $uri->getPort();
462
        }
463
464 1
        return $host;
465
    }
466
467
    /**
468
     * Populates the header collection
469
     *
470
     * @param array $headers
471
     *
472
     * @return Collection
473
     */
474 124
    private function populateHeaderCollection(array $headers): Collection
475
    {
476 124
        $collection = new Collection();
477
478 124
        foreach ($headers as $name => $value) {
479 32
            $this->checkHeaderName($name);
480
481 32
            $name  = (string) $name;
482 32
            $value = $this->getHeaderValue($value);
483
484 32
            $collection->set($name, $value);
485
        }
486
487 124
        return $collection;
488
    }
489
490
    /**
491
     * Set a valid stream
492
     *
493
     * @param mixed  $body
494
     * @param string $mode
495
     *
496
     * @return StreamInterface
497
     */
498 125
    private function processBody($body = 'php://memory', string $mode = 'r+b'): StreamInterface
499
    {
500 125
        if ($body instanceof StreamInterface) {
501 66
            return $body;
502
        }
503
504 61
        if (!is_string($body) && !is_resource($body)) {
505 1
            throw new InvalidArgumentException(
506 1
                'Invalid stream passed as a parameter'
507
            );
508
        }
509
510 60
        return new Stream($body, $mode);
511
    }
512
513
    /**
514
     * Sets the headers
515
     *
516
     * @param mixed $headers
517
     *
518
     * @return Collection
519
     */
520 127
    private function processHeaders($headers): Collection
521
    {
522 127
        if (is_array($headers)) {
523 124
            $collection = $this->populateHeaderCollection($headers);
524 124
            $collection = $this->checkHeaderHost($collection);
525
        } else {
526 3
            if (!($headers instanceof Collection)) {
527 1
                throw new InvalidArgumentException(
528 1
                    'Headers needs to be either an array or instance of Phalcon\Collection'
529
                );
530
            }
531
532 2
            $collection = $headers;
533
        }
534
535 126
        return $collection;
536
    }
537
538
    /**
539
     * Checks the protocol
540
     *
541
     * @param string $protocol
542
     *
543
     * @return string
544
     */
545 66
    private function processProtocol($protocol = ''): string
546
    {
547
        $protocols = [
548 66
            '1.0' => 1,
549
            '1.1' => 1,
550
            '2.0' => 1,
551
            '3.0' => 1,
552
        ];
553
554 66
        if (!(!empty($protocol) && is_string($protocol))) {
555 1
            throw new InvalidArgumentException('Invalid protocol value');
556
        }
557
558 65
        if (!isset($protocols[$protocol])) {
559 2
            throw new InvalidArgumentException(
560 2
                'Unsupported protocol ' . $protocol
561
            );
562
        }
563
564 63
        return $protocol;
565
    }
566
}
567