Overview

Packages

  • Compress

Classes

  • Horde_Compress
  • Horde_Compress_Base
  • Horde_Compress_Dbx
  • Horde_Compress_Exception
  • Horde_Compress_Gzip
  • Horde_Compress_Rar
  • Horde_Compress_Tar
  • Horde_Compress_Tnef
  • Horde_Compress_Translation
  • Horde_Compress_Zip
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * The Horde_Compress_zip class allows ZIP files to be created and read.
  4:  *
  5:  * The ZIP compression code is partially based on code from:
  6:  *   Eric Mueller <eric@themepark.com>
  7:  *   http://www.zend.com/codex.php?id=535&single=1
  8:  *
  9:  *   Deins125 <webmaster@atlant.ru>
 10:  *   http://www.zend.com/codex.php?id=470&single=1
 11:  *
 12:  * The ZIP compression date code is partially based on code from
 13:  *   Peter Listiak <mlady@users.sourceforge.net>
 14:  *
 15:  * Copyright 2000-2012 Horde LLC (http://www.horde.org/)
 16:  *
 17:  * See the enclosed file COPYING for license information (LGPL). If you
 18:  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 19:  *
 20:  * @author   Chuck Hagenbuch <chuck@horde.org>
 21:  * @author   Michael Cochrane <mike@graftonhall.co.nz>
 22:  * @author   Michael Slusarz <slusarz@horde.org>
 23:  * @category Horde
 24:  * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
 25:  * @package  Compress
 26:  */
 27: class Horde_Compress_Zip extends Horde_Compress_Base
 28: {
 29:     /* Constants used with decompress(). */
 30:     const ZIP_LIST = 1;
 31:     const ZIP_DATA = 2;
 32: 
 33:     /* Beginning of central directory record. */
 34:     const CTRL_DIR_HEADER = "\x50\x4b\x01\x02";
 35: 
 36:     /* End of central directory record. */
 37:     const CTRL_DIR_END = "\x50\x4b\x05\x06\x00\x00\x00\x00";
 38: 
 39:     /* Beginning of file contents. */
 40:     const FILE_HEADER = "\x50\x4b\x03\x04";
 41: 
 42:     /**
 43:      */
 44:     public $canCompress = true;
 45: 
 46:     /**
 47:      */
 48:     public $canDecompress = true;
 49: 
 50:     /**
 51:      * ZIP compression methods.
 52:      *
 53:      * @var array
 54:      */
 55:     protected $_methods = array(
 56:         0x0 => 'None',
 57:         0x1 => 'Shrunk',
 58:         0x2 => 'Super Fast',
 59:         0x3 => 'Fast',
 60:         0x4 => 'Normal',
 61:         0x5 => 'Maximum',
 62:         0x6 => 'Imploded',
 63:         0x8 => 'Deflated'
 64:     );
 65: 
 66:     /**
 67:      * Temporary data for compressing files.
 68:      *
 69:      * @var array
 70:      */
 71:     protected $_ctrldir;
 72: 
 73:     /**
 74:      * Temporary contents for compressing files.
 75:      *
 76:      * @var resource
 77:      */
 78:     protected $_tmp;
 79: 
 80:     /**
 81:      * @param array $data    The data to compress.
 82:      * <pre>
 83:      * Requires an array of arrays - each subarray should contain the
 84:      * following fields:
 85:      * data - (string/resource) The data to compress.
 86:      * name - (string) The pathname to the file.
 87:      * time - (integer) [optional] The timestamp to use for the file.
 88:      * </pre>
 89:      * @param array $params  The parameter array.
 90:      * <pre>
 91:      * stream - (boolean) If set, return a stream instead of a string.
 92:      *            DEFAULT: Return string
 93:      * </pre>
 94:      *
 95:      * @return mixed  The ZIP file as either a string or a stream resource.
 96:      */
 97:     public function compress($data, $params = array())
 98:     {
 99:         if (!Horde_Util::extensionExists('zlib')) {
100:             throw new Horde_Compress_Exception(Horde_Compress_Translation::t("This server can't compress zip files."));
101:         }
102: 
103:         $this->_ctrldir = array();
104:         $this->_tmp = fopen('php://temp', 'r+');
105: 
106:         reset($data);
107:         while (list(, $val) = each($data)) {
108:             $this->_addToZipFile($val);
109:         }
110: 
111:         /* Creates the ZIP file.
112:          * Official ZIP file format: http://www.pkware.com/appnote.txt */
113:         $dir = implode('', $this->_ctrldir);
114: 
115:         fseek($this->_tmp, 0, SEEK_END);
116:         $offset = ftell($this->_tmp);
117: 
118:         fwrite($this->_tmp,
119:             $dir . self::CTRL_DIR_END .
120:             /* Total # of entries "on this disk". */
121:             pack('v', count($this->_ctrldir)) .
122:             /* Total # of entries overall. */
123:             pack('v', count($this->_ctrldir)) .
124:             /* Size of central directory. */
125:             pack('V', strlen($dir)) .
126:             /* Offset to start of central dir. */
127:             pack('V', $offset) .
128:             /* ZIP file comment length. */
129:             "\x00\x00"
130:         );
131: 
132:         rewind($this->_tmp);
133: 
134:         if (empty($params['stream'])) {
135:             $out = stream_get_contents($this->_tmp);
136:             fclose($this->_tmp);
137:             return $out;
138:         }
139: 
140:         return $this->_tmp;
141:     }
142: 
143:     /**
144:      * @param array $params  The parameter array.
145:      * <pre>
146:      * The following parameters are REQUIRED:
147:      * 'action' - (integer) The action to take on the data.  Either
148:      *                      self::ZIP_LIST or self::ZIP_DATA.
149:      *
150:      * The following parameters are REQUIRED for self::ZIP_DATA also:
151:      * 'info' - (array) The zipfile list.
152:      * 'key' - (integer) The position of the file in the archive list.
153:      * </pre>
154:      *
155:      * @return mixed  If action is self::ZIP_DATA, the uncompressed data. If
156:      *                action is self::ZIP_LIST, the following data:
157:      * <pre>
158:      * KEY: Position in zipfile
159:      * VALUES:
160:      *   attr - File attributes
161:      *   crc - CRC checksum
162:      *   csize - Compressed file size
163:      *   date - File modification time
164:      *   name - Filename
165:      *   method - Compression method
166:      *   size - Original file size
167:      *   type - File type
168:      * </pre>
169:      * @throws Horde_Compress_Exception
170:      */
171:     public function decompress($data, array $params = array())
172:     {
173:         if (isset($params['action'])) {
174:             switch ($params['action']) {
175:             case self::ZIP_LIST:
176:                 return $this->_getZipInfo($data);
177: 
178:             case self::ZIP_DATA:
179:                 return $this->_getZipData($data, $params['info'], $params['key']);
180:             }
181:         }
182:     }
183: 
184:     /**
185:      * Get the list of files/data from the zip archive.
186:      *
187:      * @param string $data  The zipfile data.
188:      *
189:      * @return array  See decompress() for the format.
190:      *
191:      * @throws Horde_Compress_Exception
192:      */
193:     protected function _getZipInfo($data)
194:     {
195:         $entries = array();
196: 
197:         /* Get details from Central directory structure. */
198:         $fhStart = strpos($data, self::CTRL_DIR_HEADER);
199: 
200:         do {
201:             if (strlen($data) < $fhStart + 31) {
202:                 throw new Horde_Compress_Exception(Horde_Compress_Translation::t("Invalid ZIP data"));
203:             }
204:             $info = unpack('vMethod/VTime/VCRC32/VCompressed/VUncompressed/vLength', substr($data, $fhStart + 10, 20));
205: 
206:             if (!isset($this->_methods[$info['Method']])) {
207:                 throw new Horde_Compress_Exception(Horde_Compress_Translation::t("Invalid ZIP data"));
208:             }
209: 
210:             $name = substr($data, $fhStart + 46, $info['Length']);
211: 
212:             $entries[$name] = array(
213:                 'attr' => null,
214:                 'crc' => sprintf("%08s", dechex($info['CRC32'])),
215:                 'csize' => $info['Compressed'],
216:                 'date' => null,
217:                 '_dataStart' => null,
218:                 'name' => $name,
219:                 'method' => $this->_methods[$info['Method']],
220:                 '_method' => $info['Method'],
221:                 'size' => $info['Uncompressed'],
222:                 'type' => null
223:             );
224: 
225:             $entries[$name]['date'] =
226:                 mktime((($info['Time'] >> 11) & 0x1f),
227:                        (($info['Time'] >> 5) & 0x3f),
228:                        (($info['Time'] << 1) & 0x3e),
229:                        (($info['Time'] >> 21) & 0x07),
230:                        (($info['Time'] >> 16) & 0x1f),
231:                        ((($info['Time'] >> 25) & 0x7f) + 1980));
232: 
233:             if (strlen($data) < $fhStart + 43) {
234:                 throw new Horde_Compress_Exception(Horde_Compress_Translation::t("Invalid ZIP data"));
235:             }
236:             $info = unpack('vInternal/VExternal', substr($data, $fhStart + 36, 6));
237: 
238:             $entries[$name]['type'] = ($info['Internal'] & 0x01) ? 'text' : 'binary';
239:             $entries[$name]['attr'] =
240:                 (($info['External'] & 0x10) ? 'D' : '-') .
241:                 (($info['External'] & 0x20) ? 'A' : '-') .
242:                 (($info['External'] & 0x03) ? 'S' : '-') .
243:                 (($info['External'] & 0x02) ? 'H' : '-') .
244:                 (($info['External'] & 0x01) ? 'R' : '-');
245:         } while (($fhStart = strpos($data, self::CTRL_DIR_HEADER, $fhStart + 46)) !== false);
246: 
247:         /* Get details from local file header. */
248:         $fhStart = strpos($data, self::FILE_HEADER);
249: 
250:         $data_len = strlen($data);
251: 
252:         do {
253:             if ($data_len < $fhStart + 34) {
254:                 throw new Horde_Compress_Exception(Horde_Compress_Translation::t("Invalid ZIP data"));
255:             }
256:             $info = unpack('vMethod/VTime/VCRC32/VCompressed/VUncompressed/vLength/vExtraLength', substr($data, $fhStart + 8, 25));
257:             $name = substr($data, $fhStart + 30, $info['Length']);
258:             if (isset($entries[$name])) {
259:                 $entries[$name]['_dataStart'] = $fhStart + 30 + $info['Length'] + $info['ExtraLength'];
260:             }
261:         } while ($data_len > $fhStart + 30 + $info['Length'] &&
262:                  ($fhStart = strpos($data, self::FILE_HEADER, $fhStart + 30 + $info['Length'])) !== false);
263: 
264:         return array_values($entries);
265:     }
266: 
267:     /**
268:      * Returns the data for a specific archived file.
269:      *
270:      * @param string $data  The zip archive contents.
271:      * @param array $info   The information array from _getZipInfo().
272:      * @param integer $key  The position of the file in the archive.
273:      *
274:      * @return string  The file data.
275:      */
276:     protected function _getZipData($data, $info, $key)
277:     {
278:         if (($info[$key]['_method'] == 0x8) &&
279:             Horde_Util::extensionExists('zlib')) {
280:             /* If the file has been deflated, and zlib is installed,
281:                then inflate the data again. */
282:             return @gzinflate(substr($data, $info[$key]['_dataStart'], $info[$key]['csize']));
283:         } elseif ($info[$key]['_method'] == 0x0) {
284:             /* Files that aren't compressed. */
285:             return substr($data, $info[$key]['_dataStart'], $info[$key]['csize']);
286:         }
287: 
288:         return '';
289:     }
290: 
291:     /**
292:      * Checks to see if the data is a valid ZIP file.
293:      *
294:      * @param string $data  The ZIP file data.
295:      *
296:      * @return boolean  True if valid, false if invalid.
297:      */
298:     public function checkZipData($data)
299:     {
300:         return (strpos($data, self::FILE_HEADER) !== false);
301:     }
302: 
303:     /**
304:      * Converts a UNIX timestamp to a 4-byte DOS date and time format
305:      * (date in high 2-bytes, time in low 2-bytes allowing magnitude
306:      * comparison).
307:      *
308:      * @param integer $unixtime  The current UNIX timestamp.
309:      *
310:      * @return integer  The current date in a 4-byte DOS format.
311:      */
312:     protected function _unix2DOSTime($unixtime = null)
313:     {
314:         $timearray = (is_null($unixtime)) ? getdate() : getdate($unixtime);
315: 
316:         if ($timearray['year'] < 1980) {
317:             $timearray['year']    = 1980;
318:             $timearray['mon']     = 1;
319:             $timearray['mday']    = 1;
320:             $timearray['hours']   = 0;
321:             $timearray['minutes'] = 0;
322:             $timearray['seconds'] = 0;
323:         }
324: 
325:         return (($timearray['year'] - 1980) << 25) |
326:                 ($timearray['mon'] << 21) |
327:                 ($timearray['mday'] << 16) |
328:                 ($timearray['hours'] << 11) |
329:                 ($timearray['minutes'] << 5) |
330:                 ($timearray['seconds'] >> 1);
331:     }
332: 
333:     /**
334:      * Adds a "file" to the ZIP archive.
335:      *
336:      * @param array $file  See self::createZipFile().
337:      */
338:     protected function _addToZipFile($file)
339:     {
340:         $name = str_replace('\\', '/', $file['name']);
341: 
342:         /* See if time/date information has been provided. */
343:         $ftime = (isset($file['time'])) ? $file['time'] : null;
344: 
345:         /* Get the hex time. */
346:         $dtime    = dechex($this->_unix2DosTime($ftime));
347:         $hexdtime = chr(hexdec($dtime[6] . $dtime[7])) .
348:                     chr(hexdec($dtime[4] . $dtime[5])) .
349:                     chr(hexdec($dtime[2] . $dtime[3])) .
350:                     chr(hexdec($dtime[0] . $dtime[1]));
351: 
352:         /* "Local file header" segment. */
353:         if (is_resource($file['data'])) {
354:             $zdata = fopen('php://temp', 'r+');
355: 
356:             $params = new stdClass;
357:             stream_filter_register('horde_compress_filter_crc32', 'Horde_Stream_Filter_Crc32');
358:             $filter = stream_filter_prepend($file['data'], 'horde_compress_filter_crc32', STREAM_FILTER_READ, $params);
359:             $filter2 = stream_filter_append($zdata, 'zlib.deflate', STREAM_FILTER_WRITE);
360: 
361:             rewind($file['data']);
362:             stream_copy_to_stream($file['data'], $zdata);
363: 
364:             $crc = $params->crc32;
365:             $unc_len = ftell($file['data']);
366: 
367:             stream_filter_remove($filter2);
368:             stream_filter_remove($filter);
369: 
370:             fseek($zdata, 0, SEEK_END);
371:             $c_len = ftell($zdata);
372:         } else {
373:             $unc_len = strlen($file['data']);
374:             $crc = crc32($file['data']);
375:             $zdata = gzdeflate($file['data']);
376:             $c_len = strlen($zdata);
377:         }
378: 
379:         /* Common data for the two entries. */
380:         $common =
381:             "\x14\x00" .                /* Version needed to extract. */
382:             "\x00\x00" .                /* General purpose bit flag. */
383:             "\x08\x00" .                /* Compression method. */
384:             $hexdtime .                 /* Last modification time/date. */
385:             pack('V', $crc) .           /* CRC 32 information. */
386:             pack('V', $c_len) .         /* Compressed filesize. */
387:             pack('V', $unc_len) .       /* Uncompressed filesize. */
388:             pack('v', strlen($name)) .  /* Length of filename. */
389:             pack('v', 0);               /* Extra field length. */
390: 
391:         /* Add this entry to zip data. */
392:         fseek($this->_tmp, 0, SEEK_END);
393:         $old_offset = ftell($this->_tmp);
394: 
395:         fwrite($this->_tmp,
396:             self::FILE_HEADER .  /* Begin creating the ZIP data. */
397:             $common .            /* Common data. */
398:             $name
399:         );
400: 
401:         /* "File data" segment. */
402:         if (is_resource($zdata)) {
403:             rewind($zdata);
404:             stream_copy_to_stream($zdata, $this->_tmp);
405:         } else {
406:             fwrite($this->_tmp, $zdata);
407:         }
408: 
409:         /* Add to central directory record. */
410:         $this->_ctrldir[] =
411:             self::CTRL_DIR_HEADER .
412:             "\x00\x00" .              /* Version made by. */
413:             $common .                 /* Common data. */
414:             pack('v', 0) .            /* File comment length. */
415:             pack('v', 0) .            /* Disk number start. */
416:             pack('v', 0) .            /* Internal file attributes. */
417:             pack('V', 32) .           /* External file attributes -
418:                                        * 'archive' bit set. */
419:             pack('V', $old_offset) .  /* Relative offset of local
420:                                        * header. */
421:             $name;                    /* File name. */
422:     }
423: 
424: }
425: 
API documentation generated by ApiGen