Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
 
3
namespace PhpOffice\PhpSpreadsheet\Reader;
4
 
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Cell\Cell;
7
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8
use PhpOffice\PhpSpreadsheet\Reader\Csv\Delimiter;
9
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
10
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
11
use PhpOffice\PhpSpreadsheet\Spreadsheet;
12
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
13
use Throwable;
14
 
15
class Csv extends BaseReader
16
{
17
    const DEFAULT_FALLBACK_ENCODING = 'CP1252';
18
    const GUESS_ENCODING = 'guess';
19
    const UTF8_BOM = "\xEF\xBB\xBF";
20
    const UTF8_BOM_LEN = 3;
21
    const UTF16BE_BOM = "\xfe\xff";
22
    const UTF16BE_BOM_LEN = 2;
23
    const UTF16BE_LF = "\x00\x0a";
24
    const UTF16LE_BOM = "\xff\xfe";
25
    const UTF16LE_BOM_LEN = 2;
26
    const UTF16LE_LF = "\x0a\x00";
27
    const UTF32BE_BOM = "\x00\x00\xfe\xff";
28
    const UTF32BE_BOM_LEN = 4;
29
    const UTF32BE_LF = "\x00\x00\x00\x0a";
30
    const UTF32LE_BOM = "\xff\xfe\x00\x00";
31
    const UTF32LE_BOM_LEN = 4;
32
    const UTF32LE_LF = "\x0a\x00\x00\x00";
33
 
34
    /**
35
     * Input encoding.
36
     */
37
    private string $inputEncoding = 'UTF-8';
38
 
39
    /**
40
     * Fallback encoding if guess strikes out.
41
     */
42
    private string $fallbackEncoding = self::DEFAULT_FALLBACK_ENCODING;
43
 
44
    /**
45
     * Delimiter.
46
     */
47
    private ?string $delimiter = null;
48
 
49
    /**
50
     * Enclosure.
51
     */
52
    private string $enclosure = '"';
53
 
54
    /**
55
     * Sheet index to read.
56
     */
57
    private int $sheetIndex = 0;
58
 
59
    /**
60
     * Load rows contiguously.
61
     */
62
    private bool $contiguous = false;
63
 
64
    /**
65
     * The character that can escape the enclosure.
66
     * This will probably become unsupported in Php 9.
67
     * Not yet ready to mark deprecated in order to give users
68
     * a migration path.
69
     */
70
    private ?string $escapeCharacter = null;
71
 
72
    /**
73
     * Callback for setting defaults in construction.
74
     *
75
     * @var ?callable
76
     */
77
    private static $constructorCallback;
78
 
79
    /** Changed from true to false in release 4.0.0 */
80
    public const DEFAULT_TEST_AUTODETECT = false;
81
 
82
    /**
83
     * Attempt autodetect line endings (deprecated after PHP8.1)?
84
     */
85
    private bool $testAutodetect = self::DEFAULT_TEST_AUTODETECT;
86
 
87
    protected bool $castFormattedNumberToNumeric = false;
88
 
89
    protected bool $preserveNumericFormatting = false;
90
 
91
    private bool $preserveNullString = false;
92
 
93
    private bool $sheetNameIsFileName = false;
94
 
95
    private string $getTrue = 'true';
96
 
97
    private string $getFalse = 'false';
98
 
99
    private string $thousandsSeparator = ',';
100
 
101
    private string $decimalSeparator = '.';
102
 
103
    /**
104
     * Create a new CSV Reader instance.
105
     */
106
    public function __construct()
107
    {
108
        parent::__construct();
109
        $callback = self::$constructorCallback;
110
        if ($callback !== null) {
111
            $callback($this);
112
        }
113
    }
114
 
115
    /**
116
     * Set a callback to change the defaults.
117
     *
118
     * The callback must accept the Csv Reader object as the first parameter,
119
     * and it should return void.
120
     */
121
    public static function setConstructorCallback(?callable $callback): void
122
    {
123
        self::$constructorCallback = $callback;
124
    }
125
 
126
    public static function getConstructorCallback(): ?callable
127
    {
128
        return self::$constructorCallback;
129
    }
130
 
131
    public function setInputEncoding(string $encoding): self
132
    {
133
        $this->inputEncoding = $encoding;
134
 
135
        return $this;
136
    }
137
 
138
    public function getInputEncoding(): string
139
    {
140
        return $this->inputEncoding;
141
    }
142
 
143
    public function setFallbackEncoding(string $fallbackEncoding): self
144
    {
145
        $this->fallbackEncoding = $fallbackEncoding;
146
 
147
        return $this;
148
    }
149
 
150
    public function getFallbackEncoding(): string
151
    {
152
        return $this->fallbackEncoding;
153
    }
154
 
155
    /**
156
     * Move filepointer past any BOM marker.
157
     */
158
    protected function skipBOM(): void
159
    {
160
        rewind($this->fileHandle);
161
 
162
        if (fgets($this->fileHandle, self::UTF8_BOM_LEN + 1) !== self::UTF8_BOM) {
163
            rewind($this->fileHandle);
164
        }
165
    }
166
 
167
    /**
168
     * Identify any separator that is explicitly set in the file.
169
     */
170
    protected function checkSeparator(): void
171
    {
172
        $line = fgets($this->fileHandle);
173
        if ($line === false) {
174
            return;
175
        }
176
 
177
        if ((strlen(trim($line, "\r\n")) == 5) && (stripos($line, 'sep=') === 0)) {
178
            $this->delimiter = substr($line, 4, 1);
179
 
180
            return;
181
        }
182
 
183
        $this->skipBOM();
184
    }
185
 
186
    /**
187
     * Infer the separator if it isn't explicitly set in the file or specified by the user.
188
     */
189
    protected function inferSeparator(): void
190
    {
191
        if ($this->delimiter !== null) {
192
            return;
193
        }
194
 
195
        $inferenceEngine = new Delimiter($this->fileHandle, $this->getEscapeCharacter(), $this->enclosure);
196
 
197
        // If number of lines is 0, nothing to infer : fall back to the default
198
        if ($inferenceEngine->linesCounted() === 0) {
199
            $this->delimiter = $inferenceEngine->getDefaultDelimiter();
200
            $this->skipBOM();
201
 
202
            return;
203
        }
204
 
205
        $this->delimiter = $inferenceEngine->infer();
206
 
207
        // If no delimiter could be detected, fall back to the default
208
        if ($this->delimiter === null) {
209
            $this->delimiter = $inferenceEngine->getDefaultDelimiter();
210
        }
211
 
212
        $this->skipBOM();
213
    }
214
 
215
    /**
216
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
217
     */
218
    public function listWorksheetInfo(string $filename): array
219
    {
220
        // Open file
221
        $this->openFileOrMemory($filename);
222
        $fileHandle = $this->fileHandle;
223
 
224
        // Skip BOM, if any
225
        $this->skipBOM();
226
        $this->checkSeparator();
227
        $this->inferSeparator();
228
 
229
        $worksheetInfo = [];
230
        $worksheetInfo[0]['worksheetName'] = 'Worksheet';
231
        $worksheetInfo[0]['lastColumnLetter'] = 'A';
232
        $worksheetInfo[0]['lastColumnIndex'] = 0;
233
        $worksheetInfo[0]['totalRows'] = 0;
234
        $worksheetInfo[0]['totalColumns'] = 0;
235
        $delimiter = $this->delimiter ?? '';
236
 
237
        // Loop through each line of the file in turn
238
        $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
239
        while (is_array($rowData)) {
240
            ++$worksheetInfo[0]['totalRows'];
241
            $worksheetInfo[0]['lastColumnIndex'] = max($worksheetInfo[0]['lastColumnIndex'], count($rowData) - 1);
242
            $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
243
        }
244
 
245
        $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1);
246
        $worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1;
247
        $worksheetInfo[0]['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
248
 
249
        // Close file
250
        fclose($fileHandle);
251
 
252
        return $worksheetInfo;
253
    }
254
 
255
    /**
256
     * Loads Spreadsheet from file.
257
     */
258
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
259
    {
260
        // Create new Spreadsheet
261
        $spreadsheet = new Spreadsheet();
262
        $spreadsheet->setValueBinder($this->valueBinder);
263
 
264
        // Load into this instance
265
        return $this->loadIntoExisting($filename, $spreadsheet);
266
    }
267
 
268
    /**
269
     * Loads Spreadsheet from string.
270
     */
271
    public function loadSpreadsheetFromString(string $contents): Spreadsheet
272
    {
273
        // Create new Spreadsheet
274
        $spreadsheet = new Spreadsheet();
275
        $spreadsheet->setValueBinder($this->valueBinder);
276
 
277
        // Load into this instance
278
        return $this->loadStringOrFile('data://text/plain,' . urlencode($contents), $spreadsheet, true);
279
    }
280
 
281
    private function openFileOrMemory(string $filename): void
282
    {
283
        // Open file
284
        $fhandle = $this->canRead($filename);
285
        if (!$fhandle) {
286
            throw new ReaderException($filename . ' is an Invalid Spreadsheet file.');
287
        }
288
        if ($this->inputEncoding === 'UTF-8') {
289
            $encoding = self::guessEncodingBom($filename);
290
            if ($encoding !== '') {
291
                $this->inputEncoding = $encoding;
292
            }
293
        }
294
        if ($this->inputEncoding === self::GUESS_ENCODING) {
295
            $this->inputEncoding = self::guessEncoding($filename, $this->fallbackEncoding);
296
        }
297
        $this->openFile($filename);
298
        if ($this->inputEncoding !== 'UTF-8') {
299
            fclose($this->fileHandle);
300
            $entireFile = file_get_contents($filename);
301
            $fileHandle = fopen('php://memory', 'r+b');
302
            if ($fileHandle !== false && $entireFile !== false) {
303
                $this->fileHandle = $fileHandle;
304
                $data = StringHelper::convertEncoding($entireFile, 'UTF-8', $this->inputEncoding);
305
                fwrite($this->fileHandle, $data);
306
                $this->skipBOM();
307
            }
308
        }
309
    }
310
 
311
    public function setTestAutoDetect(bool $value): self
312
    {
313
        $this->testAutodetect = $value;
314
 
315
        return $this;
316
    }
317
 
318
    private function setAutoDetect(?string $value, int $version = PHP_VERSION_ID): ?string
319
    {
320
        $retVal = null;
321
        if ($value !== null && $this->testAutodetect && $version < 90000) {
322
            $retVal2 = @ini_set('auto_detect_line_endings', $value);
323
            if (is_string($retVal2)) {
324
                $retVal = $retVal2;
325
            }
326
        }
327
 
328
        return $retVal;
329
    }
330
 
331
    public function castFormattedNumberToNumeric(
332
        bool $castFormattedNumberToNumeric,
333
        bool $preserveNumericFormatting = false
334
    ): void {
335
        $this->castFormattedNumberToNumeric = $castFormattedNumberToNumeric;
336
        $this->preserveNumericFormatting = $preserveNumericFormatting;
337
    }
338
 
339
    /**
340
     * Open data uri for reading.
341
     */
342
    private function openDataUri(string $filename): void
343
    {
344
        $fileHandle = fopen($filename, 'rb');
345
        if ($fileHandle === false) {
346
            // @codeCoverageIgnoreStart
347
            throw new ReaderException('Could not open file ' . $filename . ' for reading.');
348
            // @codeCoverageIgnoreEnd
349
        }
350
 
351
        $this->fileHandle = $fileHandle;
352
    }
353
 
354
    /**
355
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
356
     */
357
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
358
    {
359
        return $this->loadStringOrFile($filename, $spreadsheet, false);
360
    }
361
 
362
    /**
363
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
364
     */
365
    private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bool $dataUri): Spreadsheet
