<?php
# ***** BEGIN LICENSE BLOCK *****
# This file is part of Clearbricks.
# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
# All rights reserved.
#
# Clearbricks is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# Clearbricks is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with Clearbricks; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# ***** END LICENSE BLOCK *****

class fileUnzip
{
	protected $file_name;
	protected $compressed_list = array();
	protected $eo_central = array();
	
	protected $zip_sig   = "\x50\x4b\x03\x04"; # local file header signature
	protected $dir_sig   = "\x50\x4b\x01\x02"; # central dir header signature
	protected $dir_sig_e = "\x50\x4b\x05\x06"; # end of central dir signature
	protected $fp = null;
	
	protected $memory_limit = null;
	
	protected $exclude_pattern = '';
	
	public function __construct($file_name)
	{
		$this->file_name = $file_name;
	}
	
	public function __destruct()
	{
		$this->close();
	}
	
	public function close()
	{
		if ($this->fp) {
			fclose($this->fp);
			$this->fp = null;
		}
		
		if ($this->memory_limit) {
			ini_set('memory_limit',$this->memory_limit);
		}
	}
	
	public function getList($stop_on_file=false,$exclude=false)
	{
		if (!empty($this->compressed_list)) {
			return $this->compressed_list;
		}
		
		if (!$this->loadFileListByEOF($stop_on_file,$exclude)) {
			if(!$this->loadFileListBySignatures($stop_on_file,$exclude)) {
				return false;
			}
		}
		
		return $this->compressed_list;
	}
	
	public function unzipAll($target)
	{
		if (empty($this->compressed_list)) {
			$this->getList();
		}
		
		foreach ($this->compressed_list as $k => $v)
		{
			if ($v['is_dir']) {
				continue;
			}
			
			$this->unzip($k,$target.'/'.$k);
		}
	}
	
	public function unzip($file_name,$target=false)
	{
		if (empty($this->compressed_list)) {
			$this->getList($file_name);
		}
		
		if (!isset($this->compressed_list[$file_name])) {
			throw new Exception(sprintf(__('File %s is not compressed in the zip.'),$file_name));
		}
		if ($this->isFileExcluded($file_name)) {
			return;
		}
		$details =& $this->compressed_list[$file_name];
		
		if ($details['is_dir']) {
			throw new Exception(sprintf(__('Trying to unzip a folder name %s'),$file_name));
		}
		
		if (!$details['uncompressed_size']) {
			return $this->putContent('',$target);
		}
		
		if ($target) {
			$this->testTargetDir(dirname($target));
		}
		
		fseek($this->fp(),$details['contents_start_offset']);
		
		$this->memoryAllocate($details['compressed_size']);
		return $this->uncompress(
			fread($this->fp(), $details['compressed_size']),
			$details['compression_method'],
			$details['uncompressed_size'],
			$target
		);
	}
	
	public function getFilesList()
	{
		if (empty($this->compressed_list)) {
			$this->getList();
		}
		
		$res = array();
		foreach ($this->compressed_list as $k => $v) {
			if (!$v['is_dir']) {
				$res[] = $k;
			}
		}
		return $res;
	}
	
	public function getDirsList()
	{
		if (empty($this->compressed_list)) {
			$this->getList();
		}
		
		$res = array();
		foreach ($this->compressed_list as $k => $v) {
			if ($v['is_dir']) {
				$res[] = substr($k,0,-1);
			}
		}
		return $res;
	}
	
	public function getRootDir()
	{
		if (empty($this->compressed_list)) {
			$this->getList();
		}
		
		$files = $this->getFilesList();
		$dirs = $this->getDirsList();
		
		$root_files = 0;
		$root_dirs = 0;
		foreach ($files as $v) { if (strpos($v,'/') === false) { $root_files++; }}
		foreach ($dirs as $v)  { if (strpos($v,'/') === false) { $root_dirs++;  }}
		
		if ($root_files == 0 && $root_dirs == 1) {
			return $dirs[0];
		} else {
			return false;
		}
	}
	
