Ods::processTableRowGroup()   B
last analyzed

Complexity

Conditions 6
Paths 6

Size

Total Lines 54
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 14.789

Importance

Changes 0
Metric Value
eloc 37
dl 0
loc 54
ccs 15
cts 40
cp 0.375
rs 8.7057
c 0
b 0
f 0
cc 6
nc 6
nop 8
crap 14.789

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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\Cell\Coordinate;
11
use PhpOffice\PhpSpreadsheet\Cell\DataType;
12
use PhpOffice\PhpSpreadsheet\Helper\Dimension as HelperDimension;
13
use PhpOffice\PhpSpreadsheet\Reader\Ods\AutoFilter;
14
use PhpOffice\PhpSpreadsheet\Reader\Ods\DefinedNames;
15
use PhpOffice\PhpSpreadsheet\Reader\Ods\FormulaTranslator;
16
use PhpOffice\PhpSpreadsheet\Reader\Ods\PageSettings;
17
use PhpOffice\PhpSpreadsheet\Reader\Ods\Properties as DocumentProperties;
18
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
19
use PhpOffice\PhpSpreadsheet\RichText\RichText;
20
use PhpOffice\PhpSpreadsheet\Shared\Date;
21
use PhpOffice\PhpSpreadsheet\Shared\File;
22
use PhpOffice\PhpSpreadsheet\Spreadsheet;
23
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
24
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
25
use Throwable;
26
use XMLReader;
27
use ZipArchive;
28
29
class Ods extends BaseReader
30
{
31
    const INITIAL_FILE = 'content.xml';
32
33
    /**
34
     * Create a new Ods Reader instance.
35
     */
36 111
    public function __construct()
37
    {
38 111
        parent::__construct();
39 111
        $this->securityScanner = XmlScanner::getInstance($this);
40
    }
41
42
    /**
43
     * Can the current IReader read the file?
44
     */
45 19
    public function canRead(string $filename): bool
46
    {
47 19
        $mimeType = 'UNKNOWN';
48
49
        // Load file
50
51 19
        if (File::testFileNoThrow($filename, '')) {
52 3
            $zip = new ZipArchive();
53 3
            if ($zip->open($filename) === true) {
54
                // check if it is an OOXML archive
55 3
                $stat = $zip->statName('mimetype');
56 3
                if (!empty($stat) && ($stat['size'] <= 255)) {
57 2
                    $mimeType = $zip->getFromName($stat['name']);
58 1
                } elseif ($zip->statName('META-INF/manifest.xml')) {
59 1
                    $xml = simplexml_load_string(
60 1
                        $this->getSecurityScannerOrThrow()
61 1
                            ->scan(
62 1
                                $zip->getFromName(
63 1
                                    'META-INF/manifest.xml'
64 1
                                )
65 1
                            )
66 1
                    );
67 1
                    if ($xml !== false) {
68 1
                        $namespacesContent = $xml->getNamespaces(true);
69 1
                        if (isset($namespacesContent['manifest'])) {
70 1
                            $manifest = $xml->children($namespacesContent['manifest']);
71 1
                            foreach ($manifest as $manifestDataSet) {
72 1
                                $manifestAttributes = $manifestDataSet->attributes($namespacesContent['manifest']);
73 1
                                if ($manifestAttributes && $manifestAttributes->{'full-path'} == '/') {
74 1
                                    $mimeType = (string) $manifestAttributes->{'media-type'};
75
76 1
                                    break;
77
                                }
78
                            }
79
                        }
80
                    }
81
                }
82
83 3
                $zip->close();
84
            }
85
        }
86
87 19
        return $mimeType === 'application/vnd.oasis.opendocument.spreadsheet';
88
    }
89
90
    /**
91
     * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
92
     *
93
     * @return string[]
94
     */
95 6
    public function listWorksheetNames(string $filename): array
96
    {
97 6
        File::assertFile($filename, self::INITIAL_FILE);
98
99 2
        $worksheetNames = [];
100
101 2
        $xml = new XMLReader();
102 2
        $xml->xml(
103 2
            $this->getSecurityScannerOrThrow()
104 2
                ->scanFile(
105 2
                    'zip://' . realpath($filename) . '#' . self::INITIAL_FILE
106 2
                )
107 2
        );
108 2
        $xml->setParserProperty(2, true);
109
110
        // Step into the first level of content of the XML
111 2
        $xml->read();
112 2
        while ($xml->read()) {
113
            // Quickly jump through to the office:body node
114 2
            while ($xml->name !== 'office:body') {
115 2
                if ($xml->isEmptyElement) {
116 2
                    $xml->read();
117
                } else {
118 2
                    $xml->next();
119
                }
120
            }
121
            // Now read each node until we find our first table:table node
122 2
            while ($xml->read()) {
123 2
                $xmlName = $xml->name;
124 2
                if ($xmlName == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
125
                    // Loop through each table:table node reading the table:name attribute for each worksheet name
126
                    do {
127 2
                        $worksheetName = $xml->getAttribute('table:name');
128 2
                        if (!empty($worksheetName)) {
129 2
                            $worksheetNames[] = $worksheetName;
130
                        }
131 2
                        $xml->next();
132 2
                    } while ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT);
133
                }
134
            }
135
        }
136
137 2
        return $worksheetNames;
138
    }
