<?php
/**
 * appRain CMF
 *
 * LICENSE
 *
 * This source file is subject to the MIT license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://www.opensource.org/licenses/mit-license.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@apprain.com so we can send you a copy immediately.
 *
 * @copyright  Copyright (c) 2010 appRain, Team. (http://www.apprain.com)
 * @license    http://www.opensource.org/licenses/mit-license.php MIT license
 *
 * HELP
 *
 * Official Website
 * http://www.apprain.com/
 *
 * Download Link
 * http://www.apprain.com/download
 *
 * Documents Link
 * http ://www.apprain.com/docs
 */

/**
 * Class Orginally Developed by : Alexandre Tedeschi (d), alexandrebr@gmail.com
 *
 * Updated by : Reazaul Karim, info@apprain.com
 */

/**
 *    1. Create ZIP by adding file manually
 *        $zip  = App::Module('Zip')
->setFileName('/var/www/works/other/wikipedia.zip')
->addDir('wikipedia')
->addFile('/var/www/works/other/wikipedia.php',  'wikipedia/wikipedia.php')
->save(true);
 *
 * 2. Compress a full directory
$zip  = App::Module('Zip')
->setFileName('racipe.zip')
->Compress('/var/www/works/other/racipe',true);
 *
 * 3. Unzip a full Zip file
$zip  = App::Module('Zip')
->setFileName('test.zip')
->Extract('/var/www');*/
class appRain_Base_Modules_Zip extends appRain_Base_Objects
{
    public $lastError;
    public $compressedList;
    public $centralDirList;
    public $endOfCentral = Array();

    private $fh;
    // Signatures
    private $zipSignature = "\x50\x4b\x03\x04";
    private $dirSignature = "\x50\x4b\x01\x02";
    private $dirSignatureE = "\x50\x4b\x05\x06";

    var $overwrite = true;
    var $files_count = 0;

    public function __construct()
    {
    }

    /* DO UNZIP SECTION
       =========================================================================*/

    public function extract($path = "")
    {
        $this->getList();
        $this->unzipAll($path);
        $this->close();
        return $this;
    }

    private function archiveFileName()
    {
        if (!$this->getFileName()) {
            die('UpZip file name not found');
        }
        return $this->getFileName();
    }

    private function getList($stopOnFile = false)
    {
        if (sizeof($this->compressedList)) {
            return $this->compressedList;
        }

        // Open file, and set file handler
        $fh = fopen($this->archiveFileName(), "r");
        $this->fh = &$fh;
        if (!$fh) {
            return false;
        }

        if (!$this->_loadFileListByEOF($fh, $stopOnFile)) {
            if (!$this->_loadFileListBySignatures($fh, $stopOnFile)) {
                return false;
            }
        }

        return $this->compressedList;
    }

    private function getExtraInfo($compressedFileName)
    {
        return
            isset($this->centralDirList[$compressedFileName]) ?
                $this->centralDirList[$compressedFileName] :
                false;
    }

    private function getZipInfo($detail = false)
    {
        return $detail ?
            $this->endOfCentral[$detail] :
            $this->endOfCentral;
    }

    private function unzip($compressedFileName, $targetFileName = false, $applyChmod = 0777)
    {
        if (!sizeof($this->compressedList)) {
            $this->getList(false, $compressedFileName);
        }

        $fdetails = &$this->compressedList[$compressedFileName];
        if (!isset($this->compressedList[$compressedFileName])) {
            return false;
        }
        if (substr($compressedFileName, -1) == "/") {
            return false;
        }
        if (!$fdetails['uncompressed_size']) {
            return $targetFileName ?
                file_put_contents($targetFileName, "") :
                "";
        }

        fseek($this->fh, $fdetails['contents-startOffset']);
        $toUncompress = fread($this->fh, $fdetails['compressed_size']);
        $ret = $this->uncompress(
            $toUncompress,
            $fdetails['compression_method'],
            $fdetails['uncompressed_size'],
            $targetFileName
        );
        unset($toUncompress);
        if ($applyChmod && $targetFileName)
            chmod($targetFileName, 0777);

        return $ret;
    }