	public function isEmpty()
	{
		if (empty($this->compressed_list)) {
			$this->getList();
		}
		
		return count($this->compressed_list) == 0;
	}
	
	public function hasFile($f)
	{
		if (empty($this->compressed_list)) {
			$this->getList();
		}
		
		return isset($this->compressed_list[$f]);
	}
	
	public function setExcludePattern($pattern)
	{
		$this->exclude_pattern = $pattern;
	}
	
	protected function fp()
	{
		if ($this->fp === null) {
			$this->fp = @fopen($this->file_name,'rb');
		}
		
		if ($this->fp === false) {
			throw new Exception('Unable to open file.');
		}
		
		return $this->fp;
	}
	
	protected function isFileExcluded($f)
	{
		if (!$this->exclude_pattern) {
			return false;
		}
		
		return preg_match($this->exclude_pattern,$f);
	}

	protected function putContent($content,$target=false)
	{
		if ($target) {
			$r = @file_put_contents($target,$content);
			if ($r === false) {
				throw new Exception(__('Unable to write destination file.'));
			}
			files::inheritChmod($target);
			return true;
		}
		return $content;
	}
	
	protected function testTargetDir($dir)
	{
		if (is_dir($dir) && !is_writable($dir)) {
			throw new Exception(__('Unable to write in target directory, permission denied.'));
		}
		
		if (!is_dir($dir)) {
			files::makeDir($dir,true);
		}
	}
	
	protected function uncompress($content,$mode,$size,$target=false)
	{
		switch ($mode)
		{
			case 0:
				# Not compressed
				$this->memoryAllocate($size*2);
				return $this->putContent($content,$target);
			case 1:
				throw new Exception('Shrunk mode is not supported.');
			case 2:
			case 3:
			case 4:
			case 5:
				throw new Exception('Compression factor '.($mode-1).' is not supported.');
			case 6:
				throw new Exception('Implode is not supported.');
			case 7:
				throw new Exception('Tokenizing compression algorithm is not supported.');
			case 8:
				# Deflate
				if (!function_exists('gzinflate')) {
					throw new Exception('Gzip functions are not available.');
				}
				$this->memoryAllocate($size*2);
				return $this->putContent(gzinflate($content,$size),$target);
			case 9:
				throw new Exception('Enhanced Deflating is not supported.');
			case 10:
				throw new Exception('PKWARE Date Compression Library Impoloding is not supported.');
			case 12:
				# Bzip2
				if (!function_exists('bzdecompress')) {
					throw new Exception('Bzip2 functions are not available.');
				}
				$this->memoryAllocate($size*2);
				return $this->putContent(bzdecompress($content),$target);
			case 18:
				throw new Exception('IBM TERSE is not supported.');
			default:
				throw new Exception('Unknown uncompress method');
		}
	}
	
