Failed Conditions
Pull Request — master (#4275)
by Owen
15:27
created

Html::listWorksheetInfo()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 16
ccs 13
cts 13
cp 1
rs 9.8666
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Reader;
4
5
use DOMAttr;
6
use DOMDocument;
7
use DOMElement;
8
use DOMNode;
9
use DOMText;
10
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
11
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
12
use PhpOffice\PhpSpreadsheet\Cell\DataType;
13
use PhpOffice\PhpSpreadsheet\Comment;
14
use PhpOffice\PhpSpreadsheet\Document\Properties;
15
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
16
use PhpOffice\PhpSpreadsheet\Helper\Dimension as CssDimension;
17
use PhpOffice\PhpSpreadsheet\Helper\Html as HelperHtml;
18
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
19
use PhpOffice\PhpSpreadsheet\Spreadsheet;
20
use PhpOffice\PhpSpreadsheet\Style\Border;
21
use PhpOffice\PhpSpreadsheet\Style\Color;
22
use PhpOffice\PhpSpreadsheet\Style\Fill;
23
use PhpOffice\PhpSpreadsheet\Style\Font;
24
use PhpOffice\PhpSpreadsheet\Style\Style;
25
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
26
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
27
use Throwable;
28
29
class Html extends BaseReader
30
{
31
    /**
32
     * Sample size to read to determine if it's HTML or not.
33
     */
34
    const TEST_SAMPLE_SIZE = 2048;
35
36
    private const STARTS_WITH_BOM = '/^(?:\xfe\xff|\xff\xfe|\xEF\xBB\xBF)/';
37
38
    private const DECLARES_CHARSET = '/\\bcharset=/i';
39
40
    /**
41
     * Input encoding.
42
     */
43
    protected string $inputEncoding = 'ANSI';
44
45
    /**
46
     * Sheet index to read.
47
     */
48
    protected int $sheetIndex = 0;
49
50
    /**
51
     * Formats.
52
     */
53
    protected array $formats = [
54
        'h1' => [
55
            'font' => [
56
                'bold' => true,
57
                'size' => 24,
58
            ],
59
        ], //    Bold, 24pt
60
        'h2' => [
61
            'font' => [
62
                'bold' => true,
63
                'size' => 18,
64
            ],
65
        ], //    Bold, 18pt
66
        'h3' => [
67
            'font' => [
68
                'bold' => true,
69
                'size' => 13.5,
70
            ],
71
        ], //    Bold, 13.5pt
72
        'h4' => [
73
            'font' => [
74
                'bold' => true,
75
                'size' => 12,
76
            ],
77
        ], //    Bold, 12pt
78
        'h5' => [
79
            'font' => [
80
                'bold' => true,
81
                'size' => 10,
82
            ],
83
        ], //    Bold, 10pt
84
        'h6' => [
85
            'font' => [
86
                'bold' => true,
87
                'size' => 7.5,
88
            ],
89
        ], //    Bold, 7.5pt
90
        'a' => [
91
            'font' => [
92
                'underline' => true,
93
                'color' => [
94
                    'argb' => Color::COLOR_BLUE,
95
                ],
96
            ],
97
        ], //    Blue underlined
98
        'hr' => [
99
            'borders' => [
100
                'bottom' => [
101
                    'borderStyle' => Border::BORDER_THIN,
102
                    'color' => [
103
                        Color::COLOR_BLACK,
104
                    ],
105
                ],
106
            ],
107
        ], //    Bottom border
108
        'strong' => [
109
            'font' => [
110
                'bold' => true,
111
            ],
112
        ], //    Bold
113
        'b' => [
114
            'font' => [
115
                'bold' => true,
116
            ],
117
        ], //    Bold
118
        'i' => [
119
            'font' => [
120
                'italic' => true,
121
            ],
122
        ], //    Italic
123
        'em' => [
124
            'font' => [
125
                'italic' => true,
126
            ],
127
        ], //    Italic
128
    ];
129
130
    protected array $rowspan = [];
131
132
    /**
133
     * Create a new HTML Reader instance.
134
     */
135 517
    public function __construct()
136
    {
137 517
        parent::__construct();
138 517
        $this->securityScanner = XmlScanner::getInstance($this);
139
    }
140
141
    /**
142
     * Validate that the current file is an HTML file.
143
     */
144 496
    public function canRead(string $filename): bool
145
    {
146
        // Check if file exists
147
        try {
148 496
            $this->openFile($filename);
149 1
        } catch (Exception) {
150 1
            return false;
151
        }
152
153 495
        $beginning = preg_replace(self::STARTS_WITH_BOM, '', $this->readBeginning()) ?? '';
154
155 495
        $startWithTag = self::startsWithTag($beginning);
156 495
        $containsTags = self::containsTags($beginning);
157 495
        $endsWithTag = self::endsWithTag($this->readEnding());
158
159 495
        fclose($this->fileHandle);
160
161 495
        return $startWithTag && $containsTags && $endsWithTag;
162
    }
163
164 495
    private function readBeginning(): string
165
    {
166 495
        fseek($this->fileHandle, 0);
167
168 495
        return (string) fread($this->fileHandle, self::TEST_SAMPLE_SIZE);
169
    }
170
171 495
    private function readEnding(): string
172
    {
173 495
        $meta = stream_get_meta_data($this->fileHandle);
174
        // Phpstan incorrectly flags following line for Php8.2-, corrected in 8.3
175 495
        $filename = $meta['uri']; //@phpstan-ignore-line
176
177 495
        clearstatcache(true, $filename);
178 495
        $size = (int) filesize($filename);
179 495
        if ($size === 0) {
180 1
            return '';
181
        }
182
183 494
        $blockSize = self::TEST_SAMPLE_SIZE;
184 494
        if ($size < $blockSize) {
185 43
            $blockSize = $size;
186
        }
187
188 494
        fseek($this->fileHandle, $size - $blockSize);
189
190 494
        return (string) fread($this->fileHandle, $blockSize);
191
    }
192
193 495
    private static function startsWithTag(string $data): bool
194
    {
195 495
        return str_starts_with(trim($data), '<');
196
    }
197
198 495
    private static function endsWithTag(string $data): bool
199
    {
200 495
        return str_ends_with(trim($data), '>');
201
    }
202
203 495
    private static function containsTags(string $data): bool
204
    {
205 495
        return strlen($data) !== strlen(strip_tags($data));
206
    }
207
208
    /**
209
     * Loads Spreadsheet from file.
210
     */
211 477
    public function loadSpreadsheetFromFile(string $filename): Spreadsheet
212
    {
213
        // Create new Spreadsheet
214 477
        $spreadsheet = new Spreadsheet();
215 477
        $spreadsheet->setValueBinder($this->valueBinder);
216
217
        // Load into this instance
218 477
        return $this->loadIntoExisting($filename, $spreadsheet);
219
    }
220
221
    //    Data Array used for testing only, should write to Spreadsheet object on completion of tests
222
223
    protected array $dataArray = [];
224
225
    protected int $tableLevel = 0;
226
227
    protected array $nestedColumn = ['A'];
228
229 492
    protected function setTableStartColumn(string $column): string
230
    {
231 492
        if ($this->tableLevel == 0) {
232 492
            $column = 'A';
233
        }
234 492
        ++$this->tableLevel;
235 492
        $this->nestedColumn[$this->tableLevel] = $column;
236
237 492
        return $this->nestedColumn[$this->tableLevel];
238
    }
239
240 488
    protected function getTableStartColumn(): string
241
    {
242 488
        return $this->nestedColumn[$this->tableLevel];
243
    }
244
245 491
    protected function releaseTableStartColumn(): string
246
    {
247 491
        --$this->tableLevel;
248
249 491
        return array_pop($this->nestedColumn);
250
    }
251
252
    /**
253
     * Flush cell.
254
     */
255 493
    protected function flushCell(Worksheet $sheet, string $column, int|string $row, mixed &$cellContent, array $attributeArray): void
256
    {
257 493
        if (is_string($cellContent)) {
258
            //    Simple String content
259 493
            if (trim($cellContent) > '') {
260
                //    Only actually write it if there's content in the string
261
                //    Write to worksheet to be done here...
262
                //    ... we return the cell, so we can mess about with styles more easily
263
264
                // Set cell value explicitly if there is data-type attribute
265 480
                if (isset($attributeArray['data-type'])) {
266 5
                    $datatype = $attributeArray['data-type'];
267 5
                    if (in_array($datatype, [DataType::TYPE_STRING, DataType::TYPE_STRING2, DataType::TYPE_INLINE])) {
268
                        //Prevent to Excel treat string with beginning equal sign or convert big numbers to scientific number
269 5
                        if (str_starts_with($cellContent, '=')) {
270 1
                            $sheet->getCell($column . $row)
271 1
                                ->getStyle()
272 1
                                ->setQuotePrefix(true);
273
                        }
274
                    }
275 5
                    if ($datatype === DataType::TYPE_BOOL) {
276 5
                        $cellContent = self::convertBoolean($cellContent);
277 5
                        if (!is_bool($cellContent)) {
278 1
                            $attributeArray['data-type'] = DataType::TYPE_STRING;
279
                        }
280
                    }
281
282
                    //catching the Exception and ignoring the invalid data types
283
                    try {
284 5
                        $sheet->setCellValueExplicit($column . $row, $cellContent, $attributeArray['data-type']);
285 1
                    } catch (SpreadsheetException) {
286 1
                        $sheet->setCellValue($column . $row, $cellContent);
287
                    }
288
                } else {
289 479
                    $sheet->setCellValue($column . $row, $cellContent);
290
                }
291 480
                $this->dataArray[$row][$column] = $cellContent;
292
            }
293
        } else {
294
            //    We have a Rich Text run
295
            //    TODO
296
            $this->dataArray[$row][$column] = 'RICH TEXT: ' . $cellContent;
297
        }
298 493
        $cellContent = (string) '';
299
    }
300
301
    /** @var array<int, array<int, string>> */
302
    private static array $falseTrueArray = [];
303
304 5
    private static function convertBoolean(?string $cellContent): bool|string
305
    {
306 5
        if ($cellContent === '1') {
307 1
            return true;
308
        }
309 4
        if ($cellContent === '0' || $cellContent === '' || $cellContent === null) {
310
            return false;
311
        }
312 4
        if (empty(self::$falseTrueArray)) {
313 1
            $calc = Calculation::getInstance();
314 1
            self::$falseTrueArray = $calc->getFalseTrueArray();
315
        }
316 4
        if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[1], true)) {
317 4
            return true;
318
        }
319 4
        if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[0], true)) {
320 4
            return false;
321
        }