139
140
    /**
141
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
142
     *
143
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
144
     */
145 6
    public function listWorksheetInfo(string $filename): array
146
    {
147 6
        File::assertFile($filename, self::INITIAL_FILE);
148
149 2
        $worksheetInfo = [];
150
151 2
        $xml = new XMLReader();
152 2
        $xml->xml(
153 2
            $this->getSecurityScannerOrThrow()
154 2
                ->scanFile(
155 2
                    'zip://' . realpath($filename) . '#' . self::INITIAL_FILE
156 2
                )
157 2
        );
158 2
        $xml->setParserProperty(2, true);
159
160
        // Step into the first level of content of the XML
161 2
        $xml->read();
162 2
        $tableVisibility = [];
163 2
        $lastTableStyle = '';
164
165 2
        while ($xml->read()) {
166 2
            if ($xml->name === 'style:style') {
167 2
                $styleType = $xml->getAttribute('style:family');
168 2
                if ($styleType === 'table') {
169 2
                    $lastTableStyle = $xml->getAttribute('style:name');
170
                }
171 2
            } elseif ($xml->name === 'style:table-properties') {
172 2
                $visibility = $xml->getAttribute('table:display');
173 2
                $tableVisibility[$lastTableStyle] = ($visibility === 'false') ? Worksheet::SHEETSTATE_HIDDEN : Worksheet::SHEETSTATE_VISIBLE;
174 2
            } elseif ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
175 2
                $worksheetNames[] = $xml->getAttribute('table:name');
176
177 2
                $styleName = $xml->getAttribute('table:style-name') ?? '';
178 2
                $visibility = $tableVisibility[$styleName] ?? '';
179 2
                $tmpInfo = [
180 2
                    'worksheetName' => (string) $xml->getAttribute('table:name'),
181 2
                    'lastColumnLetter' => 'A',
182 2
                    'lastColumnIndex' => 0,
183 2
                    'totalRows' => 0,
184 2
                    'totalColumns' => 0,
185 2
                    'sheetState' => $visibility,
186 2
                ];
187
188
                // Loop through each child node of the table:table element reading
189 2
                $currCells = 0;
190
                do {
191 2
                    $xml->read();
192 2
                    if ($xml->name == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) {
193 2
                        $rowspan = $xml->getAttribute('table:number-rows-repeated');
194 2
                        $rowspan = empty($rowspan) ? 1 : (int) $rowspan;
195 2
                        $tmpInfo['totalRows'] += $rowspan;
196 2
                        $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
197 2
                        $currCells = 0;
198
                        // Step into the row
199 2
                        $xml->read();
200
                        do {
201 2
                            $doread = true;
202 2
                            if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
203 2
                                if (!$xml->isEmptyElement) {
204 2
                                    ++$currCells;
205 2
                                    $xml->next();
206 2
                                    $doread = false;
207
                                }
208 1
                            } elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
209 1
                                $mergeSize = $xml->getAttribute('table:number-columns-repeated');
210 1
                                $currCells += (int) $mergeSize;
211
                            }
212 2
                            if ($doread) {
213 1
                                $xml->read();
214
                            }
215 2
                        } while ($xml->name != 'table:table-row');
216
                    }
217 2
                } while ($xml->name != 'table:table');
218
219 2
                $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
220 2
                $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
221 2
                $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
222 2
                $worksheetInfo[] = $tmpInfo;
223
            }
224
        }
225
226 2
        return $worksheetInfo;
227
    }
228
229
    /**
230
     * Loads PhpSpreadsheet from file.
231
     */
232 78
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
233
    {
234 78
        $spreadsheet = $this->newSpreadsheet();
235 78
        $spreadsheet->setValueBinder($this->valueBinder);
236 78
        $spreadsheet->removeSheetByIndex(0);
237
238
        // Load into this instance
239 78
        return $this->loadIntoExisting($filename, $spreadsheet);
240
    }
241
242
    /**
243
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
244
     */
