Failed Conditions
Push — master ( 11f758...e55052 )
by Adrien
18:18 queued 05:56
created

Xlsx::parseRichText()   D

Complexity

Conditions 26
Paths 3

Size

Total Lines 80
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 31.0666

Importance

Changes 0
Metric Value
cc 26
eloc 51
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 80
rs 4.1666
ccs 37
cts 46
cp 0.8043
crap 31.0666

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Reader;
4
5
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
6
use PhpOffice\PhpSpreadsheet\Cell\DataType;
7
use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
8
use PhpOffice\PhpSpreadsheet\DefinedName;
9
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
10
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\AutoFilter;
11
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Chart;
12
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\ColumnAndRowAttributes;
13
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\ConditionalStyles;
14
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\DataValidations;
15
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Hyperlinks;
16
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
17
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\PageSetup;
18
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Properties as PropertyReader;
19
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViewOptions;
20
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViews;
21
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles;
22
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
23
use PhpOffice\PhpSpreadsheet\RichText\RichText;
24
use PhpOffice\PhpSpreadsheet\Settings;
25
use PhpOffice\PhpSpreadsheet\Shared\Date;
26
use PhpOffice\PhpSpreadsheet\Shared\Drawing;
27
use PhpOffice\PhpSpreadsheet\Shared\File;
28
use PhpOffice\PhpSpreadsheet\Shared\Font;
29
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
30
use PhpOffice\PhpSpreadsheet\Spreadsheet;
31
use PhpOffice\PhpSpreadsheet\Style\Color;
32
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
33
use PhpOffice\PhpSpreadsheet\Style\Style;
34
use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing;
35
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
36
use SimpleXMLElement;
37
use stdClass;
38
use Throwable;
39
use XMLReader;
40
use ZipArchive;
41
42
class Xlsx extends BaseReader
43
{
44
    const INITIAL_FILE = '_rels/.rels';
45
46
    /**
47
     * ReferenceHelper instance.
48
     *
49
     * @var ReferenceHelper
50
     */
51
    private $referenceHelper;
52
53
    /**
54
     * Xlsx\Theme instance.
55
     *
56
     * @var Xlsx\Theme
57
     */
58
    private static $theme;
59
60
    /**
61
     * @var ZipArchive
62
     */
63
    private $zip;
64
65
    /**
66
     * Create a new Xlsx Reader instance.
67
     */
68 212
    public function __construct()
69
    {
70 212
        parent::__construct();
71 212
        $this->referenceHelper = ReferenceHelper::getInstance();
72 212
        $this->securityScanner = XmlScanner::getInstance($this);
73 212
    }
74
75
    /**
76
     * Can the current IReader read the file?
77
     */
78 17
    public function canRead(string $pFilename): bool
79
    {
80 17
        if (!File::testFileNoThrow($pFilename, self::INITIAL_FILE)) {
81 7
            return false;
82
        }
83
84 10
        $result = false;
85 10
        $this->zip = $zip = new ZipArchive();
86
87 10
        if ($zip->open($pFilename) === true) {
88 10
            [$workbookBasename] = $this->getWorkbookBaseName();
89 10
            $result = !empty($workbookBasename);
90
91 10
            $zip->close();
92
        }
93
94 10
        return $result;
95
    }
96
97
    /**
98
     * @param mixed $value
99
     */
100 192
    public static function testSimpleXml($value): SimpleXMLElement
101
    {
102 192
        return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><root></root>');
103
    }
104
105 192
    public static function getAttributes(?SimpleXMLElement $value, string $ns = ''): SimpleXMLElement
106
    {
107 192
        return self::testSimpleXml($value === null ? $value : $value->attributes($ns));
108
    }
109
110
    // Phpstan thinks, correctly, that xpath can return false.
111
    // Scrutinizer thinks it can't.
112
    // Sigh.
113 175
    private static function xpathNoFalse(SimpleXmlElement $sxml, string $path): array
114
    {
115 175
        return self::falseToArray($sxml->xpath($path));
116
    }
117
118
    /**
119
     * @param mixed $value
120
     */
121 175
    public static function falseToArray($value): array
122
    {
123 175
        return is_array($value) ? $value : [];
124
    }
125
126 192
    private function loadZip(string $filename, string $ns = ''): SimpleXMLElement
127
    {
128 192
        $contents = $this->getFromZipArchive($this->zip, $filename);
129 192
        $rels = simplexml_load_string(
130 192
            $this->securityScanner->scan($contents),
131 192
            'SimpleXMLElement',
132 192
            Settings::getLibXmlLoaderOptions(),
133
            $ns
134
        );
135
136 192
        return self::testSimpleXml($rels);
137
    }
138
139
    // This function is just to identify cases where I'm not sure
140
    // why empty namespace is required.
141 175
    private function loadZipNonamespace(string $filename, string $ns): SimpleXMLElement
142
    {
143 175
        $contents = $this->getFromZipArchive($this->zip, $filename);
144 175
        $rels = simplexml_load_string(
145 175
            $this->securityScanner->scan($contents),
146 175
            'SimpleXMLElement',
147 175
            Settings::getLibXmlLoaderOptions(),
148 175
            ($ns === '' ? $ns : '')
149
        );
150
151 175
        return self::testSimpleXml($rels);
152
    }
153
154
    private const REL_TO_MAIN = [
155
        Namespaces::PURL_OFFICE_DOCUMENT => Namespaces::PURL_MAIN,
156
    ];
157
158
    private const REL_TO_DRAWING = [
159
        Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_DRAWING,
160
    ];
161
162
    /**
163
     * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
164
     *
165
     * @param string $pFilename
166
     *
167
     * @return array
168
     */
169 11
    public function listWorksheetNames($pFilename)
170
    {
171 11
        File::assertFile($pFilename, self::INITIAL_FILE);
172
173 8
        $worksheetNames = [];
174
175 8
        $this->zip = $zip = new ZipArchive();
176 8
        $zip->open($pFilename);
177
178
        //    The files we're looking at here are small enough that simpleXML is more efficient than XMLReader
179 8
        $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
180 8
        foreach ($rels->Relationship as $relx) {
181 8
            $rel = self::getAttributes($relx);
182 8
            $relType = (string) $rel['Type'];
183 8
            $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
184 8
            if ($mainNS !== '') {
185 8
                $xmlWorkbook = $this->loadZip((string) $rel['Target'], $mainNS);
186
187 8
                if ($xmlWorkbook->sheets) {
188 8
                    foreach ($xmlWorkbook->sheets->sheet as $eleSheet) {
189
                        // Check if sheet should be skipped
190 8
                        $worksheetNames[] = (string) self::getAttributes($eleSheet)['name'];
191
                    }
192
                }
193
            }
194
        }
195
196 8
        $zip->close();
197
198 8
        return $worksheetNames;
199
    }
200
201
    /**
202
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
203
     *
204
     * @param string $pFilename
205
     *
206
     * @return array
207
     */
208 12
    public function listWorksheetInfo($pFilename)
209
    {
210 12
        File::assertFile($pFilename, self::INITIAL_FILE);
211
212 9
        $worksheetInfo = [];
213
214 9
        $this->zip = $zip = new ZipArchive();
215 9
        $zip->open($pFilename);
216
217 9
        $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
218 9
        foreach ($rels->Relationship as $relx) {
219 9
            $rel = self::getAttributes($relx);
220 9
            $relType = (string) $rel['Type'];
221 9
            $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
222 9
            if ($mainNS !== '') {
223 9
                $relTarget = (string) $rel['Target'];
224 9
                $dir = dirname($relTarget);
225 9
                $namespace = dirname($relType);
226 9
                $relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', '');
227
228 9
                $worksheets = [];
229 9
                foreach ($relsWorkbook->Relationship as $elex) {
230 9
                    $ele = self::getAttributes($elex);
231 9
                    if ((string) $ele['Type'] === "$namespace/worksheet") {
232 9
                        $worksheets[(string) $ele['Id']] = $ele['Target'];
233
                    }
234
                }
235
236 9
                $xmlWorkbook = $this->loadZip($relTarget, $mainNS);
237 9
                if ($xmlWorkbook->sheets) {
238 9
                    $dir = dirname($relTarget);
239
                    /** @var SimpleXMLElement $eleSheet */
240 9
                    foreach ($xmlWorkbook->sheets->sheet as $eleSheet) {
241
                        $tmpInfo = [
242 9
                            'worksheetName' => (string) self::getAttributes($eleSheet)['name'],
243 9
                            'lastColumnLetter' => 'A',
244 9
                            'lastColumnIndex' => 0,
245 9
                            'totalRows' => 0,
246 9
                            'totalColumns' => 0,
247
                        ];
248
249 9
                        $fileWorksheet = (string) $worksheets[(string) self::getArrayItem(self::getAttributes($eleSheet, $namespace), 'id')];
250 9
                        $fileWorksheetPath = strpos($fileWorksheet, '/') === 0 ? substr($fileWorksheet, 1) : "$dir/$fileWorksheet";
251
252 9
                        $xml = new XMLReader();
253 9
                        $xml->xml(
254 9
                            $this->securityScanner->scanFile(
255 9
                                'zip://' . File::realpath($pFilename) . '#' . $fileWorksheetPath
256
                            ),
257 9
                            null,
258 9
                            Settings::getLibXmlLoaderOptions()
259
                        );
260 9
                        $xml->setParserProperty(2, true);
261
262 9
                        $currCells = 0;
263 9
                        while ($xml->read()) {
264 9
                            if ($xml->localName == 'row' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) {
265 9
                                $row = $xml->getAttribute('r');
266 9
                                $tmpInfo['totalRows'] = $row;
267 9
                                $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
268 9
                                $currCells = 0;
269 9
                            } elseif ($xml->localName == 'c' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) {
270 9
                                $cell = $xml->getAttribute('r');
271 9
                                $currCells = $cell ? max($currCells, Coordinate::indexesFromString($cell)[0]) : ($currCells + 1);
272
                            }
273
                        }
274 9
                        $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
275 9
                        $xml->close();
276
277 9
                        $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
278 9
                        $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
279
280 9
                        $worksheetInfo[] = $tmpInfo;
281
                    }
282
                }
283
            }
284
        }
285
286 9
        $zip->close();
287
288 9
        return $worksheetInfo;
289
    }
290
291 11
    private static function castToBoolean($c)
292
    {
293 11
        $value = isset($c->v) ? (string) $c->v : null;
294 11
        if ($value == '0') {
295 6
            return false;
296 11
        } elseif ($value == '1') {
297 11
            return true;
298
        }
299
300
        return (bool) $c->v;
301
    }
302
303 3
    private static function castToError($c)
304
    {
305 3
        return isset($c->v) ? (string) $c->v : null;
306
    }
307
308 129
    private static function castToString($c)
309
    {
310 129
        return isset($c->v) ? (string) $c->v : null;
311
    }
312
313 68
    private function castToFormula($c, $r, &$cellDataType, &$value, &$calculatedValue, &$sharedFormulas, $castBaseType): void
314
    {
315 68
        $attr = $c->f->attributes();
316 68
        $cellDataType = 'f';
317 68
        $value = "={$c->f}";
318 68
        $calculatedValue = self::$castBaseType($c);
319
320
        // Shared formula?
321 68
        if (isset($attr['t']) && strtolower((string) $attr['t']) == 'shared') {
322 5
            $instance = (string) $attr['si'];
323
324 5
            if (!isset($sharedFormulas[(string) $attr['si']])) {
325 5
                $sharedFormulas[$instance] = ['master' => $r, 'formula' => $value];
326
            } else {
327 5
                $master = Coordinate::indexesFromString($sharedFormulas[$instance]['master']);
328 5
                $current = Coordinate::indexesFromString($r);
329
330 5
                $difference = [0, 0];
331 5
                $difference[0] = $current[0] - $master[0];
332 5
                $difference[1] = $current[1] - $master[1];
333
334 5
                $value = $this->referenceHelper->updateFormulaReferences($sharedFormulas[$instance]['formula'], 'A1', $difference[0], $difference[1]);
335
            }
336
        }
337 68
    }
338
339
    /**
340
     * @param string $fileName
341
     */
342 1
    private function fileExistsInArchive(ZipArchive $archive, $fileName = ''): bool
343
    {
344
        // Root-relative paths
345 1
        if (strpos($fileName, '//') !== false) {
346
            $fileName = substr($fileName, strpos($fileName, '//') + 1);
347
        }
348 1
        $fileName = File::realpath($fileName);
349
350
        // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming
351
        //    so we need to load case-insensitively from the zip file
352
353
        // Apache POI fixes
354 1
        $contents = $archive->locateName($fileName, ZipArchive::FL_NOCASE);
355 1
        if ($contents === false) {
356
            $contents = $archive->locateName(substr($fileName, 1), ZipArchive::FL_NOCASE);
357
        }
358
359 1
        return $contents !== false;
360
    }
361
362
    /**
363
     * @param string $fileName
364
     *
365
     * @return string
366
     */
367 192
    private function getFromZipArchive(ZipArchive $archive, $fileName = '')
368
    {
369
        // Root-relative paths
370 192
        if (strpos($fileName, '//') !== false) {
371 4
            $fileName = substr($fileName, strpos($fileName, '//') + 1);
372
        }
373 192
        $fileName = File::realpath($fileName);
374
375
        // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming
376
        //    so we need to load case-insensitively from the zip file
377
378
        // Apache POI fixes
379 192
        $contents = $archive->getFromName($fileName, 0, ZipArchive::FL_NOCASE);
380 192
        if ($contents === false) {
381 17
            $contents = $archive->getFromName(substr($fileName, 1), 0, ZipArchive::FL_NOCASE);
382
        }
383
384 192
        return $contents;
385
    }
386
387
    /**
388
     * Loads Spreadsheet from file.
389
     */
390 178
    public function load(string $pFilename, int $flags = 0): Spreadsheet
391
    {
392 178
        File::assertFile($pFilename, self::INITIAL_FILE);
393 175
        $this->processFlags($flags);
394
395
        // Initialisations
396 175
        $excel = new Spreadsheet();
397 175
        $excel->removeSheetByIndex(0);
398 175
        $addingFirstCellStyleXf = true;
399 175
        $addingFirstCellXf = true;
400
401 175
        $unparsedLoadedData = [];
402
403 175
        $this->zip = $zip = new ZipArchive();
404 175
        $zip->open($pFilename);
405
406
        //    Read the theme first, because we need the colour scheme when reading the styles
407 175
        [$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName();
408 175
        $wbRels = $this->loadZip("xl/_rels/${workbookBasename}.rels", Namespaces::RELATIONSHIPS);
409 175
        foreach ($wbRels->Relationship as $relx) {
410 175
            $rel = self::getAttributes($relx);
411 175
            $relTarget = (string) $rel['Target'];
412 175
            switch ($rel['Type']) {
413 175
                case "$xmlNamespaceBase/theme":
414 175
                    $themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2'];
415 175
                    $themeOrderAdditional = count($themeOrderArray);
416 175
                    $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML;
417
418 175
                    $xmlTheme = $this->loadZip("xl/{$relTarget}", $drawingNS);
419 175
                    $xmlThemeName = self::getAttributes($xmlTheme);
420 175
                    $xmlTheme = $xmlTheme->children($drawingNS);
421 175
                    $themeName = (string) $xmlThemeName['name'];
422
423 175
                    $colourScheme = self::getAttributes($xmlTheme->themeElements->clrScheme);
424 175
                    $colourSchemeName = (string) $colourScheme['name'];
425 175
                    $colourScheme = $xmlTheme->themeElements->clrScheme->children($drawingNS);
426
427 175
                    $themeColours = [];
428 175
                    foreach ($colourScheme as $k => $xmlColour) {
429 175
                        $themePos = array_search($k, $themeOrderArray);
430 175
                        if ($themePos === false) {
431 175
                            $themePos = $themeOrderAdditional++;
432
                        }
433 175
                        if (isset($xmlColour->sysClr)) {
434 174
                            $xmlColourData = self::getAttributes($xmlColour->sysClr);
435 174
                            $themeColours[$themePos] = (string) $xmlColourData['lastClr'];
436 175
                        } elseif (isset($xmlColour->srgbClr)) {
437 175
                            $xmlColourData = self::getAttributes($xmlColour->srgbClr);
438 175
                            $themeColours[$themePos] = (string) $xmlColourData['val'];
439
                        }
440
                    }
441 175
                    self::$theme = new Xlsx\Theme($themeName, $colourSchemeName, $themeColours);
442
443 175
                    break;
444
            }
445
        }
446
447 175
        $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
448
449 175
        $propertyReader = new PropertyReader($this->securityScanner, $excel->getProperties());
450 175
        foreach ($rels->Relationship as $relx) {
451 175
            $rel = self::getAttributes($relx);
452 175
            $relTarget = (string) $rel['Target'];
453 175
            $relType = (string) $rel['Type'];
454 175
            $mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
455
            switch ($relType) {
456 175
                case Namespaces::CORE_PROPERTIES:
457 173
                    $propertyReader->readCoreProperties($this->getFromZipArchive($zip, $relTarget));
458
459 173
                    break;
460 175
                case "$xmlNamespaceBase/extended-properties":
461 172
                    $propertyReader->readExtendedProperties($this->getFromZipArchive($zip, $relTarget));
462
463 172
                    break;
464 175
                case "$xmlNamespaceBase/custom-properties":
465 8
                    $propertyReader->readCustomProperties($this->getFromZipArchive($zip, $relTarget));
466
467 8
                    break;
468
                //Ribbon
469 175
                case Namespaces::EXTENSIBILITY:
470
                    $customUI = $relTarget;
471
                    if ($customUI) {
472
                        $this->readRibbon($excel, $customUI, $zip);
473
                    }
474
475
                    break;
476 175
                case "$xmlNamespaceBase/officeDocument":
477 175
                    $dir = dirname($relTarget);
478
479
                    // Do not specify namespace in next stmt - do it in Xpath
480 175
                    $relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', '');
481 175
                    $relsWorkbook->registerXPathNamespace('rel', Namespaces::RELATIONSHIPS);
482
483 175
                    $sharedStrings = [];
484
                    $relType = "rel:Relationship[@Type='"
485
                        //. Namespaces::SHARED_STRINGS
486 175
                        . "$xmlNamespaceBase/sharedStrings"
487 175
                        . "']";
488 175
                    $xpath = self::getArrayItem($relsWorkbook->xpath($relType));
489
490 175
                    if ($xpath) {
491 160
                        $xmlStrings = $this->loadZip("$dir/$xpath[Target]", $mainNS);
492 160
                        if (isset($xmlStrings->si)) {
493 115
                            foreach ($xmlStrings->si as $val) {
494 115
                                if (isset($val->t)) {
495 115
                                    $sharedStrings[] = StringHelper::controlCharacterOOXML2PHP((string) $val->t);
496 5
                                } elseif (isset($val->r)) {
497 5
                                    $sharedStrings[] = $this->parseRichText($val);
498
                                }
499
                            }
500
                        }
501
                    }
502
503 175
                    $worksheets = [];
504 175
                    $macros = $customUI = null;
505 175
                    foreach ($relsWorkbook->Relationship as $elex) {
506 175
                        $ele = self::getAttributes($elex);
507 175
                        switch ($ele['Type']) {
508 175
                            case Namespaces::WORKSHEET:
509 175
                            case Namespaces::PURL_WORKSHEET:
510 175
                                $worksheets[(string) $ele['Id']] = $ele['Target'];
511
512 175
                                break;
513
                            // a vbaProject ? (: some macros)
514 175
                            case Namespaces::VBA:
515 1
                                $macros = $ele['Target'];
516
517 1
                                break;
518
                        }
519
                    }
520
521 175
                    if ($macros !== null) {
522 1
                        $macrosCode = $this->getFromZipArchive($zip, 'xl/vbaProject.bin'); //vbaProject.bin always in 'xl' dir and always named vbaProject.bin
523 1
                        if ($macrosCode !== false) {
524 1
                            $excel->setMacrosCode($macrosCode);
525 1
                            $excel->setHasMacros(true);
526
                            //short-circuit : not reading vbaProject.bin.rel to get Signature =>allways vbaProjectSignature.bin in 'xl' dir
527 1
                            $Certificate = $this->getFromZipArchive($zip, 'xl/vbaProjectSignature.bin');
528 1
                            if ($Certificate !== false) {
529
                                $excel->setMacrosCertificate($Certificate);
530
                            }
531
                        }
532
                    }
533
534
                    $relType = "rel:Relationship[@Type='"
535 175
                        . "$xmlNamespaceBase/styles"
536 175
                        . "']";
537 175
                    $xpath = self::getArrayItem(self::xpathNoFalse($relsWorkbook, $relType));
538
539 175
                    if ($xpath === null) {
540 1
                        $xmlStyles = self::testSimpleXml(null);
541
                    } else {
542
                        // I think Nonamespace is okay because I'm using xpath.
543 175
                        $xmlStyles = $this->loadZipNonamespace("$dir/$xpath[Target]", $mainNS);
544
                    }
545
546 175
                    $xmlStyles->registerXPathNamespace('smm', Namespaces::MAIN);
547 175
                    $fills = self::xpathNoFalse($xmlStyles, 'smm:fills/smm:fill');
548 175
                    $fonts = self::xpathNoFalse($xmlStyles, 'smm:fonts/smm:font');
549 175
                    $borders = self::xpathNoFalse($xmlStyles, 'smm:borders/smm:border');
550 175
                    $xfTags = self::xpathNoFalse($xmlStyles, 'smm:cellXfs/smm:xf');
551 175
                    $cellXfTags = self::xpathNoFalse($xmlStyles, 'smm:cellStyleXfs/smm:xf');
552
553 175
                    $styles = [];
554 175
                    $cellStyles = [];
555 175
                    $numFmts = null;
556 175
                    if (/*$xmlStyles && */ $xmlStyles->numFmts[0]) {
557 99
                        $numFmts = $xmlStyles->numFmts[0];
558
                    }
559 175
                    if (isset($numFmts) && ($numFmts !== null)) {
560 99
                        $numFmts->registerXPathNamespace('sml', $mainNS);
561
                    }
562 175
                    if (!$this->readDataOnly/* && $xmlStyles*/) {
563 175
                        foreach ($xfTags as $xfTag) {
564 174
                            $xf = self::getAttributes($xfTag);
565 174
                            $numFmt = null;
566
567 174
                            if ($xf['numFmtId']) {
568 174
                                if (isset($numFmts)) {
569 98
                                    $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]"));
570
571 98
                                    if (isset($tmpNumFmt['formatCode'])) {
572 14
                                        $numFmt = (string) $tmpNumFmt['formatCode'];
573
                                    }
574
                                }
575
576
                                // We shouldn't override any of the built-in MS Excel values (values below id 164)
577
                                //  But there's a lot of naughty homebrew xlsx writers that do use "reserved" id values that aren't actually used
578
                                //  So we make allowance for them rather than lose formatting masks
579
                                if (
580 174
                                    $numFmt === null &&
581 174
                                    (int) $xf['numFmtId'] < 164 &&
582 174
                                    NumberFormat::builtInFormatCode((int) $xf['numFmtId']) !== ''
583
                                ) {
584 174
                                    $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
585
                                }
586
                            }
587 174
                            $quotePrefix = (bool) ($xf['quotePrefix'] ?? false);
588
589
                            $style = (object) [
590 174
                                'numFmt' => $numFmt ?? NumberFormat::FORMAT_GENERAL,
591 174
                                'font' => $fonts[(int) ($xf['fontId'])],
592 174
                                'fill' => $fills[(int) ($xf['fillId'])],
593 174
                                'border' => $borders[(int) ($xf['borderId'])],
594 174
                                'alignment' => $xfTag->alignment,
595 174
                                'protection' => $xfTag->protection,
596 174
                                'quotePrefix' => $quotePrefix,
597
                            ];
598 174
                            $styles[] = $style;
599
600
                            // add style to cellXf collection
601 174
                            $objStyle = new Style();
602 174
                            self::readStyle($objStyle, $style);
603 174
                            if ($addingFirstCellXf) {
604 174
                                $excel->removeCellXfByIndex(0); // remove the default style
605 174
                                $addingFirstCellXf = false;
606
                            }
607 174
                            $excel->addCellXf($objStyle);
608
                        }
609
610 175
                        foreach ($cellXfTags as $xfTag) {
611 174
                            $xf = self::getAttributes($xfTag);
612 174
                            $numFmt = NumberFormat::FORMAT_GENERAL;
613 174
                            if ($numFmts && $xf['numFmtId']) {
614 98
                                $tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]"));
615 98
                                if (isset($tmpNumFmt['formatCode'])) {
616 1
                                    $numFmt = (string) $tmpNumFmt['formatCode'];
617 98
                                } elseif ((int) $xf['numFmtId'] < 165) {
618 98
                                    $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
619
                                }
620
                            }
621
622 174
                            $quotePrefix = (bool) ($xf['quotePrefix'] ?? false);
623
624
                            $cellStyle = (object) [
625 174
                                'numFmt' => $numFmt,
626 174
                                'font' => $fonts[(int) ($xf['fontId'])],
627 174
                                'fill' => $fills[((int) $xf['fillId'])],
628 174
                                'border' => $borders[(int) ($xf['borderId'])],
629 174
                                'alignment' => $xfTag->alignment,
630 174
                                'protection' => $xfTag->protection,
631 174
                                'quotePrefix' => $quotePrefix,
632
                            ];
633 174
                            $cellStyles[] = $cellStyle;
634
635
                            // add style to cellStyleXf collection
636 174
                            $objStyle = new Style();
637 174
                            self::readStyle($objStyle, $cellStyle);
638 174
                            if ($addingFirstCellStyleXf) {
639 174
                                $excel->removeCellStyleXfByIndex(0); // remove the default style
640 174
                                $addingFirstCellStyleXf = false;
641
                            }
642 174
                            $excel->addCellStyleXf($objStyle);
643
                        }
644
                    }
645 175
                    $styleReader = new Styles($xmlStyles);
646 175
                    $styleReader->setStyleBaseData(self::$theme, $styles, $cellStyles);
647 175
                    $dxfs = $styleReader->dxfs($this->readDataOnly);
648 175
                    $styles = $styleReader->styles();
649
650 175
                    $xmlWorkbook = $this->loadZipNoNamespace($relTarget, $mainNS);
651 175
                    $xmlWorkbookNS = $this->loadZip($relTarget, $mainNS);
652
653
                    // Set base date
654 175
                    if ($xmlWorkbookNS->workbookPr) {
655 174
                        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
656 174
                        $attrs1904 = self::getAttributes($xmlWorkbookNS->workbookPr);
657 174
                        if (isset($attrs1904['date1904'])) {
658
                            if (self::boolean((string) $attrs1904['date1904'])) {
659
                                Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
660
                            }
661
                        }
662
                    }
663
664
                    // Set protection
665 175
                    $this->readProtection($excel, $xmlWorkbook);
666
667 175
                    $sheetId = 0; // keep track of new sheet id in final workbook
668 175
                    $oldSheetId = -1; // keep track of old sheet id in final workbook
669 175
                    $countSkippedSheets = 0; // keep track of number of skipped sheets
670 175
                    $mapSheetId = []; // mapping of sheet ids from old to new
671
672 175
                    $charts = $chartDetails = [];
673
674 175
                    if ($xmlWorkbookNS->sheets) {
675
                        /** @var SimpleXMLElement $eleSheet */
676 175
                        foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) {
677 175
                            $eleSheetAttr = self::getAttributes($eleSheet);
678 175
                            ++$oldSheetId;
679
680
                            // Check if sheet should be skipped
681 175
                            if (is_array($this->loadSheetsOnly) && !in_array((string) $eleSheetAttr['name'], $this->loadSheetsOnly)) {
682 1
                                ++$countSkippedSheets;
683 1
                                $mapSheetId[$oldSheetId] = null;
684
685 1
                                continue;
686
                            }
687
688
                            // Map old sheet id in original workbook to new sheet id.
689
                            // They will differ if loadSheetsOnly() is being used
690 175
                            $mapSheetId[$oldSheetId] = $oldSheetId - $countSkippedSheets;
691
692
                            // Load sheet
693 175
                            $docSheet = $excel->createSheet();
694
                            //    Use false for $updateFormulaCellReferences to prevent adjustment of worksheet
695
                            //        references in formula cells... during the load, all formulae should be correct,
696
                            //        and we're simply bringing the worksheet name in line with the formula, not the
697
                            //        reverse
698 175
                            $docSheet->setTitle((string) $eleSheetAttr['name'], false, false);
699 175
                            $fileWorksheet = (string) $worksheets[(string) self::getArrayItem(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id')];
700 175
                            $xmlSheet = $this->loadZipNoNamespace("$dir/$fileWorksheet", $mainNS);
701 175
                            $xmlSheetNS = $this->loadZip("$dir/$fileWorksheet", $mainNS);
702
703 175
                            $sharedFormulas = [];
704
705 175
                            if (isset($eleSheetAttr['state']) && (string) $eleSheetAttr['state'] != '') {
706 5
                                $docSheet->setSheetState((string) $eleSheetAttr['state']);
707
                            }
708 175
                            if ($xmlSheetNS) {
709 175
                                $xmlSheetMain = $xmlSheetNS->children($mainNS);
710
                                // Setting Conditional Styles adjusts selected cells, so we need to execute this
711
                                //    before reading the sheet view data to get the actual selected cells
712 175
                                if (!$this->readDataOnly && $xmlSheet->conditionalFormatting) {
713 16
                                    (new ConditionalStyles($docSheet, $xmlSheet, $dxfs))->load();
714
                                }
715 175
                                if (isset($xmlSheetMain->sheetViews, $xmlSheetMain->sheetViews->sheetView)) {
716 175
                                    $sheetViews = new SheetViews($xmlSheetMain->sheetViews->sheetView, $docSheet);
717 175
                                    $sheetViews->load();
718
                                }
719
720 175
                                $sheetViewOptions = new SheetViewOptions($docSheet, $xmlSheet);
721 175
                                $sheetViewOptions->load($this->getReadDataOnly());
722
723 175
                                (new ColumnAndRowAttributes($docSheet, $xmlSheet))
724 175
                                    ->load($this->getReadFilter(), $this->getReadDataOnly());
725
                            }
726
727 175
                            if ($xmlSheetNS && $xmlSheetNS->sheetData && $xmlSheetNS->sheetData->row) {
728 164
                                $cIndex = 1; // Cell Start from 1
729 164
                                foreach ($xmlSheetNS->sheetData->row as $row) {
730 164
                                    $rowIndex = 1;
731 164
                                    foreach ($row->c as $c) {
732 164
                                        $cAttr = self::getAttributes($c);
733 164
                                        $r = (string) $cAttr['r'];
734 164
                                        if ($r == '') {
735 1
                                            $r = Coordinate::stringFromColumnIndex($rowIndex) . $cIndex;
736
                                        }
737 164
                                        $cellDataType = (string) $cAttr['t'];
738 164
                                        $value = null;
739 164
                                        $calculatedValue = null;
740
741
                                        // Read cell?
742 164
                                        if ($this->getReadFilter() !== null) {
743 164
                                            $coordinates = Coordinate::coordinateFromString($r);
744
745 164
                                            if (!$this->getReadFilter()->readCell($coordinates[0], (int) $coordinates[1], $docSheet->getTitle())) {
746 3
                                                if (isset($cAttr->f)) {
747
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToError');
748
                                                }
749 3
                                                ++$rowIndex;
750
751 3
                                                continue;
752
                                            }
753
                                        }
754
755
                                        // Read cell!
756
                                        switch ($cellDataType) {
757 164
                                            case 's':
758 115
                                                if ((string) $c->v != '') {
759 115
                                                    $value = $sharedStrings[(int) ($c->v)];
760
761 115
                                                    if ($value instanceof RichText) {
762 115
                                                        $value = clone $value;
763
                                                    }
764
                                                } else {
765 2
                                                    $value = '';
766
                                                }
767
768 115
                                                break;
769 134
                                            case 'b':
770 11
                                                if (!isset($c->f)) {
771 9
                                                    $value = self::castToBoolean($c);
772
                                                } else {
773
                                                    // Formula
774 2
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToBoolean');
775 2
                                                    if (isset($c->f['t'])) {
776
                                                        $att = $c->f;
777
                                                        $docSheet->getCell($r)->setFormulaAttributes($att);
778
                                                    }
779
                                                }
780
781 11
                                                break;
782 130
                                            case 'inlineStr':
783 5
                                                if (isset($c->f)) {
784
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToError');
785
                                                } else {
786 5
                                                    $value = $this->parseRichText($c->is);
787
                                                }
788
789 5
                                                break;
790 129
                                            case 'e':
791 3
                                                if (!isset($c->f)) {
792
                                                    $value = self::castToError($c);
793
                                                } else {
794
                                                    // Formula
795 3
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToError');
796
                                                }
797
798 3
                                                break;
799
                                            default:
800 129
                                                if (!isset($c->f)) {
801 124
                                                    $value = self::castToString($c);
802
                                                } else {
803
                                                    // Formula
804 67
                                                    $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToString');
805 67
                                                    if (isset($c->f['t'])) {
806
                                                        $attributes = $c->f['t'];
807
                                                        $docSheet->getCell($r)->setFormulaAttributes(['t' => (string) $attributes]);
808
                                                    }
809
                                                }
810
811 129
                                                break;
812
                                        }
813
814
                                        // read empty cells or the cells are not empty
815 164
                                        if ($this->readEmptyCells || ($value !== null && $value !== '')) {
816
                                            // Rich text?
817 164
                                            if ($value instanceof RichText && $this->readDataOnly) {
818
                                                $value = $value->getPlainText();
819
                                            }
820
821 164
                                            $cell = $docSheet->getCell($r);
822
                                            // Assign value
823 164
                                            if ($cellDataType != '') {
824
                                                // it is possible, that datatype is numeric but with an empty string, which result in an error
825 129
                                                if ($cellDataType === DataType::TYPE_NUMERIC && $value === '') {
826
                                                    $cellDataType = DataType::TYPE_STRING;
827
                                                }
828 129
                                                $cell->setValueExplicit($value, $cellDataType);
829
                                            } else {
830 124
                                                $cell->setValue($value);
831
                                            }
832 164
                                            if ($calculatedValue !== null) {
833 68
                                                $cell->setCalculatedValue($calculatedValue);
834
                                            }
835
836
                                            // Style information?
837 164
                                            if ($cAttr['s'] && !$this->readDataOnly) {
838
                                                // no style index means 0, it seems
839 54
                                                $cell->setXfIndex(isset($styles[(int) ($cAttr['s'])]) ?
840 54
                                                    (int) ($cAttr['s']) : 0);
841
                                            }
842
                                        }
843 164
                                        ++$rowIndex;
844
                                    }
845 164
                                    ++$cIndex;
846
                                }
847
                            }
848
849 175
                            $aKeys = ['sheet', 'objects', 'scenarios', 'formatCells', 'formatColumns', 'formatRows', 'insertColumns', 'insertRows', 'insertHyperlinks', 'deleteColumns', 'deleteRows', 'selectLockedCells', 'sort', 'autoFilter', 'pivotTables', 'selectUnlockedCells'];
850 175
                            if (!$this->readDataOnly && $xmlSheet && $xmlSheet->sheetProtection) {
851 121
                                foreach ($aKeys as $key) {
852 121
                                    $method = 'set' . ucfirst($key);
853 121
                                    $docSheet->getProtection()->$method(self::boolean((string) $xmlSheet->sheetProtection[$key]));
854
                                }
855
                            }
856
857 175
                            if ($xmlSheet) {
858 171
                                $this->readSheetProtection($docSheet, $xmlSheet);
859
                            }
860
861 175
                            if ($this->readDataOnly === false) {
862 175
                                $this->readAutoFilterTables($xmlSheet, $docSheet, $dir, $fileWorksheet, $zip);
863
                            }
864
865 175
                            if ($xmlSheet && $xmlSheet->mergeCells && $xmlSheet->mergeCells->mergeCell && !$this->readDataOnly) {
866 11
                                foreach ($xmlSheet->mergeCells->mergeCell as $mergeCell) {
867 11
                                    $mergeRef = (string) $mergeCell['ref'];
868 11
                                    if (strpos($mergeRef, ':') !== false) {
869 11
                                        $docSheet->mergeCells((string) $mergeCell['ref']);
870
                                    }
871
                                }
872
                            }
873
874 175
                            if ($xmlSheet && !$this->readDataOnly) {
875 171
                                $unparsedLoadedData = (new PageSetup($docSheet, $xmlSheet))->load($unparsedLoadedData);
876
                            }
877
878 175
                            if ($xmlSheet !== false && isset($xmlSheet->extLst, $xmlSheet->extLst->ext, $xmlSheet->extLst->ext['uri']) && ($xmlSheet->extLst->ext['uri'] == '{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}')) {
879
                                // Create dataValidations node if does not exists, maybe is better inside the foreach ?
880 2
                                if (!$xmlSheet->dataValidations) {
881 1
                                    $xmlSheet->addChild('dataValidations');
882
                                }
883
884 2
                                foreach ($xmlSheet->extLst->ext->children('x14', true)->dataValidations->dataValidation as $item) {
885 2
                                    $node = $xmlSheet->dataValidations->addChild('dataValidation');
886 2
                                    foreach ($item->attributes() ?? [] as $attr) {
1 ignored issue
show
Bug introduced by
The method attributes() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

886
                                    foreach ($item->/** @scrutinizer ignore-call */ attributes() ?? [] as $attr) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
887 2
                                        $node->addAttribute($attr->getName(), $attr);
888
                                    }
889 2
                                    $node->addAttribute('sqref', $item->children('xm', true)->sqref);
890 2
                                    $node->addChild('formula1', $item->formula1->children('xm', true)->f);
891
                                }
892
                            }
893
894 175
                            if ($xmlSheet && $xmlSheet->dataValidations && !$this->readDataOnly) {
895 4
                                (new DataValidations($docSheet, $xmlSheet))->load();
896
                            }
897
898
                            // unparsed sheet AlternateContent
899 175
                            if ($xmlSheet && !$this->readDataOnly) {
900 171
                                $mc = $xmlSheet->children(Namespaces::COMPATIBILITY);
901 171
                                if ($mc->AlternateContent) {
902 1
                                    foreach ($mc->AlternateContent as $alternateContent) {
903 1
                                        $alternateContent = self::testSimpleXml($alternateContent);
904 1
                                        $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['AlternateContents'][] = $alternateContent->asXML();
905
                                    }
906
                                }
907
                            }
908
909
                            // Add hyperlinks
910 175
                            if (!$this->readDataOnly) {
911 175
                                $hyperlinkReader = new Hyperlinks($docSheet);
912
                                // Locate hyperlink relations
913 175
                                $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
914 175
                                if ($zip->locateName($relationsFileName)) {
915 114
                                    $relsWorksheet = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS);
916 114
                                    $hyperlinkReader->readHyperlinks($relsWorksheet);
917
                                }
918
919
                                // Loop through hyperlinks
920 175
                                if ($xmlSheetNS && $xmlSheetNS->children($mainNS)->hyperlinks) {
921 8
                                    $hyperlinkReader->setHyperlinks($xmlSheetNS->children($mainNS)->hyperlinks);
922
                                }
923
                            }
924
925
                            // Add comments
926 175
                            $comments = [];
927 175
                            $vmlComments = [];
928 175
                            if (!$this->readDataOnly) {
929
                                // Locate comment relations
930 175
                                $commentRelations = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
931 175
                                if ($zip->locateName($commentRelations)) {
932 114
                                    $relsWorksheet = $this->loadZip($commentRelations, Namespaces::RELATIONSHIPS);
933 114
                                    foreach ($relsWorksheet->Relationship as $elex) {
934 39
                                        $ele = self::getAttributes($elex);
935 39
                                        if ($ele['Type'] == Namespaces::COMMENTS) {
936 11
                                            $comments[(string) $ele['Id']] = (string) $ele['Target'];
937
                                        }
938 39
                                        if ($ele['Type'] == Namespaces::VML) {
939 12
                                            $vmlComments[(string) $ele['Id']] = (string) $ele['Target'];
940
                                        }
941
                                    }
942
                                }
943
944
                                // Loop through comments
945 175
                                foreach ($comments as $relName => $relPath) {
946
                                    // Load comments file
947 11
                                    $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath);
948
                                    // okay to ignore namespace - using xpath
949 11
                                    $commentsFile = $this->loadZip($relPath, '');
950
951
                                    // Utility variables
952 11
                                    $authors = [];
953 11
                                    $commentsFile->registerXpathNamespace('com', $mainNS);
954 11
                                    $authorPath = self::xpathNoFalse($commentsFile, 'com:authors/com:author');
955 11
                                    foreach ($authorPath as $author) {
956 11
                                        $authors[] = (string) $author;
957
                                    }
958
959
                                    // Loop through contents
960 11
                                    $contentPath = self::xpathNoFalse($commentsFile, 'com:commentList/com:comment');
961 11
                                    foreach ($contentPath as $comment) {
962 11
                                        $commentx = $comment->attributes();
963 11
                                        $commentModel = $docSheet->getComment((string) $commentx['ref']);
964 11
                                        if (isset($commentx['authorId'])) {
965 11
                                            $commentModel->setAuthor($authors[(int) $commentx['authorId']]);
966
                                        }
967 11
                                        $commentModel->setText($this->parseRichText($comment->children($mainNS)->text));
968
                                    }
969
                                }
970
971
                                // later we will remove from it real vmlComments
972 175
                                $unparsedVmlDrawings = $vmlComments;
973
974
                                // Loop through VML comments
975 175
                                foreach ($vmlComments as $relName => $relPath) {
976
                                    // Load VML comments file
977 12
                                    $relPath = File::realpath(dirname("$dir/$fileWorksheet") . '/' . $relPath);
978
979
                                    try {
980
                                        // no namespace okay - processed with Xpath
981 12
                                        $vmlCommentsFile = $this->loadZip($relPath, '');
982 12
                                        $vmlCommentsFile->registerXPathNamespace('v', Namespaces::URN_VML);
983
                                    } catch (Throwable $ex) {
984
                                        //Ignore unparsable vmlDrawings. Later they will be moved from $unparsedVmlDrawings to $unparsedLoadedData
985
                                        continue;
986
                                    }
987
988 12
                                    $shapes = self::xpathNoFalse($vmlCommentsFile, '//v:shape');
989 12
                                    foreach ($shapes as $shape) {
990 12
                                        $shape->registerXPathNamespace('v', Namespaces::URN_VML);
991
992 12
                                        if (isset($shape['style'])) {
993 12
                                            $style = (string) $shape['style'];
994 12
                                            $fillColor = strtoupper(substr((string) $shape['fillcolor'], 1));
995 12
                                            $column = null;
996 12
                                            $row = null;
997
998 12
                                            $clientData = $shape->xpath('.//x:ClientData');
999 12
                                            if (is_array($clientData) && !empty($clientData)) {
1000 12
                                                $clientData = $clientData[0];
1001
1002 12
                                                if (isset($clientData['ObjectType']) && (string) $clientData['ObjectType'] == 'Note') {
1003 11
                                                    $temp = $clientData->xpath('.//x:Row');
1004 11
                                                    if (is_array($temp)) {
1005 11
                                                        $row = $temp[0];
1006
                                                    }
1007
1008 11
                                                    $temp = $clientData->xpath('.//x:Column');
1009 11
                                                    if (is_array($temp)) {
1010 11
                                                        $column = $temp[0];
1011
                                                    }
1012
                                                }
1013
                                            }
1014
1015 12
                                            if (($column !== null) && ($row !== null)) {
1016
                                                // Set comment properties
1017 11
                                                $comment = $docSheet->getCommentByColumnAndRow($column + 1, $row + 1);
1018 11
                                                $comment->getFillColor()->setRGB($fillColor);
1019
1020
                                                // Parse style
1021 11
                                                $styleArray = explode(';', str_replace(' ', '', $style));
1022 11
                                                foreach ($styleArray as $stylePair) {
1023 11
                                                    $stylePair = explode(':', $stylePair);
1024
1025 11
                                                    if ($stylePair[0] == 'margin-left') {
1026 11
                                                        $comment->setMarginLeft($stylePair[1]);
1027
                                                    }
1028 11
                                                    if ($stylePair[0] == 'margin-top') {
1029 11
                                                        $comment->setMarginTop($stylePair[1]);
1030
                                                    }
1031 11
                                                    if ($stylePair[0] == 'width') {
1032 11
                                                        $comment->setWidth($stylePair[1]);
1033
                                                    }
1034 11
                                                    if ($stylePair[0] == 'height') {
1035 11
                                                        $comment->setHeight($stylePair[1]);
1036
                                                    }
1037 11
                                                    if ($stylePair[0] == 'visibility') {
1038 11
                                                        $comment->setVisible($stylePair[1] == 'visible');
1039
                                                    }
1040
                                                }
1041
1042 11
                                                unset($unparsedVmlDrawings[$relName]);
1043
                                            }
1044
                                        }
1045
                                    }
1046
                                }
1047
1048
                                // unparsed vmlDrawing
1049 175
                                if ($unparsedVmlDrawings) {
1050 1
                                    foreach ($unparsedVmlDrawings as $rId => $relPath) {
1051 1
                                        $rId = substr($rId, 3); // rIdXXX
1052 1
                                        $unparsedVmlDrawing = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['vmlDrawings'];
1053 1
                                        $unparsedVmlDrawing[$rId] = [];
1054 1
                                        $unparsedVmlDrawing[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $relPath);
1055 1
                                        $unparsedVmlDrawing[$rId]['relFilePath'] = $relPath;
1056 1
                                        $unparsedVmlDrawing[$rId]['content'] = $this->securityScanner->scan($this->getFromZipArchive($zip, $unparsedVmlDrawing[$rId]['filePath']));
1057 1
                                        unset($unparsedVmlDrawing);
1058
                                    }
1059
                                }
1060
1061
                                // Header/footer images
1062 175
                                if ($xmlSheet && $xmlSheet->legacyDrawingHF && !$this->readDataOnly) {
1063
                                    if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) {
1064
                                        $relsWorksheet = $this->loadZipNoNamespace(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels', Namespaces::RELATIONSHIPS);
1065
                                        $vmlRelationship = '';
1066
1067
                                        foreach ($relsWorksheet->Relationship as $ele) {
1068
                                            if ($ele['Type'] == Namespaces::VML) {
1069
                                                $vmlRelationship = self::dirAdd("$dir/$fileWorksheet", $ele['Target']);
1070
                                            }
1071
                                        }
1072
1073
                                        if ($vmlRelationship != '') {
1074
                                            // Fetch linked images
1075
                                            $relsVML = $this->loadZipNoNamespace(dirname($vmlRelationship) . '/_rels/' . basename($vmlRelationship) . '.rels', Namespaces::RELATIONSHIPS);
1076
                                            $drawings = [];
1077
                                            if (isset($relsVML->Relationship)) {
1078
                                                foreach ($relsVML->Relationship as $ele) {
1079
                                                    if ($ele['Type'] == Namespaces::IMAGE) {
1080
                                                        $drawings[(string) $ele['Id']] = self::dirAdd($vmlRelationship, $ele['Target']);
1081
                                                    }
1082
                                                }
1083
                                            }
1084
                                            // Fetch VML document
1085
                                            $vmlDrawing = $this->loadZipNoNamespace($vmlRelationship, '');
1086
                                            $vmlDrawing->registerXPathNamespace('v', Namespaces::URN_VML);
1087
1088
                                            $hfImages = [];
1089
1090
                                            $shapes = self::xpathNoFalse($vmlDrawing, '//v:shape');
1091
                                            foreach ($shapes as $idx => $shape) {
1092
                                                $shape->registerXPathNamespace('v', Namespaces::URN_VML);
1093
                                                $imageData = $shape->xpath('//v:imagedata');
1094
1095
                                                if (empty($imageData)) {
1096
                                                    continue;
1097
                                                }
1098
1099
                                                $imageData = $imageData[$idx];
1100
1101
                                                $imageData = self::getAttributes($imageData, Namespaces::URN_MSOFFICE);
1102
                                                $style = self::toCSSArray((string) $shape['style']);
1103
1104
                                                $hfImages[(string) $shape['id']] = new HeaderFooterDrawing();
1105
                                                if (isset($imageData['title'])) {
1106
                                                    $hfImages[(string) $shape['id']]->setName((string) $imageData['title']);
1107
                                                }
1108
1109
                                                $hfImages[(string) $shape['id']]->setPath('zip://' . File::realpath($pFilename) . '#' . $drawings[(string) $imageData['relid']], false);
1110
                                                $hfImages[(string) $shape['id']]->setResizeProportional(false);
1111
                                                $hfImages[(string) $shape['id']]->setWidth($style['width']);
1112
                                                $hfImages[(string) $shape['id']]->setHeight($style['height']);
1113
                                                if (isset($style['margin-left'])) {
1114
                                                    $hfImages[(string) $shape['id']]->setOffsetX($style['margin-left']);
1115
                                                }
1116
                                                $hfImages[(string) $shape['id']]->setOffsetY($style['margin-top']);
1117
                                                $hfImages[(string) $shape['id']]->setResizeProportional(true);
1118
                                            }
1119
1120
                                            $docSheet->getHeaderFooter()->setImages($hfImages);
1121
                                        }
1122
                                    }
1123
                                }
1124
                            }
1125
1126
                            // TODO: Autoshapes from twoCellAnchors!
1127 175
                            $filename = dirname("$dir/$fileWorksheet")
1128 175
                                . '/_rels/'
1129 175
                                . basename($fileWorksheet)
1130 175
                                . '.rels';
1131 175
                            if ($zip->locateName($filename)) {
1132 114
                                $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS);
1133 114
                                $drawings = [];
1134 114
                                foreach ($relsWorksheet->Relationship as $ele) {
1135 37
                                    if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
1136 16
                                        $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']);
1137
                                    }
1138
                                }
1139 114
                                if ($xmlSheet->drawing && !$this->readDataOnly) {
1140 16
                                    $unparsedDrawings = [];
1141 16
                                    $fileDrawing = null;
1142 16
                                    foreach ($xmlSheet->drawing as $drawing) {
1143 16
                                        $drawingRelId = (string) self::getArrayItem(self::getAttributes($drawing, $xmlNamespaceBase), 'id');
1144 16
                                        $fileDrawing = $drawings[$drawingRelId];
1145 16
                                        $filename = dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels';
1146 16
                                        $relsDrawing = $this->loadZipNoNamespace($filename, $xmlNamespaceBase);
1147 16
                                        $images = [];
1148 16
                                        $hyperlinks = [];
1149 16
                                        if ($relsDrawing && $relsDrawing->Relationship) {
1150 14
                                            foreach ($relsDrawing->Relationship as $ele) {
1151 14
                                                $eleType = (string) $ele['Type'];
1152 14
                                                if ($eleType === Namespaces::HYPERLINK) {
1153 2
                                                    $hyperlinks[(string) $ele['Id']] = (string) $ele['Target'];
1154
                                                }
1155 14
                                                if ($eleType === "$xmlNamespaceBase/image") {
1156 10
                                                    $images[(string) $ele['Id']] = self::dirAdd($fileDrawing, $ele['Target']);
1157 7
                                                } elseif ($eleType === "$xmlNamespaceBase/chart") {
1158 5
                                                    if ($this->includeCharts) {
1159 5
                                                        $charts[self::dirAdd($fileDrawing, $ele['Target'])] = [
1160 5
                                                            'id' => (string) $ele['Id'],
1161 5
                                                            'sheet' => $docSheet->getTitle(),
1162
                                                        ];
1163
                                                    }
1164
                                                }
1165
                                            }
1166
                                        }
1167 16
                                        $xmlDrawing = $this->loadZipNoNamespace($fileDrawing, '');
1168 16
                                        $xmlDrawingChildren = $xmlDrawing->children(Namespaces::SPREADSHEET_DRAWING);
1169
1170 16
                                        if ($xmlDrawingChildren->oneCellAnchor) {
1171 8
                                            foreach ($xmlDrawingChildren->oneCellAnchor as $oneCellAnchor) {
1172 8
                                                if ($oneCellAnchor->pic->blipFill) {
1173
                                                    /** @var SimpleXMLElement $blip */
1174 7
                                                    $blip = $oneCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip;
1175
                                                    /** @var SimpleXMLElement $xfrm */
1176 7
                                                    $xfrm = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm;
1177
                                                    /** @var SimpleXMLElement $outerShdw */
1178 7
                                                    $outerShdw = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw;
1179
                                                    /** @var SimpleXMLElement $hlinkClick */
1180 7
                                                    $hlinkClick = $oneCellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick;
0 ignored issues
show
Unused Code introduced by
The assignment to $hlinkClick is dead and can be removed.
Loading history...
1181
1182 7
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
1183 7
                                                    $objDrawing->setName((string) self::getArrayItem(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'name'));
1184 7
                                                    $objDrawing->setDescription((string) self::getArrayItem(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'descr'));
1185 7
                                                    $embedImageKey = (string) self::getArrayItem(
1186 7
                                                        self::getAttributes($blip, $xmlNamespaceBase),
1187 7
                                                        'embed'
1188
                                                    );
1189 7
                                                    if (isset($images[$embedImageKey])) {
1190 7
                                                        $objDrawing->setPath(
1191 7
                                                            'zip://' . File::realpath($pFilename) . '#' .
1192 7
                                                            $images[$embedImageKey],
1193 7
                                                            false
1194
                                                        );
1195
                                                    } else {
1196
                                                        $linkImageKey = (string) self::getArrayItem(
1197
                                                            $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'),
1198
                                                            'link'
1199
                                                        );
1200
                                                        if (isset($images[$linkImageKey])) {
1201
                                                            $url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
1202
                                                            $objDrawing->setPath($url);
1203
                                                        }
1204
                                                    }
1205 7
                                                    $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1));
1206
1207 7
                                                    $objDrawing->setOffsetX((int) Drawing::EMUToPixels($oneCellAnchor->from->colOff));
1208 7
                                                    $objDrawing->setOffsetY(Drawing::EMUToPixels($oneCellAnchor->from->rowOff));
1209 7
                                                    $objDrawing->setResizeProportional(false);
1210 7
                                                    $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItem((int) self::getAttributes($oneCellAnchor->ext), 'cx')));
1211 7
                                                    $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem((int) self::getAttributes($oneCellAnchor->ext), 'cy')));
1212 7
                                                    if ($xfrm) {
1213 7
                                                        $objDrawing->setRotation((int) Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($xfrm), 'rot')));
1214
                                                    }
