Passed
Push — master ( 3c7afe...11f0b5 )
by
unknown
14:12 queued 14s
created

Xlsx::readTablesInTablesFile()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 28
ccs 16
cts 16
cp 1
rs 9.2222
c 0
b 0
f 0
cc 6
nc 6
nop 8
crap 6

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Reader;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
6
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
7
use PhpOffice\PhpSpreadsheet\Cell\DataType;
8
use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
9
use PhpOffice\PhpSpreadsheet\Comment;
10
use PhpOffice\PhpSpreadsheet\DefinedName;
11
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
12
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\AutoFilter;
13
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Chart;
14
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\ColumnAndRowAttributes;
15
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\ConditionalStyles;
16
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\DataValidations;
17
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Hyperlinks;
18
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
19
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\PageSetup;
20
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Properties as PropertyReader;
21
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SharedFormula;
22
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViewOptions;
23
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViews;
24
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles;
25
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\TableReader;
26
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Theme;
27
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\WorkbookView;
28
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
29
use PhpOffice\PhpSpreadsheet\RichText\RichText;
30
use PhpOffice\PhpSpreadsheet\Shared\Date;
31
use PhpOffice\PhpSpreadsheet\Shared\Drawing;
32
use PhpOffice\PhpSpreadsheet\Shared\File;
33
use PhpOffice\PhpSpreadsheet\Shared\Font;
34
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
35
use PhpOffice\PhpSpreadsheet\Spreadsheet;
36
use PhpOffice\PhpSpreadsheet\Style\Color;
37
use PhpOffice\PhpSpreadsheet\Style\Font as StyleFont;
38
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
39
use PhpOffice\PhpSpreadsheet\Style\Style;
40
use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing;
41
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
42
use SimpleXMLElement;
43
use Throwable;
44
use XMLReader;
45
use ZipArchive;
46
47
class Xlsx extends BaseReader
48
{
49
    const INITIAL_FILE = '_rels/.rels';
50
51
    /**
52
     * ReferenceHelper instance.
53
     */
54
    private ReferenceHelper $referenceHelper;
55
56
    private ZipArchive $zip;
57
58
    private Styles $styleReader;
59
60
    private array $sharedFormulae = [];
61
62
    /**
63
     * Create a new Xlsx Reader instance.
64
     */
65 732
    public function __construct()
66
    {
67 732
        parent::__construct();
68 732
        $this->referenceHelper = ReferenceHelper::getInstance();
69 732
        $this->securityScanner = XmlScanner::getInstance($this);
70
    }
71
72
    /**
73
     * Can the current IReader read the file?
74
     */
75 34
    public function canRead(string $filename): bool
76
    {
77 34
        if (!File::testFileNoThrow($filename, self::INITIAL_FILE)) {
78 14
            return false;
79
        }
80
81 20
        $result = false;
82 20
        $this->zip = $zip = new ZipArchive();
83
84 20
        if ($zip->open($filename) === true) {
85 20
            [$workbookBasename] = $this->getWorkbookBaseName();
86 20
            $result = !empty($workbookBasename);
87
88 20
            $zip->close();
89
        }
90
91 20
        return $result;
92
    }
93
94 708
    public static function testSimpleXml(mixed $value): SimpleXMLElement
95
    {
96 708
        return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><root></root>');
97
    }
98
99 704
    public static function getAttributes(?SimpleXMLElement $value, string $ns = ''): SimpleXMLElement
100
    {
101 704
        return self::testSimpleXml($value === null ? $value : $value->attributes($ns));
102
    }
103
104
    // Phpstan thinks, correctly, that xpath can return false.
105 674
    private static function xpathNoFalse(SimpleXMLElement $sxml, string $path): array
106
    {
107 674
        return self::falseToArray($sxml->xpath($path));
108
    }
109
110 674
    public static function falseToArray(mixed $value): array
111
    {
112 674
        return is_array($value) ? $value : [];
113
    }
114
115 704
    private function loadZip(string $filename, string $ns = '', bool $replaceUnclosedBr = false): SimpleXMLElement
116
    {
117 704
        $contents = $this->getFromZipArchive($this->zip, $filename);
118 704
        if ($replaceUnclosedBr) {
119 37
            $contents = str_replace('<br>', '<br/>', $contents);
120
        }
121 704
        $rels = @simplexml_load_string(
122 704
            $this->getSecurityScannerOrThrow()->scan($contents),
123 704
            'SimpleXMLElement',
124 704
            0,
125 704
            $ns
126 704
        );
127
128 704
        return self::testSimpleXml($rels);
129
    }
130
131
    // This function is just to identify cases where I'm not sure
132
    // why empty namespace is required.
133 672
    private function loadZipNonamespace(string $filename, string $ns): SimpleXMLElement
134
    {
135 672
        $contents = $this->getFromZipArchive($this->zip, $filename);
136 672
        $rels = simplexml_load_string(
137 672
            $this->getSecurityScannerOrThrow()->scan($contents),
138 672
            'SimpleXMLElement',
139 672
            0,
140 672
            ($ns === '' ? $ns : '')
141 672
        );
142
143 667
        return self::testSimpleXml($rels);
144
    }
145
146
    private const REL_TO_MAIN = [
147
        Namespaces::PURL_OFFICE_DOCUMENT => Namespaces::PURL_MAIN,
148
        Namespaces::THUMBNAIL => '',
149
    ];
150
151
    private const REL_TO_DRAWING = [
152
        Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_DRAWING,
153
    ];
154
155
    private const REL_TO_CHART = [
156
        Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_CHART,
157
    ];
158
159
    /**
160
     * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
161
     */
162 18
    public function listWorksheetNames(string $filename): array
163
    {
164 18
        File::assertFile($filename, self::INITIAL_FILE);
165
166 15
        $worksheetNames = [];
167
168 15
        $this->zip = $zip = new ZipArchive();
169 15
        $zip->open($filename);
170
171
        //    The files we're looking at here are small enough that simpleXML is more efficient than XMLReader
172 15
        $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
173 15
        foreach ($rels->Relationship as $relx) {
174 15
            $rel = self::getAttributes($relx);
175 15
            $relType = (string) $rel['Type'];
176 15
            $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
177 15
            if ($mainNS !== '') {
178 15
                $xmlWorkbook = $this->loadZip((string) $rel['Target'], $mainNS);
179
180 15
                if ($xmlWorkbook->sheets) {
181 15
                    foreach ($xmlWorkbook->sheets->sheet as $eleSheet) {
182
                        // Check if sheet should be skipped
183 15
                        $worksheetNames[] = (string) self::getAttributes($eleSheet)['name'];
184
                    }
185
                }
186
            }
187
        }
188
189 15
        $zip->close();
190
191 15
        return $worksheetNames;
192
    }
193
194
    /**
195
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
196
     *
197
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
198
     */
199 19
    public function listWorksheetInfo(string $filename): array
200
    {
201 19
        File::assertFile($filename, self::INITIAL_FILE);
202
203 16
        $worksheetInfo = [];
204
205 16
        $this->zip = $zip = new ZipArchive();
206 16
        $zip->open($filename);
207
208 16
        $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
209 16
        foreach ($rels->Relationship as $relx) {
210 16
            $rel = self::getAttributes($relx);
211 16
            $relType = (string) $rel['Type'];
212 16
            $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
213 16
            if ($mainNS !== '') {
214 16
                $relTarget = (string) $rel['Target'];
215 16
                $dir = dirname($relTarget);
216 16
                $namespace = dirname($relType);
217 16
                $relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', Namespaces::RELATIONSHIPS);
218
219 16
                $worksheets = [];
220 16
                foreach ($relsWorkbook->Relationship as $elex) {
221 16
                    $ele = self::getAttributes($elex);
222
                    if (
223 16
                        ((string) $ele['Type'] === "$namespace/worksheet")
224 16
                        || ((string) $ele['Type'] === "$namespace/chartsheet")
225
                    ) {
226 16
                        $worksheets[(string) $ele['Id']] = $ele['Target'];
227
                    }
228
                }
229
230 16
                $xmlWorkbook = $this->loadZip($relTarget, $mainNS);
231 16
                if ($xmlWorkbook->sheets) {
232 16
                    $dir = dirname($relTarget);
233
234 16
                    foreach ($xmlWorkbook->sheets->sheet as $eleSheet) {
235 16
                        $tmpInfo = [
236 16
                            'worksheetName' => (string) self::getAttributes($eleSheet)['name'],
237 16
                            'lastColumnLetter' => 'A',
238 16
                            'lastColumnIndex' => 0,
239 16
                            'totalRows' => 0,
240 16
                            'totalColumns' => 0,
241 16
                        ];
242 16
                        $sheetState = (string) (self::getAttributes($eleSheet)['state'] ?? Worksheet::SHEETSTATE_VISIBLE);
243 16
                        $tmpInfo['sheetState'] = $sheetState;
244
245 16
                        $fileWorksheet = (string) $worksheets[self::getArrayItemString(self::getAttributes($eleSheet, $namespace), 'id')];
246 16
                        $fileWorksheetPath = str_starts_with($fileWorksheet, '/') ? substr($fileWorksheet, 1) : "$dir/$fileWorksheet";
247
248 16
                        $xml = new XMLReader();
249 16
                        $xml->xml(
250 16
                            $this->getSecurityScannerOrThrow()
251 16
                                ->scan(
252 16
                                    $this->getFromZipArchive(
253 16
                                        $this->zip,
254 16
                                        $fileWorksheetPath
255 16
                                    )
256 16
                                )
257 16
                        );
258 16
                        $xml->setParserProperty(2, true);
259
260 16
                        $currCells = 0;
261 16
                        while ($xml->read()) {
262 16
                            if ($xml->localName == 'row' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) {
263 16
                                $row = (int) $xml->getAttribute('r');
264 16
                                $tmpInfo['totalRows'] = $row;
265 16
                                $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
266 16
                                $currCells = 0;
267 16
                            } elseif ($xml->localName == 'c' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) {
268 16
                                $cell = $xml->getAttribute('r');
269 16
                                $currCells = $cell ? max($currCells, Coordinate::indexesFromString($cell)[0]) : ($currCells + 1);
270
                            }
271
                        }
272 16
                        $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
273 16
                        $xml->close();
274
275 16
                        $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
276 16
                        $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
277
278 16
                        $worksheetInfo[] = $tmpInfo;
279
                    }
280
                }
281
            }
282
        }
283
284 16
        $zip->close();
285
286 16
        return $worksheetInfo;
287
    }
288
289 22
    private static function castToBoolean(SimpleXMLElement $c): bool
290
    {
291 22
        $value = isset($c->v) ? (string) $c->v : null;
292 22
        if ($value == '0') {
293 15
            return false;
294 18
        } elseif ($value == '1') {
295 18
            return true;
296
        }
297
298
        return (bool) $c->v;
299
    }
300
301 188
    private static function castToError(?SimpleXMLElement $c): ?string
302
    {
303 188
        return isset($c, $c->v) ? (string) $c->v : null;
304
    }
305
306 532
    private static function castToString(?SimpleXMLElement $c): ?string
307
    {
308 532
        return isset($c, $c->v) ? (string) $c->v : null;
309
    }
310
311 372
    public static function replacePrefixes(string $formula): string
312
    {
313 372
        return str_replace(['_xlfn.', '_xlws.'], '', $formula);
314
    }
315
316 362
    private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, mixed &$value, mixed &$calculatedValue, string $castBaseType, bool $updateSharedCells = true): void
317
    {
318 362
        if ($c === null) {
319
            return;
320
        }
321 362
        $attr = $c->f->attributes();
322 362
        $cellDataType = DataType::TYPE_FORMULA;
323 362
        $formula = self::replacePrefixes((string) $c->f);
324 362
        $value = "=$formula";
325 362
        $calculatedValue = self::$castBaseType($c);
326
327
        // Shared formula?
328 362
        if (isset($attr['t']) && strtolower((string) $attr['t']) == 'shared') {
329 219
            $instance = (string) $attr['si'];
330
331 219
            if (!isset($this->sharedFormulae[(string) $attr['si']])) {
332 219
                $this->sharedFormulae[$instance] = new SharedFormula($r, $value);
333 218
            } elseif ($updateSharedCells === true) {
334
                // It's only worth the overhead of adjusting the shared formula for this cell if we're actually loading
335
                //     the cell, which may not be the case if we're using a read filter.
336 218
                $master = Coordinate::indexesFromString($this->sharedFormulae[$instance]->master());
337 218
                $current = Coordinate::indexesFromString($r);
338
339 218
                $difference = [0, 0];
340 218
                $difference[0] = $current[0] - $master[0];
341 218
                $difference[1] = $current[1] - $master[1];
342
343 218
                $value = $this->referenceHelper->updateFormulaReferences($this->sharedFormulae[$instance]->formula(), 'A1', $difference[0], $difference[1]);
344
            }
345
        }
346
    }
347
348 659
    private function fileExistsInArchive(ZipArchive $archive, string $fileName = ''): bool
349
    {
350
        // Root-relative paths
351 659
        if (str_contains($fileName, '//')) {
352 1
            $fileName = substr($fileName, strpos($fileName, '//') + 1);
353
        }
354 659
        $fileName = File::realpath($fileName);
355
356
        // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming
357
        //    so we need to load case-insensitively from the zip file
358
359
        // Apache POI fixes
360 659
        $contents = $archive->locateName($fileName, ZipArchive::FL_NOCASE);
361 659
        if ($contents === false) {
362 4
            $contents = $archive->locateName(substr($fileName, 1), ZipArchive::FL_NOCASE);
363
        }
364
365 659
        return $contents !== false;
366
    }
367
368 704
    private function getFromZipArchive(ZipArchive $archive, string $fileName = ''): string
369
    {
370
        // Root-relative paths
371 704
        if (str_contains($fileName, '//')) {
372 2
            $fileName = substr($fileName, strpos($fileName, '//') + 1);
373
        }
374
        // Relative paths generated by dirname($filename) when $filename
375
        // has no path (i.e.files in root of the zip archive)
376 704
        $fileName = (string) preg_replace('/^\.\//', '', $fileName);
377 704
        $fileName = File::realpath($fileName);
378
379
        // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming
380
        //    so we need to load case-insensitively from the zip file
381
382 704
        $contents = $archive->getFromName($fileName, 0, ZipArchive::FL_NOCASE);
383
384
        // Apache POI fixes
385 704
        if ($contents === false) {
386 52
            $contents = $archive->getFromName(substr($fileName, 1), 0, ZipArchive::FL_NOCASE);
387
        }
388
389
        // Has the file been saved with Windoze directory separators rather than unix?
390 704
        if ($contents === false) {
391 49
            $contents = $archive->getFromName(str_replace('/', '\\', $fileName), 0, ZipArchive::FL_NOCASE);
392
        }
393
394 704
        return ($contents === false) ? '' : $contents;
395
    }
396
397
    /**
398
     * Loads Spreadsheet from file.
399
     */