322
323 1
        return $cellContent;
324
    }
325
326 493
    private function processDomElementBody(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void
327
    {
328 493
        $attributeArray = [];
329
        /** @var DOMAttr $attribute */
330 493
        foreach ($child->attributes as $attribute) {
331 479
            $attributeArray[$attribute->name] = $attribute->value;
332
        }
333
334 493
        if ($child->nodeName === 'body') {
335 493
            $row = 1;
336 493
            $column = 'A';
337 493
            $cellContent = '';
338 493
            $this->tableLevel = 0;
339 493
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
340
        } else {
341 493
            $this->processDomElementTitle($sheet, $row, $column, $cellContent, $child, $attributeArray);
342
        }
343
    }
344
345 493
    private function processDomElementTitle(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
346
    {
347 493
        if ($child->nodeName === 'title') {
348 459
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
349
350
            try {
351 459
                $sheet->setTitle($cellContent, true, true);
352 456
                $sheet->getParent()?->getProperties()?->setTitle($cellContent);
353 3
            } catch (SpreadsheetException) {
354
                // leave default title if too long or illegal chars
355
            }
356 459
            $cellContent = '';
357
        } else {
358 493
            $this->processDomElementSpanEtc($sheet, $row, $column, $cellContent, $child, $attributeArray);
359
        }
360
    }
361
362
    private const SPAN_ETC = ['span', 'div', 'font', 'i', 'em', 'strong', 'b'];
363
364 493
    private function processDomElementSpanEtc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
365
    {
366 493
        if (in_array((string) $child->nodeName, self::SPAN_ETC, true)) {
367 444
            if (isset($attributeArray['class']) && $attributeArray['class'] === 'comment') {
368 9
                $sheet->getComment($column . $row)
369 9
                    ->getText()
370 9
                    ->createTextRun($child->textContent);
371 9
                if (isset($attributeArray['dir']) && $attributeArray['dir'] === 'rtl') {
372 1
                    $sheet->getComment($column . $row)->setTextboxDirection(Comment::TEXTBOX_DIRECTION_RTL);
373
                }
374 9
                if (isset($attributeArray['style'])) {
375 2
                    $alignStyle = $attributeArray['style'];
376 2
                    if (preg_match('/\\btext-align:\\s*(left|right|center|justify)\\b/', $alignStyle, $matches) === 1) {
377 2
                        $sheet->getComment($column . $row)->setAlignment($matches[1]);
378
                    }
379
                }
380
            } else {
381 444
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
382
            }
383
384 444
            if (isset($this->formats[$child->nodeName])) {
385 2
                $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]);
386
            }
387
        } else {
388 493
            $this->processDomElementHr($sheet, $row, $column, $cellContent, $child, $attributeArray);
389
        }
390
    }
