Completed
Push — master ( c2ce6a...013468 )
by ignace nyamagana
03:31
created

AbstractCsv::sendHeaders()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 4

Importance

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