tigerstyle-heat/includes/backup/class-backup-validator.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

695 lines
24 KiB
PHP

<?php
/**
* TigerStyle SEO Backup Validator
*
* Validates backup integrity, checksums, and structure
* to ensure reliable restoration.
*
* @package TigerStyleSEO
* @subpackage BackupValidator
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class TigerStyleSEO_Backup_Validator {
/**
* Single instance
*/
private static $instance = null;
/**
* Get instance
*/
public static function instance() {
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Logger
*/
private $logger;
/**
* Constructor
*/
private function __construct() {
$this->logger = TigerStyleSEO_Backup_Logger::instance();
}
/**
* Validate backup by ID
*/
public function validate_backup($backup_id) {
try {
$storage_manager = new TigerStyleSEO_Storage_Manager();
// Check if backup exists
if (!$storage_manager->backup_exists($backup_id)) {
throw new Exception(__('Backup does not exist', 'tigerstyle-heat'));
}
// Download backup for validation
$backup_file = $storage_manager->download_backup($backup_id);
// Extract backup temporarily
$temp_dir = $this->extract_backup_for_validation($backup_file);
try {
// Validate backup structure and integrity
$result = $this->validate_backup_directory($temp_dir);
return $result;
} finally {
// Cleanup temporary files
$this->cleanup_temp_directory($temp_dir);
}
} catch (Exception $e) {
$this->logger->error('Backup validation failed: ' . $e->getMessage(), array(
'backup_id' => $backup_id
));
throw $e;
}
}
/**
* Validate backup directory structure and integrity
*/
public function validate_backup_directory($backup_dir, $manifest = null) {
$validation_results = array(
'valid' => true,
'errors' => array(),
'warnings' => array(),
'checks' => array()
);
try {
// Load manifest if not provided
if (!$manifest) {
$manifest = $this->load_manifest($backup_dir);
}
// Check required files
$validation_results['checks']['manifest'] = $this->validate_manifest($backup_dir, $manifest);
$validation_results['checks']['structure'] = $this->validate_backup_structure($backup_dir, $manifest);
$validation_results['checks']['database'] = $this->validate_database_backup($backup_dir, $manifest);
$validation_results['checks']['files'] = $this->validate_files_backup($backup_dir, $manifest);
$validation_results['checks']['checksums'] = $this->validate_checksums($backup_dir, $manifest);
// Collect errors and warnings
foreach ($validation_results['checks'] as $check_name => $check_result) {
if (!$check_result['valid']) {
$validation_results['valid'] = false;
$validation_results['errors'] = array_merge(
$validation_results['errors'],
$check_result['errors']
);
}
$validation_results['warnings'] = array_merge(
$validation_results['warnings'],
$check_result['warnings']
);
}
$this->logger->info('Backup validation completed', array(
'backup_dir' => basename($backup_dir),
'valid' => $validation_results['valid'],
'error_count' => count($validation_results['errors']),
'warning_count' => count($validation_results['warnings'])
));
} catch (Exception $e) {
$validation_results['valid'] = false;
$validation_results['errors'][] = $e->getMessage();
$this->logger->error('Backup validation exception: ' . $e->getMessage(), array(
'backup_dir' => $backup_dir
));
}
return $validation_results;
}
/**
* Load and validate manifest file
*/
private function load_manifest($backup_dir) {
$manifest_file = $backup_dir . '/manifest.json';
if (!file_exists($manifest_file)) {
throw new Exception(__('Manifest file not found', 'tigerstyle-heat'));
}
$manifest_content = file_get_contents($manifest_file);
$manifest = json_decode($manifest_content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception(__('Invalid manifest format: ', 'tigerstyle-heat') . json_last_error_msg());
}
return $manifest;
}
/**
* Validate manifest structure and content
*/
private function validate_manifest($backup_dir, $manifest) {
$result = array(
'valid' => true,
'errors' => array(),
'warnings' => array()
);
// Check required manifest fields
$required_fields = array(
'backup_id',
'created_at',
'wordpress_version',
'site_url',
'plugin_version',
'options'
);
foreach ($required_fields as $field) {
if (!isset($manifest[$field])) {
$result['errors'][] = sprintf(__('Missing required manifest field: %s', 'tigerstyle-heat'), $field);
$result['valid'] = false;
}
}
// Validate manifest data types
if (isset($manifest['created_at']) && !$this->is_valid_datetime($manifest['created_at'])) {
$result['errors'][] = __('Invalid created_at format in manifest', 'tigerstyle-heat');
$result['valid'] = false;
}
if (isset($manifest['site_url']) && !filter_var($manifest['site_url'], FILTER_VALIDATE_URL)) {
$result['warnings'][] = __('Invalid site_url format in manifest', 'tigerstyle-heat');
}
// Check WordPress version compatibility
if (isset($manifest['wordpress_version'])) {
$current_wp_version = get_bloginfo('version');
if (version_compare($manifest['wordpress_version'], $current_wp_version, '>')) {
$result['warnings'][] = sprintf(
__('Backup was created with newer WordPress version (%s) than current (%s)', 'tigerstyle-heat'),
$manifest['wordpress_version'],
$current_wp_version
);
}
}
// Validate options structure
if (isset($manifest['options']) && !is_array($manifest['options'])) {
$result['errors'][] = __('Invalid options structure in manifest', 'tigerstyle-heat');
$result['valid'] = false;
}
return $result;
}
/**
* Validate backup directory structure
*/
private function validate_backup_structure($backup_dir, $manifest) {
$result = array(
'valid' => true,
'errors' => array(),
'warnings' => array()
);
// Check for manifest file
if (!file_exists($backup_dir . '/manifest.json')) {
$result['errors'][] = __('Manifest file is missing', 'tigerstyle-heat');
$result['valid'] = false;
}
// Check for database backup if included
if (isset($manifest['options']['include_database']) && $manifest['options']['include_database']) {
if (!file_exists($backup_dir . '/database.sql')) {
$result['errors'][] = __('Database backup file is missing', 'tigerstyle-heat');
$result['valid'] = false;
}
}
// Check for files backup if included
if (isset($manifest['options']['include_files']) && $manifest['options']['include_files']) {
if (!is_dir($backup_dir . '/files')) {
$result['errors'][] = __('Files backup directory is missing', 'tigerstyle-heat');
$result['valid'] = false;
}
}
return $result;
}
/**
* Validate database backup
*/
private function validate_database_backup($backup_dir, $manifest) {
$result = array(
'valid' => true,
'errors' => array(),
'warnings' => array()
);
// Skip if database backup not included
if (!isset($manifest['options']['include_database']) || !$manifest['options']['include_database']) {
return $result;
}
$sql_file = $backup_dir . '/database.sql';
if (!file_exists($sql_file)) {
$result['errors'][] = __('Database backup file not found', 'tigerstyle-heat');
$result['valid'] = false;
return $result;
}
// Check file size
$file_size = filesize($sql_file);
if ($file_size === 0) {
$result['errors'][] = __('Database backup file is empty', 'tigerstyle-heat');
$result['valid'] = false;
return $result;
}
// Validate SQL content structure
$sql_validation = $this->validate_sql_file($sql_file);
if (!$sql_validation['valid']) {
$result['errors'] = array_merge($result['errors'], $sql_validation['errors']);
$result['warnings'] = array_merge($result['warnings'], $sql_validation['warnings']);
$result['valid'] = false;
}
// Validate checksum if available
if (isset($manifest['database_info']['checksum'])) {
$actual_checksum = md5_file($sql_file);
if ($actual_checksum !== $manifest['database_info']['checksum']) {
$result['errors'][] = __('Database backup checksum mismatch', 'tigerstyle-heat');
$result['valid'] = false;
}
}
// Check file size against manifest
if (isset($manifest['database_info']['file_size'])) {
if ($file_size !== $manifest['database_info']['file_size']) {
$result['warnings'][] = __('Database backup file size differs from manifest', 'tigerstyle-heat');
}
}
return $result;
}
/**
* Validate SQL file structure
*/
private function validate_sql_file($sql_file) {
$result = array(
'valid' => true,
'errors' => array(),
'warnings' => array()
);
$handle = fopen($sql_file, 'r');
if (!$handle) {
$result['errors'][] = __('Cannot read database backup file', 'tigerstyle-heat');
$result['valid'] = false;
return $result;
}
$line_count = 0;
$has_wp_tables = false;
$has_create_statements = false;
$has_insert_statements = false;
try {
while (($line = fgets($handle)) !== false && $line_count < 1000) { // Check first 1000 lines
$line = trim($line);
$line_count++;
// Skip empty lines and comments
if (empty($line) || strpos($line, '--') === 0) {
continue;
}
// Check for WordPress table patterns
if (preg_match('/CREATE TABLE.*wp_\w+/', $line) || preg_match('/INSERT INTO.*wp_\w+/', $line)) {
$has_wp_tables = true;
}
// Check for CREATE TABLE statements
if (strpos($line, 'CREATE TABLE') !== false) {
$has_create_statements = true;
}
// Check for INSERT statements
if (strpos($line, 'INSERT INTO') !== false) {
$has_insert_statements = true;
}
// Check for SQL syntax errors (basic validation)
if (!$this->is_valid_sql_line($line)) {
$result['warnings'][] = sprintf(__('Potential SQL syntax issue at line %d', 'tigerstyle-heat'), $line_count);
}
}
} finally {
fclose($handle);
}
// Validate SQL content
if (!$has_wp_tables) {
$result['warnings'][] = __('No WordPress tables found in database backup', 'tigerstyle-heat');
}
if (!$has_create_statements) {
$result['errors'][] = __('No CREATE TABLE statements found in database backup', 'tigerstyle-heat');
$result['valid'] = false;
}
if (!$has_insert_statements) {
$result['warnings'][] = __('No INSERT statements found in database backup', 'tigerstyle-heat');
}
if ($line_count === 0) {
$result['errors'][] = __('Database backup file appears to be empty', 'tigerstyle-heat');
$result['valid'] = false;
}
return $result;
}
/**
* Validate files backup
*/
private function validate_files_backup($backup_dir, $manifest) {
$result = array(
'valid' => true,
'errors' => array(),
'warnings' => array()
);
// Skip if files backup not included
if (!isset($manifest['options']['include_files']) || !$manifest['options']['include_files']) {
return $result;
}
$files_dir = $backup_dir . '/files';
if (!is_dir($files_dir)) {
$result['errors'][] = __('Files backup directory not found', 'tigerstyle-heat');
$result['valid'] = false;
return $result;
}
// Check for essential WordPress files
$essential_files = array(
'wp-config.php',
'wp-content'
);
foreach ($essential_files as $file) {
$file_path = $files_dir . '/' . $file;
if (!file_exists($file_path)) {
$result['warnings'][] = sprintf(__('Essential file/directory missing: %s', 'tigerstyle-heat'), $file);
}
}
// Validate file manifest if available
if (isset($manifest['files']) && is_array($manifest['files'])) {
$file_validation = $this->validate_file_manifest($files_dir, $manifest['files']);
$result['errors'] = array_merge($result['errors'], $file_validation['errors']);
$result['warnings'] = array_merge($result['warnings'], $file_validation['warnings']);
if (!$file_validation['valid']) {
$result['valid'] = false;
}
}
return $result;
}
/**
* Validate file manifest against actual files
*/
private function validate_file_manifest($files_dir, $file_manifest) {
$result = array(
'valid' => true,
'errors' => array(),
'warnings' => array()
);
$checked_files = 0;
$missing_files = 0;
$checksum_mismatches = 0;
foreach ($file_manifest as $relative_path => $file_info) {
$full_path = $files_dir . '/' . $relative_path;
$checked_files++;
if (!file_exists($full_path)) {
$missing_files++;
if ($missing_files <= 10) { // Limit error messages
$result['errors'][] = sprintf(__('Missing file: %s', 'tigerstyle-heat'), $relative_path);
}
$result['valid'] = false;
continue;
}
// Validate file size
if (isset($file_info['size'])) {
$actual_size = filesize($full_path);
if ($actual_size !== $file_info['size']) {
$result['warnings'][] = sprintf(__('File size mismatch: %s', 'tigerstyle-heat'), $relative_path);
}
}
// Validate checksum (sample validation for performance)
if (isset($file_info['checksum']) && $checked_files % 10 === 0) { // Check every 10th file
$actual_checksum = md5_file($full_path);
if ($actual_checksum !== $file_info['checksum']) {
$checksum_mismatches++;
if ($checksum_mismatches <= 5) { // Limit error messages
$result['warnings'][] = sprintf(__('Checksum mismatch: %s', 'tigerstyle-heat'), $relative_path);
}
}
}
}
if ($missing_files > 10) {
$result['errors'][] = sprintf(__('... and %d more missing files', 'tigerstyle-heat'), $missing_files - 10);
}
if ($checksum_mismatches > 5) {
$result['warnings'][] = sprintf(__('... and %d more checksum mismatches', 'tigerstyle-heat'), $checksum_mismatches - 5);
}
return $result;
}
/**
* Validate backup checksums
*/
private function validate_checksums($backup_dir, $manifest) {
$result = array(
'valid' => true,
'errors' => array(),
'warnings' => array()
);
if (!isset($manifest['checksums']) || !is_array($manifest['checksums'])) {
$result['warnings'][] = __('No checksums found in manifest', 'tigerstyle-heat');
return $result;
}
foreach ($manifest['checksums'] as $file => $expected_checksum) {
$file_path = $backup_dir . '/' . $file;
if (!file_exists($file_path)) {
continue; // File validation handled elsewhere
}
$actual_checksum = md5_file($file_path);
if ($actual_checksum !== $expected_checksum) {
$result['errors'][] = sprintf(__('Checksum mismatch for file: %s', 'tigerstyle-heat'), $file);
$result['valid'] = false;
}
}
return $result;
}
/**
* Extract backup for validation
*/
private function extract_backup_for_validation($backup_file) {
$upload_dir = wp_upload_dir();
$temp_dir = $upload_dir['basedir'] . '/tigerstyle-validation/' . uniqid('validation_');
if (!wp_mkdir_p($temp_dir)) {
throw new Exception(__('Failed to create validation directory', 'tigerstyle-heat'));
}
$compression_manager = new TigerStyleSEO_Compression_Manager();
$compression_method = $this->detect_compression_method($backup_file);
$compression_manager->extract_archive($backup_file, $temp_dir, $compression_method);
return $temp_dir;
}
/**
* 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 'tar.gz';
case 'bz2':
return 'tar.bz2';
default:
return 'none';
}
}
/**
* Cleanup temporary directory
*/
private function cleanup_temp_directory($temp_dir) {
if (!is_dir($temp_dir)) {
return;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($temp_dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $file) {
if ($file->isDir()) {
rmdir($file->getPathname());
} else {
unlink($file->getPathname());
}
}
rmdir($temp_dir);
}
/**
* Validate datetime format
*/
private function is_valid_datetime($datetime) {
$d = DateTime::createFromFormat('Y-m-d H:i:s', $datetime);
return $d && $d->format('Y-m-d H:i:s') === $datetime;
}
/**
* Basic SQL line validation
*/
private function is_valid_sql_line($line) {
// Skip empty lines and comments
if (empty($line) || strpos($line, '--') === 0) {
return true;
}
// Check for common SQL syntax patterns
$sql_keywords = array(
'CREATE', 'DROP', 'INSERT', 'UPDATE', 'DELETE', 'SELECT',
'ALTER', 'SET', 'START', 'COMMIT', 'ROLLBACK'
);
$line_upper = strtoupper($line);
// Check if line starts with SQL keyword or is part of a multi-line statement
foreach ($sql_keywords as $keyword) {
if (strpos($line_upper, $keyword) === 0) {
return true;
}
}
// Check for VALUES clause (part of INSERT statements)
if (strpos($line_upper, 'VALUES') !== false || strpos($line_upper, '(') === 0) {
return true;
}
// Check for properly quoted strings and escaped characters
if (preg_match('/^[^\']*(?:\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'[^\']*)*$/', $line)) {
return true;
}
return false;
}
/**
* Quick backup validation (without full extraction)
*/
public function quick_validate_backup($backup_id) {
try {
$storage_manager = new TigerStyleSEO_Storage_Manager();
// Check if backup exists
if (!$storage_manager->backup_exists($backup_id)) {
return array('valid' => false, 'error' => __('Backup does not exist', 'tigerstyle-heat'));
}
// Get backup info
$backup_info = $storage_manager->get_backup_info($backup_id);
// Basic checks
if (empty($backup_info['file_size']) || $backup_info['file_size'] < 1000) {
return array('valid' => false, 'error' => __('Backup file is too small', 'tigerstyle-heat'));
}
// For local backups, check if file exists and is readable
if ($backup_info['storage_type'] === 'local') {
if (!file_exists($backup_info['file_path']) || !is_readable($backup_info['file_path'])) {
return array('valid' => false, 'error' => __('Backup file is not accessible', 'tigerstyle-heat'));
}
}
return array('valid' => true, 'message' => __('Backup appears to be valid', 'tigerstyle-heat'));
} catch (Exception $e) {
return array('valid' => false, 'error' => $e->getMessage());
}
}
/**
* Validate backup before restore (comprehensive)
*/
public function validate_for_restore($backup_id) {
// Perform quick validation first
$quick_result = $this->quick_validate_backup($backup_id);
if (!$quick_result['valid']) {
return $quick_result;
}
// Perform full validation
$full_result = $this->validate_backup($backup_id);
// Add restore-specific validations
if ($full_result['valid']) {
// Check compatibility with current WordPress version
// Check available disk space
// Check database permissions
// etc.
}
return $full_result;
}
}