245 82
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
246
    {
247 82
        File::assertFile($filename, self::INITIAL_FILE);
248
249 78
        $zip = new ZipArchive();
250 78
        $zip->open($filename);
251
252
        // Meta
253
254 78
        $xml = @simplexml_load_string(
255 78
            $this->getSecurityScannerOrThrow()
256 78
                ->scan($zip->getFromName('meta.xml'))
257 78
        );
258 78
        if ($xml === false) {
259 1
            throw new Exception('Unable to read data from {$pFilename}');
260
        }
261
262
        /** @var array{meta?: string, office?: string, dc?: string} */
263 77
        $namespacesMeta = $xml->getNamespaces(true);
264
265 77
        (new DocumentProperties($spreadsheet))->load($xml, $namespacesMeta);
266
267
        // Styles
268
269 77
        $dom = new DOMDocument('1.01', 'UTF-8');
270 77
        $dom->loadXML(
271 77
            $this->getSecurityScannerOrThrow()
272 77
                ->scan($zip->getFromName('styles.xml'))
273 77
        );
274
275 77
        $pageSettings = new PageSettings($dom);
276
277
        // Main Content
278
279 77
        $dom = new DOMDocument('1.01', 'UTF-8');
280 77
        $dom->loadXML(
281 77
            $this->getSecurityScannerOrThrow()
282 77
                ->scan($zip->getFromName(self::INITIAL_FILE))
283 77
        );
284
285 77
        $officeNs = (string) $dom->lookupNamespaceUri('office');
286 77
        $tableNs = (string) $dom->lookupNamespaceUri('table');
287 77
        $textNs = (string) $dom->lookupNamespaceUri('text');
288 77
        $xlinkNs = (string) $dom->lookupNamespaceUri('xlink');
289 77
        $styleNs = (string) $dom->lookupNamespaceUri('style');
290
291 77
        $pageSettings->readStyleCrossReferences($dom);
292
293 77
        $autoFilterReader = new AutoFilter($spreadsheet, $tableNs);
294 77
        $definedNameReader = new DefinedNames($spreadsheet, $tableNs);
295 77
        $columnWidths = [];
296 77
        $automaticStyle0 = $dom->getElementsByTagNameNS($officeNs, 'automatic-styles')->item(0);
297 77
        $automaticStyles = ($automaticStyle0 === null) ? [] : $automaticStyle0->getElementsByTagNameNS($styleNs, 'style');
298 77
        foreach ($automaticStyles as $automaticStyle) {
299 77
            $styleName = $automaticStyle->getAttributeNS($styleNs, 'name');
300 77
            $styleFamily = $automaticStyle->getAttributeNS($styleNs, 'family');
301 77
            if ($styleFamily === 'table-column') {
302 46
                $tcprops = $automaticStyle->getElementsByTagNameNS($styleNs, 'table-column-properties');
303 46
                $tcprop = $tcprops->item(0);
304 46
                if ($tcprop !== null) {
305 46
                    $columnWidth = $tcprop->getAttributeNs($styleNs, 'column-width');
306 46
                    $columnWidths[$styleName] = $columnWidth;
307
                }
308
            }
309
        }
310
311
        // Content
312 77
        $item0 = $dom->getElementsByTagNameNS($officeNs, 'body')->item(0);
313 77
        $spreadsheets = ($item0 === null) ? [] : $item0->getElementsByTagNameNS($officeNs, 'spreadsheet');
314
315 77
        foreach ($spreadsheets as $workbookData) {
316
            /** @var DOMElement $workbookData */
317 77
            $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table');
318
319 77
            $worksheetID = 0;
320 77
            foreach ($tables as $worksheetDataSet) {
321
                /** @var DOMElement $worksheetDataSet */
322 77
                $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name');
323
324
                // Check loadSheetsOnly
325
                if (
326 77
                    $this->loadSheetsOnly !== null
327
                    && $worksheetName
328 77
                    && !in_array($worksheetName, $this->loadSheetsOnly)
329
                ) {
330 3
                    continue;
331
                }
332
333 76
                $worksheetStyleName = $worksheetDataSet->getAttributeNS($tableNs, 'style-name');
334
335
                // Create sheet
336 76
                $spreadsheet->createSheet();
337 76
                $spreadsheet->setActiveSheetIndex($worksheetID);
338
339 76
                if ($worksheetName || is_numeric($worksheetName)) {
340
                    // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in
341
                    // formula cells... during the load, all formulae should be correct, and we're simply
342
                    // bringing the worksheet name in line with the formula, not the reverse
343 76
                    $spreadsheet->getActiveSheet()->setTitle((string) $worksheetName, false, false);
344
                }
345
346
                // Go through every child of table element
347 76
                $rowID = 1;
348 76
                $tableColumnIndex = 1;
349 76
                foreach ($worksheetDataSet->childNodes as $childNode) {
350
                    /** @var DOMElement $childNode */
351
352
                    // Filter elements which are not under the "table" ns
353 76
                    if ($childNode->namespaceURI != $tableNs) {
354 45
                        continue;
355
                    }
356
357 76
                    $key = self::extractNodeName($childNode->nodeName);
358
359
                    switch ($key) {
360 76
                        case 'table-header-rows':
361 76
                        case 'table-rows':
362 1
                            $this->processTableHeaderRows(
363 1
                                $childNode,
364 1
                                $tableNs,
365 1
                                $rowID,
366 1
                                $worksheetName,
367 1
                                $officeNs,
368 1
                                $textNs,
369 1
                                $xlinkNs,
370 1
                                $spreadsheet
371 1
                            );
372
373 1
                            break;
374 76
                        case 'table-row-group':
375 1
                            $this->processTableRowGroup(
376 1
                                $childNode,
377 1
                                $tableNs,
378 1
                                $rowID,
379 1
                                $worksheetName,
380 1
                                $officeNs,
381 1
                                $textNs,
382 1
                                $xlinkNs,
383 1
                                $spreadsheet
384 1
                            );
385
386 1
                            break;
387 76
                        case 'table-header-columns':
388 76
                        case 'table-columns':
389
                            $this->processTableHeaderColumns(
390
                                $childNode,
391
                                $tableNs,
392
                                $columnWidths,
393
                                $tableColumnIndex,
394
                                $spreadsheet
395
                            );
396
397
                            break;
398 76
                        case 'table-column-group':
399
                            $this->processTableColumnGroup(
400
                                $childNode,
401
                                $tableNs,
402
                                $columnWidths,
403
                                $tableColumnIndex,
404
                                $spreadsheet
405
                            );
406
407
                            break;
408 76
                        case 'table-column':
409 45
                            $this->processTableColumn(
410 45
                                $childNode,
411 45
                                $tableNs,
412 45
                                $columnWidths,
413 45
                                $tableColumnIndex,
414 45
                                $spreadsheet
415 45
                            );
416
417 45
                            break;
418 75
                        case 'table-row':
419 75
                            $this->processTableRow(
420 75
                                $childNode,
421 75
                                $tableNs,
422 75
                                $rowID,
423 75
                                $worksheetName,
424 75
                                $officeNs,
425 75
                                $textNs,
426 75
                                $xlinkNs,
427 75
                                $spreadsheet
428 75
                            );
429
430 75
                            break;
431
                    }
432
                }
433 76
                $pageSettings->setVisibilityForWorksheet(
434 76
                    $spreadsheet->getActiveSheet(),
435 76
                    $worksheetStyleName
436 76
                );
437 76
                $pageSettings->setPrintSettingsForWorksheet(
438 76
                    $spreadsheet->getActiveSheet(),
439 76
                    $worksheetStyleName
440 76
                );
441 76
                ++$worksheetID;
442
            }
443
444 77
            $autoFilterReader->read($workbookData);
445 77
            $definedNameReader->read($workbookData);
446
        }
447 77
        $spreadsheet->setActiveSheetIndex(0);
448
449 76
        if ($zip->locateName('settings.xml') !== false) {
450 69
            $this->processSettings($zip, $spreadsheet);
451
        }
452
453
        // Return
454 76
        return $spreadsheet;
455
    }
