From 1bcdf15533a5b286e67b81b7e79402eb859044bd Mon Sep 17 00:00:00 2001 From: basbl Date: Fri, 13 Dec 2019 15:59:30 +0100 Subject: [PATCH 1/7] Add maennchen/zipstream-php dependency The built-in ZipArchive class does not have the ability to accept streams. This means that we would always have to write the zip to disk. The ZipStream library does offer support for writing to streams. --- composer.json | 3 +- composer.lock | 344 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 344 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index b3be69b0..6ce3a728 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,8 @@ "ext-zlib": "*", "markbaker/complex": "^1.4", "markbaker/matrix": "^1.2", - "psr/simple-cache": "^1.0" + "psr/simple-cache": "^1.0", + "maennchen/zipstream-php": "^2.0" }, "require-dev": { "dompdf/dompdf": "^0.8.5", diff --git a/composer.lock b/composer.lock index 0575eefe..41ec1b9b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,69 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9c60146d8c78c13d2610a2cec23339a2", + "content-hash": "ab06908c3ff8187971def16c578f1ced", "packages": [ + { + "name": "maennchen/zipstream-php", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "9ceee828f9620b2e5c075e551ec7ed8a7035ac95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9ceee828f9620b2e5c075e551ec7ed8a7035ac95", + "reference": "9ceee828f9620b2e5c075e551ec7ed8a7035ac95", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "myclabs/php-enum": "^1.5", + "php": ">= 7.1", + "psr/http-message": "^1.0" + }, + "require-dev": { + "ext-zip": "*", + "guzzlehttp/guzzle": ">= 6.3", + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": ">= 7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "time": "2020-02-23T01:48:39+00:00" + }, { "name": "markbaker/complex", "version": "1.4.8", @@ -170,6 +231,102 @@ ], "time": "2019-10-06T11:29:25+00:00" }, + { + "name": "myclabs/php-enum", + "version": "1.7.6", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "5f36467c7a87e20fbdc51e524fd8f9d1de80187c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/5f36467c7a87e20fbdc51e524fd8f9d1de80187c", + "reference": "5f36467c7a87e20fbdc51e524fd8f9d1de80187c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "http://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "time": "2020-02-14T08:15:52+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, { "name": "psr/simple-cache", "version": "1.0.1", @@ -323,6 +480,12 @@ "Xdebug", "performance" ], + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + } + ], "time": "2020-03-01T12:26:26+00:00" }, { @@ -667,6 +830,12 @@ } ], "description": "A tool to automatically fix PHP code style", + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], "time": "2020-04-15T18:51:10+00:00" }, { @@ -1703,6 +1872,16 @@ "testing", "xunit" ], + "funding": [ + { + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-04-23T04:39:42+00:00" }, { @@ -2566,6 +2745,12 @@ "fpdi", "pdf" ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", + "type": "tidelift" + } + ], "time": "2020-03-23T15:53:59+00:00" }, { @@ -2693,6 +2878,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-03-30T11:42:42+00:00" }, { @@ -2763,6 +2962,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-03-27T16:56:45+00:00" }, { @@ -2871,6 +3084,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-03-27T16:56:45+00:00" }, { @@ -2920,6 +3147,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-03-27T16:56:45+00:00" }, { @@ -2974,6 +3215,20 @@ "configuration", "options" ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-03-27T16:56:45+00:00" }, { @@ -3032,6 +3287,20 @@ "polyfill", "portable" ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-02-27T09:26:54+00:00" }, { @@ -3091,6 +3360,20 @@ "portable", "shim" ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-03-09T19:04:49+00:00" }, { @@ -3205,6 +3488,20 @@ "portable", "shim" ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-02-27T09:26:54+00:00" }, { @@ -3263,6 +3560,20 @@ "portable", "shim" ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-02-27T09:26:54+00:00" }, { @@ -3312,6 +3623,20 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-03-27T16:56:45+00:00" }, { @@ -3420,6 +3745,20 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-03-27T16:56:45+00:00" }, { @@ -3594,5 +3933,6 @@ "ext-zip": "*", "ext-zlib": "*" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "1.1.0" } From ccb49de301d0b1eb932b70c393b6f3bfc461461f Mon Sep 17 00:00:00 2001 From: basbl Date: Fri, 13 Dec 2019 15:59:55 +0100 Subject: [PATCH 2/7] Add support for streaming zip files in the xlsx and ods writers --- src/PhpSpreadsheet/Writer/IWriter.php | 2 +- src/PhpSpreadsheet/Writer/Ods.php | 70 +++++++++------- src/PhpSpreadsheet/Writer/Xlsx.php | 115 +++++++++++++++----------- 3 files changed, 108 insertions(+), 79 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/IWriter.php b/src/PhpSpreadsheet/Writer/IWriter.php index 448b532f..97b14bef 100644 --- a/src/PhpSpreadsheet/Writer/IWriter.php +++ b/src/PhpSpreadsheet/Writer/IWriter.php @@ -59,7 +59,7 @@ interface IWriter /** * Save PhpSpreadsheet to file. * - * @param string $pFilename Name of the file to save + * @param resource|string $pFilename Name of the file to save * * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception */ diff --git a/src/PhpSpreadsheet/Writer/Ods.php b/src/PhpSpreadsheet/Writer/Ods.php index 43659fa0..2a1dba02 100644 --- a/src/PhpSpreadsheet/Writer/Ods.php +++ b/src/PhpSpreadsheet/Writer/Ods.php @@ -12,7 +12,9 @@ use PhpOffice\PhpSpreadsheet\Writer\Ods\Mimetype; use PhpOffice\PhpSpreadsheet\Writer\Ods\Settings; use PhpOffice\PhpSpreadsheet\Writer\Ods\Styles; use PhpOffice\PhpSpreadsheet\Writer\Ods\Thumbnails; -use ZipArchive; +use ZipStream\Exception\OverflowException; +use ZipStream\Option\Archive; +use ZipStream\ZipStream; class Ods extends BaseWriter { @@ -30,6 +32,11 @@ class Ods extends BaseWriter */ private $spreadSheet; + /** + * @var bool|resource + */ + private $fileHandle; + /** * Create a new Ods. * @@ -73,7 +80,7 @@ class Ods extends BaseWriter /** * Save PhpSpreadsheet to file. * - * @param string $pFilename + * @param resource|string $pFilename * * @throws WriterException */ @@ -88,31 +95,41 @@ class Ods extends BaseWriter // If $pFilename is php://output or php://stdout, make it a temporary file... $originalFilename = $pFilename; - if (strtolower($pFilename) == 'php://output' || strtolower($pFilename) == 'php://stdout') { + if (is_resource($pFilename)) { + $this->fileHandle = $pFilename; + } elseif (strtolower($pFilename) == 'php://output' || strtolower($pFilename) == 'php://stdout') { + // If $pFilename is php://output or php://stdout, make it a temporary file... $pFilename = @tempnam(File::sysGetTempDir(), 'phpxltmp'); if ($pFilename == '') { $pFilename = $originalFilename; } + $this->fileHandle = fopen($pFilename, 'wb+'); + } else { + $this->fileHandle = fopen($pFilename, 'wb+'); } - $zip = $this->createZip($pFilename); + $zip = $this->createZip(); - $zip->addFromString('META-INF/manifest.xml', $this->getWriterPart('meta_inf')->writeManifest()); - $zip->addFromString('Thumbnails/thumbnail.png', $this->getWriterPart('thumbnails')->writeThumbnail()); - $zip->addFromString('content.xml', $this->getWriterPart('content')->write()); - $zip->addFromString('meta.xml', $this->getWriterPart('meta')->write()); - $zip->addFromString('mimetype', $this->getWriterPart('mimetype')->write()); - $zip->addFromString('settings.xml', $this->getWriterPart('settings')->write()); - $zip->addFromString('styles.xml', $this->getWriterPart('styles')->write()); + $zip->addFile('META-INF/manifest.xml', $this->getWriterPart('meta_inf')->writeManifest()); + $zip->addFile('Thumbnails/thumbnail.png', $this->getWriterPart('thumbnails')->writeThumbnail()); + $zip->addFile('content.xml', $this->getWriterPart('content')->write()); + $zip->addFile('meta.xml', $this->getWriterPart('meta')->write()); + $zip->addFile('mimetype', $this->getWriterPart('mimetype')->write()); + $zip->addFile('settings.xml', $this->getWriterPart('settings')->write()); + $zip->addFile('styles.xml', $this->getWriterPart('styles')->write()); // Close file - if ($zip->close() === false) { - throw new WriterException("Could not close zip file $pFilename."); + try { + $zip->finish(); + } catch (OverflowException $e) { + throw new WriterException('Could not close resource.'); } + rewind($this->fileHandle); + // If a temporary file was used, copy it to the correct file stream if ($originalFilename != $pFilename) { - if (copy($pFilename, $originalFilename) === false) { + if (stream_copy_to_stream($this->fileHandle, fopen($originalFilename, 'wb+')) === false) { throw new WriterException("Could not copy temporary zip file $pFilename to $originalFilename."); } @unlink($pFilename); @@ -122,28 +139,23 @@ class Ods extends BaseWriter /** * Create zip object. * - * @param string $pFilename - * * @throws WriterException * - * @return ZipArchive + * @return ZipStream */ - private function createZip($pFilename) + private function createZip() { - // Create new ZIP file and open it for writing - $zip = new ZipArchive(); - - if (file_exists($pFilename)) { - unlink($pFilename); - } // Try opening the ZIP file - if ($zip->open($pFilename, ZipArchive::OVERWRITE) !== true) { - if ($zip->open($pFilename, ZipArchive::CREATE) !== true) { - throw new WriterException("Could not open $pFilename for writing."); - } + if ($this->fileHandle === false) { + throw new WriterException('Could not open resource for writing.'); } - return $zip; + // Create new ZIP stream + $options = new Archive(); + $options->setEnableZip64(false); + $options->setOutputStream($this->fileHandle); + + return new ZipStream(null, $options); } /** diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index 1fa59e51..2297002b 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -24,6 +24,9 @@ use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Theme; use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Workbook; use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Worksheet; use ZipArchive; +use ZipStream\Exception\OverflowException; +use ZipStream\Option\Archive; +use ZipStream\ZipStream; class Xlsx extends BaseWriter { @@ -104,6 +107,11 @@ class Xlsx extends BaseWriter */ private $drawingHashTable; + /** + * @var bool|resource + */ + private $fileHandle; + /** * Create a new Xlsx Writer. * @@ -166,7 +174,7 @@ class Xlsx extends BaseWriter /** * Save PhpSpreadsheet to file. * - * @param string $pFilename + * @param resource|string $pFilename * * @throws WriterException */ @@ -176,13 +184,24 @@ class Xlsx extends BaseWriter // garbage collect $this->spreadSheet->garbageCollect(); - // If $pFilename is php://output or php://stdout, make it a temporary file... $originalFilename = $pFilename; - if (strtolower($pFilename) == 'php://output' || strtolower($pFilename) == 'php://stdout') { + + if (is_resource($pFilename)) { + $this->fileHandle = $pFilename; + } elseif (strtolower($pFilename) == 'php://output' || strtolower($pFilename) == 'php://stdout') { + // If $pFilename is php://output or php://stdout, make it a temporary file... $pFilename = @tempnam(File::sysGetTempDir(), 'phpxltmp'); if ($pFilename == '') { $pFilename = $originalFilename; } + $this->fileHandle = fopen($pFilename, 'wb+'); + } else { + $this->fileHandle = fopen($pFilename, 'wb+'); + } + + // Try opening the ZIP file + if ($this->fileHandle === false) { + throw new WriterException('Could not open resource for writing.'); } $saveDebugLog = Calculation::getInstance($this->spreadSheet)->getDebugLog()->getWriteDebugLog(); @@ -207,83 +226,77 @@ class Xlsx extends BaseWriter // Create drawing dictionary $this->drawingHashTable->addFromSource($this->getWriterPart('Drawing')->allDrawings($this->spreadSheet)); - $zip = new ZipArchive(); + $options = new Archive(); + $options->setEnableZip64(false); + $options->setOutputStream($this->fileHandle); - if (file_exists($pFilename)) { - unlink($pFilename); - } - // Try opening the ZIP file - if ($zip->open($pFilename, ZipArchive::OVERWRITE) !== true) { - if ($zip->open($pFilename, ZipArchive::CREATE) !== true) { - throw new WriterException('Could not open ' . $pFilename . ' for writing.'); - } - } + $zip = new ZipStream(null, $options); // Add [Content_Types].xml to ZIP file - $zip->addFromString('[Content_Types].xml', $this->getWriterPart('ContentTypes')->writeContentTypes($this->spreadSheet, $this->includeCharts)); + $zip->addFile('[Content_Types].xml', $this->getWriterPart('ContentTypes')->writeContentTypes($this->spreadSheet, $this->includeCharts)); //if hasMacros, add the vbaProject.bin file, Certificate file(if exists) if ($this->spreadSheet->hasMacros()) { $macrosCode = $this->spreadSheet->getMacrosCode(); if ($macrosCode !== null) { // we have the code ? - $zip->addFromString('xl/vbaProject.bin', $macrosCode); //allways in 'xl', allways named vbaProject.bin + $zip->addFile('xl/vbaProject.bin', $macrosCode); //allways in 'xl', allways named vbaProject.bin if ($this->spreadSheet->hasMacrosCertificate()) { //signed macros ? // Yes : add the certificate file and the related rels file - $zip->addFromString('xl/vbaProjectSignature.bin', $this->spreadSheet->getMacrosCertificate()); - $zip->addFromString('xl/_rels/vbaProject.bin.rels', $this->getWriterPart('RelsVBA')->writeVBARelationships($this->spreadSheet)); + $zip->addFile('xl/vbaProjectSignature.bin', $this->spreadSheet->getMacrosCertificate()); + $zip->addFile('xl/_rels/vbaProject.bin.rels', $this->getWriterPart('RelsVBA')->writeVBARelationships($this->spreadSheet)); } } } //a custom UI in this workbook ? add it ("base" xml and additional objects (pictures) and rels) if ($this->spreadSheet->hasRibbon()) { $tmpRibbonTarget = $this->spreadSheet->getRibbonXMLData('target'); - $zip->addFromString($tmpRibbonTarget, $this->spreadSheet->getRibbonXMLData('data')); + $zip->addFile($tmpRibbonTarget, $this->spreadSheet->getRibbonXMLData('data')); if ($this->spreadSheet->hasRibbonBinObjects()) { $tmpRootPath = dirname($tmpRibbonTarget) . '/'; $ribbonBinObjects = $this->spreadSheet->getRibbonBinObjects('data'); //the files to write foreach ($ribbonBinObjects as $aPath => $aContent) { - $zip->addFromString($tmpRootPath . $aPath, $aContent); + $zip->addFile($tmpRootPath . $aPath, $aContent); } //the rels for files - $zip->addFromString($tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels', $this->getWriterPart('RelsRibbonObjects')->writeRibbonRelationships($this->spreadSheet)); + $zip->addFile($tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels', $this->getWriterPart('RelsRibbonObjects')->writeRibbonRelationships($this->spreadSheet)); } } // Add relationships to ZIP file - $zip->addFromString('_rels/.rels', $this->getWriterPart('Rels')->writeRelationships($this->spreadSheet)); - $zip->addFromString('xl/_rels/workbook.xml.rels', $this->getWriterPart('Rels')->writeWorkbookRelationships($this->spreadSheet)); + $zip->addFile('_rels/.rels', $this->getWriterPart('Rels')->writeRelationships($this->spreadSheet)); + $zip->addFile('xl/_rels/workbook.xml.rels', $this->getWriterPart('Rels')->writeWorkbookRelationships($this->spreadSheet)); // Add document properties to ZIP file - $zip->addFromString('docProps/app.xml', $this->getWriterPart('DocProps')->writeDocPropsApp($this->spreadSheet)); - $zip->addFromString('docProps/core.xml', $this->getWriterPart('DocProps')->writeDocPropsCore($this->spreadSheet)); + $zip->addFile('docProps/app.xml', $this->getWriterPart('DocProps')->writeDocPropsApp($this->spreadSheet)); + $zip->addFile('docProps/core.xml', $this->getWriterPart('DocProps')->writeDocPropsCore($this->spreadSheet)); $customPropertiesPart = $this->getWriterPart('DocProps')->writeDocPropsCustom($this->spreadSheet); if ($customPropertiesPart !== null) { - $zip->addFromString('docProps/custom.xml', $customPropertiesPart); + $zip->addFile('docProps/custom.xml', $customPropertiesPart); } // Add theme to ZIP file - $zip->addFromString('xl/theme/theme1.xml', $this->getWriterPart('Theme')->writeTheme($this->spreadSheet)); + $zip->addFile('xl/theme/theme1.xml', $this->getWriterPart('Theme')->writeTheme($this->spreadSheet)); // Add string table to ZIP file - $zip->addFromString('xl/sharedStrings.xml', $this->getWriterPart('StringTable')->writeStringTable($this->stringTable)); + $zip->addFile('xl/sharedStrings.xml', $this->getWriterPart('StringTable')->writeStringTable($this->stringTable)); // Add styles to ZIP file - $zip->addFromString('xl/styles.xml', $this->getWriterPart('Style')->writeStyles($this->spreadSheet)); + $zip->addFile('xl/styles.xml', $this->getWriterPart('Style')->writeStyles($this->spreadSheet)); // Add workbook to ZIP file - $zip->addFromString('xl/workbook.xml', $this->getWriterPart('Workbook')->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas)); + $zip->addFile('xl/workbook.xml', $this->getWriterPart('Workbook')->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas)); $chartCount = 0; // Add worksheets for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { - $zip->addFromString('xl/worksheets/sheet' . ($i + 1) . '.xml', $this->getWriterPart('Worksheet')->writeWorksheet($this->spreadSheet->getSheet($i), $this->stringTable, $this->includeCharts)); + $zip->addFile('xl/worksheets/sheet' . ($i + 1) . '.xml', $this->getWriterPart('Worksheet')->writeWorksheet($this->spreadSheet->getSheet($i), $this->stringTable, $this->includeCharts)); if ($this->includeCharts) { $charts = $this->spreadSheet->getSheet($i)->getChartCollection(); if (count($charts) > 0) { foreach ($charts as $chart) { - $zip->addFromString('xl/charts/chart' . ($chartCount + 1) . '.xml', $this->getWriterPart('Chart')->writeChart($chart, $this->preCalculateFormulas)); + $zip->addFile('xl/charts/chart' . ($chartCount + 1) . '.xml', $this->getWriterPart('Chart')->writeChart($chart, $this->preCalculateFormulas)); ++$chartCount; } } @@ -294,19 +307,19 @@ class Xlsx extends BaseWriter // Add worksheet relationships (drawings, ...) for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { // Add relationships - $zip->addFromString('xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts)); + $zip->addFile('xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts)); // Add unparsedLoadedData $sheetCodeName = $this->spreadSheet->getSheet($i)->getCodeName(); $unparsedLoadedData = $this->spreadSheet->getUnparsedLoadedData(); if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'])) { foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'] as $ctrlProp) { - $zip->addFromString($ctrlProp['filePath'], $ctrlProp['content']); + $zip->addFile($ctrlProp['filePath'], $ctrlProp['content']); } } if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'])) { foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'] as $ctrlProp) { - $zip->addFromString($ctrlProp['filePath'], $ctrlProp['content']); + $zip->addFile($ctrlProp['filePath'], $ctrlProp['content']); } } @@ -319,13 +332,13 @@ class Xlsx extends BaseWriter // Add drawing and image relationship parts if (($drawingCount > 0) || ($chartCount > 0)) { // Drawing relationships - $zip->addFromString('xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts)); + $zip->addFile('xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts)); // Drawings - $zip->addFromString('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); + $zip->addFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); } elseif (isset($unparsedLoadedData['sheets'][$sheetCodeName]['drawingAlternateContents'])) { // Drawings - $zip->addFromString('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); + $zip->addFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); } // Add unparsed drawings @@ -334,7 +347,7 @@ class Xlsx extends BaseWriter $drawingFile = array_search($relId, $unparsedLoadedData['sheets'][$sheetCodeName]['drawingOriginalIds']); if ($drawingFile !== false) { $drawingFile = ltrim($drawingFile, '.'); - $zip->addFromString('xl' . $drawingFile, $drawingXml); + $zip->addFile('xl' . $drawingFile, $drawingXml); } } } @@ -342,30 +355,30 @@ class Xlsx extends BaseWriter // Add comment relationship parts if (count($this->spreadSheet->getSheet($i)->getComments()) > 0) { // VML Comments - $zip->addFromString('xl/drawings/vmlDrawing' . ($i + 1) . '.vml', $this->getWriterPart('Comments')->writeVMLComments($this->spreadSheet->getSheet($i))); + $zip->addFile('xl/drawings/vmlDrawing' . ($i + 1) . '.vml', $this->getWriterPart('Comments')->writeVMLComments($this->spreadSheet->getSheet($i))); // Comments - $zip->addFromString('xl/comments' . ($i + 1) . '.xml', $this->getWriterPart('Comments')->writeComments($this->spreadSheet->getSheet($i))); + $zip->addFile('xl/comments' . ($i + 1) . '.xml', $this->getWriterPart('Comments')->writeComments($this->spreadSheet->getSheet($i))); } // Add unparsed relationship parts if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'])) { foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'] as $vmlDrawing) { - $zip->addFromString($vmlDrawing['filePath'], $vmlDrawing['content']); + $zip->addFile($vmlDrawing['filePath'], $vmlDrawing['content']); } } // Add header/footer relationship parts if (count($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages()) > 0) { // VML Drawings - $zip->addFromString('xl/drawings/vmlDrawingHF' . ($i + 1) . '.vml', $this->getWriterPart('Drawing')->writeVMLHeaderFooterImages($this->spreadSheet->getSheet($i))); + $zip->addFile('xl/drawings/vmlDrawingHF' . ($i + 1) . '.vml', $this->getWriterPart('Drawing')->writeVMLHeaderFooterImages($this->spreadSheet->getSheet($i))); // VML Drawing relationships - $zip->addFromString('xl/drawings/_rels/vmlDrawingHF' . ($i + 1) . '.vml.rels', $this->getWriterPart('Rels')->writeHeaderFooterDrawingRelationships($this->spreadSheet->getSheet($i))); + $zip->addFile('xl/drawings/_rels/vmlDrawingHF' . ($i + 1) . '.vml.rels', $this->getWriterPart('Rels')->writeHeaderFooterDrawingRelationships($this->spreadSheet->getSheet($i))); // Media foreach ($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages() as $image) { - $zip->addFromString('xl/media/' . $image->getIndexedFilename(), file_get_contents($image->getPath())); + $zip->addFile('xl/media/' . $image->getIndexedFilename(), file_get_contents($image->getPath())); } } } @@ -388,7 +401,7 @@ class Xlsx extends BaseWriter $imageContents = file_get_contents($imagePath); } - $zip->addFromString('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents); + $zip->addFile('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents); } elseif ($this->getDrawingHashTable()->getByIndex($i) instanceof MemoryDrawing) { ob_start(); call_user_func( @@ -398,7 +411,7 @@ class Xlsx extends BaseWriter $imageContents = ob_get_contents(); ob_end_clean(); - $zip->addFromString('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents); + $zip->addFile('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents); } } @@ -406,13 +419,17 @@ class Xlsx extends BaseWriter Calculation::getInstance($this->spreadSheet)->getDebugLog()->setWriteDebugLog($saveDebugLog); // Close file - if ($zip->close() === false) { - throw new WriterException("Could not close zip file $pFilename."); + try { + $zip->finish(); + } catch (OverflowException $e) { + throw new WriterException('Could not close resource.'); } + rewind($this->fileHandle); + // If a temporary file was used, copy it to the correct file stream if ($originalFilename != $pFilename) { - if (copy($pFilename, $originalFilename) === false) { + if (stream_copy_to_stream($this->fileHandle, fopen($originalFilename, 'wb+')) === false) { throw new WriterException("Could not copy temporary zip file $pFilename to $originalFilename."); } @unlink($pFilename); From 11499aad9ae44409b81245e9f489a66f1fa6599e Mon Sep 17 00:00:00 2001 From: basbl Date: Mon, 16 Dec 2019 09:38:41 +0100 Subject: [PATCH 3/7] Add resource parameter handling to html, csv and xls writers --- src/PhpSpreadsheet/Writer/Csv.php | 14 +++++++++++--- src/PhpSpreadsheet/Writer/Html.php | 9 +++++++-- src/PhpSpreadsheet/Writer/Xls.php | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Csv.php b/src/PhpSpreadsheet/Writer/Csv.php index 1166bd25..1cf64634 100644 --- a/src/PhpSpreadsheet/Writer/Csv.php +++ b/src/PhpSpreadsheet/Writer/Csv.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; class Csv extends BaseWriter { @@ -77,7 +78,7 @@ class Csv extends BaseWriter /** * Save PhpSpreadsheet to file. * - * @param string $pFilename + * @param resource|string $pFilename * * @throws Exception */ @@ -92,9 +93,14 @@ class Csv extends BaseWriter Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE); // Open file - $fileHandle = fopen($pFilename, 'wb+'); + if (is_resource($pFilename)) { + $fileHandle = $pFilename; + } else { + $fileHandle = fopen($pFilename, 'wb+'); + } + if ($fileHandle === false) { - throw new Exception("Could not open file $pFilename for writing."); + throw new WriterException("Could not open file $pFilename for writing."); } if ($this->excelCompatibility) { @@ -126,6 +132,8 @@ class Csv extends BaseWriter } // Close file + rewind($fileHandle); + fclose($fileHandle); Calculation::setArrayReturnType($saveArrayReturnType); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 37d68e91..91beedb7 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -146,7 +146,7 @@ class Html extends BaseWriter /** * Save Spreadsheet to file. * - * @param string $pFilename + * @param resource|string $pFilename * * @throws WriterException */ @@ -164,7 +164,12 @@ class Html extends BaseWriter $this->buildCSS(!$this->useInlineCss); // Open file - $fileHandle = fopen($pFilename, 'wb+'); + if (is_resource($pFilename)) { + $fileHandle = $pFilename; + } else { + $fileHandle = fopen($pFilename, 'wb+'); + } + if ($fileHandle === false) { throw new WriterException("Could not open file $pFilename for writing."); } diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index 6932eb1a..95693dde 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -115,7 +115,7 @@ class Xls extends BaseWriter /** * Save Spreadsheet to file. * - * @param string $pFilename + * @param resource|string $pFilename * * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception */ From b60b1d25c04180a3c8ce71dc84cf1c841f5f5b5c Mon Sep 17 00:00:00 2001 From: basbl Date: Mon, 27 Apr 2020 11:47:22 +0200 Subject: [PATCH 4/7] Test scrutinizer --- src/PhpSpreadsheet/Writer/Ods.php | 32 ++++++++++++++++++----------- src/PhpSpreadsheet/Writer/Xlsx.php | 33 ++++++++++++++++++------------ 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Ods.php b/src/PhpSpreadsheet/Writer/Ods.php index 2a1dba02..08bd2629 100644 --- a/src/PhpSpreadsheet/Writer/Ods.php +++ b/src/PhpSpreadsheet/Writer/Ods.php @@ -93,18 +93,18 @@ class Ods extends BaseWriter // garbage collect $this->spreadSheet->garbageCollect(); - // If $pFilename is php://output or php://stdout, make it a temporary file... $originalFilename = $pFilename; if (is_resource($pFilename)) { $this->fileHandle = $pFilename; - } elseif (strtolower($pFilename) == 'php://output' || strtolower($pFilename) == 'php://stdout') { - // If $pFilename is php://output or php://stdout, make it a temporary file... - $pFilename = @tempnam(File::sysGetTempDir(), 'phpxltmp'); - if ($pFilename == '') { - $pFilename = $originalFilename; - } - $this->fileHandle = fopen($pFilename, 'wb+'); } else { + // If $pFilename is php://output or php://stdout, make it a temporary file... + if (in_array(strtolower($pFilename), ['php://output', 'php://stdout'], true)) { + $pFilename = @tempnam(File::sysGetTempDir(), 'phpxltmp'); + if ($pFilename === '') { + $pFilename = $originalFilename; + } + } + $this->fileHandle = fopen($pFilename, 'wb+'); } @@ -128,11 +128,19 @@ class Ods extends BaseWriter rewind($this->fileHandle); // If a temporary file was used, copy it to the correct file stream - if ($originalFilename != $pFilename) { - if (stream_copy_to_stream($this->fileHandle, fopen($originalFilename, 'wb+')) === false) { + if ($originalFilename !== $pFilename) { + $destinationFileHandle = fopen($originalFilename, 'wb+'); + if (!is_resource($destinationFileHandle)) { + throw new WriterException("Could not open resource $originalFilename for writing."); + } + + if (stream_copy_to_stream($this->fileHandle, $destinationFileHandle) === false) { throw new WriterException("Could not copy temporary zip file $pFilename to $originalFilename."); } - @unlink($pFilename); + + if (is_string($pFilename) && !unlink($pFilename)) { + throw new WriterException('Could not unlink temporary zip file.'); + } } } @@ -146,7 +154,7 @@ class Ods extends BaseWriter private function createZip() { // Try opening the ZIP file - if ($this->fileHandle === false) { + if (!is_resource($this->fileHandle)) { throw new WriterException('Could not open resource for writing.'); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index 2297002b..9c947d0a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -185,22 +185,21 @@ class Xlsx extends BaseWriter $this->spreadSheet->garbageCollect(); $originalFilename = $pFilename; - if (is_resource($pFilename)) { $this->fileHandle = $pFilename; - } elseif (strtolower($pFilename) == 'php://output' || strtolower($pFilename) == 'php://stdout') { - // If $pFilename is php://output or php://stdout, make it a temporary file... - $pFilename = @tempnam(File::sysGetTempDir(), 'phpxltmp'); - if ($pFilename == '') { - $pFilename = $originalFilename; - } - $this->fileHandle = fopen($pFilename, 'wb+'); } else { + // If $pFilename is php://output or php://stdout, make it a temporary file... + if (in_array(strtolower($pFilename), ['php://output', 'php://stdout'], true)) { + $pFilename = @tempnam(File::sysGetTempDir(), 'phpxltmp'); + if ($pFilename === '') { + $pFilename = $originalFilename; + } + } + $this->fileHandle = fopen($pFilename, 'wb+'); } - // Try opening the ZIP file - if ($this->fileHandle === false) { + if (!is_resource($this->fileHandle)) { throw new WriterException('Could not open resource for writing.'); } @@ -428,11 +427,19 @@ class Xlsx extends BaseWriter rewind($this->fileHandle); // If a temporary file was used, copy it to the correct file stream - if ($originalFilename != $pFilename) { - if (stream_copy_to_stream($this->fileHandle, fopen($originalFilename, 'wb+')) === false) { + if ($originalFilename !== $pFilename) { + $destinationFileHandle = fopen($originalFilename, 'wb+'); + if (!is_resource($destinationFileHandle)) { + throw new WriterException("Could not open resource $originalFilename for writing."); + } + + if (stream_copy_to_stream($this->fileHandle, $destinationFileHandle) === false) { throw new WriterException("Could not copy temporary zip file $pFilename to $originalFilename."); } - @unlink($pFilename); + + if (is_string($pFilename) && !unlink($pFilename)) { + throw new WriterException('Could not unlink temporary zip file.'); + } } } else { throw new WriterException('PhpSpreadsheet object unassigned.'); From 9cdbddf3bfa82fefc59052cee67bfbe08e94d49a Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sat, 16 May 2020 18:08:10 +0900 Subject: [PATCH 5/7] Early bail out if resource cannot be opened --- src/PhpSpreadsheet/Writer/Ods.php | 9 +++++++-- src/PhpSpreadsheet/Writer/Xlsx.php | 11 ++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Ods.php b/src/PhpSpreadsheet/Writer/Ods.php index 08bd2629..9de19eb4 100644 --- a/src/PhpSpreadsheet/Writer/Ods.php +++ b/src/PhpSpreadsheet/Writer/Ods.php @@ -33,7 +33,7 @@ class Ods extends BaseWriter private $spreadSheet; /** - * @var bool|resource + * @var resource */ private $fileHandle; @@ -105,7 +105,12 @@ class Ods extends BaseWriter } } - $this->fileHandle = fopen($pFilename, 'wb+'); + $fileHandle = fopen($pFilename, 'wb+'); + if ($fileHandle === false) { + throw new WriterException('Could not open file ' . $pFilename . ' for writing.'); + } + + $this->fileHandle = $fileHandle; } $zip = $this->createZip(); diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index 9c947d0a..3b4bdfd9 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -108,7 +108,7 @@ class Xlsx extends BaseWriter private $drawingHashTable; /** - * @var bool|resource + * @var resource */ private $fileHandle; @@ -196,11 +196,12 @@ class Xlsx extends BaseWriter } } - $this->fileHandle = fopen($pFilename, 'wb+'); - } + $fileHandle = fopen($pFilename, 'wb+'); + if ($fileHandle === false) { + throw new WriterException('Could not open file ' . $pFilename . ' for writing.'); + } - if (!is_resource($this->fileHandle)) { - throw new WriterException('Could not open resource for writing.'); + $this->fileHandle = $fileHandle; } $saveDebugLog = Calculation::getInstance($this->spreadSheet)->getDebugLog()->getWriteDebugLog(); From fcf78204670fe5c7ab298d9c2cdd9b1c15000c31 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sat, 16 May 2020 20:12:28 +0900 Subject: [PATCH 6/7] All writers can write to stream --- src/PhpSpreadsheet/Shared/OLE/PPS/Root.php | 44 ++---------------- src/PhpSpreadsheet/Writer/BaseWriter.php | 45 +++++++++++++++++++ src/PhpSpreadsheet/Writer/Csv.php | 5 --- src/PhpSpreadsheet/Writer/Html.php | 21 +++------ src/PhpSpreadsheet/Writer/Ods.php | 43 +----------------- src/PhpSpreadsheet/Writer/Pdf.php | 10 ++--- src/PhpSpreadsheet/Writer/Pdf/Mpdf.php | 2 +- src/PhpSpreadsheet/Writer/Xls.php | 4 +- src/PhpSpreadsheet/Writer/Xlsx.php | 43 +----------------- .../Functional/StreamTest.php | 45 +++++++++++++++++++ 10 files changed, 111 insertions(+), 151 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Functional/StreamTest.php diff --git a/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php b/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php index c52cea23..472483eb 100644 --- a/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php +++ b/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php @@ -22,7 +22,6 @@ namespace PhpOffice\PhpSpreadsheet\Shared\OLE\PPS; // use PhpOffice\PhpSpreadsheet\Shared\OLE; use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS; -use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; /** * Class for creating Root PPS's for OLE containers. @@ -33,23 +32,11 @@ use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; */ class Root extends PPS { - /** - * Directory for temporary files. - * - * @var string - */ - protected $tempDirectory; - /** * @var resource */ private $fileHandle; - /** - * @var string - */ - private $tempFilename; - /** * @var int */ @@ -67,8 +54,6 @@ class Root extends PPS */ public function __construct($time_1st, $time_2nd, $raChild) { - $this->tempDirectory = \PhpOffice\PhpSpreadsheet\Shared\File::sysGetTempDir(); - parent::__construct(null, OLE::ascToUcs('Root Entry'), OLE::OLE_PPS_TYPE_ROOT, null, null, null, $time_1st, $time_2nd, null, $raChild); } @@ -79,14 +64,14 @@ class Root extends PPS * If a resource pointer to a stream created by fopen() is passed * it will be used, but you have to close such stream by yourself. * - * @param resource|string $filename the name of the file or stream where to save the OLE container - * - * @throws WriterException + * @param resource $fileHandle the name of the file or stream where to save the OLE container * * @return bool true on success */ - public function save($filename) + public function save($fileHandle) { + $this->fileHandle = $fileHandle; + // Initial Setting for saving $this->bigBlockSize = pow( 2, @@ -97,23 +82,6 @@ class Root extends PPS (isset($this->smallBlockSize)) ? self::adjust2($this->smallBlockSize) : 6 ); - if (is_resource($filename)) { - $this->fileHandle = $filename; - } elseif ($filename == '-' || $filename == '') { - if ($this->tempDirectory === null) { - $this->tempDirectory = \PhpOffice\PhpSpreadsheet\Shared\File::sysGetTempDir(); - } - $this->tempFilename = tempnam($this->tempDirectory, 'OLE_PPS_Root'); - $this->fileHandle = fopen($this->tempFilename, 'w+b'); - if ($this->fileHandle == false) { - throw new WriterException("Can't create temporary file."); - } - } else { - $this->fileHandle = fopen($filename, 'wb'); - } - if ($this->fileHandle == false) { - throw new WriterException("Can't open $filename. It may be in use or protected."); - } // Make an array of PPS's (for Save) $aList = []; PPS::_savePpsSetPnt($aList, [$this]); @@ -132,10 +100,6 @@ class Root extends PPS // Write Big Block Depot and BDList and Adding Header informations $this->_saveBbd($iSBDcnt, $iBBcnt, $iPPScnt); - if (!is_resource($filename)) { - fclose($this->fileHandle); - } - return true; } diff --git a/src/PhpSpreadsheet/Writer/BaseWriter.php b/src/PhpSpreadsheet/Writer/BaseWriter.php index f13150d7..dc7a0431 100644 --- a/src/PhpSpreadsheet/Writer/BaseWriter.php +++ b/src/PhpSpreadsheet/Writer/BaseWriter.php @@ -35,6 +35,16 @@ abstract class BaseWriter implements IWriter */ private $diskCachingDirectory = './'; + /** + * @var resource + */ + protected $fileHandle; + + /** + * @var bool + */ + private $shouldCloseFile; + public function getIncludeCharts() { return $this->includeCharts; @@ -83,4 +93,39 @@ abstract class BaseWriter implements IWriter { return $this->diskCachingDirectory; } + + /** + * Open file handle. + * + * @param resource|string $filename + */ + public function openFileHandle($filename): void + { + if (is_resource($filename)) { + $this->fileHandle = $filename; + $this->shouldCloseFile = false; + + return; + } + + $fileHandle = fopen($filename, 'wb+'); + if ($fileHandle === false) { + throw new Exception('Could not open file ' . $filename . ' for writing.'); + } + + $this->fileHandle = $fileHandle; + $this->shouldCloseFile = true; + } + + /** + * Close file handle only we opened it ourselves. + */ + protected function maybeCloseFileHandle(): void + { + if ($this->shouldCloseFile) { + if (!fclose($this->fileHandle)) { + throw new Exception('Could not close file after writing.'); + } + } + } } diff --git a/src/PhpSpreadsheet/Writer/Csv.php b/src/PhpSpreadsheet/Writer/Csv.php index 1cf64634..e5c08c13 100644 --- a/src/PhpSpreadsheet/Writer/Csv.php +++ b/src/PhpSpreadsheet/Writer/Csv.php @@ -131,11 +131,6 @@ class Csv extends BaseWriter $this->writeLine($fileHandle, $cellsArray[0]); } - // Close file - rewind($fileHandle); - - fclose($fileHandle); - Calculation::setArrayReturnType($saveArrayReturnType); Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog); } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 91beedb7..ed6db1b7 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -164,32 +164,23 @@ class Html extends BaseWriter $this->buildCSS(!$this->useInlineCss); // Open file - if (is_resource($pFilename)) { - $fileHandle = $pFilename; - } else { - $fileHandle = fopen($pFilename, 'wb+'); - } - - if ($fileHandle === false) { - throw new WriterException("Could not open file $pFilename for writing."); - } + $this->openFileHandle($pFilename); // Write headers - fwrite($fileHandle, $this->generateHTMLHeader(!$this->useInlineCss)); + fwrite($this->fileHandle, $this->generateHTMLHeader(!$this->useInlineCss)); // Write navigation (tabs) if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) { - fwrite($fileHandle, $this->generateNavigation()); + fwrite($this->fileHandle, $this->generateNavigation()); } // Write data - fwrite($fileHandle, $this->generateSheetData()); + fwrite($this->fileHandle, $this->generateSheetData()); // Write footer - fwrite($fileHandle, $this->generateHTMLFooter()); + fwrite($this->fileHandle, $this->generateHTMLFooter()); - // Close file - fclose($fileHandle); + $this->maybeCloseFileHandle(); Calculation::setArrayReturnType($saveArrayReturnType); Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog); diff --git a/src/PhpSpreadsheet/Writer/Ods.php b/src/PhpSpreadsheet/Writer/Ods.php index 9de19eb4..bb981d35 100644 --- a/src/PhpSpreadsheet/Writer/Ods.php +++ b/src/PhpSpreadsheet/Writer/Ods.php @@ -32,11 +32,6 @@ class Ods extends BaseWriter */ private $spreadSheet; - /** - * @var resource - */ - private $fileHandle; - /** * Create a new Ods. * @@ -93,25 +88,7 @@ class Ods extends BaseWriter // garbage collect $this->spreadSheet->garbageCollect(); - $originalFilename = $pFilename; - if (is_resource($pFilename)) { - $this->fileHandle = $pFilename; - } else { - // If $pFilename is php://output or php://stdout, make it a temporary file... - if (in_array(strtolower($pFilename), ['php://output', 'php://stdout'], true)) { - $pFilename = @tempnam(File::sysGetTempDir(), 'phpxltmp'); - if ($pFilename === '') { - $pFilename = $originalFilename; - } - } - - $fileHandle = fopen($pFilename, 'wb+'); - if ($fileHandle === false) { - throw new WriterException('Could not open file ' . $pFilename . ' for writing.'); - } - - $this->fileHandle = $fileHandle; - } + $this->openFileHandle($pFilename); $zip = $this->createZip(); @@ -130,23 +107,7 @@ class Ods extends BaseWriter throw new WriterException('Could not close resource.'); } - rewind($this->fileHandle); - - // If a temporary file was used, copy it to the correct file stream - if ($originalFilename !== $pFilename) { - $destinationFileHandle = fopen($originalFilename, 'wb+'); - if (!is_resource($destinationFileHandle)) { - throw new WriterException("Could not open resource $originalFilename for writing."); - } - - if (stream_copy_to_stream($this->fileHandle, $destinationFileHandle) === false) { - throw new WriterException("Could not copy temporary zip file $pFilename to $originalFilename."); - } - - if (is_string($pFilename) && !unlink($pFilename)) { - throw new WriterException('Could not unlink temporary zip file.'); - } - } + $this->maybeCloseFileHandle(); } /** diff --git a/src/PhpSpreadsheet/Writer/Pdf.php b/src/PhpSpreadsheet/Writer/Pdf.php index d9184560..52f95749 100644 --- a/src/PhpSpreadsheet/Writer/Pdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf.php @@ -255,17 +255,14 @@ abstract class Pdf extends Html Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE); // Open file - $fileHandle = fopen($pFilename, 'w'); - if ($fileHandle === false) { - throw new WriterException("Could not open file $pFilename for writing."); - } + $this->openFileHandle($pFilename); // Set PDF $this->isPdf = true; // Build CSS $this->buildCSS(true); - return $fileHandle; + return $this->fileHandle; } /** @@ -275,8 +272,7 @@ abstract class Pdf extends Html */ protected function restoreStateAfterSave($fileHandle) { - // Close file - fclose($fileHandle); + $this->maybeCloseFileHandle(); Calculation::setArrayReturnType($this->saveArrayReturnType); } diff --git a/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php index fd2664a8..5fdc4ea8 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php @@ -65,7 +65,7 @@ class Mpdf extends Pdf } // Create PDF - $config = ['tempDir' => $this->tempDir]; + $config = ['tempDir' => $this->tempDir . '/mpdf']; $pdf = $this->createExternalWriterInstance($config); $ortmp = $orientation; $pdf->_setPageSize(strtoupper($paperSize), $ortmp); diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index 95693dde..fb42bf9e 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -221,7 +221,9 @@ class Xls extends BaseWriter $root = new Root(time(), time(), $arrRootData); // save the OLE file - $root->save($pFilename); + $this->openFileHandle($pFilename); + $root->save($this->fileHandle); + $this->maybeCloseFileHandle(); Functions::setReturnDateType($saveDateReturnType); Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog); diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index 3b4bdfd9..aea6a26c 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -107,11 +107,6 @@ class Xlsx extends BaseWriter */ private $drawingHashTable; - /** - * @var resource - */ - private $fileHandle; - /** * Create a new Xlsx Writer. * @@ -184,25 +179,7 @@ class Xlsx extends BaseWriter // garbage collect $this->spreadSheet->garbageCollect(); - $originalFilename = $pFilename; - if (is_resource($pFilename)) { - $this->fileHandle = $pFilename; - } else { - // If $pFilename is php://output or php://stdout, make it a temporary file... - if (in_array(strtolower($pFilename), ['php://output', 'php://stdout'], true)) { - $pFilename = @tempnam(File::sysGetTempDir(), 'phpxltmp'); - if ($pFilename === '') { - $pFilename = $originalFilename; - } - } - - $fileHandle = fopen($pFilename, 'wb+'); - if ($fileHandle === false) { - throw new WriterException('Could not open file ' . $pFilename . ' for writing.'); - } - - $this->fileHandle = $fileHandle; - } + $this->openFileHandle($pFilename); $saveDebugLog = Calculation::getInstance($this->spreadSheet)->getDebugLog()->getWriteDebugLog(); Calculation::getInstance($this->spreadSheet)->getDebugLog()->setWriteDebugLog(false); @@ -425,23 +402,7 @@ class Xlsx extends BaseWriter throw new WriterException('Could not close resource.'); } - rewind($this->fileHandle); - - // If a temporary file was used, copy it to the correct file stream - if ($originalFilename !== $pFilename) { - $destinationFileHandle = fopen($originalFilename, 'wb+'); - if (!is_resource($destinationFileHandle)) { - throw new WriterException("Could not open resource $originalFilename for writing."); - } - - if (stream_copy_to_stream($this->fileHandle, $destinationFileHandle) === false) { - throw new WriterException("Could not copy temporary zip file $pFilename to $originalFilename."); - } - - if (is_string($pFilename) && !unlink($pFilename)) { - throw new WriterException('Could not unlink temporary zip file.'); - } - } + $this->maybeCloseFileHandle(); } else { throw new WriterException('PhpSpreadsheet object unassigned.'); } diff --git a/tests/PhpSpreadsheetTests/Functional/StreamTest.php b/tests/PhpSpreadsheetTests/Functional/StreamTest.php new file mode 100644 index 00000000..20e588bb --- /dev/null +++ b/tests/PhpSpreadsheetTests/Functional/StreamTest.php @@ -0,0 +1,45 @@ +getActiveSheet()->setCellValue('A1', 'foo'); + $writer = IOFactory::createWriter($spreadsheet, $format); + + $stream = fopen('php://memory', 'wb+'); + self::assertSame(0, fstat($stream)['size']); + + $writer->save($stream); + + self::assertIsResource($stream, 'should not close the stream for further usage out of PhpSpreadsheet'); + self::assertGreaterThan(0, fstat($stream)['size'], 'something should have been written to the stream'); + self::assertGreaterThan(0, ftell($stream), 'should not be rewinded, because not all streams support it'); + } +} From e34535329da3696b2ac5a432652e6cc6e504de8b Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sat, 16 May 2020 20:25:01 +0900 Subject: [PATCH 7/7] Remove obsolete arguments --- src/PhpSpreadsheet/Writer/Pdf.php | 4 +--- src/PhpSpreadsheet/Writer/Pdf/Dompdf.php | 2 +- src/PhpSpreadsheet/Writer/Pdf/Mpdf.php | 2 +- src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Pdf.php b/src/PhpSpreadsheet/Writer/Pdf.php index 52f95749..f0c1798f 100644 --- a/src/PhpSpreadsheet/Writer/Pdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf.php @@ -267,10 +267,8 @@ abstract class Pdf extends Html /** * Save PhpSpreadsheet to PDF file, post-save. - * - * @param resource $fileHandle */ - protected function restoreStateAfterSave($fileHandle) + protected function restoreStateAfterSave(): void { $this->maybeCloseFileHandle(); diff --git a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php index 3c3044d7..783bf253 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php @@ -73,6 +73,6 @@ class Dompdf extends Pdf // Write to file fwrite($fileHandle, $pdf->output()); - parent::restoreStateAfterSave($fileHandle); + parent::restoreStateAfterSave(); } } diff --git a/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php index 5fdc4ea8..2922db15 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php @@ -95,7 +95,7 @@ class Mpdf extends Pdf // Write to file fwrite($fileHandle, $pdf->Output('', 'S')); - parent::restoreStateAfterSave($fileHandle); + parent::restoreStateAfterSave(); } /** diff --git a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php index 8a97b8fe..6ba77c75 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php @@ -93,6 +93,6 @@ class Tcpdf extends Pdf // Write to file fwrite($fileHandle, $pdf->output($pFilename, 'S')); - parent::restoreStateAfterSave($fileHandle); + parent::restoreStateAfterSave(); } }