Xml::__construct()   A
last analyzed

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