tigerstyle-heat/includes/backup/class-restore-engine.php
Ryan Malloy 0028738e33 Initial commit: TigerStyle Heat v2.0.0
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.
2026-05-27 13:41:35 -06:00

723 lines
23 KiB
PHP

<?php
/**
* TigerStyle SEO Restore Engine
*
* Core restore processing engine with safety checks,
* validation, and rollback capabilities.
*
* @package TigerStyleSEO
* @subpackage RestoreEngine
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class TigerStyleSEO_Restore_Engine {
/**
* Single instance
*/
private static $instance = null;
/**
* Get instance
*/
public static function instance() {
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Compression manager
*/
private $compression_manager;
/**
* Storage manager
*/
private $storage_manager;
/**
* Logger
*/
private $logger;
/**
* Validator
*/
private $validator;
/**
* Current restore ID
*/
private $current_restore_id;
/**
* Backup manifest
*/
private $backup_manifest;
/**
* Restore settings
*/
private $settings;
/**
* Constructor
*/
private function __construct() {
$this->compression_manager = TigerStyleSEO_Compression_Manager::instance();
$this->storage_manager = TigerStyleSEO_Storage_Manager::instance();
$this->logger = TigerStyleSEO_Backup_Logger::instance();
$this->validator = TigerStyleSEO_Backup_Validator::instance();
$this->settings = get_option('tigerstyle_backup_settings', array());
}
/**
* Restore from backup
*
* @param array $options Restore options
* @return string Restore ID
*/
public function restore_backup($options = array()) {
$defaults = array(
'backup_id' => '',
'restore_files' => true,
'restore_database' => true,
'create_rollback' => true,
'force_restore' => false,
'validate_before_restore' => true
);
$options = wp_parse_args($options, $defaults);
if (empty($options['backup_id'])) {
throw new Exception(__('Backup ID is required', 'tigerstyle-heat'));
}
// Generate unique restore ID
$this->current_restore_id = 'restore_' . time() . '_' . wp_generate_password(8, false);
try {
// Initialize restore process
$this->init_restore_process($options);
// Download and extract backup
$backup_dir = $this->prepare_backup_for_restore($options['backup_id']);
// Load and validate manifest
$this->load_backup_manifest($backup_dir);
// Validate backup integrity
if ($options['validate_before_restore']) {
$this->validate_backup_integrity($backup_dir);
}
// Create rollback backup if requested
if ($options['create_rollback']) {
$this->create_rollback_backup();
}
// Perform restoration
if ($options['restore_database']) {
$this->restore_database($backup_dir);
}
if ($options['restore_files']) {
$this->restore_files($backup_dir);
}
// Finalize restoration
$this->finalize_restoration($backup_dir);
// Log success
$this->logger->info('Restore completed successfully', array(
'restore_id' => $this->current_restore_id,
'backup_id' => $options['backup_id'],
'options' => $options
));
// Update progress to 100%
$this->update_restore_progress(100, __('Restore completed successfully', 'tigerstyle-heat'));
return $this->current_restore_id;
} catch (Exception $e) {
$this->logger->error('Restore failed: ' . $e->getMessage(), array(
'restore_id' => $this->current_restore_id,
'backup_id' => $options['backup_id'],
'options' => $options
));
// Cleanup on failure
$this->cleanup_failed_restore();
throw $e;
}
}
/**
* Initialize restore process
*/
private function init_restore_process($options) {
// Set progress to 0%
$this->update_restore_progress(0, __('Initializing restore...', 'tigerstyle-heat'));
// Check system requirements
$this->check_restore_requirements($options);
// Set time limit
@set_time_limit(0);
// Increase memory limit if possible
@ini_set('memory_limit', '512M');
// Check permissions
$this->check_restore_permissions();
}
/**
* Check restore requirements
*/
private function check_restore_requirements($options) {
// Check if backup exists
if (!$this->storage_manager->backup_exists($options['backup_id'])) {
throw new Exception(__('Backup not found', 'tigerstyle-heat'));
}
// Check disk space
$backup_info = $this->storage_manager->get_backup_info($options['backup_id']);
$required_space = $backup_info['size'] * 3; // Backup size + extraction + current files
$available_space = disk_free_space(ABSPATH);
if ($available_space !== false && $available_space < $required_space) {
throw new Exception(__('Insufficient disk space for restore', 'tigerstyle-heat'));
}
// Check if WordPress is currently being used heavily
if (!$options['force_restore'] && $this->is_site_busy()) {
throw new Exception(__('Site appears to be busy. Use force_restore option to override.', 'tigerstyle-heat'));
}
}
/**
* Check restore permissions
*/
private function check_restore_permissions() {
// Check file system permissions
if (!is_writable(ABSPATH)) {
throw new Exception(__('WordPress root directory is not writable', 'tigerstyle-heat'));
}
if (!is_writable(WP_CONTENT_DIR)) {
throw new Exception(__('WordPress content directory is not writable', 'tigerstyle-heat'));
}
// Check database permissions
global $wpdb;
$test_result = $wpdb->query("CREATE TEMPORARY TABLE tigerstyle_test (id int)");
if ($test_result === false) {
throw new Exception(__('Insufficient database permissions for restore', 'tigerstyle-heat'));
}
}
/**
* Prepare backup for restore
*/
private function prepare_backup_for_restore($backup_id) {
$this->update_restore_progress(5, __('Downloading backup...', 'tigerstyle-heat'));
// Download backup from storage
$backup_file = $this->storage_manager->download_backup($backup_id);
$this->update_restore_progress(15, __('Extracting backup...', 'tigerstyle-heat'));
// Extract backup
$backup_dir = $this->extract_backup($backup_file);
return $backup_dir;
}
/**
* Extract backup
*/
private function extract_backup($backup_file) {
$upload_dir = wp_upload_dir();
$extract_dir = $upload_dir['basedir'] . '/tigerstyle-restores/' . $this->current_restore_id;
if (!wp_mkdir_p($extract_dir)) {
throw new Exception(__('Failed to create extraction directory', 'tigerstyle-heat'));
}
// Detect compression method from file extension
$compression_method = $this->detect_compression_method($backup_file);
// Extract using appropriate method
$this->compression_manager->extract_archive($backup_file, $extract_dir, $compression_method);
return $extract_dir;
}
/**
* Load backup manifest
*/
private function load_backup_manifest($backup_dir) {
$manifest_file = $backup_dir . '/manifest.json';
if (!file_exists($manifest_file)) {
throw new Exception(__('Backup manifest not found', 'tigerstyle-heat'));
}
$manifest_content = file_get_contents($manifest_file);
$this->backup_manifest = json_decode($manifest_content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception(__('Invalid backup manifest format', 'tigerstyle-heat'));
}
$this->logger->info('Backup manifest loaded', array(
'restore_id' => $this->current_restore_id,
'backup_id' => $this->backup_manifest['backup_id'],
'created_at' => $this->backup_manifest['created_at']
));
}
/**
* Validate backup integrity
*/
private function validate_backup_integrity($backup_dir) {
$this->update_restore_progress(20, __('Validating backup integrity...', 'tigerstyle-heat'));
if (!$this->validator->validate_backup_directory($backup_dir, $this->backup_manifest)) {
throw new Exception(__('Backup integrity validation failed', 'tigerstyle-heat'));
}
$this->logger->info('Backup integrity validated successfully', array(
'restore_id' => $this->current_restore_id,
'backup_id' => $this->backup_manifest['backup_id']
));
}
/**
* Create rollback backup
*/
private function create_rollback_backup() {
$this->update_restore_progress(25, __('Creating rollback backup...', 'tigerstyle-heat'));
try {
$backup_engine = new TigerStyleSEO_Backup_Engine();
$rollback_id = $backup_engine->create_backup(array(
'type' => 'rollback',
'description' => 'Rollback backup before restore - ' . current_time('mysql'),
'compression' => 'zip',
'storage_location' => 'local',
'include_files' => true,
'include_database' => true
));
// Store rollback ID for potential use
update_option('tigerstyle_last_rollback_backup', $rollback_id);
$this->logger->info('Rollback backup created', array(
'restore_id' => $this->current_restore_id,
'rollback_backup_id' => $rollback_id
));
} catch (Exception $e) {
$this->logger->warning('Failed to create rollback backup: ' . $e->getMessage());
// Don't fail the restore if rollback backup fails
}
}
/**
* Restore database
*/
private function restore_database($backup_dir) {
$this->update_restore_progress(30, __('Restoring database...', 'tigerstyle-heat'));
$sql_file = $backup_dir . '/database.sql';
if (!file_exists($sql_file)) {
throw new Exception(__('Database backup file not found', 'tigerstyle-heat'));
}
global $wpdb;
// Read and execute SQL file in chunks
$handle = fopen($sql_file, 'r');
if (!$handle) {
throw new Exception(__('Failed to open database backup file', 'tigerstyle-heat'));
}
try {
$sql_buffer = '';
$line_count = 0;
$total_lines = $this->count_file_lines($sql_file);
while (($line = fgets($handle)) !== false) {
$line_count++;
// Skip comments and empty lines
$line = trim($line);
if (empty($line) || strpos($line, '--') === 0) {
continue;
}
$sql_buffer .= $line;
// Execute complete statements
if (substr($line, -1) === ';') {
$result = $wpdb->query($sql_buffer);
if ($result === false && !empty($wpdb->last_error)) {
$this->logger->warning('Database restore warning: ' . $wpdb->last_error, array(
'sql' => substr($sql_buffer, 0, 200) . '...'
));
}
$sql_buffer = '';
// Update progress
if ($line_count % 100 === 0) {
$progress = 30 + (($line_count / $total_lines) * 40); // 30-70%
$this->update_restore_progress($progress, sprintf(__('Restoring database: %d%%', 'tigerstyle-heat'), ($line_count / $total_lines) * 100));
}
}
}
// Execute any remaining SQL
if (!empty(trim($sql_buffer))) {
$wpdb->query($sql_buffer);
}
} finally {
fclose($handle);
}
// Clear WordPress caches
wp_cache_flush();
$this->logger->info('Database restore completed', array(
'restore_id' => $this->current_restore_id,
'lines_processed' => $line_count
));
}
/**
* Restore files
*/
private function restore_files($backup_dir) {
$this->update_restore_progress(70, __('Restoring files...', 'tigerstyle-heat'));
$files_dir = $backup_dir . '/files';
if (!is_dir($files_dir)) {
throw new Exception(__('Files backup directory not found', 'tigerstyle-heat'));
}
// Count total files for progress tracking
$total_files = $this->count_directory_files($files_dir);
$processed_files = 0;
// Restore files
$this->restore_directory_recursive($files_dir, ABSPATH, $total_files, $processed_files);
$this->logger->info('File restore completed', array(
'restore_id' => $this->current_restore_id,
'files_restored' => $processed_files
));
}
/**
* Restore directory recursively
*/
private function restore_directory_recursive($source_dir, $dest_dir, $total_files, &$processed_files) {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($source_dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item) {
$source_path = $item->getPathname();
$relative_path = substr($source_path, strlen($source_dir) + 1);
$dest_path = $dest_dir . $relative_path;
if ($item->isDir()) {
if (!is_dir($dest_path)) {
wp_mkdir_p($dest_path);
}
} elseif ($item->isFile()) {
// Create destination directory if it doesn't exist
$dest_parent = dirname($dest_path);
if (!is_dir($dest_parent)) {
wp_mkdir_p($dest_parent);
}
// Copy file with error handling
if (!copy($source_path, $dest_path)) {
$this->logger->warning('Failed to restore file: ' . $relative_path);
} else {
// Preserve file permissions
$source_perms = fileperms($source_path);
chmod($dest_path, $source_perms);
}
$processed_files++;
// Update progress
if ($processed_files % 50 === 0) {
$progress = 70 + (($processed_files / $total_files) * 25); // 70-95%
$this->update_restore_progress($progress, sprintf(__('Restored %d of %d files', 'tigerstyle-heat'), $processed_files, $total_files));
}
}
}
}
/**
* Finalize restoration
*/
private function finalize_restoration($backup_dir) {
$this->update_restore_progress(95, __('Finalizing restoration...', 'tigerstyle-heat'));
// Update site URL if necessary
$this->update_site_urls();
// Clear all caches
$this->clear_all_caches();
// Flush rewrite rules
flush_rewrite_rules();
// Run any post-restore hooks
do_action('tigerstyle_backup_restore_completed', $this->current_restore_id, $this->backup_manifest);
// Cleanup extraction directory
$this->cleanup_extraction_directory($backup_dir);
$this->logger->info('Restoration finalized', array(
'restore_id' => $this->current_restore_id
));
}
/**
* Update site URLs if necessary
*/
private function update_site_urls() {
$current_home_url = home_url();
$current_site_url = site_url();
$backup_site_url = $this->backup_manifest['site_url'] ?? '';
// If URLs are different, ask user what to do
if (!empty($backup_site_url) && $backup_site_url !== $current_home_url) {
$this->logger->info('Site URL mismatch detected', array(
'current_url' => $current_home_url,
'backup_url' => $backup_site_url
));
// For now, keep current URLs
// In a full implementation, you might want to prompt the user
}
}
/**
* Clear all caches
*/
private function clear_all_caches() {
// WordPress object cache
wp_cache_flush();
// Opcache
if (function_exists('opcache_reset')) {
opcache_reset();
}
// Clear transients
global $wpdb;
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_%'");
// Clear common caching plugins
$this->clear_plugin_caches();
}
/**
* Clear plugin caches
*/
private function clear_plugin_caches() {
// WP Rocket
if (function_exists('rocket_clean_domain')) {
rocket_clean_domain();
}
// W3 Total Cache
if (function_exists('w3tc_flush_all')) {
w3tc_flush_all();
}
// WP Super Cache
if (function_exists('wp_cache_clear_cache')) {
wp_cache_clear_cache();
}
// LiteSpeed Cache
if (class_exists('LiteSpeed_Cache_API')) {
LiteSpeed_Cache_API::purge_all();
}
}
/**
* Update restore progress
*/
private function update_restore_progress($percentage, $message) {
$progress = array(
'percentage' => $percentage,
'message' => $message,
'timestamp' => time(),
'restore_id' => $this->current_restore_id
);
set_transient('tigerstyle_restore_progress_' . $this->current_restore_id, $progress, 3600);
}
/**
* Detect compression method from filename
*/
private function detect_compression_method($filename) {
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
switch ($extension) {
case 'zip':
return 'zip';
case 'gz':
return pathinfo(pathinfo($filename, PATHINFO_FILENAME), PATHINFO_EXTENSION) === 'tar' ? 'tar.gz' : 'gz';
case 'bz2':
return pathinfo(pathinfo($filename, PATHINFO_FILENAME), PATHINFO_EXTENSION) === 'tar' ? 'tar.bz2' : 'bz2';
default:
return 'none';
}
}
/**
* Count lines in file
*/
private function count_file_lines($filename) {
$handle = fopen($filename, 'r');
if (!$handle) {
return 0;
}
$line_count = 0;
while (fgets($handle) !== false) {
$line_count++;
}
fclose($handle);
return $line_count;
}
/**
* Count files in directory
*/
private function count_directory_files($directory) {
$count = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$count++;
}
}
return $count;
}
/**
* Check if site is busy
*/
private function is_site_busy() {
// Simple check: if there are many active sessions or high CPU usage
// This is a basic implementation - in production you might want more sophisticated checks
$load_average = sys_getloadavg();
if ($load_average && $load_average[0] > 2.0) {
return true;
}
return false;
}
/**
* Cleanup extraction directory
*/
private function cleanup_extraction_directory($backup_dir) {
if (is_dir($backup_dir)) {
$this->remove_directory($backup_dir);
}
}
/**
* Remove directory recursively
*/
private function remove_directory($dir) {
if (!is_dir($dir)) {
return;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $file) {
if ($file->isDir()) {
rmdir($file->getPathname());
} else {
unlink($file->getPathname());
}
}
rmdir($dir);
}
/**
* Cleanup failed restore
*/
private function cleanup_failed_restore() {
// Update progress to indicate failure
$this->update_restore_progress(0, __('Restore failed', 'tigerstyle-heat'));
// Add to failed restores list
$failed_restores = get_option('tigerstyle_failed_restores', array());
$failed_restores[] = array(
'restore_id' => $this->current_restore_id,
'timestamp' => time(),
'error' => 'Restore process failed'
);
update_option('tigerstyle_failed_restores', array_slice($failed_restores, -10)); // Keep last 10 failures
}
/**
* Rollback to previous state
*/
public function rollback_restore() {
$rollback_backup_id = get_option('tigerstyle_last_rollback_backup');
if (empty($rollback_backup_id)) {
throw new Exception(__('No rollback backup available', 'tigerstyle-heat'));
}
// Restore from rollback backup
return $this->restore_backup(array(
'backup_id' => $rollback_backup_id,
'restore_files' => true,
'restore_database' => true,
'create_rollback' => false, // Don't create another rollback
'validate_before_restore' => false // Skip validation for rollback
));
}
}