d6b3e8cff0
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 23s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 13s
Universal: Build & Release / Promote to RC (pull_request) Failing after 11s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 34s
SFTP UX improvements: - SshKeyField: file upload button (FileReader → base64 → hidden field), key never displayed as readable text, __KEEP_EXISTING__ sentinel preserves DB value on re-save without re-uploading - Auth type dropdown: password / key file / key file + passphrase with conditional field visibility via showon - Required field markers on host, username, path, password - Remove insecure FTP option from remote storage dropdown Security: - Private key stored base64-encoded in database - SftpUploader decodes base64 before writing temp file - ProfileTable::store() handles sentinel to prevent key leakage - Key content never rendered in HTML form output
110 lines
3.6 KiB
PHP
110 lines
3.6 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
|
|
*
|
|
* Custom field for SSH private key input.
|
|
* Supports both file upload (via FileReader JS) and paste-in textarea.
|
|
* The key content is stored in the database, not as a file on disk.
|
|
*/
|
|
|
|
namespace Joomla\Component\MokoSuiteBackup\Administrator\Field;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
use Joomla\CMS\Form\FormField;
|
|
use Joomla\CMS\Language\Text;
|
|
|
|
class SshKeyField extends FormField
|
|
{
|
|
protected $type = 'SshKey';
|
|
|
|
protected function getInput(): string
|
|
{
|
|
$value = $this->value ?? '';
|
|
$id = $this->id;
|
|
$name = $this->name;
|
|
|
|
$hasKey = !empty($value) && str_contains($value, 'PRIVATE KEY');
|
|
|
|
$html = '<div id="' . htmlspecialchars($id) . '-wrapper">';
|
|
|
|
/* Status badge */
|
|
if ($hasKey) {
|
|
$html .= '<span class="badge bg-success me-2">'
|
|
. '<span class="icon-lock" aria-hidden="true"></span> '
|
|
. Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_LOADED')
|
|
. '</span>';
|
|
}
|
|
|
|
/* File upload button */
|
|
$html .= '<label class="btn btn-outline-secondary btn-sm" for="' . htmlspecialchars($id) . '-file">';
|
|
$html .= '<span class="icon-upload" aria-hidden="true"></span> ';
|
|
$html .= $hasKey ? Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_REPLACE') : Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_UPLOAD');
|
|
$html .= '</label>';
|
|
$html .= '<input type="file" id="' . htmlspecialchars($id) . '-file"'
|
|
. ' accept=".pem,.key,.openssh,.ppk,*" style="display:none;"'
|
|
. ' onchange="mokoSshKeyFileSelected(\'' . htmlspecialchars($id) . '\', this)">';
|
|
|
|
$html .= '<span id="' . htmlspecialchars($id) . '-status" class="ms-2 text-muted small"></span>';
|
|
|
|
if ($hasKey) {
|
|
$html .= ' <button type="button" class="btn btn-sm btn-outline-danger ms-2"'
|
|
. ' onclick="mokoSshKeyClear(\'' . htmlspecialchars($id) . '\')">'
|
|
. '<span class="icon-times" aria-hidden="true"></span> '
|
|
. Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_CLEAR')
|
|
. '</button>';
|
|
}
|
|
|
|
/* Hidden field — key data is NEVER rendered as visible text.
|
|
On existing keys, we submit a sentinel value to preserve the DB value
|
|
unless a new file is uploaded or clear is clicked. */
|
|
if ($hasKey) {
|
|
$html .= '<input type="hidden" name="' . htmlspecialchars($name) . '" id="' . htmlspecialchars($id) . '"'
|
|
. ' value="__KEEP_EXISTING__">';
|
|
} else {
|
|
$html .= '<input type="hidden" name="' . htmlspecialchars($name) . '" id="' . htmlspecialchars($id) . '"'
|
|
. ' value="">';
|
|
}
|
|
|
|
$html .= '</div>';
|
|
$html .= $this->getScript();
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function getScript(): string
|
|
{
|
|
return <<<'JS'
|
|
<script>
|
|
function mokoSshKeyFileSelected(fieldId, input) {
|
|
if (!input.files || !input.files[0]) return;
|
|
var file = input.files[0];
|
|
var reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
/* Base64 encode the key before storing in the hidden field */
|
|
var content = e.target.result;
|
|
var encoded = btoa(content);
|
|
document.getElementById(fieldId).value = encoded;
|
|
var status = document.getElementById(fieldId + '-status');
|
|
if (status) status.textContent = file.name + ' uploaded';
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
function mokoSshKeyClear(fieldId) {
|
|
document.getElementById(fieldId).value = '';
|
|
var status = document.getElementById(fieldId + '-status');
|
|
if (status) status.textContent = 'Key removed';
|
|
var fileInput = document.getElementById(fieldId + '-file');
|
|
if (fileInput) fileInput.value = '';
|
|
}
|
|
</script>
|
|
JS;
|
|
}
|
|
}
|