PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
tablepress / libraries / vendor / PhpSpreadsheet / Reader / Xlsx.php

Xlsx.php in TablePress – Tables in WordPress made easy 3.4, at libraries/vendor/PhpSpreadsheet/Reader/Xlsx.php

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