diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml
index b278f8b..cf696d8 100644
--- a/.mokogitea/manifest.xml
+++ b/.mokogitea/manifest.xml
@@ -5,7 +5,7 @@
Package - MokoJoomBackup
MokoConsulting
Full-site backup and restore for Joomla — database, files, and configuration
- 01.06.00-dev
+ 01.06.01-dev
GNU General Public License v3
diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml
index 621b899..2c0bfe5 100644
--- a/.mokogitea/workflows/issue-branch.yml
+++ b/.mokogitea/workflows/issue-branch.yml
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Automation
-# VERSION: 01.06.00
+# VERSION: 01.06.01
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
diff --git a/README.md b/README.md
index 8044fea..183aadd 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# MokoJoomBackup
-
+
Full-site backup and restore for Joomla — database, files, and configuration.
diff --git a/source/packages/com_mokojoombackup/mokojoombackup.xml b/source/packages/com_mokojoombackup/mokojoombackup.xml
index 3efdff6..4e329e5 100644
--- a/source/packages/com_mokojoombackup/mokojoombackup.xml
+++ b/source/packages/com_mokojoombackup/mokojoombackup.xml
@@ -8,7 +8,7 @@
-->
com_mokojoombackup
- 01.06.00
+ 01.06.01-dev
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/com_mokojoombackup/src/Controller/AjaxController.php b/source/packages/com_mokojoombackup/src/Controller/AjaxController.php
index c924acd..6b3c3e6 100644
--- a/source/packages/com_mokojoombackup/src/Controller/AjaxController.php
+++ b/source/packages/com_mokojoombackup/src/Controller/AjaxController.php
@@ -18,6 +18,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Session\Session;
use Joomla\Component\MokoJoomBackup\Administrator\Engine\SteppedBackupEngine;
+use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
class AjaxController extends BaseController
{
@@ -196,7 +197,7 @@ class AjaxController extends BaseController
}
// Try to load log from file alongside the archive
- $logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $record->absolute_path);
+ $logPath = BackupDirectory::logPathFromArchive($record->absolute_path);
$logContent = '';
if (is_file($logPath)) {
@@ -239,17 +240,9 @@ class AjaxController extends BaseController
return;
}
- // Resolve [DEFAULT_DIR] placeholder
- $defaultDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
- $resolved = str_replace('[DEFAULT_DIR]', $defaultDir, $rawPath);
+ $resolved = BackupDirectory::resolve($rawPath);
- // Resolve relative paths from JPATH_ROOT
- if ($resolved !== '' && $resolved[0] !== '/' && !preg_match('#^[A-Za-z]:[/\\\\]#', $resolved)) {
- $resolved = JPATH_ROOT . '/' . $resolved;
- }
-
- // Skip check if unresolved placeholders remain
- if (preg_match('/\[.+\]/', $resolved)) {
+ if (BackupDirectory::hasPlaceholders($resolved)) {
$this->sendJson([
'error' => false,
'exists' => null,
diff --git a/source/packages/com_mokojoombackup/src/Engine/BackupEngine.php b/source/packages/com_mokojoombackup/src/Engine/BackupEngine.php
index 84b3c49..d9863cf 100644
--- a/source/packages/com_mokojoombackup/src/Engine/BackupEngine.php
+++ b/source/packages/com_mokojoombackup/src/Engine/BackupEngine.php
@@ -13,6 +13,7 @@ namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
+use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
use Joomla\Event\Event;
class BackupEngine
@@ -56,24 +57,20 @@ class BackupEngine
}
// Read settings directly from profile columns
- $excludeDirs = $this->parseNewlineList($profile->exclude_dirs ?? '');
- $excludeFiles = $this->parseNewlineList($profile->exclude_files ?? '');
- $excludeTables = $this->parseNewlineList($profile->exclude_tables ?? '');
+ $excludeDirs = BackupDirectory::parseNewlineList($profile->exclude_dirs ?? '');
+ $excludeFiles = BackupDirectory::parseNewlineList($profile->exclude_files ?? '');
+ $excludeTables = BackupDirectory::parseNewlineList($profile->exclude_tables ?? '');
// Resolve placeholders in directory and filename
$resolver = new PlaceholderResolver($profile);
- $configuredDir = $profile->backup_dir ?: '[DEFAULT_DIR]';
- $this->backupDir = $this->resolveBackupDir($resolver->resolve($configuredDir));
+ $configuredDir = $profile->backup_dir ?: BackupDirectory::PLACEHOLDER;
+ $this->backupDir = BackupDirectory::resolve($resolver->resolve($configuredDir));
- if (!is_dir($this->backupDir)) {
- if (!mkdir($this->backupDir, 0755, true)) {
- return ['success' => false, 'message' => 'Cannot create backup directory: ' . $this->backupDir, 'record_id' => 0];
- }
+ if (!BackupDirectory::ensureReady($this->backupDir)) {
+ return ['success' => false, 'message' => 'Cannot create backup directory: ' . $this->backupDir, 'record_id' => 0];
}
- $this->protectBackupDir($this->backupDir);
-
// Create backup record
$now = date('Y-m-d H:i:s');
$tag = $resolver->getTag();
@@ -474,21 +471,6 @@ class BackupEngine
$zip->close();
}
- /**
- * Parse a newline-separated text field into an array of trimmed, non-empty strings.
- */
- private function parseNewlineList(string $text): array
- {
- if (empty($text)) {
- return [];
- }
-
- return array_values(array_filter(
- array_map('trim', explode("\n", str_replace("\r", '', $text))),
- fn($line) => $line !== ''
- ));
- }
-
/**
* Dispatch the onMokoJoomBackupAfterRun event so plugins (actionlog, etc.) can react.
*/
@@ -512,38 +494,6 @@ class BackupEngine
}
}
- /**
- * Resolve a backup directory path. Absolute paths are used as-is,
- * relative paths are resolved from JPATH_ROOT.
- */
- private function resolveBackupDir(string $dir): string
- {
- if ($dir !== '' && ($dir[0] === '/' || preg_match('#^[A-Za-z]:[/\\\\]#', $dir))) {
- return rtrim($dir, '/\\');
- }
-
- return JPATH_ROOT . '/' . $dir;
- }
-
- private function protectBackupDir(string $dir): void
- {
- $htaccess = $dir . '/.htaccess';
-
- if (!is_file($htaccess)) {
- if (@file_put_contents($htaccess, "# Apache 2.4+\n\n Require all denied\n\n# Apache 2.2\n\n Order deny,allow\n Deny from all\n\n") === false) {
- error_log('MokoJoomBackup: Could not create .htaccess in backup directory: ' . $dir);
- }
- }
-
- $index = $dir . '/index.html';
-
- if (!is_file($index)) {
- if (@file_put_contents($index, '') === false) {
- error_log('MokoJoomBackup: Could not create index.html in backup directory: ' . $dir);
- }
- }
- }
-
private function log(string $message): void
{
$this->log[] = '[' . date('H:i:s') . '] ' . $message;
diff --git a/source/packages/com_mokojoombackup/src/Engine/PlaceholderResolver.php b/source/packages/com_mokojoombackup/src/Engine/PlaceholderResolver.php
index a4afed0..094ed0f 100644
--- a/source/packages/com_mokojoombackup/src/Engine/PlaceholderResolver.php
+++ b/source/packages/com_mokojoombackup/src/Engine/PlaceholderResolver.php
@@ -16,6 +16,7 @@ namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
+use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
class PlaceholderResolver
{
@@ -38,7 +39,7 @@ class PlaceholderResolver
'[site_name]' => 'Joomla site name (sanitized)',
'[type]' => 'Backup type (full, database, files, differential)',
'[random]' => 'Random 6-character hex string',
- '[DEFAULT_DIR]' => 'Default backup directory (administrator/components/com_mokojoombackup/backups)',
+ '[DEFAULT_DIR]' => 'Default backup directory',
];
private array $replacements;
@@ -75,7 +76,7 @@ class PlaceholderResolver
'[site_name]' => $this->sanitize($siteName ?: 'joomla'),
'[type]' => $profile->backup_type ?? 'full',
'[random]' => bin2hex(random_bytes(3)),
- '[DEFAULT_DIR]' => JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups',
+ '[DEFAULT_DIR]' => BackupDirectory::getDefaultAbsolute(),
];
}
diff --git a/source/packages/com_mokojoombackup/src/Engine/SteppedBackupEngine.php b/source/packages/com_mokojoombackup/src/Engine/SteppedBackupEngine.php
index fc5af70..152bd98 100644
--- a/source/packages/com_mokojoombackup/src/Engine/SteppedBackupEngine.php
+++ b/source/packages/com_mokojoombackup/src/Engine/SteppedBackupEngine.php
@@ -21,6 +21,7 @@ namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
+use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
class SteppedBackupEngine
{
@@ -52,26 +53,22 @@ class SteppedBackupEngine
$session->backupType = $profile->backup_type;
// Parse profile settings
- $session->excludeDirs = $this->parseNewlineList($profile->exclude_dirs ?? '');
- $session->excludeFiles = $this->parseNewlineList($profile->exclude_files ?? '');
- $session->excludeTables = $this->parseNewlineList($profile->exclude_tables ?? '');
- $session->backupDir = $profile->backup_dir ?: '[DEFAULT_DIR]';
+ $session->excludeDirs = BackupDirectory::parseNewlineList($profile->exclude_dirs ?? '');
+ $session->excludeFiles = BackupDirectory::parseNewlineList($profile->exclude_files ?? '');
+ $session->excludeTables = BackupDirectory::parseNewlineList($profile->exclude_tables ?? '');
+ $session->backupDir = $profile->backup_dir ?: BackupDirectory::PLACEHOLDER;
$session->remoteStorage = $profile->remote_storage ?? 'none';
$session->includeMokoRestore = (bool) ($profile->include_mokorestore ?? false);
$session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true);
// Resolve placeholders in directory and filename
$resolver = new PlaceholderResolver($profile);
- $backupDir = $this->resolveBackupDir($resolver->resolve($session->backupDir));
+ $backupDir = BackupDirectory::resolve($resolver->resolve($session->backupDir));
- if (!is_dir($backupDir)) {
- if (!mkdir($backupDir, 0755, true)) {
- return ['error' => true, 'message' => 'Cannot create backup directory: ' . $backupDir];
- }
+ if (!BackupDirectory::ensureReady($backupDir)) {
+ return ['error' => true, 'message' => 'Cannot create backup directory: ' . $backupDir];
}
- $this->protectBackupDir($backupDir);
-
$now = date('Y-m-d H:i:s');
$tag = $resolver->getTag();
$nameFormat = $profile->archive_name_format ?? '[host]_[datetime]_profile[profile_id]';
@@ -422,7 +419,7 @@ class SteppedBackupEngine
$logContent = implode("\n", $session->log);
// Write log file alongside the archive
- $logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $session->archivePath);
+ $logPath = BackupDirectory::logPathFromArchive($session->archivePath);
if (@file_put_contents($logPath, $logContent) === false) {
error_log('MokoJoomBackup: Could not write log file: ' . $logPath);
}
@@ -554,47 +551,4 @@ class SteppedBackupEngine
return $tables;
}
- /**
- * Resolve a backup directory path. Absolute paths are used as-is,
- * relative paths are resolved from JPATH_ROOT.
- */
- private function resolveBackupDir(string $dir): string
- {
- if ($dir !== '' && ($dir[0] === '/' || preg_match('#^[A-Za-z]:[/\\\\]#', $dir))) {
- return rtrim($dir, '/\\');
- }
-
- return JPATH_ROOT . '/' . $dir;
- }
-
- private function protectBackupDir(string $dir): void
- {
- $htaccess = $dir . '/.htaccess';
-
- if (!is_file($htaccess)) {
- if (@file_put_contents($htaccess, "# Apache 2.4+\n\n Require all denied\n\n# Apache 2.2\n\n Order deny,allow\n Deny from all\n\n") === false) {
- error_log('MokoJoomBackup: Could not create .htaccess in backup directory: ' . $dir);
- }
- }
-
- $index = $dir . '/index.html';
-
- if (!is_file($index)) {
- if (@file_put_contents($index, '') === false) {
- error_log('MokoJoomBackup: Could not create index.html in backup directory: ' . $dir);
- }
- }
- }
-
- private function parseNewlineList(string $text): array
- {
- if (empty($text)) {
- return [];
- }
-
- return array_values(array_filter(
- array_map('trim', explode("\n", str_replace("\r", '', $text))),
- fn($line) => $line !== ''
- ));
- }
}
diff --git a/source/packages/com_mokojoombackup/src/Field/FolderPickerField.php b/source/packages/com_mokojoombackup/src/Field/FolderPickerField.php
index 17bcaee..9ba5187 100644
--- a/source/packages/com_mokojoombackup/src/Field/FolderPickerField.php
+++ b/source/packages/com_mokojoombackup/src/Field/FolderPickerField.php
@@ -15,6 +15,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
use Joomla\CMS\Language\Text;
+use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
class FolderPickerField extends FormField
{
@@ -49,7 +50,7 @@ class FolderPickerField extends FormField
$sanitizedSiteName = preg_replace('/[^a-zA-Z0-9._-]/', '', str_replace(' ', '-', trim($siteName)));
$placeholders = [
- '[DEFAULT_DIR]' => JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups',
+ '[DEFAULT_DIR]' => BackupDirectory::getDefaultAbsolute(),
'[host]' => $hostname,
'[site_name]' => $sanitizedSiteName ?: 'joomla',
'[profile_id]' => '1',
diff --git a/source/packages/com_mokojoombackup/src/Model/DashboardModel.php b/source/packages/com_mokojoombackup/src/Model/DashboardModel.php
index 4435e54..778f12b 100644
--- a/source/packages/com_mokojoombackup/src/Model/DashboardModel.php
+++ b/source/packages/com_mokojoombackup/src/Model/DashboardModel.php
@@ -14,6 +14,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
+use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
class DashboardModel extends BaseDatabaseModel
{
@@ -122,11 +123,9 @@ class DashboardModel extends BaseDatabaseModel
'detail' => $aesSupport ? 'Available' : 'Requires libzip 1.2.0+',
];
- // Backup directory writable — check the default path
- $defaultDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
- $backupDir = $defaultDir;
+ // Backup directory writable — check the first published profile's dir
+ $backupDir = BackupDirectory::getDefaultAbsolute();
- // If profiles use a custom directory, check that instead
$db2 = $this->getDatabase();
$qDir = $db2->getQuery(true)
->select($db2->quoteName('backup_dir'))
@@ -138,16 +137,10 @@ class DashboardModel extends BaseDatabaseModel
$profileDir = $db2->loadResult();
if ($profileDir) {
- // Absolute paths used as-is, relative resolved from JPATH_ROOT
- if ($profileDir[0] === '/' || preg_match('#^[A-Za-z]:[/\\\\]#', $profileDir)) {
- $backupDir = rtrim($profileDir, '/\\');
- } else {
- $backupDir = JPATH_ROOT . '/' . $profileDir;
- }
+ $backupDir = BackupDirectory::resolve($profileDir);
}
- // Skip filesystem check if path contains placeholders (resolved at backup time)
- if (preg_match('/\[.+\]/', $backupDir)) {
+ if (BackupDirectory::hasPlaceholders($backupDir)) {
$checks[] = (object) [
'label' => 'Backup Directory',
'status' => true,
@@ -182,14 +175,12 @@ class DashboardModel extends BaseDatabaseModel
public function isUsingDefaultBackupDir(): bool
{
$db = $this->getDatabase();
- $default = 'administrator/components/com_mokojoombackup/backups';
-
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokojoombackup_profiles'))
->where($db->quoteName('published') . ' = 1')
- ->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote($default)
- . ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('[DEFAULT_DIR]')
+ ->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote(BackupDirectory::DEFAULT_RELATIVE)
+ . ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote(BackupDirectory::PLACEHOLDER)
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('')
. ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)');
$db->setQuery($query);
diff --git a/source/packages/com_mokojoombackup/src/Table/ProfileTable.php b/source/packages/com_mokojoombackup/src/Table/ProfileTable.php
index 78892a7..980bae5 100644
--- a/source/packages/com_mokojoombackup/src/Table/ProfileTable.php
+++ b/source/packages/com_mokojoombackup/src/Table/ProfileTable.php
@@ -13,6 +13,7 @@ namespace Joomla\Component\MokoJoomBackup\Administrator\Table;
defined('_JEXEC') or die;
use Joomla\CMS\Table\Table;
+use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
use Joomla\Database\DatabaseDriver;
class ProfileTable extends Table
@@ -35,49 +36,17 @@ class ProfileTable extends Table
private function protectWebAccessibleDir(string $dir): void
{
- // Resolve [DEFAULT_DIR] placeholder
- $defaultDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
- $resolved = str_replace('[DEFAULT_DIR]', $defaultDir, $dir);
+ $resolved = BackupDirectory::resolve($dir);
- // Resolve relative paths from JPATH_ROOT
- if ($resolved !== '' && $resolved[0] !== '/' && !preg_match('#^[A-Za-z]:[/\\\\]#', $resolved)) {
- $resolved = JPATH_ROOT . '/' . $resolved;
- }
-
- // Skip if unresolved placeholders remain
- if (preg_match('/\[.+\]/', $resolved)) {
+ if (BackupDirectory::hasPlaceholders($resolved)) {
return;
}
- // Only protect directories under the web root
- $jRoot = realpath(JPATH_ROOT) ?: JPATH_ROOT;
- $realDir = realpath($resolved) ?: $resolved;
-
- if (strpos($realDir, $jRoot) !== 0) {
+ if (!BackupDirectory::isWebAccessible($resolved)) {
return;
}
- if (!is_dir($resolved)) {
- @mkdir($resolved, 0755, true);
- }
-
- if (is_dir($resolved)) {
- $htaccess = $resolved . '/.htaccess';
-
- if (!is_file($htaccess)) {
- if (@file_put_contents($htaccess, "# Apache 2.4+\n\n Require all denied\n\n# Apache 2.2\n\n Order deny,allow\n Deny from all\n\n") === false) {
- error_log('MokoJoomBackup: Could not create .htaccess in: ' . $resolved);
- }
- }
-
- $index = $resolved . '/index.html';
-
- if (!is_file($index)) {
- if (@file_put_contents($index, '') === false) {
- error_log('MokoJoomBackup: Could not create index.html in: ' . $resolved);
- }
- }
- }
+ BackupDirectory::ensureReady($resolved);
}
public function check(): bool
diff --git a/source/packages/com_mokojoombackup/src/Utility/BackupDirectory.php b/source/packages/com_mokojoombackup/src/Utility/BackupDirectory.php
new file mode 100644
index 0000000..9048f19
--- /dev/null
+++ b/source/packages/com_mokojoombackup/src/Utility/BackupDirectory.php
@@ -0,0 +1,153 @@
+
+ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ */
+
+namespace Joomla\Component\MokoJoomBackup\Administrator\Utility;
+
+defined('_JEXEC') or die;
+
+class BackupDirectory
+{
+ public const DEFAULT_RELATIVE = 'administrator/components/com_mokojoombackup/backups';
+
+ public const PLACEHOLDER = '[DEFAULT_DIR]';
+
+ private const HTACCESS_CONTENT = <<<'HTACCESS'
+# Apache 2.4+
+
+ Require all denied
+
+# Apache 2.2
+
+ Order deny,allow
+ Deny from all
+
+HTACCESS;
+
+ private const INDEX_CONTENT = '';
+
+ /**
+ * Get the absolute default backup directory path.
+ */
+ public static function getDefaultAbsolute(): string
+ {
+ return JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
+ }
+
+ /**
+ * Resolve a backup directory path. Replaces [DEFAULT_DIR] placeholder,
+ * then resolves relative paths from JPATH_ROOT.
+ *
+ * @param string $dir Raw directory value from profile
+ *
+ * @return string Absolute path (may still contain other placeholders)
+ */
+ public static function resolve(string $dir): string
+ {
+ if ($dir === '' || $dir === self::PLACEHOLDER) {
+ $dir = self::getDefaultAbsolute();
+ } else {
+ $dir = str_replace(self::PLACEHOLDER, self::getDefaultAbsolute(), $dir);
+ }
+
+ if ($dir !== '' && ($dir[0] === '/' || preg_match('#^[A-Za-z]:[/\\\\]#', $dir))) {
+ return rtrim($dir, '/\\');
+ }
+
+ return JPATH_ROOT . '/' . $dir;
+ }
+
+ /**
+ * Check whether a resolved path still contains unresolved placeholders.
+ */
+ public static function hasPlaceholders(string $path): bool
+ {
+ return (bool) preg_match('/\[.+\]/', $path);
+ }
+
+ /**
+ * Check whether a resolved absolute path is inside the web root.
+ */
+ public static function isWebAccessible(string $absolutePath): bool
+ {
+ $jRoot = realpath(JPATH_ROOT) ?: JPATH_ROOT;
+ $realDir = realpath($absolutePath) ?: $absolutePath;
+
+ return strpos($realDir, $jRoot) === 0;
+ }
+
+ /**
+ * Create .htaccess and index.html protection files in a directory.
+ * Only creates files if they don't already exist.
+ */
+ public static function protect(string $dir): void
+ {
+ if (!is_dir($dir)) {
+ return;
+ }
+
+ $htaccess = $dir . '/.htaccess';
+
+ if (!is_file($htaccess)) {
+ if (@file_put_contents($htaccess, self::HTACCESS_CONTENT . "\n") === false) {
+ error_log('MokoJoomBackup: Could not create .htaccess in: ' . $dir);
+ }
+ }
+
+ $index = $dir . '/index.html';
+
+ if (!is_file($index)) {
+ if (@file_put_contents($index, self::INDEX_CONTENT) === false) {
+ error_log('MokoJoomBackup: Could not create index.html in: ' . $dir);
+ }
+ }
+ }
+
+ /**
+ * Ensure the backup directory exists, create it if needed,
+ * and apply web protection if it's inside the web root.
+ *
+ * @return bool True if directory exists and is usable
+ */
+ public static function ensureReady(string $dir): bool
+ {
+ if (!is_dir($dir)) {
+ if (!@mkdir($dir, 0755, true)) {
+ return false;
+ }
+ }
+
+ self::protect($dir);
+
+ return true;
+ }
+
+ /**
+ * Parse a newline-separated text field into an array of trimmed, non-empty strings.
+ */
+ public static function parseNewlineList(string $text): array
+ {
+ if (empty($text)) {
+ return [];
+ }
+
+ return array_values(array_filter(
+ array_map('trim', explode("\n", str_replace("\r", '', $text))),
+ fn($line) => $line !== ''
+ ));
+ }
+
+ /**
+ * Derive the log file path from an archive path.
+ */
+ public static function logPathFromArchive(string $archivePath): string
+ {
+ return preg_replace('/\.(zip|tar\.gz)$/i', '.log', $archivePath);
+ }
+}
diff --git a/source/packages/com_mokojoombackup/src/Utility/index.html b/source/packages/com_mokojoombackup/src/Utility/index.html
new file mode 100644
index 0000000..2efb97f
--- /dev/null
+++ b/source/packages/com_mokojoombackup/src/Utility/index.html
@@ -0,0 +1 @@
+
diff --git a/source/packages/plg_actionlog_mokojoombackup/mokojoombackup.xml b/source/packages/plg_actionlog_mokojoombackup/mokojoombackup.xml
index 4d5a2fc..78ba61b 100644
--- a/source/packages/plg_actionlog_mokojoombackup/mokojoombackup.xml
+++ b/source/packages/plg_actionlog_mokojoombackup/mokojoombackup.xml
@@ -8,7 +8,7 @@
-->
plg_actionlog_mokojoombackup
- 01.06.00
+ 01.06.01-dev
2026-06-04
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_console_mokojoombackup/mokojoombackup.xml b/source/packages/plg_console_mokojoombackup/mokojoombackup.xml
index ffe491f..db4a597 100644
--- a/source/packages/plg_console_mokojoombackup/mokojoombackup.xml
+++ b/source/packages/plg_console_mokojoombackup/mokojoombackup.xml
@@ -8,7 +8,7 @@
-->
plg_console_mokojoombackup
- 01.06.00
+ 01.06.01-dev
2026-06-04
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_content_mokojoombackup/mokojoombackup.xml b/source/packages/plg_content_mokojoombackup/mokojoombackup.xml
index 78a1df3..9a2d0c3 100644
--- a/source/packages/plg_content_mokojoombackup/mokojoombackup.xml
+++ b/source/packages/plg_content_mokojoombackup/mokojoombackup.xml
@@ -8,7 +8,7 @@
-->
plg_content_mokojoombackup
- 01.06.00
+ 01.06.01-dev
2026-06-04
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_quickicon_mokojoombackup/mokojoombackup.xml b/source/packages/plg_quickicon_mokojoombackup/mokojoombackup.xml
index 1f2c4bd..a2e801e 100644
--- a/source/packages/plg_quickicon_mokojoombackup/mokojoombackup.xml
+++ b/source/packages/plg_quickicon_mokojoombackup/mokojoombackup.xml
@@ -1,7 +1,7 @@
plg_quickicon_mokojoombackup
- 01.06.00
+ 01.06.01-dev
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_system_mokojoombackup/mokojoombackup.xml b/source/packages/plg_system_mokojoombackup/mokojoombackup.xml
index 90d6794..ca26b17 100644
--- a/source/packages/plg_system_mokojoombackup/mokojoombackup.xml
+++ b/source/packages/plg_system_mokojoombackup/mokojoombackup.xml
@@ -8,7 +8,7 @@
-->
plg_system_mokojoombackup
- 01.06.00
+ 01.06.01-dev
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_task_mokojoombackup/mokojoombackup.xml b/source/packages/plg_task_mokojoombackup/mokojoombackup.xml
index 0963993..7d91a30 100644
--- a/source/packages/plg_task_mokojoombackup/mokojoombackup.xml
+++ b/source/packages/plg_task_mokojoombackup/mokojoombackup.xml
@@ -8,7 +8,7 @@
-->
plg_task_mokojoombackup
- 01.06.00
+ 01.06.01-dev
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/packages/plg_webservices_mokojoombackup/mokojoombackup.xml b/source/packages/plg_webservices_mokojoombackup/mokojoombackup.xml
index 1f9a674..76c500d 100644
--- a/source/packages/plg_webservices_mokojoombackup/mokojoombackup.xml
+++ b/source/packages/plg_webservices_mokojoombackup/mokojoombackup.xml
@@ -8,7 +8,7 @@
-->
plg_webservices_mokojoombackup
- 01.06.00
+ 01.06.01-dev
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/pkg_mokojoombackup.xml b/source/pkg_mokojoombackup.xml
index 6dd458b..f4a39f7 100644
--- a/source/pkg_mokojoombackup.xml
+++ b/source/pkg_mokojoombackup.xml
@@ -8,7 +8,7 @@
Package - MokoJoomBackup
mokojoombackup
- 01.06.00
+ 01.06.01-dev
2026-06-02
Moko Consulting
hello@mokoconsulting.tech
diff --git a/source/script.php b/source/script.php
index a5b776c..0ceb68d 100644
--- a/source/script.php
+++ b/source/script.php
@@ -191,15 +191,25 @@ class Pkg_MokoJoomBackupInstallerScript
$db->setQuery($query);
$db->execute();
- // Create default backup directory
+ // Create and protect default backup directory
$backupDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
if (!is_dir($backupDir)) {
mkdir($backupDir, 0755, true);
+ }
- // Protect backup directory with .htaccess
- file_put_contents($backupDir . '/.htaccess', "# Apache 2.4+\n\n Require all denied\n\n# Apache 2.2\n\n Order deny,allow\n Deny from all\n\n");
- file_put_contents($backupDir . '/index.html', '');
+ if (is_dir($backupDir)) {
+ $htaccess = $backupDir . '/.htaccess';
+
+ if (!is_file($htaccess)) {
+ file_put_contents($htaccess, "# Apache 2.4+\n\n Require all denied\n\n# Apache 2.2\n\n Order deny,allow\n Deny from all\n\n");
+ }
+
+ $index = $backupDir . '/index.html';
+
+ if (!is_file($index)) {
+ file_put_contents($index, '');
+ }
}
}