security = tigerstyle_life9()->get_security(); $this->file_scanner = new TigerStyle_Life9_File_Scanner(); $this->database_backup = new TigerStyle_Life9_Database_Backup(); $this->storage_manager = new TigerStyle_Life9_Storage_Manager(); $this->progress = [ 'stage' => 'idle', 'progress' => 0, 'files_processed' => 0, 'total_files' => 0, 'current_file' => '', 'bytes_processed' => 0, 'total_bytes' => 0 ]; } /** * Start backup process * * @param array $config Backup configuration * @return int|false Backup ID or false on failure */ public function start_backup($config) { try { // Validate configuration $validator = new TigerStyle_Life9_Validator(); if (!$validator->validate_backup_config($config)) { $this->log_error('Invalid backup configuration', $validator->get_errors()); return false; } $this->config = $config; // Create backup record $this->backup_id = $this->create_backup_record(); if (!$this->backup_id) { return false; } // Log backup start $this->security->log_security_event('backup_started', [ 'backup_id' => $this->backup_id, 'backup_name' => $config['backup_name'] ?? 'Unnamed', 'backup_type' => $config['backup_type'] ?? 'full' ]); // Execute backup asynchronously wp_schedule_single_event(time(), 'tigerstyle_life9_execute_backup', [$this->backup_id, $config]); return $this->backup_id; } catch (Exception $e) { $this->log_error('Backup start failed', ['error' => $e->getMessage()]); return false; } } /** * Execute backup process * * @param int $backup_id Backup ID * @param array $config Backup configuration */ public function execute_backup($backup_id, $config) { $this->backup_id = $backup_id; $this->config = $config; try { $this->update_backup_status('running'); $this->log_info('Backup execution started'); // Create temporary working directory $temp_dir = $this->create_temp_directory(); if (!$temp_dir) { throw new Exception('Failed to create temporary directory'); } $backup_parts = []; // Stage 1: Database backup if (!empty($config['include_database'])) { $this->update_progress('database', 0); $db_file = $this->backup_database($temp_dir); if ($db_file) { $backup_parts['database'] = $db_file; $this->log_info('Database backup completed'); } else { throw new Exception('Database backup failed'); } $this->update_progress('database', 100); } // Stage 2: Files backup if (!empty($config['include_files'])) { $this->update_progress('files', 0); $files_archive = $this->backup_files($temp_dir); if ($files_archive) { $backup_parts['files'] = $files_archive; $this->log_info('Files backup completed'); } else { throw new Exception('Files backup failed'); } $this->update_progress('files', 100); } // Stage 3: Create final archive $this->update_progress('archive', 0); $final_archive = $this->create_final_archive($backup_parts, $temp_dir); if (!$final_archive) { throw new Exception('Failed to create final archive'); } $this->update_progress('archive', 100); // Stage 4: Storage and cleanup $this->update_progress('storage', 0); $stored_path = $this->store_backup($final_archive); if (!$stored_path) { throw new Exception('Failed to store backup'); } // Generate checksum $encryption = new TigerStyle_Life9_Encryption(); $checksum = $encryption->file_checksum($stored_path); // Update backup record $this->finalize_backup_record($stored_path, filesize($stored_path), $checksum); // Cleanup temporary files $this->cleanup_temp_directory($temp_dir); $this->update_backup_status('completed'); $this->log_info('Backup completed successfully'); // Log completion $this->security->log_security_event('backup_completed', [ 'backup_id' => $this->backup_id, 'file_size' => filesize($stored_path), 'checksum' => $checksum ]); } catch (Exception $e) { $this->log_error('Backup failed', ['error' => $e->getMessage()]); $this->update_backup_status('failed'); // Cleanup on failure if (isset($temp_dir)) { $this->cleanup_temp_directory($temp_dir); } if (isset($stored_path) && file_exists($stored_path)) { unlink($stored_path); } } } /** * Cancel backup process * * @param int $backup_id Backup ID * @return bool Success status */ public function cancel_backup($backup_id) { try { global $wpdb; $backup = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}tigerstyle_life9_backups WHERE id = %d", $backup_id )); if (!$backup) { return false; } if (!in_array($backup->status, ['pending', 'running'])) { return false; // Cannot cancel completed/failed backups } // Update status $wpdb->update( $wpdb->prefix . 'tigerstyle_life9_backups', ['status' => 'cancelled', 'completed_at' => current_time('mysql')], ['id' => $backup_id], ['%s', '%s'], ['%d'] ); // Log cancellation $this->log_info('Backup cancelled by user', $backup_id); $this->security->log_security_event('backup_cancelled', [ 'backup_id' => $backup_id ]); return true; } catch (Exception $e) { error_log('TigerStyle Life9: Cancel backup error - ' . $e->getMessage()); return false; } } /** * Create backup record in database * * @return int|false Backup ID or false on failure */ private function create_backup_record() { try { global $wpdb; $result = $wpdb->insert( $wpdb->prefix . 'tigerstyle_life9_backups', [ 'name' => $this->config['backup_name'] ?? 'Backup ' . date('Y-m-d H:i:s'), 'status' => 'pending', 'created_at' => current_time('mysql'), 'backup_type' => $this->config['backup_type'] ?? 'full', 'includes_files' => !empty($this->config['include_files']) ? 1 : 0, 'includes_database' => !empty($this->config['include_database']) ? 1 : 0, 'compression' => $this->config['compression_method'] ?? 'zip', 'settings' => wp_json_encode($this->config) ], ['%s', '%s', '%s', '%s', '%d', '%d', '%s', '%s'] ); return $result ? $wpdb->insert_id : false; } catch (Exception $e) { error_log('TigerStyle Life9: Create backup record error - ' . $e->getMessage()); return false; } } /** * Backup database * * @param string $temp_dir Temporary directory * @return string|false Database backup file path or false on failure */ private function backup_database($temp_dir) { try { $this->log_info('Starting database backup'); $db_config = [ 'include_tables' => $this->config['database_tables'] ?? [], 'exclude_tables' => $this->config['exclude_database_tables'] ?? [], 'add_drop_table' => true, 'add_if_not_exists' => false, 'disable_keys' => true, 'where_conditions' => $this->config['database_where'] ?? [] ]; $db_file = $temp_dir . '/database.sql'; $success = $this->database_backup->export_database($db_file, $db_config); if ($success && file_exists($db_file)) { // Compress database file $compressed_file = $temp_dir . '/database.sql.gz'; if ($this->compress_file($db_file, $compressed_file)) { unlink($db_file); // Remove uncompressed version return $compressed_file; } return $db_file; } return false; } catch (Exception $e) { $this->log_error('Database backup failed', ['error' => $e->getMessage()]); return false; } } /** * Backup files * * @param string $temp_dir Temporary directory * @return string|false Files backup archive path or false on failure */ private function backup_files($temp_dir) { try { $this->log_info('Starting files backup'); // Scan files to backup $scan_config = [ 'include_paths' => $this->config['include_paths'] ?? [ABSPATH], 'exclude_patterns' => $this->get_exclude_patterns(), 'follow_symlinks' => false, 'max_file_size' => $this->get_max_file_size() ]; $files = $this->file_scanner->scan_files($scan_config); if (empty($files)) { $this->log_error('No files found to backup'); return false; } $this->progress['total_files'] = count($files); $this->progress['total_bytes'] = array_sum(array_column($files, 'size')); // Create files archive $archive_file = $temp_dir . '/files.' . $this->get_compression_extension(); switch ($this->config['compression_method'] ?? 'zip') { case 'zip': $success = $this->create_zip_archive($files, $archive_file); break; case 'tar': $success = $this->create_tar_archive($files, $archive_file); break; default: $success = $this->create_zip_archive($files, $archive_file); } return $success ? $archive_file : false; } catch (Exception $e) { $this->log_error('Files backup failed', ['error' => $e->getMessage()]); return false; } } /** * Create ZIP archive * * @param array $files List of files * @param string $archive_path Archive file path * @return bool Success status */ private function create_zip_archive($files, $archive_path) { if (!class_exists('ZipArchive')) { $this->log_error('ZipArchive class not available'); return false; } $zip = new ZipArchive(); $result = $zip->open($archive_path, ZipArchive::CREATE | ZipArchive::OVERWRITE); if ($result !== true) { $this->log_error('Failed to create ZIP archive', ['error_code' => $result]); return false; } $processed = 0; $base_path = rtrim(ABSPATH, '/'); foreach ($files as $file) { if (!$this->security->validate_path($file['path'], ABSPATH)) { $this->log_error('Invalid file path skipped', ['path' => $file['path']]); continue; } if (!file_exists($file['path']) || !is_readable($file['path'])) { $this->log_error('File not readable, skipped', ['path' => $file['path']]); continue; } // Get relative path for archive $relative_path = ltrim(str_replace($base_path, '', $file['path']), '/'); if ($file['type'] === 'directory') { $zip->addEmptyDir($relative_path); } else { $zip->addFile($file['path'], $relative_path); } $processed++; $this->progress['files_processed'] = $processed; $this->progress['current_file'] = $relative_path; $this->progress['progress'] = round(($processed / $this->progress['total_files']) * 100); // Update progress periodically if ($processed % 100 === 0) { $this->update_progress('files', $this->progress['progress']); } } $result = $zip->close(); if (!$result) { $this->log_error('Failed to finalize ZIP archive'); return false; } $this->log_info('ZIP archive created successfully', [ 'file_count' => $processed, 'archive_size' => filesize($archive_path) ]); return true; } /** * Create TAR archive * * @param array $files List of files * @param string $archive_path Archive file path * @return bool Success status */ private function create_tar_archive($files, $archive_path) { try { $tar_command = 'tar -czf ' . escapeshellarg($archive_path); $base_path = rtrim(ABSPATH, '/'); // Create file list $file_list = tempnam(sys_get_temp_dir(), 'tigerstyle_life9_files_'); $handle = fopen($file_list, 'w'); if (!$handle) { throw new Exception('Failed to create file list'); } foreach ($files as $file) { if (!$this->security->validate_path($file['path'], ABSPATH)) { continue; } $relative_path = ltrim(str_replace($base_path, '', $file['path']), '/'); fwrite($handle, $relative_path . "\n"); } fclose($handle); // Execute tar command $tar_command .= ' -C ' . escapeshellarg($base_path) . ' -T ' . escapeshellarg($file_list); exec($tar_command . ' 2>&1', $output, $return_code); // Cleanup file list unlink($file_list); if ($return_code !== 0) { $this->log_error('TAR command failed', [ 'command' => $tar_command, 'output' => implode("\n", $output), 'return_code' => $return_code ]); return false; } $this->log_info('TAR archive created successfully', [ 'archive_size' => filesize($archive_path) ]); return true; } catch (Exception $e) { $this->log_error('TAR archive creation failed', ['error' => $e->getMessage()]); return false; } } /** * Create final backup archive * * @param array $backup_parts Individual backup parts * @param string $temp_dir Temporary directory * @return string|false Final archive path or false on failure */ private function create_final_archive($backup_parts, $temp_dir) { try { $final_archive = $temp_dir . '/backup_' . date('Y-m-d_H-i-s') . '.zip'; $zip = new ZipArchive(); $result = $zip->open($final_archive, ZipArchive::CREATE | ZipArchive::OVERWRITE); if ($result !== true) { $this->log_error('Failed to create final archive', ['error_code' => $result]); return false; } // Add backup parts to final archive foreach ($backup_parts as $type => $file_path) { if (file_exists($file_path)) { $zip->addFile($file_path, basename($file_path)); } } // Add backup manifest $manifest = [ 'backup_id' => $this->backup_id, 'created_at' => current_time('mysql'), 'wordpress_version' => get_bloginfo('version'), 'php_version' => PHP_VERSION, 'plugin_version' => TIGERSTYLE_LIFE9_VERSION, 'backup_type' => $this->config['backup_type'] ?? 'full', 'includes_files' => !empty($this->config['include_files']), 'includes_database' => !empty($this->config['include_database']), 'parts' => array_keys($backup_parts) ]; $zip->addFromString('backup-manifest.json', wp_json_encode($manifest, JSON_PRETTY_PRINT)); $result = $zip->close(); if (!$result) { $this->log_error('Failed to finalize backup archive'); return false; } return $final_archive; } catch (Exception $e) { $this->log_error('Final archive creation failed', ['error' => $e->getMessage()]); return false; } } /** * Store backup in final location * * @param string $archive_path Temporary archive path * @return string|false Final storage path or false on failure */ private function store_backup($archive_path) { try { $upload_dir = wp_upload_dir(); $backup_dir = $upload_dir['basedir'] . '/tigerstyle-life9/backups'; // Ensure backup directory exists if (!file_exists($backup_dir)) { wp_mkdir_p($backup_dir); } $filename = 'backup_' . $this->backup_id . '_' . date('Y-m-d_H-i-s') . '.zip'; $final_path = $backup_dir . '/' . $filename; // Move archive to final location if (!rename($archive_path, $final_path)) { throw new Exception('Failed to move backup to final location'); } // Set proper permissions chmod($final_path, 0644); $this->log_info('Backup stored successfully', [ 'path' => $final_path, 'size' => filesize($final_path) ]); return $final_path; } catch (Exception $e) { $this->log_error('Backup storage failed', ['error' => $e->getMessage()]); return false; } } /** * Finalize backup record * * @param string $file_path Final backup file path * @param int $file_size File size in bytes * @param string $checksum File checksum */ private function finalize_backup_record($file_path, $file_size, $checksum) { global $wpdb; $wpdb->update( $wpdb->prefix . 'tigerstyle_life9_backups', [ 'file_path' => $file_path, 'file_size' => $file_size, 'hash' => $checksum, 'completed_at' => current_time('mysql') ], ['id' => $this->backup_id], ['%s', '%d', '%s', '%s'], ['%d'] ); } /** * Create temporary directory * * @return string|false Temporary directory path or false on failure */ private function create_temp_directory() { $upload_dir = wp_upload_dir(); $temp_base = $upload_dir['basedir'] . '/tigerstyle-life9/temp'; if (!file_exists($temp_base)) { wp_mkdir_p($temp_base); } $temp_dir = $temp_base . '/backup_' . $this->backup_id . '_' . time(); if (wp_mkdir_p($temp_dir)) { return $temp_dir; } return false; } /** * Cleanup temporary directory * * @param string $temp_dir Temporary directory path */ private function cleanup_temp_directory($temp_dir) { if (file_exists($temp_dir)) { $this->recursive_rmdir($temp_dir); } } /** * Recursively remove directory * * @param string $dir Directory path */ private function recursive_rmdir($dir) { if (is_dir($dir)) { $objects = scandir($dir); foreach ($objects as $object) { if ($object != "." && $object != "..") { if (is_dir($dir . "/" . $object)) { $this->recursive_rmdir($dir . "/" . $object); } else { unlink($dir . "/" . $object); } } } rmdir($dir); } } /** * Get exclude patterns * * @return array */ private function get_exclude_patterns() { $default_patterns = [ '*.log', '*.tmp', '*~', '.DS_Store', 'Thumbs.db', 'wp-content/cache/*', 'wp-content/backup/*', 'wp-content/uploads/tigerstyle-life9/*' ]; $custom_patterns = $this->config['exclude_patterns'] ?? []; return array_merge($default_patterns, $custom_patterns); } /** * Get maximum file size for backup * * @return int Maximum file size in bytes */ private function get_max_file_size() { $max_size_mb = get_option('tigerstyle_life9_max_backup_size_mb', 1000); return $max_size_mb * 1024 * 1024; } /** * Get compression file extension * * @return string */ private function get_compression_extension() { switch ($this->config['compression_method'] ?? 'zip') { case 'tar': return 'tar.gz'; case 'gzip': return 'gz'; default: return 'zip'; } } /** * Compress file using gzip * * @param string $source Source file path * @param string $destination Destination file path * @return bool Success status */ private function compress_file($source, $destination) { if (!function_exists('gzencode')) { return false; } $data = file_get_contents($source); if ($data === false) { return false; } $compressed = gzencode($data, 9); if ($compressed === false) { return false; } return file_put_contents($destination, $compressed) !== false; } /** * Update backup status * * @param string $status New status */ private function update_backup_status($status) { global $wpdb; $wpdb->update( $wpdb->prefix . 'tigerstyle_life9_backups', ['status' => $status], ['id' => $this->backup_id], ['%s'], ['%d'] ); } /** * Update backup progress * * @param string $stage Current stage * @param int $progress Progress percentage */ private function update_progress($stage, $progress) { $this->progress['stage'] = $stage; $this->progress['progress'] = $progress; // Store progress in database or cache for real-time updates set_transient('tigerstyle_life9_backup_progress_' . $this->backup_id, $this->progress, 300); } /** * Log info message * * @param string $message Log message * @param array $context Additional context */ private function log_info($message, $context = []) { $this->log_message('info', $message, $context); } /** * Log error message * * @param string $message Log message * @param array $context Additional context */ private function log_error($message, $context = []) { $this->log_message('error', $message, $context); } /** * Log message to database * * @param string $level Log level * @param string $message Log message * @param array $context Additional context */ private function log_message($level, $message, $context = []) { global $wpdb; $wpdb->insert( $wpdb->prefix . 'tigerstyle_life9_logs', [ 'backup_id' => $this->backup_id ?? 0, 'level' => $level, 'message' => $message, 'context' => wp_json_encode($context), 'created_at' => current_time('mysql') ], ['%d', '%s', '%s', '%s', '%s'] ); // Also log to WordPress error log for debugging error_log("TigerStyle Life9 [{$level}]: {$message}"); } } // Register backup execution hook add_action('tigerstyle_life9_execute_backup', function($backup_id, $config) { $backup_engine = new TigerStyle_Life9_Backup_Engine(); $backup_engine->execute_backup($backup_id, $config); }, 10, 2);