1215 7
                                                    if ($outerShdw) {
1216 2
                                                        $shadow = $objDrawing->getShadow();
1217 2
                                                        $shadow->setVisible(true);
1218 2
                                                        $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($outerShdw), 'blurRad')));
1219 2
                                                        $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($outerShdw), 'dist')));
1220 2
                                                        $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($outerShdw), 'dir')));
1221 2
                                                        $shadow->setAlignment((string) self::getArrayItem(self::getAttributes($outerShdw), 'algn'));
1222 2
                                                        $clr = $outerShdw->srgbClr ?? $outerShdw->prstClr;
1223 2
                                                        $shadow->getColor()->setRGB(self::getArrayItem(self::getAttributes($clr), 'val'));
1224 2
                                                        $shadow->setAlpha(self::getArrayItem(self::getAttributes($clr->alpha), 'val') / 1000);
1225
                                                    }
1226
1227 7
                                                    $this->readHyperLinkDrawing($objDrawing, $oneCellAnchor, $hyperlinks);
1228
1229 7
                                                    $objDrawing->setWorksheet($docSheet);
1230 1
                                                } elseif ($this->includeCharts && $oneCellAnchor->graphicFrame) {
1231
                                                    // Exported XLSX from Google Sheets positions charts with a oneCellAnchor
1232 1
                                                    $coordinates = Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1);