400 677
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
401
    {
402 677
        File::assertFile($filename, self::INITIAL_FILE);
403
404
        // Initialisations
405 674
        $excel = $this->newSpreadsheet();
406 674
        $excel->setValueBinder($this->valueBinder);
407 674
        $excel->removeSheetByIndex(0);
408 674
        $addingFirstCellStyleXf = true;
409 674
        $addingFirstCellXf = true;
410
411 674
        $unparsedLoadedData = [];
412
413 674
        $this->zip = $zip = new ZipArchive();
414 674
        $zip->open($filename);
415
416
        //    Read the theme first, because we need the colour scheme when reading the styles
417 674
        [$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName();
418 674
        $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML;
419 674
        $chartNS = self::REL_TO_CHART[$xmlNamespaceBase] ?? Namespaces::CHART;
420 674
        $wbRels = $this->loadZip("xl/_rels/{$workbookBasename}.rels", Namespaces::RELATIONSHIPS);
421 674
        $theme = null;
422 674
        $this->styleReader = new Styles();
423 674
        foreach ($wbRels->Relationship as $relx) {
424 673
            $rel = self::getAttributes($relx);
425 673
            $relTarget = (string) $rel['Target'];
426 673
            if (str_starts_with($relTarget, '/xl/')) {
427 12
                $relTarget = substr($relTarget, 4);
428
            }
429 673
            switch ($rel['Type']) {
430 673
                case "$xmlNamespaceBase/theme":
431 657
                    if (!$this->fileExistsInArchive($zip, "xl/{$relTarget}")) {
432 3
                        break; // issue3770
433
                    }
434 654
                    $themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2'];
435 654
                    $themeOrderAdditional = count($themeOrderArray);
436
437 654
                    $xmlTheme = $this->loadZip("xl/{$relTarget}", $drawingNS);
438 654
                    $xmlThemeName = self::getAttributes($xmlTheme);
439 654
                    $xmlTheme = $xmlTheme->children($drawingNS);
440 654
                    $themeName = (string) $xmlThemeName['name'];
441
442 654
                    $colourScheme = self::getAttributes($xmlTheme->themeElements->clrScheme);
443 654
                    $colourSchemeName = (string) $colourScheme['name'];
444 654
                    $excel->getTheme()->setThemeColorName($colourSchemeName);
445 654
                    $colourScheme = $xmlTheme->themeElements->clrScheme->children($drawingNS);
446
447 654
                    $themeColours = [];
448 654
                    foreach ($colourScheme as $k => $xmlColour) {
449 654
                        $themePos = array_search($k, $themeOrderArray);
450 654
                        if ($themePos === false) {
451 654
                            $themePos = $themeOrderAdditional++;
452
                        }
453 654
                        if (isset($xmlColour->sysClr)) {
454 636
                            $xmlColourData = self::getAttributes($xmlColour->sysClr);
455 636
                            $themeColours[$themePos] = (string) $xmlColourData['lastClr'];
456 636
                            $excel->getTheme()->setThemeColor($k, (string) $xmlColourData['lastClr']);
457 654
                        } elseif (isset($xmlColour->srgbClr)) {
458 654
                            $xmlColourData = self::getAttributes($xmlColour->srgbClr);
459 654
                            $themeColours[$themePos] = (string) $xmlColourData['val'];
460 654
                            $excel->getTheme()->setThemeColor($k, (string) $xmlColourData['val']);
461
                        }
462
                    }
463 654
                    $theme = new Theme($themeName, $colourSchemeName, $themeColours);
464 654
                    $this->styleReader->setTheme($theme);
465
466 654
                    $fontScheme = self::getAttributes($xmlTheme->themeElements->fontScheme);
467 654
                    $fontSchemeName = (string) $fontScheme['name'];
468 654
                    $excel->getTheme()->setThemeFontName($fontSchemeName);
469 654
                    $majorFonts = [];
470 654
                    $minorFonts = [];
471 654
                    $fontScheme = $xmlTheme->themeElements->fontScheme->children($drawingNS);
472 654
                    $majorLatin = self::getAttributes($fontScheme->majorFont->latin)['typeface'] ?? '';
473 654
                    $majorEastAsian = self::getAttributes($fontScheme->majorFont->ea)['typeface'] ?? '';
474 654
                    $majorComplexScript = self::getAttributes($fontScheme->majorFont->cs)['typeface'] ?? '';
475 654
                    $minorLatin = self::getAttributes($fontScheme->minorFont->latin)['typeface'] ?? '';
476 654
                    $minorEastAsian = self::getAttributes($fontScheme->minorFont->ea)['typeface'] ?? '';
477 654
                    $minorComplexScript = self::getAttributes($fontScheme->minorFont->cs)['typeface'] ?? '';
478
479 654
                    foreach ($fontScheme->majorFont->font as $xmlFont) {
480 634
                        $fontAttributes = self::getAttributes($xmlFont);
481 634
                        $script = (string) ($fontAttributes['script'] ?? '');
482 634
                        if (!empty($script)) {
483 634
                            $majorFonts[$script] = (string) ($fontAttributes['typeface'] ?? '');
484
                        }
485
                    }
486 654
                    foreach ($fontScheme->minorFont->font as $xmlFont) {
487 634
                        $fontAttributes = self::getAttributes($xmlFont);
488 634
                        $script = (string) ($fontAttributes['script'] ?? '');
489 634
                        if (!empty($script)) {
490 634
                            $minorFonts[$script] = (string) ($fontAttributes['typeface'] ?? '');
491
                        }
492
                    }
493 654
                    $excel->getTheme()->setMajorFontValues($majorLatin, $majorEastAsian, $majorComplexScript, $majorFonts);
494 654
                    $excel->getTheme()->setMinorFontValues($minorLatin, $minorEastAsian, $minorComplexScript, $minorFonts);
495
496 654
                    break;
497
            }
498
        }
499
500 674
        $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
501
502 674
        $propertyReader = new PropertyReader($this->getSecurityScannerOrThrow(), $excel->getProperties());
503 674
        $charts = $chartDetails = [];
504 674
        foreach ($rels->Relationship as $relx) {
505 674
            $rel = self::getAttributes($relx);
506 674
            $relTarget = (string) $rel['Target'];
507
            // issue 3553
508 674
            if ($relTarget[0] === '/') {
509 7
                $relTarget = substr($relTarget, 1);
510
            }
511 674
            $relType = (string) $rel['Type'];
512 674
            $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
513
            switch ($relType) {
514 667
                case Namespaces::CORE_PROPERTIES:
515 655
                    $propertyReader->readCoreProperties($this->getFromZipArchive($zip, $relTarget));
516
517 655
                    break;
518 674
                case "$xmlNamespaceBase/extended-properties":
519 646
                    $propertyReader->readExtendedProperties($this->getFromZipArchive($zip, $relTarget));
520
521 646
                    break;
522 674
                case "$xmlNamespaceBase/custom-properties":
523 57
                    $propertyReader->readCustomProperties($this->getFromZipArchive($zip, $relTarget));
524
525 57
                    break;
526
                    //Ribbon
527 674
                case Namespaces::EXTENSIBILITY:
528 2
                    $customUI = $relTarget;
529 2
                    if ($customUI) {
530 2
                        $this->readRibbon($excel, $customUI, $zip);
531
                    }
532
533 2
                    break;
534 674
                case "$xmlNamespaceBase/officeDocument":
535 674
                    $dir = dirname($relTarget);
536
537
                    // Do not specify namespace in next stmt - do it in Xpath
538 674
                    $relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', Namespaces::RELATIONSHIPS);
539 674
                    $relsWorkbook->registerXPathNamespace('rel', Namespaces::RELATIONSHIPS);
540
541 674
                    $worksheets = [];
542 674
                    $macros = $customUI = null;
543 674
                    foreach ($relsWorkbook->Relationship as $elex) {
544 674
                        $ele = self::getAttributes($elex);
545 674
                        switch ($ele['Type']) {
546 667
                            case Namespaces::WORKSHEET:
547 667
                            case Namespaces::PURL_WORKSHEET:
548 674
                                $worksheets[(string) $ele['Id']] = $ele['Target'];
549
550 674
                                break;
551 667
                            case Namespaces::CHARTSHEET:
552 2
                                if ($this->includeCharts === true) {
553 1
                                    $worksheets[(string) $ele['Id']] = $ele['Target'];
554
                                }
555
556 2
                                break;
557
                                // a vbaProject ? (: some macros)
558 667
                            case Namespaces::VBA:
559 3
                                $macros = $ele['Target'];
560
561 3
                                break;
562
                        }
563
                    }
564
565 674
                    if ($macros !== null) {
566 3
                        $macrosCode = $this->getFromZipArchive($zip, 'xl/vbaProject.bin'); //vbaProject.bin always in 'xl' dir and always named vbaProject.bin
567 3
                        if (!empty($macrosCode)) {
568 3
                            $excel->setMacrosCode($macrosCode);
569 3
                            $excel->setHasMacros(true);
570
                            //short-circuit : not reading vbaProject.bin.rel to get Signature =>allways vbaProjectSignature.bin in 'xl' dir
571 3
                            $Certificate = $this->getFromZipArchive($zip, 'xl/vbaProjectSignature.bin');
572 3
                            $excel->setMacrosCertificate($Certificate);
573
                        }
574
                    }
575
576 674
                    $relType = "rel:Relationship[@Type='"
577 674
                        . "$xmlNamespaceBase/styles"
578 674
                        . "']";
579
                    /** @var ?SimpleXMLElement */
580 674
                    $xpath = self::getArrayItem(self::xpathNoFalse($relsWorkbook, $relType));
581
582 674
                    if ($xpath === null) {
583 1
                        $xmlStyles = self::testSimpleXml(null);
584
                    } else {
585 674
                        $stylesTarget = (string) $xpath['Target'];
586 674
                        $stylesTarget = str_starts_with($stylesTarget, '/') ? substr($stylesTarget, 1) : "$dir/$stylesTarget";
587 674
                        $xmlStyles = $this->loadZip($stylesTarget, $mainNS);
588
                    }
589
590 674
                    $palette = self::extractPalette($xmlStyles);
591 674
                    $this->styleReader->setWorkbookPalette($palette);
592 674
                    $fills = self::extractStyles($xmlStyles, 'fills', 'fill');
593 674
                    $fonts = self::extractStyles($xmlStyles, 'fonts', 'font');
594 674
                    $borders = self::extractStyles($xmlStyles, 'borders', 'border');
595 674
                    $xfTags = self::extractStyles($xmlStyles, 'cellXfs', 'xf');
596 674
                    $cellXfTags = self::extractStyles($xmlStyles, 'cellStyleXfs', 'xf');
597
598 674
                    $styles = [];
599 674
                    $cellStyles = [];
600 674
                    $numFmts = null;
601 674
                    if (/*$xmlStyles && */ $xmlStyles->numFmts[0]) {
602 247
                        $numFmts = $xmlStyles->numFmts[0];
603
                    }
604 674
                    if (isset($numFmts)) {
605 247
                        $numFmts->registerXPathNamespace('sml', $mainNS);
606
                    }
607 674
                    $this->styleReader->setNamespace($mainNS);
608 674
                    if (!$this->readDataOnly/* && $xmlStyles*/) {
609 672
                        foreach ($xfTags as $xfTag) {
610 672
                            $xf = self::getAttributes($xfTag);
611 672
                            $numFmt = null;
612
613 672
                            if ($xf['numFmtId']) {
614 670
                                if (isset($numFmts)) {
615
                                    /** @var ?SimpleXMLElement */
616 247
                                    $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]"));
617
618 247
                                    if (isset($tmpNumFmt['formatCode'])) {
619 246
                                        $numFmt = (string) $tmpNumFmt['formatCode'];
620
                                    }
621
                                }
622
623
                                // We shouldn't override any of the built-in MS Excel values (values below id 164)
624
                                //  But there's a lot of naughty homebrew xlsx writers that do use "reserved" id values that aren't actually used
625
                                //  So we make allowance for them rather than lose formatting masks
626
                                if (
627 670
                                    $numFmt === null
628 670
                                    && (int) $xf['numFmtId'] < 164
629 670
                                    && NumberFormat::builtInFormatCode((int) $xf['numFmtId']) !== ''
630
                                ) {
631 659
                                    $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
632
                                }
633
                            }
634 672
                            $quotePrefix = (bool) (string) ($xf['quotePrefix'] ?? '');
635
636 672
                            $style = (object) [
637 672
                                'numFmt' => $numFmt ?? NumberFormat::FORMAT_GENERAL,
638 672
                                'font' => $fonts[(int) ($xf['fontId'])],
639 672
                                'fill' => $fills[(int) ($xf['fillId'])],
640 672
                                'border' => $borders[(int) ($xf['borderId'])],
641 672
                                'alignment' => $xfTag->alignment,
642 672
                                'protection' => $xfTag->protection,
643 672
                                'quotePrefix' => $quotePrefix,
644 672
                            ];
645 672
                            $styles[] = $style;
646
647
                            // add style to cellXf collection
648 672
                            $objStyle = new Style();
649 672
                            $this->styleReader->readStyle($objStyle, $style);
650 672
                            if ($addingFirstCellXf) {
651 672
                                $excel->removeCellXfByIndex(0); // remove the default style
652 672
                                $addingFirstCellXf = false;
653
                            }
654 672
                            $excel->addCellXf($objStyle);
655
                        }
656
657 672
                        foreach ($cellXfTags as $xfTag) {
658 671
                            $xf = self::getAttributes($xfTag);
659 671
                            $numFmt = NumberFormat::FORMAT_GENERAL;
660 671
                            if ($numFmts && $xf['numFmtId']) {
661
                                /** @var ?SimpleXMLElement */
662 247
                                $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]"));
663 247
                                if (isset($tmpNumFmt['formatCode'])) {
664 29
                                    $numFmt = (string) $tmpNumFmt['formatCode'];
665 245
                                } elseif ((int) $xf['numFmtId'] < 165) {
666 245
                                    $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
667
                                }
668
                            }
669
670 671
                            $quotePrefix = (bool) (string) ($xf['quotePrefix'] ?? '');
671
672 671
                            $cellStyle = (object) [
673 671
                                'numFmt' => $numFmt,
674 671
                                'font' => $fonts[(int) ($xf['fontId'])],
675 671
                                'fill' => $fills[((int) $xf['fillId'])],
676 671
                                'border' => $borders[(int) ($xf['borderId'])],
677 671
                                'alignment' => $xfTag->alignment,
678 671
                                'protection' => $xfTag->protection,
679 671
                                'quotePrefix' => $quotePrefix,
680 671
                            ];
681 671
                            $cellStyles[] = $cellStyle;
682
683
                            // add style to cellStyleXf collection
684 671
                            $objStyle = new Style();
685 671
                            $this->styleReader->readStyle($objStyle, $cellStyle);
686 671
                            if ($addingFirstCellStyleXf) {
687 671
                                $excel->removeCellStyleXfByIndex(0); // remove the default style
688 671
                                $addingFirstCellStyleXf = false;
689
                            }
690 671
                            $excel->addCellStyleXf($objStyle);
691
                        }
692
                    }
693 674
                    $this->styleReader->setStyleXml($xmlStyles);
694 674
                    $this->styleReader->setNamespace($mainNS);
695 674
                    $this->styleReader->setStyleBaseData($theme, $styles, $cellStyles);
696 674
                    $dxfs = $this->styleReader->dxfs($this->readDataOnly);
697 674
                    $tableStyles = $this->styleReader->tableStyles($this->readDataOnly);
698 674
                    $styles = $this->styleReader->styles();
699
700
                    // Read content after setting the styles
701 674
                    $sharedStrings = [];
702 674
                    $relType = "rel:Relationship[@Type='"
703 674
                        //. Namespaces::SHARED_STRINGS
704 674
                        . "$xmlNamespaceBase/sharedStrings"
705 674
                        . "']";
706
                    /** @var ?SimpleXMLElement */
707 674
                    $xpath = self::getArrayItem($relsWorkbook->xpath($relType));
708
709 674
                    if ($xpath) {
710 617
                        $sharedStringsTarget = (string) $xpath['Target'];
711 617
                        $sharedStringsTarget = str_starts_with($sharedStringsTarget, '/') ? substr($sharedStringsTarget, 1) : "$dir/$sharedStringsTarget";
712 617
                        $xmlStrings = $this->loadZip($sharedStringsTarget, $mainNS);
713 615
                        if (isset($xmlStrings->si)) {
714 495
                            foreach ($xmlStrings->si as $val) {
715 495
                                if (isset($val->t)) {
716 492
                                    $sharedStrings[] = StringHelper::controlCharacterOOXML2PHP((string) $val->t);
717 39
                                } elseif (isset($val->r)) {
718 39
                                    $sharedStrings[] = $this->parseRichText($val);
719
                                } else {
720 1
                                    $sharedStrings[] = '';
721
                                }
722
                            }
723
                        }
724
                    }
725
726 672
                    $xmlWorkbook = $this->loadZipNoNamespace($relTarget, $mainNS);
727 667
                    $xmlWorkbookNS = $this->loadZip($relTarget, $mainNS);
728
729
                    // Set base date
730 667
                    $excel->setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
731 667
                    if ($xmlWorkbookNS->workbookPr) {
732 658
                        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
733 658
                        $attrs1904 = self::getAttributes($xmlWorkbookNS->workbookPr);
734 658
                        if (isset($attrs1904['date1904'])) {
735 14
                            if (self::boolean((string) $attrs1904['date1904'])) {
736 3
                                Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
737 3
                                $excel->setExcelCalendar(Date::CALENDAR_MAC_1904);
738
                            }
739
                        }
740
                    }
741
742
                    // Set protection
743 667
                    $this->readProtection($excel, $xmlWorkbook);
744
745 667
                    $sheetId = 0; // keep track of new sheet id in final workbook
746 667
                    $oldSheetId = -1; // keep track of old sheet id in final workbook
747 667
                    $countSkippedSheets = 0; // keep track of number of skipped sheets
748 667
                    $mapSheetId = []; // mapping of sheet ids from old to new
749
750 667
                    $charts = $chartDetails = [];
751
752 667
                    if ($xmlWorkbookNS->sheets) {
753 667
                        foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) {
754 667
                            $eleSheetAttr = self::getAttributes($eleSheet);
755 667
                            ++$oldSheetId;
756
757
                            // Check if sheet should be skipped
758 667
                            if (is_array($this->loadSheetsOnly) && !in_array((string) $eleSheetAttr['name'], $this->loadSheetsOnly)) {
759 6
                                ++$countSkippedSheets;
760 6
                                $mapSheetId[$oldSheetId] = null;
761
762 6
                                continue;
763
                            }
764
765 666
                            $sheetReferenceId = self::getArrayItemString(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id');
766 666
                            if (isset($worksheets[$sheetReferenceId]) === false) {
767 1
                                ++$countSkippedSheets;
768 1
                                $mapSheetId[$oldSheetId] = null;
769
770 1
                                continue;
771
                            }
772
                            // Map old sheet id in original workbook to new sheet id.
773
                            // They will differ if loadSheetsOnly() is being used
774 666
                            $mapSheetId[$oldSheetId] = $oldSheetId - $countSkippedSheets;
775
776
                            // Load sheet
777 666
                            $docSheet = $excel->createSheet();
778
                            //    Use false for $updateFormulaCellReferences to prevent adjustment of worksheet
779
                            //        references in formula cells... during the load, all formulae should be correct,
780
                            //        and we're simply bringing the worksheet name in line with the formula, not the
781
                            //        reverse
782 666
                            $docSheet->setTitle((string) $eleSheetAttr['name'], false, false);
783
784 666
                            $fileWorksheet = (string) $worksheets[$sheetReferenceId];
785
                            // issue 3665 adds test for /.
786
                            // This broke XlsxRootZipFilesTest,
787
                            //  but Excel reports an error with that file.
788
                            //  Testing dir for . avoids this problem.
789
                            //  It might be better just to drop the test.
790 666
                            if ($fileWorksheet[0] == '/' && $dir !== '.') {
791 12
                                $fileWorksheet = substr($fileWorksheet, strlen($dir) + 2);
792
                            }
793 666
                            $xmlSheet = $this->loadZipNoNamespace("$dir/$fileWorksheet", $mainNS);
794 666
                            $xmlSheetNS = $this->loadZip("$dir/$fileWorksheet", $mainNS);
795
796
                            // Shared Formula table is unique to each Worksheet, so we need to reset it here
797 666
                            $this->sharedFormulae = [];
798
799 666
                            if (isset($eleSheetAttr['state']) && (string) $eleSheetAttr['state'] != '') {
800 38
                                $docSheet->setSheetState((string) $eleSheetAttr['state']);
801
                            }
802 666
                            if ($xmlSheetNS) {
803 666
                                $xmlSheetMain = $xmlSheetNS->children($mainNS);
804
                                // Setting Conditional Styles adjusts selected cells, so we need to execute this
805
                                //    before reading the sheet view data to get the actual selected cells
806 666
                                if (!$this->readDataOnly && ($xmlSheet->conditionalFormatting)) {
807 221
                                    (new ConditionalStyles($docSheet, $xmlSheet, $dxfs, $this->styleReader))->load();
808
                                }
809 666
                                if (!$this->readDataOnly && $xmlSheet->extLst) {
810 198
                                    (new ConditionalStyles($docSheet, $xmlSheet, $dxfs, $this->styleReader))->loadFromExt();
811
                                }
812 666
                                if (isset($xmlSheetMain->sheetViews, $xmlSheetMain->sheetViews->sheetView)) {
813 663
                                    $sheetViews = new SheetViews($xmlSheetMain->sheetViews->sheetView, $docSheet);
814 663
                                    $sheetViews->load();
815
                                }
816
817 666
                                $sheetViewOptions = new SheetViewOptions($docSheet, $xmlSheetNS);
818 666
                                $sheetViewOptions->load($this->readDataOnly, $this->styleReader);
819
820 666
                                (new ColumnAndRowAttributes($docSheet, $xmlSheetNS))
821 666
                                    ->load($this->getReadFilter(), $this->readDataOnly, $this->ignoreRowsWithNoCells);
822
                            }
823
824 666
                            $holdSelectedCells = $docSheet->getSelectedCells();
825 666
                            if ($xmlSheetNS && $xmlSheetNS->sheetData && $xmlSheetNS->sheetData->row) {
826 624
                                $cIndex = 1; // Cell Start from 1
827 624
                                foreach ($xmlSheetNS->sheetData->row as $row) {
828 624
                                    $rowIndex = 1;
829 624
                                    foreach ($row->c as $c) {
830 623
                                        $cAttr = self::getAttributes($c);
831 623
                                        $r = (string) $cAttr['r'];
832 623
                                        if ($r == '') {
833 2
                                            $r = Coordinate::stringFromColumnIndex($rowIndex) . $cIndex;
834
                                        }
835 623
                                        $cellDataType = (string) $cAttr['t'];
836 623
                                        $originalCellDataTypeNumeric = $cellDataType === '';
837 623
                                        $value = null;
838 623
                                        $calculatedValue = null;
839
840
                                        // Read cell?
841 623
                                        $coordinates = Coordinate::coordinateFromString($r);
842
843 623
                                        if (!$this->getReadFilter()->readCell($coordinates[0], (int) $coordinates[1], $docSheet->getTitle())) {
844
                                            // Normally, just testing for the f attribute should identify this cell as containing a formula
845
                                            // that we need to read, even though it is outside of the filter range, in case it is a shared formula.
846
                                            // But in some cases, this attribute isn't set; so we need to delve a level deeper and look at
847
                                            // whether or not the cell has a child formula element that is shared.
848 4
                                            if (isset($cAttr->f) || (isset($c->f, $c->f->attributes()['t']) && strtolower((string) $c->f->attributes()['t']) === 'shared')) {
849
                                                $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError', false);
850
                                            }
851 4
                                            ++$rowIndex;
852
853 4
                                            continue;
854
                                        }
855
856
                                        // Read cell!
857 623
                                        $useFormula = isset($c->f)
858 623
                                            && ((string) $c->f !== '' || (isset($c->f->attributes()['t']) && strtolower((string) $c->f->attributes()['t']) === 'shared'));
859
                                        switch ($cellDataType) {
860 22
                                            case DataType::TYPE_STRING:
861 494
                                                if ((string) $c->v != '') {
862 494
                                                    $value = $sharedStrings[(int) ($c->v)];
863
864 494
                                                    if ($value instanceof RichText) {
865 36
                                                        $value = clone $value;
866
                                                    }
867
                                                } else {
868 16
                                                    $value = '';
869
                                                }
870
871 494
                                                break;
872 18
                                            case DataType::TYPE_BOOL:
873 22
                                                if (!$useFormula) {
874 16
                                                    if (isset($c->v)) {
875 16
                                                        $value = self::castToBoolean($c);
876
                                                    } else {
877 1
                                                        $value = null;
878 1
                                                        $cellDataType = DataType::TYPE_NULL;
879
                                                    }
880
                                                } else {
881
                                                    // Formula
882 6
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToBoolean');
883 6
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
884
                                                }
885
886 22
                                                break;
887 18
                                            case DataType::TYPE_STRING2:
888 222
                                                if ($useFormula) {
889 220
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
890 220
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
891
                                                } else {
892 3
                                                    $value = self::castToString($c);
893
                                                }
894
895 222
                                                break;
896 18
                                            case DataType::TYPE_INLINE:
897 13
                                                if ($useFormula) {
898
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError');
899
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
900
                                                } else {
901 13
                                                    $value = $this->parseRichText($c->is);
902
                                                }
903
904 13
                                                break;
905 18
                                            case DataType::TYPE_ERROR:
906 188
                                                if (!$useFormula) {
907
                                                    $value = self::castToError($c);
908
                                                } else {
909
                                                    // Formula
910 188
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError');
911 188
                                                    $eattr = $c->attributes();
912 188
                                                    if (isset($eattr['vm'])) {
913 1
                                                        if ($calculatedValue === ExcelError::VALUE()) {
914 1
                                                            $calculatedValue = ExcelError::SPILL();
915
                                                        }
916
                                                    }
917
                                                }
918
919 188
                                                break;
920
                                            default:
921 522
                                                if (!$useFormula) {
922 516
                                                    $value = self::castToString($c);
923 516
                                                    if (is_numeric($value)) {
924 494
                                                        $value += 0;
925 494
                                                        $cellDataType = DataType::TYPE_NUMERIC;
926
                                                    }
927
                                                } else {
928
                                                    // Formula
929 341
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
930 341
                                                    if (is_numeric($calculatedValue)) {
931 338
                                                        $calculatedValue += 0;
932
                                                    }
933 341
                                                    self::storeFormulaAttributes($c->f, $docSheet, $r);
934
                                                }
935
936 522
                                                break;
937
                                        }
938
939
                                        // read empty cells or the cells are not empty
940 623
                                        if ($this->readEmptyCells || ($value !== null && $value !== '')) {
941
                                            // Rich text?
942 623
                                            if ($value instanceof RichText && $this->readDataOnly) {
943
                                                $value = $value->getPlainText();
944
                                            }
945
946 623
                                            $cell = $docSheet->getCell($r);
947
                                            // Assign value
948 623
                                            if ($cellDataType != '') {
949
                                                // it is possible, that datatype is numeric but with an empty string, which result in an error
950 616
                                                if ($cellDataType === DataType::TYPE_NUMERIC && ($value === '' || $value === null)) {
951 1
                                                    $cellDataType = DataType::TYPE_NULL;
952
                                                }
953 616
                                                if ($cellDataType !== DataType::TYPE_NULL) {
954 616
                                                    $cell->setValueExplicit($value, $cellDataType);
955
                                                }
956
                                            } else {
957 293
                                                $cell->setValue($value);
958
                                            }
959 623
                                            if ($calculatedValue !== null) {
960 353
                                                $cell->setCalculatedValue($calculatedValue, $originalCellDataTypeNumeric);
961
                                            }
962
963
                                            // Style information?
964 623
                                            if (!$this->readDataOnly) {
965 621
                                                $cAttrS = (int) ($cAttr['s'] ?? 0);
966
                                                // no style index means 0, it seems
967 621
                                                $cAttrS = isset($styles[$cAttrS]) ? $cAttrS : 0;
968 621
                                                $cell->setXfIndex($cAttrS);
969
                                                // issue 3495
970 621
                                                if ($cellDataType === DataType::TYPE_FORMULA && $styles[$cAttrS]->quotePrefix === true) {
971 2
                                                    $holdSelected = $docSheet->getSelectedCells();
972 2
                                                    $cell->getStyle()->setQuotePrefix(false);
973 2
                                                    $docSheet->setSelectedCells($holdSelected);
974
                                                }
975
                                            }
976
                                        }
977 623
                                        ++$rowIndex;
978
                                    }
979 624
                                    ++$cIndex;
980
                                }
981
                            }
982 666
                            $docSheet->setSelectedCells($holdSelectedCells);
983 666
                            if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->ignoredErrors) {
984 4
                                foreach ($xmlSheetNS->ignoredErrors->ignoredError as $ignoredError) {
985 4
                                    $this->processIgnoredErrors($ignoredError, $docSheet);
986
                                }
987
                            }
988
989 666
                            if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->sheetProtection) {
990 68
                                $protAttr = $xmlSheetNS->sheetProtection->attributes() ?? [];
991 68
                                foreach ($protAttr as $key => $value) {
992 68
                                    $method = 'set' . ucfirst($key);
993 68
                                    $docSheet->getProtection()->$method(self::boolean((string) $value));
994
                                }
995
                            }
996
997 666
                            if ($xmlSheet) {
998 656
                                $this->readSheetProtection($docSheet, $xmlSheet);
999
                            }
1000
1001 666
                            if ($this->readDataOnly === false) {
1002 664
                                $this->readAutoFilter($xmlSheetNS, $docSheet);
1003 664
                                $this->readBackgroundImage($xmlSheetNS, $docSheet, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels');
1004
                            }
1005
1006 666
                            $this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS, $tableStyles, $dxfs);
1007
1008 666
                            if ($xmlSheetNS && $xmlSheetNS->mergeCells && $xmlSheetNS->mergeCells->mergeCell && !$this->readDataOnly) {
1009 65
                                foreach ($xmlSheetNS->mergeCells->mergeCell as $mergeCellx) {
1010 65
                                    $mergeCell = $mergeCellx->attributes();
1011 65
                                    $mergeRef = (string) ($mergeCell['ref'] ?? '');
1012 65
                                    if (str_contains($mergeRef, ':')) {
1013 65
                                        $docSheet->mergeCells($mergeRef, Worksheet::MERGE_CELL_CONTENT_HIDE);
1014
                                    }
1015
                                }
1016
                            }
1017
1018 666
                            if ($xmlSheet && !$this->readDataOnly) {
1019 654
                                $unparsedLoadedData = (new PageSetup($docSheet, $xmlSheet))->load($unparsedLoadedData);
1020
                            }
1021
1022 666
                            if (isset($xmlSheet->extLst->ext)) {
1023 198
                                foreach ($xmlSheet->extLst->ext as $extlst) {
1024 198
                                    $extAttrs = $extlst->attributes() ?? [];
1025 198
                                    $extUri = (string) ($extAttrs['uri'] ?? '');
1026 198
                                    if ($extUri !== '{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}') {
1027 192
                                        continue;
1028
                                    }
1029
                                    // Create dataValidations node if does not exists, maybe is better inside the foreach ?
1030 6
                                    if (!$xmlSheet->dataValidations) {
1031 1
                                        $xmlSheet->addChild('dataValidations');
1032
                                    }
1033
1034 6
                                    foreach ($extlst->children(Namespaces::DATA_VALIDATIONS1)->dataValidations->dataValidation as $item) {
1035 6
                                        $item = self::testSimpleXml($item);
1036 6
                                        $node = self::testSimpleXml($xmlSheet->dataValidations)->addChild('dataValidation');
1037 6
                                        foreach ($item->attributes() ?? [] as $attr) {
1038 6
                                            $node->addAttribute($attr->getName(), $attr);
1039
                                        }
1040 6
                                        $node->addAttribute('sqref', $item->children(Namespaces::DATA_VALIDATIONS2)->sqref);
1041 6
                                        if (isset($item->formula1)) {
1042 6
                                            $childNode = $node->addChild('formula1');
1043 6
                                            if ($childNode !== null) { // null should never happen
1044
                                                // see https://github.com/phpstan/phpstan/issues/8236
1045 6
                                                $childNode[0] = (string) $item->formula1->children(Namespaces::DATA_VALIDATIONS2)->f; // @phpstan-ignore-line
1046
                                            }
1047
                                        }
1048
                                    }
1049
                                }
1050
                            }
1051
1052 666
                            if ($xmlSheet && $xmlSheet->dataValidations && !$this->readDataOnly) {
1053 22
                                (new DataValidations($docSheet, $xmlSheet))->load();
1054
                            }
1055
1056
                            // unparsed sheet AlternateContent
1057 666
                            if ($xmlSheet && !$this->readDataOnly) {
1058 654
                                $mc = $xmlSheet->children(Namespaces::COMPATIBILITY);
1059 654
                                if ($mc->AlternateContent) {
1060 4
                                    foreach ($mc->AlternateContent as $alternateContent) {
1061 4
                                        $alternateContent = self::testSimpleXml($alternateContent);
1062 4
                                        $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['AlternateContents'][] = $alternateContent->asXML();
1063
                                    }
1064
                                }
1065
                            }
1066
1067
                            // Add hyperlinks
1068 666
                            if (!$this->readDataOnly) {
1069 664
                                $hyperlinkReader = new Hyperlinks($docSheet);
1070
                                // Locate hyperlink relations
1071 664
                                $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
1072 664
                                if ($zip->locateName($relationsFileName) !== false) {
1073 547
                                    $relsWorksheet = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS);
1074 547
                                    $hyperlinkReader->readHyperlinks($relsWorksheet);
1075
                                }
1076
1077
                                // Loop through hyperlinks
1078 664
                                if ($xmlSheetNS && $xmlSheetNS->children($mainNS)->hyperlinks) {
1079 18
                                    $hyperlinkReader->setHyperlinks($xmlSheetNS->children($mainNS)->hyperlinks);
1080
                                }
1081
                            }
1082
1083
                            // Add comments
1084 666
                            $comments = [];
1085 666
                            $vmlComments = [];
1086 666
                            if (!$this->readDataOnly) {
1087
                                // Locate comment relations
1088 664
                                $commentRelations = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
1089 664
                                if ($zip->locateName($commentRelations) !== false) {
1090 547
                                    $relsWorksheet = $this->loadZip($commentRelations, Namespaces::RELATIONSHIPS);
1091 547
                                    foreach ($relsWorksheet->Relationship as $elex) {
1092 396
                                        $ele = self::getAttributes($elex);
1093 396
                                        if ($ele['Type'] == Namespaces::COMMENTS) {
1094 34
                                            $comments[(string) $ele['Id']] = (string) $ele['Target'];
1095
                                        }
1096 396
                                        if ($ele['Type'] == Namespaces::VML) {
1097 37
                                            $vmlComments[(string) $ele['Id']] = (string) $ele['Target'];
1098
                                        }
1099
                                    }
1100
                                }
1101
1102
                                // Loop through comments
1103 664
                                foreach ($comments as $relName => $relPath) {
1104
                                    // Load comments file
1105 34
                                    $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath);
1106
                                    // okay to ignore namespace - using xpath
1107 34
                                    $commentsFile = $this->loadZip($relPath, '');
1108
1109
                                    // Utility variables
1110 34
                                    $authors = [];
1111 34
                                    $commentsFile->registerXpathNamespace('com', $mainNS);
1112 34
                                    $authorPath = self::xpathNoFalse($commentsFile, 'com:authors/com:author');
1113 34
                                    foreach ($authorPath as $author) {
1114 34
                                        $authors[] = (string) $author;
1115
                                    }
1116
1117
                                    // Loop through contents
1118 34
                                    $contentPath = self::xpathNoFalse($commentsFile, 'com:commentList/com:comment');
1119 34
                                    foreach ($contentPath as $comment) {
1120 34
                                        $commentx = $comment->attributes();
1121 34
                                        $commentModel = $docSheet->getComment((string) $commentx['ref']);
1122 34
                                        if (isset($commentx['authorId'])) {
1123 34
                                            $commentModel->setAuthor($authors[(int) $commentx['authorId']]);
1124
                                        }
1125 34
                                        $commentModel->setText($this->parseRichText($comment->children($mainNS)->text));
1126
                                    }
1127
                                }
1128
1129
                                // later we will remove from it real vmlComments
1130 664
                                $unparsedVmlDrawings = $vmlComments;
1131 664
                                $vmlDrawingContents = [];
1132
1133
                                // Loop through VML comments
1134 664
                                foreach ($vmlComments as $relName => $relPath) {
1135
                                    // Load VML comments file
1136 37
                                    $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath);
1137
1138
                                    try {
1139
                                        // no namespace okay - processed with Xpath
1140 37
                                        $vmlCommentsFile = $this->loadZip($relPath, '', true);
1141 37
                                        $vmlCommentsFile->registerXPathNamespace('v', Namespaces::URN_VML);
1142
                                    } catch (Throwable) {
1143
                                        //Ignore unparsable vmlDrawings. Later they will be moved from $unparsedVmlDrawings to $unparsedLoadedData
1144
                                        continue;
1145
                                    }
1146
1147
                                    // Locate VML drawings image relations
1148 37
                                    $drowingImages = [];
1149 37
                                    $VMLDrawingsRelations = dirname($relPath) . '/_rels/' . basename($relPath) . '.rels';
1150 37
                                    $vmlDrawingContents[$relName] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $relPath));
