AbstractCsv::sendHeaders()   A
last analyzed

Complexity

Conditions 4
Paths 5

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
nc 5
nop 1
dl 0
loc 25
ccs 16
cts 16
cp 1
crap 4
rs 9.52
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * League.Csv (https://csv.thephpleague.com)
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Csv;
15
16
use Generator;
17
use SplFileObject;
18
use function filter_var;
19
use function get_class;
20
use function mb_strlen;
21
use function rawurlencode;
22
use function sprintf;
23
use function str_replace;
24
use function str_split;
25
use function strcspn;
26
use function strlen;
27
use const FILTER_FLAG_STRIP_HIGH;
28
use const FILTER_FLAG_STRIP_LOW;
29
use const FILTER_SANITIZE_STRING;
30
31
/**
32
 * An abstract class to enable CSV document loading.
33
 */
34
abstract class AbstractCsv implements ByteSequence
35
{
36
    /**
37
     * The stream filter mode (read or write).
38
     *
39
     * @var int
40
     */
41
    protected $stream_filter_mode;
42
43
    /**
44
     * collection of stream filters.
45
     *
46
     * @var bool[]
47
     */
48
    protected $stream_filters = [];
49
50
    /**
51
     * The CSV document BOM sequence.
52
     *
53
     * @var string|null
54
     */
55
    protected $input_bom = null;
56
57
    /**
58
     * The Output file BOM character.
59
     *
60
     * @var string
61
     */
62
    protected $output_bom = '';
63
64
    /**
65
     * the field delimiter (one character only).
66
     *
67
     * @var string
68
     */
69
    protected $delimiter = ',';
70
71
    /**
72
     * the field enclosure character (one character only).
73
     *
74
     * @var string
75
     */
76
    protected $enclosure = '"';
77
78
    /**
79
     * the field escape character (one character only).
80
     *
81
     * @var string
82
     */
83
    protected $escape = '\\';
84
85
    /**
86
     * The CSV document.
87
     *
88
     * @var SplFileObject|Stream
89
     */
90
    protected $document;
91
92
    /**
93
     * Tells whether the Input BOM must be included or skipped.
94
     *
95
     * @var bool
96
     */
97
    protected $is_input_bom_included = false;
98
99
    /**
100
     * New instance.
101
     *
102
     * @param SplFileObject|Stream $document The CSV Object instance
103
     */
104 51
    protected function __construct($document)
105
    {
106 51
        $this->document = $document;
107 51
        [$this->delimiter, $this->enclosure, $this->escape] = $this->document->getCsvControl();
108 51
        $this->resetProperties();
109 51
    }
110
111
    /**
112
     * Reset dynamic object properties to improve performance.
113
     */
114 48
    protected function resetProperties(): void
115
    {
116 48
    }
117
118
    /**
119
     * {@inheritdoc}
120
     */
121 51
    public function __destruct()
122
    {
123 51
        unset($this->document);
124 51
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129 3
    public function __clone()
130
    {
131 3
        throw new Exception(sprintf('An object of class %s cannot be cloned', static::class));
132
    }
133
134
    /**
135
     * Return a new instance from a SplFileObject.
136
     *
137
     * @return static
138
     */
139 48
    public static function createFromFileObject(SplFileObject $file)
140
    {
141 48
        return new static($file);
142
    }
143
144
    /**
145
     * Return a new instance from a PHP resource stream.
146
     *
147
     * @param resource $stream
148
     *
149
     * @return static
150
     */
151 6
    public static function createFromStream($stream)
152
    {
153 6
        return new static(new Stream($stream));
154
    }
155
156
    /**
157
     * Return a new instance from a string.
158
     *
159
     * @return static
160
     */
161 18
    public static function createFromString(string $content = '')
162
    {
163 18
        return new static(Stream::createFromString($content));
164
    }
165
166
    /**
167
     * Return a new instance from a file path.
168
     *
169
     * @param resource|null $context the resource context
170
     *
171
     * @return static
172
     */
173 6
    public static function createFromPath(string $path, string $open_mode = 'r+', $context = null)
174
    {
175 6
        return new static(Stream::createFromPath($path, $open_mode, $context));
176
    }
177
178
    /**
179
     * Returns the current field delimiter.
180
     */
181 21
    public function getDelimiter(): string
182
    {
183 21
        return $this->delimiter;
184
    }
185
186
    /**
187
     * Returns the current field enclosure.
188
     */
189 3
    public function getEnclosure(): string
190
    {
191 3
        return $this->enclosure;
192
    }
193
194
    /**
195
     * Returns the pathname of the underlying document.
196
     */
197 12
    public function getPathname(): string
198
    {
199 12
        return $this->document->getPathname();
200
    }
201
202
    /**
203
     * Returns the current field escape character.
204
     */
205 3
    public function getEscape(): string
206
    {
207 3
        return $this->escape;
208
    }
209
210
    /**
211
     * Returns the BOM sequence in use on Output methods.
212
     */
213 3
    public function getOutputBOM(): string
214
    {
215 3
        return $this->output_bom;
216
    }
217
218
    /**
219
     * Returns the BOM sequence of the given CSV.
220
     */
221 69
    public function getInputBOM(): string
222
    {
223 69
        if (null !== $this->input_bom) {
224 12
            return $this->input_bom;
225
        }
226
227 69
        $this->document->setFlags(SplFileObject::READ_CSV);
228 69
        $this->document->rewind();
229 69
        $this->input_bom = bom_match((string) $this->document->fread(4));
230
231 69
        return $this->input_bom;
232
    }
233
234
    /**
235
     * Returns the stream filter mode.
236
     */
237 3
    public function getStreamFilterMode(): int
238
    {
239 3
        return $this->stream_filter_mode;
240
    }
241
242
    /**
243
     * Tells whether the stream filter capabilities can be used.
244
     */
245 6
    public function supportsStreamFilter(): bool
246
    {
247 6
        return $this->document instanceof Stream;
248
    }
249
250
    /**
251
     * Tell whether the specify stream filter is attach to the current stream.
252
     */
253 3
    public function hasStreamFilter(string $filtername): bool
254
    {
255 3
        return $this->stream_filters[$filtername] ?? false;
256
    }
257
258
    /**
259
     * Tells whether the BOM can be stripped if presents.
260
     */
261 3
    public function isInputBOMIncluded(): bool
262
    {
263 3
        return $this->is_input_bom_included;
264
    }
265
266
    /**
267
     * Retuns the CSV document as a Generator of string chunk.
268
     *
269
     * @param int $length number of bytes read
270
     *
271
     * @throws Exception if the number of bytes is lesser than 1
272
     */
273 18
    public function chunk(int $length): Generator
274
    {
275 18
        if ($length < 1) {
276 3
            throw new InvalidArgument(sprintf('%s() expects the length to be a positive integer %d given', __METHOD__, $length));
277
        }
278
279 15
        $input_bom = $this->getInputBOM();
280 15
        $this->document->rewind();
281 15
        $this->document->setFlags(0);
282 15
        $this->document->fseek(strlen($input_bom));
283
        /** @var  array<int, string> $chunks */
284 15
        $chunks = str_split($this->output_bom.$this->document->fread($length), $length);
285 15
        foreach ($chunks as $chunk) {
286 15
            yield $chunk;
287
        }
288
289 15
        while ($this->document->valid()) {
290 3
            yield $this->document->fread($length);
291
        }
292 15
    }
293
294
    /**
295
     * DEPRECATION WARNING! This method will be removed in the next major point release.
296
     *
297
     * @deprecated deprecated since version 9.1.0
298
     * @see AbstractCsv::getContent
299
     *
300
     * Retrieves the CSV content
301
     */
302 3
    public function __toString(): string
303
    {
304 3
        return $this->getContent();
305
    }
306
307
    /**
308
     * Retrieves the CSV content.
309
     */
310 21
    public function getContent(): string
311
    {
312 21
        $raw = '';
313 21
        foreach ($this->chunk(8192) as $chunk) {
314 21
            $raw .= $chunk;
315
        }
316
317 21
        return $raw;
318
    }
319
320
    /**
321
     * Outputs all data on the CSV file.
322
     *
323
     * @return int Returns the number of characters read from the handle
324
     *             and passed through to the output.
325
     */
326 12
    public function output(string $filename = null): int
327
    {
328 12
        if (null !== $filename) {
329 9
            $this->sendHeaders($filename);
330
        }
331
332 9
        $this->document->rewind();
333 9
        if (!$this->is_input_bom_included) {
334 9
            $this->document->fseek(strlen($this->getInputBOM()));
335
        }
336
337 9
        echo $this->output_bom;
338
339 9
        return strlen($this->output_bom) + (int) $this->document->fpassthru();
340
    }
341
342
    /**
343
     * Send the CSV headers.
344
     *
345
     * Adapted from Symfony\Component\HttpFoundation\ResponseHeaderBag::makeDisposition
346
     *
347
     * @throws Exception if the submitted header is invalid according to RFC 6266
348
     *
349
     * @see https://tools.ietf.org/html/rfc6266#section-4.3
350
     */
351 9
    protected function sendHeaders(string $filename): void
352
    {
353 9
        if (strlen($filename) != strcspn($filename, '\\/')) {
354 3
            throw new InvalidArgument('The filename cannot contain the "/" and "\\" characters.');
355
        }
356
357 6
        $flag = FILTER_FLAG_STRIP_LOW;
358 6
        if (strlen($filename) !== mb_strlen($filename)) {
359 3
            $flag |= FILTER_FLAG_STRIP_HIGH;
360
        }
361
362
        /** @var string $filtered_name */
363 6
        $filtered_name = filter_var($filename, FILTER_SANITIZE_STRING, $flag);
364 6
        $filename_fallback = str_replace('%', '', $filtered_name);
365
366 6
        $disposition = sprintf('attachment; filename="%s"', str_replace('"', '\\"', $filename_fallback));
367 6
        if ($filename !== $filename_fallback) {
368 3
            $disposition .= sprintf("; filename*=utf-8''%s", rawurlencode($filename));
369
        }
370
371 6
        header('Content-Type: text/csv');
372 6
        header('Content-Transfer-Encoding: binary');
373 6
        header('Content-Description: File Transfer');
374 6
        header('Content-Disposition: '.$disposition);
375 6
    }
376
377
    /**
378
     * Sets the field delimiter.
379
     *
380
     * @throws Exception If the Csv control character is not one character only.
381
     *
382
     * @return static
383
     */
384 21
    public function setDelimiter(string $delimiter): self
385
    {
386 21
        if ($delimiter === $this->delimiter) {
387 12
            return $this;
388
        }
389
390 18
        if (1 === strlen($delimiter)) {
391 18
            $this->delimiter = $delimiter;
392 18
            $this->resetProperties();
393
394 18
            return $this;
395
        }
396
397 3
        throw new InvalidArgument(sprintf('%s() expects delimiter to be a single character %s given', __METHOD__, $delimiter));
398
    }
399
400
    /**
401
     * Sets the field enclosure.
402
     *
403
     * @throws Exception If the Csv control character is not one character only.
404
     *
405
     * @return static
406
     */
407 3
    public function setEnclosure(string $enclosure): self
408
    {
409 3
        if ($enclosure === $this->enclosure) {
410 3
            return $this;
411
        }
412
413 3
        if (1 === strlen($enclosure)) {
414 3
            $this->enclosure = $enclosure;
415 3
            $this->resetProperties();
416
417 3
            return $this;
418
        }
419
420 3
        throw new InvalidArgument(sprintf('%s() expects enclosure to be a single character %s given', __METHOD__, $enclosure));
421
    }
422
423
    /**
424
     * Sets the field escape character.
425
     *
426
     * @throws Exception If the Csv control character is not one character only.
427
     *
428
     * @return static
429
     */
430 3
    public function setEscape(string $escape): self
431
    {
432 3
        if ($escape === $this->escape) {
433 3
            return $this;
434
        }
435
436 3
        if ('' === $escape || 1 === strlen($escape)) {
437 3
            $this->escape = $escape;
438 3
            $this->resetProperties();
439
440 3
            return $this;
441
        }
442
443 3
        throw new InvalidArgument(sprintf('%s() expects escape to be a single character or the empty string %s given', __METHOD__, $escape));
444
    }
445
446
    /**
447
     * Enables BOM Stripping.
448
     *
449
     * @return static
450
     */
451 3
    public function skipInputBOM(): self
452
    {
453 3
        $this->is_input_bom_included = false;
454
455 3
        return $this;
456
    }
457
458
    /**
459
     * Disables skipping Input BOM.
460
     *
461
     * @return static
462
     */
463 6
    public function includeInputBOM(): self
464
    {
465 6
        $this->is_input_bom_included = true;
466
467 6
        return $this;
468
    }
469
470
    /**
471
     * Sets the BOM sequence to prepend the CSV on output.
472
     *
473
     * @return static
474
     */
475 9
    public function setOutputBOM(string $str): self
476
    {
477 9
        $this->output_bom = $str;
478
479 9
        return $this;
480
    }
481
482
    /**
483
     * append a stream filter.
484
     *
485
     * @param null|mixed $params
486
     *
487
     * @throws Exception If the stream filter API can not be used
488
     *
489
     * @return static
490
     */
491 15
    public function addStreamFilter(string $filtername, $params = null): self
492
    {
493 15
        if (!$this->document instanceof Stream) {
494 3
            throw new UnavailableFeature('The stream filter API can not be used with a '.get_class($this->document).' instance.');
495
        }
496
497 12
        $this->document->appendFilter($filtername, $this->stream_filter_mode, $params);
498 9
        $this->stream_filters[$filtername] = true;
499 9
        $this->resetProperties();
500 9
        $this->input_bom = null;
501
502 9
        return $this;
503
    }
504
}
505