366
    {
367
        // Deprecated in Php8.1
368
        $iniset = $this->setAutoDetect('1');
369
 
370
        try {
371
            $this->loadStringOrFile2($filename, $spreadsheet, $dataUri);
372
            $this->setAutoDetect($iniset);
373
        } catch (Throwable $e) {
374
            $this->setAutoDetect($iniset);
375
 
376
            throw $e;
377
        }
378
 
379
        return $spreadsheet;
380
    }
381
 
382
    private function loadStringOrFile2(string $filename, Spreadsheet $spreadsheet, bool $dataUri): void
383
    {
384
 
385
        // Open file
386
        if ($dataUri) {
387
            $this->openDataUri($filename);
388
        } else {
389
            $this->openFileOrMemory($filename);
390
        }
391
        $fileHandle = $this->fileHandle;
392
 
393
        // Skip BOM, if any
394
        $this->skipBOM();
395
        $this->checkSeparator();
396
        $this->inferSeparator();
397
 
398
        // Create new PhpSpreadsheet object
399
        while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
400
            $spreadsheet->createSheet();
401
        }
402
        $sheet = $spreadsheet->setActiveSheetIndex($this->sheetIndex);
403
        if ($this->sheetNameIsFileName) {
404
            $sheet->setTitle(substr(basename($filename, '.csv'), 0, Worksheet::SHEET_TITLE_MAXIMUM_LENGTH));
405
        }