1233 1
                                                    $offsetX = Drawing::EMUToPixels($oneCellAnchor->from->colOff);
1234 1
                                                    $offsetY = Drawing::EMUToPixels($oneCellAnchor->from->rowOff);
1235 1
                                                    $width = Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($oneCellAnchor->ext), 'cx'));
1236 1
                                                    $height = Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($oneCellAnchor->ext), 'cy'));
1237
1238 1
                                                    $graphic = $oneCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
1239
                                                    /** @var SimpleXMLElement $chartRef */
1240 1
                                                    $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
1241 1
                                                    $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
1242
1243 1
                                                    $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
1244 1
                                                        'fromCoordinate' => $coordinates,
1245 1
                                                        'fromOffsetX' => $offsetX,
1246 1
                                                        'fromOffsetY' => $offsetY,
1247 1
                                                        'width' => $width,
1248 1
                                                        'height' => $height,
1249 1
                                                        'worksheetTitle' => $docSheet->getTitle(),
1250
                                                    ];
1251
                                                }
1252
                                            }
1253
                                        }
1254 16
                                        if ($xmlDrawingChildren->twoCellAnchor) {
1255 8
                                            foreach ($xmlDrawingChildren->twoCellAnchor as $twoCellAnchor) {
1256 8
                                                if ($twoCellAnchor->pic->blipFill) {
1257 5
                                                    $blip = $twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip;
1258 5
                                                    $xfrm = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm;
1259 5
                                                    $outerShdw = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw;
1260 5
                                                    $hlinkClick = $twoCellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick;
1261 5
                                                    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
1262 5
                                                    $objDrawing->setName((string) self::getArrayItem(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'name'));
1263 5
                                                    $objDrawing->setDescription((string) self::getArrayItem(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'descr'));
1264 5
                                                    $embedImageKey = (string) self::getArrayItem(
1265 5
                                                        self::getAttributes($blip, $xmlNamespaceBase),
1266 5
                                                        'embed'
1267
                                                    );
1268 5
                                                    if (isset($images[$embedImageKey])) {
1269 4
                                                        $objDrawing->setPath(
1270 4
                                                            'zip://' . File::realpath($pFilename) . '#' .
1271 4
                                                            $images[$embedImageKey],
1272 4
                                                            false
1273
                                                        );
1274
                                                    } else {
1275 1
                                                        $linkImageKey = (string) self::getArrayItem(
1276 1
                                                            $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'),
1277 1
                                                            'link'
1278
                                                        );
1279 1
                                                        if (isset($images[$linkImageKey])) {
1280 1
                                                            $url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
1281 1
                                                            $objDrawing->setPath($url);
1282
                                                        }
1283
                                                    }
1284 5
                                                    $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1));
1285
1286 5
                                                    $objDrawing->setOffsetX(Drawing::EMUToPixels($twoCellAnchor->from->colOff));
1287 5
                                                    $objDrawing->setOffsetY(Drawing::EMUToPixels($twoCellAnchor->from->rowOff));
1288 5
                                                    $objDrawing->setResizeProportional(false);
1289
1290 5
                                                    if ($xfrm) {
1291 5
                                                        $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($xfrm->ext), 'cx')));