456
457 1
    private function processTableHeaderRows(
458
        DOMElement $childNode,
459
        string $tableNs,
460
        int &$rowID,
461
        string $worksheetName,
462
        string $officeNs,
463
        string $textNs,
464
        string $xlinkNs,
465
        Spreadsheet $spreadsheet
466
    ): void {
467 1
        foreach ($childNode->childNodes as $grandchildNode) {
468
            /** @var DOMElement $grandchildNode */
469 1
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
470
            switch ($grandkey) {
471 1
                case 'table-row':
472 1
                    $this->processTableRow(
473 1
                        $grandchildNode,
474 1
                        $tableNs,
475 1
                        $rowID,
476 1
                        $worksheetName,
477 1
                        $officeNs,
478 1
                        $textNs,
479 1
                        $xlinkNs,
480 1
                        $spreadsheet
481 1
                    );
482
483 1
                    break;
484
            }
485
        }
486
    }
487
488 1
    private function processTableRowGroup(
489
        DOMElement $childNode,
490
        string $tableNs,
491
        int &$rowID,
492
        string $worksheetName,
493
        string $officeNs,
494
        string $textNs,
495
        string $xlinkNs,
496
        Spreadsheet $spreadsheet
497
    ): void {
498 1
        foreach ($childNode->childNodes as $grandchildNode) {
499
            /** @var DOMElement $grandchildNode */
500 1
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
501
            switch ($grandkey) {
502 1
                case 'table-row':
503 1
                    $this->processTableRow(
504 1
                        $grandchildNode,
505 1
                        $tableNs,
506 1
                        $rowID,
507 1
                        $worksheetName,
508 1
                        $officeNs,
509 1
                        $textNs,
510 1
                        $xlinkNs,
511 1
                        $spreadsheet
512 1
                    );
513
514 1
                    break;
515
                case 'table-header-rows':
516
                case 'table-rows':
517
                    $this->processTableHeaderRows(
518
                        $grandchildNode,
519
                        $tableNs,
520
                        $rowID,
521
                        $worksheetName,
522
                        $officeNs,
523
                        $textNs,
524
                        $xlinkNs,
525
                        $spreadsheet
526
                    );
527
528
                    break;
529
                case 'table-row-group':
530
                    $this->processTableRowGroup(
531
                        $grandchildNode,
532
                        $tableNs,
533
                        $rowID,
534
                        $worksheetName,
535
                        $officeNs,
536
                        $textNs,
537
                        $xlinkNs,
538
                        $spreadsheet
539
                    );
540
541
                    break;
542
            }
543
        }
544
    }
