Completed
Pull Request — master (#309)
by ignace nyamagana
02:28
created

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