Merge pull request 'fix: installer postflight is honest about a partial/failed install' (#239) from fix/installer-honesty into dev
This commit was merged in pull request #239.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoGitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 02.58.14
|
||||
# VERSION: 02.58.15
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
- The backend controller, Web Services API controller, and legacy `cli/mokosuitebackup.php` now run backups through the shared `BackupRunner` (gaining the normalized complete/warning/fail status) instead of instantiating `BackupEngine` directly.
|
||||
|
||||
### Fixed
|
||||
- Package installer is now **honest about success**: the postflight no longer prints "installed successfully" / next-steps when the install actually failed or only partially completed. Joomla logs a failed child extension but still runs the package postflight, so the postflight now (a) verifies every bundled child declared in the manifest actually registered in `#__extensions` (matched by element + type, and folder/group for plugins) and (b) verifies the component's schema tables — derived dynamically from the installed `install.mysql.sql` — actually exist. If anything is missing it enqueues an error listing what's missing and stops before the success message. Fail-open: any manifest/DB/IO glitch is treated as "nothing missing" so a transient error never turns a good install into a false failure. Unconditional housekeeping (e.g. download-key restore) still runs regardless.
|
||||
- Pre-update/uninstall backup no longer **white-screens** the update on large sites. The synchronous backup that runs inside the extension update/uninstall request now raises `max_execution_time`/`memory_limit` (and `ignore_user_abort`) like the web-cron path, so it can't exhaust the request's default limits mid-backup. (Core Joomla updates additionally get a full-screen backup screen — see below.)
|
||||
- Archive names for **CLI/console-triggered backups** no longer come out as `joomla.invalid_…`. The `[HOST]` placeholder took `$_SERVER['HTTP_HOST']` verbatim, but Joomla's console fills that with the reserved sentinel host `joomla.invalid`; the resolver now treats that (like empty/`localhost`) as unusable and falls back to the configured `live_site` host, then the system hostname. (Set the site's *live_site* to get the exact domain in CLI-built names.)
|
||||
- Standalone restore script generation no longer aborts backups with `str_replace() expects at least 3 arguments, 2 given`. `MokoRestore::generateStandaloneScript()` had a `str_replace()` call (the "Backup Archive" pre-check rewrite) that was missing its `$php` subject argument, so **every** standalone-mode backup fatally errored while "Generating standalone restore.php…" — the archive still finalized and uploaded, but no `restore.php` was ever produced (the true root cause behind #226). (#226)
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
|
||||
INGROUP: Template-Joomla.Documentation
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 02.58.14
|
||||
VERSION: 02.58.15
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
display label there.
|
||||
-->
|
||||
<name>MokoSuiteBackup</name>
|
||||
<version>02.58.14</version>
|
||||
<version>02.58.15</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/* 02.58.15 — no schema changes */
|
||||
@@ -8,7 +8,7 @@
|
||||
-->
|
||||
<extension type="module" client="administrator" method="upgrade">
|
||||
<name>Module - MokoSuiteBackup - cPanel</name>
|
||||
<version>02.58.14</version>
|
||||
<version>02.58.15</version>
|
||||
<creationDate>2026-06-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="actionlog" method="upgrade">
|
||||
<name>Action Log - MokoSuiteBackup</name>
|
||||
<version>02.58.14</version>
|
||||
<version>02.58.15</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="console" method="upgrade">
|
||||
<name>Console - MokoSuiteBackup</name>
|
||||
<version>02.58.14</version>
|
||||
<version>02.58.15</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="content" method="upgrade">
|
||||
<name>Content - MokoSuiteBackup</name>
|
||||
<version>02.58.14</version>
|
||||
<version>02.58.15</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="quickicon" method="upgrade">
|
||||
<name>Quick Icon - MokoSuiteBackup</name>
|
||||
<version>02.58.14</version>
|
||||
<version>02.58.15</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoSuiteBackup</name>
|
||||
<version>02.58.14</version>
|
||||
<version>02.58.15</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="task" method="upgrade">
|
||||
<name>Task - MokoSuiteBackup</name>
|
||||
<version>02.58.14</version>
|
||||
<version>02.58.15</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="webservices" method="upgrade">
|
||||
<name>Web Services - MokoSuiteBackup</name>
|
||||
<version>02.58.14</version>
|
||||
<version>02.58.15</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuiteBackup</name>
|
||||
<packagename>mokosuitebackup</packagename>
|
||||
<version>02.58.14</version>
|
||||
<version>02.58.15</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -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