Compare commits

...

33 Commits

Author SHA1 Message Date
jmiller e3bd4a36ff chore: sync pr-check.yml from Template-Generic [skip ci] 2026-07-06 03:50:41 +00:00
jmiller da9265e329 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-07-06 03:50:41 +00:00
jmiller b9c0ddf2b9 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-07-06 03:50:40 +00:00
jmiller 41e8bffd26 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-07-06 03:50:39 +00:00
gitea-actions[bot] 8be1e332c6 chore(release): build 02.58.00 [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
2026-07-06 00:32:07 +00:00
jmiller ba91f74edc Merge pull request 'release: promote dev → main (installer + admin-menu + UI fixes)' (#224) from dev into main
Sync Workflows to Repos / sync (push) Has been skipped
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 9s
Universal: PR Check / Branch Policy (pull_request) Successful in 9s
Universal: PR Check / Secret Scan (pull_request) Successful in 29s
Universal: PR Check / Validate PR (pull_request) Failing after 54s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m31s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
2026-07-06 00:30:29 +00:00
mokogitea-actions[bot] 387bfbdeda chore(version): pre-release bump to 02.57.13-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 1m53s
2026-07-06 00:17:00 +00:00
jmiller f7d8d82a4e Merge pull request 'refactor(ui): modal padding via Bootstrap container, drop msbackup class' (#223) from fix/modal-container-padding into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 27s
2026-07-06 00:16:33 +00:00
mokogitea-actions[bot] 9e5a942800 chore(version): pre-release bump to 02.57.12-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
2026-07-06 00:16:10 +00:00
jmiller dad3ce2db3 refactor(ui): modal padding via Bootstrap container, drop msbackup class
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s
Per feedback, wrap the destination modal content in a Bootstrap .container-fluid
and give that a 1rem frame in the WAM stylesheet, instead of a custom
.msbackup class on .modal-content. Inner header/body/footer drop their own
horizontal padding so the frame isn't doubled.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-05 19:15:49 -05:00
mokogitea-actions[bot] 8325afb03a chore(version): pre-release bump to 02.57.11-dev [skip ci] 2026-07-06 00:06:23 +00:00
jmiller 662918db38 Merge pull request 'feat(ui): SFTP key file upload + msbackup modal padding frame' (#222) from fix/modal-msbackup-class into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 27s
2026-07-06 00:04:47 +00:00
jmiller 1ab99daaba feat(ui): SFTP key file upload; msbackup modal padding frame
Generic: Project CI / Lint & Validate (pull_request) Successful in 18s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
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 / Secret Scan (pull_request) Successful in 8s
Universal: PR Check / Validate PR (pull_request) Failing after 14s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 5s
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
- Add/Edit Destination modal: SFTP key can now be UPLOADED as a file (read into
  the key textarea client-side via FileReader) instead of copy/paste; pasting
  still works.
- Give .modal-content a "msbackup" class with a 1rem padding frame — Bootstrap's
  .modal-content has no padding of its own, which made the modal feel cramped.
  Styling lives in the WAM stylesheet (media/.../admin.css), scoped to .msbackup.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-05 19:04:05 -05:00
mokogitea-actions[bot] c84dcf2864 chore(version): pre-release bump to 02.57.10-dev [skip ci] 2026-07-05 23:59:02 +00:00
jmiller e1f5afb301 Merge pull request 'fix(plugin): pre-update backup notifies on success, not only failure' (#221) from fix/preupdate-notification into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 42s
2026-07-05 23:58:21 +00:00
mokogitea-actions[bot] dfd4e77ade chore(version): pre-release bump to 02.57.09-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 3s
2026-07-05 23:58:13 +00:00
jmiller 1a1c81550c fix(plugin): pre-update backup notifies on success too, not only failure
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 3s
Generic: Project CI / Lint & Validate (pull_request) Successful in 17s
Universal: PR Check / Secret Scan (pull_request) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 44s
Universal: PR Check / Validate PR (pull_request) Failing after 1m5s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m13s
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
runPreActionBackup() only enqueued a message when the backup failed, so a
successful pre-update backup produced no admin notification at all. Now it
surfaces every outcome via the BackupRunner status: complete -> success,
warning (archive made but remote upload failed) -> warning, fail -> error.

Refs #192

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-05 18:57:42 -05:00
mokogitea-actions[bot] 9571b3759a chore(version): pre-release bump to 02.57.08-dev [skip ci] 2026-07-05 23:49:26 +00:00
jmiller 115ccfb069 Merge pull request 'fix(ui): WAM-loaded CSS, per-destination keep-local, tidy destination modal' (#220) from fix/remote-modal-cleanup into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 28s
2026-07-05 23:49:04 +00:00
mokogitea-actions[bot] 2d538d3372 chore(version): pre-release bump to 02.57.07-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
2026-07-05 23:48:51 +00:00
jmiller 97e8d0cbe9 fix(ui): WAM-loaded CSS, per-destination keep-local, tidy destination modal
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 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 25s
Generic: Project CI / Lint & Validate (pull_request) Successful in 12s
Universal: PR Check / Validate PR (pull_request) Failing after 47s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 54s
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
- Move component admin CSS to the Joomla Web Asset Manager: new
  media/com_mokosuitebackup/css/admin.css + media/joomla.asset.json, a <media>
  entry in the manifest, and useStyle('com_mokosuitebackup.admin') in the
  profile edit view. Removes the inline <style> block (no more inline CSS).
- Remove the profile-level "Keep local copy" field/"Remote" fieldset from
  forms/profile.xml — it is now per remote destination (modal remoteKeepLocal).
- Clean up the Add/Edit Destination modal padding/margins (in admin.css).

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-05 18:48:33 -05:00
mokogitea-actions[bot] 8d5b33a74d chore(version): pre-release bump to 02.57.06-dev [skip ci] 2026-07-05 23:32:04 +00:00
jmiller 77bfaff8cd Merge pull request 'fix(installer): self-heal admin menu + alias collision + short-constant label' (#219) from fix/admin-menu-selfheal into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 26s
2026-07-05 23:31:41 +00:00
mokogitea-actions[bot] 1c420b2277 chore(version): pre-release bump to 02.57.05-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
2026-07-05 23:31:24 +00:00
jmiller 2af2641712 fix(installer): self-heal admin menu; fix alias collision; short-constant label
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
Fallout of the duplicate-component element bug:

- Preflight now runs removeOrphanedComponent() BEFORE the component install
  recreates its menu, so the orphan's "Backup" menu (alias "backup") no longer
  collides ("The alias backup is already being used by the Backup menu item").
- ensureSubmenuItems() recreates the top-level "Backup" menu when it is missing
  (via new createTopMenu()) instead of bailing out — so a failed install that
  deleted the parent menu no longer leaves Backup gone from the admin menu (which
  also broke the cPanel module and MokoSuiteClient component detection).
- The top-level menu label now uses the short constant COM_MOKOJOOMBACKUP_SHORT
  (manifest <menu> + enforced/normalized in the script).

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-05 18:31:06 -05:00
mokogitea-actions[bot] a3c7183374 chore(version): pre-release bump to 02.57.04-dev [skip ci] 2026-07-05 23:15:31 +00:00
jmiller 4f91d1e432 Merge pull request 'chore(sync): cascade main -> dev' (#212) from main into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 39s
2026-07-05 23:03:57 +00:00
jmiller 604412f82f Merge chore/normalize-docs: normalize doc/config refs
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 3s
Generic: Project CI / Lint & Validate (pull_request) Successful in 15s
Universal: PR Check / Secret Scan (pull_request) Successful in 11s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 15s
Universal: PR Check / Validate PR (pull_request) Failing after 50s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
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
2026-07-05 22:50:52 +00:00
jmiller 94ff15c128 chore(docs): normalize refs — MokoStandards->org wiki, moko-platform->mokocli, residual MokoGitea/MokoSuite
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 34s
Non-workflow docs/config only (CLAUDE.md, README, branch-protection, ISSUE_TEMPLATE/config.yml,
ci-issue-reporter). Workflow files, manifest schema, and Makefile install paths left for the
forge-rebrand gate. Authored-by: Moko Consulting
2026-07-05 22:50:50 +00:00
mokogitea-actions[bot] a92f423767 chore(version): pre-release bump to 02.57.02-dev [skip ci] 2026-07-05 22:44:13 +00:00
jmiller ba2cda1385 Merge pull request 'fix(installer): drop legacy remote columns in PHP, not PREPARE-based SQL' (#218) from fix/sql-prepared-statement-protocol into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 21s
2026-07-05 22:43:53 +00:00
mokogitea-actions[bot] 72aeea6955 chore(version): pre-release bump to 02.57.01-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 3s
2026-07-05 22:41:55 +00:00
jmiller 8a85370e32 fix(installer): drop legacy remote columns in PHP, not PREPARE-based SQL
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 34s
02.56.01.sql used PREPARE/EXECUTE/DEALLOCATE to conditionally drop the 26 legacy
remote_storage/ftp_*/sftp_*/gdrive_*/s3_* columns portably. Joomla's installer
rejects those statements on MySQL 8 (error 1295: "This command is not supported
in the prepared statement protocol yet"), aborting the whole component install
when updating from an old schema.

Neutralize 02.56.01.sql (now a no-op comment) and move the purge into
Pkg_MokoSuiteBackupInstallerScript::dropLegacyRemoteColumns(): gate on
INFORMATION_SCHEMA, then issue one plain ALTER dropping only the columns that
still exist — portable on MariaDB and MySQL 8, idempotent, non-fatal.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-05 17:41:26 -05:00
39 changed files with 325 additions and 93 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 Gitea wiki, not `docs/` files - **Wiki**: documentation lives in the MokoGitea wiki, not `docs/` files
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home) - **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki)
## Coding Standards ## Coding Standards
+4 -7
View File
@@ -380,13 +380,10 @@ 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 if ('## [' + version + ']') in content else content.replace(old, new, 1)
if marker not in content: open('CHANGELOG.md', 'w').write(content)
new = f'## [Unreleased]\n\n{marker} --- {date}'
content = content.replace('## [Unreleased]', new, 1)
open('CHANGELOG.md', 'w').write(content)
" "$VERSION" "$DATE" " "$VERSION" "$DATE"
git add CHANGELOG.md git add CHANGELOG.md
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
+5 -4
View File
@@ -131,10 +131,11 @@ jobs:
test: test:
name: Tests name: Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: lint # Independent job (no `needs: lint`): the Gitea Actions scheduler does not
# Run only when lint succeeded; always() forces evaluation so a skipped # offer the dependent 2nd job of a needs-chain to runners, so it stalls in
# lint (e.g. template repos) skips this job cleanly instead of hanging. # "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos
if: ${{ always() && needs.lint.result == 'success' }} # directly (same condition lint uses) instead of gating on lint's result.
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps: steps:
- name: Checkout - name: Checkout
+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.00 # VERSION: 01.00.00
# 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"
+1 -1
View File
@@ -210,7 +210,7 @@ jobs:
- name: Check for merge conflict markers - name: Check for merge conflict markers
run: | run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogitea' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found in source files" echo "::error::Merge conflict markers found in source files"
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
+21
View File
@@ -2,6 +2,27 @@
## [Unreleased] ## [Unreleased]
## [02.58.00] --- 2026-07-06
### Added
- SFTP destinations: **upload an SSH private key file** in the Add/Edit Destination modal instead of pasting it (reads the file into the key field client-side; pasting still works).
### Changed
- Add/Edit Destination modal: wrap the content in a Bootstrap `.container-fluid` and give it a 1rem padding frame via the WAM stylesheet (Bootstrap's `.modal-content` has no padding of its own).
### Fixed
- Pre-update backup now shows an admin notification on **every** outcome (success / warning / failure) — previously a *successful* pre-update backup fired nothing, so it looked like the notification wasn't working. (#192)
### Changed
- Component admin CSS now loads via the Joomla **Web Asset Manager** (`media/com_mokosuitebackup/` + `joomla.asset.json`) instead of an inline `<style>` block.
- "Keep local copy" is configured **per remote destination** (in the Add/Edit Destination modal); the redundant profile-level field and its "Remote" tab are removed.
- Add/Edit Destination modal: cleaned up the cramped padding/margins.
### Fixed
- Admin menu / duplicate-component fallout: (a) the orphaned `com_component-mokosuitebackup` is now removed in **preflight** too, so its "Backup" menu no longer collides on install ("The alias backup is already being used"); (b) `ensureSubmenuItems()` **recreates the top-level "Backup" menu** if it was deleted, instead of giving up — so Backup no longer disappears from the admin menu (and the cPanel module / MokoSuiteClient can find it again); (c) the component menu label now uses the short constant `COM_MOKOJOOMBACKUP_SHORT`. (#213 fallout)
- Installer no longer aborts on MySQL 8 with "This command is not supported in the prepared statement protocol yet" (error 1295). The legacy remote-column purge migration (`02.56.01.sql`) used `PREPARE`/`EXECUTE`/`DEALLOCATE`, which Joomla's installer rejects; the drop now runs in the package installer script via an INFORMATION_SCHEMA-gated plain `ALTER` (portable across MariaDB and MySQL 8). (#213 update path)
## [02.57.00] --- 2026-07-05 ## [02.57.00] --- 2026-07-05
### Added ### Added
+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 Gitea 1. Fork the repository on MokoGitea
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.00 VERSION: 02.58.00
BRIEF: Security vulnerability reporting and handling policy BRIEF: Security vulnerability reporting and handling policy
--> -->
@@ -205,19 +205,9 @@
/> />
</fieldset> </fieldset>
<fieldset name="remote" label="COM_MOKOJOOMBACKUP_FIELDSET_REMOTE"> <!-- The "Keep local copy" setting is now configured per remote destination
<field (in the Add/Edit Destination modal), so the profile-level field and its
name="remote_keep_local" "Remote" tab have been removed. -->
type="radio"
label="COM_MOKOJOOMBACKUP_FIELD_KEEP_LOCAL"
description="COM_MOKOJOOMBACKUP_FIELD_KEEP_LOCAL_DESC"
default="1"
class="btn-group"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<fieldset name="retention" label="COM_MOKOJOOMBACKUP_FIELDSET_RETENTION"> <fieldset name="retention" label="COM_MOKOJOOMBACKUP_FIELDSET_RETENTION">
<field <field
@@ -0,0 +1,33 @@
/**
* MokoSuiteBackup — admin styles (loaded via the Joomla Web Asset Manager).
*/
/* Add/Edit Destination modal.
* The modal content is wrapped in a Bootstrap .container-fluid because
* Bootstrap's .modal-content has no padding of its own. Give that container a
* single, comfortable 1rem frame and drop the inner sections' horizontal padding
* so it isn't doubled. */
#remoteModal .modal-content > .container-fluid {
padding: 1rem;
}
#remoteModal .modal-header,
#remoteModal .modal-body,
#remoteModal .modal-footer {
padding-left: 0;
padding-right: 0;
}
#remoteModal .modal-body .row + .row,
#remoteModal .remote-type-fields {
margin-top: 0.25rem;
}
#remoteModal hr {
margin: 1rem 0;
opacity: 0.12;
}
#remoteModal .form-label {
margin-bottom: 0.35rem;
}
@@ -0,0 +1,14 @@
{
"$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
"name": "com_mokosuitebackup",
"version": "1.0.0",
"description": "MokoSuiteBackup web assets",
"license": "GPL-3.0-or-later",
"assets": [
{
"name": "com_mokosuitebackup.admin",
"type": "style",
"uri": "com_mokosuitebackup/css/admin.css"
}
]
}
@@ -17,7 +17,7 @@
display label there. display label there.
--> -->
<name>MokoSuiteBackup</name> <name>MokoSuiteBackup</name>
<version>02.57.00</version> <version>02.58.00</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>
@@ -47,7 +47,7 @@
</update> </update>
<administration> <administration>
<menu img="class:archive">Backup</menu> <menu img="class:archive">COM_MOKOJOOMBACKUP_SHORT</menu>
<submenu> <submenu>
<menu link="option=com_mokosuitebackup&amp;view=dashboard" <menu link="option=com_mokosuitebackup&amp;view=dashboard"
img="class:home" img="class:home"
@@ -78,6 +78,11 @@
</languages> </languages>
</administration> </administration>
<media destination="com_mokosuitebackup" folder="media">
<folder>css</folder>
<filename>joomla.asset.json</filename>
</media>
<api> <api>
<files folder="api"> <files folder="api">
<folder>src</folder> <folder>src</folder>
@@ -1,32 +1,13 @@
-- Purge legacy single-remote storage columns from installs where they are still present. -- Legacy single-remote storage columns are now purged by the package installer
-- script (Pkg_MokoSuiteBackupInstallerScript::dropLegacyRemoteColumns()), NOT here.
-- --
-- Background: 02.52.25.sql originally used `DROP COLUMN IF EXISTS`, which is a -- The 26 legacy remote_storage/ftp_*/sftp_*/gdrive_*/s3_* columns must be removed
-- MariaDB-only extension and errors on Oracle MySQL 8.x. On MySQL 8 installs the -- portably across MariaDB and MySQL 8. `DROP COLUMN IF EXISTS` is MariaDB-only
-- migration failed but Joomla still recorded the schema as applied, leaving all 26 -- (errors on MySQL 8); plain `DROP COLUMN` errors when a column is absent. The
-- legacy remote_storage/ftp_*/sftp_*/gdrive_*/s3_* columns stranded on the profiles -- previous version gated the ALTER on INFORMATION_SCHEMA and ran it via
-- table. This migration removes them portably. -- PREPARE/EXECUTE/DEALLOCATE — but those statements are rejected by Joomla's
-- installer SQL path (MySQL error 1295: "This command is not supported in the
-- prepared statement protocol yet"), which ABORTED the whole component install.
-- --
-- It must be safe on BOTH engines AND on installs where the columns are already gone -- The drop is now done in PHP (INFORMATION_SCHEMA gate + a plain ALTER for only the
-- (MariaDB, or anyone who ran the corrected 02.52.25). Plain `DROP COLUMN` errors when -- columns that still exist), so this migration is intentionally a no-op.
-- a column is absent, and `DROP COLUMN IF EXISTS` errors on MySQL 8 — so neither works
-- unconditionally. We gate the drop on INFORMATION_SCHEMA and build the ALTER via a
-- prepared statement, which runs on MySQL 8 and MariaDB alike. All 26 columns were
-- created and dropped together, so the presence of `remote_storage` gates the whole set.
-- When the columns are absent this is a no-op (`DO 0`).
SET @moko_has_legacy_remote := (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '#__mokosuitebackup_profiles'
AND COLUMN_NAME = 'remote_storage'
);
SET @moko_drop_legacy_remote := IF(@moko_has_legacy_remote > 0,
'ALTER TABLE `#__mokosuitebackup_profiles` DROP COLUMN `remote_storage`, DROP COLUMN `ftp_host`, DROP COLUMN `ftp_port`, DROP COLUMN `ftp_username`, DROP COLUMN `ftp_password`, DROP COLUMN `ftp_path`, DROP COLUMN `ftp_passive`, DROP COLUMN `ftp_ssl`, DROP COLUMN `sftp_host`, DROP COLUMN `sftp_port`, DROP COLUMN `sftp_username`, DROP COLUMN `sftp_auth_type`, DROP COLUMN `sftp_password`, DROP COLUMN `sftp_key_data`, DROP COLUMN `sftp_passphrase`, DROP COLUMN `sftp_path`, DROP COLUMN `gdrive_client_id`, DROP COLUMN `gdrive_client_secret`, DROP COLUMN `gdrive_refresh_token`, DROP COLUMN `gdrive_folder_id`, DROP COLUMN `s3_endpoint`, DROP COLUMN `s3_region`, DROP COLUMN `s3_access_key`, DROP COLUMN `s3_secret_key`, DROP COLUMN `s3_bucket`, DROP COLUMN `s3_path`',
'DO 0'
);
PREPARE moko_stmt FROM @moko_drop_legacy_remote;
EXECUTE moko_stmt;
DEALLOCATE PREPARE moko_stmt;
@@ -0,0 +1 @@
/* 02.57.01 — no schema changes */
@@ -0,0 +1 @@
/* 02.57.02 — no schema changes */
@@ -0,0 +1 @@
/* 02.57.04 — no schema changes */
@@ -0,0 +1 @@
/* 02.57.05 — no schema changes */
@@ -0,0 +1 @@
/* 02.57.06 — no schema changes */
@@ -0,0 +1 @@
/* 02.57.07 — no schema changes */
@@ -0,0 +1 @@
/* 02.57.08 — no schema changes */
@@ -0,0 +1 @@
/* 02.57.09 — no schema changes */
@@ -0,0 +1 @@
/* 02.57.10 — no schema changes */
@@ -0,0 +1 @@
/* 02.57.11 — no schema changes */
@@ -0,0 +1 @@
/* 02.57.12 — no schema changes */
@@ -0,0 +1 @@
/* 02.57.13 — no schema changes */
@@ -0,0 +1 @@
/* 02.58.00 — no schema changes */
@@ -19,6 +19,9 @@ HTMLHelper::_('behavior.formvalidator');
HTMLHelper::_('behavior.keepalive'); HTMLHelper::_('behavior.keepalive');
HTMLHelper::_('bootstrap.modal'); HTMLHelper::_('bootstrap.modal');
/* Component CSS via the Web Asset Manager (declared in media/joomla.asset.json) */
$this->getDocument()->getWebAssetManager()->useStyle('com_mokosuitebackup.admin');
$profileId = (int) $this->item->id; $profileId = (int) $this->item->id;
$token = Session::getFormToken(); $token = Session::getFormToken();
?> ?>
@@ -121,6 +124,7 @@ $token = Session::getFormToken();
<div class="modal fade" id="remoteModal" tabindex="-1" aria-labelledby="remoteModalLabel" aria-hidden="true"> <div class="modal fade" id="remoteModal" tabindex="-1" aria-labelledby="remoteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="container-fluid">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="remoteModalLabel"><?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_ADD'); ?></h5> <h5 class="modal-title" id="remoteModalLabel"><?php echo Text::_('COM_MOKOJOOMBACKUP_REMOTE_ADD'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?php echo Text::_('JCLOSE'); ?>"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="<?php echo Text::_('JCLOSE'); ?>"></button>
@@ -190,7 +194,9 @@ $token = Session::getFormToken();
</div> </div>
<div class="mb-3" id="remoteSftpKeyWrap"> <div class="mb-3" id="remoteSftpKeyWrap">
<label for="remoteCfg_sftp_key_data" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY'); ?></label> <label for="remoteCfg_sftp_key_data" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY'); ?></label>
<textarea class="form-control" id="remoteCfg_sftp_key_data" rows="4" placeholder="Paste SSH private key or leave as-is to keep existing"></textarea> <input type="file" class="form-control mb-2" id="remoteSftpKeyFile" accept=".pem,.key,.ppk,.openssh,.rsa,.ed25519,text/plain">
<textarea class="form-control" id="remoteCfg_sftp_key_data" rows="4" placeholder="Upload a key file above, or paste the SSH private key here (leave as-is to keep existing)"></textarea>
<div class="form-text" id="remoteSftpKeyFileMsg"></div>
</div> </div>
<div class="mb-3" id="remoteSftpPassphraseWrap"> <div class="mb-3" id="remoteSftpPassphraseWrap">
<label for="remoteCfg_sftp_passphrase" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE'); ?></label> <label for="remoteCfg_sftp_passphrase" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSPHRASE'); ?></label>
@@ -257,6 +263,7 @@ $token = Session::getFormToken();
<?php echo Text::_('JAPPLY'); ?> <?php echo Text::_('JAPPLY'); ?>
</button> </button>
</div> </div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -452,6 +459,29 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
// ---- Upload SSH key file (fills the key textarea; no paste required) ----
const keyFileInput = document.getElementById('remoteSftpKeyFile');
if (keyFileInput) {
keyFileInput.addEventListener('change', function() {
const msg = document.getElementById('remoteSftpKeyFileMsg');
const file = this.files && this.files[0];
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = function(ev) {
document.getElementById('remoteCfg_sftp_key_data').value = ev.target.result;
if (msg) { msg.textContent = 'Loaded "' + file.name + '".'; msg.className = 'form-text text-success'; }
};
reader.onerror = function() {
if (msg) { msg.textContent = 'Could not read that file.'; msg.className = 'form-text text-danger'; }
};
reader.readAsText(file);
});
}
// ---- Add button ---- // ---- Add button ----
document.getElementById('btnAddRemote').addEventListener('click', function() { document.getElementById('btnAddRemote').addEventListener('click', function() {
openEdit(0); openEdit(0);
@@ -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.00</version> <version>02.58.00</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.00</version> <version>02.58.00</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.00</version> <version>02.58.00</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="content" method="upgrade"> <extension type="plugin" group="content" method="upgrade">
<name>Content - MokoSuiteBackup</name> <name>Content - MokoSuiteBackup</name>
<version>02.57.00</version> <version>02.58.00</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>
@@ -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.00</version> <version>02.58.00</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.00</version> <version>02.58.00</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>
@@ -390,12 +390,28 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
try { try {
$result = (new BackupRunner())->run($profileId, $description, 'preaction'); $result = (new BackupRunner())->run($profileId, $description, 'preaction');
$app = Factory::getApplication();
$status = $result['status'] ?? (!empty($result['success']) ? 'complete' : 'fail');
if (!$result['success']) { /* Always surface the outcome so the pre-update backup is visible in the
Factory::getApplication()->enqueueMessage( admin — previously only failures produced a message, so a successful
'MokoSuiteBackup: ' . $description . ' failed — ' . $result['message'], pre-update backup fired no notification at all. */
if ($status === 'warning') {
$app->enqueueMessage(
'MokoSuiteBackup: ' . $description . ' completed with warnings — '
. ($result['message'] ?: 'the local archive was created but a remote upload failed; see the backup log.'),
'warning' 'warning'
); );
} elseif ($status === 'fail' || empty($result['success'])) {
$app->enqueueMessage(
'MokoSuiteBackup: ' . $description . ' failed — ' . ($result['message'] ?: 'unknown error'),
'error'
);
} else {
$app->enqueueMessage(
'MokoSuiteBackup: ' . $description . ' completed successfully.',
'message'
);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log('MokoSuiteBackup: ' . $description . ' failed: ' . $e->getMessage()); error_log('MokoSuiteBackup: ' . $description . ' failed: ' . $e->getMessage());
@@ -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.00</version> <version>02.58.00</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="webservices" method="upgrade"> <extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoSuiteBackup</name> <name>Web Services - MokoSuiteBackup</name>
<version>02.57.00</version> <version>02.58.00</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.00</version> <version>02.58.00</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>
+148 -17
View File
@@ -77,6 +77,13 @@ class Pkg_MokoSuiteBackupInstallerScript
$this->backupDownloadKey(); $this->backupDownloadKey();
} }
/* Remove any orphaned mis-registered component BEFORE the component install
recreates its admin menu. The orphan (element com_component-mokosuitebackup)
owns a "Backup" menu with alias "backup"; if it survives, the real
component's menu creation collides ("The alias backup is already being used")
and the whole install aborts. */
$this->removeOrphanedComponent();
return true; return true;
} }
@@ -177,6 +184,10 @@ class Pkg_MokoSuiteBackupInstallerScript
briefly used the "Type - Name" convention. Self-healing on every update. */ briefly used the "Type - Name" convention. Self-healing on every update. */
$this->removeOrphanedComponent(); $this->removeOrphanedComponent();
/* Drop legacy single-remote storage columns portably (replaces the old
PREPARE/EXECUTE migration that MySQL 8 rejected in Joomla's installer). */
$this->dropLegacyRemoteColumns();
/* Install completion notice (install and update) */ /* Install completion notice (install and update) */
$this->installSuccessful(); $this->installSuccessful();
@@ -273,6 +284,54 @@ class Pkg_MokoSuiteBackupInstallerScript
} }
} }
/**
* Drop the 26 legacy single-remote storage columns from the profiles table if
* they are still present. Replaces sql/updates/mysql/02.56.01.sql, whose
* PREPARE/EXECUTE/DEALLOCATE approach is rejected by Joomla's installer on
* MySQL 8 (error 1295) and aborted the whole install. Done here in PHP: gate on
* INFORMATION_SCHEMA, then issue a plain ALTER for only the columns that exist —
* portable across MariaDB and MySQL 8. Idempotent and non-fatal.
*
* @return void
*/
private function dropLegacyRemoteColumns(): void
{
try {
$db = Factory::getDbo();
$table = $db->getPrefix() . 'mokosuitebackup_profiles';
$columns = [
'remote_storage',
'ftp_host', 'ftp_port', 'ftp_username', 'ftp_password', 'ftp_path', 'ftp_passive', 'ftp_ssl',
'sftp_host', 'sftp_port', 'sftp_username', 'sftp_auth_type', 'sftp_password', 'sftp_key_data', 'sftp_passphrase', 'sftp_path',
'gdrive_client_id', 'gdrive_client_secret', 'gdrive_refresh_token', 'gdrive_folder_id',
's3_endpoint', 's3_region', 's3_access_key', 's3_secret_key', 's3_bucket', 's3_path',
];
// Which legacy columns still exist on this install?
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('COLUMN_NAME'))
->from($db->quoteName('INFORMATION_SCHEMA.COLUMNS'))
->where($db->quoteName('TABLE_SCHEMA') . ' = DATABASE()')
->where($db->quoteName('TABLE_NAME') . ' = ' . $db->quote($table))
->where($db->quoteName('COLUMN_NAME') . ' IN (' . implode(',', array_map([$db, 'quote'], $columns)) . ')')
);
$present = $db->loadColumn();
if (empty($present)) {
return;
}
$drops = array_map(fn($c) => 'DROP COLUMN ' . $db->quoteName($c), $present);
$db->setQuery('ALTER TABLE ' . $db->quoteName($table) . ' ' . implode(', ', $drops));
$db->execute();
} catch (\Throwable $e) {
/* Best-effort — a leftover column is harmless and must never abort the install */
Log::add('MokoSuiteBackup legacy-remote-column drop failed: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/** /**
* Recursively delete a directory (version-independent, no Folder dependency). * Recursively delete a directory (version-independent, no Folder dependency).
* *
@@ -570,23 +629,7 @@ class Pkg_MokoSuiteBackupInstallerScript
try { try {
$db = Factory::getDbo(); $db = Factory::getDbo();
/* Find the parent menu item for our component */ /* Component extension_id first — needed to (re)create the parent menu. */
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('menutype')])
->from($db->quoteName('#__menu'))
->where($db->quoteName('client_id') . ' = 1')
->where($db->quoteName('level') . ' = 1')
->where($db->quoteName('link') . ' LIKE ' . $db->quote('index.php?option=com_mokosuitebackup%'))
->setLimit(1);
$db->setQuery($query);
$parent = $db->loadObject();
if (!$parent) {
error_log('MokoSuiteBackup: ensureSubmenuItems() — parent menu item not found');
return;
}
/* Get the component extension_id */
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName('extension_id')) ->select($db->quoteName('extension_id'))
->from($db->quoteName('#__extensions')) ->from($db->quoteName('#__extensions'))
@@ -601,6 +644,41 @@ class Pkg_MokoSuiteBackupInstallerScript
return; return;
} }
/* Find the top-level "Backup" parent menu item for our component */
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('menutype')])
->from($db->quoteName('#__menu'))
->where($db->quoteName('client_id') . ' = 1')
->where($db->quoteName('level') . ' = 1')
->where($db->quoteName('link') . ' LIKE ' . $db->quote('index.php?option=com_mokosuitebackup%'))
->setLimit(1);
$db->setQuery($query);
$parent = $db->loadObject();
/* Self-heal: if the top-level menu was deleted (e.g. by a failed install
after the duplicate-component mess), recreate it — otherwise the whole
Backup menu stays gone and dependents (cPanel module, MokoSuiteClient)
can't locate the component. */
if (!$parent) {
$parent = $this->createTopMenu($componentId);
if (!$parent) {
error_log('MokoSuiteBackup: ensureSubmenuItems() — parent menu missing and could not be created');
return;
}
}
/* Keep the top-level menu label on the short constant and owned by the
real component. */
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__menu'))
->set($db->quoteName('title') . ' = ' . $db->quote('COM_MOKOJOOMBACKUP_SHORT'))
->set($db->quoteName('component_id') . ' = ' . (int) $componentId)
->where($db->quoteName('id') . ' = ' . (int) $parent->id)
);
$db->execute();
foreach ($submenus as $submenu) { foreach ($submenus as $submenu) {
/* Check if this submenu item already exists */ /* Check if this submenu item already exists */
$query = $db->getQuery(true) $query = $db->getQuery(true)
@@ -666,6 +744,59 @@ class Pkg_MokoSuiteBackupInstallerScript
} }
} }
/**
* Create the top-level "Backup" admin menu item for the component when it is
* missing (deleted by a failed install / the duplicate-component fallout).
* Label uses the short constant. Returns a lightweight {id, menutype} object,
* or null on failure.
*
* @param int $componentId Extension id of com_mokosuitebackup
*
* @return object|null
*/
private function createTopMenu(int $componentId): ?object
{
try {
$table = Factory::getApplication()
->bootComponent('com_menus')
->getMVCFactory()
->createTable('Menu', 'Administrator');
$table->menutype = 'main';
$table->title = 'COM_MOKOJOOMBACKUP_SHORT';
$table->alias = 'backup';
$table->link = 'index.php?option=com_mokosuitebackup';
$table->type = 'component';
$table->published = 1;
$table->parent_id = 1;
$table->level = 1;
$table->component_id = $componentId;
$table->client_id = 1;
$table->img = 'class:archive';
$table->params = '{}';
$table->language = '*';
$table->access = 1;
$table->setLocation(1, 'last-child');
if (!$table->check() || !$table->store()) {
error_log('MokoSuiteBackup: createTopMenu() failed: ' . $table->getError());
return null;
}
$obj = new \stdClass();
$obj->id = (int) $table->id;
$obj->menutype = 'main';
return $obj;
} catch (\Throwable $e) {
error_log('MokoSuiteBackup: createTopMenu() exception: ' . $e->getMessage());
return null;
}
}
private function fixPackageClientId(): void private function fixPackageClientId(): void
{ {
try { try {