Passed
Push — master ( f4919a...817556 )
by
unknown
20:07 queued 09:43
created

Xml::unentity()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 7
rs 10
c 0
b 0
f 0
ccs 5
cts 5
cp 1
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Reader;
4
5
use DateTime;
6
use DateTimeZone;
7
use PhpOffice\PhpSpreadsheet\Cell\AddressHelper;
8
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
9
use PhpOffice\PhpSpreadsheet\Cell\DataType;
10
use PhpOffice\PhpSpreadsheet\DefinedName;
11
use PhpOffice\PhpSpreadsheet\Helper\Html as HelperHtml;
12
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
13
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
14
use PhpOffice\PhpSpreadsheet\Reader\Xml\PageSettings;
15
use PhpOffice\PhpSpreadsheet\Reader\Xml\Properties;
16
use PhpOffice\PhpSpreadsheet\Reader\Xml\Style;
17
use PhpOffice\PhpSpreadsheet\RichText\RichText;
18
use PhpOffice\PhpSpreadsheet\Settings;
19
use PhpOffice\PhpSpreadsheet\Shared\Date;
20
use PhpOffice\PhpSpreadsheet\Shared\File;
21
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
22
use PhpOffice\PhpSpreadsheet\Spreadsheet;
23
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
24
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
25
use SimpleXMLElement;
26
use Throwable;
27
28
/**
29
 * Reader for SpreadsheetML, the XML schema for Microsoft Office Excel 2003.
30
 */