1151 37
                                    if ($zip->locateName($VMLDrawingsRelations) !== false) {
1152 17
                                        $relsVMLDrawing = $this->loadZip($VMLDrawingsRelations, Namespaces::RELATIONSHIPS);
1153 17
                                        foreach ($relsVMLDrawing->Relationship as $elex) {
1154 8
                                            $ele = self::getAttributes($elex);
1155 8
                                            if ($ele['Type'] == Namespaces::IMAGE) {
1156 8
                                                $drowingImages[(string) $ele['Id']] = (string) $ele['Target'];
1157
                                            }
1158
                                        }
1159
                                    }
1160
1161 37
                                    $shapes = self::xpathNoFalse($vmlCommentsFile, '//v:shape');
1162 37
                                    foreach ($shapes as $shape) {
1163 36
                                        $shape->registerXPathNamespace('v', Namespaces::URN_VML);
1164
1165 36
                                        if (isset($shape['style'])) {
1166 36
                                            $style = (string) $shape['style'];
1167 36
                                            $fillColor = strtoupper(substr((string) $shape['fillcolor'], 1));
1168 36
                                            $column = null;
1169 36
                                            $row = null;
1170 36
                                            $textHAlign = null;
1171 36
                                            $fillImageRelId = null;
1172 36
                                            $fillImageTitle = '';
1173
1174 36
                                            $clientData = $shape->xpath('.//x:ClientData');
1175 36
                                            $textboxDirection = '';
1176 36
                                            $textboxPath = $shape->xpath('.//v:textbox');
1177 36
                                            $textbox = (string) ($textboxPath[0]['style'] ?? '');
1178 36
                                            if (preg_match('/rtl/i', $textbox) === 1) {
1179 1
                                                $textboxDirection = Comment::TEXTBOX_DIRECTION_RTL;
1180 35
                                            } elseif (preg_match('/ltr/i', $textbox) === 1) {
1181 1
                                                $textboxDirection = Comment::TEXTBOX_DIRECTION_LTR;
1182
                                            }
1183 36
                                            if (is_array($clientData) && !empty($clientData)) {
1184
                                                /** @var SimpleXMLElement */
1185 35
                                                $clientData = $clientData[0];
1186
1187 35
                                                if (isset($clientData['ObjectType']) && (string) $clientData['ObjectType'] == 'Note') {
1188 33
                                                    $temp = $clientData->xpath('.//x:Row');
1189 33
                                                    if (is_array($temp)) {
1190 33
                                                        $row = $temp[0];
1191
                                                    }
1192
1193 33
                                                    $temp = $clientData->xpath('.//x:Column');
1194 33
                                                    if (is_array($temp)) {
1195 33
                                                        $column = $temp[0];
1196
                                                    }
1197 33
                                                    $temp = $clientData->xpath('.//x:TextHAlign');
1198 33
                                                    if (!empty($temp)) {
1199 2
                                                        $textHAlign = strtolower($temp[0]);
1200
                                                    }
1201
                                                }
1202
                                            }
1203 36
                                            $rowx = (string) $row;
1204 36
                                            $colx = (string) $column;
1205 36
                                            if (is_numeric($rowx) && is_numeric($colx) && $textHAlign !== null) {
1206 2
                                                $docSheet->getComment([1 + (int) $colx, 1 + (int) $rowx], false)->setAlignment((string) $textHAlign);
1207
                                            }
1208 36
                                            if (is_numeric($rowx) && is_numeric($colx) && $textboxDirection !== '') {
1209 2
                                                $docSheet->getComment([1 + (int) $colx, 1 + (int) $rowx], false)->setTextboxDirection($textboxDirection);
1210
                                            }
1211
1212 36
                                            $fillImageRelNode = $shape->xpath('.//v:fill/@o:relid');
1213 36
                                            if (is_array($fillImageRelNode) && !empty($fillImageRelNode)) {
1214
                                                /** @var SimpleXMLElement */
1215 5
                                                $fillImageRelNode = $fillImageRelNode[0];
1216
1217 5
                                                if (isset($fillImageRelNode['relid'])) {
1218 5
                                                    $fillImageRelId = (string) $fillImageRelNode['relid'];
1219
                                                }
1220
                                            }
1221
1222 36
                                            $fillImageTitleNode = $shape->xpath('.//v:fill/@o:title');
1223 36
                                            if (is_array($fillImageTitleNode) && !empty($fillImageTitleNode)) {
1224
                                                /** @var SimpleXMLElement */
1225 3
                                                $fillImageTitleNode = $fillImageTitleNode[0];
1226
1227 3
                                                if (isset($fillImageTitleNode['title'])) {
1228 3
                                                    $fillImageTitle = (string) $fillImageTitleNode['title'];
1229
                                                }
1230
                                            }
1231
1232 36
                                            if (($column !== null) && ($row !== null)) {
1233
                                                // Set comment properties
1234 33
                                                $comment = $docSheet->getComment([(int) $column + 1, (int) $row + 1]);
1235 33
                                                $comment->getFillColor()->setRGB($fillColor);
1236 33
                                                if (isset($drowingImages[$fillImageRelId])) {
1237 5
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
1238 5
                                                    $objDrawing->setName($fillImageTitle);
1239 5
                                                    $imagePath = str_replace(['../', '/xl/'], 'xl/', $drowingImages[$fillImageRelId]);
1240 5
                                                    $objDrawing->setPath(
1241 5
                                                        'zip://' . File::realpath($filename) . '#' . $imagePath,
1242 5
                                                        true,
1243 5
                                                        $zip
1244 5
                                                    );
1245 5
                                                    $comment->setBackgroundImage($objDrawing);
1246
                                                }
1247
1248
                                                // Parse style
1249 33
                                                $styleArray = explode(';', str_replace(' ', '', $style));
1250 33
                                                foreach ($styleArray as $stylePair) {
1251 33
                                                    $stylePair = explode(':', $stylePair);
1252
1253 33
                                                    if ($stylePair[0] == 'margin-left') {
1254 29
                                                        $comment->setMarginLeft($stylePair[1]);
1255
                                                    }
1256 33
                                                    if ($stylePair[0] == 'margin-top') {
1257 29
                                                        $comment->setMarginTop($stylePair[1]);
1258
                                                    }
1259 33
                                                    if ($stylePair[0] == 'width') {
1260 29
                                                        $comment->setWidth($stylePair[1]);
1261
                                                    }
1262 33
                                                    if ($stylePair[0] == 'height') {
1263 29
                                                        $comment->setHeight($stylePair[1]);
1264
                                                    }
1265 33
                                                    if ($stylePair[0] == 'visibility') {
1266 33
                                                        $comment->setVisible($stylePair[1] == 'visible');
1267
                                                    }
1268
                                                }
1269
1270 33
                                                unset($unparsedVmlDrawings[$relName]);
1271
                                            }
1272
                                        }
1273
                                    }
