dae30161ae
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 8s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 8s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 5s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 41s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 22s
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
New #__mokosuitebackup_remotes table stores remote destinations with JSON params per type (SFTP/S3/GDrive/FTP). Each profile can have multiple enabled destinations — the engine uploads to all of them. Database: - New table with profile_id FK, type, enabled, params JSON, ordering - Migration auto-converts existing profile remote columns to new table - RemoteTable, RemoteModel, RemotesModel classes Engine: - BackupEngine: loadRemoteDestinations() + createUploaderFromParams() iterates all enabled remotes, falls back to legacy columns - SteppedBackupEngine: one upload step per remote destination, persisted via session.remoteDestinations + remoteIndex - Local copy only deleted when ALL uploads succeed UI: - Profile edit: "Remote Destinations" linked table with AJAX CRUD - Add/edit modal with type selector showing dynamic fields - Toggle enabled/disabled, delete with confirmation - Legacy fields hidden when remotes configured, shown as fallback - Secrets masked in responses, merged from DB on save Closes #97
616 lines
24 KiB
PHP
616 lines
24 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\HTML\HTMLHelper;
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\Router\Route;
|
|
use Joomla\CMS\Session\Session;
|
|
|
|
HTMLHelper::_('behavior.formvalidator');
|
|
HTMLHelper::_('behavior.keepalive');
|
|
|
|
$profileId = (int) $this->item->id;
|
|
$token = Session::getFormToken();
|
|
?>
|
|
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&layout=edit&id=' . $profileId); ?>"
|
|
method="post" name="adminForm" id="adminForm" class="form-validate">
|
|
|
|
<div class="main-card">
|
|
<?php echo HTMLHelper::_('uitab.startTabSet', 'profileTab', ['active' => 'general']); ?>
|
|
|
|
<?php echo HTMLHelper::_('uitab.addTab', 'profileTab', 'general', Text::_('COM_MOKOJOOMBACKUP_TAB_GENERAL')); ?>
|
|
<div class="row">
|
|
<div class="col-lg-9">
|
|
<?php echo $this->form->renderFieldset('general'); ?>
|
|
</div>
|
|
<div class="col-lg-3">
|
|
<?php echo $this->form->renderFieldset('sidebar'); ?>
|
|
</div>
|
|
</div>
|
|
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
|
|
|
<?php echo HTMLHelper::_('uitab.addTab', 'profileTab', 'archive', Text::_('COM_MOKOJOOMBACKUP_TAB_ARCHIVE')); ?>
|
|
<div class="row">
|
|
<div class="col-lg-9">
|
|
<?php echo $this->form->renderFieldset('archive'); ?>
|
|
</div>
|
|
</div>
|
|
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
|
|
|
<?php echo HTMLHelper::_('uitab.addTab', 'profileTab', 'filters', Text::_('COM_MOKOJOOMBACKUP_TAB_FILTERS')); ?>
|
|
<div class="row">
|
|
<div class="col-lg-12">
|
|
<?php echo $this->form->renderFieldset('filters'); ?>
|
|
</div>
|
|
</div>
|
|
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
|
|
|
<?php echo HTMLHelper::_('uitab.addTab', 'profileTab', 'notifications', Text::_('COM_MOKOJOOMBACKUP_TAB_NOTIFICATIONS')); ?>
|
|
<div class="row">
|
|
<div class="col-lg-9">
|
|
<?php echo $this->form->renderFieldset('notifications'); ?>
|
|
</div>
|
|
</div>
|
|
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
|
|
|
<?php echo HTMLHelper::_('uitab.addTab', 'profileTab', 'remote', Text::_('COM_MOKOJOOMBACKUP_TAB_REMOTE')); ?>
|
|
<div class="row">
|
|
<div class="col-lg-12">
|
|
<?php // ---- Remote Destinations (multi-remote) ---- ?>
|
|
<?php if ($profileId): ?>
|
|
<div id="mokoRemoteDestinations" class="mb-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h3 class="mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_DESTINATIONS'); ?></h3>
|
|
<button type="button" class="btn btn-success btn-sm" id="btnAddRemote">
|
|
<span class="icon-plus" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_ADD'); ?>
|
|
</button>
|
|
</div>
|
|
|
|
<table class="table" id="remoteDestTable">
|
|
<thead>
|
|
<tr>
|
|
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_TITLE'); ?></th>
|
|
<th style="width:120px"><?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_TYPE'); ?></th>
|
|
<th style="width:100px"><?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_STATUS'); ?></th>
|
|
<th style="width:160px"><?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_ACTIONS'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="remoteDestBody">
|
|
<tr id="remoteDestLoading">
|
|
<td colspan="4" class="text-center text-muted">
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_LOADING'); ?>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p class="text-muted small" id="remoteDestEmpty" style="display:none;">
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_NONE_CONFIGURED'); ?>
|
|
</p>
|
|
</div>
|
|
<hr>
|
|
<?php endif; ?>
|
|
|
|
<?php // ---- Legacy single-remote fields ---- ?>
|
|
<div id="legacyRemoteFields">
|
|
<div class="alert alert-info small" id="legacyRemoteNote" style="display:none;">
|
|
<span class="icon-info-circle" aria-hidden="true"></span>
|
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_LEGACY_NOTE'); ?>
|
|
</div>
|
|
<?php echo $this->form->renderFieldset('remote'); ?>
|
|
<?php echo $this->form->renderFieldset('ftp'); ?>
|
|
<?php echo $this->form->renderFieldset('google_drive'); ?>
|
|
<?php echo $this->form->renderFieldset('s3'); ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
|
|
|
<?php echo HTMLHelper::_('uitab.endTabSet'); ?>
|
|
</div>
|
|
|
|
<input type="hidden" name="task" value="">
|
|
<?php echo HTMLHelper::_('form.token'); ?>
|
|
</form>
|
|
|
|
<?php // ---- Remote Destination Add/Edit Modal ---- ?>
|
|
<?php if ($profileId): ?>
|
|
<div class="modal fade" id="remoteModal" tabindex="-1" aria-labelledby="remoteModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="remoteModalLabel"><?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_ADD'); ?></h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?php echo Text::_('JCLOSE'); ?>"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="remoteEditId" value="0">
|
|
|
|
<div class="mb-3">
|
|
<label for="remoteTitle" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_TITLE'); ?></label>
|
|
<input type="text" class="form-control" id="remoteTitle" maxlength="255" required>
|
|
</div>
|
|
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<label for="remoteType" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_TYPE'); ?></label>
|
|
<select class="form-select" id="remoteType">
|
|
<option value="sftp"><?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_SFTP'); ?></option>
|
|
<option value="s3"><?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_S3'); ?></option>
|
|
<option value="google_drive"><?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_GDRIVE'); ?></option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_ENABLED'); ?></label>
|
|
<div class="form-check form-switch mt-2">
|
|
<input class="form-check-input" type="checkbox" id="remoteEnabled" checked>
|
|
<label class="form-check-label" for="remoteEnabled"><?php echo Text::_('JYES'); ?></label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_KEEP_LOCAL'); ?></label>
|
|
<div class="form-check form-switch mt-2">
|
|
<input class="form-check-input" type="checkbox" id="remoteKeepLocal" checked>
|
|
<label class="form-check-label" for="remoteKeepLocal"><?php echo Text::_('JYES'); ?></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<?php // SFTP fields ?>
|
|
<div id="remoteFields_sftp" class="remote-type-fields">
|
|
<div class="row mb-3">
|
|
<div class="col-md-8">
|
|
<label for="remoteCfg_sftp_host" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_HOST'); ?></label>
|
|
<input type="text" class="form-control" id="remoteCfg_sftp_host" maxlength="255">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label for="remoteCfg_sftp_port" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_PORT'); ?></label>
|
|
<input type="number" class="form-control" id="remoteCfg_sftp_port" value="22" min="1" max="65535">
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="remoteCfg_sftp_username" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME'); ?></label>
|
|
<input type="text" class="form-control" id="remoteCfg_sftp_username" maxlength="255">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="remoteCfg_sftp_auth_type" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_AUTH_TYPE'); ?></label>
|
|
<select class="form-select" id="remoteCfg_sftp_auth_type">
|
|
<option value="key"><?php echo Text::_('COM_MOKOJOOMBACKUP_SFTP_AUTH_KEY'); ?></option>
|
|
<option value="password"><?php echo Text::_('COM_MOKOJOOMBACKUP_SFTP_AUTH_PASSWORD'); ?></option>
|
|
<option value="key_passphrase"><?php echo Text::_('COM_MOKOJOOMBACKUP_SFTP_AUTH_KEY_PASSPHRASE'); ?></option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3" id="remoteSftpPasswordWrap">
|
|
<label for="remoteCfg_sftp_password" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD'); ?></label>
|
|
<input type="password" class="form-control" id="remoteCfg_sftp_password" maxlength="255">
|
|
</div>
|
|
<div class="mb-3" id="remoteSftpKeyWrap">
|
|
<label for="remoteCfg_sftp_key_data" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY'); ?></label>
|
|
<textarea class="form-control" id="remoteCfg_sftp_key_data" rows="4" placeholder="Paste SSH private key or leave as-is to keep existing"></textarea>
|
|
</div>
|
|
<div class="mb-3" id="remoteSftpPassphraseWrap">
|
|
<label for="remoteCfg_sftp_passphrase" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE'); ?></label>
|
|
<input type="password" class="form-control" id="remoteCfg_sftp_passphrase" maxlength="255">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="remoteCfg_sftp_path" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_PATH'); ?></label>
|
|
<input type="text" class="form-control" id="remoteCfg_sftp_path" value="/backups" maxlength="512">
|
|
</div>
|
|
</div>
|
|
|
|
<?php // S3 fields ?>
|
|
<div id="remoteFields_s3" class="remote-type-fields" style="display:none;">
|
|
<div class="mb-3">
|
|
<label for="remoteCfg_s3_endpoint" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_S3_ENDPOINT'); ?></label>
|
|
<input type="text" class="form-control" id="remoteCfg_s3_endpoint" maxlength="512" placeholder="https://s3.amazonaws.com">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="remoteCfg_s3_region" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_S3_REGION'); ?></label>
|
|
<input type="text" class="form-control" id="remoteCfg_s3_region" value="us-east-1" maxlength="50">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="remoteCfg_s3_access_key" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_S3_ACCESS_KEY'); ?></label>
|
|
<input type="text" class="form-control" id="remoteCfg_s3_access_key" maxlength="255">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="remoteCfg_s3_secret_key" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_S3_SECRET_KEY'); ?></label>
|
|
<input type="password" class="form-control" id="remoteCfg_s3_secret_key" maxlength="255">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="remoteCfg_s3_bucket" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_S3_BUCKET'); ?></label>
|
|
<input type="text" class="form-control" id="remoteCfg_s3_bucket" maxlength="255">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="remoteCfg_s3_path" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_S3_PATH'); ?></label>
|
|
<input type="text" class="form-control" id="remoteCfg_s3_path" value="/backups" maxlength="512">
|
|
</div>
|
|
</div>
|
|
|
|
<?php // Google Drive fields ?>
|
|
<div id="remoteFields_google_drive" class="remote-type-fields" style="display:none;">
|
|
<div class="mb-3">
|
|
<label for="remoteCfg_gdrive_client_id" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_GDRIVE_CLIENT_ID'); ?></label>
|
|
<input type="text" class="form-control" id="remoteCfg_gdrive_client_id" maxlength="255">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="remoteCfg_gdrive_client_secret" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_GDRIVE_CLIENT_SECRET'); ?></label>
|
|
<input type="password" class="form-control" id="remoteCfg_gdrive_client_secret" maxlength="255">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="remoteCfg_gdrive_refresh_token" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_GDRIVE_REFRESH_TOKEN'); ?></label>
|
|
<input type="text" class="form-control" id="remoteCfg_gdrive_refresh_token" maxlength="512">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="remoteCfg_gdrive_folder_id" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_GDRIVE_FOLDER_ID'); ?></label>
|
|
<input type="text" class="form-control" id="remoteCfg_gdrive_folder_id" maxlength="255">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo Text::_('JCANCEL'); ?></button>
|
|
<button type="button" class="btn btn-primary" id="btnSaveRemote">
|
|
<span class="icon-save" aria-hidden="true"></span>
|
|
<?php echo Text::_('JAPPLY'); ?>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
'use strict';
|
|
|
|
const profileId = <?php echo $profileId; ?>;
|
|
const token = '<?php echo $token; ?>';
|
|
|
|
if (!profileId) return;
|
|
|
|
const baseUrl = 'index.php?option=com_mokosuitebackup&task=ajax.';
|
|
const tbody = document.getElementById('remoteDestBody');
|
|
const emptyMsg = document.getElementById('remoteDestEmpty');
|
|
const loadingTr = document.getElementById('remoteDestLoading');
|
|
const legacy = document.getElementById('legacyRemoteFields');
|
|
const legacyNote = document.getElementById('legacyRemoteNote');
|
|
const modal = new bootstrap.Modal(document.getElementById('remoteModal'));
|
|
|
|
// Type badge colours
|
|
const typeBadge = {sftp: 'bg-primary', s3: 'bg-warning text-dark', google_drive: 'bg-success'};
|
|
const typeLabel = {
|
|
sftp: '<?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_SFTP', true); ?>',
|
|
s3: '<?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_S3', true); ?>',
|
|
google_drive: '<?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_GDRIVE', true); ?>'
|
|
};
|
|
|
|
// Config field mappings per type
|
|
const configFields = {
|
|
sftp: ['host','port','username','auth_type','password','key_data','passphrase','path'],
|
|
s3: ['endpoint','region','access_key','secret_key','bucket','path'],
|
|
google_drive: ['client_id','client_secret','refresh_token','folder_id']
|
|
};
|
|
|
|
// Prefix mapping for config field IDs
|
|
const fieldPrefix = {sftp: 'sftp_', s3: 's3_', google_drive: 'gdrive_'};
|
|
|
|
let remotesData = [];
|
|
|
|
// ---- Load remotes ----
|
|
function loadRemotes() {
|
|
loadingTr.style.display = '';
|
|
emptyMsg.style.display = 'none';
|
|
|
|
fetch(baseUrl + 'listRemotes&profile_id=' + profileId + '&' + token + '=1', {
|
|
method: 'POST',
|
|
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
loadingTr.style.display = 'none';
|
|
|
|
if (data.error) {
|
|
showTableMessage(data.message, 'text-danger');
|
|
return;
|
|
}
|
|
|
|
remotesData = data.items || [];
|
|
renderTable();
|
|
})
|
|
.catch(() => {
|
|
loadingTr.style.display = 'none';
|
|
showTableMessage('Failed to load remotes', 'text-danger');
|
|
});
|
|
}
|
|
|
|
function renderTable() {
|
|
while (tbody.firstChild) tbody.removeChild(tbody.firstChild);
|
|
|
|
if (!remotesData.length) {
|
|
emptyMsg.style.display = '';
|
|
legacy.style.display = '';
|
|
legacyNote.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
emptyMsg.style.display = 'none';
|
|
legacy.style.display = 'none';
|
|
legacyNote.style.display = 'block';
|
|
|
|
remotesData.forEach(function(item) {
|
|
const tr = document.createElement('tr');
|
|
|
|
// Title cell
|
|
const tdTitle = document.createElement('td');
|
|
tdTitle.textContent = item.title;
|
|
tr.appendChild(tdTitle);
|
|
|
|
// Type badge cell
|
|
const tdType = document.createElement('td');
|
|
const badgeSpan = document.createElement('span');
|
|
badgeSpan.className = 'badge ' + (typeBadge[item.type] || 'bg-secondary');
|
|
badgeSpan.textContent = typeLabel[item.type] || item.type;
|
|
tdType.appendChild(badgeSpan);
|
|
tr.appendChild(tdType);
|
|
|
|
// Enabled toggle cell
|
|
const tdEnabled = document.createElement('td');
|
|
const toggleSpan = document.createElement('span');
|
|
toggleSpan.className = 'badge ' + (item.enabled ? 'bg-success' : 'bg-secondary');
|
|
toggleSpan.style.cursor = 'pointer';
|
|
toggleSpan.setAttribute('data-toggle-id', item.id);
|
|
toggleSpan.textContent = item.enabled ? 'Enabled' : 'Disabled';
|
|
tdEnabled.appendChild(toggleSpan);
|
|
tr.appendChild(tdEnabled);
|
|
|
|
// Actions cell
|
|
const tdActions = document.createElement('td');
|
|
|
|
const editBtn = document.createElement('button');
|
|
editBtn.type = 'button';
|
|
editBtn.className = 'btn btn-sm btn-outline-primary me-1';
|
|
editBtn.setAttribute('data-edit-id', item.id);
|
|
editBtn.title = 'Edit';
|
|
const editIcon = document.createElement('span');
|
|
editIcon.className = 'icon-pencil';
|
|
editIcon.setAttribute('aria-hidden', 'true');
|
|
editBtn.appendChild(editIcon);
|
|
tdActions.appendChild(editBtn);
|
|
|
|
const delBtn = document.createElement('button');
|
|
delBtn.type = 'button';
|
|
delBtn.className = 'btn btn-sm btn-outline-danger';
|
|
delBtn.setAttribute('data-delete-id', item.id);
|
|
delBtn.title = 'Delete';
|
|
const delIcon = document.createElement('span');
|
|
delIcon.className = 'icon-trash';
|
|
delIcon.setAttribute('aria-hidden', 'true');
|
|
delBtn.appendChild(delIcon);
|
|
tdActions.appendChild(delBtn);
|
|
|
|
tr.appendChild(tdActions);
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function showTableMessage(message, cssClass) {
|
|
while (tbody.firstChild) tbody.removeChild(tbody.firstChild);
|
|
const tr = document.createElement('tr');
|
|
const td = document.createElement('td');
|
|
td.setAttribute('colspan', '4');
|
|
td.className = cssClass || '';
|
|
td.textContent = message;
|
|
tr.appendChild(td);
|
|
tbody.appendChild(tr);
|
|
}
|
|
|
|
// ---- Toggle enabled ----
|
|
tbody.addEventListener('click', function(e) {
|
|
const toggle = e.target.closest('[data-toggle-id]');
|
|
if (toggle) {
|
|
const id = toggle.getAttribute('data-toggle-id');
|
|
const body = new URLSearchParams();
|
|
body.set(token, '1');
|
|
body.set('remote_id', id);
|
|
body.set('profile_id', profileId);
|
|
|
|
fetch(baseUrl + 'toggleRemote', {
|
|
method: 'POST',
|
|
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
|
body: body
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => { if (!data.error) loadRemotes(); })
|
|
.catch(() => {});
|
|
return;
|
|
}
|
|
|
|
const editBtn = e.target.closest('[data-edit-id]');
|
|
if (editBtn) {
|
|
openEdit(parseInt(editBtn.getAttribute('data-edit-id'), 10));
|
|
return;
|
|
}
|
|
|
|
const delBtn = e.target.closest('[data-delete-id]');
|
|
if (delBtn) {
|
|
if (!confirm('<?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_DELETE_CONFIRM', true); ?>')) return;
|
|
const id = delBtn.getAttribute('data-delete-id');
|
|
const body = new URLSearchParams();
|
|
body.set(token, '1');
|
|
body.set('remote_id', id);
|
|
body.set('profile_id', profileId);
|
|
|
|
fetch(baseUrl + 'deleteRemote', {
|
|
method: 'POST',
|
|
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
|
body: body
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => { if (!data.error) loadRemotes(); })
|
|
.catch(() => {});
|
|
}
|
|
});
|
|
|
|
// ---- Add button ----
|
|
document.getElementById('btnAddRemote').addEventListener('click', function() {
|
|
openEdit(0);
|
|
});
|
|
|
|
// ---- Open modal for add / edit ----
|
|
function openEdit(id) {
|
|
document.getElementById('remoteEditId').value = id;
|
|
document.getElementById('remoteTitle').value = '';
|
|
document.getElementById('remoteType').value = 'sftp';
|
|
document.getElementById('remoteEnabled').checked = true;
|
|
document.getElementById('remoteKeepLocal').checked = true;
|
|
|
|
// Clear all config fields
|
|
document.querySelectorAll('.remote-type-fields input, .remote-type-fields textarea, .remote-type-fields select').forEach(function(el) {
|
|
if (el.type === 'number') {
|
|
el.value = el.defaultValue || '';
|
|
} else if (el.tagName === 'SELECT') {
|
|
el.selectedIndex = 0;
|
|
} else {
|
|
el.value = '';
|
|
}
|
|
});
|
|
|
|
// Restore defaults
|
|
const portField = document.getElementById('remoteCfg_sftp_port');
|
|
if (portField) portField.value = '22';
|
|
const s3Region = document.getElementById('remoteCfg_s3_region');
|
|
if (s3Region) s3Region.value = 'us-east-1';
|
|
const sftpPath = document.getElementById('remoteCfg_sftp_path');
|
|
if (sftpPath) sftpPath.value = '/backups';
|
|
const s3Path = document.getElementById('remoteCfg_s3_path');
|
|
if (s3Path) s3Path.value = '/backups';
|
|
|
|
if (id) {
|
|
const item = remotesData.find(r => r.id === id);
|
|
if (item) {
|
|
document.getElementById('remoteTitle').value = item.title;
|
|
document.getElementById('remoteType').value = item.type;
|
|
document.getElementById('remoteEnabled').checked = !!item.enabled;
|
|
document.getElementById('remoteKeepLocal').checked = !!item.keep_local;
|
|
|
|
// Populate config fields
|
|
const prefix = fieldPrefix[item.type] || '';
|
|
const fields = configFields[item.type] || [];
|
|
fields.forEach(function(f) {
|
|
const el = document.getElementById('remoteCfg_' + prefix + f);
|
|
if (el && item.config && item.config[f] !== undefined) {
|
|
el.value = item.config[f];
|
|
}
|
|
});
|
|
}
|
|
document.getElementById('remoteModalLabel').textContent =
|
|
'<?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_EDIT', true); ?>';
|
|
} else {
|
|
document.getElementById('remoteModalLabel').textContent =
|
|
'<?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_ADD', true); ?>';
|
|
}
|
|
|
|
updateTypeFields();
|
|
modal.show();
|
|
}
|
|
|
|
// ---- Type selector toggles field visibility ----
|
|
document.getElementById('remoteType').addEventListener('change', updateTypeFields);
|
|
|
|
function updateTypeFields() {
|
|
const type = document.getElementById('remoteType').value;
|
|
document.querySelectorAll('.remote-type-fields').forEach(function(el) {
|
|
el.style.display = 'none';
|
|
});
|
|
const target = document.getElementById('remoteFields_' + type);
|
|
if (target) target.style.display = '';
|
|
|
|
// SFTP auth_type sub-fields
|
|
if (type === 'sftp') {
|
|
updateSftpAuthFields();
|
|
}
|
|
}
|
|
|
|
const sftpAuthType = document.getElementById('remoteCfg_sftp_auth_type');
|
|
if (sftpAuthType) {
|
|
sftpAuthType.addEventListener('change', updateSftpAuthFields);
|
|
}
|
|
|
|
function updateSftpAuthFields() {
|
|
const auth = document.getElementById('remoteCfg_sftp_auth_type').value;
|
|
document.getElementById('remoteSftpPasswordWrap').style.display = (auth === 'password') ? '' : 'none';
|
|
document.getElementById('remoteSftpKeyWrap').style.display = (auth === 'key' || auth === 'key_passphrase') ? '' : 'none';
|
|
document.getElementById('remoteSftpPassphraseWrap').style.display = (auth === 'key_passphrase') ? '' : 'none';
|
|
}
|
|
|
|
// ---- Save remote ----
|
|
document.getElementById('btnSaveRemote').addEventListener('click', function() {
|
|
const type = document.getElementById('remoteType').value;
|
|
const title = document.getElementById('remoteTitle').value.trim();
|
|
|
|
if (!title) {
|
|
document.getElementById('remoteTitle').focus();
|
|
return;
|
|
}
|
|
|
|
// Build config object from visible fields
|
|
const config = {};
|
|
const prefix = fieldPrefix[type] || '';
|
|
const fields = configFields[type] || [];
|
|
|
|
fields.forEach(function(f) {
|
|
const el = document.getElementById('remoteCfg_' + prefix + f);
|
|
if (el) {
|
|
config[f] = el.value;
|
|
}
|
|
});
|
|
|
|
const body = new URLSearchParams();
|
|
body.set(token, '1');
|
|
body.set('remote_id', document.getElementById('remoteEditId').value);
|
|
body.set('profile_id', profileId);
|
|
body.set('remote_title', title);
|
|
body.set('remote_type', type);
|
|
body.set('remote_enabled', document.getElementById('remoteEnabled').checked ? '1' : '0');
|
|
body.set('remote_keep_local', document.getElementById('remoteKeepLocal').checked ? '1' : '0');
|
|
body.set('remote_config', JSON.stringify(config));
|
|
|
|
document.getElementById('btnSaveRemote').disabled = true;
|
|
|
|
fetch(baseUrl + 'saveRemote', {
|
|
method: 'POST',
|
|
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
|
body: body
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
document.getElementById('btnSaveRemote').disabled = false;
|
|
|
|
if (data.error) {
|
|
alert(data.message || 'Save failed');
|
|
return;
|
|
}
|
|
|
|
modal.hide();
|
|
loadRemotes();
|
|
})
|
|
.catch(() => {
|
|
document.getElementById('btnSaveRemote').disabled = false;
|
|
alert('Network error');
|
|
});
|
|
});
|
|
|
|
// Initial load
|
|
loadRemotes();
|
|
});
|
|
</script>
|
|
<?php endif; ?>
|