From f6c4ef055533f32410336a4d96dc0a58d43715d4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 6 Jul 2026 11:14:57 -0500 Subject: [PATCH] fix: honest installer success for MokoSuiteCross package Gate the license / next-steps message in the package postflight behind a verification that every child extension declared in the manifest actually installed. If any component/module/plugin is missing from #__extensions, enqueue an error listing them instead of the success message. Fails open so a transient glitch never fakes a failure; housekeeping runs unconditionally. Claude-Session: https://claude.ai/code/session_01B9aZHSWbiiZykJD88pYx8R --- CHANGELOG.md | 3 ++ source/script.php | 104 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86a037a5..a82e7f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] +### Fixed +- **Honest installer success**: The package installer no longer shows the license / next-steps message when a bundled child extension failed to install. Postflight now verifies every component, module, and plugin declared in the package manifest actually landed in `#__extensions` and, if any are missing, enqueues an error listing them instead of the success message. The check fails open, so a transient query/IO glitch never turns a good install into a false failure; all housekeeping (download-key restore, plugin enabling, migration detection) still runs unconditionally. + ## [01.12.00] --- 2026-06-28 ## [01.12.00] --- 2026-06-28 diff --git a/source/script.php b/source/script.php index 7c4bfcbc..e1ef18df 100644 --- a/source/script.php +++ b/source/script.php @@ -40,8 +40,8 @@ class Pkg_MokoSuiteCrossInstallerScript public function postflight(string $type, InstallerAdapter $parent): void { + // Housekeeping — runs unconditionally regardless of child-install result. $this->restoreDownloadKey(); - $this->warnMissingLicenseKey(); $db = Factory::getDbo(); @@ -76,6 +76,108 @@ class Pkg_MokoSuiteCrossInstallerScript $this->detectPerfectPublisherPro($db); } + + // Be honest about success. Joomla's package installer only LOGS a failed child + // sub-install but still runs this postflight, so don't show the license / + // next-steps message if a bundled extension is actually missing. Fails open + // (see missingChildExtensions) so a query/IO glitch never fakes a failure. + $missing = $this->missingChildExtensions($parent); + + if (!empty($missing)) + { + Factory::getApplication()->enqueueMessage( + '

MokoSuiteCross did not install correctly.

' + . '

The following bundled extensions are missing: ' + . htmlspecialchars(implode(', ', $missing), ENT_QUOTES) . '

' + . '

Please uninstall MokoSuiteCross and reinstall the full package.

', + 'error' + ); + + return; + } + + // Warn if no license key is configured (success / next-steps path). + $this->warnMissingLicenseKey(); + } + + /** + * Verify every child extension declared in the package manifest actually landed + * in #__extensions. Returns readable labels of any that are missing. + * + * Matching per Joomla's #__extensions uniqueness: + * - type = the "type" attribute + * - element = the "id" attribute, EXCEPT for plugins the manifest often + * declares the full `plg__` id, so strip a leading + * `plg__` to get the real element; components/modules use id as-is + * - plugins ALSO match folder = the "group" attribute + * + * FAILS OPEN: any error (unreadable manifest, query failure) returns [] so a + * transient glitch never turns a good install into a false failure. + */ + private function missingChildExtensions(InstallerAdapter $parent): array + { + try + { + $manifest = $parent->getParent()->getManifest(); + + if (!$manifest || !isset($manifest->files) || !isset($manifest->files->file)) + { + return []; + } + + $db = Factory::getDbo(); + $missing = []; + + foreach ($manifest->files->file as $file) + { + $attrs = $file->attributes(); + $id = isset($attrs['id']) ? (string) $attrs['id'] : ''; + $exType = isset($attrs['type']) ? (string) $attrs['type'] : ''; + + if ($id === '' || $exType === '') + { + continue; + } + + $group = isset($attrs['group']) ? (string) $attrs['group'] : ''; + $element = $id; + + // Plugin element in #__extensions is the id minus any leading plg__. + if ($exType === 'plugin' && $group !== '') + { + $prefix = 'plg_' . $group . '_'; + + if (strpos($element, $prefix) === 0) + { + $element = substr($element, \strlen($prefix)); + } + } + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote($element)) + ->where($db->quoteName('type') . ' = ' . $db->quote($exType)); + + if ($exType === 'plugin' && $group !== '') + { + $query->where($db->quoteName('folder') . ' = ' . $db->quote($group)); + } + + if ((int) $db->setQuery($query)->loadResult() === 0) + { + $label = trim((string) $file); + $missing[] = $label !== '' ? preg_replace('/\.zip$/i', '', $label) : $id; + } + } + + return $missing; + } + catch (\Throwable $e) + { + // Fail open — never fake a failure on a glitch. + return []; + } } private function detectPerfectPublisherPro($db): void