480 lines
15 KiB
PHP
480 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* Copyright 2000-2017 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* See the enclosed file COPYING for license information (LGPL). If you
|
|
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
|
|
*
|
|
* The ZIP compression code is partially based on code from:
|
|
* Eric Mueller <eric@themepark.com>
|
|
* http://www.zend.com/codex.php?id=535&single=1
|
|
*
|
|
* Deins125 <webmaster@atlant.ru>
|
|
* http://www.zend.com/codex.php?id=470&single=1
|
|
*
|
|
* The ZIP compression date code is partially based on code from
|
|
* Peter Listiak <mlady@users.sourceforge.net>
|
|
*
|
|
* Official ZIP file format: https://support.pkware.com/display/PKZIP/APPNOTE
|
|
*
|
|
* @author Chuck Hagenbuch <chuck@horde.org>
|
|
* @author Michael Cochrane <mike@graftonhall.co.nz>
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @category Horde
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
|
* @package Compress
|
|
*/
|
|
|
|
/**
|
|
* The Horde_Compress_zip class allows ZIP files to be created and read.
|
|
*
|
|
* @author Chuck Hagenbuch <chuck@horde.org>
|
|
* @author Michael Cochrane <mike@graftonhall.co.nz>
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @category Horde
|
|
* @copyright 2000-2017 Horde LLC
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
|
* @package Compress
|
|
*/
|
|
class Horde_Compress_Zip extends Horde_Compress_Base
|
|
{
|
|
/**
|
|
* Return file list.
|
|
*
|
|
* @see decompress().
|
|
*/
|
|
const ZIP_LIST = 1;
|
|
|
|
/**
|
|
* Return file data.
|
|
*
|
|
* @see decompress().
|
|
*/
|
|
const ZIP_DATA = 2;
|
|
|
|
/**
|
|
* Beginning of central directory record.
|
|
*/
|
|
const CTRL_DIR_HEADER = "\x50\x4b\x01\x02";
|
|
|
|
/**
|
|
* End of central directory record.
|
|
*/
|
|
const CTRL_DIR_END = "\x50\x4b\x05\x06\x00\x00\x00\x00";
|
|
|
|
/**
|
|
* Beginning of file contents.
|
|
*/
|
|
const FILE_HEADER = "\x50\x4b\x03\x04";
|
|
|
|
/**
|
|
*/
|
|
public $canCompress = true;
|
|
|
|
/**
|
|
*/
|
|
public $canDecompress = true;
|
|
|
|
/**
|
|
* ZIP compression methods.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_methods = array(
|
|
0x0 => 'None',
|
|
0x1 => 'Shrunk',
|
|
0x2 => 'Super Fast',
|
|
0x3 => 'Fast',
|
|
0x4 => 'Normal',
|
|
0x5 => 'Maximum',
|
|
0x6 => 'Imploded',
|
|
0x8 => 'Deflated'
|
|
);
|
|
|
|
/**
|
|
* Temporary data for compressing files.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_ctrldir;
|
|
|
|
/**
|
|
* Temporary contents for compressing files.
|
|
*
|
|
* @var resource
|
|
*/
|
|
protected $_tmp;
|
|
|
|
/**
|
|
* @param array $data The data to compress. Requires an array of
|
|
* arrays. Each subarray should contain these
|
|
* fields:
|
|
* - data: (string/resource) The data to compress.
|
|
* - name: (string) The pathname to the file.
|
|
* - time: (integer) [optional] The timestamp to use for the file.
|
|
* @param array $params The parameter array.
|
|
* - stream: (boolean) If set, return a stream instead of a string.
|
|
* DEFAULT: Return string
|
|
*
|
|
* @return mixed The ZIP file as either a string or a stream resource.
|
|
*/
|
|
public function compress($data, array $params = array())
|
|
{
|
|
if (!Horde_Util::extensionExists('zlib')) {
|
|
throw new Horde_Compress_Exception(
|
|
Horde_Compress_Translation::t(
|
|
"This server can't compress zip files."
|
|
)
|
|
);
|
|
}
|
|
|
|
$this->_ctrldir = array();
|
|
$this->_tmp = fopen('php://temp', 'r+');
|
|
|
|
foreach ($data as $val) {
|
|
$this->_addToZipFile($val);
|
|
}
|
|
|
|
/* Create the ZIP file. */
|
|
$dir = implode('', $this->_ctrldir);
|
|
|
|
fseek($this->_tmp, 0, SEEK_END);
|
|
$offset = ftell($this->_tmp);
|
|
|
|
fwrite(
|
|
$this->_tmp,
|
|
$dir . self::CTRL_DIR_END .
|
|
/* Total # of entries "on this disk". */
|
|
pack('v', count($this->_ctrldir)) .
|
|
/* Total # of entries overall. */
|
|
pack('v', count($this->_ctrldir)) .
|
|
/* Size of central directory. */
|
|
pack('V', strlen($dir)) .
|
|
/* Offset to start of central dir. */
|
|
pack('V', $offset) .
|
|
/* ZIP file comment length. */
|
|
"\x00\x00"
|
|
);
|
|
|
|
rewind($this->_tmp);
|
|
|
|
if (empty($params['stream'])) {
|
|
$out = stream_get_contents($this->_tmp);
|
|
fclose($this->_tmp);
|
|
return $out;
|
|
}
|
|
|
|
return $this->_tmp;
|
|
}
|
|
|
|
/**
|
|
* @param array $params The parameter array.
|
|
* - action: (integer) [REQUIRED] The action to take on the data. Either
|
|
* self::ZIP_LIST or self::ZIP_DATA.
|
|
* - info: (array) [REQUIRED for ZIP_DATA] The zipfile list.
|
|
* - key: (integer) [REQUIRED for ZIP_DATA] The position of the file in
|
|
* the archive list.
|
|
*
|
|
* @return mixed If action is self::ZIP_DATA, the uncompressed data. If
|
|
* action is self::ZIP_LIST, an array with the KEY as the
|
|
* position in the zipfile and these values:
|
|
* - attr: File attributes
|
|
* - crc: CRC checksum
|
|
* - csize: Compressed file size
|
|
* - date: File modification time
|
|
* - name: Filename
|
|
* - method: Compression method
|
|
* - size: Original file size
|
|
* - type: File type
|
|
* @throws Horde_Compress_Exception
|
|
*/
|
|
public function decompress($data, array $params = array())
|
|
{
|
|
if (isset($params['action'])) {
|
|
switch ($params['action']) {
|
|
case self::ZIP_LIST:
|
|
return $this->_getZipInfo($data);
|
|
|
|
case self::ZIP_DATA:
|
|
return $this->_getZipData(
|
|
$data, $params['info'], $params['key']
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the list of files/data from the zip archive.
|
|
*
|
|
* @param string $data The zipfile data.
|
|
*
|
|
* @return array See decompress() for the format.
|
|
*
|
|
* @throws Horde_Compress_Exception
|
|
*/
|
|
protected function _getZipInfo($data)
|
|
{
|
|
$entries = array();
|
|
|
|
/* Get details from Central directory structure. */
|
|
$fhStart = strpos($data, self::CTRL_DIR_HEADER);
|
|
|
|
do {
|
|
if (strlen($data) < $fhStart + 31) {
|
|
throw new Horde_Compress_Exception(
|
|
Horde_Compress_Translation::t("Invalid ZIP data")
|
|
);
|
|
}
|
|
$info = unpack(
|
|
'vMethod/VTime/VCRC32/VCompressed/VUncompressed/vLength',
|
|
substr($data, $fhStart + 10, 20)
|
|
);
|
|
|
|
if (!isset($this->_methods[$info['Method']])) {
|
|
throw new Horde_Compress_Exception(
|
|
Horde_Compress_Translation::t("Invalid ZIP data")
|
|
);
|
|
}
|
|
|
|
$name = substr($data, $fhStart + 46, $info['Length']);
|
|
|
|
$entries[$name] = array(
|
|
'attr' => null,
|
|
'crc' => sprintf("%08s", dechex($info['CRC32'])),
|
|
'csize' => $info['Compressed'],
|
|
'date' => null,
|
|
'_dataStart' => null,
|
|
'name' => $name,
|
|
'method' => $this->_methods[$info['Method']],
|
|
'_method' => $info['Method'],
|
|
'size' => $info['Uncompressed'],
|
|
'type' => null
|
|
);
|
|
|
|
$entries[$name]['date'] =
|
|
mktime((($info['Time'] >> 11) & 0x1f),
|
|
(($info['Time'] >> 5) & 0x3f),
|
|
(($info['Time'] << 1) & 0x3e),
|
|
(($info['Time'] >> 21) & 0x0f),
|
|
(($info['Time'] >> 16) & 0x1f),
|
|
((($info['Time'] >> 25) & 0x7f) + 1980));
|
|
|
|
if (strlen($data) < $fhStart + 43) {
|
|
throw new Horde_Compress_Exception(
|
|
Horde_Compress_Translation::t("Invalid ZIP data")
|
|
);
|
|
}
|
|
$info = unpack(
|
|
'vInternal/VExternal',
|
|
substr($data, $fhStart + 36, 6)
|
|
);
|
|
|
|
$entries[$name]['type'] = ($info['Internal'] & 0x01)
|
|
? 'text'
|
|
: 'binary';
|
|
$entries[$name]['attr'] =
|
|
(($info['External'] & 0x10) ? 'D' : '-') .
|
|
(($info['External'] & 0x20) ? 'A' : '-') .
|
|
(($info['External'] & 0x03) ? 'S' : '-') .
|
|
(($info['External'] & 0x02) ? 'H' : '-') .
|
|
(($info['External'] & 0x01) ? 'R' : '-');
|
|
} while (($fhStart = strpos($data, self::CTRL_DIR_HEADER, $fhStart + 46)) !== false);
|
|
|
|
/* Get details from local file header. */
|
|
$fhStart = strpos($data, self::FILE_HEADER);
|
|
|
|
$data_len = strlen($data);
|
|
|
|
do {
|
|
if ($data_len < $fhStart + 34) {
|
|
throw new Horde_Compress_Exception(
|
|
Horde_Compress_Translation::t("Invalid ZIP data")
|
|
);
|
|
}
|
|
$info = unpack(
|
|
'vMethod/VTime/VCRC32/VCompressed/VUncompressed/vLength/vExtraLength',
|
|
substr($data, $fhStart + 8, 25)
|
|
);
|
|
$name = substr($data, $fhStart + 30, $info['Length']);
|
|
if (isset($entries[$name])) {
|
|
$entries[$name]['_dataStart'] = $fhStart + 30
|
|
+ $info['Length'] + $info['ExtraLength'];
|
|
}
|
|
} while ($data_len > $fhStart + 30 + $info['Length'] &&
|
|
($fhStart = strpos($data, self::FILE_HEADER, $fhStart + 30 + $info['Length'])) !== false);
|
|
|
|
return array_values($entries);
|
|
}
|
|
|
|
/**
|
|
* Returns the data for a specific archived file.
|
|
*
|
|
* @param string $data The zip archive contents.
|
|
* @param array $info The information array from _getZipInfo().
|
|
* @param integer $key The position of the file in the archive.
|
|
*
|
|
* @return string The file data.
|
|
*/
|
|
protected function _getZipData($data, $info, $key)
|
|
{
|
|
if (($info[$key]['_method'] == 0x8) &&
|
|
Horde_Util::extensionExists('zlib')) {
|
|
/* If the file has been deflated, and zlib is installed,
|
|
then inflate the data again. */
|
|
return @gzinflate(
|
|
substr($data, $info[$key]['_dataStart'], $info[$key]['csize'])
|
|
);
|
|
} elseif ($info[$key]['_method'] == 0x0) {
|
|
/* Files that aren't compressed. */
|
|
return substr(
|
|
$data, $info[$key]['_dataStart'], $info[$key]['csize']
|
|
);
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Checks to see if the data is a valid ZIP file.
|
|
*
|
|
* @param string $data The ZIP file data.
|
|
*
|
|
* @return boolean True if valid, false if invalid.
|
|
*/
|
|
public function checkZipData($data)
|
|
{
|
|
return (strpos($data, self::FILE_HEADER) !== false);
|
|
}
|
|
|
|
/**
|
|
* Converts a UNIX timestamp to a 4-byte DOS date and time format (date in
|
|
* high 2-bytes, time in low 2-bytes allowing magnitude comparison).
|
|
*
|
|
* @param integer $unixtime The current UNIX timestamp.
|
|
*
|
|
* @return integer The current date in a 4-byte DOS format.
|
|
*/
|
|
protected function _unix2DOSTime($unixtime = null)
|
|
{
|
|
$timearray = (is_null($unixtime)) ? getdate() : getdate($unixtime);
|
|
|
|
if ($timearray['year'] < 1980) {
|
|
$timearray['year'] = 1980;
|
|
$timearray['mon'] = 1;
|
|
$timearray['mday'] = 1;
|
|
$timearray['hours'] = 0;
|
|
$timearray['minutes'] = 0;
|
|
$timearray['seconds'] = 0;
|
|
}
|
|
|
|
return (($timearray['year'] - 1980) << 25) |
|
|
($timearray['mon'] << 21) |
|
|
($timearray['mday'] << 16) |
|
|
($timearray['hours'] << 11) |
|
|
($timearray['minutes'] << 5) |
|
|
($timearray['seconds'] >> 1);
|
|
}
|
|
|
|
/**
|
|
* Adds a "file" to the ZIP archive.
|
|
*
|
|
* @param array $file See self::createZipFile().
|
|
*/
|
|
protected function _addToZipFile($file)
|
|
{
|
|
$name = str_replace('\\', '/', $file['name']);
|
|
|
|
/* See if time/date information has been provided. */
|
|
$ftime = (isset($file['time'])) ? $file['time'] : null;
|
|
|
|
/* Get the hex time. */
|
|
$dtime = sprintf('%08s', dechex($this->_unix2DosTime($ftime)));
|
|
$hexdtime = chr(hexdec($dtime[6] . $dtime[7])) .
|
|
chr(hexdec($dtime[4] . $dtime[5])) .
|
|
chr(hexdec($dtime[2] . $dtime[3])) .
|
|
chr(hexdec($dtime[0] . $dtime[1]));
|
|
|
|
/* "Local file header" segment. */
|
|
if (is_resource($file['data'])) {
|
|
$zdata = fopen('php://temp', 'r+');
|
|
|
|
$params = new stdClass;
|
|
stream_filter_register(
|
|
'horde_compress_filter_crc32', 'Horde_Stream_Filter_Crc32'
|
|
);
|
|
$filter = stream_filter_prepend(
|
|
$file['data'],
|
|
'horde_compress_filter_crc32',
|
|
STREAM_FILTER_READ,
|
|
$params
|
|
);
|
|
$filter2 = stream_filter_append(
|
|
$zdata,
|
|
'zlib.deflate',
|
|
STREAM_FILTER_WRITE
|
|
);
|
|
|
|
rewind($file['data']);
|
|
stream_copy_to_stream($file['data'], $zdata);
|
|
|
|
$crc = $params->crc32;
|
|
$unc_len = ftell($file['data']);
|
|
|
|
stream_filter_remove($filter2);
|
|
stream_filter_remove($filter);
|
|
|
|
fseek($zdata, 0, SEEK_END);
|
|
$c_len = ftell($zdata);
|
|
} else {
|
|
$unc_len = strlen($file['data']);
|
|
$crc = crc32($file['data']);
|
|
$zdata = gzdeflate($file['data']);
|
|
$c_len = strlen($zdata);
|
|
}
|
|
|
|
/* Common data for the two entries. */
|
|
$common =
|
|
"\x14\x00" . /* Version needed to extract. */
|
|
"\x00\x00" . /* General purpose bit flag. */
|
|
"\x08\x00" . /* Compression method. */
|
|
$hexdtime . /* Last modification time/date. */
|
|
pack('V', $crc) . /* CRC 32 information. */
|
|
pack('V', $c_len) . /* Compressed filesize. */
|
|
pack('V', $unc_len) . /* Uncompressed filesize. */
|
|
pack('v', strlen($name)) . /* Length of filename. */
|
|
pack('v', 0); /* Extra field length. */
|
|
|
|
/* Add this entry to zip data. */
|
|
fseek($this->_tmp, 0, SEEK_END);
|
|
$old_offset = ftell($this->_tmp);
|
|
|
|
fwrite($this->_tmp,
|
|
self::FILE_HEADER . /* Begin creating the ZIP data. */
|
|
$common . /* Common data. */
|
|
$name
|
|
);
|
|
|
|
/* "File data" segment. */
|
|
if (is_resource($zdata)) {
|
|
rewind($zdata);
|
|
stream_copy_to_stream($zdata, $this->_tmp);
|
|
} else {
|
|
fwrite($this->_tmp, $zdata);
|
|
}
|
|
|
|
/* Add to central directory record. */
|
|
$this->_ctrldir[] =
|
|
self::CTRL_DIR_HEADER .
|
|
"\x00\x00" . /* Version made by. */
|
|
$common . /* Common data. */
|
|
pack('v', 0) . /* File comment length. */
|
|
pack('v', 0) . /* Disk number start. */
|
|
pack('v', 0) . /* Internal file attributes. */
|
|
pack('V', 32) . /* External file attributes -
|
|
* 'archive' bit set. */
|
|
pack('V', $old_offset) . /* Relative offset of local header. */
|
|
$name; /* File name. */
|
|
}
|
|
|
|
}
|