From f341c086a227ae88c32b809d9ba2de4489c77944 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 6 Jul 2026 10:45:59 -0500 Subject: [PATCH] refactor(installer): replace hardcoded extension list with dynamic manifest verification Read package manifest via $adapter->getParent()->getManifest() to discover children dynamically instead of maintaining a hardcoded EXPECTED_EXTENSIONS array. Fail-open pattern wraps verification in try/catch. Only the success path (plugin enables, license warning) is gated; unconditional housekeeping (DLID restore) runs regardless. Claude-Session: https://claude.ai/code/session_01CwLGvFJPjoPTp9BEnSjtJf --- source/script.php | 153 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 115 insertions(+), 38 deletions(-) diff --git a/source/script.php b/source/script.php index 7a76b9a..555192b 100644 --- a/source/script.php +++ b/source/script.php @@ -1,9 +1,10 @@ 'component', 'element' => 'com_mokosuitefield'], - ['type' => 'plugin', 'element' => 'mokosuitefield', 'folder' => 'system'], - ['type' => 'plugin', 'element' => 'mokosuitefield', 'folder' => 'webservices'], - ]; + private string $savedDlid = ''; - private const PACKAGE_ELEMENT = 'pkg_mokosuitefield'; - private const PACKAGE_LABEL = 'MokoSuite Field'; + public function preflight(string $type, InstallerAdapter $adapter): bool + { + if ($type === 'update') + { + $this->savedDlid = $this->getExistingDlid(); + } + + return true; + } public function postflight(string $type, InstallerAdapter $adapter): void { + if ($this->savedDlid) + { + $this->restoreDlid($this->savedDlid); + } + if ($type === 'install') { - $missing = $this->verifyInstallation(); + $missing = $this->verifyChildren($adapter); if (!empty($missing)) { Factory::getApplication()->enqueueMessage( - '' . self::PACKAGE_LABEL . ' — Incomplete Installation
' + 'MokoSuite Field — Incomplete Installation
' . 'The following extensions failed to install: ' . implode(', ', $missing) . '.
Please check file permissions and server logs, then reinstall the package.', 'error' ); - $this->setPackageEnabled(false); return; } + + $this->enablePlugin('system', 'mokosuitefield'); + $this->enablePlugin('webservices', 'mokosuitefield'); } $this->warnMissingLicenseKey(); } - private function verifyInstallation(): array + private function verifyChildren(InstallerAdapter $adapter): array { - $missing = []; - try { - $db = Factory::getDbo(); + $manifest = $adapter->getParent()->getManifest(); + $files = $manifest->files->file ?? []; - foreach (self::EXPECTED_EXTENSIONS as $ext) + if (!$files) { + return []; + } + + $db = Factory::getContainer()->get(DatabaseInterface::class); + $missing = []; + + foreach ($files as $file) + { + $type = (string) ($file['type'] ?? ''); + $id = (string) ($file['id'] ?? ''); + $group = (string) ($file['group'] ?? ''); + + if ($type === '' || $id === '') + { + continue; + } + + $element = ($type === 'plugin' && $group !== '') + ? preg_replace('/^plg_' . preg_quote($group, '/') . '_/', '', $id) + : $id; + $query = $db->getQuery(true) ->select('COUNT(*)') ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote($ext['type'])) - ->where($db->quoteName('element') . ' = ' . $db->quote($ext['element'])); + ->where($db->quoteName('element') . ' = ' . $db->quote($element)) + ->where($db->quoteName('type') . ' = ' . $db->quote($type)); - if (isset($ext['folder'])) + if ($type === 'plugin' && $group !== '') { - $query->where($db->quoteName('folder') . ' = ' . $db->quote($ext['folder'])); + $query->where($db->quoteName('folder') . ' = ' . $db->quote($group)); } $db->setQuery($query); if ((int) $db->loadResult() === 0) { - $label = isset($ext['folder']) - ? $ext['type'] . ':' . $ext['folder'] . '/' . $ext['element'] - : $ext['type'] . ':' . $ext['element']; - $missing[] = $label; + $missing[] = htmlspecialchars($type . ': ' . $id, ENT_QUOTES, 'UTF-8'); } } + + return $missing; + } + catch (\Throwable $e) + { + return []; + } + } + + private function getExistingDlid(): string + { + try + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('extra_query')) + ->from($db->quoteName('#__update_sites')) + ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuiteField%') + . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuiteField%') . ')') + ->setLimit(1) + ); + + $extraQuery = $db->loadResult() ?? ''; + + if (preg_match('/dlid=([a-zA-Z0-9]+)/', $extraQuery, $matches)) + { + return $matches[1]; + } } catch (\Throwable $e) { - Log::add(self::PACKAGE_LABEL . ' install verification failed: ' . $e->getMessage(), Log::ERROR, 'mokosuite'); - $missing[] = 'verification query failed'; + Log::add('Field install: failed to read DLID — ' . $e->getMessage(), Log::WARNING, 'mokosuite.field'); } - return $missing; + return ''; } - private function setPackageEnabled(bool $enabled): void + private function restoreDlid(string $dlid): void { try { - $db = Factory::getDbo(); + $db = Factory::getContainer()->get(DatabaseInterface::class); $db->setQuery( $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = ' . ($enabled ? 1 : 0)) - ->where($db->quoteName('type') . ' = ' . $db->quote('package')) - ->where($db->quoteName('element') . ' = ' . $db->quote(self::PACKAGE_ELEMENT)) + ->update($db->quoteName('#__update_sites')) + ->set($db->quoteName('extra_query') . ' = ' . $db->quote('dlid=' . $dlid)) + ->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuiteField%') + . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuiteField%') . ')') ); $db->execute(); } catch (\Throwable $e) { - Log::add(self::PACKAGE_LABEL . ' failed to toggle package state: ' . $e->getMessage(), Log::ERROR, 'mokosuite'); + Log::add('Field install: failed to restore DLID — ' . $e->getMessage(), Log::WARNING, 'mokosuite.field'); + } + } + + private function enablePlugin(string $group, string $element): void + { + try + { + $db = Factory::getContainer()->get(DatabaseInterface::class); + $db->setQuery( + $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($group)) + ->where($db->quoteName('element') . ' = ' . $db->quote($element)) + ); + $db->execute(); + } + catch (\Throwable $e) + { + Log::add('Field install: failed to enable ' . $group . '/' . $element . ' — ' . $e->getMessage(), Log::WARNING, 'mokosuite.field'); } } @@ -112,7 +190,7 @@ class Pkg_MokoSuiteFieldInstallerScript { try { - $db = Factory::getDbo(); + $db = Factory::getContainer()->get(DatabaseInterface::class); $app = Factory::getApplication(); $query = $db->getQuery(true) @@ -154,7 +232,6 @@ class Pkg_MokoSuiteFieldInstallerScript } catch (\Throwable $e) { - // Silent — avoid breaking install if update_sites query fails } } }