zZzzZ.
home2
/
zrzvbdry
/
wiredsave.com
/
wp-content
/
plugins
/
backup
/
src
/
JetBackup
/
Cron
/
Task
New Folder
Editing: Restore.php
Save
Cancel
<?php namespace JetBackup\Cron\Task; use __PHP_Incomplete_Class; use Exception; use JetBackup\BackupJob\BackupJob; use JetBackup\Data\Engine; use JetBackup\Data\Mysqldump; use JetBackup\Encryption\Crypt; use JetBackup\Entities\Util; use JetBackup\Exception\DBException; use JetBackup\Exception\ExecutionTimeException; use JetBackup\Exception\JetBackupLinuxException; use JetBackup\Exception\RestoreException; use JetBackup\Exception\SnapshotMetaException; use JetBackup\Exception\TaskException; use JetBackup\Factory; use JetBackup\Filesystem\File; use JetBackup\JetBackup; use JetBackup\JetBackupLinux\JetBackupLinux; use JetBackup\Queue\Queue; use JetBackup\Queue\QueueItem; use JetBackup\Queue\QueueItemRestore; use JetBackup\Snapshot\Snapshot; use SleekDB\Exceptions\InvalidArgumentException; use SleekDB\Exceptions\IOException; if (!defined( '__JETBACKUP__')) die('Direct access is not allowed'); class Restore extends Task { const LOG_FILENAME = 'restore'; const PLUGINS_PATH = JetBackup::WP_ROOT_PATH . JetBackup::SEP . 'wp-content' . JetBackup::SEP . 'plugins'; const MU_PLUGINS_PATH = JetBackup::WP_ROOT_PATH . JetBackup::SEP . 'wp-content' . JetBackup::SEP . 'mu-plugins'; const PROTECTED_PLUGINS = ['backup']; // Add this protected attribute to store protected plugin names. const CACHE_PLUGINS = ['redis-cache']; private QueueItemRestore $_queue_item_restore; private Mysqldump $_mysql; private array $_disabled_plugins=[]; /** * */ public function __construct() { parent::__construct(self::LOG_FILENAME); } /** * @return void * @throws IOException * @throws InvalidArgumentException * @throws TaskException * @throws ExecutionTimeException */ public function execute():void { parent::execute(); $this->_queue_item_restore = $this->getQueueItem()->getItemData(); if($this->getQueueItem()->getStatus() == Queue::STATUS_RESTORE_WAITING_FOR_RESTORE) { $this->getLogController()->logMessage("Starting restore"); $this->getQueueItem()->getProgress()->setTotalItems( count(Queue::STATUS_RESTORE_NAMES) + 3); $this->getQueueItem()->save(); $this->getQueueItem()->updateProgress('Starting restore'); } else if($this->getQueueItem()->getStatus() > Queue::STATUS_RESTORE_WAITING_FOR_RESTORE) { $this->getLogController()->logMessage('Resumed Restore'); } try { if(!$this->_queue_item_restore->getSnapshotId() && !$this->_queue_item_restore->getSnapshotPath()) throw new RestoreException("No snapshot id or path provided"); $this->getLogController()->logDebug('Item data: ' . print_r($this->_queue_item_restore, 1)); $snapshot = $this->getSnapshot(); if($snapshot->getEngine() == Engine::ENGINE_JB) { $this->func([$this, '_addToQueue']); $this->func([$this, '_updateQueue']); } else { $mysql_auth = $this->_fetchMySQLAuth(); $this->_mysql = new Mysqldump($mysql_auth->db_name, $mysql_auth->db_user, $mysql_auth->db_password, $mysql_auth->db_host); $this->_mysql->setLogController($this->getLogController()); $this->func([$this, '_preFetchAdminUser']); $this->func([$this, '_database']); $this->func([$this, '_files']); $this->func([$this, '_postRestoreDBPrefix']); $this->func([$this, '_rewriteViewDefinersAndPrefixes']); $this->func([$this, '_validateViewsCompile']); $this->func([$this, '_postRestoreDomainMigration']); $this->func([$this, '_postInsertAdminUser']); $this->func([$this, '_postRestoreHealthCheck']); $this->func([$this, '_postRestoreActions']); // must be called after WordPress is loaded } if($this->getQueueItem()->getStatus() < Queue::STATUS_DONE) $this->getQueueItem()->updateStatus(Queue::STATUS_DONE); $this->getLogController()->logMessage('Completed!'); } catch(ExecutionTimeException $e) { throw $e; } catch( Exception $e) { $this->getQueueItem()->updateStatus(Queue::STATUS_FAILED); $this->getLogController()->logError($e->getMessage()); $this->getLogController()->logMessage('Failed!'); } $this->getQueueItem()->updateProgress($this->getQueueItem()->getStatus() == Queue::STATUS_DONE ? 'Restore Completed' : 'Error during restore', QueueItem::PROGRESS_LAST_STEP); $this->getLogController()->logMessage('Total time: ' . $this->getExecutionTimeElapsed()); } public function _validateViewsCompile(): void { $views = $this->_mysql->query_exec( "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_SCHEMA = DATABASE()" ) ?? []; $workspace = $this->getQueueItem()->getWorkspace(); $database_path = $workspace . JetBackup::SEP . Snapshot::SKELETON_DATABASE_DIRNAME; $visited = []; foreach ($views as $v) { $name = $v->TABLE_NAME ?? current((array)$v); try { // Force dependency resolution $this->_mysql->query_exec("SELECT * FROM `{$name}` LIMIT 0"); } catch (\Throwable $e) { $msg = $e->getMessage(); $this->getLogController()->logError("View `{$name}` fails to resolve: " . $msg); // 1) Try to (re)import this view from snapshot dumps (pulls its deps too) try { $dumpPath = self::_dumpPathFor($database_path, $name); if ($dumpPath) { $this->getLogController()->logMessage("Attempting to (re)import missing/broken view `{$name}` from dump..."); $this->_importWithDeps($database_path, $name, $visited); // Re-test after import $this->_mysql->query_exec("SELECT * FROM `{$name}` LIMIT 0"); $this->getLogController()->logMessage("View `{$name}` now resolves after re-import."); continue; // good now } else { $this->getLogController()->logMessage("No dump found for view `{$name}`."); } } catch (\Throwable $e2) { $this->getLogController()->logError("Re-import attempt for `{$name}` failed: " . $e2->getMessage()); } // 2) Last resort: drop the broken view so it won’t crash the site at runtime try { $this->getLogController()->logMessage("Dropping unresolved view `{$name}` to avoid runtime failures."); $this->_mysql->query_exec("DROP VIEW IF EXISTS `{$name}`"); } catch (\Throwable $e3) { $this->getLogController()->logError("Failed dropping unresolved view `{$name}`: " . $e3->getMessage()); } } } } public function _updateQueue() { if(!$this->_queue_item_restore->getQueueGroupId()) throw new RestoreException("Can't find queue group"); $this->getLogController()->logMessage('Monitoring JetBackup Linux queue item status'); while(true) { try { $response = JetBackupLinux::getQueueGroup($this->_queue_item_restore->getQueueGroupId()); } catch(JetBackupLinuxException $e) { throw new RestoreException("Failed fetching JetBackup Linux queue item status. Error: " . $e->getMessage()); } $remote_progress = $response['progress']; $progress = $this->getQueueItem()->getProgress(); $current_status = JetBackupLinux::QUEUE_STATUS_RESTORE_MAPPING[$response['status']] ?? 0; $old_status = $this->getQueueItem()->getStatus(); if($current_status) { $name = JetBackupLinux::QUEUE_STATUS_RESTORE_ACCOUNT_NAMES[$response['status']]; if($current_status != $old_status) { $this->getQueueItem()->updateStatus($current_status); $this->getQueueItem()->updateProgress($name); } if($response['status'] == JetBackupLinux::QUEUE_STATUS_RESTORE_ACCOUNT_HOMEDIR) { $name .= " ({$remote_progress['completed_files']}/{$remote_progress['total_files']})"; } $this->getLogController()->logMessage($name . '...'); } if($response['status'] == JetBackupLinux::QUEUE_STATUS_RESTORE_ACCOUNT_HOMEDIR && isset($remote_progress['total_files']) && intval($remote_progress['total_files']) > 1) { $progress->setTotalSubItems((int) $remote_progress['total_files']); $progress->setCurrentSubItem((int) $remote_progress['completed_files']); } else { if(isset($remote_progress['subtotal'])) $progress->setTotalSubItems($remote_progress['subtotal']); if(isset($remote_progress['subcompleted'])) $progress->setCurrentSubItem($remote_progress['subcompleted']); } if($response['status'] >= JetBackupLinux::QUEUE_STATUS_COMPLETED) { $this->getQueueItem()->setStatus(JetBackupLinux::QUEUE_STATUS_MAPPING[$response['status']]); $this->getQueueItem()->save(); return; } else { $progress->setMessage(JetBackupLinux::QUEUE_STATUS_RESTORE_ACCOUNT_NAMES[$response['status']] ?? 'Processing'); $this->getQueueItem()->save(); } sleep(5); } } /** * Recreate all views so they: * - reference the **local** table prefix (not the backup’s) * - have NO explicit DEFINER * - use SQL SECURITY INVOKER * - are created with CREATE OR REPLACE (idempotent) * - are applied in dependency order (retry passes) */ public function _rewriteViewDefinersAndPrefixes(): void { $this->getLogController()->logMessage("Rewriting view definers and prefixes..."); $mysql_auth = $this->_fetchMySQLAuth(); $local_prefix = $mysql_auth->table_prefix; $backup_prefix = $this->_getDatabasePrefix(); if (!$backup_prefix || $backup_prefix === $local_prefix) { $this->getLogController()->logMessage("No prefix change detected; skipping view rewrite."); return; } // Fetch all view names in current DB $rows = $this->_mysql->query_exec( "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_SCHEMA = DATABASE()" ) ?? []; $views = []; foreach ($rows as $r) { $views[] = $r->TABLE_NAME ?? current((array)$r); } // Helper: does an object (table or view) exist now? $exists = function(string $name): bool { try { return $this->_mysql->tableExists($name, true); } catch (\Exception $e) { return false; } }; /** * Build a queue: for each source view, prepare: * - the final (normalized, prefix-fixed) DDL * - the new target name (with local prefix) * - the conservative list of referenced objects (to order creation) */ $queue = []; // [oldName => ['new' => newName, 'ddl' => ddl, 'deps' => []]] foreach ($views as $oldName) { if (!$oldName) continue; $row = $this->_mysql->query_exec("SHOW CREATE VIEW `{$oldName}`"); $ddl = (!empty($row) && isset($row[0])) ? ($row[0]->{'Create View'} ?? null) : null; if (!$ddl) { $this->getLogController()->logMessage("SHOW CREATE VIEW returned empty for `{$oldName}`, skipping."); continue; } // 1) Normalize header (remove DEFINER/ALGORITHM, force OR REPLACE + INVOKER) $ddl = $this->_normalizeCreateViewSQL($ddl); // 2) Replace backup prefix -> local prefix in identifiers (header + body) $ddl = preg_replace_callback( '/`([A-Za-z0-9_]+)`/u', function ($m) use ($backup_prefix, $local_prefix) { $tok = $m[1]; return (strpos($tok, $backup_prefix) === 0) ? '`' . $local_prefix . substr($tok, strlen($backup_prefix)) . '`' : $m[0]; }, $ddl ); $ddl = preg_replace( '/\b' . preg_quote($backup_prefix, '/') . '([A-Za-z0-9_]+)\b/u', $local_prefix . '$1', $ddl ); // 3) Decide the new view name (swap prefix if the name itself starts with backup_prefix) $newName = (strpos($oldName, $backup_prefix) === 0) ? $local_prefix . substr($oldName, strlen($backup_prefix)) : $oldName; // 4) Ensure the CREATE header uses the new object name $ddl = preg_replace( '/\bCREATE\s+(?:OR\s+REPLACE\s+)?(?:SQL\s+SECURITY\s+INVOKER\s+)?VIEW\s+`?([A-Za-z0-9_]+)`?/i', 'CREATE OR REPLACE SQL SECURITY INVOKER VIEW `' . $newName . '`', $ddl, 1 ); // 5) Collect referenced objects conservatively from the DDL body $deps = []; if (preg_match('/\bAS\b(.*)\z/is', $ddl, $mBody)) { $body = $mBody[1]; // FROM/JOIN `db`.`obj` or `obj` or db.obj or obj if (preg_match_all('/\b(?:FROM|JOIN)\s+`?([A-Za-z0-9_]+)`?(?:\.`?([A-Za-z0-9_]+)`?)?/i', $body, $mm, PREG_SET_ORDER)) { foreach ($mm as $g) { $obj = isset($g[2]) && $g[2] ? $g[2] : $g[1]; if (strpos($obj, $backup_prefix) === 0) { $obj = $local_prefix . substr($obj, strlen($backup_prefix)); } $deps[] = $obj; } } } $deps = array_values(array_unique($deps)); $queue[$oldName] = ['new' => $newName, 'ddl' => $ddl, 'deps' => $deps]; } // Retry passes: create views whose deps already exist; loop until no progress $maxPasses = 5; for ($pass = 1; $pass <= $maxPasses && !empty($queue); $pass++) { $progress = 0; foreach (array_keys($queue) as $oldName) { $newName = $queue[$oldName]['new']; $ddl = $queue[$oldName]['ddl']; $deps = $queue[$oldName]['deps']; // if any dependency doesn't exist yet, skip for this pass $blocked = false; foreach ($deps as $d) { if (!$exists($d)) { $blocked = true; break; } } if ($blocked) { $this->getLogController()->logMessage("Waiting to recreate `{$oldName}`; missing deps: " . implode(', ', array_filter($deps, fn($d) => !$exists($d)))); continue; } try { // be idempotent on target name $this->_mysql->query_exec("DROP VIEW IF EXISTS `{$newName}`"); $this->_mysql->query_exec($ddl); // if we changed the name, drop the old name to prevent stale alias if ($newName !== $oldName) { $this->_mysql->query_exec("DROP VIEW IF EXISTS `{$oldName}`"); } $this->getLogController()->logMessage("Recreated view `{$newName}` (from `{$oldName}`) with updated prefix and INVOKER."); unset($queue[$oldName]); $progress++; } catch (\Throwable $e) { $this->getLogController()->logError("Failed to recreate view `{$oldName}` → `{$newName}`: " . $e->getMessage()); // leave it for next pass } } if ($progress === 0) break; // nothing more we can resolve by ordering } // Any unresolved views will be surfaced by _validateViewsCompile() } /** * Header-level normalization for CREATE VIEW: * - remove DEFINER and ALGORITHM * - force CREATE OR REPLACE * - force SQL SECURITY INVOKER */ public function _normalizeCreateViewSQL(string $sql): string { if (!preg_match('/\bCREATE\b.*\bVIEW\b/is', $sql)) return $sql; // Split once at first AS (header vs body) $parts = preg_split('/\bAS\b/i', $sql, 2); $header = $parts[0] ?? $sql; $body = $parts[1] ?? ''; // Remove versioned DEFINER blocks like /*!50013 DEFINER=`user`@`host` SQL SECURITY DEFINER */ $header = preg_replace( '/\/\*!\d+\s+DEFINER\s*=\s*[^*]+SQL\s+SECURITY\s+(?:DEFINER|INVOKER)\s*\*\//i', ' ', $header ); // Remove plain DEFINER=... $header = preg_replace( '/\bDEFINER\s*=\s*(?:`[^`]+`@`[^`]+`|\'[^\']+\'@\'[^\']+\'|[^ \t\n\r\f\)]+)\s*/i', ' ', $header ); // Remove ALGORITHM=... $header = preg_replace('/\bALGORITHM\s*=\s*\w+\s*/i', ' ', $header); // Force CREATE OR REPLACE $header = preg_replace('/\bCREATE\s+(?!OR\s+REPLACE\b)/i', 'CREATE OR REPLACE ', $header, 1); // Ensure SQL SECURITY INVOKER if (preg_match('/\bSQL\s+SECURITY\s+(?:DEFINER|INVOKER)\b/i', $header)) { $header = preg_replace('/\bSQL\s+SECURITY\s+(?:DEFINER|INVOKER)\b/i', 'SQL SECURITY INVOKER', $header, 1); } else { $header = preg_replace( '/\b(CREATE\s+(?:OR\s+REPLACE\s+)?)(VIEW\b)/i', '$1SQL SECURITY INVOKER $2', $header, 1 ); } // Tidy whitespace $header = preg_replace('/[ \t]+/', ' ', trim($header)); if ($body === '') return $header; return $header . ' AS' . (preg_match('/^\s/', $body) ? '' : ' ') . $body; } /** * @throws DBException * @throws RestoreException * @throws IOException * @throws InvalidArgumentException */ public function _addToQueue() { if($this->_queue_item_restore->getQueueGroupId()) return; $this->getLogController()->logMessage('Adding restore to JetBackup Linux queue'); $snapshot = $this->getSnapshot(); $files = $items = []; $homedir_item_id = $database_item_id = null; foreach ($snapshot->getItems() as $item) { switch($item->getBackupContains()) { case BackupJob::BACKUP_ACCOUNT_CONTAINS_HOMEDIR: $homedir_item_id = $item->getUniqueId(); break; case BackupJob::BACKUP_ACCOUNT_CONTAINS_DATABASE: $database_item_id = $item->getUniqueId(); break; } } $options = $this->_queue_item_restore->getOptions(); if ($homedir_item_id && ($options & QueueItemRestore::OPTION_RESTORE_FILES_ENTIRE)) { $files[$homedir_item_id][Factory::getWPHelper()->getWordPressRelativePublicDir()] = 'Directory'; $items[] = $homedir_item_id; } if ($homedir_item_id && ($options & QueueItemRestore::OPTION_RESTORE_FILES_INCLUDE)) { $files = []; $public_dir = Factory::getWPHelper()->getWordPressRelativePublicDir(); foreach ($this->_queue_item_restore->getFileManager() as $file) { $path = $public_dir . JetBackup::SEP . trim($file['path'], JetBackup::SEP); $this->getLogController()->logMessage("Path: $path"); $files[$homedir_item_id][$path] = $file['type']; } $items[] = $homedir_item_id; } if ($database_item_id && ($options & QueueItemRestore::OPTION_RESTORE_DATABASE_ENTIRE)) { $items[] = $database_item_id; } try { $response = JetBackupLinux::addQueueItems($items, $files, ['merge' => 1]); } catch (Exception $e) { throw new RestoreException("Failed adding to JetBackup Linux queue. Error: " . $e->getMessage()); } $this->_queue_item_restore->setQueueGroupId($response['_id']); $this->getQueueItem()->save(); } /** * @return Snapshot */ private function getSnapshot():Snapshot { return $this->func(function() { if($this->_queue_item_restore->getSnapshotId()) { $snapshot = new Snapshot($this->_queue_item_restore->getSnapshotId()); if($snapshot->getId() && $snapshot->getEngine() == Engine::ENGINE_JB) return $snapshot; } $workspace = $this->getQueueItem()->getWorkspace(); $meta_file = sprintf(Snapshot::META_FILEPATH, $workspace); if(!file_exists($meta_file)) throw new RestoreException("Backup meta file not exists ($meta_file)"); $snapshot = new Snapshot(); try { $snapshot->importMeta($meta_file, true); } catch(SnapshotMetaException $e) { throw new RestoreException("Failed read snapshot meta file. Error: " . $e->getMessage()); } return $snapshot; }, [], '_loadSnapshot'); } /** * @return object|null */ private function _fetchMySQLAuth():object { return $this->func(function() { try { // Pass queue unique ID as decryption key for runtime credentials return Factory::getWPHelper()::parseWpConfig($this->getQueueItem()->getUniqueId()); } catch (Exception $e) { throw new RestoreException($e->getMessage()); } }, [], '_MySQLAuth'); } /** * @param string $dumpPath * * @return bool */ private static function _isViewDump(string $dumpPath): bool { $fh = @fopen($dumpPath, 'rb'); if (!$fh) return false; $chunk = fread($fh, 262144); // read a small chunk fclose($fh); if ($chunk === false || $chunk === '') return false; // normalize & look for CREATE VIEW $sql = preg_replace('/\/\*![0-9]+\s*/', ' ', $chunk); $sql = str_replace('*/', ' ', $sql); $sql = preg_replace('/\s+/', ' ', $sql); return (bool) preg_match( '/\bCREATE\s+(?:OR\s+REPLACE\s+)?(?:ALGORITHM=\w+\s+)?(?:DEFINER=\S+\s+)?(?:SQL\s+SECURITY\s+(?:DEFINER|INVOKER)\s+)?VIEW\b/i', $sql ); } /** * @return void * @throws Exception */ public function _database(): void { if (!$this->_queue_item_restore->isRestoreDatabase()) return; $this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_DATABASE); $this->getQueueItem()->updateProgress('Restoring database'); $this->getLogController()->logMessage('Importing all database tables'); $workspace = $this->getQueueItem()->getWorkspace(); $database_path = $workspace . JetBackup::SEP . Snapshot::SKELETON_DATABASE_DIRNAME; $visited = []; // per-run dedupe of imported dumps // Relax constraints for faster/safer bulk import $this->_mysql->query_exec('SET FOREIGN_KEY_CHECKS=0'); $this->_mysql->query_exec('SET UNIQUE_CHECKS=0'); $this->_mysql->query_exec('SET SQL_NOTES=0'); try { // Phase 1: Import all non-view dumps $this->foreachCallable([$this, '_listDatabaseTables'], [], function ($i, $table_dump) use ($database_path, &$visited) { // Normalize dump base name -> table $base = $table_dump; if (str_ends_with($base, '.gz')) $base = substr($base, 0, -3); if (str_ends_with($base, '.sql')) $base = substr($base, 0, -4); $table = $base; $dumpPath = self::_dumpPathFor($database_path, $table); if (!$dumpPath) return; // Skip views in phase 1 if (self::_isViewDump($dumpPath)) return; $this->_importWithDeps($database_path, $table, $visited); // Progress: count only real imports $p = $this->getQueueItem()->getProgress(); $p->increaseCurrentSubItem(); $this->getQueueItem()->save(); }, 'database_tables_phase1'); // Phase 2: Import views $this->foreachCallable([$this, '_listDatabaseTables'], [], function ($i, $table_dump) use ($database_path, &$visited) { // Normalize dump base name -> table $base = $table_dump; if (str_ends_with($base, '.gz')) $base = substr($base, 0, -3); if (str_ends_with($base, '.sql')) $base = substr($base, 0, -4); $table = $base; $dumpPath = self::_dumpPathFor($database_path, $table); if (!$dumpPath) return; // Only views in phase 2 if (!self::_isViewDump($dumpPath)) return; $this->_importWithDeps($database_path, $table, $visited); // Progress: count only real imports $p = $this->getQueueItem()->getProgress(); $p->increaseCurrentSubItem(); $this->getQueueItem()->save(); }, 'database_tables_phase2'); } finally { // Always restore original settings $this->_mysql->query_exec('SET SQL_NOTES=1'); $this->_mysql->query_exec('SET UNIQUE_CHECKS=1'); $this->_mysql->query_exec('SET FOREIGN_KEY_CHECKS=1'); } } /** Return ALL referenced objects for a view dump; empty for base-table dumps */ private static function _viewReferencedObjectsFromDump(string $path): array { $fh = @fopen($path, 'rb'); if (!$fh) return []; $chunk = fread($fh, 262144); fclose($fh); if ($chunk === false || $chunk === '') return []; // Strip versioned comments and normalize spaces $sql = preg_replace('/\/\*![0-9]+\s*/', ' ', $chunk); $sql = str_replace('*/', ' ', $sql); $sql = preg_replace('/\s+/', ' ', $sql); // Only for CREATE VIEW... if (!preg_match('/\bCREATE\s+(?:OR\s+REPLACE\s+)?(?:ALGORITHM=\w+\s+)?(?:DEFINER=\S+\s+)?(?:SQL\s+SECURITY\s+(?:DEFINER|INVOKER)\s+)?VIEW\b/i', $sql)) { return []; } $bases = []; // Handle: FROM `db`.`table` or JOIN `db`.`table` if (preg_match_all('/\b(?:FROM|JOIN)\s+`([^`]+)`\.`([^`]+)`/i', $sql, $m)) { foreach ($m[2] as $t) $bases[] = $t; } // Handle: FROM `table` or JOIN `table` if (preg_match_all('/\b(?:FROM|JOIN)\s+`([^`]+)`(?!\.)/i', $sql, $m)) { foreach ($m[1] as $t) $bases[] = $t; } // Handle: FROM db.table or JOIN db.table (unquoted) if (preg_match_all('/\b(?:FROM|JOIN)\s+([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)/i', $sql, $m)) { foreach ($m[2] as $t) $bases[] = $t; } // Handle: FROM table or JOIN table (unquoted) if (preg_match_all('/\b(?:FROM|JOIN)\s+([A-Za-z0-9_]+)\b(?!\.)/i', $sql, $m)) { foreach ($m[1] as $t) $bases[] = $t; } // De-dupe return array_values(array_unique($bases)); } private static function _dumpPathFor(string $database_path, string $table): ?string { $p = $database_path . JetBackup::SEP . $table . '.sql'; return is_file($p) ? $p : (is_file($p . '.gz') ? $p . '.gz' : null); } /** Normalize id for visited map: foo.sql or foo.sql.gz → foo.sql */ private static function _dumpId(string $dumpPath): string { return basename(preg_replace('/\.gz$/', '', $dumpPath)); } /** Recursively import a dump and all its referenced deps (from snapshot), once */ private function _importWithDeps(string $database_path, string $table, array &$visited): void { $dump = self::_dumpPathFor($database_path, $table); // If there is no dump for this dep but it already exists in DB, proceed. if (!$dump) { if ($this->_mysql->tableExists($table, true)) return; throw new RestoreException("Missing dump for dependency `{$table}` required by another view."); } $id = self::_dumpId($dump); if (!empty($visited[$id])) return; // Recurse for nested deps first (only for view dumps) $deps = self::_viewReferencedObjectsFromDump($dump); foreach ($deps as $dep) { $this->_importWithDeps($database_path, $dep, $visited); } $this->getLogController()->logMessage(" - Importing: {$table} (" . basename($dump) . ")"); $this->_mysql->import($dump); $visited[$id] = true; } /** * @throws Exception */ public function _preFetchAdminUser():void { $tables_to_restore = $this->_listDatabaseTables(); $matchingUser = false; foreach ($tables_to_restore as $table) { if (str_starts_with($table, $this->_getDatabasePrefix() . "users") || str_starts_with($table, $this->_getDatabasePrefix() . "usermeta")) { $matchingUser = true; break; } } if(!$matchingUser) return; $this->getLogController()->logMessage(' Fetching admin users'); $admin_user = $this->_getAdminUser(); $admin_user = $admin_user ? serialize($admin_user) : null; if(!$admin_user) return; $mysql_auth = $this->_fetchMySQLAuth(); $this->_queue_item_restore->setAdminUser(Crypt::encrypt($admin_user, $mysql_auth->db_password)); $this->getQueueItem()->save(); } /** * @throws RestoreException */ public function _postInsertAdminUser():void { if(!($admin_user = $this->_queue_item_restore->getAdminUser())) return; $this->getLogController()->logMessage(' Inserting admin user'); $mysql_auth = $this->_fetchMySQLAuth(); $admin_user = Crypt::decrypt($admin_user, $mysql_auth->db_password); $this->_insertAdminUser( (array) unserialize( $admin_user )); } /** * @param array $user_data * * @return void * @throws RestoreException */ private function _insertAdminUser(array $user_data): void { $mysql_auth = $this->_fetchMySQLAuth(); $user_table = $mysql_auth->table_prefix . 'users'; $query = "SHOW COLUMNS FROM `$user_table`"; $columns = $this->_mysql->query_exec($query); if (!$columns || count($columns) === 0) throw new RestoreException("The table $user_table does not exist or has no columns."); $existing_columns = array_map(fn($column) => $column->Field, $columns); $filtered_user_data = array_intersect_key($user_data, array_flip($existing_columns)); if (empty($filtered_user_data)) throw new RestoreException("No valid user data found to insert."); // Prepare SQL query parts $columns_str = implode(", ", array_keys($filtered_user_data)); $placeholders = implode(", ", array_map(fn($key) => ":$key", array_keys($filtered_user_data))); $update_str = implode(", ", array_map(fn($key) => "`$key` = VALUES(`$key`)", array_keys($filtered_user_data))); $query = "INSERT INTO `$user_table` ($columns_str) VALUES ($placeholders) ON DUPLICATE KEY UPDATE $update_str"; $params = array_combine(array_map(fn($key) => ":$key", array_keys($filtered_user_data)), array_values($filtered_user_data)); try { $this->_mysql->query_exec($query, $params); $this->getLogController()->logMessage("Admin user successfully inserted or updated in $user_table"); // Set admin capabilities if (isset($user_data['ID'])) { $user_id = $user_data['ID']; $usermeta_table = $mysql_auth->table_prefix . 'usermeta'; // Insert or update wp_capabilities $capabilities_query = "INSERT INTO `$usermeta_table` (`user_id`, `meta_key`, `meta_value`) VALUES (:user_id, :meta_key_cap, :meta_value_cap) ON DUPLICATE KEY UPDATE `meta_value` = :meta_value_cap"; $params_capabilities = [ ':user_id' => $user_id, ':meta_key_cap' => $mysql_auth->table_prefix . 'capabilities', ':meta_value_cap' => serialize(['administrator' => true]), ]; $this->_mysql->query_exec($capabilities_query, $params_capabilities); $this->getLogController()->logMessage("Admin capabilities set for user ID $user_id"); // Insert or update wp_user_level $user_level_query = "INSERT INTO `$usermeta_table` (`user_id`, `meta_key`, `meta_value`) VALUES (:user_id, :meta_key_lvl, :meta_value_lvl) ON DUPLICATE KEY UPDATE `meta_value` = :meta_value_lvl"; $params_user_level = [ ':user_id' => $user_id, ':meta_key_lvl' => $mysql_auth->table_prefix . 'user_level', ':meta_value_lvl' => '10', // Administrator level ]; $this->_mysql->query_exec($user_level_query, $params_user_level); $this->getLogController()->logMessage("Admin user level set for user ID $user_id"); } } catch (Exception $e) { throw new RestoreException("Failed to insert or update admin user: " . $e->getMessage()); } } /** * @return array|null * @throws Exception */ private function _getAdminUser(): ?array { return $this->func(function(){ $this->getLogController()->logMessage(' - Handling admin user'); $mysql_auth = $this->_fetchMySQLAuth(); $table_prefix = $mysql_auth->table_prefix ?? 'wp_'; // Fetch all users with session tokens $query = "SELECT u.ID, u.user_login, um.meta_value AS session_tokens FROM `{$table_prefix}usermeta` um JOIN `{$table_prefix}users` u ON um.user_id = u.ID WHERE um.meta_key = 'session_tokens'"; $users = $this->_mysql->query_exec($query); $latest_user = null; $latest_expiration = 0; // Loop through each user and find the latest session foreach ($users as $user) { $session_tokens = unserialize($user->session_tokens); if (!is_array($session_tokens)) continue; foreach ($session_tokens as $session) { if (isset($session['expiration']) && $session['expiration'] > $latest_expiration) { $latest_expiration = $session['expiration']; $latest_user = $user->user_login; } } } if (!$latest_user) return null; // Fetch full user details (Same as the original function) $query = "SELECT * FROM `{$table_prefix}users` WHERE `user_login` = :username"; $params = [':username' => $latest_user]; $user = $this->_mysql->query_exec($query, $params); if (!isset($user[0])) return null; return (array) $user[0]; // Return the full user details }, [], 'getAdminUser'); } /** * @return array * @throws DBException * @throws IOException * @throws InvalidArgumentException */ public function _listDatabaseTables():array { $output = []; $items = $this->getSnapshot()->getItems(); foreach ($items as $item) { // Skip if the item is not a database backup if ($item->getBackupContains() !== BackupJob::BACKUP_ACCOUNT_CONTAINS_DATABASE) continue; $dbName = $item->getName(); $includedDatabases = $this->_queue_item_restore->getIncludedDatabases(); $excludedDatabases = $this->_queue_item_restore->getExcludedDatabases(); // Restore only included tables if ($includedDatabases && !in_array($dbName, $includedDatabases)) continue; // Exclude specific tables if ($excludedDatabases && in_array($dbName, $excludedDatabases)) continue; // Add the database backup to the output $output[] = basename($item->getPath()); } $progress = $this->getQueueItem()->getProgress(); $progress->setTotalSubItems(sizeof($output)); $progress->setCurrentSubItem(0); $this->getQueueItem()->save(); $this->getLogController()->logMessage('Total of ' . sizeof($output) . ' database tables found for restore'); return $output; } /** * @return void * @throws RestoreException */ public function _files():void { if(!$this->_queue_item_restore->isRestoreHomedir()) return; $this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_FILES); $this->getQueueItem()->updateProgress('Restoring homedir files'); $this->getLogController()->logMessage("Restoring homedir files"); $workspace = $this->getQueueItem()->getWorkspace(); $source = new File($workspace . JetBackup::SEP . Snapshot::SKELETON_FILES_DIRNAME); if(!$source->exists() || !$source->isDir()) throw new RestoreException("Can't find source directory ({$source->path()})"); $target = new File(Factory::getWPHelper()->getWordPressHomedir()); if(!$target->exists() || !$target->isDir()) throw new RestoreException("Can't find target directory ({$target->path()})"); $queue = [$source->dir()]; while($queue) { $dir = array_shift($queue); $dir_path = $dir->path; //$contains = 0; while(($entry = $dir->read()) !== false) { if($entry == '.' || $entry == '..') continue; $source_file = new File($dir_path . JetBackup::SEP . $entry); $target_file = new File($target->path() . JetBackup::SEP . trim(substr($source_file->path(), strlen($source->path())), JetBackup::SEP)); $this->getLogController()->logDebug("Restoring file {$source_file->path()} to {$target_file->path()}"); if ($source_file->isDir()) { switch (true) { case (!$target_file->exists()): // Case 1: Target does not exist → Create the directory $this->getLogController()->logDebug("[Directory Mode] Target {$target_file->path()} does not exist → Create the directory"); mkdir($target_file->path(), $source_file->mode()); break; case ($target_file->isLink() && $target_file->isDir($target_file->path())): // Case 2: Target is a symlink to a directory → Keep it, do nothing $this->getLogController()->logDebug("[Directory Mode] Target {$target_file->path()} is a symlink to a directory → Keep it, do nothing"); break; case (!$target_file->isDir()): // Case 3: Target exists but is NOT a directory → Remove it $this->getLogController()->logDebug("[Directory Mode] Target {$target_file->path()} exists but is NOT a directory → Remove it"); unlink($target_file->path()); mkdir($target_file->path(), $source_file->mode()); break; default: // Case 4: Target is an existing directory → Just update permissions $this->getLogController()->logDebug("[Directory Mode] Target {$target_file->path()} is an existing directory → Just update permissions"); chmod($target_file->path(), $source_file->mode()); break; } // Add directory to queue for further processing $queue[] = $source_file->dir(); } else { switch (true) { case (!$target_file->exists()): // Case 1: Target does not exist → Move file $this->getLogController()->logDebug("[File Mode] Target {$target_file->path()} does not exist → Move file"); rename($source_file->path(), $target_file->path()); break; case ( $target_file->mtime() != $source_file->mtime() || $target_file->size() != $source_file->size() ): // Case 2: Target exists but is outdated → Remove and replace $this->getLogController()->logDebug("[File Mode] Target {$target_file->path()} exists but is outdated → Remove and replace"); unlink($target_file->path()); rename($source_file->path(), $target_file->path()); break; default: // Case 3: Target is identical → No need to replace, just remove source $this->getLogController()->logDebug("[File Mode] Target {$target_file->path()} is identical → No need to replace, just remove source"); unlink($source_file->path()); break; } } } $dir->close(); // TODO we need to find better way to delete empty folders // delete the folder if it's empty //if(!$contains) rmdir($dir_path); } } /** * @return null */ private function _getSiteURL() { return $this->func(function() { $snapshot = $this->getSnapshot(); foreach($snapshot->getItems() as $item) { if ($item->getBackupContains() != BackupJob::BACKUP_ACCOUNT_CONTAINS_FULL) continue; $prefix = $item->getParams()['site_url'] ?? ''; if($prefix) return $prefix; } return ''; }, [], '_fetchSiteURL'); } /** * @return string */ private function _getDatabasePrefix():string { return $this->func(function() { $snapshot = $this->getSnapshot(); foreach($snapshot->getItems() as $item) { if ($item->getBackupContains() != BackupJob::BACKUP_ACCOUNT_CONTAINS_DATABASE) continue; $prefix = $item->getParams()['db_prefix'] ?? ''; if($prefix) return $prefix; } return ''; }, [], '_fetchDatabasePrefix'); } /** * @return void * @throws RestoreException * @throws Exception */ public function _postRestoreDBPrefix():void { if(!$this->_queue_item_restore->isRestoreDatabase()) return; $this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_POST_RESTORE_DB_PREFIX); $this->getQueueItem()->updateProgress('Post restore - DB Prefix'); if(!($backup_prefix = $this->_getDatabasePrefix())) throw new RestoreException("Can't find backup database prefix"); $mysql_auth = $this->_fetchMySQLAuth(); $local_prefix = $mysql_auth->table_prefix; if($local_prefix == $backup_prefix) return; $this->getLogController()->logMessage("Local DB Prefix ($local_prefix) is different from ($backup_prefix)"); $backup_tables = $this->_fetchBackupTables(); if(!sizeof($backup_tables)) return; $this->getLogController()->logMessage("Searching for orphaned tables"); $this->foreach($backup_tables, function($i, $table) use ($local_prefix, $backup_prefix) { $table_name = $local_prefix . $table; $table_name_old = $backup_prefix . $table; $this->getLogController()->logMessage("- Dropping database table `$table_name` foreign keys"); $this->_dropForeignKeys($table_name); $this->getLogController()->logMessage("- Dropping database table `$table_name` if exists"); $this->_mysql->query_exec("DROP TABLE IF EXISTS `$table_name`"); $this->getLogController()->logMessage("- Renaming database table `$table_name_old` to `$table_name`"); $this->_mysql->query_exec("RENAME TABLE `$table_name_old` TO `$table_name`"); }, '_dropTables'); $usermeta_table = $local_prefix . 'usermeta'; $options_table = $local_prefix . 'options'; // Search and replace prefix in the dynamically generated usermeta table $this->_replacePrefixInTable($usermeta_table, 'meta_key', 'umeta_id'); $this->_replacePrefixInTable($usermeta_table, 'meta_value', 'umeta_id'); // Search and replace prefix in the dynamically generated options table $this->_replacePrefixInTable($options_table, 'option_name', 'option_id'); $this->_replacePrefixInTable($options_table, 'option_value', 'option_id'); } private static function _getParseDomain($url): string { $compa = parse_url($url); $domain = $compa['host'] ?? ''; $port = $compa['port'] ?? ''; $path = $compa['path'] ?? ''; $domain = preg_replace("/(www|\dww|w\dw|ww\d)\./", "", $domain); return $domain . ($port ? ":$port" : '') . $path; } private static function _buildSiteURL():string { $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http"; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; // Default to localhost if missing $script_path = $_SERVER['SCRIPT_NAME'] ?? $_SERVER['PHP_SELF'] ?? $_SERVER['REQUEST_URI']; $script_dir = dirname($script_path); return rtrim($protocol . "://" . $host . $script_dir, '/'); } public function _postRestoreDomainMigration(): void { $this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_POST_RESTORE_DOMAIN_MIGRATION); $this->getQueueItem()->updateProgress('Post restore - Domain Migration'); $backup_domain = self::_getParseDomain($this->_getSiteURL()); $local_domain = self::_getParseDomain(self::_buildSiteURL()); $alternate_path = strlen(Factory::getSettingsRestore()->isRestoreAlternatePathEnabled() ? JetBackup::CRON_PUBLIC_URL : ''); if($alternate_path) $local_domain = substr($local_domain, 0, -$alternate_path); if(!$local_domain) { $this->getLogController()->logMessage("[Error] Couldn't retrieve local domain, exiting"); return; } if ($backup_domain === $local_domain) { $this->getLogController()->logMessage("Domain from backup ($backup_domain) matches local domain ($local_domain), no migration needed"); return; } $this->foreachCallable([$this, '_fetchTables'], [], function($i, $table_details) use ($backup_domain, $local_domain) { $table_name = current(get_object_vars($table_details)); $this->getLogController()->logMessage("Processing table $table_name"); $cols = $this->_fetchTableColumns($table_name); $primary_key = $this->_getTablePrimaryKey($table_name); if (!$primary_key) { $this->getLogController()->logMessage("Skipping table $table_name - No primary key found."); return; } $this->foreach($cols, function($i, $column) use ($table_name, $backup_domain, $local_domain, $primary_key) { $this->getLogController()->logMessage("Checking column for table $table_name: $column"); // Select primary key and target column $query = "SELECT `$primary_key`, `$column` FROM `$table_name` WHERE `$column` LIKE :old_domain"; $rows = $this->_mysql->query_exec($query, [':old_domain' => "%$backup_domain%"]); if (empty($rows)) return; foreach ($rows as $row) { $original_data = $row->{$column}; $replaced_data = $this->_changeColumnsValue($original_data, $backup_domain, $local_domain); if ($replaced_data !== $original_data) { $update_query = "UPDATE `$table_name` SET `$column` = :new_value WHERE `$primary_key` = :primary_key"; $result = $this->_mysql->query_exec($update_query, [ ':new_value' => $replaced_data, ':primary_key' => $row->{$primary_key} ]); if ($result === false) { $this->getLogController()->logMessage("SQL GENERAL ERROR"); } $this->getLogController()->logMessage("Updated column: $column in table: $table_name (Row ID: {$row->{$primary_key}})"); } } }, '_fixTableColumns_' . $table_name); }); } /** * Get primary key of a table */ private function _getTablePrimaryKey(string $table_name): ?string { return $this->func(function($table_name){ $query = "SHOW KEYS FROM `$table_name` WHERE Key_name = 'PRIMARY'"; $result = $this->_mysql->query_exec($query); return !empty($result) ? $result[0]->Column_name : null; }, [$table_name], '_getTablePrimaryKey_' . $table_name); } /** * @param $data * @param $old_value * @param $new_value * * @return __PHP_Incomplete_Class|array|bool|float|int|mixed|string|string[]|null */ private function _changeColumnsValue($data, $old_value, $new_value) { $this->getLogController()->logDebug("Processing data of type: " . gettype($data)); switch(true) { case is_numeric($data) || is_bool($data) || is_null($data) || $data instanceof \__PHP_Incomplete_Class: return $data; case is_array($data): $this->getLogController()->logDebug("Data is array"); foreach ($data as $key => $value) $data[$key] = $this->_changeColumnsValue($value, $old_value, $new_value); return $data; case is_object($data): $this->getLogController()->logDebug("Data is object"); foreach ($data as $key => $value) $data->{$key} = $this->_changeColumnsValue($value, $old_value, $new_value); return $data; case (($unserialized = @unserialize($data, ['allowed_classes' => false])) !== false || $data == 'b:0;'): $this->getLogController()->logDebug("Data is serialized"); return serialize($this->_changeColumnsValue($unserialized, $old_value, $new_value)); case is_string($data): $this->getLogController()->logDebug("Performing simple replacement from $old_value to $new_value"); $new_data = str_replace($old_value, $new_value, $data); if ($new_data !== $data) $this->getLogController()->logDebug("Replacement successful. New data: " . substr($new_data, 0, 50) . "..."); else $this->getLogController()->logDebug("No replacement made in this string."); return $new_data; } return $data; } /** * @return void * Specific plugin actions needed after restore * At this point we assume WordPress is loaded and healthy * */ public function _postRestoreActions(): void { $this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_POST_RESTORE_PLUGIN_ACTIONS); $this->getQueueItem()->updateProgress('Post restore - Actions'); $this->getLogController()->logMessage('Post restore - Actions'); $actions = Factory::getSettingsIntegrations()->getInegrations(); if(!$actions) return; $this->foreach($actions, function ($key, $action) { $method = "JetBackup\\Integrations\\Vendors\\$action"; if(!class_exists($method)) return; $this->getLogController()->logMessage("Doing $action post restore actions"); $instance = new $method(); $instance->execute(); }, '_postRestorePluginActions'); } /** * @return array */ public function _fetchBackupTables(): array { return $this->func(function() { $backup_prefix = $this->_getDatabasePrefix(); // Only return BASE TABLEs that start with the backup prefix. $rows = $this->_mysql->query_exec( "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME LIKE :p", [':p' => $backup_prefix . '%'] ) ?? []; // Return suffix (strip the prefix) to match your existing caller logic. return array_map(function($r) use ($backup_prefix){ $name = is_object($r) ? $r->TABLE_NAME : (is_array($r) ? $r['TABLE_NAME'] : (string)$r); return substr($name, strlen($backup_prefix)); }, $rows); }, [], '_fetchBackupTables'); } /** * @return array */ public function _fetchTables(): array { return $this->func(function() { return $this->_mysql->query_exec("SHOW TABLES"); }, [], '_fetchTables'); } /** * @param $table_name * * @return array */ public function _fetchTableColumns($table_name): array { return $this->func(function() use ($table_name) { $output = []; $results = $this->_mysql->query_exec("SHOW COLUMNS FROM `$table_name`"); foreach ($results as $row) $output[] = $row->Field; return $output; }, [], '_fetchTableColumns_' . $table_name); } /** * @param $table * * @return void * @throws RestoreException */ private function _dropForeignKeys($table) { try { // Drop foreign keys within the table $sql = "SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_TYPE = 'FOREIGN KEY' AND TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table_name"; $constraints = $this->_mysql->query_exec($sql, [':table_name' => $table]); foreach ($constraints as $constraint) { $constraint_name = $constraint->CONSTRAINT_NAME; $alter_query = "ALTER TABLE `$table` DROP FOREIGN KEY `$constraint_name`"; $this->getLogController()->logMessage("Dropping foreign key: $constraint_name from table: $table"); $this->_mysql->query_exec($alter_query); } // Drop foreign keys that reference the table from other tables $sql = "SELECT TABLE_NAME, CONSTRAINT_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA = DATABASE() AND REFERENCED_TABLE_NAME = :table_name"; $constraints = $this->_mysql->query_exec($sql, [':table_name' => $table]); foreach ($constraints as $constraint) { $referencing_table = $constraint->TABLE_NAME; $constraint_name = $constraint->CONSTRAINT_NAME; $alter_query = "ALTER TABLE `$referencing_table` DROP FOREIGN KEY `$constraint_name`"; $this->getLogController()->logMessage("Dropping foreign key: $constraint_name from table: `$referencing_table` that references `$table`"); $this->_mysql->query_exec($alter_query); } } catch ( Exception $e) { $this->getLogController()->logError("Error dropping foreign keys for table $table: " . $e->getMessage()); throw new RestoreException($e->getMessage()); } } /** * @param $table_name * @param $column_name * @param $id_column * * @return void * @throws Exception */ private function _replacePrefixInTable($table_name, $column_name, $id_column) { $this->getLogController()->logMessage("Starting prefix replacement in $table_name.$column_name"); $backup_prefix = $this->_getDatabasePrefix(); $mysql_auth = $this->_fetchMySQLAuth(); $select_query = "SELECT `$id_column`, `$column_name` FROM `$table_name` WHERE TRIM(`$column_name`) LIKE :old_prefix"; $this->getLogController()->logMessage("Running SQL: $select_query with :old_prefix => '$backup_prefix%'"); $rows = $this->_mysql->query_exec($select_query, [':old_prefix' => $backup_prefix . '%']); $this->getLogController()->logMessage("Number of rows found: " . count($rows)); if (!$rows) { $this->getLogController()->logMessage("No rows found needing prefix replacement in $table_name.$column_name"); return; } foreach ($rows as $row) { $original_data = $row->{$column_name}; $new_data = str_replace($backup_prefix, $mysql_auth->table_prefix, $original_data); // Log the raw data $this->getLogController()->logMessage("Row data for $id_column: {$row->{$id_column}} => $original_data"); // Log the before and after values $this->getLogController()->logMessage("Original value in {$table_name}.{$column_name} for {$id_column}: {$row->{$id_column}} is: {$original_data}"); $this->getLogController()->logMessage("New value after replacement in {$table_name}.{$column_name} for {$id_column}: {$row->{$id_column}} is: {$new_data}"); if ($new_data !== $original_data) { $update_query = "UPDATE `{$table_name}` SET `{$column_name}` = :new_value WHERE `{$id_column}` = :id"; $this->getLogController()->logMessage("Updating {$table_name}.{$column_name} for {$id_column}: {$row->{$id_column}}"); $this->getLogController()->logMessage("Executing update query: {$update_query} with new_value: {$new_data}, id: {$row->{$id_column}}"); $this->_mysql->query_exec($update_query, [ ':new_value' => $new_data, ':id' => $row->{$id_column}, ]); $this->getLogController()->logMessage("Updated {$table_name}.{$column_name} for {$id_column}: {$row->{$id_column}} successfully"); } else { $this->getLogController()->logMessage("No change required for {$id_column}: {$row->{$id_column}}"); } } } public function _postRestoreHealthCheck():void { $this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_POST_RESTORE_HEALTH_CHECK); $this->getQueueItem()->updateProgress('Post restore - Health Check'); $this->getLogController()->logMessage('Post restore - Health Check'); $this->func([$this, '_checkWPHealth']); $this->func([$this, '_checkDatabaseConnection']); $this->func([$this, '_checkConfig']); $this->func([$this, '_flushRewriteRules']); } public function _checkConfig() { $this->getLogController()->logMessage('Testing wp-config file'); $wp_config_file = JetBackup::WP_ROOT_PATH . JetBackup::SEP . 'wp-config.php'; if (!file_exists($wp_config_file)) { $this->getLogController()->logError('wp-config.php is missing'); return; } $required_constants = ['DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_HOST']; foreach ($required_constants as $constant) { if (!defined($constant)) $this->getLogController()->logError("Constant $constant is not defined in wp-config.php"); } $this->getLogController()->logMessage('wp-config file OK'); } public function _flushRewriteRules() { if (!function_exists('flush_rewrite_rules')) return; if (function_exists('wp_cache_flush')) wp_cache_flush(); $this->getLogController()->logMessage('Executing flush_rewrite_rules immediately'); flush_rewrite_rules(true); } public function _checkDatabaseConnection() { global $wpdb; $this->getLogController()->logMessage('Testing Database Connection'); try { if (!isset($wpdb) || empty($wpdb)) $this->_loadWP(); $wpdb->get_results("SELECT 1"); $this->getLogController()->logMessage('Database Connection OK'); } catch (Exception $e) { $this->getLogController()->logError($e->getMessage()); } } /** * @return void * * Some hosts use symlinked mu-plugins pointing to shared system directories. * During migration, these links break, causing missing files and errors. * */ public function _validateMUPlugins() { $mu_plugins_path = self::MU_PLUGINS_PATH; $this->getLogController()->logMessage("Validating MU Plugins folder: {$mu_plugins_path}"); if (!is_dir($mu_plugins_path)) { $this->getLogController()->logMessage("MU Plugins folder does not exist: {$mu_plugins_path}, nothing to do..."); return; } $mu_plugins = glob($mu_plugins_path . JetBackup::SEP . '*.php'); $broken_links = []; if (empty($mu_plugins)) { $this->getLogController()->logMessage("No MU Plugins found."); return; } foreach ($mu_plugins as $plugin) { if (is_link($plugin) && !file_exists($plugin)) $broken_links[] = $plugin; } if (empty($broken_links)) { $this->getLogController()->logMessage("No Broken MU Plugins found, folder is valid."); return; } // Remove broken links foreach ($broken_links as $link) { $this->getLogController()->logError("Broken MU Plugin found: {$link}"); if (@unlink($link)) { $this->getLogController()->logMessage("Successfully removed: {$link}"); } else { $this->getLogController()->logError("Failed to remove: {$link}"); } } $this->getLogController()->logDebug("Broken links removed: " . json_encode($broken_links)); } public function _checkWPHealth() { try { $this->_loadWP(); // Ensure WordPress is fully loaded by checking a key constant or function if (!function_exists('wp_get_current_user')) throw new Exception('WordPress did not fully load. A fatal error may have occurred.'); } catch(Exception $e) { $this->getLogController()->logError('Failed to load WordPress: ' . $e->getMessage()); $this->func(function() { $this->getLogController()->logMessage('Scanning and disabling all active plugins'); $plugin_dirs = glob(self::PLUGINS_PATH . JetBackup::SEP . '*', GLOB_ONLYDIR); $this->getLogController()->logDebug('Found plugin directories: ' . json_encode($plugin_dirs)); $this->foreach($plugin_dirs, function($i, $plugin_dir) { $this->_disablePlugin(basename($plugin_dir)); }, '_disableAllPlugins'); }, [], '_disableAllPlugins'); $disabled_plugins = glob(self::PLUGINS_PATH . JetBackup::SEP . '*_disabled_' . $this->getQueueItem()->getUniqueId(), GLOB_ONLYDIR); $this->foreach($disabled_plugins, function($i, $plugin_dir) { $plugin_name = basename($plugin_dir); $this->_enablePlugin($plugin_name); try { $this->_loadWP(); } catch(Exception $e) { $this->getLogController()->logError('Fatal error detected when enabling plugin: ' . $plugin_name . ' - ' . $e->getMessage()); $this->_disablePlugin($plugin_name); } }, '_checkPluginsOneByOne'); } } public function _enablePlugin($plugin_name):void { $plugin_dir = self::PLUGINS_PATH . JetBackup::SEP . $plugin_name; $plugin_dir_disabled = $plugin_dir . '_disabled_' . $this->getQueueItem()->getUniqueId(); if(!file_exists($plugin_dir_disabled)) return; $this->getLogController()->logMessage("Enabling plugin: $plugin_name"); if (!is_dir($plugin_dir_disabled)) { $this->getLogController()->logError("Failed to rename plugin directory $plugin_dir_disabled"); return; } rename($plugin_dir_disabled, $plugin_dir); $this->getLogController()->logMessage("Renamed plugin directory from $plugin_dir_disabled to $plugin_dir"); if (in_array($plugin_name, self::CACHE_PLUGINS)) { $cache_file = JetBackup::WP_ROOT_PATH . JetBackup::SEP . 'wp-content' . JetBackup::SEP . 'object-cache.php'; if (file_exists($cache_file)) { $this->getLogController()->logMessage("Renamed $cache_file to enable it"); rename($cache_file . '_disabled_' . $this->getQueueItem()->getUniqueId(), $cache_file); } } } // disable plugin from the db will not cover some fatal errors, we are forcing it by changing the name private function _disablePlugin($plugin_name):void { $plugin_dir = self::PLUGINS_PATH . JetBackup::SEP . $plugin_name; $plugin_dir_disabled = $plugin_dir . '_disabled_' . $this->getQueueItem()->getUniqueId(); $plugin_file = $plugin_dir . JetBackup::SEP . $plugin_name . '.php'; if(!file_exists($plugin_file)) return; $this->getLogController()->logMessage("Disabling plugin: $plugin_name"); if(in_array($plugin_name, self::PROTECTED_PLUGINS)) { $this->getLogController()->logMessage("Skipped disabling protected plugin: $plugin_name"); return; } if(file_exists($plugin_dir_disabled)) rename($plugin_dir_disabled, $plugin_dir_disabled . '_' . Util::generateRandomString()); if (!is_dir($plugin_dir)) { $this->getLogController()->logError("Failed to rename plugin directory $plugin_dir"); return; } rename($plugin_dir, $plugin_dir_disabled); $this->getLogController()->logMessage("Renamed plugin directory from $plugin_dir to $plugin_dir_disabled"); if (in_array($plugin_name, self::CACHE_PLUGINS)) { $this->getLogController()->logMessage("Problematic plugin is object cache, disabling 'object-cache.php' file"); $cache_file = JetBackup::WP_ROOT_PATH . JetBackup::SEP . 'wp-content' . JetBackup::SEP . 'object-cache.php'; if (file_exists($cache_file)) { $this->getLogController()->logMessage("Renamed $cache_file to disable it"); rename($cache_file, $cache_file . '_disabled_' . $this->getQueueItem()->getUniqueId()); } } //$this->_disabled_plugins[] = $plugin_name; } /** * @return void * @throws Exception */ private function _loadWP():void { ob_start(); // Start output buffering $load_file = JetBackup::WP_ROOT_PATH . JetBackup::SEP . 'wp-load.php'; try { if (file_exists($load_file)) { require_once($load_file); // Temporary disable cron events if(!defined('DISABLE_WP_CRON')) define('DISABLE_WP_CRON', true); $output = ob_get_clean(); // Get buffered output // Log the output, even if it seems normal if (!empty($output)) $this->getLogController()->logMessage('Output from WordPress load: ' . $output); $this->getLogController()->logMessage('WordPress ecosystem loaded successfully'); } else { throw new Exception('wp-load.php not found in the specified WordPress root.'); } } catch (\Throwable $e) { $output = ob_get_clean(); // Clean the buffer in case of an error $this->getLogController()->logError('Failed loading WordPress: ' . $e->getMessage() . ' | Buffered Output: ' . $output); throw new Exception('Failed loading WordPress: ' . $e->getMessage()); } } }