36ec6dd5a3
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 4s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Generic: Project CI / Lint & Validate (push) Successful in 11s
Generic: Project CI / Lint & Validate (pull_request) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 12s
Universal: PR Check / Validate PR (pull_request) Failing after 48s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 54s
Generic: Project CI / Tests (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
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
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) 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
SteppedBackupEngine now sends email + ntfy notifications on both success (completeRecord) and failure (failRecord). Previously only BackupEngine (synchronous CLI/toolbar path) sent notifications. Download link in backups template now includes the CSRF token in the URL query string, fixing "security token did not match" error when clicking download buttons.
368 lines
13 KiB
PHP
368 lines
13 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; ?>
|
|
<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 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, 3000);
|
|
return;
|
|
}
|
|
|
|
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;
|
|
|
|
// 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';
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
<!-- 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>
|