* @copyright 2009-2014 A. Grandt * @license GNU LGPL 2.1 * @link http://www.phpclasses.org/package/6110 * @link https://github.com/Grandt/PHPZip * @version 1.62 */ class wpml_zip { const VERSION = 1.62; 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 = 010173200020; // Permission 755 drwxr-xr-x = (((S_IFDIR | 0755) << 16) | S_DOS_D); const EXT_FILE_ATTR_FILE = 020151000040; // Permission 644 -rw-r--r-- = (((S_IFREG | 0644) << 16) | S_DOS_A); const ATTR_VERSION_TO_EXTRACT = "\x14\x00"; // Version needed to extract const ATTR_MADE_BY_VERSION = "\x1E\x03"; // Made By Version // UID 1000, GID 0 const EXTRA_FIELD_NEW_UNIX_GUID = "\x75\x78\x0B\x00\x01\x04\xE8\x03\x00\x00\x04\x00\x00\x00\x00"; // Unix file types const S_IFIFO = 0010000; // named pipe (fifo) const S_IFCHR = 0020000; // character special const S_IFDIR = 0040000; // directory const S_IFBLK = 0060000; // block special const S_IFREG = 0100000; // regular const S_IFLNK = 0120000; // symbolic link const S_IFSOCK = 0140000; // socket // setuid/setgid/sticky bits, the same as for chmod: const S_ISUID = 0004000; // set user id on execution const S_ISGID = 0002000; // set group id on execution const S_ISTXT = 0001000; // sticky bit // And of course, the other 12 bits are for the permissions, the same as for chmod: // When addding these up, you can also just write the permissions as a simgle octal number // ie. 0755. The leading 0 specifies octal notation. const S_IRWXU = 0000700; // RWX mask for owner const S_IRUSR = 0000400; // R for owner const S_IWUSR = 0000200; // W for owner const S_IXUSR = 0000100; // X for owner const S_IRWXG = 0000070; // RWX mask for group const S_IRGRP = 0000040; // R for group const S_IWGRP = 0000020; // W for group const S_IXGRP = 0000010; // X for group const S_IRWXO = 0000007; // RWX mask for other const S_IROTH = 0000004; // R for other const S_IWOTH = 0000002; // W for other const S_IXOTH = 0000001; // X for other const S_ISVTX = 0001000; // save swapped text even after use // Filetype, sticky and permissions are added up, and shifted 16 bits left BEFORE adding the DOS flags. // DOS file type flags, we really only use the S_DOS_D flag. const S_DOS_A = 0000040; // DOS flag for Archive const S_DOS_D = 0000020; // DOS flag for Directory const S_DOS_V = 0000010; // DOS flag for Volume const S_DOS_S = 0000004; // DOS flag for System const S_DOS_H = 0000002; // DOS flag for Hidden const S_DOS_R = 0000001; // DOS flag for Read Only private $zipMemoryThreshold = 1048576; // Autocreate tempfile if the zip data exceeds 1048576 bytes (1 MB) private $zipData = NULL; private $zipFile = NULL; private $zipComment = NULL; private $cdRec = array(); // central directory private $offset = 0; private $isFinalized = FALSE; private $addExtraField = TRUE; private $streamChunkSize = 65536; private $streamFilePath = NULL; private $streamTimestamp = NULL; private $streamFileComment = NULL; private $streamFile = NULL; private $streamData = NULL; private $streamFileLength = 0; private $streamExtFileAttr = null; /** * A custom temporary folder, or a callable that returns a custom temporary file. * @var string|callable */ public static $temp = null; /** * Constructor. * * @param boolean $useZipFile Write temp zip data to tempFile? Default FALSE */ function __construct($useZipFile = FALSE) { if ($useZipFile) { $this->zipFile = tmpfile(); } else { $this->zipData = ""; } } function __destruct() { if (is_resource($this->zipFile)) { fclose($this->zipFile); } $this->zipData = NULL; } /** * 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; } /** * Set zip file to write zip data to. * This will cause all present and future data written to this class to be written to this file. * This can be used at any time, even after the Zip Archive have been finalized. Any previous file will be closed. * Warning: If the given file already exists, it will be overwritten. * * @param string $fileName * @return bool $success */ public function setZipFile($fileName) { if (is_file($fileName)) { unlink($fileName); } $fd=fopen($fileName, "x+b"); if (is_resource($this->zipFile)) { rewind($this->zipFile); while (!feof($this->zipFile)) { fwrite($fd, fread($this->zipFile, $this->streamChunkSize)); } fclose($this->zipFile); } else { fwrite($fd, $this->zipData); $this->zipData = NULL; } $this->zipFile = $fd; 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. * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this. * @return bool $success */ public function addDirectory($directoryPath, $timestamp = 0, $fileComment = NULL, $extFileAttr = self::EXT_FILE_ATTR_DIR) { 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, $extFileAttr); 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. * @param bool $compress (Optional) Compress file, if set to FALSE the file will only be stored. Default TRUE. * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this. * @return bool $success */ public function addFile($data, $filePath, $timestamp = 0, $fileComment = NULL, $compress = TRUE, $extFileAttr = self::EXT_FILE_ATTR_FILE) { if ($this->isFinalized) { return FALSE; } if (is_resource($data) && get_resource_type($data) == "stream") { $this->addLargeFile($data, $filePath, $timestamp, $fileComment, $extFileAttr); return FALSE; } $gzData = ""; $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)); if ($compress) { $gzTmp = gzcompress($data); $gzData = substr(substr($gzTmp, 0, strlen($gzTmp) - 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); } else { $gzLength = $dataLength; } if ($gzLength >= $dataLength) { $gzLength = $dataLength; $gzData = $data; $gzType = "\x00\x00"; // Compression type 0 = stored $gpFlags = "\x00\x00"; // Compression type 0 = stored } if (!is_resource($this->zipFile) && ($this->offset + $gzLength) > $this->zipMemoryThreshold) { $this->zipflush(); } $this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr); $this->zipwrite($gzData); return TRUE; } /** * 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. * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this. * @return bool $success */ public function addLargeFile($dataFile, $filePath, $timestamp = 0, $fileComment = NULL, $extFileAttr = self::EXT_FILE_ATTR_FILE) { if ($this->isFinalized) { return FALSE; } if (is_string($dataFile) && is_file($dataFile)) { $this->processFile($dataFile, $filePath, $timestamp, $fileComment, $extFileAttr); } else if (is_resource($dataFile) && get_resource_type($dataFile) == "stream") { $fh = $dataFile; $this->openStream($filePath, $timestamp, $fileComment, $extFileAttr); while (!feof($fh)) { $this->addStreamData(fread($fh, $this->streamChunkSize)); } $this->closeStream(); } 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. * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this. * @throws Exception Throws an exception in case of errors * @return bool $success */ public function openStream($filePath, $timestamp = 0, $fileComment = null, $extFileAttr = self::EXT_FILE_ATTR_FILE) { if (!function_exists('sys_get_temp_dir')) { throw new Exception("Zip " . self::VERSION . " requires PHP version 5.2.1 or above if large files are used."); } if ($this->isFinalized) { return FALSE; } $this->zipflush(); if (strlen($this->streamFilePath) > 0) { $this->closeStream(); } $this->streamFile = self::getTemporaryFile(); $this->streamData = fopen($this->streamFile, "wb"); $this->streamFilePath = $filePath; $this->streamTimestamp = $timestamp; $this->streamFileComment = $fileComment; $this->streamFileLength = 0; $this->streamExtFileAttr = $extFileAttr; return TRUE; } /** * Add data to the open stream. * * @param string $data * @throws Exception Throws an exception in case of errors * @return mixed length in 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)) { throw new Exception("File IO: Error writing; Length mismatch: Expected " . strlen($data) . " bytes, wrote " . ($length === FALSE ? "NONE!" : $length)); } $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->streamExtFileAttr); $this->streamData = null; $this->streamFilePath = null; $this->streamTimestamp = null; $this->streamFileComment = null; $this->streamFileLength = 0; $this->streamExtFileAttr = null; // 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, $extFileAttr = self::EXT_FILE_ATTR_FILE) { if ($this->isFinalized) { return FALSE; } $tempzip = self::getTemporaryFile(); $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, $extFileAttr); fseek($file_handle, 34); $pos = 34; while (!feof($file_handle) && $pos < $eof) { $datalen = $this->streamChunkSize; if ($pos + $this->streamChunkSize > $eof) { $datalen = $eof-$pos; } $data = fread($file_handle, $datalen); $pos += $datalen; $this->zipwrite($data); } 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(); } $cd = implode("", $this->cdRec); $cdRecSize = pack("v", sizeof($this->cdRec)); $cdRec = $cd . self::ZIP_END_OF_CENTRAL_DIRECTORY . $cdRecSize . $cdRecSize . pack("VV", strlen($cd), $this->offset); if (!empty($this->zipComment)) { $cdRec .= pack("v", strlen($this->zipComment)) . $this->zipComment; } else { $cdRec .= "\x00\x00"; } $this->zipwrite($cdRec); $this->isFinalized = TRUE; $this->cdRec = NULL; return TRUE; } return FALSE; } /** * Get the zip file contents * If the zip haven't been finalized yet, this will cause it to become finalized * * @return zip data */ public function getZipData() { if (!$this->isFinalized) { $this->finalize(); } if (!is_resource($this->zipFile)) { return $this->zipData; } else { rewind($this->zipFile); $filestat = fstat($this->zipFile); return fread($this->zipFile, $filestat['size']); } } /** * Send the archive as a zip download * * @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. * @param String $contentType Content mime type. Optional, defaults to "application/zip". * @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. * @param bool $inline Use Content-Disposition with "inline" instead of "attached". Optional, defaults to FALSE. * @throws Exception Throws an exception in case of errors * @return bool Always returns true (for backward compatibility). */ function sendZip($fileName = null, $contentType = "application/zip", $utf8FileName = null, $inline = false) { if (!$this->isFinalized) { $this->finalize(); } $headerFile = null; $headerLine = null; if(headers_sent($headerFile, $headerLine)) { throw new Exception("Unable to send file '$fileName'. Headers have already been sent from '$headerFile' in line $headerLine"); } if(ob_get_contents() !== false && strlen(ob_get_contents())) { throw new Exception("Unable to send file '$fileName'. Output buffer contains the following text (typically warnings or errors):\n" . 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: close"); header("Content-Type: " . $contentType); $cd = "Content-Disposition: "; if ($inline) { $cd .= "inline"; } else { $cd .= "attached"; } if ($fileName) { $cd .= '; filename="' . $fileName . '"'; } if ($utf8FileName) { $cd .= "; filename*=UTF-8''" . rawurlencode($utf8FileName); } header($cd); header("Content-Length: ". $this->getArchiveSize()); if (!is_resource($this->zipFile)) { echo $this->zipData; } else { rewind($this->zipFile); while (!feof($this->zipFile)) { echo fread($this->zipFile, $this->streamChunkSize); } } return true; } /** * Return the current size of the archive * * @return $size Size of the archive */ public function getArchiveSize() { if (!is_resource($this->zipFile)) { return strlen($this->zipData); } $filestat = fstat($this->zipFile); return $filestat['size']; } /** * 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 int $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); 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"); $localExtraField = ""; $centralExtraField = ""; if ($this->addExtraField) { $localExtraField .= "\x55\x54\x09\x00\x03" . $tsPack . $tsPack . wpml_zip::EXTRA_FIELD_NEW_UNIX_GUID; $centralExtraField .= "\x55\x54\x05\x00\x03" . $tsPack . wpml_zip::EXTRA_FIELD_NEW_UNIX_GUID; } if ($isFileUTF8 || $isCommentUTF8) { $flag = 0; $gpFlagsV = unpack("vflags", $gpFlags); if (isset($gpFlagsV['flags'])) { $flag = $gpFlagsV['flags']; } $gpFlags = pack("v", $flag | (1 << 11)); if ($isFileUTF8) { $utfPathExtraField = "\x75\x70" . pack ("v", (5 + strlen($filePath))) . "\x01" . pack("V", crc32($filePath)) . $filePath; $localExtraField .= $utfPathExtraField; $centralExtraField .= $utfPathExtraField; } if ($isCommentUTF8) { $centralExtraField .= "\x75\x63" // utf8 encoded file comment extra field . pack ("v", (5 + strlen($fileComment))) . "\x01" . pack("V", crc32($fileComment)) . $fileComment; } } $header = $gpFlags . $gzType . $dosTime. $fileCRC32 . pack("VVv", $gzLength, $dataLength, strlen($filePath)); // File name length $zipEntry = self::ZIP_LOCAL_FILE_HEADER . self::ATTR_VERSION_TO_EXTRACT . $header . pack("v", strlen($localExtraField)) // Extra field length . $filePath // FileName . $localExtraField; // Extra fields $this->zipwrite($zipEntry); $cdEntry = self::ZIP_CENTRAL_FILE_HEADER . self::ATTR_MADE_BY_VERSION . ($dataLength === 0 ? "\x0A\x00" : self::ATTR_VERSION_TO_EXTRACT) . $header . pack("v", strlen($centralExtraField)) // Extra field length . pack("v", $fileCommentLength) // File comment length . "\x00\x00" // Disk number start . "\x00\x00" // internal file attributes . pack("V", $extFileAttr) // External file attributes . pack("V", $this->offset) // Relative offset of local header . $filePath // FileName . $centralExtraField; // Extra fields if (!empty($fileComment)) { $cdEntry .= $fileComment; // Comment } $this->cdRec[] = $cdEntry; $this->offset += strlen($zipEntry) + $gzLength; } private function zipwrite($data) { if (!is_resource($this->zipFile)) { $this->zipData .= $data; } else { fwrite($this->zipFile, $data); fflush($this->zipFile); } } private function zipflush() { if (!is_resource($this->zipFile)) { $this->zipFile = tmpfile(); fwrite($this->zipFile, $this->zipData); $this->zipData = NULL; } } /** * Returns the path to a temporary file. * @return string */ private static function getTemporaryFile() { if(is_callable(self::$temp)) { $temporaryFile = @call_user_func(self::$temp); if(is_string($temporaryFile) && strlen($temporaryFile) && is_writable($temporaryFile)) { return $temporaryFile; } } $temporaryDirectory = (is_string(self::$temp) && strlen(self::$temp)) ? self::$temp : sys_get_temp_dir(); return tempnam($temporaryDirectory, 'Zip'); } }