    private function unzipAll($targetDir = false, $baseDir = "", $maintainStructure = true, $applyChmod = 0777)
    {
        if ($targetDir === false) {
            $targetDir = dirname($_SERVER['SCRIPT_FILENAME']) . "/";
        }
        if (substr($targetDir, -1) != "/") {
            $targetDir .= "/";
        }

        $lista = $this->getList();
        if (sizeof($lista)) {
            foreach ($lista as $fileName => $trash) {
                $dirname = dirname($fileName);
                $outDN = "$targetDir/$dirname";

                if (substr($dirname, 0, strlen($baseDir)) != $baseDir) {
                    continue;
                }
                if (!is_dir($outDN) && $maintainStructure) {
                    $str = "";
                    $folders = explode("/", $dirname);
                    foreach ($folders as $folder) {
                        $str = $str ? "$str/$folder" : $folder;
                        if (!is_dir("$targetDir/$str")) {
                            mkdir("$targetDir/$str");
                            if ($applyChmod) {
                                chmod("$targetDir/$str", $applyChmod);
                            }
                        }
                    }
                }
                if (substr($fileName, -1, 1) == "/") {
                    continue;
                }
                $maintainStructure ?
                    $this->unzip($fileName, "$targetDir/$fileName", $applyChmod) :
                    $this->unzip($fileName, "$targetDir/" . basename($fileName), $applyChmod);
            }
        }
    }

    private function close()
    {
        if ($this->fh) {
            fclose($this->fh);
        }
    }

    private function __destroy()
    {
        $this->close();
    }

    private function uncompress(&$content, $mode, $uncompressedSize, $targetFileName = false)
    {
        switch ($mode) {
            case 0:
                // Not compressed
                return $targetFileName ?
                    file_put_contents($targetFileName, $content) :
                    $content;
            case 1:
                return false;
            case 2:
            case 3:
            case 4:
            case 5:
                return false;
            case 6:
                return false;
            case 7:
                return false;
            case 8:
                // Deflate
                return $targetFileName ?
                    file_put_contents($targetFileName, gzinflate($content, $uncompressedSize)) :
                    gzinflate($content, $uncompressedSize);
            case 9:
                return false;
            case 10:
                return false;
            case 12:
                // Bzip2
                return $targetFileName ?
                    file_put_contents($targetFileName, bzdecompress($content)) :
                    bzdecompress($content);
            case 18:
                return false;
            default:
                return false;
        }
    }

    private function getLastError()
    {
        return $this->lastError;
    }