391
392 493
    private function processDomElementHr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
393
    {
394 493
        if ($child->nodeName === 'hr') {
395 1
            $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
396 1
            ++$row;
397 1
            if (isset($this->formats[$child->nodeName])) {
398 1
                $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]);
399
            }
400 1
            ++$row;
401
        }
402
        // fall through to br
403 493
        $this->processDomElementBr($sheet, $row, $column, $cellContent, $child, $attributeArray);
404
    }
405
406 493
    private function processDomElementBr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
407
    {
408 493
        if ($child->nodeName === 'br' || $child->nodeName === 'hr') {
409 4
            if ($this->tableLevel > 0) {
410
                //    If we're inside a table, replace with a newline and set the cell to wrap
411 4
                $cellContent .= "\n";
412 4
                $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true);
413
            } else {
414
                //    Otherwise flush our existing content and move the row cursor on
415 1
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
416 1
                ++$row;
417
            }
418
        } else {
419 493
            $this->processDomElementA($sheet, $row, $column, $cellContent, $child, $attributeArray);
420
        }
421
    }
422
423 493
    private function processDomElementA(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
424
    {
425 493
        if ($child->nodeName === 'a') {
426 12
            foreach ($attributeArray as $attributeName => $attributeValue) {
427
                switch ($attributeName) {
428 12
                    case 'href':
429 3
                        $sheet->getCell($column . $row)->getHyperlink()->setUrl($attributeValue);
430 3
                        if (isset($this->formats[$child->nodeName])) {
431 3
                            $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]);
432
                        }
433
434 3
                        break;
435 10
                    case 'class':
436 9
                        if ($attributeValue === 'comment-indicator') {
437 9
                            break; // Ignore - it's just a red square.
438
                        }
439
                }
440
            }
441
            // no idea why this should be needed
442
            //$cellContent .= ' ';
443 12
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
444
        } else {
445 493
            $this->processDomElementH1Etc($sheet, $row, $column, $cellContent, $child, $attributeArray);
446
        }
447
    }
448
449
    private const H1_ETC = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'p'];
450
451 493
    private function processDomElementH1Etc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
452
    {
453 493
        if (in_array((string) $child->nodeName, self::H1_ETC, true)) {
454 2
            if ($this->tableLevel > 0) {
455
                //    If we're inside a table, replace with a newline
456 1
                $cellContent .= $cellContent ? "\n" : '';
457 1
                $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true);
458 1
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
459
            } else {
460 2
                if ($cellContent > '') {
461 1
                    $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
462 1
                    ++$row;
463
                }
464 2
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
465 2
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
466
467 2
                if (isset($this->formats[$child->nodeName])) {
468 1
                    $sheet->getStyle($column . $row)->applyFromArray($this->formats[$child->nodeName]);
469
                }
470
471 2
                ++$row;
472 2
                $column = 'A';
473
            }
474
        } else {
475 493
            $this->processDomElementLi($sheet, $row, $column, $cellContent, $child, $attributeArray);
476
        }
477
    }
