3 * Class to create and manage a Zip file.
5 * Initially inspired by CreateZipFile by Rochak Chauhan www.rochakchauhan.com (http://www.phpclasses.org/browse/package/2322.html)
7 * http://www.pkware.com/documents/casestudies/APPNOTE.TXT Zip file specification.
9 * License: GNU LGPL, Attribution required for commercial implementations, requested for everything else.
11 * @author A. Grandt <php@grandt.com>
12 * @copyright 2009-2014 A. Grandt
13 * @license GNU LGPL 2.1
14 * @link http://www.phpclasses.org/package/6110
15 * @link https://github.com/Grandt/PHPZip
21 const ZIP_LOCAL_FILE_HEADER
= "\x50\x4b\x03\x04"; // Local file header signature
22 const ZIP_CENTRAL_FILE_HEADER
= "\x50\x4b\x01\x02"; // Central file header signature
23 const ZIP_END_OF_CENTRAL_DIRECTORY
= "\x50\x4b\x05\x06\x00\x00\x00\x00"; //end of Central directory record
25 const EXT_FILE_ATTR_DIR
= 010173200020; // Permission 755 drwxr-xr-x = (((S_IFDIR | 0755) << 16) | S_DOS_D);
26 const EXT_FILE_ATTR_FILE
= 020151000040; // Permission 644 -rw-r--r-- = (((S_IFREG | 0644) << 16) | S_DOS_A);
28 const ATTR_VERSION_TO_EXTRACT
= "\x14\x00"; // Version needed to extract
29 const ATTR_MADE_BY_VERSION
= "\x1E\x03"; // Made By Version
32 const S_IFIFO
= 0010000; // named pipe (fifo)
33 const S_IFCHR
= 0020000; // character special
34 const S_IFDIR
= 0040000; // directory
35 const S_IFBLK
= 0060000; // block special
36 const S_IFREG
= 0100000; // regular
37 const S_IFLNK
= 0120000; // symbolic link
38 const S_IFSOCK
= 0140000; // socket
40 // setuid/setgid/sticky bits, the same as for chmod:
42 const S_ISUID
= 0004000; // set user id on execution
43 const S_ISGID
= 0002000; // set group id on execution
44 const S_ISTXT
= 0001000; // sticky bit
46 // And of course, the other 12 bits are for the permissions, the same as for chmod:
47 // When addding these up, you can also just write the permissions as a simgle octal number
48 // ie. 0755. The leading 0 specifies octal notation.
49 const S_IRWXU
= 0000700; // RWX mask for owner
50 const S_IRUSR
= 0000400; // R for owner
51 const S_IWUSR
= 0000200; // W for owner
52 const S_IXUSR
= 0000100; // X for owner
53 const S_IRWXG
= 0000070; // RWX mask for group
54 const S_IRGRP
= 0000040; // R for group
55 const S_IWGRP
= 0000020; // W for group
56 const S_IXGRP
= 0000010; // X for group
57 const S_IRWXO
= 0000007; // RWX mask for other
58 const S_IROTH
= 0000004; // R for other
59 const S_IWOTH
= 0000002; // W for other
60 const S_IXOTH
= 0000001; // X for other
61 const S_ISVTX
= 0001000; // save swapped text even after use
63 // Filetype, sticky and permissions are added up, and shifted 16 bits left BEFORE adding the DOS flags.
65 // DOS file type flags, we really only use the S_DOS_D flag.
67 const S_DOS_A
= 0000040; // DOS flag for Archive
68 const S_DOS_D
= 0000020; // DOS flag for Directory
69 const S_DOS_V
= 0000010; // DOS flag for Volume
70 const S_DOS_S
= 0000004; // DOS flag for System
71 const S_DOS_H
= 0000002; // DOS flag for Hidden
72 const S_DOS_R
= 0000001; // DOS flag for Read Only
74 private $zipMemoryThreshold = 1048576; // Autocreate tempfile if the zip data exceeds 1048576 bytes (1 MB)
76 private $zipData = NULL;
77 private $zipFile = NULL;
78 private $zipComment = NULL;
79 private $cdRec = array(); // central directory
81 private $isFinalized = FALSE;
82 private $addExtraField = TRUE;
84 private $streamChunkSize = 65536;
85 private $streamFilePath = NULL;
86 private $streamTimestamp = NULL;
87 private $streamFileComment = NULL;
88 private $streamFile = NULL;
89 private $streamData = NULL;
90 private $streamFileLength = 0;
91 private $streamExtFileAttr = null;
96 * @param boolean $useZipFile Write temp zip data to tempFile? Default FALSE
98 function __construct($useZipFile = FALSE) {
100 $this->zipFile
= tmpfile();
106 function __destruct() {
107 if (is_resource($this->zipFile
)) {
108 fclose($this->zipFile
);
110 $this->zipData
= NULL;
114 * Extra fields on the Zip directory records are Unix time codes needed for compatibility on the default Mac zip archive tool.
115 * These are enabled as default, as they do no harm elsewhere and only add 26 bytes per file added.
117 * @param bool $setExtraField TRUE (default) will enable adding of extra fields, anything else will disable it.
119 function setExtraField($setExtraField = TRUE) {
120 $this->addExtraField
= ($setExtraField === TRUE);
124 * Set Zip archive comment.
126 * @param string $newComment New comment. NULL to clear.
127 * @return bool $success
129 public function setComment($newComment = NULL) {
130 if ($this->isFinalized
) {
133 $this->zipComment
= $newComment;
139 * Set zip file to write zip data to.
140 * This will cause all present and future data written to this class to be written to this file.
141 * This can be used at any time, even after the Zip Archive have been finalized. Any previous file will be closed.
142 * Warning: If the given file already exists, it will be overwritten.
144 * @param string $fileName
145 * @return bool $success
147 public function setZipFile($fileName) {
148 if (is_file($fileName)) {
151 $fd=fopen($fileName, "x+b");
152 if (is_resource($this->zipFile
)) {
153 rewind($this->zipFile
);
154 while (!feof($this->zipFile
)) {
155 fwrite($fd, fread($this->zipFile
, $this->streamChunkSize
));
158 fclose($this->zipFile
);
160 fwrite($fd, $this->zipData
);
161 $this->zipData
= NULL;
163 $this->zipFile
= $fd;
169 * Add an empty directory entry to the zip archive.
170 * Basically this is only used if an empty directory is added.
172 * @param string $directoryPath Directory Path and name to be added to the archive.
173 * @param int $timestamp (Optional) Timestamp for the added directory, if omitted or set to 0, the current time will be used.
174 * @param string $fileComment (Optional) Comment to be added to the archive for this directory. To use fileComment, timestamp must be given.
175 * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this.
176 * @return bool $success
178 public function addDirectory($directoryPath, $timestamp = 0, $fileComment = NULL, $extFileAttr = self
::EXT_FILE_ATTR_DIR
) {
179 if ($this->isFinalized
) {
182 $directoryPath = str_replace("\\", "/", $directoryPath);
183 $directoryPath = rtrim($directoryPath, "/");
185 if (strlen($directoryPath) > 0) {
186 $this->buildZipEntry($directoryPath.'/', $fileComment, "\x00\x00", "\x00\x00", $timestamp, "\x00\x00\x00\x00", 0, 0, $extFileAttr);
193 * Add a file to the archive at the specified location and file name.
195 * @param string $data File data.
196 * @param string $filePath Filepath and name to be used in the archive.
197 * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used.
198 * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given.
199 * @param bool $compress (Optional) Compress file, if set to FALSE the file will only be stored. Default TRUE.
200 * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this.
201 * @return bool $success
203 public function addFile($data, $filePath, $timestamp = 0, $fileComment = NULL, $compress = TRUE, $extFileAttr = self
::EXT_FILE_ATTR_FILE
) {
204 if ($this->isFinalized
) {
208 if (is_resource($data) && get_resource_type($data) == "stream") {
209 $this->addLargeFile($data, $filePath, $timestamp, $fileComment, $extFileAttr);
214 $gzType = "\x08\x00"; // Compression type 8 = deflate
215 $gpFlags = "\x00\x00"; // General Purpose bit flags for compression type 8 it is: 0=Normal, 1=Maximum, 2=Fast, 3=super fast compression.
216 $dataLength = strlen($data);
217 $fileCRC32 = pack("V", crc32($data));
220 $gzTmp = gzcompress($data);
221 $gzData = substr(substr($gzTmp, 0, strlen($gzTmp) - 4), 2); // gzcompress adds a 2 byte header and 4 byte CRC we can't use.
222 // The 2 byte header does contain useful data, though in this case the 2 parameters we'd be interrested in will always be 8 for compression type, and 2 for General purpose flag.
223 $gzLength = strlen($gzData);
225 $gzLength = $dataLength;
228 if ($gzLength >= $dataLength) {
229 $gzLength = $dataLength;
231 $gzType = "\x00\x00"; // Compression type 0 = stored
232 $gpFlags = "\x00\x00"; // Compression type 0 = stored
235 if (!is_resource($this->zipFile
) && ($this->offset +
$gzLength) > $this->zipMemoryThreshold
) {
239 $this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr);
241 $this->zipwrite($gzData);
247 * Add the content to a directory.
249 * @author Adam Schmalhofer <Adam.Schmalhofer@gmx.de>
252 * @param string $realPath Path on the file system.
253 * @param string $zipPath Filepath and name to be used in the archive.
254 * @param bool $recursive Add content recursively, default is TRUE.
255 * @param bool $followSymlinks Follow and add symbolic links, if they are accessible, default is TRUE.
256 * @param array &$addedFiles Reference to the added files, this is used to prevent duplicates, efault is an empty array.
257 * If you start the function by parsing an array, the array will be populated with the realPath
258 * and zipPath kay/value pairs added to the archive by the function.
259 * @param bool $overrideFilePermissions Force the use of the file/dir permissions set in the $extDirAttr
260 * and $extFileAttr parameters.
261 * @param int $extDirAttr Permissions for directories.
262 * @param int $extFileAttr Permissions for files.
264 public function addDirectoryContent($realPath, $zipPath, $recursive = TRUE, $followSymlinks = TRUE, &$addedFiles = array(),
265 $overrideFilePermissions = FALSE, $extDirAttr = self
::EXT_FILE_ATTR_DIR
, $extFileAttr = self
::EXT_FILE_ATTR_FILE
) {
266 if (file_exists($realPath) && !isset($addedFiles[realpath($realPath)])) {
267 if (is_dir($realPath)) {
268 if ($overrideFilePermissions) {
269 $this->addDirectory($zipPath, 0, null, $extDirAttr);
271 $this->addDirectory($zipPath, 0, null, self
::getFileExtAttr($realPath));
275 $addedFiles[realpath($realPath)] = $zipPath;
277 $iter = new DirectoryIterator($realPath);
278 foreach ($iter as $file) {
279 if ($file->isDot()) {
282 $newRealPath = $file->getPathname();
283 $newZipPath = self
::pathJoin($zipPath, $file->getFilename());
285 if (file_exists($newRealPath) && ($followSymlinks === TRUE || !is_link($newRealPath))) {
286 if ($file->isFile()) {
287 $addedFiles[realpath($newRealPath)] = $newZipPath;
288 if ($overrideFilePermissions) {
289 $this->addLargeFile($newRealPath, $newZipPath, 0, null, $extFileAttr);
291 $this->addLargeFile($newRealPath, $newZipPath, 0, null, self
::getFileExtAttr($newRealPath));
293 } else if ($recursive === TRUE) {
294 $this->addDirectoryContent($newRealPath, $newZipPath, $recursive, $followSymlinks, $addedFiles, $overrideFilePermissions, $extDirAttr, $extFileAttr);
296 if ($overrideFilePermissions) {
297 $this->addDirectory($zipPath, 0, null, $extDirAttr);
299 $this->addDirectory($zipPath, 0, null, self
::getFileExtAttr($newRealPath));
308 * Add a file to the archive at the specified location and file name.
310 * @param string $dataFile File name/path.
311 * @param string $filePath Filepath and name to be used in the archive.
312 * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used.
313 * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given.
314 * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this.
315 * @return bool $success
317 public function addLargeFile($dataFile, $filePath, $timestamp = 0, $fileComment = NULL, $extFileAttr = self
::EXT_FILE_ATTR_FILE
) {
318 if ($this->isFinalized
) {
322 if (is_string($dataFile) && is_file($dataFile)) {
323 $this->processFile($dataFile, $filePath, $timestamp, $fileComment, $extFileAttr);
324 } else if (is_resource($dataFile) && get_resource_type($dataFile) == "stream") {
326 $this->openStream($filePath, $timestamp, $fileComment, $extFileAttr);
329 $this->addStreamData(fread($fh, $this->streamChunkSize
));
331 $this->closeStream($this->addExtraField
);
337 * Create a stream to be used for large entries.
339 * @param string $filePath Filepath and name to be used in the archive.
340 * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used.
341 * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given.
342 * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this.
343 * @return bool $success
345 public function openStream($filePath, $timestamp = 0, $fileComment = null, $extFileAttr = self
::EXT_FILE_ATTR_FILE
) {
346 if (!function_exists('sys_get_temp_dir')) {
347 die ("ERROR: Zip " . self
::VERSION
. " requires PHP version 5.2.1 or above if large files are used.");
350 if ($this->isFinalized
) {
356 if (strlen($this->streamFilePath
) > 0) {
357 $this->closeStream();
360 $this->streamFile
= tempnam(sys_get_temp_dir(), 'Zip');
361 $this->streamData
= fopen($this->streamFile
, "wb");
362 $this->streamFilePath
= $filePath;
363 $this->streamTimestamp
= $timestamp;
364 $this->streamFileComment
= $fileComment;
365 $this->streamFileLength
= 0;
366 $this->streamExtFileAttr
= $extFileAttr;
372 * Add data to the open stream.
374 * @param string $data
375 * @return mixed length in bytes added or FALSE if the archive is finalized or there are no open stream.
377 public function addStreamData($data) {
378 if ($this->isFinalized
|| strlen($this->streamFilePath
) == 0) {
382 $length = fwrite($this->streamData
, $data, strlen($data));
383 if ($length != strlen($data)) {
384 die ("<p>Length mismatch</p>\n");
386 $this->streamFileLength +
= $length;
392 * Close the current stream.
394 * @return bool $success
396 public function closeStream() {
397 if ($this->isFinalized
|| strlen($this->streamFilePath
) == 0) {
401 fflush($this->streamData
);
402 fclose($this->streamData
);
404 $this->processFile($this->streamFile
, $this->streamFilePath
, $this->streamTimestamp
, $this->streamFileComment
, $this->streamExtFileAttr
);
406 $this->streamData
= null;
407 $this->streamFilePath
= null;
408 $this->streamTimestamp
= null;
409 $this->streamFileComment
= null;
410 $this->streamFileLength
= 0;
411 $this->streamExtFileAttr
= null;
413 // Windows is a little slow at times, so a millisecond later, we can unlink this.
414 unlink($this->streamFile
);
416 $this->streamFile
= null;
421 private function processFile($dataFile, $filePath, $timestamp = 0, $fileComment = null, $extFileAttr = self
::EXT_FILE_ATTR_FILE
) {
422 if ($this->isFinalized
) {
426 $tempzip = tempnam(sys_get_temp_dir(), 'ZipStream');
428 $zip = new ZipArchive
;
429 if ($zip->open($tempzip) === TRUE) {
430 $zip->addFile($dataFile, 'file');
434 $file_handle = fopen($tempzip, "rb");
435 $stats = fstat($file_handle);
436 $eof = $stats['size']-72;
438 fseek($file_handle, 6);
440 $gpFlags = fread($file_handle, 2);
441 $gzType = fread($file_handle, 2);
442 fread($file_handle, 4);
443 $fileCRC32 = fread($file_handle, 4);
444 $v = unpack("Vval", fread($file_handle, 4));
445 $gzLength = $v['val'];
446 $v = unpack("Vval", fread($file_handle, 4));
447 $dataLength = $v['val'];
449 $this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr);
451 fseek($file_handle, 34);
454 while (!feof($file_handle) && $pos < $eof) {
455 $datalen = $this->streamChunkSize
;
456 if ($pos +
$this->streamChunkSize
> $eof) {
457 $datalen = $eof-$pos;
459 $data = fread($file_handle, $datalen);
462 $this->zipwrite($data);
465 fclose($file_handle);
472 * A closed archive can no longer have new files added to it.
474 * @return bool $success
476 public function finalize() {
477 if (!$this->isFinalized
) {
478 if (strlen($this->streamFilePath
) > 0) {
479 $this->closeStream();
481 $cd = implode("", $this->cdRec
);
483 $cdRecSize = pack("v", sizeof($this->cdRec
));
484 $cdRec = $cd . self
::ZIP_END_OF_CENTRAL_DIRECTORY
485 . $cdRecSize . $cdRecSize
486 . pack("VV", strlen($cd), $this->offset
);
487 if (!empty($this->zipComment
)) {
488 $cdRec .= pack("v", strlen($this->zipComment
)) . $this->zipComment
;
490 $cdRec .= "\x00\x00";
493 $this->zipwrite($cdRec);
495 $this->isFinalized
= TRUE;
504 * Get the handle ressource for the archive zip file.
505 * If the zip haven't been finalized yet, this will cause it to become finalized
507 * @return zip file handle
509 public function getZipFile() {
510 if (!$this->isFinalized
) {
516 rewind($this->zipFile
);
518 return $this->zipFile
;
522 * Get the zip file contents
523 * If the zip haven't been finalized yet, this will cause it to become finalized
527 public function getZipData() {
528 if (!$this->isFinalized
) {
531 if (!is_resource($this->zipFile
)) {
532 return $this->zipData
;
534 rewind($this->zipFile
);
535 $filestat = fstat($this->zipFile
);
536 return fread($this->zipFile
, $filestat['size']);
541 * Send the archive as a zip download
543 * @param String $fileName The name of the Zip archive, in ISO-8859-1 (or ASCII) encoding, ie. "archive.zip". Optional, defaults to NULL, which means that no ISO-8859-1 encoded file name will be specified.
544 * @param String $contentType Content mime type. Optional, defaults to "application/zip".
545 * @param String $utf8FileName The name of the Zip archive, in UTF-8 encoding. Optional, defaults to NULL, which means that no UTF-8 encoded file name will be specified.
546 * @param bool $inline Use Content-Disposition with "inline" instead of "attached". Optional, defaults to FALSE.
547 * @return bool $success
549 function sendZip($fileName = null, $contentType = "application/zip", $utf8FileName = null, $inline = false) {
550 if (!$this->isFinalized
) {
556 if (!headers_sent($headerFile, $headerLine) or die("<p><strong>Error:</strong> Unable to send file $fileName. HTML Headers have already been sent from <strong>$headerFile</strong> in line <strong>$headerLine</strong></p>")) {
557 if ((ob_get_contents() === FALSE || ob_get_contents() == '') or die("\n<p><strong>Error:</strong> Unable to send file <strong>$fileName</strong>. Output buffer contains the following text (typically warnings or errors):<br>" . htmlentities(ob_get_contents()) . "</p>")) {
558 if (ini_get('zlib.output_compression')) {
559 ini_set('zlib.output_compression', 'Off');
562 header("Pragma: public");
563 header("Last-Modified: " . gmdate("D, d M Y H:i:s T"));
564 header("Expires: 0");
565 header("Accept-Ranges: bytes");
566 header("Connection: close");
567 header("Content-Type: " . $contentType);
568 $cd = "Content-Disposition: ";
575 $cd .= '; filename="' . $fileName . '"';
578 $cd .= "; filename*=UTF-8''" . rawurlencode($utf8FileName);
581 header("Content-Length: ". $this->getArchiveSize());
583 if (!is_resource($this->zipFile
)) {
586 rewind($this->zipFile
);
588 while (!feof($this->zipFile
)) {
589 echo fread($this->zipFile
, $this->streamChunkSize
);
599 * Return the current size of the archive
601 * @return $size Size of the archive
603 public function getArchiveSize() {
604 if (!is_resource($this->zipFile
)) {
605 return strlen($this->zipData
);
607 $filestat = fstat($this->zipFile
);
609 return $filestat['size'];
613 * Calculate the 2 byte dostime used in the zip entries.
615 * @param int $timestamp
616 * @return 2-byte encoded DOS Date
618 private function getDosTime($timestamp = 0) {
619 $timestamp = (int)$timestamp;
620 $oldTZ = @date_default_timezone_get();
621 date_default_timezone_set('UTC');
622 $date = ($timestamp == 0 ? getdate() : getdate($timestamp));
623 date_default_timezone_set($oldTZ);
624 if ($date["year"] >= 1980) {
625 return pack("V", (($date["mday"] +
($date["mon"] << 5) +
(($date["year"]-1980) << 9)) << 16) |
626 (($date["seconds"] >> 1) +
($date["minutes"] << 5) +
($date["hours"] << 11)));
628 return "\x00\x00\x00\x00";
632 * Build the Zip file structures
634 * @param string $filePath
635 * @param string $fileComment
636 * @param string $gpFlags
637 * @param string $gzType
638 * @param int $timestamp
639 * @param string $fileCRC32
640 * @param int $gzLength
641 * @param int $dataLength
642 * @param int $extFileAttr Use self::EXT_FILE_ATTR_FILE for files, self::EXT_FILE_ATTR_DIR for Directories.
644 private function buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr) {
645 $filePath = str_replace("\\", "/", $filePath);
646 $fileCommentLength = (empty($fileComment) ? 0 : strlen($fileComment));
647 $timestamp = (int)$timestamp;
648 $timestamp = ($timestamp == 0 ? time() : $timestamp);
650 $dosTime = $this->getDosTime($timestamp);
651 $tsPack = pack("V", $timestamp);
653 $ux = "\x75\x78\x0B\x00\x01\x04\xE8\x03\x00\x00\x04\x00\x00\x00\x00";
655 if (!isset($gpFlags) || strlen($gpFlags) != 2) {
656 $gpFlags = "\x00\x00";
659 $isFileUTF8 = mb_check_encoding($filePath, "UTF-8") && !mb_check_encoding($filePath, "ASCII");
660 $isCommentUTF8 = !empty($fileComment) && mb_check_encoding($fileComment, "UTF-8") && !mb_check_encoding($fileComment, "ASCII");
661 if ($isFileUTF8 || $isCommentUTF8) {
663 $gpFlagsV = unpack("vflags", $gpFlags);
664 if (isset($gpFlagsV['flags'])) {
665 $flag = $gpFlagsV['flags'];
667 $gpFlags = pack("v", $flag | (1 << 11));
670 $header = $gpFlags . $gzType . $dosTime. $fileCRC32
671 . pack("VVv", $gzLength, $dataLength, strlen($filePath)); // File name length
673 $zipEntry = self
::ZIP_LOCAL_FILE_HEADER
;
674 $zipEntry .= self
::ATTR_VERSION_TO_EXTRACT
;
675 $zipEntry .= $header;
676 $zipEntry .= pack("v", ($this->addExtraField
? 28 : 0)); // Extra field length
677 $zipEntry .= $filePath; // FileName
679 if ($this->addExtraField
) {
680 $zipEntry .= "\x55\x54\x09\x00\x03" . $tsPack . $tsPack . $ux;
682 $this->zipwrite($zipEntry);
684 $cdEntry = self
::ZIP_CENTRAL_FILE_HEADER
;
685 $cdEntry .= self
::ATTR_MADE_BY_VERSION
;
686 $cdEntry .= ($dataLength === 0 ? "\x0A\x00" : self
::ATTR_VERSION_TO_EXTRACT
);
688 $cdEntry .= pack("v", ($this->addExtraField
? 24 : 0)); // Extra field length
689 $cdEntry .= pack("v", $fileCommentLength); // File comment length
690 $cdEntry .= "\x00\x00"; // Disk number start
691 $cdEntry .= "\x00\x00"; // internal file attributes
692 $cdEntry .= pack("V", $extFileAttr); // External file attributes
693 $cdEntry .= pack("V", $this->offset
); // Relative offset of local header
694 $cdEntry .= $filePath; // FileName
696 if ($this->addExtraField
) {
697 $cdEntry .= "\x55\x54\x05\x00\x03" . $tsPack . $ux;
699 if (!empty($fileComment)) {
700 $cdEntry .= $fileComment; // Comment
703 $this->cdRec
[] = $cdEntry;
704 $this->offset +
= strlen($zipEntry) +
$gzLength;
707 private function zipwrite($data) {
708 if (!is_resource($this->zipFile
)) {
709 $this->zipData
.= $data;
711 fwrite($this->zipFile
, $data);
712 fflush($this->zipFile
);
716 private function zipflush() {
717 if (!is_resource($this->zipFile
)) {
718 $this->zipFile
= tmpfile();
719 fwrite($this->zipFile
, $this->zipData
);
720 $this->zipData
= NULL;
725 * Join $file to $dir path, and clean up any excess slashes.
728 * @param string $file
730 public static function pathJoin($dir, $file) {
731 if (empty($dir) || empty($file)) {
732 return self
::getRelativePath($dir . $file);
734 return self
::getRelativePath($dir . '/' . $file);
738 * Clean up a path, removing any unnecessary elements such as /./, // or redundant ../ segments.
739 * If the path starts with a "/", it is deemed an absolute path and any /../ in the beginning is stripped off.
740 * The returned path will not end in a "/".
742 * Sometimes, when a path is generated from multiple fragments,
743 * you can get something like "../data/html/../images/image.jpeg"
744 * This will normalize that example path to "../data/images/image.jpeg"
746 * @param string $path The path to clean up
747 * @return string the clean path
749 public static function getRelativePath($path) {
750 $path = preg_replace("#/+\.?/+#", "/", str_replace("\\", "/", $path));
751 $dirs = explode("/", rtrim(preg_replace('#^(?:\./)+#', '', $path), '/'));
758 if (empty($dirs[0])) {
760 $dirs = array_splice($dirs, 1);
761 } else if (preg_match("#[A-Za-z]:#", $dirs[0])) {
762 $root = strtoupper($dirs[0]) . "/";
763 $dirs = array_splice($dirs, 1);
767 foreach ($dirs as $dir) {
770 $newDirs[++
$offset] = $dir;
775 if ($subOffset > $sub) {
783 $root = str_repeat("../", $sub);
785 return $root . implode("/", array_slice($newDirs, 0, $offset));
789 * Create the file permissions for a file or directory, for use in the extFileAttr parameters.
791 * @param int $owner Unix permisions for owner (octal from 00 to 07)
792 * @param int $group Unix permisions for group (octal from 00 to 07)
793 * @param int $other Unix permisions for others (octal from 00 to 07)
794 * @param bool $isFile
795 * @return EXTRERNAL_REF field.
797 public static function generateExtAttr($owner = 07, $group = 05, $other = 05, $isFile = true) {
798 $fp = $isFile ? self
::S_IFREG
: self
::S_IFDIR
;
799 $fp |= (($owner & 07) << 6) | (($group & 07) << 3) | ($other & 07);
801 return ($fp << 16) | ($isFile ? self
::S_DOS_A
: self
::S_DOS_D
);
805 * Get the file permissions for a file or directory, for use in the extFileAttr parameters.
807 * @param string $filename
808 * @return external ref field, or FALSE if the file is not found.
810 public static function getFileExtAttr($filename) {
811 if (file_exists($filename)) {
812 $fp = fileperms($filename) << 16;
813 return $fp | (is_dir($filename) ? self
::S_DOS_D
: self
::S_DOS_A
);