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