Compare commits

..

19 Commits

Author SHA1 Message Date
gitea-actions[bot] aaf189b87a chore: promote changelog [Unreleased] → [01.38.05] 2026-06-23 16:59:10 +00:00
gitea-actions[bot] 61023821e6 chore(release): build 01.38.05 [skip ci]
Publish to Composer / Publish Package (release) Failing after 34s
2026-06-23 16:59:02 +00:00
jmiller 02a6e30db1 Merge pull request 'feat: Comprehensive help modal for backup directory + fix help button' (#128) from fix/folder-picker-tooltip into main 2026-06-23 16:58:45 +00:00
gitea-actions[bot] 5a0cd51df6 chore(version): pre-release bump to 01.38.05-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 4s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 3s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 5s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 33s
2026-06-23 16:58:33 +00:00
Jonathan Miller 12c832d7fe feat: comprehensive help modal for backup directory field
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 3s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Universal: PR Check / Secret Scan (pull_request) Successful in 4s
Universal: Build & Release / Promote to RC (pull_request) Failing after 8s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 43s
Expanded the help modal with:
- Absolute paths: starts with / or drive letter, used as-is
- Relative paths: ./backups, ../backups, ../../backups with table
  showing URL-style conventions and resolved examples
- Placeholder paths: detailed descriptions of each placeholder
  with current server values
- Recommended configurations table: single site, multi-site,
  date-organized, per-profile, shared hosting
- Security warnings for web-root-accessible paths
- Help button uses JS click handler with Bootstrap 5 fallback
  (fixes non-working tooltip icon)
2026-06-23 11:58:20 -05:00
gitea-actions[bot] 65c8820db4 chore: promote changelog [Unreleased] → [01.38.04] 2026-06-23 16:53:45 +00:00
gitea-actions[bot] 0f914c3061 chore(release): build 01.38.04 [skip ci]
Publish to Composer / Publish Package (release) Failing after 42s
2026-06-23 16:53:42 +00:00
jmiller 4191f44c1b Merge pull request 'feat: Uppercase all placeholders + EXAMPLE prefix in display' (#127) from fix/uppercase-placeholders into main 2026-06-23 16:53:28 +00:00
gitea-actions[bot] fb99afbeba chore(version): pre-release bump to 01.38.04-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 1s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 3s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 17s
Publish to Composer / Publish Package (release) Failing after 33s
2026-06-23 16:53:16 +00:00
Jonathan Miller de632e9c5c feat: uppercase all placeholders + EXAMPLE prefix in resolution display
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
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 10s
Universal: PR Check / Validate PR (pull_request) Failing after 11s
Universal: PR Check / Secret Scan (pull_request) Successful in 12s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 14s
Universal: Build & Release / Promote to RC (pull_request) Failing after 10s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 47s
All placeholders changed from lowercase to UPPERCASE:
[host] → [HOST], [site_name] → [SITE_NAME], [date] → [DATE],
[datetime] → [DATETIME], [profile_id] → [PROFILE_ID], etc.

[HOME] and [DEFAULT_DIR] were already uppercase — now consistent.

SQL migration 01.39.01 updates existing profile data in the database.
Resolution display prefixed with "EXAMPLE:" to clarify these are
example values resolved at backup time.

13 files updated across engines, fields, forms, templates, and SQL.
2026-06-23 11:52:52 -05:00
gitea-actions[bot] 53ff99148c chore: promote changelog [Unreleased] → [01.38.03] 2026-06-23 16:50:30 +00:00
gitea-actions[bot] c2ff3b272a chore(release): build 01.38.03 [skip ci]
Publish to Composer / Publish Package (release) Failing after 4s
2026-06-23 16:50:27 +00:00
jmiller 747b68c179 Merge pull request 'fix: Resolve [site_name] and all placeholders in checkDir AJAX' (#126) from fix/site-name-resolution into main 2026-06-23 16:50:07 +00:00
gitea-actions[bot] cbff40d04c chore(version): pre-release bump to 01.38.03-dev [skip ci]
Publish to Composer / Publish Package (release) Failing after 7s
Universal: Build & Release / Promote to RC (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
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 6s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 21s
2026-06-23 16:49:45 +00:00
Jonathan Miller e415e701cd fix: resolve [site_name] and other placeholders in checkDir AJAX
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
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 3s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 15s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 8s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Universal: Build & Release / Promote to RC (pull_request) Failing after 15s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 46s
BackupDirectory::resolve() only handles [HOME] and [DEFAULT_DIR].
The checkDir AJAX endpoint now uses PlaceholderResolver to also
resolve [site_name], [host], [profile_id], [date], etc. before
checking if the directory exists. This makes the "Resolves to"
display accurate for all placeholder types.
2026-06-23 11:49:21 -05:00
jmiller d184ed9de0 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-23 16:38:26 +00:00
jmiller 297f27c807 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-23 16:38:26 +00:00
jmiller 30e8d7baa9 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-23 16:38:25 +00:00
jmiller efc5754bef chore: sync auto-bump.yml from Template-Generic [skip ci] 2026-06-23 16:38:23 +00:00
26 changed files with 271 additions and 109 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 01.38.02
# VERSION: 01.38.05
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+6 -11
View File
@@ -1,19 +1,14 @@
# Changelog
## [Unreleased]
## [01.38.02] --- 2026-06-23
## [01.38.05] --- 2026-06-23
## [01.38.02] --- 2026-06-23
## [01.38.05] --- 2026-06-23
## [01.38.01] --- 2026-06-23
## [01.38.04] --- 2026-06-23
## [01.38.01] --- 2026-06-23
## [01.38.04] --- 2026-06-23
## [01.38.00] --- 2026-06-23
## [01.38.03] --- 2026-06-23
## [01.38.00] --- 2026-06-23
### Added
- Standalone restore script mode — restore.php as separate file that scans for backup ZIPs in its directory (#107)
- MokoRestore profile option: None / Wrapped / Standalone
- Standalone mode uploads restore.php alongside backup to remote storage
## [01.38.03] --- 2026-06-23
+1 -1
View File
@@ -1,6 +1,6 @@
# MokoSuiteBackup
<!-- VERSION: 01.38.02 -->
<!-- VERSION: 01.38.05 -->
Full-site backup and restore for Joomla — database, files, and configuration.
@@ -75,10 +75,10 @@
type="PlaceholderText"
label="COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT"
description="COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT_DESC"
default="[host]_[datetime]_profile[profile_id]"
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]"
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
@@ -130,9 +130,9 @@ COM_MOKOJOOMBACKUP_FIELD_ENCRYPTION_PASSWORD_DESC="Set a password to encrypt the
COM_MOKOJOOMBACKUP_FIELD_SPLIT_SIZE="Split Size (MB)"
COM_MOKOJOOMBACKUP_FIELD_SPLIT_SIZE_DESC="Split archive into parts of this size in MB. 0 = no splitting."
COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR="Backup Directory"
COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC="Directory where backup archives are stored. Supports placeholders: [HOME] (user home directory), [host], [date], [year], [month], [day], [profile_name], [site_name], [type]. Use [HOME]/backups to store outside the web root. Absolute paths (starting with /) are used as-is; relative paths resolve from the Joomla root."
COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC="Directory where backup archives are stored. Supports placeholders: [HOME] (user home directory), [HOST], [DATE], [YEAR], [MONTH], [DAY], [PROFILE_NAME], [SITE_NAME], [TYPE]. Use [HOME]/backups to store outside the web root. Absolute paths (starting with /) are used as-is; relative paths resolve from the Joomla root."
COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT="Archive Name Format"
COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT_DESC="Filename template for backup archives (without extension). Placeholders: [host] hostname, [date] Ymd, [time] His, [datetime] Ymd_His, [year] [month] [day] [hour] [minute] [second], [profile_id], [profile_name], [site_name], [type], [random]."
COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT_DESC="Filename template for backup archives (without extension). Placeholders: [HOST] hostname, [DATE] Ymd, [TIME] His, [DATETIME] Ymd_His, [YEAR] [MONTH] [DAY] [HOUR] [MINUTE] [SECOND], [PROFILE_ID], [PROFILE_NAME], [SITE_NAME], [TYPE], [RANDOM]."
COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE="MokoRestore Script"
COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC="Include the MokoRestore standalone restore wizard. 'Wrapped' bundles it inside the backup ZIP. 'Standalone' generates a separate restore.php that scans for backup ZIPs in its directory — ideal for remote servers."
COM_MOKOJOOMBACKUP_MOKORESTORE_NONE="None"
@@ -7,7 +7,7 @@
-->
<extension type="component" method="upgrade">
<name>MokoSuiteBackup</name>
<version>01.38.02</version>
<version>01.38.05</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
`compression_level` TINYINT(1) UNSIGNED NOT NULL DEFAULT 5 COMMENT '0=none, 9=max',
`split_size` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '0=no split, otherwise MB per part',
`backup_dir` VARCHAR(512) NOT NULL DEFAULT '[DEFAULT_DIR]',
`archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders',
`archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[HOST]_[DATETIME]_profile[PROFILE_ID]' COMMENT 'Filename format with placeholders',
`exclude_dirs` TEXT NOT NULL COMMENT 'Newline-separated directory paths to exclude',
`exclude_files` TEXT NOT NULL COMMENT 'Newline-separated filename patterns to exclude',
`exclude_tables` TEXT NOT NULL COMMENT 'Newline-separated table names to exclude',
@@ -9,4 +9,4 @@ ALTER TABLE `#__mokosuitebackup_records` MODIFY `log` MEDIUMTEXT DEFAULT NULL;
ALTER TABLE `#__mokosuitebackup_profiles` ADD COLUMN `notify_user_groups` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Comma-separated Joomla user group IDs' AFTER `notify_email`;
-- Add archive_name_format column with placeholder support
ALTER TABLE `#__mokosuitebackup_profiles` ADD COLUMN `archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders' AFTER `backup_dir`;
ALTER TABLE `#__mokosuitebackup_profiles` ADD COLUMN `archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[HOST]_[DATETIME]_profile[PROFILE_ID]' COMMENT 'Filename format with placeholders' AFTER `backup_dir`;
@@ -0,0 +1,34 @@
-- MokoSuiteBackup 01.39.01 — Uppercase all placeholders in profile data
UPDATE `#__mokosuitebackup_profiles` SET
`archive_name_format` = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
`archive_name_format`,
'[host]', '[HOST]'),
'[site_name]', '[SITE_NAME]'),
'[datetime]', '[DATETIME]'),
'[date]', '[DATE]'),
'[time]', '[TIME]'),
'[year]', '[YEAR]'),
'[month]', '[MONTH]'),
'[day]', '[DAY]'),
'[hour]', '[HOUR]'),
'[minute]', '[MINUTE]'),
'[second]', '[SECOND]'),
'[profile_id]', '[PROFILE_ID]'),
'[profile_name]', '[PROFILE_NAME]'),
'[type]', '[TYPE]'),
'[random]', '[RANDOM]')
WHERE `archive_name_format` REGEXP '\\[[a-z]';
UPDATE `#__mokosuitebackup_profiles` SET
`backup_dir` = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
`backup_dir`,
'[host]', '[HOST]'),
'[site_name]', '[SITE_NAME]'),
'[date]', '[DATE]'),
'[year]', '[YEAR]'),
'[month]', '[MONTH]'),
'[day]', '[DAY]'),
'[profile_id]', '[PROFILE_ID]'),
'[profile_name]', '[PROFILE_NAME]')
WHERE `backup_dir` REGEXP '\\[[a-z]';
@@ -15,8 +15,10 @@ namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Session\Session;
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\PlaceholderResolver;
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\SteppedBackupEngine;
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\SteppedRestoreEngine;
use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
@@ -283,7 +285,32 @@ class AjaxController extends BaseController
return;
}
$resolved = BackupDirectory::resolve($rawPath);
/* Resolve all placeholders — both directory ([HOME], [DEFAULT_DIR])
and name-level ([SITE_NAME], [HOST], [PROFILE_ID], etc.) */
$profileId = $this->input->getInt('profile_id', 0);
if ($profileId > 0) {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuitebackup_profiles'))
->where($db->quoteName('id') . ' = ' . $profileId);
$db->setQuery($query);
$profile = $db->loadObject();
}
if (empty($profile)) {
/* No profile context — create a minimal dummy for PlaceholderResolver */
$profile = (object) [
'id' => 1,
'title' => 'default',
'backup_type' => 'full',
];
}
$resolver = new PlaceholderResolver($profile);
$withNamePlaceholders = $resolver->resolve($rawPath);
$resolved = BackupDirectory::resolve($withNamePlaceholders);
if (BackupDirectory::hasPlaceholders($resolved)) {
$this->sendJson([
@@ -88,7 +88,7 @@ class BackupEngine
$archiveName = '';
$archiver = $this->createArchiver($archiveFormat);
$archiveExt = $archiver->getExtension();
$nameFormat = $profile->archive_name_format ?? '[host]_[datetime]_profile[profile_id]';
$nameFormat = $profile->archive_name_format ?? '[HOST]_[DATETIME]_profile[PROFILE_ID]';
$archiveName = $resolver->resolve($nameFormat) . '.' . $archiveExt;
if (empty($description)) {
@@ -7,7 +7,7 @@
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* Resolves placeholders like [host], [date], [profile_name] in backup
* Resolves placeholders like [HOST], [DATE], [PROFILE_NAME] in backup
* directory paths and archive filename formats.
*/
@@ -24,21 +24,21 @@ class PlaceholderResolver
* Supported placeholders and their descriptions (for documentation).
*/
public const PLACEHOLDERS = [
'[host]' => 'Server hostname',
'[date]' => 'Date as Ymd (e.g. 20260604)',
'[time]' => 'Time as His (e.g. 143025)',
'[datetime]' => 'Date and time as Ymd_His',
'[year]' => 'Four-digit year',
'[month]' => 'Two-digit month',
'[day]' => 'Two-digit day',
'[hour]' => 'Two-digit hour (24h)',
'[minute]' => 'Two-digit minute',
'[second]' => 'Two-digit second',
'[profile_id]' => 'Backup profile ID',
'[profile_name]' => 'Profile title (sanitized)',
'[site_name]' => 'Joomla site name (sanitized)',
'[type]' => 'Backup type (full, database, files, differential)',
'[random]' => 'Random 6-character hex string',
'[HOST]' => 'Server hostname',
'[DATE]' => 'Date as Ymd (e.g. 20260604)',
'[TIME]' => 'Time as His (e.g. 143025)',
'[DATETIME]' => 'Date and time as Ymd_His',
'[YEAR]' => 'Four-digit year',
'[MONTH]' => 'Two-digit month',
'[DAY]' => 'Two-digit day',
'[HOUR]' => 'Two-digit hour (24h)',
'[MINUTE]' => 'Two-digit minute',
'[SECOND]' => 'Two-digit second',
'[PROFILE_ID]' => 'Backup profile ID',
'[PROFILE_NAME]' => 'Profile title (sanitized)',
'[SITE_NAME]' => 'Joomla site name (sanitized)',
'[TYPE]' => 'Backup type (full, database, files, differential)',
'[RANDOM]' => 'Random 6-character hex string',
'[DEFAULT_DIR]' => 'Default backup directory',
'[HOME]' => 'Home directory of the PHP process owner',
];
@@ -62,21 +62,21 @@ class PlaceholderResolver
}
$this->replacements = [
'[host]' => $hostname,
'[date]' => $now->format('Ymd'),
'[time]' => $now->format('His'),
'[datetime]' => $now->format('Ymd_His'),
'[year]' => $now->format('Y'),
'[month]' => $now->format('m'),
'[day]' => $now->format('d'),
'[hour]' => $now->format('H'),
'[minute]' => $now->format('i'),
'[second]' => $now->format('s'),
'[profile_id]' => (string) ($profile->id ?? '0'),
'[profile_name]' => $this->sanitize($profile->title ?? 'default'),
'[site_name]' => $this->sanitize($siteName ?: 'joomla'),
'[type]' => $profile->backup_type ?? 'full',
'[random]' => bin2hex(random_bytes(3)),
'[HOST]' => $hostname,
'[DATE]' => $now->format('Ymd'),
'[TIME]' => $now->format('His'),
'[DATETIME]' => $now->format('Ymd_His'),
'[YEAR]' => $now->format('Y'),
'[MONTH]' => $now->format('m'),
'[DAY]' => $now->format('d'),
'[HOUR]' => $now->format('H'),
'[MINUTE]' => $now->format('i'),
'[SECOND]' => $now->format('s'),
'[PROFILE_ID]' => (string) ($profile->id ?? '0'),
'[PROFILE_NAME]' => $this->sanitize($profile->title ?? 'default'),
'[SITE_NAME]' => $this->sanitize($siteName ?: 'joomla'),
'[TYPE]' => $profile->backup_type ?? 'full',
'[RANDOM]' => bin2hex(random_bytes(3)),
'[DEFAULT_DIR]' => BackupDirectory::getDefaultAbsolute(),
'[HOME]' => BackupDirectory::getHomeDirectory(),
];
@@ -103,7 +103,7 @@ class PlaceholderResolver
*/
public function getHostname(): string
{
return $this->replacements['[host]'];
return $this->replacements['[HOST]'];
}
/**
@@ -111,7 +111,7 @@ class PlaceholderResolver
*/
public function getTag(): string
{
return $this->replacements['[datetime]'];
return $this->replacements['[DATETIME]'];
}
/**
@@ -83,7 +83,7 @@ class SteppedBackupEngine
$now = date('Y-m-d H:i:s');
$tag = $resolver->getTag();
$nameFormat = $profile->archive_name_format ?? '[host]_[datetime]_profile[profile_id]';
$nameFormat = $profile->archive_name_format ?? '[HOST]_[DATETIME]_profile[PROFILE_ID]';
$archiveName = $resolver->resolve($nameFormat) . '.zip';
$session->archivePath = $backupDir . '/' . $archiveName;
@@ -52,15 +52,15 @@ class FolderPickerField extends FormField
$placeholders = [
'[DEFAULT_DIR]' => BackupDirectory::getDefaultAbsolute(),
'[HOME]' => BackupDirectory::getHomeDirectory(),
'[host]' => $hostname,
'[site_name]' => $sanitizedSiteName ?: 'joomla',
'[profile_id]' => '1',
'[profile_name]' => 'default',
'[type]' => 'full',
'[year]' => date('Y'),
'[month]' => date('m'),
'[day]' => date('d'),
'[date]' => date('Ymd'),
'[HOST]' => $hostname,
'[SITE_NAME]' => $sanitizedSiteName ?: 'joomla',
'[PROFILE_ID]' => '1',
'[PROFILE_NAME]' => 'default',
'[TYPE]' => 'full',
'[YEAR]' => date('Y'),
'[MONTH]' => date('m'),
'[DAY]' => date('d'),
'[DATE]' => date('Ymd'),
];
$placeholdersJson = json_encode($placeholders);
@@ -96,7 +96,7 @@ class FolderPickerField extends FormField
<span class="icon-folder-open" aria-hidden="true"></span>
Browse
</button>
<button type="button" class="btn btn-outline-info" data-bs-toggle="modal" data-bs-target="#{$id}_helpModal" title="Available placeholders">
<button type="button" class="btn btn-outline-info" id="{$id}_helpBtn" title="Help — placeholders, paths, and examples">
<span class="icon-question-circle" aria-hidden="true"></span>
</button>
</div>
@@ -104,12 +104,12 @@ class FolderPickerField extends FormField
<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>
<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}">
@@ -124,36 +124,112 @@ class FolderPickerField extends FormField
The default backup directory is inside the web root. Backup archives may be directly downloadable if <code>.htaccess</code> is not supported. For better security, use a path outside the web root.
</div>
<div class="modal fade" id="{$id}_helpModal" tabindex="-1" aria-labelledby="{$id}_helpLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="{$id}_helpLabel">Backup Directory Placeholders</h5>
<h5 class="modal-title" id="{$id}_helpLabel">Backup Directory — Help</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Use these placeholders in the backup directory path. They are resolved at backup time.</p>
<h6 class="text-primary">How Path Resolution Works</h6>
<p>The backup directory path is resolved at backup time. You can use <strong>absolute paths</strong>, <strong>relative paths</strong>, or <strong>placeholder paths</strong>.</p>
<div class="card mb-3">
<div class="card-header fw-bold">Absolute Paths</div>
<div class="card-body py-2">
<p class="mb-1">Start with <code>/</code> (Linux) or a drive letter (Windows). Used as-is.</p>
<ul class="mb-0">
<li><code>/home/user/backups</code> — Fixed path on the server</li>
<li><code>/var/backups/joomla</code> — System backup directory</li>
</ul>
</div>
</div>
<div class="card mb-3">
<div class="card-header fw-bold">Relative Paths</div>
<div class="card-body py-2">
<p class="mb-1">Paths that do <strong>not</strong> start with <code>/</code> are resolved relative to the Joomla root directory, using the same conventions as URL paths:</p>
<table class="table table-sm mb-2">
<thead><tr><th>Path</th><th>Meaning</th><th>Resolves To</th></tr></thead>
<tbody>
<tr><td><code>backups</code></td><td>Subdirectory of Joomla root</td><td><code>{$jRoot}/backups</code></td></tr>
<tr><td><code>./backups</code></td><td>Same as above (explicit current dir)</td><td><code>{$jRoot}/backups</code></td></tr>
<tr><td><code>../backups</code></td><td>One level <strong>above</strong> Joomla root</td><td>Parent of <code>{$jRoot}</code></td></tr>
<tr><td><code>../../backups</code></td><td>Two levels above Joomla root</td><td>Grandparent of <code>{$jRoot}</code></td></tr>
</tbody>
</table>
<div class="alert alert-warning py-1 px-2 mb-0" style="font-size:0.85rem;">
<strong>Warning:</strong> Relative paths that stay inside the web root may expose backup files to direct download if .htaccess is not supported. Use <code>../</code> or <code>[HOME]</code> to store backups outside the web root.
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header fw-bold">Placeholder Paths (Recommended)</div>
<div class="card-body py-2">
<p class="mb-1">Use <code>[PLACEHOLDER]</code> tokens that are replaced with actual values at backup time. This makes paths <strong>portable</strong> across servers.</p>
<ul class="mb-0">
<li><code>[HOME]/backups</code> — User's home directory + /backups</li>
<li><code>[HOME]/[HOST]/backups</code> — Per-site subdirectory under home</li>
<li><code>[DEFAULT_DIR]</code> — Joomla's default backup directory</li>
</ul>
</div>
</div>
<h6 class="text-primary mt-3">Available Placeholders</h6>
<table class="table table-sm table-striped">
<thead><tr><th>Placeholder</th><th>Description</th><th>Example</th></tr></thead>
<thead><tr><th>Placeholder</th><th>Description</th><th>Current Value</th></tr></thead>
<tbody>
<tr><td><code>[HOME]</code></td><td>Home directory of the server user</td><td><code>{$placeholders['[HOME]']}</code></td></tr>
<tr><td><code>[DEFAULT_DIR]</code></td><td>Default backup directory (inside web root)</td><td><code>{$placeholders['[DEFAULT_DIR]']}</code></td></tr>
<tr><td><code>[host]</code></td><td>Server hostname</td><td><code>{$placeholders['[host]']}</code></td></tr>
<tr><td><code>[site_name]</code></td><td>Joomla site name</td><td><code>{$placeholders['[site_name]']}</code></td></tr>
<tr><td><code>[date]</code></td><td>Date (Ymd)</td><td><code>{$placeholders['[date]']}</code></td></tr>
<tr><td><code>[year]</code></td><td>Four-digit year</td><td><code>{$placeholders['[year]']}</code></td></tr>
<tr><td><code>[month]</code></td><td>Two-digit month</td><td><code>{$placeholders['[month]']}</code></td></tr>
<tr><td><code>[day]</code></td><td>Two-digit day</td><td><code>{$placeholders['[day]']}</code></td></tr>
<tr><td><code>[profile_id]</code></td><td>Backup profile ID</td><td><code>1</code></td></tr>
<tr><td><code>[profile_name]</code></td><td>Profile title</td><td><code>default</code></td></tr>
<tr><td><code>[type]</code></td><td>Backup type</td><td><code>full</code></td></tr>
<tr><td><code>[HOME]</code></td><td>Home directory of the PHP process owner. Detected from environment, POSIX, or JPATH_ROOT.</td><td><code>{$placeholders['[HOME]']}</code></td></tr>
<tr><td><code>[DEFAULT_DIR]</code></td><td>Default backup directory inside the Joomla web root. Protected by .htaccess but not recommended for production.</td><td><code>{$placeholders['[DEFAULT_DIR]']}</code></td></tr>
<tr><td><code>[HOST]</code></td><td>Server hostname from HTTP_HOST. Sanitized to alphanumeric, dots, and hyphens.</td><td><code>{$placeholders['[HOST]']}</code></td></tr>
<tr><td><code>[SITE_NAME]</code></td><td>Joomla site name from Global Configuration. Spaces become hyphens, special characters stripped.</td><td><code>{$placeholders['[SITE_NAME]']}</code></td></tr>
<tr><td><code>[DATE]</code></td><td>Current date in Ymd format (e.g. 20260623).</td><td><code>{$placeholders['[DATE]']}</code></td></tr>
<tr><td><code>[YEAR]</code></td><td>Four-digit year.</td><td><code>{$placeholders['[YEAR]']}</code></td></tr>
<tr><td><code>[MONTH]</code></td><td>Two-digit month (01-12).</td><td><code>{$placeholders['[MONTH]']}</code></td></tr>
<tr><td><code>[DAY]</code></td><td>Two-digit day (01-31).</td><td><code>{$placeholders['[DAY]']}</code></td></tr>
<tr><td><code>[PROFILE_ID]</code></td><td>Numeric ID of the backup profile being used.</td><td><code>1</code></td></tr>
<tr><td><code>[PROFILE_NAME]</code></td><td>Title of the backup profile, sanitized for filesystem use.</td><td><code>default</code></td></tr>
<tr><td><code>[TYPE]</code></td><td>Backup type: full, database, files, or differential.</td><td><code>full</code></td></tr>
</tbody>
</table>
<h6>Recommended Paths</h6>
<ul class="list-unstyled">
<li><code>[HOME]/backups</code> — Outside web root (recommended)</li>
<li><code>[HOME]/backups/[host]</code> — Per-site subdirectory</li>
<li><code>[DEFAULT_DIR]</code> — Inside web root (protected by .htaccess)</li>
</ul>
<h6 class="text-primary mt-3">Recommended Configurations</h6>
<table class="table table-sm">
<thead><tr><th>Use Case</th><th>Path</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td><strong>Single site, secure</strong></td>
<td><code>[HOME]/backups</code></td>
<td>Outside web root. Best for most sites.</td>
</tr>
<tr>
<td><strong>Multiple sites on one server</strong></td>
<td><code>[HOME]/backups/[HOST]</code></td>
<td>Each site gets its own subdirectory.</td>
</tr>
<tr>
<td><strong>Date-organized</strong></td>
<td><code>[HOME]/backups/[YEAR]/[MONTH]</code></td>
<td>Backups sorted by year and month.</td>
</tr>
<tr>
<td><strong>Per-profile</strong></td>
<td><code>[HOME]/backups/[PROFILE_NAME]</code></td>
<td>Separate directory for each backup profile.</td>
</tr>
<tr>
<td><strong>Shared hosting (default)</strong></td>
<td><code>[DEFAULT_DIR]</code></td>
<td>Inside web root, protected by .htaccess. Use only if you cannot write outside web root.</td>
</tr>
</tbody>
</table>
<div class="alert alert-info py-2 mt-3 mb-0">
<strong>Tip:</strong> The directory is created automatically if it doesn't exist. Placeholders are resolved fresh each time a backup runs, so date-based paths create new directories over time.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
@@ -188,6 +264,36 @@ class FolderPickerField extends FormField
});
});
/* Help button — open modal with Bootstrap 5 or fallback */
var helpBtn = document.getElementById('{$id}_helpBtn');
var helpModal = document.getElementById('{$id}_helpModal');
if (helpBtn && helpModal) {
helpBtn.addEventListener('click', function(e) {
e.preventDefault();
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
var modal = bootstrap.Modal.getOrCreateInstance(helpModal);
modal.show();
} else {
helpModal.classList.add('show');
helpModal.style.display = 'block';
helpModal.setAttribute('aria-hidden', 'false');
document.body.classList.add('modal-open');
var backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop fade show';
backdrop.id = '{$id}_backdrop';
document.body.appendChild(backdrop);
helpModal.querySelector('.btn-close, [data-bs-dismiss]').addEventListener('click', function() {
helpModal.classList.remove('show');
helpModal.style.display = 'none';
helpModal.setAttribute('aria-hidden', 'true');
document.body.classList.remove('modal-open');
var bd = document.getElementById('{$id}_backdrop');
if (bd) bd.remove();
});
}
});
}
var fieldId = '{$id}';
var btn = document.getElementById(fieldId + '_btn');
var browser = document.getElementById(fieldId + '_browser');
@@ -195,7 +301,7 @@ class FolderPickerField extends FormField
var input = document.getElementById(fieldId);
var placeholders = {$placeholdersJson};
// Resolve placeholders in a path (forward: [site_name] -> actual value)
// Resolve placeholders in a path (forward: [SITE_NAME] -> actual value)
function resolve(path) {
for (var key in placeholders) {
path = path.split(key).join(placeholders[key]);
@@ -322,7 +428,7 @@ class FolderPickerField extends FormField
fullResolved.className = 'mt-1';
var arrow = document.createElement('span');
arrow.className = 'text-muted';
arrow.textContent = 'Resolves to: ';
arrow.textContent = 'EXAMPLE: ';
fullResolved.appendChild(arrow);
var code = document.createElement('code');
code.textContent = resolve(val);
@@ -33,8 +33,8 @@ class PlaceholderTextField extends FormField
$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]'];
$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 . '"'
@@ -65,7 +65,7 @@ class HtmlView extends BaseHtmlView
}
// "View Backups" link button
$backupsUrl = Route::_('index.php?option=com_mokosuitebackup&view=backups&filter[profile_id]=' . $profileId);
$backupsUrl = Route::_('index.php?option=com_mokosuitebackup&view=backups&filter[PROFILE_ID]=' . $profileId);
$toolbar->linkButton('view-backups', 'COM_MOKOJOOMBACKUP_VIEW_BACKUPS')
->url($backupsUrl)
->icon('icon-database')
@@ -78,7 +78,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<?php echo $this->escape($item->backup_type); ?>
</td>
<td class="text-center">
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=backups&filter[profile_id]=' . $item->id); ?>">
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=backups&filter[PROFILE_ID]=' . $item->id); ?>">
<span class="badge bg-<?php echo ($item->backup_count > 0) ? 'info' : 'secondary'; ?>">
<?php echo (int) $item->backup_count; ?>
</span>
@@ -403,7 +403,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
var label = document.createElement('label');
label.className = 'form-check-label';
label.setAttribute('for', 'mb-rtype-' + type);
label.textContent = typeLabels[type] || type;
label.textContent = typeLabels[TYPE] || type;
div.appendChild(input);
div.appendChild(label);
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="actionlog" method="upgrade">
<name>Action Log - MokoSuiteBackup</name>
<version>01.38.02</version>
<version>01.38.05</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.38.02</version>
<version>01.38.05</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.38.02</version>
<version>01.38.05</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.38.02</version>
<version>01.38.05</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.38.02</version>
<version>01.38.05</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.38.02</version>
<version>01.38.05</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.38.02</version>
<version>01.38.05</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.38.02</version>
<version>01.38.05</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>