    private function _loadFileListByEOF(&$fh, $stopOnFile = false)
    {
        // Check if there's a valid Central Dir signature.
        // Let's consider a file comment smaller than 1024 characters...
        // Actually, it length can be 65536.. But we're not going to support it.

        for ($x = 0; $x < 1024; $x++) {
            fseek($fh, -22 - $x, SEEK_END);

            $signature = fread($fh, 4);
            if ($signature == $this->dirSignatureE) {
                // If found EOF Central Dir
                $eodir['disk_number_this'] = unpack("v", fread($fh, 2)); // number of this disk
                $eodir['disk_number'] = unpack("v", fread($fh, 2)); // number of the disk with the start of the central directory
                $eodir['total_entries_this'] = unpack("v", fread($fh, 2)); // total number of entries in the central dir on this disk
                $eodir['total_entries'] = unpack("v", fread($fh, 2)); // total number of entries in
                $eodir['size_of_cd'] = unpack("V", fread($fh, 4)); // size of the central directory
                $eodir['offset_start_cd'] = unpack("V", fread($fh, 4)); // offset of start of central directory with respect to the starting disk number
                $zipFileCommentLenght = unpack("v", fread($fh, 2)); // zipfile comment length
                $eodir['zipfile_comment'] = $zipFileCommentLenght[1] ? fread($fh, $zipFileCommentLenght[1]) : ''; // zipfile comment
                $this->endOfCentral = Array(
                    'disk_number_this' => $eodir['disk_number_this'][1],
                    'disk_number' => $eodir['disk_number'][1],
                    'total_entries_this' => $eodir['total_entries_this'][1],
                    'total_entries' => $eodir['total_entries'][1],
                    'size_of_cd' => $eodir['size_of_cd'][1],
                    'offset_start_cd' => $eodir['offset_start_cd'][1],
                    'zipfile_comment' => $eodir['zipfile_comment'],
                );

                // Then, load file list
                fseek($fh, $this->endOfCentral['offset_start_cd']);
                $signature = fread($fh, 4);

                while ($signature == $this->dirSignature) {
                    $dir['version_madeby'] = unpack("v", fread($fh, 2)); // version made by
                    $dir['version_needed'] = unpack("v", fread($fh, 2)); // version needed to extract
                    $dir['general_bit_flag'] = unpack("v", fread($fh, 2)); // general purpose bit flag
                    $dir['compression_method'] = unpack("v", fread($fh, 2)); // compression method
                    $dir['lastmod_time'] = unpack("v", fread($fh, 2)); // last mod file time
                    $dir['lastmod_date'] = unpack("v", fread($fh, 2)); // last mod file date
                    $dir['crc-32'] = fread($fh, 4); // crc-32
                    $dir['compressed_size'] = unpack("V", fread($fh, 4)); // compressed size
                    $dir['uncompressed_size'] = unpack("V", fread($fh, 4)); // uncompressed size
                    $fileNameLength = unpack("v", fread($fh, 2)); // filename length
                    $extraFieldLength = unpack("v", fread($fh, 2)); // extra field length
                    $fileCommentLength = unpack("v", fread($fh, 2)); // file comment length
                    $dir['disk_number_start'] = unpack("v", fread($fh, 2)); // disk number start
                    $dir['internal_attributes'] = unpack("v", fread($fh, 2)); // internal file attributes-byte1
                    $dir['external_attributes1'] = unpack("v", fread($fh, 2)); // external file attributes-byte2
                    $dir['external_attributes2'] = unpack("v", fread($fh, 2)); // external file attributes
                    $dir['relative_offset'] = unpack("V", fread($fh, 4)); // relative offset of local header
                    $dir['file_name'] = fread($fh, $fileNameLength[1]); // filename
                    $dir['extra_field'] = $extraFieldLength[1] ? fread($fh, $extraFieldLength[1]) : ''; // extra field
                    $dir['file_comment'] = $fileCommentLength[1] ? fread($fh, $fileCommentLength[1]) : ''; // file comment

                    // Convert the date and time, from MS-DOS format to UNIX Timestamp
                    $BINlastmod_date = str_pad(decbin($dir['lastmod_date'][1]), 16, '0', STR_PAD_LEFT);
                    $BINlastmod_time = str_pad(decbin($dir['lastmod_time'][1]), 16, '0', STR_PAD_LEFT);
                    $lastmod_dateY = bindec(substr($BINlastmod_date, 0, 7)) + 1980;
                    $lastmod_dateM = bindec(substr($BINlastmod_date, 7, 4));
                    $lastmod_dateD = bindec(substr($BINlastmod_date, 11, 5));
                    $lastmod_timeH = bindec(substr($BINlastmod_time, 0, 5));
                    $lastmod_timeM = bindec(substr($BINlastmod_time, 5, 6));
                    $lastmod_timeS = bindec(substr($BINlastmod_time, 11, 5));

                    // Some protection agains attacks...
                    $dir['file_name'] = $this->_decodeFilename($dir['file_name']);
                    if (!$dir['file_name'] = $this->_protect($dir['file_name'])) {
                        continue;
                    }
                    $this->centralDirList[$dir['file_name']] = Array(
                        'version_madeby' => $dir['version_madeby'][1],
                        'version_needed' => $dir['version_needed'][1],
                        'general_bit_flag' => str_pad(decbin($dir['general_bit_flag'][1]), 8, '0', STR_PAD_LEFT),
                        'compression_method' => $dir['compression_method'][1],
                        'lastmod_datetime' => mktime($lastmod_timeH, $lastmod_timeM, $lastmod_timeS, $lastmod_dateM, $lastmod_dateD, $lastmod_dateY),
                        'crc-32' => str_pad(dechex(ord($dir['crc-32'][3])), 2, '0', STR_PAD_LEFT) .
                            str_pad(dechex(ord($dir['crc-32'][2])), 2, '0', STR_PAD_LEFT) .
                            str_pad(dechex(ord($dir['crc-32'][1])), 2, '0', STR_PAD_LEFT) .
                            str_pad(dechex(ord($dir['crc-32'][0])), 2, '0', STR_PAD_LEFT),
                        'compressed_size' => $dir['compressed_size'][1],
                        'uncompressed_size' => $dir['uncompressed_size'][1],
                        'disk_number_start' => $dir['disk_number_start'][1],
                        'internal_attributes' => $dir['internal_attributes'][1],
                        'external_attributes1' => $dir['external_attributes1'][1],
                        'external_attributes2' => $dir['external_attributes2'][1],
                        'relative_offset' => $dir['relative_offset'][1],
                        'file_name' => $dir['file_name'],
                        'extra_field' => $dir['extra_field'],
                        'file_comment' => $dir['file_comment'],
                    );
                    $signature = fread($fh, 4);
                }

                // If loaded centralDirs, then try to identify the offsetPosition of the compressed data.
                if ($this->centralDirList) foreach ($this->centralDirList as $filename => $details) {
                    $i = $this->_getFileHeaderInformation($fh, $details['relative_offset']);
                    $this->compressedList[$filename]['file_name'] = $filename;
                    $this->compressedList[$filename]['compression_method'] = $details['compression_method'];
                    $this->compressedList[$filename]['version_needed'] = $details['version_needed'];
                    $this->compressedList[$filename]['lastmod_datetime'] = $details['lastmod_datetime'];
                    $this->compressedList[$filename]['crc-32'] = $details['crc-32'];
                    $this->compressedList[$filename]['compressed_size'] = $details['compressed_size'];
                    $this->compressedList[$filename]['uncompressed_size'] = $details['uncompressed_size'];
                    $this->compressedList[$filename]['lastmod_datetime'] = $details['lastmod_datetime'];
                    $this->compressedList[$filename]['extra_field'] = $i['extra_field'];
                    $this->compressedList[$filename]['contents-startOffset'] = $i['contents-startOffset'];
                    if (strtolower($stopOnFile) == strtolower($filename))
                        break;
                }
                return true;
            }
        }
        return false;
    }