1292 5
                                                        $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($xfrm->ext), 'cy')));
1293 5
                                                        $objDrawing->setRotation(Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($xfrm), 'rot')));
1294
                                                    }
1295 5
                                                    if ($outerShdw) {
1296
                                                        $shadow = $objDrawing->getShadow();
1297
                                                        $shadow->setVisible(true);
1298
                                                        $shadow->setBlurRadius(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($outerShdw), 'blurRad')));
1299
                                                        $shadow->setDistance(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($outerShdw), 'dist')));
1300
                                                        $shadow->setDirection(Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($outerShdw), 'dir')));
1301
                                                        $shadow->setAlignment((string) self::getArrayItem(self::getAttributes($outerShdw), 'algn'));
1302
                                                        $clr = $outerShdw->srgbClr ?? $outerShdw->prstClr;
1303
                                                        $shadow->getColor()->setRGB(self::getArrayItem(self::getAttributes($clr), 'val'));
1304
                                                        $shadow->setAlpha(self::getArrayItem(self::getAttributes($clr->alpha), 'val') / 1000);
1305
                                                    }
1306
1307 5
                                                    $this->readHyperLinkDrawing($objDrawing, $twoCellAnchor, $hyperlinks);
1308
1309 5
                                                    $objDrawing->setWorksheet($docSheet);
