Completed
Push — master ( 39705e...b136da )
by ignace nyamagana
20s queued 15s
created

AbstractCsv::getPathname()   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.2.0
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 mb_strlen;
26
use function rawurlencode;
27
use function sprintf;
28
use function str_replace;
29
use function str_split;
30
use function strcspn;
31
use function strlen;
32
33
/**
34
 * An abstract class to enable CSV document loading.
35
 */
36
abstract class AbstractCsv implements ByteSequence
37
{
38
    /**
39
     * The stream filter mode (read or write).
40
     *
41
     * @var int
42
     */
43
    protected $stream_filter_mode;
44
45
    /**
46
     * collection of stream filters.
47
     *
48
     * @var bool[]
49
     */
50
    protected $stream_filters = [];
51
52
    /**
53
     * The CSV document BOM sequence.
54
     *
55
     * @var string|null
56
     */
57
    protected $input_bom = null;
58
59
    /**
60
     * The Output file BOM character.
61
     *
62
     * @var string
63
     */
64
    protected $output_bom = '';
65
66
    /**
67
     * the field delimiter (one character only).
68
     *
69
     * @var string
70
     */
71
    protected $delimiter = ',';
72
73
    /**
74
     * the field enclosure character (one character only).
75
     *
76
     * @var string
77
     */
78
    protected $enclosure = '"';
79
80
    /**
81
     * the field escape character (one character only).
82
     *
83
     * @var string
84
     */
85
    protected $escape = '\\';
86
87
    /**
88
     * The CSV document.
89
     *
90
     * @var SplFileObject|Stream
91
     */
92
    protected $document;
93
94
    /**
95
     * New instance.
96
     *
97
     * @param SplFileObject|Stream $document The CSV Object instance
98
     */
99 36
    protected function __construct($document)
100
    {
101 36
        $this->document = $document;
102 36
        list($this->delimiter, $this->enclosure, $this->escape) = $this->document->getCsvControl();
103 36
        $this->resetProperties();
104 36
    }
105
106
    /**
107
     * Reset dynamic object properties to improve performance.
108
     */
109 33
    protected function resetProperties()
110
    {
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', static::class));
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 6
    public static function createFromPath(string $path, string $open_mode = 'r+', $context = null)
169
    {
170 6
        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 pathname of the underlying document.
191
     */
192 12
    public function getPathname(): string
193
    {
194 12
        return $this->document->getPathname();
195
    }
196
197
    /**
198
     * Returns the current field escape character.
199
     */
200 3
    public function getEscape(): string
201
    {
202 3
        return $this->escape;
203
    }
204
205
    /**
206
     * Returns the BOM sequence in use on Output methods.
207
     */
208 3
    public function getOutputBOM(): string
209
    {
210 3
        return $this->output_bom;
211
    }
212
213
    /**
214
     * Returns the BOM sequence of the given CSV.
215
     */
216 54
    public function getInputBOM(): string
217
    {
218 54
        if (null !== $this->input_bom) {
219 3
            return $this->input_bom;
220
        }
221
222 54
        $this->document->setFlags(SplFileObject::READ_CSV);
223 54
        $this->document->rewind();
224 54
        $this->input_bom = bom_match((string) $this->document->fread(4));
225
226 54
        return $this->input_bom;
227
    }
228
229
    /**
230
     * Returns the stream filter mode.
231
     */
232 3
    public function getStreamFilterMode(): int
233
    {
234 3
        return $this->stream_filter_mode;
235
    }
236
237
    /**
238
     * Tells whether the stream filter capabilities can be used.
239
     */
240 6
    public function supportsStreamFilter(): bool
241
    {
242 6
        return $this->document instanceof Stream;
243
    }
244
245
    /**
246
     * Tell whether the specify stream filter is attach to the current stream.
247
     */
248 3
    public function hasStreamFilter(string $filtername): bool
249
    {
250 3
        return $this->stream_filters[$filtername] ?? false;
251
    }
252
253
    /**
254
     * Retuns the CSV document as a Generator of string chunk.
255
     *
256
     * @param int $length number of bytes read
257
     *
258
     * @throws Exception if the number of bytes is lesser than 1
259
     */
260 15
    public function chunk(int $length): Generator
261
    {
262 15
        if ($length < 1) {
263 3
            throw new Exception(sprintf('%s() expects the length to be a positive integer %d given', __METHOD__, $length));
264
        }
265
266 12
        $input_bom = $this->getInputBOM();
267 12
        $this->document->rewind();
268 12
        $this->document->fseek(strlen($input_bom));
269 12
        foreach (str_split($this->output_bom.$this->document->fread($length), $length) as $chunk) {
270 12
            yield $chunk;
271
        }
272
273 12
        while ($this->document->valid()) {
274 7
            yield $this->document->fread($length);
275
        }
276 12
    }
277
278
    /**
279
     * DEPRECATION WARNING! This method will be removed in the next major point release.
280
     *
281
     * @deprecated deprecated since version 9.1.0
282
     * @see AbstractCsv::getContent
283
     *
284
     * Retrieves the CSV content
285
     */
286 3
    public function __toString(): string
287
    {
288 3
        return $this->getContent();
289
    }
290
291
    /**
292
     * Retrieves the CSV content.
293
     */
294 18
    public function getContent(): string
295
    {
296 18
        $raw = '';
297 18
        foreach ($this->chunk(8192) as $chunk) {
298 18
            $raw .= $chunk;
299
        }
300
301 18
        return $raw;
302
    }
303
304
    /**
305
     * Outputs all data on the CSV file.
306
     *
307
     * @return int Returns the number of characters read from the handle
308
     *             and passed through to the output.
309
     */
310 9
    public function output(string $filename = null): int
311
    {
312 9
        if (null !== $filename) {
313 9
            $this->sendHeaders($filename);
314
        }
315 6
        $input_bom = $this->getInputBOM();
316 6
        $this->document->rewind();
317 6
        $this->document->fseek(strlen($input_bom));
318 6
        echo $this->output_bom;
319
320 6
        return strlen($this->output_bom) + $this->document->fpassthru();
321
    }
322
323
    /**
324
     * Send the CSV headers.
325
     *
326
     * Adapted from Symfony\Component\HttpFoundation\ResponseHeaderBag::makeDisposition
327
     *
328
     * @throws Exception if the submitted header is invalid according to RFC 6266
329
     *
330
     * @see https://tools.ietf.org/html/rfc6266#section-4.3
331
     */
332 9
    protected function sendHeaders(string $filename)
333
    {
334 9
        if (strlen($filename) != strcspn($filename, '\\/')) {
335 3
            throw new Exception('The filename cannot contain the "/" and "\\" characters.');
336
        }
337
338 6
        $flag = FILTER_FLAG_STRIP_LOW;
339 6
        if (strlen($filename) !== mb_strlen($filename)) {
340 3
            $flag |= FILTER_FLAG_STRIP_HIGH;
341
        }
342
343 6
        $filenameFallback = str_replace('%', '', filter_var($filename, FILTER_SANITIZE_STRING, $flag));
344
345 6
        $disposition = sprintf('attachment; filename="%s"', str_replace('"', '\\"', $filenameFallback));
346 6
        if ($filename !== $filenameFallback) {
347 3
            $disposition .= sprintf("; filename*=utf-8''%s", rawurlencode($filename));
348
        }
349
350 6
        header('Content-Type: text/csv');
351 6
        header('Content-Transfer-Encoding: binary');
352 6
        header('Content-Description: File Transfer');
353 6
        header('Content-Disposition: '.$disposition);
354 6
    }
355
356
    /**
357
     * Sets the field delimiter.
358
     *
359
     * @throws Exception If the Csv control character is not one character only.
360
     *
361
     * @return static
362
     */
363 18
    public function setDelimiter(string $delimiter): self
364
    {
365 18
        if ($delimiter === $this->delimiter) {
366 9
            return $this;
367
        }
368
369 15
        if (1 === strlen($delimiter)) {
370 15
            $this->delimiter = $delimiter;
371 15
            $this->resetProperties();
372
373 15
            return $this;
374
        }
375
376 3
        throw new Exception(sprintf('%s() expects delimiter to be a single character %s given', __METHOD__, $delimiter));
377
    }
378
379
    /**
380
     * Sets the field enclosure.
381
     *
382
     * @throws Exception If the Csv control character is not one character only.
383
     *
384
     * @return static
385
     */
386 3
    public function setEnclosure(string $enclosure): self
387
    {
388 3
        if ($enclosure === $this->enclosure) {
389 3
            return $this;
390
        }
391
392 3
        if (1 === strlen($enclosure)) {
393 3
            $this->enclosure = $enclosure;
394 3
            $this->resetProperties();
395
396 3
            return $this;
397
        }
398
399 3
        throw new Exception(sprintf('%s() expects enclosure to be a single character %s given', __METHOD__, $enclosure));
400
    }
401
402
    /**
403
     * Sets the field escape character.
404
     *
405
     * @throws Exception If the Csv control character is not one character only.
406
     *
407
     * @return static
408
     */
409 3
    public function setEscape(string $escape): self
410
    {
411 3
        if ($escape === $this->escape) {
412 3
            return $this;
413
        }
414
415 3
        if ('' === $escape || 1 === strlen($escape)) {
416 3
            $this->escape = $escape;
417 3
            $this->resetProperties();
418
419 3
            return $this;
420
        }
421
422 3
        throw new Exception(sprintf('%s() expects escape to be a single character or the empty string %s given', __METHOD__, $escape));
423
    }
424
425
    /**
426
     * Sets the BOM sequence to prepend the CSV on output.
427
     *
428
     * @return static
429
     */
430 9
    public function setOutputBOM(string $str): self
431
    {
432 9
        $this->output_bom = $str;
433
434 9
        return $this;
435
    }
436
437
    /**
438
     * append a stream filter.
439
     *
440
     * @param null|mixed $params
441
     *
442
     * @throws Exception If the stream filter API can not be used
443
     *
444
     * @return static
445
     */
446 15
    public function addStreamFilter(string $filtername, $params = null): self
447
    {
448 15
        if (!$this->document instanceof Stream) {
449 3
            throw new Exception('The stream filter API can not be used');
450
        }
451
452 12
        $this->document->appendFilter($filtername, $this->stream_filter_mode, $params);
453 9
        $this->stream_filters[$filtername] = true;
454 9
        $this->resetProperties();
455 9
        $this->input_bom = null;
456
457 9
        return $this;
458
    }
459
}
460