Compare commits

..

56 Commits

Author SHA1 Message Date
gitea-actions[bot] b2572e676f chore(version): pre-release bump to 02.53.01-dev [skip ci] 2026-07-04 20:24:51 +00:00
jmiller d155958be3 chore(manifests): hardcode names/descriptions and enforce Type - Name convention
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Generic: Project CI / Lint & Validate (pull_request) Successful in 11s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 8s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 8s
Generic: Repo Health / Access control (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 28s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 45s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 31s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Successful in 4m23s
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
Per the package-XML convention, manifests must not use language strings for
titles/descriptions, and <name> must follow "Type - Name".

- Component <name> "MokoSuiteBackup" → "Component - MokoSuiteBackup"
- Module <name> "mod_mokosuitebackup_cpanel" → "Module - MokoSuiteBackup - cPanel"
- Hardcode every <description> (pkg, com, mod, all 7 plugins) with the literal
  text previously stored in the *_DESCRIPTION language constants
- Hardcode the component admin submenu titles (Dashboard, Backup Records,
  Content Snapshots, Backup Profiles)

Plugin <name> values already followed the Group - Name format and are
unchanged. All manifests validated as well-formed XML.
2026-07-04 15:24:18 -05:00
gitea-actions[bot] 6ff350fb1c chore: promote changelog [Unreleased] → [02.53.00] 2026-07-04 20:21:10 +00:00
gitea-actions[bot] c492d30b1c chore(release): build 02.53.00 [skip ci] 2026-07-04 20:20:57 +00:00
jmiller 23360f4cbc Merge pull request 'Add COM_MOKOJOOMBACKUP_SHORT and apply short name to views and admin menu' (#200) from chore/short-name-branding into main
Sync Workflows to Repos / sync (push) Failing after 2s
2026-07-04 20:20:37 +00:00
jmiller ece864329e chore: sync pr-check.yml from Template-Generic [skip ci] 2026-07-04 19:39:08 +00:00
jmiller 6cf34ea2ed chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-07-04 19:39:06 +00:00
jmiller 1ee5e6ba9b chore: sync branch-cleanup.yml from Template-Generic [skip ci] 2026-07-04 19:39:05 +00:00
gitea-actions[bot] 1be16af5a4 chore(version): pre-release bump to 02.52.27-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
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 35s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Successful in 5m7s
2026-07-04 19:21:24 +00:00
jmiller a808789acb feat: add COM_MOKOJOOMBACKUP_SHORT and apply short name to views and admin menu
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 23s
- Add COM_MOKOJOOMBACKUP_SHORT="Backup" to the component .ini and .sys.ini
  (en-GB + en-US) so both view titles and the admin sidebar menu resolve it.
- Prefix every admin view's toolbar title with the short brand via the
  constant (e.g. "Backup: Dashboard", "Backup: Records"), and tighten the
  view title strings that redundantly led with "Backup"/"MokoSuiteBackup".
- Admin sidebar top-level menu now shows the short name. Hardcoded as
  "Backup" in the manifest per the package-xml convention (no language
  strings in package XML).
2026-07-04 14:20:54 -05:00
jmiller 406b666486 chore: sync sync-on-merge.yml from Template-Generic [skip ci] 2026-07-04 19:06:42 +00:00
jmiller 77d93602d6 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-07-04 19:06:41 +00:00
jmiller c5f4daa2e6 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-07-04 19:06:40 +00:00
jmiller caaedfdff5 chore: sync notify.yml from Template-Generic [skip ci] 2026-07-04 19:06:39 +00:00
jmiller 6792688740 chore: sync cleanup.yml from Template-Generic [skip ci] 2026-07-04 19:06:38 +00:00
jmiller 318b1469e3 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-07-04 19:06:37 +00:00
jmiller b6b101be3a chore: sync auto-release.yml from Template-Generic [skip ci] 2026-07-04 19:06:36 +00:00
jmiller 13af787c13 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-30 19:19:09 +00:00
gitea-actions[bot] 481e395624 chore: promote changelog [Unreleased] → [02.52.24] 2026-06-30 19:18:28 +00:00
gitea-actions[bot] 202a26847b chore(release): build 02.52.24 [skip ci] 2026-06-30 19:18:21 +00:00
jmiller 7b38e238f5 Merge pull request 'fix: use warning status when backup succeeds but upload fails' (#199) from fix/upload-fail-warning-status into main 2026-06-30 19:18:06 +00:00
gitea-actions[bot] 9820d75212 chore(version): pre-release bump to 02.52.24-dev [skip ci]
Universal: Build & Release / Promote to RC (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 1s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 24s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Successful in 2m56s
2026-06-30 19:17:57 +00:00
jmiller 9c0c6eae15 docs: add warning status changes to changelog
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 6s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 23s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 36s
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_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-30 14:17:34 -05:00
gitea-actions[bot] 1daa6869cc chore(version): pre-release bump to 02.52.23-dev [skip ci] 2026-06-30 19:14:33 +00:00
jmiller aefa46e0c4 fix: use warning status when backup succeeds but remote upload fails
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
Previously a successful backup with a failed remote upload was marked
as "complete", hiding the upload failure. Now these records get a
"warning" status with a yellow badge so operators can see at a glance
which backups didn't reach their remote destination.

Warning-status records are treated as usable backups throughout:
- Downloadable, browsable, and restorable (the archive is intact)
- Counted in dashboard stats, storage totals, and success streaks
- Included in purge operations and differential base lookups
- Shown with yellow "warning" badge in list, detail, and cpanel module
- Filterable via the status dropdown on Backup Records

Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-30 14:14:06 -05:00
jmiller ed55ab068b chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-30 18:56:24 +00:00
gitea-actions[bot] 765e6feea6 chore: promote changelog [Unreleased] → [02.52.22] 2026-06-30 18:53:53 +00:00
gitea-actions[bot] 007036301d chore(release): build 02.52.22 [skip ci] 2026-06-30 18:53:45 +00:00
jmiller 1db4015003 Merge pull request 'fix: cancel stalled backups with ACL + auto-timeout failsafe' (#197) from fix/cancel-stalled-backup into main 2026-06-30 18:53:15 +00:00
gitea-actions[bot] 8dac5a7448 chore(version): pre-release bump to 02.52.22-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
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 22s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 11m45s
2026-06-30 18:53:06 +00:00
jmiller 4560ffb84b docs: update changelog with cancel stalled and pre-update modal
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Generic: Project CI / Lint & Validate (pull_request) Successful in 11s
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 29s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 34s
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_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-30 13:52:42 -05:00
gitea-actions[bot] 5be922613f chore(version): pre-release bump to 02.52.21-dev [skip ci] 2026-06-30 18:50:40 +00:00
jmiller b26a21820b fix: auto-cancel stalled backups after 30 min timeout
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 29s
Universal: PR Check / Validate PR (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: Build & Release / Promote to RC (pull_request) Failing after 14s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 13s
Generic: Project CI / Lint & Validate (pull_request) Successful in 43s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 47s
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
PreflightCheck now auto-cancels "running" backup records that have
exceeded 30 minutes, treating them as stalled. Partial archive files
are cleaned up. The auto-cancelled records are surfaced as warnings
so the user knows what happened.

Records younger than 30 minutes are assumed to be legitimately running
and still block new backups for the same profile.

Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-30 13:50:18 -05:00
gitea-actions[bot] a56f72b186 chore(version): pre-release bump to 02.52.20-dev [skip ci] 2026-06-30 18:49:16 +00:00
jmiller 1eb1c18bdf fix: add cancel stalled backup action with ACL permission
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
Backups stuck in "running" status block all future backups for the same
profile via the preflight check. Previously the only fix was a manual
DB update.

Adds a toolbar button and AJAX endpoint to cancel stalled backups:
- New ACL permission: mokosuitebackup.backup.cancel
- BackupsController::cancelStalled() for toolbar (multi-select)
- AjaxController::cancelBackup() for AJAX/API use
- Sets status to "fail", cleans up partial archive files
- Updated preflight error message to mention the cancel action
- Language keys for en-GB and en-US

Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-30 13:48:51 -05:00
jmiller eea1a40265 Merge pull request 'fix: remove orphaned deploy-manual workflow' (#195) from fix/remove-deploy-manual into main
fix: remove orphaned deploy-manual workflow [skip ci]
2026-06-30 18:33:51 +00:00
jmiller ebc692789c chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-30 18:12:34 +00:00
gitea-actions[bot] 983ec77dbd chore: promote changelog [Unreleased] → [02.52.18] 2026-06-30 18:12:06 +00:00
gitea-actions[bot] 25cf65f4cf chore(release): build 02.52.18 [skip ci] 2026-06-30 18:11:56 +00:00
jmiller 2d3a697f22 Merge pull request 'fix: remote upload prefix mismatch and restore security file' (#193) from fix/sftp-upload-and-restore-security into main 2026-06-30 18:11:32 +00:00
jmiller a4df3a651d fix: remove orphaned deploy-manual workflow [skip ci] 2026-06-30 18:07:18 +00:00
gitea-actions[bot] 3d9c48f40f chore(version): pre-release bump to 02.52.18-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 33s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Successful in 2m23s
2026-06-30 18:00:27 +00:00
jmiller 4093267984 fix: add verbose error_log throughout restore script
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 8s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Universal: PR Check / Secret Scan (pull_request) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 30s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 41s
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
Every action handler now logs entry, key parameters, outcomes, and
failures to PHP error_log. Security file creation logs directory
permissions, PHP user, and the specific error when file_put_contents
fails. Database import logs SQL file size, statement counts, and
individual errors. Cleanup logs each file removal success/failure.

Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-30 12:59:59 -05:00
gitea-actions[bot] 236609341f chore(version): pre-release bump to 02.52.17-dev [skip ci] 2026-06-30 17:53:59 +00:00
jmiller 20ce945e73 fix: recreate security file if missing while verification is pending
Universal: PR Check / Branch Policy (pull_request) Failing after 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 11s
Generic: Project CI / Lint & Validate (pull_request) Successful in 13s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 34s
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 security file was only written inside the code-generation block
(first page load). If the file was deleted or failed to write, it
was never recreated because the session already held the code. Now
file writing is a separate check that runs whenever verification is
pending and the file is missing.

Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-30 12:53:36 -05:00
gitea-actions[bot] f2f424a565 chore(version): pre-release bump to 02.52.16-dev [skip ci] 2026-06-30 17:50:30 +00:00
jmiller 3a6bb1c783 fix: remote upload prefix mismatch and restore security file visibility
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 23s
Remote uploaders (SFTP, FTP, S3, Google Drive) expect type-prefixed
property names (sftp_host, ftp_port, etc.) but createUploaderFromParams
passes unprefixed keys from the remotes table params JSON. Add prefix
mapping in createUploaderFromParams to bridge the naming gap.

Rename .mokorestore-security.php to mokorestore-security.php (no leading
dot) so the file is visible in file managers and not blocked by web
server dotfile rules. Also clean it up in actionCleanup.

Closes #13

Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-30 12:50:02 -05:00
jmiller 785ffd85a3 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-28 20:10:19 +00:00
jmiller 71da4af64b chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-28 19:39:25 +00:00
jmiller 4d5711c304 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-28 19:39:22 +00:00
gitea-actions[bot] 1a4bb32c6c chore: promote changelog [Unreleased] → [01.45.00] 2026-06-28 19:31:25 +00:00
gitea-actions[bot] 13a526d6be chore(release): build 01.45.00 [skip ci] 2026-06-28 19:31:21 +00:00
jmiller babdb9e390 ci: add submodules: recursive to checkout (fixes MokoSuiteClient packaging) 2026-06-28 19:30:15 +00:00
jmiller 57c9ea600b ci: add submodules: recursive to checkout (fixes MokoSuiteClient packaging) 2026-06-28 19:25:18 +00:00
jmiller afffef78bd chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-28 19:12:12 +00:00
jmiller 720f008050 Merge pull request 'release: promote dev to main (v01.43.37)' (#159) from dev into main 2026-06-28 19:03:51 +00:00
67 changed files with 480 additions and 379 deletions
+16 -6
View File
@@ -7,7 +7,7 @@
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# VERSION: 05.01.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +=======================================================================+
@@ -64,10 +64,14 @@ jobs:
promote-rc:
name: Promote to RC
runs-on: release
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event.action == 'synchronize' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
!startsWith(github.event.repository.name, 'Template-') &&
(
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event.action == 'synchronize' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
)
steps:
- name: Checkout repository
@@ -75,6 +79,7 @@ jobs:
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
submodules: recursive
- name: Setup mokocli tools
env:
@@ -163,9 +168,13 @@ jobs:
release:
name: Build & Release Pipeline
runs-on: release
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
if: >-
github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
!startsWith(github.event.repository.name, 'Template-') &&
(
github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
)
steps:
- name: Checkout repository
@@ -173,6 +182,7 @@ jobs:
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
submodules: recursive
- name: Configure git for bot pushes
run: |
+2 -1
View File
@@ -33,7 +33,8 @@ jobs:
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
# URL-encode the branch name's slashes (no PHP dependency on the runner)
ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g')
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
+5 -1
View File
@@ -6,7 +6,7 @@
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.gitea/workflows/ci-generic.yml
# PATH: /.mokogitea/workflows/ci-generic.yml
# VERSION: 01.00.00
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
@@ -32,6 +32,8 @@ jobs:
lint:
name: Lint & Validate
runs-on: ubuntu-latest
# Skip on template repos (Template-*) — they hold placeholder scaffolding, not buildable source.
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps:
- name: Checkout
@@ -130,6 +132,8 @@ jobs:
name: Tests
runs-on: ubuntu-latest
needs: lint
# Skip on template repos (Template-*) — see lint job.
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps:
- name: Checkout
+1 -1
View File
@@ -6,7 +6,7 @@
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/cleanup.yml
# PATH: /.mokogitea/workflows/cleanup.yml
# VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
-126
View File
@@ -1,126 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
name: "Universal: Deploy to Dev (Manual)"
on:
workflow_dispatch:
inputs:
clear_remote:
description: 'Delete all remote files before uploading'
required: false
default: 'false'
type: boolean
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
deploy:
name: SFTP Deploy to Dev
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Setup MokoStandards tools
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
id: check
env:
HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
PORT: ${{ vars.DEV_FTP_PORT }}
run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
- name: Deploy via SFTP
if: steps.check.outputs.skip != 'true'
env:
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
> /tmp/sftp-config.json
if [ -n "$SFTP_KEY" ]; then
echo "$SFTP_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
fi
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Summary
if: always()
run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
fi
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 01.45.05
# VERSION: 02.53.01
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+1 -1
View File
@@ -6,7 +6,7 @@
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/notify.yml
# PATH: /.mokogitea/workflows/notify.yml
# VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure
+4 -2
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# INGROUP: mokocli.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 09.23.00
# BRIEF: PR gate — branch policy + code validation before merge
@@ -126,6 +126,8 @@ jobs:
validate:
name: Validate PR
runs-on: ubuntu-latest
# Skip on template repos (Template-*) — no real manifest/source/changelog to validate.
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps:
- name: Checkout
+6 -2
View File
@@ -48,9 +48,13 @@ jobs:
build:
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
runs-on: release
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
if: >-
github.event_name == 'workflow_dispatch' ||
github.event_name == 'push'
!startsWith(github.event.repository.name, 'Template-') &&
(
github.event_name == 'workflow_dispatch' ||
github.event_name == 'push'
)
steps:
- name: Checkout
+31
View File
@@ -0,0 +1,31 @@
name: Sync Workflows to Repos
on:
push:
branches:
- main
paths:
- '.mokogitea/workflows/**'
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout mokocli
uses: actions/checkout@v4
with:
repository: MokoConsulting/mokocli
token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: Setup PHP
uses: https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/raw/branch/main/actions/setup-php@v1
with:
php-version: '8.1'
- name: Install dependencies
run: composer install --no-dev --no-interaction
- name: Sync workflows to generic repos
run: php automation/bulk_sync.php --platform generic --org MokoConsulting --workflows-only --auto-merge --token "${{ secrets.MOKOGITEA_TOKEN }}"
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+22 -148
View File
@@ -1,158 +1,32 @@
# Changelog
## [Unreleased]
### Added
- Customizable restore script filename per backup profile (reduces discoverability on remote servers)
- MokoRestore standalone mode: multi-ZIP selector when multiple backup archives are present
- MokoRestore preflight: Joomla installation detection warning before overwriting an existing site
- MokoRestore error handling: try/catch on fetch calls, HTTP status checks, JSON parse recovery
- Download button on individual backup record detail toolbar
- Profile column in backup records list links to the profile edit view
## [02.53.00] --- 2026-07-04
### Changed
- Moved download, browse archive, and view log actions from backup list rows into the individual backup record view
- Removed "Run Backup" / "Backup Now" buttons from profiles list, profile edit toolbar, and backup records view (backups are triggered from the dashboard only)
- Removed ordering field from profiles; default sort is now by ID ascending
- MokoRestore cleanup and security messages now reference the actual script filename instead of hardcoded "restore.php"
## [02.53.00] --- 2026-07-04
### Fixed
- Bootstrap 5 modal conversion for snapshots view (data-bs-dismiss, modal-footer, getOrCreateInstance)
- ntfy default URL changed from ntfy.sh to ntfy.mokoconsulting.tech
- Untranslated JFIELD_ORDERING_ASC / JFIELD_ORDERING_LABEL language keys replaced with component-specific keys
- Options page title now shows "MokoSuiteBackup Options" instead of raw language key
- Profile dropdown IDs in backup records and dashboard show "#ID — Title (type)" format
- MokoRestore stalling: unhandled promise rejections from network errors or non-JSON responses left UI in loading state
## [01.43.00] --- 2026-06-24
## [02.52.24] --- 2026-06-30
## [01.43.00] --- 2026-06-24
## [02.52.24] --- 2026-06-30
## [01.42.00] --- 2026-06-23
## [01.42.00] --- 2026-06-23
## [01.41.00] — 2026-06-23
### Added — Multi-Remote Storage
- New `#__mokosuitebackup_remotes` table for multiple destinations per profile
- Remote destinations UI: AJAX-driven add/edit/delete/toggle modal on profile edit
- Engine uploads to ALL enabled destinations (BackupEngine + SteppedBackupEngine)
- Migration auto-converts existing SFTP/S3/GDrive/FTP profile columns to new table
- Backward compatibility: falls back to legacy single-remote columns if table empty
- Secrets masked in API responses, merged from DB on save
### Added — Content Snapshots
- Lightweight JSON snapshots of articles, categories, and modules
- Includes tags, custom fields, workflow associations, field values
- Restore modes: Replace (clean slate), Merge (upsert), Selective (per-article)
- Snapshot retention: max count + max age with automatic cleanup
- Scheduled snapshot task via com_scheduler
- CLI: `mokosuitebackup:snapshot create|restore|list|delete`
- REST API: create, list, restore, delete, download snapshots
- Tabbed browse modal: Articles / Categories / Modules with item counts
### Added — SFTP Remote Storage
- SFTP support with SSH key file authentication (key stored base64 in database)
- Auth type dropdown: Password / Key File / Key File + Passphrase
- SshKeyField: file upload via FileReader, key never exposed in HTML
- SFTP remote directory browser for path selection
- `__KEEP_EXISTING__` sentinel preserves key on profile re-save
### Added — MokoRestore Wizard (9 steps)
- Per-table conflict resolution: Replace / Skip / Merge / Data Only
- Preset buttons: "All Replace", "All Skip", "Everything except users"
- Post-restore actions: reset passwords, hits, versions, sessions, cache
- Auto-detect sanitized passwords and prompt for reset (random temp password)
- Standalone mode: restore.php scans directory for ZIP files
- Wrapped mode: restore.php bundled inside backup ZIP
- Security gate with filesystem verification + path traversal protection
### Added — Data Sanitization
- Sanitize user passwords: replace hashes with invalid sentinel
- Sanitize user emails: replace with dummy values
- Clear session data: exclude `#__session` table
- Preserve super admin credentials (optional)
- GDPR-friendly backup sharing for demos and staging sites
### Added — Backup Engine
- Pre-flight validation: directory, disk space, extensions, credentials, running backups
- Auto-verify archive integrity after creation (ZIP, tar.gz, 7z)
- 7z archive format via system 7za/7z CLI binary with native encryption
- Streaming database dump to temp file (prevents OOM on large sites)
- S3 streaming upload via CURLOPT_PUT (prevents OOM)
- Graceful remote degradation: local backup preserved if upload fails
- DatabaseDumper::dumpToFile() for memory-efficient operation
### Added — Admin UI
- Dashboard: snapshot widget, 30-day backup trend chart, per-profile storage breakdown
- CPanel admin dashboard module (mod_mokosuitebackup_cpanel) with quick actions
- Backup type filter dropdown in backups list
- Backup comparison: select two backups for side-by-side diff
- Archive browser: view files inside backup without extracting
- Manual purge: delete backups older than a date with count preview
- Backup count badges on profile list
- "Do not navigate away" warning in backup/restore progress modals
- Clickable placeholder pills for backup directory and archive name fields
- Comprehensive help modal with absolute/relative/placeholder path documentation
- Placeholder resolution display with EXAMPLE prefix
- All placeholders UPPERCASE: [HOST], [SITE_NAME], [DATE], [DATETIME], etc.
### Added — CLI & API
- `mokosuitebackup:restore` with --files-only, --db-only, --password options
- `mokosuitebackup:snapshot` with create, restore, list, delete actions
- REST API for snapshots: create, list, restore, delete, download
- Profile credentials masked in API responses
### Added — Notifications & Logging
- Email/ntfy notifications for site restore, snapshot create/restore
- Joomla Action Logs for restore, snapshot, and snapshot restore events
- Global ntfy server/topic/token settings (fallback for profiles)
### Added — Security & Configuration
- Webcron secret field with CSPRNG generator + strength meter
- IP whitelist field with current IP detection + one-click "Add my IP"
- 10 ACL permissions with full enforcement audit across all controllers
- Config defaults: archive format, MokoRestore mode, sanitization settings
- Path traversal protection on all archive extraction (ZIP, tar.gz, JPA)
### Fixed
- CLI RestoreCommand passed wrong arguments (filepath instead of record ID)
- JPA path traversal: reject `../` in archive entry paths
- S3Uploader OOM: streaming upload instead of file_get_contents
- DatabaseDumper OOM: streaming to file instead of in-memory string
- AkeebaImporter: removed unserialize() (PHP object injection risk)
- BackupTable: delete DB row before file (prevents data loss)
- RestoreEngine: staging path sanitized with preg_replace
- API profiles: sensitive fields masked with `***`
- Webcron: missing return after sendJsonResponse on auth failure
- loadFormData(): cast array to object (PHP 8.x TypeError fix)
- MokoRestore data-only mode: uses REPLACE INTO for existing rows
- Plaintext archive deleted on encryption failure
- TarGzArchiver: intermediate .tar cleaned in finally block
- Install script: single-line comments converted to block comments
- Orphaned root-level webservices plugin files removed
- include_mokorestore column: TINYINT changed to VARCHAR(20)
- Snapshot fields_values: scoped dump and restore to com_content.article (previously destroyed values for contacts, users, etc.)
- Run Backup button: accept CSRF token from GET (fixes "token did not match" on profile edit)
- SFTP fields: moved into remote fieldset for showon visibility; removed required attr that blocked non-SFTP saves
- Script.php merge conflict markers resolved
## [01.24.00] — 2026-06-02
## [02.52.22] --- 2026-06-30
### Added
- Initial release: full-site backup and restore for Joomla 6
- Database, files, and configuration backup
- ZIP and tar.gz archive formats with AES-256 encryption
- Differential backups based on file manifests
- FTP/FTPS, S3, Google Drive remote storage
- MokoRestore standalone restore wizard
- CLI backup and restore commands
- REST API for remote management
- Scheduled tasks via com_scheduler
- Email and ntfy push notifications
- Per-profile retention, exclusions, and notifications
- Akeeba Backup migration tool
- Admin dashboard with system health checks
- Cancel Stalled toolbar button on Backup Records view to cancel backups stuck in "running" status
- New ACL permission `mokosuitebackup.backup.cancel` for cancel stalled action
- AJAX endpoint `ajax.cancelBackup` for programmatic/API cancel
- Auto-timeout failsafe: preflight auto-cancels "running" backups older than 30 minutes
- Pre-extension-update backup progress modal (Bootstrap 5 modal with stepped AJAX progress bar)
- New `warning` backup status for records where archive succeeded but remote upload failed
- Warning-status records are downloadable, browsable, restorable, and purgeable
- Warning status filter option in Backup Records dropdown
- Yellow "Warning" badge in backup list, detail view, and cpanel module
### Fixed
- Pre-update backup ran synchronously with no browser feedback — page hung until complete
- Stalled backups permanently blocked future backups for the same profile
- Preflight error message now directs users to Cancel Stalled action
- Backups with failed remote uploads were marked as "complete", hiding the upload failure
## [02.52.18] --- 2026-06-30
+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: 01.45.05
VERSION: 02.53.01
BRIEF: Security vulnerability reporting and handling policy
-->
@@ -15,5 +15,6 @@
<action name="mokosuitebackup.backup.purge" title="COM_MOKOSUITEBACKUP_ACTION_BACKUP_PURGE" />
<action name="mokosuitebackup.backup.compare" title="COM_MOKOSUITEBACKUP_ACTION_BACKUP_COMPARE" />
<action name="mokosuitebackup.backup.browse" title="COM_MOKOSUITEBACKUP_ACTION_BACKUP_BROWSE" />
<action name="mokosuitebackup.backup.cancel" title="COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL" />
</section>
</access>
@@ -15,6 +15,7 @@
>
<option value="">COM_MOKOJOOMBACKUP_FILTER_STATUS_ALL</option>
<option value="complete">COM_MOKOJOOMBACKUP_STATUS_COMPLETE</option>
<option value="warning">COM_MOKOJOOMBACKUP_STATUS_WARNING</option>
<option value="running">COM_MOKOJOOMBACKUP_STATUS_RUNNING</option>
<option value="fail">COM_MOKOJOOMBACKUP_STATUS_FAIL</option>
<option value="pending">COM_MOKOJOOMBACKUP_STATUS_PENDING</option>
@@ -5,6 +5,7 @@
; @license GPL-3.0-or-later
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
COM_MOKOJOOMBACKUP_SHORT="Backup"
COM_MOKOJOOMBACKUP_CONFIGURATION="MokoSuiteBackup Options"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
@@ -22,7 +23,7 @@ COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE="Restore Backup"
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE_DESC="Allows users in this group to restore the site from a backup archive. This is a destructive operation that overwrites the current site."
; Dashboard view
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="MokoSuiteBackup Dashboard"
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="Dashboard"
COM_MOKOJOOMBACKUP_DASHBOARD_LAST_BACKUP="Last Backup"
COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS="No backups yet"
COM_MOKOJOOMBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled"
@@ -44,14 +45,14 @@ COM_MOKOJOOMBACKUP_DASHBOARD_BACKUP_TREND="Backup Trend (30 days)"
; Backups view
COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED="%d backup records deleted."
COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED_1="%d backup record deleted."
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Backup Records"
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Records"
COM_MOKOJOOMBACKUP_BACKUPS_TABLE_CAPTION="Table of backup records"
COM_MOKOJOOMBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup."
COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW="Backup Now"
COM_MOKOJOOMBACKUP_DOWNLOAD="Download"
; Backup detail view
COM_MOKOJOOMBACKUP_BACKUP_DETAIL="Backup Detail"
COM_MOKOJOOMBACKUP_BACKUP_DETAIL="Detail"
COM_MOKOJOOMBACKUP_VIEW_LOG="Backup Log"
COM_MOKOJOOMBACKUP_BROWSE_ARCHIVE="Browse Archive Contents"
COM_MOKOJOOMBACKUP_BROWSE_COL_NAME="Name"
@@ -75,7 +76,7 @@ COM_MOKOJOOMBACKUP_FIELD_DB_SIZE="DB Size"
COM_MOKOJOOMBACKUP_FIELD_REMOTE="Remote Path"
; Profiles view
COM_MOKOJOOMBACKUP_PROFILES_TITLE="Backup Profiles"
COM_MOKOJOOMBACKUP_PROFILES_TITLE="Profiles"
COM_MOKOJOOMBACKUP_PROFILES_TABLE_CAPTION="Table of backup profiles"
COM_MOKOJOOMBACKUP_NO_PROFILES="No backup profiles found."
COM_MOKOJOOMBACKUP_PROFILE_NEW="New Profile"
@@ -207,6 +208,7 @@ COM_MOKOJOOMBACKUP_TYPE_DIFFERENTIAL="Differential (changed files + full DB)"
; Status labels
COM_MOKOJOOMBACKUP_STATUS_COMPLETE="Complete"
COM_MOKOJOOMBACKUP_STATUS_WARNING="Warning"
COM_MOKOJOOMBACKUP_STATUS_RUNNING="Running"
COM_MOKOJOOMBACKUP_STATUS_FAIL="Failed"
COM_MOKOJOOMBACKUP_STATUS_PENDING="Pending"
@@ -450,6 +452,8 @@ COM_MOKOSUITEBACKUP_ACTION_BACKUP_COMPARE="Compare Backups"
COM_MOKOSUITEBACKUP_ACTION_BACKUP_COMPARE_DESC="Allows users to compare two backup records side-by-side."
COM_MOKOSUITEBACKUP_ACTION_BACKUP_BROWSE="Browse Archives"
COM_MOKOSUITEBACKUP_ACTION_BACKUP_BROWSE_DESC="Allows users to view file listings inside backup archives without extracting."
COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL="Cancel Stalled Backup"
COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL_DESC="Allows users to cancel backup records stuck in running status and clean up partial archive files."
; Snapshot ACL
COM_MOKOSUITEBACKUP_ACTION_SNAPSHOT_MANAGE="Manage Snapshots"
@@ -500,6 +504,12 @@ COM_MOKOJOOMBACKUP_PURGE_INVALID_DATE="Invalid date. Please select a valid date.
COM_MOKOJOOMBACKUP_PURGE_SUCCESS="%d backup(s) purged successfully."
COM_MOKOJOOMBACKUP_PURGE_PARTIAL="%d backup(s) purged, but %d could not be deleted."
; Cancel Stalled Backup
COM_MOKOJOOMBACKUP_TOOLBAR_CANCEL_STALLED="Cancel Stalled"
COM_MOKOJOOMBACKUP_CANCEL_NONE_SELECTED="No backup records selected."
COM_MOKOJOOMBACKUP_CANCEL_NONE_RUNNING="None of the selected backups are in running status."
COM_MOKOJOOMBACKUP_CANCEL_SUCCESS="%d stalled backup(s) cancelled."
; Remote Destinations (multi-remote)
COM_MOKOJOOMBACKUP_REMOTE_DESTINATIONS="Remote Destinations"
COM_MOKOJOOMBACKUP_REMOTE_ADD="Add Destination"
@@ -5,6 +5,7 @@
; @license GPL-3.0-or-later
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
COM_MOKOJOOMBACKUP_SHORT="Backup"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration."
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
@@ -5,6 +5,7 @@
; @license GPL-3.0-or-later
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
COM_MOKOJOOMBACKUP_SHORT="Backup"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
@@ -18,7 +19,7 @@ COM_MOKOSUITEBACKUP_ACTION_BACKUP_DOWNLOAD_DESC="Allows users in this group to d
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE="Restore Backup"
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE_DESC="Allows users in this group to restore the site from a backup archive. This is a destructive operation that overwrites the current site."
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="MokoSuiteBackup Dashboard"
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="Dashboard"
COM_MOKOJOOMBACKUP_DASHBOARD_LAST_BACKUP="Last Backup"
COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS="No backups yet"
COM_MOKOJOOMBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled"
@@ -30,8 +31,8 @@ COM_MOKOJOOMBACKUP_DASHBOARD_QUICK_ACTIONS="Quick Actions"
COM_MOKOJOOMBACKUP_DASHBOARD_SCHEDULED_TASKS="Scheduled Tasks"
COM_MOKOJOOMBACKUP_DASHBOARD_UPDATE_SITE="Update Site"
COM_MOKOJOOMBACKUP_DASHBOARD_SYSTEM_HEALTH="System Health"
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Backup Records"
COM_MOKOJOOMBACKUP_PROFILES_TITLE="Backup Profiles"
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Records"
COM_MOKOJOOMBACKUP_PROFILES_TITLE="Profiles"
COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW="Backup Now"
COM_MOKOJOOMBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup."
COM_MOKOJOOMBACKUP_NO_PROFILES="No backup profiles found."
@@ -116,3 +117,16 @@ COM_MOKOJOOMBACKUP_PURGE_NONE_FOUND="No completed backups found before the selec
COM_MOKOJOOMBACKUP_PURGE_INVALID_DATE="Invalid date. Please select a valid date."
COM_MOKOJOOMBACKUP_PURGE_SUCCESS="%d backup(s) purged successfully."
COM_MOKOJOOMBACKUP_PURGE_PARTIAL="%d backup(s) purged, but %d could not be deleted."
; Cancel Stalled Backup
COM_MOKOJOOMBACKUP_TOOLBAR_CANCEL_STALLED="Cancel Stalled"
COM_MOKOJOOMBACKUP_CANCEL_NONE_SELECTED="No backup records selected."
COM_MOKOJOOMBACKUP_CANCEL_NONE_RUNNING="None of the selected backups are in running status."
COM_MOKOJOOMBACKUP_CANCEL_SUCCESS="%d stalled backup(s) cancelled."
; Backup status
COM_MOKOJOOMBACKUP_STATUS_WARNING="Warning"
; ACL - Cancel
COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL="Cancel Stalled Backup"
COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL_DESC="Allows users to cancel backup records stuck in running status and clean up partial archive files."
@@ -5,6 +5,7 @@
; @license GPL-3.0-or-later
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
COM_MOKOJOOMBACKUP_SHORT="Backup"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration."
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
@@ -6,15 +6,15 @@
* @license GNU General Public License version 3 or later; see LICENSE
-->
<extension type="component" method="upgrade">
<name>MokoSuiteBackup</name>
<version>01.45.05</version>
<name>Component - MokoSuiteBackup</name>
<version>02.53.01</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>COM_MOKOJOOMBACKUP_DESCRIPTION</description>
<description>Full-site backup and restore for Joomla — database, files, and configuration.</description>
<namespace path="src">Joomla\Component\MokoSuiteBackup</namespace>
@@ -37,20 +37,20 @@
</update>
<administration>
<menu img="class:archive">COM_MOKOJOOMBACKUP</menu>
<menu img="class:archive">Backup</menu>
<submenu>
<menu link="option=com_mokosuitebackup&amp;view=dashboard"
img="class:home"
alt="Dashboard">COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD</menu>
alt="Dashboard">Dashboard</menu>
<menu link="option=com_mokosuitebackup&amp;view=backups"
img="class:database"
alt="Backups">COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS</menu>
alt="Backups">Backup Records</menu>
<menu link="option=com_mokosuitebackup&amp;view=snapshots"
img="class:camera"
alt="Snapshots">COM_MOKOJOOMBACKUP_SUBMENU_SNAPSHOTS</menu>
alt="Snapshots">Content Snapshots</menu>
<menu link="option=com_mokosuitebackup&amp;view=profiles"
img="class:cog"
alt="Profiles">COM_MOKOJOOMBACKUP_SUBMENU_PROFILES</menu>
alt="Profiles">Backup Profiles</menu>
</submenu>
<files folder=".">
<filename>access.xml</filename>
@@ -65,7 +65,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_records` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`profile_id` INT(11) UNSIGNED NOT NULL DEFAULT 1,
`description` VARCHAR(255) NOT NULL DEFAULT '',
`status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending, running, complete, fail',
`status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending, running, complete, warning, fail',
`origin` VARCHAR(20) NOT NULL DEFAULT 'backend' COMMENT 'backend, cli, api, scheduled',
`backup_type` VARCHAR(20) NOT NULL DEFAULT 'full' COMMENT 'full, database, files',
`archivename` VARCHAR(512) NOT NULL DEFAULT '',
@@ -1 +0,0 @@
/* 01.44.02 — no schema changes */
@@ -1 +0,0 @@
/* 01.44.03 — no schema changes */
@@ -0,0 +1 @@
/* 01.45.00 — no schema changes */
@@ -1 +0,0 @@
/* 01.45.02 — no schema changes */
@@ -1 +0,0 @@
/* 01.45.03 — no schema changes */
@@ -1 +0,0 @@
/* 01.45.05 — no schema changes */
@@ -0,0 +1 @@
/* 02.52.16 — no schema changes */
@@ -0,0 +1 @@
/* 02.52.17 — no schema changes */
@@ -0,0 +1 @@
/* 02.52.18 — no schema changes */
@@ -0,0 +1 @@
/* 02.52.20 — no schema changes */
@@ -0,0 +1 @@
/* 02.52.21 — no schema changes */
@@ -0,0 +1 @@
/* 02.52.22 — no schema changes */
@@ -0,0 +1 @@
/* 02.52.23 — no schema changes */
@@ -0,0 +1 @@
/* 02.52.24 — no schema changes */
@@ -0,0 +1 @@
/* 02.52.27 — no schema changes */
@@ -0,0 +1 @@
/* 02.53.00 — no schema changes */
@@ -0,0 +1 @@
/* 02.53.01 — no schema changes */
@@ -84,6 +84,67 @@ class AjaxController extends BaseController
$this->sendJson($result);
}
/**
* Cancel a backup record stuck in "running" status.
* POST: task=ajax.cancelBackup&id=123
*/
public function cancelBackup(): void
{
if (!Session::checkToken('get') && !Session::checkToken('post')) {
$this->sendJson(['error' => true, 'message' => 'Invalid token'], 403);
return;
}
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.cancel', 'com_mokosuitebackup')) {
$this->sendJson(['error' => true, 'message' => 'Access denied'], 403);
return;
}
$id = $this->input->getInt('id', 0);
if (!$id) {
$this->sendJson(['error' => true, 'message' => 'Missing record ID']);
return;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName(['id', 'status', 'absolute_path']))
->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('id') . ' = ' . $id);
$db->setQuery($query);
$record = $db->loadObject();
if (!$record) {
$this->sendJson(['error' => true, 'message' => 'Record not found'], 404);
return;
}
if ($record->status !== 'running') {
$this->sendJson(['error' => true, 'message' => 'Backup is not in running status']);
return;
}
$update = $db->getQuery(true)
->update($db->quoteName('#__mokosuitebackup_records'))
->set($db->quoteName('status') . ' = ' . $db->quote('fail'))
->set($db->quoteName('backupend') . ' = ' . $db->quote(date('Y-m-d H:i:s')))
->where($db->quoteName('id') . ' = ' . $id);
$db->setQuery($update);
$db->execute();
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
@unlink($record->absolute_path);
}
$this->sendJson(['error' => false, 'message' => 'Backup cancelled']);
}
/**
* Browse server directories for the folder picker field.
* POST: task=ajax.browseDir&path=/some/path
@@ -451,7 +512,7 @@ class AjaxController extends BaseController
return;
}
if ($record->status !== 'complete' || !$record->filesexist) {
if (!\in_array($record->status, ['complete', 'warning'], true) || !$record->filesexist) {
$this->sendJson(['error' => true, 'message' => 'Archive not available']);
return;
@@ -747,7 +808,7 @@ class AjaxController extends BaseController
->select('COUNT(*)')
->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff))
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')');
$db->setQuery($query);
$count = (int) $db->loadResult();
} catch (\Exception $e) {
@@ -199,7 +199,7 @@ class BackupsController extends AdminController
->select($db->quoteName('id'))
->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff))
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')');
$db->setQuery($query);
$ids = $db->loadColumn();
@@ -235,6 +235,76 @@ class BackupsController extends AdminController
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
}
/**
* Cancel selected backup records that are stuck in "running" status.
*
* Sets their status to "fail", cleans up partial archive files,
* and destroys any associated stepped session.
*/
public function cancelStalled(): void
{
$this->checkToken();
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.cancel', 'com_mokosuitebackup')) {
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
return;
}
$cid = $this->input->get('cid', [], 'array');
if (empty($cid)) {
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_CANCEL_NONE_SELECTED'), 'warning');
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
return;
}
$db = $this->app->getContainer()->get('DatabaseDriver');
$cancelled = 0;
$skipped = 0;
foreach ($cid as $id) {
$id = (int) $id;
$query = $db->getQuery(true)
->select($db->quoteName(['id', 'status', 'absolute_path']))
->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('id') . ' = ' . $id);
$db->setQuery($query);
$record = $db->loadObject();
if (!$record || $record->status !== 'running') {
$skipped++;
continue;
}
$update = $db->getQuery(true)
->update($db->quoteName('#__mokosuitebackup_records'))
->set($db->quoteName('status') . ' = ' . $db->quote('fail'))
->set($db->quoteName('backupend') . ' = ' . $db->quote(date('Y-m-d H:i:s')))
->where($db->quoteName('id') . ' = ' . $id);
$db->setQuery($update);
$db->execute();
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
@unlink($record->absolute_path);
}
$cancelled++;
}
if ($cancelled > 0) {
$this->setMessage(Text::sprintf('COM_MOKOJOOMBACKUP_CANCEL_SUCCESS', $cancelled));
} elseif ($skipped > 0) {
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_CANCEL_NONE_RUNNING'), 'warning');
}
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
}
/**
* No-op target for the purge toolbar button.
*
@@ -375,7 +375,7 @@ class BackupEngine
// Final record update (includes fields needed by NotificationSender)
$update = (object) [
'id' => $recordId,
'status' => 'complete',
'status' => $uploadFailed ? 'warning' : 'complete',
'description' => $description,
'backup_type' => $profile->backup_type,
'archivename' => $archiveName,
@@ -547,7 +547,16 @@ class BackupEngine
*/
private function createUploaderFromParams(string $type, array $params): RemoteUploaderInterface
{
$fake = (object) $params;
$prefixMap = ['ftp' => 'ftp_', 'sftp' => 'sftp_', 's3' => 's3_', 'google_drive' => 'gdrive_'];
$prefix = $prefixMap[$type] ?? '';
$prefixed = [];
foreach ($params as $key => $value) {
$prefixed[$prefix . $key] = $value;
}
$fake = (object) $prefixed;
return match ($type) {
'ftp' => new FtpUploader($fake),
@@ -597,7 +606,7 @@ class BackupEngine
->select($db->quoteName('manifest'))
->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('profile_id') . ' = ' . $profileId)
->where($db->quoteName('status') . ' = ' . $db->quote('complete'))
->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')')
->where($db->quoteName('manifest') . ' != ' . $db->quote(''))
->where($db->quoteName('backup_type') . ' = ' . $db->quote('full'))
->order($db->quoteName('backupstart') . ' DESC');
@@ -346,6 +346,9 @@ define('MOKOJOOMBACKUP_RESTORE', 1);
define('RESTORE_DIR', __DIR__);
define('BACKUP_FILE', RESTORE_DIR . '/site-backup.zip');
error_log('MokoRestore: Script loaded — RESTORE_DIR=' . RESTORE_DIR);
error_log('MokoRestore: PHP ' . PHP_VERSION . ', SAPI=' . php_sapi_name() . ', memory_limit=' . ini_get('memory_limit'));
session_start();
if (empty($_SESSION['restore_token'])) {
@@ -358,25 +361,37 @@ $token = $_SESSION['restore_token'];
// Write a security file to the web root with a random code.
// The user must read the code from the file and enter it in the browser
// to prove they have filesystem access before any restore actions are allowed.
$securityFile = RESTORE_DIR . '/.mokorestore-security.php';
$securityFile = RESTORE_DIR . '/mokorestore-security.php';
$securityCode = $_SESSION['security_code'] ?? '';
if (empty($securityCode)) {
$securityCode = strtoupper(substr(bin2hex(random_bytes(4)), 0, 8));
$_SESSION['security_code'] = $securityCode;
$_SESSION['security_verified'] = false;
}
// Write (or recreate) the security file whenever verification is still pending
if (empty($_SESSION['security_verified']) && !is_file($securityFile)) {
error_log('MokoRestore: Writing security file: ' . $securityFile);
error_log('MokoRestore: Target directory: ' . RESTORE_DIR . ' (writable: ' . (is_writable(RESTORE_DIR) ? 'yes' : 'NO') . ')');
// Write security file with the code
$securityContent = "<?php die('MokoRestore Security Code: " . $securityCode . "'); ?>\n"
. "MokoRestore Security Verification\n"
. "==================================\n"
. "Code: " . $securityCode . "\n"
. "Enter this code in the MokoRestore browser interface to proceed.\n"
. "This file will be deleted automatically after verification.\n";
if (file_put_contents($securityFile, $securityContent) === false) {
// Cannot write security file — skip verification to avoid locking user out
$written = @file_put_contents($securityFile, $securityContent);
if ($written === false) {
$err = error_get_last();
error_log('MokoRestore: FAILED to write security file — ' . ($err['message'] ?? 'unknown error'));
error_log('MokoRestore: Directory permissions: ' . decoct(@fileperms(RESTORE_DIR) & 0777) . ', owner: ' . @fileowner(RESTORE_DIR) . ', PHP user: ' . (function_exists('posix_getuid') ? posix_getuid() : 'n/a'));
error_log('MokoRestore: Security verification SKIPPED — user will not be challenged');
$_SESSION['security_verified'] = true;
error_log('MokoRestore: Cannot write security file — verification skipped (check directory permissions)');
} else {
error_log('MokoRestore: Security file created (' . $written . ' bytes)');
}
}
@@ -387,15 +402,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
if ($inputCode === $securityCode) {
$_SESSION['security_verified'] = true;
error_log('MokoRestore: Security code VERIFIED');
// Delete the security file
if (is_file($securityFile)) {
@unlink($securityFile);
error_log('MokoRestore: Security file deleted');
}
echo json_encode(['success' => true, 'message' => 'Security verified']);
} else {
echo json_encode(['success' => false, 'message' => 'Incorrect security code. Check the file: .mokorestore-security.php']);
error_log('MokoRestore: Security code REJECTED (input=' . $inputCode . ')');
echo json_encode(['success' => false, 'message' => 'Incorrect security code. Check the file: mokorestore-security.php']);
}
exit;
@@ -414,7 +431,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
}
if (!$securityVerified) {
echo json_encode(['success' => false, 'message' => 'Security verification required. Enter the code from .mokorestore-security.php']);
echo json_encode(['success' => false, 'message' => 'Security verification required. Enter the code from mokorestore-security.php']);
exit;
}
@@ -424,9 +441,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
@ignore_user_abort(true);
try {
error_log('MokoRestore: Action dispatched — ' . $_POST['action']);
$result = handleAction($_POST['action'], $_POST);
error_log('MokoRestore: Action ' . $_POST['action'] . ' completed — ' . ($result['success'] ? 'OK' : 'FAIL: ' . ($result['message'] ?? '')));
echo json_encode($result);
} catch (Throwable $e) {
error_log('MokoRestore: Action ' . $_POST['action'] . ' EXCEPTION — ' . $e->getMessage());
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
@@ -551,10 +571,14 @@ function actionPreflight(): array
function actionExtract(array $data): array
{
error_log('MokoRestore: Extract — target=' . BACKUP_FILE . ', exists=' . (file_exists(BACKUP_FILE) ? 'yes' : 'no'));
if (!file_exists(BACKUP_FILE)) {
throw new RuntimeException('Backup file not found: site-backup.zip');
}
error_log('MokoRestore: Extract — archive size=' . number_format(filesize(BACKUP_FILE) / 1048576, 2) . ' MB');
$zip = new ZipArchive();
if ($zip->open(BACKUP_FILE) !== true) {
@@ -591,6 +615,8 @@ function actionExtract(array $data): array
$count = $zip->numFiles;
$zip->close();
error_log('MokoRestore: Extract — ' . $count . ' files extracted to ' . RESTORE_DIR);
// Pre-fill from configuration.php.bak (sanitized backup) or
// configuration.php (legacy/unsanitized backup). Skip [SANITIZED:] values.
$existingConfig = [];
@@ -719,6 +745,8 @@ function actionDatabase(array $data): array
$user = $data['db_user'] ?? '';
$pass = $data['db_pass'] ?? '';
error_log('MokoRestore: Database import — host=' . $host . ', db=' . $name . ', user=' . $user);
if (empty($name) || empty($user)) {
throw new RuntimeException('Database name and user are required');
}
@@ -726,9 +754,12 @@ function actionDatabase(array $data): array
$sqlFile = RESTORE_DIR . '/database.sql';
if (!is_file($sqlFile)) {
error_log('MokoRestore: Database import — no database.sql found, skipping');
return ['success' => true, 'message' => 'No database.sql found — skipped', 'statements' => 0, 'errors' => 0];
}
error_log('MokoRestore: Database import — SQL file size=' . number_format(filesize($sqlFile) / 1048576, 2) . ' MB');
$pdo = new PDO(
"mysql:host={$host};dbname={$name};charset=utf8mb4",
$user,
@@ -835,6 +866,14 @@ function actionDatabase(array $data): array
$msg .= " ({$errors} warnings)";
}
error_log('MokoRestore: Database import — ' . $msg);
if (!empty($errorList)) {
foreach ($errorList as $i => $err) {
error_log('MokoRestore: DB error ' . ($i + 1) . ': ' . $err);
}
}
return [
'success' => ($statements > 0 || $errors === 0),
'message' => $msg,
@@ -847,6 +886,7 @@ function actionDatabase(array $data): array
function actionConfig(array $data): array
{
error_log('MokoRestore: Config rebuild started');
$host = $data['db_host'] ?? 'localhost';
$dbName = $data['db_name'] ?? '';
$dbUser = $data['db_user'] ?? '';
@@ -867,6 +907,7 @@ function actionConfig(array $data): array
// debug, cache, SEF, editor, etc.). Fall back to existing config
// for legacy/unsanitized backups, or build from scratch if neither exists.
$basePath = is_file($bakPath) ? $bakPath : (is_file($configPath) ? $configPath : null);
error_log('MokoRestore: Config — base template: ' . ($basePath ?? 'none (building from scratch)'));
if ($basePath !== null) {
$config = file_get_contents($basePath);
@@ -919,9 +960,12 @@ function actionConfig(array $data): array
}
if (file_put_contents($configPath, $config) === false) {
error_log('MokoRestore: Config — FAILED to write ' . $configPath);
return ['success' => false, 'message' => 'Failed to write Joomla config file — check directory permissions'];
}
error_log('MokoRestore: Config — written to ' . $configPath . ' (' . filesize($configPath) . ' bytes)');
// Remove .bak after successful rebuild
if (is_file($bakPath)) {
@unlink($bakPath);
@@ -1175,6 +1219,8 @@ function actionResetAdmin(array $data): array
$userId = (int) ($data['admin_id'] ?? 0);
$password = $data['new_password'] ?? '';
error_log('MokoRestore: Admin password reset — user_id=' . $userId);
if ($userId < 1 || strlen($password) < 8) {
throw new RuntimeException('Select an admin and enter a password (8+ characters)');
}
@@ -1188,6 +1234,7 @@ function actionResetAdmin(array $data): array
throw new RuntimeException('User not found or password unchanged');
}
error_log('MokoRestore: Admin password reset — success');
return ['success' => true, 'message' => 'Admin password updated successfully'];
}
@@ -1197,6 +1244,7 @@ function actionPostRestore(array $data): array
$prefix = getValidatedPrefix($data);
$tasks = json_decode($data['tasks'] ?? '[]', true) ?: [];
$results = [];
error_log('MokoRestore: Post-restore — ' . count($tasks) . ' task(s): ' . implode(', ', $tasks));
foreach ($tasks as $task) {
try {
@@ -1319,6 +1367,7 @@ function actionProvision(array $data): array
$prefix = getValidatedPrefix($data);
$tasks = json_decode($data['tasks'] ?? '[]', true) ?: [];
$results = [];
error_log('MokoRestore: Provisioning — ' . count($tasks) . ' task(s): ' . implode(', ', $tasks));
foreach ($tasks as $task) {
try {
@@ -1395,16 +1444,24 @@ function actionProvision(array $data): array
function actionCleanup(): array
{
error_log('MokoRestore: Cleanup started');
$removed = [];
foreach (['database.sql', 'site-backup.zip'] as $file) {
foreach (['database.sql', 'site-backup.zip', 'mokorestore-security.php'] as $file) {
$path = RESTORE_DIR . '/' . $file;
if (is_file($path) && @unlink($path)) {
$removed[] = $file;
if (is_file($path)) {
if (@unlink($path)) {
$removed[] = $file;
error_log('MokoRestore: Cleanup — removed ' . $file);
} else {
error_log('MokoRestore: Cleanup — FAILED to remove ' . $file);
}
}
}
error_log('MokoRestore: Cleanup complete — removed ' . count($removed) . ' file(s)');
return [
'success' => true,
'message' => 'Removed: ' . (empty($removed) ? '(none)' : implode(', ', $removed))
@@ -1570,14 +1627,14 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
<!-- Step 0: Security Verification -->
<div class="mr-panel <?php echo $securityVerified ? '' : 'visible'; ?>" id="panel0">
<h2>Security Verification</h2>
<p class="mr-desc">To prevent unauthorized access, enter the security code from the file <code>.mokorestore-security.php</code> in your site root.</p>
<p class="mr-desc">To prevent unauthorized access, enter the security code from the file <code>mokorestore-security.php</code> in your site root.</p>
<div style="border:1px solid #e2e8f0;border-radius:8px;padding:1.25rem;margin-bottom:1.25rem;background:#f8fafc">
<div style="font-weight:600;font-size:0.9rem;color:#334155;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem">
<span style="font-size:1.1rem">&#128274;</span> How to find the code
</div>
<ol style="margin:0;padding-left:1.25rem;color:#475569;font-size:0.9rem;line-height:1.6">
<li>Connect to your server via FTP, SSH, or file manager</li>
<li>Open <code>.mokorestore-security.php</code> in the site root directory</li>
<li>Open <code>mokorestore-security.php</code> in the site root directory</li>
<li>Copy the 8-character code and enter it below</li>
</ol>
</div>
@@ -165,7 +165,7 @@ class PreflightCheck
->select($db->quoteName('total_size'))
->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('profile_id') . ' = ' . (int) $profile->id)
->where($db->quoteName('status') . ' = ' . $db->quote('complete'))
->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')')
->where($db->quoteName('total_size') . ' > 0')
->order($db->quoteName('backupstart') . ' DESC');
$db->setQuery($query, 0, 1);
@@ -194,22 +194,58 @@ class PreflightCheck
}
}
private const STALE_TIMEOUT_MINUTES = 30;
/**
* Check if another backup is already running for this profile.
*
* Backups running longer than STALE_TIMEOUT_MINUTES are automatically
* marked as failed so they don't permanently block future runs.
*/
private function checkRunningBackup(object $profile, object $db): void
{
$query = $db->getQuery(true)
->select('COUNT(*)')
->select($db->quoteName(['id', 'backupstart', 'absolute_path']))
->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('profile_id') . ' = ' . (int) $profile->id)
->where($db->quoteName('status') . ' = ' . $db->quote('running'));
$db->setQuery($query);
$running = (int) $db->loadResult();
$rows = $db->loadObjectList();
if ($running > 0) {
if (empty($rows)) {
return;
}
$cutoff = time() - (self::STALE_TIMEOUT_MINUTES * 60);
$stillAlive = 0;
foreach ($rows as $row) {
$started = strtotime($row->backupstart);
if ($started !== false && $started < $cutoff) {
$update = $db->getQuery(true)
->update($db->quoteName('#__mokosuitebackup_records'))
->set($db->quoteName('status') . ' = ' . $db->quote('fail'))
->set($db->quoteName('backupend') . ' = ' . $db->quote(date('Y-m-d H:i:s')))
->where($db->quoteName('id') . ' = ' . (int) $row->id);
$db->setQuery($update);
$db->execute();
if (!empty($row->absolute_path) && is_file($row->absolute_path)) {
@unlink($row->absolute_path);
}
$this->warnings[] = 'Auto-cancelled stalled backup #' . $row->id
. ' (started ' . $row->backupstart . ', exceeded '
. self::STALE_TIMEOUT_MINUTES . ' min timeout)';
} else {
$stillAlive++;
}
}
if ($stillAlive > 0) {
$this->errors[] = 'Another backup is already running for profile: ' . $profile->title
. ' — wait for it to finish or delete the stale record';
. ' — wait for it to finish or use Cancel Stalled from the Backup Records toolbar';
}
}
@@ -67,7 +67,7 @@ class RestoreEngine
return ['success' => false, 'message' => 'Backup record not found: ' . $recordId];
}
if ($record->status !== 'complete') {
if ($record->status !== 'complete' && $record->status !== 'warning') {
return ['success' => false, 'message' => 'Cannot restore from incomplete backup (status: ' . $record->status . ')'];
}
@@ -394,8 +394,14 @@ class SteppedBackupEngine
$restoreScriptName = MokoRestore::sanitizeScriptName($restoreScriptName);
$restoreDir = dirname($session->archivePath);
$session->restoreScriptPath = $restoreDir . '/' . $restoreScriptName;
MokoRestore::generateStandalone($session->restoreScriptPath);
$session->log('Standalone ' . $restoreScriptName . ' generated');
try {
MokoRestore::generateStandalone($session->restoreScriptPath);
$session->log('Standalone ' . $restoreScriptName . ' generated');
} catch (\Throwable $e) {
$session->log('MokoRestore error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
$session->log('Stack trace: ' . $e->getTraceAsString());
}
}
// Update record
@@ -641,7 +647,7 @@ class SteppedBackupEngine
$update = (object) [
'id' => $session->recordId,
'status' => 'complete',
'status' => $uploadFailed ? 'warning' : 'complete',
'backupend' => date('Y-m-d H:i:s'),
'total_size' => $totalSize,
'checksum' => $checksum,
@@ -883,7 +889,16 @@ class SteppedBackupEngine
*/
private function createUploaderFromParams(string $type, array $params): RemoteUploaderInterface
{
$fake = (object) $params;
$prefixMap = ['ftp' => 'ftp_', 'sftp' => 'sftp_', 's3' => 's3_', 'google_drive' => 'gdrive_'];
$prefix = $prefixMap[$type] ?? '';
$prefixed = [];
foreach ($params as $key => $value) {
$prefixed[$prefix . $key] = $value;
}
$fake = (object) $prefixed;
return match ($type) {
'ftp' => new FtpUploader($fake),
@@ -64,7 +64,7 @@ class SteppedRestoreEngine
return ['error' => true, 'message' => 'Backup record not found: ' . $recordId];
}
if ($record->status !== 'complete') {
if ($record->status !== 'complete' && $record->status !== 'warning') {
return ['error' => true, 'message' => 'Cannot restore from incomplete backup (status: ' . $record->status . ')'];
}
@@ -70,7 +70,7 @@ class BackupStatusHelper
])
->from($db->quoteName('#__mokosuitebackup_records', 'r'))
->join('LEFT', $db->quoteName('#__mokosuitebackup_profiles', 'p') . ' ON p.id = r.profile_id')
->where($db->quoteName('r.status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'fail'])) . ')')
->where($db->quoteName('r.status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning', 'fail'])) . ')')
->order($db->quoteName('r.backupstart') . ' DESC');
if ($profileId !== null) {
@@ -148,7 +148,7 @@ class BackupStatusHelper
$query = $db->getQuery(true)
->select($db->quoteName('status'))
->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'fail'])) . ')')
->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning', 'fail'])) . ')')
->order($db->quoteName('backupstart') . ' DESC')
->setLimit(50);
@@ -156,7 +156,7 @@ class BackupStatusHelper
$streak = 0;
foreach ($statuses as $s) {
if ($s === 'complete') {
if ($s === 'complete' || $s === 'warning') {
$streak++;
} else {
break;
@@ -30,7 +30,7 @@ class DashboardModel extends BaseDatabaseModel
->select('r.*, p.title AS profile_title')
->from($db->quoteName('#__mokosuitebackup_records', 'r'))
->join('LEFT', $db->quoteName('#__mokosuitebackup_profiles', 'p') . ' ON p.id = r.profile_id')
->where($db->quoteName('r.status') . ' = ' . $db->quote('complete'))
->where($db->quoteName('r.status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')')
->order($db->quoteName('r.backupend') . ' DESC');
$db->setQuery($query, 0, 1);
@@ -75,7 +75,7 @@ class DashboardModel extends BaseDatabaseModel
->select('COUNT(*) AS total_count')
->select('COALESCE(SUM(' . $db->quoteName('total_size') . '), 0) AS total_size')
->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
->where($db->quoteName('status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')');
$db->setQuery($query);
$stats = $db->loadObject();
@@ -274,7 +274,7 @@ class DashboardModel extends BaseDatabaseModel
->select('COALESCE(SUM(r.total_size), 0) AS total_size')
->from($db->quoteName('#__mokosuitebackup_records', 'r'))
->join('LEFT', $db->quoteName('#__mokosuitebackup_profiles', 'p') . ' ON p.id = r.profile_id')
->where($db->quoteName('r.status') . ' = ' . $db->quote('complete'))
->where($db->quoteName('r.status') . ' IN (' . implode(',', array_map([$db, 'quote'], ['complete', 'warning'])) . ')')
->group($db->quoteName('r.profile_id'))
->order('total_size DESC');
$db->setQuery($query);
@@ -37,11 +37,11 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void
{
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUP_DETAIL'), 'database');
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_SHORT') . ': ' . Text::_('COM_MOKOJOOMBACKUP_BACKUP_DETAIL'), 'database');
$user = Factory::getApplication()->getIdentity();
if ($this->item->status === 'complete'
if (\in_array($this->item->status, ['complete', 'warning'], true)
&& !empty($this->item->filesexist)
&& $user->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup')
) {
@@ -99,7 +99,7 @@ class HtmlView extends BaseHtmlView
{
$user = Factory::getApplication()->getIdentity();
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUPS_TITLE'), 'database');
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_SHORT') . ': ' . Text::_('COM_MOKOJOOMBACKUP_BACKUPS_TITLE'), 'database');
if ($user->authorise('mokosuitebackup.backup.restore', 'com_mokosuitebackup')) {
ToolbarHelper::custom('backups.restore', 'upload', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE', true);
@@ -113,6 +113,10 @@ class HtmlView extends BaseHtmlView
ToolbarHelper::custom('backups.compare', 'copy', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_COMPARE', true);
}
if ($user->authorise('mokosuitebackup.backup.cancel', 'com_mokosuitebackup')) {
ToolbarHelper::custom('backups.cancelStalled', 'stop-circle', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_CANCEL_STALLED', true);
}
if ($user->authorise('core.delete', 'com_mokosuitebackup')) {
ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'backups.delete');
}
@@ -52,7 +52,7 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void
{
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_TITLE'), 'archive');
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_SHORT') . ': ' . Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_TITLE'), 'archive');
ToolbarHelper::preferences('com_mokosuitebackup');
}
}
@@ -44,7 +44,7 @@ class HtmlView extends BaseHtmlView
? $user->authorise('core.create', 'com_mokosuitebackup')
: $user->authorise('core.edit', 'com_mokosuitebackup');
ToolbarHelper::title(Text::_($title), 'cog');
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_SHORT') . ': ' . Text::_($title), 'cog');
if ($canSave) {
ToolbarHelper::apply('profile.apply');
@@ -49,7 +49,7 @@ class HtmlView extends BaseHtmlView
{
$user = Factory::getApplication()->getIdentity();
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_PROFILES_TITLE'), 'cog');
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_SHORT') . ': ' . Text::_('COM_MOKOJOOMBACKUP_PROFILES_TITLE'), 'cog');
if ($user->authorise('core.create', 'com_mokosuitebackup')) {
ToolbarHelper::addNew('profile.add');
@@ -38,7 +38,7 @@ class HtmlView extends BaseHtmlView
{
$user = Factory::getApplication()->getIdentity();
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_SNAPSHOTS_TITLE'), 'camera');
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_SHORT') . ': ' . Text::_('COM_MOKOJOOMBACKUP_SNAPSHOTS_TITLE'), 'camera');
if ($user->authorise('mokosuitebackup.snapshot.manage', 'com_mokosuitebackup')) {
ToolbarHelper::custom('snapshots.create', 'plus', '', 'COM_MOKOJOOMBACKUP_SNAPSHOT_CREATE', false);
@@ -30,6 +30,7 @@ $ajaxUrl = Route::_('index.php?option=com_mokosuitebackup&format=json', false)
<?php
$statusClass = match ($this->item->status) {
'complete' => 'badge bg-success',
'warning' => 'badge bg-warning text-dark',
'running' => 'badge bg-info',
'fail' => 'badge bg-danger',
default => 'badge bg-secondary',
@@ -92,6 +92,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<?php
$statusClass = match ($item->status) {
'complete' => 'badge bg-success',
'warning' => 'badge bg-warning text-dark',
'running' => 'badge bg-info',
'fail' => 'badge bg-danger',
default => 'badge bg-secondary',
@@ -11,6 +11,7 @@ MOD_MOKOSUITEBACKUP_CPANEL_NOT_INSTALLED="MokoSuiteBackup is not installed or is
MOD_MOKOSUITEBACKUP_CPANEL_LAST_BACKUP="Last Backup"
MOD_MOKOSUITEBACKUP_CPANEL_STATUS_OK="Success"
MOD_MOKOSUITEBACKUP_CPANEL_STATUS_WARNING="Warning"
MOD_MOKOSUITEBACKUP_CPANEL_STATUS_FAIL="Failed"
MOD_MOKOSUITEBACKUP_CPANEL_NO_BACKUPS="No backups yet."
MOD_MOKOSUITEBACKUP_CPANEL_FILES_TABLES="%d files, %d tables"
@@ -7,15 +7,15 @@
* @license GNU General Public License version 3 or later; see LICENSE
-->
<extension type="module" client="administrator" method="upgrade">
<name>mod_mokosuitebackup_cpanel</name>
<version>01.45.05</version>
<name>Module - MokoSuiteBackup - cPanel</name>
<version>02.53.01</version>
<creationDate>2026-06-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>MOD_MOKOSUITEBACKUP_CPANEL_DESCRIPTION</description>
<description>Displays backup status, Backup Now buttons, and quick links on the admin dashboard.</description>
<namespace path="src">Joomla\Module\MokoSuiteBackupCpanel</namespace>
@@ -51,10 +51,20 @@ $moduleId = 'mod-msb-cpanel-' . $displayData['module']->id;
<?php if ($latest) : ?>
<div class="d-flex align-items-center justify-content-between">
<div>
<span class="badge <?php echo $latest['status'] === 'complete' ? 'bg-success' : 'bg-danger'; ?>">
<?php echo $latest['status'] === 'complete'
? Text::_('MOD_MOKOSUITEBACKUP_CPANEL_STATUS_OK')
: Text::_('MOD_MOKOSUITEBACKUP_CPANEL_STATUS_FAIL'); ?>
<?php
$cpanelBadge = match ($latest['status']) {
'complete' => 'bg-success',
'warning' => 'bg-warning text-dark',
default => 'bg-danger',
};
$cpanelLabel = match ($latest['status']) {
'complete' => Text::_('MOD_MOKOSUITEBACKUP_CPANEL_STATUS_OK'),
'warning' => Text::_('MOD_MOKOSUITEBACKUP_CPANEL_STATUS_WARNING'),
default => Text::_('MOD_MOKOSUITEBACKUP_CPANEL_STATUS_FAIL'),
};
?>
<span class="badge <?php echo $cpanelBadge; ?>">
<?php echo $cpanelLabel; ?>
</span>
<span class="ms-1 small text-muted">
<?php echo htmlspecialchars($latest['profile'] ?? ''); ?>
@@ -7,14 +7,14 @@
-->
<extension type="plugin" group="actionlog" method="upgrade">
<name>Action Log - MokoSuiteBackup</name>
<version>01.45.05</version>
<version>02.53.01</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION</description>
<description>Logs MokoSuiteBackup actions (backup, restore, profile changes) to User Action Logs.</description>
<namespace path="src">Joomla\Plugin\Actionlog\MokoSuiteBackup</namespace>
@@ -7,14 +7,14 @@
-->
<extension type="plugin" group="console" method="upgrade">
<name>Console - MokoSuiteBackup</name>
<version>01.45.05</version>
<version>02.53.01</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION</description>
<description>CLI commands for MokoSuiteBackup: run, list, profiles, restore, cleanup.</description>
<namespace path="src">Joomla\Plugin\Console\MokoSuiteBackup</namespace>
@@ -7,14 +7,14 @@
-->
<extension type="plugin" group="content" method="upgrade">
<name>Content - MokoSuiteBackup</name>
<version>01.45.05</version>
<version>02.53.01</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>PLG_CONTENT_MOKOJOOMBACKUP_DESCRIPTION</description>
<description>Automatically triggers a backup before extension installs or updates.</description>
<namespace path="src">Joomla\Plugin\Content\MokoSuiteBackup</namespace>
@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="quickicon" method="upgrade">
<name>Quick Icon - MokoSuiteBackup</name>
<version>01.45.05</version>
<version>02.53.01</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>PLG_QUICKICON_MOKOJOOMBACKUP_DESCRIPTION</description>
<description>Shows backup status on the administrator dashboard.</description>
<namespace path="src">Joomla\Plugin\Quickicon\MokoSuiteBackup</namespace>
@@ -7,14 +7,14 @@
-->
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoSuiteBackup</name>
<version>01.45.05</version>
<version>02.53.01</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>PLG_SYSTEM_MOKOJOOMBACKUP_DESCRIPTION</description>
<description>Automatic cleanup of expired backup archives and scheduled backup triggers.</description>
<namespace path="src">Joomla\Plugin\System\MokoSuiteBackup</namespace>
@@ -7,14 +7,14 @@
-->
<extension type="plugin" group="task" method="upgrade">
<name>Task - MokoSuiteBackup</name>
<version>01.45.05</version>
<version>02.53.01</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION</description>
<description>Scheduled task plugin for MokoSuiteBackup. Run backup profiles on a schedule via Joomla's Scheduled Tasks.</description>
<namespace path="src">Joomla\Plugin\Task\MokoSuiteBackup</namespace>
@@ -7,14 +7,14 @@
-->
<extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoSuiteBackup</name>
<version>01.45.05</version>
<version>02.53.01</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION</description>
<description>REST API for remote backup management.</description>
<namespace path="src">Joomla\Plugin\WebServices\MokoSuiteBackup</namespace>
+4 -3
View File
@@ -8,14 +8,14 @@
<extension type="package" method="upgrade">
<name>Package - MokoSuiteBackup</name>
<packagename>mokosuitebackup</packagename>
<version>01.45.05</version>
<version>02.53.01</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>PKG_MOKOJOOMBCKUP_DESCRIPTION</description>
<description>Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API.</description>
<scriptfile>script.php</scriptfile>
@@ -29,6 +29,7 @@
<file type="plugin" id="mokosuitebackup" group="content">plg_content_mokosuitebackup.zip</file>
<file type="plugin" id="mokosuitebackup" group="actionlog">plg_actionlog_mokosuitebackup.zip</file>
<file type="module" id="mod_mokosuitebackup_cpanel" client="administrator">mod_mokosuitebackup_cpanel.zip</file>
<file type="package" id="pkg_mokosuiteclient">MokoSuiteClient.zip</file>
</files>
<languages>
@@ -36,7 +37,7 @@
</languages>
<updateservers>
<server type="extension" name="MokoSuiteBackup Updates">https://git.mokoconsulting.tech/api/packages/MokoConsulting/generic/MokoSuiteBackup/latest/updates.xml</server>
<server type="extension" name="MokoSuiteBackup Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/updates.xml</server>
</updateservers>
<dlid prefix="dlid=" suffix=""/>
<blockChildUninstall>true</blockChildUninstall>