fix: resolve relative paths in folder browser, add placeholders help modal

- browseDir() now resolves paths via BackupDirectory::resolve() before
  permission check, fixing "access denied" when browsing from [DEFAULT_DIR]
- SQL default for backup_dir changed to [DEFAULT_DIR] placeholder
- Added help button (?) next to Browse that opens a modal listing all
  available placeholders with examples and recommended paths
This commit is contained in:
Jonathan Miller
2026-06-11 12:30:23 -05:00
parent ace33b60fe
commit e8b40d9389
3 changed files with 47 additions and 3 deletions
@@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
`archive_format` VARCHAR(10) NOT NULL DEFAULT 'zip',
`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 'administrator/components/com_mokosuitebackup/backups',
`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',
`exclude_dirs` TEXT NOT NULL COMMENT 'Newline-separated directory paths to exclude',
`exclude_files` TEXT NOT NULL COMMENT 'Newline-separated filename patterns to exclude',
@@ -81,7 +81,7 @@ INSERT IGNORE INTO `#__mokosuitebackup_profiles` (
`published`, `ordering`, `created`, `modified`
) VALUES (
1, 'Default Backup Profile', 'Full site backup with default settings', 'full',
'zip', 5, 0, 'administrator/components/com_mokosuitebackup/backups',
'zip', 5, 0, '[DEFAULT_DIR]',
'administrator/components/com_mokosuitebackup/backups\ntmp\ncache\nlogs\nadministrator/logs',
'.gitignore\n.htaccess.bak',
'#__session',
@@ -82,7 +82,10 @@ class AjaxController extends BaseController
}
$requestPath = $this->input->getString('path', JPATH_ROOT);
$path = realpath($requestPath) ?: $requestPath;
// Resolve placeholders and relative paths before permission check
$resolved = BackupDirectory::resolve($requestPath);
$path = realpath($resolved) ?: $resolved;
// Security: restrict browsing to site root and current user's home
$jRoot = realpath(JPATH_ROOT);
@@ -96,6 +96,9 @@ 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">
<span class="icon-question-circle" aria-hidden="true"></span>
</button>
</div>
<div class="mt-1" id="{$id}_status">
<small class="{$statusClass}">
@@ -107,6 +110,44 @@ class FolderPickerField extends FormField
<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 class="modal fade" id="{$id}_helpModal" tabindex="-1" aria-labelledby="{$id}_helpLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="{$id}_helpLabel">Backup Directory Placeholders</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>
<table class="table table-sm table-striped">
<thead><tr><th>Placeholder</th><th>Description</th><th>Example</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>
</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>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div id="{$id}_browser" class="card mt-2" style="display:none; max-height:300px; overflow-y:auto;">
<div class="card-body p-2">
<div id="{$id}_tree"></div>