From 3c469f0dae0ee9ef13c8f49b38f5ff8ead7b9bc3 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Fri, 5 Jun 2026 00:11:46 -0500 Subject: [PATCH] feat: placeholder support, download fix, table exclusion modes, log viewer, detail view - Add PlaceholderResolver for [host], [date], [profile_name], etc. in backup_dir and new archive_name_format profile field - Fix download ERR_INVALID_RESPONSE by flushing output buffers before sending file headers - Table exclusions now have separate Data and Structure checkboxes (backward compatible with existing newline format) - Backup log files written alongside archive for posterity - Log viewer modal in backup records list and inline in detail view - Clickable record description links to detail view with checksum, file path, DB size, and full log - Dashboard health check shows actual resolved backup directory path - Fix update site link to use list view (avoids Joomla core bug) - Schema migration 01.01.09 for archive_name_format column Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/com_mokobackup/forms/profile.xml | 9 ++ .../language/en-GB/com_mokobackup.ini | 13 +- .../language/en-US/com_mokobackup.ini | 9 +- .../com_mokobackup/sql/install.mysql.sql | 1 + .../sql/updates/mysql/01.01.09.sql | 3 + .../src/Controller/AjaxController.php | 52 ++++++++ .../src/Controller/BackupsController.php | 27 ++-- .../src/Engine/BackupEngine.php | 35 ++++- .../src/Engine/DatabaseDumper.php | 102 ++++++++++++--- .../src/Engine/PlaceholderResolver.php | 122 ++++++++++++++++++ .../src/Engine/SteppedBackupEngine.php | 36 ++++-- .../src/Field/DatabaseTablesField.php | 82 +++++++++--- .../src/Model/DashboardModel.php | 28 +++- .../com_mokobackup/tmpl/backup/default.php | 69 +++++++++- .../com_mokobackup/tmpl/backups/default.php | 67 +++++++++- src/script.php | 2 +- 16 files changed, 584 insertions(+), 73 deletions(-) create mode 100644 src/packages/com_mokobackup/sql/updates/mysql/01.01.09.sql create mode 100644 src/packages/com_mokobackup/src/Engine/PlaceholderResolver.php diff --git a/src/packages/com_mokobackup/forms/profile.xml b/src/packages/com_mokobackup/forms/profile.xml index 827f6cd5..123e01c6 100644 --- a/src/packages/com_mokobackup/forms/profile.xml +++ b/src/packages/com_mokobackup/forms/profile.xml @@ -70,6 +70,15 @@ default="administrator/components/com_mokobackup/backups" addfieldprefix="Joomla\Component\MokoBackup\Administrator\Field" /> + sendJson(['error' => true, 'message' => 'Invalid token']); + + return; + } + + $id = $this->input->getInt('id', 0); + + if (!$id) { + $this->sendJson(['error' => true, 'message' => 'Missing record ID']); + + return; + } + + $db = \Joomla\CMS\Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName(['absolute_path', 'log'])) + ->from($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('id') . ' = ' . $id); + $db->setQuery($query); + $record = $db->loadObject(); + + if (!$record) { + $this->sendJson(['error' => true, 'message' => 'Record not found']); + + return; + } + + // Try to load log from file alongside the archive + $logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $record->absolute_path); + $logContent = ''; + + if (is_file($logPath)) { + $logContent = file_get_contents($logPath); + } elseif (!empty($record->log)) { + // Fall back to database-stored log + $logContent = $record->log; + } + + $this->sendJson([ + 'error' => false, + 'log' => $logContent ?: '(no log available)', + 'source' => is_file($logPath) ? 'file' : 'database', + ]); + } + /** * Send a JSON response and close the application. */ diff --git a/src/packages/com_mokobackup/src/Controller/BackupsController.php b/src/packages/com_mokobackup/src/Controller/BackupsController.php index 0ad04905..c8a3f156 100644 --- a/src/packages/com_mokobackup/src/Controller/BackupsController.php +++ b/src/packages/com_mokobackup/src/Controller/BackupsController.php @@ -68,17 +68,28 @@ class BackupsController extends AdminController return; } - $app = $this->app; - $app->clearHeaders(); - $app->setHeader('Content-Type', 'application/zip'); - $app->setHeader('Content-Disposition', 'attachment; filename="' . basename($item->archivename) . '"'); - $app->setHeader('Content-Length', (string) filesize($item->absolute_path)); - $app->setHeader('Cache-Control', 'no-cache, must-revalidate'); - $app->sendHeaders(); + // Flush any output buffers to prevent HTML mixing with binary data + while (@ob_end_clean()) { + // clear all buffers + } + + $filename = basename($item->archivename); + $filesize = filesize($item->absolute_path); + + // Detect content type from file extension + $contentType = str_ends_with($filename, '.tar.gz') + ? 'application/gzip' + : 'application/zip'; + + header('Content-Type: ' . $contentType); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + header('Content-Length: ' . $filesize); + header('Cache-Control: no-cache, must-revalidate'); + header('Pragma: no-cache'); readfile($item->absolute_path); - $app->close(); + $this->app->close(); } /** diff --git a/src/packages/com_mokobackup/src/Engine/BackupEngine.php b/src/packages/com_mokobackup/src/Engine/BackupEngine.php index 1b239fd8..59254f47 100644 --- a/src/packages/com_mokobackup/src/Engine/BackupEngine.php +++ b/src/packages/com_mokobackup/src/Engine/BackupEngine.php @@ -60,21 +60,24 @@ class BackupEngine $excludeFiles = $this->parseNewlineList($profile->exclude_files ?? ''); $excludeTables = $this->parseNewlineList($profile->exclude_tables ?? ''); - // Determine backup directory - $this->backupDir = JPATH_ROOT . '/' . ($profile->backup_dir ?: 'administrator/components/com_mokobackup/backups'); + // Resolve placeholders in directory and filename + $resolver = new PlaceholderResolver($profile); + + $configuredDir = $profile->backup_dir ?: 'administrator/components/com_mokobackup/backups'; + $this->backupDir = $this->resolveBackupDir($resolver->resolve($configuredDir)); if (!is_dir($this->backupDir)) { mkdir($this->backupDir, 0755, true); } // Create backup record - $now = date('Y-m-d H:i:s'); - $tag = date('Ymd_His'); - $hostname = preg_replace('/[^a-zA-Z0-9._-]/', '', $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? php_uname('n')); + $now = date('Y-m-d H:i:s'); + $tag = $resolver->getTag(); $archiveFormat = $profile->archive_format ?? 'zip'; $archiver = $this->createArchiver($archiveFormat); $archiveExt = $archiver->getExtension(); - $archiveName = $hostname . '_' . $tag . '_profile' . $profileId . '.' . $archiveExt; + $nameFormat = $profile->archive_name_format ?? '[host]_[datetime]_profile[profile_id]'; + $archiveName = $resolver->resolve($nameFormat) . '.' . $archiveExt; if (empty($description)) { $description = $profile->title . ' — ' . $now; @@ -233,6 +236,11 @@ class BackupEngine } } + // Write log file alongside the archive + $logContent = implode("\n", $this->log); + $logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $archivePath); + @file_put_contents($logPath, $logContent); + // Final record update $update = (object) [ 'id' => $recordId, @@ -246,7 +254,7 @@ class BackupEngine 'remote_filename' => $remoteFilename, 'checksum' => $checksum, 'manifest' => !empty($manifest) ? json_encode($manifest) : '', - 'log' => implode("\n", $this->log), + 'log' => $logContent, ]; $db->updateObject('#__mokobackup_records', $update, 'id'); @@ -489,6 +497,19 @@ 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 log(string $message): void { $this->log[] = '[' . date('H:i:s') . '] ' . $message; diff --git a/src/packages/com_mokobackup/src/Engine/DatabaseDumper.php b/src/packages/com_mokobackup/src/Engine/DatabaseDumper.php index 3c81269a..05661c58 100644 --- a/src/packages/com_mokobackup/src/Engine/DatabaseDumper.php +++ b/src/packages/com_mokobackup/src/Engine/DatabaseDumper.php @@ -16,15 +16,33 @@ use Joomla\CMS\Factory; class DatabaseDumper { - private array $excludeTables; + /** @var array Tables to exclude entirely (both structure and data) */ + private array $excludeBoth = []; + + /** @var array Tables to exclude data only (structure is kept) */ + private array $excludeDataOnly = []; + + /** @var array Tables to exclude structure only (data is kept — unusual) */ + private array $excludeStructureOnly = []; + private int $tablesCount = 0; /** - * @param array $excludeTables Table names to exclude (with #__ prefix) + * @param array $excludeTables Table names to exclude (with #__ prefix). + * Supports suffixes: :data-only, :structure-only. + * No suffix = exclude both (backward compatible). */ public function __construct(array $excludeTables = []) { - $this->excludeTables = $excludeTables; + foreach ($excludeTables as $entry) { + if (str_ends_with($entry, ':data-only')) { + $this->excludeDataOnly[] = substr($entry, 0, -10); + } elseif (str_ends_with($entry, ':structure-only')) { + $this->excludeStructureOnly[] = substr($entry, 0, -15); + } else { + $this->excludeBoth[] = $entry; + } + } } /** @@ -62,29 +80,49 @@ class DatabaseDumper // Check if excluded $abstractName = '#__' . substr($table, strlen($prefix)); - if ($this->isExcluded($abstractName, $table)) { + if ($this->isExcludedBoth($abstractName, $table)) { continue; } + $skipData = $this->isExcludedDataOnly($abstractName, $table); + $skipStructure = $this->isExcludedStructureOnly($abstractName, $table); + $this->tablesCount++; - // Get CREATE TABLE statement - $db->setQuery('SHOW CREATE TABLE ' . $db->quoteName($table)); - $createRow = $db->loadRow(); + $output[] = '-- --------------------------------------------------------'; + $output[] = '-- Table: ' . $table; - if (!$createRow || empty($createRow[1])) { - continue; + if ($skipData) { + $output[] = '-- (data excluded)'; + } + + if ($skipStructure) { + $output[] = '-- (structure excluded)'; } $output[] = '-- --------------------------------------------------------'; - $output[] = '-- Table: ' . $table; - $output[] = '-- --------------------------------------------------------'; - $output[] = ''; - $output[] = 'DROP TABLE IF EXISTS ' . $db->quoteName($table) . ';'; - $output[] = $createRow[1] . ';'; $output[] = ''; - // Dump data in chunks + // Get CREATE TABLE statement (unless structure is excluded) + if (!$skipStructure) { + $db->setQuery('SHOW CREATE TABLE ' . $db->quoteName($table)); + $createRow = $db->loadRow(); + + if (!$createRow || empty($createRow[1])) { + continue; + } + + $output[] = 'DROP TABLE IF EXISTS ' . $db->quoteName($table) . ';'; + $output[] = $createRow[1] . ';'; + $output[] = ''; + } + + // Dump data (unless data is excluded) + if ($skipData) { + $output[] = ''; + continue; + } + $db->setQuery('SELECT COUNT(*) FROM ' . $db->quoteName($table)); $rowCount = (int) $db->loadResult(); @@ -135,11 +173,39 @@ class DatabaseDumper } /** - * Check if a table is excluded. + * Check if a table is fully excluded (both data and structure). */ - private function isExcluded(string $abstractName, string $realName): bool + private function isExcludedBoth(string $abstractName, string $realName): bool { - foreach ($this->excludeTables as $pattern) { + foreach ($this->excludeBoth as $pattern) { + if ($pattern === $abstractName || $pattern === $realName) { + return true; + } + } + + return false; + } + + /** + * Check if a table's data is excluded (structure only). + */ + private function isExcludedDataOnly(string $abstractName, string $realName): bool + { + foreach ($this->excludeDataOnly as $pattern) { + if ($pattern === $abstractName || $pattern === $realName) { + return true; + } + } + + return false; + } + + /** + * Check if a table's structure is excluded (data only). + */ + private function isExcludedStructureOnly(string $abstractName, string $realName): bool + { + foreach ($this->excludeStructureOnly as $pattern) { if ($pattern === $abstractName || $pattern === $realName) { return true; } diff --git a/src/packages/com_mokobackup/src/Engine/PlaceholderResolver.php b/src/packages/com_mokobackup/src/Engine/PlaceholderResolver.php new file mode 100644 index 00000000..712b219a --- /dev/null +++ b/src/packages/com_mokobackup/src/Engine/PlaceholderResolver.php @@ -0,0 +1,122 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + * + * Resolves placeholders like [host], [date], [profile_name] in backup + * directory paths and archive filename formats. + */ + +namespace Joomla\Component\MokoBackup\Administrator\Engine; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; + +class PlaceholderResolver +{ + /** + * Supported placeholders and their descriptions (for documentation). + */ + public const PLACEHOLDERS = [ + '[host]' => 'Server hostname', + '[date]' => 'Date as Ymd (e.g. 20260604)', + '[time]' => 'Time as His (e.g. 143025)', + '[datetime]' => 'Date and time as Ymd_His', + '[year]' => 'Four-digit year', + '[month]' => 'Two-digit month', + '[day]' => 'Two-digit day', + '[hour]' => 'Two-digit hour (24h)', + '[minute]' => 'Two-digit minute', + '[second]' => 'Two-digit second', + '[profile_id]' => 'Backup profile ID', + '[profile_name]' => 'Profile title (sanitized)', + '[site_name]' => 'Joomla site name (sanitized)', + '[type]' => 'Backup type (full, database, files, differential)', + '[random]' => 'Random 6-character hex string', + ]; + + private array $replacements; + + /** + * @param object $profile The backup profile object + */ + public function __construct(object $profile) + { + $now = new \DateTimeImmutable('now'); + $hostname = preg_replace('/[^a-zA-Z0-9._-]/', '', $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? php_uname('n')); + + $siteName = ''; + + try { + $siteName = Factory::getApplication()->get('sitename', ''); + } catch (\Throwable $e) { + // Fallback: not critical + } + + $this->replacements = [ + '[host]' => $hostname, + '[date]' => $now->format('Ymd'), + '[time]' => $now->format('His'), + '[datetime]' => $now->format('Ymd_His'), + '[year]' => $now->format('Y'), + '[month]' => $now->format('m'), + '[day]' => $now->format('d'), + '[hour]' => $now->format('H'), + '[minute]' => $now->format('i'), + '[second]' => $now->format('s'), + '[profile_id]' => (string) ($profile->id ?? '0'), + '[profile_name]' => $this->sanitize($profile->title ?? 'default'), + '[site_name]' => $this->sanitize($siteName ?: 'joomla'), + '[type]' => $profile->backup_type ?? 'full', + '[random]' => bin2hex(random_bytes(3)), + ]; + } + + /** + * Replace all placeholders in a string. + * + * @param string $template String containing [placeholder] tokens + * + * @return string Resolved string + */ + public function resolve(string $template): string + { + return str_replace( + array_keys($this->replacements), + array_values($this->replacements), + $template + ); + } + + /** + * Get the raw hostname value (for backward compatibility). + */ + public function getHostname(): string + { + return $this->replacements['[host]']; + } + + /** + * Get the datetime tag value (for backward compatibility). + */ + public function getTag(): string + { + return $this->replacements['[datetime]']; + } + + /** + * Sanitize a string for use in filenames/paths. + * Keeps alphanumerics, dots, hyphens, underscores. Replaces spaces with hyphens. + */ + private function sanitize(string $value): string + { + $value = str_replace(' ', '-', trim($value)); + + return preg_replace('/[^a-zA-Z0-9._-]/', '', $value); + } +} diff --git a/src/packages/com_mokobackup/src/Engine/SteppedBackupEngine.php b/src/packages/com_mokobackup/src/Engine/SteppedBackupEngine.php index 8a550d48..7a7559a0 100644 --- a/src/packages/com_mokobackup/src/Engine/SteppedBackupEngine.php +++ b/src/packages/com_mokobackup/src/Engine/SteppedBackupEngine.php @@ -60,17 +60,18 @@ class SteppedBackupEngine $session->includeMokoRestore = (bool) ($profile->include_mokorestore ?? false); $session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true); - // Build archive path - $backupDir = JPATH_ROOT . '/' . $session->backupDir; + // Resolve placeholders in directory and filename + $resolver = new PlaceholderResolver($profile); + $backupDir = $this->resolveBackupDir($resolver->resolve($session->backupDir)); if (!is_dir($backupDir)) { mkdir($backupDir, 0755, true); } - $now = date('Y-m-d H:i:s'); - $tag = date('Ymd_His'); - $hostname = preg_replace('/[^a-zA-Z0-9._-]/', '', $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? php_uname('n')); - $archiveName = $hostname . '_' . $tag . '_profile' . $profileId . '.zip'; + $now = date('Y-m-d H:i:s'); + $tag = $resolver->getTag(); + $nameFormat = $profile->archive_name_format ?? '[host]_[datetime]_profile[profile_id]'; + $archiveName = $resolver->resolve($nameFormat) . '.zip'; $session->archivePath = $backupDir . '/' . $archiveName; $session->archiveName = $archiveName; @@ -408,12 +409,18 @@ class SteppedBackupEngine */ private function completeRecord(SteppedSession $session): void { - $db = Factory::getDbo(); + $db = Factory::getDbo(); + $logContent = implode("\n", $session->log); + + // Write log file alongside the archive + $logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $session->archivePath); + @file_put_contents($logPath, $logContent); + $update = (object) [ 'id' => $session->recordId, 'status' => 'complete', 'backupend' => date('Y-m-d H:i:s'), - 'log' => implode("\n", $session->log), + 'log' => $logContent, ]; $db->updateObject('#__mokobackup_records', $update, 'id'); @@ -536,6 +543,19 @@ 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 parseNewlineList(string $text): array { if (empty($text)) { diff --git a/src/packages/com_mokobackup/src/Field/DatabaseTablesField.php b/src/packages/com_mokobackup/src/Field/DatabaseTablesField.php index e5630177..937202db 100644 --- a/src/packages/com_mokobackup/src/Field/DatabaseTablesField.php +++ b/src/packages/com_mokobackup/src/Field/DatabaseTablesField.php @@ -26,17 +26,29 @@ class DatabaseTablesField extends FormField $tables = $db->getTableList(); $prefix = $db->getPrefix(); - // Parse current exclusions (newline-separated) - $excluded = []; + // Parse current exclusions (newline-separated, with optional :data-only suffix) + $excludeData = []; + $excludeStructure = []; if (!empty($this->value)) { - $excluded = array_filter(array_map('trim', explode("\n", str_replace("\r", '', $this->value)))); - } + $lines = array_filter(array_map('trim', explode("\n", str_replace("\r", '', $this->value)))); - // Normalize: replace literal #__ with actual prefix for comparison - $excludedNormalized = array_map(function ($t) use ($prefix) { - return str_replace('#__', $prefix, $t); - }, $excluded); + foreach ($lines as $line) { + // Normalize table name to real prefix for comparison + if (str_ends_with($line, ':data-only')) { + $tableName = str_replace('#__', $prefix, substr($line, 0, -10)); + $excludeData[$tableName] = true; + } elseif (str_ends_with($line, ':structure-only')) { + $tableName = str_replace('#__', $prefix, substr($line, 0, -15)); + $excludeStructure[$tableName] = true; + } else { + // No suffix = exclude both (backward compatible) + $tableName = str_replace('#__', $prefix, $line); + $excludeData[$tableName] = true; + $excludeStructure[$tableName] = true; + } + } + } $id = htmlspecialchars($this->id, ENT_QUOTES, 'UTF-8'); $name = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8'); @@ -47,12 +59,16 @@ class DatabaseTablesField extends FormField $html .= '
'; $html .= ''; $html .= ''; - $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; $html .= ''; $html .= ''; foreach ($tables as $table) { - $isExcluded = \in_array($table, $excludedNormalized, true); + $dataChecked = isset($excludeData[$table]) ? ' checked' : ''; + $structureChecked = isset($excludeStructure[$table]) ? ' checked' : ''; // Convert to #__ notation for storage $storeValue = $table; @@ -63,10 +79,12 @@ class DatabaseTablesField extends FormField $safeValue = htmlspecialchars($storeValue, ENT_QUOTES, 'UTF-8'); $safeTable = htmlspecialchars($table, ENT_QUOTES, 'UTF-8'); - $checked = $isExcluded ? ' checked' : ''; $html .= ''; - $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; $html .= ''; $html .= ''; } @@ -78,20 +96,44 @@ class DatabaseTablesField extends FormField diff --git a/src/packages/com_mokobackup/tmpl/backups/default.php b/src/packages/com_mokobackup/tmpl/backups/default.php index 590f519b..cba340d6 100644 --- a/src/packages/com_mokobackup/tmpl/backups/default.php +++ b/src/packages/com_mokobackup/tmpl/backups/default.php @@ -99,7 +99,12 @@ $listDirn = $this->escape($this->state->get('list.direction')); id); ?> -
' . Text::_('COM_MOKOBACKUP_FIELD_EXCLUDE_DATA') . '' . Text::_('COM_MOKOBACKUP_FIELD_EXCLUDE_STRUCTURE') . '' . Text::_('COM_MOKOBACKUP_FIELD_TABLE_NAME') . '
' . $safeTable . '
- escape($item->description); ?> + + escape($item->description); ?> + + checksum)) : ?> +
: checksum, 0, 16); ?>... +
escape($item->profile_title ?? 'Profile #' . $item->profile_id); ?> @@ -130,13 +135,18 @@ $listDirn = $this->escape($this->state->get('list.direction')); backupstart, Text::_('DATE_FORMAT_LC4')); ?> + status === 'complete' && $item->filesexist) : ?> + id; ?> @@ -274,5 +284,58 @@ $listDirn = $this->escape($this->state->get('list.direction')); // Expose for toolbar button window.mokobackupStart = startSteppedBackup; + + // View Log modal handler + document.addEventListener('click', function(e) { + var btn = e.target.closest('.mb-view-log'); + if (!btn) return; + e.preventDefault(); + var recordId = btn.getAttribute('data-id'); + var modal = document.getElementById('mb-log-modal'); + var body = document.getElementById('mb-log-body'); + body.textContent = 'Loading...'; + modal.style.display = 'block'; + + var form = new URLSearchParams(); + form.append('task', 'ajax.viewLog'); + form.append('id', recordId); + form.append(TOKEN_NAME, '1'); + + fetch(AJAX_URL, { + method: 'POST', + body: form, + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }) + .then(function(r) { return r.json(); }) + .then(function(data) { + if (data.error) { + body.textContent = data.message || 'Error loading log'; + } else { + body.textContent = data.log; + } + }) + .catch(function(err) { + body.textContent = 'Error: ' + err.message; + }); + }); + + document.addEventListener('click', function(e) { + if (e.target.id === 'mb-log-modal' || e.target.classList.contains('mb-log-close')) { + document.getElementById('mb-log-modal').style.display = 'none'; + } + }); })(); + + + diff --git a/src/script.php b/src/script.php index d5cc0001..d970bcdd 100644 --- a/src/script.php +++ b/src/script.php @@ -190,7 +190,7 @@ class Pkg_MokoBackupInstallerScript if ($updateSiteId > 0) { $editUrl = Route::_( - 'index.php?option=com_installer&view=updatesites&task=updatesite.edit&id=' . $updateSiteId + 'index.php?option=com_installer&view=updatesites&filter[search]=mokobackup' ); Factory::getApplication()->enqueueMessage(