From d1bab197c125b4d4b8f2902d5764d9c7e8da9c28 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 12 Dec 2023 22:51:57 +0100 Subject: [PATCH] Backup using own TAR stream when PharData not available --- .../app/libraries/snappymail/stream/tar.php | 372 ++++++++++++++++++ .../app/libraries/snappymail/stream/zip.php | 16 +- .../app/libraries/snappymail/upgrade.php | 16 +- 3 files changed, 396 insertions(+), 8 deletions(-) create mode 100644 snappymail/v/0.0.0/app/libraries/snappymail/stream/tar.php diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/stream/tar.php b/snappymail/v/0.0.0/app/libraries/snappymail/stream/tar.php new file mode 100644 index 000000000..f3a124f21 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/stream/tar.php @@ -0,0 +1,372 @@ +out = $target; + } else { + throw new \Exception("Failed to open output: {$target}"); + } + if (self::DEFLATE === $compression) { + $this->gzip = array('w' => null, 'h' => null, 'l' => 0); + } + } + + function __destruct() + { + $this->close(); + } + + public function pushHttpHeaders(string $name) : void + { + if ($i = \ob_get_level()) { + # Clear buffers: + while ($i-- && \ob_end_clean()); + \ob_get_level() || \header('Content-Encoding: '); + } + \header('Cache-Control: no-store, no-cache, must-revalidate'); + \header('Pragma: no-cache'); + \header('Content-Transfer-Encoding: binary'); + if (false !== $this->gzip) { + $name .= '.tgz'; + $name = \preg_match('#^[\x01-\x7F]*$#D', $name) ? $name : '=?UTF-8?B?'.\base64_encode($name).'?='; + \header("Content-Disposition: attachment; filename={$name}"); + \header("Content-Type: application/x-gzip; name={$name}"); + } else { + $name .= '.tar'; + $name = \preg_match('#^[\x01-\x7F]*$#D', $name) ? $name : '=?UTF-8?B?'.\base64_encode($name).'?='; + \header("Content-Disposition: attachment; filename={$name}"); + \header("Content-Type: application/x-ustar; name={$name}"); + } + } + + public function close() : void + { + if ($this->out) { + if ($this->started) { + // Write tar footer + $this->write(pack('a1024', '')); + // Stop compression + if (!empty($this->gzip['w'])) { + \stream_filter_remove($this->gzip['w']); + // hash_final is a string, not an integer + $crc = \hash_final($this->gzip['h'], 1); + // write the little endian CRC32 and uncompressed file size + \fwrite($this->out, $crc[3].$crc[2].$crc[1].$crc[0].\pack('V', $this->gzip['l']), 8); + } + } + \fclose($this->out); + $this->out = null; + } + } + + public function addFile($fileinfo, string $name = null) : bool + { + if (!($fileinfo instanceof \SplFileInfo)) { + $fileinfo = new \SplFileInfo($fileinfo); + } + if (!$name) { + $name = $fileinfo->getFilename(); + } + if ($fileinfo->isLink()) { + $stat = \lstat($fileinfo); + $target = $fileinfo->getLinkTarget(); + if (\dirname($fileinfo->getPathname()) === \dirname($fileinfo->getRealPath())) { + $target = \basename($target); + } + $this->writeEntryHeader( + $name, + static::TYPE_LINK, + 0, + $stat['uid'], + $stat['gid'], + $stat['mode'], + $stat['mtime'], + $target + ); + } else if ($fileinfo->isDir()) { + $this->addDir($name, $fileinfo); + } else if ($fileinfo->isFile()) { + return $this->addFromStream($fileinfo->openFile('rb'), $name); + } + return true; + } + + public function addFromStream($resource, string $name, int $time = 0) : bool + { + if (\is_resource($resource)) { + $temp = new TarTempResource(); + while (!\feof($resource)) { + $data = $resource->fread(4096); + if (false === $data || '' === $data) { + break; + } + $temp->write($data); + } + $temp->rewind(); + $resource = $temp; + } else if (!$resource instanceof \SplFileObject) { + throw new \Exception('Invalid resource'); + } + + $this->writeEntryHeader( + \strtr($name, '\\', '/'), + static::TYPE_FILE, + $resource->getSize(), + $resource->getOwner(), + $resource->getGroup(), + $resource->getPerms(), + $time ?: $resource->getMTime() + ); + $l = 0; + while (!$resource->eof()) { + // deflate works best with buffers >32K + $data = $resource->fread(65536); + if (false === $data || '' === $data) { + break; + } + $l += $this->write($data); + } + if ($l = $l % 512) { + $l = 512 - $l; + $this->write(\pack("a{$l}", '')); + } + return true; + } + + public function addFromString(string $name, string $data, int $time = 0) : bool + { + $this->writeEntryHeader( + \strtr($name, '\\', '/'), + static::TYPE_FILE, + \strlen($data), + 0, + 0, + 420, + $time + ); + $l += $this->write($data); + if ($l = $l % 512) { + $l = 512 - $l; + $this->write(\pack("a{$l}", '')); + } + return true; + } + + public function addDir(string $dirname, \SplFileInfo $fileinfo = null) : void + { + $this->writeEntryHeader( + \rtrim(\strtr($dirname, '\\', '/'), '/') . '/', + static::TYPE_DIR, + 0, + $fileinfo ? $fileinfo->getOwner() : 0, + $fileinfo ? $fileinfo->getGroup() : 0, + $fileinfo ? $fileinfo->getPerms() : 493, + $fileinfo ? $fileinfo->getMTime() : 0 + ); + } + + public function addRecursive(string $dir, string $target_dir = '', $ignore = '#/(\\.hg(/|$)|\\.hgignore)#') : void + { + if (!$this->out) { + throw new \Exception('Stream closed'); + } + \clearstatcache(); + $dir = \rtrim($dir,'\\/') . '/'; + $dirl = \strlen($dir); + if ($target_dir) { + $target_dir = \rtrim($target_dir,'\\/') . '/'; + } + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS /*| \FilesystemIterator::FOLLOW_SYMLINKS*/), + \RecursiveIteratorIterator::SELF_FIRST, + \RecursiveIteratorIterator::CATCH_GET_CHILD); + $ignore_paths = array(); + foreach ($iterator as $name => $fileinfo) { + if ($ignore_paths && \preg_match('#'.\implode('|',$ignore_paths).'#', $name)) { + continue; + } + if (!$ignore || !\preg_match($ignore, $name)) { + $this->addFile($fileinfo, $target_dir . \substr($name, $dirl)); + } + // like: tar --exclude-caches -czf file.tgz * + if (\strpos($name, 'CACHEDIR.TAG')) { + $ignore_paths[] = \preg_quote(\dirname($name) . '/','#'); + } + } + } + + protected function start() + { + if ($this->started) { + return; + } + $this->started = true; + if (false !== $this->gzip) { + // Write gzip header, see http://www.zlib.org/rfc-gzip.html#member-format + if (!\fwrite($this->out, "\x1F\x8B\x08\x00".\pack('V', \time())."\0\x03", 10)) { + throw new \Exception('Failed to write to stream'); + } + // Start ZLIB compression (RFC 1950) + $this->gzip['w'] = \stream_filter_append($this->out, 'zlib.deflate', STREAM_FILTER_WRITE, array( + 'level' => 9, +// 'window' => 32768, + 'memory' => 9, + )); + // Start CRC32 hashing + $this->gzip['h'] = \hash_init('crc32b'); + } + } + + protected function write($string)/* : int|bool*/ + { + if (!$this->out) { + throw new \Exception('Stream closed'); + } + if (!$this->started) { + $this->start(); + } + $length = \strlen($string); + $written = 0; + while ($written < $length) { + $bytes = \fwrite($this->out, $written ? \substr($string, $written) : $string); + if (!$bytes) { + return $written ?: false; + } + $written += $bytes; + } + + if (!empty($this->gzip['h'])) { + $this->gzip['l'] += $length; + \hash_update($this->gzip['h'], $string); + } + + return $written; + } + + protected function writeEntryHeader($name, $type, $size, $uid = 0, $gid = 0, $perm = 0, $mtime = 0, $link = '', $prefix = '') + { + // handle long filename length + $paxdata = $paxname = ''; + if (100 < \strlen($link)) { + $length = \strlen($link) + 11; + $length += \strlen($length); + $paxdata = "{$length} linkpath={$link}\n"; + $link = '././@LongSymLink'; + } + $l = \strlen($name); + if (($paxdata?90:100) < $l) { + // split into name and prefix + $p = \strpos($name, '/', \max(0, $l - 90)); + if ($p && $p < $l-1) { + $file = \substr($name, $p+1); + $prefix = \substr($name, 0, $p); + $paxname = \preg_replace('#(^|/)([^/]+)$#', '$1PaxHeader/$2', $file); + } else { + $file = \basename($name); + if (static::TYPE_DIR === $type) { + $file .= '/'; + } + $prefix = \dirname($name); + $paxname = 'PaxHeader/' . $file; + } + if (100 < \strlen($file) || 155 < \strlen($prefix)) { + // POSIX.1-2001/pax + $length = $l + 7; + $length += \strlen($length); + $paxdata = "{$length} path={$name}\n{$paxdata}"; + if (static::TYPE_DIR === $type) { + $name = \substr($file, 0, 98) . '/'; + } else { + $name = \substr($file, 0, 99); + } + } else { + // POSIX ustar + $name = $file; + } + $paxname = \substr($paxname, 0, 98); + } + if ($paxdata) { + /* Add? + $data .= "30 mtime=1461056595.149922432\n" + $data .= "30 ctime=1461056595.149922432\n" + */ + $this->writeUStarEntryHeader( + $paxname ?: \preg_replace('#(^|/)([^/]+)$#', '$1PaxHeader/$2', $name), + 'x', + \strlen($paxdata), + $uid, + $gid, + $perm, + $mtime, + '', + $prefix + ); + $l = 512 * \ceil(\strlen($paxdata) / 512); + $this->write(\pack("a{$l}", $paxdata)); + } + + $this->writeUStarEntryHeader($name, $type, $size, $uid, $gid, $perm, $mtime, $link, $prefix); + } + + // Writes Pre-POSIX.1-1988 (i.e. v7) and POSIX UStar headers + protected function writeUStarEntryHeader($name, $type, $size, $uid = 0, $gid = 0, $perm = 0, $mtime = 0, $link = '', $prefix = '') + { + $data = \pack('a100a8a8a8a12A12', + $name, + // values in octal + \sprintf("%06u ", \substr(\decoct($perm),-3)), + \sprintf("%06u ", \decoct($uid)), + \sprintf("%06u ", \decoct($gid)), + \sprintf("%011u ", \decoct($size)), + \sprintf("%011u", \decoct($mtime ?: \time()))); + $this->write($data); + $checksum = 0; + $i = 148; + while ($i--) { + $checksum += \ord($data[$i]); + } + + $data = \pack('a1a100a6a2a32a32a8a8a155a12', $type, $link, 'ustar', '00', '', '', '000000 ', '000000 ', $prefix, ''); + $checksum += 256; + $i = 356; + while ($i--) { + $checksum += \ord($data[$i]); + } + + $this->write(\pack('a8', \sprintf('%06u ', \decoct($checksum))) . $data); + } + +} + +class TarTempResource extends \SplTempFileObject +{ + #[\ReturnTypeWillChange] + public function getOwner()/*: int|false*/ { return 0; } + #[\ReturnTypeWillChange] + public function getGroup()/*: int|false*/ { return 0; } + #[\ReturnTypeWillChange] + public function getPerms()/*: int|false*/ { return 420; } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/stream/zip.php b/snappymail/v/0.0.0/app/libraries/snappymail/stream/zip.php index 7e804bd95..5a105a313 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/stream/zip.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/stream/zip.php @@ -40,8 +40,7 @@ class ZIP } if (\is_resource($target)) { $this->out = $target; - } - if (!$this->out) { + } else { throw new \Exception("Failed to open output: {$target}"); } @@ -80,7 +79,7 @@ class ZIP \header('Cache-Control: no-store'); \header('Pragma: no-cache'); \header('Content-Transfer-Encoding: binary'); - $name = "{$name}.zip"; + $name .= '.zip'; $name = \preg_match('#^[\x01-\x7F]*$#D', $name) ? $name : '=?UTF-8?B?'.\base64_encode($name).'?='; \header("Content-Disposition: attachment; filename={$name}"); \header("Content-Type: application/zip; name={$name}"); @@ -114,6 +113,10 @@ class ZIP public function addFromStream($resource, string $name, int $time = 0) : bool { + if (!$this->out) { + throw new \Exception('Stream closed'); + } + if ($resource instanceof \SplFileObject) { if (!$time) { $time = $resource->getMTime(); @@ -170,6 +173,10 @@ class ZIP public function addFromString(string $name, string $data, int $time = 0) : bool { + if (!$this->out) { + throw new \Exception('Stream closed'); + } + $file = new ZipEntry($name, $time, $this->compression); $file->u_len = \strlen($data); $file->setCrc32(\crc32($data)); @@ -202,6 +209,9 @@ class ZIP public function addRecursive(string $dir, string $target_dir = '', string $ignore = '#/(\\.hg(/|$)|\\.hgignore)#') : void { + if (!$this->out) { + throw new \Exception('Stream closed'); + } \clearstatcache(); $dir = \rtrim($dir,'\\/') . '/'; $dirl = \strlen($dir); diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/upgrade.php b/snappymail/v/0.0.0/app/libraries/snappymail/upgrade.php index 99be6d0ef..25bc65979 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/upgrade.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/upgrade.php @@ -166,12 +166,15 @@ abstract class Upgrade public static function backup() : string { - if (!\class_exists('PharData')) { - throw new \Exception('PHP Phar is disabled, you must enable it'); - } // $tar_destination = APP_DATA_FOLDER_PATH . APP_VERSION . '.tar'; - $tar_destination = APP_DATA_FOLDER_PATH . 'backup-' . \date('YmdHis') . '.tar'; - $tar = new \PharData($tar_destination); + $tar_destination = APP_DATA_FOLDER_PATH . 'backup-' . \date('YmdHis'); + if (\class_exists('PharData')) { + $tar_destination .= '.tar'; + $tar = new \PharData($tar_destination); + } else { + $tar_destination .= '.tgz'; + $tar = new \SnappyMail\Stream\TAR($tar_destination); + } $files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator(APP_DATA_FOLDER_PATH . '_data_'), \RecursiveIteratorIterator::SELF_FIRST @@ -183,6 +186,9 @@ abstract class Upgrade $tar->addFile($file, \substr($file, $l)); } } + if ($tar instanceof \SnappyMail\Stream\TAR) { + return $tar_destination; + } $tar->compress(\Phar::GZ); \unlink($tar_destination); return $tar_destination . '.gz';