Skip to content

Commit 82ea1d5

Browse files
authored
Fix for #1516 (#1530)
This problem is that ZipStream, in contrast to ZipArchive, is saving 2 files with the same path. I have opened an issue with ZipStream, who agree that this appears to be a bug. For the case in question, PhpSpreadsheet is attempting to save a file with the same path twice (and unexpectedly succeeding) because of a clone operation. This fix attempts to rectify the problem by keeping track of all the paths being saved in the zip file, and not attempting to save any duplicate paths. The problem case attempted to save printersettings1.bin twice, but there are other possible exposures, e.g. by cloning a sheet with a drawing.The new test cases clone an existing sample which has both printer settings and drawings.
1 parent 12dd92b commit 82ea1d5

File tree

2 files changed

+154
-35
lines changed

2 files changed

+154
-35
lines changed

src/PhpSpreadsheet/Writer/Xlsx.php

+53-35
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ class Xlsx extends BaseWriter
107107
*/
108108
private $drawingHashTable;
109109

110+
/**
111+
* Private handle for zip stream.
112+
*
113+
* @var ZipStream
114+
*/
115+
private $zip;
116+
110117
/**
111118
* Create a new Xlsx Writer.
112119
*/
@@ -173,6 +180,7 @@ public function save($pFilename): void
173180
{
174181
if ($this->spreadSheet !== null) {
175182
// garbage collect
183+
$this->pathNames = [];
176184
$this->spreadSheet->garbageCollect();
177185

178186
$this->openFileHandle($pFilename);
@@ -203,73 +211,73 @@ public function save($pFilename): void
203211
$options->setEnableZip64(false);
204212
$options->setOutputStream($this->fileHandle);
205213

206-
$zip = new ZipStream(null, $options);
214+
$this->zip = new ZipStream(null, $options);
207215

208216
// Add [Content_Types].xml to ZIP file
209-
$zip->addFile('[Content_Types].xml', $this->getWriterPart('ContentTypes')->writeContentTypes($this->spreadSheet, $this->includeCharts));
217+
$this->addZipFile('[Content_Types].xml', $this->getWriterPart('ContentTypes')->writeContentTypes($this->spreadSheet, $this->includeCharts));
210218

211219
//if hasMacros, add the vbaProject.bin file, Certificate file(if exists)
212220
if ($this->spreadSheet->hasMacros()) {
213221
$macrosCode = $this->spreadSheet->getMacrosCode();
214222
if ($macrosCode !== null) {
215223
// we have the code ?
216-
$zip->addFile('xl/vbaProject.bin', $macrosCode); //allways in 'xl', allways named vbaProject.bin
224+
$this->addZipFile('xl/vbaProject.bin', $macrosCode); //allways in 'xl', allways named vbaProject.bin
217225
if ($this->spreadSheet->hasMacrosCertificate()) {
218226
//signed macros ?
219227
// Yes : add the certificate file and the related rels file
220-
$zip->addFile('xl/vbaProjectSignature.bin', $this->spreadSheet->getMacrosCertificate());
221-
$zip->addFile('xl/_rels/vbaProject.bin.rels', $this->getWriterPart('RelsVBA')->writeVBARelationships($this->spreadSheet));
228+
$this->addZipFile('xl/vbaProjectSignature.bin', $this->spreadSheet->getMacrosCertificate());
229+
$this->addZipFile('xl/_rels/vbaProject.bin.rels', $this->getWriterPart('RelsVBA')->writeVBARelationships($this->spreadSheet));
222230
}
223231
}
224232
}
225233
//a custom UI in this workbook ? add it ("base" xml and additional objects (pictures) and rels)
226234
if ($this->spreadSheet->hasRibbon()) {
227235
$tmpRibbonTarget = $this->spreadSheet->getRibbonXMLData('target');
228-
$zip->addFile($tmpRibbonTarget, $this->spreadSheet->getRibbonXMLData('data'));
236+
$this->addZipFile($tmpRibbonTarget, $this->spreadSheet->getRibbonXMLData('data'));
229237
if ($this->spreadSheet->hasRibbonBinObjects()) {
230238
$tmpRootPath = dirname($tmpRibbonTarget) . '/';
231239
$ribbonBinObjects = $this->spreadSheet->getRibbonBinObjects('data'); //the files to write
232240
foreach ($ribbonBinObjects as $aPath => $aContent) {
233-
$zip->addFile($tmpRootPath . $aPath, $aContent);
241+
$this->addZipFile($tmpRootPath . $aPath, $aContent);
234242
}
235243
//the rels for files
236-
$zip->addFile($tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels', $this->getWriterPart('RelsRibbonObjects')->writeRibbonRelationships($this->spreadSheet));
244+
$this->addZipFile($tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels', $this->getWriterPart('RelsRibbonObjects')->writeRibbonRelationships($this->spreadSheet));
237245
}
238246
}
239247

240248
// Add relationships to ZIP file
241-
$zip->addFile('_rels/.rels', $this->getWriterPart('Rels')->writeRelationships($this->spreadSheet));
242-
$zip->addFile('xl/_rels/workbook.xml.rels', $this->getWriterPart('Rels')->writeWorkbookRelationships($this->spreadSheet));
249+
$this->addZipFile('_rels/.rels', $this->getWriterPart('Rels')->writeRelationships($this->spreadSheet));
250+
$this->addZipFile('xl/_rels/workbook.xml.rels', $this->getWriterPart('Rels')->writeWorkbookRelationships($this->spreadSheet));
243251

244252
// Add document properties to ZIP file
245-
$zip->addFile('docProps/app.xml', $this->getWriterPart('DocProps')->writeDocPropsApp($this->spreadSheet));
246-
$zip->addFile('docProps/core.xml', $this->getWriterPart('DocProps')->writeDocPropsCore($this->spreadSheet));
253+
$this->addZipFile('docProps/app.xml', $this->getWriterPart('DocProps')->writeDocPropsApp($this->spreadSheet));
254+
$this->addZipFile('docProps/core.xml', $this->getWriterPart('DocProps')->writeDocPropsCore($this->spreadSheet));
247255
$customPropertiesPart = $this->getWriterPart('DocProps')->writeDocPropsCustom($this->spreadSheet);
248256
if ($customPropertiesPart !== null) {
249-
$zip->addFile('docProps/custom.xml', $customPropertiesPart);
257+
$this->addZipFile('docProps/custom.xml', $customPropertiesPart);
250258
}
251259

252260
// Add theme to ZIP file
253-
$zip->addFile('xl/theme/theme1.xml', $this->getWriterPart('Theme')->writeTheme($this->spreadSheet));
261+
$this->addZipFile('xl/theme/theme1.xml', $this->getWriterPart('Theme')->writeTheme($this->spreadSheet));
254262

255263
// Add string table to ZIP file
256-
$zip->addFile('xl/sharedStrings.xml', $this->getWriterPart('StringTable')->writeStringTable($this->stringTable));
264+
$this->addZipFile('xl/sharedStrings.xml', $this->getWriterPart('StringTable')->writeStringTable($this->stringTable));
257265

258266
// Add styles to ZIP file
259-
$zip->addFile('xl/styles.xml', $this->getWriterPart('Style')->writeStyles($this->spreadSheet));
267+
$this->addZipFile('xl/styles.xml', $this->getWriterPart('Style')->writeStyles($this->spreadSheet));
260268

261269
// Add workbook to ZIP file
262-
$zip->addFile('xl/workbook.xml', $this->getWriterPart('Workbook')->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas));
270+
$this->addZipFile('xl/workbook.xml', $this->getWriterPart('Workbook')->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas));
263271

264272
$chartCount = 0;
265273
// Add worksheets
266274
for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) {
267-
$zip->addFile('xl/worksheets/sheet' . ($i + 1) . '.xml', $this->getWriterPart('Worksheet')->writeWorksheet($this->spreadSheet->getSheet($i), $this->stringTable, $this->includeCharts));
275+
$this->addZipFile('xl/worksheets/sheet' . ($i + 1) . '.xml', $this->getWriterPart('Worksheet')->writeWorksheet($this->spreadSheet->getSheet($i), $this->stringTable, $this->includeCharts));
268276
if ($this->includeCharts) {
269277
$charts = $this->spreadSheet->getSheet($i)->getChartCollection();
270278
if (count($charts) > 0) {
271279
foreach ($charts as $chart) {
272-
$zip->addFile('xl/charts/chart' . ($chartCount + 1) . '.xml', $this->getWriterPart('Chart')->writeChart($chart, $this->preCalculateFormulas));
280+
$this->addZipFile('xl/charts/chart' . ($chartCount + 1) . '.xml', $this->getWriterPart('Chart')->writeChart($chart, $this->preCalculateFormulas));
273281
++$chartCount;
274282
}
275283
}
@@ -280,19 +288,19 @@ public function save($pFilename): void
280288
// Add worksheet relationships (drawings, ...)
281289
for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) {
282290
// Add relationships
283-
$zip->addFile('xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts));
291+
$this->addZipFile('xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts));
284292

285293
// Add unparsedLoadedData
286294
$sheetCodeName = $this->spreadSheet->getSheet($i)->getCodeName();
287295
$unparsedLoadedData = $this->spreadSheet->getUnparsedLoadedData();
288296
if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'])) {
289297
foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'] as $ctrlProp) {
290-
$zip->addFile($ctrlProp['filePath'], $ctrlProp['content']);
298+
$this->addZipFile($ctrlProp['filePath'], $ctrlProp['content']);
291299
}
292300
}
293301
if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'])) {
294302
foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'] as $ctrlProp) {
295-
$zip->addFile($ctrlProp['filePath'], $ctrlProp['content']);
303+
$this->addZipFile($ctrlProp['filePath'], $ctrlProp['content']);
296304
}
297305
}
298306

@@ -305,13 +313,13 @@ public function save($pFilename): void
305313
// Add drawing and image relationship parts
306314
if (($drawingCount > 0) || ($chartCount > 0)) {
307315
// Drawing relationships
308-
$zip->addFile('xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts));
316+
$this->addZipFile('xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts));
309317

310318
// Drawings
311-
$zip->addFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts));
319+
$this->addZipFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts));
312320
} elseif (isset($unparsedLoadedData['sheets'][$sheetCodeName]['drawingAlternateContents'])) {
313321
// Drawings
314-
$zip->addFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts));
322+
$this->addZipFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts));
315323
}
316324

