Completed
Pull Request — master (#396)
by
unknown
10:53
created

AbstractCsv::addStreamFilter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 13
ccs 8
cts 8
cp 1
crap 2
rs 9.8333
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 42
    protected function __construct($document)
105
    {
106 42
        $this->document = $document;
107 42
        [$this->delimiter, $this->enclosure, $this->escape] = $this->document->getCsvControl();
108 42
        $this->resetProperties();
109 42
    }
110
111
    /**
112
     * Reset dynamic object properties to improve performance.
113
     */
114 39
    protected function resetProperties(): void
115
    {
116 39
    }
117
118
    /**
119
     * {@inheritdoc}
120
     */
121 42
    public function __destruct()
122
    {
123 42
        unset($this->document);
124 42
    }
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 39
    public static function createFromFileObject(SplFileObject $file)
140
    {
141 39
        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 9
    public static function createFromString(string $content = '')
162
    {
163 9
        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 60
    public function getInputBOM(): string
222
    {
223 60
        if (null !== $this->input_bom) {
224 6
            return $this->input_bom;
225
        }
226
227 60
        $this->document->setFlags(SplFileObject::READ_CSV);
228 60
        $this->document->rewind();
229 60
        $this->input_bom = bom_match((string) $this->document->fread(4));
230
231 60
        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
	public function detectDelimiter(): string
378
	{
379
		$this->document->rewind();
380
	    $delimiters = array(
381
	        ';' => 0,
382
	        ',' => 0,
383
	        "\t" => 0,
384 21
	        "|" => 0
385
	    );
386 21
387 12
	    $firstLine = $this->document->fgets();
388
	    foreach ($delimiters as $delimiter => &$count) {
389
	        $count = count(str_getcsv($firstLine, $delimiter));
390 18
	    }
391 18
392 18
		if(max($delimiters) == 0)
393
			throw new DelimiterNotFound("Standard delimiter not found in file");
394 18
395
	    $this->delimiter = array_search(max($delimiters), $delimiters);
396
		return $this->delimiter
397 3
	}
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected '}', expecting ';'
Loading history...
398
399
    /**
400
     * Sets the field delimiter.
401
     *
402
     * @throws Exception If the Csv control character is not one character only.
403
     *
404
     * @return static
405
     */
406
    public function setDelimiter(string $delimiter): self
407 3
    {
408
        if ($delimiter === $this->delimiter) {
409 3
            return $this;
410 3
        }
411
412
        if (1 === strlen($delimiter)) {
413 3
            $this->delimiter = $delimiter;
414 3
            $this->resetProperties();
415 3
416
            return $this;
417 3
        }
418
419
        throw new InvalidArgument(sprintf('%s() expects delimiter to be a single character %s given', __METHOD__, $delimiter));
420 3
    }
421
422
    /**
423
     * Sets the field enclosure.
424
     *
425
     * @throws Exception If the Csv control character is not one character only.
426
     *
427
     * @return static
428
     */
429
    public function setEnclosure(string $enclosure): self
430 3
    {
431
        if ($enclosure === $this->enclosure) {
432 3
            return $this;
433 3
        }
434
435
        if (1 === strlen($enclosure)) {
436 3
            $this->enclosure = $enclosure;
437 3
            $this->resetProperties();
438 3
439
            return $this;
440 3
        }
441
442
        throw new InvalidArgument(sprintf('%s() expects enclosure to be a single character %s given', __METHOD__, $enclosure));
443 3
    }
444
445
    /**
446
     * Sets the field escape character.
447
     *
448
     * @throws Exception If the Csv control character is not one character only.
449
     *
450
     * @return static
451 3
     */
452
    public function setEscape(string $escape): self
453 3
    {
454
        if ($escape === $this->escape) {
455 3
            return $this;
456
        }
457
458
        if ('' === $escape || 1 === strlen($escape)) {
459
            $this->escape = $escape;
460
            $this->resetProperties();
461
462
            return $this;
463 6
        }
464
465 6
        throw new InvalidArgument(sprintf('%s() expects escape to be a single character or the empty string %s given', __METHOD__, $escape));
466
    }
467 6
468
    /**
469
     * Enables BOM Stripping.
470
     *
471
     * @return static
472
     */
473
    public function skipInputBOM(): self
474
    {
475 9
        $this->is_input_bom_included = false;
476
477 9
        return $this;
478
    }
479 9
480
    /**
481
     * Disables skipping Input BOM.
482
     *
483
     * @return static
484
     */
485
    public function includeInputBOM(): self
486
    {
487
        $this->is_input_bom_included = true;
488
489
        return $this;
490
    }
491 15
492
    /**
493 15
     * Sets the BOM sequence to prepend the CSV on output.
494 3
     *
495
     * @return static
496
     */
497 12
    public function setOutputBOM(string $str): self
498 9
    {
499 9
        $this->output_bom = $str;
500 9
501
        return $this;
502 9
    }
503
504
    /**
505
     * append a stream filter.
506
     *
507
     * @param null|mixed $params
508
     *
509
     * @throws Exception If the stream filter API can not be used
510
     *
511
     * @return static
512
     */
513
    public function addStreamFilter(string $filtername, $params = null): self
514
    {
515
        if (!$this->document instanceof Stream) {
516
            throw new UnavailableFeature('The stream filter API can not be used with a '.get_class($this->document).' instance.');
517
        }
518
519
        $this->document->appendFilter($filtername, $this->stream_filter_mode, $params);
520
        $this->stream_filters[$filtername] = true;
521
        $this->resetProperties();
522
        $this->input_bom = null;
523
524
        return $this;
525
    }
526
}
527