From c1521cb2354c32b8cf735fb3867739fcb5cfc3ac Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 11:20:35 -0500 Subject: [PATCH] feat: add dashboard view and console, content, actionlog plugins (#24, #25, #26, #27) Dashboard view becomes the default landing page with status cards, quick actions (backup now w/ profile selector), and system health checks. Three new plugins round out the package: - plg_console_mokobackup: CLI commands (run, list, profiles, restore, cleanup) - plg_content_mokobackup: auto-backup before extension install/update - plg_actionlog_mokobackup: logs backup and profile actions to User Action Logs BackupEngine now dispatches onMokoBackupAfterRun for plugin listeners. Package manifest and install script updated to include and auto-enable the new plugins. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../language/en-GB/com_mokobackup.ini | 15 + .../language/en-US/com_mokobackup.ini | 13 + src/packages/com_mokobackup/mokobackup.xml | 1 + .../src/Controller/DisplayController.php | 2 +- .../src/Engine/BackupEngine.php | 29 ++ .../src/Model/DashboardModel.php | 163 +++++++++++ .../src/View/Dashboard/HtmlView.php | 48 ++++ .../com_mokobackup/tmpl/dashboard/default.php | 265 ++++++++++++++++++ .../en-GB/plg_actionlog_mokobackup.ini | 9 + .../en-GB/plg_actionlog_mokobackup.sys.ini | 3 + .../en-US/plg_actionlog_mokobackup.ini | 9 + .../en-US/plg_actionlog_mokobackup.sys.ini | 3 + .../plg_actionlog_mokobackup/mokobackup.php | 11 + .../plg_actionlog_mokobackup/mokobackup.xml | 32 +++ .../services/provider.php | 37 +++ .../src/Extension/MokoBackupActionlog.php | 174 ++++++++++++ .../language/en-GB/plg_console_mokobackup.ini | 3 + .../en-GB/plg_console_mokobackup.sys.ini | 3 + .../language/en-US/plg_console_mokobackup.ini | 3 + .../en-US/plg_console_mokobackup.sys.ini | 3 + .../plg_console_mokobackup/mokobackup.php | 11 + .../plg_console_mokobackup/mokobackup.xml | 32 +++ .../services/provider.php | 37 +++ .../src/Command/CleanupCommand.php | 125 +++++++++ .../src/Command/ListCommand.php | 87 ++++++ .../src/Command/ProfilesCommand.php | 68 +++++ .../src/Command/RestoreCommand.php | 101 +++++++ .../src/Command/RunCommand.php | 68 +++++ .../src/Extension/MokoBackupConsole.php | 45 +++ .../language/en-GB/plg_content_mokobackup.ini | 9 + .../en-GB/plg_content_mokobackup.sys.ini | 3 + .../language/en-US/plg_content_mokobackup.ini | 9 + .../en-US/plg_content_mokobackup.sys.ini | 3 + .../plg_content_mokobackup/mokobackup.php | 11 + .../plg_content_mokobackup/mokobackup.xml | 71 +++++ .../services/provider.php | 37 +++ .../src/Extension/MokoBackupContent.php | 95 +++++++ src/pkg_mokobackup.xml | 3 + src/script.php | 33 +++ 39 files changed, 1673 insertions(+), 1 deletion(-) create mode 100644 src/packages/com_mokobackup/src/Model/DashboardModel.php create mode 100644 src/packages/com_mokobackup/src/View/Dashboard/HtmlView.php create mode 100644 src/packages/com_mokobackup/tmpl/dashboard/default.php create mode 100644 src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.ini create mode 100644 src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.sys.ini create mode 100644 src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.ini create mode 100644 src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.sys.ini create mode 100644 src/packages/plg_actionlog_mokobackup/mokobackup.php create mode 100644 src/packages/plg_actionlog_mokobackup/mokobackup.xml create mode 100644 src/packages/plg_actionlog_mokobackup/services/provider.php create mode 100644 src/packages/plg_actionlog_mokobackup/src/Extension/MokoBackupActionlog.php create mode 100644 src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.ini create mode 100644 src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.sys.ini create mode 100644 src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.ini create mode 100644 src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.sys.ini create mode 100644 src/packages/plg_console_mokobackup/mokobackup.php create mode 100644 src/packages/plg_console_mokobackup/mokobackup.xml create mode 100644 src/packages/plg_console_mokobackup/services/provider.php create mode 100644 src/packages/plg_console_mokobackup/src/Command/CleanupCommand.php create mode 100644 src/packages/plg_console_mokobackup/src/Command/ListCommand.php create mode 100644 src/packages/plg_console_mokobackup/src/Command/ProfilesCommand.php create mode 100644 src/packages/plg_console_mokobackup/src/Command/RestoreCommand.php create mode 100644 src/packages/plg_console_mokobackup/src/Command/RunCommand.php create mode 100644 src/packages/plg_console_mokobackup/src/Extension/MokoBackupConsole.php create mode 100644 src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.ini create mode 100644 src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.sys.ini create mode 100644 src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.ini create mode 100644 src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.sys.ini create mode 100644 src/packages/plg_content_mokobackup/mokobackup.php create mode 100644 src/packages/plg_content_mokobackup/mokobackup.xml create mode 100644 src/packages/plg_content_mokobackup/services/provider.php create mode 100644 src/packages/plg_content_mokobackup/src/Extension/MokoBackupContent.php diff --git a/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini b/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini index 0921f20..442c06a 100644 --- a/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini +++ b/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini @@ -8,9 +8,24 @@ COM_MOKOBACKUP="MokoJoomBackup" COM_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla" ; Submenu +COM_MOKOBACKUP_SUBMENU_DASHBOARD="Dashboard" COM_MOKOBACKUP_SUBMENU_BACKUPS="Backup Records" COM_MOKOBACKUP_SUBMENU_PROFILES="Backup Profiles" +; Dashboard view +COM_MOKOBACKUP_DASHBOARD_TITLE="MokoJoomBackup Dashboard" +COM_MOKOBACKUP_DASHBOARD_LAST_BACKUP="Last Backup" +COM_MOKOBACKUP_DASHBOARD_NO_BACKUPS="No backups yet" +COM_MOKOBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled" +COM_MOKOBACKUP_DASHBOARD_NO_SCHEDULED="No tasks scheduled" +COM_MOKOBACKUP_DASHBOARD_TOTAL_BACKUPS="Total Backups" +COM_MOKOBACKUP_DASHBOARD_STORAGE="Storage Used" +COM_MOKOBACKUP_DASHBOARD_FAILURES_7D="%d failures (7 days)" +COM_MOKOBACKUP_DASHBOARD_QUICK_ACTIONS="Quick Actions" +COM_MOKOBACKUP_DASHBOARD_SCHEDULED_TASKS="Scheduled Tasks" +COM_MOKOBACKUP_DASHBOARD_UPDATE_SITE="Update Site" +COM_MOKOBACKUP_DASHBOARD_SYSTEM_HEALTH="System Health" + ; Backups view COM_MOKOBACKUP_BACKUPS_TITLE="Backup Records" COM_MOKOBACKUP_BACKUPS_TABLE_CAPTION="Table of backup records" diff --git a/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini b/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini index 309264d..577580b 100644 --- a/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini +++ b/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini @@ -6,8 +6,21 @@ COM_MOKOBACKUP="MokoJoomBackup" COM_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla" +COM_MOKOBACKUP_SUBMENU_DASHBOARD="Dashboard" COM_MOKOBACKUP_SUBMENU_BACKUPS="Backup Records" COM_MOKOBACKUP_SUBMENU_PROFILES="Backup Profiles" +COM_MOKOBACKUP_DASHBOARD_TITLE="MokoJoomBackup Dashboard" +COM_MOKOBACKUP_DASHBOARD_LAST_BACKUP="Last Backup" +COM_MOKOBACKUP_DASHBOARD_NO_BACKUPS="No backups yet" +COM_MOKOBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled" +COM_MOKOBACKUP_DASHBOARD_NO_SCHEDULED="No tasks scheduled" +COM_MOKOBACKUP_DASHBOARD_TOTAL_BACKUPS="Total Backups" +COM_MOKOBACKUP_DASHBOARD_STORAGE="Storage Used" +COM_MOKOBACKUP_DASHBOARD_FAILURES_7D="%d failures (7 days)" +COM_MOKOBACKUP_DASHBOARD_QUICK_ACTIONS="Quick Actions" +COM_MOKOBACKUP_DASHBOARD_SCHEDULED_TASKS="Scheduled Tasks" +COM_MOKOBACKUP_DASHBOARD_UPDATE_SITE="Update Site" +COM_MOKOBACKUP_DASHBOARD_SYSTEM_HEALTH="System Health" COM_MOKOBACKUP_BACKUPS_TITLE="Backup Records" COM_MOKOBACKUP_PROFILES_TITLE="Backup Profiles" COM_MOKOBACKUP_TOOLBAR_BACKUP_NOW="Backup Now" diff --git a/src/packages/com_mokobackup/mokobackup.xml b/src/packages/com_mokobackup/mokobackup.xml index 1dcc977..f715793 100644 --- a/src/packages/com_mokobackup/mokobackup.xml +++ b/src/packages/com_mokobackup/mokobackup.xml @@ -40,6 +40,7 @@ COM_MOKOBACKUP + COM_MOKOBACKUP_SUBMENU_DASHBOARD COM_MOKOBACKUP_SUBMENU_BACKUPS COM_MOKOBACKUP_SUBMENU_PROFILES diff --git a/src/packages/com_mokobackup/src/Controller/DisplayController.php b/src/packages/com_mokobackup/src/Controller/DisplayController.php index 5e4ec11..5425324 100644 --- a/src/packages/com_mokobackup/src/Controller/DisplayController.php +++ b/src/packages/com_mokobackup/src/Controller/DisplayController.php @@ -16,5 +16,5 @@ use Joomla\CMS\MVC\Controller\BaseController; class DisplayController extends BaseController { - protected $default_view = 'backups'; + protected $default_view = 'dashboard'; } diff --git a/src/packages/com_mokobackup/src/Engine/BackupEngine.php b/src/packages/com_mokobackup/src/Engine/BackupEngine.php index 60e0910..cf456cd 100644 --- a/src/packages/com_mokobackup/src/Engine/BackupEngine.php +++ b/src/packages/com_mokobackup/src/Engine/BackupEngine.php @@ -13,6 +13,7 @@ namespace Joomla\Component\MokoBackup\Administrator\Engine; defined('_JEXEC') or die; use Joomla\CMS\Factory; +use Joomla\Event\Event; class BackupEngine { @@ -250,6 +251,9 @@ class BackupEngine // Send success notification NotificationSender::send($profile, $update, true, implode("\n", $this->log)); + // Dispatch event for actionlog and other listeners + $this->dispatchAfterRun(true, $recordId, $description, $profileId, $origin); + return [ 'success' => true, 'message' => 'Backup complete: ' . $archiveName . ' (' . $sizeHuman . ')', @@ -275,6 +279,9 @@ class BackupEngine // Send failure notification NotificationSender::send($profile, $update, false, implode("\n", $this->log)); + // Dispatch event for actionlog and other listeners + $this->dispatchAfterRun(false, $recordId, $description, $profileId, $origin); + return ['success' => false, 'message' => 'Backup failed: ' . $e->getMessage(), 'record_id' => $recordId]; } } @@ -445,6 +452,28 @@ class BackupEngine )); } + /** + * Dispatch the onMokoBackupAfterRun event so plugins (actionlog, etc.) can react. + */ + private function dispatchAfterRun(bool $success, int $recordId, string $description, int $profileId, string $origin): void + { + try { + $app = Factory::getApplication(); + + $event = new Event('onMokoBackupAfterRun', [ + 'success' => $success, + 'record_id' => $recordId, + 'description' => $description, + 'profile_id' => $profileId, + 'origin' => $origin, + ]); + + $app->getDispatcher()->dispatch('onMokoBackupAfterRun', $event); + } catch (\Throwable $e) { + // Never let a listener failure break the backup result + } + } + private function log(string $message): void { $this->log[] = '[' . date('H:i:s') . '] ' . $message; diff --git a/src/packages/com_mokobackup/src/Model/DashboardModel.php b/src/packages/com_mokobackup/src/Model/DashboardModel.php new file mode 100644 index 0000000..d10a2b2 --- /dev/null +++ b/src/packages/com_mokobackup/src/Model/DashboardModel.php @@ -0,0 +1,163 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Component\MokoBackup\Administrator\Model; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Model\BaseDatabaseModel; + +class DashboardModel extends BaseDatabaseModel +{ + /** + * Get the most recent completed backup record. + * + * @return object|null + */ + public function getLastBackup(): ?object + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('r.*, p.title AS profile_title') + ->from($db->quoteName('#__mokobackup_records', 'r')) + ->join('LEFT', $db->quoteName('#__mokobackup_profiles', 'p') . ' ON p.id = r.profile_id') + ->where($db->quoteName('r.status') . ' = ' . $db->quote('complete')) + ->order($db->quoteName('r.backupend') . ' DESC'); + $db->setQuery($query, 0, 1); + + return $db->loadObject() ?: null; + } + + /** + * Query com_scheduler for the next scheduled MokoBackup task. + * + * @return object|null Object with next_execution and title, or null + */ + public function getNextScheduled(): ?object + { + $db = $this->getDatabase(); + + try { + $query = $db->getQuery(true) + ->select($db->quoteName(['t.next_execution', 't.title'])) + ->from($db->quoteName('#__scheduler_tasks', 't')) + ->where($db->quoteName('t.type') . ' = ' . $db->quote('mokobackup.run_profile')) + ->where($db->quoteName('t.state') . ' = 1') + ->order($db->quoteName('t.next_execution') . ' ASC'); + $db->setQuery($query, 0, 1); + + return $db->loadObject() ?: null; + } catch (\Throwable $e) { + return null; + } + } + + /** + * Get backup statistics. + * + * @return object Object with total_count, total_size, fail_count_7d + */ + public function getStats(): object + { + $db = $this->getDatabase(); + + // Total completed backups and storage + $query = $db->getQuery(true) + ->select('COUNT(*) AS total_count') + ->select('COALESCE(SUM(' . $db->quoteName('total_size') . '), 0) AS total_size') + ->from($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('status') . ' = ' . $db->quote('complete')); + $db->setQuery($query); + $stats = $db->loadObject(); + + // Failures in last 7 days + $cutoff = date('Y-m-d H:i:s', strtotime('-7 days')); + $query = $db->getQuery(true) + ->select('COUNT(*) AS fail_count') + ->from($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('status') . ' = ' . $db->quote('fail')) + ->where($db->quoteName('backupstart') . ' >= ' . $db->quote($cutoff)); + $db->setQuery($query); + $stats->fail_count_7d = (int) $db->loadResult(); + + return $stats; + } + + /** + * Check system health for backup readiness. + * + * @return array Array of check results [{label, status, detail}] + */ + public function getSystemHealth(): array + { + $checks = []; + + // PHP version + $checks[] = (object) [ + 'label' => 'PHP Version', + 'status' => version_compare(PHP_VERSION, '8.1.0', '>='), + 'detail' => PHP_VERSION, + ]; + + // ZipArchive extension + $checks[] = (object) [ + 'label' => 'ZipArchive', + 'status' => extension_loaded('zip'), + 'detail' => extension_loaded('zip') ? 'Loaded' : 'Not loaded', + ]; + + // AES-256 encryption support + $aesSupport = defined('ZipArchive::EM_AES_256'); + $checks[] = (object) [ + 'label' => 'AES-256 Encryption', + 'status' => $aesSupport, + 'detail' => $aesSupport ? 'Available' : 'Requires libzip 1.2.0+', + ]; + + // Backup directory writable + $backupDir = JPATH_ADMINISTRATOR . '/components/com_mokobackup/backups'; + $writable = is_dir($backupDir) && is_writable($backupDir); + $checks[] = (object) [ + 'label' => 'Backup Directory', + 'status' => $writable, + 'detail' => $writable ? 'Writable' : 'Not writable or missing', + ]; + + // Disk space + $freeSpace = @disk_free_space($backupDir ?: JPATH_ROOT); + $freeGB = $freeSpace ? round($freeSpace / 1073741824, 1) : 0; + $checks[] = (object) [ + 'label' => 'Free Disk Space', + 'status' => $freeGB >= 1.0, + 'detail' => $freeGB . ' GB free', + ]; + + return $checks; + } + + /** + * Get published backup profiles for the quick-action selector. + * + * @return array + */ + public function getProfiles(): array + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['id', 'title', 'backup_type'])) + ->from($db->quoteName('#__mokobackup_profiles')) + ->where($db->quoteName('published') . ' = 1') + ->order($db->quoteName('ordering') . ' ASC'); + $db->setQuery($query); + + return $db->loadObjectList() ?: []; + } +} diff --git a/src/packages/com_mokobackup/src/View/Dashboard/HtmlView.php b/src/packages/com_mokobackup/src/View/Dashboard/HtmlView.php new file mode 100644 index 0000000..bbfa660 --- /dev/null +++ b/src/packages/com_mokobackup/src/View/Dashboard/HtmlView.php @@ -0,0 +1,48 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Component\MokoBackup\Administrator\View\Dashboard; + +defined('_JEXEC') or die; + +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + public ?object $lastBackup = null; + public ?object $nextScheduled = null; + public object $stats; + public array $systemHealth = []; + public array $profiles = []; + + public function display($tpl = null): void + { + /** @var \Joomla\Component\MokoBackup\Administrator\Model\DashboardModel $model */ + $model = $this->getModel(); + + $this->lastBackup = $model->getLastBackup(); + $this->nextScheduled = $model->getNextScheduled(); + $this->stats = $model->getStats(); + $this->systemHealth = $model->getSystemHealth(); + $this->profiles = $model->getProfiles(); + + $this->addToolbar(); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + ToolbarHelper::title(Text::_('COM_MOKOBACKUP_DASHBOARD_TITLE'), 'archive'); + ToolbarHelper::preferences('com_mokobackup'); + } +} diff --git a/src/packages/com_mokobackup/tmpl/dashboard/default.php b/src/packages/com_mokobackup/tmpl/dashboard/default.php new file mode 100644 index 0000000..8d7d43c --- /dev/null +++ b/src/packages/com_mokobackup/tmpl/dashboard/default.php @@ -0,0 +1,265 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Session\Session; + +$ajaxToken = Session::getFormToken(); +$ajaxUrl = Route::_('index.php?option=com_mokobackup&format=json', false); +?> +
+ +
+
+
+ +
+ lastBackup) : ?> +

