<?php namespace PhpZip; use PhpZip\Constants\UnixStat; use PhpZip\Constants\ZipCompressionLevel; use PhpZip\Constants\ZipCompressionMethod; use PhpZip\Constants\ZipEncryptionMethod; use PhpZip\Constants\ZipOptions; use PhpZip\Constants\ZipPlatform; use PhpZip\Exception\InvalidArgumentException; use PhpZip\Exception\ZipEntryNotFoundException; use PhpZip\Exception\ZipException; use PhpZip\IO\Stream\ResponseStream; use PhpZip\IO\Stream\ZipEntryStreamWrapper; use PhpZip\IO\ZipReader; use PhpZip\IO\ZipWriter; use PhpZip\Model\Data\ZipFileData; use PhpZip\Model\Data\ZipNewData; use PhpZip\Model\ImmutableZipContainer; use PhpZip\Model\ZipContainer; use PhpZip\Model\ZipEntry; use PhpZip\Model\ZipEntryMatcher; use PhpZip\Model\ZipInfo; use PhpZip\Util\FilesUtil; use PhpZip\Util\StringUtil; use Psr\Http\Message\ResponseInterface; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo as SymfonySplFileInfo; /** * Create, open .ZIP files, modify, get info and extract files. * * Implemented support traditional PKWARE encryption and WinZip AES encryption. * Implemented support ZIP64. * Support ZipAlign functional. * * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification * * @author Ne-Lexa alexey@nelexa.ru * @license MIT */ class ZipFile implements ZipFileInterface { /** @var array default mime types */ private static $defaultMimeTypes = [ 'zip' => 'application/zip', 'apk' => 'application/vnd.android.package-archive', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'epub' => 'application/epub+zip', 'jar' => 'application/java-archive', 'odt' => 'application/vnd.oasis.opendocument.text', 'pptx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xpi' => 'application/x-xpinstall', ]; /** @var ZipContainer */ protected $zipContainer; /** @var ZipReader|null */ private $reader; /** * ZipFile constructor. */ public function __construct() { $this->zipContainer = $this->createZipContainer(null); } /** * @param resource $inputStream * @param array $options * * @return ZipReader */ protected function createZipReader($inputStream, array $options = []) { return new ZipReader($inputStream, $options); } /** * @return ZipWriter */ protected function createZipWriter() { return new ZipWriter($this->zipContainer); } /** * @param ImmutableZipContainer|null $sourceContainer * * @return ZipContainer */ protected function createZipContainer(ImmutableZipContainer $sourceContainer = null) { return new ZipContainer($sourceContainer); } /** * Open zip archive from file. * * @param string $filename * @param array $options * * @throws ZipException if can't open file * * @return ZipFile */ public function openFile($filename, array $options = []) { if (!file_exists($filename)) { throw new ZipException("File {$filename} does not exist."); } if (!($handle = @fopen($filename, 'rb'))) { throw new ZipException("File {$filename} can't open."); } return $this->openFromStream($handle, $options); } /** * Open zip archive from raw string data. * * @param string $data * @param array $options * * @throws ZipException if can't open temp stream * * @return ZipFile */ public function openFromString($data, array $options = []) { if ($data === null || $data === '') { throw new InvalidArgumentException('Empty string passed'); } if (!($handle = fopen('php://temp', 'r+b'))) { // @codeCoverageIgnoreStart throw new ZipException('A temporary resource cannot be opened for writing.'); // @codeCoverageIgnoreEnd } fwrite($handle, $data); rewind($handle); return $this->openFromStream($handle, $options); } /** * Open zip archive from stream resource. * * @param resource $handle * @param array $options * * @throws ZipException * * @return ZipFile */ public function openFromStream($handle, array $options = []) { $this->reader = $this->createZipReader($handle, $options); $this->zipContainer = $this->createZipContainer($this->reader->read()); return $this; } /** * @return string[] returns the list files */ public function getListFiles() { // strval is needed to cast entry names to string type return array_map('strval', array_keys($this->zipContainer->getEntries())); } /** * @return int returns the number of entries in this ZIP file */ public function count() { return $this->zipContainer->count(); } /** * Returns the file comment. * * @return string|null the file comment */ public function getArchiveComment() { return $this->zipContainer->getArchiveComment(); } /** * Set archive comment. * * @param string|null $comment * * @return ZipFile */ public function setArchiveComment($comment = null) { $this->zipContainer->setArchiveComment($comment); return $this; } /** * Checks if there is an entry in the archive. * * @param string $entryName * * @return bool */ public function hasEntry($entryName) { return $this->zipContainer->hasEntry($entryName); } /** * Returns ZipEntry object. * * @param string $entryName * * @throws ZipEntryNotFoundException * * @return ZipEntry */ public function getEntry($entryName) { return $this->zipContainer->getEntry($entryName); } /** * Checks that the entry in the archive is a directory. * Returns true if and only if this ZIP entry represents a directory entry * (i.e. end with '/'). * * @param string $entryName * * @throws ZipEntryNotFoundException * * @return bool */ public function isDirectory($entryName) { return $this->getEntry($entryName)->isDirectory(); } /** * Returns entry comment. * * @param string $entryName * * @throws ZipEntryNotFoundException * @throws ZipException * * @return string */ public function getEntryComment($entryName) { return $this->getEntry($entryName)->getComment(); } /** * Set entry comment. * * @param string $entryName * @param string|null $comment * * @throws ZipException * @throws ZipEntryNotFoundException * * @return ZipFile */ public function setEntryComment($entryName, $comment = null) { $this->getEntry($entryName)->setComment($comment); return $this; } /** * Returns the entry contents. * * @param string $entryName * * @throws ZipException * @throws ZipEntryNotFoundException * * @return string */ public function getEntryContents($entryName) { $zipData = $this->zipContainer->getEntry($entryName)->getData(); if ($zipData === null) { throw new ZipException(sprintf('No data for zip entry %s', $entryName)); } return $zipData->getDataAsString(); } /** * @param string $entryName * * @throws ZipException * @throws ZipEntryNotFoundException * * @return resource */ public function getEntryStream($entryName) { $resource = ZipEntryStreamWrapper::wrap($this->zipContainer->getEntry($entryName)); rewind($resource); return $resource; } /** * Get info by entry. * * @param string|ZipEntry $entryName * * @throws ZipEntryNotFoundException * @throws ZipException * * @return ZipInfo */ public function getEntryInfo($entryName) { return new ZipInfo($this->zipContainer->getEntry($entryName)); } /** * Get info by all entries. * * @return ZipInfo[] */ public function getAllInfo() { $infoMap = []; foreach ($this->zipContainer->getEntries() as $name => $entry) { $infoMap[$name] = new ZipInfo($entry); } return $infoMap; } /** * @return ZipEntryMatcher */ public function matcher() { return $this->zipContainer->matcher(); } /** * Returns an array of zip records (ex. for modify time). * * @return ZipEntry[] array of raw zip entries */ public function getEntries() { return $this->zipContainer->getEntries(); } /** * Extract the archive contents (unzip). * * Extract the complete archive or the given files to the specified destination. * * @param string $destDir location where to extract the files * @param array|string|null $entries entries to extract * @param array $options extract options * @param array $extractedEntries if the extractedEntries argument * is present, then the specified * array will be filled with * information about the * extracted entries * * @throws ZipException * * @return ZipFile */ public function extractTo($destDir, $entries = null, array $options = [], &$extractedEntries = []) { if (!file_exists($destDir)) { throw new ZipException(sprintf('Destination %s not found', $destDir)); } if (!is_dir($destDir)) { throw new ZipException('Destination is not directory'); } if (!is_writable($destDir)) { throw new ZipException('Destination is not writable directory'); } if ($extractedEntries === null) { $extractedEntries = []; } $defaultOptions = [ ZipOptions::EXTRACT_SYMLINKS => false, ]; /** @noinspection AdditionOperationOnArraysInspection */ $options += $defaultOptions; $zipEntries = $this->zipContainer->getEntries(); if (!empty($entries)) { if (\is_string($entries)) { $entries = (array) $entries; } if (\is_array($entries)) { $entries = array_unique($entries); $zipEntries = array_intersect_key($zipEntries, array_flip($entries)); } } if (empty($zipEntries)) { return $this; } /** @var int[] $lastModDirs */ $lastModDirs = []; krsort($zipEntries, \SORT_NATURAL); $symlinks = []; $destDir = rtrim($destDir, '/\\'); foreach ($zipEntries as $entryName => $entry) { $unixMode = $entry->getUnixMode(); $entryName = FilesUtil::normalizeZipPath($entryName); $file = $destDir . \DIRECTORY_SEPARATOR . $entryName; $extractedEntries[$file] = $entry; $modifyTimestamp = $entry->getMTime()->getTimestamp(); $atime = $entry->getATime(); $accessTimestamp = $atime === null ? null : $atime->getTimestamp(); $dir = $entry->isDirectory() ? $file : \dirname($file); if (!is_dir($dir)) { $dirMode = $entry->isDirectory() ? $unixMode : 0755; if ($dirMode === 0) { $dirMode = 0755; } if (!mkdir($dir, $dirMode, true) && !is_dir($dir)) { // @codeCoverageIgnoreStart throw new \RuntimeException(sprintf('Directory "%s" was not created', $dir)); // @codeCoverageIgnoreEnd } chmod($dir, $dirMode); } $parts = explode('/', rtrim($entryName, '/')); $path = $destDir . \DIRECTORY_SEPARATOR; foreach ($parts as $part) { if (!isset($lastModDirs[$path]) || $lastModDirs[$path] > $modifyTimestamp) { $lastModDirs[$path] = $modifyTimestamp; } $path .= $part . \DIRECTORY_SEPARATOR; } if ($entry->isDirectory()) { $lastModDirs[$dir] = $modifyTimestamp; continue; } $zipData = $entry->getData(); if ($zipData === null) { continue; } if ($entry->isUnixSymlink()) { $symlinks[$file] = $zipData->getDataAsString(); continue; } /** @noinspection PhpUsageOfSilenceOperatorInspection */ if (!($handle = @fopen($file, 'w+b'))) { // @codeCoverageIgnoreStart throw new ZipException( sprintf( 'Cannot extract zip entry %s. File %s cannot open for write.', $entry->getName(), $file ) ); // @codeCoverageIgnoreEnd } try { $zipData->copyDataToStream($handle); } catch (ZipException $e) { unlink($file); throw $e; } fclose($handle); if ($unixMode === 0) { $unixMode = 0644; } chmod($file, $unixMode); if ($accessTimestamp !== null) { /** @noinspection PotentialMalwareInspection */ touch($file, $modifyTimestamp, $accessTimestamp); } else { touch($file, $modifyTimestamp); } } $allowSymlink = (bool) $options[ZipOptions::EXTRACT_SYMLINKS]; foreach ($symlinks as $linkPath => $target) { if (!FilesUtil::symlink($target, $linkPath, $allowSymlink)) { unset($extractedEntries[$linkPath]); } } krsort($lastModDirs, \SORT_NATURAL); foreach ($lastModDirs as $dir => $lastMod) { touch($dir, $lastMod); } ksort($extractedEntries); return $this; } /** * Add entry from the string. * * @param string $entryName zip entry name * @param string $contents string contents * @param int|null $compressionMethod Compression method. * Use {@see ZipCompressionMethod::STORED}, * {@see ZipCompressionMethod::DEFLATED} or * {@see ZipCompressionMethod::BZIP2}. * If null, then auto choosing method. * * @throws ZipException * * @return ZipFile */ public function addFromString($entryName, $contents, $compressionMethod = null) { $entryName = $this->normalizeEntryName($entryName); if ($contents === null) { throw new InvalidArgumentException('Contents is null'); } $contents = (string) $contents; $length = \strlen($contents); if ($compressionMethod === null || $compressionMethod === ZipEntry::UNKNOWN) { if ($length < 512) { $compressionMethod = ZipCompressionMethod::STORED; } else { $mimeType = FilesUtil::getMimeTypeFromString($contents); $compressionMethod = FilesUtil::isBadCompressionMimeType($mimeType) ? ZipCompressionMethod::STORED : ZipCompressionMethod::DEFLATED; } } $zipEntry = new ZipEntry($entryName); $zipEntry->setData(new ZipNewData($zipEntry, $contents)); $zipEntry->setUncompressedSize($length); $zipEntry->setCompressionMethod($compressionMethod); $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX); $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX); $zipEntry->setUnixMode(0100644); $zipEntry->setTime(time()); $this->addZipEntry($zipEntry); return $this; } /** * @param string $entryName * * @return string */ protected function normalizeEntryName($entryName) { if ($entryName === null) { throw new InvalidArgumentException('Entry name is null'); } $entryName = ltrim((string) $entryName, '\\/'); if (\DIRECTORY_SEPARATOR === '\\') { $entryName = str_replace('\\', '/', $entryName); } if ($entryName === '') { throw new InvalidArgumentException('Empty entry name'); } return $entryName; } /** * @param Finder $finder * @param array $options * * @throws ZipException * * @return ZipEntry[] */ public function addFromFinder(Finder $finder, array $options = []) { $defaultOptions = [ ZipOptions::STORE_ONLY_FILES => false, ZipOptions::COMPRESSION_METHOD => null, ZipOptions::MODIFIED_TIME => null, ]; /** @noinspection AdditionOperationOnArraysInspection */ $options += $defaultOptions; if ($options[ZipOptions::STORE_ONLY_FILES]) { $finder->files(); } $entries = []; foreach ($finder as $fileInfo) { if ($fileInfo->isReadable()) { $entry = $this->addSplFile($fileInfo, null, $options); $entries[$entry->getName()] = $entry; } } return $entries; } /** * @param \SplFileInfo $file * @param string|null $entryName * @param array $options * * @throws ZipException * * @return ZipEntry */ public function addSplFile(\SplFileInfo $file, $entryName = null, array $options = []) { if ($file instanceof \DirectoryIterator) { throw new InvalidArgumentException('File should not be \DirectoryIterator.'); } $defaultOptions = [ ZipOptions::COMPRESSION_METHOD => null, ZipOptions::MODIFIED_TIME => null, ]; /** @noinspection AdditionOperationOnArraysInspection */ $options += $defaultOptions; if (!$file->isReadable()) { throw new InvalidArgumentException(sprintf('File %s is not readable', $file->getPathname())); } if ($entryName === null) { if ($file instanceof SymfonySplFileInfo) { $entryName = $file->getRelativePathname(); } else { $entryName = $file->getBasename(); } } $entryName = $this->normalizeEntryName($entryName); $entryName = $file->isDir() ? rtrim($entryName, '/\\') . '/' : $entryName; $zipEntry = new ZipEntry($entryName); $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX); $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX); $zipData = null; $filePerms = $file->getPerms(); if ($file->isLink()) { $linkTarget = $file->getLinkTarget(); $lengthLinkTarget = \strlen($linkTarget); $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED); $zipEntry->setUncompressedSize($lengthLinkTarget); $zipEntry->setCompressedSize($lengthLinkTarget); $zipEntry->setCrc(crc32($linkTarget)); $filePerms |= UnixStat::UNX_IFLNK; $zipData = new ZipNewData($zipEntry, $linkTarget); } elseif ($file->isFile()) { if (isset($options[ZipOptions::COMPRESSION_METHOD])) { $compressionMethod = $options[ZipOptions::COMPRESSION_METHOD]; } elseif ($file->getSize() < 512) { $compressionMethod = ZipCompressionMethod::STORED; } else { $compressionMethod = FilesUtil::isBadCompressionFile($file->getPathname()) ? ZipCompressionMethod::STORED : ZipCompressionMethod::DEFLATED; } $zipEntry->setCompressionMethod($compressionMethod); $zipData = new ZipFileData($zipEntry, $file); } elseif ($file->isDir()) { $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED); $zipEntry->setUncompressedSize(0); $zipEntry->setCompressedSize(0); $zipEntry->setCrc(0); } $zipEntry->setUnixMode($filePerms); $timestamp = null; if (isset($options[ZipOptions::MODIFIED_TIME])) { $mtime = $options[ZipOptions::MODIFIED_TIME]; if ($mtime instanceof \DateTimeInterface) { $timestamp = $mtime->getTimestamp(); } elseif (is_numeric($mtime)) { $timestamp = (int) $mtime; } elseif (\is_string($mtime)) { $timestamp = strtotime($mtime); if ($timestamp === false) { $timestamp = null; } } } if ($timestamp === null) { $timestamp = $file->getMTime(); } $zipEntry->setTime($timestamp); $zipEntry->setData($zipData); $this->addZipEntry($zipEntry); return $zipEntry; } /** * @param ZipEntry $zipEntry */ protected function addZipEntry(ZipEntry $zipEntry) { $this->zipContainer->addEntry($zipEntry); } /** * Add entry from the file. * * @param string $filename destination file * @param string|null $entryName zip Entry name * @param int|null $compressionMethod Compression method. * Use {@see ZipCompressionMethod::STORED}, * {@see ZipCompressionMethod::DEFLATED} or * {@see ZipCompressionMethod::BZIP2}. * If null, then auto choosing method. * * @throws ZipException * * @return ZipFile */ public function addFile($filename, $entryName = null, $compressionMethod = null) { if ($filename === null) { throw new InvalidArgumentException('Filename is null'); } $this->addSplFile( new \SplFileInfo($filename), $entryName, [ ZipOptions::COMPRESSION_METHOD => $compressionMethod, ] ); return $this; } /** * Add entry from the stream. * * @param resource $stream stream resource * @param string $entryName zip Entry name * @param int|null $compressionMethod Compression method. * Use {@see ZipCompressionMethod::STORED}, * {@see ZipCompressionMethod::DEFLATED} or * {@see ZipCompressionMethod::BZIP2}. * If null, then auto choosing method. * * @throws ZipException * * @return ZipFile */ public function addFromStream($stream, $entryName, $compressionMethod = null) { if (!\is_resource($stream)) { throw new InvalidArgumentException('Stream is not resource'); } $entryName = $this->normalizeEntryName($entryName); $zipEntry = new ZipEntry($entryName); $fstat = fstat($stream); if ($fstat !== false) { $unixMode = $fstat['mode']; $length = $fstat['size']; if ($compressionMethod === null || $compressionMethod === ZipEntry::UNKNOWN) { if ($length < 512) { $compressionMethod = ZipCompressionMethod::STORED; } else { rewind($stream); $bufferContents = stream_get_contents($stream, min(1024, $length)); rewind($stream); $mimeType = FilesUtil::getMimeTypeFromString($bufferContents); $compressionMethod = FilesUtil::isBadCompressionMimeType($mimeType) ? ZipCompressionMethod::STORED : ZipCompressionMethod::DEFLATED; } $zipEntry->setUncompressedSize($length); } } else { $unixMode = 0100644; if ($compressionMethod === null || $compressionMethod === ZipEntry::UNKNOWN) { $compressionMethod = ZipCompressionMethod::DEFLATED; } } $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX); $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX); $zipEntry->setUnixMode($unixMode); $zipEntry->setCompressionMethod($compressionMethod); $zipEntry->setTime(time()); $zipEntry->setData(new ZipNewData($zipEntry, $stream)); $this->addZipEntry($zipEntry); return $this; } /** * Add an empty directory in the zip archive. * * @param string $dirName * * @throws ZipException * * @return ZipFile */ public function addEmptyDir($dirName) { $dirName = $this->normalizeEntryName($dirName); $dirName = rtrim($dirName, '\\/') . '/'; $zipEntry = new ZipEntry($dirName); $zipEntry->setCompressionMethod(ZipCompressionMethod::STORED); $zipEntry->setUncompressedSize(0); $zipEntry->setCompressedSize(0); $zipEntry->setCrc(0); $zipEntry->setCreatedOS(ZipPlatform::OS_UNIX); $zipEntry->setExtractedOS(ZipPlatform::OS_UNIX); $zipEntry->setUnixMode(040755); $zipEntry->setTime(time()); $this->addZipEntry($zipEntry); return $this; } /** * Add directory not recursively to the zip archive. * * @param string $inputDir Input directory * @param string $localPath add files to this directory, or the root * @param int|null $compressionMethod Compression method. * * Use {@see ZipCompressionMethod::STORED}, {@see * ZipCompressionMethod::DEFLATED} or * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * * @return ZipFile */ public function addDir($inputDir, $localPath = '/', $compressionMethod = null) { if ($inputDir === null) { throw new InvalidArgumentException('Input dir is null'); } $inputDir = (string) $inputDir; if ($inputDir === '') { throw new InvalidArgumentException('The input directory is not specified'); } if (!is_dir($inputDir)) { throw new InvalidArgumentException(sprintf('The "%s" directory does not exist.', $inputDir)); } $inputDir = rtrim($inputDir, '/\\') . \DIRECTORY_SEPARATOR; $directoryIterator = new \DirectoryIterator($inputDir); return $this->addFilesFromIterator($directoryIterator, $localPath, $compressionMethod); } /** * Add recursive directory to the zip archive. * * @param string $inputDir Input directory * @param string $localPath add files to this directory, or the root * @param int|null $compressionMethod Compression method. * Use {@see ZipCompressionMethod::STORED}, {@see * ZipCompressionMethod::DEFLATED} or * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * * @return ZipFile * * @see ZipCompressionMethod::STORED * @see ZipCompressionMethod::DEFLATED * @see ZipCompressionMethod::BZIP2 */ public function addDirRecursive($inputDir, $localPath = '/', $compressionMethod = null) { if ($inputDir === null) { throw new InvalidArgumentException('Input dir is null'); } $inputDir = (string) $inputDir; if ($inputDir === '') { throw new InvalidArgumentException('The input directory is not specified'); } if (!is_dir($inputDir)) { throw new InvalidArgumentException(sprintf('The "%s" directory does not exist.', $inputDir)); } $inputDir = rtrim($inputDir, '/\\') . \DIRECTORY_SEPARATOR; $directoryIterator = new \RecursiveDirectoryIterator($inputDir); return $this->addFilesFromIterator($directoryIterator, $localPath, $compressionMethod); } /** * Add directories from directory iterator. * * @param \Iterator $iterator directory iterator * @param string $localPath add files to this directory, or the root * @param int|null $compressionMethod Compression method. * Use {@see ZipCompressionMethod::STORED}, {@see * ZipCompressionMethod::DEFLATED} or * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * * @return ZipFile * * @see ZipCompressionMethod::STORED * @see ZipCompressionMethod::DEFLATED * @see ZipCompressionMethod::BZIP2 */ public function addFilesFromIterator( \Iterator $iterator, $localPath = '/', $compressionMethod = null ) { $localPath = (string) $localPath; if ($localPath !== '') { $localPath = trim($localPath, '\\/'); } else { $localPath = ''; } $iterator = $iterator instanceof \RecursiveIterator ? new \RecursiveIteratorIterator($iterator) : new \IteratorIterator($iterator); /** * @var string[] $files * @var string $path */ $files = []; foreach ($iterator as $file) { if ($file instanceof \SplFileInfo) { if ($file->getBasename() === '..') { continue; } if ($file->getBasename() === '.') { $files[] = \dirname($file->getPathname()); } else { $files[] = $file->getPathname(); } } } if (empty($files)) { return $this; } natcasesort($files); $path = array_shift($files); $this->doAddFiles($path, $files, $localPath, $compressionMethod); return $this; } /** * Add files from glob pattern. * * @param string $inputDir Input directory * @param string $globPattern glob pattern * @param string $localPath add files to this directory, or the root * @param int|null $compressionMethod Compression method. * Use {@see ZipCompressionMethod::STORED}, * {@see ZipCompressionMethod::DEFLATED} or * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * * @return ZipFile * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ public function addFilesFromGlob($inputDir, $globPattern, $localPath = '/', $compressionMethod = null) { return $this->addGlob($inputDir, $globPattern, $localPath, false, $compressionMethod); } /** * Add files from glob pattern. * * @param string $inputDir Input directory * @param string $globPattern glob pattern * @param string $localPath add files to this directory, or the root * @param bool $recursive recursive search * @param int|null $compressionMethod Compression method. * Use {@see ZipCompressionMethod::STORED}, * {@see ZipCompressionMethod::DEFLATED} or * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * * @return ZipFile * * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ private function addGlob( $inputDir, $globPattern, $localPath = '/', $recursive = true, $compressionMethod = null ) { if ($inputDir === null) { throw new InvalidArgumentException('Input dir is null'); } $inputDir = (string) $inputDir; if ($inputDir === '') { throw new InvalidArgumentException('The input directory is not specified'); } if (!is_dir($inputDir)) { throw new InvalidArgumentException(sprintf('The "%s" directory does not exist.', $inputDir)); } $globPattern = (string) $globPattern; if (empty($globPattern)) { throw new InvalidArgumentException('The glob pattern is not specified'); } $inputDir = rtrim($inputDir, '/\\') . \DIRECTORY_SEPARATOR; $globPattern = $inputDir . $globPattern; $filesFound = FilesUtil::globFileSearch($globPattern, \GLOB_BRACE, $recursive); if ($filesFound === false || empty($filesFound)) { return $this; } $this->doAddFiles($inputDir, $filesFound, $localPath, $compressionMethod); return $this; } /** * Add files recursively from glob pattern. * * @param string $inputDir Input directory * @param string $globPattern glob pattern * @param string $localPath add files to this directory, or the root * @param int|null $compressionMethod Compression method. * Use {@see ZipCompressionMethod::STORED}, * {@see ZipCompressionMethod::DEFLATED} or * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * * @return ZipFile * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ public function addFilesFromGlobRecursive($inputDir, $globPattern, $localPath = '/', $compressionMethod = null) { return $this->addGlob($inputDir, $globPattern, $localPath, true, $compressionMethod); } /** * Add files from regex pattern. * * @param string $inputDir search files in this directory * @param string $regexPattern regex pattern * @param string $localPath add files to this directory, or the root * @param int|null $compressionMethod Compression method. * Use {@see ZipCompressionMethod::STORED}, * {@see ZipCompressionMethod::DEFLATED} or * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * * @return ZipFile * * @internal param bool $recursive Recursive search */ public function addFilesFromRegex($inputDir, $regexPattern, $localPath = '/', $compressionMethod = null) { return $this->addRegex($inputDir, $regexPattern, $localPath, false, $compressionMethod); } /** * Add files from regex pattern. * * @param string $inputDir search files in this directory * @param string $regexPattern regex pattern * @param string $localPath add files to this directory, or the root * @param bool $recursive recursive search * @param int|null $compressionMethod Compression method. * Use {@see ZipCompressionMethod::STORED}, * {@see ZipCompressionMethod::DEFLATED} or * {@see ZipCompressionMethod::BZIP2}. * If null, then auto choosing method. * * @throws ZipException * * @return ZipFile */ private function addRegex( $inputDir, $regexPattern, $localPath = '/', $recursive = true, $compressionMethod = null ) { $regexPattern = (string) $regexPattern; if (empty($regexPattern)) { throw new InvalidArgumentException('The regex pattern is not specified'); } $inputDir = (string) $inputDir; if ($inputDir === '') { throw new InvalidArgumentException('The input directory is not specified'); } if (!is_dir($inputDir)) { throw new InvalidArgumentException(sprintf('The "%s" directory does not exist.', $inputDir)); } $inputDir = rtrim($inputDir, '/\\') . \DIRECTORY_SEPARATOR; $files = FilesUtil::regexFileSearch($inputDir, $regexPattern, $recursive); if (empty($files)) { return $this; } $this->doAddFiles($inputDir, $files, $localPath, $compressionMethod); return $this; } /** * @param string $fileSystemDir * @param array $files * @param string $zipPath * @param int|null $compressionMethod * * @throws ZipException */ private function doAddFiles($fileSystemDir, array $files, $zipPath, $compressionMethod = null) { $fileSystemDir = rtrim($fileSystemDir, '/\\') . \DIRECTORY_SEPARATOR; if (!empty($zipPath) && \is_string($zipPath)) { $zipPath = trim($zipPath, '\\/') . '/'; } else { $zipPath = '/'; } /** * @var string $file */ foreach ($files as $file) { $filename = str_replace($fileSystemDir, $zipPath, $file); $filename = ltrim($filename, '\\/'); if (is_dir($file) && FilesUtil::isEmptyDir($file)) { $this->addEmptyDir($filename); } elseif (is_file($file)) { $this->addFile($file, $filename, $compressionMethod); } } } /** * Add files recursively from regex pattern. * * @param string $inputDir search files in this directory * @param string $regexPattern regex pattern * @param string $localPath add files to this directory, or the root * @param int|null $compressionMethod Compression method. * Use {@see ZipCompressionMethod::STORED}, * {@see ZipCompressionMethod::DEFLATED} or * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * * @return ZipFile * * @internal param bool $recursive Recursive search */ public function addFilesFromRegexRecursive($inputDir, $regexPattern, $localPath = '/', $compressionMethod = null) { return $this->addRegex($inputDir, $regexPattern, $localPath, true, $compressionMethod); } /** * Add array data to archive. * Keys is local names. * Values is contents. * * @param array $mapData associative array for added to zip */ public function addAll(array $mapData) { foreach ($mapData as $localName => $content) { $this[$localName] = $content; } } /** * Rename the entry. * * @param string $oldName old entry name * @param string $newName new entry name * * @throws ZipException * * @return ZipFile */ public function rename($oldName, $newName) { if ($oldName === null || $newName === null) { throw new InvalidArgumentException('name is null'); } $oldName = ltrim((string) $oldName, '\\/'); $newName = ltrim((string) $newName, '\\/'); if ($oldName !== $newName) { $this->zipContainer->renameEntry($oldName, $newName); } return $this; } /** * Delete entry by name. * * @param string $entryName zip Entry name * * @throws ZipEntryNotFoundException if entry not found * * @return ZipFile */ public function deleteFromName($entryName) { $entryName = ltrim((string) $entryName, '\\/'); if (!$this->zipContainer->deleteEntry($entryName)) { throw new ZipEntryNotFoundException($entryName); } return $this; } /** * Delete entries by glob pattern. * * @param string $globPattern Glob pattern * * @return ZipFile * @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax */ public function deleteFromGlob($globPattern) { if ($globPattern === null || !\is_string($globPattern) || empty($globPattern)) { throw new InvalidArgumentException('The glob pattern is not specified'); } $globPattern = '~' . FilesUtil::convertGlobToRegEx($globPattern) . '~si'; $this->deleteFromRegex($globPattern); return $this; } /** * Delete entries by regex pattern. * * @param string $regexPattern Regex pattern * * @return ZipFile */ public function deleteFromRegex($regexPattern) { if ($regexPattern === null || !\is_string($regexPattern) || empty($regexPattern)) { throw new InvalidArgumentException('The regex pattern is not specified'); } $this->matcher()->match($regexPattern)->delete(); return $this; } /** * Delete all entries. * * @return ZipFile */ public function deleteAll() { $this->zipContainer->deleteAll(); return $this; } /** * Set compression level for new entries. * * @param int $compressionLevel * * @return ZipFile * * @see ZipCompressionLevel::NORMAL * @see ZipCompressionLevel::SUPER_FAST * @see ZipCompressionLevel::FAST * @see ZipCompressionLevel::MAXIMUM */ public function setCompressionLevel($compressionLevel = ZipCompressionLevel::NORMAL) { $compressionLevel = (int) $compressionLevel; foreach ($this->zipContainer->getEntries() as $entry) { $entry->setCompressionLevel($compressionLevel); } return $this; } /** * @param string $entryName * @param int $compressionLevel * * @throws ZipException * * @return ZipFile * * @see ZipCompressionLevel::NORMAL * @see ZipCompressionLevel::SUPER_FAST * @see ZipCompressionLevel::FAST * @see ZipCompressionLevel::MAXIMUM */ public function setCompressionLevelEntry($entryName, $compressionLevel) { $compressionLevel = (int) $compressionLevel; $this->getEntry($entryName)->setCompressionLevel($compressionLevel); return $this; } /** * @param string $entryName * @param int $compressionMethod Compression method. * Use {@see ZipCompressionMethod::STORED}, {@see ZipCompressionMethod::DEFLATED} * or * {@see ZipCompressionMethod::BZIP2}. If null, then auto choosing method. * * @throws ZipException * * @return ZipFile * * @see ZipCompressionMethod::STORED * @see ZipCompressionMethod::DEFLATED * @see ZipCompressionMethod::BZIP2 */ public function setCompressionMethodEntry($entryName, $compressionMethod) { $this->zipContainer ->getEntry($entryName) ->setCompressionMethod($compressionMethod) ; return $this; } /** * zipalign is optimization to Android application (APK) files. * * @param int|null $align * * @return ZipFile * * @see https://developer.android.com/studio/command-line/zipalign.html */ public function setZipAlign($align = null) { $this->zipContainer->setZipAlign($align); return $this; } /** * Set password to all input encrypted entries. * * @param string $password Password * * @return ZipFile */ public function setReadPassword($password) { $this->zipContainer->setReadPassword($password); return $this; } /** * Set password to concrete input entry. * * @param string $entryName * @param string $password Password * * @throws ZipException * * @return ZipFile */ public function setReadPasswordEntry($entryName, $password) { $this->zipContainer->setReadPasswordEntry($entryName, $password); return $this; } /** * Sets a new password for all files in the archive. * * @param string $password Password * @param int|null $encryptionMethod Encryption method * * @return ZipFile */ public function setPassword($password, $encryptionMethod = ZipEncryptionMethod::WINZIP_AES_256) { $this->zipContainer->setWritePassword($password); if ($encryptionMethod !== null) { $this->zipContainer->setEncryptionMethod($encryptionMethod); } return $this; } /** * Sets a new password of an entry defined by its name. * * @param string $entryName * @param string $password * @param int|null $encryptionMethod * * @throws ZipException * * @return ZipFile */ public function setPasswordEntry($entryName, $password, $encryptionMethod = null) { $this->getEntry($entryName)->setPassword($password, $encryptionMethod); return $this; } /** * Disable encryption for all entries that are already in the archive. * * @return ZipFile */ public function disableEncryption() { $this->zipContainer->removePassword(); return $this; } /** * Disable encryption of an entry defined by its name. * * @param string $entryName * * @return ZipFile */ public function disableEncryptionEntry($entryName) { $this->zipContainer->removePasswordEntry($entryName); return $this; } /** * Undo all changes done in the archive. * * @return ZipFile */ public function unchangeAll() { $this->zipContainer->unchangeAll(); return $this; } /** * Undo change archive comment. * * @return ZipFile */ public function unchangeArchiveComment() { $this->zipContainer->unchangeArchiveComment(); return $this; } /** * Revert all changes done to an entry with the given name. * * @param string|ZipEntry $entry Entry name or ZipEntry * * @return ZipFile */ public function unchangeEntry($entry) { $this->zipContainer->unchangeEntry($entry); return $this; } /** * Save as file. * * @param string $filename Output filename * * @throws ZipException * * @return ZipFile */ public function saveAsFile($filename) { $filename = (string) $filename; $tempFilename = $filename . '.temp' . uniqid('', false); if (!($handle = @fopen($tempFilename, 'w+b'))) { throw new InvalidArgumentException(sprintf('Cannot open "%s" for writing.', $tempFilename)); } $this->saveAsStream($handle); $reopen = false; if ($this->reader !== null) { $meta = $this->reader->getStreamMetaData(); if ($meta['wrapper_type'] === 'plainfile' && isset($meta['uri'])) { $readFilePath = realpath($meta['uri']); $writeFilePath = realpath($filename); if ($readFilePath !== false && $writeFilePath !== false && $readFilePath === $writeFilePath) { $this->reader->close(); $reopen = true; } } } if (!@rename($tempFilename, $filename)) { if (is_file($tempFilename)) { unlink($tempFilename); } throw new ZipException(sprintf('Cannot move %s to %s', $tempFilename, $filename)); } if ($reopen) { return $this->openFile($filename); } return $this; } /** * Save as stream. * * @param resource $handle Output stream resource * * @throws ZipException * * @return ZipFile */ public function saveAsStream($handle) { if (!\is_resource($handle)) { throw new InvalidArgumentException('handle is not resource'); } ftruncate($handle, 0); $this->writeZipToStream($handle); fclose($handle); return $this; } /** * Output .ZIP archive as attachment. * Die after output. * * @param string $outputFilename Output filename * @param string|null $mimeType Mime-Type * @param bool $attachment Http Header 'Content-Disposition' if true then attachment otherwise inline * * @throws ZipException */ public function outputAsAttachment($outputFilename, $mimeType = null, $attachment = true) { $outputFilename = (string) $outputFilename; if ($mimeType === null) { $mimeType = $this->getMimeTypeByFilename($outputFilename); } if (!($handle = fopen('php://temp', 'w+b'))) { throw new InvalidArgumentException('php://temp cannot open for write.'); } $this->writeZipToStream($handle); $this->close(); $size = fstat($handle)['size']; $headerContentDisposition = 'Content-Disposition: ' . ($attachment ? 'attachment' : 'inline'); if (!empty($outputFilename)) { $headerContentDisposition .= '; filename="' . basename($outputFilename) . '"'; } header($headerContentDisposition); header('Content-Type: ' . $mimeType); header('Content-Length: ' . $size); rewind($handle); try { echo stream_get_contents($handle, -1, 0); } finally { fclose($handle); } } /** * @param string $outputFilename * * @return string */ protected function getMimeTypeByFilename($outputFilename) { $outputFilename = (string) $outputFilename; $ext = strtolower(pathinfo($outputFilename, \PATHINFO_EXTENSION)); if (!empty($ext) && isset(self::$defaultMimeTypes[$ext])) { return self::$defaultMimeTypes[$ext]; } return self::$defaultMimeTypes['zip']; } /** * Output .ZIP archive as PSR-7 Response. * * @param ResponseInterface $response Instance PSR-7 Response * @param string $outputFilename Output filename * @param string|null $mimeType Mime-Type * @param bool $attachment Http Header 'Content-Disposition' if true then attachment otherwise inline * * @throws ZipException * * @return ResponseInterface */ public function outputAsResponse(ResponseInterface $response, $outputFilename, $mimeType = null, $attachment = true) { $outputFilename = (string) $outputFilename; if ($mimeType === null) { $mimeType = $this->getMimeTypeByFilename($outputFilename); } if (!($handle = fopen('php://temp', 'w+b'))) { throw new InvalidArgumentException('php://temp cannot open for write.'); } $this->writeZipToStream($handle); $this->close(); rewind($handle); $contentDispositionValue = ($attachment ? 'attachment' : 'inline'); if (!empty($outputFilename)) { $contentDispositionValue .= '; filename="' . basename($outputFilename) . '"'; } $stream = new ResponseStream($handle); $size = $stream->getSize(); if ($size !== null) { /** @noinspection CallableParameterUseCaseInTypeContextInspection */ $response = $response->withHeader('Content-Length', (string) $size); } return $response ->withHeader('Content-Type', $mimeType) ->withHeader('Content-Disposition', $contentDispositionValue) ->withBody($stream) ; } /** * @param resource $handle * * @throws ZipException */ protected function writeZipToStream($handle) { $this->onBeforeSave(); $this->createZipWriter()->write($handle); } /** * Returns the zip archive as a string. * * @throws ZipException * * @return string */ public function outputAsString() { if (!($handle = fopen('php://temp', 'w+b'))) { throw new InvalidArgumentException('php://temp cannot open for write.'); } $this->writeZipToStream($handle); rewind($handle); try { return stream_get_contents($handle); } finally { fclose($handle); } } /** * Event before save or output. */ protected function onBeforeSave() { } /** * Close zip archive and release input stream. */ public function close() { if ($this->reader !== null) { $this->reader->close(); $this->reader = null; } $this->zipContainer = $this->createZipContainer(null); gc_collect_cycles(); } /** * Save and reopen zip archive. * * @throws ZipException * * @return ZipFile */ public function rewrite() { if ($this->reader === null) { throw new ZipException('input stream is null'); } $meta = $this->reader->getStreamMetaData(); if ($meta['wrapper_type'] !== 'plainfile' || !isset($meta['uri'])) { throw new ZipException('Overwrite is only supported for open local files.'); } return $this->saveAsFile($meta['uri']); } /** * Release all resources. */ public function __destruct() { $this->close(); } /** * Offset to set. * * @see http://php.net/manual/en/arrayaccess.offsetset.php * * @param string $entryName the offset to assign the value to * @param string|\DirectoryIterator|\SplFileInfo|resource $contents the value to set * * @throws ZipException * * @see ZipFile::addFromString * @see ZipFile::addEmptyDir * @see ZipFile::addFile * @see ZipFile::addFilesFromIterator */ public function offsetSet($entryName, $contents) { if ($entryName === null) { throw new InvalidArgumentException('Key must not be null, but must contain the name of the zip entry.'); } $entryName = ltrim((string) $entryName, '\\/'); if ($entryName === '') { throw new InvalidArgumentException('Key is empty, but must contain the name of the zip entry.'); } if ($contents instanceof \DirectoryIterator) { $this->addFilesFromIterator($contents, $entryName); } elseif ($contents instanceof \SplFileInfo) { $this->addSplFile($contents, $entryName); } elseif (StringUtil::endsWith($entryName, '/')) { $this->addEmptyDir($entryName); } elseif (\is_resource($contents)) { $this->addFromStream($contents, $entryName); } else { $this->addFromString($entryName, (string) $contents); } } /** * Offset to unset. * * @see http://php.net/manual/en/arrayaccess.offsetunset.php * * @param string $entryName the offset to unset * * @throws ZipEntryNotFoundException */ public function offsetUnset($entryName) { $this->deleteFromName($entryName); } /** * Return the current element. * * @see http://php.net/manual/en/iterator.current.php * * @throws ZipException * * @return mixed can return any type * * @since 5.0.0 */ public function current() { return $this->offsetGet($this->key()); } /** * Offset to retrieve. * * @see http://php.net/manual/en/arrayaccess.offsetget.php * * @param string $entryName the offset to retrieve * * @throws ZipException * * @return string|null */ public function offsetGet($entryName) { return $this->getEntryContents($entryName); } /** * Return the key of the current element. * * @see http://php.net/manual/en/iterator.key.php * * @return mixed scalar on success, or null on failure * * @since 5.0.0 */ public function key() { return key($this->zipContainer->getEntries()); } /** * Move forward to next element. * * @see http://php.net/manual/en/iterator.next.php * @since 5.0.0 */ public function next() { next($this->zipContainer->getEntries()); } /** * Checks if current position is valid. * * @see http://php.net/manual/en/iterator.valid.php * * @return bool The return value will be casted to boolean and then evaluated. * Returns true on success or false on failure. * * @since 5.0.0 */ public function valid() { return $this->offsetExists($this->key()); } /** * Whether a offset exists. * * @see http://php.net/manual/en/arrayaccess.offsetexists.php * * @param string $entryName an offset to check for * * @return bool true on success or false on failure. * The return value will be casted to boolean if non-boolean was returned. */ public function offsetExists($entryName) { return $this->hasEntry($entryName); } /** * Rewind the Iterator to the first element. * * @see http://php.net/manual/en/iterator.rewind.php * @since 5.0.0 */ public function rewind() { reset($this->zipContainer->getEntries()); } }