31
class Xml extends BaseReader
32
{
33
    public const NAMESPACES_SS = 'urn:schemas-microsoft-com:office:spreadsheet';
34
35
    /**
36
     * Formats.
37
     */
38
    protected array $styles = [];
39
40
    /**
41
     * Create a new Excel2003XML Reader instance.
42
     */
43 82
    public function __construct()
44
    {
45 82
        parent::__construct();
46 82
        $this->securityScanner = XmlScanner::getInstance($this);
47
        /** @var callable */
48 82
        $unentity = [self::class, 'unentity'];
49 82
        $this->securityScanner->setAdditionalCallback($unentity);
50
    }
51
52 46
    public static function unentity(string $contents): string
53
    {
54 46
        $contents = preg_replace('/&(amp|lt|gt|quot|apos);/', "\u{fffe}\u{feff}\$1;", trim($contents)) ?? $contents;
55 46
        $contents = html_entity_decode($contents, ENT_NOQUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
56 46
        $contents = str_replace("\u{fffe}\u{feff}", '&', $contents);
57
58 46
        return $contents;
59
    }
60
61
    private string $fileContents = '';
62
63
    private string $xmlFailMessage = '';
64
65 50
    public static function xmlMappings(): array
66
    {
67 50
        return array_merge(
68 50
            Style\Fill::FILL_MAPPINGS,
69 50
            Style\Border::BORDER_MAPPINGS
70 50
        );
71
    }
72
73
    /**
74
     * Can the current IReader read the file?
75
     */
76 51
    public function canRead(string $filename): bool
77
    {
78
        //    Office                    xmlns:o="urn:schemas-microsoft-com:office:office"
79
        //    Excel                    xmlns:x="urn:schemas-microsoft-com:office:excel"
80
        //    XML Spreadsheet            xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
81
        //    Spreadsheet component    xmlns:c="urn:schemas-microsoft-com:office:component:spreadsheet"
82
        //    XML schema                 xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882"
83
        //    XML data type            xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"
84
        //    MS-persist recordset    xmlns:rs="urn:schemas-microsoft-com:rowset"
85
        //    Rowset                    xmlns:z="#RowsetSchema"
86
        //
87
88 51
        $signature = [
89 51
            '<?xml version="1.0"',
90 51
            'xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet',
91 51
        ];
92
93
        // Open file
94 51
        $data = file_get_contents($filename) ?: '';
95
96
        // Why?
97
        //$data = str_replace("'", '"', $data); // fix headers with single quote
98
99 51
        $valid = true;
100 51
        foreach ($signature as $match) {
101
            // every part of the signature must be present
102 51
            if (!str_contains($data, $match)) {
103 20
                $valid = false;
104
105 20
                break;
106
            }
107
        }
108
109
        //    Retrieve charset encoding
110 51
        if (preg_match('/<?xml.*encoding=[\'"](.*?)[\'"].*?>/m', $data, $matches)) {
111 12
            $charSet = strtoupper($matches[1]);
112 12
            if (preg_match('/^ISO-8859-\d[\dL]?$/i', $charSet) === 1) {
113 1
                $data = StringHelper::convertEncoding($data, 'UTF-8', $charSet);
114 1
                $data = (string) preg_replace('/(<?xml.*encoding=[\'"]).*?([\'"].*?>)/um', '$1' . 'UTF-8' . '$2', $data, 1);
115
            }
116
        }
117 51
        $this->fileContents = $data;
118
119 51
        return $valid;
120
    }
121
122
    /** @return false|SimpleXMLElement */
123 46
    private function trySimpleXMLLoadStringPrivate(string $filename, string $fileOrString = 'file'): SimpleXMLElement|bool
124
    {
125 46
        $this->xmlFailMessage = "Cannot load invalid XML $fileOrString: " . $filename;
126 46
        $xml = false;
127
128
        try {
129 46
            $data = $this->fileContents;
130 46
            $continue = true;
131 46
            if ($data === '' && $fileOrString === 'file') {
132
                if ($filename === '') {
133
                    $this->xmlFailMessage = 'Cannot load empty path';
134
                    $continue = false;
135
                } else {
136
                    $datax = @file_get_contents($filename);
137
                    $data = $datax ?: '';
138
                    $continue = $datax !== false;
139
                }
140
            }
141 46
            if ($continue) {
142 46
                $xml = @simplexml_load_string(
143 46
                    $this->getSecurityScannerOrThrow()->scan($data),
144 46
                    'SimpleXMLElement',
145 46
                    Settings::getLibXmlLoaderOptions()
146 46
                );
147
            }
148
        } catch (Throwable $e) {
149
            throw new Exception($this->xmlFailMessage, 0, $e);
150
        }
151 46
        $this->fileContents = '';
152
153 46
        return $xml;
154
    }
155
156
    /**
157
     * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
158
     */
159 4
    public function listWorksheetNames(string $filename): array
160
    {
161 4
        File::assertFile($filename);
162 4
        if (!$this->canRead($filename)) {
163 2
            throw new Exception($filename . ' is an Invalid Spreadsheet file.');
164
        }
165
166 2
        $worksheetNames = [];
167
168 2
        $xml = $this->trySimpleXMLLoadStringPrivate($filename);
169 2
        if ($xml === false) {
170 1
            throw new Exception("Problem reading {$filename}");
171
        }
172
173 1
        $xml_ss = $xml->children(self::NAMESPACES_SS);
174 1
        foreach ($xml_ss->Worksheet as $worksheet) {
175 1
            $worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS);
176 1
            $worksheetNames[] = (string) $worksheet_ss['Name'];
177
        }
178
179 1
        return $worksheetNames;
180
    }
181
182
    /**
183
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
184
     */
185 4
    public function listWorksheetInfo(string $filename): array
186
    {
187 4
        File::assertFile($filename);
188 4
        if (!$this->canRead($filename)) {
189 2
            throw new Exception($filename . ' is an Invalid Spreadsheet file.');
190
        }
191
192 2
        $worksheetInfo = [];
193
194 2
        $xml = $this->trySimpleXMLLoadStringPrivate($filename);
195 2
        if ($xml === false) {
196 1
            throw new Exception("Problem reading {$filename}");
197
        }
198
199 1
        $worksheetID = 1;
200 1
        $xml_ss = $xml->children(self::NAMESPACES_SS);
201 1
        foreach ($xml_ss->Worksheet as $worksheet) {
202 1
            $worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS);
203
204 1
            $tmpInfo = [];
205 1
            $tmpInfo['worksheetName'] = '';
206 1
            $tmpInfo['lastColumnLetter'] = 'A';
207 1
            $tmpInfo['lastColumnIndex'] = 0;
208 1
            $tmpInfo['totalRows'] = 0;
209 1
            $tmpInfo['totalColumns'] = 0;
210
211 1
            $tmpInfo['worksheetName'] = "Worksheet_{$worksheetID}";
212 1
            if (isset($worksheet_ss['Name'])) {
213 1
                $tmpInfo['worksheetName'] = (string) $worksheet_ss['Name'];
214
            }
215
216 1
            if (isset($worksheet->Table->Row)) {
217 1
                $rowIndex = 0;
218
219 1
                foreach ($worksheet->Table->Row as $rowData) {
220 1
                    $columnIndex = 0;
221 1
                    $rowHasData = false;
222
223 1
                    foreach ($rowData->Cell as $cell) {
224 1
                        if (isset($cell->Data)) {
225 1
                            $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex);
226 1
                            $rowHasData = true;
227
                        }
228
229 1
                        ++$columnIndex;
230
                    }
231
232 1
                    ++$rowIndex;
233
234 1
                    if ($rowHasData) {
235 1
                        $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex);
236
                    }
237
                }
238
            }
