Compare commits

...

46 Commits

Author SHA1 Message Date
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
jmiller 129be7ef9d fix(release): idempotent changelog promote + dedupe 02.56.11 header
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: PR Check / Require Docs Update (pull_request) Failing after 5s
Generic: Project CI / Lint & Validate (pull_request) Successful in 11s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 35s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 1m13s
Universal: PR Check / Validate PR (pull_request) Failing after 1m11s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m22s
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
Guard the promote so it only inserts a version header when absent (stops the
empty duplicate headers each build was appending), and remove the duplicate
02.56.11 header the non-idempotent promote just created.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-05 17:28:08 -05:00
jmiller 14392de078 Merge pull request 'docs(changelog): backfill 02.56.07 + 02.56.08 (shipped with empty changelogs)' (#215) from fix/changelog-backfill into main
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
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Generic: Project CI / Lint & Validate (pull_request) Successful in 15s
Universal: PR Check / Validate PR (pull_request) Failing after 13s
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 5s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 14s
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:26:17 +00:00
jmiller 8bdadb68c2 docs(changelog): backfill 02.56.07/08 + dedupe blank duplicate headers
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Universal: PR Check / Require Docs Update (pull_request) Failing after 5s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
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
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 31s
Generic: Project CI / Lint & Validate (pull_request) Successful in 52s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 56s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 58s
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:26:05 -05:00
gitea-actions[bot] bcc4054ced chore: promote changelog [Unreleased] → [02.56.11]
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
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Generic: Project CI / Lint & Validate (pull_request) Successful in 13s
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 3s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 10s
Universal: PR Check / Validate PR (pull_request) Failing after 33s
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:23:35 +00:00
gitea-actions[bot] 0cba7f81e8 chore(release): build 02.56.11 [skip ci] 2026-07-05 22:23:26 +00:00
jmiller 2688ea352b Merge pull request 'fix(installer): self-heal orphaned com_component-mokosuitebackup on update' (#216) from fix/orphan-component-cleanup 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
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 4s
Sync Workflows to Repos / sync (push) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
Generic: Project CI / Lint & Validate (pull_request) Successful in 49s
Universal: PR Check / Validate PR (pull_request) Failing after 46s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Failing after 58s
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:23:08 +00:00
mokogitea-actions[bot] 39d94a31d6 chore(version): pre-release bump to 02.56.11-dev [skip ci]
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 31s
2026-07-05 22:14:22 +00:00
jmiller 1029e40908 fix(installer): self-heal orphaned com_component-mokosuitebackup on update
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Wiki Update Reminder (pull_request) Successful in 3s
Universal: PR Check / Require Docs Update (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Universal: PR Check / Validate PR (pull_request) Failing after 13s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 52s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 50s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 29s
Universal: Build & Release / Promote to RC (pull_request) Failing after 15s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
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
The old "Type - Name" component <name> made Joomla register a duplicate
component under element com_component-mokosuitebackup (its own extension row,
admin menu, schema rows and administrator/ folder), orphaning the real
com_mokosuitebackup on the old version. The manifest fix (#213) prevents new
occurrences; this reconciles already-affected sites automatically.

Package postflight now removes any component whose element matches
%component%mokosuitebackup% (excluding com_mokosuitebackup): deletes its
#__menu, #__schemas and #__update_sites_extensions rows, the #__extensions
record, and the stray admin folder. Idempotent and non-fatal.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-05 17:11:51 -05:00
gitea-actions[bot] ec3094385f chore: promote changelog [Unreleased] → [02.56.08]
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 8s
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 4s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 17s
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m17s
Universal: PR Check / Validate PR (pull_request) Failing after 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
2026-07-05 22:02:08 +00:00
gitea-actions[bot] 4a2bb1b19d chore(release): build 02.56.08 [skip ci] 2026-07-05 22:01:48 +00:00
jmiller 3c5ad43e97 Merge pull request 'fix(manifest): component &lt;name&gt; must be element-safe (com_mokosuitebackup)' (#213) from fix/component-element-name into main
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
Universal: PR Check / Secret Scan (pull_request) Successful in 8s
Generic: Project CI / Lint & Validate (pull_request) Successful in 15s
Cascade Main -> Dev / Cascade main -> dev (push) Successful in 5s
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 / 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:01:17 +00:00
mokogitea-actions[bot] f92e5227dc chore(version): pre-release bump to 02.56.08-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Successful in 3s
Universal: Build & Release / Promote to RC (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 / Build & Release Pipeline (pull_request) Successful in 53s
2026-07-05 21:58:57 +00:00
44 changed files with 587 additions and 115 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
+6 -3
View File
@@ -380,9 +380,12 @@ 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()
old = '## [Unreleased]' marker = f'## [{version}]'
new = f'## [Unreleased]\n\n## [{version}] --- {date}' # Idempotent: only promote when this version header isn't already present,
content = content.replace(old, new, 1) # 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) open('CHANGELOG.md', 'w').write(content)
" "$VERSION" "$DATE" " "$VERSION" "$DATE"
git add CHANGELOG.md git add CHANGELOG.md
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Automation # INGROUP: mokocli.Automation
# VERSION: 01.00.00 # VERSION: 02.58.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"
+50 -8
View File
@@ -1,15 +1,57 @@
# Changelog # Changelog
## [Unreleased] ## [Unreleased]
## [02.56.07] --- 2026-07-05
## [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)
## [02.56.11] --- 2026-07-05
### Fixed
- Package `postflight` now removes the orphaned `com_component-mokosuitebackup` registration (stray extension record, admin menu items, schema rows, and `administrator/` folder) left behind by the old "Type - Name" component `<name>` — self-healing on update so affected sites reconcile to the real `com_mokosuitebackup`. (#213 follow-up)
## [02.56.08] --- 2026-07-05
### Fixed
- Component manifest `<name>` reverted to the element-safe `MokoSuiteBackup`. The "Type - Name" convention derived element `com_component-mokosuitebackup`, registering a duplicate component and leaving the real `com_mokosuitebackup` orphaned on the old version. (#213)
## [02.56.07] --- 2026-07-05 ## [02.56.07] --- 2026-07-05
## [02.56.05] --- 2026-07-05 ### Security
- Mask the FTP password in `AjaxController::maskSecrets()`/`mergeExistingSecrets()` — stored FTP remotes no longer leak their password via `listRemotes`. (#169)
- Stop the API single-item view (`Backups/JsonapiView`) leaking the server `absolute_path`. (#187)
- SFTP uploader uses `StrictHostKeyChecking=accept-new` instead of `no`. (#182)
## [02.56.05] --- 2026-07-05 ### Fixed
- `DatabaseDumper` writes a real newline after `DROP TABLE` (was a literal `\n`). (#179)
## [02.56.00] --- 2026-07-04 - Profile-picker forms order by `id` instead of the dropped `ordering` column. (#180)
- `uninstall.mysql.sql` now drops the snapshots table. (#181)
- CLI snapshot delete uses the `data_file` column (was non-existent `file_path`). (#184)
## [02.56.00] --- 2026-07-04 - Remove the duplicate pre-update backup (content plugin no longer subscribes to `onExtensionBeforeUpdate`). (#186)
- `DatabaseImporter` collects non-fatal statement errors (`getErrors()`/`hasErrors()`). (#188)
+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.56.07 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.56.07</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.56.08 — no schema changes */
@@ -0,0 +1 @@
/* 02.56.11 — 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::_('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>
@@ -259,6 +265,7 @@ $token = Session::getFormToken();
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>
<script> <script>
@@ -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.56.07</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.56.07</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.56.07</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>
@@ -13,7 +13,7 @@ namespace Joomla\Plugin\Console\MokoSuiteBackup\Command;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine; use Joomla\Component\MokoSuiteBackup\Administrator\Service\BackupRunner;
use Joomla\Console\Command\AbstractCommand; use Joomla\Console\Command\AbstractCommand;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@@ -40,20 +40,19 @@ class RunCommand extends AbstractCommand
$io->title('MokoSuiteBackup — Run Backup'); $io->title('MokoSuiteBackup — Run Backup');
$io->text('Profile ID: ' . $profileId); $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.'); $io->error('MokoSuiteBackup component not installed.');
return 1; return 1;
} }
if (!class_exists(BackupEngine::class)) { if (!class_exists(BackupRunner::class)) {
require_once $engineFile; require_once $runnerFile;
} }
$engine = new BackupEngine(); $result = (new BackupRunner())->run($profileId, $desc ?: 'CLI backup', 'cli');
$result = $engine->run($profileId, $desc ?: 'CLI backup', 'cli');
if ($result['success']) { if ($result['success']) {
$io->success($result['message']); $io->success($result['message']);
@@ -7,7 +7,7 @@
--> -->
<extension type="plugin" group="content" method="upgrade"> <extension type="plugin" group="content" method="upgrade">
<name>Content - MokoSuiteBackup</name> <name>Content - MokoSuiteBackup</name>
<version>02.56.07</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>
@@ -14,7 +14,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine; use Joomla\Component\MokoSuiteBackup\Administrator\Service\BackupRunner;
use Joomla\Event\Event; use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface; use Joomla\Event\SubscriberInterface;
@@ -61,19 +61,18 @@ final class MokoSuiteBackupContent extends CMSPlugin implements SubscriberInterf
$session->set('mokosuitebackup.content_last_autobackup', time()); $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; return;
} }
if (!class_exists(BackupEngine::class)) { if (!class_exists(BackupRunner::class)) {
require_once $engineFile; require_once $runnerFile;
} }
try { try {
$engine = new BackupEngine(); (new BackupRunner())->run($profileId, $description, 'backend');
$engine->run($profileId, $description, 'backend');
} catch (\Throwable $e) { } catch (\Throwable $e) {
// Non-fatal — log and continue with the install/update // Non-fatal — log and continue with the install/update
Factory::getApplication()->enqueueMessage( Factory::getApplication()->enqueueMessage(
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="quickicon" method="upgrade"> <extension type="plugin" group="quickicon" method="upgrade">
<name>Quick Icon - MokoSuiteBackup</name> <name>Quick Icon - MokoSuiteBackup</name>
<version>02.56.07</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.56.07</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>
@@ -15,7 +15,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine; use Joomla\Component\MokoSuiteBackup\Administrator\Service\BackupRunner;
use Joomla\Event\Event; use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface; use Joomla\Event\SubscriberInterface;
@@ -91,8 +91,7 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
@ini_set('memory_limit', '512M'); @ini_set('memory_limit', '512M');
try { try {
$engine = new BackupEngine(); $result = (new BackupRunner())->run($profileId, 'Web cron backup', 'webcron');
$result = $engine->run($profileId, 'Web cron backup', 'webcron');
$this->sendJsonResponse( $this->sendJsonResponse(
$result['success'], $result['success'],
@@ -390,14 +389,29 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
$profileId = (int) $params->get('default_profile', 1); $profileId = (int) $params->get('default_profile', 1);
try { try {
$engine = new BackupEngine(); $result = (new BackupRunner())->run($profileId, $description, 'preaction');
$result = $engine->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.56.07</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>
@@ -72,7 +72,7 @@ final class MokoSuiteBackupTask extends CMSPlugin implements SubscriberInterface
$profileId = (int) ($params->profile_id ?? 1); $profileId = (int) ($params->profile_id ?? 1);
// Load the backup engine from the component // Load the backup engine from the component
$engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Engine/BackupEngine.php'; $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/src/Service/BackupRunner.php';
if (!file_exists($engineFile)) { if (!file_exists($engineFile)) {
$this->logTask('MokoSuiteBackup component not installed — cannot run backup.'); $this->logTask('MokoSuiteBackup component not installed — cannot run backup.');
@@ -81,11 +81,11 @@ final class MokoSuiteBackupTask extends CMSPlugin implements SubscriberInterface
} }
// The autoloader should handle this via namespace, but ensure class is available // The autoloader should handle this via namespace, but ensure class is available
if (!class_exists('\\Joomla\\Component\\MokoSuiteBackup\\Administrator\\Engine\\BackupEngine')) { if (!class_exists('\\Joomla\\Component\\MokoSuiteBackup\\Administrator\\Service\\BackupRunner')) {
require_once $engineFile; 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'); $result = $engine->run($profileId, 'Scheduled task backup', 'scheduled');
if ($result['success']) { if ($result['success']) {
@@ -7,7 +7,7 @@
--> -->
<extension type="plugin" group="webservices" method="upgrade"> <extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoSuiteBackup</name> <name>Web Services - MokoSuiteBackup</name>
<version>02.56.07</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.56.07</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>
+254 -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;
} }
@@ -172,6 +179,15 @@ class Pkg_MokoSuiteBackupInstallerScript
/* Migrate profiles with old default backup_dir values to [DEFAULT_DIR] placeholder */ /* Migrate profiles with old default backup_dir values to [DEFAULT_DIR] placeholder */
$this->migrateDefaultBackupDir(); $this->migrateDefaultBackupDir();
/* Remove the orphaned mis-registered component (element
com_component-mokosuitebackup) left behind when the component <name>
briefly used the "Type - Name" convention. Self-healing on every update. */
$this->removeOrphanedComponent();
/* 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();
@@ -192,6 +208,155 @@ class Pkg_MokoSuiteBackupInstallerScript
} }
} }
/**
* Remove the orphaned component registration created when the component
* <name> briefly used the "Type - Name" display convention. Joomla derives a
* component's element from its <name>, so "Component - MokoSuiteBackup"
* produced element com_component-mokosuitebackup — its own extension record,
* admin menu items, schema rows and administrator/ folder — orphaning the real
* com_mokosuitebackup on the old version. This reconciles affected sites on
* update. Idempotent and non-fatal: does nothing when no orphan exists.
*
* @return void
*/
private function removeOrphanedComponent(): void
{
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName(['extension_id', 'element']))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
->where($db->quoteName('client_id') . ' = 1')
->where($db->quoteName('element') . ' LIKE ' . $db->quote('%component%mokosuitebackup%'))
->where($db->quoteName('element') . ' <> ' . $db->quote('com_mokosuitebackup'));
$db->setQuery($query);
$orphans = $db->loadObjectList();
if (empty($orphans)) {
return;
}
foreach ($orphans as $orphan) {
$id = (int) $orphan->extension_id;
/* Admin menu items owned by the orphan component */
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__menu'))
->where($db->quoteName('component_id') . ' = ' . $id)
)->execute();
/* Schema-version rows */
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__schemas'))
->where($db->quoteName('extension_id') . ' = ' . $id)
)->execute();
/* Update-site mapping (leave #__update_sites; harmless if orphaned) */
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__update_sites_extensions'))
->where($db->quoteName('extension_id') . ' = ' . $id)
)->execute();
/* The bogus extension record itself */
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__extensions'))
->where($db->quoteName('extension_id') . ' = ' . $id)
)->execute();
/* The stray admin component folder on disk */
$this->deleteFolderRecursive(JPATH_ADMINISTRATOR . '/components/' . $orphan->element);
Factory::getApplication()->enqueueMessage(
'MokoSuiteBackup: removed orphaned component registration "'
. htmlspecialchars($orphan->element, ENT_QUOTES, 'UTF-8') . '".',
'info'
);
}
} catch (\Throwable $e) {
/* Never block the install/update over best-effort cleanup */
Log::add('MokoSuiteBackup orphan-component cleanup failed: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/**
* 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).
*
* @param string $dir Absolute path
*
* @return void
*/
private function deleteFolderRecursive(string $dir): void
{
if ($dir === '' || !is_dir($dir)) {
return;
}
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item) {
$item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
}
@rmdir($dir);
}
/** /**
* Generate a cryptographically random webcron secret word on fresh install. * Generate a cryptographically random webcron secret word on fresh install.
*/ */
@@ -464,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'))
@@ -495,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)
@@ -560,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 {