* @copyright 2009-2013 A. Grandt * @license GNU LGPL, Attribution required for commercial implementations, requested for everything else. * @link http://www.phpclasses.org/package/6116 * @link https://github.com/Grandt/PHPZip * @version 1.37 */ class ZipStream { const VERSION = 1.37; const ZIP_LOCAL_FILE_HEADER = "\x50\x4b\x03\x04"; // Local file header signature const ZIP_CENTRAL_FILE_HEADER = "\x50\x4b\x01\x02"; // Central file header signature const ZIP_END_OF_CENTRAL_DIRECTORY = "\x50\x4b\x05\x06\x00\x00\x00\x00"; //end of Central directory record const EXT_FILE_ATTR_DIR = "\x10\x00\xFF\x41"; const EXT_FILE_ATTR_FILE = "\x00\x00\xFF\x81"; const ATTR_VERSION_TO_EXTRACT = "\x14\x00"; // Version needed to extract const ATTR_MADE_BY_VERSION = "\x1E\x03"; // Made By Version private $zipMemoryThreshold = 1048576; // Autocreate tempfile if the zip data exceeds 1048576 bytes (1 MB) private $zipComment = null; private $cdRec = array(); // central directory private $offset = 0; private $isFinalized = FALSE; private $addExtraField = TRUE; private $streamChunkSize = 16384; // 65536; private $streamFilePath = null; private $streamTimeStamp = null; private $streamComment = null; private $streamFile = null; private $streamData = null; private $streamFileLength = 0; /** * Constructor. * * @param String $archiveName Name to send to the HTTP client. * @param String $contentType Content mime type. Optional, defaults to "application/zip". */ function __construct($archiveName = "", $contentType = "application/zip") { if (!function_exists('sys_get_temp_dir')) { die ("ERROR: ZipStream " . self::VERSION . " requires PHP version 5.2.1 or above."); } $headerFile = null; $headerLine = null; if (!headers_sent($headerFile, $headerLine) or die("

Error: Unable to send file $archiveName. HTML Headers have already been sent from $headerFile in line $headerLine

")) { if ((ob_get_contents() === FALSE || ob_get_contents() == '') or die("\n

Error: Unable to send file $archiveName.epub. Output buffer contains the following text (typically warnings or errors):
" . ob_get_contents() . "