406
 
407
        // Set our starting row based on whether we're in contiguous mode or not
408
        $currentRow = 1;
409
        $outRow = 0;
410
 
411
        // Loop through each line of the file in turn
412
        $delimiter = $this->delimiter ?? '';
413
        $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
414
        $valueBinder = $this->valueBinder ?? Cell::getValueBinder();
415
        $preserveBooleanString = method_exists($valueBinder, 'getBooleanConversion') && $valueBinder->getBooleanConversion();
416
        $this->getTrue = Calculation::getTRUE();
417
        $this->getFalse = Calculation::getFALSE();
418
        $this->thousandsSeparator = StringHelper::getThousandsSeparator();
419
        $this->decimalSeparator = StringHelper::getDecimalSeparator();
420
        while (is_array($rowData)) {
421
            $noOutputYet = true;
422
            $columnLetter = 'A';
423
            foreach ($rowData as $rowDatum) {
424
                if ($preserveBooleanString) {
425
                    $rowDatum = $rowDatum ?? '';
426
                } else {
427
                    $this->convertBoolean($rowDatum);
428
                }
429
                $numberFormatMask = $this->castFormattedNumberToNumeric ? $this->convertFormattedNumber($rowDatum) : '';
430
                if (($rowDatum !== '' || $this->preserveNullString) && $this->readFilter->readCell($columnLetter, $currentRow)) {
431
                    if ($this->contiguous) {
432
                        if ($noOutputYet) {
433
                            $noOutputYet = false;
434
                            ++$outRow;
435
                        }
436
                    } else {
437
                        $outRow = $currentRow;
438
                    }
439
                    // Set basic styling for the value (Note that this could be overloaded by styling in a value binder)
440
                    if ($numberFormatMask !== '') {
441
                        $sheet->getStyle($columnLetter . $outRow)
442
                            ->getNumberFormat()
443
                            ->setFormatCode($numberFormatMask);
444
                    }
445
                    // Set cell value
446
                    $sheet->getCell($columnLetter . $outRow)->setValue($rowDatum);
447
                }
448
                ++$columnLetter;
449
            }
450
            $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
451
            ++$currentRow;
452
        }