478
479 493
    private function processDomElementLi(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
480
    {
481 493
        if ($child->nodeName === 'li') {
482 2
            if ($this->tableLevel > 0) {
483
                //    If we're inside a table, replace with a newline
484 1
                $cellContent .= $cellContent ? "\n" : '';
485 1
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
486
            } else {
487 2
                if ($cellContent > '') {
488 1
                    $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
489
                }
490 2
                ++$row;
491 2
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
492 2
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
493 2
                $column = 'A';
494
            }
495
        } else {
496 493
            $this->processDomElementImg($sheet, $row, $column, $cellContent, $child, $attributeArray);
497
        }
498
    }
499
500 493
    private function processDomElementImg(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
501
    {
502 493
        if ($child->nodeName === 'img') {
503 10
            $this->insertImage($sheet, $column, $row, $attributeArray);
504
        } else {
505 493
            $this->processDomElementTable($sheet, $row, $column, $cellContent, $child, $attributeArray);
506
        }
507
    }
508
509
    private string $currentColumn = 'A';
510
511 493
    private function processDomElementTable(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
512
    {
513 493
        if ($child->nodeName === 'table') {
514 492
            if (isset($attributeArray['class'])) {
515 444
                $classes = explode(' ', $attributeArray['class']);
516 444
                $sheet->setShowGridlines(in_array('gridlines', $classes, true));
517 444
                $sheet->setPrintGridlines(in_array('gridlinesp', $classes, true));
518
            }
519 492
            $this->currentColumn = 'A';
520 492
            $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
521 492
            $column = $this->setTableStartColumn($column);
522 492
            if ($this->tableLevel > 1 && $row > 1) {
523 2
                --$row;
524
            }
525 492
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
526 491
            $column = $this->releaseTableStartColumn();
527 491
            if ($this->tableLevel > 1) {
528 2
                ++$column;
529
            } else {
530 491
                ++$row;
531
            }
532
        } else {
533 493
            $this->processDomElementTr($sheet, $row, $column, $cellContent, $child, $attributeArray);
534
        }
535
    }
536
537 493
    private function processDomElementTr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
538
    {
539 493
        if ($child->nodeName === 'col') {
540 442
            $this->applyInlineStyle($sheet, -1, $this->currentColumn, $attributeArray);
541 442
            ++$this->currentColumn;
542 493
        } elseif ($child->nodeName === 'tr') {
543 488
            $column = $this->getTableStartColumn();
544 488
            $cellContent = '';
545 488
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
546
547 487
            if (isset($attributeArray['height'])) {
548 1
                $sheet->getRowDimension($row)->setRowHeight($attributeArray['height']);
549
            }
550
551 487
            ++$row;
552
        } else {
553 493
            $this->processDomElementThTdOther($sheet, $row, $column, $cellContent, $child, $attributeArray);
554
        }
555
    }
556
557 493
    private function processDomElementThTdOther(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
558
    {
559 493
        if ($child->nodeName !== 'td' && $child->nodeName !== 'th') {
560 493
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
561
        } else {
562 488
            $this->processDomElementThTd($sheet, $row, $column, $cellContent, $child, $attributeArray);
563
        }
564
    }
565
566 487
    private function processDomElementBgcolor(Worksheet $sheet, int $row, string $column, array $attributeArray): void
567
    {
568 487
        if (isset($attributeArray['bgcolor'])) {
569 1
            $sheet->getStyle("$column$row")->applyFromArray(
570 1
                [
571 1
                    'fill' => [
572 1
                        'fillType' => Fill::FILL_SOLID,
573 1
                        'color' => ['rgb' => $this->getStyleColor($attributeArray['bgcolor'])],
574 1
                    ],
575 1
                ]
576 1
            );
577
        }
578
    }
579
580 487
    private function processDomElementWidth(Worksheet $sheet, string $column, array $attributeArray): void
581
    {
582 487
        if (isset($attributeArray['width'])) {
583 1
            $sheet->getColumnDimension($column)->setWidth((new CssDimension($attributeArray['width']))->width());
584
        }
585
    }
586
587 487
    private function processDomElementHeight(Worksheet $sheet, int $row, array $attributeArray): void
588
    {
589 487
        if (isset($attributeArray['height'])) {
590 1
            $sheet->getRowDimension($row)->setRowHeight((new CssDimension($attributeArray['height']))->height());
591
        }
592
    }
593
594 487
    private function processDomElementAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
595
    {
596 487
        if (isset($attributeArray['align'])) {
597 1
            $sheet->getStyle($column . $row)->getAlignment()->setHorizontal($attributeArray['align']);
598
        }
599
    }
600
601 487
    private function processDomElementVAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
602
    {
603 487
        if (isset($attributeArray['valign'])) {
604 1
            $sheet->getStyle($column . $row)->getAlignment()->setVertical($attributeArray['valign']);
605
        }
606
    }
607
608 487
    private function processDomElementDataFormat(Worksheet $sheet, int $row, string $column, array $attributeArray): void
609
    {
610 487
        if (isset($attributeArray['data-format'])) {
611 1
            $sheet->getStyle($column . $row)->getNumberFormat()->setFormatCode($attributeArray['data-format']);
612
        }
613
    }
614
615 488
    private function processDomElementThTd(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
616
    {
617 488
        while (isset($this->rowspan[$column . $row])) {
618 3
            ++$column;
619
        }
620 488
        $this->processDomElement($child, $sheet, $row, $column, $cellContent);
621
622
        // apply inline style
623 487
        $this->applyInlineStyle($sheet, $row, $column, $attributeArray);
624
625 487
        $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
626
627 487
        $this->processDomElementBgcolor($sheet, $row, $column, $attributeArray);
628 487
        $this->processDomElementWidth($sheet, $column, $attributeArray);
629 487
        $this->processDomElementHeight($sheet, $row, $attributeArray);
630 487
        $this->processDomElementAlign($sheet, $row, $column, $attributeArray);
631 487
        $this->processDomElementVAlign($sheet, $row, $column, $attributeArray);
632 487
        $this->processDomElementDataFormat($sheet, $row, $column, $attributeArray);
633
634 487
        if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
635
            //create merging rowspan and colspan
636 2
            $columnTo = $column;
637 2
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
638 2
                ++$columnTo;
639
            }
640 2
            $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
641 2
            foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
642 2
                $this->rowspan[$value] = true;
643
            }
644 2
            $sheet->mergeCells($range);
645 2
            $column = $columnTo;
646 487
        } elseif (isset($attributeArray['rowspan'])) {
647
            //create merging rowspan
648 3
            $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
649 3
            foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
650 3
                $this->rowspan[$value] = true;
651
            }
652 3
            $sheet->mergeCells($range);
653 487
        } elseif (isset($attributeArray['colspan'])) {
654
            //create merging colspan
655 3
            $columnTo = $column;
656 3
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
657 3
                ++$columnTo;
658
            }
659 3
            $sheet->mergeCells($column . $row . ':' . $columnTo . $row);
660 3
            $column = $columnTo;
661
        }
662
663 487
        ++$column;
664
    }
