YetiForceCompany/YetiForceCRM

View on GitHub
app/Zip.php

Summary

Maintainability
B
6 hrs
Test Coverage
C
79%
<?php
/**
 * A file archive, compressed with Zip.
 *
 * @package App
 *
 * @copyright YetiForce S.A.
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
 * @author    Mariusz Krzaczkowski <m.krzaczkowski@yetiforce.com>
 * @author    Radosław Skrzypczak <r.skrzypczak@yetiforce.com>
 */

namespace App;

/**
 * Zip class.
 */
class Zip extends \ZipArchive
{
    /**
     * Files extension for extract.
     *
     * @var array
     */
    protected $onlyExtensions;

    /**
     * Illegal extensions for extract.
     *
     * @var array
     */
    protected $illegalExtensions;

    /**
     * Check files before unpacking.
     *
     * @var bool
     */
    protected $checkFiles = true;

    /**
     * Open file and initialization unpack.
     *
     * @param bool  $fileName
     * @param array $options
     *
     * @throws Exceptions\AppException
     *
     * @return bool|Zip
     */
    public static function openFile($fileName = false, $options = [])
    {
        if (!$fileName) {
            throw new \App\Exceptions\AppException('No file name');
        }
        $zip = new self($fileName, $options);
        if (!file_exists($fileName) || !$zip->open($fileName)) {
            throw new \App\Exceptions\AppException('Unable to open the zip file');
        }
        if (!$zip->checkFreeSpace()) {
            throw new \App\Exceptions\AppException('The content of the zip file is too large');
        }
        foreach ($options as $key => $value) {
            $zip->{$key} = $value;
        }
        return $zip;
    }

    /**
     * Open file for create zip file.
     *
     * @param string $fileName
     *
     * @throws \App\Exceptions\AppException
     *
     * @return \App\Zip
     */
    public static function createFile($fileName)
    {
        $zip = new self();
        if (true !== $zip->open($fileName, self::CREATE | self::OVERWRITE)) {
            throw new \App\Exceptions\AppException('Unable to create the zip file');
        }
        return $zip;
    }

    /**
     * Function to extract files.
     *
     * @param      $toDir Target directory
     * @param bool $close
     *
     * @return string|string[] Unpacked files
     */
    public function unzip($toDir, bool $close = true)
    {
        if (\is_string($toDir)) {
            $toDir = [$toDir];
        }
        $files = $created = [];
        foreach ($toDir as $dir => $target) {
            for ($i = 0; $i < $this->numFiles; ++$i) {
                $zipPath = $this->getNameIndex($i);
                $path = \str_replace('\\', '/', $zipPath);
                if ((!\is_numeric($dir) && 0 !== strpos($path, $dir . '/')) || $this->validateFile($zipPath)) {
                    continue;
                }
                $files[] = $zipPath;
                $file = $target . '/' . (\is_numeric($dir) ? $path : substr($path, \strlen($dir) + 1));
                $fileDir = \dirname($file);
                if (!isset($created[$fileDir])) {
                    if (!is_dir($fileDir)) {
                        mkdir($fileDir, 0755, true);
                    }
                    $created[$fileDir] = true;
                }
                if (!$this->isDir($path)) {
                    // Read from Zip and write to disk
                    $fpr = $this->getStream($zipPath);
                    $fpw = fopen($file, 'w');
                    while ($data = fread($fpr, 1024)) {
                        fwrite($fpw, $data);
                    }
                    fclose($fpr);
                    fclose($fpw);
                }
            }
        }
        if ($close) {
            $this->close();
        }
        return $files;
    }

