Compare commits

...

24 Commits

Author SHA1 Message Date
gitea-actions[bot] d9557489d5 chore: promote changelog [Unreleased] → [01.36.00] 2026-06-23 15:48:58 +00:00
gitea-actions[bot] 089ec69595 chore(release): build 01.36.00 [skip ci]
Publish to Composer / Publish Package (release) Failing after 7s
2026-06-23 15:48:50 +00:00
jmiller 7427cbb043 Merge pull request 'feat: Clickable placeholder pills for backup dir and archive name' (#102) from feat/clickable-placeholders into main 2026-06-23 15:48:34 +00:00
Jonathan Miller 456e744d81 feat: clickable placeholder pills for backup dir and archive name fields
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
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: PR Check / Branch Policy (pull_request) Failing after 4s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 12s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 14s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 26s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 9m59s
Both Backup Directory and Archive Name Format fields now show clickable
placeholder tags below the input. Clicking a tag inserts the placeholder
at the current cursor position using selectionStart/End.

- FolderPickerField: pills for [HOME], [DEFAULT_DIR], [host], [site_name],
  [date], [profile_id], [profile_name], [type]
- PlaceholderTextField: new custom field type used by archive_name_format,
  configurable placeholders via XML attribute
- Cursor position preserved after insert, input event dispatched for
  live status updates
2026-06-23 10:47:45 -05:00
gitea-actions[bot] 6d5ef50727 chore: promote changelog [Unreleased] → [01.35.04] 2026-06-23 15:41:32 +00:00
gitea-actions[bot] 00e7963988 chore(release): build 01.35.04 [skip ci]
Publish to Composer / Publish Package (release) Failing after 4s
2026-06-23 15:41:29 +00:00
jmiller bc06657317 Merge pull request 'fix: SFTP fields not showing when SFTP selected' (#99) from fix/sftp-key-upload into main 2026-06-23 15:40:43 +00:00
Jonathan Miller bda4b0a23d Merge branch 'fix/sftp-key-upload' of https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup into fix/sftp-key-upload
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 1s
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 5s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 9s
RC Revert / Rename rc/ back to dev/ (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
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 38s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 44s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 21s
# Conflicts:
#	.mokogitea/workflows/issue-branch.yml
#	README.md
#	source/packages/com_mokosuitebackup/mokosuitebackup.xml
#	source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml
#	source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml
#	source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml
#	source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml
#	source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml
#	source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml
#	source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml
#	source/pkg_mokosuitebackup.xml
2026-06-23 10:40:27 -05:00
Jonathan Miller e327f9cf5c chore: normalize workflows 2026-06-23 10:40:16 -05:00
Jonathan Miller 5b9351e5f0 Merge remote-tracking branch 'origin/main' into fix/sftp-key-upload 2026-06-23 10:38:26 -05:00
gitea-actions[bot] 5785e9fd1e chore(version): pre-release bump to 01.35.04-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 32s
2026-06-23 15:37:59 +00:00
Jonathan Miller 1e9c8d54f4 fix: remove required attr from SFTP showon fields — blocks save when not SFTP
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
Joomla validates required fields server-side regardless of showon
visibility. SFTP fields with required="true" block saving when
remote_storage is set to None or S3/GDrive because the hidden
fields submit empty values. Validation should be done in
ProfileTable::check() conditionally instead.
2026-06-23 10:37:38 -05:00
jmiller 7515274712 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-23 13:54:34 +00:00
jmiller 0be459fe34 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-23 13:54:33 +00:00
jmiller 11ccdbfde4 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-23 13:54:32 +00:00
jmiller fd517c16f3 chore: sync auto-bump.yml from Template-Generic [skip ci] 2026-06-23 13:54:31 +00:00
gitea-actions[bot] fe76f81b47 chore: promote changelog [Unreleased] → [01.35.03] 2026-06-23 13:53:26 +00:00
gitea-actions[bot] 18127454b5 chore(release): build 01.35.03 [skip ci]
Publish to Composer / Publish Package (release) Failing after 5s
2026-06-23 13:53:23 +00:00
jmiller 7826c315b1 Merge pull request 'feat: SFTP key file upload, auth type dropdown, security hardening' (#96) from fix/sftp-key-upload into main 2026-06-23 13:53:03 +00:00
gitea-actions[bot] e329dbd99b chore(version): pre-release bump to 01.35.03-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 7s
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 3s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 20s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 2m33s
2026-06-23 13:52:11 +00:00
Jonathan Miller d6b3e8cff0 feat: SFTP key file upload, auth type dropdown, security hardening
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
2026-06-23 08:51:49 -05:00
gitea-actions[bot] 80c97620a5 chore: promote changelog [Unreleased] → [01.35.01] 2026-06-23 13:33:26 +00:00
gitea-actions[bot] 33d852bacf chore(release): build 01.35.01 [skip ci]
Publish to Composer / Publish Package (release) Failing after 3s
2026-06-23 13:33:24 +00:00
jmiller 8be0500913 Merge pull request 'fix: SFTP fields not showing when SFTP selected' (#95) from fix/sftp-showon into main 2026-06-23 13:32:53 +00:00
21 changed files with 297 additions and 39 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 01.35.00
# VERSION: 01.36.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+6 -18
View File
@@ -1,26 +1,14 @@
# Changelog
## [Unreleased]
## [01.35.00] --- 2026-06-23
## [01.36.00] --- 2026-06-23
## [01.35.00] --- 2026-06-23
## [01.36.00] --- 2026-06-23
### Added
- SFTP remote storage with SSH key file authentication — key stored securely in database
- CLI restore options: --files-only, --db-only, --no-preserve-config, --password
## [01.35.04] --- 2026-06-23
## [01.34.00] --- 2026-06-23
## [01.35.04] --- 2026-06-23
## [01.34.00] --- 2026-06-23
## [01.35.03] --- 2026-06-23
### Added
- Dashboard: snapshot widget, backup trend chart (30 days), and storage breakdown by profile (#61)
## [01.33.00] --- 2026-06-23
## [01.33.00] --- 2026-06-23
### Added
- Backup comparison: select two backups to view side-by-side size, file count, and duration differences (#64)
- Selective article restore: browse articles inside a snapshot and restore individual items (#58)
- Archive browser: view files inside a backup without extracting (#59)
## [01.35.03] --- 2026-06-23
+1 -1
View File
@@ -1,6 +1,6 @@
# MokoSuiteBackup
<!-- VERSION: 01.35.00 -->
<!-- VERSION: 01.36.00 -->
Full-site backup and restore for Joomla — database, files, and configuration.
@@ -72,12 +72,14 @@
/>
<field
name="archive_name_format"
type="text"
type="PlaceholderText"
label="COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT"
description="COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT_DESC"
default="[host]_[datetime]_profile[profile_id]"
maxlength="512"
hint="[host]_[datetime]_profile[profile_id]"
placeholders="[host],[datetime],[date],[time],[year],[month],[day],[hour],[minute],[second],[profile_id],[profile_name],[site_name],[type],[random]"
addfieldprefix="Joomla\Component\MokoSuiteBackup\Administrator\Field"
/>
<field
name="include_mokorestore"
@@ -159,7 +161,6 @@
default="none"
>
<option value="none">COM_MOKOJOOMBACKUP_REMOTE_NONE</option>
<option value="ftp">COM_MOKOJOOMBACKUP_REMOTE_FTP</option>
<option value="sftp">COM_MOKOJOOMBACKUP_REMOTE_SFTP</option>
<option value="google_drive">COM_MOKOJOOMBACKUP_REMOTE_GDRIVE</option>
<option value="s3">COM_MOKOJOOMBACKUP_REMOTE_S3</option>
@@ -203,23 +204,34 @@
maxlength="255"
showon="remote_storage:sftp"
/>
<field
name="sftp_auth_type"
type="list"
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_AUTH_TYPE"
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_AUTH_TYPE_DESC"
default="key"
showon="remote_storage:sftp"
>
<option value="password">COM_MOKOJOOMBACKUP_SFTP_AUTH_PASSWORD</option>
<option value="key">COM_MOKOJOOMBACKUP_SFTP_AUTH_KEY</option>
<option value="key_passphrase">COM_MOKOJOOMBACKUP_SFTP_AUTH_KEY_PASSPHRASE</option>
</field>
<field
name="sftp_password"
type="password"
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD"
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD_DESC"
maxlength="255"
showon="remote_storage:sftp"
showon="remote_storage:sftp[AND]sftp_auth_type:password"
/>
<field
name="sftp_key_data"
type="textarea"
type="SshKey"
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY"
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_DESC"
rows="6"
cols="60"
filter="raw"
showon="remote_storage:sftp"
showon="remote_storage:sftp[AND]sftp_auth_type:key,key_passphrase"
addfieldprefix="Joomla\Component\MokoSuiteBackup\Administrator\Field"
/>
<field
name="sftp_passphrase"
@@ -227,7 +239,7 @@
label="COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE"
description="COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE_DESC"
maxlength="255"
showon="remote_storage:sftp"
showon="remote_storage:sftp[AND]sftp_auth_type:key_passphrase"
/>
<field
name="sftp_path"
@@ -256,7 +256,17 @@ COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME_DESC="Username for SSH authentication"
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD="SSH Password"
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD_DESC="Password for SSH authentication. Leave blank if using a key file."
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY="SSH Private Key"
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_DESC="Paste the contents of your SSH private key (e.g. id_rsa or id_ed25519). The key is stored securely in the database and written to a temp file with 0600 permissions only during upload, then deleted. Leave blank to use password authentication."
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_DESC="Upload or paste your SSH private key (e.g. id_rsa or id_ed25519). The key is stored securely in the database and written to a temp file with 0600 permissions only during upload, then deleted. Leave blank to use password authentication."
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_UPLOAD="Upload Key File"
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_REPLACE="Replace Key"
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_LOADED="Key loaded"
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_NONE="No key file"
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_CLEAR="Remove Key"
COM_MOKOJOOMBACKUP_FIELD_SFTP_AUTH_TYPE="Authentication Type"
COM_MOKOJOOMBACKUP_FIELD_SFTP_AUTH_TYPE_DESC="Choose how to authenticate with the SFTP server."
COM_MOKOJOOMBACKUP_SFTP_AUTH_PASSWORD="Password"
COM_MOKOJOOMBACKUP_SFTP_AUTH_KEY="Key File"
COM_MOKOJOOMBACKUP_SFTP_AUTH_KEY_PASSPHRASE="Key File + Passphrase"
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE="Key Passphrase"
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE_DESC="Passphrase for the private key, if encrypted. Leave blank for unencrypted keys."
COM_MOKOJOOMBACKUP_FIELD_SFTP_PATH="Remote Path"
@@ -7,7 +7,7 @@
-->
<extension type="component" method="upgrade">
<name>MokoSuiteBackup</name>
<version>01.35.01</version>
<version>01.36.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
`sftp_host` VARCHAR(255) NOT NULL DEFAULT '',
`sftp_port` INT(5) UNSIGNED NOT NULL DEFAULT 22,
`sftp_username` VARCHAR(255) NOT NULL DEFAULT '',
`sftp_auth_type` VARCHAR(20) NOT NULL DEFAULT 'key',
`sftp_password` VARCHAR(255) NOT NULL DEFAULT '',
`sftp_key_data` MEDIUMTEXT,
`sftp_passphrase` VARCHAR(255) NOT NULL DEFAULT '',
@@ -0,0 +1,4 @@
-- MokoSuiteBackup 01.36.00 — SFTP auth type column
ALTER TABLE `#__mokosuitebackup_profiles`
ADD COLUMN `sftp_auth_type` VARCHAR(20) NOT NULL DEFAULT 'key' AFTER `sftp_username`;
@@ -141,7 +141,15 @@ class SftpUploader implements RemoteUploaderInterface
$tmpDir = sys_get_temp_dir();
$keyFile = $tmpDir . '/mokobackup-sftp-' . bin2hex(random_bytes(8)) . '.key';
if (file_put_contents($keyFile, $this->keyData) === false) {
/* Key is stored base64-encoded in the database — decode before writing */
$keyContent = base64_decode($this->keyData, true);
if ($keyContent === false) {
/* Fallback: might be raw PEM (legacy or paste) */
$keyContent = $this->keyData;
}
if (file_put_contents($keyFile, $keyContent) === false) {
throw new \RuntimeException('Cannot write temporary SSH key file');
}
@@ -100,6 +100,17 @@ class FolderPickerField extends FormField
<span class="icon-question-circle" aria-hidden="true"></span>
</button>
</div>
<div class="mt-1 mb-1" id="{$id}_placeholders" style="display:flex; flex-wrap:wrap; gap:4px;">
<span class="text-muted small me-1" style="line-height:24px;">Insert:</span>
<button type="button" class="btn btn-outline-secondary btn-sm py-0 px-1 moko-ph-insert" data-field="{$id}" data-ph="[HOME]" title="Home directory">[HOME]</button>
<button type="button" class="btn btn-outline-secondary btn-sm py-0 px-1 moko-ph-insert" data-field="{$id}" data-ph="[DEFAULT_DIR]" title="Default backup dir">[DEFAULT_DIR]</button>
<button type="button" class="btn btn-outline-secondary btn-sm py-0 px-1 moko-ph-insert" data-field="{$id}" data-ph="[host]" title="Server hostname">[host]</button>
<button type="button" class="btn btn-outline-secondary btn-sm py-0 px-1 moko-ph-insert" data-field="{$id}" data-ph="[site_name]" title="Joomla site name">[site_name]</button>
<button type="button" class="btn btn-outline-secondary btn-sm py-0 px-1 moko-ph-insert" data-field="{$id}" data-ph="[date]" title="Date (Ymd)">[date]</button>
<button type="button" class="btn btn-outline-secondary btn-sm py-0 px-1 moko-ph-insert" data-field="{$id}" data-ph="[profile_id]" title="Profile ID">[profile_id]</button>
<button type="button" class="btn btn-outline-secondary btn-sm py-0 px-1 moko-ph-insert" data-field="{$id}" data-ph="[profile_name]" title="Profile name">[profile_name]</button>
<button type="button" class="btn btn-outline-secondary btn-sm py-0 px-1 moko-ph-insert" data-field="{$id}" data-ph="[type]" title="Backup type">[type]</button>
</div>
<div class="mt-1" id="{$id}_status">
<small class="{$statusClass}">
<span class="{$statusIcon}" aria-hidden="true"></span>
@@ -155,6 +166,26 @@ class FolderPickerField extends FormField
</div>
<script>
(function() {
/* Clickable placeholder insertion at cursor position */
document.querySelectorAll('.moko-ph-insert[data-field="{$id}"]').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.preventDefault();
var target = document.getElementById(this.getAttribute('data-field'));
var ph = this.getAttribute('data-ph');
if (!target) return;
var start = target.selectionStart || 0;
var end = target.selectionEnd || 0;
var val = target.value;
target.value = val.substring(0, start) + ph + val.substring(end);
/* Move cursor to after the inserted placeholder */
var newPos = start + ph.length;
target.setSelectionRange(newPos, newPos);
target.focus();
/* Trigger input event so status updates */
target.dispatchEvent(new Event('input', { bubbles: true }));
});
});
var fieldId = '{$id}';
var btn = document.getElementById(fieldId + '_btn');
var browser = document.getElementById(fieldId + '_browser');
@@ -0,0 +1,78 @@
<?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
*
* Text field with clickable placeholder pills that insert at cursor position.
* Used for backup directory and archive name format fields.
*/
namespace Joomla\Component\MokoSuiteBackup\Administrator\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Form\FormField;
class PlaceholderTextField extends FormField
{
protected $type = 'PlaceholderText';
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');
$hint = htmlspecialchars($this->element['hint'] ?? '', ENT_QUOTES, 'UTF-8');
$max = (int) ($this->element['maxlength'] ?? 512);
$placeholderAttr = (string) ($this->element['placeholders'] ?? '');
$placeholders = array_filter(array_map('trim', explode(',', $placeholderAttr)));
if (empty($placeholders)) {
$placeholders = ['[host]', '[date]', '[datetime]', '[time]', '[year]', '[month]', '[day]',
'[hour]', '[minute]', '[second]', '[profile_id]', '[profile_name]', '[site_name]', '[type]', '[random]'];
}
$html = '<input type="text" name="' . $name . '" id="' . $id . '" value="' . $value . '"'
. ' class="form-control" maxlength="' . $max . '"'
. ($hint ? ' placeholder="' . $hint . '"' : '') . '>';
$html .= '<div class="mt-1" style="display:flex; flex-wrap:wrap; gap:4px;">';
$html .= '<span class="text-muted small me-1" style="line-height:24px;">Insert:</span>';
foreach ($placeholders as $ph) {
$html .= '<button type="button" class="btn btn-outline-secondary btn-sm py-0 px-1 moko-ph-insert"'
. ' data-field="' . $id . '" data-ph="' . htmlspecialchars($ph) . '">'
. htmlspecialchars($ph) . '</button>';
}
$html .= '</div>';
$html .= <<<JS
<script>
document.querySelectorAll('.moko-ph-insert[data-field="{$id}"]').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.preventDefault();
var target = document.getElementById(this.getAttribute('data-field'));
var ph = this.getAttribute('data-ph');
if (!target) return;
var start = target.selectionStart || 0;
var end = target.selectionEnd || 0;
var val = target.value;
target.value = val.substring(0, start) + ph + val.substring(end);
var newPos = start + ph.length;
target.setSelectionRange(newPos, newPos);
target.focus();
target.dispatchEvent(new Event('input', { bubbles: true }));
});
});
</script>
JS;
return $html;
}
}
@@ -0,0 +1,109 @@
<?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;
}
}
@@ -25,6 +25,23 @@ class ProfileTable extends Table
public function store($updateNulls = true): bool
{
/* Handle SSH key sentinel — when __KEEP_EXISTING__ is submitted,
preserve the current DB value instead of overwriting with the sentinel.
This prevents the key from being exposed in the form HTML. */
if (isset($this->sftp_key_data) && $this->sftp_key_data === '__KEEP_EXISTING__') {
if ($this->id) {
$db = $this->getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('sftp_key_data'))
->from($db->quoteName($this->_tbl))
->where($db->quoteName('id') . ' = ' . (int) $this->id);
$db->setQuery($query);
$this->sftp_key_data = $db->loadResult() ?: '';
} else {
$this->sftp_key_data = '';
}
}
$result = parent::store($updateNulls);
if ($result && !empty($this->backup_dir)) {
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="actionlog" method="upgrade">
<name>Action Log - MokoSuiteBackup</name>
<version>01.35.01</version>
<version>01.36.00</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="console" method="upgrade">
<name>Console - MokoSuiteBackup</name>
<version>01.35.01</version>
<version>01.36.00</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="content" method="upgrade">
<name>Content - MokoSuiteBackup</name>
<version>01.35.01</version>
<version>01.36.00</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="quickicon" method="upgrade">
<name>Quick Icon - MokoSuiteBackup</name>
<version>01.35.01</version>
<version>01.36.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoSuiteBackup</name>
<version>01.35.01</version>
<version>01.36.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="task" method="upgrade">
<name>Task - MokoSuiteBackup</name>
<version>01.35.01</version>
<version>01.36.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoSuiteBackup</name>
<version>01.35.01</version>
<version>01.36.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+1 -1
View File
@@ -8,7 +8,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoSuiteBackup</name>
<packagename>mokosuitebackup</packagename>
<version>01.35.01</version>
<version>01.36.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>