mirror of
https://github.com/Ne-Lexa/php-zip.git
synced 2025-08-03 14:07:23 +02:00
Added a new option for extracting unix symlinks.
Added new parameter to get the list of extracted files.
This commit is contained in:
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
namespace PhpZip\Constants;
|
namespace PhpZip\Constants;
|
||||||
|
|
||||||
|
use PhpZip\IO\ZipReader;
|
||||||
|
use PhpZip\ZipFile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface ZipOptions.
|
* Interface ZipOptions.
|
||||||
*/
|
*/
|
||||||
@@ -10,20 +13,50 @@ interface ZipOptions
|
|||||||
/**
|
/**
|
||||||
* Boolean option for store just file names (skip directory names).
|
* Boolean option for store just file names (skip directory names).
|
||||||
*
|
*
|
||||||
* @var string
|
* @see ZipFile::addFromFinder()
|
||||||
*/
|
*/
|
||||||
const STORE_ONLY_FILES = 'only_files';
|
const STORE_ONLY_FILES = 'only_files';
|
||||||
|
|
||||||
/** @var string */
|
/**
|
||||||
|
* Uses the specified compression method.
|
||||||
|
*
|
||||||
|
* @see ZipFile::addFromFinder()
|
||||||
|
* @see ZipFile::addSplFile()
|
||||||
|
*/
|
||||||
const COMPRESSION_METHOD = 'compression_method';
|
const COMPRESSION_METHOD = 'compression_method';
|
||||||
|
|
||||||
/** @var string */
|
/**
|
||||||
|
* Set the specified record modification time.
|
||||||
|
* The value can be {@see \DateTimeInterface}, integer timestamp
|
||||||
|
* or a string of any format.
|
||||||
|
*
|
||||||
|
* @see ZipFile::addFromFinder()
|
||||||
|
* @see ZipFile::addSplFile()
|
||||||
|
*/
|
||||||
const MODIFIED_TIME = 'mtime';
|
const MODIFIED_TIME = 'mtime';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* Specifies the encoding of the record name for cases when the UTF-8
|
||||||
|
* usage flag is not set.
|
||||||
*
|
*
|
||||||
|
* The most commonly used encodings are compiled into the constants
|
||||||
|
* of the {@see DosCodePage} class.
|
||||||
|
*
|
||||||
|
* @see ZipFile::openFile()
|
||||||
|
* @see ZipFile::openFromString()
|
||||||
|
* @see ZipFile::openFromStream()
|
||||||
|
* @see ZipReader::getDefaultOptions()
|
||||||
* @see DosCodePage::getCodePages()
|
* @see DosCodePage::getCodePages()
|
||||||
*/
|
*/
|
||||||
const CHARSET = 'charset';
|
const CHARSET = 'charset';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows ({@see true}) or denies ({@see false}) unpacking unix symlinks.
|
||||||
|
*
|
||||||
|
* This is a potentially dangerous operation for uncontrolled zip files.
|
||||||
|
* By default is ({@see false}).
|
||||||
|
*
|
||||||
|
* @see https://josipfranjkovic.blogspot.com/2014/12/reading-local-files-from-facebooks.html
|
||||||
|
*/
|
||||||
|
const EXTRACT_SYMLINKS = 'extract_symlinks';
|
||||||
}
|
}
|
||||||
|
@@ -43,9 +43,10 @@ final class FilesUtil
|
|||||||
\RecursiveIteratorIterator::CHILD_FIRST
|
\RecursiveIteratorIterator::CHILD_FIRST
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** @var \SplFileInfo $fileInfo */
|
||||||
foreach ($files as $fileInfo) {
|
foreach ($files as $fileInfo) {
|
||||||
$function = ($fileInfo->isDir() ? 'rmdir' : 'unlink');
|
$function = ($fileInfo->isDir() ? 'rmdir' : 'unlink');
|
||||||
$function($fileInfo->getRealPath());
|
$function($fileInfo->getPathname());
|
||||||
}
|
}
|
||||||
rmdir($dir);
|
rmdir($dir);
|
||||||
}
|
}
|
||||||
@@ -303,36 +304,19 @@ final class FilesUtil
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $linkPath
|
|
||||||
* @param string $target
|
* @param string $target
|
||||||
|
* @param string $path
|
||||||
|
* @param bool $allowSymlink
|
||||||
*
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function symlink($target, $linkPath)
|
public static function symlink($target, $path, $allowSymlink)
|
||||||
{
|
{
|
||||||
if (\DIRECTORY_SEPARATOR === '\\') {
|
if (\DIRECTORY_SEPARATOR === '\\' || !$allowSymlink) {
|
||||||
$linkPath = str_replace('/', '\\', $linkPath);
|
return file_put_contents($path, $target) !== false;
|
||||||
$target = str_replace('/', '\\', $target);
|
|
||||||
$abs = null;
|
|
||||||
|
|
||||||
if (!self::isAbsolutePath($target)) {
|
|
||||||
$abs = realpath(\dirname($linkPath) . \DIRECTORY_SEPARATOR . $target);
|
|
||||||
|
|
||||||
if (\is_string($abs)) {
|
|
||||||
$target = $abs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!symlink($target, $linkPath)) {
|
return symlink($target, $path);
|
||||||
if (\DIRECTORY_SEPARATOR === '\\' && is_file($target)) {
|
|
||||||
return copy($target, $linkPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
namespace PhpZip;
|
namespace PhpZip;
|
||||||
|
|
||||||
|
use PhpZip\Constants\UnixStat;
|
||||||
use PhpZip\Constants\ZipCompressionLevel;
|
use PhpZip\Constants\ZipCompressionLevel;
|
||||||
use PhpZip\Constants\ZipCompressionMethod;
|
use PhpZip\Constants\ZipCompressionMethod;
|
||||||
use PhpZip\Constants\ZipEncryptionMethod;
|
use PhpZip\Constants\ZipEncryptionMethod;
|
||||||
@@ -375,14 +376,19 @@ class ZipFile implements ZipFileInterface
|
|||||||
* Extract the complete archive or the given files to the specified destination.
|
* Extract the complete archive or the given files to the specified destination.
|
||||||
*
|
*
|
||||||
* @param string $destDir location where to extract the files
|
* @param string $destDir location where to extract the files
|
||||||
* @param array|string|null $entries The entries to extract. It accepts either
|
* @param array|string|null $entries entries to extract
|
||||||
* a single entry name or an array of names.
|
* @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
|
* @throws ZipException
|
||||||
*
|
*
|
||||||
* @return ZipFile
|
* @return ZipFile
|
||||||
*/
|
*/
|
||||||
public function extractTo($destDir, $entries = null)
|
public function extractTo($destDir, $entries = null, array $options = [], &$extractedEntries = [])
|
||||||
{
|
{
|
||||||
if (!file_exists($destDir)) {
|
if (!file_exists($destDir)) {
|
||||||
throw new ZipException(sprintf('Destination %s not found', $destDir));
|
throw new ZipException(sprintf('Destination %s not found', $destDir));
|
||||||
@@ -396,7 +402,14 @@ class ZipFile implements ZipFileInterface
|
|||||||
throw new ZipException('Destination is not writable directory');
|
throw new ZipException('Destination is not writable directory');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($extractedEntries === null) {
|
||||||
$extractedEntries = [];
|
$extractedEntries = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultOptions = [
|
||||||
|
ZipOptions::EXTRACT_SYMLINKS => false,
|
||||||
|
];
|
||||||
|
$options += $defaultOptions;
|
||||||
|
|
||||||
$zipEntries = $this->zipContainer->getEntries();
|
$zipEntries = $this->zipContainer->getEntries();
|
||||||
|
|
||||||
@@ -497,9 +510,8 @@ class ZipFile implements ZipFileInterface
|
|||||||
unlink($file);
|
unlink($file);
|
||||||
|
|
||||||
throw $e;
|
throw $e;
|
||||||
} finally {
|
|
||||||
fclose($handle);
|
|
||||||
}
|
}
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
if ($unixMode === 0) {
|
if ($unixMode === 0) {
|
||||||
$unixMode = 0644;
|
$unixMode = 0644;
|
||||||
@@ -514,8 +526,10 @@ class ZipFile implements ZipFileInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$allowSymlink = (bool) $options[ZipOptions::EXTRACT_SYMLINKS];
|
||||||
|
|
||||||
foreach ($symlinks as $linkPath => $target) {
|
foreach ($symlinks as $linkPath => $target) {
|
||||||
if (!FilesUtil::symlink($target, $linkPath)) {
|
if (!FilesUtil::symlink($target, $linkPath, $allowSymlink)) {
|
||||||
unset($extractedEntries[$linkPath]);
|
unset($extractedEntries[$linkPath]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -526,7 +540,7 @@ class ZipFile implements ZipFileInterface
|
|||||||
touch($dir, $lastMod);
|
touch($dir, $lastMod);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ksort($extractedEntries);
|
ksort($extractedEntries);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@@ -663,9 +677,24 @@ class ZipFile implements ZipFileInterface
|
|||||||
$entryName = $file->isDir() ? rtrim($entryName, '/\\') . '/' : $entryName;
|
$entryName = $file->isDir() ? rtrim($entryName, '/\\') . '/' : $entryName;
|
||||||
|
|
||||||
$zipEntry = new ZipEntry($entryName);
|
$zipEntry = new ZipEntry($entryName);
|
||||||
$zipData = null;
|
$zipEntry->setCreatedOS(ZipPlatform::OS_UNIX);
|
||||||
|
$zipEntry->setExtractedOS(ZipPlatform::OS_UNIX);
|
||||||
|
|
||||||
if ($file->isFile()) {
|
$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])) {
|
if (isset($options[ZipOptions::COMPRESSION_METHOD])) {
|
||||||
$compressionMethod = $options[ZipOptions::COMPRESSION_METHOD];
|
$compressionMethod = $options[ZipOptions::COMPRESSION_METHOD];
|
||||||
} elseif ($file->getSize() < 512) {
|
} elseif ($file->getSize() < 512) {
|
||||||
@@ -685,21 +714,9 @@ class ZipFile implements ZipFileInterface
|
|||||||
$zipEntry->setUncompressedSize(0);
|
$zipEntry->setUncompressedSize(0);
|
||||||
$zipEntry->setCompressedSize(0);
|
$zipEntry->setCompressedSize(0);
|
||||||
$zipEntry->setCrc(0);
|
$zipEntry->setCrc(0);
|
||||||
} elseif ($file->isLink()) {
|
|
||||||
$linkTarget = $file->getLinkTarget();
|
|
||||||
$lengthLinkTarget = \strlen($linkTarget);
|
|
||||||
|
|
||||||
$zipEntry->setCompressionMethod(ZipCompressionMethod::STORED);
|
|
||||||
$zipEntry->setUncompressedSize($lengthLinkTarget);
|
|
||||||
$zipEntry->setCompressedSize($lengthLinkTarget);
|
|
||||||
$zipEntry->setCrc(crc32($linkTarget));
|
|
||||||
|
|
||||||
$zipData = new ZipNewData($zipEntry, $linkTarget);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$zipEntry->setCreatedOS(ZipPlatform::OS_UNIX);
|
$zipEntry->setUnixMode($filePerms);
|
||||||
$zipEntry->setExtractedOS(ZipPlatform::OS_UNIX);
|
|
||||||
$zipEntry->setUnixMode($file->getPerms());
|
|
||||||
|
|
||||||
$timestamp = null;
|
$timestamp = null;
|
||||||
|
|
||||||
|
@@ -293,14 +293,19 @@ interface ZipFileInterface extends \Countable, \ArrayAccess, \Iterator
|
|||||||
* Extract the complete archive or the given files to the specified destination.
|
* Extract the complete archive or the given files to the specified destination.
|
||||||
*
|
*
|
||||||
* @param string $destDir location where to extract the files
|
* @param string $destDir location where to extract the files
|
||||||
* @param array|string|null $entries The entries to extract. It accepts either
|
* @param array|string|null $entries entries to extract
|
||||||
* a single entry name or an array of names.
|
* @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
|
* @throws ZipException
|
||||||
*
|
*
|
||||||
* @return ZipFile
|
* @return ZipFile
|
||||||
*/
|
*/
|
||||||
public function extractTo($destDir, $entries = null);
|
public function extractTo($destDir, $entries = null, array $options = [], &$extractedEntries = []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add entry from the string.
|
* Add entry from the string.
|
||||||
|
88
tests/SymlinkTest.php
Normal file
88
tests/SymlinkTest.php
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpZip\Tests;
|
||||||
|
|
||||||
|
use PhpZip\Constants\ZipOptions;
|
||||||
|
use PhpZip\Util\FilesUtil;
|
||||||
|
use PhpZip\ZipFile;
|
||||||
|
use Symfony\Component\Finder\Finder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @small
|
||||||
|
*/
|
||||||
|
final class SymlinkTest extends ZipFileTest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* This method is called before the first test of this test class is run.
|
||||||
|
*/
|
||||||
|
public static function setUpBeforeClass()
|
||||||
|
{
|
||||||
|
parent::setUpBeforeClass();
|
||||||
|
|
||||||
|
if (\DIRECTORY_SEPARATOR === '\\') {
|
||||||
|
self::markTestSkipped('only linux test');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideAllowSymlink
|
||||||
|
*
|
||||||
|
* @param bool $allowSymlink
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function testSymlink($allowSymlink)
|
||||||
|
{
|
||||||
|
if (!is_dir($this->outputDirname)) {
|
||||||
|
self::assertTrue(mkdir($this->outputDirname, 0755, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
$contentsFile = random_bytes(100);
|
||||||
|
$filePath = $this->outputDirname . '/file.bin';
|
||||||
|
$symlinkPath = $this->outputDirname . '/symlink.bin';
|
||||||
|
$symlinkTarget = basename($filePath);
|
||||||
|
self::assertNotFalse(file_put_contents($filePath, $contentsFile));
|
||||||
|
self::assertTrue(symlink($symlinkTarget, $symlinkPath));
|
||||||
|
|
||||||
|
$finder = (new Finder())->in($this->outputDirname);
|
||||||
|
$zipFile = new ZipFile();
|
||||||
|
$zipFile->addFromFinder($finder);
|
||||||
|
$zipFile->saveAsFile($this->outputFilename);
|
||||||
|
$zipFile->close();
|
||||||
|
|
||||||
|
self::assertCorrectZipArchive($this->outputFilename);
|
||||||
|
|
||||||
|
FilesUtil::removeDir($this->outputDirname);
|
||||||
|
self::assertFalse(is_dir($this->outputDirname));
|
||||||
|
self::assertTrue(mkdir($this->outputDirname, 0755, true));
|
||||||
|
|
||||||
|
$zipFile->openFile($this->outputFilename);
|
||||||
|
$zipFile->extractTo($this->outputDirname, null, [
|
||||||
|
ZipOptions::EXTRACT_SYMLINKS => $allowSymlink,
|
||||||
|
]);
|
||||||
|
$zipFile->close();
|
||||||
|
|
||||||
|
$splFileInfo = new \SplFileInfo($symlinkPath);
|
||||||
|
|
||||||
|
if ($allowSymlink) {
|
||||||
|
self::assertTrue($splFileInfo->isLink());
|
||||||
|
self::assertSame($splFileInfo->getLinkTarget(), $symlinkTarget);
|
||||||
|
} else {
|
||||||
|
self::assertFalse($splFileInfo->isLink());
|
||||||
|
self::assertStringEqualsFile($symlinkPath, $symlinkTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Generator
|
||||||
|
*/
|
||||||
|
public function provideAllowSymlink()
|
||||||
|
{
|
||||||
|
yield 'allow' => [true];
|
||||||
|
yield 'deny' => [false];
|
||||||
|
}
|
||||||
|
}
|
@@ -1009,16 +1009,16 @@ class ZipFileTest extends ZipTestCase
|
|||||||
'test1.txt' => random_bytes(255),
|
'test1.txt' => random_bytes(255),
|
||||||
'test2.txt' => random_bytes(255),
|
'test2.txt' => random_bytes(255),
|
||||||
'test/test 2/test3.txt' => random_bytes(255),
|
'test/test 2/test3.txt' => random_bytes(255),
|
||||||
'test empty/dir' => null,
|
'test empty/dir/' => null,
|
||||||
];
|
];
|
||||||
|
|
||||||
$zipFile = new ZipFile();
|
$zipFile = new ZipFile();
|
||||||
|
|
||||||
foreach ($entries as $entryName => $value) {
|
foreach ($entries as $entryName => $contents) {
|
||||||
if ($value === null) {
|
if ($contents === null) {
|
||||||
$zipFile->addEmptyDir($entryName);
|
$zipFile->addEmptyDir($entryName);
|
||||||
} else {
|
} else {
|
||||||
$zipFile->addFromString($entryName, $value);
|
$zipFile->addFromString($entryName, $contents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$zipFile->saveAsFile($this->outputFilename);
|
$zipFile->saveAsFile($this->outputFilename);
|
||||||
@@ -1027,19 +1027,28 @@ class ZipFileTest extends ZipTestCase
|
|||||||
static::assertTrue(mkdir($this->outputDirname, 0755, true));
|
static::assertTrue(mkdir($this->outputDirname, 0755, true));
|
||||||
|
|
||||||
$zipFile->openFile($this->outputFilename);
|
$zipFile->openFile($this->outputFilename);
|
||||||
$zipFile->extractTo($this->outputDirname);
|
$zipFile->extractTo($this->outputDirname, null, [], $extractedEntries);
|
||||||
|
|
||||||
foreach ($entries as $entryName => $value) {
|
foreach ($entries as $entryName => $contents) {
|
||||||
$fullExtractedFilename = $this->outputDirname . \DIRECTORY_SEPARATOR . $entryName;
|
$fullExtractedFilename = $this->outputDirname . \DIRECTORY_SEPARATOR . $entryName;
|
||||||
|
|
||||||
if ($value === null) {
|
static::assertTrue(
|
||||||
|
isset($extractedEntries[$fullExtractedFilename]),
|
||||||
|
'No extract info for ' . $fullExtractedFilename
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($contents === null) {
|
||||||
static::assertTrue(is_dir($fullExtractedFilename));
|
static::assertTrue(is_dir($fullExtractedFilename));
|
||||||
static::assertTrue(FilesUtil::isEmptyDir($fullExtractedFilename));
|
static::assertTrue(FilesUtil::isEmptyDir($fullExtractedFilename));
|
||||||
} else {
|
} else {
|
||||||
static::assertTrue(is_file($fullExtractedFilename));
|
static::assertTrue(is_file($fullExtractedFilename));
|
||||||
$contents = file_get_contents($fullExtractedFilename);
|
$contents = file_get_contents($fullExtractedFilename);
|
||||||
static::assertSame($contents, $value);
|
static::assertSame($contents, $contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @var ZipEntry $entry */
|
||||||
|
$entry = $extractedEntries[$fullExtractedFilename];
|
||||||
|
static::assertSame($entry->getName(), $entryName);
|
||||||
}
|
}
|
||||||
$zipFile->close();
|
$zipFile->close();
|
||||||
}
|
}
|
||||||
@@ -2431,7 +2440,7 @@ class ZipFileTest extends ZipTestCase
|
|||||||
$zipFile->saveAsFile($this->outputFilename);
|
$zipFile->saveAsFile($this->outputFilename);
|
||||||
$zipAfterBeforeWrite = $zipFile->getEntry('file 1');
|
$zipAfterBeforeWrite = $zipFile->getEntry('file 1');
|
||||||
|
|
||||||
static::assertEquals($zipAfterBeforeWrite, $zipEntryBeforeWrite);
|
static::assertSame($zipAfterBeforeWrite, $zipEntryBeforeWrite);
|
||||||
|
|
||||||
$zipFile->close();
|
$zipFile->close();
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user