545
546 75
    private function processTableRow(
547
        DOMElement $childNode,
548
        string $tableNs,
549
        int &$rowID,
550
        string $worksheetName,
551
        string $officeNs,
552
        string $textNs,
553
        string $xlinkNs,
554
        Spreadsheet $spreadsheet
555
    ): void {
556 75
        if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) {
557 26
            $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated');
558
        } else {
559 75
            $rowRepeats = 1;
560
        }
561
562 75
        $columnID = 'A';
563
        /** @var DOMElement|DOMText $cellData */
564 75
        foreach ($childNode->childNodes as $cellData) {
565 75
            if ($cellData instanceof DOMText) {
566 2
                continue; // should just be whitespace
567
            }
568 75
            if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
569 2
                if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
570 1
                    $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
571
                } else {
572 2
                    $colRepeats = 1;
573
                }
574
575 2
                for ($i = 0; $i < $colRepeats; ++$i) {
576 2
                    ++$columnID;
577
                }
578
579 2
                continue;
580
            }
581
582
            // Initialize variables
583 75
            $formatting = $hyperlink = null;
584 75
            $hasCalculatedValue = false;
585 75
            $cellDataFormula = '';
586 75
            $cellDataType = '';
587 75
            $cellDataRef = '';
588
589 75
            if ($cellData->hasAttributeNS($tableNs, 'formula')) {
590 29
                $cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula');
591 29
                $hasCalculatedValue = true;
592
            }
593 75
            if ($cellData->hasAttributeNS($tableNs, 'number-matrix-columns-spanned')) {
594 12
                if ($cellData->hasAttributeNS($tableNs, 'number-matrix-rows-spanned')) {
595 12
                    $cellDataType = 'array';
596 12
                    $arrayRow = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-rows-spanned');
597 12
                    $arrayCol = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-columns-spanned');
598 12
                    $lastRow = $rowID + $arrayRow - 1;
599 12
                    $lastCol = $columnID;
600 12
                    while ($arrayCol > 1) {
601 7
                        ++$lastCol;
602 7
                        --$arrayCol;
603
                    }
604 12
                    $cellDataRef = "$columnID$rowID:$lastCol$lastRow";
605
                }
606
            }
607
608
            // Annotations
609 75
            $annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation');
610
611 75
            if ($annotation->length > 0 && $annotation->item(0) !== null) {
612 11
                $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p');
613 11
                $textNodeLength = $textNode->length;
614 11
                $newLineOwed = false;
615 11
                for ($textNodeIndex = 0; $textNodeIndex < $textNodeLength; ++$textNodeIndex) {
616 11
                    $textNodeItem = $textNode->item($textNodeIndex);
617 11
                    if ($textNodeItem !== null) {
618 11
                        $text = $this->scanElementForText($textNodeItem);
619 11
                        if ($newLineOwed) {
620 1
                            $spreadsheet->getActiveSheet()
621 1
                                ->getComment($columnID . $rowID)
622 1
                                ->getText()
623 1
                                ->createText("\n");
624
                        }
625 11
                        $newLineOwed = true;
626
627 11
                        $spreadsheet->getActiveSheet()
628 11
                            ->getComment($columnID . $rowID)
629 11
                            ->getText()
630 11
                            ->createText(
631 11
                                $this->parseRichText($text)
632 11
                            );
633
                    }
634
                }
635
            }
636
637
            // Content
638
639
            /** @var DOMElement[] $paragraphs */
640 75
            $paragraphs = [];
641
642 75
            foreach ($cellData->childNodes as $item) {
643
                /** @var DOMElement $item */
644
645
                // Filter text:p elements
646 75
                if ($item->nodeName == 'text:p') {
647 75
                    $paragraphs[] = $item;
648
                }
649
            }
