Completed
Push — master ( cd3f09...40bc2e )
by ignace nyamagana
02:21
created

AbstractCsv::isInputBOMIncluded()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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