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