1513 lines
49 KiB
PHP
1513 lines
49 KiB
PHP
<?php
|
|
|
|
namespace PhpZip;
|
|
|
|
use PhpZip\Exception\InvalidArgumentException;
|
|
use PhpZip\Exception\ZipException;
|
|
use PhpZip\Exception\ZipNotFoundEntry;
|
|
use PhpZip\Exception\ZipUnsupportMethod;
|
|
use PhpZip\Model\Entry\ZipNewEntry;
|
|
use PhpZip\Model\ZipEntry;
|
|
use PhpZip\Model\ZipEntryMatcher;
|
|
use PhpZip\Model\ZipInfo;
|
|
use PhpZip\Model\ZipModel;
|
|
use PhpZip\Stream\ResponseStream;
|
|
use PhpZip\Stream\ZipInputStream;
|
|
use PhpZip\Stream\ZipInputStreamInterface;
|
|
use PhpZip\Stream\ZipOutputStream;
|
|
use PhpZip\Util\FilesUtil;
|
|
use PhpZip\Util\StringUtil;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
|
|
/**
|
|
* Create, open .ZIP files, modify, get info and extract files.
|
|
*
|
|
* Implemented support traditional PKWARE encryption and WinZip AES encryption.
|
|
* Implemented support ZIP64.
|
|
* Implemented support skip a preamble like the one found in self extracting archives.
|
|
* 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
|
|
{
|
|
/**
|
|
* Allow compression methods.
|
|
* @var int[]
|
|
*/
|
|
private static $allowCompressionMethods = [
|
|
self::METHOD_STORED,
|
|
self::METHOD_DEFLATED,
|
|
self::METHOD_BZIP2,
|
|
ZipEntry::UNKNOWN
|
|
];
|
|
|
|
/**
|
|
* Allow encryption methods.
|
|
* @var int[]
|
|
*/
|
|
private static $allowEncryptionMethods = [
|
|
self::ENCRYPTION_METHOD_TRADITIONAL,
|
|
self::ENCRYPTION_METHOD_WINZIP_AES_128,
|
|
self::ENCRYPTION_METHOD_WINZIP_AES_192,
|
|
self::ENCRYPTION_METHOD_WINZIP_AES_256
|
|
];
|
|
|
|
/**
|
|
* Default mime types.
|
|
*
|
|
* @var array
|
|
*/
|
|
private static $defaultMimeTypes = [
|
|
'zip' => 'application/zip',
|
|
'apk' => 'application/vnd.android.package-archive',
|
|
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'jar' => 'application/java-archive',
|
|
'epub' => 'application/epub+zip'
|
|
];
|
|
|
|
/**
|
|
* Input seekable input stream.
|
|
*
|
|
* @var ZipInputStreamInterface
|
|
*/
|
|
protected $inputStream;
|
|
/**
|
|
* @var ZipModel
|
|
*/
|
|
protected $zipModel;
|
|
|
|
/**
|
|
* ZipFile constructor.
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->zipModel = new ZipModel();
|
|
}
|
|
|
|
/**
|
|
* Open zip archive from file
|
|
*
|
|
* @param string $filename
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException if file doesn't exists.
|
|
* @throws ZipException if can't open file.
|
|
*/
|
|
public function openFile($filename)
|
|
{
|
|
if (!file_exists($filename)) {
|
|
throw new InvalidArgumentException("File $filename can't exists.");
|
|
}
|
|
if (!($handle = @fopen($filename, 'rb'))) {
|
|
throw new ZipException("File $filename can't open.");
|
|
}
|
|
$this->openFromStream($handle);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Open zip archive from raw string data.
|
|
*
|
|
* @param string $data
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException if data not available.
|
|
* @throws ZipException if can't open temp stream.
|
|
*/
|
|
public function openFromString($data)
|
|
{
|
|
if (null === $data || strlen($data) === 0) {
|
|
throw new InvalidArgumentException("Data not available");
|
|
}
|
|
if (!($handle = fopen('php://temp', 'r+b'))) {
|
|
throw new ZipException("Can't open temp stream.");
|
|
}
|
|
fwrite($handle, $data);
|
|
rewind($handle);
|
|
$this->openFromStream($handle);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Open zip archive from stream resource
|
|
*
|
|
* @param resource $handle
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException Invalid stream resource
|
|
* or resource cannot seekable stream
|
|
*/
|
|
public function openFromStream($handle)
|
|
{
|
|
if (!is_resource($handle)) {
|
|
throw new InvalidArgumentException("Invalid stream resource.");
|
|
}
|
|
$type = get_resource_type($handle);
|
|
if ('stream' !== $type) {
|
|
throw new InvalidArgumentException("Invalid resource type - $type.");
|
|
}
|
|
$meta = stream_get_meta_data($handle);
|
|
if ('dir' === $meta['stream_type']) {
|
|
throw new InvalidArgumentException("Invalid stream type - {$meta['stream_type']}.");
|
|
}
|
|
if (!$meta['seekable']) {
|
|
throw new InvalidArgumentException("Resource cannot seekable stream.");
|
|
}
|
|
$this->inputStream = new ZipInputStream($handle);
|
|
$this->zipModel = $this->inputStream->readZip();
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return string[] Returns the list files.
|
|
*/
|
|
public function getListFiles()
|
|
{
|
|
return array_keys($this->zipModel->getEntries());
|
|
}
|
|
|
|
/**
|
|
* @return int Returns the number of entries in this ZIP file.
|
|
*/
|
|
public function count()
|
|
{
|
|
return $this->zipModel->count();
|
|
}
|
|
|
|
/**
|
|
* Returns the file comment.
|
|
*
|
|
* @return string The file comment.
|
|
*/
|
|
public function getArchiveComment()
|
|
{
|
|
return $this->zipModel->getArchiveComment();
|
|
}
|
|
|
|
/**
|
|
* Set archive comment.
|
|
*
|
|
* @param null|string $comment
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException Length comment out of range
|
|
*/
|
|
public function setArchiveComment($comment = null)
|
|
{
|
|
$this->zipModel->setArchiveComment($comment);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @return bool
|
|
* @throws ZipNotFoundEntry
|
|
*/
|
|
public function isDirectory($entryName)
|
|
{
|
|
return $this->zipModel->getEntry($entryName)->isDirectory();
|
|
}
|
|
|
|
/**
|
|
* Returns entry comment.
|
|
*
|
|
* @param string $entryName
|
|
* @return string
|
|
* @throws ZipNotFoundEntry
|
|
*/
|
|
public function getEntryComment($entryName)
|
|
{
|
|
return $this->zipModel->getEntry($entryName)->getComment();
|
|
}
|
|
|
|
/**
|
|
* Set entry comment.
|
|
*
|
|
* @param string $entryName
|
|
* @param string|null $comment
|
|
* @return ZipFileInterface
|
|
* @throws ZipNotFoundEntry
|
|
*/
|
|
public function setEntryComment($entryName, $comment = null)
|
|
{
|
|
$this->zipModel->getEntryForChanges($entryName)->setComment($comment);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Returns the entry contents.
|
|
*
|
|
* @param string $entryName
|
|
* @return string
|
|
*/
|
|
public function getEntryContents($entryName)
|
|
{
|
|
return $this->zipModel->getEntry($entryName)->getEntryContent();
|
|
}
|
|
|
|
/**
|
|
* Checks if there is an entry in the archive.
|
|
*
|
|
* @param string $entryName
|
|
* @return bool
|
|
*/
|
|
public function hasEntry($entryName)
|
|
{
|
|
return $this->zipModel->hasEntry($entryName);
|
|
}
|
|
|
|
/**
|
|
* Get info by entry.
|
|
*
|
|
* @param string|ZipEntry $entryName
|
|
* @return ZipInfo
|
|
* @throws ZipNotFoundEntry
|
|
*/
|
|
public function getEntryInfo($entryName)
|
|
{
|
|
return new ZipInfo($this->zipModel->getEntry($entryName));
|
|
}
|
|
|
|
/**
|
|
* Get info by all entries.
|
|
*
|
|
* @return ZipInfo[]
|
|
*/
|
|
public function getAllInfo()
|
|
{
|
|
return array_map([$this, 'getEntryInfo'], $this->zipModel->getEntries());
|
|
}
|
|
|
|
/**
|
|
* @return ZipEntryMatcher
|
|
*/
|
|
public function matcher()
|
|
{
|
|
return $this->zipModel->matcher();
|
|
}
|
|
|
|
/**
|
|
* Extract the archive contents
|
|
*
|
|
* Extract the complete archive or the given files to the specified destination.
|
|
*
|
|
* @param string $destination Location where to extract the files.
|
|
* @param array|string|null $entries The entries to extract. It accepts either
|
|
* a single entry name or an array of names.
|
|
* @return ZipFileInterface
|
|
* @throws ZipException
|
|
*/
|
|
public function extractTo($destination, $entries = null)
|
|
{
|
|
if (!file_exists($destination)) {
|
|
throw new ZipException("Destination " . $destination . " not found");
|
|
}
|
|
if (!is_dir($destination)) {
|
|
throw new ZipException("Destination is not directory");
|
|
}
|
|
if (!is_writable($destination)) {
|
|
throw new ZipException("Destination is not writable directory");
|
|
}
|
|
|
|
$zipEntries = $this->zipModel->getEntries();
|
|
|
|
if (!empty($entries)) {
|
|
if (is_string($entries)) {
|
|
$entries = (array)$entries;
|
|
}
|
|
if (is_array($entries)) {
|
|
$entries = array_unique($entries);
|
|
$flipEntries = array_flip($entries);
|
|
$zipEntries = array_filter($zipEntries, function (ZipEntry $zipEntry) use ($flipEntries) {
|
|
return isset($flipEntries[$zipEntry->getName()]);
|
|
});
|
|
}
|
|
}
|
|
|
|
foreach ($zipEntries as $entry) {
|
|
$file = $destination . DIRECTORY_SEPARATOR . $entry->getName();
|
|
if ($entry->isDirectory()) {
|
|
if (!is_dir($file)) {
|
|
if (!mkdir($file, 0755, true)) {
|
|
throw new ZipException("Can not create dir " . $file);
|
|
}
|
|
chmod($file, 0755);
|
|
touch($file, $entry->getTime());
|
|
}
|
|
continue;
|
|
}
|
|
$dir = dirname($file);
|
|
if (!is_dir($dir)) {
|
|
if (!mkdir($dir, 0755, true)) {
|
|
throw new ZipException("Can not create dir " . $dir);
|
|
}
|
|
chmod($dir, 0755);
|
|
touch($dir, $entry->getTime());
|
|
}
|
|
if (false === file_put_contents($file, $entry->getEntryContent())) {
|
|
throw new ZipException('Can not extract file ' . $entry->getName());
|
|
}
|
|
touch($file, $entry->getTime());
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add entry from the string.
|
|
*
|
|
* @param string $localName Zip entry name.
|
|
* @param string $contents String contents.
|
|
* @param int|null $compressionMethod Compression method.
|
|
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
|
|
* If null, then auto choosing method.
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException If incorrect data or entry name.
|
|
* @throws ZipUnsupportMethod
|
|
* @see ZipFileInterface::METHOD_STORED
|
|
* @see ZipFileInterface::METHOD_DEFLATED
|
|
* @see ZipFileInterface::METHOD_BZIP2
|
|
*/
|
|
public function addFromString($localName, $contents, $compressionMethod = null)
|
|
{
|
|
if (null === $contents) {
|
|
throw new InvalidArgumentException("Contents is null");
|
|
}
|
|
$localName = (string)$localName;
|
|
if (null === $localName || 0 === strlen($localName)) {
|
|
throw new InvalidArgumentException("Incorrect entry name " . $localName);
|
|
}
|
|
$contents = (string)$contents;
|
|
$length = strlen($contents);
|
|
if (null === $compressionMethod) {
|
|
if ($length >= 512) {
|
|
$compressionMethod = ZipEntry::UNKNOWN;
|
|
} else {
|
|
$compressionMethod = ZipFileInterface::METHOD_STORED;
|
|
}
|
|
} elseif (!in_array($compressionMethod, self::$allowCompressionMethods, true)) {
|
|
throw new ZipUnsupportMethod('Unsupported compression method ' . $compressionMethod);
|
|
}
|
|
$externalAttributes = 0100644 << 16;
|
|
|
|
$entry = new ZipNewEntry($contents);
|
|
$entry->setName($localName);
|
|
$entry->setMethod($compressionMethod);
|
|
$entry->setTime(time());
|
|
$entry->setExternalAttributes($externalAttributes);
|
|
|
|
$this->zipModel->addEntry($entry);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add entry from the file.
|
|
*
|
|
* @param string $filename Destination file.
|
|
* @param string|null $localName Zip Entry name.
|
|
* @param int|null $compressionMethod Compression method.
|
|
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
|
|
* If null, then auto choosing method.
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
* @throws ZipUnsupportMethod
|
|
* @see ZipFileInterface::METHOD_STORED
|
|
* @see ZipFileInterface::METHOD_DEFLATED
|
|
* @see ZipFileInterface::METHOD_BZIP2
|
|
*/
|
|
public function addFile($filename, $localName = null, $compressionMethod = null)
|
|
{
|
|
if (null === $filename) {
|
|
throw new InvalidArgumentException("Filename is null");
|
|
}
|
|
if (!is_file($filename)) {
|
|
throw new InvalidArgumentException("File $filename is not exists");
|
|
}
|
|
|
|
if (null === $compressionMethod) {
|
|
if (function_exists('mime_content_type')) {
|
|
$mimeType = @mime_content_type($filename);
|
|
$type = strtok($mimeType, '/');
|
|
if ('image' === $type) {
|
|
$compressionMethod = ZipFileInterface::METHOD_STORED;
|
|
} elseif ('text' === $type && filesize($filename) < 150) {
|
|
$compressionMethod = ZipFileInterface::METHOD_STORED;
|
|
} else {
|
|
$compressionMethod = ZipEntry::UNKNOWN;
|
|
}
|
|
} elseif (@filesize($filename) >= 512) {
|
|
$compressionMethod = ZipEntry::UNKNOWN;
|
|
} else {
|
|
$compressionMethod = ZipFileInterface::METHOD_STORED;
|
|
}
|
|
} elseif (!in_array($compressionMethod, self::$allowCompressionMethods, true)) {
|
|
throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod);
|
|
}
|
|
|
|
if (!($handle = @fopen($filename, 'rb'))) {
|
|
throw new InvalidArgumentException('File ' . $filename . ' can not open.');
|
|
}
|
|
if (null === $localName) {
|
|
$localName = basename($filename);
|
|
}
|
|
$this->addFromStream($handle, $localName, $compressionMethod);
|
|
$this->zipModel->getEntry($localName)->setTime(filemtime($filename));
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add entry from the stream.
|
|
*
|
|
* @param resource $stream Stream resource.
|
|
* @param string $localName Zip Entry name.
|
|
* @param int|null $compressionMethod Compression method.
|
|
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
|
|
* If null, then auto choosing method.
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
* @throws ZipUnsupportMethod
|
|
* @see ZipFileInterface::METHOD_STORED
|
|
* @see ZipFileInterface::METHOD_DEFLATED
|
|
* @see ZipFileInterface::METHOD_BZIP2
|
|
*/
|
|
public function addFromStream($stream, $localName, $compressionMethod = null)
|
|
{
|
|
if (!is_resource($stream)) {
|
|
throw new InvalidArgumentException("stream is not resource");
|
|
}
|
|
$localName = (string)$localName;
|
|
if (empty($localName)) {
|
|
throw new InvalidArgumentException("Incorrect entry name " . $localName);
|
|
}
|
|
$fstat = fstat($stream);
|
|
$length = $fstat['size'];
|
|
if (null === $compressionMethod) {
|
|
if ($length >= 512) {
|
|
$compressionMethod = ZipEntry::UNKNOWN;
|
|
} else {
|
|
$compressionMethod = ZipFileInterface::METHOD_STORED;
|
|
}
|
|
} elseif (!in_array($compressionMethod, self::$allowCompressionMethods, true)) {
|
|
throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod);
|
|
}
|
|
|
|
$mode = sprintf('%o', $fstat['mode']);
|
|
$externalAttributes = (octdec($mode) & 0xffff) << 16;
|
|
|
|
$entry = new ZipNewEntry($stream);
|
|
$entry->setName($localName);
|
|
$entry->setMethod($compressionMethod);
|
|
$entry->setTime(time());
|
|
$entry->setExternalAttributes($externalAttributes);
|
|
|
|
$this->zipModel->addEntry($entry);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add an empty directory in the zip archive.
|
|
*
|
|
* @param string $dirName
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public function addEmptyDir($dirName)
|
|
{
|
|
$dirName = (string)$dirName;
|
|
if (strlen($dirName) === 0) {
|
|
throw new InvalidArgumentException("DirName empty");
|
|
}
|
|
$dirName = rtrim($dirName, '/') . '/';
|
|
$externalAttributes = 040755 << 16;
|
|
|
|
$entry = new ZipNewEntry();
|
|
$entry->setName($dirName);
|
|
$entry->setTime(time());
|
|
$entry->setMethod(ZipFileInterface::METHOD_STORED);
|
|
$entry->setSize(0);
|
|
$entry->setCompressedSize(0);
|
|
$entry->setCrc(0);
|
|
$entry->setExternalAttributes($externalAttributes);
|
|
|
|
$this->zipModel->addEntry($entry);
|
|
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 ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
|
|
* If null, then auto choosing method.
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public function addDir($inputDir, $localPath = "/", $compressionMethod = null)
|
|
{
|
|
$inputDir = (string)$inputDir;
|
|
if (null === $inputDir || strlen($inputDir) === 0) {
|
|
throw new InvalidArgumentException('Input dir empty');
|
|
}
|
|
if (!is_dir($inputDir)) {
|
|
throw new InvalidArgumentException('Directory ' . $inputDir . ' can\'t exists');
|
|
}
|
|
$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 ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
|
|
* If null, then auto choosing method.
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
* @throws ZipUnsupportMethod
|
|
* @see ZipFileInterface::METHOD_STORED
|
|
* @see ZipFileInterface::METHOD_DEFLATED
|
|
* @see ZipFileInterface::METHOD_BZIP2
|
|
*/
|
|
public function addDirRecursive($inputDir, $localPath = "/", $compressionMethod = null)
|
|
{
|
|
$inputDir = (string)$inputDir;
|
|
if (null === $inputDir || strlen($inputDir) === 0) {
|
|
throw new InvalidArgumentException('Input dir empty');
|
|
}
|
|
if (!is_dir($inputDir)) {
|
|
throw new InvalidArgumentException('Directory ' . $inputDir . ' can\'t exists');
|
|
}
|
|
$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 ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
|
|
* If null, then auto choosing method.
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
* @throws ZipUnsupportMethod
|
|
* @see ZipFileInterface::METHOD_STORED
|
|
* @see ZipFileInterface::METHOD_DEFLATED
|
|
* @see ZipFileInterface::METHOD_BZIP2
|
|
*/
|
|
public function addFilesFromIterator(
|
|
\Iterator $iterator,
|
|
$localPath = '/',
|
|
$compressionMethod = null
|
|
) {
|
|
$localPath = (string)$localPath;
|
|
if (null !== $localPath && 0 !== strlen($localPath)) {
|
|
$localPath = rtrim($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);
|
|
foreach ($files as $file) {
|
|
$relativePath = str_replace($path, $localPath, $file);
|
|
$relativePath = ltrim($relativePath, '/');
|
|
if (is_dir($file)) {
|
|
FilesUtil::isEmptyDir($file) && $this->addEmptyDir($relativePath);
|
|
} elseif (is_file($file)) {
|
|
$this->addFile($file, $relativePath, $compressionMethod);
|
|
}
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add files from glob pattern.
|
|
*
|
|
* @param string $inputDir Input directory
|
|
* @param string $globPattern Glob pattern.
|
|
* @param string|null $localPath Add files to this directory, or the root.
|
|
* @param int|null $compressionMethod Compression method.
|
|
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
|
|
* If null, then auto choosing method.
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
* @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|null $localPath Add files to this directory, or the root.
|
|
* @param bool $recursive Recursive search.
|
|
* @param int|null $compressionMethod Compression method.
|
|
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
|
|
* If null, then auto choosing method.
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
* @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
|
|
*/
|
|
private function addGlob(
|
|
$inputDir,
|
|
$globPattern,
|
|
$localPath = '/',
|
|
$recursive = true,
|
|
$compressionMethod = null
|
|
) {
|
|
$inputDir = (string)$inputDir;
|
|
if (null === $inputDir || 0 === strlen($inputDir)) {
|
|
throw new InvalidArgumentException('Input dir empty');
|
|
}
|
|
if (!is_dir($inputDir)) {
|
|
throw new InvalidArgumentException('Directory ' . $inputDir . ' can\'t exists');
|
|
}
|
|
$globPattern = (string)$globPattern;
|
|
if (empty($globPattern)) {
|
|
throw new InvalidArgumentException("glob pattern empty");
|
|
}
|
|
|
|
$inputDir = rtrim($inputDir, '/\\') . DIRECTORY_SEPARATOR;
|
|
$globPattern = $inputDir . $globPattern;
|
|
|
|
$filesFound = FilesUtil::globFileSearch($globPattern, GLOB_BRACE, $recursive);
|
|
if (false === $filesFound || empty($filesFound)) {
|
|
return $this;
|
|
}
|
|
if (!empty($localPath) && is_string($localPath)) {
|
|
$localPath = rtrim($localPath, '/') . '/';
|
|
} else {
|
|
$localPath = "/";
|
|
}
|
|
|
|
/**
|
|
* @var string $file
|
|
*/
|
|
foreach ($filesFound as $file) {
|
|
$filename = str_replace($inputDir, $localPath, $file);
|
|
$filename = ltrim($filename, '/');
|
|
if (is_dir($file)) {
|
|
FilesUtil::isEmptyDir($file) && $this->addEmptyDir($filename);
|
|
} elseif (is_file($file)) {
|
|
$this->addFile($file, $filename, $compressionMethod);
|
|
}
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add files recursively from glob pattern.
|
|
*
|
|
* @param string $inputDir Input directory
|
|
* @param string $globPattern Glob pattern.
|
|
* @param string|null $localPath Add files to this directory, or the root.
|
|
* @param int|null $compressionMethod Compression method.
|
|
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
|
|
* If null, then auto choosing method.
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
* @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|null $localPath Add files to this directory, or the root.
|
|
* @param int|null $compressionMethod Compression method.
|
|
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
|
|
* If null, then auto choosing method.
|
|
* @return ZipFileInterface
|
|
* @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|null $localPath Add files to this directory, or the root.
|
|
* @param bool $recursive Recursive search.
|
|
* @param int|null $compressionMethod Compression method.
|
|
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
|
|
* If null, then auto choosing method.
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
private function addRegex(
|
|
$inputDir,
|
|
$regexPattern,
|
|
$localPath = "/",
|
|
$recursive = true,
|
|
$compressionMethod = null
|
|
) {
|
|
$regexPattern = (string)$regexPattern;
|
|
if (empty($regexPattern)) {
|
|
throw new InvalidArgumentException("regex pattern empty");
|
|
}
|
|
$inputDir = (string)$inputDir;
|
|
if (null === $inputDir || 0 === strlen($inputDir)) {
|
|
throw new InvalidArgumentException('Input dir empty');
|
|
}
|
|
if (!is_dir($inputDir)) {
|
|
throw new InvalidArgumentException('Directory ' . $inputDir . ' can\'t exists');
|
|
}
|
|
$inputDir = rtrim($inputDir, '/\\') . DIRECTORY_SEPARATOR;
|
|
|
|
$files = FilesUtil::regexFileSearch($inputDir, $regexPattern, $recursive);
|
|
if (false === $files || empty($files)) {
|
|
return $this;
|
|
}
|
|
if (!empty($localPath) && is_string($localPath)) {
|
|
$localPath = rtrim($localPath, '/') . '/';
|
|
} else {
|
|
$localPath = "/";
|
|
}
|
|
$inputDir = rtrim($inputDir, '/\\') . DIRECTORY_SEPARATOR;
|
|
|
|
/**
|
|
* @var string $file
|
|
*/
|
|
foreach ($files as $file) {
|
|
$filename = str_replace($inputDir, $localPath, $file);
|
|
$filename = ltrim($filename, '/');
|
|
if (is_dir($file)) {
|
|
FilesUtil::isEmptyDir($file) && $this->addEmptyDir($filename);
|
|
} elseif (is_file($file)) {
|
|
$this->addFile($file, $filename, $compressionMethod);
|
|
}
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add files recursively from regex pattern.
|
|
*
|
|
* @param string $inputDir Search files in this directory.
|
|
* @param string $regexPattern Regex pattern.
|
|
* @param string|null $localPath Add files to this directory, or the root.
|
|
* @param int|null $compressionMethod Compression method.
|
|
* Use ZipFile::METHOD_STORED, ZipFile::METHOD_DEFLATED or ZipFile::METHOD_BZIP2.
|
|
* If null, then auto choosing method.
|
|
* @return ZipFileInterface
|
|
* @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.
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
* @throws ZipNotFoundEntry
|
|
*/
|
|
public function rename($oldName, $newName)
|
|
{
|
|
if (null === $oldName || null === $newName) {
|
|
throw new InvalidArgumentException("name is null");
|
|
}
|
|
if ($oldName !== $newName) {
|
|
$this->zipModel->renameEntry($oldName, $newName);
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Delete entry by name.
|
|
*
|
|
* @param string $entryName Zip Entry name.
|
|
* @return ZipFileInterface
|
|
* @throws ZipNotFoundEntry If entry not found.
|
|
*/
|
|
public function deleteFromName($entryName)
|
|
{
|
|
$entryName = (string)$entryName;
|
|
if (!$this->zipModel->deleteEntry($entryName)) {
|
|
throw new ZipNotFoundEntry("Entry " . $entryName . ' not found!');
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Delete entries by glob pattern.
|
|
*
|
|
* @param string $globPattern Glob pattern
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
* @sse https://en.wikipedia.org/wiki/Glob_(programming) Glob pattern syntax
|
|
*/
|
|
public function deleteFromGlob($globPattern)
|
|
{
|
|
if (null === $globPattern || !is_string($globPattern) || empty($globPattern)) {
|
|
throw new InvalidArgumentException("Glob pattern is empty");
|
|
}
|
|
$globPattern = '~' . FilesUtil::convertGlobToRegEx($globPattern) . '~si';
|
|
$this->deleteFromRegex($globPattern);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Delete entries by regex pattern.
|
|
*
|
|
* @param string $regexPattern Regex pattern
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public function deleteFromRegex($regexPattern)
|
|
{
|
|
if (null === $regexPattern || !is_string($regexPattern) || empty($regexPattern)) {
|
|
throw new InvalidArgumentException("Regex pattern is empty.");
|
|
}
|
|
$this->matcher()->match($regexPattern)->delete();
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Delete all entries
|
|
* @return ZipFileInterface
|
|
*/
|
|
public function deleteAll()
|
|
{
|
|
$this->zipModel->deleteAll();
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set compression level for new entries.
|
|
*
|
|
* @param int $compressionLevel
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
* @see ZipFileInterface::LEVEL_DEFAULT_COMPRESSION
|
|
* @see ZipFileInterface::LEVEL_SUPER_FAST
|
|
* @see ZipFileInterface::LEVEL_FAST
|
|
* @see ZipFileInterface::LEVEL_BEST_COMPRESSION
|
|
*/
|
|
public function setCompressionLevel($compressionLevel = ZipFileInterface::LEVEL_DEFAULT_COMPRESSION)
|
|
{
|
|
if ($compressionLevel < ZipFileInterface::LEVEL_DEFAULT_COMPRESSION ||
|
|
$compressionLevel > ZipFileInterface::LEVEL_BEST_COMPRESSION
|
|
) {
|
|
throw new InvalidArgumentException('Invalid compression level. Minimum level ' .
|
|
ZipFileInterface::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFileInterface::LEVEL_BEST_COMPRESSION);
|
|
}
|
|
$this->matcher()->all()->invoke(function ($entry) use ($compressionLevel) {
|
|
$this->setCompressionLevelEntry($entry, $compressionLevel);
|
|
});
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param string $entryName
|
|
* @param int $compressionLevel
|
|
* @return ZipFileInterface
|
|
* @throws ZipException
|
|
* @see ZipFileInterface::LEVEL_DEFAULT_COMPRESSION
|
|
* @see ZipFileInterface::LEVEL_SUPER_FAST
|
|
* @see ZipFileInterface::LEVEL_FAST
|
|
* @see ZipFileInterface::LEVEL_BEST_COMPRESSION
|
|
*/
|
|
public function setCompressionLevelEntry($entryName, $compressionLevel)
|
|
{
|
|
if (null !== $compressionLevel) {
|
|
if ($compressionLevel < ZipFileInterface::LEVEL_DEFAULT_COMPRESSION ||
|
|
$compressionLevel > ZipFileInterface::LEVEL_BEST_COMPRESSION
|
|
) {
|
|
throw new InvalidArgumentException('Invalid compression level. Minimum level ' .
|
|
ZipFileInterface::LEVEL_DEFAULT_COMPRESSION . '. Maximum level ' . ZipFileInterface::LEVEL_BEST_COMPRESSION);
|
|
}
|
|
$entry = $this->zipModel->getEntry($entryName);
|
|
if ($entry->getCompressionLevel() !== $compressionLevel) {
|
|
$entry = $this->zipModel->getEntryForChanges($entry);
|
|
$entry->setCompressionLevel($compressionLevel);
|
|
}
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param string $entryName
|
|
* @param int $compressionMethod
|
|
* @return ZipFileInterface
|
|
* @throws ZipException
|
|
* @see ZipFileInterface::METHOD_STORED
|
|
* @see ZipFileInterface::METHOD_DEFLATED
|
|
* @see ZipFileInterface::METHOD_BZIP2
|
|
*/
|
|
public function setCompressionMethodEntry($entryName, $compressionMethod)
|
|
{
|
|
if (!in_array($compressionMethod, self::$allowCompressionMethods, true)) {
|
|
throw new ZipUnsupportMethod('Unsupported method ' . $compressionMethod);
|
|
}
|
|
$entry = $this->zipModel->getEntry($entryName);
|
|
if ($entry->getMethod() !== $compressionMethod) {
|
|
$this->zipModel
|
|
->getEntryForChanges($entry)
|
|
->setMethod($compressionMethod);
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* zipalign is optimization to Android application (APK) files.
|
|
*
|
|
* @param int|null $align
|
|
* @return ZipFileInterface
|
|
* @link https://developer.android.com/studio/command-line/zipalign.html
|
|
*/
|
|
public function setZipAlign($align = null)
|
|
{
|
|
$this->zipModel->setZipAlign($align);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set password to all input encrypted entries.
|
|
*
|
|
* @param string $password Password
|
|
* @return ZipFileInterface
|
|
* @deprecated using ZipFileInterface::setReadPassword()
|
|
*/
|
|
public function withReadPassword($password)
|
|
{
|
|
return $this->setReadPassword($password);
|
|
}
|
|
|
|
/**
|
|
* Set password to all input encrypted entries.
|
|
*
|
|
* @param string $password Password
|
|
* @return ZipFileInterface
|
|
*/
|
|
public function setReadPassword($password)
|
|
{
|
|
$this->zipModel->setReadPassword($password);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set password to concrete input entry.
|
|
*
|
|
* @param string $entryName
|
|
* @param string $password Password
|
|
* @return ZipFileInterface
|
|
*/
|
|
public function setReadPasswordEntry($entryName, $password)
|
|
{
|
|
$this->zipModel->setReadPasswordEntry($entryName, $password);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set password for all entries for update.
|
|
*
|
|
* @param string $password If password null then encryption clear
|
|
* @param int|null $encryptionMethod Encryption method
|
|
* @return ZipFileInterface
|
|
* @deprecated using ZipFileInterface::setPassword()
|
|
*/
|
|
public function withNewPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256)
|
|
{
|
|
return $this->setPassword($password, $encryptionMethod);
|
|
}
|
|
|
|
/**
|
|
* Sets a new password for all files in the archive.
|
|
*
|
|
* @param string $password
|
|
* @param int|null $encryptionMethod Encryption method
|
|
* @return ZipFileInterface
|
|
* @throws ZipException
|
|
*/
|
|
public function setPassword($password, $encryptionMethod = self::ENCRYPTION_METHOD_WINZIP_AES_256)
|
|
{
|
|
$this->zipModel->setWritePassword($password);
|
|
if (null !== $encryptionMethod) {
|
|
if (!in_array($encryptionMethod, self::$allowEncryptionMethods)) {
|
|
throw new ZipException('Invalid encryption method');
|
|
}
|
|
$this->zipModel->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
|
|
* @return ZipFileInterface
|
|
* @throws ZipException
|
|
*/
|
|
public function setPasswordEntry($entryName, $password, $encryptionMethod = null)
|
|
{
|
|
if (null !== $encryptionMethod) {
|
|
if (!in_array($encryptionMethod, self::$allowEncryptionMethods)) {
|
|
throw new ZipException('Invalid encryption method');
|
|
}
|
|
}
|
|
$this->matcher()->add($entryName)->setPassword($password, $encryptionMethod);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Remove password for all entries for update.
|
|
* @return ZipFileInterface
|
|
* @deprecated using ZipFileInterface::disableEncryption()
|
|
*/
|
|
public function withoutPassword()
|
|
{
|
|
return $this->disableEncryption();
|
|
}
|
|
|
|
/**
|
|
* Disable encryption for all entries that are already in the archive.
|
|
* @return ZipFileInterface
|
|
*/
|
|
public function disableEncryption()
|
|
{
|
|
$this->zipModel->removePassword();
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Disable encryption of an entry defined by its name.
|
|
* @param string $entryName
|
|
* @return ZipFileInterface
|
|
*/
|
|
public function disableEncryptionEntry($entryName)
|
|
{
|
|
$this->zipModel->removePasswordEntry($entryName);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Undo all changes done in the archive
|
|
* @return ZipFileInterface
|
|
*/
|
|
public function unchangeAll()
|
|
{
|
|
$this->zipModel->unchangeAll();
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Undo change archive comment
|
|
* @return ZipFileInterface
|
|
*/
|
|
public function unchangeArchiveComment()
|
|
{
|
|
$this->zipModel->unchangeArchiveComment();
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Revert all changes done to an entry with the given name.
|
|
*
|
|
* @param string|ZipEntry $entry Entry name or ZipEntry
|
|
* @return ZipFileInterface
|
|
*/
|
|
public function unchangeEntry($entry)
|
|
{
|
|
$this->zipModel->unchangeEntry($entry);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Save as file.
|
|
*
|
|
* @param string $filename Output filename
|
|
* @return ZipFileInterface
|
|
* @throws InvalidArgumentException
|
|
* @throws ZipException
|
|
*/
|
|
public function saveAsFile($filename)
|
|
{
|
|
$filename = (string)$filename;
|
|
|
|
$tempFilename = $filename . '.temp' . uniqid();
|
|
if (!($handle = @fopen($tempFilename, 'w+b'))) {
|
|
throw new InvalidArgumentException("File " . $tempFilename . ' can not open from write.');
|
|
}
|
|
$this->saveAsStream($handle);
|
|
|
|
if (!@rename($tempFilename, $filename)) {
|
|
throw new ZipException('Can not move ' . $tempFilename . ' to ' . $filename);
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Save as stream.
|
|
*
|
|
* @param resource $handle Output stream resource
|
|
* @return ZipFileInterface
|
|
* @throws ZipException
|
|
*/
|
|
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 InvalidArgumentException
|
|
*/
|
|
public function outputAsAttachment($outputFilename, $mimeType = null, $attachment = true)
|
|
{
|
|
$outputFilename = (string)$outputFilename;
|
|
|
|
if (empty($mimeType) || !is_string($mimeType) && !empty($outputFilename)) {
|
|
$ext = strtolower(pathinfo($outputFilename, PATHINFO_EXTENSION));
|
|
|
|
if (!empty($ext) && isset(self::$defaultMimeTypes[$ext])) {
|
|
$mimeType = self::$defaultMimeTypes[$ext];
|
|
}
|
|
}
|
|
if (empty($mimeType)) {
|
|
$mimeType = self::$defaultMimeTypes['zip'];
|
|
}
|
|
|
|
$content = $this->outputAsString();
|
|
$this->close();
|
|
|
|
$headerContentDisposition = 'Content-Disposition: ' . ($attachment ? 'attachment' : 'inline');
|
|
if (!empty($outputFilename)) {
|
|
$headerContentDisposition .= '; filename="' . basename($outputFilename) . '"';
|
|
}
|
|
|
|
header($headerContentDisposition);
|
|
header("Content-Type: " . $mimeType);
|
|
header("Content-Length: " . strlen($content));
|
|
exit($content);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @return ResponseInterface
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public function outputAsResponse(ResponseInterface $response, $outputFilename, $mimeType = null, $attachment = true)
|
|
{
|
|
$outputFilename = (string)$outputFilename;
|
|
|
|
if (empty($mimeType) || !is_string($mimeType) && !empty($outputFilename)) {
|
|
$ext = strtolower(pathinfo($outputFilename, PATHINFO_EXTENSION));
|
|
|
|
if (!empty($ext) && isset(self::$defaultMimeTypes[$ext])) {
|
|
$mimeType = self::$defaultMimeTypes[$ext];
|
|
}
|
|
}
|
|
if (empty($mimeType)) {
|
|
$mimeType = self::$defaultMimeTypes['zip'];
|
|
}
|
|
|
|
if (!($handle = fopen('php://memory', 'w+b'))) {
|
|
throw new InvalidArgumentException("Memory can not open from write.");
|
|
}
|
|
$this->writeZipToStream($handle);
|
|
rewind($handle);
|
|
|
|
$contentDispositionValue = ($attachment ? 'attachment' : 'inline');
|
|
if (!empty($outputFilename)) {
|
|
$contentDispositionValue .= '; filename="' . basename($outputFilename) . '"';
|
|
}
|
|
|
|
$stream = new ResponseStream($handle);
|
|
$response->withHeader('Content-Type', $mimeType);
|
|
$response->withHeader('Content-Disposition', $contentDispositionValue);
|
|
$response->withHeader('Content-Length', $stream->getSize());
|
|
$response->withBody($stream);
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* @param $handle
|
|
*/
|
|
protected function writeZipToStream($handle)
|
|
{
|
|
$output = new ZipOutputStream($handle, $this->zipModel);
|
|
$output->writeZip();
|
|
}
|
|
|
|
/**
|
|
* Returns the zip archive as a string.
|
|
* @return string
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public function outputAsString()
|
|
{
|
|
if (!($handle = fopen('php://memory', 'w+b'))) {
|
|
throw new InvalidArgumentException("Memory can not open from write.");
|
|
}
|
|
$this->writeZipToStream($handle);
|
|
rewind($handle);
|
|
$content = stream_get_contents($handle);
|
|
fclose($handle);
|
|
return $content;
|
|
}
|
|
|
|
/**
|
|
* Close zip archive and release input stream.
|
|
*/
|
|
public function close()
|
|
{
|
|
if (null !== $this->inputStream) {
|
|
$this->inputStream->close();
|
|
$this->inputStream = null;
|
|
$this->zipModel = new ZipModel();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save and reopen zip archive.
|
|
* @return ZipFileInterface
|
|
* @throws ZipException
|
|
*/
|
|
public function rewrite()
|
|
{
|
|
if (null === $this->inputStream) {
|
|
throw new ZipException('input stream is null');
|
|
}
|
|
$meta = stream_get_meta_data($this->inputStream->getStream());
|
|
$content = $this->outputAsString();
|
|
$this->close();
|
|
if ('plainfile' === $meta['wrapper_type']) {
|
|
/**
|
|
* @var resource $uri
|
|
*/
|
|
$uri = $meta['uri'];
|
|
if (file_put_contents($uri, $content) === false) {
|
|
throw new ZipException("Can not overwrite the zip file in the {$uri} file.");
|
|
}
|
|
if (!($handle = @fopen($uri, 'rb'))) {
|
|
throw new ZipException("File {$uri} can't open.");
|
|
}
|
|
return $this->openFromStream($handle);
|
|
}
|
|
return $this->openFromString($content);
|
|
}
|
|
|
|
/**
|
|
* Release all resources
|
|
*/
|
|
public function __destruct()
|
|
{
|
|
$this->close();
|
|
}
|
|
|
|
/**
|
|
* Offset to set
|
|
* @link http://php.net/manual/en/arrayaccess.offsetset.php
|
|
* @param string $entryName The offset to assign the value to.
|
|
* @param mixed $contents The value to set.
|
|
* @throws InvalidArgumentException
|
|
* @see ZipFile::addFromString
|
|
* @see ZipFile::addEmptyDir
|
|
* @see ZipFile::addFile
|
|
* @see ZipFile::addFilesFromIterator
|
|
*/
|
|
public function offsetSet($entryName, $contents)
|
|
{
|
|
if (null === $entryName) {
|
|
throw new InvalidArgumentException('entryName is null');
|
|
}
|
|
$entryName = (string)$entryName;
|
|
if (strlen($entryName) === 0) {
|
|
throw new InvalidArgumentException('entryName is empty');
|
|
}
|
|
if ($contents instanceof \SplFileInfo) {
|
|
if ($contents instanceof \DirectoryIterator) {
|
|
$this->addFilesFromIterator($contents, $entryName);
|
|
return;
|
|
}
|
|
$this->addFile($contents->getPathname(), $entryName);
|
|
return;
|
|
}
|
|
if (StringUtil::endsWith($entryName, '/')) {
|
|
$this->addEmptyDir($entryName);
|
|
} elseif (is_resource($contents)) {
|
|
$this->addFromStream($contents, $entryName);
|
|
} else {
|
|
$contents = (string)$contents;
|
|
$this->addFromString($entryName, $contents);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Offset to unset
|
|
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
|
|
* @param string $entryName The offset to unset.
|
|
* @throws ZipUnsupportMethod
|
|
*/
|
|
public function offsetUnset($entryName)
|
|
{
|
|
$this->deleteFromName($entryName);
|
|
}
|
|
|
|
/**
|
|
* Return the current element
|
|
* @link http://php.net/manual/en/iterator.current.php
|
|
* @return mixed Can return any type.
|
|
* @since 5.0.0
|
|
*/
|
|
public function current()
|
|
{
|
|
return $this->offsetGet($this->key());
|
|
}
|
|
|
|
/**
|
|
* Offset to retrieve
|
|
* @link http://php.net/manual/en/arrayaccess.offsetget.php
|
|
* @param string $entryName The offset to retrieve.
|
|
* @return string|null
|
|
* @throws ZipNotFoundEntry
|
|
*/
|
|
public function offsetGet($entryName)
|
|
{
|
|
return $this->getEntryContents($entryName);
|
|
}
|
|
|
|
/**
|
|
* Return the key of the current element
|
|
* @link 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->zipModel->getEntries());
|
|
}
|
|
|
|
/**
|
|
* Move forward to next element
|
|
* @link http://php.net/manual/en/iterator.next.php
|
|
* @return void Any returned value is ignored.
|
|
* @since 5.0.0
|
|
*/
|
|
public function next()
|
|
{
|
|
next($this->zipModel->getEntries());
|
|
}
|
|
|
|
/**
|
|
* Checks if current position is valid
|
|
* @link http://php.net/manual/en/iterator.valid.php
|
|
* @return boolean 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
|
|
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
|
|
* @param string $entryName An offset to check for.
|
|
* @return boolean 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
|
|
* @link http://php.net/manual/en/iterator.rewind.php
|
|
* @return void Any returned value is ignored.
|
|
* @since 5.0.0
|
|
*/
|
|
public function rewind()
|
|
{
|
|
reset($this->zipModel->getEntries());
|
|
}
|
|
}
|