899a33bc58
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
254 lines
8.0 KiB
PHP
254 lines
8.0 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
|
|
*
|
|
* 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;
|
|
}
|
|
}
|