")) { if (ini_get('zlib.output_compression')) { ini_set('zlib.output_compression', 'Off'); } header('Pragma: public'); header("Last-Modified: " . gmdate("D, d M Y H:i:s T")); header("Expires: 0"); header("Accept-Ranges: bytes"); //header("Connection: Keep-Alive"); header("Content-Type: " . $contentType); header('Content-Disposition: attachment; filename="' . $archiveName . '";'); header("Content-Transfer-Encoding: binary"); flush(); } } } function __destruct() { $this->isFinalized = TRUE; $this->cdRec = null; exit; } /** * Extra fields on the Zip directory records are Unix time codes needed for compatibility on the default Mac zip archive tool. * These are enabled as default, as they do no harm elsewhere and only add 26 bytes per file added. * * @param bool $setExtraField TRUE (default) will enable adding of extra fields, anything else will disable it. */ function setExtraField($setExtraField = TRUE) { $this->addExtraField = ($setExtraField === TRUE); } /** * Set Zip archive comment. * * @param string $newComment New comment. null to clear. * @return bool $success */ public function setComment($newComment = null) { if ($this->isFinalized) { return FALSE; } $this->zipComment = $newComment; return TRUE; } /** * Add an empty directory entry to the zip archive. * Basically this is only used if an empty directory is added. * * @param string $directoryPath Directory Path and name to be added to the archive. * @param int $timestamp (Optional) Timestamp for the added directory, if omitted or set to 0, the current time will be used. * @param string $fileComment (Optional) Comment to be added to the archive for this directory. To use fileComment, timestamp must be given. * @return bool $success */ public function addDirectory($directoryPath, $timestamp = 0, $fileComment = null) { if ($this->isFinalized) { return FALSE; } $directoryPath = str_replace("\\", "/", $directoryPath); $directoryPath = rtrim($directoryPath, "/"); if (strlen($directoryPath) > 0) { $this->buildZipEntry($directoryPath.'/', $fileComment, "\x00\x00", "\x00\x00", $timestamp, "\x00\x00\x00\x00", 0, 0, self::EXT_FILE_ATTR_DIR); return TRUE; } return FALSE; } /** * Add a file to the archive at the specified location and file name. * * @param string $data File data. * @param string $filePath Filepath and name to be used in the archive. * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used. * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given. * @return bool $success */ public function addFile($data, $filePath, $timestamp = 0, $fileComment = null) { if ($this->isFinalized) { return FALSE; } if (is_resource($data) && get_resource_type($data) == "stream") { $this->addLargeFile($data, $filePath, $timestamp, $fileComment); return FALSE; } $gzType = "\x08\x00"; // Compression type 8 = deflate $gpFlags = "\x00\x00"; // General Purpose bit flags for compression type 8 it is: 0=Normal, 1=Maximum, 2=Fast, 3=super fast compression. $dataLength = strlen($data); $fileCRC32 = pack("V", crc32($data)); $gzData = gzcompress($data); $gzData = substr(substr($gzData, 0, strlen($gzData) - 4), 2); // gzcompress adds a 2 byte header and 4 byte CRC we can't use. // 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. $gzLength = strlen($gzData); if ($gzLength >= $dataLength) { $gzLength = $dataLength; $gzData = $data; $gzType = "\x00\x00"; // Compression type 0 = stored $gpFlags = "\x00\x00"; // Compression type 0 = stored } $this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, self::EXT_FILE_ATTR_FILE); print ($gzData); return TRUE; } /** * Add the content to a directory. * * @author Adam Schmalhofer * @author A. Grandt * * @param String $realPath Path on the file system. * @param String $zipPath Filepath and name to be used in the archive. * @param bool $recursive Add content recursively, default is TRUE. * @param bool $followSymlinks Follow and add symbolic links, if they are accessible, default is TRUE. * @param array &$addedFiles Reference to the added files, this is used to prevent duplicates, efault is an empty array. * If you start the function by parsing an array, the array will be populated with the realPath * and zipPath kay/value pairs added to the archive by the function. */ public function addDirectoryContent($realPath, $zipPath, $recursive = TRUE, $followSymlinks = TRUE, &$addedFiles = array()) { if (file_exists($realPath) && !isset($addedFiles[realpath($realPath)])) { if (is_dir($realPath)) { $this->addDirectory($zipPath); } $addedFiles[realpath($realPath)] = $zipPath; $iter = new DirectoryIterator($realPath); foreach ($iter as $file) { if ($file->isDot()) { continue; } $newRealPath = $file->getPathname(); $newZipPath = self::pathJoin($zipPath, $file->getFilename()); if (file_exists($newRealPath) && ($followSymlinks === TRUE || !is_link($newRealPath))) { if ($file->isFile()) { $addedFiles[realpath($newRealPath)] = $newZipPath; $this->addLargeFile($newRealPath, $newZipPath); } else if ($recursive === TRUE) { $this->addDirectoryContent($newRealPath, $newZipPath, $recursive); } else { $this->addDirectory($zipPath); } } } } } /** * Add a file to the archive at the specified location and file name. * * @param string $dataFile File name/path. * @param string $filePath Filepath and name to be used in the archive. * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used. * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given. * @return bool $success */ public function addLargeFile($dataFile, $filePath, $timestamp = 0, $fileComment = null) { if ($this->isFinalized) { return FALSE; } if (is_string($dataFile) && is_file($dataFile)) { $this->processFile($dataFile, $filePath, $timestamp, $fileComment); } else if (is_resource($dataFile) && get_resource_type($dataFile) == "stream") { $fh = $dataFile; $this->openStream($filePath, $timestamp, $fileComment); while (!feof($fh)) { $this->addStreamData(fread($fh, $this->streamChunkSize)); } $this->closeStream($this->addExtraField); } return TRUE; } /** * Create a stream to be used for large entries. * * @param string $filePath Filepath and name to be used in the archive. * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used. * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given. * @return bool $success */ public function openStream($filePath, $timestamp = 0, $fileComment = null) { if (!function_exists('sys_get_temp_dir')) { die ("ERROR: Zip " . self::VERSION . " requires PHP version 5.2.1 or above if large files are used."); } if ($this->isFinalized) { return FALSE; } if (strlen($this->streamFilePath) > 0) { closeStream(); } $this->streamFile = tempnam(sys_get_temp_dir(), 'ZipStream'); $this->streamData = fopen($this->streamFile, "wb"); $this->streamFilePath = $filePath; $this->streamTimestamp = $timestamp; $this->streamFileComment = $fileComment; $this->streamFileLength = 0; return TRUE; } /** * Add data to the open stream. * * @param String $data * @return $length bytes added or FALSE if the archive is finalized or there are no open stream. */ public function addStreamData($data) { if ($this->isFinalized || strlen($this->streamFilePath) == 0) { return FALSE; } $length = fwrite($this->streamData, $data, strlen($data)); if ($length != strlen($data)) { die ("