665
666 493
    protected function processDomElement(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent): void
667
    {
668 493
        foreach ($element->childNodes as $child) {
669 493
            if ($child instanceof DOMText) {
670 490
                $domText = (string) preg_replace('/\s+/', ' ', trim($child->nodeValue ?? ''));
671 490
                if ($domText === "\u{a0}") {
672 12
                    $domText = '';
673
                }
674 490
                if (is_string($cellContent)) {
675
                    //    simply append the text if the cell content is a plain text string
676 490
                    $cellContent .= $domText;
677
                }
678
                //    but if we have a rich text run instead, we need to append it correctly
679
                //    TODO
680 493
            } elseif ($child instanceof DOMElement) {
681 493
                $this->processDomElementBody($sheet, $row, $column, $cellContent, $child);
682
            }
683
        }
684
    }
685
686
    /**
687
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
688
     */
689 477
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
690
    {
691
        // Validate
692 477
        if (!$this->canRead($filename)) {
693 1
            throw new Exception($filename . ' is an Invalid HTML file.');
694
        }
695
696
        // Create a new DOM object
697 476
        $dom = new DOMDocument();
698
699
        // Reload the HTML file into the DOM object
700
        try {
701 476
            $convert = $this->getSecurityScannerOrThrow()->scanFile($filename);
702 475
            $convert = self::replaceNonAsciiIfNeeded($convert);
703 475
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
704 2
        } catch (Throwable $e) {
705 2
            $loaded = false;
706
        }
707 476
        if ($loaded === false) {
708 2
            throw new Exception('Failed to load ' . $filename . ' as a DOM Document', 0, $e ?? null);
709
        }
710 474
        self::loadProperties($dom, $spreadsheet);
711
712 474
        return $this->loadDocument($dom, $spreadsheet);
713
    }
714
715 493
    private static function loadProperties(DOMDocument $dom, Spreadsheet $spreadsheet): void
716
    {
717 493
        $properties = $spreadsheet->getProperties();
718 493
        foreach ($dom->getElementsByTagName('meta') as $meta) {
719 452
            $metaContent = (string) $meta->getAttribute('content');
720 452
            if ($metaContent !== '') {
721 444
                $metaName = (string) $meta->getAttribute('name');
722
                switch ($metaName) {
723 444
                    case 'author':
724 442
                        $properties->setCreator($metaContent);
725
726 442
                        break;
727 444
                    case 'category':
728 1
                        $properties->setCategory($metaContent);
729
730 1
                        break;
731 444
                    case 'company':
732 1
                        $properties->setCompany($metaContent);
733
734 1
                        break;
735 444
                    case 'created':
736 442
                        $properties->setCreated($metaContent);
737
738 442
                        break;
739 444
                    case 'description':
740 1
                        $properties->setDescription($metaContent);
741
742 1
                        break;
743 444
                    case 'keywords':
744 1
                        $properties->setKeywords($metaContent);
745
746 1
                        break;
747 444
                    case 'lastModifiedBy':
748 442
                        $properties->setLastModifiedBy($metaContent);
749
750 442
                        break;
751 444
                    case 'manager':
752 1
                        $properties->setManager($metaContent);
753
754 1
                        break;
755 444
                    case 'modified':
756 442
                        $properties->setModified($metaContent);
757
758 442
                        break;
759 444
                    case 'subject':
760 1
                        $properties->setSubject($metaContent);
761
762 1
                        break;
763 444
                    case 'title':
764 440
                        $properties->setTitle($metaContent);
765
766 440
                        break;
767 444
                    case 'viewport':
768 1
                        $properties->setViewport($metaContent);
769
770 1
                        break;
771
                    default:
772 444
                        if (preg_match('/^custom[.](bool|date|float|int|string)[.](.+)$/', $metaName, $matches) === 1) {
773 1
                            match ($matches[1]) {
774 1
                                'bool' => $properties->setCustomProperty($matches[2], (bool) $metaContent, Properties::PROPERTY_TYPE_BOOLEAN),
775 1
                                'float' => $properties->setCustomProperty($matches[2], (float) $metaContent, Properties::PROPERTY_TYPE_FLOAT),
776 1
                                'int' => $properties->setCustomProperty($matches[2], (int) $metaContent, Properties::PROPERTY_TYPE_INTEGER),
777 1
                                'date' => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_DATE),
778
                                // string
779 1
                                default => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_STRING),
780 1
                            };
781
                        }
782
                }
783
            }
784
        }