239
240 1
            $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
241 1
            $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1;
242
243 1
            $worksheetInfo[] = $tmpInfo;
244 1
            ++$worksheetID;
245
        }
246
247 1
        return $worksheetInfo;
248
    }
249
250
    /**
251
     * Loads Spreadsheet from string.
252
     */
253 15
    public function loadSpreadsheetFromString(string $contents): Spreadsheet
254
    {
255
        // Create new Spreadsheet
256 15
        $spreadsheet = new Spreadsheet();
257 15
        $spreadsheet->setValueBinder($this->valueBinder);
258 15
        $spreadsheet->removeSheetByIndex(0);
259
260
        // Load into this instance
261 15
        return $this->loadIntoExisting($contents, $spreadsheet, true);
262
    }
263
264
    /**
265
     * Loads Spreadsheet from file.
266
     */
267 31
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
268
    {
269
        // Create new Spreadsheet
270 31
        $spreadsheet = new Spreadsheet();
271 31
        $spreadsheet->setValueBinder($this->valueBinder);
272 31
        $spreadsheet->removeSheetByIndex(0);
273
274
        // Load into this instance
275 31
        return $this->loadIntoExisting($filename, $spreadsheet);
276
    }
277
278
    /**
279
     * Loads from file or contents into Spreadsheet instance.
280
     *
281
     * @param string $filename file name if useContents is false else file contents
282
     */
