From 978dfdfcf3aa66c1598eaa38fd339a5becf302cb Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 21 Jun 2026 09:54:15 -0500 Subject: [PATCH] fix(install): reinstall broken plugins from package zip in postflight When Joomla installs plugins with empty element, files go to the group root. Postflight now: 1. Deletes orphan rows (empty element or display-name-as-element) 2. Cleans stale files from group roots 3. Checks each expected plugin directory exists 4. If missing, extracts the plugin zip from the package source dir This guarantees all core plugins are correctly installed after every update, regardless of the MySQL DEFAULT '' issue. --- source/script.php | 101 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 93 insertions(+), 8 deletions(-) diff --git a/source/script.php b/source/script.php index 98c5b85c..d920a019 100644 --- a/source/script.php +++ b/source/script.php @@ -62,8 +62,12 @@ class Pkg_MokosuiteclientInstallerScript } } + /** @var \Joomla\CMS\Installer\InstallerAdapter|null */ + private $installerParent = null; + public function postflight($type, $parent) { + $this->installerParent = $parent; // Migrate MokoWaaS database tables to MokoSuiteClient naming $this->migrateWaasTables(); @@ -527,20 +531,29 @@ class Pkg_MokosuiteclientInstallerScript { $db = Factory::getDbo(); - // Delete orphaned extension rows with empty element — they'll be - // recreated correctly on the next package update + // 1. Delete orphaned extension rows with empty element $db->setQuery("DELETE FROM " . $db->quoteName('#__extensions') . " WHERE " . $db->quoteName('element') . " = ''" . " AND " . $db->quoteName('type') . " = 'plugin'"); $db->execute(); $deleted = $db->getAffectedRows(); + // Also delete rows where element is the display name (spaces) + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' LIKE ' . $db->quote('% %')) + ->where($db->quoteName('element') . ' LIKE ' . $db->quote('%mokosuiteclient%')) + ); + $db->execute(); + $deleted += $db->getAffectedRows(); + if ($deleted > 0) { - Log::add("Deleted {$deleted} orphaned plugin row(s) with empty element", Log::INFO, 'mokosuiteclient'); + Log::add("Deleted {$deleted} orphaned plugin row(s)", Log::INFO, 'mokosuiteclient'); } - // Clean up stale plugin files that leaked to plugin group roots + // 2. Clean up stale plugin files that leaked to group roots $groupDirs = [JPATH_PLUGINS . '/system', JPATH_PLUGINS . '/task', JPATH_PLUGINS . '/webservices']; foreach ($groupDirs as $groupDir) @@ -552,7 +565,6 @@ class Pkg_MokosuiteclientInstallerScript if (is_dir($path)) { $this->rmdirRecursive($path); - Log::add("Removed stale: {$path}", Log::INFO, 'mokosuiteclient'); } } @@ -562,20 +574,93 @@ class Pkg_MokosuiteclientInstallerScript @unlink($staleXml); } - // Remove dirs with spaces (Joomla uses display name as dir when element is empty) + // Remove dirs with spaces (Joomla uses display name as dir) foreach (glob($groupDir . '/*mokosuiteclient*', GLOB_ONLYDIR) ?: [] as $badDir) { if (strpos(basename($badDir), ' ') !== false) { $this->rmdirRecursive($badDir); - Log::add("Removed bad dir: " . basename($badDir), Log::INFO, 'mokosuiteclient'); } } } + + // 3. Reinstall plugins that are missing their directory + $this->reinstallBrokenPlugins(); + } + catch (\Throwable $e) + { + Log::add('Empty element cleanup error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient'); + } + } + + /** + * Reinstall plugins whose files are missing from disk. + * + * Uses the sub-extension zip files from the package source directory + * (still available during postflight) to reinstall any plugin that + * doesn't have its directory on disk. + */ + private function reinstallBrokenPlugins(): void + { + if (!$this->installerParent) + { + return; + } + + try + { + $installer = $this->installerParent->getParent(); + $sourceDir = $installer->getPath('source'); + + if (empty($sourceDir) || !is_dir($sourceDir . '/packages')) + { + return; + } + + // Plugins that should exist on disk + $expected = [ + 'system' => ['mokosuiteclient_offline', 'mokosuiteclient_firewall', 'mokosuiteclient_tenant', 'mokosuiteclient_devtools', 'mokosuiteclient_dbip'], + 'task' => ['mokosuiteclient_tickets'], + ]; + + foreach ($expected as $group => $elements) + { + foreach ($elements as $element) + { + $pluginDir = JPATH_PLUGINS . '/' . $group . '/' . $element; + + if (is_dir($pluginDir)) + { + continue; // Already installed correctly + } + + $zipName = 'plg_' . $group . '_' . $element . '.zip'; + $zipPath = $sourceDir . '/packages/' . $zipName; + + if (!is_file($zipPath)) + { + continue; + } + + // Extract the zip to the correct plugin directory + $zip = new \ZipArchive(); + + if ($zip->open($zipPath) !== true) + { + continue; + } + + @mkdir($pluginDir, 0755, true); + $zip->extractTo($pluginDir); + $zip->close(); + + Log::add("Reinstalled {$group}/{$element} from package zip", Log::INFO, 'mokosuiteclient'); + } + } } catch (\Throwable $e) { - Log::add('Empty element cleanup error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient'); + Log::add('Plugin reinstall error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient'); } }