From 2af264171212d41b857bb0cc80e73d8bd8f55701 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 5 Jul 2026 18:31:06 -0500 Subject: [PATCH] fix(installer): self-heal admin menu; fix alias collision; short-constant label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fallout of the duplicate-component element bug: - Preflight now runs removeOrphanedComponent() BEFORE the component install recreates its menu, so the orphan's "Backup" menu (alias "backup") no longer collides ("The alias backup is already being used by the Backup menu item"). - ensureSubmenuItems() recreates the top-level "Backup" menu when it is missing (via new createTopMenu()) instead of bailing out — so a failed install that deleted the parent menu no longer leaves Backup gone from the admin menu (which also broke the cPanel module and MokoSuiteClient component detection). - The top-level menu label now uses the short constant COM_MOKOJOOMBACKUP_SHORT (manifest + enforced/normalized in the script). Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i --- CHANGELOG.md | 1 + .../com_mokosuitebackup/mokosuitebackup.xml | 2 +- source/script.php | 113 +++++++++++++++--- 3 files changed, 98 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26def769..c95165fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] ### Fixed +- Admin menu / duplicate-component fallout: (a) the orphaned `com_component-mokosuitebackup` is now removed in **preflight** too, so its "Backup" menu no longer collides on install ("The alias backup is already being used"); (b) `ensureSubmenuItems()` **recreates the top-level "Backup" menu** if it was deleted, instead of giving up — so Backup no longer disappears from the admin menu (and the cPanel module / MokoSuiteClient can find it again); (c) the component menu label now uses the short constant `COM_MOKOJOOMBACKUP_SHORT`. (#213 fallout) - Installer no longer aborts on MySQL 8 with "This command is not supported in the prepared statement protocol yet" (error 1295). The legacy remote-column purge migration (`02.56.01.sql`) used `PREPARE`/`EXECUTE`/`DEALLOCATE`, which Joomla's installer rejects; the drop now runs in the package installer script via an INFORMATION_SCHEMA-gated plain `ALTER` (portable across MariaDB and MySQL 8). (#213 update path) ## [02.57.00] --- 2026-07-05 diff --git a/source/packages/com_mokosuitebackup/mokosuitebackup.xml b/source/packages/com_mokosuitebackup/mokosuitebackup.xml index 6fb65dbe..04e1fc5a 100644 --- a/source/packages/com_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/com_mokosuitebackup/mokosuitebackup.xml @@ -47,7 +47,7 @@ - Backup + COM_MOKOJOOMBACKUP_SHORT backupDownloadKey(); } + /* Remove any orphaned mis-registered component BEFORE the component install + recreates its admin menu. The orphan (element com_component-mokosuitebackup) + owns a "Backup" menu with alias "backup"; if it survives, the real + component's menu creation collides ("The alias backup is already being used") + and the whole install aborts. */ + $this->removeOrphanedComponent(); + return true; } @@ -622,23 +629,7 @@ class Pkg_MokoSuiteBackupInstallerScript try { $db = Factory::getDbo(); - /* Find the parent menu item for our component */ - $query = $db->getQuery(true) - ->select([$db->quoteName('id'), $db->quoteName('menutype')]) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('client_id') . ' = 1') - ->where($db->quoteName('level') . ' = 1') - ->where($db->quoteName('link') . ' LIKE ' . $db->quote('index.php?option=com_mokosuitebackup%')) - ->setLimit(1); - $db->setQuery($query); - $parent = $db->loadObject(); - - if (!$parent) { - error_log('MokoSuiteBackup: ensureSubmenuItems() — parent menu item not found'); - return; - } - - /* Get the component extension_id */ + /* Component extension_id first — needed to (re)create the parent menu. */ $query = $db->getQuery(true) ->select($db->quoteName('extension_id')) ->from($db->quoteName('#__extensions')) @@ -653,6 +644,41 @@ class Pkg_MokoSuiteBackupInstallerScript return; } + /* Find the top-level "Backup" parent menu item for our component */ + $query = $db->getQuery(true) + ->select([$db->quoteName('id'), $db->quoteName('menutype')]) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('client_id') . ' = 1') + ->where($db->quoteName('level') . ' = 1') + ->where($db->quoteName('link') . ' LIKE ' . $db->quote('index.php?option=com_mokosuitebackup%')) + ->setLimit(1); + $db->setQuery($query); + $parent = $db->loadObject(); + + /* Self-heal: if the top-level menu was deleted (e.g. by a failed install + after the duplicate-component mess), recreate it — otherwise the whole + Backup menu stays gone and dependents (cPanel module, MokoSuiteClient) + can't locate the component. */ + if (!$parent) { + $parent = $this->createTopMenu($componentId); + + if (!$parent) { + error_log('MokoSuiteBackup: ensureSubmenuItems() — parent menu missing and could not be created'); + return; + } + } + + /* Keep the top-level menu label on the short constant and owned by the + real component. */ + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('title') . ' = ' . $db->quote('COM_MOKOJOOMBACKUP_SHORT')) + ->set($db->quoteName('component_id') . ' = ' . (int) $componentId) + ->where($db->quoteName('id') . ' = ' . (int) $parent->id) + ); + $db->execute(); + foreach ($submenus as $submenu) { /* Check if this submenu item already exists */ $query = $db->getQuery(true) @@ -718,6 +744,59 @@ class Pkg_MokoSuiteBackupInstallerScript } } + /** + * Create the top-level "Backup" admin menu item for the component when it is + * missing (deleted by a failed install / the duplicate-component fallout). + * Label uses the short constant. Returns a lightweight {id, menutype} object, + * or null on failure. + * + * @param int $componentId Extension id of com_mokosuitebackup + * + * @return object|null + */ + private function createTopMenu(int $componentId): ?object + { + try { + $table = Factory::getApplication() + ->bootComponent('com_menus') + ->getMVCFactory() + ->createTable('Menu', 'Administrator'); + + $table->menutype = 'main'; + $table->title = 'COM_MOKOJOOMBACKUP_SHORT'; + $table->alias = 'backup'; + $table->link = 'index.php?option=com_mokosuitebackup'; + $table->type = 'component'; + $table->published = 1; + $table->parent_id = 1; + $table->level = 1; + $table->component_id = $componentId; + $table->client_id = 1; + $table->img = 'class:archive'; + $table->params = '{}'; + $table->language = '*'; + $table->access = 1; + + $table->setLocation(1, 'last-child'); + + if (!$table->check() || !$table->store()) { + error_log('MokoSuiteBackup: createTopMenu() failed: ' . $table->getError()); + + return null; + } + + $obj = new \stdClass(); + $obj->id = (int) $table->id; + $obj->menutype = 'main'; + + return $obj; + } catch (\Throwable $e) { + error_log('MokoSuiteBackup: createTopMenu() exception: ' . $e->getMessage()); + + return null; + } + } + private function fixPackageClientId(): void { try {