283 46
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, bool $useContents = false): Spreadsheet
284
    {
285 46
        if ($useContents) {
286 15
            $this->fileContents = $filename;
287 15
            $fileOrString = 'string';
288
        } else {
289 31
            File::assertFile($filename);
290 30
            if (!$this->canRead($filename)) {
291 3
                throw new Exception($filename . ' is an Invalid Spreadsheet file.');
292
            }
293 27
            $fileOrString = 'file';
294
        }
295
296 42
        $xml = $this->trySimpleXMLLoadStringPrivate($filename, $fileOrString);
297 42
        if ($xml === false) {
298 3
            throw new Exception($this->xmlFailMessage);
299
        }
300
301 39
        $namespaces = $xml->getNamespaces(true);
302
303 39
        (new Properties($spreadsheet))->readProperties($xml, $namespaces);
304
305 39
        $this->styles = (new Style())->parseStyles($xml, $namespaces);
306 39
        if (isset($this->styles['Default'])) {
307 33
            $spreadsheet->getCellXfCollection()[0]->applyFromArray($this->styles['Default']);
308
        }
309
310 39
        $worksheetID = 0;
311 39
        $xml_ss = $xml->children(self::NAMESPACES_SS);
312
313
        /** @var null|SimpleXMLElement $worksheetx */
314 39
        foreach ($xml_ss->Worksheet as $worksheetx) {
315 39
            $worksheet = $worksheetx ?? new SimpleXMLElement('<xml></xml>');
316 39
            $worksheet_ss = self::getAttributes($worksheet, self::NAMESPACES_SS);
317
318
            if (
319 39
                isset($this->loadSheetsOnly, $worksheet_ss['Name'])
320 39
                && (!in_array($worksheet_ss['Name'], $this->loadSheetsOnly))
321
            ) {
322 2
                continue;
323
            }
324
325
            // Create new Worksheet
326 38
            $spreadsheet->createSheet();
327 38
            $spreadsheet->setActiveSheetIndex($worksheetID);
328 38
            $worksheetName = '';
329 38
            if (isset($worksheet_ss['Name'])) {
330 38
                $worksheetName = (string) $worksheet_ss['Name'];
331
                //    Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in
332
                //        formula cells... during the load, all formulae should be correct, and we're simply bringing
333
                //        the worksheet name in line with the formula, not the reverse
334 38
                $spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false);
335
            }
336 38
            if (isset($worksheet_ss['Protected'])) {
337 2
                $protection = (string) $worksheet_ss['Protected'] === '1';
338 2
                $spreadsheet->getActiveSheet()->getProtection()->setSheet($protection);
339
            }
340
341
            // locally scoped defined names
342 38
            if (isset($worksheet->Names[0])) {
343 2
                foreach ($worksheet->Names[0] as $definedName) {
344 2
                    $definedName_ss = self::getAttributes($definedName, self::NAMESPACES_SS);
345 2
                    $name = (string) $definedName_ss['Name'];
346 2
                    $definedValue = (string) $definedName_ss['RefersTo'];
347 2
                    $convertedValue = AddressHelper::convertFormulaToA1($definedValue);
348 2
                    if ($convertedValue[0] === '=') {
349 2
                        $convertedValue = substr($convertedValue, 1);
350
                    }
351 2
                    $spreadsheet->addDefinedName(DefinedName::createInstance($name, $spreadsheet->getActiveSheet(), $convertedValue, true));
352
                }
353
            }
354
355 38
            $columnID = 'A';
356 38
            if (isset($worksheet->Table->Column)) {
357 17
                foreach ($worksheet->Table->Column as $columnData) {
358 17
                    $columnData_ss = self::getAttributes($columnData, self::NAMESPACES_SS);
359 17
                    $colspan = 0;
360 17
                    if (isset($columnData_ss['Span'])) {
361 12
                        $spanAttr = (string) $columnData_ss['Span'];
362 12
                        if (is_numeric($spanAttr)) {
363 12
                            $colspan = max(0, (int) $spanAttr);
364
                        }
365
                    }
366 17
                    if (isset($columnData_ss['Index'])) {
367 14
                        $columnID = Coordinate::stringFromColumnIndex((int) $columnData_ss['Index']);
368
                    }
369 17
                    $columnWidth = null;
370 17
                    if (isset($columnData_ss['Width'])) {
371 16
                        $columnWidth = $columnData_ss['Width'];
372
                    }
373 17
                    $columnVisible = null;
374 17
                    if (isset($columnData_ss['Hidden'])) {
375 11
                        $columnVisible = ((string) $columnData_ss['Hidden']) !== '1';
376
                    }
377 17
                    while ($colspan >= 0) {
378 17
                        if (isset($columnWidth)) {
379 16
                            $spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setWidth($columnWidth / 5.4);
380
                        }
381 17
                        if (isset($columnVisible)) {
382 11
                            $spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setVisible($columnVisible);
383
                        }
384 17
                        ++$columnID;
385 17
                        --$colspan;
386
                    }
387
                }
388
            }
389
390 38
            $rowID = 1;
391 38
            if (isset($worksheet->Table->Row)) {
392 37
                $additionalMergedCells = 0;
393 37
                foreach ($worksheet->Table->Row as $rowData) {
394 37
                    $rowHasData = false;
395 37
                    $row_ss = self::getAttributes($rowData, self::NAMESPACES_SS);
396 37
                    if (isset($row_ss['Index'])) {
397 6
                        $rowID = (int) $row_ss['Index'];
398
                    }
399 37
                    if (isset($row_ss['Hidden'])) {
400 10
                        $rowVisible = ((string) $row_ss['Hidden']) !== '1';
401 10
                        $spreadsheet->getActiveSheet()->getRowDimension($rowID)->setVisible($rowVisible);
402
                    }
403
404 37
                    $columnID = 'A';
405 37
                    foreach ($rowData->Cell as $cell) {
406 37
                        $arrayRef = '';
407 37
                        $cell_ss = self::getAttributes($cell, self::NAMESPACES_SS);
408 37
                        if (isset($cell_ss['Index'])) {
409 18
                            $columnID = Coordinate::stringFromColumnIndex((int) $cell_ss['Index']);
410
                        }
411 37
                        $cellRange = $columnID . $rowID;
412 37
                        if (isset($cell_ss['ArrayRange'])) {
413 1
                            $arrayRange = (string) $cell_ss['ArrayRange'];
414 1
                            $arrayRef = AddressHelper::convertFormulaToA1($arrayRange, $rowID, Coordinate::columnIndexFromString($columnID));
415
                        }
416
417 37
                        if ($this->getReadFilter() !== null) {
418 37
                            if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
419 1
                                ++$columnID;
420
421 1
                                continue;
422
                            }
423
                        }
424
425 37
                        if (isset($cell_ss['HRef'])) {
426 13
                            $spreadsheet->getActiveSheet()->getCell($cellRange)->getHyperlink()->setUrl((string) $cell_ss['HRef']);
427
                        }
428
429 37
                        if ((isset($cell_ss['MergeAcross'])) || (isset($cell_ss['MergeDown']))) {
430 9
                            $columnTo = $columnID;
431 9
                            if (isset($cell_ss['MergeAcross'])) {
432 9
                                $additionalMergedCells += (int) $cell_ss['MergeAcross'];
433 9
                                $columnTo = Coordinate::stringFromColumnIndex((int) (Coordinate::columnIndexFromString($columnID) + $cell_ss['MergeAcross']));
434
                            }
435 9
                            $rowTo = $rowID;
436 9
                            if (isset($cell_ss['MergeDown'])) {
437 9
                                $rowTo = $rowTo + $cell_ss['MergeDown'];
438
                            }
439 9
                            $cellRange .= ':' . $columnTo . $rowTo;
440 9
                            $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE);
441
                        }
442
443 37
                        $hasCalculatedValue = false;
444 37
                        $cellDataFormula = '';
445 37
                        if (isset($cell_ss['Formula'])) {
446 19
                            $cellDataFormula = $cell_ss['Formula'];
447 19
                            $hasCalculatedValue = true;
448 19
                            if ($arrayRef !== '') {
449 1
                                $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setFormulaAttributes(['t' => 'array', 'ref' => $arrayRef]);
450
                            }
451
                        }
452 37
                        if (isset($cell->Data)) {
453 37
                            $cellData = $cell->Data;
454 37
                            $cellValue = (string) $cellData;
455 37
                            $type = DataType::TYPE_NULL;
456 37
                            $cellData_ss = self::getAttributes($cellData, self::NAMESPACES_SS);
457 37
                            if (isset($cellData_ss['Type'])) {
458 37
                                $cellDataType = $cellData_ss['Type'];
459
                                switch ($cellDataType) {
460
                                    /*
461
                                    const TYPE_STRING        = 's';
462
                                    const TYPE_FORMULA        = 'f';
463
                                    const TYPE_NUMERIC        = 'n';
464
                                    const TYPE_BOOL            = 'b';
465
                                    const TYPE_NULL            = 'null';
466
                                    const TYPE_INLINE        = 'inlineStr';
467
                                    const TYPE_ERROR        = 'e';
468
                                    */
469 37
                                    case 'String':
470 33
                                        $type = DataType::TYPE_STRING;
471 33
                                        $rich = $cellData->children('http://www.w3.org/TR/REC-html40');
472 33
                                        if ($rich) {
473
                                            // in case of HTML content we extract the payload
474
                                            // and convert it into a rich text object
475 2
                                            $content = $cellData->asXML() ?: '';
476 2
                                            $html = new HelperHtml();
477 2
                                            $cellValue = $html->toRichTextObject($content, true);
478
                                        }
479
480 33
                                        break;
481 17
                                    case 'Number':
482 17
                                        $type = DataType::TYPE_NUMERIC;
483 17
                                        $cellValue = (float) $cellValue;
484 17
                                        if (floor($cellValue) == $cellValue) {
485 17
                                            $cellValue = (int) $cellValue;
486
                                        }
487
488 17
                                        break;
489 11
                                    case 'Boolean':
490 9
                                        $type = DataType::TYPE_BOOL;
491 9
                                        $cellValue = ($cellValue != 0);
492
493 9
                                        break;
494 11
                                    case 'DateTime':
495 11
                                        $type = DataType::TYPE_NUMERIC;
496 11
                                        $dateTime = new DateTime($cellValue, new DateTimeZone('UTC'));
497 11
                                        $cellValue = Date::PHPToExcel($dateTime);
498
499 11
                                        break;
500 9
                                    case 'Error':
501 9
                                        $type = DataType::TYPE_ERROR;
502 9
                                        $hasCalculatedValue = false;
503
504 9
                                        break;
505
                                }
506
                            }
507
508 37
                            $originalType = $type;
509 37
                            if ($hasCalculatedValue) {
510 19
                                $type = DataType::TYPE_FORMULA;
511 19
                                $columnNumber = Coordinate::columnIndexFromString($columnID);
512 19
                                $cellDataFormula = AddressHelper::convertFormulaToA1($cellDataFormula, $rowID, $columnNumber);
513
                            }
514
515 37
                            $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValueExplicit((($hasCalculatedValue) ? $cellDataFormula : $cellValue), $type);
516 37
                            if ($hasCalculatedValue) {
517 19
                                $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setCalculatedValue($cellValue, $originalType === DataType::TYPE_NUMERIC);
518
                            }
519 37
                            $rowHasData = true;
520
                        }
521
522 37
                        if (isset($cell->Comment)) {
523 10
                            $this->parseCellComment($cell->Comment, $spreadsheet, $columnID, $rowID);
524
                        }
525
526 37
                        if (isset($cell_ss['StyleID'])) {
527 22
                            $style = (string) $cell_ss['StyleID'];
528 22
                            if ((isset($this->styles[$style])) && (!empty($this->styles[$style]))) {
529 22
                                $spreadsheet->getActiveSheet()->getStyle($cellRange)
530 22
                                    ->applyFromArray($this->styles[$style]);
531
                            }
532
                        }
533 37
                        ++$columnID;
534 37
                        while ($additionalMergedCells > 0) {
535 9
                            ++$columnID;
536 9
                            --$additionalMergedCells;
537
                        }
538
                    }
539
540 37
                    if ($rowHasData) {
541 37
                        if (isset($row_ss['Height'])) {
542 17
                            $rowHeight = $row_ss['Height'];
543 17
                            $spreadsheet->getActiveSheet()->getRowDimension($rowID)->setRowHeight((float) $rowHeight);
544
                        }
545
                    }
546
547 37
                    ++$rowID;
548
                }