1274
                                }
1275
1276
                                // unparsed vmlDrawing
1277 664
                                if ($unparsedVmlDrawings) {
1278 6
                                    foreach ($unparsedVmlDrawings as $rId => $relPath) {
1279 6
                                        $rId = substr($rId, 3); // rIdXXX
1280 6
                                        $unparsedVmlDrawing = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['vmlDrawings'];
1281 6
                                        $unparsedVmlDrawing[$rId] = [];
1282 6
                                        $unparsedVmlDrawing[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $relPath);
1283 6
                                        $unparsedVmlDrawing[$rId]['relFilePath'] = $relPath;
1284 6
                                        $unparsedVmlDrawing[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedVmlDrawing[$rId]['filePath']));
1285 6
                                        unset($unparsedVmlDrawing);
1286
                                    }
1287
                                }
1288
1289
                                // Header/footer images
1290 664
                                if ($xmlSheetNS && $xmlSheetNS->legacyDrawingHF) {
1291 2
                                    $vmlHfRid = '';
1292 2
                                    $vmlHfRidAttr = $xmlSheetNS->legacyDrawingHF->attributes(Namespaces::SCHEMA_OFFICE_DOCUMENT);
1293 2
                                    if ($vmlHfRidAttr !== null && isset($vmlHfRidAttr['id'])) {
1294 2
                                        $vmlHfRid = (string) $vmlHfRidAttr['id'][0];
1295
                                    }
1296 2
                                    if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') !== false) {
1297 2
                                        $relsWorksheet = $this->loadZipNoNamespace(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels', Namespaces::RELATIONSHIPS);
1298 2
                                        $vmlRelationship = '';
1299
1300 2
                                        foreach ($relsWorksheet->Relationship as $ele) {
1301 2
                                            if ((string) $ele['Type'] == Namespaces::VML && (string) $ele['Id'] === $vmlHfRid) {
1302 2
                                                $vmlRelationship = self::dirAdd("$dir/$fileWorksheet", $ele['Target']);
1303
1304 2
                                                break;
1305
                                            }
1306
                                        }
1307
1308 2
                                        if ($vmlRelationship != '') {
1309
                                            // Fetch linked images
1310 2
                                            $relsVML = $this->loadZipNoNamespace(dirname($vmlRelationship) . '/_rels/' . basename($vmlRelationship) . '.rels', Namespaces::RELATIONSHIPS);
1311 2
                                            $drawings = [];
1312 2
                                            if (isset($relsVML->Relationship)) {
1313 2
                                                foreach ($relsVML->Relationship as $ele) {
1314 2
                                                    if ($ele['Type'] == Namespaces::IMAGE) {
1315 2
                                                        $drawings[(string) $ele['Id']] = self::dirAdd($vmlRelationship, $ele['Target']);
1316
                                                    }
1317
                                                }
1318
                                            }
1319
                                            // Fetch VML document
1320 2
                                            $vmlDrawing = $this->loadZipNoNamespace($vmlRelationship, '');
1321 2
                                            $vmlDrawing->registerXPathNamespace('v', Namespaces::URN_VML);
1322
1323 2
                                            $hfImages = [];
1324
1325 2
                                            $shapes = self::xpathNoFalse($vmlDrawing, '//v:shape');
1326 2
                                            foreach ($shapes as $idx => $shape) {
1327 2
                                                $shape->registerXPathNamespace('v', Namespaces::URN_VML);
1328 2
                                                $imageData = $shape->xpath('//v:imagedata');
1329
1330 2
                                                if (empty($imageData)) {
1331
                                                    continue;
1332
                                                }
1333
1334 2
                                                $imageData = $imageData[$idx];
1335
1336 2
                                                $imageData = self::getAttributes($imageData, Namespaces::URN_MSOFFICE);
1337 2
                                                $style = self::toCSSArray((string) $shape['style']);
1338
1339 2
                                                if (array_key_exists((string) $imageData['relid'], $drawings)) {
1340 2
                                                    $shapeId = (string) $shape['id'];
1341 2
                                                    $hfImages[$shapeId] = new HeaderFooterDrawing();
1342 2
                                                    if (isset($imageData['title'])) {
1343 2
                                                        $hfImages[$shapeId]->setName((string) $imageData['title']);
1344
                                                    }
1345
1346 2
                                                    $hfImages[$shapeId]->setPath('zip://' . File::realpath($filename) . '#' . $drawings[(string) $imageData['relid']], false, $zip);
1347 2
                                                    $hfImages[$shapeId]->setResizeProportional(false);
1348 2
                                                    $hfImages[$shapeId]->setWidth($style['width']);
1349 2
                                                    $hfImages[$shapeId]->setHeight($style['height']);
1350 2
                                                    if (isset($style['margin-left'])) {
1351 2
                                                        $hfImages[$shapeId]->setOffsetX($style['margin-left']);
1352
                                                    }
1353 2
                                                    $hfImages[$shapeId]->setOffsetY($style['margin-top']);
1354 2
                                                    $hfImages[$shapeId]->setResizeProportional(true);
1355
                                                }
1356
                                            }
1357
1358 2
                                            $docSheet->getHeaderFooter()->setImages($hfImages);
1359
                                        }
1360
                                    }
1361
                                }
