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 +324 -2 3.3.4 → 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;
@@ -63,8 +68,29 @@
63 68 protected array $sharedFormulae = [];
64 69
65 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
@@ -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
@@ -943,8 +1075,12 @@
943 1075 }
944 1076
945 1077 $this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS, $tableStyles, $dxfs);
946 1078
1079 + if ($this->readDataOnly === false) {
1080 + $this->readPivotTables($docSheet, $dir, $fileWorksheet, $zip, $unparsedLoadedData);
1081 + }
1082 +
947 1083 if ($xmlSheetNS && $xmlSheetNS->mergeCells && $xmlSheetNS->mergeCells->mergeCell && !$this->readDataOnly) {
948 1084 foreach ($xmlSheetNS->mergeCells->mergeCell as $mergeCellx) {
949 1085 $mergeCell = $mergeCellx->attributes();
950 1086 $mergeRef = (string) ($mergeCell['ref'] ?? '');
@@ -992,8 +1128,15 @@
992 1128 if ($xmlSheet && $xmlSheet->dataValidations && !$this->readDataOnly) {
993 1129 (new DataValidations($docSheet, $xmlSheet))->load();
994 1130 }
995 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 +
996 1139 // unparsed sheet AlternateContent
997 1140 if ($xmlSheet && !$this->readDataOnly) {
998 1141 $mc = $xmlSheet->children(Namespaces::COMPATIBILITY);
999 1142 if ($mc->AlternateContent) {
@@ -1825,8 +1968,24 @@
1825 1968 $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $locatedSheet, $extractedRange, false));
1826 1969 }
1827 1970 }
1828 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 + }
1829 1988 }
1830 1989 if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
1831 1990 $excel->createSheet();
1832 1991 }
@@ -1885,8 +2044,11 @@
1885 2044 break;
1886 2045
1887 2046 // unparsed
1888 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':
1889 2051 $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType'];
1890 2052
1891 2053 break;
1892 2054 }
@@ -2086,9 +2248,9 @@
2086 2248 // no style index means 0, it seems
2087 2249 $cAttrS = isset($styles[$cAttrS]) ? $cAttrS : 0;
2088 2250 $cell->setXfIndex($cAttrS);
2089 2251 // issue 3495
2090 - if ($cellDataType === DataType::TYPE_FORMULA && $styles[$cAttrS]->quotePrefix === true) { //* @phpstan-ignore-line
2252 + if ($cellDataType === DataType::TYPE_FORMULA && $styles[$cAttrS]->quotePrefix === true) { //* @phpstan-ignore property.notFound (quotePrefix does exist)
2091 2253 $holdSelected = $docSheet->getSelectedCells();
2092 2254 $cell->getStyle()->setQuotePrefix(false);
2093 2255 $docSheet->setSelectedCells($holdSelected);
2094 2256 }
@@ -2634,8 +2796,168 @@
2634 2796 }
2635 2797 }
2636 2798 }
2637 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);
2638 2960 }
2639 2961
2640 2962 /** @return mixed[] */
2641 2963 private static function extractStyles(?SimpleXMLElement $sxml, string $node1, string $node2): array