    private function _loadFileListBySignatures(&$fh, $stopOnFile = false)
    {
        fseek($fh, 0);

        $return = false;
        for (; ;) {
            $details = $this->_getFileHeaderInformation($fh);
            if (!$details) {
                fseek($fh, 12 - 4, SEEK_CUR); // 12: Data descriptor - 4: Signature (that will be read again)
                $details = $this->_getFileHeaderInformation($fh);
            }
            if (!$details) {
                break;
            }
            $filename = $details['file_name'];
            $this->compressedList[$filename] = $details;
            $return = true;
            if (strtolower($stopOnFile) == strtolower($filename))
                break;
        }

        return $return;
    }

    private function _getFileHeaderInformation(&$fh, $startOffset = false)
    {
        if ($startOffset !== false) {
            fseek($fh, $startOffset);
        }
        $signature = fread($fh, 4);
        if ($signature == $this->zipSignature) {
            // Get information about the zipped file
            $file['version_needed'] = unpack("v", fread($fh, 2)); // version needed to extract
            $file['general_bit_flag'] = unpack("v", fread($fh, 2)); // general purpose bit flag
            $file['compression_method'] = unpack("v", fread($fh, 2)); // compression method
            $file['lastmod_time'] = unpack("v", fread($fh, 2)); // last mod file time
            $file['lastmod_date'] = unpack("v", fread($fh, 2)); // last mod file date
            $file['crc-32'] = fread($fh, 4); // crc-32
            $file['compressed_size'] = unpack("V", fread($fh, 4)); // compressed size
            $file['uncompressed_size'] = unpack("V", fread($fh, 4)); // uncompressed size
            $fileNameLength = unpack("v", fread($fh, 2)); // filename length
            $extraFieldLength = unpack("v", fread($fh, 2)); // extra field length
            $file['file_name'] = fread($fh, $fileNameLength[1]); // filename
            $file['extra_field'] = $extraFieldLength[1] ? fread($fh, $extraFieldLength[1]) : ''; // extra field
            $file['contents-startOffset'] = ftell($fh);

            // Bypass the whole compressed contents, and look for the next file
            fseek($fh, $file['compressed_size'][1], SEEK_CUR);

            // Convert the date and time, from MS-DOS format to UNIX Timestamp
            $BINlastmod_date = str_pad(decbin($file['lastmod_date'][1]), 16, '0', STR_PAD_LEFT);
            $BINlastmod_time = str_pad(decbin($file['lastmod_time'][1]), 16, '0', STR_PAD_LEFT);
            $lastmod_dateY = bindec(substr($BINlastmod_date, 0, 7)) + 1980;
            $lastmod_dateM = bindec(substr($BINlastmod_date, 7, 4));
            $lastmod_dateD = bindec(substr($BINlastmod_date, 11, 5));
            $lastmod_timeH = bindec(substr($BINlastmod_time, 0, 5));
            $lastmod_timeM = bindec(substr($BINlastmod_time, 5, 6));
            $lastmod_timeS = bindec(substr($BINlastmod_time, 11, 5));

            // Some protection agains attacks...
            $file['file_name'] = $this->_decodeFilename($file['file_name']);
            if (!$file['file_name'] = $this->_protect($file['file_name']))
                return false;

            // Mount file table
            $i = Array(
                'file_name' => $file['file_name'],
                'compression_method' => $file['compression_method'][1],
                'version_needed' => $file['version_needed'][1],
                'lastmod_datetime' => mktime($lastmod_timeH, $lastmod_timeM, $lastmod_timeS, $lastmod_dateM, $lastmod_dateD, $lastmod_dateY),
                'crc-32' => str_pad(dechex(ord($file['crc-32'][3])), 2, '0', STR_PAD_LEFT) .
                    str_pad(dechex(ord($file['crc-32'][2])), 2, '0', STR_PAD_LEFT) .
                    str_pad(dechex(ord($file['crc-32'][1])), 2, '0', STR_PAD_LEFT) .
                    str_pad(dechex(ord($file['crc-32'][0])), 2, '0', STR_PAD_LEFT),
                'compressed_size' => $file['compressed_size'][1],
                'uncompressed_size' => $file['uncompressed_size'][1],
                'extra_field' => $file['extra_field'],
                'general_bit_flag' => str_pad(decbin($file['general_bit_flag'][1]), 8, '0', STR_PAD_LEFT),
                'contents-startOffset' => $file['contents-startOffset']
            );
            return $i;
        }
        return false;
    }

