diff options
Diffstat (limited to 'vendor/mikespub/php-epub-meta/src/Tools/ZipEdit.php')
-rw-r--r-- | vendor/mikespub/php-epub-meta/src/Tools/ZipEdit.php | 450 |
1 files changed, 450 insertions, 0 deletions
diff --git a/vendor/mikespub/php-epub-meta/src/Tools/ZipEdit.php b/vendor/mikespub/php-epub-meta/src/Tools/ZipEdit.php new file mode 100644 index 000000000..ff9f607cf --- /dev/null +++ b/vendor/mikespub/php-epub-meta/src/Tools/ZipEdit.php @@ -0,0 +1,450 @@ +<?php +/** + * ZipEdit class + * + * @author mikespub + */ + +namespace SebLucas\EPubMeta\Tools; + +use ZipStream\ZipStream; +use DateTime; +use Exception; +use ZipArchive; + +/** + * ZipEdit class allows to edit zip files on the fly and stream them afterwards + */ +class ZipEdit +{ + public const DOWNLOAD = 1; // download (default) + public const NOHEADER = 4; // option to use with DOWNLOAD: no header is sent + public const FILE = 8; // output to file , or add from file + public const STRING = 32; // output to string, or add from string + public const MIME_TYPE = 'application/epub+zip'; + + /** @var ZipArchive|null */ + private $mZip; + /** @var array<string, mixed>|null */ + private $mEntries; + /** @var array<string, mixed> */ + private $mChanges = []; + /** @var string|null */ + private $mFileName; + private bool $mSaveMe = false; + + public function __construct() + { + $this->mZip = null; + $this->mEntries = null; + $this->mFileName = null; + } + + /** + * Destructor + */ + public function __destruct() + { + $this->Close(); + } + + /** + * Open a zip file and read it's entries + * + * @param string $inFileName + * @param int|null $inFlags + * @return boolean True if zip file has been correctly opended, else false + */ + public function Open($inFileName, $inFlags = 0) // ZipArchive::RDONLY) + { + $this->Close(); + + $this->mZip = new ZipArchive(); + $result = $this->mZip->open($inFileName, ZipArchive::RDONLY); + if ($result !== true) { + return false; + } + + $this->mFileName = $inFileName; + + $this->mEntries = []; + $this->mChanges = []; + + for ($i = 0; $i < $this->mZip->numFiles; $i++) { + $entry = $this->mZip->statIndex($i); + $fileName = $entry['name']; + $this->mEntries[$fileName] = $entry; + $this->mChanges[$fileName] = ['status' => 'unchanged']; + } + + return true; + } + + /** + * Check if a file exist in the zip entries + * + * @param string $inFileName File to search + * + * @return boolean True if the file exist, else false + */ + public function FileExists($inFileName) + { + if (!isset($this->mZip)) { + return false; + } + + if (!isset($this->mEntries[$inFileName])) { + return false; + } + + return true; + } + + /** + * Read the content of a file in the zip entries + * + * @param string $inFileName File to search + * + * @return mixed File content the file exist, else false + */ + public function FileRead($inFileName) + { + if (!isset($this->mZip)) { + return false; + } + + if (!isset($this->mEntries[$inFileName])) { + return false; + } + + $data = false; + + $changes = $this->mChanges[$inFileName] ?? ['status' => 'unchanged']; + switch ($changes['status']) { + case 'unchanged': + $data = $this->mZip->getFromName($inFileName); + break; + case 'added': + case 'modified': + if (isset($changes['data'])) { + $data = $changes['data']; + } elseif (isset($changes['path'])) { + $data = file_get_contents($changes['path']); + } + break; + case 'deleted': + default: + break; + } + return $data; + } + + /** + * Get a file handler to a file in the zip entries (read-only) + * + * @param string $inFileName File to search + * + * @return resource|bool File handler if the file exist, else false + */ + public function FileStream($inFileName) + { + if (!isset($this->mZip)) { + return false; + } + + if (!isset($this->mEntries[$inFileName])) { + return false; + } + + // @todo streaming of added/modified data? + return $this->mZip->getStream($inFileName); + } + + /** + * Summary of FileAdd + * @param string $inFileName + * @param mixed $inData + * @return bool + */ + public function FileAdd($inFileName, $inData) + { + if (!isset($this->mZip)) { + return false; + } + + $this->mEntries[$inFileName] = [ + 'name' => $inFileName, // 'foobar/baz', + 'size' => strlen($inData), + 'mtime' => time(), // 1123164748, + ]; + $this->mChanges[$inFileName] = ['status' => 'added', 'data' => $inData]; + return true; + } + + /** + * Summary of FileAddPath + * @param string $inFileName + * @param string $inFilePath + * @return bool + */ + public function FileAddPath($inFileName, $inFilePath) + { + if (!isset($this->mZip)) { + return false; + } + + $this->mEntries[$inFileName] = [ + 'name' => $inFileName, // 'foobar/baz', + 'size' => filesize($inFilePath), + 'mtime' => filemtime($inFilePath), // 1123164748, + ]; + $this->mChanges[$inFileName] = ['status' => 'added', 'path' => $inFilePath]; + return true; + } + + /** + * Summary of FileDelete + * @param string $inFileName + * @return bool + */ + public function FileDelete($inFileName) + { + if (!$this->FileExists($inFileName)) { + return false; + } + + $this->mEntries[$inFileName]['size'] = 0; + $this->mEntries[$inFileName]['mtime'] = time(); + $this->mChanges[$inFileName] = ['status' => 'deleted']; + return true; + } + + /** + * Replace the content of a file in the zip entries + * + * @param string $inFileName File with content to replace + * @param string|bool $inData Data content to replace, or false to delete + * @return bool + */ + public function FileReplace($inFileName, $inData) + { + if (!isset($this->mZip)) { + return false; + } + + if ($inData === false) { + return $this->FileDelete($inFileName); + } + + $this->mEntries[$inFileName] ??= []; + $this->mEntries[$inFileName]['name'] = $inFileName; + $this->mEntries[$inFileName]['size'] = strlen($inData); + $this->mEntries[$inFileName]['mtime'] = time(); + $this->mChanges[$inFileName] = ['status' => 'modified', 'data' => $inData]; + return true; + } + + /** + * Return the state of the file. + * @param mixed $inFileName + * @return string|bool 'u'=unchanged, 'm'=modified, 'd'=deleted, 'a'=added, false=unknown + */ + public function FileGetState($inFileName) + { + $changes = $this->mChanges[$inFileName] ?? ['status' => false]; + return $changes['status']; + } + + /** + * Summary of FileCancelModif + * @param string $inFileName + * @param bool $ReplacedAndDeleted + * @return int + */ + public function FileCancelModif($inFileName, $ReplacedAndDeleted = true) + { + // cancel added, modified or deleted modifications on a file in the archive + // return the number of cancels + + $nbr = 0; + + $this->mChanges[$inFileName] = ['status' => 'unchanged']; + $nbr += 1; + return $nbr; + } + + /** + * Close the zip file + * + * @return void + * @throws Exception + */ + public function Close() + { + if (!isset($this->mZip)) { + return; + } + + $outFileName = $this->mFileName . '.copy'; + if ($this->mSaveMe) { + $outFileStream = fopen($outFileName, 'wb+'); + if ($outFileStream === false) { + throw new Exception('Unable to open zip copy ' . $outFileName); + } + $this->Flush(self::DOWNLOAD, $this->mFileName, self::MIME_TYPE, false, $outFileStream); + $result = fclose($outFileStream); + if ($result === false) { + throw new Exception('Unable to close zip copy ' . $outFileName); + } + } + + if (!$this->mZip->close()) { + $status = $this->mZip->getStatusString(); + $this->mZip = null; + throw new Exception($status); + } + if ($this->mSaveMe) { + $result = rename($outFileName, $this->mFileName); + if ($result === false) { + throw new Exception('Unable to rename zip copy ' . $outFileName); + } + $this->mSaveMe = false; + } + $this->mZip = null; + } + + /** + * Summary of SaveBeforeClose + * @return void + */ + public function SaveBeforeClose() + { + // Coming from EPub()->download() without fileName, called in EPub()->save() + // This comes right before EPub()->zip->close(), at which point we're lost + $this->mSaveMe = true; + } + + /** + * Summary of Flush + * @param mixed $render + * @param mixed $outFileName + * @param mixed $contentType + * @param bool $sendHeaders + * @param resource|null $outFileStream + * @return void + */ + public function Flush($render = self::DOWNLOAD, $outFileName = '', $contentType = '', $sendHeaders = true, $outFileStream = null) + { + // we don't want to close the zip file to save all changes here - probably what you needed :-) + //$this->Close(); + + $outFileName = $outFileName ?: $this->mFileName; + $contentType = $contentType ?: self::MIME_TYPE; + if ($outFileStream) { + $sendHeaders = false; + } + if (!$sendHeaders) { + $render = $render | self::NOHEADER; + } + if (($render & self::NOHEADER) !== self::NOHEADER) { + $sendHeaders = true; + } else { + $sendHeaders = false; + } + + $outZipStream = new ZipStream( + outputName: basename($outFileName), + outputStream: $outFileStream, + sendHttpHeaders: $sendHeaders, + contentType: $contentType, + ); + foreach ($this->mEntries as $fileName => $entry) { + $changes = $this->mChanges[$fileName]; + switch ($changes['status']) { + case 'unchanged': + // Automatic binding of $this + $callback = function () use ($fileName) { + // this expects a stream as result, not the actual data + return $this->mZip->getStream($fileName); + }; + $date = new DateTime(); + $date->setTimestamp($entry['mtime']); + $outZipStream->addFileFromCallback( + fileName: $fileName, + exactSize: $entry['size'], + lastModificationDateTime: $date, + callback: $callback, + ); + break; + case 'added': + case 'modified': + if (isset($changes['data'])) { + $outZipStream->addFile( + fileName: $fileName, + data: $changes['data'], + ); + } elseif (isset($changes['path'])) { + $outZipStream->addFileFromPath( + fileName: $fileName, + path: $changes['path'], + ); + } + break; + case 'deleted': + default: + break; + } + } + + $outZipStream->finish(); + } + + /** + * Summary of copyTest + * @param string $inFileName + * @param string $outFileName + * @return void + */ + public static function copyTest($inFileName, $outFileName) + { + $inZipFile = new ZipArchive(); + $result = $inZipFile->open($inFileName, ZipArchive::RDONLY); + if ($result !== true) { + throw new Exception('Unable to open zip file ' . $inFileName); + } + + $entries = []; + for ($i = 0; $i < $inZipFile->numFiles; $i++) { + $entry = $inZipFile->statIndex($i); + $fileName = $entry['name']; + $entries[$fileName] = $entry; + } + + // see ZipStreamTest.php + $outFileStream = fopen($outFileName, 'wb+'); + + $outZipStream = new ZipStream( + outputName: basename($outFileName), + outputStream: $outFileStream, + sendHttpHeaders: false, + ); + foreach ($entries as $fileName => $entry) { + $date = new DateTime(); + $date->setTimestamp($entry['mtime']); + // does not work in v2 - the zip stream is not seekable, but ZipStream checks for it in Stream.php + // does work in v3 - implemented using addFileFromCallback, so we might as well use that :-) + $outZipStream->addFileFromCallback( + fileName: $fileName, + exactSize: $entry['size'], + lastModificationDateTime: $date, + callback: function () use ($inZipFile, $fileName) { + // this expects a stream as result, not the actual data + return $inZipFile->getStream($fileName); + }, + ); + } + + $outZipStream->finish(); + fclose($outFileStream); + } +} |