File: /home/mobilech/public_html/wp-content/plugins/backup/src/JetBackup/Cron/Task/PreRestore.php
<?php
namespace JetBackup\Cron\Task;
use Exception;
use JetBackup\Archive\Archive;
use JetBackup\BackupJob\BackupJob;
use JetBackup\Data\Engine;
use JetBackup\Destination\Destination;
use JetBackup\Encryption\Crypt;
use JetBackup\Entities\Util;
use JetBackup\Exception\DBException;
use JetBackup\Exception\DownloaderException;
use JetBackup\Exception\ExecutionTimeException;
use JetBackup\Exception\RestoreException;
use JetBackup\Exception\SGBExtractorException;
use JetBackup\Exception\SnapshotMetaException;
use JetBackup\Exception\TaskException;
use JetBackup\Factory;
use JetBackup\JetBackup;
use JetBackup\License\License;
use JetBackup\Queue\Queue;
use JetBackup\Queue\QueueItem;
use JetBackup\Queue\QueueItemRestore;
use JetBackup\ResumableTask\ResumableTask;
use JetBackup\SGB\Extractor;
use JetBackup\Snapshot\Snapshot;
use JetBackup\Snapshot\SnapshotDownload;
use JetBackup\Wordpress\Helper;
use JetBackup\Wordpress\Wordpress;
use SleekDB\Exceptions\InvalidArgumentException;
use SleekDB\Exceptions\IOException;
if (!defined( '__JETBACKUP__')) die('Direct access is not allowed');
class PreRestore extends Task {
const LOG_FILENAME = 'pre_restore';
const RESTORE_FILE_NAME = 'jetbackup.restore';
private QueueItemRestore $_queue_item_restore;
public ?Snapshot $_snapshot=null;
public function __construct() {
parent::__construct(self::LOG_FILENAME);
}
/**
* @return void
* @throws DBException
* @throws IOException
* @throws InvalidArgumentException
* @throws TaskException
*/
public function execute():void {
parent::execute();
if($this->getQueueItem()->getStatus() >= Queue::STATUS_RESTORE_WAITING_FOR_RESTORE) {
// If external restore hasn't initiated within 24 hours we need to abort the restore process
if($this->getQueueItem()->getStatusTime() < (time() - (60 * 60 * 24))) {
$this->getLogController()->logError("External restore hasn't initiated within the last 24 hours, Aborting the restore");
$this->getQueueItem()->updateStatus(Queue::STATUS_ABORTED);
$this->getQueueItem()->updateProgress('Restore Aborted!', QueueItem::PROGRESS_LAST_STEP);
} else {
$this->getLogController()->logMessage('Waiting for external restore');
}
return;
}
$this->_queue_item_restore = $this->getQueueItem()->getItemData();
if($this->_queue_item_restore->getSnapshotId()) {
$snapshot = new Snapshot($this->_queue_item_restore->getSnapshotId());
$destination = new Destination($snapshot->getDestinationId());
if(!License::isValid() &&
!in_array($destination->getType(), Destination::LICENSE_EXCLUDED) &&
$snapshot->getEngine() != Engine::ENGINE_JB) {
$this->getLogController()->logError("You can't restore from {$destination->getType()} destination without a license");
$this->getQueueItem()->updateStatus(Queue::STATUS_ABORTED);
$this->getQueueItem()->updateProgress('Restore Aborted!', QueueItem::PROGRESS_LAST_STEP);
return;
}
}
if($this->getQueueItem()->getStatus() == Queue::STATUS_PENDING) {
$this->getLogController()->logMessage("Starting restore");
$this->getQueueItem()->getProgress()->setTotalItems( count(Queue::STATUS_PRE_RESTORE_NAMES) + 3);
$this->getQueueItem()->save();
$this->getQueueItem()->updateProgress('Starting restore');
} else if($this->getQueueItem()->getStatus() > Queue::STATUS_PENDING) {
$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));
$this->_snapshot = $this->func([$this, '_download']);
$this->func([$this, '_extract']);
$this->func([$this, '_build_url']);
if($this->getQueueItem()->getStatus() < Queue::STATUS_DONE) $this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_WAITING_FOR_RESTORE);
$this->getLogController()->logMessage('Completed!');
} 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_RESTORE_WAITING_FOR_RESTORE ? 'Pre Restore Completed!' : 'Pre Restore Failed!', QueueItem::PROGRESS_LAST_STEP);
$this->getLogController()->logMessage('Total time: ' . $this->getExecutionTimeElapsed());
}
public static function findPublicRestoreFiles() : array {
$basePath = Factory::getWPHelper()->getRestoreFileLocation() . JetBackup::SEP . self::RESTORE_FILE_NAME . '.*';
if (defined('GLOB_BRACE')) return glob($basePath . '.{php,php.lock}', \GLOB_BRACE);
$phpFiles = glob($basePath . '.php');
$phpLockFiles = glob($basePath . '.php.lock');
return array_merge($phpFiles ?: [], $phpLockFiles ?: []);
}
/**
* @return void
* @throws IOException
* @throws InvalidArgumentException
* @throws RestoreException
*/
public function _build_url() {
$this->getLogController()->logMessage('[ _build_url ]');
$this->getLogController()->logMessage('Execution time: ' . $this->getExecutionTimeElapsed());
$this->getLogController()->logMessage('TTL time: ' . $this->getExecutionTimeLimit());
$this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_BUILD_URL);
$this->getQueueItem()->updateProgress('Building restore URL');
$template_file = JetBackup::SRC_PATH . JetBackup::SEP . 'Restore' . JetBackup::SEP . 'restore.template.php';
if(!file_exists($template_file)) throw new RestoreException("Restore template file not found");
$public_dir = rtrim(Factory::getWPHelper()->getWordPressHomedir(), JetBackup::SEP);
foreach (self::findPublicRestoreFiles() as $file) @unlink($file);
// Capture runtime DB credentials for cloud environments where wp-config.php
// doesn't contain literal credentials (e.g., WordPress.com, WP Cloud, Porkbun)
$runtime_credentials = json_encode([
'db_name' => defined('DB_NAME') ? DB_NAME : '',
'db_user' => defined('DB_USER') ? DB_USER : '',
'db_password' => defined('DB_PASSWORD') ? DB_PASSWORD : '',
'db_host' => defined('DB_HOST') ? DB_HOST : '',
'table_prefix' => $GLOBALS['table_prefix'] ?? 'wp_',
]);
$encrypted_creds = Crypt::encrypt($runtime_credentials, $this->getQueueItem()->getUniqueId());
$content = "<?php define('__JETBACKUP_RESTORE__', true); ?>\n";
$content .= "<?php define('WP_ROOT', '$public_dir'); ?>\n";
$content .= "<?php define('PUBLIC_PATH', '" . (Factory::getSettingsRestore()->isRestoreAlternatePathEnabled() ? str_repeat('../', substr_count(JetBackup::CRON_PUBLIC_URL, '/')) : '') . "'); ?>\n";
$content .= "<?php define('JB_RUNTIME_CREDENTIALS', '$encrypted_creds'); ?>\n";
$content .= file_get_contents($template_file);
$restore_file_name = self::RESTORE_FILE_NAME . '.' . Util::generateRandomString(24) . '.php';
$restore_file = Factory::getWPHelper()->getRestoreFileLocation() . JetBackup::SEP . $restore_file_name;
if(!file_put_contents($restore_file, $content)) throw new RestoreException("Failed creating restore file $restore_file");
// Dev Remark
//symlink('wp-content/plugins/backup/src/JetBackup/Restore/restore.template.php', $restore_file);
$alternate_path = Factory::getSettingsRestore()->isRestoreAlternatePathEnabled() ? JetBackup::CRON_PUBLIC_URL : '';
$url = Wordpress::getSiteURL() . $alternate_path . '/' . $restore_file_name . '?id=' . $this->getQueueItem()->getUniqueId();
$this->getLogController()->logMessage('Restore URL: ' . $url);
$this->_queue_item_restore->setRestoreURL($url);
$this->getQueueItem()->save();
}
/**
* @return void
* @throws DBException
* @throws DownloaderException
* @throws ExecutionTimeException
* @throws IOException
* @throws InvalidArgumentException
*/
public function _extract() {
if($this->_snapshot->getEngine() == Engine::ENGINE_JB) return;
$this->getLogController()->logMessage('[ _extract ]');
$this->getLogController()->logMessage('Execution time: ' . $this->getExecutionTimeElapsed());
$this->getLogController()->logMessage('TTL time: ' . $this->getExecutionTimeLimit());
$this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_EXTRACT);
$this->getQueueItem()->updateProgress('Extracting backup items');
if($this->_snapshot->getEngine() == Engine::ENGINE_SGB) {
// SGB snapshot has only 1 item
$item = $this->_snapshot->getItems()[0];
$path = $this->getQueueItem()->getWorkspace() . JetBackup::SEP . $item->getPath();
try {
$extractor = new Extractor($path, $this->getQueueItem()->getWorkspace());
$extractor->setLogController($this->getLogController());
$extractor->extract(function() {
$this->checkExecutionTime();
});
} catch(SGBExtractorException $e) {
throw new DownloaderException($e->getMessage());
}
unlink($path);
} else {
$this->func(function () {
$this->_snapshot->extract($this->getQueueItem()->getWorkspace(), $this->getLogController(), function(string $type, string $action, int $total, int $read) {
$progress = $this->getQueueItem()->getProgress();
$progress->setMessage($type); // gzip / archive
$progress->setSubMessage($action); // decompress / extract
$progress->setTotalSubItems($total);
$progress->setCurrentSubItem($read);
$this->getQueueItem()->save();
// Call checkExecutionTime, passing the desired variables
$this->checkExecutionTime(function () use ($type, $action, $total, $read) {
$progress = $this->getQueueItem()->getProgress();
$progress->setMessage($type);
$progress->setSubMessage('Waiting for next cron');
$progress->setTotalSubItems($total);
$progress->setCurrentSubItem($read);
$this->getQueueItem()->save();
});
}, $this->_queue_item_restore->getExcludes(), $this->_queue_item_restore->getIncludes());
}, [], 'snapshot_extract');
// done extract, reset sub process bar
$this->getQueueItem()->getProgress()->resetSub();
$this->getQueueItem()->save();
}
}
/**
* @return void
* @throws RestoreException
* @throws DBException
* @throws IOException
* @throws InvalidArgumentException
* @throws Exception
*/
public function _download():Snapshot {
$this->getLogController()->logMessage('[ _download ]');
$this->getLogController()->logMessage('Execution time: ' . $this->getExecutionTimeElapsed());
$this->getLogController()->logMessage('TTL time: ' . $this->getExecutionTimeLimit());
$this->getQueueItem()->updateStatus(Queue::STATUS_RESTORE_DOWNLOAD);
if($this->_queue_item_restore->getSnapshotId()) {
$this->getLogController()->logMessage('[ _download ] Downloading backup');
$this->getQueueItem()->updateProgress('Downloading backup');
$snapshot = new Snapshot($this->_queue_item_restore->getSnapshotId());
$this->getLogController()->logDebug('[ _download ] getOptions bitwise value: ' . $this->_queue_item_restore->getOptions());
$this->getLogController()->logDebug('[ _download ] getOptions decimal value: ' . decbin($this->_queue_item_restore->getOptions()));
if($snapshot->getEngine() == Engine::ENGINE_JB) return $snapshot;
$items_excluded = [];
if (($this->_queue_item_restore->getOptions() & QueueItemRestore::OPTION_RESTORE_FILES_SKIP)) {
$this->getLogController()->logDebug('[ _download ] Skipping Homedir');
$items_excluded[] = BackupJob::BACKUP_ACCOUNT_CONTAINS_HOMEDIR;
}
if (($this->_queue_item_restore->getOptions() & QueueItemRestore::OPTION_RESTORE_DATABASE_SKIP)) {
$this->getLogController()->logDebug('[ _download ] Skipping Database');
$items_excluded[] = BackupJob::BACKUP_ACCOUNT_CONTAINS_DATABASE;
}
$this->getLogController()->logDebug('[ _download ] Excluded items: ' . print_r($items_excluded, true));
$download = new SnapshotDownload($snapshot, $this->getQueueItem()->getWorkspace());
$download->setLogController($this->getLogController());
$download->setQueueItem($this->getQueueItem());
$download->setTask($this);
$download->setExcludedItems($items_excluded);
$download->setExcludedDatabases($this->_queue_item_restore->getExcludedDatabases());
$download->setIncludedDatabases($this->_queue_item_restore->getIncludedDatabases());
$download->downloadAll();
// done downloading, reset sub process bar
$this->getQueueItem()->getProgress()->resetSub();
$this->getQueueItem()->save();
} elseif($this->_queue_item_restore->getSnapshotPath()) {
$this->getLogController()->logMessage('[ _download ] Extracting backup from path');
$this->getQueueItem()->updateProgress('Extracting backup from path');
$snapshot = $this->_extractBackupFile();
}
return $snapshot;
}
/**
* @throws RestoreException
* @throws Exception
*/
private function _extractBackupFile():Snapshot {
$path = $this->_queue_item_restore->getSnapshotPath();
if(!file_exists($path)) throw new RestoreException("The provided backup path doesn't exists");
if(Archive::isGzCompressed($path)) {
$this->getLogController()->logMessage('[ _extractBackupFile ] Decompressing GZIP');
$this->func(['\JetBackup\Archive\Gzip', 'decompress'], [$path, ResumableTask::PARAMS_EXECUTION_TIME]);
$path = substr($path, 0, -3); // remove .gz from name
}
if(!Archive::isTar($path)) throw new RestoreException("Invalid backup file provided, Should be tar.gz/tar.gz file");
$this->func(function($path) {
$archive = new Archive($path);
$archive->setExtractFileCallback(function($type,$action,$total,$read) {
$progress = $this->getQueueItem()->getProgress();
$progress->setMessage($type);
$progress->setSubMessage($action);
$progress->setTotalSubItems($total);
$progress->setCurrentSubItem($read);
$this->getQueueItem()->save();
$this->checkExecutionTime(function () use ($type, $action, $total, $read) {
$progress = $this->getQueueItem()->getProgress();
$progress->setMessage($type);
$progress->setSubMessage('Waiting for next cron');
$progress->setTotalSubItems($total);
$progress->setCurrentSubItem($read);
$this->getQueueItem()->save();
});
});
$archive->setExcludeCallback(function($path, $is_dir) {
$excludes = $this->_queue_item_restore->getExcludes();
foreach($excludes as $exclude) if(fnmatch($exclude, $path) || ($is_dir && str_ends_with($exclude, '/') && fnmatch(substr($exclude, 0, -1), $path))) return true;
return false;
});
$archive->setLogController($this->getLogController());
$archive->extract($this->getQueueItem()->getWorkspace());
unlink($path);
}, [$path], 'extract');
// done extract, reset sub process bar
$this->getQueueItem()->getProgress()->resetSub();
$this->getQueueItem()->save();
$meta_file = sprintf(Snapshot::META_FILEPATH, $this->getQueueItem()->getWorkspace());
if(!file_exists($meta_file)) throw new RestoreException("The provided backup doesn't contains meta file");
$this->getLogController()->logDebug("[ _extractBackupFile ] Importing meta file $meta_file");
$snapshot = new Snapshot();
try {
$snapshot->importMeta($meta_file, true);
} catch(SnapshotMetaException $e) {
throw new RestoreException("Can't use the provided backup file. Error: " . $e->getMessage());
}
return $snapshot;
}
}