    private function _decodeFilename($filename)
    {
        $from = "\xb7\xb5\xb6\xc7\x8e\x8f\x92\x80\xd4\x90\xd2\xd3\xde\xd6\xd7\xd8\xd1\xa5\xe3\xe0" .
            "\xe2\xe5\x99\x9d\xeb\xe9\xea\x9a\xed\xe8\xe1\x85\xa0\x83\xc6\x84\x86\x91\x87\x8a" .
            "\x82\x88\x89\x8d\xa1\x8c\x8b\xd0\xa4\x95\xa2\x93\xe4\x94\x9b\x97\xa3\x96\xec\xe7" .
            "\x98ï";
        $to = "ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûýþÿ´";

        return strtr($filename, $from, $to);
    }

    private function _protect($fullPath)
    {

        $fullPath = strtr($fullPath, ":*<>|\"\x0\\", "......./");
        while ($fullPath[0] == "/")
            $fullPath = substr($fullPath, 1);

        if (substr($fullPath, -1) == "/") {
            $base = '';
            $fullPath = substr($fullPath, 0, -1);
        }
        else {
            $base = basename($fullPath);
            $fullPath = dirname($fullPath);
        }

        $parts = explode("/", $fullPath);
        $lastIdx = false;
        foreach ($parts as $idx => $part) {
            if ($part == ".") {
                unset($parts[$idx]);
            }
            elseif ($part == "..") {
                unset($parts[$idx]);
                if ($lastIdx !== false) {
                    unset($parts[$lastIdx]);
                }
            }
            elseif ($part === '') {
                unset($parts[$idx]);
            }
            else {
                $lastIdx = $idx;
            }
        }
        $fullPath = sizeof($parts) ? implode("/", $parts) . "/" : "";
        return $fullPath . $base;
    }