1310 4
                                                } elseif (($this->includeCharts) && ($twoCellAnchor->graphicFrame)) {
1311 4
                                                    $fromCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1);
1312 4
                                                    $fromOffsetX = Drawing::EMUToPixels($twoCellAnchor->from->colOff);
1313 4
                                                    $fromOffsetY = Drawing::EMUToPixels($twoCellAnchor->from->rowOff);
1314 4
                                                    $toCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1);
1315 4
                                                    $toOffsetX = Drawing::EMUToPixels($twoCellAnchor->to->colOff);
1316 4
                                                    $toOffsetY = Drawing::EMUToPixels($twoCellAnchor->to->rowOff);
1317 4
                                                    $graphic = $twoCellAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic;
1318
                                                    /** @var SimpleXMLElement $chartRef */
1319 4
                                                    $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart;
1320 4
                                                    $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase);
1321
1322 4
                                                    $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [
1323 4
                                                        'fromCoordinate' => $fromCoordinate,
1324 4
                                                        'fromOffsetX' => $fromOffsetX,
1325 4
                                                        'fromOffsetY' => $fromOffsetY,
1326 4
                                                        'toCoordinate' => $toCoordinate,
1327 4
                                                        'toOffsetX' => $toOffsetX,
1328 4
                                                        'toOffsetY' => $toOffsetY,
1329 4
                                                        'worksheetTitle' => $docSheet->getTitle(),
1330
                                                    ];