    /**
     * Simple extract the archive contents.
     *
     * @param string $toDir
     *
     * @throws \App\Exceptions\AppException
     *
     * @return array
     */
    public function extract(string $toDir)
    {
        if (!is_dir($toDir) && !mkdir($toDir, 0755, true) && !is_dir($toDir)) {
            throw new \App\Exceptions\AppException('Directory unable to create it');
        }
        $fileList = [];
        for ($i = 0; $i < $this->numFiles; ++$i) {
            $path = $this->getNameIndex($i);
            if ($this->validateFile(\str_replace('\\', '/', $path))) {
                continue;
            }
            $fileList[] = $path;
        }
        $this->extractTo($toDir, $fileList);
        return $fileList;
    }

    /**
     * Check illegal characters.
     *
     * @param string $path
     *
     * @return bool
     */
    public function validateFile(string $path)
    {
        if (!Validator::path($path)) {
            return true;
        }
        $validate = false;
        if ($this->checkFiles && !$this->isDir($path)) {
            $extension = pathinfo($path, PATHINFO_EXTENSION);
            if (isset($this->onlyExtensions) && !\in_array($extension, $this->onlyExtensions)) {
                $validate = true;
            }
            if (isset($this->illegalExtensions) && \in_array($extension, $this->illegalExtensions)) {
                $validate = true;
            }
            $stat = $this->statName($path);
            $fileInstance = \App\Fields\File::loadFromInfo([
                'content' => $this->getFromName($path),
                'path' => $this->getLocalPath($path),
                'name' => basename($path),
                'size' => $stat['size'],
                'validateAllCodeInjection' => true,
            ]);
            if (!$fileInstance->validate()) {
                $validate = true;
            }
        }
        return $validate;
    }

    /**
     * Check if the file path is directory.
     *
     * @param string $filePath
     *
     * @return bool
     */
    public function isDir($filePath)
    {
        if ('/' === substr($filePath, -1, 1)) {
            return true;
        }
        return false;
    }

    /**
     * Function to extract single file.
     *
     * @param string $compressedFileName
     * @param string $targetFileName
     *
     * @return bool
     */
    public function unzipFile($compressedFileName, $targetFileName)
    {
        return copy($this->getLocalPath($compressedFileName), $targetFileName);
    }

    /**
     * Get compressed file path.
     *
     * @param string $compressedFileName
     *
     * @return string
     */
    public function getLocalPath($compressedFileName)
    {
        return "zip://{$this->filename}#{$compressedFileName}";
    }

    /**
     * Check free disk space.
     *
     * @return bool
     */
    public function checkFreeSpace()
    {
        $df = disk_free_space(ROOT_DIRECTORY . \DIRECTORY_SEPARATOR);
        $size = 0;
        for ($i = 0; $i < $this->numFiles; ++$i) {
            $stat = $this->statIndex($i);
            $size += $stat['size'];
        }
        return $df > $size;
    }

    /**
     * Copy the directory on the disk into zip file.
     *
     * @param string $dir
     * @param string $localName
     * @param bool   $relativePath
     */
    public function addDirectory(string $dir, string $localName = '', bool $relativePath = false)
    {
        if ($localName) {
            $localName .= '/';
        }
        $path = realpath($dir);
        $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::LEAVES_ONLY);
        $pathToTrim = $relativePath ? $path : ROOT_DIRECTORY;
        foreach ($files as $file) {
            if (!$file->isDir()) {
                $filePath = $file->getRealPath();
                $zipPath = str_replace(\DIRECTORY_SEPARATOR, '/', Fields\File::getLocalPath($filePath, $pathToTrim));
                $this->addFile($filePath, $localName . $zipPath);
            }
        }
    }

    /**
     * Push out the file content for download.
     *
     * @param string $name
     */
    public function download(string $name)
    {
        $fileName = $this->filename;
        $this->close();
        header('cache-control: private, max-age=120, must-revalidate');
        header('pragma: no-cache');
        header('expires: 0');
        header('content-type: application/zip');
        header('content-disposition: attachment; filename="' . $name . '.zip";');
        header('accept-ranges: bytes');
        header('content-length: ' . filesize($fileName));
        readfile($fileName);
        unlink($fileName);
    }
}