785 493
        if (!empty($dom->baseURI)) {
786 1
            $properties->setHyperlinkBase($dom->baseURI);
787
        }
788
    }
789
790 12
    private static function replaceNonAscii(array $matches): string
791
    {
792 12
        return '&#' . mb_ord($matches[0], 'UTF-8') . ';';
793
    }
794
795 494
    private static function replaceNonAsciiIfNeeded(string $convert): ?string
796
    {
797 494
        if (preg_match(self::STARTS_WITH_BOM, $convert) !== 1 && preg_match(self::DECLARES_CHARSET, $convert) !== 1) {
798 41
            $lowend = "\u{80}";
799 41
            $highend = "\u{10ffff}";
800 41
            $regexp = "/[$lowend-$highend]/u";
801
            /** @var callable $callback */
802 41
            $callback = [self::class, 'replaceNonAscii'];
803 41
            $convert = preg_replace_callback($regexp, $callback, $convert);
804
        }
805
806 494
        return $convert;
807
    }
808
809
    /**
810
     * Spreadsheet from content.
811
     */
812 19
    public function loadFromString(string $content, ?Spreadsheet $spreadsheet = null): Spreadsheet
813
    {
814
        //    Create a new DOM object
815 19
        $dom = new DOMDocument();
816
817
        //    Reload the HTML file into the DOM object
818
        try {
819 19
            $convert = $this->getSecurityScannerOrThrow()->scan($content);
820 19
            $convert = self::replaceNonAsciiIfNeeded($convert);
821 19
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
822
        } catch (Throwable $e) {
823
            $loaded = false;
824
        }
825 19
        if ($loaded === false) {
826
            throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null);
827
        }
828 19
        $spreadsheet = $spreadsheet ?? new Spreadsheet();
829 19
        $spreadsheet->setValueBinder($this->valueBinder);
830 19
        self::loadProperties($dom, $spreadsheet);
831
832 19
        return $this->loadDocument($dom, $spreadsheet);
833
    }
834
835
    /**
836
     * Loads PhpSpreadsheet from DOMDocument into PhpSpreadsheet instance.
837
     */
838 493
    private function loadDocument(DOMDocument $document, Spreadsheet $spreadsheet): Spreadsheet
839
    {
840 493
        while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
841 2
            $spreadsheet->createSheet();
842
        }
843 493
        $spreadsheet->setActiveSheetIndex($this->sheetIndex);
844
845
        // Discard white space
846 493
        $document->preserveWhiteSpace = false;
847
848 493
        $row = 0;
849 493
        $column = 'A';
850 493
        $content = '';
851 493
        $this->rowspan = [];
852 493
        $this->processDomElement($document, $spreadsheet->getActiveSheet(), $row, $column, $content);
853
854
        // Return
855 492
        return $spreadsheet;
856
    }
857
858
    /**
859
     * Get sheet index.
860
     */
861 1
    public function getSheetIndex(): int
862
    {
863 1
        return $this->sheetIndex;
864
    }
865
866
    /**
867
     * Set sheet index.
868
     *
869
     * @param int $sheetIndex Sheet index
870
     *
871
     * @return $this
872
     */
873 2
    public function setSheetIndex(int $sheetIndex): static
874
    {
875 2
        $this->sheetIndex = $sheetIndex;
876
877 2
        return $this;
878
    }
879
880
    /**
881
     * Apply inline css inline style.
882
     *
883
     * NOTES :
884
     * Currently only intended for td & th element,
885
     * and only takes 'background-color' and 'color'; property with HEX color
886
     *
887
     * TODO :
888
     * - Implement to other propertie, such as border
889
     */
890 487
    private function applyInlineStyle(Worksheet &$sheet, int $row, string $column, array $attributeArray): void
891
    {
892 487
        if (!isset($attributeArray['style'])) {
893 481
            return;
894
        }
895
896 16
        if ($row <= 0 || $column === '') {
897 1
            $cellStyle = new Style();
898 16
        } elseif (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
899 1
            $columnTo = $column;
900 1
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
901 1
                ++$columnTo;
902
            }
903 1
            $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
904 1
            $cellStyle = $sheet->getStyle($range);
905 16
        } elseif (isset($attributeArray['rowspan'])) {
906 1
            $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
907 1
            $cellStyle = $sheet->getStyle($range);
908 16
        } elseif (isset($attributeArray['colspan'])) {
909 1
            $columnTo = $column;
910 1
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
911 1
                ++$columnTo;
912
            }
913 1
            $range = $column . $row . ':' . $columnTo . $row;
914 1
            $cellStyle = $sheet->getStyle($range);
915
        } else {
916 16
            $cellStyle = $sheet->getStyle($column . $row);
917
        }
918
919
        // add color styles (background & text) from dom element,currently support : td & th, using ONLY inline css style with RGB color
920 16
        $styles = explode(';', $attributeArray['style']);
921 16
        foreach ($styles as $st) {
922 16
            $value = explode(':', $st);
923 16
            $styleName = isset($value[0]) ? trim($value[0]) : null;
924 16
            $styleValue = isset($value[1]) ? trim($value[1]) : null;
925 16
            $styleValueString = (string) $styleValue;
926
927 16
            if (!$styleName) {
928 12
                continue;
929
            }
930
931
            switch ($styleName) {
932 16
                case 'background':
933 16
                case 'background-color':
934 3
                    $styleColor = $this->getStyleColor($styleValueString);
935
936 3
                    if (!$styleColor) {
937 1
                        continue 2;
938
                    }
939
940 3
                    $cellStyle->applyFromArray(['fill' => ['fillType' => Fill::FILL_SOLID, 'color' => ['rgb' => $styleColor]]]);
941
942 3
                    break;
943 16
                case 'color':
944 3
                    $styleColor = $this->getStyleColor($styleValueString);
945
946 3
                    if (!$styleColor) {
947 1
                        continue 2;
948
                    }
949
950 3
                    $cellStyle->applyFromArray(['font' => ['color' => ['rgb' => $styleColor]]]);
951
952 3
                    break;
953
954 13
                case 'border':
955 3
                    $this->setBorderStyle($cellStyle, $styleValueString, 'allBorders');
956
957 3
                    break;
958
959 11
                case 'border-top':
960 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'top');
961
962 1
                    break;
963
964 11
                case 'border-bottom':
965 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'bottom');
966
967 1
                    break;
