Compare commits

..

2 Commits

Author SHA1 Message Date
mokogitea-actions[bot] e474c7cf27 chore(version): pre-release bump to 02.56.09-dev [skip ci] 2026-07-05 22:07:10 +00:00
jmiller 58a876f6d1 docs(changelog): record component <name> fix + backfill 02.56.07 security/integrity fixes
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 48s
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-05 17:03:09 -05:00
28 changed files with 58 additions and 282 deletions
+2 -2
View File
@@ -59,8 +59,8 @@ Joomla **package** with four sub-extensions:
- **Attribution**: `Authored-by: Moko Consulting` - **Attribution**: `Authored-by: Moko Consulting`
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`) - **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
- **Minification**: handled at build time (CI) - **Minification**: handled at build time (CI)
- **Wiki**: documentation lives in the MokoGitea wiki, not `docs/` files - **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki) - **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home)
## Coding Standards ## Coding Standards
+3 -6
View File
@@ -380,12 +380,9 @@ jobs:
import sys import sys
version, date = sys.argv[1], sys.argv[2] version, date = sys.argv[1], sys.argv[2]
content = open('CHANGELOG.md').read() content = open('CHANGELOG.md').read()
marker = f'## [{version}]' old = '## [Unreleased]'
# Idempotent: only promote when this version header isn't already present, new = f'## [Unreleased]\n\n## [{version}] --- {date}'
# otherwise re-runs (or same-version builds) create empty duplicate headers. content = content.replace(old, new, 1)
if marker not in content:
new = f'## [Unreleased]\n\n{marker} --- {date}'
content = content.replace('## [Unreleased]', new, 1)
open('CHANGELOG.md', 'w').write(content) open('CHANGELOG.md', 'w').write(content)
" "$VERSION" "$DATE" " "$VERSION" "$DATE"
git add CHANGELOG.md git add CHANGELOG.md
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Automation # INGROUP: mokocli.Automation
# VERSION: 02.57.03 # VERSION: 02.56.09
# BRIEF: Auto-create feature branch when an issue is opened # BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch" name: "Universal: Issue Branch"
+15 -22
View File
@@ -1,24 +1,8 @@
# Changelog # Changelog
## [Unreleased] ## [Unreleased]
## [02.57.00] --- 2026-07-05
### Added
- `BackupRunner` service — a single synchronous entry point for backups that unifies final status and notification gating across the pre-update, web-cron, pre-install, scheduled-task and CLI triggers (delegates to `BackupEngine`; no double-notify). Returns a normalized result including the real complete/warning/fail status. Dashboard "Backup Now" (AJAX) unchanged; installer progress popup deferred to #196. (#214)
### Fixed ### Fixed
- Release automation: the changelog-promote step is now idempotent — it only inserts a `## [version]` header when that version isn't already present, preventing the empty, duplicated version headers that repeated/same-version builds were producing. (workflow) - Component manifest `<name>` reverted to the element-safe `MokoSuiteBackup`. The "Type - Name" display convention made Joomla derive the component element as `com_component-mokosuitebackup`, registering a duplicate component and leaving the real `com_mokosuitebackup` orphaned on the old version — the admin component appeared "stuck" and served the old UI (broken remote-destinations tab, legacy fields). (#213)
## [02.56.11] --- 2026-07-05
### Fixed
- Package `postflight` now removes the orphaned `com_component-mokosuitebackup` registration (stray extension record, admin menu items, schema rows, and `administrator/` folder) left behind by the old "Type - Name" component `<name>` — self-healing on update so affected sites reconcile to the real `com_mokosuitebackup`. (#213 follow-up)
## [02.56.08] --- 2026-07-05
### Fixed
- Component manifest `<name>` reverted to the element-safe `MokoSuiteBackup`. The "Type - Name" convention derived element `com_component-mokosuitebackup`, registering a duplicate component and leaving the real `com_mokosuitebackup` orphaned on the old version. (#213)
## [02.56.07] --- 2026-07-05 ## [02.56.07] --- 2026-07-05
@@ -28,9 +12,18 @@
- SFTP uploader uses `StrictHostKeyChecking=accept-new` instead of `no`. (#182) - SFTP uploader uses `StrictHostKeyChecking=accept-new` instead of `no`. (#182)
### Fixed ### Fixed
- `DatabaseDumper` writes a real newline after `DROP TABLE` (was a literal `\n`). (#179) - `DatabaseDumper` writes a real newline after `DROP TABLE` (was a literal `\n`), fixing streamed dumps. (#179)
- Profile-picker forms order by `id` instead of the dropped `ordering` column. (#180) - Profile-picker forms order by `id` instead of the dropped `ordering` column (query failed on upgraded sites). (#180)
- `uninstall.mysql.sql` now drops the snapshots table. (#181) - `uninstall.mysql.sql` now drops the snapshots table. (#181)
- CLI snapshot delete uses the `data_file` column (was non-existent `file_path`). (#184) - CLI snapshot delete uses the real `data_file` column (was `file_path`, so files were never removed). (#184)
- Remove the duplicate pre-update backup (content plugin no longer subscribes to `onExtensionBeforeUpdate`). (#186) - Remove the duplicate pre-update backup — the content plugin no longer subscribes to `onExtensionBeforeUpdate` (owned by the system plugin). (#186)
- `DatabaseImporter` collects non-fatal statement errors (`getErrors()`/`hasErrors()`). (#188) - `DatabaseImporter` collects non-fatal statement errors (`getErrors()`/`hasErrors()`) so restores are no longer silent. (#188)
## [02.56.05] --- 2026-07-05
## [02.56.05] --- 2026-07-05
## [02.56.00] --- 2026-07-04
## [02.56.00] --- 2026-07-04
+1 -1
View File
@@ -4,7 +4,7 @@ Thank you for your interest in contributing to MokoSuiteBackup.
## Getting Started ## Getting Started
1. Fork the repository on MokoGitea 1. Fork the repository on Gitea
2. Create a feature branch from `dev` (`feature/your-feature`) 2. Create a feature branch from `dev` (`feature/your-feature`)
3. Make your changes following the coding standards below 3. Make your changes following the coding standards below
4. Submit a pull request targeting `dev` 4. Submit a pull request targeting `dev`
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation INGROUP: Template-Joomla.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
PATH: /SECURITY.md PATH: /SECURITY.md
VERSION: 02.57.03 VERSION: 02.56.09
BRIEF: Security vulnerability reporting and handling policy BRIEF: Security vulnerability reporting and handling policy
--> -->
@@ -17,7 +17,7 @@
display label there. display label there.
--> -->
<name>MokoSuiteBackup</name> <name>MokoSuiteBackup</name>
<version>02.57.03</version> <version>02.56.09</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1 +0,0 @@
/* 02.56.08 — no schema changes */
@@ -0,0 +1 @@
/* 02.56.09 — no schema changes */
@@ -1 +0,0 @@
/* 02.56.11 — no schema changes */
@@ -1 +0,0 @@
/* 02.57.00 — no schema changes */
@@ -1 +0,0 @@
/* 02.57.03 — no schema changes */
@@ -1,109 +0,0 @@
<?php
/**
* @package MokoSuiteBackup
* @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* Single synchronous entry point for running a backup.
*
* Every non-interactive trigger — the web-cron handler, the pre-install /
* pre-update / pre-uninstall hooks, the scheduled task plugin and the CLI
* command — runs through here, so the final status, notification gating and
* retention are applied identically regardless of what started the backup.
* (The dashboard's AJAX "Backup Now" is the interactive equivalent, driven by
* SteppedBackupEngine.)
*
* BackupEngine::run() already sends notifications (gated by the profile's
* notify_on_success / notify_on_failure) and applies retention, so this class
* DELEGATES rather than reimplementing that logic — there is exactly one
* notification layer. Its value is being the documented seam where origin-aware
* behaviour can live and normalising the result, including the real
* complete / warning / fail status that the engine records but does not return.
*/
namespace Joomla\Component\MokoSuiteBackup\Administrator\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
final class BackupRunner
{
/**
* Run a full backup synchronously and return a normalised result.
*
* @param int $profileId Profile to back up.
* @param string $description Human-readable description stored on the record.
* @param string $origin Trigger origin (webcron|preaction|scheduled|cli|backend…).
*
* @return array{success:bool,status:string,message:string,record_id:int,warnings:array}
*/
public function run(int $profileId, string $description, string $origin = 'backend'): array
{
// TODO(#196): an interactive progress popup for installer-triggered runs
// (onExtensionBeforeUpdate) is a separate follow-up — the installer
// request cannot drive the AJAX/stepped progress modal.
try {
if (!class_exists(BackupEngine::class)) {
require_once __DIR__ . '/../Engine/BackupEngine.php';
}
$result = (new BackupEngine())->run($profileId, $description, $origin);
} catch (\Throwable $e) {
error_log('MokoSuiteBackup: backup run failed (' . $origin . '): ' . $e->getMessage());
return [
'success' => false,
'status' => 'fail',
'message' => 'Backup failed: ' . $e->getMessage(),
'record_id' => 0,
'warnings' => [],
];
}
$success = (bool) ($result['success'] ?? false);
$recordId = (int) ($result['record_id'] ?? 0);
return [
'success' => $success,
'status' => $this->resolveStatus($success, $recordId),
'message' => (string) ($result['message'] ?? ''),
'record_id' => $recordId,
'warnings' => $result['warnings'] ?? [],
];
}
/**
* Read the status BackupEngine wrote to the record (complete/warning/fail)
* but does not return, so callers can distinguish a warning (archive created,
* remote upload failed) from a clean success.
*/
private function resolveStatus(bool $success, int $recordId): string
{
if (!$success) {
return 'fail';
}
if ($recordId <= 0) {
return 'complete';
}
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('status'))
->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('id') . ' = ' . (int) $recordId);
$db->setQuery($query);
$status = (string) $db->loadResult();
return $status !== '' ? $status : 'complete';
} catch (\Throwable $e) {
return 'complete';
}
}
}
@@ -8,7 +8,7 @@
--> -->
<extension type="module" client="administrator" method="upgrade"> <extension type="module" client="administrator" method="upgrade">
<name>Module - MokoSuiteBackup - cPanel</name> <name>Module - MokoSuiteBackup - cPanel</name>
<version>02.57.03</version> <version>02.56.09</version>
<creationDate>2026-06-23</creationDate> <creationDate>2026-06-23</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
--> -->
<extension type="plugin" group="actionlog" method="upgrade"> <extension type="plugin" group="actionlog" method="upgrade">
<name>Action Log - MokoSuiteBackup</name> <name>Action Log - MokoSuiteBackup</name>
<version>02.57.03</version> <version>02.56.09</version>
<creationDate>2026-06-04</creationDate> <creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
--> -->
<extension type="plugin" group="console" method="upgrade"> <extension type="plugin" group="console" method="upgrade">
<name>Console - MokoSuiteBackup</name> <name>Console - MokoSuiteBackup</name>
<version>02.57.03</version> <version>02.56.09</version>
<creationDate>2026-06-04</creationDate> <creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -13,7 +13,7 @@ namespace Joomla\Plugin\Console\MokoSuiteBackup\Command;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\Component\MokoSuiteBackup\Administrator\Service\BackupRunner; use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
use Joomla\Console\Command\AbstractCommand; use Joomla\Console\Command\AbstractCommand;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@@ -40,19 +40,20 @@ class RunCommand extends AbstractCommand
$io->title('MokoSuiteBackup — Run Backup'); $io->title('MokoSuiteBackup — Run Backup');
$io->text('Profile ID: ' . $profileId); $io->text('Profile ID: ' . $profileId);
$runnerFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Service/BackupRunner.php'; $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Engine/BackupEngine.php';
if (!file_exists($runnerFile)) { if (!file_exists($engineFile)) {
$io->error('MokoSuiteBackup component not installed.'); $io->error('MokoSuiteBackup component not installed.');
return 1; return 1;
} }
if (!class_exists(BackupRunner::class)) { if (!class_exists(BackupEngine::class)) {
require_once $runnerFile; require_once $engineFile;
} }
$result = (new BackupRunner())->run($profileId, $desc ?: 'CLI backup', 'cli'); $engine = new BackupEngine();
$result = $engine->run($profileId, $desc ?: 'CLI backup', 'cli');
if ($result['success']) { if ($result['success']) {
$io->success($result['message']); $io->success($result['message']);
@@ -7,7 +7,7 @@
--> -->
<extension type="plugin" group="content" method="upgrade"> <extension type="plugin" group="content" method="upgrade">
<name>Content - MokoSuiteBackup</name> <name>Content - MokoSuiteBackup</name>
<version>02.57.03</version> <version>02.56.09</version>
<creationDate>2026-06-04</creationDate> <creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -14,7 +14,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoSuiteBackup\Administrator\Service\BackupRunner; use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
use Joomla\Event\Event; use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface; use Joomla\Event\SubscriberInterface;
@@ -61,18 +61,19 @@ final class MokoSuiteBackupContent extends CMSPlugin implements SubscriberInterf
$session->set('mokosuitebackup.content_last_autobackup', time()); $session->set('mokosuitebackup.content_last_autobackup', time());
$runnerFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Service/BackupRunner.php'; $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Engine/BackupEngine.php';
if (!file_exists($runnerFile)) { if (!file_exists($engineFile)) {
return; return;
} }
if (!class_exists(BackupRunner::class)) { if (!class_exists(BackupEngine::class)) {
require_once $runnerFile; require_once $engineFile;
} }
try { try {
(new BackupRunner())->run($profileId, $description, 'backend'); $engine = new BackupEngine();
$engine->run($profileId, $description, 'backend');
} catch (\Throwable $e) { } catch (\Throwable $e) {
// Non-fatal — log and continue with the install/update // Non-fatal — log and continue with the install/update
Factory::getApplication()->enqueueMessage( Factory::getApplication()->enqueueMessage(
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="quickicon" method="upgrade"> <extension type="plugin" group="quickicon" method="upgrade">
<name>Quick Icon - MokoSuiteBackup</name> <name>Quick Icon - MokoSuiteBackup</name>
<version>02.57.03</version> <version>02.56.09</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
--> -->
<extension type="plugin" group="system" method="upgrade"> <extension type="plugin" group="system" method="upgrade">
<name>System - MokoSuiteBackup</name> <name>System - MokoSuiteBackup</name>
<version>02.57.03</version> <version>02.56.09</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -15,7 +15,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoSuiteBackup\Administrator\Service\BackupRunner; use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
use Joomla\Event\Event; use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface; use Joomla\Event\SubscriberInterface;
@@ -91,7 +91,8 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
@ini_set('memory_limit', '512M'); @ini_set('memory_limit', '512M');
try { try {
$result = (new BackupRunner())->run($profileId, 'Web cron backup', 'webcron'); $engine = new BackupEngine();
$result = $engine->run($profileId, 'Web cron backup', 'webcron');
$this->sendJsonResponse( $this->sendJsonResponse(
$result['success'], $result['success'],
@@ -389,7 +390,8 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
$profileId = (int) $params->get('default_profile', 1); $profileId = (int) $params->get('default_profile', 1);
try { try {
$result = (new BackupRunner())->run($profileId, $description, 'preaction'); $engine = new BackupEngine();
$result = $engine->run($profileId, $description, 'preaction');
if (!$result['success']) { if (!$result['success']) {
Factory::getApplication()->enqueueMessage( Factory::getApplication()->enqueueMessage(
@@ -7,7 +7,7 @@
--> -->
<extension type="plugin" group="task" method="upgrade"> <extension type="plugin" group="task" method="upgrade">
<name>Task - MokoSuiteBackup</name> <name>Task - MokoSuiteBackup</name>
<version>02.57.03</version> <version>02.56.09</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -72,7 +72,7 @@ final class MokoSuiteBackupTask extends CMSPlugin implements SubscriberInterface
$profileId = (int) ($params->profile_id ?? 1); $profileId = (int) ($params->profile_id ?? 1);
// Load the backup engine from the component // Load the backup engine from the component
$engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Service/BackupRunner.php'; $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Engine/BackupEngine.php';
if (!file_exists($engineFile)) { if (!file_exists($engineFile)) {
$this->logTask('MokoSuiteBackup component not installed — cannot run backup.'); $this->logTask('MokoSuiteBackup component not installed — cannot run backup.');
@@ -81,11 +81,11 @@ final class MokoSuiteBackupTask extends CMSPlugin implements SubscriberInterface
} }
// The autoloader should handle this via namespace, but ensure class is available // The autoloader should handle this via namespace, but ensure class is available
if (!class_exists('\\Joomla\\Component\\MokoSuiteBackup\\Administrator\\Service\\BackupRunner')) { if (!class_exists('\\Joomla\\Component\\MokoSuiteBackup\\Administrator\\Engine\\BackupEngine')) {
require_once $engineFile; require_once $engineFile;
} }
$engine = new \Joomla\Component\MokoSuiteBackup\Administrator\Service\BackupRunner(); $engine = new \Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine();
$result = $engine->run($profileId, 'Scheduled task backup', 'scheduled'); $result = $engine->run($profileId, 'Scheduled task backup', 'scheduled');
if ($result['success']) { if ($result['success']) {
@@ -7,7 +7,7 @@
--> -->
<extension type="plugin" group="webservices" method="upgrade"> <extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoSuiteBackup</name> <name>Web Services - MokoSuiteBackup</name>
<version>02.57.03</version> <version>02.56.09</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
+1 -1
View File
@@ -8,7 +8,7 @@
<extension type="package" method="upgrade"> <extension type="package" method="upgrade">
<name>Package - MokoSuiteBackup</name> <name>Package - MokoSuiteBackup</name>
<packagename>mokosuitebackup</packagename> <packagename>mokosuitebackup</packagename>
<version>02.57.03</version> <version>02.56.09</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
-106
View File
@@ -172,11 +172,6 @@ class Pkg_MokoSuiteBackupInstallerScript
/* Migrate profiles with old default backup_dir values to [DEFAULT_DIR] placeholder */ /* Migrate profiles with old default backup_dir values to [DEFAULT_DIR] placeholder */
$this->migrateDefaultBackupDir(); $this->migrateDefaultBackupDir();
/* Remove the orphaned mis-registered component (element
com_component-mokosuitebackup) left behind when the component <name>
briefly used the "Type - Name" convention. Self-healing on every update. */
$this->removeOrphanedComponent();
/* Install completion notice (install and update) */ /* Install completion notice (install and update) */
$this->installSuccessful(); $this->installSuccessful();
@@ -197,107 +192,6 @@ class Pkg_MokoSuiteBackupInstallerScript
} }
} }
/**
* Remove the orphaned component registration created when the component
* <name> briefly used the "Type - Name" display convention. Joomla derives a
* component's element from its <name>, so "Component - MokoSuiteBackup"
* produced element com_component-mokosuitebackup — its own extension record,
* admin menu items, schema rows and administrator/ folder — orphaning the real
* com_mokosuitebackup on the old version. This reconciles affected sites on
* update. Idempotent and non-fatal: does nothing when no orphan exists.
*
* @return void
*/
private function removeOrphanedComponent(): void
{
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName(['extension_id', 'element']))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
->where($db->quoteName('client_id') . ' = 1')
->where($db->quoteName('element') . ' LIKE ' . $db->quote('%component%mokosuitebackup%'))
->where($db->quoteName('element') . ' <> ' . $db->quote('com_mokosuitebackup'));
$db->setQuery($query);
$orphans = $db->loadObjectList();
if (empty($orphans)) {
return;
}
foreach ($orphans as $orphan) {
$id = (int) $orphan->extension_id;
/* Admin menu items owned by the orphan component */
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__menu'))
->where($db->quoteName('component_id') . ' = ' . $id)
)->execute();
/* Schema-version rows */
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__schemas'))
->where($db->quoteName('extension_id') . ' = ' . $id)
)->execute();
/* Update-site mapping (leave #__update_sites; harmless if orphaned) */
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__update_sites_extensions'))
->where($db->quoteName('extension_id') . ' = ' . $id)
)->execute();
/* The bogus extension record itself */
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__extensions'))
->where($db->quoteName('extension_id') . ' = ' . $id)
)->execute();
/* The stray admin component folder on disk */
$this->deleteFolderRecursive(JPATH_ADMINISTRATOR . '/components/' . $orphan->element);
Factory::getApplication()->enqueueMessage(
'MokoSuiteBackup: removed orphaned component registration "'
. htmlspecialchars($orphan->element, ENT_QUOTES, 'UTF-8') . '".',
'info'
);
}
} catch (\Throwable $e) {
/* Never block the install/update over best-effort cleanup */
Log::add('MokoSuiteBackup orphan-component cleanup failed: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/**
* Recursively delete a directory (version-independent, no Folder dependency).
*
* @param string $dir Absolute path
*
* @return void
*/
private function deleteFolderRecursive(string $dir): void
{
if ($dir === '' || !is_dir($dir)) {
return;
}
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item) {
$item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
}
@rmdir($dir);
}
/** /**
* Generate a cryptographically random webcron secret word on fresh install. * Generate a cryptographically random webcron secret word on fresh install.
*/ */