+ lastBackup->backupend, Text::_('DATE_FORMAT_LC4')); ?> +

+ + escape($this->lastBackup->profile_title); ?> + — + lastBackup->total_size); ?> + + +

+ +
+
+
+ +
+
+
+ +
+ nextScheduled) : ?> +

+ nextScheduled->next_execution, Text::_('DATE_FORMAT_LC4')); ?> +

+ escape($this->nextScheduled->title); ?> + +

+ +
+
+
+ +
+
+
+ +
+

stats->total_count; ?>

+
+
+
+ +
+
+
+ +
+

+ stats->total_size); ?> +

+ stats->fail_count_7d > 0) : ?> + + stats->fail_count_7d); ?> + + +
+
+
+
+ + +
+
+
+
+
+
+
+ profiles)) : ?> +
+ + +
+ + + +
+
+
+ + +
+
+
+
+
+
+ + + systemHealth as $check) : ?> + + + + + + + +
+ status) : ?> + + + + + escape($check->label); ?>escape($check->detail); ?>
+
+
+
+
+ + + + + diff --git a/src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.ini b/src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.ini new file mode 100644 index 0000000..6997740 --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.ini @@ -0,0 +1,9 @@ +; MokoJoomBackup — Actionlog Plugin language file (en-GB) +PLG_ACTIONLOG_MOKOBACKUP="Action Log - MokoJoomBackup" +PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs." +PLG_ACTIONLOG_MOKOBACKUP_PROFILE_CREATED="User {username} created backup profile "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_PROFILE_UPDATED="User {username} updated backup profile "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_PROFILE_DELETED="User {username} deleted backup profile "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_RECORD_DELETED="User {username} deleted backup record "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_BACKUP_COMPLETE="Backup completed: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})" +PLG_ACTIONLOG_MOKOBACKUP_BACKUP_FAILED="Backup FAILED: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})" diff --git a/src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.sys.ini b/src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.sys.ini new file mode 100644 index 0000000..3e1c655 --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.sys.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Actionlog Plugin system language file (en-GB) +PLG_ACTIONLOG_MOKOBACKUP="Action Log - MokoJoomBackup" +PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs." diff --git a/src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.ini b/src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.ini new file mode 100644 index 0000000..27cf1d6 --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.ini @@ -0,0 +1,9 @@ +; MokoJoomBackup — Actionlog Plugin language file (en-US) +PLG_ACTIONLOG_MOKOBACKUP="Action Log - MokoJoomBackup" +PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs." +PLG_ACTIONLOG_MOKOBACKUP_PROFILE_CREATED="User {username} created backup profile "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_PROFILE_UPDATED="User {username} updated backup profile "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_PROFILE_DELETED="User {username} deleted backup profile "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_RECORD_DELETED="User {username} deleted backup record "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_BACKUP_COMPLETE="Backup completed: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})" +PLG_ACTIONLOG_MOKOBACKUP_BACKUP_FAILED="Backup FAILED: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})" diff --git a/src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.sys.ini b/src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.sys.ini new file mode 100644 index 0000000..1737124 --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.sys.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Actionlog Plugin system language file (en-US) +PLG_ACTIONLOG_MOKOBACKUP="Action Log - MokoJoomBackup" +PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs." diff --git a/src/packages/plg_actionlog_mokobackup/mokobackup.php b/src/packages/plg_actionlog_mokobackup/mokobackup.php new file mode 100644 index 0000000..2a4226a --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/mokobackup.php @@ -0,0 +1,11 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; diff --git a/src/packages/plg_actionlog_mokobackup/mokobackup.xml b/src/packages/plg_actionlog_mokobackup/mokobackup.xml new file mode 100644 index 0000000..b5fe605 --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/mokobackup.xml @@ -0,0 +1,32 @@ + + + + plg_actionlog_mokobackup + 01.01.04-dev + 2026-06-04 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION + + Joomla\Plugin\Actionlog\MokoBackup + + + mokobackup.php + services + src + + + + language/en-GB/plg_actionlog_mokobackup.ini + language/en-GB/plg_actionlog_mokobackup.sys.ini + + diff --git a/src/packages/plg_actionlog_mokobackup/services/provider.php b/src/packages/plg_actionlog_mokobackup/services/provider.php new file mode 100644 index 0000000..b13a445 --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/services/provider.php @@ -0,0 +1,37 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\Actionlog\MokoBackup\Extension\MokoBackupActionlog; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MokoBackupActionlog( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('actionlog', 'mokobackup') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/src/packages/plg_actionlog_mokobackup/src/Extension/MokoBackupActionlog.php b/src/packages/plg_actionlog_mokobackup/src/Extension/MokoBackupActionlog.php new file mode 100644 index 0000000..2cb97e7 --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/src/Extension/MokoBackupActionlog.php @@ -0,0 +1,174 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Actionlog\MokoBackup\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Event\Model; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\Actionlogs\Administrator\Helper\ActionlogsHelper; +use Joomla\Event\Event; +use Joomla\Event\SubscriberInterface; + +final class MokoBackupActionlog extends CMSPlugin implements SubscriberInterface +{ + protected $autoloadLanguage = true; + + public static function getSubscribedEvents(): array + { + return [ + 'onContentAfterSave' => 'onContentAfterSave', + 'onContentAfterDelete' => 'onContentAfterDelete', + 'onMokoBackupAfterRun' => 'onMokoBackupAfterRun', + ]; + } + + /** + * Log when a backup profile is saved (created or updated). + */ + public function onContentAfterSave(Event $event): void + { + [$context, $table, $isNew] = array_values($event->getArguments()); + + if ($context !== 'com_mokobackup.profile') { + return; + } + + $messageKey = $isNew + ? 'PLG_ACTIONLOG_MOKOBACKUP_PROFILE_CREATED' + : 'PLG_ACTIONLOG_MOKOBACKUP_PROFILE_UPDATED'; + + $this->addLog( + [ + $messageKey, + 'id' => $table->id, + 'title' => $table->title, + 'userid' => $this->getCurrentUserId(), + 'username' => $this->getCurrentUserName(), + ], + $messageKey, + 'com_mokobackup.profile', + $this->getCurrentUserId() + ); + } + + /** + * Log when a backup profile or record is deleted. + */ + public function onContentAfterDelete(Event $event): void + { + [$context, $table] = array_values($event->getArguments()); + + if ($context === 'com_mokobackup.profile') { + $this->addLog( + [ + 'PLG_ACTIONLOG_MOKOBACKUP_PROFILE_DELETED', + 'id' => $table->id, + 'title' => $table->title ?? '', + 'userid' => $this->getCurrentUserId(), + 'username' => $this->getCurrentUserName(), + ], + 'PLG_ACTIONLOG_MOKOBACKUP_PROFILE_DELETED', + 'com_mokobackup.profile', + $this->getCurrentUserId() + ); + } elseif ($context === 'com_mokobackup.backup') { + $this->addLog( + [ + 'PLG_ACTIONLOG_MOKOBACKUP_RECORD_DELETED', + 'id' => $table->id, + 'title' => $table->description ?? 'Backup #' . $table->id, + 'userid' => $this->getCurrentUserId(), + 'username' => $this->getCurrentUserName(), + ], + 'PLG_ACTIONLOG_MOKOBACKUP_RECORD_DELETED', + 'com_mokobackup.backup', + $this->getCurrentUserId() + ); + } + } + + /** + * Log when a backup completes or fails. + * This event should be dispatched from BackupEngine. + */ + public function onMokoBackupAfterRun(Event $event): void + { + $args = $event->getArguments(); + + $success = $args['success'] ?? false; + $recordId = $args['record_id'] ?? 0; + $description = $args['description'] ?? ''; + $profileId = $args['profile_id'] ?? 0; + $origin = $args['origin'] ?? 'backend'; + + $messageKey = $success + ? 'PLG_ACTIONLOG_MOKOBACKUP_BACKUP_COMPLETE' + : 'PLG_ACTIONLOG_MOKOBACKUP_BACKUP_FAILED'; + + $this->addLog( + [ + $messageKey, + 'id' => $recordId, + 'title' => $description ?: 'Backup #' . $recordId, + 'profile_id' => $profileId, + 'origin' => $origin, + 'userid' => $this->getCurrentUserId(), + 'username' => $this->getCurrentUserName(), + ], + $messageKey, + 'com_mokobackup.backup', + $this->getCurrentUserId() + ); + } + + /** + * Write an action log entry. + */ + private function addLog(array $message, string $messageLanguageKey, string $context, int $userId): void + { + $params = [ + 'message_language_key' => $messageLanguageKey, + 'message' => json_encode($message), + 'date' => date('Y-m-d H:i:s'), + 'extension' => 'com_mokobackup', + 'user_id' => $userId, + 'ip_address' => ActionlogsHelper::getIp(), + 'item_id' => $message['id'] ?? 0, + ]; + + try { + $db = Factory::getDbo(); + $db->insertObject('#__action_logs', (object) $params); + } catch (\Throwable $e) { + // Non-critical — don't break the operation + } + } + + private function getCurrentUserId(): int + { + try { + return (int) Factory::getApplication()->getIdentity()->id; + } catch (\Throwable $e) { + return 0; + } + } + + private function getCurrentUserName(): string + { + try { + return Factory::getApplication()->getIdentity()->username ?: 'system'; + } catch (\Throwable $e) { + return 'system'; + } + } +} diff --git a/src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.ini b/src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.ini new file mode 100644 index 0000000..4b87bca --- /dev/null +++ b/src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Console Plugin language file (en-GB) +PLG_CONSOLE_MOKOBACKUP="Console - MokoJoomBackup" +PLG_CONSOLE_MOKOBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup." diff --git a/src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.sys.ini b/src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.sys.ini new file mode 100644 index 0000000..02fb8d8 --- /dev/null +++ b/src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.sys.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Console Plugin system language file (en-GB) +PLG_CONSOLE_MOKOBACKUP="Console - MokoJoomBackup" +PLG_CONSOLE_MOKOBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup." diff --git a/src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.ini b/src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.ini new file mode 100644 index 0000000..9fa5c15 --- /dev/null +++ b/src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Console Plugin language file (en-US) +PLG_CONSOLE_MOKOBACKUP="Console - MokoJoomBackup" +PLG_CONSOLE_MOKOBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup." diff --git a/src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.sys.ini b/src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.sys.ini new file mode 100644 index 0000000..d22c08c --- /dev/null +++ b/src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.sys.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Console Plugin system language file (en-US) +PLG_CONSOLE_MOKOBACKUP="Console - MokoJoomBackup" +PLG_CONSOLE_MOKOBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup." diff --git a/src/packages/plg_console_mokobackup/mokobackup.php b/src/packages/plg_console_mokobackup/mokobackup.php new file mode 100644 index 0000000..724a1bb --- /dev/null +++ b/src/packages/plg_console_mokobackup/mokobackup.php @@ -0,0 +1,11 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; diff --git a/src/packages/plg_console_mokobackup/mokobackup.xml b/src/packages/plg_console_mokobackup/mokobackup.xml new file mode 100644 index 0000000..c641770 --- /dev/null +++ b/src/packages/plg_console_mokobackup/mokobackup.xml @@ -0,0 +1,32 @@ + + + + plg_console_mokobackup + 01.01.04-dev + 2026-06-04 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_CONSOLE_MOKOBACKUP_DESCRIPTION + + Joomla\Plugin\Console\MokoBackup + + + mokobackup.php + services + src + + + + language/en-GB/plg_console_mokobackup.ini + language/en-GB/plg_console_mokobackup.sys.ini + + diff --git a/src/packages/plg_console_mokobackup/services/provider.php b/src/packages/plg_console_mokobackup/services/provider.php new file mode 100644 index 0000000..3bacb2f --- /dev/null +++ b/src/packages/plg_console_mokobackup/services/provider.php @@ -0,0 +1,37 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\Console\MokoBackup\Extension\MokoBackupConsole; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MokoBackupConsole( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('console', 'mokobackup') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/src/packages/plg_console_mokobackup/src/Command/CleanupCommand.php b/src/packages/plg_console_mokobackup/src/Command/CleanupCommand.php new file mode 100644 index 0000000..b26a2a5 --- /dev/null +++ b/src/packages/plg_console_mokobackup/src/Command/CleanupCommand.php @@ -0,0 +1,125 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Console\MokoBackup\Command; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\Console\Command\AbstractCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class CleanupCommand extends AbstractCommand +{ + protected static $defaultName = 'mokobackup:cleanup'; + + protected function configure(): void + { + $this->setDescription('Clean up old backup records and archive files'); + $this->addOption('max-age', null, InputOption::VALUE_REQUIRED, 'Max age in days', '30'); + $this->addOption('max-count', null, InputOption::VALUE_REQUIRED, 'Max number of backups to keep', '10'); + $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be deleted without deleting'); + } + + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $maxAge = (int) $input->getOption('max-age'); + $maxCount = (int) $input->getOption('max-count'); + $dryRun = $input->getOption('dry-run'); + + $io->title('MokoJoomBackup — Cleanup'); + + if ($dryRun) { + $io->note('Dry run — no files will be deleted.'); + } + + $db = Factory::getDbo(); + $deleted = 0; + + // Delete by age + $cutoff = date('Y-m-d H:i:s', strtotime("-{$maxAge} days")); + $query = $db->getQuery(true) + ->select('id, absolute_path, description, backupstart') + ->from($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff)) + ->where($db->quoteName('status') . ' = ' . $db->quote('complete')); + $db->setQuery($query); + $expired = $db->loadObjectList(); + + foreach ($expired as $record) { + $io->text('Expired: #' . $record->id . ' — ' . $record->backupstart . ' — ' . ($record->description ?: 'no description')); + + if (!$dryRun) { + if (!empty($record->absolute_path) && is_file($record->absolute_path)) { + @unlink($record->absolute_path); + } + + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('id') . ' = ' . (int) $record->id) + ); + $db->execute(); + } + + $deleted++; + } + + // Enforce max count + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('status') . ' = ' . $db->quote('complete')); + $db->setQuery($query); + $totalCount = (int) $db->loadResult(); + + if ($totalCount > $maxCount) { + $excess = $totalCount - $maxCount; + $query = $db->getQuery(true) + ->select('id, absolute_path, description, backupstart') + ->from($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('status') . ' = ' . $db->quote('complete')) + ->order($db->quoteName('backupstart') . ' ASC'); + $db->setQuery($query, 0, $excess); + $oldest = $db->loadObjectList(); + + foreach ($oldest as $record) { + $io->text('Over limit: #' . $record->id . ' — ' . $record->backupstart); + + if (!$dryRun) { + if (!empty($record->absolute_path) && is_file($record->absolute_path)) { + @unlink($record->absolute_path); + } + + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('id') . ' = ' . (int) $record->id) + ); + $db->execute(); + } + + $deleted++; + } + } + + if ($deleted === 0) { + $io->success('No backups to clean up.'); + } else { + $io->success(($dryRun ? 'Would delete ' : 'Deleted ') . $deleted . ' backup record(s).'); + } + + return self::SUCCESS; + } +} diff --git a/src/packages/plg_console_mokobackup/src/Command/ListCommand.php b/src/packages/plg_console_mokobackup/src/Command/ListCommand.php new file mode 100644 index 0000000..8fcdddd --- /dev/null +++ b/src/packages/plg_console_mokobackup/src/Command/ListCommand.php @@ -0,0 +1,87 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Console\MokoBackup\Command; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\Console\Command\AbstractCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class ListCommand extends AbstractCommand +{ + protected static $defaultName = 'mokobackup:list'; + + protected function configure(): void + { + $this->setDescription('List backup records'); + $this->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Number of records to show', '20'); + $this->addOption('status', 's', InputOption::VALUE_OPTIONAL, 'Filter by status (complete, fail, running)'); + } + + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $limit = (int) $input->getOption('limit'); + $status = $input->getOption('status'); + + $io->title('MokoJoomBackup — Backup Records'); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('r.id, r.description, r.status, r.origin, r.backup_type, r.total_size, r.backupstart, r.backupend') + ->select($db->quoteName('p.title', 'profile_title')) + ->from($db->quoteName('#__mokobackup_records', 'r')) + ->join('LEFT', $db->quoteName('#__mokobackup_profiles', 'p') . ' ON p.id = r.profile_id') + ->order($db->quoteName('r.backupstart') . ' DESC'); + + if ($status) { + $query->where($db->quoteName('r.status') . ' = ' . $db->quote($status)); + } + + $db->setQuery($query, 0, $limit); + $records = $db->loadObjectList(); + + if (empty($records)) { + $io->info('No backup records found.'); + + return self::SUCCESS; + } + + $rows = []; + + foreach ($records as $record) { + $size = $record->total_size > 0 + ? round($record->total_size / 1048576, 2) . ' MB' + : '—'; + + $rows[] = [ + $record->id, + $record->profile_title ?: '—', + $record->status, + $record->backup_type, + $size, + $record->origin, + $record->backupstart, + ]; + } + + $io->table( + ['ID', 'Profile', 'Status', 'Type', 'Size', 'Origin', 'Started'], + $rows + ); + + return self::SUCCESS; + } +} diff --git a/src/packages/plg_console_mokobackup/src/Command/ProfilesCommand.php b/src/packages/plg_console_mokobackup/src/Command/ProfilesCommand.php new file mode 100644 index 0000000..4d81616 --- /dev/null +++ b/src/packages/plg_console_mokobackup/src/Command/ProfilesCommand.php @@ -0,0 +1,68 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Console\MokoBackup\Command; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\Console\Command\AbstractCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class ProfilesCommand extends AbstractCommand +{ + protected static $defaultName = 'mokobackup:profiles'; + + protected function configure(): void + { + $this->setDescription('List available backup profiles'); + } + + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $io->title('MokoJoomBackup — Backup Profiles'); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('id, title, backup_type, published, ordering') + ->from($db->quoteName('#__mokobackup_profiles')) + ->order($db->quoteName('ordering') . ' ASC'); + $db->setQuery($query); + $profiles = $db->loadObjectList(); + + if (empty($profiles)) { + $io->info('No backup profiles found.'); + + return self::SUCCESS; + } + + $rows = []; + + foreach ($profiles as $profile) { + $rows[] = [ + $profile->id, + $profile->title, + $profile->backup_type, + $profile->published ? 'Yes' : 'No', + ]; + } + + $io->table( + ['ID', 'Title', 'Type', 'Published'], + $rows + ); + + return self::SUCCESS; + } +} diff --git a/src/packages/plg_console_mokobackup/src/Command/RestoreCommand.php b/src/packages/plg_console_mokobackup/src/Command/RestoreCommand.php new file mode 100644 index 0000000..fb857d4 --- /dev/null +++ b/src/packages/plg_console_mokobackup/src/Command/RestoreCommand.php @@ -0,0 +1,101 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Console\MokoBackup\Command; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\Component\MokoBackup\Administrator\Engine\RestoreEngine; +use Joomla\Console\Command\AbstractCommand; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class RestoreCommand extends AbstractCommand +{ + protected static $defaultName = 'mokobackup:restore'; + + protected function configure(): void + { + $this->setDescription('Restore a backup by record ID'); + $this->addArgument('id', InputArgument::REQUIRED, 'Backup record ID to restore'); + } + + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $recordId = (int) $input->getArgument('id'); + + $io->title('MokoJoomBackup — Restore Backup'); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('id') . ' = ' . $recordId); + $db->setQuery($query); + $record = $db->loadObject(); + + if (!$record) { + $io->error('Backup record not found: ' . $recordId); + + return self::FAILURE; + } + + if ($record->status !== 'complete') { + $io->error('Cannot restore — backup status is: ' . $record->status); + + return self::FAILURE; + } + + if (empty($record->absolute_path) || !is_file($record->absolute_path)) { + $io->error('Backup archive not found: ' . ($record->absolute_path ?: 'no path')); + + return self::FAILURE; + } + + $io->warning('This will overwrite the current site files and/or database.'); + $io->text('Archive: ' . $record->absolute_path); + $io->text('Type: ' . $record->backup_type); + + if (!$io->confirm('Are you sure you want to continue?', false)) { + $io->info('Restore cancelled.'); + + return self::SUCCESS; + } + + $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokobackup/src/Engine/RestoreEngine.php'; + + if (!file_exists($engineFile)) { + $io->error('RestoreEngine not found. Is the component fully installed?'); + + return self::FAILURE; + } + + if (!class_exists(RestoreEngine::class)) { + require_once $engineFile; + } + + $engine = new RestoreEngine(); + $result = $engine->restore($record->absolute_path, $record->backup_type); + + if ($result['success']) { + $io->success($result['message']); + + return self::SUCCESS; + } + + $io->error($result['message']); + + return self::FAILURE; + } +} diff --git a/src/packages/plg_console_mokobackup/src/Command/RunCommand.php b/src/packages/plg_console_mokobackup/src/Command/RunCommand.php new file mode 100644 index 0000000..52185b9 --- /dev/null +++ b/src/packages/plg_console_mokobackup/src/Command/RunCommand.php @@ -0,0 +1,68 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Console\MokoBackup\Command; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\Component\MokoBackup\Administrator\Engine\BackupEngine; +use Joomla\Console\Command\AbstractCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class RunCommand extends AbstractCommand +{ + protected static $defaultName = 'mokobackup:run'; + + protected function configure(): void + { + $this->setDescription('Run a backup using a specified profile'); + $this->addOption('profile', 'p', InputOption::VALUE_REQUIRED, 'Profile ID to use', '1'); + $this->addOption('description', 'd', InputOption::VALUE_OPTIONAL, 'Backup description', ''); + } + + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $profileId = (int) $input->getOption('profile'); + $desc = $input->getOption('description') ?: ''; + + $io->title('MokoJoomBackup — Run Backup'); + $io->text('Profile ID: ' . $profileId); + + $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokobackup/src/Engine/BackupEngine.php'; + + if (!file_exists($engineFile)) { + $io->error('MokoJoomBackup component not installed.'); + + return self::FAILURE; + } + + if (!class_exists(BackupEngine::class)) { + require_once $engineFile; + } + + $engine = new BackupEngine(); + $result = $engine->run($profileId, $desc ?: 'CLI backup', 'cli'); + + if ($result['success']) { + $io->success($result['message']); + + return self::SUCCESS; + } + + $io->error($result['message']); + + return self::FAILURE; + } +} diff --git a/src/packages/plg_console_mokobackup/src/Extension/MokoBackupConsole.php b/src/packages/plg_console_mokobackup/src/Extension/MokoBackupConsole.php new file mode 100644 index 0000000..5c2ee4f --- /dev/null +++ b/src/packages/plg_console_mokobackup/src/Extension/MokoBackupConsole.php @@ -0,0 +1,45 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Console\MokoBackup\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Event\Event; +use Joomla\Event\SubscriberInterface; +use Joomla\Plugin\Console\MokoBackup\Command\CleanupCommand; +use Joomla\Plugin\Console\MokoBackup\Command\ListCommand; +use Joomla\Plugin\Console\MokoBackup\Command\ProfilesCommand; +use Joomla\Plugin\Console\MokoBackup\Command\RestoreCommand; +use Joomla\Plugin\Console\MokoBackup\Command\RunCommand; + +final class MokoBackupConsole extends CMSPlugin implements SubscriberInterface +{ + protected $autoloadLanguage = true; + + public static function getSubscribedEvents(): array + { + return [ + \Joomla\Application\Event\ApplicationEvents::BEFORE_EXECUTE => 'registerCommands', + ]; + } + + public function registerCommands(Event $event): void + { + $app = $this->getApplication(); + + $app->addCommand(new RunCommand()); + $app->addCommand(new ListCommand()); + $app->addCommand(new ProfilesCommand()); + $app->addCommand(new RestoreCommand()); + $app->addCommand(new CleanupCommand()); + } +} diff --git a/src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.ini b/src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.ini new file mode 100644 index 0000000..5f23262 --- /dev/null +++ b/src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.ini @@ -0,0 +1,9 @@ +; MokoJoomBackup — Content Plugin language file (en-GB) +PLG_CONTENT_MOKOBACKUP="Content - MokoJoomBackup" +PLG_CONTENT_MOKOBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates." +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_INSTALL="Backup Before Install" +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_INSTALL_DESC="Run an automatic backup before a new extension is installed." +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_UPDATE="Backup Before Update" +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_UPDATE_DESC="Run an automatic backup before an extension is updated." +PLG_CONTENT_MOKOBACKUP_FIELD_PROFILE="Backup Profile" +PLG_CONTENT_MOKOBACKUP_FIELD_PROFILE_DESC="Which backup profile to use for automatic backups." diff --git a/src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.sys.ini b/src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.sys.ini new file mode 100644 index 0000000..3d79871 --- /dev/null +++ b/src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.sys.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Content Plugin system language file (en-GB) +PLG_CONTENT_MOKOBACKUP="Content - MokoJoomBackup" +PLG_CONTENT_MOKOBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates." diff --git a/src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.ini b/src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.ini new file mode 100644 index 0000000..1bac9a8 --- /dev/null +++ b/src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.ini @@ -0,0 +1,9 @@ +; MokoJoomBackup — Content Plugin language file (en-US) +PLG_CONTENT_MOKOBACKUP="Content - MokoJoomBackup" +PLG_CONTENT_MOKOBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates." +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_INSTALL="Backup Before Install" +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_INSTALL_DESC="Run an automatic backup before a new extension is installed." +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_UPDATE="Backup Before Update" +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_UPDATE_DESC="Run an automatic backup before an extension is updated." +PLG_CONTENT_MOKOBACKUP_FIELD_PROFILE="Backup Profile" +PLG_CONTENT_MOKOBACKUP_FIELD_PROFILE_DESC="Which backup profile to use for automatic backups." diff --git a/src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.sys.ini b/src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.sys.ini new file mode 100644 index 0000000..7a612b3 --- /dev/null +++ b/src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.sys.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Content Plugin system language file (en-US) +PLG_CONTENT_MOKOBACKUP="Content - MokoJoomBackup" +PLG_CONTENT_MOKOBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates." diff --git a/src/packages/plg_content_mokobackup/mokobackup.php b/src/packages/plg_content_mokobackup/mokobackup.php new file mode 100644 index 0000000..2dd15e4 --- /dev/null +++ b/src/packages/plg_content_mokobackup/mokobackup.php @@ -0,0 +1,11 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; diff --git a/src/packages/plg_content_mokobackup/mokobackup.xml b/src/packages/plg_content_mokobackup/mokobackup.xml new file mode 100644 index 0000000..a337b10 --- /dev/null +++ b/src/packages/plg_content_mokobackup/mokobackup.xml @@ -0,0 +1,71 @@ + + + + plg_content_mokobackup + 01.01.04-dev + 2026-06-04 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_CONTENT_MOKOBACKUP_DESCRIPTION + + Joomla\Plugin\Content\MokoBackup + + + mokobackup.php + services + src + + + + language/en-GB/plg_content_mokobackup.ini + language/en-GB/plg_content_mokobackup.sys.ini + + + + +
+ + + + + + + + + + + +
+
+
+
diff --git a/src/packages/plg_content_mokobackup/services/provider.php b/src/packages/plg_content_mokobackup/services/provider.php new file mode 100644 index 0000000..4635162 --- /dev/null +++ b/src/packages/plg_content_mokobackup/services/provider.php @@ -0,0 +1,37 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\Content\MokoBackup\Extension\MokoBackupContent; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MokoBackupContent( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('content', 'mokobackup') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/src/packages/plg_content_mokobackup/src/Extension/MokoBackupContent.php b/src/packages/plg_content_mokobackup/src/Extension/MokoBackupContent.php new file mode 100644 index 0000000..b27d119 --- /dev/null +++ b/src/packages/plg_content_mokobackup/src/Extension/MokoBackupContent.php @@ -0,0 +1,95 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Content\MokoBackup\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoBackup\Administrator\Engine\BackupEngine; +use Joomla\Event\Event; +use Joomla\Event\SubscriberInterface; + +final class MokoBackupContent extends CMSPlugin implements SubscriberInterface +{ + protected $autoloadLanguage = true; + + public static function getSubscribedEvents(): array + { + return [ + 'onExtensionBeforeInstall' => 'onExtensionBeforeInstall', + 'onExtensionBeforeUpdate' => 'onExtensionBeforeUpdate', + ]; + } + + /** + * Trigger a backup before a new extension is installed. + */ + public function onExtensionBeforeInstall(Event $event): void + { + if (!(int) $this->params->get('backup_before_install', 0)) { + return; + } + + $this->triggerAutoBackup('Pre-install backup'); + } + + /** + * Trigger a backup before an extension is updated. + */ + public function onExtensionBeforeUpdate(Event $event): void + { + if (!(int) $this->params->get('backup_before_update', 1)) { + return; + } + + $this->triggerAutoBackup('Pre-update backup'); + } + + /** + * Run a backup using the configured profile. + */ + private function triggerAutoBackup(string $description): void + { + $profileId = (int) $this->params->get('profile_id', 1); + + // Throttle: only one auto-backup per hour via session + $session = Factory::getSession(); + $lastRun = $session->get('mokobackup.content_last_autobackup', 0); + + if (time() - $lastRun < 3600) { + return; + } + + $session->set('mokobackup.content_last_autobackup', time()); + + $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokobackup/src/Engine/BackupEngine.php'; + + if (!file_exists($engineFile)) { + return; + } + + if (!class_exists(BackupEngine::class)) { + require_once $engineFile; + } + + try { + $engine = new BackupEngine(); + $engine->run($profileId, $description, 'backend'); + } catch (\Throwable $e) { + // Non-fatal — log and continue with the install/update + Factory::getApplication()->enqueueMessage( + 'MokoJoomBackup auto-backup failed: ' . $e->getMessage(), + 'warning' + ); + } + } +} diff --git a/src/pkg_mokobackup.xml b/src/pkg_mokobackup.xml index d02c244..b370006 100644 --- a/src/pkg_mokobackup.xml +++ b/src/pkg_mokobackup.xml @@ -25,6 +25,9 @@ plg_task_mokobackup.zip plg_quickicon_mokobackup.zip plg_webservices_mokobackup.zip + plg_console_mokobackup.zip + plg_content_mokobackup.zip + plg_actionlog_mokobackup.zip diff --git a/src/script.php b/src/script.php index ea6ed40..409a88c 100644 --- a/src/script.php +++ b/src/script.php @@ -108,6 +108,39 @@ class Pkg_MokoBackupInstallerScript $db->setQuery($query); $db->execute(); + // Enable the console plugin automatically + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('console')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokobackup')); + + $db->setQuery($query); + $db->execute(); + + // Enable the content plugin automatically + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('content')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokobackup')); + + $db->setQuery($query); + $db->execute(); + + // Enable the actionlog plugin automatically + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('actionlog')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokobackup')); + + $db->setQuery($query); + $db->execute(); + // Create default backup directory $backupDir = JPATH_ADMINISTRATOR . '/components/com_mokobackup/backups';