968
969 11
                case 'border-left':
970 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'left');
971
972 1
                    break;
973
974 11
                case 'border-right':
975 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'right');
976
977 1
                    break;
978
979 10
                case 'font-size':
980 1
                    $cellStyle->getFont()->setSize(
981 1
                        (float) $styleValue
982 1
                    );
983
984 1
                    break;
985
986 10
                case 'font-weight':
987 1
                    if ($styleValue === 'bold' || $styleValue >= 500) {
988 1
                        $cellStyle->getFont()->setBold(true);
989
                    }
990
991 1
                    break;
992
993 10
                case 'font-style':
994 1
                    if ($styleValue === 'italic') {
995 1
                        $cellStyle->getFont()->setItalic(true);
996
                    }
997
998 1
                    break;
999
1000 10
                case 'font-family':
1001 1
                    $cellStyle->getFont()->setName(str_replace('\'', '', $styleValueString));
1002
1003 1
                    break;
1004
1005 10
                case 'text-decoration':
1006
                    switch ($styleValue) {
1007 1
                        case 'underline':
1008 1
                            $cellStyle->getFont()->setUnderline(Font::UNDERLINE_SINGLE);
1009
1010 1
                            break;
1011 1
                        case 'line-through':
1012 1
                            $cellStyle->getFont()->setStrikethrough(true);
1013
1014 1
                            break;
1015
                    }
1016
1017 1
                    break;
1018
1019 9
                case 'text-align':
1020 1
                    $cellStyle->getAlignment()->setHorizontal($styleValueString);
1021
1022 1
                    break;
1023
1024 9
                case 'vertical-align':
1025 2
                    $cellStyle->getAlignment()->setVertical($styleValueString);
1026
1027 2
                    break;
1028
1029 9
                case 'width':
1030 2
                    if ($column !== '') {
1031 2
                        $sheet->getColumnDimension($column)->setWidth(
1032 2
                            (new CssDimension($styleValue ?? ''))->width()
1033 2
                        );
1034
                    }
1035
1036 2
                    break;
1037
1038 7
                case 'height':
1039 1
                    if ($row > 0) {
1040 1
                        $sheet->getRowDimension($row)->setRowHeight(
1041 1
                            (new CssDimension($styleValue ?? ''))->height()
1042 1
                        );
1043
                    }
1044
1045 1
                    break;
1046
1047 6
                case 'word-wrap':
1048 1
                    $cellStyle->getAlignment()->setWrapText(
1049 1
                        $styleValue === 'break-word'
1050 1
                    );
1051
1052 1
                    break;
1053
1054 6
                case 'text-indent':
1055 2
                    $cellStyle->getAlignment()->setIndent(
1056 2
                        (int) str_replace(['px'], '', $styleValueString)
1057 2
                    );
1058
1059 2
                    break;
1060
            }
1061
        }
1062
    }
1063
1064
    /**
1065
     * Check if has #, so we can get clean hex.
1066
     */
1067 7
    public function getStyleColor(?string $value): string
1068
    {
1069 7
        $value = (string) $value;
1070 7
        if (str_starts_with($value, '#')) {
1071 5
            return substr($value, 1);
1072
        }
1073
1074 4
        return HelperHtml::colourNameLookup($value);
1075
    }
1076
1077 10
    private function insertImage(Worksheet $sheet, string $column, int $row, array $attributes): void
1078
    {
1079 10
        if (!isset($attributes['src'])) {
1080 1
            return;
1081
        }
1082 9
        $styleArray = self::getStyleArray($attributes);
1083
1084 9
        $src = $attributes['src'];
1085 9
        if (substr($src, 0, 5) !== 'data:') {
1086 5
            $src = urldecode($src);
1087
        }
1088 9
        $width = isset($attributes['width']) ? (float) $attributes['width'] : ($styleArray['width'] ?? null);
1089 9
        $height = isset($attributes['height']) ? (float) $attributes['height'] : ($styleArray['height'] ?? null);
1090 9
        $name = $attributes['alt'] ?? null;
1091
1092 9
        $drawing = new Drawing();
1093 9
        $drawing->setPath($src, false);
1094 8
        if ($drawing->getPath() === '') {
1095
            return;
1096
        }
1097 8
        $drawing->setWorksheet($sheet);
1098 8
        $drawing->setCoordinates($column . $row);
1099 8
        $drawing->setOffsetX(0);
1100 8
        $drawing->setOffsetY(10);
1101 8
        $drawing->setResizeProportional(true);
1102
1103 8
        if ($name) {
1104 7
            $drawing->setName($name);
1105
        }
1106
1107 8
        if ($width) {
1108 5
            if ($height) {
1109 2
                $drawing->setWidthAndHeight((int) $width, (int) $height);
1110
            } else {
1111 3
                $drawing->setWidth((int) $width);
1112
            }
1113 3
        } elseif ($height) {
1114 1
            $drawing->setHeight((int) $height);
1115
        }
1116
1117 8
        $sheet->getColumnDimension($column)->setWidth(
1118 8
            $drawing->getWidth() / 6
1119 8
        );
1120
1121 8
        $sheet->getRowDimension($row)->setRowHeight(
1122 8
            $drawing->getHeight() * 0.9
1123 8
        );
1124
1125 8
        if (isset($styleArray['opacity'])) {
1126
            $opacity = $styleArray['opacity'];
1127
            if (is_numeric($opacity)) {
1128
                $drawing->setOpacity((int) ($opacity * 100000));
1129
            }
1130
        }
1131
    }
