Compare commits

..

38 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
gitea-actions[bot] ea91b49e50 chore(release): build 02.57.00 [skip ci] 2026-07-05 22:30:31 +00:00
jmiller 99d8e954bd Merge pull request 'feat: BackupRunner helper — unify backup run/status/notification' (#214) from feat/backup-runner-helper into main
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
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 9s
Universal: PR Check / Validate PR (pull_request) Failing after 15s
Generic: Project CI / Lint & Validate (pull_request) Successful in 55s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 59s
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:29:38 +00:00
jmiller 28ca110d7b docs(changelog): add BackupRunner entry (#214)
Universal: PR Check / Branch Policy (pull_request) Failing after 4s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 11s
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 2s
Universal: PR Check / Require Docs Update (pull_request) Failing after 8s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Universal: PR Check / Secret Scan (pull_request) Successful in 11s
Branch Cleanup / Delete merged branch (pull_request) Successful in 4s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 15s
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
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 15s
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m3s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 51s
Generic: Project CI / Tests (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (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
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report: Scripts Governance (pull_request) Has been cancelled
Generic: Repo Health / Report: Repository Health (pull_request) Has been cancelled
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-05 17:29:28 -05:00
jmiller 307067580c feat: BackupRunner helper — unify backup run/status/notification
Every non-interactive backup trigger used a slightly different path, so
final status and notification gating behaved inconsistently depending on
what started the backup (this is why pre-update backups did not gate
notifications the way "Backup Now" does).

Add src/Service/BackupRunner.php as the single synchronous entry point.
It delegates to BackupEngine (which already sends notifications gated by
the profile's notify_on_success/notify_on_failure and applies retention —
so there is exactly ONE notification layer), and normalises the result,
including the real complete/warning/fail status the engine records but did
not return. BackupRunner self-loads BackupEngine so callers require one file.

Routed through BackupRunner:
- plg_system  runPreActionBackup() (preaction) + web-cron handler (webcron)
- plg_content triggerAutoBackup() (pre-install)
- plg_task    scheduled task run
- plg_console RunCommand (cli)

The dashboard AJAX/stepped "Backup Now" flow is unchanged (it is the
interactive equivalent). The installer progress popup is deferred — the
installer request cannot drive the AJAX modal (TODO(#196) noted in code).

php -l clean on all changed files.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-05 17:28:59 -05:00
jmiller 7f5f041588 Merge pull request 'fix(release): idempotent changelog promote (no duplicate version headers)' (#217) from fix/changelog-promote-idempotent into main
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
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 14s
Sync Workflows to Repos / sync (push) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 14s
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m6s
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:28:19 +00:00
45 changed files with 459 additions and 116 deletions
+2 -2
View File
@@ -59,8 +59,8 @@ Joomla **package** with four sub-extensions:
- **Attribution**: `Authored-by: Moko Consulting`
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
- **Minification**: handled at build time (CI)
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/mokocli/wiki/Home)
- **Wiki**: documentation lives in the MokoGitea wiki, not `docs/` files
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki)
## Coding Standards
+4 -7
View File
@@ -380,13 +380,10 @@ jobs:
import sys
version, date = sys.argv[1], sys.argv[2]
content = open('CHANGELOG.md').read()
marker = f'## [{version}]'
# Idempotent: only promote when this version header isn't already present,
# otherwise re-runs (or same-version builds) create empty duplicate headers.
if marker not in content:
new = f'## [Unreleased]\n\n{marker} --- {date}'
content = content.replace('## [Unreleased]', new, 1)
open('CHANGELOG.md', 'w').write(content)
old = '## [Unreleased]'
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
content = content if ('## [' + version + ']') in content else content.replace(old, new, 1)
open('CHANGELOG.md', 'w').write(content)
" "$VERSION" "$DATE"
git add CHANGELOG.md
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
+5 -4
View File
@@ -131,10 +131,11 @@ jobs:
test:
name: Tests
runs-on: ubuntu-latest
needs: lint
# Run only when lint succeeded; always() forces evaluation so a skipped
# lint (e.g. template repos) skips this job cleanly instead of hanging.
if: ${{ always() && needs.lint.result == 'success' }}
# Independent job (no `needs: lint`): the Gitea Actions scheduler does not
# offer the dependent 2nd job of a needs-chain to runners, so it stalls in
# "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos
# directly (same condition lint uses) instead of gating on lint's result.
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps:
- name: Checkout
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 02.56.14
# VERSION: 01.00.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+1 -1
View File
@@ -210,7 +210,7 @@ jobs:
- name: Check for merge conflict markers
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
echo "::error::Merge conflict markers found in source files"
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
+27
View File
@@ -1,6 +1,33 @@
# Changelog
## [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
### 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
- 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)
+1 -1
View File
@@ -4,7 +4,7 @@ Thank you for your interest in contributing to MokoSuiteBackup.
## Getting Started
1. Fork the repository on Gitea
1. Fork the repository on MokoGitea
2. Create a feature branch from `dev` (`feature/your-feature`)
3. Make your changes following the coding standards below
4. Submit a pull request targeting `dev`
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
PATH: /SECURITY.md
VERSION: 02.56.14
VERSION: 02.58.00
BRIEF: Security vulnerability reporting and handling policy
-->
@@ -205,19 +205,9 @@
/>
</fieldset>
<fieldset name="remote" label="COM_MOKOJOOMBACKUP_FIELDSET_REMOTE">
<field
name="remote_keep_local"
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>
<!-- The "Keep local copy" setting is now configured per remote destination
(in the Add/Edit Destination modal), so the profile-level field and its
"Remote" tab have been removed. -->
<fieldset name="retention" label="COM_MOKOJOOMBACKUP_FIELDSET_RETENTION">
<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.
-->
<name>MokoSuiteBackup</name>
<version>02.56.14</version>
<version>02.58.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -47,7 +47,7 @@
</update>
<administration>
<menu img="class:archive">Backup</menu>
<menu img="class:archive">COM_MOKOJOOMBACKUP_SHORT</menu>
<submenu>
<menu link="option=com_mokosuitebackup&amp;view=dashboard"
img="class:home"
@@ -78,6 +78,11 @@
</languages>
</administration>
<media destination="com_mokosuitebackup" folder="media">
<folder>css</folder>
<filename>joomla.asset.json</filename>
</media>
<api>
<files folder="api">
<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
-- MariaDB-only extension and errors on Oracle MySQL 8.x. On MySQL 8 installs the
-- migration failed but Joomla still recorded the schema as applied, leaving all 26
-- legacy remote_storage/ftp_*/sftp_*/gdrive_*/s3_* columns stranded on the profiles
-- table. This migration removes them portably.
-- The 26 legacy remote_storage/ftp_*/sftp_*/gdrive_*/s3_* columns must be removed
-- portably across MariaDB and MySQL 8. `DROP COLUMN IF EXISTS` is MariaDB-only
-- (errors on MySQL 8); plain `DROP COLUMN` errors when a column is absent. The
-- previous version gated the ALTER on INFORMATION_SCHEMA and ran it via
-- 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
-- (MariaDB, or anyone who ran the corrected 02.52.25). Plain `DROP COLUMN` errors when
-- 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;
-- The drop is now done in PHP (INFORMATION_SCHEMA gate + a plain ALTER for only the
-- columns that still exist), so this migration is intentionally a no-op.
@@ -1 +0,0 @@
/* 02.56.14 — no schema changes */
@@ -0,0 +1 @@
/* 02.57.00 — no schema changes */
@@ -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 */
@@ -0,0 +1,109 @@
<?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';
}
}
}
@@ -19,6 +19,9 @@ HTMLHelper::_('behavior.formvalidator');
HTMLHelper::_('behavior.keepalive');
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;
$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-dialog modal-lg">
<div class="modal-content">
<div class="container-fluid">
<div class="modal-header">
<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>
@@ -190,7 +194,9 @@ $token = Session::getFormToken();
</div>
<div class="mb-3" id="remoteSftpKeyWrap">
<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 class="mb-3" id="remoteSftpPassphraseWrap">
<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'); ?>
</button>
</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 ----
document.getElementById('btnAddRemote').addEventListener('click', function() {
openEdit(0);
@@ -8,7 +8,7 @@
-->
<extension type="module" client="administrator" method="upgrade">
<name>Module - MokoSuiteBackup - cPanel</name>
<version>02.56.14</version>
<version>02.58.00</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.56.14</version>
<version>02.58.00</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.56.14</version>
<version>02.58.00</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -13,7 +13,7 @@ namespace Joomla\Plugin\Console\MokoSuiteBackup\Command;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
use Joomla\Component\MokoSuiteBackup\Administrator\Service\BackupRunner;
use Joomla\Console\Command\AbstractCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -40,20 +40,19 @@ class RunCommand extends AbstractCommand
$io->title('MokoSuiteBackup — Run Backup');
$io->text('Profile ID: ' . $profileId);
$engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Engine/BackupEngine.php';
$runnerFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Service/BackupRunner.php';
if (!file_exists($engineFile)) {
if (!file_exists($runnerFile)) {
$io->error('MokoSuiteBackup component not installed.');
return 1;
}
if (!class_exists(BackupEngine::class)) {
require_once $engineFile;
if (!class_exists(BackupRunner::class)) {
require_once $runnerFile;
}
$engine = new BackupEngine();
$result = $engine->run($profileId, $desc ?: 'CLI backup', 'cli');
$result = (new BackupRunner())->run($profileId, $desc ?: 'CLI backup', 'cli');
if ($result['success']) {
$io->success($result['message']);
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="content" method="upgrade">
<name>Content - MokoSuiteBackup</name>
<version>02.56.14</version>
<version>02.58.00</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -14,7 +14,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
use Joomla\Component\MokoSuiteBackup\Administrator\Service\BackupRunner;
use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;
@@ -61,19 +61,18 @@ final class MokoSuiteBackupContent extends CMSPlugin implements SubscriberInterf
$session->set('mokosuitebackup.content_last_autobackup', time());
$engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Engine/BackupEngine.php';
$runnerFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Service/BackupRunner.php';
if (!file_exists($engineFile)) {
if (!file_exists($runnerFile)) {
return;
}
if (!class_exists(BackupEngine::class)) {
require_once $engineFile;
if (!class_exists(BackupRunner::class)) {
require_once $runnerFile;
}
try {
$engine = new BackupEngine();
$engine->run($profileId, $description, 'backend');
(new BackupRunner())->run($profileId, $description, 'backend');
} catch (\Throwable $e) {
// Non-fatal — log and continue with the install/update
Factory::getApplication()->enqueueMessage(
@@ -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.56.14</version>
<version>02.58.00</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.56.14</version>
<version>02.58.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -15,7 +15,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
use Joomla\Component\MokoSuiteBackup\Administrator\Service\BackupRunner;
use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;
@@ -91,8 +91,7 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
@ini_set('memory_limit', '512M');
try {
$engine = new BackupEngine();
$result = $engine->run($profileId, 'Web cron backup', 'webcron');
$result = (new BackupRunner())->run($profileId, 'Web cron backup', 'webcron');
$this->sendJsonResponse(
$result['success'],
@@ -390,14 +389,29 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
$profileId = (int) $params->get('default_profile', 1);
try {
$engine = new BackupEngine();
$result = $engine->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']) {
Factory::getApplication()->enqueueMessage(
'MokoSuiteBackup: ' . $description . ' failed — ' . $result['message'],
/* Always surface the outcome so the pre-update backup is visible in the
admin — previously only failures produced a message, so a successful
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'
);
} 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) {
error_log('MokoSuiteBackup: ' . $description . ' failed: ' . $e->getMessage());
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="task" method="upgrade">
<name>Task - MokoSuiteBackup</name>
<version>02.56.14</version>
<version>02.58.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -72,7 +72,7 @@ final class MokoSuiteBackupTask extends CMSPlugin implements SubscriberInterface
$profileId = (int) ($params->profile_id ?? 1);
// Load the backup engine from the component
$engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Engine/BackupEngine.php';
$engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Service/BackupRunner.php';
if (!file_exists($engineFile)) {
$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
if (!class_exists('\\Joomla\\Component\\MokoSuiteBackup\\Administrator\\Engine\\BackupEngine')) {
if (!class_exists('\\Joomla\\Component\\MokoSuiteBackup\\Administrator\\Service\\BackupRunner')) {
require_once $engineFile;
}
$engine = new \Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine();
$engine = new \Joomla\Component\MokoSuiteBackup\Administrator\Service\BackupRunner();
$result = $engine->run($profileId, 'Scheduled task backup', 'scheduled');
if ($result['success']) {
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoSuiteBackup</name>
<version>02.56.14</version>
<version>02.58.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+1 -1
View File
@@ -8,7 +8,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoSuiteBackup</name>
<packagename>mokosuitebackup</packagename>
<version>02.56.14</version>
<version>02.58.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+148 -17
View File
@@ -77,6 +77,13 @@ class Pkg_MokoSuiteBackupInstallerScript
$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;
}
@@ -177,6 +184,10 @@ class Pkg_MokoSuiteBackupInstallerScript
briefly used the "Type - Name" convention. Self-healing on every update. */
$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) */
$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).
*
@@ -570,23 +629,7 @@ class Pkg_MokoSuiteBackupInstallerScript
try {
$db = Factory::getDbo();
/* Find the 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();
if (!$parent) {
error_log('MokoSuiteBackup: ensureSubmenuItems() — parent menu item not found');
return;
}
/* Get the component extension_id */
/* Component extension_id first — needed to (re)create the parent menu. */
$query = $db->getQuery(true)
->select($db->quoteName('extension_id'))
->from($db->quoteName('#__extensions'))
@@ -601,6 +644,41 @@ class Pkg_MokoSuiteBackupInstallerScript
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) {
/* Check if this submenu item already exists */
$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
{
try {