Completed
Push — master ( a327ab...c6c647 )
by ignace nyamagana
55:37 queued 54:07
created

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