549
            }
550
551 38
            $dataValidations = new Xml\DataValidations();
552 38
            $dataValidations->loadDataValidations($worksheet, $spreadsheet);
553 38
            $xmlX = $worksheet->children(Namespaces::URN_EXCEL);
554 38
            if (isset($xmlX->WorksheetOptions)) {
555 33
                if (isset($xmlX->WorksheetOptions->ShowPageBreakZoom)) {
556 3
                    $spreadsheet->getActiveSheet()->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW);
557
                }
558 33
                if (isset($xmlX->WorksheetOptions->Zoom)) {
559 3
                    $zoomScaleNormal = (int) $xmlX->WorksheetOptions->Zoom;
560 3
                    if ($zoomScaleNormal > 0) {
561 3
                        $spreadsheet->getActiveSheet()->getSheetView()->setZoomScaleNormal($zoomScaleNormal);
562 3
                        $spreadsheet->getActiveSheet()->getSheetView()->setZoomScale($zoomScaleNormal);
563
                    }
564
                }
565 33
                if (isset($xmlX->WorksheetOptions->PageBreakZoom)) {
566 3
                    $zoomScaleNormal = (int) $xmlX->WorksheetOptions->PageBreakZoom;
567 3
                    if ($zoomScaleNormal > 0) {
568 3
                        $spreadsheet->getActiveSheet()->getSheetView()->setZoomScaleSheetLayoutView($zoomScaleNormal);
569
                    }
570
                }