Length mismatch

\n"); } $this->streamFileLength += $length; return $length; } /** * Close the current stream. * * @return bool $success */ public function closeStream() { if ($this->isFinalized || strlen($this->streamFilePath) == 0) { return FALSE; } fflush($this->streamData); fclose($this->streamData); $this->processFile($this->streamFile, $this->streamFilePath, $this->streamTimestamp, $this->streamFileComment); $this->streamData = null; $this->streamFilePath = null; $this->streamTimestamp = null; $this->streamFileComment = null; $this->streamFileLength = 0; // Windows is a little slow at times, so a millisecond later, we can unlink this. unlink($this->streamFile); $this->streamFile = null; return TRUE; } private function processFile($dataFile, $filePath, $timestamp = 0, $fileComment = null) { if ($this->isFinalized) { return FALSE; } $tempzip = tempnam(sys_get_temp_dir(), 'ZipStream'); $zip = new ZipArchive; if ($zip->open($tempzip) === TRUE) { $zip->addFile($dataFile, 'file'); $zip->close(); } $file_handle = fopen($tempzip, "rb"); $stats = fstat($file_handle); $eof = $stats['size']-72; fseek($file_handle, 6); $gpFlags = fread($file_handle, 2); $gzType = fread($file_handle, 2); fread($file_handle, 4); $fileCRC32 = fread($file_handle, 4); $v = unpack("Vval", fread($file_handle, 4)); $gzLength = $v['val']; $v = unpack("Vval", fread($file_handle, 4)); $dataLength = $v['val']; $this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, self::EXT_FILE_ATTR_FILE); fseek($file_handle, 34); $pos = 34; while (!feof($file_handle) && $pos < $eof) { $datalen = $this->streamChunkSize; if ($pos + $this->streamChunkSize > $eof) { $datalen = $eof-$pos; } echo fread($file_handle, $datalen); $pos += $datalen; flush(); } fclose($file_handle); unlink($tempzip); } /** * Close the archive. * A closed archive can no longer have new files added to it. * @return bool $success */ public function finalize() { if (!$this->isFinalized) { if (strlen($this->streamFilePath) > 0) { $this->closeStream(); } $cdRecSize = pack("v", sizeof($this->cdRec)); $cd = implode("", $this->cdRec); print($cd); print(self::ZIP_END_OF_CENTRAL_DIRECTORY); print($cdRecSize.$cdRecSize); print(pack("VV", strlen($cd), $this->offset)); if (!empty($this->zipComment)) { print(pack("v", strlen($this->zipComment))); print($this->zipComment); } else { print("\x00\x00"); } flush(); $this->isFinalized = TRUE; $cd = null; $this->cdRec = null; return TRUE; } return FALSE; } /** * Calculate the 2 byte dostime used in the zip entries. * * @param int $timestamp * @return 2-byte encoded DOS Date */ private function getDosTime($timestamp = 0) { $timestamp = (int)$timestamp; $oldTZ = @date_default_timezone_get(); date_default_timezone_set('UTC'); $date = ($timestamp == 0 ? getdate() : getdate($timestamp)); date_default_timezone_set($oldTZ); if ($date["year"] >= 1980) { return pack("V", (($date["mday"] + ($date["mon"] << 5) + (($date["year"]-1980) << 9)) << 16) | (($date["seconds"] >> 1) + ($date["minutes"] << 5) + ($date["hours"] << 11))); } return "\x00\x00\x00\x00"; } /** * Build the Zip file structures * * @param String $filePath * @param String $fileComment * @param String $gpFlags * @param String $gzType * @param int $timestamp * @param string $fileCRC32 * @param int $gzLength * @param int $dataLength * @param integer $extFileAttr Use self::EXT_FILE_ATTR_FILE for files, self::EXT_FILE_ATTR_DIR for Directories. */ private function buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr) { $filePath = str_replace("\\", "/", $filePath); $fileCommentLength = (empty($fileComment) ? 0 : strlen($fileComment)); $timestamp = (int)$timestamp; $timestamp = ($timestamp == 0 ? time() : $timestamp); $dosTime = $this->getDosTime($timestamp); $tsPack = pack("V", $timestamp); $ux = "\x75\x78\x0B\x00\x01\x04\xE8\x03\x00\x00\x04\x00\x00\x00\x00"; if (!isset($gpFlags) || strlen($gpFlags) != 2) { $gpFlags = "\x00\x00"; } $isFileUTF8 = mb_check_encoding($filePath, "UTF-8") && !mb_check_encoding($filePath, "ASCII"); $isCommentUTF8 = !empty($fileComment) && mb_check_encoding($fileComment, "UTF-8") && !mb_check_encoding($fileComment, "ASCII"); if ($isFileUTF8 || $isCommentUTF8) { $flag = 0; $gpFlagsV = unpack("vflags", $gpFlags); if (isset($gpFlagsV['flags'])) { $flag = $gpFlagsV['flags']; } $gpFlags = pack("v", $flag | (1 << 11)); } $header = $gpFlags . $gzType . $dosTime. $fileCRC32 . pack("VVv", $gzLength, $dataLength, strlen($filePath)); // File name length $zipEntry = self::ZIP_LOCAL_FILE_HEADER; $zipEntry .= self::ATTR_VERSION_TO_EXTRACT; $zipEntry .= $header; $zipEntry .= $this->addExtraField ? "\x1C\x00" : "\x00\x00"; // Extra field length $zipEntry .= $filePath; // FileName // Extra fields if ($this->addExtraField) { $zipEntry .= "\x55\x54\x09\x00\x03" . $tsPack . $tsPack . $ux; } print($zipEntry); $cdEntry = self::ZIP_CENTRAL_FILE_HEADER; $cdEntry .= self::ATTR_MADE_BY_VERSION; $cdEntry .= ($dataLength === 0 ? "\x0A\x00" : self::ATTR_VERSION_TO_EXTRACT); $cdEntry .= $header; $cdEntry .= $this->addExtraField ? "\x18\x00" : "\x00\x00"; // Extra field length $cdEntry .= pack("v", $fileCommentLength); // File comment length $cdEntry .= "\x00\x00"; // Disk number start $cdEntry .= "\x00\x00"; // internal file attributes $cdEntry .= $extFileAttr; // External file attributes $cdEntry .= pack("V", $this->offset); // Relative offset of local header $cdEntry .= $filePath; // FileName // Extra fields if ($this->addExtraField) { $cdEntry .= "\x55\x54\x05\x00\x03" . $tsPack . $ux; } if (!empty($fileComment)) { $cdEntry .= $fileComment; // Comment } $this->cdRec[] = $cdEntry; $this->offset += strlen($zipEntry) + $gzLength; } /** * Join $file to $dir path, and clean up any excess slashes. * * @param String $dir * @param String $file */ public static function pathJoin($dir, $file) { if (empty($dir) || empty($file)) { return self::getRelativePath($dir . $file); } return self::getRelativePath($dir . '/' . $file); } /** * Clean up a path, removing any unnecessary elements such as /./, // or redundant ../ segments. * If the path starts with a "/", it is deemed an absolute path and any /../ in the beginning is stripped off. * The returned path will not end in a "/". * * @param String $path The path to clean up * @return String the clean path */ public static function getRelativePath($path) { $path = preg_replace("#/+\.?/+#", "/", str_replace("\\", "/", $path)); $dirs = explode("/", rtrim(preg_replace('#^(?:\./)+#', '', $path), '/')); $offset = 0; $sub = 0; $subOffset = 0; $root = ""; if (empty($dirs[0])) { $root = "/"; $dirs = array_splice($dirs, 1); } else if (preg_match("#[A-Za-z]:#", $dirs[0])) { $root = strtoupper($dirs[0]) . "/"; $dirs = array_splice($dirs, 1); } $newDirs = array(); foreach ($dirs as $dir) { if ($dir !== "..") { $subOffset--; $newDirs[++$offset] = $dir; } else { $subOffset++; if (--$offset < 0) { $offset = 0; if ($subOffset > $sub) { $sub++; } } } } if (empty($root)) { $root = str_repeat("../", $sub); } return $root . implode("/", array_slice($newDirs, 0, $offset)); } }