Completed
Pull Request — master (#48)
by Frederik
02:12
created

HeaderValue::fromEncodedString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
crap 1
1
<?php
2
declare(strict_types=1);
3
4
namespace Genkgo\Mail\Header;
5
6
final class HeaderValue
7
{
8
    private const PARSE_START = 1;
9
    
10
    private const PARSE_QUOTE = 2;
11
12
    /**
13
     * @var string
14
     */
15
    private $value;
16
17
    /**
18
     * @var array
19
     */
20
    private $parameters = [];
21
22
    /**
23
     * @var array
24
     */
25
    private $parametersForceNewLine = [];
26
27
    /**
28
     * @var bool
29
     */
30
    private $needsEncoding = true;
31
32
    /**
33
     * @param string $value
34
     */
35 138
    public function __construct(string $value)
36
    {
37 138
        $this->assertValid($value);
38
39 135
        $this->value = $value;
40 135
    }
41
42
    /**
43
     * @param HeaderValueParameter $parameter
44
     * @param bool $forceNewLine
45
     * @return HeaderValue
46
     */
47 46
    public function withParameter(HeaderValueParameter $parameter, bool $forceNewLine = false): HeaderValue
48
    {
49 46
        $clone = clone $this;
50 46
        $clone->parameters[$parameter->getName()] = $parameter;
51 46
        $clone->parametersForceNewLine[$parameter->getName()] = $forceNewLine;
52 46
        return $clone;
53
    }
54
55
    /**
56
     * @return string
57
     */
58 23
    public function getRaw(): string
59
    {
60 23
        return $this->value;
61
    }
62
63
    /**
64
     * @return string
65
     */
66 114
    public function __toString(): string
67
    {
68 114
        $value = $this->value;
69
70 114
        if ($this->needsEncoding) {
71 106
            $value = new OptimalEncodedHeaderValue($value);
72
        }
73
74 114
        $parameters = [];
75
76 114
        foreach ($this->parameters as $name => $parameter) {
77 39
            $parameters[] = \sprintf(
78 39
                ';%s%s',
79 39
                empty($this->parametersForceNewLine[$name]) ? ' ' : "\r\n ",
80 39
                (string) $parameter
81
            );
82
        }
83
84 114
        return \implode('', \array_merge([$value], $parameters));
85
    }
86
87
    /**
88
     * @param string $value
89
     */
90 138
    private function assertValid(string $value): void
91
    {
92 138
        if ($this->validate($value) === false) {
93 3
            throw new \InvalidArgumentException('Invalid header value ' . $value);
94
        }
95 135
    }
96
97
    /**
98
     * @param string $value
99
     * @return bool
100
     */
101 138
    private function validate(string $value): bool
102
    {
103 138
        $total = \strlen($value);
104
105 138
        for ($i = 0; $i < $total; $i += 1) {
106 138
            $ord = \ord($value[$i]);
107 138
            if ($ord === 10) {
108 1
                return false;
109
            }
110 138
            if ($ord === 13) {
111 6
                if ($i + 2 >= $total) {
112
                    return false;
113
                }
114 6
                $lf = \ord($value[$i + 1]);
115 6
                $sp = \ord($value[$i + 2]);
116 6
                if ($lf !== 10 || ! \in_array($sp, [9, 32], true)) {
117 2
                    return false;
118
                }
119
                // skip over the LF following this
120 4
                $i += 2;
121
            }
122
        }
123
124 135
        return true;
125
    }
126
127
    /**
128
     * @param string $name
129
     * @return HeaderValueParameter
130
     */
131 14
    public function getParameter(string $name): HeaderValueParameter
132
    {
133 14
        if (isset($this->parameters[$name])) {
134 14
            return $this->parameters[$name];
135
        }
136
137
        throw new \UnexpectedValueException('No parameter with name ' . $name);
138
    }
139
140
    /**
141
     * @param string $value
142
     * @return HeaderValue
143
     */
144 22
    public static function fromEncodedString(string $value): HeaderValue
145
    {
146 22
        $headerValue = new self($value);
147 22
        $headerValue->needsEncoding = false;
148 22
        return $headerValue;
149
    }
150
151
    /**
152
     * @param string $headerValueAsString
153
     * @return HeaderValue
154
     */
155 35
    public static function fromString(string $headerValueAsString): HeaderValue
156
    {
157 35
        $values = [];
158
159 35
        $headerValueAsString = \trim($headerValueAsString);
160
161 35
        $length = \strlen($headerValueAsString) - 1;
162 35
        $n = -1;
163 35
        $state = self::PARSE_START;
164 35
        $escapeNext = false;
165 35
        $sequence = '';
166
167 35
        while ($n < $length) {
168 35
            $n++;
169
170 35
            $char = $headerValueAsString[$n];
171
172 35
            $sequence .= $char;
173
174 35
            if ($char === '\\') {
175
                $escapeNext = true;
176
                continue;
177
            }
178
179 35
            if ($escapeNext) {
180
                $escapeNext = false;
181
                continue;
182
            }
183
184
            switch ($state) {
185 35
                case self::PARSE_QUOTE:
186 5
                    if ($char === '"') {
187 5
                        $state = self::PARSE_START;
188
                    }
189
190 5
                    break;
191
                default:
192 35
                    if ($char === '"') {
193 5
                        $state = self::PARSE_QUOTE;
194
                    }
195
196 35
                    if ($char === ';') {
197 8
                        $values[] = \trim(\substr($sequence, 0, -1));
198 8
                        $sequence = '';
199 8
                        $state = self::PARSE_START;
200
                    }
201 35
                    break;
202
            }
203
        }
204
205 35
        $values[] = \trim($sequence);
206
207 35
        $headerValue = new self($values[0]);
208
209 35
        $parameters = [];
210 35
        foreach (\array_slice($values, 1) as $parameterString) {
211 8
            $parameter = HeaderValueParameter::fromString($parameterString);
212 8
            $parameters[$parameter->getName()] = $parameter;
213
        }
214
215 35
        $headerValue->parameters = $parameters;
216 35
        return $headerValue;
217
    }
218
}
219