1331
                                                }
1332
                                            }
1333
                                        }
1334 16
                                        if (empty($relsDrawing) && $xmlDrawing->count() == 0) {
1335
                                            // Save Drawing without rels and children as unparsed
1336 2
                                            $unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML();
1337
                                        }
1338
                                    }
1339
1340
                                    // store original rId of drawing files
1341 16
                                    $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'] = [];
1342 16
                                    foreach ($relsWorksheet->Relationship as $ele) {
1343 16
                                        if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") {
1344 16
                                            $drawingRelId = (string) $ele['Id'];
1345 16
                                            $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingOriginalIds'][(string) $ele['Target']] = $drawingRelId;
1346 16
                                            if (isset($unparsedDrawings[$drawingRelId])) {
1347 2
                                                $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['Drawings'][$drawingRelId] = $unparsedDrawings[$drawingRelId];
1348
                                            }
1349
                                        }
1350
                                    }
1351
1352
                                    // unparsed drawing AlternateContent
1353 16
                                    $xmlAltDrawing = $this->loadZip($fileDrawing, Namespaces::COMPATIBILITY);
1354
1355 16
                                    if ($xmlAltDrawing->AlternateContent) {
1356 1
                                        foreach ($xmlAltDrawing->AlternateContent as $alternateContent) {
1357 1
                                            $alternateContent = self::testSimpleXml($alternateContent);
1358 1
                                            $unparsedLoadedData['sheets'][$docSheet->getCodeName()]['drawingAlternateContents'][] = $alternateContent->asXML();
1359
                                        }
1360
                                    }
1361
                                }
1362
                            }
1363
1364 175
                            $this->readFormControlProperties($excel, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData);
1365 175
                            $this->readPrinterSettings($excel, $dir, $fileWorksheet, $docSheet, $unparsedLoadedData);
1366
1367
                            // Loop through definedNames
1368 175
                            if ($xmlWorkbook->definedNames) {
1369 139
                                foreach ($xmlWorkbook->definedNames->definedName as $definedName) {
1370
                                    // Extract range
1371 54
                                    $extractedRange = (string) $definedName;
1372 54
                                    if (($spos = strpos($extractedRange, '!')) !== false) {
1373 54
                                        $extractedRange = substr($extractedRange, 0, $spos) . str_replace('$', '', substr($extractedRange, $spos));
1374
                                    } else {
1375 14
                                        $extractedRange = str_replace('$', '', $extractedRange);
1376
                                    }
1377
1378
                                    // Valid range?
1379 54
                                    if ($extractedRange == '') {
1380
                                        continue;
1381
                                    }
1382
1383
                                    // Some definedNames are only applicable if we are on the same sheet...
1384 54
                                    if ((string) $definedName['localSheetId'] != '' && (string) $definedName['localSheetId'] == $oldSheetId) {
1385
                                        // Switch on type
1386 24
                                        switch ((string) $definedName['name']) {
1387 24
                                            case '_xlnm._FilterDatabase':
1388 4
                                                if ((string) $definedName['hidden'] !== '1') {
1389
                                                    $extractedRange = explode(',', $extractedRange);
1390
                                                    foreach ($extractedRange as $range) {
1391
                                                        $autoFilterRange = $range;
1392
                                                        if (strpos($autoFilterRange, ':') !== false) {
1393
                                                            $docSheet->getAutoFilter()->setRange($autoFilterRange);
1394
                                                        }
1395
                                                    }
1396
                                                }
1397
1398 4
                                                break;
1399 20
                                            case '_xlnm.Print_Titles':
1400
                                                // Split $extractedRange
1401 1
                                                $extractedRange = explode(',', $extractedRange);
1402
1403
                                                // Set print titles
1404 1
                                                foreach ($extractedRange as $range) {
1405 1
                                                    $matches = [];
1406 1
                                                    $range = str_replace('$', '', $range);
1407
1408
                                                    // check for repeating columns, e g. 'A:A' or 'A:D'
1409 1
                                                    if (preg_match('/!?([A-Z]+)\:([A-Z]+)$/', $range, $matches)) {
1410
                                                        $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$matches[1], $matches[2]]);
1411 1
                                                    } elseif (preg_match('/!?(\d+)\:(\d+)$/', $range, $matches)) {
1412
                                                        // check for repeating rows, e.g. '1:1' or '1:5'
1413 1
                                                        $docSheet->getPageSetup()->setRowsToRepeatAtTop([$matches[1], $matches[2]]);
1414
                                                    }
1415
                                                }
1416
1417 1
                                                break;
1418 19
                                            case '_xlnm.Print_Area':
1419 4
                                                $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
1420 4
                                                $newRangeSets = [];
1421 4
                                                foreach ($rangeSets as $rangeSet) {
1422 4
                                                    [$sheetName, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true);
1423 4
                                                    if (strpos($rangeSet, ':') === false) {
1424
                                                        $rangeSet = $rangeSet . ':' . $rangeSet;
1425
                                                    }
1426 4
                                                    $newRangeSets[] = str_replace('$', '', $rangeSet);
1427
                                                }
1428 4
                                                $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets));
1429
1430 4
                                                break;
1431
                                            default:
1432 15
                                                break;
1433
                                        }
1434
                                    }
1435
                                }
1436
                            }
1437
1438
                            // Next sheet id
1439 175
                            ++$sheetId;
1440
                        }
1441
1442
                        // Loop through definedNames
1443 175
                        if ($xmlWorkbook->definedNames) {
1444 139
                            foreach ($xmlWorkbook->definedNames->definedName as $definedName) {
1445
                                // Extract range
1446 54
                                $extractedRange = (string) $definedName;
1447
1448
                                // Valid range?
1449 54
                                if ($extractedRange == '') {
1450
                                    continue;
1451
                                }
1452
1453
                                // Some definedNames are only applicable if we are on the same sheet...
1454 54
                                if ((string) $definedName['localSheetId'] != '') {
1455
                                    // Local defined name
1456
                                    // Switch on type
1457 24
                                    switch ((string) $definedName['name']) {
1458 24
                                        case '_xlnm._FilterDatabase':
1459 20
                                        case '_xlnm.Print_Titles':
1460 19
                                        case '_xlnm.Print_Area':
1461 9
                                            break;
1462
                                        default:
1463 15
                                            if ($mapSheetId[(int) $definedName['localSheetId']] !== null) {
1464 15
                                                $range = Worksheet::extractSheetTitle((string) $definedName, true);
1465 15
                                                $scope = $excel->getSheet($mapSheetId[(int) $definedName['localSheetId']]);
1466 15
                                                if (strpos((string) $definedName, '!') !== false) {
1467 15
                                                    $range[0] = str_replace("''", "'", $range[0]);
1468 15
                                                    $range[0] = str_replace("'", '', $range[0]);
1469 15
                                                    if ($worksheet = $excel->getSheetByName($range[0])) {
1470 15
                                                        $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $worksheet, $extractedRange, true, $scope));
1471
                                                    } else {
1472 15
                                                        $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true, $scope));
1473
                                                    }
1474
                                                } else {
1475
                                                    $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true));
1476
                                                }
1477
                                            }
1478
1479 24
                                            break;
1480
                                    }
1481 43
                                } elseif (!isset($definedName['localSheetId'])) {
1482 43
                                    $definedRange = (string) $definedName;
1483
                                    // "Global" definedNames
1484 43
                                    $locatedSheet = null;
1485 43
                                    if (strpos((string) $definedName, '!') !== false) {
1486
                                        // Modify range, and extract the first worksheet reference
1487
                                        // Need to split on a comma or a space if not in quotes, and extract the first part.
1488 42
                                        $definedNameValueParts = preg_split("/[ ,](?=([^']*'[^']*')*[^']*$)/miuU", $definedRange);
1489
                                        // Extract sheet name
1490 42
                                        [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true);
1491 42
                                        $extractedSheetName = trim($extractedSheetName, "'");
1492
1493
                                        // Locate sheet
1494 42
                                        $locatedSheet = $excel->getSheetByName($extractedSheetName);
1495
                                    }
1496
1497 43
                                    if ($locatedSheet === null && !DefinedName::testIfFormula($definedRange)) {
1498 1
                                        $definedRange = '#REF!';
1499
                                    }
1500 43
                                    $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $locatedSheet, $definedRange, false));
1501
                                }
1502
                            }
1503
                        }
1504
                    }
1505
1506 175
                    $workbookView = $xmlWorkbook->children($mainNS)->bookViews->workbookView;
1507 175
                    if ((!$this->readDataOnly || !empty($this->loadSheetsOnly)) && !empty($workbookView)) {
1508 174
                        $workbookViewAttributes = self::testSimpleXml(self::getAttributes($workbookView));
1509
                        // active sheet index
1510 174
                        $activeTab = (int) $workbookViewAttributes->activeTab; // refers to old sheet index
1511
1512
                        // keep active sheet index if sheet is still loaded, else first sheet is set as the active
1513 174
                        if (isset($mapSheetId[$activeTab]) && $mapSheetId[$activeTab] !== null) {
1514 174
                            $excel->setActiveSheetIndex($mapSheetId[$activeTab]);
1515
                        } else {
1516
                            if ($excel->getSheetCount() == 0) {
1517
                                $excel->createSheet();
1518
                            }
1519
                            $excel->setActiveSheetIndex(0);
1520
                        }
1521
1522 174
                        if (isset($workbookViewAttributes->showHorizontalScroll)) {
1523 87
                            $showHorizontalScroll = (string) $workbookViewAttributes->showHorizontalScroll;
1524 87
                            $excel->setShowHorizontalScroll($this->castXsdBooleanToBool($showHorizontalScroll));
1525
                        }
1526
1527 174
                        if (isset($workbookViewAttributes->showVerticalScroll)) {
1528 87
                            $showVerticalScroll = (string) $workbookViewAttributes->showVerticalScroll;
1529 87
                            $excel->setShowVerticalScroll($this->castXsdBooleanToBool($showVerticalScroll));
1530
                        }
1531
1532 174
                        if (isset($workbookViewAttributes->showSheetTabs)) {
1533 87
                            $showSheetTabs = (string) $workbookViewAttributes->showSheetTabs;
1534 87
                            $excel->setShowSheetTabs($this->castXsdBooleanToBool($showSheetTabs));
1535
                        }
1536
1537 174
                        if (isset($workbookViewAttributes->minimized)) {
1538 87
                            $minimized = (string) $workbookViewAttributes->minimized;
1539 87
                            $excel->setMinimized($this->castXsdBooleanToBool($minimized));
1540
                        }
1541
1542 174
                        if (isset($workbookViewAttributes->autoFilterDateGrouping)) {
1543 87
                            $autoFilterDateGrouping = (string) $workbookViewAttributes->autoFilterDateGrouping;
1544 87
                            $excel->setAutoFilterDateGrouping($this->castXsdBooleanToBool($autoFilterDateGrouping));
1545
                        }
1546
1547 174
                        if (isset($workbookViewAttributes->firstSheet)) {
1548 87
                            $firstSheet = (string) $workbookViewAttributes->firstSheet;
1549 87
                            $excel->setFirstSheetIndex((int) $firstSheet);
1550
                        }
1551
1552 174
                        if (isset($workbookViewAttributes->visibility)) {
1553 87
                            $visibility = (string) $workbookViewAttributes->visibility;
1554 87
                            $excel->setVisibility($visibility);
1555
                        }
1556
1557 174
                        if (isset($workbookViewAttributes->tabRatio)) {
1558 88
                            $tabRatio = (string) $workbookViewAttributes->tabRatio;
1559 88
                            $excel->setTabRatio((int) $tabRatio);
1560
                        }
1561
                    }