650
651 75
            if (count($paragraphs) > 0) {
652
                // Consolidate if there are multiple p records (maybe with spans as well)
653 75
                $dataArray = [];
654
655
                // Text can have multiple text:p and within those, multiple text:span.
656
                // text:p newlines, but text:span does not.
657
                // Also, here we assume there is no text data is span fields are specified, since
658
                // we have no way of knowing proper positioning anyway.
659
660 75
                foreach ($paragraphs as $pData) {
661 75
                    $dataArray[] = $this->scanElementForText($pData);
662
                }
663 75
                $allCellDataText = implode("\n", $dataArray);
664
665 75
                $type = $cellData->getAttributeNS($officeNs, 'value-type');
666
667
                switch ($type) {
668 75
                    case 'string':
669 49
                        $type = DataType::TYPE_STRING;
670 49
                        $dataValue = $allCellDataText;
671
672 49
                        foreach ($paragraphs as $paragraph) {
673 49
                            $link = $paragraph->getElementsByTagNameNS($textNs, 'a');
674 49
                            if ($link->length > 0 && $link->item(0) !== null) {
675 7
                                $hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href');
676
                            }
677
                        }
678
679 49
                        break;
680 51
                    case 'boolean':
681 9
                        $type = DataType::TYPE_BOOL;
682 9
                        $dataValue = ($cellData->getAttributeNS($officeNs, 'boolean-value') === 'true') ? true : false;
683
684 9
                        break;
685 49
                    case 'percentage':
686 4
                        $type = DataType::TYPE_NUMERIC;
687 4
                        $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
688
689
                        // percentage should always be float
690
                        //if (floor($dataValue) == $dataValue) {
691
                        //    $dataValue = (int) $dataValue;
692
                        //}
693 4
                        $formatting = NumberFormat::FORMAT_PERCENTAGE_00;
694
695 4
                        break;
696 49
                    case 'currency':
697 4
                        $type = DataType::TYPE_NUMERIC;
698 4
                        $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
699
700 4
                        if (floor($dataValue) == $dataValue) {
701 4
                            $dataValue = (int) $dataValue;
702
                        }
703 4
                        $formatting = NumberFormat::FORMAT_CURRENCY_USD_INTEGER;
704
705 4
                        break;
706 45
                    case 'float':
707 45
                        $type = DataType::TYPE_NUMERIC;
708 45
                        $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
709
710 45
                        if (floor($dataValue) == $dataValue) {
711 41
                            if ($dataValue == (int) $dataValue) {
712 41
                                $dataValue = (int) $dataValue;
713
                            }
714
                        }
715
716 45
                        break;
717 10
                    case 'date':
718 9
                        $type = DataType::TYPE_NUMERIC;
719 9
                        $value = $cellData->getAttributeNS($officeNs, 'date-value');
720 9
                        $dataValue = Date::convertIsoDate($value);
721
722 9
                        if ($dataValue != floor($dataValue)) {
723 6
                            $formatting = NumberFormat::FORMAT_DATE_XLSX15
724 6
                                . ' '
725 6
                                . NumberFormat::FORMAT_DATE_TIME4;
726
                        } else {
727 9
                            $formatting = NumberFormat::FORMAT_DATE_XLSX15;
728
                        }
729
730 9
                        break;
731 7
                    case 'time':
732 6
                        $type = DataType::TYPE_NUMERIC;
733
734 6
                        $timeValue = $cellData->getAttributeNS($officeNs, 'time-value');
735
736 6
                        $dataValue = Date::PHPToExcel(
737 6
                            strtotime(
738 6
                                '01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS') ?? [])
739 6
                            )
740 6
                        );
741 6
                        $formatting = NumberFormat::FORMAT_DATE_TIME4;
742
743 6
                        break;
744
                    default:
745 1
                        $dataValue = null;
746
                }
747
            } else {
748 43
                $type = DataType::TYPE_NULL;
749 43
                $dataValue = null;
750
            }
751
752 75
            if ($hasCalculatedValue) {
753 29
                $type = DataType::TYPE_FORMULA;
754 29
                $cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1);
755 29
                $cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula);
756
            }
757
758 75
            if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
759 38
                $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
760
            } else {
761 75
                $colRepeats = 1;
762
            }
763
764 75
            if ($type !== null) { // @phpstan-ignore-line
765 75
                for ($i = 0; $i < $colRepeats; ++$i) {
766 75
                    if ($i > 0) {
767 38
                        ++$columnID;
768
                    }
769
770 75
                    if ($type !== DataType::TYPE_NULL) {
771 75
                        for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {
772 75
                            $rID = $rowID + $rowAdjust;
773
774 75
                            $cell = $spreadsheet->getActiveSheet()
775 75
                                ->getCell($columnID . $rID);
776
777
                            // Set value
778 75
                            if ($hasCalculatedValue) {
779 29
                                $cell->setValueExplicit($cellDataFormula, $type);
780 29
                                if ($cellDataType === 'array') {
781 12
                                    $cell->setFormulaAttributes(['t' => 'array', 'ref' => $cellDataRef]);
782
                                }
783 71
                            } elseif ($type !== '' || $dataValue !== null) {
784 71
                                $cell->setValueExplicit($dataValue, $type);
785
                            }
786
787 75
                            if ($hasCalculatedValue) {
788 29
                                $cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC);
789
                            }
790
791
                            // Set other properties
792 75
                            if ($formatting !== null) {
793 13
                                $spreadsheet->getActiveSheet()
794 13
                                    ->getStyle($columnID . $rID)
795 13
                                    ->getNumberFormat()
796 13
                                    ->setFormatCode($formatting);
797
                            } else {
798 75
                                $spreadsheet->getActiveSheet()
799 75
                                    ->getStyle($columnID . $rID)
800 75
                                    ->getNumberFormat()
801 75
                                    ->setFormatCode(NumberFormat::FORMAT_GENERAL);
802
                            }
803
804 75
                            if ($hyperlink !== null) {
805 7
                                if ($hyperlink[0] === '#') {
806 1
                                    $hyperlink = 'sheet://' . substr($hyperlink, 1);
807
                                }
808 7
                                $cell->getHyperlink()
809 7
                                    ->setUrl($hyperlink);
810
                            }
811
                        }
812
                    }
813
                }
