ContentDisposition   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 270
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 106
c 1
b 0
f 0
dl 0
loc 270
ccs 116
cts 116
cp 1
rs 9.92
wmc 31

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A unsetParameter() 0 4 2
A setParameter() 0 4 2
A setFilename() 0 12 2
A getValue() 0 8 2
A getName() 0 3 1
A __toString() 0 3 1
A setType() 0 8 3
A fromValue() 0 19 5
B toAscii() 0 76 7
A getType() 0 3 1
A hasParameter() 0 3 1
A isValid() 0 3 1
A getParameter() 0 7 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Stadly\Http\Header\Response;
6
7
use Cocur\Slugify\Slugify;
8
use InvalidArgumentException;
9
use OutOfBoundsException;
10
use Stadly\Http\Exception\InvalidHeader;
11
use Stadly\Http\Header\Value\ContentDisposition\ExtendedParameter;
12
use Stadly\Http\Header\Value\ContentDisposition\Parameter;
13
use Stadly\Http\Header\Value\ContentDisposition\RegularParameter;
14
use Stadly\Http\Utilities\Rfc2616;
15
use Stadly\Http\Utilities\Rfc6266;
16
17
/**
18
 * Class for handling the HTTP header field Content-Disposition.
19
 *
20
 * Specification: https://tools.ietf.org/html/rfc6266#section-4
21
 */