    /* DO ZIP SECTION
       =========================================================================*/
    public function Compress($path = null, $isAutoDownload = false)
    {
        if (!isset($path) || !file_exists($path)) return false;
        $this->setTmpRootPath($path);
        $this->createBundle($path);
        $this->Save($isAutoDownload);
    }

    public function createBundle($dir = null)
    {
        $listDir = array();
        if (!isset($dir)) return $listDir;

        if ($handler = opendir($dir)) {
            while (($sub = readdir($handler)) !== FALSE) {
                if ($sub != "." && $sub != ".." && $sub != "Thumb.db") {
                    if (is_file($dir . DS . $sub)) {
                        $this->addFile($dir . DS . $sub, str_replace($this->getTmpRootPath() . DS, "", $dir . DS . $sub));
                    }
                    elseif (is_dir($dir . DS . $sub)) {
                        $this->addDir(str_replace($this->getTmpRootPath() . DS, "", $dir . DS . $sub));
                        $listDir[$sub] = $this->createBundle($dir . "/" . $sub);
                    }
                }
            }
            closedir($handler);
        }
        return $listDir;
    }


    public function addDir($dirname, $fileComments = '')
    {
        if (substr($dirname, -1) != '/')
            $dirname .= '/';
        $this->addFile(false, $dirname, $fileComments);

        return $this;
    }

    public function addFile($filename, $cfilename, $fileComments = '', $data = false)
    {
        if (!($fh = &$this->fh))
            $fh = fopen($this->archiveFileName(), $this->overwrite ? 'wb' : 'a+b');

        // $filename can be a local file OR the data wich will be compressed
        if (substr($cfilename, -1) == '/') {
            $details['uncsize'] = 0;
            $data = '';
        }
        elseif (file_exists($filename)) {
            $details['uncsize'] = filesize($filename);
            $data = file_get_contents($filename);
        }
        elseif ($filename) {
            echo "<b>Cannot add $filename. File not found</b><br>";
            return false;
        }
        else {
            $details['uncsize'] = strlen($filename);
            // DATA is given.. use it! :|
        }

        // if data to compress is too small, just store it
        if ($details['uncsize'] < 256) {
            $details['comsize'] = $details['uncsize'];
            $details['vneeded'] = 10;
            $details['cmethod'] = 0;
            $zdata = &$data;
        }
        else {
            // otherwise, compress it
            $zdata = gzcompress($data);
            $zdata = substr(substr($zdata, 0, strlen($zdata) - 4), 2); // fix crc bug (thanks to Eric Mueller)
            $details['comsize'] = strlen($zdata);
            $details['vneeded'] = 10;
            $details['cmethod'] = 8;
        }

        $details['bitflag'] = 0;
        $details['crc_32'] = crc32($data);

        // Convert date and time to DOS Format, and set then
        $lastmod_timeS = str_pad(decbin(date('s') >= 32 ? date('s') - 32 : date('s')), 5, '0', STR_PAD_LEFT);
        $lastmod_timeM = str_pad(decbin(date('i')), 6, '0', STR_PAD_LEFT);
        $lastmod_timeH = str_pad(decbin(date('H')), 5, '0', STR_PAD_LEFT);
        $lastmod_dateD = str_pad(decbin(date('d')), 5, '0', STR_PAD_LEFT);
        $lastmod_dateM = str_pad(decbin(date('m')), 4, '0', STR_PAD_LEFT);
        $lastmod_dateY = str_pad(decbin(date('Y') - 1980), 7, '0', STR_PAD_LEFT);

        # echo "ModTime: $lastmod_timeS-$lastmod_timeM-$lastmod_timeH (".date("s H H").")\n";
        # echo "ModDate: $lastmod_dateD-$lastmod_dateM-$lastmod_dateY (".date("d m Y").")\n";
        $details['modtime'] = bindec("$lastmod_timeH$lastmod_timeM$lastmod_timeS");
        $details['moddate'] = bindec("$lastmod_dateY$lastmod_dateM$lastmod_dateD");

        $details['offset'] = ftell($fh);
        fwrite($fh, $this->zipSignature);
        fwrite($fh, pack('s', $details['vneeded'])); // version_needed
        fwrite($fh, pack('s', $details['bitflag'])); // general_bit_flag
        fwrite($fh, pack('s', $details['cmethod'])); // compression_method
        fwrite($fh, pack('s', $details['modtime'])); // lastmod_time
        fwrite($fh, pack('s', $details['moddate'])); // lastmod_date
        fwrite($fh, pack('V', $details['crc_32'])); // crc-32
        fwrite($fh, pack('I', $details['comsize'])); // compressed_size
        fwrite($fh, pack('I', $details['uncsize'])); // uncompressed_size
        fwrite($fh, pack('s', strlen($cfilename))); // file_name_length
        fwrite($fh, pack('s', 0)); // extra_field_length
        fwrite($fh, $cfilename); // file_name
        // ignoring extra_field
        fwrite($fh, $zdata);

        // Append it to central dir
        $details['external_attributes'] = (substr($cfilename, -1) == '/' && !$zdata) ? 16 : 32; // Directory or file name
        $details['comments'] = $fileComments;
        $this->appendCentralDir($cfilename, $details);
        $this->files_count++;

        return $this;
    }