814
            }
815
816
            // Merged cells
817 75
            $this->processMergedCells($cellData, $tableNs, $type, $columnID, $rowID, $spreadsheet);
818
819 75
            ++$columnID;
820
        }
821 75
        $rowID += $rowRepeats;
822
    }
823
824 76
    private static function extractNodeName(string $key): string
825
    {
826
        // Remove ns from node name
827 76
        if (str_contains($key, ':')) {
828 76
            $keyChunks = explode(':', $key);
829 76
            $key = array_pop($keyChunks);
830
        }
831
832 76
        return $key;
833
    }
834
835
    /**
836
     * @param string[] $columnWidths
837
     */
838
    private function processTableHeaderColumns(
839
        DOMElement $childNode,
840
        string $tableNs,
841
        array $columnWidths,
842
        int &$tableColumnIndex,
843
        Spreadsheet $spreadsheet
844
    ): void {
845
        foreach ($childNode->childNodes as $grandchildNode) {
846
            /** @var DOMElement $grandchildNode */
847
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
848
            switch ($grandkey) {
849
                case 'table-column':
850
                    $this->processTableColumn(
851
                        $grandchildNode,
852
                        $tableNs,
853
                        $columnWidths,
854
                        $tableColumnIndex,
855
                        $spreadsheet
856
                    );
857
858
                    break;
859
            }
860
        }
861
    }
862
863
    /**
864
     * @param string[] $columnWidths
865
     */
866
    private function processTableColumnGroup(
867
        DOMElement $childNode,
868
        string $tableNs,
869
        array $columnWidths,
870
        int &$tableColumnIndex,
871
        Spreadsheet $spreadsheet
872
    ): void {
873
        foreach ($childNode->childNodes as $grandchildNode) {
874
            /** @var DOMElement $grandchildNode */
875
            $grandkey = self::extractNodeName($grandchildNode->nodeName);
876
            switch ($grandkey) {
877
                case 'table-column':
878
                    $this->processTableColumn(
879
                        $grandchildNode,
880
                        $tableNs,
881
                        $columnWidths,
882
                        $tableColumnIndex,
883
                        $spreadsheet
884
                    );
885
886
                    break;
887
                case 'table-header-columns':
888
                case 'table-columns':
889
                    $this->processTableHeaderColumns(
890
                        $grandchildNode,
891
                        $tableNs,
892
                        $columnWidths,
893
                        $tableColumnIndex,
894
                        $spreadsheet
895
                    );
896
897
                    break;
898
                case 'table-column-group':
899
                    $this->processTableColumnGroup(
900
                        $grandchildNode,
901
                        $tableNs,
902
                        $columnWidths,
903
                        $tableColumnIndex,
904
                        $spreadsheet
905
                    );
906
907
                    break;
908
            }
909
        }
910
    }
911
912
    /**
913
     * @param string[] $columnWidths
914
     */
915 45
    private function processTableColumn(
916
        DOMElement $childNode,
917
        string $tableNs,
918
        array $columnWidths,
919
        int &$tableColumnIndex,
920
        Spreadsheet $spreadsheet
921
    ): void {
922 45
        if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) {
923 40
            $rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated');
924
        } else {
925 19
            $rowRepeats = 1;
926
        }
927 45
        $tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name');
928 45
        if (isset($columnWidths[$tableStyleName])) {
929 45
            $columnWidth = new HelperDimension($columnWidths[$tableStyleName]);
930 45
            $tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex);
931 45
            for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) {
932
                /** @var string $tableColumnString */
933 45
                $spreadsheet->getActiveSheet()
934 45
                    ->getColumnDimension($tableColumnString)
935 45
                    ->setWidth($columnWidth->toUnit('cm'), 'cm');
936 45
                ++$tableColumnString;
937
            }
938
        }
939 45
        $tableColumnIndex += $rowRepeats;
940
    }
941
942 69
    private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void
943
    {
944 69
        $dom = new DOMDocument('1.01', 'UTF-8');
945 69
        $dom->loadXML(
946 69
            $this->getSecurityScannerOrThrow()
947 69
                ->scan($zip->getFromName('settings.xml'))
948 69
        );
949 69
        $configNs = (string) $dom->lookupNamespaceUri('config');
950 69
        $officeNs = (string) $dom->lookupNamespaceUri('office');
951 69
        $settings = $dom->getElementsByTagNameNS($officeNs, 'settings')
952 69
            ->item(0);
953 69
        if ($settings !== null) {
954 69
            $this->lookForActiveSheet($settings, $spreadsheet, $configNs);
955 69
            $this->lookForSelectedCells($settings, $spreadsheet, $configNs);
956
        }
957
    }