571 33
                if (isset($xmlX->WorksheetOptions->ShowPageBreakZoom)) {
572 3
                    $spreadsheet->getActiveSheet()->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW);
573
                }
574 33
                if (isset($xmlX->WorksheetOptions->FreezePanes)) {
575 4
                    $freezeRow = $freezeColumn = 1;
576 4
                    if (isset($xmlX->WorksheetOptions->SplitHorizontal)) {
577 4
                        $freezeRow = (int) $xmlX->WorksheetOptions->SplitHorizontal + 1;
578
                    }
579 4
                    if (isset($xmlX->WorksheetOptions->SplitVertical)) {
580 4
                        $freezeColumn = (int) $xmlX->WorksheetOptions->SplitVertical + 1;
581
                    }
582 4
                    $leftTopRow = (string) $xmlX->WorksheetOptions->TopRowBottomPane;
583 4
                    $leftTopColumn = (string) $xmlX->WorksheetOptions->LeftColumnRightPane;
584 4
                    if (is_numeric($leftTopRow) && is_numeric($leftTopColumn)) {
585 4
                        $leftTopCoordinate = Coordinate::stringFromColumnIndex((int) $leftTopColumn + 1) . (string) ($leftTopRow + 1);
586 4
                        $spreadsheet->getActiveSheet()->freezePane(Coordinate::stringFromColumnIndex($freezeColumn) . (string) $freezeRow, $leftTopCoordinate, !isset($xmlX->WorksheetOptions->FrozenNoSplit));
587
                    } else {
588
                        $spreadsheet->getActiveSheet()->freezePane(Coordinate::stringFromColumnIndex($freezeColumn) . (string) $freezeRow, null, !isset($xmlX->WorksheetOptions->FrozenNoSplit));
589
                    }
590 32
                } elseif (isset($xmlX->WorksheetOptions->SplitVertical) || isset($xmlX->WorksheetOptions->SplitHorizontal)) {
591 1
                    if (isset($xmlX->WorksheetOptions->SplitHorizontal)) {
592 1
                        $ySplit = (int) $xmlX->WorksheetOptions->SplitHorizontal;
593 1
                        $spreadsheet->getActiveSheet()->setYSplit($ySplit);
594
                    }
595 1
                    if (isset($xmlX->WorksheetOptions->SplitVertical)) {
596 1
                        $xSplit = (int) $xmlX->WorksheetOptions->SplitVertical;
597 1
                        $spreadsheet->getActiveSheet()->setXSplit($xSplit);
598
                    }
599 1
                    if (isset($xmlX->WorksheetOptions->LeftColumnVisible) || isset($xmlX->WorksheetOptions->TopRowVisible)) {
600 1
                        $leftTopColumn = $leftTopRow = 1;
601 1
                        if (isset($xmlX->WorksheetOptions->LeftColumnVisible)) {
602 1
                            $leftTopColumn = 1 + (int) $xmlX->WorksheetOptions->LeftColumnVisible;
603
                        }
604 1
                        if (isset($xmlX->WorksheetOptions->TopRowVisible)) {
605 1
                            $leftTopRow = 1 + (int) $xmlX->WorksheetOptions->TopRowVisible;
606
                        }
607 1
                        $leftTopCoordinate = Coordinate::stringFromColumnIndex($leftTopColumn) . "$leftTopRow";
608 1
                        $spreadsheet->getActiveSheet()->setTopLeftCell($leftTopCoordinate);
609
                    }
610
611 1
                    $leftTopColumn = $leftTopRow = 1;
612 1
                    if (isset($xmlX->WorksheetOptions->LeftColumnRightPane)) {
613 1
                        $leftTopColumn = 1 + (int) $xmlX->WorksheetOptions->LeftColumnRightPane;
614
                    }
615 1
                    if (isset($xmlX->WorksheetOptions->TopRowBottomPane)) {
616 1
                        $leftTopRow = 1 + (int) $xmlX->WorksheetOptions->TopRowBottomPane;
617
                    }
618 1
                    $leftTopCoordinate = Coordinate::stringFromColumnIndex($leftTopColumn) . "$leftTopRow";
619 1
                    $spreadsheet->getActiveSheet()->setPaneTopLeftCell($leftTopCoordinate);
620
                }
