feat: purge, CPanel module, 7z format, SFTP browser (#119, #105, #122, #98)
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 10s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 3s
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 4s
Universal: PR Check / Validate PR (pull_request) Failing after 3s
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 13s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 4m50s
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
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Failing after 10s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 3s
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 4s
Universal: PR Check / Validate PR (pull_request) Failing after 3s
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 13s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 4m50s
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
#119: Manual purge — toolbar button opens modal with date picker, AJAX count preview, confirmation before bulk delete. #105: CPanel admin dashboard module (mod_mokosuitebackup_cpanel) — backup status, quick action buttons per profile, next scheduled, stats, and quick links. Registered in package manifest. #122: 7z archive format via system 7za/7z CLI binary with optional password encryption. New SevenZipArchiver engine class. #98: SFTP remote file browser — custom SftpPathField with "Browse Remote" button, modal directory listing via SSH ls, click to navigate, double-click to select. Also: CHANGELOG updated, wiki Home updated, #121 verified (encryption field already visible in Archive Settings tab). Closes #119, closes #105, closes #122, closes #98, closes #121
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
<?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
|
||||
*
|
||||
* SFTP remote path field with Browse Remote button and modal directory browser.
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Field;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Form\FormField;
|
||||
|
||||
class SftpPathField extends FormField
|
||||
{
|
||||
protected $type = 'SftpPath';
|
||||
|
||||
protected function getInput(): string
|
||||
{
|
||||
$value = htmlspecialchars($this->value ?: $this->default, ENT_QUOTES, 'UTF-8');
|
||||
$id = htmlspecialchars($this->id, ENT_QUOTES, 'UTF-8');
|
||||
$name = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
return <<<HTML
|
||||
<div class="input-group">
|
||||
<input type="text" name="{$name}" id="{$id}" value="{$value}"
|
||||
class="form-control" maxlength="512"
|
||||
placeholder="/backups" />
|
||||
<button type="button" class="btn btn-outline-secondary" id="{$id}_browseBtn"
|
||||
title="Browse directories on the remote SFTP server">
|
||||
<span class="icon-folder-open" aria-hidden="true"></span>
|
||||
Browse Remote
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal fade" id="{$id}_sftpModal" tabindex="-1" aria-labelledby="{$id}_sftpModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="{$id}_sftpModalLabel">
|
||||
<span class="icon-folder-open" aria-hidden="true"></span>
|
||||
Browse Remote SFTP Directory
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="{$id}_sftpStatus" class="mb-2">
|
||||
<small class="text-muted">Click "Browse Remote" to connect...</small>
|
||||
</div>
|
||||
<div id="{$id}_sftpCurrent" class="mb-2 p-2 bg-light border rounded" style="font-family:monospace; font-size:0.85rem;">
|
||||
/
|
||||
</div>
|
||||
<div id="{$id}_sftpTree" class="border rounded" style="max-height:350px; overflow-y:auto;">
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">
|
||||
Click a directory to navigate into it. Click "Select This Directory" to use the current path.
|
||||
<br>SFTP credentials must be saved in the profile before browsing.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="{$id}_sftpSelect">
|
||||
<span class="icon-checkmark" aria-hidden="true"></span>
|
||||
Select This Directory
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
var fieldId = '{$id}';
|
||||
var input = document.getElementById(fieldId);
|
||||
var browseBtn = document.getElementById(fieldId + '_browseBtn');
|
||||
var modalEl = document.getElementById(fieldId + '_sftpModal');
|
||||
var treeEl = document.getElementById(fieldId + '_sftpTree');
|
||||
var statusEl = document.getElementById(fieldId + '_sftpStatus');
|
||||
var currentEl = document.getElementById(fieldId + '_sftpCurrent');
|
||||
var selectBtn = document.getElementById(fieldId + '_sftpSelect');
|
||||
var currentPath = '/';
|
||||
|
||||
function getProfileId() {
|
||||
var el = document.getElementById('jform_id');
|
||||
return el ? parseInt(el.value, 10) || 0 : 0;
|
||||
}
|
||||
|
||||
function showModal() {
|
||||
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
|
||||
var modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||
modal.show();
|
||||
}
|
||||
}
|
||||
|
||||
function hideModal() {
|
||||
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
|
||||
var modal = bootstrap.Modal.getInstance(modalEl);
|
||||
if (modal) modal.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the status message using safe DOM methods (no innerHTML).
|
||||
* @param {string} cssClass - CSS class for the small element
|
||||
* @param {string} iconClass - Icon CSS class (e.g. 'icon-spinner icon-spin'), or empty
|
||||
* @param {string} text - Plain text message
|
||||
*/
|
||||
function setStatus(cssClass, iconClass, text) {
|
||||
while (statusEl.firstChild) statusEl.removeChild(statusEl.firstChild);
|
||||
var small = document.createElement('small');
|
||||
small.className = cssClass;
|
||||
if (iconClass) {
|
||||
var icon = document.createElement('span');
|
||||
icon.className = iconClass;
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
small.appendChild(icon);
|
||||
small.appendChild(document.createTextNode(' '));
|
||||
}
|
||||
small.appendChild(document.createTextNode(text));
|
||||
statusEl.appendChild(small);
|
||||
}
|
||||
|
||||
function loadSftpDir(path) {
|
||||
currentPath = path;
|
||||
currentEl.textContent = path;
|
||||
while (treeEl.firstChild) treeEl.removeChild(treeEl.firstChild);
|
||||
setStatus('text-muted', 'icon-spinner icon-spin', 'Connecting to remote server...');
|
||||
|
||||
var profileId = getProfileId();
|
||||
if (!profileId) {
|
||||
setStatus('text-danger', '', 'Please save the profile first so SFTP credentials are available.');
|
||||
return;
|
||||
}
|
||||
|
||||
var form = new URLSearchParams();
|
||||
form.append('task', 'ajax.browseSftpDir');
|
||||
form.append('profile_id', profileId);
|
||||
form.append('path', path);
|
||||
|
||||
var tokenName = Joomla.getOptions('csrf.token') || '';
|
||||
if (tokenName) form.append(tokenName, '1');
|
||||
|
||||
fetch('index.php?option=com_mokosuitebackup&format=json', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(function(r) {
|
||||
if (!r.ok) throw new Error('Server error (HTTP ' + r.status + ')');
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.error) {
|
||||
setStatus('text-danger', 'icon-warning', data.message || 'Error');
|
||||
return;
|
||||
}
|
||||
var count = data.dirs ? data.dirs.length : 0;
|
||||
setStatus('text-success', 'icon-publish', 'Connected \u2014 ' + count + ' subdirectories');
|
||||
currentPath = data.current || path;
|
||||
currentEl.textContent = currentPath;
|
||||
renderSftpTree(data);
|
||||
})
|
||||
.catch(function(err) {
|
||||
setStatus('text-danger', 'icon-warning', err.message);
|
||||
});
|
||||
}
|
||||
|
||||
function renderSftpTree(data) {
|
||||
while (treeEl.firstChild) treeEl.removeChild(treeEl.firstChild);
|
||||
var list = document.createElement('div');
|
||||
list.className = 'list-group list-group-flush';
|
||||
|
||||
/* Parent / back button */
|
||||
if (data.parent !== null && data.parent !== undefined) {
|
||||
var up = document.createElement('a');
|
||||
up.href = '#';
|
||||
up.className = 'list-group-item list-group-item-action py-1';
|
||||
var upIcon = document.createElement('span');
|
||||
upIcon.className = 'icon-arrow-up-4';
|
||||
upIcon.setAttribute('aria-hidden', 'true');
|
||||
up.appendChild(upIcon);
|
||||
up.appendChild(document.createTextNode(' .. (parent directory)'));
|
||||
up.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
loadSftpDir(data.parent);
|
||||
});
|
||||
list.appendChild(up);
|
||||
}
|
||||
|
||||
/* Directory entries */
|
||||
var dirs = data.dirs || [];
|
||||
|
||||
dirs.forEach(function(dir) {
|
||||
var item = document.createElement('a');
|
||||
item.href = '#';
|
||||
item.className = 'list-group-item list-group-item-action py-1';
|
||||
var folderIcon = document.createElement('span');
|
||||
folderIcon.className = 'icon-folder';
|
||||
folderIcon.setAttribute('aria-hidden', 'true');
|
||||
item.appendChild(folderIcon);
|
||||
item.appendChild(document.createTextNode(' ' + dir.name));
|
||||
|
||||
item.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
loadSftpDir(dir.path);
|
||||
});
|
||||
|
||||
/* Double-click to select and close */
|
||||
item.addEventListener('dblclick', function(e) {
|
||||
e.preventDefault();
|
||||
input.value = dir.path;
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
hideModal();
|
||||
});
|
||||
|
||||
list.appendChild(item);
|
||||
});
|
||||
|
||||
if (dirs.length === 0) {
|
||||
var empty = document.createElement('div');
|
||||
empty.className = 'list-group-item text-muted py-2';
|
||||
empty.textContent = '(no subdirectories)';
|
||||
list.appendChild(empty);
|
||||
}
|
||||
|
||||
treeEl.appendChild(list);
|
||||
}
|
||||
|
||||
/* Browse button click */
|
||||
browseBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
var startPath = input.value.trim() || '/';
|
||||
showModal();
|
||||
loadSftpDir(startPath);
|
||||
});
|
||||
|
||||
/* Select button — use the current directory */
|
||||
selectBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
input.value = currentPath;
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
hideModal();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user