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.
500 lines
16 KiB
PHP
500 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* Core Backup Engine for TigerStyle SEO
|
|
* Handles filesystem and database backup operations with chunked processing
|
|
*/
|
|
|
|
// Prevent direct access
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
class TigerStyleSEO_Backup_Engine {
|
|
|
|
/**
|
|
* Single instance
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Backup settings
|
|
*/
|
|
private $settings = array();
|
|
|
|
/**
|
|
* Logger instance
|
|
*/
|
|
private $logger = null;
|
|
|
|
/**
|
|
* Storage manager instance
|
|
*/
|
|
private $storage = null;
|
|
|
|
/**
|
|
* Compression manager instance
|
|
*/
|
|
private $compression = 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 the backup engine
|
|
*/
|
|
private function init() {
|
|
$this->settings = $this->get_default_settings();
|
|
$this->logger = TigerStyleSEO_Backup_Logger::instance();
|
|
$this->storage = TigerStyleSEO_Storage_Manager::instance();
|
|
$this->compression = TigerStyleSEO_Compression_Manager::instance();
|
|
}
|
|
|
|
/**
|
|
* Get default backup settings
|
|
*/
|
|
private function get_default_settings() {
|
|
return array(
|
|
'backup_location' => WP_CONTENT_DIR . '/tigerstyle-backups/',
|
|
'chunk_size' => 5242880, // 5MB chunks
|
|
'max_execution_time' => 300, // 5 minutes - reasonable limit for backup operations
|
|
'compression_method' => 'zip',
|
|
'exclude_patterns' => array(
|
|
'*.log',
|
|
'cache/*',
|
|
'logs/*',
|
|
'node_modules/*',
|
|
'wp-content/tigerstyle-backups/*',
|
|
'*.tmp',
|
|
'.git/*',
|
|
'.svn/*'
|
|
),
|
|
'include_uploads' => true,
|
|
'include_themes' => true,
|
|
'include_plugins' => true,
|
|
'include_wp_core' => false,
|
|
'database_batch_size' => 1000
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create a complete backup
|
|
*/
|
|
public function create_backup($options = array()) {
|
|
$start_time = microtime(true);
|
|
$backup_id = $this->generate_backup_id();
|
|
|
|
$this->logger->info("Starting backup creation", array('backup_id' => $backup_id));
|
|
|
|
try {
|
|
// Set reasonable execution time limit to prevent runaway processes
|
|
set_time_limit($this->settings['max_execution_time']);
|
|
|
|
// Log the execution time limit being set
|
|
$this->logger->info("Setting execution time limit", array(
|
|
'time_limit_seconds' => $this->settings['max_execution_time']
|
|
));
|
|
|
|
// Create backup directory
|
|
$backup_dir = $this->create_backup_directory($backup_id);
|
|
if (!$backup_dir) {
|
|
throw new Exception('Failed to create backup directory');
|
|
}
|
|
|
|
$manifest = array(
|
|
'backup_id' => $backup_id,
|
|
'created_at' => current_time('mysql'),
|
|
'wordpress_version' => get_bloginfo('version'),
|
|
'plugin_version' => TIGERSTYLE_HEAT_VERSION,
|
|
'site_url' => site_url(),
|
|
'files' => array(),
|
|
'database' => array(),
|
|
'compression' => $this->settings['compression_method'],
|
|
'checksum' => '',
|
|
'size' => 0
|
|
);
|
|
|
|
// Backup files
|
|
if (!isset($options['skip_files']) || !$options['skip_files']) {
|
|
$this->logger->info("Starting file backup", array('backup_id' => $backup_id));
|
|
$file_backup = $this->backup_files($backup_dir, $backup_id);
|
|
$manifest['files'] = $file_backup;
|
|
}
|
|
|
|
// Backup database
|
|
if (!isset($options['skip_database']) || !$options['skip_database']) {
|
|
$this->logger->info("Starting database backup", array('backup_id' => $backup_id));
|
|
$db_backup = $this->backup_database($backup_dir, $backup_id);
|
|
$manifest['database'] = $db_backup;
|
|
}
|
|
|
|
// Create manifest file
|
|
$manifest_file = $backup_dir . '/manifest.json';
|
|
file_put_contents($manifest_file, json_encode($manifest, JSON_PRETTY_PRINT));
|
|
|
|
// Compress backup
|
|
$this->logger->info("Compressing backup", array('backup_id' => $backup_id));
|
|
$compressed_file = $this->compression->compress_directory($backup_dir, $backup_id);
|
|
|
|
if (!$compressed_file) {
|
|
throw new Exception('Failed to compress backup');
|
|
}
|
|
|
|
// Calculate final size and checksum
|
|
$manifest['size'] = filesize($compressed_file);
|
|
$manifest['checksum'] = md5_file($compressed_file);
|
|
|
|
// Update manifest in compressed file
|
|
file_put_contents($manifest_file, json_encode($manifest, JSON_PRETTY_PRINT));
|
|
|
|
// Store backup using storage manager
|
|
$stored_backup = $this->storage->store_backup($compressed_file, $backup_id, $manifest);
|
|
|
|
// Cleanup temporary files
|
|
$this->cleanup_temp_directory($backup_dir);
|
|
|
|
$duration = microtime(true) - $start_time;
|
|
$this->logger->info("Backup completed successfully", array(
|
|
'backup_id' => $backup_id,
|
|
'duration' => round($duration, 2) . 's',
|
|
'size' => $this->format_bytes($manifest['size'])
|
|
));
|
|
|
|
// Save backup record to database
|
|
$this->save_backup_record($backup_id, $manifest, $stored_backup, $compressed_file);
|
|
|
|
return array(
|
|
'success' => true,
|
|
'backup_id' => $backup_id,
|
|
'manifest' => $manifest,
|
|
'storage' => $stored_backup,
|
|
'duration' => $duration
|
|
);
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error("Backup failed", array(
|
|
'backup_id' => $backup_id,
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString()
|
|
));
|
|
|
|
// Cleanup on failure
|
|
if (isset($backup_dir)) {
|
|
$this->cleanup_temp_directory($backup_dir);
|
|
}
|
|
|
|
return array(
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
'backup_id' => $backup_id
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Backup files with chunked processing
|
|
*/
|
|
private function backup_files($backup_dir, $backup_id) {
|
|
$files_dir = $backup_dir . '/files/';
|
|
wp_mkdir_p($files_dir);
|
|
|
|
$file_list = array();
|
|
$total_size = 0;
|
|
$file_count = 0;
|
|
|
|
// Define directories to backup
|
|
$backup_paths = array();
|
|
|
|
if ($this->settings['include_uploads']) {
|
|
$backup_paths[] = WP_CONTENT_DIR . '/uploads/';
|
|
}
|
|
|
|
if ($this->settings['include_themes']) {
|
|
$backup_paths[] = WP_CONTENT_DIR . '/themes/';
|
|
}
|
|
|
|
if ($this->settings['include_plugins']) {
|
|
$backup_paths[] = WP_CONTENT_DIR . '/plugins/';
|
|
}
|
|
|
|
if ($this->settings['include_wp_core']) {
|
|
$backup_paths[] = ABSPATH;
|
|
}
|
|
|
|
foreach ($backup_paths as $path) {
|
|
if (!is_dir($path)) {
|
|
continue;
|
|
}
|
|
|
|
$this->logger->debug("Backing up directory", array('path' => $path));
|
|
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
|
|
foreach ($iterator as $file) {
|
|
if ($file->isDir()) {
|
|
continue;
|
|
}
|
|
|
|
$filepath = $file->getPathname();
|
|
$relative_path = str_replace(ABSPATH, '', $filepath);
|
|
|
|
// Check exclusion patterns
|
|
if ($this->should_exclude_file($relative_path)) {
|
|
continue;
|
|
}
|
|
|
|
// Copy file to backup directory
|
|
$backup_filepath = $files_dir . $relative_path;
|
|
$backup_file_dir = dirname($backup_filepath);
|
|
|
|
if (!is_dir($backup_file_dir)) {
|
|
wp_mkdir_p($backup_file_dir);
|
|
}
|
|
|
|
if (copy($filepath, $backup_filepath)) {
|
|
$file_size = filesize($filepath);
|
|
$file_list[] = array(
|
|
'path' => $relative_path,
|
|
'size' => $file_size,
|
|
'modified' => filemtime($filepath),
|
|
'checksum' => md5_file($filepath)
|
|
);
|
|
|
|
$total_size += $file_size;
|
|
$file_count++;
|
|
|
|
// Update progress every 100 files
|
|
if ($file_count % 100 === 0) {
|
|
$this->update_backup_progress($backup_id, 'files', $file_count);
|
|
}
|
|
} else {
|
|
$this->logger->warning("Failed to copy file", array('file' => $filepath));
|
|
}
|
|
}
|
|
}
|
|
|
|
return array(
|
|
'file_count' => $file_count,
|
|
'total_size' => $total_size,
|
|
'files' => $file_list
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Backup database with batch processing
|
|
*/
|
|
private function backup_database($backup_dir, $backup_id) {
|
|
global $wpdb;
|
|
|
|
$db_dir = $backup_dir . '/database/';
|
|
wp_mkdir_p($db_dir);
|
|
|
|
$sql_file = $db_dir . 'database.sql';
|
|
$tables_info = array();
|
|
|
|
// Get all WordPress tables
|
|
$tables = $wpdb->get_col("SHOW TABLES LIKE '{$wpdb->prefix}%'");
|
|
|
|
$sql_content = "-- TigerStyle SEO Database Backup\n";
|
|
$sql_content .= "-- Created: " . current_time('mysql') . "\n";
|
|
$sql_content .= "-- WordPress Version: " . get_bloginfo('version') . "\n\n";
|
|
$sql_content .= "SET FOREIGN_KEY_CHECKS = 0;\n";
|
|
$sql_content .= "SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';\n\n";
|
|
|
|
foreach ($tables as $table) {
|
|
$this->logger->debug("Backing up table", array('table' => $table));
|
|
|
|
// Get table structure
|
|
$create_table = $wpdb->get_row("SHOW CREATE TABLE `{$table}`", ARRAY_N);
|
|
$sql_content .= "\n-- Table structure for `{$table}`\n";
|
|
$sql_content .= "DROP TABLE IF EXISTS `{$table}`;\n";
|
|
$sql_content .= $create_table[1] . ";\n\n";
|
|
|
|
// Get table data in batches
|
|
$row_count = $wpdb->get_var("SELECT COUNT(*) FROM `{$table}`");
|
|
$offset = 0;
|
|
$batch_size = $this->settings['database_batch_size'];
|
|
|
|
$sql_content .= "-- Data for table `{$table}`\n";
|
|
|
|
while ($offset < $row_count) {
|
|
$rows = $wpdb->get_results("SELECT * FROM `{$table}` LIMIT {$offset}, {$batch_size}", ARRAY_A);
|
|
|
|
if (empty($rows)) {
|
|
break;
|
|
}
|
|
|
|
foreach ($rows as $row) {
|
|
$values = array();
|
|
foreach ($row as $value) {
|
|
$values[] = $wpdb->prepare('%s', $value);
|
|
}
|
|
|
|
$sql_content .= "INSERT INTO `{$table}` VALUES (" . implode(', ', $values) . ");\n";
|
|
}
|
|
|
|
$offset += $batch_size;
|
|
|
|
// Update progress
|
|
$progress = min(100, ($offset / $row_count) * 100);
|
|
$this->update_backup_progress($backup_id, 'database', $progress, $table);
|
|
}
|
|
|
|
$tables_info[] = array(
|
|
'name' => $table,
|
|
'rows' => $row_count,
|
|
'size' => $wpdb->get_var("SELECT ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'DB Size in MB' FROM information_schema.tables WHERE table_schema='{$wpdb->dbname}' AND table_name='{$table}'")
|
|
);
|
|
}
|
|
|
|
$sql_content .= "\nSET FOREIGN_KEY_CHECKS = 1;\n";
|
|
|
|
// Write SQL file
|
|
file_put_contents($sql_file, $sql_content);
|
|
|
|
return array(
|
|
'table_count' => count($tables),
|
|
'total_rows' => array_sum(array_column($tables_info, 'rows')),
|
|
'sql_file' => 'database/database.sql',
|
|
'sql_size' => filesize($sql_file),
|
|
'tables' => $tables_info
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if file should be excluded
|
|
*/
|
|
private function should_exclude_file($filepath) {
|
|
foreach ($this->settings['exclude_patterns'] as $pattern) {
|
|
if (fnmatch($pattern, $filepath)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Generate unique backup ID
|
|
*/
|
|
private function generate_backup_id() {
|
|
return 'backup_' . date('Y-m-d_H-i-s') . '_' . wp_generate_password(8, false);
|
|
}
|
|
|
|
/**
|
|
* Create backup directory
|
|
*/
|
|
private function create_backup_directory($backup_id) {
|
|
$backup_dir = $this->settings['backup_location'] . $backup_id . '/';
|
|
|
|
if (wp_mkdir_p($backup_dir)) {
|
|
// Create .htaccess for security
|
|
$htaccess_content = "Order deny,allow\nDeny from all\n";
|
|
file_put_contents($backup_dir . '.htaccess', $htaccess_content);
|
|
|
|
return $backup_dir;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Cleanup temporary directory
|
|
*/
|
|
private function cleanup_temp_directory($directory) {
|
|
if (!is_dir($directory)) {
|
|
return;
|
|
}
|
|
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::CHILD_FIRST
|
|
);
|
|
|
|
foreach ($iterator as $file) {
|
|
if ($file->isDir()) {
|
|
rmdir($file->getPathname());
|
|
} else {
|
|
unlink($file->getPathname());
|
|
}
|
|
}
|
|
|
|
rmdir($directory);
|
|
}
|
|
|
|
/**
|
|
* Update backup progress
|
|
*/
|
|
private function update_backup_progress($backup_id, $stage, $progress, $details = '') {
|
|
update_option('tigerstyle_backup_progress_' . $backup_id, array(
|
|
'stage' => $stage,
|
|
'progress' => $progress,
|
|
'details' => $details,
|
|
'updated' => time()
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Save backup record to database
|
|
*/
|
|
private function save_backup_record($backup_id, $manifest, $storage_info, $compressed_file) {
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->prefix . 'tigerstyle_backup_metadata';
|
|
|
|
$wpdb->insert(
|
|
$table_name,
|
|
array(
|
|
'backup_id' => $backup_id,
|
|
'storage_type' => $storage_info['type'] ?? 'local',
|
|
'file_path' => $storage_info['path'] ?? $compressed_file,
|
|
'file_size' => $manifest['size'],
|
|
'created_at' => current_time('mysql'),
|
|
'metadata_json' => json_encode($manifest)
|
|
),
|
|
array('%s', '%s', '%s', '%d', '%s', '%s')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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 backup progress
|
|
*/
|
|
public function get_backup_progress($backup_id) {
|
|
return get_option('tigerstyle_backup_progress_' . $backup_id, array());
|
|
}
|
|
|
|
/**
|
|
* Cleanup backup progress
|
|
*/
|
|
public function cleanup_backup_progress($backup_id) {
|
|
delete_option('tigerstyle_backup_progress_' . $backup_id);
|
|
}
|
|
} |