	protected function loadFileListByEOF($stop_on_file=false,$exclude=false)
	{
		$fp = $this->fp();
		
		for ($x=0; $x<1024; $x++)
		{
			fseek($fp,-22-$x,SEEK_END);
			$signature = fread($fp,4);
			
			if ($signature == $this->dir_sig_e)
			{
				$dir_list = array();
				
				$eodir = array(
					'disk_number_this'   => unpack('v', fread($fp,2)),
					'disk_number'        => unpack('v', fread($fp,2)),
					'total_entries_this' => unpack('v', fread($fp,2)),
					'total_entries'      => unpack('v', fread($fp,2)),
					'size_of_cd'         => unpack('V', fread($fp,4)),
					'offset_start_cd'    => unpack('V', fread($fp,4))
				);
				
				$zip_comment_len = unpack('v', fread($fp,2));
				$eodir['zipfile_comment'] = $zip_comment_len[1] ? fread($fp,$zip_comment_len) : '';
				
				$this->eo_central = 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']
				);
				
				fseek($fp, $this->eo_central['offset_start_cd']);
				$signature = fread($fp,4);
				
				while ($signature == $this->dir_sig)
				{
					$dir = array();
					$dir['version_madeby']       = unpack("v",fread($fp, 2)); # version made by
					$dir['version_needed']       = unpack("v",fread($fp, 2)); # version needed to extract
					$dir['general_bit_flag']     = unpack("v",fread($fp, 2)); # general purpose bit flag
					$dir['compression_method']   = unpack("v",fread($fp, 2)); # compression method
					$dir['lastmod_time']         = unpack("v",fread($fp, 2)); # last mod file time
					$dir['lastmod_date']         = unpack("v",fread($fp, 2)); # last mod file date
					$dir['crc-32']               = fread($fp,4);              # crc-32
					$dir['compressed_size']      = unpack("V",fread($fp, 4)); # compressed size
					$dir['uncompressed_size']    = unpack("V",fread($fp, 4)); # uncompressed size
					
					$file_name_len               = unpack("v",fread($fp, 2)); # filename length
					$extra_field_len             = unpack("v",fread($fp, 2)); # extra field length
					$file_comment_len            = unpack("v",fread($fp, 2)); # file comment length
					
					$dir['disk_number_start']    = unpack("v",fread($fp, 2)); # disk number start
					$dir['internal_attributes']  = unpack("v",fread($fp, 2)); # internal file attributes-byte1
					$dir['external_attributes1'] = unpack("v",fread($fp, 2)); # external file attributes-byte2
					$dir['external_attributes2'] = unpack("v",fread($fp, 2)); # external file attributes
					$dir['relative_offset']      = unpack("V",fread($fp, 4)); # relative offset of local header
					$dir['file_name']            = $this->cleanFileName(fread($fp, $file_name_len[1]));          # filename
					$dir['extra_field']          = $extra_field_len[1] ? fread($fp, $extra_field_len[1]) : '';   # extra field
					$dir['file_comment']         = $file_comment_len[1] ? fread($fp, $file_comment_len[1]) : ''; # file comment			
					
					$dir_list[$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'     => $this->getTimeStamp($dir['lastmod_date'][1],$dir['lastmod_time'][1]),
						'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($fp, 4);
				}
				
				foreach ($dir_list as $k => $v)
				{
					if ($exclude && preg_match($exclude,$k)) {
						continue;
					}
					
					$i = $this->getFileHeaderInformation($v['relative_offset']);
					
					$this->compressed_list[$k]['file_name']            = $k;
					$this->compressed_list[$k]['is_dir']               = $v['external_attributes1'] == 16 || substr($k,-1,1) == '/';
					$this->compressed_list[$k]['compression_method']   = $v['compression_method'];
					$this->compressed_list[$k]['version_needed']       = $v['version_needed'];
					$this->compressed_list[$k]['lastmod_datetime']     = $v['lastmod_datetime'];
					$this->compressed_list[$k]['crc-32']               = $v['crc-32'];
					$this->compressed_list[$k]['compressed_size']      = $v['compressed_size'];
					$this->compressed_list[$k]['uncompressed_size']    = $v['uncompressed_size'];
					$this->compressed_list[$k]['lastmod_datetime']     = $v['lastmod_datetime'];
					$this->compressed_list[$k]['extra_field']          = $i['extra_field'];
					$this->compressed_list[$k]['contents_start_offset'] = $i['contents_start_offset'];
					
					if(strtolower($stop_on_file) == strtolower($k)) {
						break;
					}
				}
				return true;
			}
		}
		return false;
	}
	