1362
                            }
1363
1364
                            // TODO: Autoshapes from twoCellAnchors!
1365 666
                            $drawingFilename = dirname("$dir/$fileWorksheet")
1366 666
                                . '/_rels/'
1367 666
                                . basename($fileWorksheet)
1368 666
                                . '.rels';
1369 666
                            if (str_starts_with($drawingFilename, 'xl//xl/')) {
1370
                                $drawingFilename = substr($drawingFilename, 4);
1371
                            }
1372 666
                            if (str_starts_with($drawingFilename, '/xl//xl/')) {
1373
                                $drawingFilename = substr($drawingFilename, 5);
1374
                            }
1375 666
                            if ($zip->locateName($drawingFilename) !== false) {
1376 549
                                $relsWorksheet = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS);
1377 549
                                $drawings = [];
1378 549
                                foreach ($relsWorksheet->Relationship as $elex) {
1379 397
                                    $ele = self::getAttributes($elex);
1380 397
                                    if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
1381 129
                                        $eleTarget = (string) $ele['Target'];
1382 129
                                        if (str_starts_with($eleTarget, '/xl/')) {
1383 4
                                            $drawings[(string) $ele['Id']] = substr($eleTarget, 1);
1384
                                        } else {
1385 126
                                            $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']);
1386
                                        }
1387
                                    }
1388
                                }
1389
1390 549
                                if ($xmlSheetNS->drawing && !$this->readDataOnly) {
1391 129
                                    $unparsedDrawings = [];
1392 129
                                    $fileDrawing = null;
1393 129
                                    foreach ($xmlSheetNS->drawing as $drawing) {
1394 129
                                        $drawingRelId = self::getArrayItemString(self::getAttributes($drawing, $xmlNamespaceBase), 'id');
1395 129
                                        $fileDrawing = $drawings[$drawingRelId];
1396 129
                                        $drawingFilename = dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels';
1397 129
                                        $relsDrawing = $this->loadZip($drawingFilename, Namespaces::RELATIONSHIPS);
1398
1399 129
                                        $images = [];
1400 129
                                        $hyperlinks = [];
1401 129
                                        if ($relsDrawing && $relsDrawing->Relationship) {
1402 109
                                            foreach ($relsDrawing->Relationship as $elex) {
1403 109
                                                $ele = self::getAttributes($elex);
1404 109
                                                $eleType = (string) $ele['Type'];
1405 109
                                                if ($eleType === Namespaces::HYPERLINK) {
1406 3
                                                    $hyperlinks[(string) $ele['Id']] = (string) $ele['Target'];
1407
                                                }
1408 109
                                                if ($eleType === "$xmlNamespaceBase/image") {
1409 59
                                                    $eleTarget = (string) $ele['Target'];
1410 59
                                                    if (str_starts_with($eleTarget, '/xl/')) {
1411 1
                                                        $eleTarget = substr($eleTarget, 1);
1412 1
                                                        $images[(string) $ele['Id']] = $eleTarget;
1413
                                                    } else {
1414 58
                                                        $images[(string) $ele['Id']] = self::dirAdd($fileDrawing, $eleTarget);
1415
                                                    }
1416 74
                                                } elseif ($eleType === "$xmlNamespaceBase/chart") {
1417 70
                                                    if ($this->includeCharts) {
1418 69
                                                        $eleTarget = (string) $ele['Target'];
1419 69
                                                        if (str_starts_with($eleTarget, '/xl/')) {
1420 3
                                                            $index = substr($eleTarget, 1);
1421
                                                        } else {
1422 67
                                                            $index = self::dirAdd($fileDrawing, $eleTarget);
1423
                                                        }
1424 69
                                                        $charts[$index] = [
1425 69
                                                            'id' => (string) $ele['Id'],
1426 69
                                                            'sheet' => $docSheet->getTitle(),
1427 69
                                                        ];
1428
                                                    }
1429
                                                }
1430
                                            }
1431
                                        }
1432
1433 129
                                        $xmlDrawing = $this->loadZipNoNamespace($fileDrawing, '');
1434 129
                                        $xmlDrawingChildren = $xmlDrawing->children(Namespaces::SPREADSHEET_DRAWING);
1435
1436 129
                                        if ($xmlDrawingChildren->oneCellAnchor) {
1437 23
                                            foreach ($xmlDrawingChildren->oneCellAnchor as $oneCellAnchor) {
1438 23
                                                $oneCellAnchor = self::testSimpleXml($oneCellAnchor);
1439 23
                                                if ($oneCellAnchor->pic->blipFill) {
1440 16
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
1441 16
                                                    $blip = $oneCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip;
1442 16
                                                    if (isset($blip, $blip->alphaModFix)) {
1443 1
                                                        $temp = (string) $blip->alphaModFix->attributes()->amt;
1444 1
                                                        if (is_numeric($temp)) {
1445 1
                                                            $objDrawing->setOpacity((int) $temp);
1446
                                                        }
1447
                                                    }
1448 16
                                                    $xfrm = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm;
1449 16
                                                    $outerShdw = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw;
1450
1451 16
                                                    $objDrawing->setName(self::getArrayItemString(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'name'));
1452 16
                                                    $objDrawing->setDescription(self::getArrayItemString(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'descr'));
1453 16
                                                    $embedImageKey = self::getArrayItemString(
1454 16
                                                        self::getAttributes($blip, $xmlNamespaceBase),
1455 16
                                                        'embed'
1456 16
                                                    );
1457 16
                                                    if (isset($images[$embedImageKey])) {
1458 16
                                                        $objDrawing->setPath(
1459 16
                                                            'zip://' . File::realpath($filename) . '#'
1460 16
                                                            . $images[$embedImageKey],
1461 16
                                                            false,
1462 16
                                                            $zip
1463 16
                                                        );
1464
                                                    } else {
1465
                                                        $linkImageKey = self::getArrayItemString(
1466
                                                            $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'),
1467
                                                            'link'
1468
                                                        );
1469
                                                        if (isset($images[$linkImageKey])) {
1470
                                                            $url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
1471
                                                            $objDrawing->setPath($url, false);
1472
                                                        }
1473
                                                        if ($objDrawing->getPath() === '') {
1474
                                                            continue;
1475
                                                        }
1476
                                                    }
1477 16
                                                    $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1));
1478
1479 16
                                                    $objDrawing->setOffsetX((int) Drawing::EMUToPixels($oneCellAnchor->from->colOff));
1480 16
                                                    $objDrawing->setOffsetY(Drawing::EMUToPixels($oneCellAnchor->from->rowOff));
1481 16
                                                    $objDrawing->setResizeProportional(false);
1482 16
                                                    $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cx')));
1483 16
                                                    $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cy')));
1484 16
                                                    if ($xfrm) {
1485 16
                                                        $objDrawing->setRotation((int) Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($xfrm), 'rot')));
1486 16
                                                        $objDrawing->setFlipVertical((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipV'));
1487 16
                                                        $objDrawing->setFlipHorizontal((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipH'));
1488
                                                    }
1489 16
                                                    if ($outerShdw) {
1490 3
                                                        $shadow = $objDrawing->getShadow();
1491 3
                                                        $shadow->setVisible(true);
1492 3
                                                        $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'blurRad')));
1493 3
                                                        $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dist')));
1494 3
                                                        $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dir')));
1495 3
                                                        $shadow->setAlignment(self::getArrayItemString(self::getAttributes($outerShdw), 'algn'));
1496 3
                                                        $clr = $outerShdw->srgbClr ?? $outerShdw->prstClr;
1497 3
                                                        $shadow->getColor()->setRGB(self::getArrayItemString(self::getAttributes($clr), 'val'));
1498 3
                                                        if ($clr->alpha) {
1499 3
                                                            $alpha = StringHelper::convertToString(self::getArrayItem(self::getAttributes($clr->alpha), 'val'));
1500 3
                                                            if (is_numeric($alpha)) {
1501 3
                                                                $alpha = (int) ($alpha / 1000);
1502 3
                                                                $shadow->setAlpha($alpha);
1503
                                                            }
1504
                                                        }
1505
                                                    }
1506
1507 16
                                                    $this->readHyperLinkDrawing($objDrawing, $oneCellAnchor, $hyperlinks);
1508
1509 16
                                                    $objDrawing->setWorksheet($docSheet);
1510 7
                                                } elseif ($this->includeCharts && $oneCellAnchor->graphicFrame) {
1511
                                                    // Exported XLSX from Google Sheets positions charts with a oneCellAnchor
1512 4
                                                    $coordinates = Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1);
1513 4
                                                    $offsetX = Drawing::EMUToPixels($oneCellAnchor->from->colOff);
1514 4
                                                    $offsetY = Drawing::EMUToPixels($oneCellAnchor->from->rowOff);
1515 4
                                                    $width = Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cx'));
1516 4
                                                    $height = Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($oneCellAnchor->ext), 'cy'));
1517
1518 4
                                                    $graphic = $oneCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
1519 4
                                                    $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
1520 4
                                                    $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
1521
1522 4
                                                    $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
1523 4
                                                        'fromCoordinate' => $coordinates,
1524 4
                                                        'fromOffsetX' => $offsetX,
1525 4
                                                        'fromOffsetY' => $offsetY,
1526 4
                                                        'width' => $width,
1527 4
                                                        'height' => $height,
1528 4
                                                        'worksheetTitle' => $docSheet->getTitle(),
1529 4
                                                        'oneCellAnchor' => true,
1530 4
                                                    ];
1531
                                                }
1532
                                            }
1533
                                        }
