diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 91ca2ac..44e04f5 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 01.07.28 +# VERSION: 01.07.30 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index b2eefe8..86e2e5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # MokoSuite NPO Nonprofit management for MokoSuite. Layer 2 add-on (requires CRM). +## [Unreleased] +### Fixed +- Installer no longer shows the "license / next-steps" success message when a bundled child extension failed to install. The package `postflight` now verifies every child declared in the manifest actually landed in `#__extensions` and shows an honest error instead. Fails open so a transient glitch never fakes a failure. + ## Features - Donor management with giving levels - Donation tracking with fund allocation diff --git a/source/packages/com_mokosuitenpo/mokosuitenpo.xml b/source/packages/com_mokosuitenpo/mokosuitenpo.xml index ac0e63e..410b490 100644 --- a/source/packages/com_mokosuitenpo/mokosuitenpo.xml +++ b/source/packages/com_mokosuitenpo/mokosuitenpo.xml @@ -7,7 +7,7 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 01.07.28 + 01.07.30 8.3 MokoSuite NPO component Moko\Component\MokoSuiteNpo diff --git a/source/pkg_mokosuitenpo.xml b/source/pkg_mokosuitenpo.xml index 3e6f378..aaa73af 100644 --- a/source/pkg_mokosuitenpo.xml +++ b/source/pkg_mokosuitenpo.xml @@ -2,7 +2,7 @@ Package - MokoSuite NPO mokosuitenpo - 01.07.28 + 01.07.30 2026-06-11 Moko Consulting hello@mokoconsulting.tech diff --git a/source/script.php b/source/script.php index 045022c..7b403b6 100644 --- a/source/script.php +++ b/source/script.php @@ -19,9 +19,112 @@ class Pkg_MokoSuiteNPOInstallerScript { public function postflight(string $type, InstallerAdapter $adapter): void { + // 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 glitch never fakes a failure. + $missing = $this->missingChildExtensions($adapter); + + if (!empty($missing)) + { + Factory::getApplication()->enqueueMessage( + '

MokoSuiteNPO did not install correctly.

' + . '

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

' + . '

Please uninstall MokoSuiteNPO and reinstall the full package.

', + 'error' + ); + + return; + } + $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 where the + * manifest declares the full plg__ id, so a + * leading plg__ is stripped to get the real element + * - 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. + * + * @param InstallerAdapter $adapter The package installer adapter. + * + * @return string[] Labels of missing child extensions. + */ + private function missingChildExtensions(InstallerAdapter $adapter): array + { + try + { + $manifest = $adapter->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 warnMissingLicenseKey(): void { try