From ede07c6675e42f85cea67c5a4e7452b9762516ba Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 23 May 2026 23:39:32 -0500 Subject: [PATCH] feat: dynamic plugin version + plugin protection (no lock) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Read plugin_version from manifest XML instead of hardcoding - Hide MokoWaaS from plugin/installer list for non-master users - Block non-master uninstall and disable attempts - No self-healing lock — master users can still disable if needed Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Extension/MokoWaaS.php | 166 +++++++++++++++++- 1 file changed, 164 insertions(+), 2 deletions(-) diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php index 9766490..25214b1 100644 --- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php +++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php @@ -65,6 +65,39 @@ class MokoWaaS extends CMSPlugin */ private const HEARTBEAT_KEY = 'moko-waas-hb-2026-x9k4m'; + /** + * Get the plugin version from the manifest XML. + * + * @return string Version string (e.g. '02.03.04') + * + * @since 02.03.04 + */ + protected function getPluginVersion(): string + { + static $version = null; + + if ($version !== null) + { + return $version; + } + + $manifestFile = JPATH_PLUGINS . '/system/mokowaas/mokowaas.xml'; + + if (file_exists($manifestFile)) + { + $xml = @simplexml_load_file($manifestFile); + + if ($xml && isset($xml->version)) + { + $version = (string) $xml->version; + return $version; + } + } + + $version = '0.0.0'; + return $version; + } + /** * Load the language file on instantiation. * @@ -869,6 +902,7 @@ class MokoWaaS extends CMSPlugin } $this->enforceAdminRestrictions(); + $this->protectPlugin(); } /** @@ -902,6 +936,134 @@ class MokoWaaS extends CMSPlugin } $this->injectFavicon($doc); + + // Hide MokoWaaS from plugin list for non-master users + if (!$this->isMasterUser()) + { + $this->hidePluginFromList($doc); + } + } + + /** + * Hide MokoWaaS plugin and package from the extensions list via JS. + * + * @param \Joomla\CMS\Document\HtmlDocument $doc Document object + * + * @return void + * + * @since 02.03.04 + */ + protected function hidePluginFromList($doc) + { + $option = $this->app->input->get('option', ''); + $view = $this->app->input->get('view', ''); + + if ($option !== 'com_plugins' && $option !== 'com_installer') + { + return; + } + + $doc->addScriptDeclaration(" + document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('tr').forEach(function(row) { + var text = row.textContent || ''; + if (text.indexOf('mokowaas') !== -1 || text.indexOf('MokoWaaS') !== -1) { + row.style.display = 'none'; + } + }); + }); + "); + } + + /** + * Protect the plugin from being disabled or uninstalled by non-master users. + * Does NOT self-heal (no lock) — master users can still disable if needed. + * + * @return void + * + * @since 02.03.04 + */ + protected function protectPlugin() + { + if ($this->isMasterUser()) + { + return; + } + + $option = $this->app->input->get('option', ''); + $task = $this->app->input->get('task', ''); + + // Block non-master from disabling or uninstalling MokoWaaS + if ($option === 'com_installer' && strpos($task, 'manage.remove') !== false) + { + $cid = $this->app->input->get('cid', [], 'array'); + + if ($this->isOurExtension($cid)) + { + $this->app->enqueueMessage('MokoWaaS cannot be uninstalled.', 'error'); + $this->app->redirect('index.php?option=com_installer&view=manage'); + } + } + } + + /** + * Check if any of the given extension IDs belong to MokoWaaS. + * + * @param array $ids Extension IDs to check + * + * @return bool + * + * @since 02.03.04 + */ + protected function isOurExtension(array $ids): bool + { + if (empty($ids)) + { + return false; + } + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('extension_id') . ' IN (' . implode(',', array_map('intval', $ids)) . ')') + ->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') + . ' OR ' . $db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas') . ')'); + + return (int) $db->setQuery($query)->loadResult() > 0; + } + + /** + * Prevent non-master users from disabling the plugin via save. + * + * @param string $context Extension context + * @param object $table Extension table row + * @param bool $isNew Whether this is a new record + * + * @return bool False to cancel save + * + * @since 02.03.04 + */ + public function onExtensionBeforeSave($context, $table, $isNew) + { + if ($context !== 'com_plugins.plugin') + { + return true; + } + + if ($table->element !== 'mokowaas' || $table->folder !== 'system') + { + return true; + } + + // Non-master users cannot disable the plugin + if (!$this->isMasterUser() && (int) $table->enabled === 0) + { + $this->app->enqueueMessage('MokoWaaS cannot be disabled.', 'error'); + $table->enabled = 1; + } + + return true; } /** @@ -1301,7 +1463,7 @@ class MokoWaaS extends CMSPlugin 'users' => $users, 'extensions' => $extensions, 'brand' => $this->params->get('brand_name', 'MokoWaaS'), - 'plugin_version' => '02.01.39', + 'plugin_version' => $this->getPluginVersion(), ]); } @@ -1455,7 +1617,7 @@ class MokoWaaS extends CMSPlugin return [ 'brand' => $this->params->get('brand_name', 'MokoWaaS'), - 'plugin_version' => '02.01.22', + 'plugin_version' => $this->getPluginVersion(), 'joomla_version' => JVERSION, 'php_version' => PHP_VERSION, 'server_name' => $config->get('sitename', ''),