1534 129
                                        if ($xmlDrawingChildren->twoCellAnchor) {
1535 92
                                            foreach ($xmlDrawingChildren->twoCellAnchor as $twoCellAnchor) {
1536 92
                                                $twoCellAnchor = self::testSimpleXml($twoCellAnchor);
1537 92
                                                if ($twoCellAnchor->pic->blipFill) {
1538 45
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
1539 45
                                                    $blip = $twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip;
1540 45
                                                    if (isset($blip, $blip->alphaModFix)) {
1541 3
                                                        $temp = (string) $blip->alphaModFix->attributes()->amt;
1542 3
                                                        if (is_numeric($temp)) {
1543 3
                                                            $objDrawing->setOpacity((int) $temp);
1544
                                                        }
1545
                                                    }
1546 45
                                                    if (isset($twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->srcRect)) {
1547 7
                                                        $objDrawing->setSrcRect($twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->srcRect->attributes());
1548
                                                    }
1549 45
                                                    $xfrm = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm;
1550 45
                                                    $outerShdw = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw;
1551 45
                                                    $editAs = $twoCellAnchor->attributes();
1552 45
                                                    if (isset($editAs, $editAs['editAs'])) {
1553 40
                                                        $objDrawing->setEditAs($editAs['editAs']);
1554
                                                    }
1555 45
                                                    $objDrawing->setName((string) self::getArrayItemString(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'name'));
1556 45
                                                    $objDrawing->setDescription(self::getArrayItemString(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'descr'));
1557 45
                                                    $embedImageKey = self::getArrayItemString(
1558 45
                                                        self::getAttributes($blip, $xmlNamespaceBase),
1559 45
                                                        'embed'
1560 45
                                                    );
1561 45
                                                    if (isset($images[$embedImageKey])) {
1562 42
                                                        $objDrawing->setPath(
1563 42
                                                            'zip://' . File::realpath($filename) . '#'
1564 42
                                                            . $images[$embedImageKey],
1565 42
                                                            false,
1566 42
                                                            $zip
1567 42
                                                        );
1568
                                                    } else {
1569 3
                                                        $linkImageKey = self::getArrayItemString(
1570 3
                                                            $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'),
1571 3
                                                            'link'
1572 3
                                                        );
1573 3
                                                        if (isset($images[$linkImageKey])) {
1574 3
                                                            $url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
1575 3
                                                            $objDrawing->setPath($url, false);
1576
                                                        }
1577 2
                                                        if ($objDrawing->getPath() === '') {
1578 1
                                                            continue;
1579
                                                        }
1580
                                                    }
1581 43
                                                    $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1));
1582
1583 43
                                                    $objDrawing->setOffsetX(Drawing::EMUToPixels($twoCellAnchor->from->colOff));
1584 43
                                                    $objDrawing->setOffsetY(Drawing::EMUToPixels($twoCellAnchor->from->rowOff));
1585
1586 43
                                                    $objDrawing->setCoordinates2(Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1));
1587
1588 43
                                                    $objDrawing->setOffsetX2(Drawing::EMUToPixels($twoCellAnchor->to->colOff));
1589 43
                                                    $objDrawing->setOffsetY2(Drawing::EMUToPixels($twoCellAnchor->to->rowOff));
1590
1591 43
                                                    $objDrawing->setResizeProportional(false);
1592
1593 43
                                                    if ($xfrm) {
1594 43
                                                        $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($xfrm->ext), 'cx')));
1595 43
                                                        $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($xfrm->ext), 'cy')));
1596 43
                                                        $objDrawing->setRotation(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($xfrm), 'rot')));
1597 43
                                                        $objDrawing->setFlipVertical((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipV'));
1598 43
                                                        $objDrawing->setFlipHorizontal((bool) self::getArrayItem(self::getAttributes($xfrm), 'flipH'));
1599
                                                    }
1600 43
                                                    if ($outerShdw) {
1601 1
                                                        $shadow = $objDrawing->getShadow();
1602 1
                                                        $shadow->setVisible(true);
1603 1
                                                        $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'blurRad')));
1604 1
                                                        $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dist')));
1605 1
                                                        $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItemIntOrSxml(self::getAttributes($outerShdw), 'dir')));
1606 1
                                                        $shadow->setAlignment(self::getArrayItemString(self::getAttributes($outerShdw), 'algn'));
1607 1
                                                        $clr = $outerShdw->srgbClr ?? $outerShdw->prstClr;
1608 1
                                                        $shadow->getColor()->setRGB(self::getArrayItemString(self::getAttributes($clr), 'val'));
1609 1
                                                        if ($clr->alpha) {
1610 1
                                                            $alpha = StringHelper::convertToString(self::getArrayItem(self::getAttributes($clr->alpha), 'val'));
1611 1
                                                            if (is_numeric($alpha)) {
1612 1
                                                                $alpha = (int) ($alpha / 1000);
1613 1
                                                                $shadow->setAlpha($alpha);
1614
                                                            }
1615
                                                        }
1616
                                                    }
1617
1618 43
                                                    $this->readHyperLinkDrawing($objDrawing, $twoCellAnchor, $hyperlinks);
1619
1620 43
                                                    $objDrawing->setWorksheet($docSheet);
1621 71
                                                } elseif (($this->includeCharts) && ($twoCellAnchor->graphicFrame)) {
1622 65
                                                    $fromCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1);
1623 65
                                                    $fromOffsetX = Drawing::EMUToPixels($twoCellAnchor->from->colOff);
1624 65
                                                    $fromOffsetY = Drawing::EMUToPixels($twoCellAnchor->from->rowOff);
1625 65
                                                    $toCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1);
1626 65
                                                    $toOffsetX = Drawing::EMUToPixels($twoCellAnchor->to->colOff);
1627 65
                                                    $toOffsetY = Drawing::EMUToPixels($twoCellAnchor->to->rowOff);
1628 65
                                                    $graphic = $twoCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
1629 65
                                                    $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
1630 65
                                                    $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
1631
1632 65
                                                    $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
1633 65
                                                        'fromCoordinate' => $fromCoordinate,
1634 65
                                                        'fromOffsetX' => $fromOffsetX,
1635 65
                                                        'fromOffsetY' => $fromOffsetY,
1636 65
                                                        'toCoordinate' => $toCoordinate,
1637 65
                                                        'toOffsetX' => $toOffsetX,
1638 65
                                                        'toOffsetY' => $toOffsetY,
1639 65
                                                        'worksheetTitle' => $docSheet->getTitle(),
1640 65
                                                    ];
1641
                                                }
1642
                                            }
1643
                                        }
1644 128
                                        if ($xmlDrawingChildren->absoluteAnchor) {
1645 1
                                            foreach ($xmlDrawingChildren->absoluteAnchor as $absoluteAnchor) {
1646 1
                                                if (($this->includeCharts) && ($absoluteAnchor->graphicFrame)) {
1647 1
                                                    $graphic = $absoluteAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
1648 1
                                                    $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
1649 1
                                                    $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
1650 1
                                                    $width = Drawing::EMUToPixels((int) self::getArrayItemString(self::getAttributes($absoluteAnchor->ext), 'cx')[0]);
1651 1
                                                    $height = Drawing::EMUToPixels((int) self::getArrayItemString(self::getAttributes($absoluteAnchor->ext), 'cy')[0]);
1652
1653 1
                                                    $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
1654 1
                                                        'fromCoordinate' => 'A1',
1655 1
                                                        'fromOffsetX' => 0,
1656 1
                                                        'fromOffsetY' => 0,
1657 1
                                                        'width' => $width,
1658 1
                                                        'height' => $height,
1659 1
                                                        'worksheetTitle' => $docSheet->getTitle(),
1660 1
                                                    ];
1661
                                                }
1662
                                            }
1663
                                        }
1664 128
                                        if (empty($relsDrawing) && $xmlDrawing->count() == 0) {
1665
                                            // Save Drawing without rels and children as unparsed
1666 25
                                            $unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML();
1667
                                        }
1668
                                    }
1669
1670
                                    // store original rId of drawing files
1671 128
                                    $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'] = [];
1672 128
                                    foreach ($relsWorksheet->Relationship as $elex) {
1673 128
                                        $ele = self::getAttributes($elex);
1674 128
                                        if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
1675 128
                                            $drawingRelId = (string) $ele['Id'];
1676 128
                                            $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'][(string) $ele['Target']] = $drawingRelId;
1677 128
                                            if (isset($unparsedDrawings[$drawingRelId])) {
1678 25
                                                $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['Drawings'][$drawingRelId] = $unparsedDrawings[$drawingRelId];
1679
                                            }
1680
                                        }
1681
                                    }
1682 128
                                    if ($xmlSheet->legacyDrawing && !$this->readDataOnly) {
1683 13
                                        foreach ($xmlSheet->legacyDrawing as $drawing) {
1684 13
                                            $drawingRelId = self::getArrayItemString(self::getAttributes($drawing, $xmlNamespaceBase), 'id');
1685 13
                                            if (isset($vmlDrawingContents[$drawingRelId])) {
1686 13
                                                if (self::onlyNoteVml($vmlDrawingContents[$drawingRelId]) === false) {
1687 5
                                                    $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['legacyDrawing'] = $vmlDrawingContents[$drawingRelId];
1688
                                                }
1689
                                            }
1690
                                        }
1691
                                    }
1692
1693
                                    // unparsed drawing AlternateContent
1694 128
                                    $xmlAltDrawing = $this->loadZip((string) $fileDrawing, Namespaces::COMPATIBILITY);
1695
1696 128
                                    if ($xmlAltDrawing->AlternateContent) {
1697 4
                                        foreach ($xmlAltDrawing->AlternateContent as $alternateContent) {
1698 4
                                            $alternateContent = self::testSimpleXml($alternateContent);
1699 4
                                            $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingAlternateContents'][] = $alternateContent->asXML();
1700
                                        }
1701
                                    }
1702
                                }
1703
                            }
1704
1705 665
                            $this->readFormControlProperties($excel, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData);
1706 665
                            $this->readPrinterSettings($excel, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData);
1707
1708
                            // Loop through definedNames
1709 665
                            if ($xmlWorkbook->definedNames) {
1710 358
                                foreach ($xmlWorkbook->definedNames->definedName as $definedName) {
1711
                                    // Extract range
1712 106
                                    $extractedRange = (string) $definedName;
1713 106
                                    if (($spos = strpos($extractedRange, '!')) !== false) {
1714 88
                                        $extractedRange = substr($extractedRange, 0, $spos) . str_replace('$', '', substr($extractedRange, $spos));
1715
                                    } else {
1716 33
                                        $extractedRange = str_replace('$', '', $extractedRange);
1717
                                    }
1718
1719
                                    // Valid range?
1720 106
                                    if ($extractedRange == '') {
1721
                                        continue;
1722
                                    }
1723
1724
                                    // Some definedNames are only applicable if we are on the same sheet...
1725 106
                                    if ((string) $definedName['localSheetId'] != '' && (string) $definedName['localSheetId'] == $oldSheetId) {
1726
                                        // Switch on type
1727 50
                                        switch ((string) $definedName['name']) {
1728 50
                                            case '_xlnm._FilterDatabase':
1729 20
                                                if ((string) $definedName['hidden'] !== '1') {
1730
                                                    $extractedRange = explode(',', $extractedRange);
1731
                                                    foreach ($extractedRange as $range) {
1732
                                                        $autoFilterRange = $range;
1733
                                                        if (str_contains($autoFilterRange, ':')) {
1734
                                                            $docSheet->getAutoFilter()->setRange($autoFilterRange);
1735
                                                        }
1736
                                                    }
1737
                                                }
1738
1739 20
                                                break;
1740 30
                                            case '_xlnm.Print_Titles':
1741
                                                // Split $extractedRange
1742 3
                                                $extractedRange = explode(',', $extractedRange);
1743
1744
                                                // Set print titles
1745 3
                                                foreach ($extractedRange as $range) {
1746 3
                                                    $matches = [];
1747 3
                                                    $range = str_replace('$', '', $range);
1748
1749
                                                    // check for repeating columns, e g. 'A:A' or 'A:D'
1750 3
                                                    if (preg_match('/!?([A-Z]+)\:([A-Z]+)$/', $range, $matches)) {
1751
                                                        $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$matches[1], $matches[2]]);
1752 3
                                                    } elseif (preg_match('/!?(\d+)\:(\d+)$/', $range, $matches)) {
1753
                                                        // check for repeating rows, e.g. '1:1' or '1:5'
1754 3
                                                        $docSheet->getPageSetup()->setRowsToRepeatAtTop([$matches[1], $matches[2]]);
1755
                                                    }
1756
                                                }
1757
1758 3
                                                break;
1759 29
                                            case '_xlnm.Print_Area':
1760 11
                                                $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) ?: [];
1761 11
                                                $newRangeSets = [];
1762 11
                                                foreach ($rangeSets as $rangeSet) {
1763 11
                                                    [, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true);
1764 11
                                                    if (empty($rangeSet)) {
1765
                                                        continue;
1766
                                                    }
1767 11
                                                    if (!str_contains($rangeSet, ':')) {
1768
                                                        $rangeSet = $rangeSet . ':' . $rangeSet;
1769
                                                    }
1770 11
                                                    $newRangeSets[] = str_replace('$', '', $rangeSet);
1771
                                                }
1772 11
                                                if (count($newRangeSets) > 0) {
1773 11
                                                    $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets));
1774
                                                }
1775
1776 11
                                                break;
1777
                                            default:
1778 19
                                                break;
1779
                                        }
1780
                                    }
1781
                                }
1782
                            }
1783
1784
                            // Next sheet id
1785 665
                            ++$sheetId;
1786
                        }
1787
1788
                        // Loop through definedNames
1789 666
                        if ($xmlWorkbook->definedNames) {
1790 358
                            foreach ($xmlWorkbook->definedNames->definedName as $definedName) {
1791
                                // Extract range
1792 106
                                $extractedRange = (string) $definedName;
1793
1794
                                // Valid range?
1795 106
                                if ($extractedRange == '') {
1796
                                    continue;
1797
                                }
1798
1799
                                // Some definedNames are only applicable if we are on the same sheet...
1800 106
                                if ((string) $definedName['localSheetId'] != '') {
1801
                                    // Local defined name
1802
                                    // Switch on type
1803 50
                                    switch ((string) $definedName['name']) {
1804 50
                                        case '_xlnm._FilterDatabase':
1805 30
                                        case '_xlnm.Print_Titles':
1806 29
                                        case '_xlnm.Print_Area':
1807 33
                                            break;
1808
                                        default:
1809 19
                                            if ($mapSheetId[(int) $definedName['localSheetId']] !== null) {
1810 19
                                                $range = Worksheet::extractSheetTitle($extractedRange, true);
1811 19
                                                $scope = $excel->getSheet($mapSheetId[(int) $definedName['localSheetId']]);
1812 19
                                                if (str_contains((string) $definedName, '!')) {
1813 19
                                                    $range[0] = str_replace("''", "'", $range[0]);
1814 19
                                                    $range[0] = str_replace("'", '', $range[0]);
1815 19
                                                    if ($worksheet = $excel->getSheetByName($range[0])) {
1816 19
                                                        $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $worksheet, $extractedRange, true, $scope));
1817
                                                    } else {
1818 14
                                                        $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true, $scope));
1819
                                                    }
1820
                                                } else {
1821
                                                    $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true));
1822
                                                }
1823
                                            }
1824
1825 19
                                            break;
1826
                                    }
