64ffbb9d61
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 10s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 39s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 18s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 4m10s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
#100: Run Backup button on profiles list (per-row) and edit toolbar, backup count badge linking to filtered backups view, View Backups toolbar button on profile edit. #101: Profile → filtered backup list link (included in #100). #104: Snapshot browse modal now shows tabbed view (Articles, Categories, Modules) with item counts. AjaxController returns all content types. Categories show indented hierarchy. #108: "Do not navigate away or close this window" warning banner added to both backup and restore progress modals. #110: Joomla Action Logs integration — RestoreEngine, SnapshotEngine, and SnapshotRestoreEngine now dispatch events that the actionlog plugin logs to #__action_logs. Closes #100, closes #101, closes #104, closes #108, closes #110
866 lines
34 KiB
PHP
866 lines
34 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoSuiteBackup
|
|
* @subpackage com_mokosuitebackup
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*/
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\HTML\HTMLHelper;
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\Layout\LayoutHelper;
|
|
use Joomla\CMS\Router\Route;
|
|
use Joomla\CMS\Session\Session;
|
|
|
|
HTMLHelper::_('behavior.multiselect');
|
|
$user = Factory::getApplication()->getIdentity();
|
|
$canDownload = $user->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup');
|
|
|
|
$ajaxToken = Session::getFormToken();
|
|
$ajaxUrl = Route::_('index.php?option=com_mokosuitebackup&format=json', false);
|
|
|
|
$listOrder = $this->escape($this->state->get('list.ordering'));
|
|
$listDirn = $this->escape($this->state->get('list.direction'));
|
|
?>
|
|
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=backups'); ?>" method="post" name="adminForm" id="adminForm">
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<div id="j-main-container" class="j-main-container">
|
|
<!-- Profile selector for Backup Now -->
|
|
<?php $canRun = $user->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup'); ?>
|
|
<?php if (!empty($this->profiles) && $canRun) : ?>
|
|
<div class="card mb-3">
|
|
<div class="card-body d-flex align-items-center gap-3">
|
|
<label for="mb-profile-select" class="form-label mb-0 fw-bold">
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_BACKUP_PROFILE'); ?>:
|
|
</label>
|
|
<select id="mb-profile-select" class="form-select" style="max-width:300px;">
|
|
<?php foreach ($this->profiles as $profile) : ?>
|
|
<option value="<?php echo (int) $profile->id; ?>">
|
|
<?php echo $this->escape($profile->title); ?>
|
|
(<?php echo $this->escape($profile->backup_type); ?>)
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<button type="button" class="btn btn-primary" onclick="window.mokosuitebackupStart()">
|
|
<span class="icon-download" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW'); ?>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?>
|
|
|
|
<?php if (empty($this->items)) : ?>
|
|
<div class="alert alert-info">
|
|
<span class="icon-info-circle" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_NO_BACKUPS'); ?>
|
|
</div>
|
|
<?php else : ?>
|
|
<table class="table" id="backupList">
|
|
<caption class="visually-hidden"><?php echo Text::_('COM_MOKOJOOMBACKUP_BACKUPS_TABLE_CAPTION'); ?></caption>
|
|
<thead>
|
|
<tr>
|
|
<td class="w-1 text-center">
|
|
<?php echo HTMLHelper::_('grid.checkall'); ?>
|
|
</td>
|
|
<th scope="col">
|
|
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_DESCRIPTION', 'a.description', $listDirn, $listOrder); ?>
|
|
</th>
|
|
<th scope="col" class="w-10">
|
|
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_PROFILE', 'a.profile_id', $listDirn, $listOrder); ?>
|
|
</th>
|
|
<th scope="col" class="w-10">
|
|
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_STATUS', 'a.status', $listDirn, $listOrder); ?>
|
|
</th>
|
|
<th scope="col" class="w-10">
|
|
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_TYPE', 'a.backup_type', $listDirn, $listOrder); ?>
|
|
</th>
|
|
<th scope="col" class="w-10">
|
|
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_SIZE', 'a.total_size', $listDirn, $listOrder); ?>
|
|
</th>
|
|
<th scope="col" class="w-10">
|
|
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_DATE', 'a.backupstart', $listDirn, $listOrder); ?>
|
|
</th>
|
|
<th scope="col" class="w-5">
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_ACTIONS'); ?>
|
|
</th>
|
|
<th scope="col" class="w-5">
|
|
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($this->items as $i => $item) : ?>
|
|
<tr>
|
|
<td class="text-center">
|
|
<?php echo HTMLHelper::_('grid.id', $i, $item->id); ?>
|
|
</td>
|
|
<td>
|
|
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=backup&id=' . $item->id); ?>">
|
|
<?php echo $this->escape($item->description); ?>
|
|
</a>
|
|
<?php if (!empty($item->checksum)) : ?>
|
|
<br><small class="text-muted font-monospace"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_CHECKSUM'); ?>: <?php echo substr($item->checksum, 0, 16); ?>...</small>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<?php echo $this->escape($item->profile_title ?? 'Profile #' . $item->profile_id); ?>
|
|
</td>
|
|
<td>
|
|
<?php
|
|
$statusClass = match ($item->status) {
|
|
'complete' => 'badge bg-success',
|
|
'running' => 'badge bg-info',
|
|
'fail' => 'badge bg-danger',
|
|
default => 'badge bg-secondary',
|
|
};
|
|
?>
|
|
<span class="<?php echo $statusClass; ?>"><?php echo $this->escape($item->status); ?></span>
|
|
</td>
|
|
<td>
|
|
<?php echo $this->escape($item->backup_type); ?>
|
|
</td>
|
|
<td>
|
|
<?php
|
|
if ($item->total_size > 0) {
|
|
echo HTMLHelper::_('number.bytes', $item->total_size);
|
|
} else {
|
|
echo '—';
|
|
}
|
|
?>
|
|
</td>
|
|
<td>
|
|
<?php echo HTMLHelper::_('date', $item->backupstart, Text::_('DATE_FORMAT_LC4')); ?>
|
|
</td>
|
|
<td class="d-flex gap-1">
|
|
<?php if ($item->status === 'complete' && $item->filesexist && $canDownload) : ?>
|
|
<?php
|
|
$isWebAccessible = !empty($item->absolute_path)
|
|
&& strpos(realpath($item->absolute_path) ?: $item->absolute_path, realpath(JPATH_ROOT) ?: JPATH_ROOT) === 0;
|
|
?>
|
|
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.download&id=' . $item->id . '&' . Session::getFormToken() . '=1'); ?>"
|
|
class="btn btn-sm btn-outline-primary" title="<?php echo Text::_('COM_MOKOJOOMBACKUP_DOWNLOAD'); ?>">
|
|
<span class="icon-download"></span>
|
|
</a>
|
|
<?php if ($isWebAccessible) : ?>
|
|
<span class="badge bg-warning text-dark" title="<?php echo Text::_('COM_MOKOJOOMBACKUP_WEB_ACCESSIBLE_WARNING'); ?>">
|
|
<span class="icon-warning-circle" aria-hidden="true"></span>
|
|
</span>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
<?php if ($item->status === 'complete' && $item->filesexist) : ?>
|
|
<button type="button" class="btn btn-sm btn-outline-info mb-browse-archive"
|
|
data-id="<?php echo (int) $item->id; ?>"
|
|
title="<?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_ARCHIVE'); ?>">
|
|
<span class="icon-folder-open"></span>
|
|
</button>
|
|
<?php endif; ?>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary mb-view-log"
|
|
data-id="<?php echo (int) $item->id; ?>"
|
|
title="<?php echo Text::_('COM_MOKOJOOMBACKUP_VIEW_LOG'); ?>">
|
|
<span class="icon-file-alt"></span>
|
|
</button>
|
|
</td>
|
|
<td>
|
|
<?php echo (int) $item->id; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
|
|
<?php echo $this->pagination->getListFooter(); ?>
|
|
<?php endif; ?>
|
|
|
|
<input type="hidden" name="task" value="">
|
|
<input type="hidden" name="boxchecked" value="0">
|
|
<?php echo HTMLHelper::_('form.token'); ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Stepped Backup Modal (for shared hosting) -->
|
|
<div id="mokosuitebackup-modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); z-index:10000;">
|
|
<div style="max-width:500px; margin:10% auto; background:#fff; border-radius:8px; padding:2rem; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
|
|
<h3 id="mb-modal-title" style="margin:0 0 1rem;">Backup in Progress</h3>
|
|
<div class="alert alert-warning py-1 px-2 mb-2" style="font-size:0.85rem;">
|
|
<span class="icon-warning-circle" aria-hidden="true"></span>
|
|
<strong>Do not navigate away or close this window</strong> while the backup is running.
|
|
</div>
|
|
<div style="background:#e9ecef; border-radius:4px; overflow:hidden; height:24px; margin-bottom:0.5rem;">
|
|
<div id="mb-progress-bar" style="height:100%; background:#0d6efd; transition:width 0.3s; width:0%; display:flex; align-items:center; justify-content:center; color:#fff; font-size:0.8rem; font-weight:bold;">0%</div>
|
|
</div>
|
|
<p id="mb-status" style="color:#666; font-size:0.9rem; margin:0.5rem 0;">Initializing...</p>
|
|
<p id="mb-phase" style="color:#999; font-size:0.8rem; margin:0;">Phase: init</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
const AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
|
|
const TOKEN_NAME = <?php echo json_encode($ajaxToken); ?>;
|
|
|
|
// Override the toolbar "Backup Now" button to use stepped backup
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Find the backup toolbar button and override it
|
|
const toolbarBtn = document.querySelector('[onclick*="backups.start"], .button-download');
|
|
if (toolbarBtn) {
|
|
toolbarBtn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
startSteppedBackup();
|
|
return false;
|
|
}, true);
|
|
}
|
|
});
|
|
|
|
var backupRunning = false;
|
|
|
|
function warnBeforeClose(e) {
|
|
if (backupRunning) {
|
|
e.preventDefault();
|
|
e.returnValue = '';
|
|
}
|
|
}
|
|
|
|
window.addEventListener('beforeunload', warnBeforeClose);
|
|
|
|
function showModal() {
|
|
backupRunning = true;
|
|
document.getElementById('mokosuitebackup-modal').style.display = 'block';
|
|
}
|
|
|
|
function hideModal() {
|
|
backupRunning = false;
|
|
document.getElementById('mokosuitebackup-modal').style.display = 'none';
|
|
}
|
|
|
|
function updateProgress(progress, message, phase) {
|
|
const bar = document.getElementById('mb-progress-bar');
|
|
bar.style.width = progress + '%';
|
|
bar.textContent = progress + '%';
|
|
document.getElementById('mb-status').textContent = message;
|
|
document.getElementById('mb-phase').textContent = 'Phase: ' + phase;
|
|
}
|
|
|
|
async function postAjax(params) {
|
|
const form = new URLSearchParams();
|
|
form.append(TOKEN_NAME, '1');
|
|
for (const [k, v] of Object.entries(params)) {
|
|
form.append(k, v);
|
|
}
|
|
const res = await fetch(AJAX_URL, {
|
|
method: 'POST',
|
|
body: form,
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
});
|
|
return res.json();
|
|
}
|
|
|
|
async function startSteppedBackup() {
|
|
const profileSelect = document.getElementById('mb-profile-select');
|
|
const profileId = profileSelect ? profileSelect.value : '1';
|
|
|
|
showModal();
|
|
updateProgress(0, 'Initializing backup...', 'init');
|
|
|
|
try {
|
|
// Init
|
|
const initResult = await postAjax({
|
|
task: 'ajax.init',
|
|
profile_id: profileId
|
|
});
|
|
|
|
if (initResult.error) {
|
|
updateProgress(0, 'ERROR: ' + initResult.message, 'failed');
|
|
setTimeout(hideModal, 5000);
|
|
return;
|
|
}
|
|
|
|
// Show preflight warnings if any
|
|
if (initResult.warnings && initResult.warnings.length > 0) {
|
|
var warningEl = document.getElementById('mb-phase');
|
|
warningEl.textContent = 'Warnings: ' + initResult.warnings.join('; ');
|
|
warningEl.style.color = '#856404';
|
|
}
|
|
|
|
const sessionId = initResult.session_id;
|
|
updateProgress(initResult.progress, initResult.message, initResult.phase);
|
|
|
|
// Run steps until done
|
|
let done = false;
|
|
while (!done) {
|
|
const stepResult = await postAjax({
|
|
task: 'ajax.step',
|
|
session_id: sessionId
|
|
});
|
|
|
|
if (stepResult.error) {
|
|
updateProgress(0, 'ERROR: ' + stepResult.message, 'failed');
|
|
setTimeout(hideModal, 5000);
|
|
return;
|
|
}
|
|
|
|
updateProgress(stepResult.progress, stepResult.message, stepResult.phase);
|
|
done = stepResult.done || false;
|
|
}
|
|
|
|
// Complete
|
|
document.getElementById('mb-modal-title').textContent = 'Backup Complete';
|
|
setTimeout(function() {
|
|
hideModal();
|
|
location.reload();
|
|
}, 2000);
|
|
|
|
} catch (err) {
|
|
updateProgress(0, 'ERROR: ' + err.message, 'failed');
|
|
setTimeout(hideModal, 5000);
|
|
}
|
|
}
|
|
|
|
// Expose for toolbar button
|
|
window.mokosuitebackupStart = startSteppedBackup;
|
|
|
|
// Intercept Restore toolbar button to show the modal
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var restoreBtn = document.querySelector('[onclick*="backups.restore"], .button-upload');
|
|
if (restoreBtn) {
|
|
restoreBtn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Get selected record from checkboxes
|
|
var checked = document.querySelectorAll('input[name="cid[]"]:checked');
|
|
if (checked.length === 0) {
|
|
alert('<?php echo Text::_('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED', true); ?>');
|
|
return false;
|
|
}
|
|
document.getElementById('mb-restore-record-id').value = checked[0].value;
|
|
document.getElementById('mb-restore-modal').style.display = 'block';
|
|
return false;
|
|
}, true);
|
|
}
|
|
});
|
|
|
|
// Close restore modal
|
|
document.addEventListener('click', function(e) {
|
|
if (e.target.classList.contains('mb-restore-close') || e.target.id === 'mb-restore-modal') {
|
|
document.getElementById('mb-restore-modal').style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// AJAX stepped restore
|
|
var restoreRunning = false;
|
|
|
|
function showRestoreProgress() {
|
|
restoreRunning = true;
|
|
document.getElementById('mb-restore-modal').style.display = 'none';
|
|
document.getElementById('mb-restore-progress-modal').style.display = 'block';
|
|
}
|
|
|
|
function hideRestoreProgress() {
|
|
restoreRunning = false;
|
|
document.getElementById('mb-restore-progress-modal').style.display = 'none';
|
|
}
|
|
|
|
function updateRestoreProgress(progress, message, phase) {
|
|
var bar = document.getElementById('mb-restore-progress-bar');
|
|
bar.style.width = progress + '%';
|
|
bar.textContent = progress + '%';
|
|
document.getElementById('mb-restore-status').textContent = message;
|
|
document.getElementById('mb-restore-phase').textContent = 'Phase: ' + phase;
|
|
}
|
|
|
|
window.addEventListener('beforeunload', function(e) {
|
|
if (restoreRunning) {
|
|
e.preventDefault();
|
|
e.returnValue = '';
|
|
}
|
|
});
|
|
|
|
async function startSteppedRestore(e) {
|
|
e.preventDefault();
|
|
|
|
var recordId = document.getElementById('mb-restore-record-id').value;
|
|
var restoreFiles = document.getElementById('mb-restore-files').checked ? 1 : 0;
|
|
var restoreDb = document.getElementById('mb-restore-db').checked ? 1 : 0;
|
|
var preserveConfig = document.getElementById('mb-restore-config').checked ? 1 : 0;
|
|
var password = document.getElementById('mb-restore-password').value;
|
|
|
|
showRestoreProgress();
|
|
updateRestoreProgress(0, 'Initializing restore...', 'init');
|
|
|
|
try {
|
|
var initResult = await postAjax({
|
|
task: 'ajax.restoreInit',
|
|
id: recordId,
|
|
restore_files: restoreFiles,
|
|
restore_db: restoreDb,
|
|
preserve_config: preserveConfig,
|
|
encryption_password: password
|
|
});
|
|
|
|
if (initResult.error) {
|
|
updateRestoreProgress(0, 'ERROR: ' + initResult.message, 'failed');
|
|
document.getElementById('mb-restore-title').textContent = 'Restore Failed';
|
|
setTimeout(hideRestoreProgress, 5000);
|
|
return;
|
|
}
|
|
|
|
var sessionId = initResult.session_id;
|
|
updateRestoreProgress(initResult.progress, initResult.message, initResult.phase);
|
|
|
|
var done = false;
|
|
while (!done) {
|
|
var stepResult = await postAjax({
|
|
task: 'ajax.restoreStep',
|
|
session_id: sessionId
|
|
});
|
|
|
|
if (stepResult.error) {
|
|
updateRestoreProgress(0, 'ERROR: ' + stepResult.message, 'failed');
|
|
document.getElementById('mb-restore-title').textContent = 'Restore Failed';
|
|
setTimeout(hideRestoreProgress, 5000);
|
|
return;
|
|
}
|
|
|
|
updateRestoreProgress(stepResult.progress, stepResult.message, stepResult.phase);
|
|
done = stepResult.done || false;
|
|
}
|
|
|
|
document.getElementById('mb-restore-title').textContent = 'Restore Complete';
|
|
setTimeout(function() {
|
|
hideRestoreProgress();
|
|
location.reload();
|
|
}, 2000);
|
|
|
|
} catch (err) {
|
|
updateRestoreProgress(0, 'ERROR: ' + err.message, 'failed');
|
|
document.getElementById('mb-restore-title').textContent = 'Restore Failed';
|
|
setTimeout(hideRestoreProgress, 5000);
|
|
}
|
|
}
|
|
|
|
// Attach the AJAX restore handler to the restore form
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var restoreForm = document.getElementById('mb-restore-form');
|
|
if (restoreForm) {
|
|
restoreForm.addEventListener('submit', startSteppedRestore);
|
|
}
|
|
});
|
|
|
|
// View Log modal handler
|
|
document.addEventListener('click', function(e) {
|
|
var btn = e.target.closest('.mb-view-log');
|
|
if (!btn) return;
|
|
e.preventDefault();
|
|
var recordId = btn.getAttribute('data-id');
|
|
var modal = document.getElementById('mb-log-modal');
|
|
var body = document.getElementById('mb-log-body');
|
|
body.textContent = 'Loading...';
|
|
modal.style.display = 'block';
|
|
|
|
var form = new URLSearchParams();
|
|
form.append('task', 'ajax.viewLog');
|
|
form.append('id', recordId);
|
|
form.append(TOKEN_NAME, '1');
|
|
|
|
fetch(AJAX_URL, {
|
|
method: 'POST',
|
|
body: form,
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.error) {
|
|
body.textContent = data.message || 'Error loading log';
|
|
} else {
|
|
body.textContent = data.log;
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
body.textContent = 'Error: ' + err.message;
|
|
});
|
|
});
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (e.target.id === 'mb-log-modal' || e.target.classList.contains('mb-log-close')) {
|
|
document.getElementById('mb-log-modal').style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Browse Archive modal handler
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
var units = ['B', 'KB', 'MB', 'GB'];
|
|
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
if (i >= units.length) i = units.length - 1;
|
|
return (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + ' ' + units[i];
|
|
}
|
|
|
|
function browseSetMessage(tbody, message, cssClass) {
|
|
tbody.textContent = '';
|
|
var tr = document.createElement('tr');
|
|
var td = document.createElement('td');
|
|
td.setAttribute('colspan', '3');
|
|
td.className = cssClass || 'text-center';
|
|
td.textContent = message;
|
|
tr.appendChild(td);
|
|
tbody.appendChild(tr);
|
|
}
|
|
|
|
function browseAddFileRow(tbody, file) {
|
|
var tr = document.createElement('tr');
|
|
|
|
var tdName = document.createElement('td');
|
|
tdName.style.wordBreak = 'break-all';
|
|
tdName.style.fontSize = '0.85rem';
|
|
var code = document.createElement('code');
|
|
code.textContent = file.name;
|
|
tdName.appendChild(code);
|
|
tr.appendChild(tdName);
|
|
|
|
var tdSize = document.createElement('td');
|
|
tdSize.className = 'text-end text-nowrap';
|
|
tdSize.textContent = formatFileSize(file.size);
|
|
tr.appendChild(tdSize);
|
|
|
|
var tdComp = document.createElement('td');
|
|
tdComp.className = 'text-end text-nowrap';
|
|
tdComp.textContent = formatFileSize(file.compressed_size);
|
|
tr.appendChild(tdComp);
|
|
|
|
tbody.appendChild(tr);
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
var btn = e.target.closest('.mb-browse-archive');
|
|
if (!btn) return;
|
|
e.preventDefault();
|
|
var recordId = btn.getAttribute('data-id');
|
|
var modal = document.getElementById('mb-browse-modal');
|
|
var tbody = document.getElementById('mb-browse-tbody');
|
|
var summary = document.getElementById('mb-browse-summary');
|
|
browseSetMessage(tbody, 'Loading...');
|
|
summary.textContent = '';
|
|
modal.style.display = 'block';
|
|
|
|
postAjax({ task: 'ajax.browseArchive', id: recordId })
|
|
.then(function(data) {
|
|
if (data.error) {
|
|
browseSetMessage(tbody, data.message || 'Error', 'text-danger');
|
|
return;
|
|
}
|
|
tbody.textContent = '';
|
|
if (data.files.length === 0) {
|
|
browseSetMessage(tbody, 'Archive is empty', 'text-center text-muted');
|
|
} else {
|
|
for (var i = 0; i < data.files.length; i++) {
|
|
browseAddFileRow(tbody, data.files[i]);
|
|
}
|
|
}
|
|
var text = data.total_files + ' files, ' + formatFileSize(data.total_size) + ' uncompressed';
|
|
if (data.truncated) {
|
|
text += ' (showing first ' + data.files.length + ')';
|
|
}
|
|
summary.textContent = text;
|
|
})
|
|
.catch(function(err) {
|
|
browseSetMessage(tbody, 'Error: ' + err.message, 'text-danger');
|
|
});
|
|
});
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (e.target.id === 'mb-browse-modal' || e.target.classList.contains('mb-browse-close')) {
|
|
document.getElementById('mb-browse-modal').style.display = 'none';
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
<!-- Restore Confirmation Modal -->
|
|
<div id="mb-restore-modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); z-index:10000;">
|
|
<div style="max-width:500px; margin:8% auto; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
|
|
<h4 style="margin:0;"><?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE'); ?></h4>
|
|
<button type="button" class="btn-close mb-restore-close" aria-label="Close"></button>
|
|
</div>
|
|
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.restore'); ?>" method="post" id="mb-restore-form">
|
|
<input type="hidden" name="id" id="mb-restore-record-id" value="">
|
|
<div style="padding:1.5rem;">
|
|
<div class="alert alert-danger">
|
|
<span class="icon-warning-circle" aria-hidden="true"></span>
|
|
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_CONFIRM'); ?></strong>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="restore_files" value="1" id="mb-restore-files" checked>
|
|
<label class="form-check-label" for="mb-restore-files">
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_FILES'); ?>
|
|
</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="restore_db" value="1" id="mb-restore-db" checked>
|
|
<label class="form-check-label" for="mb-restore-db">
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_DATABASE'); ?>
|
|
</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" name="preserve_config" value="1" id="mb-restore-config" checked>
|
|
<label class="form-check-label" for="mb-restore-config">
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_PRESERVE_CONFIG'); ?>
|
|
<small class="text-muted d-block"><?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_PRESERVE_CONFIG_DESC'); ?></small>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="mb-restore-password" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_ENCRYPTION_PASSWORD'); ?></label>
|
|
<input type="password" class="form-control" id="mb-restore-password" name="encryption_password"
|
|
placeholder="<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_PASSWORD_PLACEHOLDER'); ?>" autocomplete="off">
|
|
</div>
|
|
</div>
|
|
<div style="padding:0 1.5rem 1.5rem; text-align:right;">
|
|
<button type="button" class="btn btn-secondary mb-restore-close"><?php echo Text::_('JCANCEL'); ?></button>
|
|
<button type="submit" class="btn btn-danger">
|
|
<span class="icon-upload" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE'); ?>
|
|
</button>
|
|
</div>
|
|
<?php echo HTMLHelper::_('form.token'); ?>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Restore Progress Modal -->
|
|
<div id="mb-restore-progress-modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); z-index:10000;">
|
|
<div style="max-width:500px; margin:10% auto; background:#fff; border-radius:8px; padding:2rem; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
|
|
<h3 id="mb-restore-title" style="margin:0 0 1rem;">Restore in Progress</h3>
|
|
<div style="background:#e9ecef; border-radius:4px; overflow:hidden; height:24px; margin-bottom:0.5rem;">
|
|
<div id="mb-restore-progress-bar" style="height:100%; background:#dc3545; transition:width 0.3s; width:0%; display:flex; align-items:center; justify-content:center; color:#fff; font-size:0.8rem; font-weight:bold;">0%</div>
|
|
</div>
|
|
<p id="mb-restore-status" style="color:#666; font-size:0.9rem; margin:0.5rem 0;">Initializing...</p>
|
|
<p id="mb-restore-phase" style="color:#999; font-size:0.8rem; margin:0;">Phase: init</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Log Viewer Modal -->
|
|
<div id="mb-log-modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); z-index:10000;">
|
|
<div style="max-width:700px; margin:5% auto; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.3); display:flex; flex-direction:column; max-height:80vh;">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
|
|
<h4 style="margin:0;"><?php echo Text::_('COM_MOKOJOOMBACKUP_VIEW_LOG'); ?></h4>
|
|
<button type="button" class="btn-close mb-log-close" aria-label="Close"></button>
|
|
</div>
|
|
<div style="padding:1rem 1.5rem; overflow-y:auto; flex:1;">
|
|
<pre id="mb-log-body" style="white-space:pre-wrap; word-break:break-word; font-size:0.85rem; margin:0; background:#f8f9fa; padding:1rem; border-radius:4px;"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Archive Browser Modal -->
|
|
<div id="mb-browse-modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); z-index:10000;">
|
|
<div style="max-width:800px; margin:5% auto; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.3); display:flex; flex-direction:column; max-height:80vh;">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
|
|
<h4 style="margin:0;">
|
|
<span class="icon-folder-open" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_ARCHIVE'); ?>
|
|
</h4>
|
|
<button type="button" class="btn-close mb-browse-close" aria-label="Close"></button>
|
|
</div>
|
|
<div style="padding:0.75rem 1.5rem; border-bottom:1px solid #dee2e6; background:#f8f9fa;">
|
|
<small id="mb-browse-summary" class="text-muted"></small>
|
|
</div>
|
|
<div style="padding:0; overflow-y:auto; flex:1;">
|
|
<table class="table table-sm table-striped mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_COL_NAME'); ?></th>
|
|
<th class="text-end" style="width:100px;"><?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_COL_SIZE'); ?></th>
|
|
<th class="text-end" style="width:120px;"><?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_COL_COMPRESSED'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="mb-browse-tbody">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backup Comparison Modal -->
|
|
<div id="mb-compare-modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); z-index:10000;">
|
|
<div style="max-width:800px; margin:5% auto; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.3); display:flex; flex-direction:column; max-height:85vh;">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
|
|
<h4 style="margin:0;">
|
|
<span class="icon-copy" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_TITLE'); ?>
|
|
</h4>
|
|
<button type="button" class="btn-close mb-compare-close" aria-label="Close"></button>
|
|
</div>
|
|
<div style="padding:1rem 1.5rem; overflow-y:auto; flex:1;">
|
|
<div id="mb-compare-loading" style="text-align:center; padding:2rem;">
|
|
<span class="icon-spinner icon-spin" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_LOADING'); ?>
|
|
</div>
|
|
<div id="mb-compare-error" style="display:none;" class="alert alert-danger"></div>
|
|
<table id="mb-compare-table" class="table table-striped" style="display:none;">
|
|
<thead>
|
|
<tr>
|
|
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_FIELD'); ?></th>
|
|
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_BACKUP'); ?> 1</th>
|
|
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_BACKUP'); ?> 2</th>
|
|
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_DELTA'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="mb-compare-body"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
var COMPARE_AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
|
|
var COMPARE_TOKEN = <?php echo json_encode($ajaxToken); ?>;
|
|
|
|
function mbCmpFormatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
var units = ['B', 'KB', 'MB', 'GB'];
|
|
var i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024));
|
|
if (i >= units.length) i = units.length - 1;
|
|
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
|
|
}
|
|
|
|
function mbCmpFormatDuration(seconds) {
|
|
if (seconds <= 0) return '0s';
|
|
var m = Math.floor(seconds / 60);
|
|
var s = seconds % 60;
|
|
return m > 0 ? m + 'm ' + s + 's' : s + 's';
|
|
}
|
|
|
|
function mbCmpDeltaCell(value, unit) {
|
|
if (value === 0) return '<span class="text-muted">—</span>';
|
|
var isPositive = value > 0;
|
|
var colorClass = isPositive ? 'text-danger' : 'text-success';
|
|
var display;
|
|
if (unit === 'bytes') {
|
|
display = (isPositive ? '+' : '') + mbCmpFormatBytes(value);
|
|
} else if (unit === 'duration') {
|
|
display = (isPositive ? '+' : '-') + mbCmpFormatDuration(Math.abs(value));
|
|
} else {
|
|
display = (isPositive ? '+' : '') + value.toLocaleString();
|
|
}
|
|
return '<span class="fw-bold ' + colorClass + '">' + display + '</span>';
|
|
}
|
|
|
|
function mbShowCompareModal(id1, id2) {
|
|
var modal = document.getElementById('mb-compare-modal');
|
|
var loading = document.getElementById('mb-compare-loading');
|
|
var errorEl = document.getElementById('mb-compare-error');
|
|
var table = document.getElementById('mb-compare-table');
|
|
var body = document.getElementById('mb-compare-body');
|
|
|
|
modal.style.display = 'block';
|
|
loading.style.display = 'block';
|
|
errorEl.style.display = 'none';
|
|
table.style.display = 'none';
|
|
body.innerHTML = '';
|
|
|
|
var form = new URLSearchParams();
|
|
form.append('task', 'ajax.compareBackups');
|
|
form.append('id1', id1);
|
|
form.append('id2', id2);
|
|
form.append(COMPARE_TOKEN, '1');
|
|
|
|
fetch(COMPARE_AJAX_URL, {
|
|
method: 'POST',
|
|
body: form,
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
loading.style.display = 'none';
|
|
|
|
if (data.error) {
|
|
errorEl.textContent = data.message || 'Error loading comparison';
|
|
errorEl.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
var b1 = data.backup1;
|
|
var b2 = data.backup2;
|
|
var d = data.delta;
|
|
|
|
var dur1 = 0, dur2 = 0;
|
|
if (b1.backupstart !== '0000-00-00 00:00:00' && b1.backupend !== '0000-00-00 00:00:00') {
|
|
dur1 = (new Date(b1.backupend).getTime() - new Date(b1.backupstart).getTime()) / 1000;
|
|
}
|
|
if (b2.backupstart !== '0000-00-00 00:00:00' && b2.backupend !== '0000-00-00 00:00:00') {
|
|
dur2 = (new Date(b2.backupend).getTime() - new Date(b2.backupstart).getTime()) / 1000;
|
|
}
|
|
|
|
var rows = [
|
|
['<?php echo Text::_('JGRID_HEADING_ID', true); ?>', '#' + b1.id, '#' + b2.id, ''],
|
|
['<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_DESCRIPTION', true); ?>', b1.description || '—', b2.description || '—', ''],
|
|
['<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_PROFILE', true); ?>', b1.profile_title || '—', b2.profile_title || '—', ''],
|
|
['<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_STATUS', true); ?>', b1.status, b2.status, ''],
|
|
['<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_TYPE', true); ?>', b1.backup_type, b2.backup_type, ''],
|
|
['<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_SIZE', true); ?>', mbCmpFormatBytes(b1.total_size), mbCmpFormatBytes(b2.total_size), mbCmpDeltaCell(d.size_diff, 'bytes')],
|
|
['<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_DB_SIZE', true); ?>', mbCmpFormatBytes(b1.db_size), mbCmpFormatBytes(b2.db_size), mbCmpDeltaCell(b2.db_size - b1.db_size, 'bytes')],
|
|
['<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_FILES_COUNT', true); ?>', b1.files_count.toLocaleString(), b2.files_count.toLocaleString(), mbCmpDeltaCell(d.files_diff, 'number')],
|
|
['<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_TABLES_COUNT', true); ?>', b1.tables_count.toLocaleString(), b2.tables_count.toLocaleString(), mbCmpDeltaCell(d.tables_diff, 'number')],
|
|
['<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_DATE', true); ?>', b1.backupstart, b2.backupstart, ''],
|
|
['<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_DURATION', true); ?>', mbCmpFormatDuration(dur1), mbCmpFormatDuration(dur2), mbCmpDeltaCell(d.duration_diff_seconds, 'duration')],
|
|
];
|
|
|
|
var html = '';
|
|
for (var i = 0; i < rows.length; i++) {
|
|
html += '<tr><td class="fw-bold">' + rows[i][0] + '</td><td>' + rows[i][1] + '</td><td>' + rows[i][2] + '</td><td>' + rows[i][3] + '</td></tr>';
|
|
}
|
|
body.innerHTML = html;
|
|
table.style.display = 'table';
|
|
})
|
|
.catch(function(err) {
|
|
loading.style.display = 'none';
|
|
errorEl.textContent = 'Error: ' + err.message;
|
|
errorEl.style.display = 'block';
|
|
});
|
|
}
|
|
|
|
// Close compare modal
|
|
document.addEventListener('click', function(e) {
|
|
if (e.target.id === 'mb-compare-modal' || e.target.classList.contains('mb-compare-close')) {
|
|
document.getElementById('mb-compare-modal').style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Intercept Compare toolbar button
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var compareBtn = document.querySelector('[onclick*="backups.compare"], .button-copy');
|
|
if (compareBtn) {
|
|
compareBtn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
var checked = document.querySelectorAll('input[name="cid[]"]:checked');
|
|
if (checked.length !== 2) {
|
|
alert('<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_SELECT_TWO', true); ?>');
|
|
return false;
|
|
}
|
|
|
|
mbShowCompareModal(checked[0].value, checked[1].value);
|
|
return false;
|
|
}, true);
|
|
}
|
|
});
|
|
})();
|
|
</script>
|