22
final class ContentDisposition implements Header
23
{
24
    private const INVALID_CHAR_PLACEHOLDER = '-';
25
26
    /**
27
     * @var string Type.
28
     */
29
    private $type;
30
31
    /**
32
     * @var array<Parameter> Parameters.
33
     */
34
    private $parameters = [];
35
36
    /**
37
     * Constructor.
38
     *
39
     * @param string $type Type. Usually `attachment` or `inline`.
40
     * @param Parameter ...$parameters Parameters.
41
     */
42 3
    public function __construct(string $type, Parameter ...$parameters)
43
    {
44 3
        $this->setType($type);
45 1
        $this->setParameter(...$parameters);
46
    }
47
48
    /**
49
     * Construct header from value.
50
     *
51
     * @param string $value Header value.
52
     * @return self Header generated based on the value.
53
     * @throws InvalidHeader If the header value is invalid.
54
     */
55 2
    public static function fromValue(string $value): self
56
    {
57 2
        $regEx = '{^' . Rfc6266::CONTENT_DISPOSITION_VALUE_CAPTURE . '$}';
58 2
        $plainValue = mb_convert_encoding($value, 'ISO-8859-1', 'UTF-8');
59 2
        if ($plainValue !== $value || preg_match($regEx, $value, $matches) !== 1) {
60 1
            throw new InvalidHeader('Invalid header value: ' . $value);
61
        }
62
63 1
        $parameters = [];
64 1
        if (isset($matches['DISPOSITION_PARAMS'])) {
65 1
            $parameterRegEx = '{' . Rfc6266::DISPOSITION_PARM_CAPTURE . '}';
66 1
            preg_match_all($parameterRegEx, $matches['DISPOSITION_PARAMS'], $parameterMatches);
67
68 1
            foreach ($parameterMatches['DISPOSITION_PARM'] as $parameter) {
69 1
                $parameters[] = Parameter::fromString($parameter);
70
            }
71
        }
72
73 1
        return new self($matches['DISPOSITION_TYPE'], ...$parameters);
74
    }
75
76
    /**
77
     * @inheritDoc
78
     * @throws void The header is always valid.
79
     */
80 3
    public function __toString(): string
81
    {
82 3
        return $this->getName() . ': ' . $this->getValue();
83
    }
84
85
    /**
86
     * @return true The header is always valid.
87
     */
88 1
    public function isValid(): bool
89
    {
90 1
        return true;
91
    }
92
93
    /**
94
     * @inheritDoc
95
     */
96 1
    public function getName(): string
97
    {
98 1
        return 'Content-Disposition';
99
    }
100
101
    /**
102
     * @inheritDoc
103
     * @throws void The header is always valid.
104
     */
105 3
    public function getValue(): string
106
    {
107 3
        $contentDisposition = $this->type;
108 3
        if ($this->parameters !== []) {
109 2
            $contentDisposition .= '; ' . implode('; ', $this->parameters);
110
        }
111
112 3
        return $contentDisposition;
113
    }
114
115
    /**
116
     * @return string Type.
117
     */
118 1
    public function getType(): string
119
    {
120 1
        return $this->type;
121
    }
122
123
    /**
124
     * Set type.
125
     *
126
     * @param string $type Type. Usually `attachment` or `inline`.
127
     */
128 3
    public function setType(string $type): void
129
    {
130 3
        $plainType = mb_convert_encoding($type, 'ISO-8859-1', 'UTF-8');
131 3
        if ($plainType !== $type || preg_match('{^' . Rfc6266::DISPOSITION_TYPE . '$}', $type) !== 1) {
132 2
            throw new InvalidArgumentException('Invalid type: ' . $type);
133
        }
134
135 3
        $this->type = $type;
136
    }
137
138
    /**
139
     * @param string $name Parameter name.
140
     * @return bool Whether the parameter exists.
141
     */
142 2
    public function hasParameter(string $name): bool
143
    {
144 2
        return array_key_exists(strtolower($name), $this->parameters);
145
    }
146
147
    /**
148
     * @param string $name Parameter name.
149
     * @return Parameter Parameter.
150
     * @throws OutOfBoundsException If the parameter does not exist.
151
     */
152 2
    public function getParameter(string $name): Parameter
153
    {
154 2
        if (!$this->hasParameter($name)) {
155 1
            throw new OutOfBoundsException('Parameter not found: ' . $name);
156
        }
157
158 1
        return $this->parameters[strtolower($name)];
159
    }
160
161
    /**
162
     * Set parameters.
163
     *
164
     * @param Parameter ...$parameters Parameters to set.
165
     */
166 3
    public function setParameter(Parameter ...$parameters): void
167
    {
168 3
        foreach ($parameters as $parameter) {
169 3
            $this->parameters[strtolower($parameter->getName())] = $parameter;
170
        }
171
    }
172
173
    /**
174
     * Unset parameters.
175
     *
176
     * @param string ...$names Parameter names.
177
     */
178 4
    public function unsetParameter(string ...$names): void
179
    {
180 4
        foreach ($names as $name) {
181 4
            unset($this->parameters[strtolower($name)]);
182
        }
183
    }
184
185
    /**
186
     * The Content-Disposition header has two filename parameters:
187
     * - `filename`, which accepts only ASCII characters.
188
     * - `filename*`, which  accepts all characters, but is not supported by all clients.
189
     *
190
     * This method sets the `filename` parameter to an ASCII-version of the filename,
191
     * and additionally sets the `filename*` parameter if needed.
192
     *
193
     * @param string $filename Filename.
194
     */
195 3
    public function setFilename(string $filename): void
196
    {
197 3
        $this->unsetParameter('filename', 'filename*');
198
199 3
        $ascii = self::toAscii($filename);
200 3
        if ($filename === $ascii) {
201
            // Filename can be encoded using ASCII.
202 2
            $this->setParameter(new RegularParameter('filename', $filename));
203
        } else {
204
            // Filename cannot be encoded using ASCII.
205 1
            $this->setParameter(new RegularParameter('filename', $ascii));
206 1
            $this->setParameter(new ExtendedParameter('filename*', $filename));
207
        }
208
    }
209
210
    /**
211
     * Convert a string so that it contains only ASCII chars.
212
     *
213
     * @param string $string String.
214
     * @return string Converted string containing only ASCII chars.
215
     */
216 3
    private static function toAscii(string $string): string
217
    {
218 3
        $slugify = new Slugify([
219 3
            'regexp' => '{' . self::INVALID_CHAR_PLACEHOLDER . '}',
220 3
            'lowercase' => false,
221 3
            'trim' => false,
222 3
            'rulesets' => [
223 3
                'arabic',
224 3
                'armenian',
225 3
                'austrian',
226 3
                'azerbaijani',
227 3
                'bulgarian',
228 3
                'burmese',
229 3
                'chinese',
230 3
                'croatian',
231 3
                'czech',
232 3
                'danish',
233 3
                'default',
234 3
                'esperanto',
235 3
                'estonian',
236 3
                'finnish',
237 3
                'french',
238 3
                'georgian',
239 3
                'german',
240 3
                'greek',
241 3
                'hindi',
242 3
                'hungarian',
243 3
                'italian',
244 3
                'latvian',
245 3
                'lithuanian',
246 3
                'macedonian',
247 3
                'norwegian',
248 3
                'persian',
249 3
                'polish',
250 3
                'portuguese-brazil',
251 3
                'romanian',
252 3
                'russian',
253 3
                'serbian',
254 3
                'slovak',
255 3
                'swedish',
256 3
                'turkish',
257 3
                'turkmen',
258 3
                'ukrainian',
259 3
                'vietnamese',
260 3
            ],
261 3
        ]);
262
263 3
        $chars = preg_split('{}u', $string, -1, PREG_SPLIT_NO_EMPTY);
264 3
        assert($chars !== false);
265 3
        $asciiString = '';
266 3
        $lastCharIsPlaceholder = false;
267 3
        $regEx = '{^(?:' . Rfc2616::QDTEXT . '|' . Rfc2616::QUOTED_PAIR . ')+$}';
268 3
        foreach ($chars as $char) {
269 3
            $plainChar = mb_convert_encoding($char, 'ISO-8859-1', 'UTF-8');
270
271
            // If the char is not ASCII, try converting it to one or more similar ASCII chars.
272 3
            $utf8Char = mb_convert_encoding($plainChar, 'UTF-8', 'ISO-8859-1');
273 3
            if ($utf8Char !== $char || preg_match($regEx, addslashes($plainChar)) !== 1) {
0 ignored issues
show
Bug introduced by
It seems like $plainChar can also be of type array; however, parameter $string of addslashes() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

273
            if ($utf8Char !== $char || preg_match($regEx, addslashes(/** @scrutinizer ignore-type */ $plainChar)) !== 1) {
Loading history...
274 1
                $char = $slugify->slugify($char);
275 1
                $plainChar = mb_convert_encoding($char, 'ISO-8859-1', 'UTF-8');
276
            }
277
278
            // If the char could not be converted to ASCII, replace it with a placeholder.
279 3
            $utf8Char = mb_convert_encoding($plainChar, 'UTF-8', 'ISO-8859-1');
280 3
            if ($utf8Char !== $char || preg_match($regEx, addslashes($plainChar)) !== 1) {
281 1
                if (!$lastCharIsPlaceholder) {
282 1
                    $asciiString .= self::INVALID_CHAR_PLACEHOLDER;
283 1
                    $lastCharIsPlaceholder = true;
284
                }
285
            } else {
286 3
                $asciiString .= $char;
287 3
                $lastCharIsPlaceholder = false;
288
            }
289
        }
290
291 3
        return $asciiString;
292
    }
293
}
294