fix: installer postflight honest about partial/failed install
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Secret Scan (pull_request) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 37s
Generic: Project CI / Lint & Validate (pull_request) Successful in 33s
Universal: PR Check / Validate PR (pull_request) Failing after 28s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 32s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Secret Scan (pull_request) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 37s
Generic: Project CI / Lint & Validate (pull_request) Successful in 33s
Universal: PR Check / Validate PR (pull_request) Failing after 28s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 32s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Joomla logs a failed child extension but still runs the package postflight, so the 'installed successfully' message could appear on a broken install. In postflight, before the success/next-steps messages: - verify every bundled child declared in the package manifest actually registered in #__extensions (match element+type, and folder=group for plugins), and - verify the component's schema tables (parsed dynamically from the installed install.mysql.sql) exist in the DB. If anything is missing, enqueue an error listing it and RETURN before the success message. Fail-open (try/catch -> nothing missing) so a transient manifest/DB/IO glitch can't fail a good install. Unconditional housekeeping (download-key restore, menu fixes) still runs regardless. Names escaped with htmlspecialchars; table/child lists derived dynamically, never hard-coded. Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
This commit is contained in:
@@ -188,6 +188,35 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
PREPARE/EXECUTE migration that MySQL 8 rejected in Joomla's installer). */
|
||||
$this->dropLegacyRemoteColumns();
|
||||
|
||||
/* Honesty gate: Joomla logs a failed child extension but still runs the
|
||||
package postflight, so verify every bundled child actually registered
|
||||
before claiming success. If any are missing, say so and do NOT print the
|
||||
success / next-steps messages. (All housekeeping above runs regardless.)
|
||||
Fail-open — a manifest/DB glitch returns no missing children. */
|
||||
$problems = $this->findMissingPackageChildren($parent);
|
||||
|
||||
foreach ($this->findMissingComponentTables() as $table) {
|
||||
$problems[] = 'table ' . $table;
|
||||
}
|
||||
|
||||
if (!empty($problems)) {
|
||||
$safe = array_map(
|
||||
static fn($m) => htmlspecialchars($m, ENT_QUOTES, 'UTF-8'),
|
||||
$problems
|
||||
);
|
||||
$detail = count($safe) > 3
|
||||
? count($safe) . ' bundled extensions/tables are missing'
|
||||
: 'missing: ' . implode(', ', $safe);
|
||||
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<strong>MokoSuiteBackup did not install correctly</strong> — ' . $detail
|
||||
. '. Review the installation log and re-install.',
|
||||
'error'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/* Install completion notice (install and update) */
|
||||
$this->installSuccessful();
|
||||
|
||||
@@ -208,6 +237,133 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify every bundled child extension declared in the package manifest
|
||||
* actually registered in #__extensions. Joomla logs a failed child but does
|
||||
* not fail the package, so this answers "did the package really install?"
|
||||
* honestly. Returns human-readable labels for any missing children (empty =
|
||||
* all present). Matching mirrors extension uniqueness: element + type, and
|
||||
* for plugins also folder (group), since plugins are only unique that way.
|
||||
*
|
||||
* Fail-open: any manifest/DB error returns [] so a transient glitch can never
|
||||
* turn a good install into a false failure.
|
||||
*
|
||||
* @param InstallerAdapter $parent Package installer adapter
|
||||
*
|
||||
* @return string[] Labels of missing children (empty when everything installed)
|
||||
*/
|
||||
private function findMissingPackageChildren(InstallerAdapter $parent): array
|
||||
{
|
||||
try {
|
||||
$manifest = $parent->getParent()->getManifest();
|
||||
|
||||
if (!$manifest instanceof \SimpleXMLElement
|
||||
|| !isset($manifest->files) || !isset($manifest->files->file)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
$missing = [];
|
||||
|
||||
foreach ($manifest->files->file as $file) {
|
||||
$attrs = $file->attributes();
|
||||
$element = isset($attrs['id']) ? (string) $attrs['id'] : '';
|
||||
$extType = isset($attrs['type']) ? (string) $attrs['type'] : '';
|
||||
|
||||
if ($element === '' || $extType === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote($element))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote($extType));
|
||||
|
||||
$group = '';
|
||||
|
||||
/* Plugins are only unique by element + folder (group). */
|
||||
if ($extType === 'plugin' && isset($attrs['group'])) {
|
||||
$group = (string) $attrs['group'];
|
||||
|
||||
if ($group !== '') {
|
||||
$query->where($db->quoteName('folder') . ' = ' . $db->quote($group));
|
||||
}
|
||||
}
|
||||
|
||||
$db->setQuery($query);
|
||||
|
||||
if ((int) $db->loadResult() === 0) {
|
||||
$label = $extType . ' "' . $element . '"';
|
||||
|
||||
if ($group !== '') {
|
||||
$label .= ' (' . $group . ')';
|
||||
}
|
||||
|
||||
$missing[] = $label;
|
||||
}
|
||||
}
|
||||
|
||||
return $missing;
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the component's schema actually installed. The component has no
|
||||
* installer script of its own, so this package postflight is the only place
|
||||
* to catch a component whose extension row exists but whose CREATE TABLE
|
||||
* statements failed (e.g. an installer-rejected migration). Reads the
|
||||
* *installed* install SQL, extracts every declared table, and confirms each
|
||||
* exists. Table names are derived dynamically — nothing is hard-coded.
|
||||
*
|
||||
* Fail-open: any IO/DB/parse error returns [] so a glitch can't fail a good
|
||||
* install.
|
||||
*
|
||||
* @return string[] Missing table names (empty when all present)
|
||||
*/
|
||||
private function findMissingComponentTables(): array
|
||||
{
|
||||
try {
|
||||
$sqlFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/sql/install.mysql.sql';
|
||||
|
||||
if (!is_file($sqlFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sql = file_get_contents($sqlFile);
|
||||
|
||||
if ($sql === false || $sql === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!preg_match_all('/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(#__[A-Za-z0-9_]+)/i', $sql, $m)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
$prefix = $db->getPrefix();
|
||||
|
||||
/* Existing tables, compared case-insensitively. */
|
||||
$existing = array_flip(array_map('strtolower', $db->getTableList()));
|
||||
|
||||
$missing = [];
|
||||
|
||||
foreach (array_unique($m[1]) as $declared) {
|
||||
$real = str_replace('#__', $prefix, $declared);
|
||||
|
||||
if (!isset($existing[strtolower($real)])) {
|
||||
$missing[] = $real;
|
||||
}
|
||||
}
|
||||
|
||||
return $missing;
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the orphaned component registration created when the component
|
||||
* <name> briefly used the "Type - Name" display convention. Joomla derives a
|
||||
|
||||
Reference in New Issue
Block a user