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
← All changes | libraries/vendor/PhpSpreadsheet/Reader/Xlsx.php +572 -196 3.3.2 → 3.4 View file →
@@ -2,8 +2,9 @@
2 2
3 3 namespace TablePress\PhpOffice\PhpSpreadsheet\Reader;
4 4
5 5 use TablePress\Composer\Pcre\Preg;
6 +use InvalidArgumentException;
6 7 use TablePress\PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
7 8 use TablePress\PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8 9 use TablePress\PhpOffice\PhpSpreadsheet\Cell\DataType;
9 10 use TablePress\PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
@@ -17,12 +18,14 @@
17 18 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Xlsx\DataValidations;
18 19 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Xlsx\Hyperlinks;
19 20 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
20 21 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Xlsx\PageSetup;
22 +use TablePress\PhpOffice\PhpSpreadsheet\Reader\Xlsx\PivotTableReader;
21 23 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Xlsx\Properties as PropertyReader;
22 24 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Xlsx\SharedFormula;
23 25 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViewOptions;
24 26 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViews;
27 +use TablePress\PhpOffice\PhpSpreadsheet\Reader\Xlsx\Sparklines;
25 28 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles;
26 29 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Xlsx\TableReader;
27 30 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Xlsx\Theme;
28 31 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Xlsx\WorkbookView;
@@ -31,9 +34,11 @@
31 34 use TablePress\PhpOffice\PhpSpreadsheet\Shared\Date;
32 35 use TablePress\PhpOffice\PhpSpreadsheet\Shared\Drawing;
33 36 use TablePress\PhpOffice\PhpSpreadsheet\Shared\File;
34 37 use TablePress\PhpOffice\PhpSpreadsheet\Shared\Font;
38 +use TablePress\PhpOffice\PhpSpreadsheet\Shared\OLE;
35 39 use TablePress\PhpOffice\PhpSpreadsheet\Shared\StringHelper;
40 +use TablePress\PhpOffice\PhpSpreadsheet\Shared\Xlsx\AgileEncryption;
36 41 use TablePress\PhpOffice\PhpSpreadsheet\Spreadsheet;
37 42 use TablePress\PhpOffice\PhpSpreadsheet\Style\Color;
38 43 use TablePress\PhpOffice\PhpSpreadsheet\Style\Font as StyleFont;
39 44 use TablePress\PhpOffice\PhpSpreadsheet\Style\NumberFormat;
@@ -52,19 +57,40 @@
52 57
53 58 /**
54 59 * ReferenceHelper instance.
55 60 */
56 - private ReferenceHelper $referenceHelper;
61 + protected ReferenceHelper $referenceHelper;
57 62
58 - private ZipArchive $zip;
63 + protected ZipArchive $zip;
59 64
60 65 private Styles $styleReader;
61 66
62 67 /** @var SharedFormula[] */
63 - private array $sharedFormulae = [];
68 + protected array $sharedFormulae = [];
64 69
65 - private bool $parseHuge = false;
70 + protected bool $parseHuge = false;
66 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 +
67 93 /**
68 94 * Allow use of LIBXML_PARSEHUGE.
69 95 * This option can lead to memory leaks and failures,
70 96 * and is not recommended. But some very large spreadsheets
@@ -90,9 +116,9 @@
90 116 */
91 117 public function canRead(string $filename): bool
92 118 {
93 119 if (!File::testFileNoThrow($filename, self::INITIAL_FILE)) {
94 - return false;
120 + return $this->hasEncryptedPackage($filename);
95 121 }
96 122
97 123 $result = false;
98 124 $this->zip = $zip = new ZipArchive();
@@ -106,8 +132,77 @@
106 132
107 133 return $result;
108 134 }
109 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 +
110 205 /**
111 206 * @param mixed $value
112 207 */
113 208 public static function testSimpleXml($value): SimpleXMLElement
@@ -184,8 +279,23 @@
184 279 * @return string[]
185 280 */
186 281 public function listWorksheetNames(string $filename): array
187 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 + {
188 298 File::assertFile($filename, self::INITIAL_FILE);
189 299
190 300 $worksheetNames = [];
191 301
@@ -221,8 +331,25 @@
221 331 * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
222 332 */
223 333 public function listWorksheetInfo(string $filename): array
224 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 + {
225 352 File::assertFile($filename, self::INITIAL_FILE);
226 353
227 354 $worksheetInfo = [];
228 355
@@ -322,9 +449,9 @@
322 449
323 450 return $worksheetInfo;
324 451 }
325 452
326 - private static function castToBoolean(SimpleXMLElement $c): bool
453 + protected static function castToBoolean(SimpleXMLElement $c): bool
327 454 {
328 455 $value = isset($c->v) ? (string) $c->v : null;
329 456 if ($value == '0') {
330 457 return false;
@@ -334,14 +461,14 @@
334 461
335 462 return (bool) $c->v;
336 463 }
337 464
338 - private static function castToError(?SimpleXMLElement $c): ?string
465 + protected static function castToError(?SimpleXMLElement $c): ?string
339 466 {
340 467 return isset($c, $c->v) ? (string) $c->v : null;
341 468 }
342 469
343 - private static function castToString(?SimpleXMLElement $c): ?string
470 + protected static function castToString(?SimpleXMLElement $c): ?string
344 471 {
345 472 return isset($c, $c->v) ? (string) $c->v : null;
346 473 }
347 474
@@ -353,9 +480,9 @@
353 480 /**
354 481 * @param mixed $value
355 482 * @param mixed $calculatedValue
356 483 */
357 - private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, &$value, &$calculatedValue, string $castBaseType, bool $updateSharedCells = true): void
484 + protected function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, &$value, &$calculatedValue, string $castBaseType, bool $updateSharedCells = true): void
358 485 {
359 486 if ($c === null) {
360 487 return;
361 488 }
@@ -405,9 +532,9 @@
405 532
406 533 return $contents !== false;
407 534 }
408 535
409 - private function getFromZipArchive(ZipArchive $archive, string $fileName = ''): string
536 + protected function getFromZipArchive(ZipArchive $archive, string $fileName = ''): string
410 537 {
411 538 // Root-relative paths
412 539 if (str_contains($fileName, '//')) {
413 540 $fileName = (string) substr($fileName, strpos($fileName, '//') + 1);
@@ -586,8 +713,9 @@
586 713 $relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', Namespaces::RELATIONSHIPS);
587 714 $relsWorkbook->registerXPathNamespace('rel', Namespaces::RELATIONSHIPS);
588 715
589 716 $worksheets = [];
717 + $pivotCacheRels = [];
590 718 $macros = $customUI = null;
591 719 foreach ($relsWorkbook->Relationship as $elex) {
592 720 $ele = self::getAttributes($elex);
593 721 switch ($ele['Type']) {
@@ -601,8 +729,12 @@
601 729 $worksheets[(string) $ele['Id']] = $ele['Target'];
602 730 }
603 731
604 732 break;
733 + case Namespaces::RELATIONSHIPS_PIVOT_CACHE_DEFINITION:
734 + $pivotCacheRels[(string) $ele['Id']] = File::realpath("$dir/" . (string) $ele['Target']);
735 +
736 + break;
605 737 // a vbaProject ? (: some macros)
606 738 case Namespaces::VBA:
607 739 $macros = $ele['Target'];
608 740
@@ -897,190 +1029,28 @@
897 1029 $sheetViewOptions = new SheetViewOptions($docSheet, $xmlSheetNS);
898 1030 $sheetViewOptions->load($this->readDataOnly, $this->styleReader);
899 1031
900 1032 (new ColumnAndRowAttributes($docSheet, $xmlSheetNS))
901 - ->load($this->getReadFilter(), $this->readDataOnly, $this->ignoreRowsWithNoCells);
1033 + ->load($this->readFilter, $this->readDataOnly, $this->ignoreRowsWithNoCells);
902 1034 }
903 1035
904 1036 $holdSelectedCells = $docSheet->getSelectedCells();
905 - if ($xmlSheetNS && $xmlSheetNS->sheetData && $xmlSheetNS->sheetData->row) {
906 - $cIndex = 1; // Cell Start from 1
907 - foreach ($xmlSheetNS->sheetData->row as $row) {
908 - $rowIndex = 1;
909 - foreach ($row->c as $c) {
910 - $cAttr = self::getAttributes($c);
911 - $r = (string) $cAttr['r'];
912 - if ($r == '') {
913 - $r = Coordinate::stringFromColumnIndex($rowIndex) . $cIndex;
914 - }
915 - $cellDataType = (string) $cAttr['t'];
916 - $originalCellDataTypeNumeric = $cellDataType === '';
917 - $value = null;
918 - $calculatedValue = null;
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 + );
919 1052
920 - // Read cell?
921 - $coordinates = Coordinate::coordinateFromString($r);
922 -
923 - if (!$this->getReadFilter()->readCell($coordinates[0], (int) $coordinates[1], $docSheet->getTitle())) {
924 - // Normally, just testing for the f attribute should identify this cell as containing a formula
925 - // that we need to read, even though it is outside of the filter range, in case it is a shared formula.
926 - // But in some cases, this attribute isn't set; so we need to delve a level deeper and look at
927 - // whether or not the cell has a child formula element that is shared.
928 - if (isset($cAttr->f) || (isset($c->f, $c->f->attributes()['t']) && strtolower((string) $c->f->attributes()['t']) === 'shared')) {
929 - $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError', false);
930 - }
931 - ++$rowIndex;
932 -
933 - continue;
934 - }
935 -
936 - // Read cell!
937 - $useFormula = isset($c->f)
938 - && ((string) $c->f !== '' || (isset($c->f->attributes()['t']) && strtolower((string) $c->f->attributes()['t']) === 'shared'));
939 - switch ($cellDataType) {
940 - case DataType::TYPE_STRING:
941 - if ((string) $c->v != '') {
942 - $value = $sharedStrings[(int) ($c->v)];
943 -
944 - if ($value instanceof RichText) {
945 - $value = clone $value;
946 - }
947 - } else {
948 - $value = '';
949 - }
950 -
951 - break;
952 - case DataType::TYPE_BOOL:
953 - if (!$useFormula) {
954 - if (isset($c->v)) {
955 - $value = self::castToBoolean($c);
956 - } else {
957 - $value = null;
958 - $cellDataType = DataType::TYPE_NULL;
959 - }
960 - } else {
961 - // Formula
962 - $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToBoolean');
963 - self::storeFormulaAttributes($c->f, $docSheet, $r);
964 - }
965 -
966 - break;
967 - case DataType::TYPE_STRING2:
968 - if ($useFormula) {
969 - $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
970 - self::storeFormulaAttributes($c->f, $docSheet, $r);
971 - } else {
972 - $value = self::castToString($c);
973 - }
974 -
975 - break;
976 - case DataType::TYPE_INLINE:
977 - if ($useFormula) {
978 - $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError');
979 - self::storeFormulaAttributes($c->f, $docSheet, $r);
980 - } else {
981 - $value = $this->parseRichText($c->is);
982 - }
983 -
984 - break;
985 - case DataType::TYPE_ERROR:
986 - if (isset($cAttr->vm, $richData['image']['rId' . $cAttr->vm]) && !$useFormula) {
987 - $imagePath = $dir . '/' . str_replace('../', '', $richData['image']['rId' . $cAttr->vm]);
988 - $objDrawing = new \TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
989 - $objDrawing->setPath(
990 - 'zip://' . File::realpath($filename) . '#' . $imagePath,
991 - false,
992 - $zip
993 - );
994 -
995 - $objDrawing->setCoordinates($r);
996 - $objDrawing->setResizeProportional(false);
997 - $objDrawing->setInCell(true);
998 - $objDrawing->setWorksheet($docSheet);
999 -
1000 - $value = $objDrawing;
1001 - $cellDataType = DataType::TYPE_DRAWING_IN_CELL;
1002 - $c->t = DataType::TYPE_ERROR;
1003 -
1004 - break;
1005 - }
1006 -
1007 - if (!$useFormula) {
1008 - $value = self::castToError($c);
1009 - } else {
1010 - // Formula
1011 - $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError');
1012 - $eattr = $c->attributes();
1013 - if (isset($eattr['vm'])) {
1014 - if ($calculatedValue === ExcelError::VALUE()) {
1015 - $calculatedValue = ExcelError::SPILL();
1016 - }
1017 - }
1018 - }
1019 -
1020 - break;
1021 - default:
1022 - if (!$useFormula) {
1023 - $value = self::castToString($c);
1024 - if (is_numeric($value)) {
1025 - $value += 0;
1026 - $cellDataType = DataType::TYPE_NUMERIC;
1027 - }
1028 - } else {
1029 - // Formula
1030 - $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
1031 - if (is_numeric($calculatedValue)) {
1032 - $calculatedValue += 0;
1033 - }
1034 - self::storeFormulaAttributes($c->f, $docSheet, $r);
1035 - }
1036 -
1037 - break;
1038 - }
1039 -
1040 - // read empty cells or the cells are not empty
1041 - if ($this->readEmptyCells || ($value !== null && $value !== '')) {
1042 - // Rich text?
1043 - if ($value instanceof RichText && $this->readDataOnly) {
1044 - $value = $value->getPlainText();
1045 - }
1046 -
1047 - $cell = $docSheet->getCell($r);
1048 - // Assign value
1049 - if ($cellDataType != '') {
1050 - // it is possible, that datatype is numeric but with an empty string, which result in an error
1051 - if ($cellDataType === DataType::TYPE_NUMERIC && ($value === '' || $value === null)) {
1052 - $cellDataType = DataType::TYPE_NULL;
1053 - }
1054 - if ($cellDataType !== DataType::TYPE_NULL) {
1055 - $cell->setValueExplicit($value, $cellDataType);
1056 - }
1057 - } else {
1058 - $cell->setValue($value);
1059 - }
1060 - if ($calculatedValue !== null) {
1061 - $cell->setCalculatedValue($calculatedValue, $originalCellDataTypeNumeric);
1062 - }
1063 -
1064 - // Style information?
1065 - if (!$this->readDataOnly) {
1066 - $cAttrS = (int) ($cAttr['s'] ?? 0);
1067 - // no style index means 0, it seems
1068 - $cAttrS = isset($styles[$cAttrS]) ? $cAttrS : 0;
1069 - $cell->setXfIndex($cAttrS);
1070 - // issue 3495
1071 - if ($cellDataType === DataType::TYPE_FORMULA && $styles[$cAttrS]->quotePrefix === true) { //* @phpstan-ignore-line
1072 - $holdSelected = $docSheet->getSelectedCells();
1073 - $cell->getStyle()->setQuotePrefix(false);
1074 - $docSheet->setSelectedCells($holdSelected);
1075 - }
1076 - }
1077 - }
1078 - ++$rowIndex;
1079 - }
1080 - ++$cIndex;
1081 - }
1082 - }
1083 1053 $docSheet->setSelectedCells($holdSelectedCells);
1084 1054 if (!$this->readDataOnly && $xmlSheetNS && $xmlSheetNS->ignoredErrors) {
1085 1055 foreach ($xmlSheetNS->ignoredErrors->ignoredError as $ignoredError) {
1086 1056 $this->processIgnoredErrors($ignoredError, $docSheet);
@@ -1105,8 +1075,12 @@
1105 1075 }
1106 1076
1107 1077 $this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS, $tableStyles, $dxfs);
1108 1078
1079 + if ($this->readDataOnly === false) {
1080 + $this->readPivotTables($docSheet, $dir, $fileWorksheet, $zip, $unparsedLoadedData);
1081 + }
1082 +
1109 1083 if ($xmlSheetNS && $xmlSheetNS->mergeCells && $xmlSheetNS->mergeCells->mergeCell && !$this->readDataOnly) {
1110 1084 foreach ($xmlSheetNS->mergeCells->mergeCell as $mergeCellx) {
1111 1085 $mergeCell = $mergeCellx->attributes();
1112 1086 $mergeRef = (string) ($mergeCell['ref'] ?? '');
@@ -1154,8 +1128,15 @@
1154 1128 if ($xmlSheet && $xmlSheet->dataValidations && !$this->readDataOnly) {
1155 1129 (new DataValidations($docSheet, $xmlSheet))->load();
1156 1130 }
1157 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 +
1158 1139 // unparsed sheet AlternateContent
1159 1140 if ($xmlSheet && !$this->readDataOnly) {
1160 1141 $mc = $xmlSheet->children(Namespaces::COMPATIBILITY);
1161 1142 if ($mc->AlternateContent) {
@@ -1987,8 +1968,24 @@
1987 1968 $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $locatedSheet, $extractedRange, false));
1988 1969 }
1989 1970 }
1990 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 + }
1991 1988 }
1992 1989 if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
1993 1990 $excel->createSheet();
1994 1991 }
@@ -2047,8 +2044,11 @@
2047 2044 break;
2048 2045
2049 2046 // unparsed
2050 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 2051 $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType'];
2052 2052
2053 2053 break;
2054 2054 }
@@ -2062,9 +2062,208 @@
2062 2062
2063 2063 return $excel;
2064 2064 }
2065 2065
2066 - private function parseRichText(?SimpleXMLElement $is): RichText
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
2067 2266 {
2068 2267 $value = new RichText();
2069 2268
2070 2269 if (isset($is->t)) {
@@ -2382,9 +2581,9 @@
2382 2581 private static function getLockValue(SimpleXMLElement $protection, string $key): ?bool
2383 2582 {
2384 2583 $returnValue = null;
2385 2584 $protectKey = $protection[$key];
2386 - if (!empty($protectKey)) {
2585 + if (isset($protectKey)) {
2387 2586 $protectKey = (string) $protectKey;
2388 2587 $returnValue = $protectKey !== 'false' && (bool) $protectKey;
2389 2588 }
2390 2589
@@ -2599,8 +2798,168 @@
2599 2798 }
2600 2799 }
2601 2800 }
2602 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 +
2603 2962 /** @return mixed[] */
2604 2963 private static function extractStyles(?SimpleXMLElement $sxml, string $node1, string $node2): array
2605 2964 {
2606 2965 $array = [];
@@ -2640,8 +2999,10 @@
2640 2999 $formula = (string) ($attributes['formula'] ?? '');
2641 3000 $formulaRange = (string) ($attributes['formulaRange'] ?? '');
2642 3001 $twoDigitTextYear = (string) ($attributes['twoDigitTextYear'] ?? '');
2643 3002 $evalError = (string) ($attributes['evalError'] ?? '');
3003 + $attributes2 = self::getAttributes($xml, Namespaces::MISLEADING_FORMAT);
3004 + $misleadingFormat = (string) ($attributes2['misleadingFormat'] ?? '');
2644 3005 if (!empty($sqref)) {
2645 3006 $explodedSqref = explode(' ', $sqref);
2646 3007 $pattern1 = '/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/';
2647 3008 foreach ($explodedSqref as $sqref1) {
@@ -2661,22 +3022,37 @@
2661 3022 if (!$cellCollection->has2("$col$row")) {
2662 3023 continue;
2663 3024 }
2664 3025 if ($numberStoredAsText === '1') {
2665 - $sheet->getCell("$col$row")->getIgnoredErrors()->setNumberStoredAsText(true);
3026 + $sheet->getCell("$col$row")
3027 + ->getIgnoredErrors()
3028 + ->setNumberStoredAsText(true);
2666 3029 }
2667 3030 if ($formula === '1') {
2668 - $sheet->getCell("$col$row")->getIgnoredErrors()->setFormula(true);
3031 + $sheet->getCell("$col$row")
3032 + ->getIgnoredErrors()
3033 + ->setFormula(true);
2669 3034 }
2670 3035 if ($formulaRange === '1') {
2671 - $sheet->getCell("$col$row")->getIgnoredErrors()->setFormulaRange(true);
3036 + $sheet->getCell("$col$row")
3037 + ->getIgnoredErrors()
3038 + ->setFormulaRange(true);
2672 3039 }
2673 3040 if ($twoDigitTextYear === '1') {
2674 - $sheet->getCell("$col$row")->getIgnoredErrors()->setTwoDigitTextYear(true);
3041 + $sheet->getCell("$col$row")
3042 + ->getIgnoredErrors()
3043 + ->setTwoDigitTextYear(true);
2675 3044 }
2676 3045 if ($evalError === '1') {
2677 - $sheet->getCell("$col$row")->getIgnoredErrors()->setEvalError(true);
3046 + $sheet->getCell("$col$row")
3047 + ->getIgnoredErrors()
3048 + ->setEvalError(true);
2678 3049 }
3050 + if ($misleadingFormat === '1') {
3051 + $sheet->getCell("$col$row")
3052 + ->getIgnoredErrors()
3053 + ->setMisleadingFormat(true);
3054 + }
2679 3055 }
2680 3056 }
2681 3057 }
2682 3058 }
@@ -2682,9 +3058,9 @@
2682 3058 }
2683 3059 }
2684 3060 }
2685 3061
2686 - private static function storeFormulaAttributes(SimpleXMLElement $f, Worksheet $docSheet, string $r): void
3062 + protected static function storeFormulaAttributes(SimpleXMLElement $f, Worksheet $docSheet, string $r): void
2687 3063 {
2688 3064 $formulaAttributes = [];
2689 3065 $attributes = $f->attributes();
2690 3066 if (isset($attributes['t'])) {