PluginProbe
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin / 6.5.1.7
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin v6.5.1.7
6.5.1.7 6.5.1.6 6.5.1.5 6.5.1.4 6.5.1.3 6.5.1.2 6.5.1.1 6.5.0.9 6.5.0.8 6.5.0.7 6.5.0.6 trunk 3.4.2.40 3.4.2.41 3.4.2.42 3.4.2.43 3.4.2.44 3.4.2.45 3.4.2.46 3.4.2.47 3.4.2.48 3.4.2.49 3.4.2.50 6.3.2 6.3.3.1 All 47 releases
wpdatatables / lib / phpoffice / phpspreadsheet / src / PhpSpreadsheet / Reader / Xlsx.php

Xlsx.php in wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin 6.5.1.7, at lib/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx.php

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