    public function setExtra($filename, $property, $value)
    {
        $this->centraldirs[$filename][$property] = $value;
    }

    public function save($autoDownload = false, $zipComments = '')
    {
        if (!($fh = &$this->fh)) {
            $fh = fopen($this->archiveFileName(), $this->overwrite ? 'w' : 'a+');
        }
        $cdrec = "";
        foreach ($this->centraldirs as $filename => $cd) {
            $cdrec .= $this->dirSignature;
            $cdrec .= "\x0\x0"; // version made by
            $cdrec .= pack('v', $cd['vneeded']); // version needed to extract
            $cdrec .= "\x0\x0"; // general bit flag
            $cdrec .= pack('v', $cd['cmethod']); // compression method
            $cdrec .= pack('v', $cd['modtime']); // lastmod time
            $cdrec .= pack('v', $cd['moddate']); // lastmod date
            $cdrec .= pack('V', $cd['crc_32']); // crc32
            $cdrec .= pack('V', $cd['comsize']); // compressed filesize
            $cdrec .= pack('V', $cd['uncsize']); // uncompressed filesize
            $cdrec .= pack('v', strlen($filename)); // file comment length
            $cdrec .= pack('v', 0); // extra field length
            $cdrec .= pack('v', strlen($cd['comments'])); // file comment length
            $cdrec .= pack('v', 0); // disk number start
            $cdrec .= pack('v', 0); // internal file attributes
            $cdrec .= pack('V', $cd['external_attributes']); // internal file attributes
            $cdrec .= pack('V', $cd['offset']); // relative offset of local header
            $cdrec .= $filename;
            $cdrec .= $cd['comments'];
        }
        $before_cd = ftell($fh);
        fwrite($fh, $cdrec);

        // end of central dir
        fwrite($fh, $this->dirSignatureE);
        fwrite($fh, pack('v', 0)); // number of this disk
        fwrite($fh, pack('v', 0)); // number of the disk with the start of the central directory
        fwrite($fh, pack('v', $this->files_count)); // total # of entries "on this disk"
        fwrite($fh, pack('v', $this->files_count)); // total # of entries overall
        fwrite($fh, pack('V', strlen($cdrec))); // size of central dir
        fwrite($fh, pack('V', $before_cd)); // offset to start of central dir
        fwrite($fh, pack('v', strlen($zipComments))); // .zip file comment length
        fwrite($fh, $zipComments);

        fclose($fh);

        if ($autoDownload) {
            App::Helper('Utility')->download($this->archiveFileName());
        }
    }

    private function appendCentralDir($filename, $properties)
    {
        $this->centraldirs[$filename] = $properties;
    }
}
// END ZIP CLASS
// info@apprain.com