1562
1563 175
                    break;
1564
            }
1565
        }
1566
1567 175
        if (!$this->readDataOnly) {
1568 175
            $contentTypes = $this->loadZip('[Content_Types].xml');
1569
1570
            // Default content types
1571 175
            foreach ($contentTypes->Default as $contentType) {
1572 175
                switch ($contentType['ContentType']) {
1573 175
                    case 'application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings':
1574 28
                        $unparsedLoadedData['default_content_types'][(string) $contentType['Extension']] = (string) $contentType['ContentType'];
1575
1576 28
                        break;
1577
                }
1578
            }
1579
1580
            // Override content types
1581 175
            foreach ($contentTypes->Override as $contentType) {
1582 175
                switch ($contentType['ContentType']) {
1583 175
                    case 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml':
1584 5
                        if ($this->includeCharts) {
1585 5
                            $chartEntryRef = ltrim((string) $contentType['PartName'], '/');
1586 5
                            $chartElements = $this->loadZip($chartEntryRef);
1587 5
                            $objChart = Chart::readChart($chartElements, basename($chartEntryRef, '.xml'));
1588
1589 5
                            if (isset($charts[$chartEntryRef])) {
1590 5
                                $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id'];
1591 5
                                if (isset($chartDetails[$chartPositionRef])) {
1592 5
                                    $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart);
1593 5
                                    $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet']));
1594 5
                                    $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']);
1595 5
                                    if (array_key_exists('toCoordinate', $chartDetails[$chartPositionRef])) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $chartDetails does not seem to be defined for all execution paths leading up to this point.
Loading history...
1596
                                        // For oneCellAnchor positioned charts, toCoordinate is not in the data. Does it need to be calculated?
1597 4
                                        $objChart->setBottomRightPosition($chartDetails[$chartPositionRef]['toCoordinate'], $chartDetails[$chartPositionRef]['toOffsetX'], $chartDetails[$chartPositionRef]['toOffsetY']);
1598
                                    }
1599
                                }
1600
                            }
1601
                        }
1602
1603 5
                        break;
1604
1605
                    // unparsed
1606 175
                    case 'application/vnd.ms-excel.controlproperties+xml':
1607 1
                        $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType'];
1608
1609 1
                        break;
1610
                }
1611
            }
1612
        }
1613
1614 175
        $excel->setUnparsedLoadedData($unparsedLoadedData);
1615
1616 175
        $zip->close();
1617
1618 175
        return $excel;
1619
    }
1620
1621
    /**
1622
     * @param SimpleXMLElement|stdClass $style
1623
     */
1624 174
    private static function readStyle(Style $docStyle, $style): void
1625
    {
1626 174
        $docStyle->getNumberFormat()->setFormatCode($style->numFmt);
1627
1628
        // font
1629 174
        if (isset($style->font)) {
1630 174
            Styles::readFontStyle($docStyle->getFont(), $style->font);
1631
        }
1632
1633
        // fill
1634 174
        if (isset($style->fill)) {
1635 174
            Styles::readFillStyle($docStyle->getFill(), $style->fill);
1636
        }
1637
1638
        // border
1639 174
        if (isset($style->border)) {
1640 174
            Styles::readBorderStyle($docStyle->getBorders(), $style->border);
1641
        }
1642
1643
        // alignment
1644 174
        if (isset($style->alignment)) {
1645 174
            Styles::readAlignmentStyle($docStyle->getAlignment(), $style->alignment);
1646
        }
1647
1648
        // protection
1649 174
        if (isset($style->protection)) {
1650 174
            Styles::readProtectionLocked($docStyle, $style);
1651 174
            Styles::readProtectionHidden($docStyle, $style);
1652
        }
1653
1654
        // top-level style settings
1655 174
        if (isset($style->quotePrefix)) {
1656 174
            $docStyle->setQuotePrefix((bool) $style->quotePrefix);
1657
        }
1658 174
    }
1659
1660
    /**
1661
     * @return RichText
1662
     */
1663 17
    private function parseRichText(?SimpleXMLElement $is)
1664
    {
1665 17
        $value = new RichText();
1666
1667 17
        if (isset($is->t)) {
1668 10
            $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t));
1669
        } else {
1670 7
            if (is_object($is->r)) {
1671
1672
                /** @var SimpleXMLElement $run */
1673 7
                foreach ($is->r as $run) {
1674 7
                    if (!isset($run->rPr)) {
1675 6
                        $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $run->t));
1676
                    } else {
1677 6
                        $objText = $value->createTextRun(StringHelper::controlCharacterOOXML2PHP((string) $run->t));
1678
1679 6
                        $attr = $run->rPr->rFont->attributes();
1680 6
                        if (isset($attr['val'])) {
1681 6
                            $objText->getFont()->setName((string) $attr['val']);
1682
                        }
1683 6
                        $attr = $run->rPr->sz->attributes();
1684 6
                        if (isset($attr['val'])) {
1685 6
                            $objText->getFont()->setSize((float) $attr['val']);
1686
                        }
1687 6
                        if (isset($run->rPr->color)) {
1688 6
                            $objText->getFont()->setColor(new Color(Styles::readColor($run->rPr->color)));
1689
                        }
1690 6
                        if (isset($run->rPr->b)) {
1691 6
                            $attr = $run->rPr->b->attributes();
1692
                            if (
1693 6
                                (isset($attr['val']) && self::boolean((string) $attr['val'])) ||
1694 6
                                (!isset($attr['val']))
1695
                            ) {
1696 6
                                $objText->getFont()->setBold(true);
1697
                            }
1698
                        }
1699 6
                        if (isset($run->rPr->i)) {
1700 2
                            $attr = $run->rPr->i->attributes();
1701
                            if (
1702 2
                                (isset($attr['val']) && self::boolean((string) $attr['val'])) ||
1703 2
                                (!isset($attr['val']))
1704
                            ) {
1705 2
                                $objText->getFont()->setItalic(true);
1706
                            }
1707
                        }
1708 6
                        if (isset($run->rPr->vertAlign)) {
1709
                            $attr = $run->rPr->vertAlign->attributes();
1710
                            if (isset($attr['val'])) {
1711
                                $vertAlign = strtolower((string) $attr['val']);
1712
                                if ($vertAlign == 'superscript') {
1713
                                    $objText->getFont()->setSuperscript(true);
1714
                                }
1715
                                if ($vertAlign == 'subscript') {
1716
                                    $objText->getFont()->setSubscript(true);
1717
                                }
1718
                            }
1719
                        }
1720 6
                        if (isset($run->rPr->u)) {
1721 2
                            $attr = $run->rPr->u->attributes();
1722 2
                            if (!isset($attr['val'])) {
1723
                                $objText->getFont()->setUnderline(\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE);
1724
                            } else {
1725 2
                                $objText->getFont()->setUnderline((string) $attr['val']);
1726
                            }
1727
                        }
1728 6
                        if (isset($run->rPr->strike)) {
1729 2
                            $attr = $run->rPr->strike->attributes();
1730
                            if (
1731 2
                                (isset($attr['val']) && self::boolean((string) $attr['val'])) ||
1732 2
                                (!isset($attr['val']))
1733
                            ) {
1734
                                $objText->getFont()->setStrikethrough(true);
1735
                            }
1736
                        }
1737
                    }
1738
                }
1739
            }
1740
        }
1741
1742 17
        return $value;
1743
    }
1744
1745
    private function readRibbon(Spreadsheet $excel, string $customUITarget, ZipArchive $zip): void
1746
    {
1747
        $baseDir = dirname($customUITarget);
1748
        $nameCustomUI = basename($customUITarget);
1749
        // get the xml file (ribbon)
1750
        $localRibbon = $this->getFromZipArchive($zip, $customUITarget);
1751
        $customUIImagesNames = [];
1752
        $customUIImagesBinaries = [];
1753
        // something like customUI/_rels/customUI.xml.rels
1754
        $pathRels = $baseDir . '/_rels/' . $nameCustomUI . '.rels';
1755
        $dataRels = $this->getFromZipArchive($zip, $pathRels);
1756
        if ($dataRels) {
1757
            // exists and not empty if the ribbon have some pictures (other than internal MSO)
1758
            $UIRels = simplexml_load_string(
1759
                $this->securityScanner->scan($dataRels),
1760
                'SimpleXMLElement',
1761
                Settings::getLibXmlLoaderOptions()
1762
            );
1763
            if (false !== $UIRels) {
1764
                // we need to save id and target to avoid parsing customUI.xml and "guess" if it's a pseudo callback who load the image
1765
                foreach ($UIRels->Relationship as $ele) {
1766
                    if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/image') {
1767
                        // an image ?
1768
                        $customUIImagesNames[(string) $ele['Id']] = (string) $ele['Target'];
1769
                        $customUIImagesBinaries[(string) $ele['Target']] = $this->getFromZipArchive($zip, $baseDir . '/' . (string) $ele['Target']);
1770
                    }
1771
                }
1772
            }
1773
        }
1774
        if ($localRibbon) {
1775
            $excel->setRibbonXMLData($customUITarget, $localRibbon);
1776
            if (count($customUIImagesNames) > 0 && count($customUIImagesBinaries) > 0) {
1777
                $excel->setRibbonBinObjects($customUIImagesNames, $customUIImagesBinaries);
1778
            } else {
1779
                $excel->setRibbonBinObjects(null, null);
1780
            }
1781
        } else {
1782
            $excel->setRibbonXMLData(null, null);
1783
            $excel->setRibbonBinObjects(null, null);
1784
        }
1785
    }
1786
1787 184
    private static function getArrayItem($array, $key = 0)
1788
    {
1789 184
        return $array[$key] ?? null;
1790
    }
1791
1792 34
    private static function dirAdd($base, $add)
1793
    {
1794 34
        return preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add");
1795
    }
1796
1797
    private static function toCSSArray($style)
1798
    {
1799
        $style = self::stripWhiteSpaceFromStyleString($style);
1800
1801
        $temp = explode(';', $style);
1802
        $style = [];
1803
        foreach ($temp as $item) {
1804
            $item = explode(':', $item);
1805
1806
            if (strpos($item[1], 'px') !== false) {
1807
                $item[1] = str_replace('px', '', $item[1]);
1808
            }
1809
            if (strpos($item[1], 'pt') !== false) {
1810
                $item[1] = str_replace('pt', '', $item[1]);
1811
                $item[1] = Font::fontSizeToPixels($item[1]);
1812
            }
1813
            if (strpos($item[1], 'in') !== false) {
1814
                $item[1] = str_replace('in', '', $item[1]);
1815
                $item[1] = Font::inchSizeToPixels($item[1]);
1816
            }
1817
            if (strpos($item[1], 'cm') !== false) {
1818
                $item[1] = str_replace('cm', '', $item[1]);
1819
                $item[1] = Font::centimeterSizeToPixels($item[1]);
1820
            }
1821
1822
            $style[$item[0]] = $item[1];
1823
        }
1824
1825
        return $style;
1826
    }
