diff --git a/source/packages/com_mokojoombackup/language/en-GB/com_mokojoombackup.ini b/source/packages/com_mokojoombackup/language/en-GB/com_mokojoombackup.ini index f192354..5c8ea6c 100644 --- a/source/packages/com_mokojoombackup/language/en-GB/com_mokojoombackup.ini +++ b/source/packages/com_mokojoombackup/language/en-GB/com_mokojoombackup.ini @@ -252,6 +252,7 @@ COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_IP_DESC="Comma-separated list of IP addresses ; Folder picker COM_MOKOJOOMBACKUP_FOLDER_EXISTS="Directory exists" COM_MOKOJOOMBACKUP_FOLDER_NOT_FOUND="Directory not found" +COM_MOKOJOOMBACKUP_FOLDER_PLACEHOLDER="Uses placeholders (resolved at backup time)" COM_MOKOJOOMBACKUP_BACKUP_DIR_DEFAULT="Default (inside web root)" ; Exclude fields diff --git a/source/packages/com_mokojoombackup/src/Field/FolderPickerField.php b/source/packages/com_mokojoombackup/src/Field/FolderPickerField.php index 6e60dff..3cb0603 100644 --- a/source/packages/com_mokojoombackup/src/Field/FolderPickerField.php +++ b/source/packages/com_mokojoombackup/src/Field/FolderPickerField.php @@ -35,12 +35,22 @@ class FolderPickerField extends FormField $absPath = $rawValue; } - $exists = is_dir($absPath); - $statusClass = $exists ? 'text-success' : 'text-danger'; - $statusIcon = $exists ? 'icon-publish' : 'icon-unpublish'; - $statusText = $exists - ? Text::_('COM_MOKOJOOMBACKUP_FOLDER_EXISTS') - : Text::_('COM_MOKOJOOMBACKUP_FOLDER_NOT_FOUND'); + // If path contains placeholders, show info status instead of checking disk + $hasPlaceholders = preg_match('/\[.+\]/', $absPath); + + if ($hasPlaceholders) { + $statusClass = 'text-info'; + $statusIcon = 'icon-info-circle'; + $statusText = Text::_('COM_MOKOJOOMBACKUP_FOLDER_PLACEHOLDER'); + } else { + $exists = is_dir($absPath); + $statusClass = $exists ? 'text-success' : 'text-danger'; + $statusIcon = $exists ? 'icon-publish' : 'icon-unpublish'; + $statusText = $exists + ? Text::_('COM_MOKOJOOMBACKUP_FOLDER_EXISTS') + : Text::_('COM_MOKOJOOMBACKUP_FOLDER_NOT_FOUND'); + } + $absPathSafe = htmlspecialchars($absPath, ENT_QUOTES, 'UTF-8'); return << 'Backup Directory', - 'status' => $writable, - 'detail' => ($writable ? 'Writable' : 'Not writable or missing') . ' — ' . $backupDir, - ]; + // Skip filesystem check if path contains placeholders (resolved at backup time) + if (preg_match('/\[.+\]/', $backupDir)) { + $checks[] = (object) [ + 'label' => 'Backup Directory', + 'status' => true, + 'detail' => 'Uses placeholders (resolved at backup time) — ' . $backupDir, + ]; + } else { + $writable = is_dir($backupDir) && is_writable($backupDir); + $checks[] = (object) [ + 'label' => 'Backup Directory', + 'status' => $writable, + 'detail' => ($writable ? 'Writable' : 'Not writable or missing') . ' — ' . $backupDir, + ]; + } // Disk space $freeSpace = @disk_free_space($backupDir ?: JPATH_ROOT); diff --git a/source/packages/com_mokojoombackup/tmpl/backups/default.php b/source/packages/com_mokojoombackup/tmpl/backups/default.php index 233852d..fa79ddc 100644 --- a/source/packages/com_mokojoombackup/tmpl/backups/default.php +++ b/source/packages/com_mokojoombackup/tmpl/backups/default.php @@ -198,11 +198,24 @@ $listDirn = $this->escape($this->state->get('list.direction')); } }); + var backupRunning = false; + + function warnBeforeClose(e) { + if (backupRunning) { + e.preventDefault(); + e.returnValue = ''; + } + } + + window.addEventListener('beforeunload', warnBeforeClose); + function showModal() { + backupRunning = true; document.getElementById('mokojoombackup-modal').style.display = 'block'; } function hideModal() { + backupRunning = false; document.getElementById('mokojoombackup-modal').style.display = 'none'; } diff --git a/source/packages/com_mokojoombackup/tmpl/dashboard/default.php b/source/packages/com_mokojoombackup/tmpl/dashboard/default.php index d6c810a..cde9eeb 100644 --- a/source/packages/com_mokojoombackup/tmpl/dashboard/default.php +++ b/source/packages/com_mokojoombackup/tmpl/dashboard/default.php @@ -202,11 +202,19 @@ document.querySelectorAll('.mb-tile').forEach(function(tile) { const AJAX_URL = ; const TOKEN_NAME = ; + var backupRunning = false; + + window.addEventListener('beforeunload', function(e) { + if (backupRunning) { e.preventDefault(); e.returnValue = ''; } + }); + function showModal() { + backupRunning = true; document.getElementById('mokojoombackup-modal').style.display = 'block'; } function hideModal() { + backupRunning = false; document.getElementById('mokojoombackup-modal').style.display = 'none'; } diff --git a/source/script.php b/source/script.php index 017b868..972476d 100644 --- a/source/script.php +++ b/source/script.php @@ -49,6 +49,11 @@ class Pkg_MokoJoomBackupInstallerScript return false; } + // Save download key before Joomla re-registers the update site + if ($type === 'update') { + $this->preflight_saveKey(); + } + return true; } @@ -60,8 +65,53 @@ class Pkg_MokoJoomBackupInstallerScript * * @return void */ + /** + * Called before install/update to preserve the download key. + * + * Joomla re-registers update sites from the manifest on every update, + * which can reset the extra_query (download key). We save it here + * and restore it in postflight. + */ + private ?string $savedDownloadKey = null; + + public function preflight_saveKey(): void + { + try { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('us.extra_query')) + ->from($db->quoteName('#__update_sites', 'us')) + ->join( + 'INNER', + $db->quoteName('#__update_sites_extensions', 'use') + . ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id') + ) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'e') + . ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id') + ) + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokojoombackup')) + ->where($db->quoteName('e.type') . ' = ' . $db->quote('package')) + ->setLimit(1); + $db->setQuery($query); + $key = $db->loadResult(); + + if (!empty($key)) { + $this->savedDownloadKey = $key; + } + } catch (\Throwable $e) { + // Not critical + } + } + public function postflight(string $type, InstallerAdapter $parent): void { + // Restore download key if it was saved before update + if ($this->savedDownloadKey !== null) { + $this->restoreDownloadKey(); + } + if ($type === 'install') { // Enable the system plugin automatically on fresh install $db = Factory::getDbo(); @@ -157,6 +207,45 @@ class Pkg_MokoJoomBackupInstallerScript $this->showUpdateSiteNotice(); } + /** + * Restore the download key to the (possibly new) update site record. + */ + private function restoreDownloadKey(): void + { + try { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('us.update_site_id')) + ->from($db->quoteName('#__update_sites', 'us')) + ->join( + 'INNER', + $db->quoteName('#__update_sites_extensions', 'use') + . ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id') + ) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'e') + . ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id') + ) + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokojoombackup')) + ->where($db->quoteName('e.type') . ' = ' . $db->quote('package')) + ->setLimit(1); + $db->setQuery($query); + $updateSiteId = (int) $db->loadResult(); + + if ($updateSiteId > 0) { + $query = $db->getQuery(true) + ->update($db->quoteName('#__update_sites')) + ->set($db->quoteName('extra_query') . ' = ' . $db->quote($this->savedDownloadKey)) + ->where($db->quoteName('update_site_id') . ' = ' . $updateSiteId); + $db->setQuery($query); + $db->execute(); + } + } catch (\Throwable $e) { + // Not critical + } + } + /** * Show an info message linking directly to the update site record * so the user can configure their download key.