317325
// Add unparsed drawings
@@ -320,38 +328,38 @@ public function save($pFilename): void
320328
$drawingFile = array_search($relId, $unparsedLoadedData['sheets'][$sheetCodeName]['drawingOriginalIds']);
321329
if ($drawingFile !== false) {
322330
$drawingFile = ltrim($drawingFile, '.');
323-
$zip->addFile('xl' . $drawingFile, $drawingXml);
331+
$this->addZipFile('xl' . $drawingFile, $drawingXml);
324332
}
325333
}
326334
}
327335

328336
// Add comment relationship parts
329337
if (count($this->spreadSheet->getSheet($i)->getComments()) > 0) {
330338
// VML Comments
331-
$zip->addFile('xl/drawings/vmlDrawing' . ($i + 1) . '.vml', $this->getWriterPart('Comments')->writeVMLComments($this->spreadSheet->getSheet($i)));
339+
$this->addZipFile('xl/drawings/vmlDrawing' . ($i + 1) . '.vml', $this->getWriterPart('Comments')->writeVMLComments($this->spreadSheet->getSheet($i)));
332340

333341
// Comments
334-
$zip->addFile('xl/comments' . ($i + 1) . '.xml', $this->getWriterPart('Comments')->writeComments($this->spreadSheet->getSheet($i)));
342+
$this->addZipFile('xl/comments' . ($i + 1) . '.xml', $this->getWriterPart('Comments')->writeComments($this->spreadSheet->getSheet($i)));
335343
}
336344

