2 Commits

Author SHA1 Message Date
gitea-actions[bot] 3ee620eca1 chore(version): pre-release bump to 01.04.01-dev [skip ci] 2026-06-07 11:56:00 +00:00
Jonathan Miller 608aeb3641 feat: add dashboard menu, [DEFAULT_DIR] placeholder, live dir validation, and backup security
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
- Add Dashboard as first submenu entry in component manifest
- Add [DEFAULT_DIR] placeholder to PlaceholderResolver for portable profiles
- Add live AJAX directory permission checking on backup_dir field changes
- Add web-accessible warning badge on backup download buttons
- Auto-create .htaccess protection in web-accessible backup dirs on profile save
- Auto-create .htaccess protection at backup time in both engines
- Add checkDir AJAX endpoint for real-time directory validation
- Fix script.php warnMissingLicenseKey running on uninstall
2026-06-07 06:54:46 -05:00
26 changed files with 270 additions and 18 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
<display-name>Package - MokoJoomBackup</display-name> <display-name>Package - MokoJoomBackup</display-name>
<org>MokoConsulting</org> <org>MokoConsulting</org>
<description>Full-site backup and restore for Joomla — database, files, and configuration</description> <description>Full-site backup and restore for Joomla — database, files, and configuration</description>
<version>01.04.00-dev</version> <version>01.04.01-dev</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity> </identity>
<governance> <governance>
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Automation # INGROUP: mokoplatform.Automation
# VERSION: 01.04.00 # VERSION: 01.04.01
# BRIEF: Auto-create feature branch when an issue is opened # BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch" name: "Universal: Issue Branch"
+1 -1
View File
@@ -1,6 +1,6 @@
# MokoJoomBackup # MokoJoomBackup
<!-- VERSION: 01.04.00 --> <!-- VERSION: 01.04.01 -->
Full-site backup and restore for Joomla — database, files, and configuration. Full-site backup and restore for Joomla — database, files, and configuration.
@@ -67,7 +67,7 @@
type="FolderPicker" type="FolderPicker"
label="COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR" label="COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR"
description="COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC" description="COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC"
default="administrator/components/com_mokojoombackup/backups" default="[DEFAULT_DIR]"
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field" addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field"
/> />
<field <field
@@ -269,6 +269,8 @@ COM_MOKOJOOMBACKUP_FIELD_NOTIFY_USER_GROUPS_DESC="Select Joomla user groups whos
COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING_TITLE="Backup directory is inside the web root" COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING_TITLE="Backup directory is inside the web root"
COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING="One or more profiles store backups in the default directory inside the web root. This may expose backup archives if .htaccess is not supported. Move backups to a directory outside the web root for better security." COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING="One or more profiles store backups in the default directory inside the web root. This may expose backup archives if .htaccess is not supported. Move backups to a directory outside the web root for better security."
COM_MOKOJOOMBACKUP_WEB_ACCESSIBLE_WARNING="This backup is stored inside the web root and may be directly downloadable if .htaccess is not supported."
; Errors ; Errors
COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND="Backup archive file not found or has been deleted." COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND="Backup archive file not found or has been deleted."
COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED="No backup record selected for restore." COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED="No backup record selected for restore."
@@ -6,5 +6,6 @@
COM_MOKOJOOMBACKUP="MokoJoomBackup" COM_MOKOJOOMBACKUP="MokoJoomBackup"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration." COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration."
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records" COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles" COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
@@ -53,6 +53,7 @@ COM_MOKOJOOMBACKUP_FOLDER_NOT_FOUND="Directory not found"
COM_MOKOJOOMBACKUP_BACKUP_DIR_DEFAULT="Default (inside web root)" COM_MOKOJOOMBACKUP_BACKUP_DIR_DEFAULT="Default (inside web root)"
COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING_TITLE="Backup directory is inside the web root" COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING_TITLE="Backup directory is inside the web root"
COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING="One or more profiles store backups in the default directory inside the web root. This may expose backup archives if .htaccess is not supported. Move backups to a directory outside the web root for better security." COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING="One or more profiles store backups in the default directory inside the web root. This may expose backup archives if .htaccess is not supported. Move backups to a directory outside the web root for better security."
COM_MOKOJOOMBACKUP_WEB_ACCESSIBLE_WARNING="This backup is stored inside the web root and may be directly downloadable if .htaccess is not supported."
COM_MOKOJOOMBACKUP_FOLDER_EXISTS="Directory exists" COM_MOKOJOOMBACKUP_FOLDER_EXISTS="Directory exists"
COM_MOKOJOOMBACKUP_FOLDER_NOT_FOUND="Directory not found" COM_MOKOJOOMBACKUP_FOLDER_NOT_FOUND="Directory not found"
COM_MOKOJOOMBACKUP_BACKUP_DIR_DEFAULT="Default (inside web root)" COM_MOKOJOOMBACKUP_BACKUP_DIR_DEFAULT="Default (inside web root)"
@@ -6,5 +6,6 @@
COM_MOKOJOOMBACKUP="MokoJoomBackup" COM_MOKOJOOMBACKUP="MokoJoomBackup"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration." COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration."
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records" COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles" COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
@@ -8,7 +8,7 @@
--> -->
<extension type="component" method="upgrade"> <extension type="component" method="upgrade">
<name>com_mokojoombackup</name> <name>com_mokojoombackup</name>
<version>01.04.00</version> <version>01.04.01-dev</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -40,6 +40,7 @@
<administration> <administration>
<menu img="class:archive">COM_MOKOJOOMBACKUP</menu> <menu img="class:archive">COM_MOKOJOOMBACKUP</menu>
<submenu> <submenu>
<menu link="option=com_mokojoombackup&amp;view=dashboard" img="class:home">COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD</menu>
<menu link="option=com_mokojoombackup&amp;view=backups" img="class:database">COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS</menu> <menu link="option=com_mokojoombackup&amp;view=backups" img="class:database">COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS</menu>
<menu link="option=com_mokojoombackup&amp;view=profiles" img="class:cog">COM_MOKOJOOMBACKUP_SUBMENU_PROFILES</menu> <menu link="option=com_mokojoombackup&amp;view=profiles" img="class:cog">COM_MOKOJOOMBACKUP_SUBMENU_PROFILES</menu>
</submenu> </submenu>
@@ -193,6 +193,60 @@ class AjaxController extends BaseController
]); ]);
} }
/**
* Check directory existence, writability and permissions.
* POST: task=ajax.checkDir&path=/some/path
*/
public function checkDir(): void
{
if (!Session::checkToken('get') && !Session::checkToken('post')) {
$this->sendJson(['error' => true, 'message' => 'Invalid token']);
return;
}
$rawPath = trim($this->input->getString('path', ''));
if ($rawPath === '') {
$this->sendJson(['error' => true, 'message' => 'No path provided']);
return;
}
// Resolve [DEFAULT_DIR] placeholder
$defaultDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
$resolved = str_replace('[DEFAULT_DIR]', $defaultDir, $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)) {
$this->sendJson([
'error' => false,
'exists' => null,
'writable' => null,
'resolved' => $resolved,
'placeholder' => true,
]);
return;
}
$exists = is_dir($resolved);
$writable = $exists && is_writable($resolved);
$this->sendJson([
'error' => false,
'exists' => $exists,
'writable' => $writable,
'resolved' => $resolved,
'placeholder' => false,
]);
}
/** /**
* Send a JSON response and close the application. * Send a JSON response and close the application.
*/ */
@@ -63,7 +63,7 @@ class BackupEngine
// Resolve placeholders in directory and filename // Resolve placeholders in directory and filename
$resolver = new PlaceholderResolver($profile); $resolver = new PlaceholderResolver($profile);
$configuredDir = $profile->backup_dir ?: 'administrator/components/com_mokojoombackup/backups'; $configuredDir = $profile->backup_dir ?: '[DEFAULT_DIR]';
$this->backupDir = $this->resolveBackupDir($resolver->resolve($configuredDir)); $this->backupDir = $this->resolveBackupDir($resolver->resolve($configuredDir));
if (!is_dir($this->backupDir)) { if (!is_dir($this->backupDir)) {
@@ -72,6 +72,8 @@ class BackupEngine
} }
} }
$this->protectBackupDir($this->backupDir);
// Create backup record // Create backup record
$now = date('Y-m-d H:i:s'); $now = date('Y-m-d H:i:s');
$tag = $resolver->getTag(); $tag = $resolver->getTag();
@@ -523,6 +525,21 @@ class BackupEngine
return JPATH_ROOT . '/' . $dir; return JPATH_ROOT . '/' . $dir;
} }
private function protectBackupDir(string $dir): void
{
$htaccess = $dir . '/.htaccess';
if (!is_file($htaccess)) {
@file_put_contents($htaccess, "Order deny,allow\nDeny from all\n");
}
$index = $dir . '/index.html';
if (!is_file($index)) {
@file_put_contents($index, '<!DOCTYPE html><title></title>');
}
}
private function log(string $message): void private function log(string $message): void
{ {
$this->log[] = '[' . date('H:i:s') . '] ' . $message; $this->log[] = '[' . date('H:i:s') . '] ' . $message;
@@ -38,6 +38,7 @@ class PlaceholderResolver
'[site_name]' => 'Joomla site name (sanitized)', '[site_name]' => 'Joomla site name (sanitized)',
'[type]' => 'Backup type (full, database, files, differential)', '[type]' => 'Backup type (full, database, files, differential)',
'[random]' => 'Random 6-character hex string', '[random]' => 'Random 6-character hex string',
'[DEFAULT_DIR]' => 'Default backup directory (administrator/components/com_mokojoombackup/backups)',
]; ];
private array $replacements; private array $replacements;
@@ -74,6 +75,7 @@ class PlaceholderResolver
'[site_name]' => $this->sanitize($siteName ?: 'joomla'), '[site_name]' => $this->sanitize($siteName ?: 'joomla'),
'[type]' => $profile->backup_type ?? 'full', '[type]' => $profile->backup_type ?? 'full',
'[random]' => bin2hex(random_bytes(3)), '[random]' => bin2hex(random_bytes(3)),
'[DEFAULT_DIR]' => JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups',
]; ];
} }
@@ -55,7 +55,7 @@ class SteppedBackupEngine
$session->excludeDirs = $this->parseNewlineList($profile->exclude_dirs ?? ''); $session->excludeDirs = $this->parseNewlineList($profile->exclude_dirs ?? '');
$session->excludeFiles = $this->parseNewlineList($profile->exclude_files ?? ''); $session->excludeFiles = $this->parseNewlineList($profile->exclude_files ?? '');
$session->excludeTables = $this->parseNewlineList($profile->exclude_tables ?? ''); $session->excludeTables = $this->parseNewlineList($profile->exclude_tables ?? '');
$session->backupDir = $profile->backup_dir ?: 'administrator/components/com_mokojoombackup/backups'; $session->backupDir = $profile->backup_dir ?: '[DEFAULT_DIR]';
$session->remoteStorage = $profile->remote_storage ?? 'none'; $session->remoteStorage = $profile->remote_storage ?? 'none';
$session->includeMokoRestore = (bool) ($profile->include_mokorestore ?? false); $session->includeMokoRestore = (bool) ($profile->include_mokorestore ?? false);
$session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true); $session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true);
@@ -70,6 +70,8 @@ class SteppedBackupEngine
} }
} }
$this->protectBackupDir($backupDir);
$now = date('Y-m-d H:i:s'); $now = date('Y-m-d H:i:s');
$tag = $resolver->getTag(); $tag = $resolver->getTag();
$nameFormat = $profile->archive_name_format ?? '[host]_[datetime]_profile[profile_id]'; $nameFormat = $profile->archive_name_format ?? '[host]_[datetime]_profile[profile_id]';
@@ -565,6 +567,21 @@ class SteppedBackupEngine
return JPATH_ROOT . '/' . $dir; return JPATH_ROOT . '/' . $dir;
} }
private function protectBackupDir(string $dir): void
{
$htaccess = $dir . '/.htaccess';
if (!is_file($htaccess)) {
@file_put_contents($htaccess, "Order deny,allow\nDeny from all\n");
}
$index = $dir . '/index.html';
if (!is_file($index)) {
@file_put_contents($index, '<!DOCTYPE html><title></title>');
}
}
private function parseNewlineList(string $text): array private function parseNewlineList(string $text): array
{ {
if (empty($text)) { if (empty($text)) {
@@ -49,6 +49,7 @@ class FolderPickerField extends FormField
$sanitizedSiteName = preg_replace('/[^a-zA-Z0-9._-]/', '', str_replace(' ', '-', trim($siteName))); $sanitizedSiteName = preg_replace('/[^a-zA-Z0-9._-]/', '', str_replace(' ', '-', trim($siteName)));
$placeholders = [ $placeholders = [
'[DEFAULT_DIR]' => JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups',
'[host]' => $hostname, '[host]' => $hostname,
'[site_name]' => $sanitizedSiteName ?: 'joomla', '[site_name]' => $sanitizedSiteName ?: 'joomla',
'[profile_id]' => '1', '[profile_id]' => '1',
@@ -88,7 +89,7 @@ class FolderPickerField extends FormField
<div class="input-group"> <div class="input-group">
<input type="text" name="{$name}" id="{$id}" value="{$value}" <input type="text" name="{$name}" id="{$id}" value="{$value}"
class="form-control" maxlength="512" class="form-control" maxlength="512"
placeholder="/home/user/backups/[host] or administrator/components/com_mokojoombackup/backups" /> placeholder="[DEFAULT_DIR] or /home/user/backups/[host]" />
<button type="button" class="btn btn-outline-secondary" id="{$id}_btn"> <button type="button" class="btn btn-outline-secondary" id="{$id}_btn">
<span class="icon-folder-open" aria-hidden="true"></span> <span class="icon-folder-open" aria-hidden="true"></span>
Browse Browse
@@ -100,6 +101,10 @@ class FolderPickerField extends FormField
{$statusDetail} {$statusDetail}
</small> </small>
</div> </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.
</div>
<div id="{$id}_browser" class="card mt-2" style="display:none; max-height:300px; overflow-y:auto;"> <div id="{$id}_browser" class="card mt-2" style="display:none; max-height:300px; overflow-y:auto;">
<div class="card-body p-2"> <div class="card-body p-2">
<div id="{$id}_tree"></div> <div id="{$id}_tree"></div>
@@ -132,6 +137,85 @@ class FolderPickerField extends FormField
return path; return path;
} }
var statusDiv = document.getElementById(fieldId + '_status');
var checkTimer = null;
function setStatus(cssClass, iconClass, message, codePath) {
while (statusDiv.firstChild) statusDiv.removeChild(statusDiv.firstChild);
var small = document.createElement('small');
small.className = cssClass;
var icon = document.createElement('span');
icon.className = iconClass;
icon.setAttribute('aria-hidden', 'true');
small.appendChild(icon);
small.appendChild(document.createTextNode(' ' + message));
if (codePath) {
small.appendChild(document.createTextNode(': '));
var code = document.createElement('code');
code.textContent = codePath;
small.appendChild(code);
}
statusDiv.appendChild(small);
}
function setDefaultDirWarning() {
var warn = document.getElementById(fieldId + '_defaultwarn');
var val = input.value.trim();
var isDefault = (!val || val === '[DEFAULT_DIR]' || val === 'administrator/components/com_mokojoombackup/backups');
if (warn) warn.style.display = isDefault ? 'block' : 'none';
}
function checkDirPermissions() {
var val = input.value.trim();
if (!val) return;
setStatus('text-muted', 'icon-spinner icon-spin', 'Checking...', null);
setDefaultDirWarning();
var form = new URLSearchParams();
form.append('task', 'ajax.checkDir');
form.append('path', val);
var tokenName = Joomla.getOptions('csrf.token') || '';
if (tokenName) form.append(tokenName, '1');
fetch('index.php?option=com_mokojoombackup&format=json', {
method: 'POST',
body: form,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) {
setStatus('text-danger', 'icon-unpublish', data.message || 'Error', null);
return;
}
if (data.placeholder) {
setStatus('text-info', 'icon-info-circle', 'Uses placeholders (resolved at backup time)', data.resolved);
return;
}
if (data.writable) {
setStatus('text-success', 'icon-publish', 'Writable', data.resolved);
} else if (data.exists) {
setStatus('text-warning', 'icon-warning-circle', 'Exists but not writable', data.resolved);
} else {
setStatus('text-danger', 'icon-unpublish', 'Directory not found', data.resolved);
}
})
.catch(function(err) {
setStatus('text-danger', 'icon-unpublish', 'Check failed: ' + err.message, null);
});
}
input.addEventListener('input', function() {
clearTimeout(checkTimer);
checkTimer = setTimeout(checkDirPermissions, 400);
});
input.addEventListener('change', function() {
clearTimeout(checkTimer);
checkDirPermissions();
});
btn.addEventListener('click', function() { btn.addEventListener('click', function() {
if (browser.style.display !== 'none') { if (browser.style.display !== 'none') {
browser.style.display = 'none'; browser.style.display = 'none';
@@ -205,11 +289,13 @@ class FolderPickerField extends FormField
e.preventDefault(); e.preventDefault();
// Store with placeholders reversed back in // Store with placeholders reversed back in
input.value = unresolve(dir.path); input.value = unresolve(dir.path);
checkDirPermissions();
loadDir(dir.path); loadDir(dir.path);
}); });
item.addEventListener('dblclick', function(e) { item.addEventListener('dblclick', function(e) {
e.preventDefault(); e.preventDefault();
input.value = unresolve(dir.path); input.value = unresolve(dir.path);
checkDirPermissions();
browser.style.display = 'none'; browser.style.display = 'none';
}); });
list.appendChild(item); list.appendChild(item);
@@ -232,6 +318,10 @@ class FolderPickerField extends FormField
tree.appendChild(info); tree.appendChild(info);
} }
// Run initial check on page load
setDefaultDirWarning();
checkDirPermissions();
})(); })();
</script> </script>
HTML; HTML;
@@ -189,6 +189,7 @@ class DashboardModel extends BaseDatabaseModel
->from($db->quoteName('#__mokojoombackup_profiles')) ->from($db->quoteName('#__mokojoombackup_profiles'))
->where($db->quoteName('published') . ' = 1') ->where($db->quoteName('published') . ' = 1')
->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote($default) ->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote($default)
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('[DEFAULT_DIR]')
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('') . ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('')
. ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)'); . ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)');
$db->setQuery($query); $db->setQuery($query);
@@ -22,6 +22,60 @@ class ProfileTable extends Table
parent::__construct('#__mokojoombackup_profiles', 'id', $db); parent::__construct('#__mokojoombackup_profiles', 'id', $db);
} }
public function store($updateNulls = true): bool
{
$result = parent::store($updateNulls);
if ($result && !empty($this->backup_dir)) {
$this->protectWebAccessibleDir($this->backup_dir);
}
return $result;
}
private function protectWebAccessibleDir(string $dir): void
{
// Resolve [DEFAULT_DIR] placeholder
$defaultDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
$resolved = str_replace('[DEFAULT_DIR]', $defaultDir, $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)) {
return;
}
// Only protect directories under the web root
$jRoot = realpath(JPATH_ROOT) ?: JPATH_ROOT;
$realDir = realpath($resolved) ?: $resolved;
if (strpos($realDir, $jRoot) !== 0) {
return;
}
if (!is_dir($resolved)) {
@mkdir($resolved, 0755, true);
}
if (is_dir($resolved)) {
$htaccess = $resolved . '/.htaccess';
if (!is_file($htaccess)) {
@file_put_contents($htaccess, "Order deny,allow\nDeny from all\n");
}
$index = $resolved . '/index.html';
if (!is_file($index)) {
@file_put_contents($index, '<!DOCTYPE html><title></title>');
}
}
}
public function check(): bool public function check(): bool
{ {
if (empty($this->title)) { if (empty($this->title)) {
@@ -137,10 +137,19 @@ $listDirn = $this->escape($this->state->get('list.direction'));
</td> </td>
<td class="d-flex gap-1"> <td class="d-flex gap-1">
<?php if ($item->status === 'complete' && $item->filesexist) : ?> <?php if ($item->status === 'complete' && $item->filesexist) : ?>
<?php
$isWebAccessible = !empty($item->absolute_path)
&& strpos(realpath($item->absolute_path) ?: $item->absolute_path, realpath(JPATH_ROOT) ?: JPATH_ROOT) === 0;
?>
<a href="<?php echo Route::_('index.php?option=com_mokojoombackup&task=backups.download&id=' . $item->id); ?>" <a href="<?php echo Route::_('index.php?option=com_mokojoombackup&task=backups.download&id=' . $item->id); ?>"
class="btn btn-sm btn-outline-primary" title="<?php echo Text::_('COM_MOKOJOOMBACKUP_DOWNLOAD'); ?>"> class="btn btn-sm btn-outline-primary" title="<?php echo Text::_('COM_MOKOJOOMBACKUP_DOWNLOAD'); ?>">
<span class="icon-download"></span> <span class="icon-download"></span>
</a> </a>
<?php if ($isWebAccessible) : ?>
<span class="badge bg-warning text-dark" title="<?php echo Text::_('COM_MOKOJOOMBACKUP_WEB_ACCESSIBLE_WARNING'); ?>">
<span class="icon-warning-circle" aria-hidden="true"></span>
</span>
<?php endif; ?>
<?php endif; ?> <?php endif; ?>
<button type="button" class="btn btn-sm btn-outline-secondary mb-view-log" <button type="button" class="btn btn-sm btn-outline-secondary mb-view-log"
data-id="<?php echo (int) $item->id; ?>" data-id="<?php echo (int) $item->id; ?>"
@@ -8,7 +8,7 @@
--> -->
<extension type="plugin" group="actionlog" method="upgrade"> <extension type="plugin" group="actionlog" method="upgrade">
<name>plg_actionlog_mokojoombackup</name> <name>plg_actionlog_mokojoombackup</name>
<version>01.04.00</version> <version>01.04.01-dev</version>
<creationDate>2026-06-04</creationDate> <creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -8,7 +8,7 @@
--> -->
<extension type="plugin" group="console" method="upgrade"> <extension type="plugin" group="console" method="upgrade">
<name>plg_console_mokojoombackup</name> <name>plg_console_mokojoombackup</name>
<version>01.04.00</version> <version>01.04.01-dev</version>
<creationDate>2026-06-04</creationDate> <creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -8,7 +8,7 @@
--> -->
<extension type="plugin" group="content" method="upgrade"> <extension type="plugin" group="content" method="upgrade">
<name>plg_content_mokojoombackup</name> <name>plg_content_mokojoombackup</name>
<version>01.04.00</version> <version>01.04.01-dev</version>
<creationDate>2026-06-04</creationDate> <creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="quickicon" method="upgrade"> <extension type="plugin" group="quickicon" method="upgrade">
<name>plg_quickicon_mokojoombackup</name> <name>plg_quickicon_mokojoombackup</name>
<version>01.04.00</version> <version>01.04.01-dev</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -8,7 +8,7 @@
--> -->
<extension type="plugin" group="system" method="upgrade"> <extension type="plugin" group="system" method="upgrade">
<name>plg_system_mokojoombackup</name> <name>plg_system_mokojoombackup</name>
<version>01.04.00</version> <version>01.04.01-dev</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -8,7 +8,7 @@
--> -->
<extension type="plugin" group="task" method="upgrade"> <extension type="plugin" group="task" method="upgrade">
<name>plg_task_mokojoombackup</name> <name>plg_task_mokojoombackup</name>
<version>01.04.00</version> <version>01.04.01-dev</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -8,7 +8,7 @@
--> -->
<extension type="plugin" group="webservices" method="upgrade"> <extension type="plugin" group="webservices" method="upgrade">
<name>plg_webservices_mokojoombackup</name> <name>plg_webservices_mokojoombackup</name>
<version>01.04.00</version> <version>01.04.01-dev</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
+1 -1
View File
@@ -8,7 +8,7 @@
<extension type="package" method="upgrade"> <extension type="package" method="upgrade">
<name>Package - MokoJoomBackup</name> <name>Package - MokoJoomBackup</name>
<packagename>mokojoombackup</packagename> <packagename>mokojoombackup</packagename>
<version>01.04.00</version> <version>01.04.01-dev</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
+4 -2
View File
@@ -203,8 +203,10 @@ class Pkg_MokoJoomBackupInstallerScript
} }
} }
// Warn if no license key configured // Warn if no license key configured (skip on uninstall)
$this->warnMissingLicenseKey(); if ($type !== 'uninstall') {
$this->warnMissingLicenseKey();
}
} }
/** /**