621 33
                (new PageSettings($xmlX))->loadPageSettings($spreadsheet);
622 33
                if (isset($xmlX->WorksheetOptions->TopRowVisible, $xmlX->WorksheetOptions->LeftColumnVisible)) {
623 2
                    $leftTopRow = (string) $xmlX->WorksheetOptions->TopRowVisible;
624 2
                    $leftTopColumn = (string) $xmlX->WorksheetOptions->LeftColumnVisible;
625 2
                    if (is_numeric($leftTopRow) && is_numeric($leftTopColumn)) {
626 2
                        $leftTopCoordinate = Coordinate::stringFromColumnIndex((int) $leftTopColumn + 1) . (string) ($leftTopRow + 1);
627 2
                        $spreadsheet->getActiveSheet()->setTopLeftCell($leftTopCoordinate);
628
                    }
629
                }
630 33
                $rangeCalculated = false;
631 33
                if (isset($xmlX->WorksheetOptions->Panes->Pane->RangeSelection)) {
632 13
                    if (1 === preg_match('/^R(\d+)C(\d+):R(\d+)C(\d+)$/', (string) $xmlX->WorksheetOptions->Panes->Pane->RangeSelection, $selectionMatches)) {
633 13
                        $selectedCell = Coordinate::stringFromColumnIndex((int) $selectionMatches[2])
634 13
                            . $selectionMatches[1]
635 13
                            . ':'
636 13
                            . Coordinate::stringFromColumnIndex((int) $selectionMatches[4])
637 13
                            . $selectionMatches[3];
638 13
                        $spreadsheet->getActiveSheet()->setSelectedCells($selectedCell);
639 13
                        $rangeCalculated = true;
640
                    }
641
                }
