Procházet zdrojové kódy

Added a new option for extracting unix symlinks.

Added new parameter to get the list of extracted files.
Ne-Lexa před 6 roky
rodič
revize
8dcde47072

+ 37 - 4
src/Constants/ZipOptions.php

@@ -2,6 +2,9 @@
 
 namespace PhpZip\Constants;
 
+use PhpZip\IO\ZipReader;
+use PhpZip\ZipFile;
+
 /**
  * Interface ZipOptions.
  */
@@ -10,20 +13,50 @@ interface ZipOptions
     /**
      * Boolean option for store just file names (skip directory names).
      *
-     * @var string
+     * @see ZipFile::addFromFinder()
      */
     const STORE_ONLY_FILES = 'only_files';
 
-    /** @var string */
+    /**
+     * Uses the specified compression method.
+     *
+     * @see ZipFile::addFromFinder()
+     * @see ZipFile::addSplFile()
+     */
     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';
 
     /**
-     * @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()
      */
     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';
 }

+ 8 - 24
src/Util/FilesUtil.php

@@ -43,9 +43,10 @@ final class FilesUtil
             \RecursiveIteratorIterator::CHILD_FIRST
         );
 
+        /** @var \SplFileInfo $fileInfo */
         foreach ($files as $fileInfo) {
             $function = ($fileInfo->isDir() ? 'rmdir' : 'unlink');
-            $function($fileInfo->getRealPath());
+            $function($fileInfo->getPathname());
         }
         rmdir($dir);
     }
@@ -303,36 +304,19 @@ final class FilesUtil
     }
 
     /**
-     * @param string $linkPath
      * @param string $target
+     * @param string $path
+     * @param bool   $allowSymlink
      *
      * @return bool
      */
-    public static function symlink($target, $linkPath)
+    public static function symlink($target, $path, $allowSymlink)
     {
-        if (\DIRECTORY_SEPARATOR === '\\') {
-            $linkPath = str_replace('/', '\\', $linkPath);
-            $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)) {
-            if (\DIRECTORY_SEPARATOR === '\\' && is_file($target)) {
-                return copy($target, $linkPath);
-            }
-
-            return false;
+        if (\DIRECTORY_SEPARATOR === '\\' || !$allowSymlink) {
+            return file_put_contents($path, $target) !== false;
         }
 
-        return true;
+        return symlink($target, $path);
     }
 
     /**

+ 40 - 23
src/ZipFile.php

@@ -6,6 +6,7 @@
 
 namespace PhpZip;
 
+use PhpZip\Constants\UnixStat;
 use PhpZip\Constants\ZipCompressionLevel;
 use PhpZip\Constants\ZipCompressionMethod;
 use PhpZip\Constants\ZipEncryptionMethod;
@@ -374,15 +375,20 @@ class ZipFile implements ZipFileInterface
      *
      * 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 The entries to extract. It accepts either
-     *                                   a single entry name or an array of names.
+     * @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)
+    public function extractTo($destDir, $entries = null, array $options = [], &$extractedEntries = [])
     {
         if (!file_exists($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');
         }
 
-        $extractedEntries = [];
+        if ($extractedEntries === null) {
+            $extractedEntries = [];
+        }
+
+        $defaultOptions = [
+            ZipOptions::EXTRACT_SYMLINKS => false,
+        ];
+        $options += $defaultOptions;
 
         $zipEntries = $this->zipContainer->getEntries();
 
@@ -497,9 +510,8 @@ class ZipFile implements ZipFileInterface
                 unlink($file);
 
                 throw $e;
-            } finally {
-                fclose($handle);
             }
+            fclose($handle);
 
             if ($unixMode === 0) {
                 $unixMode = 0644;
@@ -514,8 +526,10 @@ class ZipFile implements ZipFileInterface
             }
         }
 
+        $allowSymlink = (bool) $options[ZipOptions::EXTRACT_SYMLINKS];
+
         foreach ($symlinks as $linkPath => $target) {
-            if (!FilesUtil::symlink($target, $linkPath)) {
+            if (!FilesUtil::symlink($target, $linkPath, $allowSymlink)) {
                 unset($extractedEntries[$linkPath]);
             }
         }
@@ -526,7 +540,7 @@ class ZipFile implements ZipFileInterface
             touch($dir, $lastMod);
         }
 
-//        ksort($extractedEntries);
+        ksort($extractedEntries);
 
         return $this;
     }
@@ -663,9 +677,24 @@ class ZipFile implements ZipFileInterface
         $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;
 
-        if ($file->isFile()) {
+            $zipData = new ZipNewData($zipEntry, $linkTarget);
+        } elseif ($file->isFile()) {
             if (isset($options[ZipOptions::COMPRESSION_METHOD])) {
                 $compressionMethod = $options[ZipOptions::COMPRESSION_METHOD];
             } elseif ($file->getSize() < 512) {
@@ -685,21 +714,9 @@ class ZipFile implements ZipFileInterface
             $zipEntry->setUncompressedSize(0);
             $zipEntry->setCompressedSize(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->setExtractedOS(ZipPlatform::OS_UNIX);
-        $zipEntry->setUnixMode($file->getPerms());
+        $zipEntry->setUnixMode($filePerms);
 
         $timestamp = null;
 

+ 9 - 4
src/ZipFileInterface.php

@@ -292,15 +292,20 @@ interface ZipFileInterface extends \Countable, \ArrayAccess, \Iterator
      *
      * 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 The entries to extract. It accepts either
-     *                                   a single entry name or an array of names.
+     * @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);
+    public function extractTo($destDir, $entries = null, array $options = [], &$extractedEntries = []);
 
     /**
      * Add entry from the string.

+ 88 - 0
tests/SymlinkTest.php

@@ -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];
+    }
+}

+ 18 - 9
tests/ZipFileTest.php

@@ -1009,16 +1009,16 @@ class ZipFileTest extends ZipTestCase
             'test1.txt' => random_bytes(255),
             'test2.txt' => random_bytes(255),
             'test/test 2/test3.txt' => random_bytes(255),
-            'test empty/dir' => null,
+            'test empty/dir/' => null,
         ];
 
         $zipFile = new ZipFile();
 
-        foreach ($entries as $entryName => $value) {
-            if ($value === null) {
+        foreach ($entries as $entryName => $contents) {
+            if ($contents === null) {
                 $zipFile->addEmptyDir($entryName);
             } else {
-                $zipFile->addFromString($entryName, $value);
+                $zipFile->addFromString($entryName, $contents);
             }
         }
         $zipFile->saveAsFile($this->outputFilename);
@@ -1027,19 +1027,28 @@ class ZipFileTest extends ZipTestCase
         static::assertTrue(mkdir($this->outputDirname, 0755, true));
 
         $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;
 
-            if ($value === null) {
+            static::assertTrue(
+                isset($extractedEntries[$fullExtractedFilename]),
+                'No extract info for ' . $fullExtractedFilename
+            );
+
+            if ($contents === null) {
                 static::assertTrue(is_dir($fullExtractedFilename));
                 static::assertTrue(FilesUtil::isEmptyDir($fullExtractedFilename));
             } else {
                 static::assertTrue(is_file($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();
     }
@@ -2431,7 +2440,7 @@ class ZipFileTest extends ZipTestCase
         $zipFile->saveAsFile($this->outputFilename);
         $zipAfterBeforeWrite = $zipFile->getEntry('file 1');
 
-        static::assertEquals($zipAfterBeforeWrite, $zipEntryBeforeWrite);
+        static::assertSame($zipAfterBeforeWrite, $zipEntryBeforeWrite);
 
         $zipFile->close();
     }