<?php /** * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * This class serves as an abstract superclass for zip archives. * */ namespace PHPZip\Zip\Core; use com\grandt\BinStringStatic; use PHPZip\Zip\Listener\ZipArchiveListener as ZipArchiveListener; use PHPZip\Zip\Exception\IncompatiblePhpVersion as IncompatiblePhpVersionException; use PHPZip\Zip\Exception\InvalidPhpConfiguration as InvalidPhpConfigurationException; use PHPZip\Zip\Exception\HeadersSent as HeadersSentException; use PHPZip\Zip\Exception\BufferNotEmpty as BufferNotEmptyException; use PHPZip\Zip\Exception\LengthMismatch as LengthMismatchException; use ZipMerge\Zip\Core\AbstractZipWriter; use ZipMerge\Zip\Core\Header\ZipFileEntry; use ZipMerge\Zip\Stream\ZipMerge; abstract class AbstractZipArchive extends AbstractZipWriter { const APP_NAME = 'PHPZip'; const VERSION = "2.0.8"; const MIN_PHP_VERSION = 5.3; // for namespaces const CONTENT_TYPE = 'application/zip'; const NULL_BYTE = "\x00"; const NULL_WORD = "\x00\x00"; // Two nul bytes, used often enough. const NULL_DWORD = "\x00\x00\x00\x00"; const ZIP_CENTRAL_FILE_HEADER = "PK\x01\x02"; // Central file header signature const ZIP_LOCAL_FILE_HEADER = "PK\x03\x04"; // Local file header signature const ZIP_LOCAL_DATA_DESCRIPTOR = "PK\x07\x08"; // Local Header, data descriptor const ZIP_END_OF_CENTRAL_DIRECTORY = "PK\x05\x06"; // End of Central directory record const HEADER_UNIX_TYPE_1 = 'UX'; // \x55\x58 or 0x5855 It has been replaced by the extended-timestamp extra block 'UT' (0x5455) and the Unix type 2 extra block 'Ux' (0x7855). const HEADER_UNIX_TYPE_2 = 'Ux'; // \x55\x78 or 0x7855 const HEADER_UNIX_TYPE_3 = 'ux'; // \x75\x78 or 0x7875 const HEADER_EXTENDED_TIMESTAMP = 'UT'; // \x55\x54 or 0x5455 const HEADER_UNICODE_PATH = 'up'; // \x75\x70 or 0x7075 const HEADER_UNICODE_COMMENT = 'uc'; // \x75\x63 or 0x6375 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 = 20 (File is compressed using Deflate compression) const ATTR_MADE_BY_VERSION = "\x1E\x03"; // Made By Version const DEFAULT_GZ_TYPE = "\x08\x00"; // Compression type 8 = deflate const DEFAULT_GP_FLAGS = self::NULL_WORD; // General Purpose bit flags for compression type 8 it is: 0=Normal, 1=Maximum, 2=Fast, 3=super fast compression. const DEFAULT_GZ_TYPE_STORED = self::NULL_WORD; // Compression type 0 = stored const DEFAULT_GP_FLAGS_STORED = self::NULL_WORD; // Compression type 0 = stored // UID 1000, GID 0 const EXTRA_FIELD_NEW_UNIX_GUID = "ux\x0B\x00\x01\x04\xE8\x03\x00\x00\x04\x00\x00\x00\x00"; // \x75\x78 3rd gen Unis GUID const EXTRA_FIELD_NEW_UNIX_GUID_CD = "ux\x00\x00"; // \x75\x78 3rd gen Unis GUID CD record version must have length 0. protected $zipComment = null; protected $cdRec = array(); // central directory protected $offset = 0; protected $isFinalized = false; protected $addExtraField = true; protected $streamChunkSize = 0; protected $streamFilePath = null; protected $streamTimestamp = null; protected $streamFileComment = null; protected $streamFile = null; protected $streamData = null; protected $streamFileLength = 0; protected $streamExtFileAttr = null; /** * A custom temporary folder, or a callable that returns a custom temporary file. * @var string|callable */ public static $temp = null; private $_listeners = array(); private $_phpConfigurationWatch = array( // 'mbstring.func_overload' => '0' // throw an exception if setting in php.ini is not '0' ); /** * Constructor. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * @param boolean $streamChunkSize Size of each chunk * * @throws \PHPZip\Zip\Exception\InvalidPhpConfiguration In case of errors */ protected function __construct($streamChunkSize) { $this->streamChunkSize = $streamChunkSize; if (count($this->_phpConfigurationWatch) > 0) { foreach ($this->_phpConfigurationWatch as $k => $v) { $s = (string)$v; if (@ini_get($k) !== $s) { $this->_throwException(new InvalidPhpConfigurationException(array( 'setting' => $k, 'expected' => $s, ))); break; // technically not needed. } } } } /** * 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. * * @author A. Grandt <php@grandt.com> * * @param bool $setExtraField true (default) will enable adding of extra fields, anything else will disable it. */ public function setExtraField($setExtraField = true) { $this->addExtraField = ($setExtraField === true); } /** * Set Zip archive comment. * * @author A. Grandt <php@grandt.com> * * @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. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * @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) { // TODO: get rid of magic numbers. $result = false; if (!$this->isFinalized) { $directoryPath = str_replace("\\", '/', $directoryPath); $directoryPath = rtrim($directoryPath, '/'); if (BinStringStatic::_strlen($directoryPath) > 0) { $this->buildZipEntry($directoryPath.'/', $fileComment, self::DEFAULT_GZ_TYPE_STORED, self::DEFAULT_GP_FLAGS_STORED, $timestamp, "\x00\x00\x00\x00", 0, 0, $extFileAttr); $result = true; } } return $result; } /** * Add a file to the archive at the specified location and file name. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * @param string $data File data. * @param string $filePath File path 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 = null, $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 = self::DEFAULT_GZ_TYPE; $gpFlags = self::DEFAULT_GP_FLAGS; $dataLength = BinStringStatic::_strlen($data); $fileCRC32 = pack("V", crc32($data)); $gzLength = $dataLength; if ($compress) { $gzTmp = gzcompress($data); // gzcompress adds a 2 byte header and 4 byte Adler-32 CRC at the end, which we can't use. $gzData = substr($gzTmp, 2, -4); // The 2 byte header does contain useful data, // though in this case the 2 parameters we'd be interested in will // always be 8 for compression type, and 2 for General purpose flag. $gzLength = BinStringStatic::_strlen($gzData); } if ($gzLength >= $dataLength) { $gzLength = $dataLength; $gzData = $data; $gzType = self::DEFAULT_GZ_TYPE_STORED; $gpFlags = self::DEFAULT_GP_FLAGS_STORED; } $this->onBeginAddFile(array( 'gzLength' => $gzLength, )); $this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr); $this->onEndAddFile(array( 'gzData' => $gzData, )); $this->_notifyListeners(null, array( 'data' => $data, )); return true; } /** * Add the content to a directory. * * @author Adam Schmalhofer <Adam.Schmalhofer@gmx.de> * @author A. Grandt <php@grandt.com> * * @param string $realPath Path on the file system. * @param string $zipPath File path 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, default 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. * @param bool $overrideFilePermissions Force the use of the file/dir permissions set in the $extDirAttr * and $extFileAttr parameters. * @param int $extDirAttr Permissions for directories. * @param int $extFileAttr Permissions for files. */ public function addDirectoryContent($realPath, $zipPath, $recursive = true, $followSymlinks = true, &$addedFiles = array(), $overrideFilePermissions = false, $extDirAttr = self::EXT_FILE_ATTR_DIR, $extFileAttr = self::EXT_FILE_ATTR_FILE) { if (file_exists($realPath) && !isset($addedFiles[realpath($realPath)])) { if (is_dir($realPath)) { $this->addDirectory( $zipPath, 0, null, $overrideFilePermissions ? $extDirAttr : ZipUtils::getFileExtAttr($realPath) ); } $addedFiles[realpath($realPath)] = $zipPath; $iter = new \DirectoryIterator($realPath); foreach ($iter as $file) { /* @var $file \DirectoryIterator */ if ($file->isDot()) { continue; } $newRealPath = $file->getPathname(); $newZipPath = \RelativePath::pathJoin($zipPath, $file->getFilename()); if (file_exists($newRealPath) && ($followSymlinks || !is_link($newRealPath))) { if ($file->isFile()) { $addedFiles[realpath($newRealPath)] = $newZipPath; $this->addLargeFile( $newRealPath, $newZipPath, 0, null, $overrideFilePermissions ? $extFileAttr : ZipUtils::getFileExtAttr($newRealPath) ); } else if ($recursive) { $this->addDirectoryContent( $newRealPath, $newZipPath, $recursive, $followSymlinks, $addedFiles, $overrideFilePermissions, $extDirAttr, $extFileAttr ); } else { $this->addDirectory( $zipPath, 0, null, $overrideFilePermissions ? $extDirAttr : ZipUtils::getFileExtAttr($newRealPath) ); } } } } } /** * Append the contents of an existing zip file to the current, WITHOUT re-compressing the data within it. * * @param string $file the path to the zip file to be added. * @param string $subPath place the contents in the $subPath sub-folder, default is '', and places the * content in the root of the new zip file. */ public function appendZip($file, $subPath = '') { $zipMerge = new ZipMerge(null); $zipMerge->appendZip($file, $subPath, $this); $files = $zipMerge->finalize(); /* @var $files array */ foreach ($files as $fileEntry) { /* @var $fileEntry ZipFileEntry */ $fileEntry->offset = $this->offset; $this->cdRec[] = $fileEntry->getCentralDirectoryHeader(); $this->offset += BinStringStatic::_strlen( $fileEntry->getLocalHeader()) + $fileEntry->gzLength; } } /** * Add a file to the archive at the specified location and file name. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * @param string $dataFile File name/path. * @param string $filePath File path 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) { $result = false; if (!$this->isFinalized) { 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(); } $result = true; } $this->_notifyListeners(null, array( 'file' => $dataFile, 'result' => $result, )); return $result; } /** * Create a stream to be used for large entries. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * @param string $filePath File path 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 \PHPZip\Zip\Exception\IncompatiblePhpVersion Throws an exception in case of errors * * @return bool $success */ public function openStream($filePath, $timestamp = 0, $fileComment = null, $extFileAttr = self::EXT_FILE_ATTR_FILE) { $result = false; if (!function_exists('sys_get_temp_dir')) { $this->_throwException(new IncompatiblePhpVersionException(array( 'appName' => self::APP_NAME, 'appVersion' => self::VERSION, 'minVersion' => self::MIN_PHP_VERSION, ))); } if (!$this->isFinalized) { $this->onOpenStream(); if (BinStringStatic::_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; $result = true; } $this->_notifyListeners(null, array( 'file' => $this->streamFile, 'result' => $result, )); return $result; } /** * Add data to the open stream. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * @param string $data * * @throws LengthMismatchException 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 || BinStringStatic::_strlen($this->streamFilePath) == 0) { return false; } $dataLength = BinStringStatic::_strlen($data); $length = fwrite($this->streamData, $data, $dataLength); if ($length != $dataLength) { $this->_throwException(new LengthMismatchException(array( 'expected' => BinStringStatic::_strlen($data), 'written' => (!$length ? 'NONE!' : $length), ))); } $this->streamFileLength += $length; return $length; } /** * Close the current stream. * * @author A. Grandt <php@grandt.com> * * @return bool $success */ public function closeStream() { if ($this->isFinalized || BinStringStatic::_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; } /** * Process the current file. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * @param string $dataFile * @param string $filePath * @param int $timestamp * @param string $fileComment * @param int $extFileAttr * * @return bool $success */ protected function processFile($dataFile, $filePath, $timestamp = 0, $fileComment = null, $extFileAttr = self::EXT_FILE_ATTR_FILE) { // TODO: change the magic numbers below to constants. if ($this->isFinalized) { return false; } $tempZip = self::getTemporaryFile(); $zip = new \ZipArchive; $rv = $zip->open($tempZip, \ZipArchive::CREATE | \ZipArchive::OVERWRITE); if ($rv === true) { // open returns true if successful, however one of the error values is 1, which will also read as true. $zip->addFile($dataFile, 'file'); $zip->close(); } else { // TODO: An error occurred reading the ZipArchive temp file (Seen on Windows installations) } $handle = fopen($tempZip, "rb"); $stats = fstat($handle); $eof = $stats['size']-72; // set EOF to the position of the end of the zip data, before the CD record. // Should probably use 34+gzLength instead. fseek($handle, 6); // Skip Zip local file header and version $gpFlags = fread($handle, 2); $gzType = fread($handle, 2); fread($handle, 4); // Skip DOS Time and Date $fileCRC32 = fread($handle, 4); $v = unpack("Vval", fread($handle, 4)); $gzLength = $v['val']; $v = unpack("Vval", fread($handle, 4)); $dataLength = $v['val']; $this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr); $pos = 34; fseek($handle, $pos); // Position pointer at the start of the actual zip data. while (!feof($handle) && $pos < $eof) { $len = $this->streamChunkSize; if ($pos + $this->streamChunkSize > $eof) { $len = $eof - $pos; } $data = fread($handle, $len); $pos += $len; $this->onProcessFile(array( 'data' => $data, )); } fclose($handle); unlink($tempZip); $this->_notifyListeners(null, array( 'file' => $dataFile, )); return true; } /** * Build the Zip file structures * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * @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. */ protected function buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr) { $filePath = str_replace("\\", "/", $filePath); $fileCommentLength = (empty($fileComment) ? 0 : BinStringStatic::_strlen($fileComment)); $timestamp = (int)$timestamp; $timestamp = ($timestamp == 0 ? time() : $timestamp); $dosTime = ZipUtils::getDosTime($timestamp); $tsPack = pack("V", $timestamp); if (!isset($gpFlags) || BinStringStatic::_strlen($gpFlags) != 2) { $gpFlags = self::DEFAULT_GP_FLAGS; } $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"); $locExField = ""; $cenExField = ""; if ($this->addExtraField) { $locExField .= self::HEADER_EXTENDED_TIMESTAMP . "\x09\x00\x03" . $tsPack . $tsPack . self::EXTRA_FIELD_NEW_UNIX_GUID; $cenExField .= self::HEADER_EXTENDED_TIMESTAMP . "\x05\x00\x03" . $tsPack . self::EXTRA_FIELD_NEW_UNIX_GUID_CD; } if ($isFileUTF8 || $isCommentUTF8) { $flag = 0; $gpFlagsV = unpack("vflags", $gpFlags); if (isset($gpFlagsV['flags'])) { $flag = $gpFlagsV['flags']; } $gpFlags = pack("v", $flag | (1 << 11)); if ($isFileUTF8) { $utfExField = self::HEADER_UNICODE_PATH // utf8 encoded File path extra field . pack ("v", (5 + BinStringStatic::_strlen($filePath))) . "\x01" . pack("V", crc32($filePath)) . $filePath; $locExField .= $utfExField; $cenExField .= $utfExField; } if ($isCommentUTF8) { $cenExField .= self::HEADER_UNICODE_COMMENT // utf8 encoded file comment extra field . pack ("v", (5 + BinStringStatic::_strlen($fileComment))) . "\x01" . pack("V", crc32($fileComment)) . $fileComment; } } $header = $gpFlags . $gzType . $dosTime. $fileCRC32 . pack("VVv", $gzLength, $dataLength, BinStringStatic::_strlen($filePath)); // File name length $zipEntry = self::ZIP_LOCAL_FILE_HEADER . self::ATTR_VERSION_TO_EXTRACT . $header . pack("v", BinStringStatic::_strlen($locExField)) // Extra field length . $filePath // FileName . $locExField; // Extra fields $this->onBuildZipEntry(array( 'zipEntry' => $zipEntry, )); $cdEntry = self::ZIP_CENTRAL_FILE_HEADER . self::ATTR_MADE_BY_VERSION . ($dataLength === 0 ? "\x0A\x00" : self::ATTR_VERSION_TO_EXTRACT) . $header . pack("v", BinStringStatic::_strlen($cenExField)) // Extra field length . pack("v", $fileCommentLength) // File comment length . self::NULL_WORD // Disk number start . self::NULL_WORD // internal file attributes . pack("V", $extFileAttr) // External file attributes . pack("V", $this->offset) // Relative offset of local header . $filePath // FileName . $cenExField; // Extra fields if (!empty($fileComment)) { $cdEntry .= $fileComment; // Comment } $this->cdRec[] = $cdEntry; $this->offset += BinStringStatic::_strlen($zipEntry) + $gzLength; $this->_notifyListeners(null, array( 'file' => $zipEntry, )); } /** * Build the base standard response headers, and ensure the content can be streamed. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * @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 \PHPZip\Zip\Exception\IncompatiblePhpVersion, BufferNotEmpty, HeadersSent In case of errors * * @return bool Always returns true (for backward compatibility). */ public function buildResponseHeader($fileName = null, $contentType = self::CONTENT_TYPE, $utf8FileName = null, $inline = false) { $ob = null; $headerFile = null; $headerLine = null; $zlibConfig = 'zlib.output_compression'; $this->onBeginBuildResponseHeader(); if (!function_exists('sys_get_temp_dir')) { $this->_throwException(new IncompatiblePhpVersionException(array( 'appName' => self::APP_NAME, 'appVersion' => self::VERSION, 'minVersion' => self::MIN_PHP_VERSION, ))); } $ob = ob_get_contents(); if ($ob !== false && BinStringStatic::_strlen($ob)) { $this->_throwException(new BufferNotEmptyException(array( 'outputBuffer' => $ob, 'fileName' => $fileName, ))); } if (headers_sent($headerFile, $headerLine)) { $this->_throwException(new HeadersSentException(array( 'headerFile' => $headerFile, 'headerLine' => $headerLine, 'fileName' => $fileName, ))); } if (@ini_get($zlibConfig)) { @ini_set($zlibConfig, 'Off'); } $cd = 'Content-Disposition: ' . ($inline ? 'inline' : 'attachment'); if ($fileName) { $cd .= '; filename="' . $fileName . '"'; } if ($utf8FileName) { $cd .= "; filename*=UTF-8''" . rawurlencode($utf8FileName); } header('Pragma: public'); header('Last-Modified: ' . gmdate('D, d M Y H:i:s T')); header('Expires: 0'); header('Accept-Ranges: bytes'); header('Content-Type: ' . $contentType); header($cd); $this->onEndBuildResponseHeader(); $this->_notifyListeners(null, array( 'file' => $fileName, 'utf8FileName' => $utf8FileName, 'contentType' => $contentType, )); return true; } /** * Close the archive. * A closed archive can no longer have new files added to it. * * @author A. Grandt <php@grandt.com> * * @return bool Success */ public function finalize() { if (!$this->isFinalized) { if (BinStringStatic::_strlen($this->streamFilePath) > 0) { $this->closeStream(); } $cd = implode("", $this->cdRec); $cdRecSize = pack("v", sizeof($this->cdRec)); $cdRec = $cd . self::ZIP_END_OF_CENTRAL_DIRECTORY . self::NULL_DWORD // really two words, used for split archives: #ofThisDisk . #ofDiskWithCD. Both 0. . $cdRecSize . $cdRecSize . pack("VV", BinStringStatic::_strlen($cd), $this->offset); if (!empty($this->zipComment)) { $cdRec .= pack("v", BinStringStatic::_strlen($this->zipComment)) . $this->zipComment; } else { $cdRec .= self::NULL_WORD; } $this->zipWrite($cdRec); $this->zipFlushBuffer(); $this->isFinalized = true; $this->cdRec = null; return true; } return false; } /** * Check PHP version. * * @author A. Grandt <php@grandt.com> */ public function checkVersion() { if (version_compare(PHP_VERSION, self::MIN_PHP_VERSION, '<') || !function_exists('sys_get_temp_dir') ) { die ("ERROR: " . self::APP_NAME . " " . self::VERSION . " requires PHP version " . self::MIN_PHP_VERSION . " or above."); } } /* * ************************************************************************ * Abstract methods. * ************************************************************************ */ /** * Called when specialised action is needed * while building a zip entry. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * @param array $params Array that contains zipEntry. */ abstract protected function onBuildZipEntry(array $params); /** * Called when specialised action is needed * at the start of adding a file to the archive. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * @param array $params Array that contains gzLength. */ abstract protected function onBeginAddFile(array $params); /** * Called when specialised action is needed * at the end of adding a file to the archive. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * @param array $params Array that contains gzData. */ abstract protected function onEndAddFile(array $params); /** * Called when specialised action is needed * at the start of sending the zip file|stream * response headers. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos */ abstract protected function onBeginBuildResponseHeader(); /** * Called when specialised action is needed * at the end of sending the zip file|stream * response headers. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos */ abstract protected function onEndBuildResponseHeader(); /** * Called when specialised action is needed * while opening a file|stream. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos */ abstract protected function onOpenStream(); /** * Called when specialised action is needed * while processing a file. * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * @param array $params Array that contains data. */ abstract protected function onProcessFile(array $params); /** * Verify if the memory buffer is about to be exceeded. * * @author A. Grandt <php@grandt.com> * * @param int $gzLength length of the pending data. */ abstract public function zipVerifyMemBuffer($gzLength); /** * * @author A. Grandt <php@grandt.com> * * @param string $data */ //abstract public function zipWrite($data); /** * Flush Zip Data stored in memory, to a temp file. * * @author A. Grandt <php@grandt.com> * */ abstract public function zipFlush(); /** * * @author A. Grandt <php@grandt.com> * */ abstract public function zipFlushBuffer(); /* * ************************************************************************ * Listener methods. * ************************************************************************ */ /** * Listen to events fired by this class. * * @author Greg Kappatos * * @param ZipArchiveListener $listener Class that implements the ZipArchiveListener interface. */ public function addListener(ZipArchiveListener $listener) { $this->_listeners[] = $listener; } /** * Stop listening to events fired by this class. * * @author Greg Kappatos * * @param ZipArchiveListener $listener Class that implements the ZipArchiveListener interface. */ public function removeListener(ZipArchiveListener $listener) { $key = array_search($listener, $this->_listeners); if ($key !== false) { unset($this->_listeners[$key]); } } /** * Helper method to fire appropriate event. * * @author Greg Kappatos * * @param string|null $method (Optional) The name of the event to fire. If this is null, then the calling method is used. * @param array $data Method parameters passed as an array. */ private function _notifyListeners($method = null, array $data = array()) { if (is_null($method)) { $backtrace = debug_backtrace(); if (sizeof($backtrace) > 0) { $trace = $backtrace[1]; $method = 'on' . ucwords($trace['function']); } } foreach ($this->_listeners as $listener) { if (count($data) > 0) { $listener->$method($data); } else { $listener->$method(); } } } /** * Helper method to fire OnException event for listeners and then throw the appropriate exception. * * @author Greg Kappatos * * @param AbstractException $exception Whatever exception needs to be thrown. * * @throws AbstractException $exception */ private function _throwException(AbstractException $exception) { $this->_notifyListeners('onException', array( 'exception' => $exception, )); throw $exception; } /* * ************************************************************************ * Static methods/ * ************************************************************************ */ /** * * @author A. Grandt <php@grandt.com> * @author Greg Kappatos * * @return string The full path to a temporary file. */ public static function getTemporaryFile() { if (is_callable(self::$temp)) { $file = @call_user_func(self::$temp); if (is_string($file) && BinStringStatic::_strlen($file) && is_writable($file)) { return $file; } } $dir = (is_string(self::$temp) && BinStringStatic::_strlen(self::$temp)) ? self::$temp : sys_get_temp_dir(); return tempnam($dir, __NAMESPACE__); } }