337345
// Add unparsed relationship parts
338346
if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'])) {
339347
foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'] as $vmlDrawing) {
340-
$zip->addFile($vmlDrawing['filePath'], $vmlDrawing['content']);
348+
$this->addZipFile($vmlDrawing['filePath'], $vmlDrawing['content']);
341349
}
342350
}
343351

344352
// Add header/footer relationship parts
345353
if (count($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages()) > 0) {
346354
// VML Drawings
347-
$zip->addFile('xl/drawings/vmlDrawingHF' . ($i + 1) . '.vml', $this->getWriterPart('Drawing')->writeVMLHeaderFooterImages($this->spreadSheet->getSheet($i)));
355+
$this->addZipFile('xl/drawings/vmlDrawingHF' . ($i + 1) . '.vml', $this->getWriterPart('Drawing')->writeVMLHeaderFooterImages($this->spreadSheet->getSheet($i)));
348356

349357
// VML Drawing relationships
350-
$zip->addFile('xl/drawings/_rels/vmlDrawingHF' . ($i + 1) . '.vml.rels', $this->getWriterPart('Rels')->writeHeaderFooterDrawingRelationships($this->spreadSheet->getSheet($i)));
358+
$this->addZipFile('xl/drawings/_rels/vmlDrawingHF' . ($i + 1) . '.vml.rels', $this->getWriterPart('Rels')->writeHeaderFooterDrawingRelationships($this->spreadSheet->getSheet($i)));
351359

352360
// Media
353361
foreach ($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages() as $image) {
354-
$zip->addFile('xl/media/' . $image->getIndexedFilename(), file_get_contents($image->getPath()));
362+
$this->addZipFile('xl/media/' . $image->getIndexedFilename(), file_get_contents($image->getPath()));
355363
}
356364
}
357365
}
@@ -374,7 +382,7 @@ public function save($pFilename): void
374382
$imageContents = file_get_contents($imagePath);
375383
}
376384

377-
$zip->addFile('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents);
385+
$this->addZipFile('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents);
378386
} elseif ($this->getDrawingHashTable()->getByIndex($i) instanceof MemoryDrawing) {
379387
ob_start();
380388
call_user_func(
@@ -384,7 +392,7 @@ public function save($pFilename): void
384392
$imageContents = ob_get_contents();
385393
ob_end_clean();
386394

387-
$zip->addFile('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents);
395+
$this->addZipFile('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents);
388396
}
389397
}
390398

@@ -393,7 +401,7 @@ public function save($pFilename): void
393401

394402
// Close file
395403
try {
396-
$zip->finish();
404+
$this->zip->finish();
397405
} catch (OverflowException $e) {
398406
throw new WriterException('Could not close resource.');
399407
}
@@ -535,4 +543,14 @@ public function setOffice2003Compatibility($pValue)
535543

536544
return $this;
537545
}
546+
547+
private $pathNames = [];
548+
549+
private function addZipFile(string $path, string $content): void
550+
{
551+
if (!in_array($path, $this->pathNames)) {
552+
$this->pathNames[] = $path;
553+
$this->zip->addFile($path, $content);
554+
}
555+
}
538556
}

0 commit comments

Comments
 (0)