<?php
/**
* @class fileUnzip
*
* @package Clearbricks
* @subpackage Zip
*
* @copyright Olivier Meunier & Association Dotclear
* @copyright GPL-2.0-only
*/
class fileUnzip
{
protected $file_name;
protected $compressed_list = [];
protected $eo_central = [];
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 ($target) {
$this->testTargetDir(dirname($target));
}
if (!$details['uncompressed_size']) {
return $this->putContent('', $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 = [];
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 = [];
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];
}
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, (string) $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 = [];
$eodir = [
'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, (int) $zip_comment_len) : '';
$this->eo_central = [
'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 = [];
$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']] = [
'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, (string) $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, (string) $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 = [];
$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 = [
'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('../', '', (string) $n);
$n = preg_replace('#^/+#', '', (string) $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_limit && trim((string) $mem_limit) === '-1' || !files::str2bytes($mem_limit)) {
// Cope with memory_limit set to -1 in PHP.ini
return;
}
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', (string) ($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;
}
}
}
}
}