Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.52% covered (success)
96.52%
111 / 115
91.30% covered (success)
91.30%
21 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
Header
96.52% covered (success)
96.52%
111 / 115
91.30% covered (success)
91.30%
21 / 23
60
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 parse
85.00% covered (success)
85.00%
17 / 20
0.00% covered (danger)
0.00%
0 / 1
6.12
 setName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addValues
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addValue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getValues
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getValueAsString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getValueIndex
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 hasValueAtIndex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasValue
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getValuesAsStrings
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 setWrap
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getWrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasWrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setIndent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getIndent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasIndent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAttachment
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 render
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 fold
97.22% covered (success)
97.22%
35 / 36
0.00% covered (danger)
0.00%
0 / 1
15
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <dev@noladev.com>
8 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Mime\Part;
16
17/**
18 * MIME part header class
19 *
20 * @category   Pop
21 * @package    Pop\Mime
22 * @author     Nick Sagona, III <dev@noladev.com>
23 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
24 * @license    https://www.popphp.org/license     New BSD License
25 * @version    3.0.0
26 */
27class Header
28{
29
30    /**
31     * Header name
32     * @var ?string
33     */
34    protected ?string$name = null;
35
36    /**
37     * Header values
38     * @var array
39     */
40    protected array $values = [];
41
42    /**
43     * Header wrap
44     * @var int
45     */
46    protected int $wrap = 0;
47
48    /**
49     * Header wrap indent
50     * @var string
51     */
52    protected string $indent = "\t";
53
54    /**
55     * Constructor
56     *
57     * Instantiate the header object
58     *
59     * @param string $name
60     * @param mixed  $value
61     */
62    public function __construct(string $name, mixed $value = null)
63    {
64        $this->setName($name);
65
66        if ($value !== null) {
67            if (is_array($value)) {
68                $this->addValues($value);
69            } else {
70                $this->addValue($value);
71            }
72        }
73    }
74
75    /**
76     * Parse header
77     *
78     * @param  string $header
79     * @return Header
80     */
81    public static function parse(string $header): Header
82    {
83        $unfolded = Header\Lexer::unfold(trim($header, "\r\n"));
84        $lines    = explode("\r\n", $unfolded);
85
86        $firstLine  = $lines[0];
87        $colonToken = Header\Lexer::findTopLevelDelimiter($firstLine, ':');
88
89        if ($colonToken === null) {
90            return new static(trim($firstLine));
91        }
92
93        $name         = trim(substr($firstLine, 0, $colonToken->start));
94        $headerObject = new static($name);
95        $headerObject->addValue(Header\Value::parse(substr($firstLine, $colonToken->end)));
96
97        for ($i = 1, $count = count($lines); $i < $count; $i++) {
98            $line = $lines[$i];
99            if ($line === '') {
100                continue;
101            }
102
103            $lineColonToken = Header\Lexer::findTopLevelDelimiter($line, ':');
104            if ($lineColonToken === null) {
105                continue;
106            }
107
108            $lineName = trim(substr($line, 0, $lineColonToken->start));
109            if ($lineName === $name) {
110                $headerObject->addValue(Header\Value::parse(substr($line, $lineColonToken->end)));
111            }
112        }
113
114        return $headerObject;
115    }
116
117    /**
118     * Set the header name
119     *
120     * @param  string $name
121     * @return Header
122     */
123    public function setName(string $name): Header
124    {
125        $this->name = $name;
126        return $this;
127    }
128
129    /**
130     * Get the header name
131     *
132     * @return string
133     */
134    public function getName(): string
135    {
136        return $this->name;
137    }
138
139    /**
140     * Add header values
141     *
142     * @param  array $values
143     * @return Header
144     */
145    public function addValues(array $values): Header
146    {
147        foreach ($values as $value) {
148            $this->addValue($value);
149        }
150
151        return $this;
152    }
153
154    /**
155     * Add a header value
156     *
157     * @param  Header\Value|string $value
158     * @param  ?string             $scheme
159     * @param  array               $parameters
160     * @return Header
161     */
162    public function addValue(Header\Value|string $value, ?string $scheme = null, array $parameters = []): Header
163    {
164        if (is_string($value)) {
165            $value = new Header\Value($value, $scheme, $parameters);
166        }
167        $this->values[] = $value;
168        return $this;
169    }
170
171    /**
172     * Get the header values
173     *
174     * @return array
175     */
176    public function getValues(): array
177    {
178        return $this->values;
179    }
180
181    /**
182     * Get a header value object
183     *
184     * @param  int $i
185     * @return Header\Value|null
186     */
187    public function getValue(int $i = 0): Header\Value|null
188    {
189        return $this->values[$i] ?? null;
190    }
191
192    /**
193     * Get a header value as a string
194     *
195     * @param  int $i
196     * @return string|null
197     */
198    public function getValueAsString(int $i = 0): string|null
199    {
200        return isset($this->values[$i]) ? (string)$this->values[$i] : null;
201    }
202
203    /**
204     * Get index of header value
205     *
206     * @param  string $value
207     * @return int|bool
208     */
209    public function getValueIndex(string $value): int|bool
210    {
211        $result = false;
212
213        foreach ($this->values as $i => $val) {
214            if ($val->getValue() == $value) {
215                $result = $i;
216                break;
217            }
218        }
219
220        return $result;
221    }
222
223    /**
224     * Determine if the header has a value at index
225     *
226     * @param  int $i
227     * @return bool
228     */
229    public function hasValueAtIndex(int $i): bool
230    {
231        return (isset($this->values[$i]));
232    }
233
234    /**
235     * Determine if the header has a value
236     *
237     * @param  string $value
238     * @return bool
239     */
240    public function hasValue(string $value): bool
241    {
242        $result = false;
243
244        foreach ($this->values as $val) {
245            if ($val->getValue() == $value) {
246                $result = true;
247                break;
248            }
249        }
250
251        return $result;
252    }
253
254    /**
255     * Get the header values as strings
256     *
257     * @param  ?string $delimiter
258     * @return string|array
259     */
260    public function getValuesAsStrings(?string $delimiter = null): string|array
261    {
262        if (count($this->values) == 1) {
263            return (string)$this->values[0];
264        } else {
265            $values = [];
266            foreach ($this->values as $value) {
267                $values[] = (string)$value;
268            }
269
270            return ($delimiter !== null) ? implode($delimiter, $values) : $values;
271        }
272    }
273
274    /**
275     * Set the header wrap
276     *
277     * @param  int $wrap
278     * @return Header
279     */
280    public function setWrap(int $wrap): Header
281    {
282        $this->wrap = (int)$wrap;
283        return $this;
284    }
285
286    /**
287     * Get the header wrap
288     *
289     * @return int
290     */
291    public function getWrap(): int
292    {
293        return $this->wrap;
294    }
295
296    /**
297     * Has header wrap
298     *
299     * @return bool
300     */
301    public function hasWrap(): bool
302    {
303        return ($this->wrap !== 0);
304    }
305
306    /**
307     * Set the header wrap indent
308     *
309     * @param  string $indent
310     * @return Header
311     */
312    public function setIndent(string $indent): Header
313    {
314        $this->indent = $indent;
315        return $this;
316    }
317
318    /**
319     * Get the header wrap indent
320     *
321     * @return string
322     */
323    public function getIndent(): string
324    {
325        return $this->indent;
326    }
327
328    /**
329     * Has header wrap indent
330     *
331     * @return bool
332     */
333    public function hasIndent(): bool
334    {
335        return ($this->indent !== '');
336    }
337
338    /**
339     * Is the header for an attachment
340     *
341     * @return bool
342     */
343    public function isAttachment(): bool
344    {
345        if ($this->name != 'Content-Disposition') {
346            return false;
347        }
348
349        foreach ($this->values as $value) {
350            if ((stripos((string)$value, 'attachment') !== false) || (stripos((string)$value, 'inline') !== false)) {
351                return true;
352            }
353        }
354
355        return false;
356    }
357
358    /**
359     * Render the header string
360     *
361     * @return string
362     */
363    public function render(): string
364    {
365        $headers = [];
366
367        foreach ($this->values as $value) {
368            $header = $this->name . ': ' . $value->render($this->name);
369            if ((int)$this->wrap !== 0) {
370                $header = $this->fold($header);
371            }
372            $headers[] = $header;
373        }
374
375        return implode("\r\n", $headers);
376    }
377
378    /**
379     * Fold a rendered header line at RFC 5322 structural boundaries - only
380     * ever after a DELIMITER token or at an FWS token, never inside an ATOM
381     * or a QUOTED_STRING.
382     *
383     * @param  string $header
384     * @return string
385     */
386    protected function fold(string $header): string
387    {
388        $tokens = (new Header\Lexer($header))->tokenize();
389        $ranges = Header\EncodedWord::findRanges($header);
390
391        foreach ($tokens as $i => $token) {
392            if (($token->type === Header\Lexer::DELIMITER) && ($token->value === '<')) {
393                for ($j = $i + 1, $count = count($tokens); $j < $count; $j++) {
394                    if (($tokens[$j]->type === Header\Lexer::DELIMITER) && ($tokens[$j]->value === '>')) {
395                        $ranges[] = ['start' => $token->start, 'end' => $tokens[$j]->end];
396                        break;
397                    }
398                }
399            }
400        }
401
402        $result    = '';
403        $current   = '';
404        $lastEnd   = 0;
405        $skipUntil = 0;
406
407        foreach ($tokens as $token) {
408            if ($token->start < $skipUntil) {
409                continue;
410            }
411
412            $range = null;
413            foreach ($ranges as $candidate) {
414                if ($candidate['start'] === $token->start) {
415                    $range = $candidate;
416                    break;
417                }
418            }
419
420            if ($range !== null) {
421                $piece     = substr($header, $lastEnd, $range['end'] - $lastEnd);
422                $lastEnd   = $range['end'];
423                $skipUntil = $range['end'];
424                $isFws     = false;
425            } else {
426                $piece   = substr($header, $lastEnd, $token->end - $lastEnd);
427                $lastEnd = $token->end;
428                $isFws   = ($token->type === Header\Lexer::FWS);
429            }
430
431            if (($current !== '') && ((strlen($current) + strlen($piece)) > $this->wrap)) {
432                $result  .= rtrim($current) . "\r\n" . $this->indent;
433                $current  = '';
434                if ($isFws) {
435                    continue;
436                }
437            }
438
439            $current .= $piece;
440        }
441
442        $current .= substr($header, $lastEnd);
443
444        return $result . $current;
445    }
446
447    /**
448     * Render the header string
449     *
450     * @return string
451     */
452    public function __toString(): string
453    {
454        return $this->render();
455    }
456
457}