file = $fs ? $fs : new Filesystem();
}
/**
* Destructor
*/
public function __destruct()
{
if (is_object($this->repository)) {
$this->repository->close();
}
}
/**
* Create a new zip Archive if the file does not exists
* opens a zip archive if the file exists
*
* @param $pathToFile string The file to open
* @return $this Zipper instance
* @throws Exception
*/
public function make(string $pathToFile): Zipper
{
$new = $this->createArchiveFile($pathToFile);
$this->repository = new ZipRepository($pathToFile, $new);
$this->filePath = $pathToFile;
return $this;
}
/**
* Create a new zip archive or open an existing one
*
* @param $pathToFile
*
* @return $this
* @throws Exception
*
*/
public function zip($pathToFile): Zipper
{
$this->make($pathToFile);
return $this;
}
/**
* Create a new phar file or open one
*
* @param $pathToFile
*
* @return $this
* @throws Exception
*
*/
public function phar($pathToFile): Zipper
{
$this->make($pathToFile, 'phar');
return $this;
}
/**
* Create a new rar file or open one
*
* @param $pathToFile
*
* @return $this
* @throws Exception
*
*/
public function rar($pathToFile): Zipper
{
$this->make($pathToFile, 'rar');
return $this;
}
/**
* Extracts the opened zip archive to the specified location
* you can provide an array of files and folders and define if they should be a white list
* or a black list to extract. By default this method compares file names using "string starts with" logic
*
* @param $path string The path to extract to
* @param array $files An array of files
* @param int $methodFlags The Method the files should be treated
*
* @throws Exception
*/
public function extractTo(string $path, array $files = [], int $methodFlags = self::BLACKLIST): void
{
if (! $this->file->exists($path) && ! $this->file->makeDirectory($path, 0755, true)) {
throw new RuntimeException('Failed to create folder');
}
if ($methodFlags & self::EXACT_MATCH) {
$matchingMethod = function ($haystack) use ($files) {
return in_array($haystack, $files, true);
};
} else {
$matchingMethod = function ($haystack) use ($files) {
return Str::startsWith($haystack, $files);
};
}
if ($methodFlags & self::WHITELIST) {
$this->extractFilesInternal($path, $matchingMethod);
} else {
// blacklist - extract files that do not match with $matchingMethod
$this->extractFilesInternal($path, function ($filename) use ($matchingMethod) {
return ! $matchingMethod($filename);
});
}
}
/**
* Extracts matching files/folders from the opened zip archive to the specified location.
*
* @param string $extractToPath The path to extract to
* @param string $regex regular expression used to match files. See @link http://php.net/manual/en/reference.pcre.pattern.syntax.php
*
* @throws InvalidArgumentException
* @throws RuntimeException
*/
public function extractMatchingRegex(string $extractToPath, string $regex): void
{
if (empty($regex)) {
throw new InvalidArgumentException('Missing pass valid regex parameter');
}
$this->extractFilesInternal($extractToPath, function ($filename) use ($regex) {
$match = preg_match($regex, $filename);
if ($match === 1) {
return true;
} elseif ($match === false) {
//invalid pattern for preg_match raises E_WARNING and returns FALSE
//so if you have custom error_handler set to catch and throw E_WARNINGs you never end up here
//but if you have not - this will throw exception
throw new RuntimeException("regular expression match on '$filename' failed with error. Please check if pattern is valid regular expression.");
}
return false;
});
}
/**
* Gets the content of a single file if available
*
* @param $filePath string The full path (including all folders) of the file in the zip
*
* @return string returns the content or throws an exception
* @throws Exception
*
*/
public function getFileContent(string $filePath): string
{
if ($this->repository->fileExists($filePath) === false) {
throw new Exception(sprintf('The file "%s" cannot be found', $filePath));
}
return $this->repository->getFileContent($filePath);
}
/**
* Add one or multiple files to the zip.
*
* @param $pathToAdd array|string An array or string of files and folders to add
* @param mixed|null $fileName
*
* @return $this Zipper instance
*/
public function add(array|string $pathToAdd, mixed $fileName = null): Zipper
{
if (is_array($pathToAdd)) {
foreach ($pathToAdd as $key => $dir) {
if (! is_int($key)) {
$this->add($dir, $key);
} else {
$this->add($dir);
}
}
} elseif ($this->file->isFile($pathToAdd)) {
if ($fileName) {
$this->addFile($pathToAdd, $fileName);
} else {
$this->addFile($pathToAdd);
}
} else {
$this->addDir($pathToAdd);
}
return $this;
}
/**
* Add an empty directory
*
* @param $dirName
*
* @return Zipper
*/
public function addEmptyDir($dirName): Zipper
{
$this->repository->addEmptyDir($dirName);
return $this;
}
/**
* Add a file to the zip using its contents
*
* @param $filename string The name of the file to create
* @param $content string The file contents
*
* @return $this Zipper instance
*/
public function addString(string $filename, string $content): Zipper
{
$this->addFromString($filename, $content);
return $this;
}
/**
* Gets the status of the zip.
*
* @return string The status of the internal zip file
*/
public function getStatus(): string
{
return $this->repository->getStatus();
}
/**
* Remove a file or array of files and folders from the zip archive
*
* @param $fileToRemove array|string The path/array to the files in the zip
*
* @return $this Zipper instance
*/
public function remove(array|string $fileToRemove): Zipper
{
if (is_array($fileToRemove)) {
$self = $this;
$this->repository->each(function ($file) use ($fileToRemove, $self) {
if (Str::startsWith($file, $fileToRemove)) {
$self->getRepository()->removeFile($file);
}
});
} else {
$this->repository->removeFile($fileToRemove);
}
return $this;
}
/**
* Returns the path of the current zip file if there is one.
*
* @return string The path to the file
*/
public function getFilePath(): string
{
return $this->filePath;
}
/**
* Sets the password to be used for decompressing
*
* @param $password
*
* @return bool
*/
public function usePassword($password): bool
{
return $this->repository->usePassword($password);
}
/**
* Closes the zip file and frees all handles
*/
public function close(): void
{
if (null !== $this->repository) {
$this->repository->close();
}
$this->filePath = '';
}
/**
* Sets the internal folder to the given path.
* Useful for extracting only a segment of a zip file.
*
* @param string $path
*
* @return $this
*/
public function folder(string $path): Zipper
{
$this->currentFolder = $path;
return $this;
}
/**
* Resets the internal folder to the root of the zip file.
*
* @return $this
*/
public function home(): Zipper
{
$this->currentFolder = '';
return $this;
}
/**
* Deletes the archive file
*/
public function delete(): void
{
if (null !== $this->repository) {
$this->repository->close();
}
$this->file->delete($this->filePath);
$this->filePath = '';
}
/**
* Get the type of the Archive
*
* @return string
*/
public function getArchiveType(): string
{
return get_class($this->repository);
}
/**
* Get the current internal folder pointer
*
* @return string
*/
public function getCurrentFolderPath(): string
{
return $this->currentFolder;
}
/**
* Checks if a file is present in the archive
*
* @param $fileInArchive
*
* @return bool
*/
public function contains($fileInArchive): bool
{
return $this->repository->fileExists($fileInArchive);
}
/**
* @return ZipRepository
*/
public function getRepository(): ZipRepository
{
return $this->repository;
}
/**
* @return Filesystem
*/
public function getFileHandler(): Filesystem
{
return $this->file;
}
/**
* Gets the path to the internal folder
*
* @return string
*/
public function getInternalPath(): string
{
return empty($this->currentFolder) ? '' : $this->currentFolder.'/';
}
/**
* List all files that are within the archive
*
* @param string|null $regexFilter regular expression to filter returned files/folders. See @link http://php.net/manual/en/reference.pcre.pattern.syntax.php
*
* @throws RuntimeException
*
* @return array
*/
public function listFiles(string $regexFilter = null): array
{
$filesList = [];
if ($regexFilter) {
$filter = function ($file) use (&$filesList, $regexFilter) {
// push/pop an error handler here to to make sure no error/exception thrown if $expected is not a regex
set_error_handler(function () {
});
$match = preg_match($regexFilter, $file);
restore_error_handler();
if ($match === 1) {
$filesList[] = $file;
} elseif ($match === false) {
throw new RuntimeException("regular expression match on '$file' failed with error. Please check if pattern is valid regular expression.");
}
};
} else {
$filter = function ($file) use (&$filesList) {
$filesList[] = $file;
};
}
$this->repository->each($filter);
return $filesList;
}
private function getCurrentFolderWithTrailingSlash(): string
{
if (empty($this->currentFolder)) {
return '';
}
$lastChar = mb_substr($this->currentFolder, -1);
if ($lastChar !== '/' || $lastChar !== '\\') {
return $this->currentFolder.'/';
}
return $this->currentFolder;
}
//---------------------PRIVATE FUNCTIONS-------------
/**
* @param $pathToZip
*
* @return bool
* @throws Exception
*
*/
private function createArchiveFile($pathToZip): bool
{
if (! $this->file->exists($pathToZip)) {
$dirname = dirname($pathToZip);
if (! $this->file->exists($dirname) && ! $this->file->makeDirectory($dirname, 0755, true)) {
throw new RuntimeException('Failed to create folder');
} elseif (! $this->file->isWritable($dirname)) {
throw new Exception(sprintf('The path "%s" is not writeable', $pathToZip));
}
return true;
}
return false;
}
/**
* @param $pathToDir
*/
private function addDir($pathToDir): void
{
// First go over the files in this directory and add them to the repository.
foreach ($this->file->files($pathToDir) as $file) {
$this->addFile($pathToDir.'/'.basename($file));
}
// Now let's visit the subdirectories and add them, too.
foreach ($this->file->directories($pathToDir) as $dir) {
$old_folder = $this->currentFolder;
$this->currentFolder = empty($this->currentFolder) ? basename($dir) : $this->currentFolder.'/'.basename($dir);
$this->addDir($pathToDir.'/'.basename($dir));
$this->currentFolder = $old_folder;
}
}
/**
* Add the file to the zip
*
* @param string $pathToAdd
* @param string|null $fileName
*/
private function addFile(string $pathToAdd, string $fileName = null): void
{
if (! $fileName) {
$info = pathinfo($pathToAdd);
$fileName = isset($info['extension']) ?
$info['filename'].'.'.$info['extension'] :
$info['filename'];
}
$this->repository->addFile($pathToAdd, $this->getInternalPath().$fileName);
}
/**
* Add the file to the zip from content
*
* @param $filename
* @param $content
*/
private function addFromString($filename, $content): void
{
$this->repository->addFromString($this->getInternalPath().$filename, $content);
}
private function extractFilesInternal($path, callable $matchingMethod): void
{
$self = $this;
$this->repository->each(function ($fileName) use ($path, $matchingMethod, $self) {
$currentPath = $self->getCurrentFolderWithTrailingSlash();
if (! empty($currentPath) && ! Str::startsWith($fileName, $currentPath)) {
return;
}
$filename = str_replace($self->getInternalPath(), '', $fileName);
if ($matchingMethod($filename)) {
$self->extractOneFileInternal($fileName, $path);
}
});
}
/**
* @param $fileName
* @param $path
*
* @throws RuntimeException
*/
private function extractOneFileInternal($fileName, $path): void
{
$tmpPath = str_replace($this->getInternalPath(), '', $fileName);
//Prevent Zip traversal attacks
if (str_contains($fileName, '../') || str_contains($fileName, '..\\')) {
throw new RuntimeException('Special characters found within filenames');
}
// We need to create the directory first in case it doesn't exist
$dir = pathinfo($path.DIRECTORY_SEPARATOR.$tmpPath, PATHINFO_DIRNAME);
if (! $this->file->exists($dir) && ! $this->file->makeDirectory($dir, 0755, true, true)) {
throw new RuntimeException('Failed to create folders');
}
$toPath = $path.DIRECTORY_SEPARATOR.$tmpPath;
$fileStream = $this->getRepository()->getFileStream($fileName);
$this->getFileHandler()->put($toPath, $fileStream);
}
}