Make your WordPress site irresistible. Natural SEO attraction with: - robots.txt management - sitemap.xml generation - LLMs.txt support - Google integration (Analytics, Search Console, Tag Manager) - Schema.org structured data - Open Graph / Twitter Card meta tags - AMP support - Visual elements gallery - Built-in backup/restore module Includes build.sh and .distignore for WordPress-installable release ZIPs.
643 lines
21 KiB
PHP
643 lines
21 KiB
PHP
<?php
|
|
/**
|
|
* Compression Manager for TigerStyle SEO Backup System
|
|
* Supports multiple compression formats with graceful fallback
|
|
*/
|
|
|
|
// Prevent direct access
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
class TigerStyleSEO_Compression_Manager {
|
|
|
|
/**
|
|
* Single instance
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Supported compression methods in order of preference
|
|
*/
|
|
private $compression_methods = array();
|
|
|
|
/**
|
|
* Logger instance
|
|
*/
|
|
private $logger = null;
|
|
|
|
/**
|
|
* Get instance
|
|
*/
|
|
public static function instance() {
|
|
if (is_null(self::$instance)) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
private function __construct() {
|
|
$this->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;
|
|
}
|
|
}
|
|
} |