	protected function loadFileListBySignatures($stop_on_file=false,$exclude=false)
	{
		$fp = $this->fp();
		fseek($fp,0);
		
		$return = false;
		while(true)
		{
			$details = $this->getFileHeaderInformation();
			if (!$details) {
				fseek($fp,12-4,SEEK_CUR); # 12: Data descriptor - 4: Signature (that will be read again)
				$details = $this->getFileHeaderInformation();
			}
			if (!$details) {
				break;
			}
			$filename = $details['file_name'];
			
			if ($exclude && preg_match($exclude,$filename)) {
				continue;
			}
			
			$this->compressed_list[$filename] = $details;
			$return = true;
			
			if (strtolower($stop_on_file) == strtolower($filename)) {
				break;
			}
		}
		
		return $return;
	}
	
	protected function getFileHeaderInformation($start_offset=false)
	{
		$fp = $this->fp();
		
		if ($start_offset !== false) {
			fseek($fp,$start_offset);
		}
		
		$signature = fread($fp, 4);
		if ($signature == $this->zip_sig)
		{
			# Get information about the zipped file
			$file = array();
			$file['version_needed']        = unpack("v",fread($fp, 2)); # version needed to extract
			$file['general_bit_flag']      = unpack("v",fread($fp, 2)); # general purpose bit flag
			$file['compression_method']    = unpack("v",fread($fp, 2)); # compression method
			$file['lastmod_time']          = unpack("v",fread($fp, 2)); # last mod file time
			$file['lastmod_date']          = unpack("v",fread($fp, 2)); # last mod file date
			$file['crc-32']                = fread($fp,4);              # crc-32
			$file['compressed_size']       = unpack("V",fread($fp, 4)); # compressed size
			$file['uncompressed_size']     = unpack("V",fread($fp, 4)); # uncompressed size
			
			$file_name_len                 = unpack("v",fread($fp, 2)); # filename length
			$extra_field_len               = unpack("v",fread($fp, 2)); # extra field length
			
			$file['file_name']             = $this->cleanFileName(fread($fp,$file_name_len[1])); # filename
			$file['extra_field']           = $extra_field_len[1] ? fread($fp, $extra_field_len[1]) : ''; # extra field
			$file['contents_start_offset'] = ftell($fp);
			
			# Look for the next file
			fseek($fp, $file['compressed_size'][1], SEEK_CUR);
			
			# Mount file table
			$i = array(
				'file_name'            => $file['file_name'],
				'is_dir'               => substr($file['file_name'],-1,1) == '/',
				'compression_method'   => $file['compression_method'][1],
				'version_needed'       => $file['version_needed'][1],
				'lastmod_datetime'     => $this->getTimeStamp($file['lastmod_date'][1],$file['lastmod_time'][1]),
				'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_start_offset'=>$file['contents_start_offset']
			);
			return $i;
		}
		return false;
	}
	
	protected function getTimeStamp($date,$time)
	{
		$BINlastmod_date = str_pad(decbin($date), 16, '0', STR_PAD_LEFT);
		$BINlastmod_time = str_pad(decbin($time), 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)) * 2;
		
		return mktime($lastmod_timeH, $lastmod_timeM, $lastmod_timeS, $lastmod_dateM, $lastmod_dateD, $lastmod_dateY);
	}
	
	protected function cleanFileName($n)
	{
		$n = str_replace('../','',$n);
		$n = preg_replace('#^/+#','',$n);
		return $n;
	}
	
	protected function memoryAllocate($size)
	{
		$mem_used = function_exists('memory_get_usage') ? @memory_get_usage() : 4000000;
		$mem_limit = @ini_get('memory_limit');
		if ($mem_used && $mem_limit)
		{
			$mem_limit = files::str2bytes($mem_limit);
			$mem_avail = $mem_limit-$mem_used-(512*1024);
			$mem_needed = $size;
			
			if ($mem_needed > $mem_avail)
			{
				if (@ini_set('memory_limit',$mem_limit+$mem_needed+$mem_used) === false) {
					throw new Exception(__('Not enough memory to open file.'));
				}
				
				if (!$this->memory_limit) {
					$this->memory_limit = $mem_limit;
				}
			}
		}
	}
}
?>