1827
1828 3
    public static function stripWhiteSpaceFromStyleString($string)
1829
    {
1830 3
        return trim(str_replace(["\r", "\n", ' '], '', $string), ';');
1831
    }
1832
1833 121
    private static function boolean($value)
1834
    {
1835 121
        if (is_object($value)) {
1836
            $value = (string) $value;
1837
        }
1838 121
        if (is_numeric($value)) {
1839 36
            return (bool) $value;
1840
        }
1841
1842 121
        return $value === 'true' || $value === 'TRUE';
1843
    }
1844
1845
    /**
1846
     * @param \PhpOffice\PhpSpreadsheet\Worksheet\Drawing $objDrawing
1847
     * @param SimpleXMLElement $cellAnchor
1848
     * @param array $hyperlinks
1849
     */
1850 10
    private function readHyperLinkDrawing($objDrawing, $cellAnchor, $hyperlinks): void
1851
    {
1852 10
        $hlinkClick = $cellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick;
1853
1854 10
        if ($hlinkClick->count() === 0) {
1855 8
            return;
1856
        }
1857
1858 2
        $hlinkId = (string) self::getAttributes($hlinkClick, Namespaces::SCHEMA_OFFICE_DOCUMENT)['id'];
1859 2
        $hyperlink = new Hyperlink(
1860 2
            $hyperlinks[$hlinkId],
1861 2
            (string) self::getArrayItem(self::getAttributes($cellAnchor->pic->nvPicPr->cNvPr), 'name')
1862
        );
1863 2
        $objDrawing->setHyperlink($hyperlink);
1864 2
    }
1865
1866 175
    private function readProtection(Spreadsheet $excel, SimpleXMLElement $xmlWorkbook): void
1867
    {
1868 175
        if (!$xmlWorkbook->workbookProtection) {
1869 165
            return;
1870
        }
1871
1872 10
        $excel->getSecurity()->setLockRevision(self::getLockValue($xmlWorkbook->workbookProtection, 'lockRevision'));
1873 10
        $excel->getSecurity()->setLockStructure(self::getLockValue($xmlWorkbook->workbookProtection, 'lockStructure'));
1874 10
        $excel->getSecurity()->setLockWindows(self::getLockValue($xmlWorkbook->workbookProtection, 'lockWindows'));
1875
1876 10
        if ($xmlWorkbook->workbookProtection['revisionsPassword']) {
1877 1
            $excel->getSecurity()->setRevisionsPassword(
1878 1
                (string) $xmlWorkbook->workbookProtection['revisionsPassword'],
1879 1
                true
1880
            );
1881
        }
1882
1883 10
        if ($xmlWorkbook->workbookProtection['workbookPassword']) {
1884 2
            $excel->getSecurity()->setWorkbookPassword(
1885 2
                (string) $xmlWorkbook->workbookProtection['workbookPassword'],
1886 2
                true
1887
            );
1888
        }
1889 10
    }
1890
1891 10
    private static function getLockValue(SimpleXmlElement $protection, string $key): ?bool
1892
    {
1893 10
        $returnValue = null;
1894 10
        $protectKey = $protection[$key];
1895 10
        if (!empty($protectKey)) {
1896 9
            $protectKey = (string) $protectKey;
1897 9
            $returnValue = $protectKey !== 'false' && (bool) $protectKey;
1898
        }
1899
1900 10
        return $returnValue;
1901
    }
1902
1903 175
    private function readFormControlProperties(Spreadsheet $excel, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData): void
1904
    {
1905 175
        $zip = $this->zip;
1906 175
        if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) {
1907 79
            return;
1908
        }
1909
1910 114
        $filename = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
1911 114
        $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS);
1912 114
        $ctrlProps = [];
1913 114
        foreach ($relsWorksheet->Relationship as $ele) {
1914 37
            if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/ctrlProp') {
1915 1
                $ctrlProps[(string) $ele['Id']] = $ele;
1916
            }
1917
        }
1918
1919 114
        $unparsedCtrlProps = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['ctrlProps'];
1920 114
        foreach ($ctrlProps as $rId => $ctrlProp) {
1921 1
            $rId = substr($rId, 3); // rIdXXX
1922 1
            $unparsedCtrlProps[$rId] = [];
1923 1
            $unparsedCtrlProps[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $ctrlProp['Target']);
1924 1
            $unparsedCtrlProps[$rId]['relFilePath'] = (string) $ctrlProp['Target'];
1925 1
            $unparsedCtrlProps[$rId]['content'] = $this->securityScanner->scan($this->getFromZipArchive($zip, $unparsedCtrlProps[$rId]['filePath']));
1926
        }
1927 114
        unset($unparsedCtrlProps);
1928 114
    }
1929
1930 175
    private function readPrinterSettings(Spreadsheet $excel, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData): void
1931
    {
1932 175
        $zip = $this->zip;
1933 175
        if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) {
1934 79
            return;
1935
        }
1936
1937 114
        $filename = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
1938 114
        $relsWorksheet = $this->loadZipNoNamespace($filename, Namespaces::RELATIONSHIPS);
1939 114
        $sheetPrinterSettings = [];
1940 114
        foreach ($relsWorksheet->Relationship as $ele) {
1941 37
            if ((string) $ele['Type'] === Namespaces::SCHEMA_OFFICE_DOCUMENT . '/printerSettings') {
1942 24
                $sheetPrinterSettings[(string) $ele['Id']] = $ele;
1943
            }
1944
        }
1945
1946 114
        $unparsedPrinterSettings = &$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['printerSettings'];
1947 114
        foreach ($sheetPrinterSettings as $rId => $printerSettings) {
1948 24
            $rId = substr($rId, 3) . 'ps'; // rIdXXX, add 'ps' suffix to avoid identical resource identifier collision with unparsed vmlDrawing
1949 24
            $unparsedPrinterSettings[$rId] = [];
1950 24
            $unparsedPrinterSettings[$rId]['filePath'] = self::dirAdd("$dir/$fileWorksheet", $printerSettings['Target']);
1951 24
            $unparsedPrinterSettings[$rId]['relFilePath'] = (string) $printerSettings['Target'];
1952 24
            $unparsedPrinterSettings[$rId]['content'] = $this->securityScanner->scan($this->getFromZipArchive($zip, $unparsedPrinterSettings[$rId]['filePath']));
1953
        }
1954 114
        unset($unparsedPrinterSettings);
1955 114
    }
1956
1957
    /**
1958
     * Convert an 'xsd:boolean' XML value to a PHP boolean value.
1959
     * A valid 'xsd:boolean' XML value can be one of the following
1960
     * four values: 'true', 'false', '1', '0'.  It is case sensitive.
1961
     *
1962
     * Note that just doing '(bool) $xsdBoolean' is not safe,
1963
     * since '(bool) "false"' returns true.
1964
     *
1965
     * @see https://www.w3.org/TR/xmlschema11-2/#boolean
1966
     *
1967
     * @param string $xsdBoolean An XML string value of type 'xsd:boolean'
1968
     *
1969
     * @return bool  Boolean value
1970
     */
1971 87
    private function castXsdBooleanToBool($xsdBoolean)
1972
    {
1973 87
        if ($xsdBoolean === 'false') {
1974 86
            return false;
1975
        }
1976
1977 87
        return (bool) $xsdBoolean;
1978
    }
1979
1980 179
    private function getWorkbookBaseName(): array
1981
    {
1982 179
        $workbookBasename = '';
1983 179
        $xmlNamespaceBase = '';
1984
1985
        // check if it is an OOXML archive
1986 179
        $rels = $this->loadZip(self::INITIAL_FILE);
1987 179
        foreach ($rels->children(Namespaces::RELATIONSHIPS)->Relationship as $rel) {
1988 179
            $rel = self::getAttributes($rel);
1989 179
            $type = (string) $rel['Type'];
1990
            switch ($type) {
1991 179
                case Namespaces::OFFICE_DOCUMENT:
1992 177
                case Namespaces::PURL_OFFICE_DOCUMENT:
1993 179
                    $basename = basename((string) $rel['Target']);
1994 179
                    $xmlNamespaceBase = dirname($type);
1995 179
                    if (preg_match('/workbook.*\.xml/', $basename)) {
1996 179
                        $workbookBasename = $basename;
1997
                    }
1998
1999 179
                    break;
2000
            }
2001
        }
2002
2003 179
        return [$workbookBasename, $xmlNamespaceBase];
2004
    }
2005
2006 171
    private function readSheetProtection(Worksheet $docSheet, SimpleXMLElement $xmlSheet): void
2007
    {
2008 171
        if ($this->readDataOnly || !$xmlSheet->sheetProtection) {
2009 59
            return;
2010
        }
2011
2012 121
        $algorithmName = (string) $xmlSheet->sheetProtection['algorithmName'];
2013 121
        $protection = $docSheet->getProtection();
2014 121
        $protection->setAlgorithm($algorithmName);
2015
2016 121
        if ($algorithmName) {
2017 1
            $protection->setPassword((string) $xmlSheet->sheetProtection['hashValue'], true);
2018 1
            $protection->setSalt((string) $xmlSheet->sheetProtection['saltValue']);
2019 1
            $protection->setSpinCount((int) $xmlSheet->sheetProtection['spinCount']);
2020
        } else {
2021 120
            $protection->setPassword((string) $xmlSheet->sheetProtection['password'], true);
2022
        }
2023
2024 121
        if ($xmlSheet->protectedRanges->protectedRange) {
2025 2
            foreach ($xmlSheet->protectedRanges->protectedRange as $protectedRange) {
2026 2
                $docSheet->protectCells((string) $protectedRange['sqref'], (string) $protectedRange['password'], true);
2027
            }
2028
        }
2029 121
    }
2030
2031 175
    private function readAutoFilterTables(
2032
        SimpleXMLElement $xmlSheet,
2033
        Worksheet $docSheet,
2034
        string $dir,
2035
        string $fileWorksheet,
2036
        ZipArchive $zip
2037
    ): void {
2038 175
        if ($xmlSheet && $xmlSheet->autoFilter) {
2039
            // In older files, autofilter structure is defined in the worksheet file
2040 2
            (new AutoFilter($docSheet, $xmlSheet))->load();
2041 173
        } elseif ($xmlSheet && $xmlSheet->tableParts && $xmlSheet->tableParts['count'] > 0) {
2042
            // But for Office365, MS decided to make it all just a bit more complicated
2043 1
            $this->readAutoFilterTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet);
2044
        }
2045 175
    }
2046
2047 1
    private function readAutoFilterTablesInTablesFile(
2048
        SimpleXMLElement $xmlSheet,
2049
        string $dir,
2050
        string $fileWorksheet,
2051
        ZipArchive $zip,
2052
        Worksheet $docSheet
2053
    ): void {
2054 1
        foreach ($xmlSheet->tableParts->tablePart as $tablePart) {
2055 1
            $relation = self::getAttributes($tablePart, Namespaces::SCHEMA_OFFICE_DOCUMENT);
2056 1
            $tablePartRel = (string) $relation['id'];
2057 1
            $relationsFileName = dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels';
2058
2059 1
            if ($zip->locateName($relationsFileName)) {
2060 1
                $relsTableReferences = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS);
2061 1
                foreach ($relsTableReferences->Relationship as $relationship) {
2062 1
                    $relationshipAttributes = self::getAttributes($relationship, '');
2063
2064 1
                    if ((string) $relationshipAttributes['Id'] === $tablePartRel) {
2065 1
                        $relationshipFileName = (string) $relationshipAttributes['Target'];
2066 1
                        $relationshipFilePath = dirname("$dir/$fileWorksheet") . '/' . $relationshipFileName;
2067 1
                        $relationshipFilePath = File::realpath($relationshipFilePath);
2068
2069 1
                        if ($this->fileExistsInArchive($this->zip, $relationshipFilePath)) {
2070 1
                            $autoFilter = $this->loadZip($relationshipFilePath);
2071 1
                            (new AutoFilter($docSheet, $autoFilter))->load();
2072
                        }
2073
                    }
2074
                }
2075
            }
2076
        }
2077 1
    }
2078
}
2079