642 33
                if (!$rangeCalculated) {
643 32
                    if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveRow)) {
644 24
                        $activeRow = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveRow;
645
                    } else {
646 10
                        $activeRow = 0;
647
                    }
648 32
                    if (isset($xmlX->WorksheetOptions->Panes->Pane->ActiveCol)) {
649 19
                        $activeColumn = (string) $xmlX->WorksheetOptions->Panes->Pane->ActiveCol;
650
                    } else {
651 15
                        $activeColumn = 0;
652
                    }
653 32
                    if (is_numeric($activeRow) && is_numeric($activeColumn)) {
654 32
                        $selectedCell = Coordinate::stringFromColumnIndex((int) $activeColumn + 1) . (string) ($activeRow + 1);
655 32
                        $spreadsheet->getActiveSheet()->setSelectedCells($selectedCell);
656
                    }
657
                }
658
            }
659 38
            if (isset($xmlX->PageBreaks)) {
660 3
                if (isset($xmlX->PageBreaks->ColBreaks)) {
661 3
                    foreach ($xmlX->PageBreaks->ColBreaks->ColBreak as $colBreak) {
662 3
                        $colBreak = (string) $colBreak->Column;
663 3
                        $spreadsheet->getActiveSheet()->setBreak([1 + (int) $colBreak, 1], Worksheet::BREAK_COLUMN);
664
                    }
665
                }
666 3
                if (isset($xmlX->PageBreaks->RowBreaks)) {
667 3
                    foreach ($xmlX->PageBreaks->RowBreaks->RowBreak as $rowBreak) {
668 3
                        $rowBreak = (string) $rowBreak->Row;
669 3
                        $spreadsheet->getActiveSheet()->setBreak([1, (int) $rowBreak], Worksheet::BREAK_ROW);
670
                    }
671
                }
672
            }
673 38
            ++$worksheetID;
674
        }
675
676
        // Globally scoped defined names
677 39
        $activeSheetIndex = 0;
678 39
        if (isset($xml->ExcelWorkbook->ActiveSheet)) {
679 2
            $activeSheetIndex = (int) (string) $xml->ExcelWorkbook->ActiveSheet;
680
        }
681 39
        $activeWorksheet = $spreadsheet->setActiveSheetIndex($activeSheetIndex);
682 38
        if (isset($xml->Names[0])) {
683 10
            foreach ($xml->Names[0] as $definedName) {
684 10
                $definedName_ss = self::getAttributes($definedName, self::NAMESPACES_SS);
685 10
                $name = (string) $definedName_ss['Name'];
686 10
                $definedValue = (string) $definedName_ss['RefersTo'];
687 10
                $convertedValue = AddressHelper::convertFormulaToA1($definedValue);
688 10
                if ($convertedValue[0] === '=') {
689 10
                    $convertedValue = substr($convertedValue, 1);
690
                }
691 10
                $spreadsheet->addDefinedName(DefinedName::createInstance($name, $activeWorksheet, $convertedValue));
692
            }
693
        }
694
695
        // Return
696 38
        return $spreadsheet;
697
    }
698
699 10
    protected function parseCellComment(
700
        SimpleXMLElement $comment,
701
        Spreadsheet $spreadsheet,
702
        string $columnID,
703
        int $rowID
704
    ): void {
705 10
        $commentAttributes = $comment->attributes(self::NAMESPACES_SS);
706 10
        $author = 'unknown';
707 10
        if (isset($commentAttributes->Author)) {
708 9
            $author = (string) $commentAttributes->Author;
709
        }
710
711 10
        $node = $comment->Data->asXML();
712 10
        $annotation = strip_tags((string) $node);
713 10
        $spreadsheet->getActiveSheet()->getComment($columnID . $rowID)
714 10
            ->setAuthor($author)
715 10
            ->setText($this->parseRichText($annotation));
716
    }
717
718 10
    protected function parseRichText(string $annotation): RichText
719
    {
720 10
        $value = new RichText();
721
722 10
        $value->createText($annotation);
723
724 10
        return $value;
725
    }
726
727 41
    private static function getAttributes(?SimpleXMLElement $simple, string $node): SimpleXMLElement
728
    {
729 41
        return ($simple === null)
730
            ? new SimpleXMLElement('<xml></xml>')
731 41
            : ($simple->attributes($node) ?? new SimpleXMLElement('<xml></xml>'));
732
    }
733
}
734