PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / 3.1.13.4
JetBackup – Backup, Restore & Migrate v3.1.13.4
3.1.22.3 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.8.1 1.4.9 1.5.0 1.5.1 1.5.1.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.10 1.6.11 1.6.12 1.6.13 1.6.15 1.6.5.1 1.6.8.8 1.6.9 1.6.9.1 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7.5 2.0.8.7 2.0.9.11 2.0.9.14 2.0.9.15 2.0.9.6 2.0.9.7 2.0.9.9 3.1.10.7 3.1.11.1 3.1.12.3 3.1.13.4 3.1.14.17 3.1.15.4 3.1.16.1 3.1.17.5 3.1.18.10 3.1.18.8 3.1.18.9 3.1.19.8 3.1.20.3 3.1.21.3 3.1.7.9 3.1.9.2 trunk 1.1.90 1.1.91 1.2.0 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2
backup / src / JetBackup / Cron / Task / Restore.php
backup / src / JetBackup / Cron / Task Last commit date
.htaccess 9 months ago Backup.php 9 months ago Download.php 9 months ago DownloadBackupLog.php 9 months ago Export.php 9 months ago Extract.php 9 months ago PreRestore.php 9 months ago Reindex.php 9 months ago Restore.php 9 months ago RetentionCleanup.php 9 months ago System.php 9 months ago Task.php 9 months ago index.html 9 months ago web.config 9 months ago
Restore.php
1318 lines
1 <?php
2
3 namespace JetBackup\Cron\Task;
4
5 use Exception;
6 use JetBackup\BackupJob\BackupJob;
7 use JetBackup\Data\Engine;
8 use JetBackup\Data\Mysqldump;
9 use JetBackup\Encryption\Crypt;
10 use JetBackup\Entities\Util;
11 use JetBackup\Exception\AjaxException;
12 use JetBackup\Exception\DBException;
13 use JetBackup\Exception\ExecutionTimeException;
14 use JetBackup\Exception\JetBackupLinuxException;
15 use JetBackup\Exception\RestoreException;
16 use JetBackup\Exception\SnapshotMetaException;
17 use JetBackup\Exception\TaskException;
18 use JetBackup\Factory;
19 use JetBackup\Filesystem\File;
20 use JetBackup\Integrations\Integrations;
21 use JetBackup\JetBackup;
22 use JetBackup\JetBackupLinux\JetBackupLinux;
23 use JetBackup\Queue\Queue;
24 use JetBackup\Queue\QueueItem;
25 use JetBackup\Queue\QueueItemRestore;
26 use JetBackup\Snapshot\Snapshot;
27 use JetBackup\Wordpress\Wordpress;
28 use phpseclib3\Math\BigInteger\Engines\PHP;
29 use SleekDB\Exceptions\InvalidArgumentException;
30 use SleekDB\Exceptions\IOException;
31
32 if (!defined( '__JETBACKUP__')) die('Direct access is not allowed');
33
34 class Restore extends Task {
35
36 const LOG_FILENAME = 'restore';
37
38 const PLUGINS_PATH = JetBackup::WP_ROOT_PATH . JetBackup::SEP . 'wp-content' . JetBackup::SEP . 'plugins';
39 const MU_PLUGINS_PATH = JetBackup::WP_ROOT_PATH . JetBackup::SEP . 'wp-content' . JetBackup::SEP . 'mu-plugins';
40
41 const PROTECTED_PLUGINS = ['backup']; // Add this protected attribute to store protected plugin names.
42 const CACHE_PLUGINS = ['redis-cache'];
43
44
45 const MYSQL_AUTH_PATTERNS = [
46 'db_name' => "/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*['\"]((?:\\\\.|[^'\"])+)['\"]\s*\)\s*;/",
47 'db_user' => "/define\s*\(\s*['\"]DB_USER['\"]\s*,\s*['\"]((?:\\\\.|[^'\"])+)['\"]\s*\)\s*;/",
48 'db_password' => "/define\s*\(\s*['\"]DB_PASSWORD['\"]\s*,\s*['\"]((?:\\\\.|[^'\"])+)['\"]\s*\)\s*;/",
49 'db_host' => "/define\s*\(\s*['\"]DB_HOST['\"]\s*,\s*['\"]((?:\\\\.|[^'\"])+)['\"]\s*\)\s*;/",
50 'table_prefix' => "/^\s*\\\$table_prefix\s*=\s*['\"]((?:\\\\.|[^'\"])+)['\"]\s*;/",
51 ];
52
53 private QueueItemRestore $_queue_item_restore;
54 private Mysqldump $_mysql;
55 private array $_disabled_plugins=[];
56
57 /**
58 *
59 */
60 public function __construct() {
61 parent::__construct(self::LOG_FILENAME);
62 }
63
64 /**
65 * @return void
66 * @throws IOException
67 * @throws InvalidArgumentException
68 * @throws TaskException
69 * @throws ExecutionTimeException
70 */
71 public function execute():void {
72 parent::execute();
73
74 $this->_queue_item_restore = $this->getQueueItem()->getItemData();
75
76 if($this->getQueueItem()->getStatus() == Queue::STATUS_RESTORE_WAITING_FOR_RESTORE) {
77 $this->getLogController()->logMessage("Starting restore");
78
79 $this->getQueueItem()->getProgress()->setTotalItems( count(Queue::STATUS_RESTORE_NAMES) + 3);
80 $this->getQueueItem()->save();
81
82 $this->getQueueItem()->updateProgress('Starting restore');
83 } else if($this->getQueueItem()->getStatus() > Queue::STATUS_RESTORE_WAITING_FOR_RESTORE) {
84 $this->getLogController()->logMessage('Resumed Restore');
85 }
86
87 try {
88
89 if(!$this->_queue_item_restore->getSnapshotId() && !$this->_queue_item_restore->getSnapshotPath())
90 throw new RestoreException("No snapshot id or path provided");
91
92 $this->getLogController()->logDebug('Item data: ' . print_r($this->_queue_item_restore, 1));
93
94 $snapshot = $this->getSnapshot();
95
96 if($snapshot->getEngine() == Engine::ENGINE_JB) {
97 $this->func([$this, '_addToQueue']);
98 $this->func([$this, '_updateQueue']);
99 } else {
100 $mysql_auth = $this->_fetchMySQLAuth();
101 $this->_mysql = new Mysqldump($mysql_auth->db_name, $mysql_auth->db_user, $mysql_auth->db_password, $mysql_auth->db_host);
102 $this->_mysql->setLogController($this->getLogController());
103
104 $this->func([$this, '_preFetchAdminUser']);
105 $this->func([$this, '_database']);
106 $this->func([$this, '_files']);
107 $this->func([$this, '_postRestoreDBPrefix']);
108 $this->func([$this, '_postRestoreDomainMigration']);
109 $this->func([$this, '_postInsertAdminUser']);
110 $this->func([$this, '_postRestoreHealthCheck']);
111 $this->func([$this, '_postRestoreActions']); // must be called after WordPress is loaded
112 }
113
114 if($this->getQueueItem()->getStatus() < Queue::STATUS_DONE) $this->getQueueItem()->updateStatus(Queue::STATUS_DONE);
115 $this->getLogController()->logMessage('Completed!');
116 } catch(ExecutionTimeException $e) {
117 throw $e;
118 } catch( Exception $e) {
119 $this->getQueueItem()->updateStatus(Queue::STATUS_FAILED);
120 $this->getLogController()->logError($e->getMessage());
121 $this->getLogController()->logMessage('Failed!');
122 }
123
124 $this->getQueueItem()->updateProgress($this->getQueueItem()->getStatus() == Queue::STATUS_DONE ? 'Restore Completed' : 'Error during restore', QueueItem::PROGRESS_LAST_STEP);
125 $this->getLogController()->logMessage('Total time: ' . $this->getExecutionTimeElapsed());
126 }
127
128 public function _updateQueue() {
129 if(!$this->_queue_item_restore->getQueueGroupId()) throw new RestoreException("Can't find queue group");
130
131 $this->getLogController()->logMessage('Monitoring JetBackup Linux queue item status');
132
133 while(true) {
134
135 try {
136 $response = JetBackupLinux::getQueueGroup($this->_queue_item_restore->getQueueGroupId());
137 } catch(JetBackupLinuxException $e) {
138 throw new RestoreException("Failed fetching JetBackup Linux queue item status. Error: " . $e->getMessage());
139 }
140
141 $remote_progress = $response['progress'];
142 $progress = $this->getQueueItem()->getProgress();
143
144 $current_status = JetBackupLinux::QUEUE_STATUS_RESTORE_MAPPING[$response['status']] ?? 0;
145 $old_status = $this->getQueueItem()->getStatus();
146
147 if($current_status) {
148 $name = JetBackupLinux::QUEUE_STATUS_RESTORE_ACCOUNT_NAMES[$response['status']];
149 if($current_status != $old_status) {
150 $this->getQueueItem()->updateStatus($current_status);
151 $this->getQueueItem()->updateProgress($name);
152 }
153
154 if($response['status'] == JetBackupLinux::QUEUE_STATUS_RESTORE_ACCOUNT_HOMEDIR) {
155 $name .= " ({$remote_progress['completed_files']}/{$remote_progress['total_files']})";
156 }
157
158 $this->getLogController()->logMessage($name . '...');
159 }
160
161 if($response['status'] == JetBackupLinux::QUEUE_STATUS_RESTORE_ACCOUNT_HOMEDIR && isset($remote_progress['total_files']) && intval($remote_progress['total_files']) > 1) {
162 $progress->setTotalSubItems((int) $remote_progress['total_files']);
163 $progress->setCurrentSubItem((int) $remote_progress['completed_files']);
164 } else {
165 if(isset($remote_progress['subtotal'])) $progress->setTotalSubItems($remote_progress['subtotal']);
166 if(isset($remote_progress['subcompleted'])) $progress->setCurrentSubItem($remote_progress['subcompleted']);
167 }
168
169 if($response['status'] >= JetBackupLinux::QUEUE_STATUS_COMPLETED) {
170 $this->getQueueItem()->setStatus(JetBackupLinux::QUEUE_STATUS_MAPPING[$response['status']]);
171 $this->getQueueItem()->save();
172 return;
173 } else {
174 $progress->setMessage(JetBackupLinux::QUEUE_STATUS_RESTORE_ACCOUNT_NAMES[$response['status']] ?? 'Processing');
175 $this->getQueueItem()->save();
176 }
177
178 sleep(5);
179 }
180 }
181
182 /**
183 * @throws DBException
184 * @throws RestoreException
185 * @throws IOException
186 * @throws InvalidArgumentException
187 */
188 public function _addToQueue() {
189 if($this->_queue_item_restore->getQueueGroupId()) return;
190
191 $this->getLogController()->logMessage('Adding restore to JetBackup Linux queue');
192
193 $snapshot = $this->getSnapshot();
194
195 $files = $items = [];
196 $homedir_item_id = $database_item_id = null;
197
198 foreach ($snapshot->getItems() as $item) {
199 switch($item->getBackupContains()) {
200 case BackupJob::BACKUP_ACCOUNT_CONTAINS_HOMEDIR:
201 $homedir_item_id = $item->getUniqueId();
202 break;
203 case BackupJob::BACKUP_ACCOUNT_CONTAINS_DATABASE:
204 $database_item_id = $item->getUniqueId();
205 break;
206 }
207 }
208
209 $options = $this->_queue_item_restore->getOptions();
210
211 if ($homedir_item_id && ($options & QueueItemRestore::OPTION_RESTORE_FILES_ENTIRE)) {
212 $files[$homedir_item_id][Factory::getWPHelper()->getWordPressRelativePublicDir()] = 'Directory';
213 $items[] = $homedir_item_id;
214 }
215
216 if ($homedir_item_id && ($options & QueueItemRestore::OPTION_RESTORE_FILES_INCLUDE)) {
217
218 $files = [];
219
220 $public_dir = Factory::getWPHelper()->getWordPressRelativePublicDir();
221
222 foreach ($this->_queue_item_restore->getFileManager() as $file) {
223 $path = $public_dir . JetBackup::SEP . trim($file['path'], JetBackup::SEP);
224 $this->getLogController()->logMessage("Path: $path");
225 $files[$homedir_item_id][$path] = $file['type'];
226 }
227
228 $items[] = $homedir_item_id;
229 }
230
231
232 if ($database_item_id && ($options & QueueItemRestore::OPTION_RESTORE_DATABASE_ENTIRE)) {
233 $items[] = $database_item_id;
234 }
235
236 try {
237 $response = JetBackupLinux::addQueueItems($items, $files, ['merge' => 1]);
238 } catch (Exception $e) {
239 throw new RestoreException("Failed adding to JetBackup Linux queue. Error: " . $e->getMessage());
240 }
241
242 $this->_queue_item_restore->setQueueGroupId($response['_id']);
243 $this->getQueueItem()->save();
244 }
245
246 /**
247 * @return Snapshot
248 */
249 private function getSnapshot():Snapshot {
250
251 return $this->func(function() {
252
253 if($this->_queue_item_restore->getSnapshotId()) {
254 $snapshot = new Snapshot($this->_queue_item_restore->getSnapshotId());
255 if($snapshot->getId() && $snapshot->getEngine() == Engine::ENGINE_JB) return $snapshot;
256 }
257
258 $workspace = $this->getQueueItem()->getWorkspace();
259 $meta_file = sprintf(Snapshot::META_FILEPATH, $workspace);
260
261 if(!file_exists($meta_file)) throw new RestoreException("Backup meta file not exists ($meta_file)");
262
263 $snapshot = new Snapshot();
264 try {
265 $snapshot->importMeta($meta_file, true);
266 } catch(SnapshotMetaException $e) {
267 throw new RestoreException("Failed read snapshot meta file. Error: " . $e->getMessage());
268 }
269
270 return $snapshot;
271
272 }, [], '_loadSnapshot');
273 }
274
275 /**
276 * @return object|null
277 */
278 private function _fetchMySQLAuth():object {
279
280 return $this->func(function() {
281
282 $config_file = JetBackup::WP_ROOT_PATH . JetBackup::SEP . 'wp-config.php';
283 if(!file_exists($config_file)) throw new RestoreException("The wp-config.php file does not exist at the specified path. ($config_file)");
284
285 $output = new \stdClass();
286
287 if(!($f = fopen($config_file, 'r'))) throw new RestoreException("Unable to open the wp-config.php file.");
288
289 $patterns = self::MYSQL_AUTH_PATTERNS;
290
291 while (($line = fgets($f)) !== false) {
292 $line = trim($line);
293 // Skip commented lines
294 if (strpos($line, '//') === 0 || strpos($line, '#') === 0) continue;
295
296 foreach ($patterns as $key => $pattern) {
297 if (!preg_match($pattern, $line, $matches)) continue;
298 $value = stripcslashes($matches[1]);
299
300 if ($key === 'db_host' && strpos($value, ':') !== false) {
301 list($output->db_host, $output->db_port) = explode(':', $value, 2);
302 } else {
303 $output->{$key} = $value;
304 }
305
306 unset($patterns[$key]);
307 break; // Break the foreach loop if a pattern matches
308 }
309
310 // Break the while loop if all patterns are found
311 if (empty($patterns)) break;
312 }
313
314 fclose($f);
315
316 if (empty($output)) throw new RestoreException("Unable to find the database credentials in the wp-config.php file.");
317
318 // Set default port if not found
319 if (!isset($output->db_port)) $output->db_port = Factory::getSettingsGeneral()->getMySQLDefaultPort(); // Default MySQL port
320
321 return $output;
322
323 }, [], '_MySQLAuth');
324 }
325
326 /**
327 * @return void
328 */
329 public function _database():void {
330
331 if(!$this->_queue_item_restore->isRestoreDatabase()) return;
332
333 $this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_DATABASE);
334 $this->getQueueItem()->updateProgress('Restoring database');
335
336 $this->getLogController()->logMessage('Importing all database tables');
337
338 $workspace = $this->getQueueItem()->getWorkspace();
339 $database_path = $workspace . JetBackup::SEP . Snapshot::SKELETON_DATABASE_DIRNAME;
340
341 $this->foreachCallable([$this, '_listDatabaseTables'], [], function($i, $table_dump) use ($database_path) {
342
343 // remove gz suffix, we already decompressed it in PreRestore task
344 if(str_ends_with($table_dump, '.gz')) $table_dump = substr($table_dump, 0, -3);
345
346 $dump_file = $database_path . JetBackup::SEP . $table_dump;
347 $table_name = substr($table_dump, 0, -4);
348
349 $progress = $this->getQueueItem()->getProgress();
350 $progress->setSubMessage($table_name);
351 $progress->increaseCurrentSubItem();
352 $this->getQueueItem()->save();
353
354 $this->getLogController()->logMessage(' - Importing table ' . $table_name);
355
356 if(!file_exists($dump_file)) {
357 $this->getLogController()->logError('Dump file not found');
358 return;
359 }
360
361 $this->getLogController()->logMessage(' - Dump file: ' . $dump_file);
362 $this->_mysql->import($dump_file);
363
364 }, 'database_tables');
365 }
366
367 /**
368 * @throws Exception
369 */
370 public function _preFetchAdminUser():void {
371 $tables_to_restore = $this->_listDatabaseTables();
372 $matchingUser = false;
373
374 foreach ($tables_to_restore as $table) {
375 if (str_starts_with($table, $this->_getDatabasePrefix() . "users") ||
376 str_starts_with($table, $this->_getDatabasePrefix() . "usermeta")) {
377 $matchingUser = true;
378 break;
379 }
380 }
381
382 if(!$matchingUser) return;
383 $this->getLogController()->logMessage(' Fetching admin users');
384 $admin_user = $this->_getAdminUser();
385 $admin_user = $admin_user ? serialize($admin_user) : null;
386 if(!$admin_user) return;
387
388 $mysql_auth = $this->_fetchMySQLAuth();
389 $this->_queue_item_restore->setAdminUser(Crypt::encrypt($admin_user, $mysql_auth->db_password));
390 $this->getQueueItem()->save();
391
392 }
393
394 /**
395 * @throws RestoreException
396 */
397 public function _postInsertAdminUser():void {
398 if(!($admin_user = $this->_queue_item_restore->getAdminUser())) return;
399 $this->getLogController()->logMessage(' Inserting admin user');
400 $mysql_auth = $this->_fetchMySQLAuth();
401 $admin_user = Crypt::decrypt($admin_user, $mysql_auth->db_password);
402 $this->_insertAdminUser( (array) unserialize( $admin_user ));
403 }
404
405
406 /**
407 * @param array $user_data
408 *
409 * @return void
410 * @throws RestoreException
411 */
412 private function _insertAdminUser(array $user_data): void {
413 $mysql_auth = $this->_fetchMySQLAuth();
414 $user_table = $mysql_auth->table_prefix . 'users';
415
416 $query = "SHOW COLUMNS FROM `$user_table`";
417 $columns = $this->_mysql->query_exec($query);
418
419 if (!$columns || count($columns) === 0) throw new RestoreException("The table $user_table does not exist or has no columns.");
420 $existing_columns = array_map(fn($column) => $column->Field, $columns);
421 $filtered_user_data = array_intersect_key($user_data, array_flip($existing_columns));
422 if (empty($filtered_user_data)) throw new RestoreException("No valid user data found to insert.");
423
424 // Prepare SQL query parts
425 $columns_str = implode(", ", array_keys($filtered_user_data));
426 $placeholders = implode(", ", array_map(fn($key) => ":$key", array_keys($filtered_user_data)));
427 $update_str = implode(", ", array_map(fn($key) => "`$key` = VALUES(`$key`)", array_keys($filtered_user_data)));
428
429 $query = "INSERT INTO `$user_table` ($columns_str) VALUES ($placeholders)
430 ON DUPLICATE KEY UPDATE $update_str";
431
432 $params = array_combine(array_map(fn($key) => ":$key", array_keys($filtered_user_data)), array_values($filtered_user_data));
433
434 try {
435 $this->_mysql->query_exec($query, $params);
436 $this->getLogController()->logMessage("Admin user successfully inserted or updated in $user_table");
437
438 // Set admin capabilities
439 if (isset($user_data['ID'])) {
440 $user_id = $user_data['ID'];
441 $usermeta_table = $mysql_auth->table_prefix . 'usermeta';
442
443 // Insert or update wp_capabilities
444 $capabilities_query = "INSERT INTO `$usermeta_table` (`user_id`, `meta_key`, `meta_value`)
445 VALUES (:user_id, :meta_key_cap, :meta_value_cap)
446 ON DUPLICATE KEY UPDATE `meta_value` = :meta_value_cap";
447
448 $params_capabilities = [
449 ':user_id' => $user_id,
450 ':meta_key_cap' => $mysql_auth->table_prefix . 'capabilities',
451 ':meta_value_cap' => serialize(['administrator' => true]),
452 ];
453
454 $this->_mysql->query_exec($capabilities_query, $params_capabilities);
455 $this->getLogController()->logMessage("Admin capabilities set for user ID $user_id");
456
457 // Insert or update wp_user_level
458 $user_level_query = "INSERT INTO `$usermeta_table` (`user_id`, `meta_key`, `meta_value`)
459 VALUES (:user_id, :meta_key_lvl, :meta_value_lvl)
460 ON DUPLICATE KEY UPDATE `meta_value` = :meta_value_lvl";
461
462 $params_user_level = [
463 ':user_id' => $user_id,
464 ':meta_key_lvl' => $mysql_auth->table_prefix . 'user_level',
465 ':meta_value_lvl' => '10', // Administrator level
466 ];
467
468 $this->_mysql->query_exec($user_level_query, $params_user_level);
469 $this->getLogController()->logMessage("Admin user level set for user ID $user_id");
470 }
471 } catch (Exception $e) {
472 throw new RestoreException("Failed to insert or update admin user: " . $e->getMessage());
473 }
474 }
475
476
477 /**
478 * @return array|null
479 * @throws Exception
480 */
481 private function _getAdminUser(): ?array {
482
483 return $this->func(function(){
484 $this->getLogController()->logMessage(' - Handling admin user');
485
486 $mysql_auth = $this->_fetchMySQLAuth();
487 $table_prefix = $mysql_auth->table_prefix ?? 'wp_';
488
489 // Fetch all users with session tokens
490 $query = "SELECT u.ID, u.user_login, um.meta_value AS session_tokens
491 FROM `{$table_prefix}usermeta` um
492 JOIN `{$table_prefix}users` u ON um.user_id = u.ID
493 WHERE um.meta_key = 'session_tokens'";
494
495 $users = $this->_mysql->query_exec($query);
496
497 $latest_user = null;
498 $latest_expiration = 0;
499
500 // Loop through each user and find the latest session
501 foreach ($users as $user) {
502 $session_tokens = unserialize($user->session_tokens);
503
504 if (!is_array($session_tokens)) continue;
505
506 foreach ($session_tokens as $session) {
507 if (isset($session['expiration']) && $session['expiration'] > $latest_expiration) {
508 $latest_expiration = $session['expiration'];
509 $latest_user = $user->user_login;
510 }
511 }
512 }
513
514 if (!$latest_user) return null;
515
516 // Fetch full user details (Same as the original function)
517 $query = "SELECT * FROM `{$table_prefix}users` WHERE `user_login` = :username";
518 $params = [':username' => $latest_user];
519 $user = $this->_mysql->query_exec($query, $params);
520
521 if (!isset($user[0])) return null;
522 return (array) $user[0]; // Return the full user details
523 }, [], 'getAdminUser');
524 }
525
526
527 /**
528 * @return array
529 * @throws DBException
530 * @throws IOException
531 * @throws InvalidArgumentException
532 */
533 public function _listDatabaseTables():array {
534
535 $output = [];
536
537 $items = $this->getSnapshot()->getItems();
538
539 foreach ($items as $item) {
540 // Skip if the item is not a database backup
541 if ($item->getBackupContains() !== BackupJob::BACKUP_ACCOUNT_CONTAINS_DATABASE) continue;
542
543 $dbName = $item->getName();
544 $includedDatabases = $this->_queue_item_restore->getIncludedDatabases();
545 $excludedDatabases = $this->_queue_item_restore->getExcludedDatabases();
546
547 // Restore only included tables
548 if ($includedDatabases && !in_array($dbName, $includedDatabases)) continue;
549
550 // Exclude specific tables
551 if ($excludedDatabases && in_array($dbName, $excludedDatabases)) continue;
552
553 // Add the database backup to the output
554 $output[] = basename($item->getPath());
555 }
556
557 $progress = $this->getQueueItem()->getProgress();
558 $progress->setTotalSubItems(sizeof($output));
559 $progress->setCurrentSubItem(0);
560 $this->getQueueItem()->save();
561
562 $this->getLogController()->logMessage('Total of ' . sizeof($output) . ' database tables found for restore');
563
564 return $output;
565 }
566
567 /**
568 * @return void
569 * @throws RestoreException
570 */
571 public function _files():void {
572
573 if(!$this->_queue_item_restore->isRestoreHomedir()) return;
574
575 $this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_FILES);
576 $this->getQueueItem()->updateProgress('Restoring homedir files');
577 $this->getLogController()->logMessage("Restoring homedir files");
578
579 $workspace = $this->getQueueItem()->getWorkspace();
580
581 $source = new File($workspace . JetBackup::SEP . Snapshot::SKELETON_FILES_DIRNAME);
582 if(!$source->exists() || !$source->isDir()) throw new RestoreException("Can't find source directory ({$source->path()})");
583
584 $target = new File(Factory::getWPHelper()->getWordPressHomedir());
585 if(!$target->exists() || !$target->isDir()) throw new RestoreException("Can't find target directory ({$target->path()})");
586
587
588 $queue = [$source->dir()];
589
590 while($queue) {
591 $dir = array_shift($queue);
592
593 $dir_path = $dir->path;
594 //$contains = 0;
595
596 while(($entry = $dir->read()) !== false) {
597 if($entry == '.' || $entry == '..') continue;
598
599 $source_file = new File($dir_path . JetBackup::SEP . $entry);
600 $target_file = new File($target->path() . JetBackup::SEP . trim(substr($source_file->path(), strlen($source->path())), JetBackup::SEP));
601
602 $this->getLogController()->logDebug("Restoring file {$source_file->path()} to {$target_file->path()}");
603
604 if ($source_file->isDir()) {
605
606 switch (true) {
607 case (!$target_file->exists()):
608 // Case 1: Target does not exist → Create the directory
609 $this->getLogController()->logDebug("[Directory Mode] Target {$target_file->path()} does not exist → Create the directory");
610 mkdir($target_file->path(), $source_file->mode());
611 break;
612
613 case ($target_file->isLink() && $target_file->isDir($target_file->path())):
614 // Case 2: Target is a symlink to a directory → Keep it, do nothing
615 $this->getLogController()->logDebug("[Directory Mode] Target {$target_file->path()} is a symlink to a directory → Keep it, do nothing");
616 break;
617
618 case (!$target_file->isDir()):
619 // Case 3: Target exists but is NOT a directory → Remove it
620 $this->getLogController()->logDebug("[Directory Mode] Target {$target_file->path()} exists but is NOT a directory → Remove it");
621 unlink($target_file->path());
622 mkdir($target_file->path(), $source_file->mode());
623 break;
624
625 default:
626 // Case 4: Target is an existing directory → Just update permissions
627 $this->getLogController()->logDebug("[Directory Mode] Target {$target_file->path()} is an existing directory → Just update permissions");
628 chmod($target_file->path(), $source_file->mode());
629 break;
630 }
631
632 // Add directory to queue for further processing
633 $queue[] = $source_file->dir();
634
635 } else {
636
637 switch (true) {
638 case (!$target_file->exists()):
639 // Case 1: Target does not exist → Move file
640 $this->getLogController()->logDebug("[File Mode] Target {$target_file->path()} does not exist → Move file");
641 rename($source_file->path(), $target_file->path());
642 break;
643
644 case (
645 $target_file->mtime() != $source_file->mtime() ||
646 $target_file->size() != $source_file->size()
647 ):
648 // Case 2: Target exists but is outdated → Remove and replace
649 $this->getLogController()->logDebug("[File Mode] Target {$target_file->path()} exists but is outdated → Remove and replace");
650
651 unlink($target_file->path());
652 rename($source_file->path(), $target_file->path());
653 break;
654
655 default:
656 // Case 3: Target is identical → No need to replace, just remove source
657 $this->getLogController()->logDebug("[File Mode] Target {$target_file->path()} is identical → No need to replace, just remove source");
658
659 unlink($source_file->path());
660 break;
661 }
662 }
663
664
665 }
666
667 $dir->close();
668
669 // TODO we need to find better way to delete empty folders
670 // delete the folder if it's empty
671 //if(!$contains) rmdir($dir_path);
672 }
673 }
674
675 /**
676 * @return null
677 */
678 private function _getSiteURL() {
679
680 return $this->func(function() {
681
682 $snapshot = $this->getSnapshot();
683 foreach($snapshot->getItems() as $item) {
684 if ($item->getBackupContains() != BackupJob::BACKUP_ACCOUNT_CONTAINS_FULL) continue;
685 $prefix = $item->getParams()['site_url'] ?? '';
686 if($prefix) return $prefix;
687 }
688
689 return '';
690
691 }, [], '_fetchSiteURL');
692
693 }
694
695 /**
696 * @return string
697 */
698 private function _getDatabasePrefix():string {
699
700 return $this->func(function() {
701
702 $snapshot = $this->getSnapshot();
703 foreach($snapshot->getItems() as $item) {
704 if ($item->getBackupContains() != BackupJob::BACKUP_ACCOUNT_CONTAINS_DATABASE) continue;
705 $prefix = $item->getParams()['db_prefix'] ?? '';
706 if($prefix) return $prefix;
707 }
708
709 return '';
710
711 }, [], '_fetchDatabasePrefix');
712 }
713
714 /**
715 * @return void
716 * @throws RestoreException
717 * @throws Exception
718 */
719 public function _postRestoreDBPrefix():void {
720
721 if(!$this->_queue_item_restore->isRestoreDatabase()) return;
722
723 $this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_POST_RESTORE_DB_PREFIX);
724 $this->getQueueItem()->updateProgress('Post restore - DB Prefix');
725
726 if(!($backup_prefix = $this->_getDatabasePrefix())) throw new RestoreException("Can't find backup database prefix");
727
728 $mysql_auth = $this->_fetchMySQLAuth();
729 $local_prefix = $mysql_auth->table_prefix;
730
731 if($local_prefix == $backup_prefix) return;
732
733 $this->getLogController()->logMessage("Local DB Prefix ($local_prefix) is different from ($backup_prefix)");
734
735 $backup_tables = $this->_fetchBackupTables();
736
737 if(!sizeof($backup_tables)) return;
738
739 $this->getLogController()->logMessage("Searching for orphaned tables");
740 $this->foreach($backup_tables, function($i, $table) use ($local_prefix, $backup_prefix) {
741 $table_name = $local_prefix . $table;
742 $table_name_old = $backup_prefix . $table;
743
744 $this->getLogController()->logMessage("- Dropping database table `$table_name` foreign keys");
745 $this->_dropForeignKeys($table_name);
746
747 $this->getLogController()->logMessage("- Dropping database table `$table_name` if exists");
748 $this->_mysql->query_exec("DROP TABLE IF EXISTS `$table_name`");
749
750 $this->getLogController()->logMessage("- Renaming database table `$table_name_old` to `$table_name`");
751 $this->_mysql->query_exec("RENAME TABLE `$table_name_old` TO `$table_name`");
752 }, '_dropTables');
753
754
755 $usermeta_table = $local_prefix . 'usermeta';
756 $options_table = $local_prefix . 'options';
757
758 // Search and replace prefix in the dynamically generated usermeta table
759 $this->_replacePrefixInTable($usermeta_table, 'meta_key', 'umeta_id');
760 $this->_replacePrefixInTable($usermeta_table, 'meta_value', 'umeta_id');
761
762 // Search and replace prefix in the dynamically generated options table
763 $this->_replacePrefixInTable($options_table, 'option_name', 'option_id');
764 $this->_replacePrefixInTable($options_table, 'option_value', 'option_id');
765 }
766
767 /**
768 * @param $url
769 *
770 * @return string
771 */
772 private static function _getParseDomain($url): string {
773 $compa = parse_url($url);
774 $domain = $compa['host'] ?? '';
775 $port = $compa['port'] ?? '';
776 $path = $compa['path'] ?? '';
777
778 $domain = preg_replace("/(www|\dww|w\dw|ww\d)\./", "", $domain);
779
780 return $domain . ($port ? ":$port" : '') . $path;
781 }
782
783 private static function _buildSiteURL():string {
784 $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
785 $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; // Default to localhost if missing
786 $script_path = $_SERVER['SCRIPT_NAME'] ?? $_SERVER['PHP_SELF'] ?? $_SERVER['REQUEST_URI'];
787 $script_dir = dirname($script_path);
788
789 return rtrim($protocol . "://" . $host . $script_dir, '/');
790 }
791
792 /**
793 * @return void
794 */
795 public function _postRestoreDomainMigration(): void {
796 $this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_POST_RESTORE_DOMAIN_MIGRATION);
797 $this->getQueueItem()->updateProgress('Post restore - Domain Migration');
798
799 $backup_domain = self::_getParseDomain($this->_getSiteURL());
800 $local_domain = self::_getParseDomain(self::_buildSiteURL());
801
802 $alternate_path = strlen(Factory::getSettingsRestore()->isRestoreAlternatePathEnabled() ? JetBackup::CRON_PUBLIC_URL : '');
803 if($alternate_path) $local_domain = substr($local_domain, 0, -$alternate_path);
804
805 if(!$local_domain) {
806 $this->getLogController()->logMessage("[Error] Couldn't retrieve local domain, exiting");
807 return;
808 }
809
810 if ($backup_domain === $local_domain) {
811 $this->getLogController()->logMessage("Domain from backup ($backup_domain) matches local domain ($local_domain), no migration needed");
812 return;
813 }
814
815 $this->foreachCallable([$this, '_fetchTables'], [], function($i, $table_details) use ($backup_domain, $local_domain) {
816 $table_name = current(get_object_vars($table_details));
817
818 $this->getLogController()->logMessage("Processing table $table_name");
819
820 $cols = $this->_fetchTableColumns($table_name);
821 $primary_key = $this->_getTablePrimaryKey($table_name);
822
823 if (!$primary_key) {
824 $this->getLogController()->logMessage("Skipping table $table_name - No primary key found.");
825 return;
826 }
827
828 $this->foreach($cols, function($i, $column) use ($table_name, $backup_domain, $local_domain, $primary_key) {
829 $this->getLogController()->logMessage("Checking column for table $table_name: $column");
830
831 // Select primary key and target column
832 $query = "SELECT `$primary_key`, `$column` FROM `$table_name` WHERE `$column` LIKE :old_domain";
833 $rows = $this->_mysql->query_exec($query, [':old_domain' => "%$backup_domain%"]);
834
835 if (empty($rows)) return;
836
837 foreach ($rows as $row) {
838 $original_data = $row->{$column};
839 $replaced_data = $this->_changeColumnsValue($original_data, $backup_domain, $local_domain);
840
841 if ($replaced_data !== $original_data) {
842 $update_query = "UPDATE `$table_name` SET `$column` = :new_value WHERE `$primary_key` = :primary_key";
843 $result = $this->_mysql->query_exec($update_query, [
844 ':new_value' => $replaced_data,
845 ':primary_key' => $row->{$primary_key}
846 ]);
847 if ($result === false) {
848 $this->getLogController()->logMessage("SQL GENERAL ERROR");
849 }
850 $this->getLogController()->logMessage("Updated column: $column in table: $table_name (Row ID: {$row->{$primary_key}})");
851
852 }
853 }
854 }, '_fixTableColumns_' . $table_name);
855 });
856 }
857
858 /**
859 * Get primary key of a table
860 */
861 private function _getTablePrimaryKey(string $table_name): ?string {
862
863 return $this->func(function($table_name){
864 $query = "SHOW KEYS FROM `$table_name` WHERE Key_name = 'PRIMARY'";
865 $result = $this->_mysql->query_exec($query);
866
867 return !empty($result) ? $result[0]->Column_name : null;
868 }, [$table_name], '_getTablePrimaryKey_' . $table_name);
869
870 }
871
872
873 /**
874 * @param $data
875 * @param $old_value
876 * @param $new_value
877 *
878 * @return array|mixed|string|string[]
879 */
880 private function _changeColumnsValue($data, $old_value, $new_value) {
881
882 $this->getLogController()->logDebug("Processing data of type: " . gettype($data));
883
884 switch(true) {
885 case is_numeric($data) || is_bool($data) || is_null($data) || $data instanceof \__PHP_Incomplete_Class:
886 return $data;
887
888 case is_array($data):
889 $this->getLogController()->logDebug("Data is array");
890 foreach ($data as $key => $value) $data[$key] = $this->_changeColumnsValue($value, $old_value, $new_value);
891 return $data;
892
893 case is_object($data):
894 $this->getLogController()->logDebug("Data is object");
895 foreach ($data as $key => $value) $data->{$key} = $this->_changeColumnsValue($value, $old_value, $new_value);
896 return $data;
897
898 case (($unserialized = @unserialize($data, ['allowed_classes' => false])) !== false || $data == 'b:0;'):
899 $this->getLogController()->logDebug("Data is serialized");
900 return serialize($this->_changeColumnsValue($unserialized, $old_value, $new_value));
901
902 case is_string($data):
903 $this->getLogController()->logDebug("Performing simple replacement from $old_value to $new_value");
904 $new_data = str_replace($old_value, $new_value, $data);
905 if ($new_data !== $data) $this->getLogController()->logDebug("Replacement successful. New data: " . substr($new_data, 0, 50) . "...");
906 else $this->getLogController()->logDebug("No replacement made in this string.");
907 return $new_data;
908 }
909
910 return $data;
911 }
912
913 /**
914 * @return void
915 * Specific plugin actions needed after restore
916 * At this point we assume WordPress is loaded and healthy
917 *
918 */
919 public function _postRestoreActions(): void {
920
921 $this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_POST_RESTORE_PLUGIN_ACTIONS);
922 $this->getQueueItem()->updateProgress('Post restore - Actions');
923 $this->getLogController()->logMessage('Post restore - Actions');
924 $actions = Factory::getSettingsIntegrations()->getInegrations();
925 if(!$actions) return;
926
927 $this->foreach($actions, function ($key, $action) {
928 $method = "JetBackup\\Integrations\\Vendors\\$action";
929 if(!class_exists($method)) return;
930 $this->getLogController()->logMessage("Doing $action post restore actions");
931 $instance = new $method();
932 $instance->execute();
933 }, '_postRestorePluginActions');
934
935 }
936
937 /**
938 * @return array
939 */
940 public function _fetchBackupTables(): array {
941
942 return $this->func(function() {
943
944 $backup_prefix = $this->_getDatabasePrefix();
945
946 return array_map(function($a) use ($backup_prefix) {
947 $a = (array) $a;
948 return substr(reset($a), strlen($backup_prefix));
949 }, $this->_mysql->query_exec("SHOW TABLES LIKE '$backup_prefix%'"));
950 }, [], '_fetchBackupTables');
951 }
952
953 /**
954 * @return array
955 */
956 public function _fetchTables(): array {
957 return $this->func(function() {
958 return $this->_mysql->query_exec("SHOW TABLES");
959 }, [], '_fetchTables');
960 }
961
962 /**
963 * @param $table_name
964 *
965 * @return array
966 */
967 public function _fetchTableColumns($table_name): array {
968 return $this->func(function() use ($table_name) {
969 $output = [];
970 $results = $this->_mysql->query_exec("SHOW COLUMNS FROM `$table_name`");
971 foreach ($results as $row) $output[] = $row->Field;
972 return $output;
973 }, [], '_fetchTableColumns_' . $table_name);
974 }
975
976 /**
977 * @param $table
978 *
979 * @return void
980 * @throws RestoreException
981 */
982 private function _dropForeignKeys($table) {
983 try {
984 // Drop foreign keys within the table
985 $sql = "SELECT CONSTRAINT_NAME
986 FROM information_schema.TABLE_CONSTRAINTS
987 WHERE CONSTRAINT_TYPE = 'FOREIGN KEY'
988 AND TABLE_SCHEMA = DATABASE()
989 AND TABLE_NAME = :table_name";
990 $constraints = $this->_mysql->query_exec($sql, [':table_name' => $table]);
991
992 foreach ($constraints as $constraint) {
993 $constraint_name = $constraint->CONSTRAINT_NAME;
994 $alter_query = "ALTER TABLE `$table` DROP FOREIGN KEY `$constraint_name`";
995 $this->getLogController()->logMessage("Dropping foreign key: $constraint_name from table: $table");
996 $this->_mysql->query_exec($alter_query);
997 }
998
999 // Drop foreign keys that reference the table from other tables
1000 $sql = "SELECT TABLE_NAME, CONSTRAINT_NAME
1001 FROM information_schema.KEY_COLUMN_USAGE
1002 WHERE REFERENCED_TABLE_SCHEMA = DATABASE()
1003 AND REFERENCED_TABLE_NAME = :table_name";
1004 $constraints = $this->_mysql->query_exec($sql, [':table_name' => $table]);
1005
1006 foreach ($constraints as $constraint) {
1007 $referencing_table = $constraint->TABLE_NAME;
1008 $constraint_name = $constraint->CONSTRAINT_NAME;
1009 $alter_query = "ALTER TABLE `$referencing_table` DROP FOREIGN KEY `$constraint_name`";
1010 $this->getLogController()->logMessage("Dropping foreign key: $constraint_name from table: `$referencing_table` that references `$table`");
1011 $this->_mysql->query_exec($alter_query);
1012 }
1013 } catch ( Exception $e) {
1014 $this->getLogController()->logError("Error dropping foreign keys for table $table: " . $e->getMessage());
1015 throw new RestoreException($e->getMessage());
1016 }
1017 }
1018
1019 /**
1020 * @param $table_name
1021 * @param $column_name
1022 * @param $id_column
1023 *
1024 * @return void
1025 * @throws Exception
1026 */
1027 private function _replacePrefixInTable($table_name, $column_name, $id_column) {
1028 $this->getLogController()->logMessage("Starting prefix replacement in $table_name.$column_name");
1029
1030 $backup_prefix = $this->_getDatabasePrefix();
1031 $mysql_auth = $this->_fetchMySQLAuth();
1032
1033 $select_query = "SELECT `$id_column`, `$column_name` FROM `$table_name` WHERE TRIM(`$column_name`) LIKE :old_prefix";
1034 $this->getLogController()->logMessage("Executing query: $select_query with old prefix: $backup_prefix");
1035 $this->getLogController()->logMessage("Running SQL: $select_query with :old_prefix => '$backup_prefix%'");
1036
1037 $rows = $this->_mysql->query_exec($select_query, [':old_prefix' => $backup_prefix . '%']);
1038
1039 $this->getLogController()->logMessage("Number of rows found: " . count($rows));
1040
1041 if (!$rows) {
1042 $this->getLogController()->logMessage("No rows found needing prefix replacement in $table_name.$column_name");
1043 return;
1044 }
1045
1046 foreach ($rows as $row) {
1047 $original_data = $row->{$column_name};
1048 $new_data = str_replace($backup_prefix, $mysql_auth->table_prefix, $original_data);
1049
1050 // Log the raw data
1051 $this->getLogController()->logMessage("Row data for $id_column: {$row->{$id_column}} => $original_data");
1052
1053 // Log the before and after values
1054 $this->getLogController()->logMessage("Original value in {$table_name}.{$column_name} for {$id_column}: {$row->{$id_column}} is: {$original_data}");
1055 $this->getLogController()->logMessage("New value after replacement in {$table_name}.{$column_name} for {$id_column}: {$row->{$id_column}} is: {$new_data}");
1056
1057 if ($new_data !== $original_data) {
1058 $update_query = "UPDATE `{$table_name}` SET `{$column_name}` = :new_value WHERE `{$id_column}` = :id";
1059 $this->getLogController()->logMessage("Updating {$table_name}.{$column_name} for {$id_column}: {$row->{$id_column}}");
1060 $this->getLogController()->logMessage("Executing update query: {$update_query} with new_value: {$new_data}, id: {$row->{$id_column}}");
1061
1062 $this->_mysql->query_exec($update_query, [
1063 ':new_value' => $new_data,
1064 ':id' => $row->{$id_column},
1065 ]);
1066 $this->getLogController()->logMessage("Updated {$table_name}.{$column_name} for {$id_column}: {$row->{$id_column}} successfully");
1067 } else {
1068 $this->getLogController()->logMessage("No change required for {$id_column}: {$row->{$id_column}}");
1069 }
1070 }
1071 }
1072
1073 public function _postRestoreHealthCheck():void {
1074
1075 $this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_POST_RESTORE_HEALTH_CHECK);
1076 $this->getQueueItem()->updateProgress('Post restore - Health Check');
1077 $this->getLogController()->logMessage('Post restore - Health Check');
1078 $this->func([$this, '_checkWPHealth']);
1079 $this->func([$this, '_checkDatabaseConnection']);
1080 $this->func([$this, '_checkConfig']);
1081 $this->func([$this, '_flushRewriteRules']);
1082 }
1083
1084
1085 public function _checkConfig() {
1086
1087 $this->getLogController()->logMessage('Testing wp-config file');
1088
1089 $wp_config_file = JetBackup::WP_ROOT_PATH . JetBackup::SEP . 'wp-config.php';
1090
1091 if (!file_exists($wp_config_file)) {
1092 $this->getLogController()->logError('wp-config.php is missing');
1093 return;
1094 }
1095
1096 $required_constants = ['DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_HOST'];
1097 foreach ($required_constants as $constant) {
1098 if (!defined($constant)) $this->getLogController()->logError("Constant $constant is not defined in wp-config.php");
1099 }
1100
1101 $this->getLogController()->logMessage('wp-config file OK');
1102 }
1103
1104 public function _flushRewriteRules() {
1105 if (!function_exists('flush_rewrite_rules')) return;
1106 if (function_exists('wp_cache_flush')) wp_cache_flush();
1107 $this->getLogController()->logMessage('Executing flush_rewrite_rules immediately');
1108 flush_rewrite_rules(true);
1109 }
1110
1111 public function _checkDatabaseConnection() {
1112 global $wpdb;
1113 $this->getLogController()->logMessage('Testing Database Connection');
1114
1115 try {
1116 if (!isset($wpdb) || empty($wpdb)) $this->_loadWP();
1117 $wpdb->get_results("SELECT 1");
1118 $this->getLogController()->logMessage('Database Connection OK');
1119 } catch (Exception $e) {
1120 $this->getLogController()->logError($e->getMessage());
1121 }
1122 }
1123
1124 /**
1125 * @return void
1126 *
1127 * Some hosts use symlinked mu-plugins pointing to shared system directories.
1128 * During migration, these links break, causing missing files and errors.
1129 *
1130 */
1131 public function _validateMUPlugins() {
1132
1133 $mu_plugins_path = self::MU_PLUGINS_PATH;
1134
1135 $this->getLogController()->logMessage("Validating MU Plugins folder: {$mu_plugins_path}");
1136 if (!is_dir($mu_plugins_path)) {
1137 $this->getLogController()->logMessage("MU Plugins folder does not exist: {$mu_plugins_path}, nothing to do...");
1138 return;
1139 }
1140
1141 $mu_plugins = glob($mu_plugins_path . JetBackup::SEP . '*.php');
1142 $broken_links = [];
1143
1144 if (empty($mu_plugins)) {
1145 $this->getLogController()->logMessage("No MU Plugins found.");
1146 return;
1147 }
1148
1149 foreach ($mu_plugins as $plugin) {
1150 if (is_link($plugin) && !file_exists($plugin)) $broken_links[] = $plugin;
1151 }
1152
1153 if (empty($broken_links)) {
1154 $this->getLogController()->logMessage("No Broken MU Plugins found, folder is valid.");
1155 return;
1156 }
1157
1158 // Remove broken links
1159 foreach ($broken_links as $link) {
1160 $this->getLogController()->logError("Broken MU Plugin found: {$link}");
1161 if (@unlink($link)) {
1162 $this->getLogController()->logMessage("Successfully removed: {$link}");
1163 } else {
1164 $this->getLogController()->logError("Failed to remove: {$link}");
1165 }
1166 }
1167
1168 $this->getLogController()->logDebug("Broken links removed: " . json_encode($broken_links));
1169
1170 }
1171
1172
1173
1174 public function _checkWPHealth() {
1175
1176 try {
1177 $this->_loadWP();
1178
1179 // Ensure WordPress is fully loaded by checking a key constant or function
1180 if (!function_exists('wp_get_current_user'))
1181 throw new Exception('WordPress did not fully load. A fatal error may have occurred.');
1182
1183 } catch(Exception $e) {
1184 $this->getLogController()->logError('Failed to load WordPress: ' . $e->getMessage());
1185
1186 $this->func(function() {
1187
1188 $this->getLogController()->logMessage('Scanning and disabling all active plugins');
1189
1190 $plugin_dirs = glob(self::PLUGINS_PATH . JetBackup::SEP . '*', GLOB_ONLYDIR);
1191
1192 $this->getLogController()->logDebug('Found plugin directories: ' . json_encode($plugin_dirs));
1193
1194 $this->foreach($plugin_dirs, function($i, $plugin_dir) {
1195 $this->_disablePlugin(basename($plugin_dir));
1196 }, '_disableAllPlugins');
1197
1198 }, [], '_disableAllPlugins');
1199
1200 $disabled_plugins = glob(self::PLUGINS_PATH . JetBackup::SEP . '*_disabled_' . $this->getQueueItem()->getUniqueId(), GLOB_ONLYDIR);
1201
1202 $this->foreach($disabled_plugins, function($i, $plugin_dir) {
1203
1204 $plugin_name = basename($plugin_dir);
1205
1206 $this->_enablePlugin($plugin_name);
1207
1208 try {
1209 $this->_loadWP();
1210 } catch(Exception $e) {
1211 $this->getLogController()->logError('Fatal error detected when enabling plugin: ' . $plugin_name . ' - ' . $e->getMessage());
1212 $this->_disablePlugin($plugin_name);
1213 }
1214
1215 }, '_checkPluginsOneByOne');
1216 }
1217 }
1218
1219 public function _enablePlugin($plugin_name):void {
1220
1221 $plugin_dir = self::PLUGINS_PATH . JetBackup::SEP . $plugin_name;
1222 $plugin_dir_disabled = $plugin_dir . '_disabled_' . $this->getQueueItem()->getUniqueId();
1223
1224 if(!file_exists($plugin_dir_disabled)) return;
1225
1226 $this->getLogController()->logMessage("Enabling plugin: $plugin_name");
1227
1228 if (!is_dir($plugin_dir_disabled)) {
1229 $this->getLogController()->logError("Failed to rename plugin directory $plugin_dir_disabled");
1230 return;
1231 }
1232
1233 rename($plugin_dir_disabled, $plugin_dir);
1234 $this->getLogController()->logMessage("Renamed plugin directory from $plugin_dir_disabled to $plugin_dir");
1235
1236 if (in_array($plugin_name, self::CACHE_PLUGINS)) {
1237
1238 $cache_file = JetBackup::WP_ROOT_PATH . JetBackup::SEP . 'wp-content' . JetBackup::SEP . 'object-cache.php';
1239 if (file_exists($cache_file)) {
1240 $this->getLogController()->logMessage("Renamed $cache_file to enable it");
1241 rename($cache_file . '_disabled_' . $this->getQueueItem()->getUniqueId(), $cache_file);
1242 }
1243 }
1244 }
1245
1246 // disable plugin from the db will not cover some fatal errors, we are forcing it by changing the name
1247 private function _disablePlugin($plugin_name):void {
1248
1249 $plugin_dir = self::PLUGINS_PATH . JetBackup::SEP . $plugin_name;
1250 $plugin_dir_disabled = $plugin_dir . '_disabled_' . $this->getQueueItem()->getUniqueId();
1251 $plugin_file = $plugin_dir . JetBackup::SEP . $plugin_name . '.php';
1252
1253 if(!file_exists($plugin_file)) return;
1254
1255 $this->getLogController()->logMessage("Disabling plugin: $plugin_name");
1256
1257 if(in_array($plugin_name, self::PROTECTED_PLUGINS)) {
1258 $this->getLogController()->logMessage("Skipped disabling protected plugin: $plugin_name");
1259 return;
1260 }
1261
1262 if(file_exists($plugin_dir_disabled)) rename($plugin_dir_disabled, $plugin_dir_disabled . '_' . Util::generateRandomString());
1263
1264 if (!is_dir($plugin_dir)) {
1265 $this->getLogController()->logError("Failed to rename plugin directory $plugin_dir");
1266 return;
1267 }
1268
1269 rename($plugin_dir, $plugin_dir_disabled);
1270 $this->getLogController()->logMessage("Renamed plugin directory from $plugin_dir to $plugin_dir_disabled");
1271
1272 if (in_array($plugin_name, self::CACHE_PLUGINS)) {
1273 $this->getLogController()->logMessage("Problematic plugin is object cache, disabling 'object-cache.php' file");
1274
1275 $cache_file = JetBackup::WP_ROOT_PATH . JetBackup::SEP . 'wp-content' . JetBackup::SEP . 'object-cache.php';
1276 if (file_exists($cache_file)) {
1277 $this->getLogController()->logMessage("Renamed $cache_file to disable it");
1278 rename($cache_file, $cache_file . '_disabled_' . $this->getQueueItem()->getUniqueId());
1279 }
1280 }
1281
1282 //$this->_disabled_plugins[] = $plugin_name;
1283 }
1284
1285 /**
1286 * @return void
1287 * @throws Exception
1288 */
1289 private function _loadWP():void {
1290 ob_start(); // Start output buffering
1291
1292 $load_file = JetBackup::WP_ROOT_PATH . JetBackup::SEP . 'wp-load.php';
1293
1294 try {
1295
1296 if (file_exists($load_file)) {
1297 require_once($load_file);
1298
1299 // Temporary disable cron events
1300 if(!defined('DISABLE_WP_CRON')) define('DISABLE_WP_CRON', true);
1301
1302 $output = ob_get_clean(); // Get buffered output
1303
1304 // Log the output, even if it seems normal
1305 if (!empty($output)) $this->getLogController()->logMessage('Output from WordPress load: ' . $output);
1306 $this->getLogController()->logMessage('WordPress ecosystem loaded successfully');
1307
1308 } else {
1309 throw new Exception('wp-load.php not found in the specified WordPress root.');
1310 }
1311
1312 } catch (\Throwable $e) {
1313 $output = ob_get_clean(); // Clean the buffer in case of an error
1314 $this->getLogController()->logError('Failed loading WordPress: ' . $e->getMessage() . ' | Buffered Output: ' . $output);
1315 throw new Exception('Failed loading WordPress: ' . $e->getMessage());
1316 }
1317 }
1318 }