init(); } /** * Initialize compression manager */ private function init() { $this->logger = TigerStyleSEO_Backup_Logger::instance(); $this->detect_compression_methods(); } /** * Detect available compression methods */ private function detect_compression_methods() { $this->compression_methods = array(); // Check for ZIP support (most widely available) if (class_exists('ZipArchive')) { $this->compression_methods['zip'] = array( 'name' => 'ZIP', 'extension' => '.zip', 'mime_type' => 'application/zip', 'available' => true, 'compression_ratio' => 0.7, 'speed' => 'fast' ); } // Check for TAR.GZ support (better compression) if (extension_loaded('zlib') && class_exists('PharData')) { $this->compression_methods['tar.gz'] = array( 'name' => 'TAR.GZ', 'extension' => '.tar.gz', 'mime_type' => 'application/gzip', 'available' => true, 'compression_ratio' => 0.6, 'speed' => 'medium' ); } // Check for TAR.BZ2 support (best compression) if (extension_loaded('bz2') && class_exists('PharData')) { $this->compression_methods['tar.bz2'] = array( 'name' => 'TAR.BZ2', 'extension' => '.tar.bz2', 'mime_type' => 'application/bzip2', 'available' => true, 'compression_ratio' => 0.5, 'speed' => 'slow' ); } // Check for 7Z support (if available) if (extension_loaded('rar') || $this->command_exists('7z')) { $this->compression_methods['7z'] = array( 'name' => '7ZIP', 'extension' => '.7z', 'mime_type' => 'application/x-7z-compressed', 'available' => true, 'compression_ratio' => 0.4, 'speed' => 'very_slow' ); } $this->logger->info("Detected compression methods", array( 'methods' => array_keys($this->compression_methods) )); } /** * Get best available compression method */ public function get_best_compression_method($priority = 'balanced') { if (empty($this->compression_methods)) { return false; } switch ($priority) { case 'speed': $preferred_order = array('zip', 'tar.gz', 'tar.bz2', '7z'); break; case 'compression': $preferred_order = array('7z', 'tar.bz2', 'tar.gz', 'zip'); break; case 'balanced': default: $preferred_order = array('tar.gz', 'zip', 'tar.bz2', '7z'); break; } foreach ($preferred_order as $method) { if (isset($this->compression_methods[$method]) && $this->compression_methods[$method]['available']) { return $method; } } // Return first available method as fallback return array_keys($this->compression_methods)[0]; } /** * Compress directory */ public function compress_directory($directory, $backup_id, $method = null) { if (!$method) { $method = $this->get_best_compression_method(); } if (!$method || !isset($this->compression_methods[$method])) { $this->logger->error("Compression method not available", array('method' => $method)); return false; } $backup_location = get_option('backup_location', WP_CONTENT_DIR . '/tigerstyle-backups/'); $compressed_file = $backup_location . $backup_id . $this->compression_methods[$method]['extension']; // Ensure backup directory exists wp_mkdir_p(dirname($compressed_file)); $this->logger->info("Starting compression", array( 'method' => $method, 'directory' => $directory, 'output' => $compressed_file )); $start_time = microtime(true); $success = false; try { switch ($method) { case 'zip': $success = $this->create_zip_archive($directory, $compressed_file); break; case 'tar.gz': $success = $this->create_tar_gz_archive($directory, $compressed_file); break; case 'tar.bz2': $success = $this->create_tar_bz2_archive($directory, $compressed_file); break; case '7z': $success = $this->create_7z_archive($directory, $compressed_file); break; default: $this->logger->error("Unknown compression method", array('method' => $method)); return false; } if ($success && file_exists($compressed_file)) { $duration = microtime(true) - $start_time; $original_size = $this->get_directory_size($directory); $compressed_size = filesize($compressed_file); $compression_ratio = $compressed_size / $original_size; $this->logger->info("Compression completed", array( 'method' => $method, 'duration' => round($duration, 2) . 's', 'original_size' => $this->format_bytes($original_size), 'compressed_size' => $this->format_bytes($compressed_size), 'compression_ratio' => round($compression_ratio * 100, 1) . '%' )); return $compressed_file; } else { throw new Exception("Compression failed - output file not created"); } } catch (Exception $e) { $this->logger->error("Compression failed", array( 'method' => $method, 'error' => $e->getMessage() )); // Try fallback method $fallback_methods = array_keys($this->compression_methods); $current_index = array_search($method, $fallback_methods); if ($current_index !== false && isset($fallback_methods[$current_index + 1])) { $fallback_method = $fallback_methods[$current_index + 1]; $this->logger->info("Trying fallback compression method", array('method' => $fallback_method)); return $this->compress_directory($directory, $backup_id, $fallback_method); } return false; } } /** * Create ZIP archive */ private function create_zip_archive($directory, $output_file) { $zip = new ZipArchive(); $result = $zip->open($output_file, ZipArchive::CREATE | ZipArchive::OVERWRITE); if ($result !== TRUE) { throw new Exception("Cannot create ZIP archive: " . $result); } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $file) { $file_path = $file->getPathname(); $relative_path = substr($file_path, strlen($directory) + 1); if ($file->isDir()) { $zip->addEmptyDir($relative_path); } else { $zip->addFile($file_path, $relative_path); } } $result = $zip->close(); return $result; } /** * Create TAR.GZ archive */ private function create_tar_gz_archive($directory, $output_file) { $tar_file = str_replace('.gz', '', $output_file); // Create TAR first $phar = new PharData($tar_file); $phar->buildFromDirectory($directory); // Compress to GZ $phar->compress(Phar::GZ); // Remove uncompressed TAR file if (file_exists($tar_file)) { unlink($tar_file); } return file_exists($output_file); } /** * Create TAR.BZ2 archive */ private function create_tar_bz2_archive($directory, $output_file) { $tar_file = str_replace('.bz2', '', $output_file); // Create TAR first $phar = new PharData($tar_file); $phar->buildFromDirectory($directory); // Compress to BZ2 $phar->compress(Phar::BZ2); // Remove uncompressed TAR file if (file_exists($tar_file)) { unlink($tar_file); } return file_exists($output_file); } /** * Create ZIP archive using PHP ZipArchive (replaces 7z for security) */ private function create_7z_archive($directory, $output_file) { if (!extension_loaded('zip')) { throw new Exception("ZIP extension not available"); } // Convert .7z extension to .zip for compatibility if (pathinfo($output_file, PATHINFO_EXTENSION) === '7z') { $output_file = substr($output_file, 0, -2) . 'zip'; } $zip = new ZipArchive(); $result = $zip->open($output_file, ZipArchive::CREATE | ZipArchive::OVERWRITE); if ($result !== TRUE) { throw new Exception("Cannot create ZIP archive: " . $this->getZipError($result)); } // Add all files from directory recursively $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $file) { $file_path = $file->getPathname(); $relative_path = substr($file_path, strlen($directory) + 1); if ($file->isDir()) { $zip->addEmptyDir($relative_path); } elseif ($file->isFile()) { // Use maximum compression level (9) $zip->addFile($file_path, $relative_path); $zip->setCompressionName($relative_path, ZipArchive::CM_DEFLATE, 9); } } $close_result = $zip->close(); if (!$close_result) { throw new Exception("Failed to close ZIP archive"); } return file_exists($output_file); } /** * Extract compressed archive */ public function extract_archive($archive_file, $destination_directory) { if (!file_exists($archive_file)) { throw new Exception("Archive file not found: " . $archive_file); } $method = $this->detect_compression_method($archive_file); if (!$method) { throw new Exception("Cannot determine compression method for: " . $archive_file); } // Ensure destination directory exists wp_mkdir_p($destination_directory); $this->logger->info("Extracting archive", array( 'method' => $method, 'archive' => $archive_file, 'destination' => $destination_directory )); $start_time = microtime(true); try { switch ($method) { case 'zip': $success = $this->extract_zip_archive($archive_file, $destination_directory); break; case 'tar.gz': case 'tar.bz2': $success = $this->extract_tar_archive($archive_file, $destination_directory); break; case '7z': $success = $this->extract_7z_archive($archive_file, $destination_directory); break; default: throw new Exception("Unsupported compression method: " . $method); } if ($success) { $duration = microtime(true) - $start_time; $this->logger->info("Extraction completed", array( 'method' => $method, 'duration' => round($duration, 2) . 's' )); return true; } else { throw new Exception("Extraction failed"); } } catch (Exception $e) { $this->logger->error("Extraction failed", array( 'method' => $method, 'error' => $e->getMessage() )); return false; } } /** * Extract ZIP archive */ private function extract_zip_archive($archive_file, $destination) { $zip = new ZipArchive(); $result = $zip->open($archive_file); if ($result !== TRUE) { throw new Exception("Cannot open ZIP archive: " . $result); } $result = $zip->extractTo($destination); $zip->close(); return $result; } /** * Extract TAR archive (GZ or BZ2) */ private function extract_tar_archive($archive_file, $destination) { $phar = new PharData($archive_file); $phar->extractTo($destination); return true; } /** * Extract 7Z archive */ private function extract_7z_archive($archive_file, $destination) { if (!extension_loaded('zip')) { throw new Exception("ZIP extension not available"); } $zip = new ZipArchive(); $result = $zip->open($archive_file); if ($result !== TRUE) { throw new Exception("Cannot open ZIP archive: " . $this->getZipError($result)); } // Ensure destination directory exists if (!is_dir($destination)) { wp_mkdir_p($destination); } // Extract with security validation for ($i = 0; $i < $zip->numFiles; $i++) { $entry = $zip->getNameIndex($i); // Security check: prevent directory traversal if (strpos($entry, '../') !== false || strpos($entry, '..\\') !== false) { $zip->close(); throw new Exception("Archive contains unsafe path: " . $entry); } } $extract_result = $zip->extractTo($destination); $zip->close(); if (!$extract_result) { throw new Exception("Failed to extract ZIP archive"); } return true; } /** * Detect compression method from file extension */ private function detect_compression_method($file_path) { $extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION)); // Handle compound extensions if ($extension === 'gz' && strtolower(pathinfo(pathinfo($file_path, PATHINFO_FILENAME), PATHINFO_EXTENSION)) === 'tar') { return 'tar.gz'; } if ($extension === 'bz2' && strtolower(pathinfo(pathinfo($file_path, PATHINFO_FILENAME), PATHINFO_EXTENSION)) === 'tar') { return 'tar.bz2'; } $extension_map = array( 'zip' => 'zip', '7z' => '7z', 'gz' => 'tar.gz', 'bz2' => 'tar.bz2' ); return isset($extension_map[$extension]) ? $extension_map[$extension] : null; } /** * Get directory size */ private function get_directory_size($directory) { $size = 0; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS) ); foreach ($iterator as $file) { if ($file->isFile()) { $size += $file->getSize(); } } return $size; } /** * Check if PHP extension/feature exists (replaces shell command detection) */ private function command_exists($command) { switch ($command) { case '7z': // 7z now uses ZIP extension for security return extension_loaded('zip'); case 'zip': return extension_loaded('zip'); case 'tar': case 'gzip': return class_exists('PharData') && extension_loaded('zlib'); case 'bzip2': return class_exists('PharData') && extension_loaded('bz2'); default: return false; } } /** * Get human-readable ZIP error message */ private function getZipError($code) { switch($code) { case ZipArchive::ER_OK: return 'No error'; case ZipArchive::ER_MULTIDISK: return 'Multi-disk zip archives not supported'; case ZipArchive::ER_RENAME: return 'Renaming temporary file failed'; case ZipArchive::ER_CLOSE: return 'Closing zip archive failed'; case ZipArchive::ER_SEEK: return 'Seek error'; case ZipArchive::ER_READ: return 'Read error'; case ZipArchive::ER_WRITE: return 'Write error'; case ZipArchive::ER_CRC: return 'CRC error'; case ZipArchive::ER_ZIPCLOSED: return 'Containing zip archive was closed'; case ZipArchive::ER_NOENT: return 'No such file'; case ZipArchive::ER_EXISTS: return 'File already exists'; case ZipArchive::ER_OPEN: return 'Can not open file'; case ZipArchive::ER_TMPOPEN: return 'Failure to create temporary file'; case ZipArchive::ER_ZLIB: return 'Zlib error'; case ZipArchive::ER_MEMORY: return 'Memory allocation failure'; case ZipArchive::ER_CHANGED: return 'Entry has been changed'; case ZipArchive::ER_COMPNOTSUPP: return 'Compression method not supported'; case ZipArchive::ER_EOF: return 'Premature EOF'; case ZipArchive::ER_INVAL: return 'Invalid argument'; case ZipArchive::ER_NOZIP: return 'Not a zip archive'; case ZipArchive::ER_INTERNAL: return 'Internal error'; case ZipArchive::ER_INCONS: return 'Zip archive inconsistent'; case ZipArchive::ER_REMOVE: return 'Can not remove file'; case ZipArchive::ER_DELETED: return 'Entry has been deleted'; default: return "Unknown error code: $code"; } } /** * Format bytes for display */ private function format_bytes($bytes, $precision = 2) { $units = array('B', 'KB', 'MB', 'GB', 'TB'); for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) { $bytes /= 1024; } return round($bytes, $precision) . ' ' . $units[$i]; } /** * Get available compression methods */ public function get_available_methods() { return $this->compression_methods; } /** * Validate compressed file */ public function validate_archive($archive_file) { if (!file_exists($archive_file)) { return false; } $method = $this->detect_compression_method($archive_file); if (!$method) { return false; } try { switch ($method) { case 'zip': $zip = new ZipArchive(); $result = $zip->open($archive_file, ZipArchive::CHECKCONS); $zip->close(); return $result === TRUE; case 'tar.gz': case 'tar.bz2': $phar = new PharData($archive_file); return $phar->valid(); case '7z': // 7z files are now treated as ZIP files for security $zip = new ZipArchive(); $result = $zip->open($archive_file, ZipArchive::CHECKCONS); if ($result === TRUE) { $zip->close(); return true; } return false; default: return false; } } catch (Exception $e) { $this->logger->warning("Archive validation failed", array( 'file' => $archive_file, 'error' => $e->getMessage() )); return false; } } }