1132
1133 9
    private static function getStyleArray(array $attributes): array
1134
    {
1135 9
        $styleArray = [];
1136 9
        if (isset($attributes['style'])) {
1137 4
            $styles = explode(';', $attributes['style']);
1138 4
            foreach ($styles as $style) {
1139 4
                $value = explode(':', $style);
1140 4
                if (count($value) === 2) {
1141 4
                    $arrayKey = trim($value[0]);
1142 4
                    $arrayValue = trim($value[1]);
1143 4
                    if ($arrayKey === 'width') {
1144 4
                        if (substr($arrayValue, -2) === 'px') {
1145 4
                            $arrayValue = (string) (((float) substr($arrayValue, 0, -2)));
1146
                        } else {
1147
                            $arrayValue = (new CssDimension($arrayValue))->width();
1148
                        }
1149 4
                    } elseif ($arrayKey === 'height') {
1150 2
                        if (substr($arrayValue, -2) === 'px') {
1151 2
                            $arrayValue = substr($arrayValue, 0, -2);
1152
                        } else {
1153
                            $arrayValue = (new CssDimension($arrayValue))->height();
1154
                        }
1155
                    }
1156 4
                    $styleArray[$arrayKey] = $arrayValue;
1157
                }
1158
            }
1159
        }
1160
1161 9
        return $styleArray;
1162
    }
1163
1164
    private const BORDER_MAPPINGS = [
1165
        'dash-dot' => Border::BORDER_DASHDOT,
1166
        'dash-dot-dot' => Border::BORDER_DASHDOTDOT,
1167
        'dashed' => Border::BORDER_DASHED,
1168
        'dotted' => Border::BORDER_DOTTED,
1169
        'double' => Border::BORDER_DOUBLE,
1170
        'hair' => Border::BORDER_HAIR,
1171
        'medium' => Border::BORDER_MEDIUM,
1172
        'medium-dashed' => Border::BORDER_MEDIUMDASHED,
1173
        'medium-dash-dot' => Border::BORDER_MEDIUMDASHDOT,
1174
        'medium-dash-dot-dot' => Border::BORDER_MEDIUMDASHDOTDOT,
1175
        'none' => Border::BORDER_NONE,
1176
        'slant-dash-dot' => Border::BORDER_SLANTDASHDOT,
1177
        'solid' => Border::BORDER_THIN,
1178
        'thick' => Border::BORDER_THICK,
1179
    ];
1180
1181 15
    public static function getBorderMappings(): array
1182
    {
1183 15
        return self::BORDER_MAPPINGS;
1184
    }
1185
1186
    /**
1187
     * Map html border style to PhpSpreadsheet border style.
1188
     */
1189 3
    public function getBorderStyle(string $style): ?string
1190
    {
1191 3
        return self::BORDER_MAPPINGS[$style] ?? null;
1192
    }
1193
1194 3
    private function setBorderStyle(Style $cellStyle, string $styleValue, string $type): void
1195
    {
1196 3
        if (trim($styleValue) === Border::BORDER_NONE) {
1197 1
            $borderStyle = Border::BORDER_NONE;
1198 1
            $color = null;
1199
        } else {
1200 3
            $borderArray = explode(' ', $styleValue);
1201 3
            $borderCount = count($borderArray);
1202 3
            if ($borderCount >= 3) {
1203 3
                $borderStyle = $borderArray[1];
1204 3
                $color = $borderArray[2];
1205
            } else {
1206 1
                $borderStyle = $borderArray[0];
1207 1
                $color = $borderArray[1] ?? null;
1208
            }
1209
        }
1210
1211 3
        $cellStyle->applyFromArray([
1212 3
            'borders' => [
1213 3
                $type => [
1214 3
                    'borderStyle' => $this->getBorderStyle($borderStyle),
1215 3
                    'color' => ['rgb' => $this->getStyleColor($color)],
1216 3
                ],
1217 3
            ],
1218 3
        ]);
1219
    }
1220
1221
    /**
1222
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
1223
     */
1224 1
    public function listWorksheetInfo(string $filename): array
1225
    {
1226 1
        $info = [];
1227 1
        $spreadsheet = new Spreadsheet();
1228 1
        $this->loadIntoExisting($filename, $spreadsheet);
1229 1
        foreach ($spreadsheet->getAllSheets() as $sheet) {
1230 1
            $newEntry = ['worksheetName' => $sheet->getTitle()];
1231 1
            $newEntry['lastColumnLetter'] = $sheet->getHighestDataColumn();
1232 1
            $newEntry['lastColumnIndex'] = Coordinate::columnIndexFromString($sheet->getHighestDataColumn()) - 1;
1233 1
            $newEntry['totalRows'] = $sheet->getHighestDataRow();
1234 1
            $newEntry['totalColumns'] = $newEntry['lastColumnIndex'] + 1;
1235 1
            $info[] = $newEntry;
1236
        }
1237 1
        $spreadsheet->disconnectWorksheets();
1238
1239 1
        return $info;
1240
    }
1241
}
1242