1827 76
                                } elseif (!isset($definedName['localSheetId'])) {
1828
                                    // "Global" definedNames
1829 76
                                    $locatedSheet = null;
1830 76
                                    if (str_contains((string) $definedName, '!')) {
1831
                                        // Modify range, and extract the first worksheet reference
1832
                                        // Need to split on a comma or a space if not in quotes, and extract the first part.
1833 57
                                        $definedNameValueParts = preg_split("/[ ,](?=([^']*'[^']*')*[^']*$)/miuU", $extractedRange);
1834 57
                                        if (is_array($definedNameValueParts)) {
1835
                                            // Extract sheet name
1836 57
                                            [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true, true);
1837
1838
                                            // Locate sheet
1839 57
                                            $locatedSheet = $excel->getSheetByName("$extractedSheetName");
1840
                                        }
1841
                                    }
1842
1843 76
                                    if ($locatedSheet === null && !DefinedName::testIfFormula($extractedRange)) {
1844 1
                                        $extractedRange = '#REF!';
1845
                                    }
1846 76
                                    $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $locatedSheet, $extractedRange, false));
1847
                                }
1848
                            }
1849
                        }
1850
                    }
1851
1852 666
                    (new WorkbookView($excel))->viewSettings($xmlWorkbook, $mainNS, $mapSheetId, $this->readDataOnly);
1853
1854 665
                    break;
1855
            }
1856
        }
1857
1858 665
        if (!$this->readDataOnly) {
1859 663
            $contentTypes = $this->loadZip('[Content_Types].xml');
1860
1861
            // Default content types
1862 663
            foreach ($contentTypes->Default as $contentType) {
1863 661
                switch ($contentType['ContentType']) {
1864 661
                    case 'application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings':
1865 291
                        $unparsedLoadedData['default_content_types'][(string) $contentType['Extension']] = (string) $contentType['ContentType'];
1866
1867 291
                        break;
1868
                }
1869
            }
1870
1871
            // Override content types
1872 663
            foreach ($contentTypes->Override as $contentType) {
1873 662
                switch ($contentType['ContentType']) {
1874 662
                    case 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml':
1875 71
                        if ($this->includeCharts) {
1876 69
                            $chartEntryRef = ltrim((string) $contentType['PartName'], '/');
1877 69
                            $chartElements = $this->loadZip($chartEntryRef);
1878 69
                            $chartReader = new Chart($chartNS, $drawingNS);
1879 69
                            $objChart = $chartReader->readChart($chartElements, basename($chartEntryRef, '.xml'));
1880 69
                            if (isset($charts[$chartEntryRef])) {
1881 69
                                $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id'];
1882 69
                                if (isset($chartDetails[$chartPositionRef]) && $excel->getSheetByName($charts[$chartEntryRef]['sheet']) !== null) {
1883 69
                                    $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart);
1884 69
                                    $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet']));
1885
                                    // For oneCellAnchor or absoluteAnchor positioned charts,
1886
                                    //     toCoordinate is not in the data. Does it need to be calculated?
1887 69
                                    if (array_key_exists('toCoordinate', $chartDetails[$chartPositionRef])) {
1888
                                        // twoCellAnchor
1889 65
                                        $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']);
1890 65
                                        $objChart->setBottomRightPosition($chartDetails[$chartPositionRef]['toCoordinate'], $chartDetails[$chartPositionRef]['toOffsetX'], $chartDetails[$chartPositionRef]['toOffsetY']);
1891
                                    } else {
1892
                                        // oneCellAnchor or absoluteAnchor (e.g. Chart sheet)
1893 5
                                        $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']);
1894 5
                                        $objChart->setBottomRightPosition('', $chartDetails[$chartPositionRef]['width'], $chartDetails[$chartPositionRef]['height']);
1895 5
                                        if (array_key_exists('oneCellAnchor', $chartDetails[$chartPositionRef])) {
1896 4
                                            $objChart->setOneCellAnchor($chartDetails[$chartPositionRef]['oneCellAnchor']);
1897
                                        }
1898
                                    }
1899
                                }
1900
                            }
1901
                        }
1902
1903 71
                        break;
1904
1905
                        // unparsed
1906 662
                    case 'application/vnd.ms-excel.controlproperties+xml':
1907 4
                        $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType'];
1908
1909 4
                        break;
1910
                }
1911
            }
1912
        }
1913
1914 665
        $excel->setUnparsedLoadedData($unparsedLoadedData);
1915
1916 665
        $zip->close();
1917
1918 665
        return $excel;
1919
    }
1920
1921 75
    private function parseRichText(?SimpleXMLElement $is): RichText
1922
    {
1923 75
        $value = new RichText();
1924
1925 75
        if (isset($is->t)) {
1926 21
            $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t));
1927 55
        } elseif ($is !== null) {
1928 55
            if (is_object($is->r)) {
1929 55
                foreach ($is->r as $run) {
1930 52
                    if (!isset($run->rPr)) {
1931 36
                        $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $run->t));
1932
                    } else {
1933 48
                        $objText = $value->createTextRun(StringHelper::controlCharacterOOXML2PHP((string) $run->t));
1934 48
                        $objFont = $objText->getFont() ?? new StyleFont();
1935
1936 48
                        if (isset($run->rPr->rFont)) {
1937 48
                            $attr = $run->rPr->rFont->attributes();
1938 48
                            if (isset($attr['val'])) {
1939 48
                                $objFont->setName((string) $attr['val']);
1940
                            }
1941
                        }
1942 48
                        if (isset($run->rPr->sz)) {
1943 48
                            $attr = $run->rPr->sz->attributes();
1944 48
                            if (isset($attr['val'])) {
1945 48
                                $objFont->setSize((float) $attr['val']);
1946
                            }
1947
                        }
1948 48
                        if (isset($run->rPr->color)) {
1949 46
                            $objFont->setColor(new Color($this->styleReader->readColor($run->rPr->color)));
1950
                        }
1951 48
                        if (isset($run->rPr->b)) {
1952 41
                            $attr = $run->rPr->b->attributes();
1953
                            if (
1954 41
                                (isset($attr['val']) && self::boolean((string) $attr['val']))
1955 41
                                || (!isset($attr['val']))
1956
                            ) {
1957 39
                                $objFont->setBold(true);
1958
                            }
1959
                        }
1960 48
                        if (isset($run->rPr->i)) {
1961 14
                            $attr = $run->rPr->i->attributes();
1962
                            if (
1963 14
                                (isset($attr['val']) && self::boolean((string) $attr['val']))
1964 14
                                || (!isset($attr['val']))
1965
                            ) {
1966 7
                                $objFont->setItalic(true);
1967
                            }
1968
                        }
1969 48
                        if (isset($run->rPr->vertAlign)) {
1970
                            $attr = $run->rPr->vertAlign->attributes();
1971
                            if (isset($attr['val'])) {
1972
                                $vertAlign = strtolower((string) $attr['val']);
1973
                                if ($vertAlign == 'superscript') {
1974
                                    $objFont->setSuperscript(true);
1975
                                }
1976
                                if ($vertAlign == 'subscript') {
1977
                                    $objFont->setSubscript(true);
1978
                                }
1979
                            }
1980
                        }
1981 48
                        if (isset($run->rPr->u)) {
1982 11
                            $attr = $run->rPr->u->attributes();
1983 11
                            if (!isset($attr['val'])) {
1984 1
                                $objFont->setUnderline(StyleFont::UNDERLINE_SINGLE);
1985
                            } else {
1986 10
                                $objFont->setUnderline((string) $attr['val']);
1987
                            }
1988
                        }
1989 48
                        if (isset($run->rPr->strike)) {
1990 10
                            $attr = $run->rPr->strike->attributes();
1991
                            if (
1992 10
                                (isset($attr['val']) && self::boolean((string) $attr['val']))
1993 10
                                || (!isset($attr['val']))
1994
                            ) {
1995
                                $objFont->setStrikethrough(true);
1996
                            }
1997
                        }
1998
                    }
1999
                }
2000
            }
2001
        }
2002
2003 75
        return $value;
2004
    }
2005
2006 2
    private function readRibbon(Spreadsheet $excel, string $customUITarget, ZipArchive $zip): void
2007
    {
2008 2
        $baseDir = dirname($customUITarget);
2009 2
        $nameCustomUI = basename($customUITarget);
2010
        // get the xml file (ribbon)
2011 2
        $localRibbon = $this->getFromZipArchive($zip, $customUITarget);
2012 2
        $customUIImagesNames = [];
2013 2
        $customUIImagesBinaries = [];
2014
        // something like customUI/_rels/customUI.xml.rels
2015 2
        $pathRels = $baseDir . '/_rels/' . $nameCustomUI . '.rels';
2016 2
        $dataRels = $this->getFromZipArchive($zip, $pathRels);
2017 2
        if ($dataRels) {
2018
            // exists and not empty if the ribbon have some pictures (other than internal MSO)
2019
            $UIRels = simplexml_load_string(
2020
                $this->getSecurityScannerOrThrow()
2021
                    ->scan($dataRels)
2022
            );
2023
            if (false !== $UIRels) {
2024
                // we need to save id and target to avoid parsing customUI.xml and "guess" if it's a pseudo callback who load the image
2025
                foreach ($UIRels->Relationship as $ele) {
2026
                    if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/image') {
2027
                        // an image ?
2028
                        $customUIImagesNames[(string) $ele['Id']] = (string) $ele['Target'];
2029
                        $customUIImagesBinaries[(string) $ele['Target']] = $this->getFromZipArchive($zip, $baseDir . '/' . (string) $ele['Target']);
2030
                    }
2031
                }
2032
            }
2033
        }
2034 2
        if ($localRibbon) {
2035 2
            $excel->setRibbonXMLData($customUITarget, $localRibbon);
2036 2
            if (count($customUIImagesNames) > 0 && count($customUIImagesBinaries) > 0) {
2037
                $excel->setRibbonBinObjects($customUIImagesNames, $customUIImagesBinaries);
2038
            } else {
2039 2
                $excel->setRibbonBinObjects(null, null);
2040
            }
2041
        } else {
2042
            $excel->setRibbonXMLData(null, null);
2043
            $excel->setRibbonBinObjects(null, null);
2044
        }
2045
    }
2046
2047 690
    private static function getArrayItem(null|array|bool|SimpleXMLElement $array, int|string $key = 0): mixed
2048
    {
2049 690
        return ($array === null || is_bool($array)) ? null : ($array[$key] ?? null);
2050
    }
2051
2052 682
    private static function getArrayItemString(null|array|bool|SimpleXMLElement $array, int|string $key = 0): string
2053
    {
2054 682
        $retVal = self::getArrayItem($array, $key);
2055
2056 682
        return StringHelper::convertToString($retVal, false);
2057
    }
2058
2059 60
    private static function getArrayItemIntOrSxml(null|array|bool|SimpleXMLElement $array, int|string $key = 0): int|SimpleXMLElement
2060
    {
2061 60
        $retVal = self::getArrayItem($array, $key);
2062
2063 60
        return (is_int($retVal) || $retVal instanceof SimpleXMLElement) ? $retVal : 0;
2064
    }
2065
2066 369
    private static function dirAdd(null|SimpleXMLElement|string $base, null|SimpleXMLElement|string $add): string
2067
    {
2068 369
        $base = (string) $base;
2069 369
        $add = (string) $add;
2070
2071 369
        return (string) preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add");
2072
    }
2073
2074 2
    private static function toCSSArray(string $style): array
2075
    {
2076 2
        $style = self::stripWhiteSpaceFromStyleString($style);
2077
2078 2
        $temp = explode(';', $style);
2079 2
        $style = [];
2080 2
        foreach ($temp as $item) {
2081 2
            $item = explode(':', $item);
2082
2083 2
            if (str_contains($item[1], 'px')) {
2084 1
                $item[1] = str_replace('px', '', $item[1]);
2085
            }
2086 2
            if (str_contains($item[1], 'pt')) {
2087 2
                $item[1] = str_replace('pt', '', $item[1]);
2088 2
                $item[1] = (string) Font::fontSizeToPixels((int) $item[1]);
2089
            }
2090 2
            if (str_contains($item[1], 'in')) {
2091
                $item[1] = str_replace('in', '', $item[1]);
2092
                $item[1] = (string) Font::inchSizeToPixels((int) $item[1]);
2093
            }
2094 2
            if (str_contains($item[1], 'cm')) {
2095
                $item[1] = str_replace('cm', '', $item[1]);
2096
                $item[1] = (string) Font::centimeterSizeToPixels((int) $item[1]);
2097
            }
2098
2099 2
            $style[$item[0]] = $item[1];
2100
        }
2101
2102 2
        return $style;
2103
    }
2104
2105 5
    public static function stripWhiteSpaceFromStyleString(string $string): string
2106
    {
2107 5
        return trim(str_replace(["\r", "\n", ' '], '', $string), ';');
2108
    }
2109
2110 88
    private static function boolean(string $value): bool
2111
    {
2112 88
        if (is_numeric($value)) {
2113 69
            return (bool) $value;
2114
        }
2115
2116 32
        return $value === 'true' || $value === 'TRUE';
2117
    }
2118
2119 57
    private function readHyperLinkDrawing(\PhpOffice\PhpSpreadsheet\Worksheet\Drawing $objDrawing, SimpleXMLElement $cellAnchor, array $hyperlinks): void
2120
    {
2121 57
        $hlinkClick = $cellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick;
2122
2123 57
        if ($hlinkClick->count() === 0) {
2124 55
            return;
2125
        }
2126
2127 2
        $hlinkId = (string) self::getAttributes($hlinkClick, Namespaces::SCHEMA_OFFICE_DOCUMENT)['id'];
2128 2
        $hyperlink = new Hyperlink(
2129 2
            $hyperlinks[$hlinkId],
2130 2
            self::getArrayItemString(self::getAttributes($cellAnchor->pic->nvPicPr->cNvPr), 'name')
2131 2
        );
2132 2
        $objDrawing->setHyperlink($hyperlink);
2133
    }
2134
2135 667
    private function readProtection(Spreadsheet $excel, SimpleXMLElement $xmlWorkbook): void
2136
    {
2137 667
        if (!$xmlWorkbook->workbookProtection) {
2138 647
            return;
2139
        }
2140
2141 24
        $excel->getSecurity()->setLockRevision(self::getLockValue($xmlWorkbook->workbookProtection, 'lockRevision'));
2142 24
        $excel->getSecurity()->setLockStructure(self::getLockValue($xmlWorkbook->workbookProtection, 'lockStructure'));
2143 24
        $excel->getSecurity()->setLockWindows(self::getLockValue($xmlWorkbook->workbookProtection, 'lockWindows'));
2144
2145 24
        if ($xmlWorkbook->workbookProtection['revisionsPassword']) {
2146 1
            $excel->getSecurity()->setRevisionsPassword(
2147 1
                (string) $xmlWorkbook->workbookProtection['revisionsPassword'],
2148 1
                true
2149 1
            );
2150
        }
2151
2152 24
        if ($xmlWorkbook->workbookProtection['workbookPassword']) {
2153 2
            $excel->getSecurity()->setWorkbookPassword(
2154 2
                (string) $xmlWorkbook->workbookProtection['workbookPassword'],
2155 2
                true
2156 2
            );
2157
        }
2158
    }
