]>
Commit | Line | Data |
---|---|---|
1 | <?php | |
2 | /** | |
3 | * Class to create and manage a Zip file. | |
4 | * | |
5 | * Initially inspired by CreateZipFile by Rochak Chauhan www.rochakchauhan.com (http://www.phpclasses.org/browse/package/2322.html) | |
6 | * and | |
7 | * http://www.pkware.com/documents/casestudies/APPNOTE.TXT Zip file specification. | |
8 | * | |
9 | * License: GNU LGPL, Attribution required for commercial implementations, requested for everything else. | |
10 | * | |
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 | |
16 | * @version 1.60 | |
17 | */ | |
18 | class Zip { | |
19 | const VERSION = 1.60; | |
20 | ||
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 | |
24 | ||
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); | |
27 | ||
28 | const ATTR_VERSION_TO_EXTRACT = "\x14\x00"; // Version needed to extract | |
29 | const ATTR_MADE_BY_VERSION = "\x1E\x03"; // Made By Version | |
30 | ||
31 | // Unix file types | |
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 | |
39 | ||
40 | // setuid/setgid/sticky bits, the same as for chmod: | |
41 | ||
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 | |
45 | ||
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 | |
62 | ||
63 | // Filetype, sticky and permissions are added up, and shifted 16 bits left BEFORE adding the DOS flags. | |
64 | ||
65 | // DOS file type flags, we really only use the S_DOS_D flag. | |
66 | ||
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 | |
73 | ||
74 | private $zipMemoryThreshold = 1048576; // Autocreate tempfile if the zip data exceeds 1048576 bytes (1 MB) | |
75 | ||
76 | private $zipData = NULL; | |
77 | private $zipFile = NULL; | |
78 | private $zipComment = NULL; | |
79 | private $cdRec = array(); // central directory | |
80 | private $offset = 0; | |
81 | private $isFinalized = FALSE; | |
82 | private $addExtraField = TRUE; | |
83 | ||
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; | |
92 | ||
93 | /** | |
94 | * Constructor. | |
95 | * | |
96 | * @param boolean $useZipFile Write temp zip data to tempFile? Default FALSE | |
97 | */ | |
98 | function __construct($useZipFile = FALSE) { | |
99 | if ($useZipFile) { | |
100 | $this->zipFile = tmpfile(); | |
101 | } else { | |
102 | $this->zipData = ""; | |
103 | } | |
104 | } | |
105 | ||
106 | function __destruct() { | |
107 | if (is_resource($this->zipFile)) { | |
108 | fclose($this->zipFile); | |
109 | } | |
110 | $this->zipData = NULL; | |
111 | } | |
112 | ||
113 | /** | |
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. | |
116 | * | |
117 | * @param bool $setExtraField TRUE (default) will enable adding of extra fields, anything else will disable it. | |
118 | */ | |
119 | function setExtraField($setExtraField = TRUE) { | |
120 | $this->addExtraField = ($setExtraField === TRUE); | |
121 | } | |
122 | ||
123 | /** | |
124 | * Set Zip archive comment. | |
125 | * | |
126 | * @param string $newComment New comment. NULL to clear. | |
127 | * @return bool $success | |
128 | */ | |
129 | public function setComment($newComment = NULL) { | |
130 | if ($this->isFinalized) { | |
131 | return FALSE; | |
132 | } | |
133 | $this->zipComment = $newComment; | |
134 | ||
135 | return TRUE; | |
136 | } | |
137 | ||
138 | /** | |
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. | |
143 | * | |
144 | * @param string $fileName | |
145 | * @return bool $success | |
146 | */ | |
147 | public function setZipFile($fileName) { | |
148 | if (is_file($fileName)) { | |
149 | unlink($fileName); | |
150 | } | |
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)); | |
156 | } | |
157 | ||
158 | fclose($this->zipFile); | |
159 | } else { | |
160 | fwrite($fd, $this->zipData); | |
161 | $this->zipData = NULL; | |
162 | } | |
163 | $this->zipFile = $fd; | |
164 | ||
165 | return TRUE; | |
166 | } | |
167 | ||
168 | /** | |
169 | * Add an empty directory entry to the zip archive. | |
170 | * Basically this is only used if an empty directory is added. | |
171 | * | |
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 | |
177 | */ | |
178 | public function addDirectory($directoryPath, $timestamp = 0, $fileComment = NULL, $extFileAttr = self::EXT_FILE_ATTR_DIR) { | |
179 | if ($this->isFinalized) { | |
180 | return FALSE; | |
181 | } | |
182 | $directoryPath = str_replace("\\", "/", $directoryPath); | |
183 | $directoryPath = rtrim($directoryPath, "/"); | |
184 | ||
185 | if (strlen($directoryPath) > 0) { | |
186 | $this->buildZipEntry($directoryPath.'/', $fileComment, "\x00\x00", "\x00\x00", $timestamp, "\x00\x00\x00\x00", 0, 0, $extFileAttr); | |
187 | return TRUE; | |
188 | } | |
189 | return FALSE; | |
190 | } | |
191 | ||
192 | /** | |
193 | * Add a file to the archive at the specified location and file name. | |
194 | * | |
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 | |
202 | */ | |
203 | public function addFile($data, $filePath, $timestamp = 0, $fileComment = NULL, $compress = TRUE, $extFileAttr = self::EXT_FILE_ATTR_FILE) { | |
204 | if ($this->isFinalized) { | |
205 | return FALSE; | |
206 | } | |
207 | ||
208 | if (is_resource($data) && get_resource_type($data) == "stream") { | |
209 | $this->addLargeFile($data, $filePath, $timestamp, $fileComment, $extFileAttr); | |
210 | return FALSE; | |
211 | } | |
212 | ||
213 | $gzData = ""; | |
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)); | |
218 | ||
219 | if ($compress) { | |
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); | |
224 | } else { | |
225 | $gzLength = $dataLength; | |
226 | } | |
227 | ||
228 | if ($gzLength >= $dataLength) { | |
229 | $gzLength = $dataLength; | |
230 | $gzData = $data; | |
231 | $gzType = "\x00\x00"; // Compression type 0 = stored | |
232 | $gpFlags = "\x00\x00"; // Compression type 0 = stored | |
233 | } | |
234 | ||
235 | if (!is_resource($this->zipFile) && ($this->offset + $gzLength) > $this->zipMemoryThreshold) { | |
236 | $this->zipflush(); | |
237 | } | |
238 | ||
239 | $this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr); | |
240 | ||
241 | $this->zipwrite($gzData); | |
242 | ||
243 | return TRUE; | |
244 | } | |
245 | ||
246 | /** | |
247 | * Add the content to a directory. | |
248 | * | |
249 | * @author Adam Schmalhofer <Adam.Schmalhofer@gmx.de> | |
250 | * @author A. Grandt | |
251 | * | |
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. | |
263 | */ | |
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); | |
270 | } else { | |
271 | $this->addDirectory($zipPath, 0, null, self::getFileExtAttr($realPath)); | |
272 | } | |
273 | } | |
274 | ||
275 | $addedFiles[realpath($realPath)] = $zipPath; | |
276 | ||
277 | $iter = new DirectoryIterator($realPath); | |
278 | foreach ($iter as $file) { | |
279 | if ($file->isDot()) { | |
280 | continue; | |
281 | } | |
282 | $newRealPath = $file->getPathname(); | |
283 | $newZipPath = self::pathJoin($zipPath, $file->getFilename()); | |
284 | ||
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); | |
290 | } else { | |
291 | $this->addLargeFile($newRealPath, $newZipPath, 0, null, self::getFileExtAttr($newRealPath)); | |
292 | } | |
293 | } else if ($recursive === TRUE) { | |
294 | $this->addDirectoryContent($newRealPath, $newZipPath, $recursive, $followSymlinks, $addedFiles, $overrideFilePermissions, $extDirAttr, $extFileAttr); | |
295 | } else { | |
296 | if ($overrideFilePermissions) { | |
297 | $this->addDirectory($zipPath, 0, null, $extDirAttr); | |
298 | } else { | |
299 | $this->addDirectory($zipPath, 0, null, self::getFileExtAttr($newRealPath)); | |
300 | } | |
301 | } | |
302 | } | |
303 | } | |
304 | } | |
305 | } | |
306 | ||
307 | /** | |
308 | * Add a file to the archive at the specified location and file name. | |
309 | * | |
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 | |
316 | */ | |
317 | public function addLargeFile($dataFile, $filePath, $timestamp = 0, $fileComment = NULL, $extFileAttr = self::EXT_FILE_ATTR_FILE) { | |
318 | if ($this->isFinalized) { | |
319 | return FALSE; | |
320 | } | |
321 | ||
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") { | |
325 | $fh = $dataFile; | |
326 | $this->openStream($filePath, $timestamp, $fileComment, $extFileAttr); | |
327 | ||
328 | while (!feof($fh)) { | |
329 | $this->addStreamData(fread($fh, $this->streamChunkSize)); | |
330 | } | |
331 | $this->closeStream($this->addExtraField); | |
332 | } | |
333 | return TRUE; | |
334 | } | |
335 | ||
336 | /** | |
337 | * Create a stream to be used for large entries. | |
338 | * | |
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 | |
344 | */ | |
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."); | |
348 | } | |
349 | ||
350 | if ($this->isFinalized) { | |
351 | return FALSE; | |
352 | } | |
353 | ||
354 | $this->zipflush(); | |
355 | ||
356 | if (strlen($this->streamFilePath) > 0) { | |
357 | $this->closeStream(); | |
358 | } | |
359 | ||
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; | |
367 | ||
368 | return TRUE; | |
369 | } | |
370 | ||
371 | /** | |
372 | * Add data to the open stream. | |
373 | * | |
374 | * @param string $data | |
375 | * @return mixed length in bytes added or FALSE if the archive is finalized or there are no open stream. | |
376 | */ | |
377 | public function addStreamData($data) { | |
378 | if ($this->isFinalized || strlen($this->streamFilePath) == 0) { | |
379 | return FALSE; | |
380 | } | |
381 | ||
382 | $length = fwrite($this->streamData, $data, strlen($data)); | |
383 | if ($length != strlen($data)) { | |
384 | die ("<p>Length mismatch</p>\n"); | |
385 | } | |
386 | $this->streamFileLength += $length; | |
387 | ||
388 | return $length; | |
389 | } | |
390 | ||
391 | /** | |
392 | * Close the current stream. | |
393 | * | |
394 | * @return bool $success | |
395 | */ | |
396 | public function closeStream() { | |
397 | if ($this->isFinalized || strlen($this->streamFilePath) == 0) { | |
398 | return FALSE; | |
399 | } | |
400 | ||
401 | fflush($this->streamData); | |
402 | fclose($this->streamData); | |
403 | ||
404 | $this->processFile($this->streamFile, $this->streamFilePath, $this->streamTimestamp, $this->streamFileComment, $this->streamExtFileAttr); | |
405 | ||
406 | $this->streamData = null; | |
407 | $this->streamFilePath = null; | |
408 | $this->streamTimestamp = null; | |
409 | $this->streamFileComment = null; | |
410 | $this->streamFileLength = 0; | |
411 | $this->streamExtFileAttr = null; | |
412 | ||
413 | // Windows is a little slow at times, so a millisecond later, we can unlink this. | |
414 | unlink($this->streamFile); | |
415 | ||
416 | $this->streamFile = null; | |
417 | ||
418 | return TRUE; | |
419 | } | |
420 | ||
421 | private function processFile($dataFile, $filePath, $timestamp = 0, $fileComment = null, $extFileAttr = self::EXT_FILE_ATTR_FILE) { | |
422 | if ($this->isFinalized) { | |
423 | return FALSE; | |
424 | } | |
425 | ||
426 | $tempzip = tempnam(sys_get_temp_dir(), 'ZipStream'); | |
427 | ||
428 | $zip = new ZipArchive; | |
429 | if ($zip->open($tempzip) === TRUE) { | |
430 | $zip->addFile($dataFile, 'file'); | |
431 | $zip->close(); | |
432 | } | |
433 | ||
434 | $file_handle = fopen($tempzip, "rb"); | |
435 | $stats = fstat($file_handle); | |
436 | $eof = $stats['size']-72; | |
437 | ||
438 | fseek($file_handle, 6); | |
439 | ||
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']; | |
448 | ||
449 | $this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr); | |
450 | ||
451 | fseek($file_handle, 34); | |
452 | $pos = 34; | |
453 | ||
454 | while (!feof($file_handle) && $pos < $eof) { | |
455 | $datalen = $this->streamChunkSize; | |
456 | if ($pos + $this->streamChunkSize > $eof) { | |
457 | $datalen = $eof-$pos; | |
458 | } | |
459 | $data = fread($file_handle, $datalen); | |
460 | $pos += $datalen; | |
461 | ||
462 | $this->zipwrite($data); | |
463 | } | |
464 | ||
465 | fclose($file_handle); | |
466 | ||
467 | unlink($tempzip); | |
468 | } | |
469 | ||
470 | /** | |
471 | * Close the archive. | |
472 | * A closed archive can no longer have new files added to it. | |
473 | * | |
474 | * @return bool $success | |
475 | */ | |
476 | public function finalize() { | |
477 | if (!$this->isFinalized) { | |
478 | if (strlen($this->streamFilePath) > 0) { | |
479 | $this->closeStream(); | |
480 | } | |
481 | $cd = implode("", $this->cdRec); | |
482 | ||
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; | |
489 | } else { | |
490 | $cdRec .= "\x00\x00"; | |
491 | } | |
492 | ||
493 | $this->zipwrite($cdRec); | |
494 | ||
495 | $this->isFinalized = TRUE; | |
496 | $this->cdRec = NULL; | |
497 | ||
498 | return TRUE; | |
499 | } | |
500 | return FALSE; | |
501 | } | |
502 | ||
503 | /** | |
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 | |
506 | * | |
507 | * @return zip file handle | |
508 | */ | |
509 | public function getZipFile() { | |
510 | if (!$this->isFinalized) { | |
511 | $this->finalize(); | |
512 | } | |
513 | ||
514 | $this->zipflush(); | |
515 | ||
516 | rewind($this->zipFile); | |
517 | ||
518 | return $this->zipFile; | |
519 | } | |
520 | ||
521 | /** | |
522 | * Get the zip file contents | |
523 | * If the zip haven't been finalized yet, this will cause it to become finalized | |
524 | * | |
525 | * @return zip data | |
526 | */ | |
527 | public function getZipData() { | |
528 | if (!$this->isFinalized) { | |
529 | $this->finalize(); | |
530 | } | |
531 | if (!is_resource($this->zipFile)) { | |
532 | return $this->zipData; | |
533 | } else { | |
534 | rewind($this->zipFile); | |
535 | $filestat = fstat($this->zipFile); | |
536 | return fread($this->zipFile, $filestat['size']); | |
537 | } | |
538 | } | |
539 | ||
540 | /** | |
541 | * Send the archive as a zip download | |
542 | * | |
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 | |
548 | */ | |
549 | function sendZip($fileName = null, $contentType = "application/zip", $utf8FileName = null, $inline = false) { | |
550 | if (!$this->isFinalized) { | |
551 | $this->finalize(); | |
552 | } | |
553 | ||
554 | $headerFile = null; | |
555 | $headerLine = null; | |
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'); | |
560 | } | |
561 | ||
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: "; | |
569 | if ($inline) { | |
570 | $cd .= "inline"; | |
571 | } else{ | |
572 | $cd .= "attached"; | |
573 | } | |
574 | if ($fileName) { | |
575 | $cd .= '; filename="' . $fileName . '"'; | |
576 | } | |
577 | if ($utf8FileName) { | |
578 | $cd .= "; filename*=UTF-8''" . rawurlencode($utf8FileName); | |
579 | } | |
580 | header($cd); | |
581 | header("Content-Length: ". $this->getArchiveSize()); | |
582 | ||
583 | if (!is_resource($this->zipFile)) { | |
584 | echo $this->zipData; | |
585 | } else { | |
586 | rewind($this->zipFile); | |
587 | ||
588 | while (!feof($this->zipFile)) { | |
589 | echo fread($this->zipFile, $this->streamChunkSize); | |
590 | } | |
591 | } | |
592 | } | |
593 | return TRUE; | |
594 | } | |
595 | return FALSE; | |
596 | } | |
597 | ||
598 | /** | |
599 | * Return the current size of the archive | |
600 | * | |
601 | * @return $size Size of the archive | |
602 | */ | |
603 | public function getArchiveSize() { | |
604 | if (!is_resource($this->zipFile)) { | |
605 | return strlen($this->zipData); | |
606 | } | |
607 | $filestat = fstat($this->zipFile); | |
608 | ||
609 | return $filestat['size']; | |
610 | } | |
611 | ||
612 | /** | |
613 | * Calculate the 2 byte dostime used in the zip entries. | |
614 | * | |
615 | * @param int $timestamp | |
616 | * @return 2-byte encoded DOS Date | |
617 | */ | |
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))); | |
627 | } | |
628 | return "\x00\x00\x00\x00"; | |
629 | } | |
630 | ||
631 | /** | |
632 | * Build the Zip file structures | |
633 | * | |
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. | |
643 | */ | |
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); | |
649 | ||
650 | $dosTime = $this->getDosTime($timestamp); | |
651 | $tsPack = pack("V", $timestamp); | |
652 | ||
653 | $ux = "\x75\x78\x0B\x00\x01\x04\xE8\x03\x00\x00\x04\x00\x00\x00\x00"; | |
654 | ||
655 | if (!isset($gpFlags) || strlen($gpFlags) != 2) { | |
656 | $gpFlags = "\x00\x00"; | |
657 | } | |
658 | ||
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) { | |
662 | $flag = 0; | |
663 | $gpFlagsV = unpack("vflags", $gpFlags); | |
664 | if (isset($gpFlagsV['flags'])) { | |
665 | $flag = $gpFlagsV['flags']; | |
666 | } | |
667 | $gpFlags = pack("v", $flag | (1 << 11)); | |
668 | } | |
669 | ||
670 | $header = $gpFlags . $gzType . $dosTime. $fileCRC32 | |
671 | . pack("VVv", $gzLength, $dataLength, strlen($filePath)); // File name length | |
672 | ||
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 | |
678 | // Extra fields | |
679 | if ($this->addExtraField) { | |
680 | $zipEntry .= "\x55\x54\x09\x00\x03" . $tsPack . $tsPack . $ux; | |
681 | } | |
682 | $this->zipwrite($zipEntry); | |
683 | ||
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); | |
687 | $cdEntry .= $header; | |
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 | |
695 | // Extra fields | |
696 | if ($this->addExtraField) { | |
697 | $cdEntry .= "\x55\x54\x05\x00\x03" . $tsPack . $ux; | |
698 | } | |
699 | if (!empty($fileComment)) { | |
700 | $cdEntry .= $fileComment; // Comment | |
701 | } | |
702 | ||
703 | $this->cdRec[] = $cdEntry; | |
704 | $this->offset += strlen($zipEntry) + $gzLength; | |
705 | } | |
706 | ||
707 | private function zipwrite($data) { | |
708 | if (!is_resource($this->zipFile)) { | |
709 | $this->zipData .= $data; | |
710 | } else { | |
711 | fwrite($this->zipFile, $data); | |
712 | fflush($this->zipFile); | |
713 | } | |
714 | } | |
715 | ||
716 | private function zipflush() { | |
717 | if (!is_resource($this->zipFile)) { | |
718 | $this->zipFile = tmpfile(); | |
719 | fwrite($this->zipFile, $this->zipData); | |
720 | $this->zipData = NULL; | |
721 | } | |
722 | } | |
723 | ||
724 | /** | |
725 | * Join $file to $dir path, and clean up any excess slashes. | |
726 | * | |
727 | * @param string $dir | |
728 | * @param string $file | |
729 | */ | |
730 | public static function pathJoin($dir, $file) { | |
731 | if (empty($dir) || empty($file)) { | |
732 | return self::getRelativePath($dir . $file); | |
733 | } | |
734 | return self::getRelativePath($dir . '/' . $file); | |
735 | } | |
736 | ||
737 | /** | |
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 "/". | |
741 | * | |
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" | |
745 | * | |
746 | * @param string $path The path to clean up | |
747 | * @return string the clean path | |
748 | */ | |
749 | public static function getRelativePath($path) { | |
750 | $path = preg_replace("#/+\.?/+#", "/", str_replace("\\", "/", $path)); | |
751 | $dirs = explode("/", rtrim(preg_replace('#^(?:\./)+#', '', $path), '/')); | |
752 | ||
753 | $offset = 0; | |
754 | $sub = 0; | |
755 | $subOffset = 0; | |
756 | $root = ""; | |
757 | ||
758 | if (empty($dirs[0])) { | |
759 | $root = "/"; | |
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); | |
764 | } | |
765 | ||
766 | $newDirs = array(); | |
767 | foreach ($dirs as $dir) { | |
768 | if ($dir !== "..") { | |
769 | $subOffset--; | |
770 | $newDirs[++$offset] = $dir; | |
771 | } else { | |
772 | $subOffset++; | |
773 | if (--$offset < 0) { | |
774 | $offset = 0; | |
775 | if ($subOffset > $sub) { | |
776 | $sub++; | |
777 | } | |
778 | } | |
779 | } | |
780 | } | |
781 | ||
782 | if (empty($root)) { | |
783 | $root = str_repeat("../", $sub); | |
784 | } | |
785 | return $root . implode("/", array_slice($newDirs, 0, $offset)); | |
786 | } | |
787 | ||
788 | /** | |
789 | * Create the file permissions for a file or directory, for use in the extFileAttr parameters. | |
790 | * | |
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. | |
796 | */ | |
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); | |
800 | ||
801 | return ($fp << 16) | ($isFile ? self::S_DOS_A : self::S_DOS_D); | |
802 | } | |
803 | ||
804 | /** | |
805 | * Get the file permissions for a file or directory, for use in the extFileAttr parameters. | |
806 | * | |
807 | * @param string $filename | |
808 | * @return external ref field, or FALSE if the file is not found. | |
809 | */ | |
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); | |
814 | } | |
815 | return FALSE; | |
816 | } | |
817 | } | |
818 | ?> |