958
959 69
    private function lookForActiveSheet(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void
960
    {
961
        /** @var DOMElement $t */
962 69
        foreach ($settings->getElementsByTagNameNS($configNs, 'config-item') as $t) {
963 68
            if ($t->getAttributeNs($configNs, 'name') === 'ActiveTable') {
964
                try {
965 68
                    $spreadsheet->setActiveSheetIndexByName($t->nodeValue ?? '');
966 1
                } catch (Throwable) {
967
                    // do nothing
968
                }
969
970 68
                break;
971
            }
972
        }
973
    }
974
975 69
    private function lookForSelectedCells(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void
976
    {
977
        /** @var DOMElement $t */
978 69
        foreach ($settings->getElementsByTagNameNS($configNs, 'config-item-map-named') as $t) {
979 68
            if ($t->getAttributeNs($configNs, 'name') === 'Tables') {
980 68
                foreach ($t->getElementsByTagNameNS($configNs, 'config-item-map-entry') as $ws) {
981 68
                    $setRow = $setCol = '';
982 68
                    $wsname = $ws->getAttributeNs($configNs, 'name');
983 68
                    foreach ($ws->getElementsByTagNameNS($configNs, 'config-item') as $configItem) {
984 68
                        $attrName = $configItem->getAttributeNs($configNs, 'name');
985 68
                        if ($attrName === 'CursorPositionX') {
986 68
                            $setCol = $configItem->nodeValue;
987
                        }
988 68
                        if ($attrName === 'CursorPositionY') {
989 68
                            $setRow = $configItem->nodeValue;
990
                        }
991
                    }
992 68
                    $this->setSelected($spreadsheet, $wsname, "$setCol", "$setRow");
993
                }
994
995 68
                break;
996
            }
997
        }
998
    }
999
1000 68
    private function setSelected(Spreadsheet $spreadsheet, string $wsname, string $setCol, string $setRow): void
1001
    {
1002 68
        if (is_numeric($setCol) && is_numeric($setRow)) {
1003 68
            $sheet = $spreadsheet->getSheetByName($wsname);
1004 68
            if ($sheet !== null) {
1005 68
                $sheet->setSelectedCells([(int) $setCol + 1, (int) $setRow + 1]);
1006
            }
1007
        }
1008
    }
1009
1010
    /**
1011
     * Recursively scan element.
1012
     */
1013 75
    protected function scanElementForText(DOMNode $element): string
1014
    {
1015 75
        $str = '';
1016 75
        foreach ($element->childNodes as $child) {
1017
            /** @var DOMNode $child */
1018 75
            if ($child->nodeType == XML_TEXT_NODE) {
1019 75
                $str .= $child->nodeValue;
1020 18
            } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:line-break') {
1021 1
                $str .= "\n";
1022 18
            } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') {
1023
                // It's a space
1024
1025
                // Multiple spaces?
1026 6
                $attributes = $child->attributes;
1027
                /** @var ?DOMAttr $cAttr */
1028 6
                $cAttr = ($attributes === null) ? null : $attributes->getNamedItem('c');
1029 6
                $multiplier = self::getMultiplier($cAttr);
1030 6
                $str .= str_repeat(' ', $multiplier);
1031
            }
1032
1033 75
            if ($child->hasChildNodes()) {
1034 16
                $str .= $this->scanElementForText($child);
1035
            }
1036
        }
1037
1038 75
        return $str;
1039
    }
1040
1041 6
    private static function getMultiplier(?DOMAttr $cAttr): int
1042
    {
1043 6
        if ($cAttr) {
1044 6
            $multiplier = (int) $cAttr->nodeValue;
1045
        } else {
1046 6
            $multiplier = 1;
1047
        }
1048
1049 6
        return $multiplier;
1050
    }
1051
1052 11
    private function parseRichText(string $is): RichText
1053
    {
1054 11
        $value = new RichText();
1055 11
        $value->createText($is);
1056
1057 11
        return $value;
1058
    }
1059
1060 75
    private function processMergedCells(
1061
        DOMElement $cellData,
1062
        string $tableNs,
1063
        string $type,
1064
        string $columnID,
1065
        int $rowID,
1066
        Spreadsheet $spreadsheet
1067
    ): void {
1068
        if (
1069 75
            $cellData->hasAttributeNS($tableNs, 'number-columns-spanned')
1070 75
            || $cellData->hasAttributeNS($tableNs, 'number-rows-spanned')
1071
        ) {
1072 18
            if (($type !== DataType::TYPE_NULL) || ($this->readDataOnly === false)) {
1073 18
                $columnTo = $columnID;
1074
1075 18
                if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) {
1076 17
                    $columnIndex = Coordinate::columnIndexFromString($columnID);
1077 17
                    $columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned');
1078 17
                    $columnIndex -= 2;
1079
1080 17
                    $columnTo = Coordinate::stringFromColumnIndex($columnIndex + 1);
1081
                }
1082
1083 18
                $rowTo = $rowID;
1084
1085 18
                if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) {
1086 18
                    $rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1;
1087
                }
1088
1089 18
                $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo;
1090 18
                $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE);
1091
            }
1092
        }
1093
    }
1094
}
1095