453
 
454
        // Close file
455
        fclose($fileHandle);
456
    }
457
 
458
    /**
459
     * Convert string true/false to boolean, and null to null-string.
460
     */
461
    private function convertBoolean(mixed &$rowDatum): void
462
    {
463
        if (is_string($rowDatum)) {
464
            if (strcasecmp($this->getTrue, $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) {
465
                $rowDatum = true;
466
            } elseif (strcasecmp($this->getFalse, $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) {
467
                $rowDatum = false;
468
            }
469
        } else {
470
            $rowDatum = $rowDatum ?? '';
471
        }
472
    }
473
 
474
    /**
475
     * Convert numeric strings to int or float values.
476
     */
477
    private function convertFormattedNumber(mixed &$rowDatum): string
478
    {
479
        $numberFormatMask = '';
480
        if ($this->castFormattedNumberToNumeric === true && is_string($rowDatum)) {
481
            $numeric = str_replace(
482
                [$this->thousandsSeparator, $this->decimalSeparator],
483
                ['', '.'],
484
                $rowDatum
485
            );
486
 
487
            if (is_numeric($numeric)) {
488
                $decimalPos = strpos($rowDatum, $this->decimalSeparator);
489
                if ($this->preserveNumericFormatting === true) {
490
                    $numberFormatMask = (str_contains($rowDatum, $this->thousandsSeparator))
491
                        ? '#,##0' : '0';
492
                    if ($decimalPos !== false) {
493
                        $decimals = strlen($rowDatum) - $decimalPos - 1;
494
                        $numberFormatMask .= '.' . str_repeat('0', min($decimals, 6));
495
                    }
496
                }
497
 
498
                $rowDatum = ($decimalPos !== false) ? (float) $numeric : (int) $numeric;
499
            }
500
        }
501
 
502
        return $numberFormatMask;
503
    }
504
 
505
    public function getDelimiter(): ?string
506
    {
507
        return $this->delimiter;
508
    }
509
 
510
    public function setDelimiter(?string $delimiter): self
511
    {
512
        $this->delimiter = $delimiter;
513
 
514
        return $this;
515
    }
516
 
517
    public function getEnclosure(): string
518
    {
519
        return $this->enclosure;
520
    }
521
 
522
    public function setEnclosure(string $enclosure): self
523
    {
524
        if ($enclosure == '') {
525
            $enclosure = '"';
526
        }
527
        $this->enclosure = $enclosure;
528
 
529
        return $this;
530
    }
531
 
532
    public function getSheetIndex(): int
533
    {
534
        return $this->sheetIndex;
535
    }
536
 
537
    public function setSheetIndex(int $indexValue): self
538
    {
539
        $this->sheetIndex = $indexValue;
540
 
541
        return $this;
542
    }
543
 
544
    public function setContiguous(bool $contiguous): self
545
    {
546
        $this->contiguous = $contiguous;
547
 
548
        return $this;
549
    }
550
 
551
    public function getContiguous(): bool
552
    {
553
        return $this->contiguous;
554
    }
555
 
556
    /**
557
     * Php9 intends to drop support for this parameter in fgetcsv.
558
     * Not yet ready to mark deprecated in order to give users
559
     * a migration path.
560
     */
561
    public function setEscapeCharacter(string $escapeCharacter, int $version = PHP_VERSION_ID): self
562
    {
563
        if ($version >= 90000 && $escapeCharacter !== '') {
564
            throw new ReaderException('Escape character must be null string for Php9+');
565
        }
566
 
567
        $this->escapeCharacter = $escapeCharacter;
568
 
569
        return $this;
570
    }
571
 
572
    public function getEscapeCharacter(int $version = PHP_VERSION_ID): string
573
    {
574
        return $this->escapeCharacter ?? self::getDefaultEscapeCharacter($version);
575
    }
576
 
577
    /**
578
     * Can the current IReader read the file?
579
     */
580
    public function canRead(string $filename): bool
581
    {
582
        // Check if file exists
583
        try {
584
            $this->openFile($filename);
585
        } catch (ReaderException) {
586
            return false;
587
        }
588
 
589
        fclose($this->fileHandle);
590
 
591
        // Trust file extension if any
592
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
593
        if (in_array($extension, ['csv', 'tsv'])) {
594
            return true;
595
        }
596
 
597
        // Attempt to guess mimetype
598
        $type = mime_content_type($filename);
599
        $supportedTypes = [
600
            'application/csv',
601
            'text/csv',
602
            'text/plain',
603
            'inode/x-empty',
604
            'text/html',
605
        ];
606
 
607
        return in_array($type, $supportedTypes, true);
608
    }
609
 
610
    private static function guessEncodingTestNoBom(string &$encoding, string &$contents, string $compare, string $setEncoding): void
611
    {
612
        if ($encoding === '') {
613
            $pos = strpos($contents, $compare);
614
            if ($pos !== false && $pos % strlen($compare) === 0) {
615
                $encoding = $setEncoding;
616
            }
617
        }
618
    }
619
 
620
    private static function guessEncodingNoBom(string $filename): string
621
    {
622
        $encoding = '';
623
        $contents = (string) file_get_contents($filename);
624
        self::guessEncodingTestNoBom($encoding, $contents, self::UTF32BE_LF, 'UTF-32BE');
625
        self::guessEncodingTestNoBom($encoding, $contents, self::UTF32LE_LF, 'UTF-32LE');
626
        self::guessEncodingTestNoBom($encoding, $contents, self::UTF16BE_LF, 'UTF-16BE');
627
        self::guessEncodingTestNoBom($encoding, $contents, self::UTF16LE_LF, 'UTF-16LE');
628
        if ($encoding === '' && preg_match('//u', $contents) === 1) {
629
            $encoding = 'UTF-8';
630
        }
631
 
632
        return $encoding;
633
    }
634
 
635
    private static function guessEncodingTestBom(string &$encoding, string $first4, string $compare, string $setEncoding): void
636
    {
637
        if ($encoding === '') {
638
            if (str_starts_with($first4, $compare)) {
639
                $encoding = $setEncoding;
640
            }
641
        }
642
    }
643
 
644
    public static function guessEncodingBom(string $filename, ?string $convertString = null): string
645
    {
646
        $encoding = '';
647
        $first4 = $convertString ?? (string) file_get_contents($filename, false, null, 0, 4);
648
        self::guessEncodingTestBom($encoding, $first4, self::UTF8_BOM, 'UTF-8');
649
        self::guessEncodingTestBom($encoding, $first4, self::UTF16BE_BOM, 'UTF-16BE');
650
        self::guessEncodingTestBom($encoding, $first4, self::UTF32BE_BOM, 'UTF-32BE');
651
        self::guessEncodingTestBom($encoding, $first4, self::UTF32LE_BOM, 'UTF-32LE');
652
        self::guessEncodingTestBom($encoding, $first4, self::UTF16LE_BOM, 'UTF-16LE');
653
 
654
        return $encoding;
655
    }
656
 
657
    public static function guessEncoding(string $filename, string $dflt = self::DEFAULT_FALLBACK_ENCODING): string
658
    {
659
        $encoding = self::guessEncodingBom($filename);
660
        if ($encoding === '') {
661
            $encoding = self::guessEncodingNoBom($filename);
662
        }
663
 
664
        return ($encoding === '') ? $dflt : $encoding;
665
    }
666
 
667
    public function setPreserveNullString(bool $value): self
668
    {
669
        $this->preserveNullString = $value;
670
 
671
        return $this;
672
    }
673
 
674
    public function getPreserveNullString(): bool
675
    {
676
        return $this->preserveNullString;
677
    }
678
 
679
    public function setSheetNameIsFileName(bool $sheetNameIsFileName): self
680
    {
681
        $this->sheetNameIsFileName = $sheetNameIsFileName;
682
 
683
        return $this;
684
    }
685
 
686
    /**
687
     * Php8.4 deprecates use of anything other than null string
688
     * as escape Character.
689
     *
690
     * @param resource $stream
691
     * @param null|int<0, max> $length
692
     *
693
     * @return array<int,?string>|false
694
     */
695
    private static function getCsv(
696
        $stream,
697
        ?int $length = null,
698
        string $separator = ',',
699
        string $enclosure = '"',
700
        ?string $escape = null,
701
        int $version = PHP_VERSION_ID
702
    ): array|false {
703
        $escape = $escape ?? self::getDefaultEscapeCharacter();
704
        if ($version >= 80400 && $escape !== '') {
705
            return @fgetcsv($stream, $length, $separator, $enclosure, $escape);
706
        }
707
 
708
        return fgetcsv($stream, $length, $separator, $enclosure, $escape);
709
    }
710
 
711
    public static function affectedByPhp9(
712
        string $filename,
713
        string $inputEncoding = 'UTF-8',
714
        ?string $delimiter = null,
715
        string $enclosure = '"',
716
        string $escapeCharacter = '\\',
717
        int $version = PHP_VERSION_ID
718
    ): bool {
719
        if ($version < 70400 || $version >= 90000) {
720
            throw new ReaderException('Function valid only for Php7.4 or Php8');
721
        }
722
        $reader1 = new self();
723
        $reader1->setInputEncoding($inputEncoding)
724
            ->setTestAutoDetect(true)
725
            ->setEscapeCharacter($escapeCharacter)
726
            ->setDelimiter($delimiter)
727
            ->setEnclosure($enclosure);
728
        $spreadsheet1 = $reader1->load($filename);
729
        $sheet1 = $spreadsheet1->getActiveSheet();
730
        $array1 = $sheet1->toArray(null, false, false);
731
        $spreadsheet1->disconnectWorksheets();
732
 
733
        $reader2 = new self();
734
        $reader2->setInputEncoding($inputEncoding)
735
            ->setTestAutoDetect(false)
736
            ->setEscapeCharacter('')
737
            ->setDelimiter($delimiter)
738
            ->setEnclosure($enclosure);
739
        $spreadsheet2 = $reader2->load($filename);
740
        $sheet2 = $spreadsheet2->getActiveSheet();
741
        $array2 = $sheet2->toArray(null, false, false);
742
        $spreadsheet2->disconnectWorksheets();
743
 
744
        return $array1 !== $array2;
745
    }
746
 
747
    /**
748
     * The character that will be supplied to fgetcsv
749
     * when escapeCharacter is null.
750
     * It is anticipated that it will conditionally be set
751
     * to null-string for Php9 and above.
752
     */
753
    private static function getDefaultEscapeCharacter(int $version = PHP_VERSION_ID): string
754
    {
755
        return $version < 90000 ? '\\' : '';
756
    }
757
}