From 608aeb364127ecfe6c11b6c4629a8501afe2ed39 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 7 Jun 2026 06:54:12 -0500 Subject: [PATCH 1/8] feat: add dashboard menu, [DEFAULT_DIR] placeholder, live dir validation, and backup security - 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 --- .../com_mokojoombackup/forms/profile.xml | 2 +- .../language/en-GB/com_mokojoombackup.ini | 2 + .../language/en-GB/com_mokojoombackup.sys.ini | 1 + .../language/en-US/com_mokojoombackup.ini | 1 + .../language/en-US/com_mokojoombackup.sys.ini | 1 + .../com_mokojoombackup/mokojoombackup.xml | 1 + .../src/Controller/AjaxController.php | 54 +++++++++++ .../src/Engine/BackupEngine.php | 19 +++- .../src/Engine/PlaceholderResolver.php | 2 + .../src/Engine/SteppedBackupEngine.php | 19 +++- .../src/Field/FolderPickerField.php | 92 ++++++++++++++++++- .../src/Model/DashboardModel.php | 1 + .../src/Table/ProfileTable.php | 54 +++++++++++ .../tmpl/backups/default.php | 9 ++ source/script.php | 6 +- 15 files changed, 258 insertions(+), 6 deletions(-) diff --git a/source/packages/com_mokojoombackup/forms/profile.xml b/source/packages/com_mokojoombackup/forms/profile.xml index 34701a8..46aa934 100644 --- a/source/packages/com_mokojoombackup/forms/profile.xml +++ b/source/packages/com_mokojoombackup/forms/profile.xml @@ -67,7 +67,7 @@ type="FolderPicker" label="COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR" description="COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC" - default="administrator/components/com_mokojoombackup/backups" + default="[DEFAULT_DIR]" addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field" /> COM_MOKOJOOMBACKUP + COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS COM_MOKOJOOMBACKUP_SUBMENU_PROFILES diff --git a/source/packages/com_mokojoombackup/src/Controller/AjaxController.php b/source/packages/com_mokojoombackup/src/Controller/AjaxController.php index e08da9f..87f6656 100644 --- a/source/packages/com_mokojoombackup/src/Controller/AjaxController.php +++ b/source/packages/com_mokojoombackup/src/Controller/AjaxController.php @@ -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. */ diff --git a/source/packages/com_mokojoombackup/src/Engine/BackupEngine.php b/source/packages/com_mokojoombackup/src/Engine/BackupEngine.php index 62aa3eb..37f22cf 100644 --- a/source/packages/com_mokojoombackup/src/Engine/BackupEngine.php +++ b/source/packages/com_mokojoombackup/src/Engine/BackupEngine.php @@ -63,7 +63,7 @@ class BackupEngine // Resolve placeholders in directory and filename $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)); if (!is_dir($this->backupDir)) { @@ -72,6 +72,8 @@ class BackupEngine } } + $this->protectBackupDir($this->backupDir); + // Create backup record $now = date('Y-m-d H:i:s'); $tag = $resolver->getTag(); @@ -523,6 +525,21 @@ class BackupEngine 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, ''); + } + } + 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 cbac2c9..a4afed0 100644 --- a/source/packages/com_mokojoombackup/src/Engine/PlaceholderResolver.php +++ b/source/packages/com_mokojoombackup/src/Engine/PlaceholderResolver.php @@ -38,6 +38,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)', ]; private array $replacements; @@ -74,6 +75,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', ]; } diff --git a/source/packages/com_mokojoombackup/src/Engine/SteppedBackupEngine.php b/source/packages/com_mokojoombackup/src/Engine/SteppedBackupEngine.php index e54b1b6..582b3b7 100644 --- a/source/packages/com_mokojoombackup/src/Engine/SteppedBackupEngine.php +++ b/source/packages/com_mokojoombackup/src/Engine/SteppedBackupEngine.php @@ -55,7 +55,7 @@ class SteppedBackupEngine $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 ?: 'administrator/components/com_mokojoombackup/backups'; + $session->backupDir = $profile->backup_dir ?: '[DEFAULT_DIR]'; $session->remoteStorage = $profile->remote_storage ?? 'none'; $session->includeMokoRestore = (bool) ($profile->include_mokorestore ?? false); $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'); $tag = $resolver->getTag(); $nameFormat = $profile->archive_name_format ?? '[host]_[datetime]_profile[profile_id]'; @@ -565,6 +567,21 @@ class SteppedBackupEngine 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, ''); + } + } + private function parseNewlineList(string $text): array { if (empty($text)) { diff --git a/source/packages/com_mokojoombackup/src/Field/FolderPickerField.php b/source/packages/com_mokojoombackup/src/Field/FolderPickerField.php index f0ac4d2..7a9e801 100644 --- a/source/packages/com_mokojoombackup/src/Field/FolderPickerField.php +++ b/source/packages/com_mokojoombackup/src/Field/FolderPickerField.php @@ -49,6 +49,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', '[host]' => $hostname, '[site_name]' => $sanitizedSiteName ?: 'joomla', '[profile_id]' => '1', @@ -88,7 +89,7 @@ class FolderPickerField extends FormField
+ placeholder="[DEFAULT_DIR] or /home/user/backups/[host]" />
+