2159
2160 24
    private static function getLockValue(SimpleXMLElement $protection, string $key): ?bool
2161
    {
2162 24
        $returnValue = null;
2163 24
        $protectKey = $protection[$key];
2164 24
        if (!empty($protectKey)) {
2165 10
            $protectKey = (string) $protectKey;
2166 10
            $returnValue = $protectKey !== 'false' && (bool) $protectKey;
2167
        }
2168
2169 24
        return $returnValue;
2170
    }
2171
2172 665
    private function readFormControlProperties(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void
2173
    {
2174 665
        $zip = $this->zip;
2175 665
        if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') === false) {
2176 340
            return;
2177
        }
2178
2179 548
        $filename = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
2180 548
        $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS);
2181 548
        $ctrlProps = [];
2182 548
        foreach ($relsWorksheet->Relationship as $ele) {
2183 391
            if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/ctrlProp') {
2184 4
                $ctrlProps[(string) $ele['Id']] = $ele;
2185
            }
2186
        }
2187
2188 548
        $unparsedCtrlProps = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['ctrlProps'];
2189 548
        foreach ($ctrlProps as $rId => $ctrlProp) {
2190 4
            $rId = substr($rId, 3); // rIdXXX
2191 4
            $unparsedCtrlProps[$rId] = [];
2192 4
            $unparsedCtrlProps[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $ctrlProp['Target']);
2193 4
            $unparsedCtrlProps[$rId]['relFilePath'] = (string) $ctrlProp['Target'];
2194 4
            $unparsedCtrlProps[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedCtrlProps[$rId]['filePath']));
2195
        }
2196 548
        unset($unparsedCtrlProps);
2197
    }
2198
2199 665
    private function readPrinterSettings(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void
2200
    {
2201 665
        $zip = $this->zip;
2202 665
        if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels') === false) {
2203 340
            return;
2204
        }
2205
2206 548
        $filename = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
2207 548
        $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS);
2208 548
        $sheetPrinterSettings = [];
2209 548
        foreach ($relsWorksheet->Relationship as $ele) {
2210 391
            if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/printerSettings') {
2211 283
                $sheetPrinterSettings[(string) $ele['Id']] = $ele;
2212
            }
2213
        }
2214
2215 548
        $unparsedPrinterSettings = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['printerSettings'];
2216 548
        foreach ($sheetPrinterSettings as $rId => $printerSettings) {
2217 283
            $rId = substr($rId, 3); // rIdXXX
2218 283
            if (!str_ends_with($rId, 'ps')) {
2219 283
                $rId = $rId . 'ps'; // rIdXXX, add 'ps' suffix to avoid identical resource identifier collision with unparsed vmlDrawing
2220
            }
2221 283
            $unparsedPrinterSettings[$rId] = [];
2222 283
            $target = (string) str_replace('/xl/', '../', (string) $printerSettings['Target']);
2223 283
            $unparsedPrinterSettings[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $target);
2224 283
            $unparsedPrinterSettings[$rId]['relFilePath'] = $target;
2225 283
            $unparsedPrinterSettings[$rId]['content'] = $this->getSecurityScannerOrThrow()->scan($this->getFromZipArchive($zip, $unparsedPrinterSettings[$rId]['filePath']));
2226
        }
2227 548
        unset($unparsedPrinterSettings);
2228
    }
2229
2230 678
    private function getWorkbookBaseName(): array
2231
    {
2232 678
        $workbookBasename = '';
2233 678
        $xmlNamespaceBase = '';
2234
2235
        // check if it is an OOXML archive
2236 678
        $rels = $this->loadZip(self::INITIAL_FILE);
2237 678
        foreach ($rels->children(Namespaces::RELATIONSHIPS)->Relationship as $rel) {
2238 678
            $rel = self::getAttributes($rel);
2239 678
            $type = (string) $rel['Type'];
2240
            switch ($type) {
2241 670
                case Namespaces::OFFICE_DOCUMENT:
2242 657
                case Namespaces::PURL_OFFICE_DOCUMENT:
2243 678
                    $basename = basename((string) $rel['Target']);
2244 678
                    $xmlNamespaceBase = dirname($type);
2245 678
                    if (preg_match('/workbook.*\.xml/', $basename)) {
2246 678
                        $workbookBasename = $basename;
2247
                    }
2248
2249 678
                    break;
2250
            }
2251
        }
2252
2253 678
        return [$workbookBasename, $xmlNamespaceBase];
2254
    }
2255
2256 656
    private function readSheetProtection(Worksheet $docSheet, SimpleXMLElement $xmlSheet): void
2257
    {
2258 656
        if ($this->readDataOnly || !$xmlSheet->sheetProtection) {
2259 599
            return;
2260
        }
2261
2262 67
        $algorithmName = (string) $xmlSheet->sheetProtection['algorithmName'];
2263 67
        $protection = $docSheet->getProtection();
2264 67
        $protection->setAlgorithm($algorithmName);
2265
2266 67
        if ($algorithmName) {
2267 2
            $protection->setPassword((string) $xmlSheet->sheetProtection['hashValue'], true);
2268 2
            $protection->setSalt((string) $xmlSheet->sheetProtection['saltValue']);
2269 2
            $protection->setSpinCount((int) $xmlSheet->sheetProtection['spinCount']);
2270
        } else {
2271 66
            $protection->setPassword((string) $xmlSheet->sheetProtection['password'], true);
2272
        }
2273
2274 67
        if ($xmlSheet->protectedRanges->protectedRange) {
2275 4
            foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) {
2276 4
                $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true, (string) $protectedRange['name'], (string) $protectedRange['securityDescriptor']);
2277
            }
2278
        }
2279
    }
2280
2281 664
    private function readAutoFilter(
2282
        SimpleXMLElement $xmlSheet,
2283
        Worksheet $docSheet
2284
    ): void {
2285 664
        if ($xmlSheet && $xmlSheet->autoFilter) {
2286 18
            (new AutoFilter($docSheet, $xmlSheet))->load();
2287
        }
2288
    }
2289
2290 664
    private function readBackgroundImage(
2291
        SimpleXMLElement $xmlSheet,
2292
        Worksheet $docSheet,
2293
        string $relsName
2294
    ): void {
2295 664
        if ($xmlSheet && $xmlSheet->picture) {
2296 1
            $id = (string) self::getArrayItemString(self::getAttributes($xmlSheet->picture, Namespaces::SCHEMA_OFFICE_DOCUMENT), 'id');
2297 1
            $rels = $this->loadZip($relsName);
2298 1
            foreach ($rels->Relationship as $rel) {
2299 1
                $attrs = $rel->attributes() ?? [];
2300 1
                $rid = (string) ($attrs['Id'] ?? '');
2301 1
                $target = (string) ($attrs['Target'] ?? '');
2302 1
                if ($rid === $id && substr($target, 0, 2) === '..') {
2303 1
                    $target = 'xl' . substr($target, 2);
2304 1
                    $content = $this->getFromZipArchive($this->zip, $target);
2305 1
                    $docSheet->setBackgroundImage($content);
2306
                }
2307
            }
2308
        }
2309
    }
2310
2311 666
    private function readTables(
2312
        SimpleXMLElement $xmlSheet,
2313
        Worksheet $docSheet,
2314
        string $dir,
2315
        string $fileWorksheet,
2316
        ZipArchive $zip,
2317
        string $namespaceTable,
2318
        array $tableStyles,
2319
        array $dxfs
2320
    ): void {
2321 666
        if ($xmlSheet && $xmlSheet->tableParts) {
2322 37
            $attributes = $xmlSheet->tableParts->attributes() ?? ['count' => 0];
2323 37
            if (((int) $attributes['count']) > 0) {
2324 33
                $this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet, $namespaceTable, $tableStyles, $dxfs);
2325
            }
2326
        }
2327
    }
2328
2329 33
    private function readTablesInTablesFile(
2330
        SimpleXMLElement $xmlSheet,
2331
        string $dir,
2332
        string $fileWorksheet,
2333
        ZipArchive $zip,
2334
        Worksheet $docSheet,
2335
        string $namespaceTable,
2336
        array $tableStyles,
2337
        array $dxfs
2338
    ): void {
2339 33
        foreach ($xmlSheet->tableParts->tablePart as $tablePart) {
2340 33
            $relation = self::getAttributes($tablePart, Namespaces::SCHEMA_OFFICE_DOCUMENT);
2341 33
            $tablePartRel = (string) $relation['id'];
2342 33
            $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
2343
2344 33
            if ($zip->locateName($relationsFileName) !== false) {
2345 33
                $relsTableReferences = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS);
2346 33
                foreach ($relsTableReferences->Relationship as $relationship) {
2347 33
                    $relationshipAttributes = self::getAttributes($relationship, '');
2348
2349 33
                    if ((string) $relationshipAttributes['Id'] === $tablePartRel) {
2350 33
                        $relationshipFileName = (string) $relationshipAttributes['Target'];
2351 33
                        $relationshipFilePath = dirname("$dir/$fileWorksheet") . '/' . $relationshipFileName;
2352 33
                        $relationshipFilePath = File::realpath($relationshipFilePath);
2353
2354 33
                        if ($this->fileExistsInArchive($this->zip, $relationshipFilePath)) {
2355 33
                            $tableXml = $this->loadZip($relationshipFilePath, $namespaceTable);
2356 33
                            (new TableReader($docSheet, $tableXml))->load($tableStyles, $dxfs);
2357
                        }
2358
                    }
2359
                }
2360
            }
2361
        }
2362
    }
2363
2364 674
    private static function extractStyles(?SimpleXMLElement $sxml, string $node1, string $node2): array
2365
    {
2366 674
        $array = [];
2367 674
        if ($sxml && $sxml->{$node1}->{$node2}) {
2368 674
            foreach ($sxml->{$node1}->{$node2} as $node) {
2369 674
                $array[] = $node;
2370
            }
2371
        }
2372
2373 674
        return $array;
2374
    }
2375
2376 674
    private static function extractPalette(?SimpleXMLElement $sxml): array
2377
    {
2378 674
        $array = [];
2379 674
        if ($sxml && $sxml->colors->indexedColors) {
2380 15
            foreach ($sxml->colors->indexedColors->rgbColor as $node) {
2381 15
                $attr = $node->attributes();
2382 15
                if (isset($attr['rgb'])) {
2383 15
                    $array[] = (string) $attr['rgb'];
2384
                }
2385
            }
2386
        }
2387
2388 674
        return $array;
2389
    }
2390
2391 4
    private function processIgnoredErrors(SimpleXMLElement $xml, Worksheet $sheet): void
2392
    {
2393 4
        $cellCollection = $sheet->getCellCollection();
2394 4
        $attributes = self::getAttributes($xml);
2395 4
        $sqref = (string) ($attributes['sqref'] ?? '');
2396 4
        $numberStoredAsText = (string) ($attributes['numberStoredAsText'] ?? '');
2397 4
        $formula = (string) ($attributes['formula'] ?? '');
2398 4
        $formulaRange = (string) ($attributes['formulaRange'] ?? '');
2399 4
        $twoDigitTextYear = (string) ($attributes['twoDigitTextYear'] ?? '');
2400 4
        $evalError = (string) ($attributes['evalError'] ?? '');
2401 4
        if (!empty($sqref)) {
2402 4
            $explodedSqref = explode(' ', $sqref);
2403 4
            $pattern1 = '/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/';
2404 4
            foreach ($explodedSqref as $sqref1) {
2405 4
                if (preg_match($pattern1, $sqref1, $matches) === 1) {
2406 4
                    $firstRow = $matches[2];
2407 4
                    $firstCol = $matches[1];
2408 4
                    if (array_key_exists(3, $matches)) {
2409
                        // https://github.com/phpstan/phpstan/issues/11602
2410 3
                        $lastCol = $matches[4]; // @phpstan-ignore-line
2411 3
                        $lastRow = $matches[5]; // @phpstan-ignore-line
2412
                    } else {
2413 3
                        $lastCol = $firstCol;
2414 3
                        $lastRow = $firstRow;
2415
                    }
2416 4
                    ++$lastCol;
2417 4
                    for ($row = $firstRow; $row <= $lastRow; ++$row) {
2418 4
                        for ($col = $firstCol; $col !== $lastCol; ++$col) {
2419 4
                            if (!$cellCollection->has2("$col$row")) {
2420 1
                                continue;
2421
                            }
2422 4
                            if ($numberStoredAsText === '1') {
2423 4
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setNumberStoredAsText(true);
2424
                            }
2425 4
                            if ($formula === '1') {
2426 1
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setFormula(true);
2427
                            }
2428 4
                            if ($formulaRange === '1') {
2429 1
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setFormulaRange(true);
2430
                            }
2431 4
                            if ($twoDigitTextYear === '1') {
2432 1
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setTwoDigitTextYear(true);
2433
                            }
2434 4
                            if ($evalError === '1') {
2435 1
                                $sheet->getCell("$col$row")->getIgnoredErrors()->setEvalError(true);
2436
                            }
2437
                        }
2438
                    }
2439
                }
2440
            }
2441
        }
2442
    }
2443
2444 361
    private static function storeFormulaAttributes(SimpleXMLElement $f, Worksheet $docSheet, string $r): void
2445
    {
2446 361
        $formulaAttributes = [];
2447 361
        $attributes = $f->attributes();
2448 361
        if (isset($attributes['t'])) {
2449 247
            $formulaAttributes['t'] = (string) $attributes['t'];
2450
        }
2451 361
        if (isset($attributes['ref'])) {
2452 247
            $formulaAttributes['ref'] = (string) $attributes['ref'];
2453
        }
2454 361
        if (!empty($formulaAttributes)) {
2455 247
            $docSheet->getCell($r)->setFormulaAttributes($formulaAttributes);
2456
        }
2457
    }
2458
2459 13
    private static function onlyNoteVml(string $data): bool
2460
    {
2461 13
        $data = str_replace('<br>', '<br/>', $data);
2462
2463
        try {
2464 13
            $sxml = @simplexml_load_string($data);
2465
        } catch (Throwable) {
2466
            $sxml = false;
2467
        }
2468
2469 13
        if ($sxml === false) {
2470 1
            return false;
2471
        }
2472 12
        $shapes = $sxml->children(Namespaces::URN_VML);
2473 12
        foreach ($shapes->shape as $shape) {
2474 12
            $clientData = $shape->children(Namespaces::URN_EXCEL);
2475 12
            if (!isset($clientData->ClientData)) {
2476
                return false;
2477
            }
2478 12
            $attrs = $clientData->ClientData->attributes();
2479 12
            if (!isset($attrs['ObjectType'])) {
2480
                return false;
2481
            }
2482 12
            $objectType = (string) $attrs['ObjectType'];
2483 12
            if ($objectType !== 'Note') {
2484 4
                return false;
2485
            }
2486
        }
2487
2488 9
        return true;
2489
    }
2490
}
2491