Completed
Pull Request — master (#269)
by ignace nyamagana
02:11
created

AbstractCsv::getOutputBOM()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
ccs 4
cts 4
cp 1
crap 1
rs 10
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.1.0
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 11
    protected function __construct($document)
93 11
    {
94 11
        $this->document = $document;
95 22
        list($this->delimiter, $this->enclosure, $this->escape) = $this->document->getCsvControl();
96 22
    }
97 11
98
    /**
99
     * {@inheritdoc}
100
     */
101 12
    public function __destruct()
102 12
    {
103 12
        unset($this->document);
104 24
    }
105 12
106
    /**
107
     * {@inheritdoc}
108
     */
109 1
    public function __clone()
110 1
    {
111 1
        throw new Exception(sprintf('An object of class %s cannot be cloned', get_class($this)));
112 1
    }
113
114
    /**
115
     * Return a new instance from a SplFileObject
116
     *
117
     * @param SplFileObject $file
118
     *
119
     * @return static
120
     */
121 12
    public static function createFromFileObject(SplFileObject $file): self
122 12
    {
123 12
        return new static($file);
124 12
    }
125
126
    /**
127
     * Return a new instance from a PHP resource stream
128
     *
129
     * @param resource $stream
130
     *
131
     * @return static
132
     */
133 2
    public static function createFromStream($stream): self
134 2
    {
135 2
        return new static(new Stream($stream));
136 2
    }
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 2
    public static function createFromString(string $content): self
146 2
    {
147 2
        return new static(Stream::createFromString($content));
148 2
    }
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 1
    public static function createFromPath(string $path, string $open_mode = 'r+', $context = null): self
160 1
    {
161 1
        return new static(Stream::createFromPath($path, $open_mode, $context));
162 1
    }
163
164
    /**
165
     * Returns the current field delimiter
166
     *
167
     * @return string
168
     */
169 5
    public function getDelimiter(): string
170 5
    {
171 5
        return $this->delimiter;
172 5
    }
173
174
    /**
175
     * Returns the current field enclosure
176
     *
177
     * @return string
178
     */
179 1
    public function getEnclosure(): string
180 1
    {
181 1
        return $this->enclosure;
182 1
    }
183
184
    /**
185
     * Returns the current field escape character
186
     *
187
     * @return string
188
     */
189 1
    public function getEscape(): string
190 1
    {
191 1
        return $this->escape;
192 1
    }
193
194
    /**
195
     * Returns the BOM sequence in use on Output methods
196
     *
197
     * @return string
198
     */
199 1
    public function getOutputBOM(): string
200 1
    {
201 1
        return $this->output_bom;
202 1
    }
203
204
    /**
205
     * Returns the BOM sequence of the given CSV
206
     *
207
     * @return string
208
     */
209 8
    public function getInputBOM(): string
210 8
    {
211 8
        if (null !== $this->input_bom) {
212 9
            return $this->input_bom;
213 1
        }
214
215 8
        $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 8
220 8
        return $this->input_bom;
221 8
    }
222
223
    /**
224
     * Returns the stream filter mode
225
     *
226
     * @return int
227
     */
228 1
    public function getStreamFilterMode(): int
229 1
    {
230 1
        return $this->stream_filter_mode;
231 1
    }
232
233
    /**
234
     * Tells whether the stream filter capabilities can be used
235
     *
236
     * @return bool
237
     */
238 2
    public function supportsStreamFilter(): bool
239 2
    {
240 2
        return $this->document instanceof Stream;
241 2
    }
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 1
    public function hasStreamFilter(string $filtername): bool
251 1
    {
252 1
        return $this->stream_filters[$filtername] ?? false;
253 1
    }
254
255
    /**
256
     * Retuns the CSV document as a Generator of string chunk
257
     *
258
     * @param int $length number of bytes read
259
     *
260
     * @throws Exception if the number of bytes is lesser than 1
261
     *
262
     * @return Generator
263
     */
264 5
    public function chunk(int $length): Generator
265 5
    {
266 5
        if ($length < 1) {
267 6
            throw new Exception(sprintf('%s() expects the length to be a positive integer %d given', __METHOD__, $length));
268 1
        }
269
270 4
        $input_bom = $this->getInputBOM();
271 8
        $this->document->rewind();
272 8
        $this->document->fseek(strlen($input_bom));
273 8
        foreach (str_split($this->output_bom.$this->document->fread($length), $length) as $chunk) {
274 8
            yield $chunk;
275 4
        }
276
277 4
        while ($this->document->valid()) {
278 7
            yield $this->document->fread($length);
279 3
        }
280 4
    }
281 4
282
    /**
283
     * DEPRECATION WARNING! This method will be removed in the next major point release
284
     *
285
     * @deprecated deprecated since version 9.1.0
286
     * @see AbstractCsv::getContent
287
     *
288
     * Retrieves the CSV content
289
     *
290
     * @return string
291
     */
292 1
    public function __toString(): string
293 6
    {
294 1
        return $this->getContent();
295
    }
296 6
297
    /**
298
     * Retrieves the CSV content
299
     *
300
     * @return string
301
     */
302 6
    public function getContent(): string
303
    {
304 6
        $raw = '';
305 6
        foreach ($this->chunk(8192) as $chunk) {
306 6
            $raw .= $chunk;
307
        }
308 4
309 6
        return $raw;
310 4
    }
311 4
312 4
    /**
313
     * Outputs all data on the CSV file
314
     *
315 4
     * @param string $filename CSV downloaded name if present adds extra headers
316
     *
317
     * @return int Returns the number of characters read from the handle
318
     *             and passed through to the output.
319
     */
320 3
    public function output(string $filename = null): int
321
    {
322 3
        if (null !== $filename) {
323 3
            $this->sendHeaders($filename);
324
        }
325 2
        $input_bom = $this->getInputBOM();
326 5
        $this->document->rewind();
327 2
        $this->document->fseek(strlen($input_bom));
328 5
        echo $this->output_bom;
329 3
330 2
        return strlen($this->output_bom) + $this->document->fpassthru();
331 2
    }
332 2
333 2
    /**
334 2
     * Send the CSV headers
335
     *
336 2
     * Adapted from Symfony\Component\HttpFoundation\ResponseHeaderBag::makeDisposition
337
     *
338
     * @param string|null $filename CSV disposition name
339
     *
340
     * @throws Exception if the submitted header is invalid according to RFC 6266
341
     *
342
     * @see https://tools.ietf.org/html/rfc6266#section-4.3
343
     */
344 3
    protected function sendHeaders(string $filename)
345
    {
346 3
        if (strlen($filename) != strcspn($filename, '\\/')) {
347 1
            throw new Exception('The filename cannot contain the "/" and "\\" characters.');
348
        }
349
350 5
        $flag = FILTER_FLAG_STRIP_LOW;
351 2
        if (strlen($filename) !== mb_strlen($filename)) {
352 4
            $flag |= FILTER_FLAG_STRIP_HIGH;
353 1
        }
354
355 2
        $filenameFallback = str_replace('%', '', filter_var($filename, FILTER_SANITIZE_STRING, $flag));
356 2
357 4
        $disposition = sprintf('attachment; filename="%s"', str_replace('"', '\\"', $filenameFallback));
358 3
        if ($filename !== $filenameFallback) {
359 1
            $disposition .= sprintf("; filename*=utf-8''%s", rawurlencode($filename));
360
        }
361 2
362 2
        header('Content-Type: text/csv');
363 4
        header('Content-Transfer-Encoding: binary');
364 4
        header('Content-Description: File Transfer');
365 3
        header('Content-Disposition: '.$disposition);
366 2
    }
367
368 2
    /**
369 2
     * Sets the field delimiter
370 2
     *
371 2
     * @param string $delimiter
372 2
     *
373
     * @throws Exception If the Csv control character is not one character only.
374
     *
375
     * @return static
376
     */
377 6
    public function setDelimiter(string $delimiter): self
378
    {
379 6
        if ($delimiter === $this->delimiter) {
380 3
            return $this;
381
        }
382
383 11
        if (1 === strlen($delimiter)) {
384 5
            $this->delimiter = $delimiter;
385 11
            $this->resetProperties();
386 3
387 5
            return $this;
388
        }
389 5
390 6
        throw new Exception(sprintf('%s() expects delimiter to be a single character %s given', __METHOD__, $delimiter));
391 5
    }
392
393 5
    /**
394
     * Reset dynamic object properties to improve performance
395
     */
396 3
    protected function resetProperties()
397
    {
398 2
    }
399
400
    /**
401
     * Sets the field enclosure
402 2
     *
403
     * @param string $enclosure
404 2
     *
405
     * @throws Exception If the Csv control character is not one character only.
406
     *
407
     * @return static
408
     */
409 1
    public function setEnclosure(string $enclosure): self
410
    {
411 1
        if ($enclosure === $this->enclosure) {
412 1
            return $this;
413
        }
414
415 2
        if (1 === strlen($enclosure)) {
416 1
            $this->enclosure = $enclosure;
417 2
            $this->resetProperties();
418 1
419 1
            return $this;
420
        }
421 1
422 2
        throw new Exception(sprintf('%s() expects enclosure to be a single character %s given', __METHOD__, $enclosure));
423 1
    }
424
425 1
    /**
426
     * Sets the field escape character
427
     *
428 1
     * @param string $escape
429
     *
430
     * @throws Exception If the Csv control character is not one character only.
431
     *
432
     * @return static
433
     */
434 1
    public function setEscape(string $escape): self
435
    {
436 1
        if ($escape === $this->escape) {
437 1
            return $this;
438
        }
439
440 2
        if (1 === strlen($escape)) {
441 1
            $this->escape = $escape;
442 2
            $this->resetProperties();
443 1
444 1
            return $this;
445
        }
446 1
447 2
        throw new Exception(sprintf('%s() expects escape to be a single character %s given', __METHOD__, $escape));
448 1
    }
449
450 1
    /**
451
     * Sets the BOM sequence to prepend the CSV on output
452
     *
453 1
     * @param string $str The BOM sequence
454
     *
455
     * @return static
456
     */
457 3
    public function setOutputBOM(string $str): self
458
    {
459 3
        $this->output_bom = $str;
460
461 3
        return $this;
462
    }
463 3
464
    /**
465 3
     * append a stream filter
466
     *
467 3
     * @param string $filtername a string or an object that implements the '__toString' method
468
     * @param mixed  $params     additional parameters for the filter
469
     *
470
     * @throws Exception If the stream filter API can not be used
471
     *
472
     * @return static
473
     */
474 5
    public function addStreamFilter(string $filtername, $params = null): self
475
    {
476 5
        if (!$this->document instanceof Stream) {
477 1
            throw new Exception('The stream filter API can not be used');
478
        }
479
480 9
        $this->document->appendFilter($filtername, $this->stream_filter_mode, $params);
481 3
        $this->stream_filters[$filtername] = true;
482 8
        $this->resetProperties();
483 4
        $this->input_bom = null;
484
485 3
        return $this;
486 4
    }
487
}
488