diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 11958bd..9a5ae00 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.00.00 +# VERSION: 02.52.22 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index 42cc677..d8f3a35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,20 @@ # Changelog ## [Unreleased] -## [02.52.18] --- 2026-06-30 +### Added +- Cancel Stalled toolbar button on Backup Records view to cancel backups stuck in "running" status +- New ACL permission `mokosuitebackup.backup.cancel` for cancel stalled action +- AJAX endpoint `ajax.cancelBackup` for programmatic/API cancel +- Auto-timeout failsafe: preflight auto-cancels "running" backups older than 30 minutes +- Pre-extension-update backup progress modal (Bootstrap 5 modal with stepped AJAX progress bar) + +### Fixed +- Pre-update backup ran synchronously with no browser feedback — page hung until complete +- Stalled backups permanently blocked future backups for the same profile +- Preflight error message now directs users to Cancel Stalled action ## [02.52.18] --- 2026-06-30 -## [01.45.00] --- 2026-06-28 - - ## [01.45.00] --- 2026-06-28 ## [01.43.35] --- 2026-06-28 diff --git a/SECURITY.md b/SECURITY.md index 5dfd6f1..46c440f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla INGROUP: Template-Joomla.Documentation REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla PATH: /SECURITY.md -VERSION: 02.52.18 +VERSION: 02.52.22 BRIEF: Security vulnerability reporting and handling policy --> diff --git a/source/packages/MokoSuiteClient b/source/packages/MokoSuiteClient index 0a9125e..c7e6670 160000 --- a/source/packages/MokoSuiteClient +++ b/source/packages/MokoSuiteClient @@ -1 +1 @@ -Subproject commit 0a9125e51956a084941abccdf2de8ddd064777e8 +Subproject commit c7e66705443f74e3ee2ffdfecc08224cc40240aa diff --git a/source/packages/com_mokosuitebackup/access.xml b/source/packages/com_mokosuitebackup/access.xml index 37c2f9d..6f3220e 100644 --- a/source/packages/com_mokosuitebackup/access.xml +++ b/source/packages/com_mokosuitebackup/access.xml @@ -15,5 +15,6 @@ + diff --git a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini index 8547103..738fb5a 100644 --- a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini +++ b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini @@ -450,6 +450,8 @@ COM_MOKOSUITEBACKUP_ACTION_BACKUP_COMPARE="Compare Backups" COM_MOKOSUITEBACKUP_ACTION_BACKUP_COMPARE_DESC="Allows users to compare two backup records side-by-side." COM_MOKOSUITEBACKUP_ACTION_BACKUP_BROWSE="Browse Archives" COM_MOKOSUITEBACKUP_ACTION_BACKUP_BROWSE_DESC="Allows users to view file listings inside backup archives without extracting." +COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL="Cancel Stalled Backup" +COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL_DESC="Allows users to cancel backup records stuck in running status and clean up partial archive files." ; Snapshot ACL COM_MOKOSUITEBACKUP_ACTION_SNAPSHOT_MANAGE="Manage Snapshots" @@ -500,6 +502,12 @@ COM_MOKOJOOMBACKUP_PURGE_INVALID_DATE="Invalid date. Please select a valid date. COM_MOKOJOOMBACKUP_PURGE_SUCCESS="%d backup(s) purged successfully." COM_MOKOJOOMBACKUP_PURGE_PARTIAL="%d backup(s) purged, but %d could not be deleted." +; Cancel Stalled Backup +COM_MOKOJOOMBACKUP_TOOLBAR_CANCEL_STALLED="Cancel Stalled" +COM_MOKOJOOMBACKUP_CANCEL_NONE_SELECTED="No backup records selected." +COM_MOKOJOOMBACKUP_CANCEL_NONE_RUNNING="None of the selected backups are in running status." +COM_MOKOJOOMBACKUP_CANCEL_SUCCESS="%d stalled backup(s) cancelled." + ; Remote Destinations (multi-remote) COM_MOKOJOOMBACKUP_REMOTE_DESTINATIONS="Remote Destinations" COM_MOKOJOOMBACKUP_REMOTE_ADD="Add Destination" diff --git a/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini index ef2e72b..1d7328d 100644 --- a/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini +++ b/source/packages/com_mokosuitebackup/language/en-US/com_mokosuitebackup.ini @@ -116,3 +116,13 @@ COM_MOKOJOOMBACKUP_PURGE_NONE_FOUND="No completed backups found before the selec COM_MOKOJOOMBACKUP_PURGE_INVALID_DATE="Invalid date. Please select a valid date." COM_MOKOJOOMBACKUP_PURGE_SUCCESS="%d backup(s) purged successfully." COM_MOKOJOOMBACKUP_PURGE_PARTIAL="%d backup(s) purged, but %d could not be deleted." + +; Cancel Stalled Backup +COM_MOKOJOOMBACKUP_TOOLBAR_CANCEL_STALLED="Cancel Stalled" +COM_MOKOJOOMBACKUP_CANCEL_NONE_SELECTED="No backup records selected." +COM_MOKOJOOMBACKUP_CANCEL_NONE_RUNNING="None of the selected backups are in running status." +COM_MOKOJOOMBACKUP_CANCEL_SUCCESS="%d stalled backup(s) cancelled." + +; ACL - Cancel +COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL="Cancel Stalled Backup" +COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL_DESC="Allows users to cancel backup records stuck in running status and clean up partial archive files." diff --git a/source/packages/com_mokosuitebackup/mokosuitebackup.xml b/source/packages/com_mokosuitebackup/mokosuitebackup.xml index 916c79c..6aeb44e 100644 --- a/source/packages/com_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/com_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> MokoSuiteBackup - 02.52.18 + 02.52.22 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.20.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.20.sql new file mode 100644 index 0000000..4812b35 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.20.sql @@ -0,0 +1 @@ +/* 02.52.20 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.21.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.21.sql new file mode 100644 index 0000000..5727cbf --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.21.sql @@ -0,0 +1 @@ +/* 02.52.21 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.22.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.22.sql new file mode 100644 index 0000000..dd661bd --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/02.52.22.sql @@ -0,0 +1 @@ +/* 02.52.22 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php index 159af3b..f064e0b 100644 --- a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php @@ -84,6 +84,67 @@ class AjaxController extends BaseController $this->sendJson($result); } + /** + * Cancel a backup record stuck in "running" status. + * POST: task=ajax.cancelBackup&id=123 + */ + public function cancelBackup(): void + { + if (!Session::checkToken('get') && !Session::checkToken('post')) { + $this->sendJson(['error' => true, 'message' => 'Invalid token'], 403); + + return; + } + + if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.cancel', 'com_mokosuitebackup')) { + $this->sendJson(['error' => true, 'message' => 'Access denied'], 403); + + return; + } + + $id = $this->input->getInt('id', 0); + + if (!$id) { + $this->sendJson(['error' => true, 'message' => 'Missing record ID']); + + return; + } + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName(['id', 'status', 'absolute_path'])) + ->from($db->quoteName('#__mokosuitebackup_records')) + ->where($db->quoteName('id') . ' = ' . $id); + $db->setQuery($query); + $record = $db->loadObject(); + + if (!$record) { + $this->sendJson(['error' => true, 'message' => 'Record not found'], 404); + + return; + } + + if ($record->status !== 'running') { + $this->sendJson(['error' => true, 'message' => 'Backup is not in running status']); + + return; + } + + $update = $db->getQuery(true) + ->update($db->quoteName('#__mokosuitebackup_records')) + ->set($db->quoteName('status') . ' = ' . $db->quote('fail')) + ->set($db->quoteName('backupend') . ' = ' . $db->quote(date('Y-m-d H:i:s'))) + ->where($db->quoteName('id') . ' = ' . $id); + $db->setQuery($update); + $db->execute(); + + if (!empty($record->absolute_path) && is_file($record->absolute_path)) { + @unlink($record->absolute_path); + } + + $this->sendJson(['error' => false, 'message' => 'Backup cancelled']); + } + /** * Browse server directories for the folder picker field. * POST: task=ajax.browseDir&path=/some/path diff --git a/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php b/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php index 1f09d49..b2bad20 100644 --- a/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/BackupsController.php @@ -235,6 +235,76 @@ class BackupsController extends AdminController $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); } + /** + * Cancel selected backup records that are stuck in "running" status. + * + * Sets their status to "fail", cleans up partial archive files, + * and destroys any associated stepped session. + */ + public function cancelStalled(): void + { + $this->checkToken(); + + if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.cancel', 'com_mokosuitebackup')) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); + + return; + } + + $cid = $this->input->get('cid', [], 'array'); + + if (empty($cid)) { + $this->setMessage(Text::_('COM_MOKOJOOMBACKUP_CANCEL_NONE_SELECTED'), 'warning'); + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); + + return; + } + + $db = $this->app->getContainer()->get('DatabaseDriver'); + $cancelled = 0; + $skipped = 0; + + foreach ($cid as $id) { + $id = (int) $id; + + $query = $db->getQuery(true) + ->select($db->quoteName(['id', 'status', 'absolute_path'])) + ->from($db->quoteName('#__mokosuitebackup_records')) + ->where($db->quoteName('id') . ' = ' . $id); + $db->setQuery($query); + $record = $db->loadObject(); + + if (!$record || $record->status !== 'running') { + $skipped++; + + continue; + } + + $update = $db->getQuery(true) + ->update($db->quoteName('#__mokosuitebackup_records')) + ->set($db->quoteName('status') . ' = ' . $db->quote('fail')) + ->set($db->quoteName('backupend') . ' = ' . $db->quote(date('Y-m-d H:i:s'))) + ->where($db->quoteName('id') . ' = ' . $id); + $db->setQuery($update); + $db->execute(); + + if (!empty($record->absolute_path) && is_file($record->absolute_path)) { + @unlink($record->absolute_path); + } + + $cancelled++; + } + + if ($cancelled > 0) { + $this->setMessage(Text::sprintf('COM_MOKOJOOMBACKUP_CANCEL_SUCCESS', $cancelled)); + } elseif ($skipped > 0) { + $this->setMessage(Text::_('COM_MOKOJOOMBACKUP_CANCEL_NONE_RUNNING'), 'warning'); + } + + $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); + } + /** * No-op target for the purge toolbar button. * diff --git a/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php b/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php index ac62cd3..64b42b5 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php +++ b/source/packages/com_mokosuitebackup/src/Engine/PreflightCheck.php @@ -194,22 +194,58 @@ class PreflightCheck } } + private const STALE_TIMEOUT_MINUTES = 30; + /** * Check if another backup is already running for this profile. + * + * Backups running longer than STALE_TIMEOUT_MINUTES are automatically + * marked as failed so they don't permanently block future runs. */ private function checkRunningBackup(object $profile, object $db): void { $query = $db->getQuery(true) - ->select('COUNT(*)') + ->select($db->quoteName(['id', 'backupstart', 'absolute_path'])) ->from($db->quoteName('#__mokosuitebackup_records')) ->where($db->quoteName('profile_id') . ' = ' . (int) $profile->id) ->where($db->quoteName('status') . ' = ' . $db->quote('running')); $db->setQuery($query); - $running = (int) $db->loadResult(); + $rows = $db->loadObjectList(); - if ($running > 0) { + if (empty($rows)) { + return; + } + + $cutoff = time() - (self::STALE_TIMEOUT_MINUTES * 60); + $stillAlive = 0; + + foreach ($rows as $row) { + $started = strtotime($row->backupstart); + + if ($started !== false && $started < $cutoff) { + $update = $db->getQuery(true) + ->update($db->quoteName('#__mokosuitebackup_records')) + ->set($db->quoteName('status') . ' = ' . $db->quote('fail')) + ->set($db->quoteName('backupend') . ' = ' . $db->quote(date('Y-m-d H:i:s'))) + ->where($db->quoteName('id') . ' = ' . (int) $row->id); + $db->setQuery($update); + $db->execute(); + + if (!empty($row->absolute_path) && is_file($row->absolute_path)) { + @unlink($row->absolute_path); + } + + $this->warnings[] = 'Auto-cancelled stalled backup #' . $row->id + . ' (started ' . $row->backupstart . ', exceeded ' + . self::STALE_TIMEOUT_MINUTES . ' min timeout)'; + } else { + $stillAlive++; + } + } + + if ($stillAlive > 0) { $this->errors[] = 'Another backup is already running for profile: ' . $profile->title - . ' — wait for it to finish or delete the stale record'; + . ' — wait for it to finish or use Cancel Stalled from the Backup Records toolbar'; } } diff --git a/source/packages/com_mokosuitebackup/src/View/Backups/HtmlView.php b/source/packages/com_mokosuitebackup/src/View/Backups/HtmlView.php index 4816383..3b45d0f 100644 --- a/source/packages/com_mokosuitebackup/src/View/Backups/HtmlView.php +++ b/source/packages/com_mokosuitebackup/src/View/Backups/HtmlView.php @@ -113,6 +113,10 @@ class HtmlView extends BaseHtmlView ToolbarHelper::custom('backups.compare', 'copy', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_COMPARE', true); } + if ($user->authorise('mokosuitebackup.backup.cancel', 'com_mokosuitebackup')) { + ToolbarHelper::custom('backups.cancelStalled', 'stop-circle', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_CANCEL_STALLED', true); + } + if ($user->authorise('core.delete', 'com_mokosuitebackup')) { ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'backups.delete'); } diff --git a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml index 8c6c5ba..f9cd809 100644 --- a/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml +++ b/source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml @@ -8,7 +8,7 @@ --> mod_mokosuitebackup_cpanel - 02.52.18 + 02.52.22 2026-06-23 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml index 14db151..e7759ab 100644 --- a/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Action Log - MokoSuiteBackup - 02.52.18 + 02.52.22 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml index 1fdeb74..94003d1 100644 --- a/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Console - MokoSuiteBackup - 02.52.18 + 02.52.22 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml index 0c325b4..38e1384 100644 --- a/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Content - MokoSuiteBackup - 02.52.18 + 02.52.22 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml index d00530b..a757f43 100644 --- a/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml @@ -1,7 +1,7 @@ Quick Icon - MokoSuiteBackup - 02.52.18 + 02.52.22 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml index 7ad71bf..0f6282c 100644 --- a/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> System - MokoSuiteBackup - 02.52.18 + 02.52.22 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml index 0a4e874..4643360 100644 --- a/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Task - MokoSuiteBackup - 02.52.18 + 02.52.22 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml index acd32a2..868f770 100644 --- a/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> Web Services - MokoSuiteBackup - 02.52.18 + 02.52.22 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/pkg_mokosuitebackup.xml b/source/pkg_mokosuitebackup.xml index aa7a8ae..fa57eb2 100644 --- a/source/pkg_mokosuitebackup.xml +++ b/source/pkg_mokosuitebackup.xml @@ -8,7 +8,7 @@ Package - MokoSuiteBackup mokosuitebackup - 02.52.18 + 02.52.22 2026-06-02 Moko Consulting hello@mokoconsulting.tech