524 lines
20 KiB
PHP
524 lines
20 KiB
PHP
<?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];
|
|
} 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 = [];
|
|
|
|
$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, $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, $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 = [];
|
|
$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('../', '', $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_limit && trim($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', $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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|