| @@ -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 |