Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65c8820db4 | |||
| 0f914c3061 | |||
| 4191f44c1b | |||
| fb99afbeba | |||
| de632e9c5c | |||
| 53ff99148c | |||
| c2ff3b272a | |||
| 747b68c179 | |||
| cbff40d04c | |||
| e415e701cd | |||
| d184ed9de0 | |||
| 297f27c807 | |||
| 30e8d7baa9 | |||
| efc5754bef | |||
| e3e422d29e | |||
| 9f5c8c0b5e | |||
| 044e57adf3 | |||
| e7f165ac96 | |||
| fc41e1801a | |||
| 1aa35dd041 | |||
| 6a1f4a8797 | |||
| 6f6a6c705b | |||
| e8d7d1d421 | |||
| cd31617e21 | |||
| 6d9d96d7cd | |||
| df7c07bec4 | |||
| 5b4717bf6f | |||
| 65d30613b2 |
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 01.38.00
|
||||
# VERSION: 01.38.04
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
+6
-19
@@ -1,27 +1,14 @@
|
||||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
## [01.38.00] --- 2026-06-23
|
||||
## [01.38.04] --- 2026-06-23
|
||||
|
||||
## [01.38.00] --- 2026-06-23
|
||||
## [01.38.04] --- 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
|
||||
|
||||
## [01.37.00] --- 2026-06-23
|
||||
## [01.38.03] --- 2026-06-23
|
||||
|
||||
## [01.37.00] --- 2026-06-23
|
||||
## [01.38.02] --- 2026-06-23
|
||||
|
||||
### Added
|
||||
- Run Backup button on profiles list and edit views with backup count badges (#100, #101)
|
||||
- Snapshot detail view with tabbed browser for articles, categories, and modules (#104)
|
||||
- "Do not navigate away" warning in backup and restore progress modals (#108)
|
||||
- Joomla Action Logs integration for restore, snapshot, and snapshot restore events (#110)
|
||||
- 8 comprehensive testing issues created (#111-#118)
|
||||
- Manual purge feature issue (#119)
|
||||
|
||||
## [01.36.00] --- 2026-06-23
|
||||
|
||||
## [01.36.00] --- 2026-06-23
|
||||
## [01.38.02] --- 2026-06-23
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MokoSuiteBackup
|
||||
|
||||
<!-- VERSION: 01.38.00 -->
|
||||
<!-- VERSION: 01.38.04 -->
|
||||
|
||||
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.00</version>
|
||||
<version>01.38.04</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',
|
||||
@@ -39,7 +39,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
|
||||
`s3_path` VARCHAR(512) NOT NULL DEFAULT '/backups',
|
||||
`remote_keep_local` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Keep local copy after upload',
|
||||
`encryption_password` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'AES-256 archive encryption password (blank = no encryption)',
|
||||
`include_mokorestore` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include MokoRestore standalone restore script in archive',
|
||||
`include_mokorestore` VARCHAR(20) NOT NULL DEFAULT '0' COMMENT 'MokoRestore mode: 0=none, 1=wrapped, standalone',
|
||||
`notify_email` VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'Comma-separated notification emails',
|
||||
`notify_user_groups` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Comma-separated Joomla user group IDs',
|
||||
`notify_on_success` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
|
||||
@@ -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,5 @@
|
||||
-- MokoSuiteBackup 01.39.00 — Change include_mokorestore from TINYINT to VARCHAR
|
||||
-- Needed to support 'standalone' value alongside 0/1
|
||||
|
||||
ALTER TABLE `#__mokosuitebackup_profiles`
|
||||
MODIFY COLUMN `include_mokorestore` VARCHAR(20) NOT NULL DEFAULT '0';
|
||||
@@ -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([
|
||||
|
||||
@@ -15,6 +15,7 @@ defined('_JEXEC') or die;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\AdminController;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Session\Session;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\RestoreEngine;
|
||||
|
||||
@@ -34,7 +35,14 @@ class BackupsController extends AdminController
|
||||
*/
|
||||
public function start(): void
|
||||
{
|
||||
$this->checkToken();
|
||||
/* Accept token from both GET (profile Run button) and POST (backup form).
|
||||
Joomla's checkToken() throws on failure, so try GET first. */
|
||||
if (!Session::checkToken('get') && !Session::checkToken('post')) {
|
||||
$this->setMessage(Text::_('JINVALID_TOKEN_NOTICE'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
|
||||
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
|
||||
|
||||
@@ -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);
|
||||
@@ -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}">
|
||||
@@ -117,6 +117,8 @@ class FolderPickerField extends FormField
|
||||
{$statusDetail}
|
||||
</small>
|
||||
</div>
|
||||
<div class="mt-1" id="{$id}_resolved" style="font-size:0.8rem; line-height:1.6;">
|
||||
</div>
|
||||
<div id="{$id}_defaultwarn" class="alert alert-warning alert-sm mt-1 py-1 px-2" style="display:none; font-size:0.85rem;">
|
||||
<span class="icon-warning-circle" aria-hidden="true"></span>
|
||||
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.
|
||||
@@ -135,21 +137,21 @@ class FolderPickerField extends FormField
|
||||
<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>[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>
|
||||
</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>[HOME]/backups/[HOST]</code> — Per-site subdirectory</li>
|
||||
<li><code>[DEFAULT_DIR]</code> — Inside web root (protected by .htaccess)</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -193,7 +195,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]);
|
||||
@@ -284,8 +286,54 @@ class FolderPickerField extends FormField
|
||||
});
|
||||
}
|
||||
|
||||
/* Show which placeholders are in use and their resolved values */
|
||||
var resolvedDiv = document.getElementById(fieldId + '_resolved');
|
||||
|
||||
function updateResolvedDisplay() {
|
||||
while (resolvedDiv.firstChild) resolvedDiv.removeChild(resolvedDiv.firstChild);
|
||||
var val = input.value || '';
|
||||
var found = false;
|
||||
|
||||
for (var key in placeholders) {
|
||||
if (val.indexOf(key) !== -1 && placeholders[key]) {
|
||||
found = true;
|
||||
var badge = document.createElement('span');
|
||||
badge.className = 'badge bg-light text-dark border me-1 mb-1';
|
||||
badge.style.fontSize = '0.75rem';
|
||||
badge.style.fontFamily = 'monospace';
|
||||
|
||||
var keySpan = document.createElement('strong');
|
||||
keySpan.textContent = key;
|
||||
badge.appendChild(keySpan);
|
||||
|
||||
badge.appendChild(document.createTextNode(' = '));
|
||||
|
||||
var valSpan = document.createElement('span');
|
||||
valSpan.className = 'text-primary';
|
||||
valSpan.textContent = placeholders[key];
|
||||
badge.appendChild(valSpan);
|
||||
|
||||
resolvedDiv.appendChild(badge);
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
var fullResolved = document.createElement('div');
|
||||
fullResolved.className = 'mt-1';
|
||||
var arrow = document.createElement('span');
|
||||
arrow.className = 'text-muted';
|
||||
arrow.textContent = 'EXAMPLE: ';
|
||||
fullResolved.appendChild(arrow);
|
||||
var code = document.createElement('code');
|
||||
code.textContent = resolve(val);
|
||||
fullResolved.appendChild(code);
|
||||
resolvedDiv.appendChild(fullResolved);
|
||||
}
|
||||
}
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
clearTimeout(checkTimer);
|
||||
updateResolvedDisplay();
|
||||
checkTimer = setTimeout(checkDirPermissions, 400);
|
||||
});
|
||||
|
||||
@@ -399,6 +447,7 @@ class FolderPickerField extends FormField
|
||||
|
||||
// Run initial check on page load
|
||||
setDefaultDirWarning();
|
||||
updateResolvedDisplay();
|
||||
checkDirPermissions();
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -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.00</version>
|
||||
<version>01.38.04</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.00</version>
|
||||
<version>01.38.04</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.00</version>
|
||||
<version>01.38.04</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.00</version>
|
||||
<version>01.38.04</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.00</version>
|
||||
<version>01.38.04</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.00</version>
|
||||
<version>01.38.04</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.00</version>
|
||||
<version>01.38.04</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuiteBackup</name>
|
||||
<packagename>mokosuitebackup</packagename>
|
||||
<version>01.38.00</version>
|
||||
<version>01.38.04</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
Reference in New Issue
Block a user